From 27a31e42720210de6d3bc91bf3b02613ab0a3a88 Mon Sep 17 00:00:00 2001 From: bercianor Date: Tue, 30 Dec 2025 18:38:31 +0000 Subject: [PATCH 1/2] refactor: completely rewrite SqlTemplate with intelligent SQL parsing and batching --- .../flamingock-sql-template/build.gradle.kts | 15 +- .../flamingock/template/sql/SqlTemplate.java | 52 +++++- .../template/sql/util/SqlStatementParser.java | 138 ++++++++++++++ .../template/sql/SqlStatementParserTest.java | 168 ++++++++++++++++++ .../template/sql/SqlTemplateTest.java | 111 ++++++++++++ .../_001__create_test_users_table.yaml | 9 + .../sql/changes/_002__insert_test_users.yaml | 11 ++ .../test/resources/flamingock/pipeline.yaml | 4 + 8 files changed, 500 insertions(+), 8 deletions(-) create mode 100644 templates/flamingock-sql-template/src/main/java/io/flamingock/template/sql/util/SqlStatementParser.java create mode 100644 templates/flamingock-sql-template/src/test/java/io/flamingock/template/sql/SqlStatementParserTest.java create mode 100644 templates/flamingock-sql-template/src/test/java/io/flamingock/template/sql/SqlTemplateTest.java create mode 100644 templates/flamingock-sql-template/src/test/java/io/flamingock/template/sql/changes/_001__create_test_users_table.yaml create mode 100644 templates/flamingock-sql-template/src/test/java/io/flamingock/template/sql/changes/_002__insert_test_users.yaml create mode 100644 templates/flamingock-sql-template/src/test/resources/flamingock/pipeline.yaml diff --git a/templates/flamingock-sql-template/build.gradle.kts b/templates/flamingock-sql-template/build.gradle.kts index 4fc3acdce..81341fb1f 100644 --- a/templates/flamingock-sql-template/build.gradle.kts +++ b/templates/flamingock-sql-template/build.gradle.kts @@ -1,5 +1,10 @@ dependencies { implementation(project(":core:flamingock-core-commons")) + + testAnnotationProcessor(project(":core:flamingock-processor")) + testImplementation(project(":community:flamingock-auditstore-sql")) + testImplementation("com.zaxxer:HikariCP:4.0.3") + testImplementation("com.h2database:h2:2.1.214") } description = "SQL change templates for declarative database schema and data changes" @@ -8,4 +13,12 @@ java { toolchain { languageVersion.set(JavaLanguageVersion.of(8)) } -} \ No newline at end of file +} +tasks.withType().configureEach { + if (name.contains("Test", ignoreCase = true)) { + options.compilerArgs.addAll(listOf( + "-Asources=${projectDir}/src/test/java", + "-Aresources=${projectDir}/src/test/resources" + )) + } +} diff --git a/templates/flamingock-sql-template/src/main/java/io/flamingock/template/sql/SqlTemplate.java b/templates/flamingock-sql-template/src/main/java/io/flamingock/template/sql/SqlTemplate.java index c583207cc..7a036614b 100644 --- a/templates/flamingock-sql-template/src/main/java/io/flamingock/template/sql/SqlTemplate.java +++ b/templates/flamingock-sql-template/src/main/java/io/flamingock/template/sql/SqlTemplate.java @@ -18,31 +18,69 @@ import io.flamingock.api.annotations.Apply; import io.flamingock.api.annotations.Rollback; import io.flamingock.api.template.AbstractChangeTemplate; +import io.flamingock.internal.util.log.FlamingockLoggerFactory; +import io.flamingock.template.sql.util.SqlStatementParser; import java.sql.Connection; import java.sql.SQLException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.slf4j.Logger; public class SqlTemplate extends AbstractChangeTemplate { + private final Logger logger = FlamingockLoggerFactory.getLogger(SqlTemplate.class); + public SqlTemplate() { super(); } @Apply - public void apply(Connection connection) { + public void apply(Connection connection) throws SQLException { execute(connection, applyPayload); } @Rollback - public void rollback(Connection connection) { + public void rollback(Connection connection) throws SQLException { execute(connection, rollbackPayload); } - private static void execute(Connection connection, String sql) { - try { - connection.createStatement().executeUpdate(sql); - } catch (SQLException e) { - throw new RuntimeException(e); + private void execute(Connection connection, String sql) throws SQLException { + if (connection == null) { + throw new IllegalArgumentException("connection is null"); + } + if (connection.isClosed()) { + throw new IllegalArgumentException("connection is closed"); + } + + if (sql == null || sql.trim().isEmpty()) { + throw new IllegalArgumentException("SQL payload is null or empty"); + } + + List statements = SqlStatementParser.splitStatements(sql); + + // Group statements by command type for intelligent batching + Map> groupedStatements = new HashMap<>(); + for (String stmt : statements) { + String trimmed = stmt.trim(); + if (trimmed.isEmpty()) continue; + String command = SqlStatementParser.getCommand(trimmed); + groupedStatements.computeIfAbsent(command, k -> new ArrayList<>()).add(trimmed); + } + + // Execute each group + for (Map.Entry> entry : groupedStatements.entrySet()) { + List group = entry.getValue(); + if (group.size() == 1) { + // Single statement, execute individually + SqlStatementParser.executeSingle(connection, group.get(0)); + } else { + // Multiple statements of same type, batch them + SqlStatementParser.executeMany(connection, group); + } } } } diff --git a/templates/flamingock-sql-template/src/main/java/io/flamingock/template/sql/util/SqlStatementParser.java b/templates/flamingock-sql-template/src/main/java/io/flamingock/template/sql/util/SqlStatementParser.java new file mode 100644 index 000000000..6b01415cf --- /dev/null +++ b/templates/flamingock-sql-template/src/main/java/io/flamingock/template/sql/util/SqlStatementParser.java @@ -0,0 +1,138 @@ +/* + * Copyright 2023 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.template.sql.util; + +import io.flamingock.internal.util.log.FlamingockLoggerFactory; + +import java.sql.BatchUpdateException; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.List; + +import org.slf4j.Logger; + +public class SqlStatementParser { + + private static final Logger logger = FlamingockLoggerFactory.getLogger(SqlStatementParser.class); + + public static List splitStatements(String sql) { + List statements = new ArrayList<>(); + StringBuilder currentStmt = new StringBuilder(); + boolean inString = false; + boolean inComment = false; + boolean inLineComment = false; + char stringChar = '"'; + + for (int i = 0; i < sql.length(); i++) { + char c = sql.charAt(i); + char next = (i + 1 < sql.length()) ? sql.charAt(i + 1) : '\0'; + + if (!inComment && !inLineComment && !inString && c == '/' && next == '*') { + inComment = true; + i++; // skip next char + } else if (inComment && c == '*' && next == '/') { + inComment = false; + i++; // skip next char + } else if (!inComment && !inString && c == '-' && next == '-') { + inLineComment = true; + i++; // skip next char + } else if (inLineComment && c == '\n') { + inLineComment = false; + } else if (!inComment && !inLineComment && !inString && (c == '"' || c == '\'')) { + inString = true; + stringChar = c; + currentStmt.append(c); + } else if (inString && c == stringChar) { + // Check for escaped quote + if (i > 0 && sql.charAt(i - 1) == '\\') { + currentStmt.append(c); + } else { + inString = false; + currentStmt.append(c); + } + } else if (!inComment && !inLineComment && !inString && c == ';') { + statements.add(normalizeSpaces(currentStmt.toString().trim())); + currentStmt.setLength(0); + } else if (!inComment && !inLineComment) { + currentStmt.append(c); + } + // Skip comments entirely + } + if (currentStmt.length() > 0) { + statements.add(normalizeSpaces(currentStmt.toString().trim())); + } + return statements.stream().filter(s -> !s.trim().isEmpty()).collect(java.util.stream.Collectors.toList()); + } + + public static String getCommand(String sql) { + String trimmed = sql.trim(); + if (trimmed.isEmpty()) { + return "UNKNOWN"; + } + String[] parts = trimmed.split("\\s+"); + return parts.length > 0 ? parts[0].toUpperCase() : "UNKNOWN"; + } + + public static void executeSingle(Connection connection, String stmtSql) { + try (Statement stmt = connection.createStatement()) { + stmt.execute(stmtSql); + } catch (SQLException e) { + String errorMsg = "SQL execution failed: " + stmtSql; + logger.error(errorMsg, e); + throw new RuntimeException(errorMsg, e); + } + } + + public static void executeMany(Connection connection, List statements) { + try (Statement stmt = connection.createStatement()) { + for (String stmtSql : statements) { + stmt.addBatch(stmtSql); + } + stmt.executeBatch(); + } catch (BatchUpdateException e) { + // BatchUpdateException provides updateCounts with failed index + int[] updateCounts = e.getUpdateCounts(); + int failedIndex = -1; + for (int i = 0; i < updateCounts.length; i++) { + if (updateCounts[i] == Statement.EXECUTE_FAILED) { + failedIndex = i; + break; + } + } + String failedStmt = failedIndex >= 0 ? statements.get(failedIndex) : "unknown"; + String errorMsg = String.format("Batch execution failed at statement %d: %s", failedIndex + 1, failedStmt); + logger.error(errorMsg, e); + throw new RuntimeException(errorMsg, e); + } catch (SQLException e) { + String errorMsg = "Batch execution failed: " + e.getMessage(); + logger.error(errorMsg, e); + throw new RuntimeException(errorMsg, e); + } + } + + private static String normalizeSpaces(String sql) { + if (sql == null) { + return null; + } + // Replace newlines and tabs with spaces + String normalized = sql.replaceAll("[\\r\\n\\t]", " "); + // Replace multiple spaces with single space + normalized = normalized.replaceAll("\\s+", " "); + return normalized.trim(); + } +} \ No newline at end of file diff --git a/templates/flamingock-sql-template/src/test/java/io/flamingock/template/sql/SqlStatementParserTest.java b/templates/flamingock-sql-template/src/test/java/io/flamingock/template/sql/SqlStatementParserTest.java new file mode 100644 index 000000000..c8c1cd36d --- /dev/null +++ b/templates/flamingock-sql-template/src/test/java/io/flamingock/template/sql/SqlStatementParserTest.java @@ -0,0 +1,168 @@ +/* + * Copyright 2023 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.template.sql; + +import io.flamingock.template.sql.util.SqlStatementParser; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class SqlStatementParserTest { + + @Test + void splitStatements_singleStatement() { + String sql = "CREATE TABLE users (id INT, name VARCHAR(100))"; + List statements = SqlStatementParser.splitStatements(sql); + assertEquals(1, statements.size()); + assertEquals("CREATE TABLE users (id INT, name VARCHAR(100))", statements.get(0)); + } + + @Test + void splitStatements_multipleStatements() { + String sql = "CREATE TABLE users (id INT, name VARCHAR(100)); INSERT INTO users VALUES (1, 'john'); SELECT * FROM users"; + List statements = SqlStatementParser.splitStatements(sql); + + assertEquals(3, statements.size()); + assertEquals("CREATE TABLE users (id INT, name VARCHAR(100))", statements.get(0)); + assertEquals("INSERT INTO users VALUES (1, 'john')", statements.get(1)); + assertEquals("SELECT * FROM users", statements.get(2)); + } + + @Test + void splitStatements_blockComments() { + String sql = "/* comment */ CREATE TABLE test (id INT); /* another */ INSERT INTO test VALUES (1)"; + List statements = SqlStatementParser.splitStatements(sql); + + assertEquals(2, statements.size()); + assertEquals("CREATE TABLE test (id INT)", statements.get(0)); + assertEquals("INSERT INTO test VALUES (1)", statements.get(1)); + // Verificar que NO contienen comentarios + assertFalse(statements.get(0).contains("/*")); + assertFalse(statements.get(1).contains("/*")); + } + + @Test + void splitStatements_lineComments() { + String sql = "CREATE TABLE test (id INT); -- comment\nINSERT INTO test VALUES (1); -- another"; + List statements = SqlStatementParser.splitStatements(sql); + + assertEquals(2, statements.size()); + assertEquals("CREATE TABLE test (id INT)", statements.get(0)); + assertEquals("INSERT INTO test VALUES (1)", statements.get(1)); + // Verificar que NO contienen comentarios + assertFalse(statements.get(0).contains("--")); + assertFalse(statements.get(1).contains("--")); + } + + @Test + void splitStatements_mixedComments() { + String sql = "/* block */ CREATE TABLE test (id INT); -- line\nINSERT INTO test VALUES (1); /* block */"; + List statements = SqlStatementParser.splitStatements(sql); + + assertEquals(2, statements.size()); + assertEquals("CREATE TABLE test (id INT)", statements.get(0)); + assertEquals("INSERT INTO test VALUES (1)", statements.get(1)); + // Verificar que NO contienen comentarios + assertFalse(statements.stream().anyMatch(s -> s.contains("/*") || s.contains("--"))); + } + + @Test + void splitStatements_simpleStrings() { + String sql = "INSERT INTO users (name) VALUES ('john'); INSERT INTO users (name) VALUES ('jane')"; + List statements = SqlStatementParser.splitStatements(sql); + + assertEquals(2, statements.size()); + assertEquals("INSERT INTO users (name) VALUES ('john')", statements.get(0)); + assertEquals("INSERT INTO users (name) VALUES ('jane')", statements.get(1)); + } + + @Test + void splitStatements_escapedQuotes() { + String sql = "INSERT INTO users (name) VALUES ('O''Brien'); INSERT INTO users (quote) VALUES ('can''t do it')"; + List statements = SqlStatementParser.splitStatements(sql); + + assertEquals(2, statements.size()); + assertEquals("INSERT INTO users (name) VALUES ('O''Brien')", statements.get(0)); + assertEquals("INSERT INTO users (quote) VALUES ('can''t do it')", statements.get(1)); + } + + @Test + void splitStatements_doubleQuotes() { + String sql = "CREATE TABLE \"test table\" (id INT); INSERT INTO \"test table\" VALUES (1)"; + List statements = SqlStatementParser.splitStatements(sql); + + assertEquals(2, statements.size()); + assertEquals("CREATE TABLE \"test table\" (id INT)", statements.get(0)); + assertEquals("INSERT INTO \"test table\" VALUES (1)", statements.get(1)); + } + + @Test + void splitStatements_noSemicolon() { + String sql = "SELECT 1"; + List statements = SqlStatementParser.splitStatements(sql); + assertEquals(1, statements.size()); + assertEquals("SELECT 1", statements.get(0)); + } + + @Test + void splitStatements_emptyAndWhitespace() { + String sql = " ; CREATE TABLE test (id INT); ; "; + List statements = SqlStatementParser.splitStatements(sql); + assertEquals(1, statements.size()); + assertEquals("CREATE TABLE test (id INT)", statements.get(0)); + } + + @Test + void splitStatements_complexMultiLine() { + String sql = "/* Multi-line comment\n" + + " with content */\n" + + "CREATE TABLE users (\n" + + " id INT,\n" + + " name VARCHAR(100)\n" + + ");\n" + + "\n" + + "-- Line comment\n" + + "INSERT INTO users (id, name) VALUES (1, 'john');\n" + + "\n" + + "SELECT * FROM users"; + List statements = SqlStatementParser.splitStatements(sql); + + assertEquals(3, statements.size()); + assertEquals("CREATE TABLE users ( id INT, name VARCHAR(100) )", statements.get(0)); + assertEquals("INSERT INTO users (id, name) VALUES (1, 'john')", statements.get(1)); + assertEquals("SELECT * FROM users", statements.get(2)); + // Verificar que NO contienen comentarios + assertFalse(statements.stream().anyMatch(s -> s.contains("/*") || s.contains("--"))); + } + + @Test + void getCommand_basicCommands() { + assertEquals("CREATE", SqlStatementParser.getCommand("CREATE TABLE users")); + assertEquals("INSERT", SqlStatementParser.getCommand("INSERT INTO users VALUES (1)")); + assertEquals("SELECT", SqlStatementParser.getCommand("SELECT * FROM users")); + assertEquals("UPDATE", SqlStatementParser.getCommand("UPDATE users SET name = 'john'")); + assertEquals("DELETE", SqlStatementParser.getCommand("DELETE FROM users WHERE id = 1")); + } + + @Test + void getCommand_edgeCases() { + assertEquals("UNKNOWN", SqlStatementParser.getCommand("")); + assertEquals("UNKNOWN", SqlStatementParser.getCommand(" ")); + assertEquals("CREATE", SqlStatementParser.getCommand(" create table users")); + } +} diff --git a/templates/flamingock-sql-template/src/test/java/io/flamingock/template/sql/SqlTemplateTest.java b/templates/flamingock-sql-template/src/test/java/io/flamingock/template/sql/SqlTemplateTest.java new file mode 100644 index 000000000..ef587ce21 --- /dev/null +++ b/templates/flamingock-sql-template/src/test/java/io/flamingock/template/sql/SqlTemplateTest.java @@ -0,0 +1,111 @@ +/* + * Copyright 2023 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.template.sql; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import io.flamingock.api.annotations.EnableFlamingock; +import io.flamingock.store.sql.SqlAuditStore; +import io.flamingock.targetsystem.sql.SqlTargetSystem; +import io.flamingock.internal.core.builder.FlamingockFactory; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.Statement; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@EnableFlamingock(configFile = "flamingock/pipeline.yaml") +class SqlTemplateTest { + + private static final String TEST_TABLE = "test_users"; + + private static DataSource dataSource; + + @BeforeAll + static void beforeAll() { + HikariConfig config = new HikariConfig(); + config.setJdbcUrl("jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1"); + config.setUsername("sa"); + config.setPassword(""); + config.setDriverClassName("org.h2.Driver"); + + dataSource = new HikariDataSource(config); + } + + @AfterEach + void tearDown() throws Exception { + // Cleanup test table if exists + if (dataSource != null) { + try (Connection conn = dataSource.getConnection(); + Statement stmt = conn.createStatement()) { + stmt.execute("DROP TABLE IF EXISTS " + TEST_TABLE); + // Also clean Flamingock audit tables + stmt.execute("DROP TABLE IF EXISTS flamingockAuditLog"); + stmt.execute("DROP TABLE IF EXISTS flamingockLock"); + } catch (Exception e) { + // Ignore cleanup errors + } + } + } + + @AfterAll + static void tearDownAll() { + if (dataSource instanceof HikariDataSource) { + ((HikariDataSource) dataSource).close(); + } + } + + @Test + @DisplayName("WHEN sql target system THEN runs fine with Flamingock") + void happyPath() throws Exception { + SqlTargetSystem sqlTargetSystem = new SqlTargetSystem("sql", dataSource); + FlamingockFactory.getCommunityBuilder() + .setAuditStore(SqlAuditStore.from(sqlTargetSystem)) + .addTargetSystem(sqlTargetSystem) + .build() + .run(); + + // Verify table was created and data inserted + try (Connection conn = dataSource.getConnection(); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM " + TEST_TABLE)) { + + assertTrue(rs.next()); + assertEquals(3, rs.getInt(1)); + } + + // Verify specific data + try (Connection conn = dataSource.getConnection(); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT name FROM " + TEST_TABLE + " ORDER BY id")) { + + assertTrue(rs.next()); + assertEquals("Admin", rs.getString(1)); + assertTrue(rs.next()); + assertEquals("backup", rs.getString(1)); + assertTrue(rs.next()); + assertEquals("text;with;semi", rs.getString(1)); + } + } +} diff --git a/templates/flamingock-sql-template/src/test/java/io/flamingock/template/sql/changes/_001__create_test_users_table.yaml b/templates/flamingock-sql-template/src/test/java/io/flamingock/template/sql/changes/_001__create_test_users_table.yaml new file mode 100644 index 000000000..7cd8c86f6 --- /dev/null +++ b/templates/flamingock-sql-template/src/test/java/io/flamingock/template/sql/changes/_001__create_test_users_table.yaml @@ -0,0 +1,9 @@ +id: create-test-users-table +transactional: true +template: SqlTemplate +targetSystem: + id: "sql" +apply: + - "CREATE TABLE test_users (id INT PRIMARY KEY, name VARCHAR(100), role VARCHAR(50));" +rollback: + - "DROP TABLE test_users;" diff --git a/templates/flamingock-sql-template/src/test/java/io/flamingock/template/sql/changes/_002__insert_test_users.yaml b/templates/flamingock-sql-template/src/test/java/io/flamingock/template/sql/changes/_002__insert_test_users.yaml new file mode 100644 index 000000000..be54153c5 --- /dev/null +++ b/templates/flamingock-sql-template/src/test/java/io/flamingock/template/sql/changes/_002__insert_test_users.yaml @@ -0,0 +1,11 @@ +id: insert-test-users +transactional: true +template: SqlTemplate +targetSystem: + id: "sql" +apply: | + INSERT INTO test_users (id, name, role) VALUES (1, 'Admin', 'superuser'); /* comment */ + INSERT INTO test_users (id, name, role) VALUES (2, 'backup', 'readonly'); + INSERT INTO test_users (id, name, role) VALUES (3, 'text;with;semi', 'user'); +rollback: | + DELETE FROM test_users WHERE id IN (1, 2, 3); diff --git a/templates/flamingock-sql-template/src/test/resources/flamingock/pipeline.yaml b/templates/flamingock-sql-template/src/test/resources/flamingock/pipeline.yaml new file mode 100644 index 000000000..fae6b3503 --- /dev/null +++ b/templates/flamingock-sql-template/src/test/resources/flamingock/pipeline.yaml @@ -0,0 +1,4 @@ +pipeline: + stages: + - description: "SQL Template Integration Test" + location: "io.flamingock.template.sql.changes" From 97d0c719f7713865505b3bf1feb125846f35f84b Mon Sep 17 00:00:00 2001 From: Antonio Perez Dieppa Date: Wed, 31 Dec 2025 08:34:47 +0000 Subject: [PATCH 2/2] test: add failing tests --- .../template/sql/SqlStatementParserTest.java | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/templates/flamingock-sql-template/src/test/java/io/flamingock/template/sql/SqlStatementParserTest.java b/templates/flamingock-sql-template/src/test/java/io/flamingock/template/sql/SqlStatementParserTest.java index c8c1cd36d..9ade6338d 100644 --- a/templates/flamingock-sql-template/src/test/java/io/flamingock/template/sql/SqlStatementParserTest.java +++ b/templates/flamingock-sql-template/src/test/java/io/flamingock/template/sql/SqlStatementParserTest.java @@ -165,4 +165,79 @@ void getCommand_edgeCases() { assertEquals("UNKNOWN", SqlStatementParser.getCommand(" ")); assertEquals("CREATE", SqlStatementParser.getCommand(" create table users")); } + + //------------------- + + @Test + void splitStatements_preservesNewlinesInsideStringLiteral() { + String sql = "INSERT INTO t(txt) VALUES ('a\nb'); SELECT 1;"; + List statements = SqlStatementParser.splitStatements(sql); + + assertEquals(2, statements.size()); + assertEquals("INSERT INTO t(txt) VALUES ('a\nb')", statements.get(0)); // should preserve newline + assertEquals("SELECT 1", statements.get(1)); + } + + @Test + void splitStatements_preservesMultipleSpacesInsideStringLiteral() { + String sql = "INSERT INTO t(txt) VALUES ('a b');"; + List statements = SqlStatementParser.splitStatements(sql); + + assertEquals(1, statements.size()); + assertEquals("INSERT INTO t(txt) VALUES ('a b')", statements.get(0)); // double space must remain + } + + @Test + void splitStatements_stripBlockCommentDoesNotConcatenateTokens() { + String sql = "SELECT/*comment*/1; SELECT 2;"; + List statements = SqlStatementParser.splitStatements(sql); + + assertEquals(2, statements.size()); + assertEquals("SELECT 1", statements.get(0)); // should have a space + assertEquals("SELECT 2", statements.get(1)); + } + + @Test + void splitStatements_supportsHashLineCommentsIfEnabled() { + String sql = "SELECT 1; # comment\nSELECT 2;"; + List statements = SqlStatementParser.splitStatements(sql); + + assertEquals(2, statements.size()); + assertEquals("SELECT 1", statements.get(0)); + assertEquals("SELECT 2", statements.get(1)); + } + + + @Test + void splitStatements_blockCommentRemovalMustNotConcatenateTokens() { + String sql = "SELECT/*c*/1; SELECT 2;"; + List statements = SqlStatementParser.splitStatements(sql); + + assertEquals(2, statements.size()); + assertEquals("SELECT 1", statements.get(0)); // today you'll get "SELECT1" + assertEquals("SELECT 2", statements.get(1)); + } + + @Test + void splitStatements_handlesDoubledQuotesWithoutFlippingStringState() { + // the semicolon is inside the literal, then we close and continue + String sql = "INSERT INTO t(txt) VALUES ('x''y;z'); SELECT 1;"; + List statements = SqlStatementParser.splitStatements(sql); + + assertEquals(2, statements.size()); + assertEquals("INSERT INTO t(txt) VALUES ('x''y;z')", statements.get(0)); + assertEquals("SELECT 1", statements.get(1)); + } + + @Test + void splitStatements_supportsHashLineComments_ifWeClaimMySqlFriendly() { + String sql = "SELECT 1; # comment\nSELECT 2;"; + List statements = SqlStatementParser.splitStatements(sql); + + assertEquals(2, statements.size()); + assertEquals("SELECT 1", statements.get(0)); + assertEquals("SELECT 2", statements.get(1)); + } + + }