Skip to content

Commit 27a31e4

Browse files
committed
refactor: completely rewrite SqlTemplate with intelligent SQL parsing and batching
1 parent b00ce69 commit 27a31e4

8 files changed

Lines changed: 500 additions & 8 deletions

File tree

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
dependencies {
22
implementation(project(":core:flamingock-core-commons"))
3+
4+
testAnnotationProcessor(project(":core:flamingock-processor"))
5+
testImplementation(project(":community:flamingock-auditstore-sql"))
6+
testImplementation("com.zaxxer:HikariCP:4.0.3")
7+
testImplementation("com.h2database:h2:2.1.214")
38
}
49

510
description = "SQL change templates for declarative database schema and data changes"
@@ -8,4 +13,12 @@ java {
813
toolchain {
914
languageVersion.set(JavaLanguageVersion.of(8))
1015
}
11-
}
16+
}
17+
tasks.withType<JavaCompile>().configureEach {
18+
if (name.contains("Test", ignoreCase = true)) {
19+
options.compilerArgs.addAll(listOf(
20+
"-Asources=${projectDir}/src/test/java",
21+
"-Aresources=${projectDir}/src/test/resources"
22+
))
23+
}
24+
}

templates/flamingock-sql-template/src/main/java/io/flamingock/template/sql/SqlTemplate.java

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,31 +18,69 @@
1818
import io.flamingock.api.annotations.Apply;
1919
import io.flamingock.api.annotations.Rollback;
2020
import io.flamingock.api.template.AbstractChangeTemplate;
21+
import io.flamingock.internal.util.log.FlamingockLoggerFactory;
22+
import io.flamingock.template.sql.util.SqlStatementParser;
2123

2224
import java.sql.Connection;
2325
import java.sql.SQLException;
26+
import java.util.ArrayList;
27+
import java.util.HashMap;
28+
import java.util.List;
29+
import java.util.Map;
30+
31+
import org.slf4j.Logger;
2432

2533
public class SqlTemplate extends AbstractChangeTemplate<Void, String, String> {
2634

35+
private final Logger logger = FlamingockLoggerFactory.getLogger(SqlTemplate.class);
36+
2737
public SqlTemplate() {
2838
super();
2939
}
3040

3141
@Apply
32-
public void apply(Connection connection) {
42+
public void apply(Connection connection) throws SQLException {
3343
execute(connection, applyPayload);
3444
}
3545

3646
@Rollback
37-
public void rollback(Connection connection) {
47+
public void rollback(Connection connection) throws SQLException {
3848
execute(connection, rollbackPayload);
3949
}
4050

41-
private static void execute(Connection connection, String sql) {
42-
try {
43-
connection.createStatement().executeUpdate(sql);
44-
} catch (SQLException e) {
45-
throw new RuntimeException(e);
51+
private void execute(Connection connection, String sql) throws SQLException {
52+
if (connection == null) {
53+
throw new IllegalArgumentException("connection is null");
54+
}
55+
if (connection.isClosed()) {
56+
throw new IllegalArgumentException("connection is closed");
57+
}
58+
59+
if (sql == null || sql.trim().isEmpty()) {
60+
throw new IllegalArgumentException("SQL payload is null or empty");
61+
}
62+
63+
List<String> statements = SqlStatementParser.splitStatements(sql);
64+
65+
// Group statements by command type for intelligent batching
66+
Map<String, List<String>> groupedStatements = new HashMap<>();
67+
for (String stmt : statements) {
68+
String trimmed = stmt.trim();
69+
if (trimmed.isEmpty()) continue;
70+
String command = SqlStatementParser.getCommand(trimmed);
71+
groupedStatements.computeIfAbsent(command, k -> new ArrayList<>()).add(trimmed);
72+
}
73+
74+
// Execute each group
75+
for (Map.Entry<String, List<String>> entry : groupedStatements.entrySet()) {
76+
List<String> group = entry.getValue();
77+
if (group.size() == 1) {
78+
// Single statement, execute individually
79+
SqlStatementParser.executeSingle(connection, group.get(0));
80+
} else {
81+
// Multiple statements of same type, batch them
82+
SqlStatementParser.executeMany(connection, group);
83+
}
4684
}
4785
}
4886
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
/*
2+
* Copyright 2023 Flamingock (https://www.flamingock.io)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.flamingock.template.sql.util;
17+
18+
import io.flamingock.internal.util.log.FlamingockLoggerFactory;
19+
20+
import java.sql.BatchUpdateException;
21+
import java.sql.Connection;
22+
import java.sql.SQLException;
23+
import java.sql.Statement;
24+
import java.util.ArrayList;
25+
import java.util.List;
26+
27+
import org.slf4j.Logger;
28+
29+
public class SqlStatementParser {
30+
31+
private static final Logger logger = FlamingockLoggerFactory.getLogger(SqlStatementParser.class);
32+
33+
public static List<String> splitStatements(String sql) {
34+
List<String> statements = new ArrayList<>();
35+
StringBuilder currentStmt = new StringBuilder();
36+
boolean inString = false;
37+
boolean inComment = false;
38+
boolean inLineComment = false;
39+
char stringChar = '"';
40+
41+
for (int i = 0; i < sql.length(); i++) {
42+
char c = sql.charAt(i);
43+
char next = (i + 1 < sql.length()) ? sql.charAt(i + 1) : '\0';
44+
45+
if (!inComment && !inLineComment && !inString && c == '/' && next == '*') {
46+
inComment = true;
47+
i++; // skip next char
48+
} else if (inComment && c == '*' && next == '/') {
49+
inComment = false;
50+
i++; // skip next char
51+
} else if (!inComment && !inString && c == '-' && next == '-') {
52+
inLineComment = true;
53+
i++; // skip next char
54+
} else if (inLineComment && c == '\n') {
55+
inLineComment = false;
56+
} else if (!inComment && !inLineComment && !inString && (c == '"' || c == '\'')) {
57+
inString = true;
58+
stringChar = c;
59+
currentStmt.append(c);
60+
} else if (inString && c == stringChar) {
61+
// Check for escaped quote
62+
if (i > 0 && sql.charAt(i - 1) == '\\') {
63+
currentStmt.append(c);
64+
} else {
65+
inString = false;
66+
currentStmt.append(c);
67+
}
68+
} else if (!inComment && !inLineComment && !inString && c == ';') {
69+
statements.add(normalizeSpaces(currentStmt.toString().trim()));
70+
currentStmt.setLength(0);
71+
} else if (!inComment && !inLineComment) {
72+
currentStmt.append(c);
73+
}
74+
// Skip comments entirely
75+
}
76+
if (currentStmt.length() > 0) {
77+
statements.add(normalizeSpaces(currentStmt.toString().trim()));
78+
}
79+
return statements.stream().filter(s -> !s.trim().isEmpty()).collect(java.util.stream.Collectors.toList());
80+
}
81+
82+
public static String getCommand(String sql) {
83+
String trimmed = sql.trim();
84+
if (trimmed.isEmpty()) {
85+
return "UNKNOWN";
86+
}
87+
String[] parts = trimmed.split("\\s+");
88+
return parts.length > 0 ? parts[0].toUpperCase() : "UNKNOWN";
89+
}
90+
91+
public static void executeSingle(Connection connection, String stmtSql) {
92+
try (Statement stmt = connection.createStatement()) {
93+
stmt.execute(stmtSql);
94+
} catch (SQLException e) {
95+
String errorMsg = "SQL execution failed: " + stmtSql;
96+
logger.error(errorMsg, e);
97+
throw new RuntimeException(errorMsg, e);
98+
}
99+
}
100+
101+
public static void executeMany(Connection connection, List<String> statements) {
102+
try (Statement stmt = connection.createStatement()) {
103+
for (String stmtSql : statements) {
104+
stmt.addBatch(stmtSql);
105+
}
106+
stmt.executeBatch();
107+
} catch (BatchUpdateException e) {
108+
// BatchUpdateException provides updateCounts with failed index
109+
int[] updateCounts = e.getUpdateCounts();
110+
int failedIndex = -1;
111+
for (int i = 0; i < updateCounts.length; i++) {
112+
if (updateCounts[i] == Statement.EXECUTE_FAILED) {
113+
failedIndex = i;
114+
break;
115+
}
116+
}
117+
String failedStmt = failedIndex >= 0 ? statements.get(failedIndex) : "unknown";
118+
String errorMsg = String.format("Batch execution failed at statement %d: %s", failedIndex + 1, failedStmt);
119+
logger.error(errorMsg, e);
120+
throw new RuntimeException(errorMsg, e);
121+
} catch (SQLException e) {
122+
String errorMsg = "Batch execution failed: " + e.getMessage();
123+
logger.error(errorMsg, e);
124+
throw new RuntimeException(errorMsg, e);
125+
}
126+
}
127+
128+
private static String normalizeSpaces(String sql) {
129+
if (sql == null) {
130+
return null;
131+
}
132+
// Replace newlines and tabs with spaces
133+
String normalized = sql.replaceAll("[\\r\\n\\t]", " ");
134+
// Replace multiple spaces with single space
135+
normalized = normalized.replaceAll("\\s+", " ");
136+
return normalized.trim();
137+
}
138+
}

0 commit comments

Comments
 (0)