From 611dac2c91d660aa30f660a519c7d8d8d76d12d4 Mon Sep 17 00:00:00 2001 From: davidfrigolet Date: Thu, 16 Oct 2025 08:01:42 +0100 Subject: [PATCH 1/9] feat: sql audit store --- .../build.gradle.kts | 24 ++ .../community/sql/driver/SqlAuditStore.java | 77 ++++ .../sql/internal/SqlAuditPersistence.java | 59 +++ .../community/sql/internal/SqlAuditor.java | 100 +++++ .../sql/internal/SqlLockService.java | 169 ++++++++ .../community/sql/PipelineTestHelper.java | 160 ++++++++ .../community/sql/SqlAuditStoreTest.java | 361 ++++++++++++++++++ .../_001__create_index.java | 36 ++ .../_002__insert_document.java | 49 +++ .../_003__execution_with_exception.java | 50 +++ .../_001__create_index.java | 36 ++ .../_002__insert_document.java | 39 ++ .../_003__execution_with_exception.java | 40 ++ .../changes/happyPath/_001__create_index.java | 35 ++ .../happyPath/_002__insert_document.java | 38 ++ .../_003__insert_another_document.java | 38 ++ settings.gradle.kts | 6 + 17 files changed, 1317 insertions(+) create mode 100644 community/flamingock-auditstore-sql/build.gradle.kts create mode 100644 community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/driver/SqlAuditStore.java create mode 100644 community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/internal/SqlAuditPersistence.java create mode 100644 community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/internal/SqlAuditor.java create mode 100644 community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/internal/SqlLockService.java create mode 100644 community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/PipelineTestHelper.java create mode 100644 community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/SqlAuditStoreTest.java create mode 100644 community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/failedWithRollback/_001__create_index.java create mode 100644 community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/failedWithRollback/_002__insert_document.java create mode 100644 community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/failedWithRollback/_003__execution_with_exception.java create mode 100644 community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/failedWithoutRollback/_001__create_index.java create mode 100644 community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/failedWithoutRollback/_002__insert_document.java create mode 100644 community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/failedWithoutRollback/_003__execution_with_exception.java create mode 100644 community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/happyPath/_001__create_index.java create mode 100644 community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/happyPath/_002__insert_document.java create mode 100644 community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/happyPath/_003__insert_another_document.java diff --git a/community/flamingock-auditstore-sql/build.gradle.kts b/community/flamingock-auditstore-sql/build.gradle.kts new file mode 100644 index 000000000..2b0d73d35 --- /dev/null +++ b/community/flamingock-auditstore-sql/build.gradle.kts @@ -0,0 +1,24 @@ +dependencies { + api(project(":core:flamingock-core")) + api(project(":core:target-systems:sql-target-system")) + + testImplementation("mysql:mysql-connector-java:8.0.33") + testImplementation(project(":utils:test-util")) + testImplementation("org.testcontainers:mysql:1.21.3") + testImplementation("com.zaxxer:HikariCP:3.4.5") + testImplementation("org.testcontainers:junit-jupiter:1.21.3") + testImplementation("com.h2database:h2:2.2.224") + testImplementation("org.mockito:mockito-inline:4.11.0") +} + +description = "SQL audit store implementation for distributed change auditing" + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(8)) + } +} + +configurations.testImplementation { + extendsFrom(configurations.compileOnly.get()) +} \ No newline at end of file diff --git a/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/driver/SqlAuditStore.java b/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/driver/SqlAuditStore.java new file mode 100644 index 000000000..8d91e840f --- /dev/null +++ b/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/driver/SqlAuditStore.java @@ -0,0 +1,77 @@ +/* + * Copyright 2025 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.community.sql.driver; + +import io.flamingock.internal.core.store.CommunityAuditStore; +import io.flamingock.internal.core.store.audit.community.CommunityAuditPersistence; +import io.flamingock.internal.core.store.lock.community.CommunityLockService; +import io.flamingock.internal.common.core.context.ContextResolver; +import io.flamingock.internal.core.configuration.community.CommunityConfigurable; +import io.flamingock.internal.util.constants.CommunityPersistenceConstants; +import io.flamingock.internal.util.id.RunnerId; +import io.flamingock.community.sql.internal.SqlAuditPersistence; +import io.flamingock.community.sql.internal.SqlLockService; + +import javax.sql.DataSource; + +public class SqlAuditStore implements CommunityAuditStore { + + private final DataSource dataSource; + private CommunityConfigurable communityConfiguration; + private RunnerId runnerId; + private SqlAuditPersistence persistence; + private SqlLockService lockService; + private String auditRepositoryName = CommunityPersistenceConstants.DEFAULT_AUDIT_STORE_NAME; + private boolean autoCreate = true; + + public SqlAuditStore(DataSource dataSource) { + this.dataSource = dataSource; + } + + public SqlAuditStore withAuditRepositoryName(String auditRepositoryName) { + this.auditRepositoryName = auditRepositoryName; + return this; + } + + public SqlAuditStore withAutoCreate(boolean autoCreate) { + this.autoCreate = autoCreate; + return this; + } + + @Override + public void initialize(ContextResolver baseContext) { + runnerId = baseContext.getRequiredDependencyValue(RunnerId.class); + communityConfiguration = baseContext.getRequiredDependencyValue(CommunityConfigurable.class); + } + + @Override + public synchronized CommunityAuditPersistence getPersistence() { + if (persistence == null) { + persistence = new SqlAuditPersistence(communityConfiguration, dataSource, auditRepositoryName, autoCreate); + persistence.initialize(runnerId); + } + return persistence; + } + + + @Override + public synchronized CommunityLockService getLockService() { + if (lockService == null) { + lockService = new SqlLockService(dataSource); + } + return lockService; + } +} diff --git a/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/internal/SqlAuditPersistence.java b/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/internal/SqlAuditPersistence.java new file mode 100644 index 000000000..fbdca83e3 --- /dev/null +++ b/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/internal/SqlAuditPersistence.java @@ -0,0 +1,59 @@ +/* + * Copyright 2025 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.community.sql.internal; + +import io.flamingock.internal.core.configuration.community.CommunityConfigurable; +import io.flamingock.internal.core.store.audit.community.AbstractCommunityAuditPersistence; +import io.flamingock.internal.common.core.audit.AuditEntry; +import io.flamingock.internal.util.Result; +import io.flamingock.internal.util.id.RunnerId; + +import javax.sql.DataSource; +import java.util.List; + +public class SqlAuditPersistence extends AbstractCommunityAuditPersistence { + + private final DataSource dataSource; + private final String auditRepositoryName; + private final boolean autoCreate; + private SqlAuditor auditor; + + public SqlAuditPersistence(CommunityConfigurable localConfiguration, + DataSource dataSource, + String auditRepositoryName, + boolean autoCreate) { + super(localConfiguration); + this.dataSource = dataSource; + this.auditRepositoryName = auditRepositoryName; + this.autoCreate = autoCreate; + } + + @Override + protected void doInitialize(RunnerId runnerId) { + auditor = new SqlAuditor(dataSource, auditRepositoryName, autoCreate); + auditor.initialize(); + } + + @Override + public List getAuditHistory() { + return auditor.getAuditHistory(); + } + + @Override + public Result writeEntry(AuditEntry auditEntry) { + return auditor.writeEntry(auditEntry); + } +} diff --git a/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/internal/SqlAuditor.java b/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/internal/SqlAuditor.java new file mode 100644 index 000000000..0741e179a --- /dev/null +++ b/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/internal/SqlAuditor.java @@ -0,0 +1,100 @@ +/* + * Copyright 2025 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.community.sql.internal; + +import io.flamingock.internal.common.core.audit.AuditEntry; +import io.flamingock.internal.common.core.audit.AuditReader; +import io.flamingock.internal.core.store.audit.LifecycleAuditWriter; +import io.flamingock.internal.util.Result; + +import javax.sql.DataSource; +import java.sql.*; +import java.util.ArrayList; +import java.util.List; + +public class SqlAuditor implements LifecycleAuditWriter, AuditReader { + + private final DataSource dataSource; + private final String auditTableName; + private final boolean autoCreate; + + public SqlAuditor(DataSource dataSource, String auditTableName, boolean autoCreate) { + this.dataSource = dataSource; + this.auditTableName = auditTableName; + this.autoCreate = autoCreate; + } + + public void initialize() { + if (autoCreate) { + try (Connection conn = dataSource.getConnection(); + Statement stmt = conn.createStatement()) { + stmt.executeUpdate( + "CREATE TABLE IF NOT EXISTS " + auditTableName + " (" + + "id BIGINT AUTO_INCREMENT PRIMARY KEY," + + "execution_id VARCHAR(255)," + + "author VARCHAR(255)," + + "task_id VARCHAR(255)," + + "state VARCHAR(255)," + + "created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP" + + ")" + ); + } catch (SQLException e) { + throw new RuntimeException("Failed to initialize audit table", e); + } + } + } + + @Override + public Result writeEntry(AuditEntry auditEntry) { + try (Connection conn = dataSource.getConnection(); + PreparedStatement ps = conn.prepareStatement( + "INSERT INTO " + auditTableName + " (execution_id, author, task_id, state, created_at) VALUES (?, ?, ?, ?, ?)")) { + ps.setString(1, auditEntry.getExecutionId()); + ps.setString(2, auditEntry.getAuthor()); + ps.setString(3, auditEntry.getTaskId()); + ps.setString(4, auditEntry.getState().name()); + ps.setTimestamp(5, Timestamp.valueOf(auditEntry.getCreatedAt())); + ps.executeUpdate(); + return Result.OK(); + } catch (SQLException e) { + return new Result.Error(e); + } + } + + @Override + public List getAuditHistory() { + List entries = new ArrayList<>(); + try (Connection conn = dataSource.getConnection(); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT execution_id, author, task_id, state, created_at FROM " + auditTableName + " ORDER BY created_at DESC")) { + while (rs.next()) { + AuditEntry entry = new AuditEntry( + rs.getString("execution_id"), + null, + rs.getString("task_id"), + rs.getString("author"), + rs.getTimestamp("created_at").toLocalDateTime(), + AuditEntry.Status.valueOf(rs.getString("state")), + null, null, null, 0L, null, null, false, null, null + ); + entries.add(entry); + } + } catch (SQLException e) { + throw new RuntimeException("Failed to read audit history", e); + } + return entries; + } +} diff --git a/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/internal/SqlLockService.java b/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/internal/SqlLockService.java new file mode 100644 index 000000000..6984cb03f --- /dev/null +++ b/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/internal/SqlLockService.java @@ -0,0 +1,169 @@ +/* + * Copyright 2025 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.community.sql.internal; + +import io.flamingock.internal.core.store.lock.community.CommunityLockService; +import io.flamingock.internal.core.store.lock.community.CommunityLockEntry; +import io.flamingock.internal.core.store.lock.LockAcquisition; +import io.flamingock.internal.core.store.lock.LockKey; +import io.flamingock.internal.core.store.lock.LockServiceException; +import io.flamingock.internal.core.store.lock.LockStatus; +import io.flamingock.internal.util.id.RunnerId; + +import javax.sql.DataSource; +import java.sql.*; +import java.time.LocalDateTime; + +public class SqlLockService implements CommunityLockService { + + private static final String DEFAULT_LOCK_STORE_NAME = "flamingockLock"; + private final DataSource dataSource; + private String lockRepositoryName = DEFAULT_LOCK_STORE_NAME; + + public SqlLockService(DataSource dataSource) { + this.dataSource = dataSource; + } + + public SqlLockService withLockRepositoryName(String lockRepositoryName) { + this.lockRepositoryName = lockRepositoryName; + return this; + } + + public void initialize(boolean autoCreate) { + if (autoCreate) { + try (Connection conn = dataSource.getConnection(); + Statement stmt = conn.createStatement()) { + stmt.executeUpdate( + "CREATE TABLE IF NOT EXISTS " + lockRepositoryName + " (" + + "`key` VARCHAR(255) PRIMARY KEY," + + "status VARCHAR(32)," + + "owner VARCHAR(255)," + + "expires_at TIMESTAMP" + + ")" + ); + } catch (SQLException e) { + throw new RuntimeException("Failed to initialize lock table", e); + } + } + } + + @Override + public LockAcquisition upsert(LockKey key, RunnerId owner, long leaseMillis) { + String keyStr = key.toString(); + LocalDateTime expiresAt = LocalDateTime.now().plusNanos(leaseMillis * 1_000_000); + try (Connection conn = dataSource.getConnection()) { + CommunityLockEntry existing = getLockEntry(conn, keyStr); + if (existing == null || + owner.toString().equals(existing.getOwner()) || + LocalDateTime.now().isAfter(existing.getExpiresAt())) { + upsertLockEntry(conn, keyStr, owner.toString(), expiresAt); + } else { + throw new LockServiceException("upsert", keyStr, + "Still locked by " + existing.getOwner() + " until " + existing.getExpiresAt()); + } + } catch (SQLException e) { + throw new LockServiceException("upsert", keyStr, e.getMessage()); + } + return new LockAcquisition(owner, leaseMillis); + } + + @Override + public LockAcquisition extendLock(LockKey key, RunnerId owner, long leaseMillis) throws LockServiceException { + String keyStr = key.toString(); + LocalDateTime expiresAt = LocalDateTime.now().plusNanos(leaseMillis * 1_000_000); + try (Connection conn = dataSource.getConnection()) { + CommunityLockEntry existing = getLockEntry(conn, keyStr); + if (existing != null && owner.toString().equals(existing.getOwner())) { + upsertLockEntry(conn, keyStr, owner.toString(), expiresAt); + } else { + throw new LockServiceException("extendLock", keyStr, + "Lock belongs to " + (existing != null ? existing.getOwner() : "none")); + } + } catch (SQLException e) { + throw new LockServiceException("extendLock", keyStr, e.getMessage()); + } + return new LockAcquisition(owner, leaseMillis); + } + + @Override + public LockAcquisition getLock(LockKey lockKey) { + String keyStr = lockKey.toString(); + try (Connection conn = dataSource.getConnection()) { + CommunityLockEntry entry = getLockEntry(conn, keyStr); + if (entry != null) { + return new LockAcquisition(RunnerId.fromString(entry.getOwner()), + java.sql.Timestamp.valueOf(entry.getExpiresAt()).getTime() - System.currentTimeMillis()); + } + } catch (SQLException e) { + // ignore + } + return null; + } + + @Override + public void releaseLock(LockKey lockKey, RunnerId owner) { + String keyStr = lockKey.toString(); + try (Connection conn = dataSource.getConnection(); + PreparedStatement ps = conn.prepareStatement( + "SELECT owner FROM " + lockRepositoryName + " WHERE `key` = ?")) { + ps.setString(1, keyStr); + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) { + String existingOwner = rs.getString("owner"); + if (existingOwner.equals(owner.toString())) { + try (PreparedStatement delete = conn.prepareStatement( + "DELETE FROM " + lockRepositoryName + " WHERE `key` = ?")) { + delete.setString(1, keyStr); + delete.executeUpdate(); + } + } + } + } + } catch (SQLException e) { + // ignore + } + } + + private CommunityLockEntry getLockEntry(Connection conn, String key) throws SQLException { + try (PreparedStatement ps = conn.prepareStatement( + "SELECT `key`, status, owner, expires_at FROM " + lockRepositoryName + " WHERE `key` = ?")) { + ps.setString(1, key); + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) { + return new CommunityLockEntry( + rs.getString("key"), + LockStatus.valueOf(rs.getString("status")), + rs.getString("owner"), + rs.getTimestamp("expires_at").toLocalDateTime() + ); + } + } + } + return null; + } + + private void upsertLockEntry(Connection conn, String key, String owner, LocalDateTime expiresAt) throws SQLException { + try (PreparedStatement ps = conn.prepareStatement( + "INSERT INTO " + lockRepositoryName + " (`key`, status, owner, expires_at) VALUES (?, ?, ?, ?) " + + "ON DUPLICATE KEY UPDATE status = VALUES(status), owner = VALUES(owner), expires_at = VALUES(expires_at)")) { + ps.setString(1, key); + ps.setString(2, LockStatus.LOCK_HELD.name()); + ps.setString(3, owner); + ps.setTimestamp(4, Timestamp.valueOf(expiresAt)); + ps.executeUpdate(); + } + } +} diff --git a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/PipelineTestHelper.java b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/PipelineTestHelper.java new file mode 100644 index 000000000..617020b8a --- /dev/null +++ b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/PipelineTestHelper.java @@ -0,0 +1,160 @@ +/* + * 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.community.sql; + +import io.flamingock.api.StageType; +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.TargetSystem; +import io.flamingock.internal.common.core.preview.CodePreviewChange; +import io.flamingock.internal.common.core.preview.PreviewMethod; +import io.flamingock.internal.common.core.preview.PreviewPipeline; +import io.flamingock.internal.common.core.preview.PreviewStage; +import io.flamingock.internal.common.core.task.RecoveryDescriptor; +import io.flamingock.internal.common.core.task.TargetSystemDescriptor; +import io.flamingock.internal.core.task.loaded.ChangeOrderUtil; +import io.flamingock.internal.util.Pair; +import io.flamingock.internal.util.Trio; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class PipelineTestHelper { + + private static final Function, ChangeInfo> infoExtractor = c -> { + Change ann = c.getAnnotation(Change.class); + TargetSystem targetSystemAnn = c.getAnnotation(TargetSystem.class); + String targetSystemId = targetSystemAnn != null ? targetSystemAnn.id() : null; + String changeId = ann.id(); + String order = ChangeOrderUtil.getMatchedOrderFromClassName(changeId, null, c.getName()); + return new ChangeInfo(changeId, order, ann.author(), targetSystemId, ann.transactional()); + }; + + @NotNull + private static List getParameterTypes(List> second) { + return second + .stream() + .map(Class::getName) + .collect(Collectors.toList()); + } + + /** + * Builds a {@link PreviewPipeline} composed of a single {@link PreviewStage} containing one or more {@link CodePreviewChange}s. + *

+ * Each change is derived from a {@link Pair} where: + *

    + *
  • The first item is the {@link Class} annotated with {@link Change}
  • + *
  • The second item is a {@link List} of parameter types (as {@link Class}) expected by the method annotated with {@code @Execution}
  • + *
  • The third item is a {@link List} of parameter types (as {@link Class}) expected by the method annotated with {@code @RollbackExecution}
  • + *
+ * + * @param changeDefinitions varargs of pairs containing change classes and their execution method parameters + * @return a {@link PreviewPipeline} ready for preview or testing + */ + @SafeVarargs + public static PreviewPipeline getPreviewPipeline(String stageName, Trio, List>, List>>... changeDefinitions) { + + List tasks = Arrays.stream(changeDefinitions) + .map(trio -> { + ChangeInfo changeInfo = infoExtractor.apply(trio.getFirst()); + PreviewMethod rollback = null; + PreviewMethod rollbackBeforeExecution = null; + if (trio.getThird() != null) { + rollback = new PreviewMethod("rollbackExecution", getParameterTypes(trio.getThird())); + rollbackBeforeExecution = new PreviewMethod("rollbackBeforeExecution", getParameterTypes(trio.getThird())); + } + + List changes = new ArrayList<>(); + changes.add(new CodePreviewChange( + changeInfo.getChangeId(), + changeInfo.getOrder(), + changeInfo.getAuthor(), + trio.getFirst().getName(), + new PreviewMethod("execution", getParameterTypes(trio.getSecond())), + rollback, + new PreviewMethod("beforeExecution", getParameterTypes(trio.getSecond())), + rollbackBeforeExecution, + false, + changeInfo.transactional, + false, + changeInfo.targetSystem, + RecoveryDescriptor.getDefault() + )); + return changes; + }) + .flatMap(List::stream) + .collect(Collectors.toList()); + + PreviewStage stage = new PreviewStage( + stageName, + StageType.DEFAULT, + "some description", + null, + null, + tasks + ); + + return new PreviewPipeline(Collections.singletonList(stage)); + } + + @SafeVarargs + public static PreviewPipeline getPreviewPipeline(Trio, List>, List>>... changeDefinitions) { + return getPreviewPipeline("default-stage-name", changeDefinitions); + } + + + + static class ChangeInfo { + private final String changeId; + private final String order; + private final String author; + private final TargetSystemDescriptor targetSystem; + private final boolean transactional; + + public ChangeInfo(String changeId, String order, String author, String targetSystemId, boolean transactional) { + this.changeId = changeId; + this.order = order; + this.author = author; + this.targetSystem = new TargetSystemDescriptor(targetSystemId); + this.transactional = transactional; + } + + public String getChangeId() { + return changeId; + } + + public String getOrder() { + return order; + } + + public String getAuthor() { + return author; + } + + public TargetSystemDescriptor getTargetSystem() { + return targetSystem; + } + + public boolean isTransactional() { + return transactional; + } + } + +} diff --git a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/SqlAuditStoreTest.java b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/SqlAuditStoreTest.java new file mode 100644 index 000000000..eea57d307 --- /dev/null +++ b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/SqlAuditStoreTest.java @@ -0,0 +1,361 @@ +/* + * Copyright 2025 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.community.sql; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import io.flamingock.community.sql.driver.SqlAuditStore; +import io.flamingock.core.processor.util.Deserializer; +import io.flamingock.internal.core.builder.FlamingockFactory; +import io.flamingock.internal.core.runner.PipelineExecutionException; +import io.flamingock.internal.util.Trio; +import io.flamingock.internal.util.constants.CommunityPersistenceConstants; +import io.flamingock.targetsystem.mysql.SqlTargetSystem; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.testcontainers.containers.MySQLContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.*; + +@Testcontainers +class SqlAuditStoreTest { + + private static DataSource dataSource; + + @Container + public static final MySQLContainer mysqlContainer = new MySQLContainer<>("mysql:8.0") + .withDatabaseName("testdb") + .withUsername("testuser") + .withPassword("testpass"); + + @BeforeEach + void setUp() throws SQLException { + if (dataSource instanceof HikariDataSource) { + ((HikariDataSource) dataSource).close(); + } + HikariConfig config = new HikariConfig(); + config.setJdbcUrl(mysqlContainer.getJdbcUrl()); + config.setUsername(mysqlContainer.getUsername()); + config.setPassword(mysqlContainer.getPassword()); + config.setDriverClassName(mysqlContainer.getDriverClassName()); + dataSource = new HikariDataSource(config); + + try (Connection conn = dataSource.getConnection()) { + conn.createStatement().execute("DROP TABLE IF EXISTS flamingockAuditLog"); + conn.createStatement().execute("DROP TABLE IF EXISTS test_table"); + conn.createStatement().execute("DROP TABLE IF EXISTS flamingockLock"); + conn.createStatement().execute( + "CREATE TABLE test_table (" + + "id VARCHAR(255) PRIMARY KEY, " + + "name VARCHAR(255), " + + "field1 VARCHAR(255), " + + "field2 VARCHAR(255))" + ); + conn.createStatement().execute( + "CREATE TABLE flamingockLock (" + + "`key` VARCHAR(255) PRIMARY KEY, " + + "status VARCHAR(32), " + + "owner VARCHAR(255), " + + "expires_at TIMESTAMP)" + ); + } + } + + @AfterEach + void tearDown() throws SQLException { + try (Connection conn = dataSource.getConnection()) { + // Drop index if exists + try (ResultSet rs = conn.getMetaData().getIndexInfo(null, null, "test_table", false, false)) { + while (rs.next()) { + String idxName = rs.getString("INDEX_NAME"); + if ("idx_standalone_index".equals(idxName)) { + conn.createStatement().execute("DROP INDEX idx_standalone_index ON test_table"); + break; + } + } + } + conn.createStatement().execute("DROP TABLE IF EXISTS test_table"); + conn.createStatement().execute("DROP TABLE IF EXISTS flamingockLock"); + conn.createStatement().execute("DROP TABLE IF EXISTS flamingockAuditLog"); + } + if (dataSource instanceof HikariDataSource) { + ((HikariDataSource) dataSource).close(); + } + } + + @Test + @DisplayName("When standalone runs the driver should persist the audit logs and the test data") + void happyPathWithMockedPipeline() throws Exception { + try (MockedStatic mocked = Mockito.mockStatic(Deserializer.class)) { + mocked.when(Deserializer::readPreviewPipelineFromFile).thenReturn(PipelineTestHelper.getPreviewPipeline( + new Trio<>(io.flamingock.community.sql.changes.happyPath._001__create_index.class, Collections.singletonList(Connection.class), null), + new Trio<>(io.flamingock.community.sql.changes.happyPath._002__insert_document.class, Collections.singletonList(Connection.class), null), + new Trio<>(io.flamingock.community.sql.changes.happyPath._003__insert_another_document.class, Collections.singletonList(Connection.class), null) + )); + + SqlAuditStore auditStore = new SqlAuditStore(dataSource); + SqlTargetSystem targetSystem = new SqlTargetSystem("sql", dataSource); + + FlamingockFactory.getCommunityBuilder() + .setAuditStore(auditStore) + .addTargetSystem(targetSystem) + .build() + .run(); + } + + try (Connection conn = dataSource.getConnection(); + PreparedStatement ps = conn.prepareStatement( + "SELECT task_id, state FROM " + CommunityPersistenceConstants.DEFAULT_AUDIT_STORE_NAME + " ORDER BY id ASC"); + ResultSet rs = ps.executeQuery()) { + + String[] expectedTaskIds = {"create-index", "insert-document", "insert-another-document"}; + int recordCount = 0; + int startedCount = 0; + int appliedCount = 0; + + while (rs.next()) { + String taskId = rs.getString("task_id"); + String state = rs.getString("state"); + assertTrue( + java.util.Arrays.asList(expectedTaskIds).contains(taskId), + "Unexpected task_id: " + taskId + ); + assertTrue( + state.equals("STARTED") || state.equals("APPLIED"), + "Unexpected state: " + state + ); + if (state.equals("STARTED")) startedCount++; + if (state.equals("APPLIED")) appliedCount++; + recordCount++; + } + + assertEquals(6, recordCount, "Audit log should have 6 records"); + assertEquals(3, startedCount, "Should have 3 STARTED records"); + assertEquals(3, appliedCount, "Should have 3 APPLIED records"); + } + + try (Connection conn = dataSource.getConnection(); + PreparedStatement ps = conn.prepareStatement("SELECT name FROM test_table WHERE id = ?")) { + ps.setString(1, "test-client-Federico"); + try (ResultSet rs = ps.executeQuery()) { + assertTrue(rs.next()); + assertEquals("Federico", rs.getString("name")); + } + ps.setString(1, "test-client-Jorge"); + try (ResultSet rs = ps.executeQuery()) { + assertTrue(rs.next()); + assertEquals("Jorge", rs.getString("name")); + } + } + } + + + @Test + @DisplayName("When standalone runs the driver and execution fails (with rollback method) should persist all the audit logs up to the failed one (ROLLED_BACK)") + void failedWithRollback() throws Exception { + try (MockedStatic mocked = Mockito.mockStatic(Deserializer.class)) { + mocked.when(Deserializer::readPreviewPipelineFromFile).thenReturn( + PipelineTestHelper.getPreviewPipeline( + new Trio<>(io.flamingock.community.sql.changes.failedWithRollback._001__create_index.class, Collections.singletonList(Connection.class), null), + new Trio<>(io.flamingock.community.sql.changes.failedWithRollback._002__insert_document.class, Collections.singletonList(Connection.class), null), + new Trio<>(io.flamingock.community.sql.changes.failedWithRollback._003__execution_with_exception.class, Collections.singletonList(Connection.class), null) + ) + ); + + SqlAuditStore auditStore = new SqlAuditStore(dataSource); + SqlTargetSystem targetSystem = new SqlTargetSystem("sql", dataSource); + + assertThrows(PipelineExecutionException.class, () -> { + FlamingockFactory.getCommunityBuilder() + .setAuditStore(auditStore) + .addTargetSystem(targetSystem) + .build() + .run(); + }); + + try (Connection conn = dataSource.getConnection(); + PreparedStatement ps = conn.prepareStatement( + "SELECT task_id, state FROM " + CommunityPersistenceConstants.DEFAULT_AUDIT_STORE_NAME + " ORDER BY id ASC"); + ResultSet rs = ps.executeQuery()) { + + assertTrue(rs.next()); + assertEquals("create-index", rs.getString("task_id")); + assertEquals("STARTED", rs.getString("state")); + + assertTrue(rs.next()); + assertEquals("create-index", rs.getString("task_id")); + assertEquals("APPLIED", rs.getString("state")); + + assertTrue(rs.next()); + assertEquals("insert-document", rs.getString("task_id")); + assertEquals("STARTED", rs.getString("state")); + + assertTrue(rs.next()); + assertEquals("insert-document", rs.getString("task_id")); + assertEquals("APPLIED", rs.getString("state")); + + assertTrue(rs.next()); + assertEquals("execution-with-exception", rs.getString("task_id")); + assertEquals("STARTED", rs.getString("state")); + + assertTrue(rs.next()); + assertEquals("execution-with-exception", rs.getString("task_id")); + assertEquals("FAILED", rs.getString("state")); + + assertTrue(rs.next()); + assertEquals("execution-with-exception", rs.getString("task_id")); + assertEquals("ROLLED_BACK", rs.getString("state")); + + assertFalse(rs.next()); + } + + + try (Connection conn = dataSource.getConnection(); + ResultSet rs = conn.getMetaData().getIndexInfo(null, null, "test_table", false, false)) { + boolean found = false; + while (rs.next()) { + if ("idx_standalone_index".equals(rs.getString("INDEX_NAME"))) { + found = true; + break; + } + } + assertTrue(found, "Index idx_standalone_index should exist"); + } + + try (Connection conn = dataSource.getConnection(); + PreparedStatement ps = conn.prepareStatement("SELECT name FROM test_table WHERE id = ?")) { + ps.setString(1, "test-client-Federico"); + try (ResultSet rs = ps.executeQuery()) { + assertTrue(rs.next()); + assertEquals("Federico", rs.getString("name")); + } + } + + try (Connection conn = dataSource.getConnection(); + PreparedStatement ps = conn.prepareStatement("SELECT name FROM test_table WHERE id = ?")) { + ps.setString(1, "test-client-Jorge"); + try (ResultSet rs = ps.executeQuery()) { + assertFalse(rs.next()); + } + } + + } + } + + @Test + @DisplayName("When standalone runs the driver and execution fails (without rollback method) should persist all the audit logs up to the failed one (FAILED)") + void failedWithoutRollback() throws SQLException { + try (MockedStatic mocked = Mockito.mockStatic(Deserializer.class)) { + mocked.when(Deserializer::readPreviewPipelineFromFile).thenReturn( + PipelineTestHelper.getPreviewPipeline( + new Trio<>(io.flamingock.community.sql.changes.failedWithoutRollback._001__create_index.class, Collections.singletonList(Connection.class), null), + new Trio<>(io.flamingock.community.sql.changes.failedWithoutRollback._002__insert_document.class, Collections.singletonList(Connection.class), null), + new Trio<>(io.flamingock.community.sql.changes.failedWithoutRollback._003__execution_with_exception.class, Collections.singletonList(Connection.class), null) + ) + ); + + SqlAuditStore auditStore = new SqlAuditStore(dataSource); + SqlTargetSystem targetSystem = new SqlTargetSystem("sql", dataSource); + + assertThrows(PipelineExecutionException.class, () -> { + FlamingockFactory.getCommunityBuilder() + .setAuditStore(auditStore) + .addTargetSystem(targetSystem) + .build() + .run(); + }); + + + try (Connection conn = dataSource.getConnection(); + PreparedStatement ps = conn.prepareStatement( + "SELECT task_id, state FROM " + CommunityPersistenceConstants.DEFAULT_AUDIT_STORE_NAME + " ORDER BY id ASC"); + ResultSet rs = ps.executeQuery()) { + + assertTrue(rs.next()); + assertEquals("create-index", rs.getString("task_id")); + assertEquals("STARTED", rs.getString("state")); + + assertTrue(rs.next()); + assertEquals("create-index", rs.getString("task_id")); + assertEquals("APPLIED", rs.getString("state")); + + assertTrue(rs.next()); + assertEquals("insert-document", rs.getString("task_id")); + assertEquals("STARTED", rs.getString("state")); + + assertTrue(rs.next()); + assertEquals("insert-document", rs.getString("task_id")); + assertEquals("APPLIED", rs.getString("state")); + + assertTrue(rs.next()); + assertEquals("execution-with-exception", rs.getString("task_id")); + assertEquals("STARTED", rs.getString("state")); + + assertTrue(rs.next()); + assertEquals("execution-with-exception", rs.getString("task_id")); + assertEquals("FAILED", rs.getString("state")); + + assertTrue(rs.next()); + assertEquals("execution-with-exception", rs.getString("task_id")); + assertEquals("ROLLED_BACK", rs.getString("state")); + + assertFalse(rs.next()); + } + + try (Connection conn = dataSource.getConnection(); + ResultSet rs = conn.getMetaData().getIndexInfo(null, null, "test_table", false, false)) { + boolean found = false; + while (rs.next()) { + if ("idx_standalone_index".equals(rs.getString("INDEX_NAME"))) { + found = true; + break; + } + } + assertTrue(found, "Index idx_standalone_index should exist"); + } + + try (Connection conn = dataSource.getConnection(); + PreparedStatement ps = conn.prepareStatement("SELECT name FROM test_table WHERE id = ?")) { + ps.setString(1, "test-client-Federico"); + try (ResultSet rs = ps.executeQuery()) { + assertTrue(rs.next()); + assertEquals("Federico", rs.getString("name")); + } + } + + try (Connection conn = dataSource.getConnection(); + PreparedStatement ps = conn.prepareStatement("SELECT name FROM test_table WHERE id = ?")) { + ps.setString(1, "test-client-Jorge"); + try (ResultSet rs = ps.executeQuery()) { + assertFalse(rs.next()); + } + } + } + } +} diff --git a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/failedWithRollback/_001__create_index.java b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/failedWithRollback/_001__create_index.java new file mode 100644 index 000000000..2b4e5aa7a --- /dev/null +++ b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/failedWithRollback/_001__create_index.java @@ -0,0 +1,36 @@ +/* + * Copyright 2025 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.community.sql.changes.failedWithRollback; + +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.TargetSystem; + +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; + +@TargetSystem(id = "sql") +@Change(id = "create-index", transactional = false, author = "aperezdieppa") +public class _001__create_index { + + @Apply + public void execution(Connection connection) throws SQLException { + try (Statement stmt = connection.createStatement()) { + stmt.executeUpdate("CREATE INDEX idx_standalone_index ON test_table(field1, field2)"); + } + } +} diff --git a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/failedWithRollback/_002__insert_document.java b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/failedWithRollback/_002__insert_document.java new file mode 100644 index 000000000..57bdff9ca --- /dev/null +++ b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/failedWithRollback/_002__insert_document.java @@ -0,0 +1,49 @@ +/* + * Copyright 2025 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.community.sql.changes.failedWithRollback; + +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.Rollback; +import io.flamingock.api.annotations.TargetSystem; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +@TargetSystem(id = "sql") +@Change(id = "insert-document", transactional = false, author = "aperezdieppa") +public class _002__insert_document { + + @Apply + public void execution(Connection connection) throws SQLException { + try (PreparedStatement ps = connection.prepareStatement( + "INSERT INTO test_table (id, name) VALUES (?, ?)")) { + ps.setString(1, "test-client-Federico"); + ps.setString(2, "Federico"); + ps.executeUpdate(); + } + } + + @Rollback + public void rollbackExecution(Connection connection) throws SQLException { + try (PreparedStatement ps = connection.prepareStatement( + "DELETE FROM test_table WHERE id = ?")) { + ps.setString(1, "test-client-Federico"); + ps.executeUpdate(); + } + } +} diff --git a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/failedWithRollback/_003__execution_with_exception.java b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/failedWithRollback/_003__execution_with_exception.java new file mode 100644 index 000000000..3451185ba --- /dev/null +++ b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/failedWithRollback/_003__execution_with_exception.java @@ -0,0 +1,50 @@ +/* + * Copyright 2025 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.community.sql.changes.failedWithRollback; + +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.Rollback; +import io.flamingock.api.annotations.TargetSystem; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +@TargetSystem(id = "sql") +@Change(id = "execution-with-exception", transactional = false, author = "aperezdieppa") +public class _003__execution_with_exception { + + @Apply + public void execution(Connection connection) throws SQLException { + try (PreparedStatement ps = connection.prepareStatement( + "INSERT INTO test_table (id, name) VALUES (?, ?)")) { + ps.setString(1, "test-client-Jorge"); + ps.setString(2, "Jorge"); + ps.executeUpdate(); + } + throw new RuntimeException("test"); + } + + @Rollback + public void rollbackExecution(Connection connection) throws SQLException { + try (PreparedStatement ps = connection.prepareStatement( + "DELETE FROM test_table WHERE id = ?")) { + ps.setString(1, "test-client-Jorge"); + ps.executeUpdate(); + } + } +} diff --git a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/failedWithoutRollback/_001__create_index.java b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/failedWithoutRollback/_001__create_index.java new file mode 100644 index 000000000..9f0a32769 --- /dev/null +++ b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/failedWithoutRollback/_001__create_index.java @@ -0,0 +1,36 @@ +/* + * Copyright 2025 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.community.sql.changes.failedWithoutRollback; + +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.TargetSystem; + +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; + +@TargetSystem(id = "sql") +@Change(id = "create-index", transactional = false, author = "aperezdieppa") +public class _001__create_index { + + @Apply + public void execution(Connection connection) throws SQLException { + try (Statement stmt = connection.createStatement()) { + stmt.executeUpdate("CREATE INDEX idx_standalone_index ON test_table(field1, field2)"); + } + } +} diff --git a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/failedWithoutRollback/_002__insert_document.java b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/failedWithoutRollback/_002__insert_document.java new file mode 100644 index 000000000..1fcf43300 --- /dev/null +++ b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/failedWithoutRollback/_002__insert_document.java @@ -0,0 +1,39 @@ +/* + * Copyright 2025 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.community.sql.changes.failedWithoutRollback; + +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.TargetSystem; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +@TargetSystem(id = "sql") +@Change(id = "insert-document", author = "aperezdieppa") +public class _002__insert_document { + + @Apply + public void execution(Connection connection) throws SQLException { + try (PreparedStatement ps = connection.prepareStatement( + "INSERT INTO test_table (id, name) VALUES (?, ?)")) { + ps.setString(1, "test-client-Federico"); + ps.setString(2, "Federico"); + ps.executeUpdate(); + } + } +} diff --git a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/failedWithoutRollback/_003__execution_with_exception.java b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/failedWithoutRollback/_003__execution_with_exception.java new file mode 100644 index 000000000..d4323c65c --- /dev/null +++ b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/failedWithoutRollback/_003__execution_with_exception.java @@ -0,0 +1,40 @@ +/* + * Copyright 2025 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.community.sql.changes.failedWithoutRollback; + +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.TargetSystem; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +@TargetSystem(id = "sql") +@Change(id = "execution-with-exception", author = "aperezdieppa") +public class _003__execution_with_exception { + + @Apply + public void execution(Connection connection) throws SQLException { + try (PreparedStatement ps = connection.prepareStatement( + "INSERT INTO test_table (id, name) VALUES (?, ?)")) { + ps.setString(1, "test-client-Jorge"); + ps.setString(2, "Jorge"); + ps.executeUpdate(); + } + throw new RuntimeException("test"); + } +} diff --git a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/happyPath/_001__create_index.java b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/happyPath/_001__create_index.java new file mode 100644 index 000000000..ba61da194 --- /dev/null +++ b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/happyPath/_001__create_index.java @@ -0,0 +1,35 @@ +/* + * Copyright 2025 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.community.sql.changes.happyPath; + +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.TargetSystem; + +import java.sql.Connection; +import java.sql.Statement; + +@TargetSystem(id = "sql") +@Change(id = "create-index", transactional = false, author = "aperezdieppa") +public class _001__create_index { + + @Apply + public void execution(Connection connection) throws Exception { + try (Statement stmt = connection.createStatement()) { + stmt.execute("CREATE INDEX idx_standalone_index ON test_table(field1, field2)"); + } + } +} diff --git a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/happyPath/_002__insert_document.java b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/happyPath/_002__insert_document.java new file mode 100644 index 000000000..64e042fda --- /dev/null +++ b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/happyPath/_002__insert_document.java @@ -0,0 +1,38 @@ +/* + * Copyright 2025 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.community.sql.changes.happyPath; + +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.TargetSystem; + +import java.sql.Connection; +import java.sql.PreparedStatement; + +@TargetSystem(id = "sql") +@Change(id = "insert-document", author = "aperezdieppa") +public class _002__insert_document { + + @Apply + public void execution(Connection connection) throws Exception { + try (PreparedStatement ps = connection.prepareStatement( + "INSERT INTO test_table (id, name) VALUES (?, ?)")) { + ps.setString(1, "test-client-Federico"); + ps.setString(2, "Federico"); + ps.executeUpdate(); + } + } +} diff --git a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/happyPath/_003__insert_another_document.java b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/happyPath/_003__insert_another_document.java new file mode 100644 index 000000000..d5fd6292e --- /dev/null +++ b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/happyPath/_003__insert_another_document.java @@ -0,0 +1,38 @@ +/* + * Copyright 2025 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.community.sql.changes.happyPath; + +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.TargetSystem; + +import java.sql.Connection; +import java.sql.PreparedStatement; + +@TargetSystem(id = "sql") +@Change(id = "insert-another-document", author = "aperezdieppa") +public class _003__insert_another_document { + + @Apply + public void execution(Connection connection) throws Exception { + try (PreparedStatement ps = connection.prepareStatement( + "INSERT INTO test_table (id, name) VALUES (?, ?)")) { + ps.setString(1, "test-client-Jorge"); + ps.setString(2, "Jorge"); + ps.executeUpdate(); + } + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 38d340b86..05293cd45 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -63,6 +63,10 @@ include("community:flamingock-auditstore-dynamodb") project(":community:flamingock-auditstore-dynamodb").name = "flamingock-auditstore-dynamodb" project(":community:flamingock-auditstore-dynamodb").projectDir = file("community/flamingock-auditstore-dynamodb") +include("community:flamingock-auditstore-sql") +project(":community:flamingock-auditstore-sql").name = "flamingock-auditstore-sql" +project(":community:flamingock-auditstore-sql").projectDir = file("community/flamingock-auditstore-sql") + ////////////////////////////////////// // PLUGINS ////////////////////////////////////// @@ -174,3 +178,5 @@ project(":cli:flamingock-cli").projectDir = file("cli/flamingock-cli") include("e2e:core-e2e") project(":e2e:core-e2e").name = "core-e2e" project(":e2e:core-e2e").projectDir = file("e2e/core-e2e") + +include("community:flamingock-auditstore-sql") \ No newline at end of file From 9589155acf53e13f97f82d05afade25eb500c558 Mon Sep 17 00:00:00 2001 From: davidfrigolet Date: Thu, 16 Oct 2025 08:03:34 +0100 Subject: [PATCH 2/9] feat: sql audit store --- settings.gradle.kts | 2 -- 1 file changed, 2 deletions(-) diff --git a/settings.gradle.kts b/settings.gradle.kts index 05293cd45..2017ed561 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -178,5 +178,3 @@ project(":cli:flamingock-cli").projectDir = file("cli/flamingock-cli") include("e2e:core-e2e") project(":e2e:core-e2e").name = "core-e2e" project(":e2e:core-e2e").projectDir = file("e2e/core-e2e") - -include("community:flamingock-auditstore-sql") \ No newline at end of file From a841a97a12c40201f8b360d9fe0cf47172c27631 Mon Sep 17 00:00:00 2001 From: davidfrigolet Date: Sun, 19 Oct 2025 18:44:42 +0100 Subject: [PATCH 3/9] feat: add sql dialect helpers in sql audit store --- .../build.gradle.kts | 1 + .../community/sql/internal/SqlAuditor.java | 54 +++-- .../sql/internal/SqlAuditorDialectHelper.java | 203 ++++++++++++++++++ .../sql/internal/SqlLockDialectHelper.java | 145 +++++++++++++ .../sql/internal/SqlLockService.java | 18 +- .../community/sql/SqlAuditStoreTest.java | 2 +- 6 files changed, 393 insertions(+), 30 deletions(-) create mode 100644 community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/internal/SqlAuditorDialectHelper.java create mode 100644 community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/internal/SqlLockDialectHelper.java diff --git a/community/flamingock-auditstore-sql/build.gradle.kts b/community/flamingock-auditstore-sql/build.gradle.kts index 2b0d73d35..8b1b860a7 100644 --- a/community/flamingock-auditstore-sql/build.gradle.kts +++ b/community/flamingock-auditstore-sql/build.gradle.kts @@ -1,6 +1,7 @@ dependencies { api(project(":core:flamingock-core")) api(project(":core:target-systems:sql-target-system")) + implementation(project(":utils:sql-util")) testImplementation("mysql:mysql-connector-java:8.0.33") testImplementation(project(":utils:test-util")) diff --git a/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/internal/SqlAuditor.java b/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/internal/SqlAuditor.java index 0741e179a..b21f1441e 100644 --- a/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/internal/SqlAuditor.java +++ b/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/internal/SqlAuditor.java @@ -17,6 +17,7 @@ import io.flamingock.internal.common.core.audit.AuditEntry; import io.flamingock.internal.common.core.audit.AuditReader; +import io.flamingock.internal.common.core.audit.AuditTxType; import io.flamingock.internal.core.store.audit.LifecycleAuditWriter; import io.flamingock.internal.util.Result; @@ -30,27 +31,20 @@ public class SqlAuditor implements LifecycleAuditWriter, AuditReader { private final DataSource dataSource; private final String auditTableName; private final boolean autoCreate; + private final SqlAuditorDialectHelper dialectHelper; public SqlAuditor(DataSource dataSource, String auditTableName, boolean autoCreate) { this.dataSource = dataSource; this.auditTableName = auditTableName; this.autoCreate = autoCreate; + this.dialectHelper = new SqlAuditorDialectHelper(dataSource); } public void initialize() { if (autoCreate) { try (Connection conn = dataSource.getConnection(); Statement stmt = conn.createStatement()) { - stmt.executeUpdate( - "CREATE TABLE IF NOT EXISTS " + auditTableName + " (" + - "id BIGINT AUTO_INCREMENT PRIMARY KEY," + - "execution_id VARCHAR(255)," + - "author VARCHAR(255)," + - "task_id VARCHAR(255)," + - "state VARCHAR(255)," + - "created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP" + - ")" - ); + stmt.executeUpdate(dialectHelper.getCreateTableSqlString(auditTableName)); } catch (SQLException e) { throw new RuntimeException("Failed to initialize audit table", e); } @@ -61,12 +55,26 @@ public void initialize() { public Result writeEntry(AuditEntry auditEntry) { try (Connection conn = dataSource.getConnection(); PreparedStatement ps = conn.prepareStatement( - "INSERT INTO " + auditTableName + " (execution_id, author, task_id, state, created_at) VALUES (?, ?, ?, ?, ?)")) { + dialectHelper.getInsertSqlString(auditTableName))) { ps.setString(1, auditEntry.getExecutionId()); - ps.setString(2, auditEntry.getAuthor()); + ps.setString(2, auditEntry.getStageId()); ps.setString(3, auditEntry.getTaskId()); - ps.setString(4, auditEntry.getState().name()); + ps.setString(4, auditEntry.getAuthor()); ps.setTimestamp(5, Timestamp.valueOf(auditEntry.getCreatedAt())); + ps.setString(6, auditEntry.getState() != null ? auditEntry.getState().name() : null); + ps.setString(7, auditEntry.getClassName()); + ps.setString(8, auditEntry.getMethodName()); + ps.setString(9, auditEntry.getMetadata() != null ? auditEntry.getMetadata().toString() : null); + ps.setLong(10, auditEntry.getExecutionMillis()); + ps.setString(11, auditEntry.getExecutionHostname()); + ps.setString(12, auditEntry.getErrorTrace()); + ps.setString(13, auditEntry.getType() != null ? auditEntry.getType().name() : null); + ps.setString(14, auditEntry.getTxType() != null ? auditEntry.getTxType().name() : null); + ps.setString(15, auditEntry.getTargetSystemId()); + ps.setString(16, auditEntry.getOrder()); + ps.setString(17, auditEntry.getRecoveryStrategy() != null ? auditEntry.getRecoveryStrategy().name() : null); + ps.setObject(18, auditEntry.getTransactionFlag()); + ps.setObject(19, auditEntry.getSystemChange()); ps.executeUpdate(); return Result.OK(); } catch (SQLException e) { @@ -79,16 +87,28 @@ public List getAuditHistory() { List entries = new ArrayList<>(); try (Connection conn = dataSource.getConnection(); Statement stmt = conn.createStatement(); - ResultSet rs = stmt.executeQuery("SELECT execution_id, author, task_id, state, created_at FROM " + auditTableName + " ORDER BY created_at DESC")) { + ResultSet rs = stmt.executeQuery(dialectHelper.getSelectHistorySqlString(auditTableName))) { while (rs.next()) { AuditEntry entry = new AuditEntry( rs.getString("execution_id"), - null, + rs.getString("stage_id"), rs.getString("task_id"), rs.getString("author"), rs.getTimestamp("created_at").toLocalDateTime(), - AuditEntry.Status.valueOf(rs.getString("state")), - null, null, null, 0L, null, null, false, null, null + rs.getString("state") != null ? AuditEntry.Status.valueOf(rs.getString("state")) : null, + rs.getString("type") != null ? AuditEntry.ExecutionType.valueOf(rs.getString("type")) : null, + rs.getString("class_name"), + rs.getString("method_name"), + rs.getLong("execution_millis"), + rs.getString("execution_hostname"), + rs.getString("metadata"), + rs.getBoolean("system_change"), + rs.getString("error_trace"), + AuditTxType.fromString(rs.getString("tx_type")), + rs.getString("target_system_id"), + rs.getString("order_col"), + rs.getString("recovery_strategy") != null ? io.flamingock.api.RecoveryStrategy.valueOf(rs.getString("recovery_strategy")) : null, + rs.getObject("transaction_flag") != null ? rs.getBoolean("transaction_flag") : null ); entries.add(entry); } diff --git a/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/internal/SqlAuditorDialectHelper.java b/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/internal/SqlAuditorDialectHelper.java new file mode 100644 index 000000000..cc259312f --- /dev/null +++ b/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/internal/SqlAuditorDialectHelper.java @@ -0,0 +1,203 @@ +/* + * Copyright 2025 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.community.sql.internal; + +import io.flamingock.internal.common.sql.AbstractSqlDialectHelper; +import io.flamingock.internal.common.sql.SqlDialect; + +import javax.sql.DataSource; + +public final class SqlAuditorDialectHelper extends AbstractSqlDialectHelper { + + public SqlAuditorDialectHelper(DataSource dataSource) { + super(dataSource); + } + + public SqlAuditorDialectHelper(SqlDialect dialect) { + super(dialect); + } + + private static final String COMMON_COLUMNS = + "execution_id VARCHAR(255), " + + "stage_id VARCHAR(255), " + + "task_id VARCHAR(255), " + + "author VARCHAR(255), " + + "created_at %s, " + + "state VARCHAR(255), " + + "class_name VARCHAR(255), " + + "method_name VARCHAR(255), " + + "metadata %s, " + + "execution_millis BIGINT, " + + "execution_hostname VARCHAR(255), " + + "error_trace %s, " + + "type VARCHAR(50), " + + "tx_type VARCHAR(50), " + + "target_system_id VARCHAR(255), " + + "order_col VARCHAR(50), " + + "recovery_strategy VARCHAR(50), " + + "transaction_flag %s, " + + "system_change %s"; + + private String getCreatedAtType() { + switch (sqlDialect) { + case SQLSERVER: + case SYBASE: + return "DATETIME DEFAULT GETDATE()"; + case INFORMIX: + return "DATETIME YEAR TO SECOND DEFAULT CURRENT YEAR TO SECOND"; + case ORACLE: + case POSTGRESQL: + case MYSQL: + case MARIADB: + case SQLITE: + case H2: + case HSQLDB: + case DERBY: + case FIREBIRD: + return "TIMESTAMP DEFAULT CURRENT_TIMESTAMP"; + case DB2: + return "TIMESTAMP DEFAULT CURRENT TIMESTAMP"; + default: + return "TIMESTAMP"; + } + } + + private String getMetadataType() { + switch (sqlDialect) { + case ORACLE: + case DB2: + return "CLOB"; + case FIREBIRD: + return "BLOB SUB_TYPE TEXT"; + default: + return "TEXT"; + } + } + + private String getErrorTraceType() { + switch (sqlDialect) { + case ORACLE: + case DB2: + return "CLOB"; + case FIREBIRD: + return "BLOB SUB_TYPE TEXT"; + default: + return "TEXT"; + } + } + + private String getBooleanType() { + switch (sqlDialect) { + case SQLSERVER: + case SYBASE: + return "BIT"; + case ORACLE: + case DB2: + case FIREBIRD: + case INFORMIX: + return "SMALLINT"; + default: + return "BOOLEAN"; + } + } + + public String getCreateTableSqlString(String tableName) { + String columns = String.format(COMMON_COLUMNS, + getCreatedAtType(), + getMetadataType(), + getErrorTraceType(), + getBooleanType(), + getBooleanType()); + + switch (sqlDialect) { + case MYSQL: + case MARIADB: + return String.format( + "CREATE TABLE IF NOT EXISTS %s (" + + "id BIGINT AUTO_INCREMENT PRIMARY KEY, " + + columns + + ")", tableName); + case POSTGRESQL: + return String.format( + "CREATE TABLE IF NOT EXISTS %s (" + + "id SERIAL PRIMARY KEY, " + + columns + + ")", tableName); + case SQLITE: + case H2: + case HSQLDB: + case DERBY: + return String.format( + "CREATE TABLE IF NOT EXISTS %s (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + + columns + + ")", tableName); + case SQLSERVER: + case SYBASE: + return String.format( + "IF NOT EXISTS (SELECT * FROM sysobjects WHERE name='%s' AND xtype='U') " + + "CREATE TABLE %s (" + + "id BIGINT IDENTITY(1,1) PRIMARY KEY, " + + columns + + ")", tableName, tableName); + case ORACLE: + return String.format( + "BEGIN EXECUTE IMMEDIATE 'CREATE TABLE %s (" + + "id NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, " + + columns + + ")'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF; END;", tableName); + case DB2: + return String.format( + "CREATE TABLE %s (" + + "id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, " + + columns + + ")", tableName); + case FIREBIRD: + return String.format( + "CREATE TABLE %s (" + + "id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, " + + columns + + ")", tableName); + case INFORMIX: + return String.format( + "CREATE TABLE %s (" + + "id SERIAL PRIMARY KEY, " + + columns + + ")", tableName); + default: + throw new UnsupportedOperationException("Dialect not supported for CREATE TABLE: " + sqlDialect.name()); + } + } + + public String getInsertSqlString(String tableName) { + return String.format( + "INSERT INTO %s (" + + "execution_id, stage_id, task_id, author, created_at, state, class_name, method_name, metadata, " + + "execution_millis, execution_hostname, error_trace, type, tx_type, target_system_id, order_col, " + + "recovery_strategy, transaction_flag, system_change" + + ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + tableName); + } + + public String getSelectHistorySqlString(String tableName) { + return String.format( + "SELECT execution_id, stage_id, task_id, author, created_at, state, class_name, method_name, metadata, " + + "execution_millis, execution_hostname, error_trace, type, tx_type, target_system_id, order_col, " + + "recovery_strategy, transaction_flag, system_change " + + "FROM %s ORDER BY created_at DESC", + tableName); + } +} diff --git a/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/internal/SqlLockDialectHelper.java b/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/internal/SqlLockDialectHelper.java new file mode 100644 index 000000000..5dc096e02 --- /dev/null +++ b/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/internal/SqlLockDialectHelper.java @@ -0,0 +1,145 @@ +/* + * Copyright 2025 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.community.sql.internal; + +import io.flamingock.internal.common.sql.AbstractSqlDialectHelper; +import io.flamingock.internal.common.sql.SqlDialect; + +import javax.sql.DataSource; + +public final class SqlLockDialectHelper extends AbstractSqlDialectHelper { + + public SqlLockDialectHelper(DataSource dataSource) { + super(dataSource); + } + + public SqlLockDialectHelper(SqlDialect dialect) { + super(dialect); + } + + public String getCreateTableSqlString(String tableName) { + switch (sqlDialect) { + case MYSQL: + case MARIADB: + case POSTGRESQL: + case SQLITE: + case H2: + case HSQLDB: + case DERBY: + case FIREBIRD: + case INFORMIX: + return String.format( + "CREATE TABLE IF NOT EXISTS %s (" + + "`key` VARCHAR(255) PRIMARY KEY," + + "status VARCHAR(32)," + + "owner VARCHAR(255)," + + "expires_at TIMESTAMP" + + ")", tableName); + case SQLSERVER: + case SYBASE: + return String.format( + "IF NOT EXISTS (SELECT * FROM sysobjects WHERE name='%s' AND xtype='U') " + + "CREATE TABLE %s (" + + "[key] VARCHAR(255) PRIMARY KEY," + + "status VARCHAR(32)," + + "owner VARCHAR(255)," + + "expires_at DATETIME" + + ")", tableName, tableName); + case ORACLE: + return String.format( + "BEGIN EXECUTE IMMEDIATE 'CREATE TABLE %s (" + + "\"key\" VARCHAR2(255) PRIMARY KEY," + + "status VARCHAR2(32)," + + "owner VARCHAR2(255)," + + "expires_at TIMESTAMP" + + ")'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF; END;", tableName); + case DB2: + return String.format( + "CREATE TABLE %s (" + + "\"key\" VARCHAR(255) PRIMARY KEY," + + "status VARCHAR(32)," + + "owner VARCHAR(255)," + + "expires_at TIMESTAMP" + + ")", tableName); + default: + throw new UnsupportedOperationException("Dialect not supported for CREATE TABLE: " + sqlDialect.name()); + } + } + + public String getSelectLockSqlString(String tableName) { + return String.format("SELECT `key`, status, owner, expires_at FROM %s WHERE `key` = ?", tableName); + } + + public String getInsertOrUpdateLockSqlString(String tableName) { + switch (sqlDialect) { + case MYSQL: + case MARIADB: + case INFORMIX: + return String.format( + "INSERT INTO %s (`key`, status, owner, expires_at) VALUES (?, ?, ?, ?) " + + "ON DUPLICATE KEY UPDATE status = VALUES(status), owner = VALUES(owner), expires_at = VALUES(expires_at)", + tableName); + case POSTGRESQL: + return String.format( + "INSERT INTO %s (\"key\", status, owner, expires_at) VALUES (?, ?, ?, ?) " + + "ON CONFLICT (\"key\") DO UPDATE SET status = EXCLUDED.status, owner = EXCLUDED.owner, expires_at = EXCLUDED.expires_at", + tableName); + case SQLITE: + return String.format( + "INSERT OR REPLACE INTO %s (`key`, status, owner, expires_at) VALUES (?, ?, ?, ?)", + tableName); + case SQLSERVER: + case SYBASE: + return String.format( + "MERGE INTO %s AS target USING (SELECT ? AS [key], ? AS status, ? AS owner, ? AS expires_at) AS source " + + "ON (target.[key] = source.[key]) " + + "WHEN MATCHED THEN UPDATE SET status = source.status, owner = source.owner, expires_at = source.expires_at " + + "WHEN NOT MATCHED THEN INSERT ([key], status, owner, expires_at) VALUES (source.[key], source.status, source.owner, source.expires_at);", + tableName); + case ORACLE: + return String.format( + "MERGE INTO %s t USING (SELECT ? AS \"key\", ? AS status, ? AS owner, ? AS expires_at FROM dual) s " + + "ON (t.\"key\" = s.\"key\") " + + "WHEN MATCHED THEN UPDATE SET t.status = s.status, t.owner = s.owner, t.expires_at = s.expires_at " + + "WHEN NOT MATCHED THEN INSERT (\"key\", status, owner, expires_at) VALUES (s.\"key\", s.status, s.owner, s.expires_at)", + tableName); + case H2: + case HSQLDB: + case DERBY: + return String.format( + "MERGE INTO %s (`key`, status, owner, expires_at) KEY (`key`) VALUES (?, ?, ?, ?)", + tableName); + case DB2: + return String.format( + "MERGE INTO %s USING (SELECT ? AS \"key\", ? AS status, ? AS owner, ? AS expires_at FROM SYSIBM.SYSDUMMY1) AS src " + + "ON (%s.\"key\" = src.\"key\") " + + "WHEN MATCHED THEN UPDATE SET status = src.status, owner = src.owner, expires_at = src.expires_at " + + "WHEN NOT MATCHED THEN INSERT (\"key\", status, owner, expires_at) VALUES (src.\"key\", src.status, src.owner, src.expires_at)", + tableName, tableName); + case FIREBIRD: + return String.format( + "UPDATE OR INSERT INTO %s (`key`, status, owner, expires_at) VALUES (?, ?, ?, ?) MATCHING (`key`)", + tableName); + default: + throw new UnsupportedOperationException("Dialect not supported for upsert: " + sqlDialect.name()); + } + } + + public String getDeleteLockSqlString(String tableName) { + return String.format("DELETE FROM %s WHERE `key` = ?", tableName); + } +} + diff --git a/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/internal/SqlLockService.java b/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/internal/SqlLockService.java index 6984cb03f..9623407e3 100644 --- a/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/internal/SqlLockService.java +++ b/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/internal/SqlLockService.java @@ -32,9 +32,11 @@ public class SqlLockService implements CommunityLockService { private static final String DEFAULT_LOCK_STORE_NAME = "flamingockLock"; private final DataSource dataSource; private String lockRepositoryName = DEFAULT_LOCK_STORE_NAME; + private final SqlLockDialectHelper dialectHelper; public SqlLockService(DataSource dataSource) { this.dataSource = dataSource; + this.dialectHelper = new SqlLockDialectHelper(dataSource); } public SqlLockService withLockRepositoryName(String lockRepositoryName) { @@ -46,14 +48,7 @@ public void initialize(boolean autoCreate) { if (autoCreate) { try (Connection conn = dataSource.getConnection(); Statement stmt = conn.createStatement()) { - stmt.executeUpdate( - "CREATE TABLE IF NOT EXISTS " + lockRepositoryName + " (" + - "`key` VARCHAR(255) PRIMARY KEY," + - "status VARCHAR(32)," + - "owner VARCHAR(255)," + - "expires_at TIMESTAMP" + - ")" - ); + stmt.executeUpdate(dialectHelper.getCreateTableSqlString(lockRepositoryName)); } catch (SQLException e) { throw new RuntimeException("Failed to initialize lock table", e); } @@ -125,7 +120,7 @@ public void releaseLock(LockKey lockKey, RunnerId owner) { String existingOwner = rs.getString("owner"); if (existingOwner.equals(owner.toString())) { try (PreparedStatement delete = conn.prepareStatement( - "DELETE FROM " + lockRepositoryName + " WHERE `key` = ?")) { + dialectHelper.getDeleteLockSqlString(lockRepositoryName))) { delete.setString(1, keyStr); delete.executeUpdate(); } @@ -139,7 +134,7 @@ public void releaseLock(LockKey lockKey, RunnerId owner) { private CommunityLockEntry getLockEntry(Connection conn, String key) throws SQLException { try (PreparedStatement ps = conn.prepareStatement( - "SELECT `key`, status, owner, expires_at FROM " + lockRepositoryName + " WHERE `key` = ?")) { + dialectHelper.getSelectLockSqlString(lockRepositoryName))) { ps.setString(1, key); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { @@ -157,8 +152,7 @@ private CommunityLockEntry getLockEntry(Connection conn, String key) throws SQLE private void upsertLockEntry(Connection conn, String key, String owner, LocalDateTime expiresAt) throws SQLException { try (PreparedStatement ps = conn.prepareStatement( - "INSERT INTO " + lockRepositoryName + " (`key`, status, owner, expires_at) VALUES (?, ?, ?, ?) " + - "ON DUPLICATE KEY UPDATE status = VALUES(status), owner = VALUES(owner), expires_at = VALUES(expires_at)")) { + dialectHelper.getInsertOrUpdateLockSqlString(lockRepositoryName))) { ps.setString(1, key); ps.setString(2, LockStatus.LOCK_HELD.name()); ps.setString(3, owner); diff --git a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/SqlAuditStoreTest.java b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/SqlAuditStoreTest.java index eea57d307..94cb28612 100644 --- a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/SqlAuditStoreTest.java +++ b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/SqlAuditStoreTest.java @@ -23,7 +23,7 @@ import io.flamingock.internal.core.runner.PipelineExecutionException; import io.flamingock.internal.util.Trio; import io.flamingock.internal.util.constants.CommunityPersistenceConstants; -import io.flamingock.targetsystem.mysql.SqlTargetSystem; +import io.flamingock.targetsystem.sql.SqlTargetSystem; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; From 641fdb4389e677db7384de648c2f33e7fe411ba6 Mon Sep 17 00:00:00 2001 From: davidfrigolet Date: Tue, 21 Oct 2025 08:33:31 +0100 Subject: [PATCH 4/9] feat: update sql dialect helpers for sqlserver, postgresql and oracle. tests updated --- .../build.gradle.kts | 10 +- .../sql/internal/SqlAuditorDialectHelper.java | 301 ++++--- .../sql/internal/SqlLockDialectHelper.java | 30 +- .../sql/internal/SqlLockService.java | 88 +- .../community/sql/SqlAuditStoreTest.java | 759 ++++++++++++------ .../_001__create_index.java | 2 +- .../_002__insert_document.java | 2 +- .../_003__execution_with_exception.java | 2 +- .../_001__create_index.java | 2 +- .../_002__insert_document.java | 2 +- .../_003__execution_with_exception.java | 2 +- .../happyPath/_001__create_index.java | 2 +- .../happyPath/_002__insert_document.java | 2 +- .../_003__insert_another_document.java | 2 +- .../_001__create_index.java | 36 + .../_002__insert_document.java | 49 ++ .../_003__execution_with_exception.java | 50 ++ .../_001__create_index.java | 36 + .../_002__insert_document.java | 39 + .../_003__execution_with_exception.java | 40 + .../oracle/happyPath/_001__create_index.java | 35 + .../happyPath/_002__insert_document.java | 38 + .../_003__insert_another_document.java | 38 + .../_001__create_index.java | 36 + .../_002__insert_document.java | 49 ++ .../_003__execution_with_exception.java | 50 ++ .../_001__create_index.java | 36 + .../_002__insert_document.java | 39 + .../_003__execution_with_exception.java | 40 + .../happyPath/_001__create_index.java | 35 + .../happyPath/_002__insert_document.java | 38 + .../_003__insert_another_document.java | 38 + .../_001__create_index.java | 36 + .../_002__insert_document.java | 49 ++ .../_003__execution_with_exception.java | 50 ++ .../_001__create_index.java | 36 + .../_002__insert_document.java | 39 + .../_003__execution_with_exception.java | 40 + .../happyPath/_001__create_index.java | 35 + .../happyPath/_002__insert_document.java | 38 + .../_003__insert_another_document.java | 38 + 41 files changed, 1886 insertions(+), 403 deletions(-) rename community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/{ => mysql}/failedWithRollback/_001__create_index.java (94%) rename community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/{ => mysql}/failedWithRollback/_002__insert_document.java (96%) rename community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/{ => mysql}/failedWithRollback/_003__execution_with_exception.java (96%) rename community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/{ => mysql}/failedWithoutRollback/_001__create_index.java (94%) rename community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/{ => mysql}/failedWithoutRollback/_002__insert_document.java (94%) rename community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/{ => mysql}/failedWithoutRollback/_003__execution_with_exception.java (95%) rename community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/{ => mysql}/happyPath/_001__create_index.java (95%) rename community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/{ => mysql}/happyPath/_002__insert_document.java (95%) rename community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/{ => mysql}/happyPath/_003__insert_another_document.java (95%) create mode 100644 community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/oracle/failedWithRollback/_001__create_index.java create mode 100644 community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/oracle/failedWithRollback/_002__insert_document.java create mode 100644 community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/oracle/failedWithRollback/_003__execution_with_exception.java create mode 100644 community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/oracle/failedWithoutRollback/_001__create_index.java create mode 100644 community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/oracle/failedWithoutRollback/_002__insert_document.java create mode 100644 community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/oracle/failedWithoutRollback/_003__execution_with_exception.java create mode 100644 community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/oracle/happyPath/_001__create_index.java create mode 100644 community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/oracle/happyPath/_002__insert_document.java create mode 100644 community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/oracle/happyPath/_003__insert_another_document.java create mode 100644 community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/postgresql/failedWithRollback/_001__create_index.java create mode 100644 community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/postgresql/failedWithRollback/_002__insert_document.java create mode 100644 community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/postgresql/failedWithRollback/_003__execution_with_exception.java create mode 100644 community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/postgresql/failedWithoutRollback/_001__create_index.java create mode 100644 community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/postgresql/failedWithoutRollback/_002__insert_document.java create mode 100644 community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/postgresql/failedWithoutRollback/_003__execution_with_exception.java create mode 100644 community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/postgresql/happyPath/_001__create_index.java create mode 100644 community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/postgresql/happyPath/_002__insert_document.java create mode 100644 community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/postgresql/happyPath/_003__insert_another_document.java create mode 100644 community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/sqlserver/failedWithRollback/_001__create_index.java create mode 100644 community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/sqlserver/failedWithRollback/_002__insert_document.java create mode 100644 community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/sqlserver/failedWithRollback/_003__execution_with_exception.java create mode 100644 community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/sqlserver/failedWithoutRollback/_001__create_index.java create mode 100644 community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/sqlserver/failedWithoutRollback/_002__insert_document.java create mode 100644 community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/sqlserver/failedWithoutRollback/_003__execution_with_exception.java create mode 100644 community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/sqlserver/happyPath/_001__create_index.java create mode 100644 community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/sqlserver/happyPath/_002__insert_document.java create mode 100644 community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/sqlserver/happyPath/_003__insert_another_document.java diff --git a/community/flamingock-auditstore-sql/build.gradle.kts b/community/flamingock-auditstore-sql/build.gradle.kts index 8b1b860a7..2633039d5 100644 --- a/community/flamingock-auditstore-sql/build.gradle.kts +++ b/community/flamingock-auditstore-sql/build.gradle.kts @@ -4,8 +4,14 @@ dependencies { implementation(project(":utils:sql-util")) testImplementation("mysql:mysql-connector-java:8.0.33") - testImplementation(project(":utils:test-util")) + testImplementation("com.microsoft.sqlserver:mssql-jdbc:12.4.2.jre8") + testImplementation("com.oracle.database.jdbc:ojdbc8:21.9.0.0") + testImplementation("org.postgresql:postgresql:42.7.3") testImplementation("org.testcontainers:mysql:1.21.3") + testImplementation("org.testcontainers:mssqlserver:1.21.3") + testImplementation("org.testcontainers:oracle-xe:1.21.3") + testImplementation("org.testcontainers:postgresql:1.21.3") + testImplementation(project(":utils:test-util")) testImplementation("com.zaxxer:HikariCP:3.4.5") testImplementation("org.testcontainers:junit-jupiter:1.21.3") testImplementation("com.h2database:h2:2.2.224") @@ -22,4 +28,4 @@ java { configurations.testImplementation { extendsFrom(configurations.compileOnly.get()) -} \ No newline at end of file +} diff --git a/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/internal/SqlAuditorDialectHelper.java b/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/internal/SqlAuditorDialectHelper.java index cc259312f..3ac653a2d 100644 --- a/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/internal/SqlAuditorDialectHelper.java +++ b/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/internal/SqlAuditorDialectHelper.java @@ -30,36 +30,8 @@ public SqlAuditorDialectHelper(SqlDialect dialect) { super(dialect); } - private static final String COMMON_COLUMNS = - "execution_id VARCHAR(255), " + - "stage_id VARCHAR(255), " + - "task_id VARCHAR(255), " + - "author VARCHAR(255), " + - "created_at %s, " + - "state VARCHAR(255), " + - "class_name VARCHAR(255), " + - "method_name VARCHAR(255), " + - "metadata %s, " + - "execution_millis BIGINT, " + - "execution_hostname VARCHAR(255), " + - "error_trace %s, " + - "type VARCHAR(50), " + - "tx_type VARCHAR(50), " + - "target_system_id VARCHAR(255), " + - "order_col VARCHAR(50), " + - "recovery_strategy VARCHAR(50), " + - "transaction_flag %s, " + - "system_change %s"; - - private String getCreatedAtType() { + public String getCreateTableSqlString(String tableName) { switch (sqlDialect) { - case SQLSERVER: - case SYBASE: - return "DATETIME DEFAULT GETDATE()"; - case INFORMIX: - return "DATETIME YEAR TO SECOND DEFAULT CURRENT YEAR TO SECOND"; - case ORACLE: - case POSTGRESQL: case MYSQL: case MARIADB: case SQLITE: @@ -67,137 +39,226 @@ private String getCreatedAtType() { case HSQLDB: case DERBY: case FIREBIRD: - return "TIMESTAMP DEFAULT CURRENT_TIMESTAMP"; + case INFORMIX: + return String.format( + "CREATE TABLE IF NOT EXISTS %s (" + + "id %s PRIMARY KEY, " + + "execution_id VARCHAR(255), " + + "stage_id VARCHAR(255), " + + "task_id VARCHAR(255), " + + "author VARCHAR(255), " + + "created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, " + + "state VARCHAR(255), " + + "class_name VARCHAR(255), " + + "method_name VARCHAR(255), " + + "metadata %s, " + + "execution_millis %s, " + + "execution_hostname VARCHAR(255), " + + "error_trace %s, " + + "type VARCHAR(50), " + + "tx_type VARCHAR(50), " + + "target_system_id VARCHAR(255), " + + "order_col VARCHAR(50), " + + "recovery_strategy VARCHAR(50), " + + "transaction_flag %s, " + + "system_change %s" + + ")", tableName, getAutoIncrementType(), getClobType(), getBigIntType(), getClobType(), getBooleanType(), getBooleanType()); + case POSTGRESQL: + return String.format( + "CREATE TABLE IF NOT EXISTS %s (" + + "id SERIAL PRIMARY KEY," + + "execution_id VARCHAR(255)," + + "stage_id VARCHAR(255)," + + "task_id VARCHAR(255)," + + "author VARCHAR(255)," + + "created_at TIMESTAMP," + + "state VARCHAR(32)," + + "class_name VARCHAR(255)," + + "method_name VARCHAR(255)," + + "metadata TEXT," + + "execution_millis BIGINT," + + "execution_hostname VARCHAR(255)," + + "error_trace TEXT," + + "type VARCHAR(32)," + + "tx_type VARCHAR(32)," + + "target_system_id VARCHAR(255)," + + "order_col VARCHAR(255)," + + "recovery_strategy VARCHAR(32)," + + "transaction_flag BOOLEAN," + + "system_change BOOLEAN" + + ")", tableName); + + case SQLSERVER: + case SYBASE: + return String.format( + "IF NOT EXISTS (SELECT * FROM sysobjects WHERE name='%s' AND xtype='U') " + + "CREATE TABLE %s (" + + "id %s PRIMARY KEY, " + + "execution_id VARCHAR(255), " + + "stage_id VARCHAR(255), " + + "task_id VARCHAR(255), " + + "author VARCHAR(255), " + + "created_at DATETIME DEFAULT GETDATE(), " + + "state VARCHAR(255), " + + "class_name VARCHAR(255), " + + "method_name VARCHAR(255), " + + "metadata %s, " + + "execution_millis %s, " + + "execution_hostname VARCHAR(255), " + + "error_trace %s, " + + "type VARCHAR(50), " + + "tx_type VARCHAR(50), " + + "target_system_id VARCHAR(255), " + + "order_col VARCHAR(50), " + + "recovery_strategy VARCHAR(50), " + + "transaction_flag %s, " + + "system_change %s" + + ")", tableName, tableName, getAutoIncrementType(), getClobType(), getBigIntType(), getClobType(), getBooleanType(), getBooleanType()); + case ORACLE: + return String.format( + "BEGIN EXECUTE IMMEDIATE 'CREATE TABLE %s (" + + "id %s PRIMARY KEY, " + + "execution_id VARCHAR2(255), " + + "stage_id VARCHAR2(255), " + + "task_id VARCHAR2(255), " + + "author VARCHAR2(255), " + + "created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, " + + "state VARCHAR2(255), " + + "class_name VARCHAR2(255), " + + "method_name VARCHAR2(255), " + + "metadata %s, " + + "execution_millis %s, " + + "execution_hostname VARCHAR2(255), " + + "error_trace %s, " + + "type VARCHAR2(50), " + + "tx_type VARCHAR2(50), " + + "target_system_id VARCHAR2(255), " + + "order_col VARCHAR2(50), " + + "recovery_strategy VARCHAR2(50), " + + "transaction_flag %s, " + + "system_change %s" + + ")'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF; END;", tableName, getAutoIncrementType(), getClobType(), getBigIntType(), getClobType(), getBooleanType(), getBooleanType()); case DB2: - return "TIMESTAMP DEFAULT CURRENT TIMESTAMP"; + return String.format( + "CREATE TABLE %s (" + + "id %s PRIMARY KEY, " + + "execution_id VARCHAR(255), " + + "stage_id VARCHAR(255), " + + "task_id VARCHAR(255), " + + "author VARCHAR(255), " + + "created_at TIMESTAMP DEFAULT CURRENT TIMESTAMP, " + + "state VARCHAR(255), " + + "class_name VARCHAR(255), " + + "method_name VARCHAR(255), " + + "metadata %s, " + + "execution_millis %s, " + + "execution_hostname VARCHAR(255), " + + "error_trace %s, " + + "type VARCHAR(50), " + + "tx_type VARCHAR(50), " + + "target_system_id VARCHAR(255), " + + "order_col VARCHAR(50), " + + "recovery_strategy VARCHAR(50), " + + "transaction_flag %s, " + + "system_change %s" + + ")", tableName, getAutoIncrementType(), getClobType(), getBigIntType(), getClobType(), getBooleanType(), getBooleanType()); default: - return "TIMESTAMP"; + throw new UnsupportedOperationException("Dialect not supported for CREATE TABLE: " + sqlDialect.name()); } } - private String getMetadataType() { + public String getInsertSqlString(String tableName) { + return String.format( + "INSERT INTO %s (execution_id, stage_id, task_id, author, created_at, state, class_name, method_name, metadata, execution_millis, execution_hostname, error_trace, type, tx_type, target_system_id, order_col, recovery_strategy, transaction_flag, system_change) " + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", tableName); + } + + public String getSelectHistorySqlString(String tableName) { + return String.format("SELECT * FROM %s ORDER BY id ASC", tableName); + } + + private String getAutoIncrementType() { switch (sqlDialect) { - case ORACLE: + case POSTGRESQL: + return "BIGSERIAL"; + case SQLITE: + case H2: + case HSQLDB: + case DERBY: case DB2: - return "CLOB"; case FIREBIRD: - return "BLOB SUB_TYPE TEXT"; + return "BIGINT GENERATED BY DEFAULT AS IDENTITY"; + case SQLSERVER: + case SYBASE: + return "BIGINT IDENTITY(1,1)"; + case ORACLE: + return "NUMBER GENERATED BY DEFAULT AS IDENTITY"; + case INFORMIX: + return "SERIAL8"; + case MYSQL: + case MARIADB: default: - return "TEXT"; + return "BIGINT AUTO_INCREMENT"; } } - private String getErrorTraceType() { + private String getClobType() { switch (sqlDialect) { + case MYSQL: + case MARIADB: + return "LONGTEXT"; + case SQLITE: + case H2: + case HSQLDB: + case DERBY: + case FIREBIRD: + case INFORMIX: case ORACLE: case DB2: return "CLOB"; - case FIREBIRD: - return "BLOB SUB_TYPE TEXT"; + case SQLSERVER: + case SYBASE: + return "NVARCHAR(MAX)"; + case POSTGRESQL: default: return "TEXT"; } } - private String getBooleanType() { + private String getBigIntType() { switch (sqlDialect) { - case SQLSERVER: - case SYBASE: - return "BIT"; case ORACLE: - case DB2: - case FIREBIRD: - case INFORMIX: - return "SMALLINT"; + return "NUMBER(19)"; default: - return "BOOLEAN"; + return "BIGINT"; } } - public String getCreateTableSqlString(String tableName) { - String columns = String.format(COMMON_COLUMNS, - getCreatedAtType(), - getMetadataType(), - getErrorTraceType(), - getBooleanType(), - getBooleanType()); - + private String getBooleanType() { switch (sqlDialect) { case MYSQL: case MARIADB: - return String.format( - "CREATE TABLE IF NOT EXISTS %s (" + - "id BIGINT AUTO_INCREMENT PRIMARY KEY, " + - columns + - ")", tableName); + return "TINYINT(1)"; case POSTGRESQL: - return String.format( - "CREATE TABLE IF NOT EXISTS %s (" + - "id SERIAL PRIMARY KEY, " + - columns + - ")", tableName); - case SQLITE: case H2: case HSQLDB: case DERBY: - return String.format( - "CREATE TABLE IF NOT EXISTS %s (" + - "id INTEGER PRIMARY KEY AUTOINCREMENT, " + - columns + - ")", tableName); + case FIREBIRD: + case INFORMIX: + return "BOOLEAN"; + case SQLITE: + return "INTEGER"; case SQLSERVER: case SYBASE: - return String.format( - "IF NOT EXISTS (SELECT * FROM sysobjects WHERE name='%s' AND xtype='U') " + - "CREATE TABLE %s (" + - "id BIGINT IDENTITY(1,1) PRIMARY KEY, " + - columns + - ")", tableName, tableName); + return "BIT"; case ORACLE: - return String.format( - "BEGIN EXECUTE IMMEDIATE 'CREATE TABLE %s (" + - "id NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, " + - columns + - ")'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF; END;", tableName); - case DB2: - return String.format( - "CREATE TABLE %s (" + - "id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, " + - columns + - ")", tableName); - case FIREBIRD: - return String.format( - "CREATE TABLE %s (" + - "id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, " + - columns + - ")", tableName); - case INFORMIX: - return String.format( - "CREATE TABLE %s (" + - "id SERIAL PRIMARY KEY, " + - columns + - ")", tableName); + return "NUMBER(1)"; default: - throw new UnsupportedOperationException("Dialect not supported for CREATE TABLE: " + sqlDialect.name()); + return "SMALLINT"; } } - public String getInsertSqlString(String tableName) { - return String.format( - "INSERT INTO %s (" + - "execution_id, stage_id, task_id, author, created_at, state, class_name, method_name, metadata, " + - "execution_millis, execution_hostname, error_trace, type, tx_type, target_system_id, order_col, " + - "recovery_strategy, transaction_flag, system_change" + - ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", - tableName); - } - - public String getSelectHistorySqlString(String tableName) { - return String.format( - "SELECT execution_id, stage_id, task_id, author, created_at, state, class_name, method_name, metadata, " + - "execution_millis, execution_hostname, error_trace, type, tx_type, target_system_id, order_col, " + - "recovery_strategy, transaction_flag, system_change " + - "FROM %s ORDER BY created_at DESC", - tableName); + public SqlDialect getSqlDialect() { + return sqlDialect; } } diff --git a/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/internal/SqlLockDialectHelper.java b/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/internal/SqlLockDialectHelper.java index 5dc096e02..4f0e78bdb 100644 --- a/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/internal/SqlLockDialectHelper.java +++ b/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/internal/SqlLockDialectHelper.java @@ -80,7 +80,17 @@ public String getCreateTableSqlString(String tableName) { } public String getSelectLockSqlString(String tableName) { - return String.format("SELECT `key`, status, owner, expires_at FROM %s WHERE `key` = ?", tableName); + switch (sqlDialect) { + case POSTGRESQL: + return String.format("SELECT \"key\", status, owner, expires_at FROM %s WHERE \"key\" = ?", tableName); + case SQLSERVER: + case SYBASE: + return String.format("SELECT [key], status, owner, expires_at FROM %s WITH (UPDLOCK, ROWLOCK) WHERE [key] = ?", tableName); + case ORACLE: + return String.format("SELECT \"key\", status, owner, expires_at FROM %s WHERE \"key\" = ? FOR UPDATE", tableName); + default: + return String.format("SELECT `key`, status, owner, expires_at FROM %s WHERE `key` = ?", tableName); + } } public String getInsertOrUpdateLockSqlString(String tableName) { @@ -104,11 +114,14 @@ public String getInsertOrUpdateLockSqlString(String tableName) { case SQLSERVER: case SYBASE: return String.format( - "MERGE INTO %s AS target USING (SELECT ? AS [key], ? AS status, ? AS owner, ? AS expires_at) AS source " + - "ON (target.[key] = source.[key]) " + - "WHEN MATCHED THEN UPDATE SET status = source.status, owner = source.owner, expires_at = source.expires_at " + - "WHEN NOT MATCHED THEN INSERT ([key], status, owner, expires_at) VALUES (source.[key], source.status, source.owner, source.expires_at);", - tableName); + "BEGIN TRANSACTION; " + + "UPDATE %s SET status = ?, owner = ?, expires_at = ? WHERE [key] = ?; " + + "IF @@ROWCOUNT = 0 " + + "BEGIN " + + "INSERT INTO %s ([key], status, owner, expires_at) VALUES (?, ?, ?, ?) " + + "END; " + + "COMMIT TRANSACTION;", + tableName, tableName); case ORACLE: return String.format( "MERGE INTO %s t USING (SELECT ? AS \"key\", ? AS status, ? AS owner, ? AS expires_at FROM dual) s " + @@ -141,5 +154,8 @@ public String getInsertOrUpdateLockSqlString(String tableName) { public String getDeleteLockSqlString(String tableName) { return String.format("DELETE FROM %s WHERE `key` = ?", tableName); } -} + public SqlDialect getSqlDialect() { + return sqlDialect; + } +} diff --git a/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/internal/SqlLockService.java b/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/internal/SqlLockService.java index 9623407e3..0bd2ebbe3 100644 --- a/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/internal/SqlLockService.java +++ b/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/internal/SqlLockService.java @@ -15,6 +15,7 @@ */ package io.flamingock.community.sql.internal; +import io.flamingock.internal.common.sql.SqlDialect; import io.flamingock.internal.core.store.lock.community.CommunityLockService; import io.flamingock.internal.core.store.lock.community.CommunityLockEntry; import io.flamingock.internal.core.store.lock.LockAcquisition; @@ -59,15 +60,28 @@ public void initialize(boolean autoCreate) { public LockAcquisition upsert(LockKey key, RunnerId owner, long leaseMillis) { String keyStr = key.toString(); LocalDateTime expiresAt = LocalDateTime.now().plusNanos(leaseMillis * 1_000_000); + try (Connection conn = dataSource.getConnection()) { - CommunityLockEntry existing = getLockEntry(conn, keyStr); - if (existing == null || - owner.toString().equals(existing.getOwner()) || - LocalDateTime.now().isAfter(existing.getExpiresAt())) { - upsertLockEntry(conn, keyStr, owner.toString(), expiresAt); - } else { - throw new LockServiceException("upsert", keyStr, - "Still locked by " + existing.getOwner() + " until " + existing.getExpiresAt()); + conn.setAutoCommit(false); + try { + CommunityLockEntry existing = getLockEntry(conn, keyStr); + if (existing == null || + owner.toString().equals(existing.getOwner()) || + LocalDateTime.now().isAfter(existing.getExpiresAt())) { + upsertLockEntry(conn, keyStr, owner.toString(), expiresAt); + if (dialectHelper.getSqlDialect() != SqlDialect.SQLSERVER && dialectHelper.getSqlDialect() != SqlDialect.SYBASE) { + conn.commit(); + } + } else { + conn.rollback(); + throw new LockServiceException("upsert", keyStr, + "Still locked by " + existing.getOwner() + " until " + existing.getExpiresAt()); + } + } catch (Exception e) { + conn.rollback(); + throw e; + } finally { + conn.setAutoCommit(true); } } catch (SQLException e) { throw new LockServiceException("upsert", keyStr, e.getMessage()); @@ -79,13 +93,26 @@ public LockAcquisition upsert(LockKey key, RunnerId owner, long leaseMillis) { public LockAcquisition extendLock(LockKey key, RunnerId owner, long leaseMillis) throws LockServiceException { String keyStr = key.toString(); LocalDateTime expiresAt = LocalDateTime.now().plusNanos(leaseMillis * 1_000_000); + try (Connection conn = dataSource.getConnection()) { - CommunityLockEntry existing = getLockEntry(conn, keyStr); - if (existing != null && owner.toString().equals(existing.getOwner())) { - upsertLockEntry(conn, keyStr, owner.toString(), expiresAt); - } else { - throw new LockServiceException("extendLock", keyStr, - "Lock belongs to " + (existing != null ? existing.getOwner() : "none")); + conn.setAutoCommit(false); + try { + CommunityLockEntry existing = getLockEntry(conn, keyStr); + if (existing != null && owner.toString().equals(existing.getOwner())) { + upsertLockEntry(conn, keyStr, owner.toString(), expiresAt); + if (dialectHelper.getSqlDialect() != SqlDialect.SQLSERVER && dialectHelper.getSqlDialect() != SqlDialect.SYBASE) { + conn.commit(); + } + } else { + conn.rollback(); + throw new LockServiceException("extendLock", keyStr, + "Lock belongs to " + (existing != null ? existing.getOwner() : "none")); + } + } catch (Exception e) { + conn.rollback(); + throw e; + } finally { + conn.setAutoCommit(true); } } catch (SQLException e) { throw new LockServiceException("extendLock", keyStr, e.getMessage()); @@ -139,7 +166,7 @@ private CommunityLockEntry getLockEntry(Connection conn, String key) throws SQLE try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { return new CommunityLockEntry( - rs.getString("key"), + rs.getString(1), // key column LockStatus.valueOf(rs.getString("status")), rs.getString("owner"), rs.getTimestamp("expires_at").toLocalDateTime() @@ -151,13 +178,30 @@ private CommunityLockEntry getLockEntry(Connection conn, String key) throws SQLE } private void upsertLockEntry(Connection conn, String key, String owner, LocalDateTime expiresAt) throws SQLException { - try (PreparedStatement ps = conn.prepareStatement( - dialectHelper.getInsertOrUpdateLockSqlString(lockRepositoryName))) { - ps.setString(1, key); - ps.setString(2, LockStatus.LOCK_HELD.name()); - ps.setString(3, owner); - ps.setTimestamp(4, Timestamp.valueOf(expiresAt)); - ps.executeUpdate(); + String sql = dialectHelper.getInsertOrUpdateLockSqlString(lockRepositoryName); + + if (dialectHelper.getSqlDialect() == SqlDialect.SQLSERVER || dialectHelper.getSqlDialect() == SqlDialect.SYBASE) { + // For SQL Server, the SQL already contains transaction management + try (Statement stmt = conn.createStatement()) { + String formattedSql = sql.replace("?", "'" + LockStatus.LOCK_HELD.name() + "'") + .replaceFirst("'[^']*'", "'" + LockStatus.LOCK_HELD.name() + "'") + .replaceFirst("'[^']*'", "'" + owner + "'") + .replaceFirst("'[^']*'", "'" + Timestamp.valueOf(expiresAt) + "'") + .replaceFirst("'[^']*'", "'" + key + "'") + .replaceFirst("'[^']*'", "'" + key + "'") + .replaceFirst("'[^']*'", "'" + LockStatus.LOCK_HELD.name() + "'") + .replaceFirst("'[^']*'", "'" + owner + "'") + .replaceFirst("'[^']*'", "'" + Timestamp.valueOf(expiresAt) + "'"); + stmt.execute(formattedSql); + } + } else { + try (PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setString(1, key); + ps.setString(2, LockStatus.LOCK_HELD.name()); + ps.setString(3, owner); + ps.setTimestamp(4, Timestamp.valueOf(expiresAt)); + ps.executeUpdate(); + } } } } diff --git a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/SqlAuditStoreTest.java b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/SqlAuditStoreTest.java index 94cb28612..7fc4c807e 100644 --- a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/SqlAuditStoreTest.java +++ b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/SqlAuditStoreTest.java @@ -19,20 +19,21 @@ import com.zaxxer.hikari.HikariDataSource; import io.flamingock.community.sql.driver.SqlAuditStore; import io.flamingock.core.processor.util.Deserializer; +import io.flamingock.internal.common.sql.SqlDialect; import io.flamingock.internal.core.builder.FlamingockFactory; import io.flamingock.internal.core.runner.PipelineExecutionException; import io.flamingock.internal.util.Trio; import io.flamingock.internal.util.constants.CommunityPersistenceConstants; import io.flamingock.targetsystem.sql.SqlTargetSystem; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.mockito.MockedStatic; import org.mockito.Mockito; -import org.testcontainers.containers.MySQLContainer; -import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.containers.*; import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; import javax.sql.DataSource; import java.sql.Connection; @@ -40,322 +41,594 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.util.Collections; +import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.*; @Testcontainers class SqlAuditStoreTest { - private static DataSource dataSource; + static Stream dialectProvider() { + return Stream.of( + Arguments.of(SqlDialect.MYSQL, "mysql"), + Arguments.of(SqlDialect.SQLSERVER, "sqlserver"), + Arguments.of(SqlDialect.ORACLE, "oracle"), + Arguments.of(SqlDialect.POSTGRESQL, "postgresql") + ); + } - @Container - public static final MySQLContainer mysqlContainer = new MySQLContainer<>("mysql:8.0") - .withDatabaseName("testdb") - .withUsername("testuser") - .withPassword("testpass"); + private TestContext setupTest(SqlDialect sqlDialect, String dialectName) throws SQLException { + JdbcDatabaseContainer container = createContainer(dialectName); + container.start(); - @BeforeEach - void setUp() throws SQLException { - if (dataSource instanceof HikariDataSource) { - ((HikariDataSource) dataSource).close(); - } HikariConfig config = new HikariConfig(); - config.setJdbcUrl(mysqlContainer.getJdbcUrl()); - config.setUsername(mysqlContainer.getUsername()); - config.setPassword(mysqlContainer.getPassword()); - config.setDriverClassName(mysqlContainer.getDriverClassName()); - dataSource = new HikariDataSource(config); + config.setJdbcUrl(container.getJdbcUrl()); + config.setUsername(container.getUsername()); + config.setPassword(container.getPassword()); + config.setDriverClassName(container.getDriverClassName()); + DataSource dataSource = new HikariDataSource(config); - try (Connection conn = dataSource.getConnection()) { - conn.createStatement().execute("DROP TABLE IF EXISTS flamingockAuditLog"); - conn.createStatement().execute("DROP TABLE IF EXISTS test_table"); - conn.createStatement().execute("DROP TABLE IF EXISTS flamingockLock"); - conn.createStatement().execute( - "CREATE TABLE test_table (" + - "id VARCHAR(255) PRIMARY KEY, " + - "name VARCHAR(255), " + - "field1 VARCHAR(255), " + - "field2 VARCHAR(255))" - ); - conn.createStatement().execute( - "CREATE TABLE flamingockLock (" + - "`key` VARCHAR(255) PRIMARY KEY, " + - "status VARCHAR(32), " + - "owner VARCHAR(255), " + - "expires_at TIMESTAMP)" - ); + createTables(dataSource, sqlDialect); + + return new TestContext(dataSource, container, sqlDialect); + } + + private void tearDown(TestContext context) throws SQLException { + if (context.dataSource instanceof HikariDataSource) { + ((HikariDataSource) context.dataSource).close(); + } + if (context.container != null) { + context.container.stop(); } } - @AfterEach - void tearDown() throws SQLException { - try (Connection conn = dataSource.getConnection()) { - // Drop index if exists - try (ResultSet rs = conn.getMetaData().getIndexInfo(null, null, "test_table", false, false)) { - while (rs.next()) { - String idxName = rs.getString("INDEX_NAME"); - if ("idx_standalone_index".equals(idxName)) { - conn.createStatement().execute("DROP INDEX idx_standalone_index ON test_table"); - break; + private JdbcDatabaseContainer createContainer(String dialectName) { + switch (dialectName) { + case "mysql": + return new MySQLContainer<>("mysql:8.0") + .withDatabaseName("testdb") + .withUsername("testuser") + .withPassword("testpass"); + case "sqlserver": + return new MSSQLServerContainer<>("mcr.microsoft.com/mssql/server:2019-CU18-ubuntu-20.04") + .acceptLicense() + .withPassword("TestPass123!"); + case "oracle": + OracleContainer oracleContainer = new OracleContainer( + DockerImageName.parse("gvenzl/oracle-free:23-slim-faststart") + .asCompatibleSubstituteFor("gvenzl/oracle-xe")) + .withPassword("oracle123") + .withSharedMemorySize(1073741824L) + .withStartupTimeoutSeconds(900) + .withEnv("ORACLE_CHARACTERSET", "AL32UTF8"); + + return new OracleContainer( + DockerImageName.parse("gvenzl/oracle-free:23-slim-faststart") + .asCompatibleSubstituteFor("gvenzl/oracle-xe")) { + @Override + public String getDatabaseName() { + return "FREEPDB1"; } } - } - conn.createStatement().execute("DROP TABLE IF EXISTS test_table"); - conn.createStatement().execute("DROP TABLE IF EXISTS flamingockLock"); - conn.createStatement().execute("DROP TABLE IF EXISTS flamingockAuditLog"); - } - if (dataSource instanceof HikariDataSource) { - ((HikariDataSource) dataSource).close(); + .withPassword("oracle123") + .withSharedMemorySize(1073741824L) + .withStartupTimeoutSeconds(900) + .withEnv("ORACLE_CHARACTERSET", "AL32UTF8"); + case "postgresql": + return new PostgreSQLContainer<>(DockerImageName.parse("postgres:15")) + .withDatabaseName("testdb") + .withUsername("test") + .withPassword("test"); + default: + throw new IllegalArgumentException("Unsupported dialect: " + dialectName); } } - @Test - @DisplayName("When standalone runs the driver should persist the audit logs and the test data") - void happyPathWithMockedPipeline() throws Exception { - try (MockedStatic mocked = Mockito.mockStatic(Deserializer.class)) { - mocked.when(Deserializer::readPreviewPipelineFromFile).thenReturn(PipelineTestHelper.getPreviewPipeline( - new Trio<>(io.flamingock.community.sql.changes.happyPath._001__create_index.class, Collections.singletonList(Connection.class), null), - new Trio<>(io.flamingock.community.sql.changes.happyPath._002__insert_document.class, Collections.singletonList(Connection.class), null), - new Trio<>(io.flamingock.community.sql.changes.happyPath._003__insert_another_document.class, Collections.singletonList(Connection.class), null) - )); - - SqlAuditStore auditStore = new SqlAuditStore(dataSource); - SqlTargetSystem targetSystem = new SqlTargetSystem("sql", dataSource); - - FlamingockFactory.getCommunityBuilder() - .setAuditStore(auditStore) - .addTargetSystem(targetSystem) - .build() - .run(); + private void createTables(DataSource dataSource, SqlDialect dialect) throws SQLException { + try (Connection conn = dataSource.getConnection()) { + dropTablesIfExist(conn, dialect); + + String createTestTable = getCreateTestTableSql(dialect); + conn.createStatement().execute(createTestTable); + + String createLockTable = getCreateLockTableSql(dialect); + conn.createStatement().execute(createLockTable); } + } - try (Connection conn = dataSource.getConnection(); - PreparedStatement ps = conn.prepareStatement( - "SELECT task_id, state FROM " + CommunityPersistenceConstants.DEFAULT_AUDIT_STORE_NAME + " ORDER BY id ASC"); - ResultSet rs = ps.executeQuery()) { - - String[] expectedTaskIds = {"create-index", "insert-document", "insert-another-document"}; - int recordCount = 0; - int startedCount = 0; - int appliedCount = 0; - - while (rs.next()) { - String taskId = rs.getString("task_id"); - String state = rs.getString("state"); - assertTrue( - java.util.Arrays.asList(expectedTaskIds).contains(taskId), - "Unexpected task_id: " + taskId - ); - assertTrue( - state.equals("STARTED") || state.equals("APPLIED"), - "Unexpected state: " + state - ); - if (state.equals("STARTED")) startedCount++; - if (state.equals("APPLIED")) appliedCount++; - recordCount++; + private void dropTablesIfExist(Connection conn, SqlDialect dialect) throws SQLException { + String[] tables = {"flamingockAuditLog", "test_table", "flamingockLock"}; + for (String table : tables) { + try { + String dropSql = getDropTableSql(table, dialect); + conn.createStatement().execute(dropSql); + } catch (SQLException e) { + // Ignore if table doesn't exist } + } + } - assertEquals(6, recordCount, "Audit log should have 6 records"); - assertEquals(3, startedCount, "Should have 3 STARTED records"); - assertEquals(3, appliedCount, "Should have 3 APPLIED records"); + private String getDropTableSql(String tableName, SqlDialect dialect) { + if (dialect == SqlDialect.ORACLE) { + return "DROP TABLE " + tableName + " CASCADE CONSTRAINTS"; } + return "DROP TABLE IF EXISTS " + tableName; + } - try (Connection conn = dataSource.getConnection(); - PreparedStatement ps = conn.prepareStatement("SELECT name FROM test_table WHERE id = ?")) { - ps.setString(1, "test-client-Federico"); - try (ResultSet rs = ps.executeQuery()) { - assertTrue(rs.next()); - assertEquals("Federico", rs.getString("name")); - } - ps.setString(1, "test-client-Jorge"); - try (ResultSet rs = ps.executeQuery()) { - assertTrue(rs.next()); - assertEquals("Jorge", rs.getString("name")); - } + private String getCreateTestTableSql(SqlDialect dialect) { + switch (dialect) { + case MYSQL: + case SQLSERVER: + return "CREATE TABLE test_table (" + + "id VARCHAR(255) PRIMARY KEY, " + + "name VARCHAR(255), " + + "field1 VARCHAR(255), " + + "field2 VARCHAR(255))"; + case POSTGRESQL: + return "CREATE TABLE test_table (" + + "id VARCHAR(255) PRIMARY KEY," + + "name VARCHAR(255)," + + "field1 VARCHAR(255)," + + "field2 VARCHAR(255))"; + case ORACLE: + return "CREATE TABLE test_table (" + + "id VARCHAR2(255) PRIMARY KEY, " + + "name VARCHAR2(255), " + + "field1 VARCHAR2(255), " + + "field2 VARCHAR2(255))"; + default: + throw new UnsupportedOperationException("Dialect not supported: " + dialect); + } + } + + private String getCreateLockTableSql(SqlDialect dialect) { + switch (dialect) { + case MYSQL: + return "CREATE TABLE flamingockLock (" + + "`key` VARCHAR(255) PRIMARY KEY, " + + "status VARCHAR(32), " + + "owner VARCHAR(255), " + + "expires_at TIMESTAMP)"; + case POSTGRESQL: + return "CREATE TABLE flamingockLock (" + + "\"key\" VARCHAR(255) PRIMARY KEY," + + "status VARCHAR(32)," + + "owner VARCHAR(255)," + + "expires_at TIMESTAMP)"; + case SQLSERVER: + return "CREATE TABLE flamingockLock (" + + "[key] VARCHAR(255) PRIMARY KEY, " + + "status VARCHAR(32), " + + "owner VARCHAR(255), " + + "expires_at DATETIME)"; + case ORACLE: + return "CREATE TABLE flamingockLock (" + + "\"key\" VARCHAR2(255) PRIMARY KEY, " + + "status VARCHAR2(32), " + + "owner VARCHAR2(255), " + + "expires_at TIMESTAMP)"; + default: + throw new UnsupportedOperationException("Dialect not supported: " + dialect); } } + private Class[] getChangeClasses(String dialectName, String scenario) { + switch (dialectName) { + case "mysql": + if ("happyPath".equals(scenario)) { + return new Class[]{ + io.flamingock.community.sql.changes.mysql.happyPath._001__create_index.class, + io.flamingock.community.sql.changes.mysql.happyPath._002__insert_document.class, + io.flamingock.community.sql.changes.mysql.happyPath._003__insert_another_document.class + }; + } else if ("failedWithRollback".equals(scenario)) { + return new Class[]{ + io.flamingock.community.sql.changes.mysql.failedWithRollback._001__create_index.class, + io.flamingock.community.sql.changes.mysql.failedWithRollback._002__insert_document.class, + io.flamingock.community.sql.changes.mysql.failedWithRollback._003__execution_with_exception.class + }; + } else if ("failedWithoutRollback".equals(scenario)) { + return new Class[]{ + io.flamingock.community.sql.changes.mysql.failedWithoutRollback._001__create_index.class, + io.flamingock.community.sql.changes.mysql.failedWithoutRollback._002__insert_document.class, + io.flamingock.community.sql.changes.mysql.failedWithoutRollback._003__execution_with_exception.class + }; + } + break; + case "sqlserver": + if ("happyPath".equals(scenario)) { + return new Class[]{ + io.flamingock.community.sql.changes.sqlserver.happyPath._001__create_index.class, + io.flamingock.community.sql.changes.sqlserver.happyPath._002__insert_document.class, + io.flamingock.community.sql.changes.sqlserver.happyPath._003__insert_another_document.class + }; + } else if ("failedWithRollback".equals(scenario)) { + return new Class[]{ + io.flamingock.community.sql.changes.sqlserver.failedWithRollback._001__create_index.class, + io.flamingock.community.sql.changes.sqlserver.failedWithRollback._002__insert_document.class, + io.flamingock.community.sql.changes.sqlserver.failedWithRollback._003__execution_with_exception.class + }; + } else if ("failedWithoutRollback".equals(scenario)) { + return new Class[]{ + io.flamingock.community.sql.changes.sqlserver.failedWithoutRollback._001__create_index.class, + io.flamingock.community.sql.changes.sqlserver.failedWithoutRollback._002__insert_document.class, + io.flamingock.community.sql.changes.sqlserver.failedWithoutRollback._003__execution_with_exception.class + }; + } + break; + case "oracle": + if ("happyPath".equals(scenario)) { + return new Class[]{ + io.flamingock.community.sql.changes.oracle.happyPath._001__create_index.class, + io.flamingock.community.sql.changes.oracle.happyPath._002__insert_document.class, + io.flamingock.community.sql.changes.oracle.happyPath._003__insert_another_document.class + }; + } else if ("failedWithRollback".equals(scenario)) { + return new Class[]{ + io.flamingock.community.sql.changes.oracle.failedWithRollback._001__create_index.class, + io.flamingock.community.sql.changes.oracle.failedWithRollback._002__insert_document.class, + io.flamingock.community.sql.changes.oracle.failedWithRollback._003__execution_with_exception.class + }; + } else if ("failedWithoutRollback".equals(scenario)) { + return new Class[]{ + io.flamingock.community.sql.changes.oracle.failedWithoutRollback._001__create_index.class, + io.flamingock.community.sql.changes.oracle.failedWithoutRollback._002__insert_document.class, + io.flamingock.community.sql.changes.oracle.failedWithoutRollback._003__execution_with_exception.class + }; + } + break; + case "postgresql": + if ("happyPath".equals(scenario)) { + return new Class[]{ + io.flamingock.community.sql.changes.postgresql.happyPath._001__create_index.class, + io.flamingock.community.sql.changes.postgresql.happyPath._002__insert_document.class, + io.flamingock.community.sql.changes.postgresql.happyPath._003__insert_another_document.class + }; + } else if ("failedWithRollback".equals(scenario)) { + return new Class[]{ + io.flamingock.community.sql.changes.postgresql.failedWithRollback._001__create_index.class, + io.flamingock.community.sql.changes.postgresql.failedWithRollback._002__insert_document.class, + io.flamingock.community.sql.changes.postgresql.failedWithRollback._003__execution_with_exception.class + }; + } else if ("failedWithoutRollback".equals(scenario)) { + return new Class[]{ + io.flamingock.community.sql.changes.postgresql.failedWithoutRollback._001__create_index.class, + io.flamingock.community.sql.changes.postgresql.failedWithoutRollback._002__insert_document.class, + io.flamingock.community.sql.changes.postgresql.failedWithoutRollback._003__execution_with_exception.class + }; + } + break; + } + throw new IllegalArgumentException("Unsupported dialect/scenario: " + dialectName + "/" + scenario); + } + + @ParameterizedTest + @MethodSource("dialectProvider") + @DisplayName("When standalone runs the driver should persist the audit logs and the test data") + void happyPathWithMockedPipeline(SqlDialect sqlDialect, String dialectName) throws Exception { + TestContext context = setupTest(sqlDialect, dialectName); + try { + try (MockedStatic mocked = Mockito.mockStatic(Deserializer.class)) { + Class[] changeClasses = getChangeClasses(dialectName, "happyPath"); + + mocked.when(Deserializer::readPreviewPipelineFromFile).thenReturn(PipelineTestHelper.getPreviewPipeline( + new Trio<>(changeClasses[0], Collections.singletonList(Connection.class), null), + new Trio<>(changeClasses[1], Collections.singletonList(Connection.class), null), + new Trio<>(changeClasses[2], Collections.singletonList(Connection.class), null) + )); + + SqlAuditStore auditStore = new SqlAuditStore(context.dataSource); + SqlTargetSystem targetSystem = new SqlTargetSystem("sql", context.dataSource); - @Test - @DisplayName("When standalone runs the driver and execution fails (with rollback method) should persist all the audit logs up to the failed one (ROLLED_BACK)") - void failedWithRollback() throws Exception { - try (MockedStatic mocked = Mockito.mockStatic(Deserializer.class)) { - mocked.when(Deserializer::readPreviewPipelineFromFile).thenReturn( - PipelineTestHelper.getPreviewPipeline( - new Trio<>(io.flamingock.community.sql.changes.failedWithRollback._001__create_index.class, Collections.singletonList(Connection.class), null), - new Trio<>(io.flamingock.community.sql.changes.failedWithRollback._002__insert_document.class, Collections.singletonList(Connection.class), null), - new Trio<>(io.flamingock.community.sql.changes.failedWithRollback._003__execution_with_exception.class, Collections.singletonList(Connection.class), null) - ) - ); - - SqlAuditStore auditStore = new SqlAuditStore(dataSource); - SqlTargetSystem targetSystem = new SqlTargetSystem("sql", dataSource); - - assertThrows(PipelineExecutionException.class, () -> { FlamingockFactory.getCommunityBuilder() .setAuditStore(auditStore) .addTargetSystem(targetSystem) .build() .run(); - }); + } - try (Connection conn = dataSource.getConnection(); + // Verify audit logs + try (Connection conn = context.dataSource.getConnection(); PreparedStatement ps = conn.prepareStatement( "SELECT task_id, state FROM " + CommunityPersistenceConstants.DEFAULT_AUDIT_STORE_NAME + " ORDER BY id ASC"); ResultSet rs = ps.executeQuery()) { - assertTrue(rs.next()); - assertEquals("create-index", rs.getString("task_id")); - assertEquals("STARTED", rs.getString("state")); - - assertTrue(rs.next()); - assertEquals("create-index", rs.getString("task_id")); - assertEquals("APPLIED", rs.getString("state")); - - assertTrue(rs.next()); - assertEquals("insert-document", rs.getString("task_id")); - assertEquals("STARTED", rs.getString("state")); - - assertTrue(rs.next()); - assertEquals("insert-document", rs.getString("task_id")); - assertEquals("APPLIED", rs.getString("state")); - - assertTrue(rs.next()); - assertEquals("execution-with-exception", rs.getString("task_id")); - assertEquals("STARTED", rs.getString("state")); - - assertTrue(rs.next()); - assertEquals("execution-with-exception", rs.getString("task_id")); - assertEquals("FAILED", rs.getString("state")); - - assertTrue(rs.next()); - assertEquals("execution-with-exception", rs.getString("task_id")); - assertEquals("ROLLED_BACK", rs.getString("state")); - - assertFalse(rs.next()); - } - + String[] expectedTaskIds = {"create-index", "insert-document", "insert-another-document"}; + int recordCount = 0; + int startedCount = 0; + int appliedCount = 0; - try (Connection conn = dataSource.getConnection(); - ResultSet rs = conn.getMetaData().getIndexInfo(null, null, "test_table", false, false)) { - boolean found = false; while (rs.next()) { - if ("idx_standalone_index".equals(rs.getString("INDEX_NAME"))) { - found = true; - break; - } + String taskId = rs.getString("task_id"); + String state = rs.getString("state"); + assertTrue( + java.util.Arrays.asList(expectedTaskIds).contains(taskId), + "Unexpected task_id: " + taskId + ); + assertTrue( + state.equals("STARTED") || state.equals("APPLIED"), + "Unexpected state: " + state + ); + if (state.equals("STARTED")) startedCount++; + if (state.equals("APPLIED")) appliedCount++; + recordCount++; } - assertTrue(found, "Index idx_standalone_index should exist"); + + assertEquals(6, recordCount, "Audit log should have 6 records"); + assertEquals(3, startedCount, "Should have 3 STARTED records"); + assertEquals(3, appliedCount, "Should have 3 APPLIED records"); } - try (Connection conn = dataSource.getConnection(); + // Verify test data + try (Connection conn = context.dataSource.getConnection(); PreparedStatement ps = conn.prepareStatement("SELECT name FROM test_table WHERE id = ?")) { ps.setString(1, "test-client-Federico"); try (ResultSet rs = ps.executeQuery()) { assertTrue(rs.next()); assertEquals("Federico", rs.getString("name")); } - } - - try (Connection conn = dataSource.getConnection(); - PreparedStatement ps = conn.prepareStatement("SELECT name FROM test_table WHERE id = ?")) { ps.setString(1, "test-client-Jorge"); try (ResultSet rs = ps.executeQuery()) { - assertFalse(rs.next()); + assertTrue(rs.next()); + assertEquals("Jorge", rs.getString("name")); } } - + } finally { + tearDown(context); } } - @Test - @DisplayName("When standalone runs the driver and execution fails (without rollback method) should persist all the audit logs up to the failed one (FAILED)") - void failedWithoutRollback() throws SQLException { - try (MockedStatic mocked = Mockito.mockStatic(Deserializer.class)) { - mocked.when(Deserializer::readPreviewPipelineFromFile).thenReturn( - PipelineTestHelper.getPreviewPipeline( - new Trio<>(io.flamingock.community.sql.changes.failedWithoutRollback._001__create_index.class, Collections.singletonList(Connection.class), null), - new Trio<>(io.flamingock.community.sql.changes.failedWithoutRollback._002__insert_document.class, Collections.singletonList(Connection.class), null), - new Trio<>(io.flamingock.community.sql.changes.failedWithoutRollback._003__execution_with_exception.class, Collections.singletonList(Connection.class), null) - ) - ); - - SqlAuditStore auditStore = new SqlAuditStore(dataSource); - SqlTargetSystem targetSystem = new SqlTargetSystem("sql", dataSource); - - assertThrows(PipelineExecutionException.class, () -> { - FlamingockFactory.getCommunityBuilder() - .setAuditStore(auditStore) - .addTargetSystem(targetSystem) - .build() - .run(); - }); + @ParameterizedTest + @MethodSource("dialectProvider") + @DisplayName("When standalone runs the driver and execution fails (with rollback method) should persist all the audit logs up to the failed one (ROLLED_BACK)") + void failedWithRollback(SqlDialect sqlDialect, String dialectName) throws Exception { + TestContext context = setupTest(sqlDialect, dialectName); + try { + try (MockedStatic mocked = Mockito.mockStatic(Deserializer.class)) { + Class[] changeClasses = getChangeClasses(dialectName, "failedWithRollback"); + + mocked.when(Deserializer::readPreviewPipelineFromFile).thenReturn(PipelineTestHelper.getPreviewPipeline( + new Trio<>(changeClasses[0], Collections.singletonList(Connection.class), null), + new Trio<>(changeClasses[1], Collections.singletonList(Connection.class), null), + new Trio<>(changeClasses[2], Collections.singletonList(Connection.class), null) + )); + + SqlAuditStore auditStore = new SqlAuditStore(context.dataSource); + SqlTargetSystem targetSystem = new SqlTargetSystem("sql", context.dataSource); + + assertThrows(PipelineExecutionException.class, () -> { + FlamingockFactory.getCommunityBuilder() + .setAuditStore(auditStore) + .addTargetSystem(targetSystem) + .build() + .run(); + }); + + // Verify audit sequence + try (Connection conn = context.dataSource.getConnection(); + PreparedStatement ps = conn.prepareStatement( + "SELECT task_id, state FROM " + CommunityPersistenceConstants.DEFAULT_AUDIT_STORE_NAME + " ORDER BY id ASC"); + ResultSet rs = ps.executeQuery()) { + assertTrue(rs.next()); + assertEquals("create-index", rs.getString("task_id")); + assertEquals("STARTED", rs.getString("state")); - try (Connection conn = dataSource.getConnection(); - PreparedStatement ps = conn.prepareStatement( - "SELECT task_id, state FROM " + CommunityPersistenceConstants.DEFAULT_AUDIT_STORE_NAME + " ORDER BY id ASC"); - ResultSet rs = ps.executeQuery()) { + assertTrue(rs.next()); + assertEquals("create-index", rs.getString("task_id")); + assertEquals("APPLIED", rs.getString("state")); - assertTrue(rs.next()); - assertEquals("create-index", rs.getString("task_id")); - assertEquals("STARTED", rs.getString("state")); + assertTrue(rs.next()); + assertEquals("insert-document", rs.getString("task_id")); + assertEquals("STARTED", rs.getString("state")); - assertTrue(rs.next()); - assertEquals("create-index", rs.getString("task_id")); - assertEquals("APPLIED", rs.getString("state")); + assertTrue(rs.next()); + assertEquals("insert-document", rs.getString("task_id")); + assertEquals("APPLIED", rs.getString("state")); - assertTrue(rs.next()); - assertEquals("insert-document", rs.getString("task_id")); - assertEquals("STARTED", rs.getString("state")); + assertTrue(rs.next()); + assertEquals("execution-with-exception", rs.getString("task_id")); + assertEquals("STARTED", rs.getString("state")); - assertTrue(rs.next()); - assertEquals("insert-document", rs.getString("task_id")); - assertEquals("APPLIED", rs.getString("state")); + assertTrue(rs.next()); + assertEquals("execution-with-exception", rs.getString("task_id")); + assertEquals("FAILED", rs.getString("state")); - assertTrue(rs.next()); - assertEquals("execution-with-exception", rs.getString("task_id")); - assertEquals("STARTED", rs.getString("state")); + assertTrue(rs.next()); + assertEquals("execution-with-exception", rs.getString("task_id")); + assertEquals("ROLLED_BACK", rs.getString("state")); - assertTrue(rs.next()); - assertEquals("execution-with-exception", rs.getString("task_id")); - assertEquals("FAILED", rs.getString("state")); + assertFalse(rs.next()); + } - assertTrue(rs.next()); - assertEquals("execution-with-exception", rs.getString("task_id")); - assertEquals("ROLLED_BACK", rs.getString("state")); + // Verify index exists + verifyIndexExists(context); - assertFalse(rs.next()); - } + // Verify partial data + try (Connection conn = context.dataSource.getConnection(); + PreparedStatement ps = conn.prepareStatement("SELECT name FROM test_table WHERE id = ?")) { + ps.setString(1, "test-client-Federico"); + try (ResultSet rs = ps.executeQuery()) { + assertTrue(rs.next()); + assertEquals("Federico", rs.getString("name")); + } + } - try (Connection conn = dataSource.getConnection(); - ResultSet rs = conn.getMetaData().getIndexInfo(null, null, "test_table", false, false)) { - boolean found = false; - while (rs.next()) { - if ("idx_standalone_index".equals(rs.getString("INDEX_NAME"))) { - found = true; - break; + try (Connection conn = context.dataSource.getConnection(); + PreparedStatement ps = conn.prepareStatement("SELECT name FROM test_table WHERE id = ?")) { + ps.setString(1, "test-client-Jorge"); + try (ResultSet rs = ps.executeQuery()) { + assertFalse(rs.next()); } } - assertTrue(found, "Index idx_standalone_index should exist"); } + } finally { + tearDown(context); + } + } + + @ParameterizedTest + @MethodSource("dialectProvider") + @DisplayName("When standalone runs the driver and execution fails (without rollback method) should persist all the audit logs up to the failed one (FAILED)") + void failedWithoutRollback(SqlDialect sqlDialect, String dialectName) throws Exception { + TestContext context = setupTest(sqlDialect, dialectName); + try { + try (MockedStatic mocked = Mockito.mockStatic(Deserializer.class)) { + Class[] changeClasses = getChangeClasses(dialectName, "failedWithoutRollback"); + + mocked.when(Deserializer::readPreviewPipelineFromFile).thenReturn(PipelineTestHelper.getPreviewPipeline( + new Trio<>(changeClasses[0], Collections.singletonList(Connection.class), null), + new Trio<>(changeClasses[1], Collections.singletonList(Connection.class), null), + new Trio<>(changeClasses[2], Collections.singletonList(Connection.class), null) + )); + + SqlAuditStore auditStore = new SqlAuditStore(context.dataSource); + SqlTargetSystem targetSystem = new SqlTargetSystem("sql", context.dataSource); + + assertThrows(PipelineExecutionException.class, () -> { + FlamingockFactory.getCommunityBuilder() + .setAuditStore(auditStore) + .addTargetSystem(targetSystem) + .build() + .run(); + }); + + // Verify audit sequence + try (Connection conn = context.dataSource.getConnection(); + PreparedStatement ps = conn.prepareStatement( + "SELECT task_id, state FROM " + CommunityPersistenceConstants.DEFAULT_AUDIT_STORE_NAME + " ORDER BY id ASC"); + ResultSet rs = ps.executeQuery()) { - try (Connection conn = dataSource.getConnection(); - PreparedStatement ps = conn.prepareStatement("SELECT name FROM test_table WHERE id = ?")) { - ps.setString(1, "test-client-Federico"); - try (ResultSet rs = ps.executeQuery()) { assertTrue(rs.next()); - assertEquals("Federico", rs.getString("name")); + assertEquals("create-index", rs.getString("task_id")); + assertEquals("STARTED", rs.getString("state")); + + assertTrue(rs.next()); + assertEquals("create-index", rs.getString("task_id")); + assertEquals("APPLIED", rs.getString("state")); + + assertTrue(rs.next()); + assertEquals("insert-document", rs.getString("task_id")); + assertEquals("STARTED", rs.getString("state")); + + assertTrue(rs.next()); + assertEquals("insert-document", rs.getString("task_id")); + assertEquals("APPLIED", rs.getString("state")); + + assertTrue(rs.next()); + assertEquals("execution-with-exception", rs.getString("task_id")); + assertEquals("STARTED", rs.getString("state")); + + assertTrue(rs.next()); + assertEquals("execution-with-exception", rs.getString("task_id")); + assertEquals("FAILED", rs.getString("state")); + + assertTrue(rs.next()); + assertEquals("execution-with-exception", rs.getString("task_id")); + assertEquals("ROLLED_BACK", rs.getString("state")); + + assertFalse(rs.next()); } + + // Verify index exists and data state + verifyIndexExists(context); + verifyPartialDataState(context); } + } finally { + tearDown(context); + } + } + + private void verifyIndexExists(TestContext context) throws SQLException { + try (Connection conn = context.dataSource.getConnection()) { + String indexCheckSql = getIndexCheckSql(context.dialect); + try (PreparedStatement ps = conn.prepareStatement(indexCheckSql)) { + switch (context.dialect) { + case ORACLE: + ps.setString(1, "IDX_STANDALONE_INDEX"); + ps.setString(2, "TEST_TABLE"); + break; + case POSTGRESQL: + ps.setString(1, "idx_standalone_index"); + break; + case MYSQL: + case MARIADB: + ps.setString(1, "test_table"); + ps.setString(2, "idx_standalone_index"); + break; + case SQLSERVER: + case SYBASE: + ps.setString(1, "idx_standalone_index"); + break; + case H2: + case HSQLDB: + case DERBY: + case SQLITE: + ps.setString(1, "idx_standalone_index"); + break; + default: + ps.setString(1, "idx_standalone_index"); + break; + } - try (Connection conn = dataSource.getConnection(); - PreparedStatement ps = conn.prepareStatement("SELECT name FROM test_table WHERE id = ?")) { - ps.setString(1, "test-client-Jorge"); try (ResultSet rs = ps.executeQuery()) { - assertFalse(rs.next()); + boolean indexExists = rs.next(); + assertTrue(indexExists, "Index idx_standalone_index should exist"); } } } } + + private String getIndexCheckSql(SqlDialect dialect) { + switch (dialect) { + case POSTGRESQL: + return "SELECT indexname FROM pg_indexes WHERE indexname = ?"; + case MYSQL: + case MARIADB: + return "SELECT INDEX_NAME FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_NAME = ? AND INDEX_NAME = ?"; + case ORACLE: + return "SELECT INDEX_NAME FROM USER_INDEXES WHERE INDEX_NAME = ? AND TABLE_NAME = ?"; + case SQLSERVER: + case SYBASE: + return "SELECT name FROM sys.indexes WHERE name = ?"; + case H2: + case HSQLDB: + case DERBY: + case SQLITE: + return "SELECT INDEX_NAME FROM INFORMATION_SCHEMA.INDEXES WHERE INDEX_NAME = ?"; + default: + return "SELECT INDEX_NAME FROM INFORMATION_SCHEMA.INDEXES WHERE INDEX_NAME = ?"; + } + } + + private void verifyPartialDataState(TestContext context) throws SQLException { + try (Connection conn = context.dataSource.getConnection(); + PreparedStatement ps = conn.prepareStatement("SELECT name FROM test_table WHERE id = ?")) { + ps.setString(1, "test-client-Federico"); + try (ResultSet rs = ps.executeQuery()) { + assertTrue(rs.next()); + assertEquals("Federico", rs.getString("name")); + } + } + + try (Connection conn = context.dataSource.getConnection(); + PreparedStatement ps = conn.prepareStatement("SELECT name FROM test_table WHERE id = ?")) { + ps.setString(1, "test-client-Jorge"); + try (ResultSet rs = ps.executeQuery()) { + assertFalse(rs.next()); + } + } + } + + private static class TestContext { + final DataSource dataSource; + final JdbcDatabaseContainer container; + final SqlDialect dialect; + + TestContext(DataSource dataSource, JdbcDatabaseContainer container, SqlDialect dialect) { + this.dataSource = dataSource; + this.container = container; + this.dialect = dialect; + } + } } diff --git a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/failedWithRollback/_001__create_index.java b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/mysql/failedWithRollback/_001__create_index.java similarity index 94% rename from community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/failedWithRollback/_001__create_index.java rename to community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/mysql/failedWithRollback/_001__create_index.java index 2b4e5aa7a..325ab7048 100644 --- a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/failedWithRollback/_001__create_index.java +++ b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/mysql/failedWithRollback/_001__create_index.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.flamingock.community.sql.changes.failedWithRollback; +package io.flamingock.community.sql.changes.mysql.failedWithRollback; import io.flamingock.api.annotations.Apply; import io.flamingock.api.annotations.Change; diff --git a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/failedWithRollback/_002__insert_document.java b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/mysql/failedWithRollback/_002__insert_document.java similarity index 96% rename from community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/failedWithRollback/_002__insert_document.java rename to community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/mysql/failedWithRollback/_002__insert_document.java index 57bdff9ca..f56d7092a 100644 --- a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/failedWithRollback/_002__insert_document.java +++ b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/mysql/failedWithRollback/_002__insert_document.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.flamingock.community.sql.changes.failedWithRollback; +package io.flamingock.community.sql.changes.mysql.failedWithRollback; import io.flamingock.api.annotations.Apply; import io.flamingock.api.annotations.Change; diff --git a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/failedWithRollback/_003__execution_with_exception.java b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/mysql/failedWithRollback/_003__execution_with_exception.java similarity index 96% rename from community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/failedWithRollback/_003__execution_with_exception.java rename to community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/mysql/failedWithRollback/_003__execution_with_exception.java index 3451185ba..f6ee4ea10 100644 --- a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/failedWithRollback/_003__execution_with_exception.java +++ b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/mysql/failedWithRollback/_003__execution_with_exception.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.flamingock.community.sql.changes.failedWithRollback; +package io.flamingock.community.sql.changes.mysql.failedWithRollback; import io.flamingock.api.annotations.Apply; import io.flamingock.api.annotations.Change; diff --git a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/failedWithoutRollback/_001__create_index.java b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/mysql/failedWithoutRollback/_001__create_index.java similarity index 94% rename from community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/failedWithoutRollback/_001__create_index.java rename to community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/mysql/failedWithoutRollback/_001__create_index.java index 9f0a32769..f58de2853 100644 --- a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/failedWithoutRollback/_001__create_index.java +++ b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/mysql/failedWithoutRollback/_001__create_index.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.flamingock.community.sql.changes.failedWithoutRollback; +package io.flamingock.community.sql.changes.mysql.failedWithoutRollback; import io.flamingock.api.annotations.Apply; import io.flamingock.api.annotations.Change; diff --git a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/failedWithoutRollback/_002__insert_document.java b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/mysql/failedWithoutRollback/_002__insert_document.java similarity index 94% rename from community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/failedWithoutRollback/_002__insert_document.java rename to community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/mysql/failedWithoutRollback/_002__insert_document.java index 1fcf43300..c93302ebf 100644 --- a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/failedWithoutRollback/_002__insert_document.java +++ b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/mysql/failedWithoutRollback/_002__insert_document.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.flamingock.community.sql.changes.failedWithoutRollback; +package io.flamingock.community.sql.changes.mysql.failedWithoutRollback; import io.flamingock.api.annotations.Apply; import io.flamingock.api.annotations.Change; diff --git a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/failedWithoutRollback/_003__execution_with_exception.java b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/mysql/failedWithoutRollback/_003__execution_with_exception.java similarity index 95% rename from community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/failedWithoutRollback/_003__execution_with_exception.java rename to community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/mysql/failedWithoutRollback/_003__execution_with_exception.java index d4323c65c..1016279cc 100644 --- a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/failedWithoutRollback/_003__execution_with_exception.java +++ b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/mysql/failedWithoutRollback/_003__execution_with_exception.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.flamingock.community.sql.changes.failedWithoutRollback; +package io.flamingock.community.sql.changes.mysql.failedWithoutRollback; import io.flamingock.api.annotations.Apply; import io.flamingock.api.annotations.Change; diff --git a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/happyPath/_001__create_index.java b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/mysql/happyPath/_001__create_index.java similarity index 95% rename from community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/happyPath/_001__create_index.java rename to community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/mysql/happyPath/_001__create_index.java index ba61da194..deacee1aa 100644 --- a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/happyPath/_001__create_index.java +++ b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/mysql/happyPath/_001__create_index.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.flamingock.community.sql.changes.happyPath; +package io.flamingock.community.sql.changes.mysql.happyPath; import io.flamingock.api.annotations.Apply; import io.flamingock.api.annotations.Change; diff --git a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/happyPath/_002__insert_document.java b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/mysql/happyPath/_002__insert_document.java similarity index 95% rename from community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/happyPath/_002__insert_document.java rename to community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/mysql/happyPath/_002__insert_document.java index 64e042fda..09b5fbb34 100644 --- a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/happyPath/_002__insert_document.java +++ b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/mysql/happyPath/_002__insert_document.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.flamingock.community.sql.changes.happyPath; +package io.flamingock.community.sql.changes.mysql.happyPath; import io.flamingock.api.annotations.Apply; import io.flamingock.api.annotations.Change; diff --git a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/happyPath/_003__insert_another_document.java b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/mysql/happyPath/_003__insert_another_document.java similarity index 95% rename from community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/happyPath/_003__insert_another_document.java rename to community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/mysql/happyPath/_003__insert_another_document.java index d5fd6292e..987bd6765 100644 --- a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/happyPath/_003__insert_another_document.java +++ b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/mysql/happyPath/_003__insert_another_document.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.flamingock.community.sql.changes.happyPath; +package io.flamingock.community.sql.changes.mysql.happyPath; import io.flamingock.api.annotations.Apply; import io.flamingock.api.annotations.Change; diff --git a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/oracle/failedWithRollback/_001__create_index.java b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/oracle/failedWithRollback/_001__create_index.java new file mode 100644 index 000000000..28b427e3b --- /dev/null +++ b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/oracle/failedWithRollback/_001__create_index.java @@ -0,0 +1,36 @@ +/* + * Copyright 2025 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.community.sql.changes.oracle.failedWithRollback; + +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.TargetSystem; + +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; + +@TargetSystem(id = "sql") +@Change(id = "create-index", transactional = false, author = "aperezdieppa") +public class _001__create_index { + + @Apply + public void execution(Connection connection) throws SQLException { + try (Statement stmt = connection.createStatement()) { + stmt.execute("CREATE INDEX idx_standalone_index ON test_table(field1, field2)"); + } + } +} diff --git a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/oracle/failedWithRollback/_002__insert_document.java b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/oracle/failedWithRollback/_002__insert_document.java new file mode 100644 index 000000000..7db36db79 --- /dev/null +++ b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/oracle/failedWithRollback/_002__insert_document.java @@ -0,0 +1,49 @@ +/* + * Copyright 2025 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.community.sql.changes.oracle.failedWithRollback; + +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.Rollback; +import io.flamingock.api.annotations.TargetSystem; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +@TargetSystem(id = "sql") +@Change(id = "insert-document", transactional = false, author = "aperezdieppa") +public class _002__insert_document { + + @Apply + public void execution(Connection connection) throws SQLException { + try (PreparedStatement ps = connection.prepareStatement( + "INSERT INTO test_table (id, name) VALUES (?, ?)")) { + ps.setString(1, "test-client-Federico"); + ps.setString(2, "Federico"); + ps.executeUpdate(); + } + } + + @Rollback + public void rollback(Connection connection) throws Exception { + try (PreparedStatement ps = connection.prepareStatement( + "DELETE FROM test_table WHERE id = ?")) { + ps.setString(1, "test-client-Federico"); + ps.executeUpdate(); + } + } +} diff --git a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/oracle/failedWithRollback/_003__execution_with_exception.java b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/oracle/failedWithRollback/_003__execution_with_exception.java new file mode 100644 index 000000000..3eb0e3a28 --- /dev/null +++ b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/oracle/failedWithRollback/_003__execution_with_exception.java @@ -0,0 +1,50 @@ +/* + * Copyright 2025 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.community.sql.changes.oracle.failedWithRollback; + +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.Rollback; +import io.flamingock.api.annotations.TargetSystem; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +@TargetSystem(id = "sql") +@Change(id = "execution-with-exception", transactional = false, author = "aperezdieppa") +public class _003__execution_with_exception { + + @Apply + public void execution(Connection connection) throws SQLException { + try (PreparedStatement ps = connection.prepareStatement( + "INSERT INTO test_table (id, name) VALUES (?, ?)")) { + ps.setString(1, "test-client-Jorge"); + ps.setString(2, "Jorge"); + ps.executeUpdate(); + } + throw new RuntimeException("test"); + } + + @Rollback + public void rollbackExecution(Connection connection) throws SQLException { + try (PreparedStatement ps = connection.prepareStatement( + "DELETE FROM test_table WHERE id = ?")) { + ps.setString(1, "test-client-Jorge"); + ps.executeUpdate(); + } + } +} diff --git a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/oracle/failedWithoutRollback/_001__create_index.java b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/oracle/failedWithoutRollback/_001__create_index.java new file mode 100644 index 000000000..955581255 --- /dev/null +++ b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/oracle/failedWithoutRollback/_001__create_index.java @@ -0,0 +1,36 @@ +/* + * Copyright 2025 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.community.sql.changes.oracle.failedWithoutRollback; + +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.TargetSystem; + +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; + +@TargetSystem(id = "sql") +@Change(id = "create-index", transactional = false, author = "aperezdieppa") +public class _001__create_index { + + @Apply + public void execution(Connection connection) throws SQLException { + try (Statement stmt = connection.createStatement()) { + stmt.execute("CREATE INDEX idx_standalone_index ON test_table(field1, field2)"); + } + } +} diff --git a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/oracle/failedWithoutRollback/_002__insert_document.java b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/oracle/failedWithoutRollback/_002__insert_document.java new file mode 100644 index 000000000..b2bec2056 --- /dev/null +++ b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/oracle/failedWithoutRollback/_002__insert_document.java @@ -0,0 +1,39 @@ +/* + * Copyright 2025 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.community.sql.changes.oracle.failedWithoutRollback; + +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.TargetSystem; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +@TargetSystem(id = "sql") +@Change(id = "insert-document", author = "aperezdieppa") +public class _002__insert_document { + + @Apply + public void execution(Connection connection) throws SQLException { + try (PreparedStatement ps = connection.prepareStatement( + "INSERT INTO test_table (id, name) VALUES (?, ?)")) { + ps.setString(1, "test-client-Federico"); + ps.setString(2, "Federico"); + ps.executeUpdate(); + } + } +} diff --git a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/oracle/failedWithoutRollback/_003__execution_with_exception.java b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/oracle/failedWithoutRollback/_003__execution_with_exception.java new file mode 100644 index 000000000..ffaf1b6b6 --- /dev/null +++ b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/oracle/failedWithoutRollback/_003__execution_with_exception.java @@ -0,0 +1,40 @@ +/* + * Copyright 2025 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.community.sql.changes.oracle.failedWithoutRollback; + +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.TargetSystem; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +@TargetSystem(id = "sql") +@Change(id = "execution-with-exception", author = "aperezdieppa") +public class _003__execution_with_exception { + + @Apply + public void execution(Connection connection) throws SQLException { + try (PreparedStatement ps = connection.prepareStatement( + "INSERT INTO test_table (id, name) VALUES (?, ?)")) { + ps.setString(1, "test-client-Jorge"); + ps.setString(2, "Jorge"); + ps.executeUpdate(); + } + throw new RuntimeException("test"); + } +} diff --git a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/oracle/happyPath/_001__create_index.java b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/oracle/happyPath/_001__create_index.java new file mode 100644 index 000000000..fc724efe4 --- /dev/null +++ b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/oracle/happyPath/_001__create_index.java @@ -0,0 +1,35 @@ +/* + * Copyright 2025 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.community.sql.changes.oracle.happyPath; + +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.TargetSystem; + +import java.sql.Connection; +import java.sql.Statement; + +@TargetSystem(id = "sql") +@Change(id = "create-index", transactional = false, author = "aperezdieppa") +public class _001__create_index { + + @Apply + public void execution(Connection connection) throws Exception { + try (Statement stmt = connection.createStatement()) { + stmt.execute("CREATE INDEX idx_standalone_index ON test_table(field1, field2)"); + } + } +} diff --git a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/oracle/happyPath/_002__insert_document.java b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/oracle/happyPath/_002__insert_document.java new file mode 100644 index 000000000..ef8fd54ec --- /dev/null +++ b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/oracle/happyPath/_002__insert_document.java @@ -0,0 +1,38 @@ +/* + * Copyright 2025 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.community.sql.changes.oracle.happyPath; + +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.TargetSystem; + +import java.sql.Connection; +import java.sql.PreparedStatement; + +@TargetSystem(id = "sql") +@Change(id = "insert-document", author = "aperezdieppa") +public class _002__insert_document { + + @Apply + public void execution(Connection connection) throws Exception { + try (PreparedStatement ps = connection.prepareStatement( + "INSERT INTO test_table (id, name) VALUES (?, ?)")) { + ps.setString(1, "test-client-Federico"); + ps.setString(2, "Federico"); + ps.executeUpdate(); + } + } +} diff --git a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/oracle/happyPath/_003__insert_another_document.java b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/oracle/happyPath/_003__insert_another_document.java new file mode 100644 index 000000000..610a0e5f1 --- /dev/null +++ b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/oracle/happyPath/_003__insert_another_document.java @@ -0,0 +1,38 @@ +/* + * Copyright 2025 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.community.sql.changes.oracle.happyPath; + +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.TargetSystem; + +import java.sql.Connection; +import java.sql.PreparedStatement; + +@TargetSystem(id = "sql") +@Change(id = "insert-another-document", author = "aperezdieppa") +public class _003__insert_another_document { + + @Apply + public void execution(Connection connection) throws Exception { + try (PreparedStatement ps = connection.prepareStatement( + "INSERT INTO test_table (id, name) VALUES (?, ?)")) { + ps.setString(1, "test-client-Jorge"); + ps.setString(2, "Jorge"); + ps.executeUpdate(); + } + } +} diff --git a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/postgresql/failedWithRollback/_001__create_index.java b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/postgresql/failedWithRollback/_001__create_index.java new file mode 100644 index 000000000..7219f2dad --- /dev/null +++ b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/postgresql/failedWithRollback/_001__create_index.java @@ -0,0 +1,36 @@ +/* + * Copyright 2025 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.community.sql.changes.postgresql.failedWithRollback; + +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.TargetSystem; + +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; + +@TargetSystem(id = "sql") +@Change(id = "create-index", transactional = false, author = "aperezdieppa") +public class _001__create_index { + + @Apply + public void execution(Connection connection) throws SQLException { + try (Statement stmt = connection.createStatement()) { + stmt.executeUpdate("CREATE INDEX IF NOT EXISTS idx_standalone_index ON test_table(field1, field2)"); + } + } +} diff --git a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/postgresql/failedWithRollback/_002__insert_document.java b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/postgresql/failedWithRollback/_002__insert_document.java new file mode 100644 index 000000000..c21dff498 --- /dev/null +++ b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/postgresql/failedWithRollback/_002__insert_document.java @@ -0,0 +1,49 @@ +/* + * Copyright 2025 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.community.sql.changes.postgresql.failedWithRollback; + +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.Rollback; +import io.flamingock.api.annotations.TargetSystem; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +@TargetSystem(id = "sql") +@Change(id = "insert-document", transactional = false, author = "aperezdieppa") +public class _002__insert_document { + + @Apply + public void execution(Connection connection) throws SQLException { + try (PreparedStatement ps = connection.prepareStatement( + "INSERT INTO test_table (id, name) VALUES (?, ?)")) { + ps.setString(1, "test-client-Federico"); + ps.setString(2, "Federico"); + ps.executeUpdate(); + } + } + + @Rollback + public void rollbackExecution(Connection connection) throws SQLException { + try (PreparedStatement ps = connection.prepareStatement( + "DELETE FROM test_table WHERE id = ?")) { + ps.setString(1, "test-client-Federico"); + ps.executeUpdate(); + } + } +} diff --git a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/postgresql/failedWithRollback/_003__execution_with_exception.java b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/postgresql/failedWithRollback/_003__execution_with_exception.java new file mode 100644 index 000000000..147208bd3 --- /dev/null +++ b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/postgresql/failedWithRollback/_003__execution_with_exception.java @@ -0,0 +1,50 @@ +/* + * Copyright 2025 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.community.sql.changes.postgresql.failedWithRollback; + +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.Rollback; +import io.flamingock.api.annotations.TargetSystem; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +@TargetSystem(id = "sql") +@Change(id = "execution-with-exception", transactional = false, author = "aperezdieppa") +public class _003__execution_with_exception { + + @Apply + public void execution(Connection connection) throws SQLException { + try (PreparedStatement ps = connection.prepareStatement( + "INSERT INTO test_table (id, name) VALUES (?, ?)")) { + ps.setString(1, "test-client-Jorge"); + ps.setString(2, "Jorge"); + ps.executeUpdate(); + } + throw new RuntimeException("test"); + } + + @Rollback + public void rollbackExecution(Connection connection) throws SQLException { + try (PreparedStatement ps = connection.prepareStatement( + "DELETE FROM test_table WHERE id = ?")) { + ps.setString(1, "test-client-Jorge"); + ps.executeUpdate(); + } + } +} diff --git a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/postgresql/failedWithoutRollback/_001__create_index.java b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/postgresql/failedWithoutRollback/_001__create_index.java new file mode 100644 index 000000000..526ec3719 --- /dev/null +++ b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/postgresql/failedWithoutRollback/_001__create_index.java @@ -0,0 +1,36 @@ +/* + * Copyright 2025 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.community.sql.changes.postgresql.failedWithoutRollback; + +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.TargetSystem; + +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; + +@TargetSystem(id = "sql") +@Change(id = "create-index", transactional = false, author = "aperezdieppa") +public class _001__create_index { + + @Apply + public void execution(Connection connection) throws SQLException { + try (Statement stmt = connection.createStatement()) { + stmt.executeUpdate("CREATE INDEX IF NOT EXISTS idx_standalone_index ON test_table(field1, field2)"); + } + } +} diff --git a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/postgresql/failedWithoutRollback/_002__insert_document.java b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/postgresql/failedWithoutRollback/_002__insert_document.java new file mode 100644 index 000000000..5db818f03 --- /dev/null +++ b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/postgresql/failedWithoutRollback/_002__insert_document.java @@ -0,0 +1,39 @@ +/* + * Copyright 2025 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.community.sql.changes.postgresql.failedWithoutRollback; + +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.TargetSystem; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +@TargetSystem(id = "sql") +@Change(id = "insert-document", author = "aperezdieppa") +public class _002__insert_document { + + @Apply + public void execution(Connection connection) throws SQLException { + try (PreparedStatement ps = connection.prepareStatement( + "INSERT INTO test_table (id, name) VALUES (?, ?)")) { + ps.setString(1, "test-client-Federico"); + ps.setString(2, "Federico"); + ps.executeUpdate(); + } + } +} diff --git a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/postgresql/failedWithoutRollback/_003__execution_with_exception.java b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/postgresql/failedWithoutRollback/_003__execution_with_exception.java new file mode 100644 index 000000000..6ae6c99f0 --- /dev/null +++ b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/postgresql/failedWithoutRollback/_003__execution_with_exception.java @@ -0,0 +1,40 @@ +/* + * Copyright 2025 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.community.sql.changes.postgresql.failedWithoutRollback; + +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.TargetSystem; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +@TargetSystem(id = "sql") +@Change(id = "execution-with-exception", author = "aperezdieppa") +public class _003__execution_with_exception { + + @Apply + public void execution(Connection connection) throws SQLException { + try (PreparedStatement ps = connection.prepareStatement( + "INSERT INTO test_table (id, name) VALUES (?, ?)")) { + ps.setString(1, "test-client-Jorge"); + ps.setString(2, "Jorge"); + ps.executeUpdate(); + } + throw new RuntimeException("test"); + } +} diff --git a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/postgresql/happyPath/_001__create_index.java b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/postgresql/happyPath/_001__create_index.java new file mode 100644 index 000000000..8db5bb0de --- /dev/null +++ b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/postgresql/happyPath/_001__create_index.java @@ -0,0 +1,35 @@ +/* + * Copyright 2025 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.community.sql.changes.postgresql.happyPath; + +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.TargetSystem; + +import java.sql.Connection; +import java.sql.Statement; + +@TargetSystem(id = "sql") +@Change(id = "create-index", transactional = false, author = "aperezdieppa") +public class _001__create_index { + + @Apply + public void execution(Connection connection) throws Exception { + try (Statement stmt = connection.createStatement()) { + stmt.executeUpdate("CREATE INDEX IF NOT EXISTS idx_standalone_index ON test_table(field1, field2)"); + } + } +} diff --git a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/postgresql/happyPath/_002__insert_document.java b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/postgresql/happyPath/_002__insert_document.java new file mode 100644 index 000000000..c3a044c1d --- /dev/null +++ b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/postgresql/happyPath/_002__insert_document.java @@ -0,0 +1,38 @@ +/* + * Copyright 2025 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.community.sql.changes.postgresql.happyPath; + +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.TargetSystem; + +import java.sql.Connection; +import java.sql.PreparedStatement; + +@TargetSystem(id = "sql") +@Change(id = "insert-document", author = "aperezdieppa") +public class _002__insert_document { + + @Apply + public void execution(Connection connection) throws Exception { + try (PreparedStatement ps = connection.prepareStatement( + "INSERT INTO test_table (id, name) VALUES (?, ?)")) { + ps.setString(1, "test-client-Federico"); + ps.setString(2, "Federico"); + ps.executeUpdate(); + } + } +} diff --git a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/postgresql/happyPath/_003__insert_another_document.java b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/postgresql/happyPath/_003__insert_another_document.java new file mode 100644 index 000000000..4fb3eff0b --- /dev/null +++ b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/postgresql/happyPath/_003__insert_another_document.java @@ -0,0 +1,38 @@ +/* + * Copyright 2025 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.community.sql.changes.postgresql.happyPath; + +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.TargetSystem; + +import java.sql.Connection; +import java.sql.PreparedStatement; + +@TargetSystem(id = "sql") +@Change(id = "insert-another-document", author = "aperezdieppa") +public class _003__insert_another_document { + + @Apply + public void execution(Connection connection) throws Exception { + try (PreparedStatement ps = connection.prepareStatement( + "INSERT INTO test_table (id, name) VALUES (?, ?)")) { + ps.setString(1, "test-client-Jorge"); + ps.setString(2, "Jorge"); + ps.executeUpdate(); + } + } +} diff --git a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/sqlserver/failedWithRollback/_001__create_index.java b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/sqlserver/failedWithRollback/_001__create_index.java new file mode 100644 index 000000000..c2c23dbf6 --- /dev/null +++ b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/sqlserver/failedWithRollback/_001__create_index.java @@ -0,0 +1,36 @@ +/* + * Copyright 2025 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.community.sql.changes.sqlserver.failedWithRollback; + +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.TargetSystem; + +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; + +@TargetSystem(id = "sql") +@Change(id = "create-index", transactional = false, author = "aperezdieppa") +public class _001__create_index { + + @Apply + public void execution(Connection connection) throws SQLException { + try (Statement stmt = connection.createStatement()) { + stmt.execute("CREATE INDEX idx_standalone_index ON test_table(field1, field2)"); + } + } +} diff --git a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/sqlserver/failedWithRollback/_002__insert_document.java b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/sqlserver/failedWithRollback/_002__insert_document.java new file mode 100644 index 000000000..d7bfa9cf6 --- /dev/null +++ b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/sqlserver/failedWithRollback/_002__insert_document.java @@ -0,0 +1,49 @@ +/* + * Copyright 2025 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.community.sql.changes.sqlserver.failedWithRollback; + +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.Rollback; +import io.flamingock.api.annotations.TargetSystem; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +@TargetSystem(id = "sql") +@Change(id = "insert-document", transactional = false, author = "aperezdieppa") +public class _002__insert_document { + + @Apply + public void execution(Connection connection) throws SQLException { + try (PreparedStatement ps = connection.prepareStatement( + "INSERT INTO test_table (id, name) VALUES (?, ?)")) { + ps.setString(1, "test-client-Federico"); + ps.setString(2, "Federico"); + ps.executeUpdate(); + } + } + + @Rollback + public void rollbackExecution(Connection connection) throws SQLException { + try (PreparedStatement ps = connection.prepareStatement( + "DELETE FROM test_table WHERE id = ?")) { + ps.setString(1, "test-client-Federico"); + ps.executeUpdate(); + } + } +} diff --git a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/sqlserver/failedWithRollback/_003__execution_with_exception.java b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/sqlserver/failedWithRollback/_003__execution_with_exception.java new file mode 100644 index 000000000..d9ef56d0f --- /dev/null +++ b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/sqlserver/failedWithRollback/_003__execution_with_exception.java @@ -0,0 +1,50 @@ +/* + * Copyright 2025 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.community.sql.changes.sqlserver.failedWithRollback; + +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.Rollback; +import io.flamingock.api.annotations.TargetSystem; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +@TargetSystem(id = "sql") +@Change(id = "execution-with-exception", transactional = false, author = "aperezdieppa") +public class _003__execution_with_exception { + + @Apply + public void execution(Connection connection) throws SQLException { + try (PreparedStatement ps = connection.prepareStatement( + "INSERT INTO test_table (id, name) VALUES (?, ?)")) { + ps.setString(1, "test-client-Jorge"); + ps.setString(2, "Jorge"); + ps.executeUpdate(); + } + throw new RuntimeException("test"); + } + + @Rollback + public void rollbackExecution(Connection connection) throws SQLException { + try (PreparedStatement ps = connection.prepareStatement( + "DELETE FROM test_table WHERE id = ?")) { + ps.setString(1, "test-client-Jorge"); + ps.executeUpdate(); + } + } +} diff --git a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/sqlserver/failedWithoutRollback/_001__create_index.java b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/sqlserver/failedWithoutRollback/_001__create_index.java new file mode 100644 index 000000000..a287dd942 --- /dev/null +++ b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/sqlserver/failedWithoutRollback/_001__create_index.java @@ -0,0 +1,36 @@ +/* + * Copyright 2025 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.community.sql.changes.sqlserver.failedWithoutRollback; + +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.TargetSystem; + +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; + +@TargetSystem(id = "sql") +@Change(id = "create-index", transactional = false, author = "aperezdieppa") +public class _001__create_index { + + @Apply + public void execution(Connection connection) throws SQLException { + try (Statement stmt = connection.createStatement()) { + stmt.execute("CREATE INDEX idx_standalone_index ON test_table(field1, field2)"); + } + } +} diff --git a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/sqlserver/failedWithoutRollback/_002__insert_document.java b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/sqlserver/failedWithoutRollback/_002__insert_document.java new file mode 100644 index 000000000..1c50c5a6c --- /dev/null +++ b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/sqlserver/failedWithoutRollback/_002__insert_document.java @@ -0,0 +1,39 @@ +/* + * Copyright 2025 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.community.sql.changes.sqlserver.failedWithoutRollback; + +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.TargetSystem; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +@TargetSystem(id = "sql") +@Change(id = "insert-document", author = "aperezdieppa") +public class _002__insert_document { + + @Apply + public void execution(Connection connection) throws SQLException { + try (PreparedStatement ps = connection.prepareStatement( + "INSERT INTO test_table (id, name) VALUES (?, ?)")) { + ps.setString(1, "test-client-Federico"); + ps.setString(2, "Federico"); + ps.executeUpdate(); + } + } +} diff --git a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/sqlserver/failedWithoutRollback/_003__execution_with_exception.java b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/sqlserver/failedWithoutRollback/_003__execution_with_exception.java new file mode 100644 index 000000000..affff9f77 --- /dev/null +++ b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/sqlserver/failedWithoutRollback/_003__execution_with_exception.java @@ -0,0 +1,40 @@ +/* + * Copyright 2025 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.community.sql.changes.sqlserver.failedWithoutRollback; + +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.TargetSystem; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +@TargetSystem(id = "sql") +@Change(id = "execution-with-exception", author = "aperezdieppa") +public class _003__execution_with_exception { + + @Apply + public void execution(Connection connection) throws SQLException { + try (PreparedStatement ps = connection.prepareStatement( + "INSERT INTO test_table (id, name) VALUES (?, ?)")) { + ps.setString(1, "test-client-Jorge"); + ps.setString(2, "Jorge"); + ps.executeUpdate(); + } + throw new RuntimeException("test"); + } +} diff --git a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/sqlserver/happyPath/_001__create_index.java b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/sqlserver/happyPath/_001__create_index.java new file mode 100644 index 000000000..5d8d2d4cc --- /dev/null +++ b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/sqlserver/happyPath/_001__create_index.java @@ -0,0 +1,35 @@ +/* + * Copyright 2025 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.community.sql.changes.sqlserver.happyPath; + +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.TargetSystem; + +import java.sql.Connection; +import java.sql.Statement; + +@TargetSystem(id = "sql") +@Change(id = "create-index", transactional = false, author = "aperezdieppa") +public class _001__create_index { + + @Apply + public void execution(Connection connection) throws Exception { + try (Statement stmt = connection.createStatement()) { + stmt.execute("CREATE INDEX idx_standalone_index ON test_table(field1, field2)"); + } + } +} diff --git a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/sqlserver/happyPath/_002__insert_document.java b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/sqlserver/happyPath/_002__insert_document.java new file mode 100644 index 000000000..db4269da0 --- /dev/null +++ b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/sqlserver/happyPath/_002__insert_document.java @@ -0,0 +1,38 @@ +/* + * Copyright 2025 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.community.sql.changes.sqlserver.happyPath; + +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.TargetSystem; + +import java.sql.Connection; +import java.sql.PreparedStatement; + +@TargetSystem(id = "sql") +@Change(id = "insert-document", author = "aperezdieppa") +public class _002__insert_document { + + @Apply + public void execution(Connection connection) throws Exception { + try (PreparedStatement ps = connection.prepareStatement( + "INSERT INTO test_table (id, name) VALUES (?, ?)")) { + ps.setString(1, "test-client-Federico"); + ps.setString(2, "Federico"); + ps.executeUpdate(); + } + } +} diff --git a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/sqlserver/happyPath/_003__insert_another_document.java b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/sqlserver/happyPath/_003__insert_another_document.java new file mode 100644 index 000000000..9a86c938d --- /dev/null +++ b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/changes/sqlserver/happyPath/_003__insert_another_document.java @@ -0,0 +1,38 @@ +/* + * Copyright 2025 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.community.sql.changes.sqlserver.happyPath; + +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.TargetSystem; + +import java.sql.Connection; +import java.sql.PreparedStatement; + +@TargetSystem(id = "sql") +@Change(id = "insert-another-document", author = "aperezdieppa") +public class _003__insert_another_document { + + @Apply + public void execution(Connection connection) throws Exception { + try (PreparedStatement ps = connection.prepareStatement( + "INSERT INTO test_table (id, name) VALUES (?, ?)")) { + ps.setString(1, "test-client-Jorge"); + ps.setString(2, "Jorge"); + ps.executeUpdate(); + } + } +} From 5ac12ac7c1371e4e6201bff85d627f2a744724d4 Mon Sep 17 00:00:00 2001 From: davidfrigolet Date: Tue, 21 Oct 2025 18:42:16 +0100 Subject: [PATCH 5/9] feat: update sql audit store test with mariadb --- .../build.gradle.kts | 2 + .../community/sql/SqlAuditStoreTest.java | 179 +----------- .../community/sql/SqlAuditTestHelper.java | 258 ++++++++++++++++++ .../flamingock/community/sql/TestContext.java | 33 +++ 4 files changed, 305 insertions(+), 167 deletions(-) create mode 100644 community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/SqlAuditTestHelper.java create mode 100644 community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/TestContext.java diff --git a/community/flamingock-auditstore-sql/build.gradle.kts b/community/flamingock-auditstore-sql/build.gradle.kts index 2633039d5..9008f8e67 100644 --- a/community/flamingock-auditstore-sql/build.gradle.kts +++ b/community/flamingock-auditstore-sql/build.gradle.kts @@ -7,10 +7,12 @@ dependencies { testImplementation("com.microsoft.sqlserver:mssql-jdbc:12.4.2.jre8") testImplementation("com.oracle.database.jdbc:ojdbc8:21.9.0.0") testImplementation("org.postgresql:postgresql:42.7.3") + testImplementation("org.mariadb.jdbc:mariadb-java-client:3.3.2") testImplementation("org.testcontainers:mysql:1.21.3") testImplementation("org.testcontainers:mssqlserver:1.21.3") testImplementation("org.testcontainers:oracle-xe:1.21.3") testImplementation("org.testcontainers:postgresql:1.21.3") + testImplementation("org.testcontainers:mariadb:1.21.3") testImplementation(project(":utils:test-util")) testImplementation("com.zaxxer:HikariCP:3.4.5") testImplementation("org.testcontainers:junit-jupiter:1.21.3") diff --git a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/SqlAuditStoreTest.java b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/SqlAuditStoreTest.java index 7fc4c807e..d2ab2bb19 100644 --- a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/SqlAuditStoreTest.java +++ b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/SqlAuditStoreTest.java @@ -53,12 +53,13 @@ static Stream dialectProvider() { Arguments.of(SqlDialect.MYSQL, "mysql"), Arguments.of(SqlDialect.SQLSERVER, "sqlserver"), Arguments.of(SqlDialect.ORACLE, "oracle"), - Arguments.of(SqlDialect.POSTGRESQL, "postgresql") + Arguments.of(SqlDialect.POSTGRESQL, "postgresql"), + Arguments.of(SqlDialect.MARIADB, "mariadb") ); } private TestContext setupTest(SqlDialect sqlDialect, String dialectName) throws SQLException { - JdbcDatabaseContainer container = createContainer(dialectName); + JdbcDatabaseContainer container = SqlAuditTestHelper.createContainer(dialectName); container.start(); HikariConfig config = new HikariConfig(); @@ -68,7 +69,7 @@ private TestContext setupTest(SqlDialect sqlDialect, String dialectName) throws config.setDriverClassName(container.getDriverClassName()); DataSource dataSource = new HikariDataSource(config); - createTables(dataSource, sqlDialect); + SqlAuditTestHelper.createTables(dataSource, sqlDialect); return new TestContext(dataSource, container, sqlDialect); } @@ -119,102 +120,20 @@ public String getDatabaseName() { .withDatabaseName("testdb") .withUsername("test") .withPassword("test"); + case "mariadb": + return new MariaDBContainer<>("mariadb:11.3.2") + .withDatabaseName("testdb") + .withUsername("testuser") + .withPassword("testpass"); default: throw new IllegalArgumentException("Unsupported dialect: " + dialectName); } } - private void createTables(DataSource dataSource, SqlDialect dialect) throws SQLException { - try (Connection conn = dataSource.getConnection()) { - dropTablesIfExist(conn, dialect); - - String createTestTable = getCreateTestTableSql(dialect); - conn.createStatement().execute(createTestTable); - - String createLockTable = getCreateLockTableSql(dialect); - conn.createStatement().execute(createLockTable); - } - } - - private void dropTablesIfExist(Connection conn, SqlDialect dialect) throws SQLException { - String[] tables = {"flamingockAuditLog", "test_table", "flamingockLock"}; - for (String table : tables) { - try { - String dropSql = getDropTableSql(table, dialect); - conn.createStatement().execute(dropSql); - } catch (SQLException e) { - // Ignore if table doesn't exist - } - } - } - - private String getDropTableSql(String tableName, SqlDialect dialect) { - if (dialect == SqlDialect.ORACLE) { - return "DROP TABLE " + tableName + " CASCADE CONSTRAINTS"; - } - return "DROP TABLE IF EXISTS " + tableName; - } - - private String getCreateTestTableSql(SqlDialect dialect) { - switch (dialect) { - case MYSQL: - case SQLSERVER: - return "CREATE TABLE test_table (" + - "id VARCHAR(255) PRIMARY KEY, " + - "name VARCHAR(255), " + - "field1 VARCHAR(255), " + - "field2 VARCHAR(255))"; - case POSTGRESQL: - return "CREATE TABLE test_table (" + - "id VARCHAR(255) PRIMARY KEY," + - "name VARCHAR(255)," + - "field1 VARCHAR(255)," + - "field2 VARCHAR(255))"; - case ORACLE: - return "CREATE TABLE test_table (" + - "id VARCHAR2(255) PRIMARY KEY, " + - "name VARCHAR2(255), " + - "field1 VARCHAR2(255), " + - "field2 VARCHAR2(255))"; - default: - throw new UnsupportedOperationException("Dialect not supported: " + dialect); - } - } - - private String getCreateLockTableSql(SqlDialect dialect) { - switch (dialect) { - case MYSQL: - return "CREATE TABLE flamingockLock (" + - "`key` VARCHAR(255) PRIMARY KEY, " + - "status VARCHAR(32), " + - "owner VARCHAR(255), " + - "expires_at TIMESTAMP)"; - case POSTGRESQL: - return "CREATE TABLE flamingockLock (" + - "\"key\" VARCHAR(255) PRIMARY KEY," + - "status VARCHAR(32)," + - "owner VARCHAR(255)," + - "expires_at TIMESTAMP)"; - case SQLSERVER: - return "CREATE TABLE flamingockLock (" + - "[key] VARCHAR(255) PRIMARY KEY, " + - "status VARCHAR(32), " + - "owner VARCHAR(255), " + - "expires_at DATETIME)"; - case ORACLE: - return "CREATE TABLE flamingockLock (" + - "\"key\" VARCHAR2(255) PRIMARY KEY, " + - "status VARCHAR2(32), " + - "owner VARCHAR2(255), " + - "expires_at TIMESTAMP)"; - default: - throw new UnsupportedOperationException("Dialect not supported: " + dialect); - } - } - private Class[] getChangeClasses(String dialectName, String scenario) { switch (dialectName) { case "mysql": + case "mariadb": if ("happyPath".equals(scenario)) { return new Class[]{ io.flamingock.community.sql.changes.mysql.happyPath._001__create_index.class, @@ -442,7 +361,7 @@ void failedWithRollback(SqlDialect sqlDialect, String dialectName) throws Except } // Verify index exists - verifyIndexExists(context); + SqlAuditTestHelper.verifyIndexExists(context); // Verify partial data try (Connection conn = context.dataSource.getConnection(); @@ -531,7 +450,7 @@ void failedWithoutRollback(SqlDialect sqlDialect, String dialectName) throws Exc } // Verify index exists and data state - verifyIndexExists(context); + SqlAuditTestHelper.verifyIndexExists(context); verifyPartialDataState(context); } } finally { @@ -539,68 +458,6 @@ void failedWithoutRollback(SqlDialect sqlDialect, String dialectName) throws Exc } } - private void verifyIndexExists(TestContext context) throws SQLException { - try (Connection conn = context.dataSource.getConnection()) { - String indexCheckSql = getIndexCheckSql(context.dialect); - try (PreparedStatement ps = conn.prepareStatement(indexCheckSql)) { - switch (context.dialect) { - case ORACLE: - ps.setString(1, "IDX_STANDALONE_INDEX"); - ps.setString(2, "TEST_TABLE"); - break; - case POSTGRESQL: - ps.setString(1, "idx_standalone_index"); - break; - case MYSQL: - case MARIADB: - ps.setString(1, "test_table"); - ps.setString(2, "idx_standalone_index"); - break; - case SQLSERVER: - case SYBASE: - ps.setString(1, "idx_standalone_index"); - break; - case H2: - case HSQLDB: - case DERBY: - case SQLITE: - ps.setString(1, "idx_standalone_index"); - break; - default: - ps.setString(1, "idx_standalone_index"); - break; - } - - try (ResultSet rs = ps.executeQuery()) { - boolean indexExists = rs.next(); - assertTrue(indexExists, "Index idx_standalone_index should exist"); - } - } - } - } - - private String getIndexCheckSql(SqlDialect dialect) { - switch (dialect) { - case POSTGRESQL: - return "SELECT indexname FROM pg_indexes WHERE indexname = ?"; - case MYSQL: - case MARIADB: - return "SELECT INDEX_NAME FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_NAME = ? AND INDEX_NAME = ?"; - case ORACLE: - return "SELECT INDEX_NAME FROM USER_INDEXES WHERE INDEX_NAME = ? AND TABLE_NAME = ?"; - case SQLSERVER: - case SYBASE: - return "SELECT name FROM sys.indexes WHERE name = ?"; - case H2: - case HSQLDB: - case DERBY: - case SQLITE: - return "SELECT INDEX_NAME FROM INFORMATION_SCHEMA.INDEXES WHERE INDEX_NAME = ?"; - default: - return "SELECT INDEX_NAME FROM INFORMATION_SCHEMA.INDEXES WHERE INDEX_NAME = ?"; - } - } - private void verifyPartialDataState(TestContext context) throws SQLException { try (Connection conn = context.dataSource.getConnection(); PreparedStatement ps = conn.prepareStatement("SELECT name FROM test_table WHERE id = ?")) { @@ -619,16 +476,4 @@ private void verifyPartialDataState(TestContext context) throws SQLException { } } } - - private static class TestContext { - final DataSource dataSource; - final JdbcDatabaseContainer container; - final SqlDialect dialect; - - TestContext(DataSource dataSource, JdbcDatabaseContainer container, SqlDialect dialect) { - this.dataSource = dataSource; - this.container = container; - this.dialect = dialect; - } - } } diff --git a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/SqlAuditTestHelper.java b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/SqlAuditTestHelper.java new file mode 100644 index 000000000..4b65ec230 --- /dev/null +++ b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/SqlAuditTestHelper.java @@ -0,0 +1,258 @@ +/* + * Copyright 2025 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.community.sql; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import io.flamingock.internal.common.sql.SqlDialect; +import org.testcontainers.containers.*; +import org.testcontainers.utility.DockerImageName; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class SqlAuditTestHelper { + + public static JdbcDatabaseContainer createContainer(String dialectName) { + switch (dialectName) { + case "mysql": + return new MySQLContainer<>("mysql:8.0") + .withDatabaseName("testdb") + .withUsername("testuser") + .withPassword("testpass"); + case "sqlserver": + return new MSSQLServerContainer<>("mcr.microsoft.com/mssql/server:2019-CU18-ubuntu-20.04") + .acceptLicense() + .withPassword("TestPass123!"); + case "oracle": + OracleContainer oracleContainer = new OracleContainer( + DockerImageName.parse("gvenzl/oracle-free:23-slim-faststart") + .asCompatibleSubstituteFor("gvenzl/oracle-xe")) + .withPassword("oracle123") + .withSharedMemorySize(1073741824L) + .withStartupTimeoutSeconds(900) + .withEnv("ORACLE_CHARACTERSET", "AL32UTF8"); + + return new OracleContainer( + DockerImageName.parse("gvenzl/oracle-free:23-slim-faststart") + .asCompatibleSubstituteFor("gvenzl/oracle-xe")) { + @Override + public String getDatabaseName() { + return "FREEPDB1"; + } + } + .withPassword("oracle123") + .withSharedMemorySize(1073741824L) + .withStartupTimeoutSeconds(900) + .withEnv("ORACLE_CHARACTERSET", "AL32UTF8"); + case "postgresql": + return new PostgreSQLContainer<>(DockerImageName.parse("postgres:15")) + .withDatabaseName("testdb") + .withUsername("test") + .withPassword("test"); + case "mariadb": + return new MariaDBContainer<>("mariadb:11.3.2") + .withDatabaseName("testdb") + .withUsername("testuser") + .withPassword("testpass"); + default: + throw new IllegalArgumentException("Unsupported dialect: " + dialectName); + } + } + + public static void createTables(DataSource dataSource, SqlDialect dialect) throws SQLException { + try (Connection conn = dataSource.getConnection()) { + dropTablesIfExist(conn, dialect); + + String createTestTable = getCreateTestTableSql(dialect); + conn.createStatement().execute(createTestTable); + + String createLockTable = getCreateLockTableSql(dialect); + conn.createStatement().execute(createLockTable); + } + } + + private static void dropTablesIfExist(Connection conn, SqlDialect dialect) throws SQLException { + String[] tables = {"flamingockAuditLog", "test_table", "flamingockLock"}; + for (String table : tables) { + try { + String dropSql = getDropTableSql(table, dialect); + conn.createStatement().execute(dropSql); + } catch (SQLException e) { + // Ignore if table doesn't exist + } + } + } + + private static String getDropTableSql(String tableName, SqlDialect dialect) { + if (dialect == SqlDialect.ORACLE) { + return "DROP TABLE " + tableName + " CASCADE CONSTRAINTS"; + } + return "DROP TABLE IF EXISTS " + tableName; + } + + private static String getCreateTestTableSql(SqlDialect dialect) { + switch (dialect) { + case MYSQL: + case SQLSERVER: + case MARIADB: + return "CREATE TABLE test_table (" + + "id VARCHAR(255) PRIMARY KEY, " + + "name VARCHAR(255), " + + "field1 VARCHAR(255), " + + "field2 VARCHAR(255))"; + case POSTGRESQL: + return "CREATE TABLE test_table (" + + "id VARCHAR(255) PRIMARY KEY," + + "name VARCHAR(255)," + + "field1 VARCHAR(255)," + + "field2 VARCHAR(255))"; + case ORACLE: + return "CREATE TABLE test_table (" + + "id VARCHAR2(255) PRIMARY KEY, " + + "name VARCHAR2(255), " + + "field1 VARCHAR2(255), " + + "field2 VARCHAR2(255))"; + default: + throw new UnsupportedOperationException("Dialect not supported: " + dialect); + } + } + + private static String getCreateLockTableSql(SqlDialect dialect) { + switch (dialect) { + case MYSQL: + case MARIADB: + return "CREATE TABLE flamingockLock (" + + "`key` VARCHAR(255) PRIMARY KEY, " + + "status VARCHAR(32), " + + "owner VARCHAR(255), " + + "expires_at TIMESTAMP)"; + case POSTGRESQL: + return "CREATE TABLE flamingockLock (" + + "\"key\" VARCHAR(255) PRIMARY KEY," + + "status VARCHAR(32)," + + "owner VARCHAR(255)," + + "expires_at TIMESTAMP)"; + case SQLSERVER: + return "CREATE TABLE flamingockLock (" + + "[key] VARCHAR(255) PRIMARY KEY, " + + "status VARCHAR(32), " + + "owner VARCHAR(255), " + + "expires_at DATETIME)"; + case ORACLE: + return "CREATE TABLE flamingockLock (" + + "\"key\" VARCHAR2(255) PRIMARY KEY, " + + "status VARCHAR2(32), " + + "owner VARCHAR2(255), " + + "expires_at TIMESTAMP)"; + default: + throw new UnsupportedOperationException("Dialect not supported: " + dialect); + } + } + + public static DataSource createDataSource(JdbcDatabaseContainer container) { + HikariConfig config = new HikariConfig(); + config.setJdbcUrl(container.getJdbcUrl()); + config.setUsername(container.getUsername()); + config.setPassword(container.getPassword()); + config.setDriverClassName(container.getDriverClassName()); + return new HikariDataSource(config); + } + + public static void verifyPartialDataState(DataSource dataSource) throws SQLException { + try (Connection conn = dataSource.getConnection(); + PreparedStatement ps = conn.prepareStatement("SELECT name FROM test_table WHERE id = ?")) { + ps.setString(1, "test-client-Federico"); + try (ResultSet rs = ps.executeQuery()) { + if (!rs.next() || !"Federico".equals(rs.getString("name"))) { + throw new AssertionError("Federico not found"); + } + } + ps.setString(1, "test-client-Jorge"); + try (ResultSet rs = ps.executeQuery()) { + if (!rs.next() || !"Jorge".equals(rs.getString("name"))) { + throw new AssertionError("Jorge not found"); + } + } + } + } + + private static String getIndexCheckSql(SqlDialect dialect) { + switch (dialect) { + case POSTGRESQL: + return "SELECT indexname FROM pg_indexes WHERE indexname = ?"; + case MYSQL: + case MARIADB: + return "SELECT INDEX_NAME FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_NAME = ? AND INDEX_NAME = ?"; + case ORACLE: + return "SELECT INDEX_NAME FROM USER_INDEXES WHERE INDEX_NAME = ? AND TABLE_NAME = ?"; + case SQLSERVER: + case SYBASE: + return "SELECT name FROM sys.indexes WHERE name = ?"; + case H2: + case HSQLDB: + case DERBY: + case SQLITE: + default: + return "SELECT INDEX_NAME FROM INFORMATION_SCHEMA.INDEXES WHERE INDEX_NAME = ?"; + } + } + + public static void verifyIndexExists(TestContext context) throws SQLException { + try (Connection conn = context.dataSource.getConnection()) { + String indexCheckSql = getIndexCheckSql(context.dialect); + try (PreparedStatement ps = conn.prepareStatement(indexCheckSql)) { + switch (context.dialect) { + case ORACLE: + ps.setString(1, "IDX_STANDALONE_INDEX"); + ps.setString(2, "TEST_TABLE"); + break; + case POSTGRESQL: + ps.setString(1, "idx_standalone_index"); + break; + case MYSQL: + case MARIADB: + ps.setString(1, "test_table"); + ps.setString(2, "idx_standalone_index"); + break; + case SQLSERVER: + case SYBASE: + ps.setString(1, "idx_standalone_index"); + break; + case H2: + case HSQLDB: + case DERBY: + case SQLITE: + ps.setString(1, "idx_standalone_index"); + break; + default: + ps.setString(1, "idx_standalone_index"); + break; + } + + try (ResultSet rs = ps.executeQuery()) { + boolean indexExists = rs.next(); + assertTrue(indexExists, "Index idx_standalone_index should exist"); + } + } + } + } +} diff --git a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/TestContext.java b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/TestContext.java new file mode 100644 index 000000000..c8cab1942 --- /dev/null +++ b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/TestContext.java @@ -0,0 +1,33 @@ +/* + * Copyright 2025 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.community.sql; + +import io.flamingock.internal.common.sql.SqlDialect; +import org.testcontainers.containers.JdbcDatabaseContainer; + +import javax.sql.DataSource; + +public class TestContext { + final DataSource dataSource; + final JdbcDatabaseContainer container; + final SqlDialect dialect; + + TestContext(DataSource dataSource, JdbcDatabaseContainer container, SqlDialect dialect) { + this.dataSource = dataSource; + this.container = container; + this.dialect = dialect; + } +} \ No newline at end of file From ca110b30e115ff43a252f3cd2c609cced46135de Mon Sep 17 00:00:00 2001 From: davidfrigolet Date: Wed, 22 Oct 2025 16:55:39 +0100 Subject: [PATCH 6/9] fix: changes after code review --- .../community/sql/driver/SqlAuditStore.java | 8 +++- .../sql/internal/SqlLockDialectHelper.java | 44 ++++++++++++++++++- .../sql/internal/SqlLockService.java | 44 +++++-------------- 3 files changed, 62 insertions(+), 34 deletions(-) diff --git a/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/driver/SqlAuditStore.java b/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/driver/SqlAuditStore.java index 8d91e840f..a03d0ca30 100644 --- a/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/driver/SqlAuditStore.java +++ b/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/driver/SqlAuditStore.java @@ -35,6 +35,7 @@ public class SqlAuditStore implements CommunityAuditStore { private SqlAuditPersistence persistence; private SqlLockService lockService; private String auditRepositoryName = CommunityPersistenceConstants.DEFAULT_AUDIT_STORE_NAME; + private String lockRepositoryName = CommunityPersistenceConstants.DEFAULT_LOCK_STORE_NAME; private boolean autoCreate = true; public SqlAuditStore(DataSource dataSource) { @@ -46,6 +47,11 @@ public SqlAuditStore withAuditRepositoryName(String auditRepositoryName) { return this; } + public SqlAuditStore withLockRepositoryName(String lockRepositoryName) { + this.lockRepositoryName = lockRepositoryName; + return this; + } + public SqlAuditStore withAutoCreate(boolean autoCreate) { this.autoCreate = autoCreate; return this; @@ -70,7 +76,7 @@ public synchronized CommunityAuditPersistence getPersistence() { @Override public synchronized CommunityLockService getLockService() { if (lockService == null) { - lockService = new SqlLockService(dataSource); + lockService = new SqlLockService(dataSource, lockRepositoryName, autoCreate); } return lockService; } diff --git a/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/internal/SqlLockDialectHelper.java b/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/internal/SqlLockDialectHelper.java index 4f0e78bdb..f8dc75bdf 100644 --- a/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/internal/SqlLockDialectHelper.java +++ b/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/internal/SqlLockDialectHelper.java @@ -17,8 +17,12 @@ import io.flamingock.internal.common.sql.AbstractSqlDialectHelper; import io.flamingock.internal.common.sql.SqlDialect; +import io.flamingock.internal.core.store.lock.LockStatus; import javax.sql.DataSource; +import java.sql.*; +import java.time.LocalDateTime; +import java.util.Objects; public final class SqlLockDialectHelper extends AbstractSqlDialectHelper { @@ -32,9 +36,16 @@ public SqlLockDialectHelper(SqlDialect dialect) { public String getCreateTableSqlString(String tableName) { switch (sqlDialect) { + case POSTGRESQL: + return String.format( + "CREATE TABLE IF NOT EXISTS %s (" + + "\"key\" VARCHAR(255) PRIMARY KEY," + + "status VARCHAR(32)," + + "owner VARCHAR(255)," + + "expires_at TIMESTAMP" + + ")", tableName); case MYSQL: case MARIADB: - case POSTGRESQL: case SQLITE: case H2: case HSQLDB: @@ -152,9 +163,40 @@ public String getInsertOrUpdateLockSqlString(String tableName) { } public String getDeleteLockSqlString(String tableName) { + if (Objects.requireNonNull(sqlDialect) == SqlDialect.POSTGRESQL) { + return String.format("DELETE FROM %s WHERE \"key\" = ?", tableName); + } return String.format("DELETE FROM %s WHERE `key` = ?", tableName); } + public void upsertLockEntry(Connection conn, String tableName, String key, String owner, LocalDateTime expiresAt) throws SQLException { + String sql = getInsertOrUpdateLockSqlString(tableName); + + if (getSqlDialect() == SqlDialect.SQLSERVER || getSqlDialect() == SqlDialect.SYBASE) { + // For SQL Server/Sybase, use Statement and format SQL + try (Statement stmt = conn.createStatement()) { + String formattedSql = sql + .replaceFirst("\\?", "'" + LockStatus.LOCK_HELD.name() + "'") + .replaceFirst("\\?", "'" + owner + "'") + .replaceFirst("\\?", "'" + Timestamp.valueOf(expiresAt) + "'") + .replaceFirst("\\?", "'" + key + "'") + .replaceFirst("\\?", "'" + key + "'") + .replaceFirst("\\?", "'" + LockStatus.LOCK_HELD.name() + "'") + .replaceFirst("\\?", "'" + owner + "'") + .replaceFirst("\\?", "'" + Timestamp.valueOf(expiresAt) + "'"); + stmt.execute(formattedSql); + } + } else { + try (PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setString(1, key); + ps.setString(2, LockStatus.LOCK_HELD.name()); + ps.setString(3, owner); + ps.setTimestamp(4, Timestamp.valueOf(expiresAt)); + ps.executeUpdate(); + } + } + } + public SqlDialect getSqlDialect() { return sqlDialect; } diff --git a/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/internal/SqlLockService.java b/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/internal/SqlLockService.java index 0bd2ebbe3..769b39df8 100644 --- a/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/internal/SqlLockService.java +++ b/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/internal/SqlLockService.java @@ -32,17 +32,21 @@ public class SqlLockService implements CommunityLockService { private static final String DEFAULT_LOCK_STORE_NAME = "flamingockLock"; private final DataSource dataSource; - private String lockRepositoryName = DEFAULT_LOCK_STORE_NAME; + private final String lockRepositoryName; private final SqlLockDialectHelper dialectHelper; - public SqlLockService(DataSource dataSource) { + public SqlLockService(DataSource dataSource, String lockRepositoryName, boolean autoCreate) { this.dataSource = dataSource; - this.dialectHelper = new SqlLockDialectHelper(dataSource); - } - - public SqlLockService withLockRepositoryName(String lockRepositoryName) { this.lockRepositoryName = lockRepositoryName; - return this; + this.dialectHelper = new SqlLockDialectHelper(dataSource); + if (autoCreate) { + try (Connection conn = dataSource.getConnection(); + Statement stmt = conn.createStatement()) { + stmt.executeUpdate(dialectHelper.getCreateTableSqlString(lockRepositoryName)); + } catch (SQLException e) { + throw new RuntimeException("Failed to initialize lock table", e); + } + } } public void initialize(boolean autoCreate) { @@ -178,30 +182,6 @@ private CommunityLockEntry getLockEntry(Connection conn, String key) throws SQLE } private void upsertLockEntry(Connection conn, String key, String owner, LocalDateTime expiresAt) throws SQLException { - String sql = dialectHelper.getInsertOrUpdateLockSqlString(lockRepositoryName); - - if (dialectHelper.getSqlDialect() == SqlDialect.SQLSERVER || dialectHelper.getSqlDialect() == SqlDialect.SYBASE) { - // For SQL Server, the SQL already contains transaction management - try (Statement stmt = conn.createStatement()) { - String formattedSql = sql.replace("?", "'" + LockStatus.LOCK_HELD.name() + "'") - .replaceFirst("'[^']*'", "'" + LockStatus.LOCK_HELD.name() + "'") - .replaceFirst("'[^']*'", "'" + owner + "'") - .replaceFirst("'[^']*'", "'" + Timestamp.valueOf(expiresAt) + "'") - .replaceFirst("'[^']*'", "'" + key + "'") - .replaceFirst("'[^']*'", "'" + key + "'") - .replaceFirst("'[^']*'", "'" + LockStatus.LOCK_HELD.name() + "'") - .replaceFirst("'[^']*'", "'" + owner + "'") - .replaceFirst("'[^']*'", "'" + Timestamp.valueOf(expiresAt) + "'"); - stmt.execute(formattedSql); - } - } else { - try (PreparedStatement ps = conn.prepareStatement(sql)) { - ps.setString(1, key); - ps.setString(2, LockStatus.LOCK_HELD.name()); - ps.setString(3, owner); - ps.setTimestamp(4, Timestamp.valueOf(expiresAt)); - ps.executeUpdate(); - } - } + dialectHelper.upsertLockEntry(conn, lockRepositoryName, key, owner, expiresAt); } } From 04ac3beac4415307f17618a4aac9e912da3939a1 Mon Sep 17 00:00:00 2001 From: davidfrigolet Date: Wed, 22 Oct 2025 19:35:11 +0100 Subject: [PATCH 7/9] fix: changes after code review --- .../flamingock/community/sql/driver/SqlAuditStore.java | 1 + .../community/sql/internal/SqlLockService.java | 9 +-------- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/driver/SqlAuditStore.java b/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/driver/SqlAuditStore.java index a03d0ca30..79c28de66 100644 --- a/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/driver/SqlAuditStore.java +++ b/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/driver/SqlAuditStore.java @@ -77,6 +77,7 @@ public synchronized CommunityAuditPersistence getPersistence() { public synchronized CommunityLockService getLockService() { if (lockService == null) { lockService = new SqlLockService(dataSource, lockRepositoryName, autoCreate); + lockService.initialize(autoCreate); } return lockService; } diff --git a/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/internal/SqlLockService.java b/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/internal/SqlLockService.java index 769b39df8..1a3b0e0aa 100644 --- a/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/internal/SqlLockService.java +++ b/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/internal/SqlLockService.java @@ -39,14 +39,6 @@ public SqlLockService(DataSource dataSource, String lockRepositoryName, boolean this.dataSource = dataSource; this.lockRepositoryName = lockRepositoryName; this.dialectHelper = new SqlLockDialectHelper(dataSource); - if (autoCreate) { - try (Connection conn = dataSource.getConnection(); - Statement stmt = conn.createStatement()) { - stmt.executeUpdate(dialectHelper.getCreateTableSqlString(lockRepositoryName)); - } catch (SQLException e) { - throw new RuntimeException("Failed to initialize lock table", e); - } - } } public void initialize(boolean autoCreate) { @@ -60,6 +52,7 @@ public void initialize(boolean autoCreate) { } } + @Override public LockAcquisition upsert(LockKey key, RunnerId owner, long leaseMillis) { String keyStr = key.toString(); From 4108d4715880501f719007fa23b5d9fa5539fb94 Mon Sep 17 00:00:00 2001 From: davidfrigolet Date: Fri, 24 Oct 2025 07:26:29 +0100 Subject: [PATCH 8/9] feat: update sql audit store test with h2 --- .../build.gradle.kts | 1 + .../sql/internal/SqlAuditorDialectHelper.java | 42 ++++++++++++++---- .../sql/internal/SqlLockDialectHelper.java | 2 - .../community/sql/SqlAuditStoreTest.java | 43 ++++++++++++++++--- .../community/sql/SqlAuditTestHelper.java | 29 +++++++------ 5 files changed, 90 insertions(+), 27 deletions(-) diff --git a/community/flamingock-auditstore-sql/build.gradle.kts b/community/flamingock-auditstore-sql/build.gradle.kts index 9008f8e67..235028362 100644 --- a/community/flamingock-auditstore-sql/build.gradle.kts +++ b/community/flamingock-auditstore-sql/build.gradle.kts @@ -18,6 +18,7 @@ dependencies { testImplementation("org.testcontainers:junit-jupiter:1.21.3") testImplementation("com.h2database:h2:2.2.224") testImplementation("org.mockito:mockito-inline:4.11.0") + testImplementation("org.xerial:sqlite-jdbc:3.41.2.1") } description = "SQL audit store implementation for distributed change auditing" diff --git a/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/internal/SqlAuditorDialectHelper.java b/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/internal/SqlAuditorDialectHelper.java index 3ac653a2d..8bd3de305 100644 --- a/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/internal/SqlAuditorDialectHelper.java +++ b/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/internal/SqlAuditorDialectHelper.java @@ -34,10 +34,8 @@ public String getCreateTableSqlString(String tableName) { switch (sqlDialect) { case MYSQL: case MARIADB: - case SQLITE: case H2: case HSQLDB: - case DERBY: case FIREBIRD: case INFORMIX: return String.format( @@ -162,6 +160,30 @@ public String getCreateTableSqlString(String tableName) { "transaction_flag %s, " + "system_change %s" + ")", tableName, getAutoIncrementType(), getClobType(), getBigIntType(), getClobType(), getBooleanType(), getBooleanType()); + case SQLITE: + return String.format( + "CREATE TABLE IF NOT EXISTS %s (" + + "id INTEGER PRIMARY KEY, " + + "execution_id TEXT, " + + "stage_id TEXT, " + + "task_id TEXT, " + + "author TEXT, " + + "created_at DATETIME, " + + "state TEXT, " + + "class_name TEXT, " + + "method_name TEXT, " + + "metadata TEXT, " + + "execution_millis INTEGER, " + + "execution_hostname TEXT, " + + "error_trace TEXT, " + + "type TEXT, " + + "tx_type TEXT, " + + "target_system_id TEXT, " + + "order_col TEXT, " + + "recovery_strategy TEXT, " + + "transaction_flag INTEGER, " + + "system_change INTEGER" + + ")", tableName); default: throw new UnsupportedOperationException("Dialect not supported for CREATE TABLE: " + sqlDialect.name()); } @@ -169,12 +191,19 @@ public String getCreateTableSqlString(String tableName) { public String getInsertSqlString(String tableName) { return String.format( - "INSERT INTO %s (execution_id, stage_id, task_id, author, created_at, state, class_name, method_name, metadata, execution_millis, execution_hostname, error_trace, type, tx_type, target_system_id, order_col, recovery_strategy, transaction_flag, system_change) " + - "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", tableName); + "INSERT INTO %s (" + + "execution_id, stage_id, task_id, author, created_at, state, class_name, method_name, metadata, " + + "execution_millis, execution_hostname, error_trace, type, tx_type, target_system_id, order_col, recovery_strategy, transaction_flag, system_change" + + ") VALUES (" + + "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?" + + ")", tableName); } public String getSelectHistorySqlString(String tableName) { - return String.format("SELECT * FROM %s ORDER BY id ASC", tableName); + return String.format( + "SELECT execution_id, stage_id, task_id, author, created_at, state, type, class_name, method_name, " + + "execution_millis, execution_hostname, metadata, system_change, error_trace, tx_type, target_system_id, order_col, recovery_strategy, transaction_flag " + + "FROM %s ORDER BY id ASC", tableName); } private String getAutoIncrementType() { @@ -184,7 +213,6 @@ private String getAutoIncrementType() { case SQLITE: case H2: case HSQLDB: - case DERBY: case DB2: case FIREBIRD: return "BIGINT GENERATED BY DEFAULT AS IDENTITY"; @@ -210,7 +238,6 @@ private String getClobType() { case SQLITE: case H2: case HSQLDB: - case DERBY: case FIREBIRD: case INFORMIX: case ORACLE: @@ -242,7 +269,6 @@ private String getBooleanType() { case POSTGRESQL: case H2: case HSQLDB: - case DERBY: case FIREBIRD: case INFORMIX: return "BOOLEAN"; diff --git a/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/internal/SqlLockDialectHelper.java b/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/internal/SqlLockDialectHelper.java index f8dc75bdf..416c671d1 100644 --- a/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/internal/SqlLockDialectHelper.java +++ b/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/internal/SqlLockDialectHelper.java @@ -49,7 +49,6 @@ public String getCreateTableSqlString(String tableName) { case SQLITE: case H2: case HSQLDB: - case DERBY: case FIREBIRD: case INFORMIX: return String.format( @@ -142,7 +141,6 @@ public String getInsertOrUpdateLockSqlString(String tableName) { tableName); case H2: case HSQLDB: - case DERBY: return String.format( "MERGE INTO %s (`key`, status, owner, expires_at) KEY (`key`) VALUES (?, ?, ?, ?)", tableName); diff --git a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/SqlAuditStoreTest.java b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/SqlAuditStoreTest.java index d2ab2bb19..637f29218 100644 --- a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/SqlAuditStoreTest.java +++ b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/SqlAuditStoreTest.java @@ -36,10 +36,7 @@ import org.testcontainers.utility.DockerImageName; import javax.sql.DataSource; -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; +import java.sql.*; import java.util.Collections; import java.util.stream.Stream; @@ -54,11 +51,45 @@ static Stream dialectProvider() { Arguments.of(SqlDialect.SQLSERVER, "sqlserver"), Arguments.of(SqlDialect.ORACLE, "oracle"), Arguments.of(SqlDialect.POSTGRESQL, "postgresql"), - Arguments.of(SqlDialect.MARIADB, "mariadb") + Arguments.of(SqlDialect.MARIADB, "mariadb"), + Arguments.of(SqlDialect.H2, "h2") + //Disabled due to issues with file-based databases in CI environments + //Arguments.of(SqlDialect.SQLITE, "sqlite") ); } private TestContext setupTest(SqlDialect sqlDialect, String dialectName) throws SQLException { + if ("h2".equals(dialectName)) { + 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 dataSource = new HikariDataSource(config); + + SqlAuditTestHelper.createTables(dataSource, sqlDialect); + + return new TestContext(dataSource, null, sqlDialect); + } + + if ("sqlite".equals(dialectName)) { + HikariConfig config = new HikariConfig(); + String dbFile = "test_" + System.currentTimeMillis() + ".db"; + config.setJdbcUrl("jdbc:sqlite:" + dbFile); + config.setDriverClassName("org.sqlite.JDBC"); + config.setMaximumPoolSize(1); + config.setMinimumIdle(1); + config.setConnectionTimeout(30000); + config.setMaxLifetime(0); + + config.setConnectionInitSql("PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000;"); + + DataSource dataSource = new HikariDataSource(config); + SqlAuditTestHelper.createTables(dataSource, sqlDialect); + + return new TestContext(dataSource, null, SqlDialect.SQLITE); + } + JdbcDatabaseContainer container = SqlAuditTestHelper.createContainer(dialectName); container.start(); @@ -134,6 +165,8 @@ private Class[] getChangeClasses(String dialectName, String scenario) { switch (dialectName) { case "mysql": case "mariadb": + case "sqlite": + case "h2": if ("happyPath".equals(scenario)) { return new Class[]{ io.flamingock.community.sql.changes.mysql.happyPath._001__create_index.class, diff --git a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/SqlAuditTestHelper.java b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/SqlAuditTestHelper.java index 4b65ec230..71d635aea 100644 --- a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/SqlAuditTestHelper.java +++ b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/SqlAuditTestHelper.java @@ -114,6 +114,9 @@ private static String getCreateTestTableSql(SqlDialect dialect) { case MYSQL: case SQLSERVER: case MARIADB: + case SQLITE: + case H2: + case HSQLDB: return "CREATE TABLE test_table (" + "id VARCHAR(255) PRIMARY KEY, " + "name VARCHAR(255), " + @@ -140,6 +143,11 @@ private static String getCreateLockTableSql(SqlDialect dialect) { switch (dialect) { case MYSQL: case MARIADB: + case SQLITE: + case H2: + case HSQLDB: + case FIREBIRD: + case INFORMIX: return "CREATE TABLE flamingockLock (" + "`key` VARCHAR(255) PRIMARY KEY, " + "status VARCHAR(32), " + @@ -207,12 +215,14 @@ private static String getIndexCheckSql(SqlDialect dialect) { case SQLSERVER: case SYBASE: return "SELECT name FROM sys.indexes WHERE name = ?"; + case SQLITE: + // SQLite uses sqlite_master for indexes + return "SELECT name FROM sqlite_master WHERE type='index' AND name = ?"; case H2: case HSQLDB: - case DERBY: - case SQLITE: default: return "SELECT INDEX_NAME FROM INFORMATION_SCHEMA.INDEXES WHERE INDEX_NAME = ?"; + } } @@ -225,24 +235,19 @@ public static void verifyIndexExists(TestContext context) throws SQLException { ps.setString(1, "IDX_STANDALONE_INDEX"); ps.setString(2, "TEST_TABLE"); break; - case POSTGRESQL: - ps.setString(1, "idx_standalone_index"); - break; case MYSQL: case MARIADB: ps.setString(1, "test_table"); ps.setString(2, "idx_standalone_index"); break; - case SQLSERVER: - case SYBASE: - ps.setString(1, "idx_standalone_index"); - break; case H2: case HSQLDB: - case DERBY: - case SQLITE: - ps.setString(1, "idx_standalone_index"); + ps.setString(1, "IDX_STANDALONE_INDEX"); break; + case POSTGRESQL: + case SQLSERVER: + case SYBASE: + case SQLITE: default: ps.setString(1, "idx_standalone_index"); break; From 60af2a4276578a99bd108e43e77698635402c050 Mon Sep 17 00:00:00 2001 From: davidfrigolet Date: Sun, 26 Oct 2025 18:48:18 +0000 Subject: [PATCH 9/9] feat: add sqlite test in sql audit store --- .../community/sql/driver/SqlAuditStore.java | 2 +- .../sql/internal/SqlLockService.java | 3 +- .../community/sql/SqlAuditStoreTest.java | 32 +++++++++++-------- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/driver/SqlAuditStore.java b/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/driver/SqlAuditStore.java index 79c28de66..c1d907139 100644 --- a/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/driver/SqlAuditStore.java +++ b/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/driver/SqlAuditStore.java @@ -76,7 +76,7 @@ public synchronized CommunityAuditPersistence getPersistence() { @Override public synchronized CommunityLockService getLockService() { if (lockService == null) { - lockService = new SqlLockService(dataSource, lockRepositoryName, autoCreate); + lockService = new SqlLockService(dataSource, lockRepositoryName); lockService.initialize(autoCreate); } return lockService; diff --git a/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/internal/SqlLockService.java b/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/internal/SqlLockService.java index 1a3b0e0aa..1123be744 100644 --- a/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/internal/SqlLockService.java +++ b/community/flamingock-auditstore-sql/src/main/java/io/flamingock/community/sql/internal/SqlLockService.java @@ -30,12 +30,11 @@ public class SqlLockService implements CommunityLockService { - private static final String DEFAULT_LOCK_STORE_NAME = "flamingockLock"; private final DataSource dataSource; private final String lockRepositoryName; private final SqlLockDialectHelper dialectHelper; - public SqlLockService(DataSource dataSource, String lockRepositoryName, boolean autoCreate) { + public SqlLockService(DataSource dataSource, String lockRepositoryName) { this.dataSource = dataSource; this.lockRepositoryName = lockRepositoryName; this.dialectHelper = new SqlLockDialectHelper(dataSource); diff --git a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/SqlAuditStoreTest.java b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/SqlAuditStoreTest.java index 637f29218..4e9893b1a 100644 --- a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/SqlAuditStoreTest.java +++ b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/community/sql/SqlAuditStoreTest.java @@ -31,6 +31,7 @@ import org.junit.jupiter.params.provider.MethodSource; import org.mockito.MockedStatic; import org.mockito.Mockito; +import org.sqlite.SQLiteDataSource; import org.testcontainers.containers.*; import org.testcontainers.junit.jupiter.Testcontainers; import org.testcontainers.utility.DockerImageName; @@ -52,9 +53,8 @@ static Stream dialectProvider() { Arguments.of(SqlDialect.ORACLE, "oracle"), Arguments.of(SqlDialect.POSTGRESQL, "postgresql"), Arguments.of(SqlDialect.MARIADB, "mariadb"), - Arguments.of(SqlDialect.H2, "h2") - //Disabled due to issues with file-based databases in CI environments - //Arguments.of(SqlDialect.SQLITE, "sqlite") + Arguments.of(SqlDialect.H2, "h2"), + Arguments.of(SqlDialect.SQLITE, "sqlite") ); } @@ -73,21 +73,25 @@ private TestContext setupTest(SqlDialect sqlDialect, String dialectName) throws } if ("sqlite".equals(dialectName)) { - HikariConfig config = new HikariConfig(); String dbFile = "test_" + System.currentTimeMillis() + ".db"; - config.setJdbcUrl("jdbc:sqlite:" + dbFile); - config.setDriverClassName("org.sqlite.JDBC"); - config.setMaximumPoolSize(1); - config.setMinimumIdle(1); - config.setConnectionTimeout(30000); - config.setMaxLifetime(0); - config.setConnectionInitSql("PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000;"); + // Use a shared in-memory DB or file DB, but single connection + String jdbcUrl = "jdbc:sqlite:" + dbFile; - DataSource dataSource = new HikariDataSource(config); - SqlAuditTestHelper.createTables(dataSource, sqlDialect); + // Create a single-connection DataSource for SQLite + SQLiteDataSource ds = new SQLiteDataSource(); + ds.setUrl(jdbcUrl); + + try (Connection conn = ds.getConnection(); + Statement stmt = conn.createStatement()) { + stmt.execute("PRAGMA journal_mode=WAL;"); + stmt.execute("PRAGMA busy_timeout=5000;"); + } + + // Run table creation with this same DataSource + SqlAuditTestHelper.createTables(ds, sqlDialect); - return new TestContext(dataSource, null, SqlDialect.SQLITE); + return new TestContext(ds, null, SqlDialect.SQLITE); } JdbcDatabaseContainer container = SqlAuditTestHelper.createContainer(dialectName);