Skip to content
Merged
11 changes: 11 additions & 0 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"permissions": {
"allow": [
"Bash(find:*)",
"Bash(./gradlew publishToMavenLocal 2>&1)",
"Bash(grep:*)",
"Bash(./gradlew test:*)",
"Bash(git log:*)"
]
}
}
6 changes: 5 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ plugins {
val flamingockVersion = "1.2.0-beta.1"

group = "io.flamingock"
version = "1.0.0-beta.1"
version = "1.2.0-SNAPSHOT"

repositories {
mavenLocal()
Expand All @@ -31,6 +31,10 @@ dependencies {
testImplementation("org.mockito:mockito-inline:4.11.0")
testImplementation("com.zaxxer:HikariCP:4.0.3")
testImplementation("com.h2database:h2:2.1.214")
testImplementation("org.testcontainers:testcontainers:1.19.8")
testImplementation("org.testcontainers:postgresql:1.19.8")
testImplementation("org.testcontainers:junit-jupiter:1.19.8")
testImplementation("org.postgresql:postgresql:42.7.3")
}

description = "SQL change templates for declarative database schema and data changes"
Expand Down
2 changes: 1 addition & 1 deletion settings.gradle.kts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
rootProject.name = "flamingock-template-sql"
rootProject.name = "flamingock-java-template-sql"
33 changes: 28 additions & 5 deletions src/main/java/io/flamingock/template/sql/SqlTemplate.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,31 @@
import java.util.Collections;
import java.util.List;

/**
* Flamingock template that executes raw SQL statements from YAML change files.
*
* <p>YAML schema:
* <pre>
* id: create-users-table
* template: sql-template
* apply: |
* CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(100));
* INSERT INTO users VALUES (1, 'admin');
* rollback: "DROP TABLE users;" # optional
* config:
* splitStatements: true # default; set to false for drivers that accept multi-statement execute
* </pre>
*
* <p>The {@code apply} payload is split into individual statements by
* {@link io.flamingock.template.sql.util.SqlSplitterFactory}, which selects a
* dialect-aware splitter based on the JDBC connection metadata. Each statement is
* executed via {@link java.sql.Statement#execute(String)}.
*
* <p>The {@code rollback} payload is optional ({@code rollbackPayloadRequired = false}).
* If absent or blank, {@link #rollback(Connection)} is a no-op.
*
* @see SqlTemplateConfig for configuration options
*/
@ChangeTemplate(name = "sql-template", rollbackPayloadRequired = false)
public class SqlTemplate extends AbstractChangeTemplate<SqlTemplateConfig, TemplateString, TemplateString> {

Expand All @@ -56,10 +81,6 @@ public void rollback(Connection connection) {

private void execute(Connection connection, String sql) {

if (sql == null || sql.trim().isEmpty()) {
throw new IllegalArgumentException("SQL payload is null or empty");
}

boolean splitStatements = configuration == null || configuration.isSplitStatements();

List<SqlStatement> statements;
Expand All @@ -85,7 +106,9 @@ private void executeSingle(Connection connection, String stmtSql) {
try (Statement stmt = connection.createStatement()) {
stmt.execute(stmtSql);
} catch (SQLException e) {
String errorMsg = "SQL execution failed: " + stmtSql;
logger.debug("Failed SQL (full text): {}", stmtSql);
String truncated = stmtSql.length() > 200 ? stmtSql.substring(0, 200) + "..." : stmtSql;
String errorMsg = "SQL execution failed: " + truncated;
logger.error(errorMsg, e);
throw new RuntimeException(errorMsg, e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,13 @@
import io.flamingock.internal.common.sql.SqlDialect;
import io.flamingock.internal.common.sql.SqlDialectFactory;
import io.flamingock.template.sql.util.splitter.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.sql.Connection;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;

/**
* Factory for creating database-specific SQL statement splitters.
Expand Down Expand Up @@ -54,10 +59,35 @@
*/
public class SqlSplitterFactory {

private static final Logger logger = LoggerFactory.getLogger(SqlSplitterFactory.class);

private static final Map<SqlDialect, Supplier<SqlSplitter>> customSplitters = new ConcurrentHashMap<>();

/**
* Registers a custom splitter for a given dialect, overriding the built-in one.
* Useful for extending support to new dialects without modifying this factory.
*/
public static void registerSplitter(SqlDialect dialect, Supplier<SqlSplitter> supplier) {
customSplitters.put(dialect, supplier);
}

/**
* Removes a previously registered custom splitter for the given dialect,
* restoring the built-in behaviour. Intended for use in tests.
*/
public static void deregisterSplitter(SqlDialect dialect) {
customSplitters.remove(dialect);
}

public static SqlSplitter createForDialect(Connection connection) {

SqlDialect dialect = SqlDialectFactory.getSqlDialect(connection);

Supplier<SqlSplitter> custom = customSplitters.get(dialect);
if (custom != null) {
return custom.get();
}

switch (dialect) {

case MYSQL:
Expand All @@ -83,10 +113,9 @@ public static SqlSplitter createForDialect(Connection connection) {
case DB2:
return new Db2Splitter();
default:
throw new UnsupportedOperationException(
"SQL Dialect '" + dialect + "' is not supported. " +
"Supported dialects: MySQL, MariaDB, PostgreSQL, Oracle, SQL Server, SQLite, H2, Firebird, DB2, Informix, Sybase."
);
logger.warn("SQL dialect '{}' has no dedicated splitter; falling back to standard semicolon splitting. " +
"Dialect-specific features may not work.", dialect);
return new AbstractSqlSplitter() {};

}
}
Expand Down
Loading
Loading