From 0dc5fd3f232048e713c9da88573b862b3a3c3f06 Mon Sep 17 00:00:00 2001 From: dbulahov Date: Fri, 21 Aug 2026 10:52:25 +0300 Subject: [PATCH] fix close dialect gaps, add capability flags, atomic table swap, and rename for the remaining schema objects it was added: supportRenameTable supportRenameColumn supportRenameIndex supportRenameConstraint supportAtomicMultiRenameTable supportRenameView supportRenameTrigger supportRenameSequence and support them in dialects Signed-off-by: --- .../api/capability/DdlCapabilities.java | 8 +- .../DialectCapabilitiesProvider.java | 21 +++- .../dialect/api/generator/DdlGenerator.java | 115 +++++++++++++++++ .../db/clickhouse/ClickHouseDialect.java | 60 +++++++++ .../ClickHouseAlterRenameOfflineTest.java | 77 ++++++++++++ dialect/db/consistency-test/pom.xml | 117 ++++++++++++++++++ ...amilyRenameColumnVersionThresholdTest.java | 84 +++++++++++++ .../RenameFlagsAgreementTest.java | 91 ++++++++++++++ .../sql/dialect/db/derby/DerbyDialect.java | 56 +++++++++ .../sqlgen/DerbyAlterRenameOfflineTest.java | 76 ++++++++++++ .../sql/dialect/db/duckdb/DuckDbDialect.java | 36 +++++- .../sqlgen/DuckDbAlterRenameOfflineTest.java | 72 +++++++++++ .../h2/sqlgen/H2AlterRenameOfflineTest.java | 70 +++++++++++ .../dialect/db/mariadb/MariaDBDialect.java | 35 +++++- .../sqlgen/MariaDbAlterRenameOfflineTest.java | 63 ++++++++++ .../MicrosoftSqlServerDialect.java | 37 ++++++ .../sql/dialect/db/mysql/MySqlDialect.java | 90 ++++++++++++++ .../sqlgen/MySqlAlterRenameOfflineTest.java | 52 ++++++++ .../sql/dialect/db/oracle/OracleDialect.java | 45 +++++++ .../sqlgen/OracleAlterRenameOfflineTest.java | 19 +++ dialect/db/pom.xml | 1 + .../db/postgresql/PostgreSqlDialect.java | 15 +++ .../PostgreSqlAlterRenameOfflineTest.java | 33 +++++ .../sql/dialect/db/sqlite/SqliteDialect.java | 18 +++ .../sqlgen/SqliteAlterRenameOfflineTest.java | 66 ++++++++++ .../db/testsupport/GeneratorTestSupport.java | 17 +++ 26 files changed, 1366 insertions(+), 8 deletions(-) create mode 100644 dialect/db/clickhouse/src/test/java/org/eclipse/daanse/sql/dialect/db/clickhouse/sqlgen/ClickHouseAlterRenameOfflineTest.java create mode 100644 dialect/db/consistency-test/pom.xml create mode 100644 dialect/db/consistency-test/src/test/java/org/eclipse/daanse/sql/dialect/db/consistencytest/MySqlFamilyRenameColumnVersionThresholdTest.java create mode 100644 dialect/db/consistency-test/src/test/java/org/eclipse/daanse/sql/dialect/db/consistencytest/RenameFlagsAgreementTest.java create mode 100644 dialect/db/derby/src/test/java/org/eclipse/daanse/sql/dialect/db/derby/sqlgen/DerbyAlterRenameOfflineTest.java create mode 100644 dialect/db/duckdb/src/test/java/org/eclipse/daanse/sql/dialect/db/duckdb/sqlgen/DuckDbAlterRenameOfflineTest.java create mode 100644 dialect/db/h2/src/test/java/org/eclipse/daanse/sql/dialect/db/h2/sqlgen/H2AlterRenameOfflineTest.java create mode 100644 dialect/db/sqlite/src/test/java/org/eclipse/daanse/sql/dialect/db/sqlite/sqlgen/SqliteAlterRenameOfflineTest.java diff --git a/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/capability/DdlCapabilities.java b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/capability/DdlCapabilities.java index 3d53181..2c95e6f 100644 --- a/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/capability/DdlCapabilities.java +++ b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/capability/DdlCapabilities.java @@ -31,17 +31,19 @@ public record DdlCapabilities(boolean supportsDdl, boolean dropTableCascade, boo boolean dropIndexRequiresTable, boolean createTableIfNotExists, boolean createIndexIfNotExists, boolean dropIndexIfExists, boolean createOrReplaceView, boolean createOrReplaceTrigger, boolean dropViewIfExists, boolean dropConstraintIfExists, boolean dropTableIfExists, boolean dropSchemaIfExists, - boolean requiresDropSchemaRestrict, int maxColumnNameLength) { + boolean requiresDropSchemaRestrict, int maxColumnNameLength, + boolean renameTable, boolean renameColumn, boolean renameIndex, boolean renameConstraint, + boolean atomicMultiRenameTable, boolean renameView, boolean renameTrigger, boolean renameSequence) { /** All supported, no special requirements — default for modern engines. */ public static DdlCapabilities full() { return new DdlCapabilities(true, true, true, false, true, true, true, true, true, true, true, true, true, false, - 128); + 128, true, true, true, true, true, true, true, true); } /** Most conservative — DDL allowed but no convenience clauses. */ public static DdlCapabilities minimal() { return new DdlCapabilities(true, false, false, true, false, false, false, false, false, false, false, false, - false, true, 30); + false, true, 30, false, false, false, false, false, false, false, false); } } diff --git a/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/capability/DialectCapabilitiesProvider.java b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/capability/DialectCapabilitiesProvider.java index fc5ebe2..0e0dd4e 100644 --- a/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/capability/DialectCapabilitiesProvider.java +++ b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/capability/DialectCapabilitiesProvider.java @@ -42,7 +42,9 @@ default DdlCapabilities getDdlCapabilities() { dropIndexRequiresTable(), supportsCreateTableIfNotExists(), supportsCreateIndexIfNotExists(), supportsDropIndexIfExists(), supportsCreateOrReplaceView(), supportsCreateOrReplaceTrigger(), supportsDropViewIfExists(), supportsDropConstraintIfExists(), supportsDropTableIfExists(), - supportsDropSchemaIfExists(), requiresDropSchemaRestrict(), getMaxColumnNameLength()); + supportsDropSchemaIfExists(), requiresDropSchemaRestrict(), getMaxColumnNameLength(), + supportsRenameTable(), supportsRenameColumn(), supportsRenameIndex(), supportsRenameConstraint(), + supportsAtomicMultiRenameTable(), supportsRenameView(), supportsRenameTrigger(), supportsRenameSequence()); } int getMaxColumnNameLength(); @@ -239,4 +241,21 @@ default boolean requiresDropSchemaRestrict() { default boolean supportsNullsLast() { return true; } + + boolean supportsRenameSequence(); + + boolean supportsRenameTrigger(); + + boolean supportsRenameView(); + + boolean supportsAtomicMultiRenameTable(); + + boolean supportsRenameConstraint(); + + boolean supportsRenameIndex(); + + boolean supportsRenameColumn(); + + boolean supportsRenameTable(); + } diff --git a/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/DdlGenerator.java b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/DdlGenerator.java index 4a20f4e..499960b 100644 --- a/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/DdlGenerator.java +++ b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/DdlGenerator.java @@ -292,21 +292,33 @@ default String alterColumnDropDefault(TableReference table, String columnName) { } default String renameColumn(TableReference table, String oldName, String newName) { + if (!supportsRenameColumn()) { + return null; + } return new StringBuilder("ALTER TABLE ").append(qualified(table)).append(" RENAME COLUMN ") .append(quoteIdentifier(oldName)).append(" TO ").append(quoteIdentifier(newName)).toString(); } default String renameTable(TableReference table, String newName) { + if (!supportsRenameTable()) { + return null; + } return new StringBuilder("ALTER TABLE ").append(qualified(table)).append(" RENAME TO ") .append(quoteIdentifier(newName)).toString(); } default String renameIndex(String oldName, String newName, TableReference table) { + if (!supportsRenameIndex()) { + return null; + } return new StringBuilder("ALTER INDEX ").append(quoteIdentifier(oldName)).append(" RENAME TO ") .append(quoteIdentifier(newName)).toString(); } default String renameConstraint(TableReference table, String oldName, String newName) { + if (!supportsRenameConstraint()) { + return null; + } return new StringBuilder("ALTER TABLE ").append(qualified(table)).append(" RENAME CONSTRAINT ") .append(quoteIdentifier(oldName)).append(" TO ").append(quoteIdentifier(newName)).toString(); } @@ -967,4 +979,107 @@ default String qualifiedRoutine(String schemaName, String routineName) { return schemaName == null || schemaName.isBlank() ? quoteIdentifier(routineName) : quoteIdentifier(schemaName, routineName); } + + /** @return true if {@code renameTable} renders a valid statement */ + default boolean supportsRenameTable() { + return true; + } + + /** @return true if {@code renameColumn} renders a valid statement */ + default boolean supportsRenameColumn() { + return true; + } + + /** @return true if {@code renameIndex} renders a valid statement */ + default boolean supportsRenameIndex() { + return true; + } + + /** @return true if {@code renameConstraint} renders a valid statement */ + default boolean supportsRenameConstraint() { + return true; + } + + /** + * @return true if {@code renameTables} renders ONE statement that applies all + * renames atomically (MySQL family {@code RENAME TABLE a TO b, c TO d}, + * ClickHouse {@code RENAME TABLE}); false when the default renders one + * statement per pair with no atomicity guarantee + */ + default boolean supportsAtomicMultiRenameTable() { + return false; + } + + /** @return true if {@code renameView} renders a valid statement */ + default boolean supportsRenameView() { + return true; + } + + /** @return true if {@code renameTrigger} renders a valid statement */ + default boolean supportsRenameTrigger() { + return false; + } + + /** @return true if {@code renameSequence} renders a valid statement */ + default boolean supportsRenameSequence() { + return false; + } + + /** One table-rename step of a multi-rename. */ + record TableRename(TableReference table, String newName) { + } + + /** + * Renames several tables. Atomic (one statement, e.g. the classic + * {@code a→tmp, b→a, tmp→b} swap) when {@code supportsAtomicMultiRenameTable()}; + * otherwise one {@code renameTable} statement per step, in list order, no + * atomicity guarantee. Empty when any step is unsupported. + */ + default List renameTables(List renames) { + if (!supportsRenameTable() || renames == null || renames.isEmpty()) { + return List.of(); + } + return renames.stream().map(r -> renameTable(r.table(), r.newName())).toList(); + } + + /** {@code ALTER VIEW schema.view RENAME TO new}. Null when unsupported. */ + default String renameView(TableReference view, String newName) { + if (!supportsRenameView()) { + return null; + } + return new StringBuilder("ALTER VIEW ").append(qualified(view)).append(" RENAME TO ") + .append(quoteIdentifier(newName)).toString(); + } + + /** + * PostgreSQL shape: {@code ALTER TRIGGER name ON schema.table RENAME TO new} — + * the trigger's table is part of the statement, so it is part of the + * signature. Null when unsupported (the default flag is false; PostgreSQL and + * Oracle switch it on). + */ + default String renameTrigger(String triggerName, TableReference table, String newName) { + if (!supportsRenameTrigger()) { + return null; + } + return new StringBuilder("ALTER TRIGGER ").append(quoteIdentifier(triggerName)).append(" ON ") + .append(qualified(table)).append(" RENAME TO ").append(quoteIdentifier(newName)).toString(); + } + + /** {@code ALTER SEQUENCE schema.name RENAME TO new}. Empty when unsupported. */ + default Optional renameSequence(String schemaName, String name, String newName) { + if (!supportsSequences() || !supportsRenameSequence()) { + return Optional.empty(); + } + return Optional.of(new StringBuilder("ALTER SEQUENCE ").append(quoteIdentifier(schemaName, name)) + .append(" RENAME TO ").append(quoteIdentifier(newName)).toString()); + } + + /** + * Rename with the column's full definition in hand. The default ignores the + * metadata; the MySQL family uses it to render the pre-8.0/10.5.2 + * {@code ALTER TABLE t CHANGE old new } fallback. + */ + default String renameColumn(TableReference table, String oldName, String newName, ColumnMetaData currentMeta) { + return renameColumn(table, oldName, newName); + } } diff --git a/dialect/db/clickhouse/src/main/java/org/eclipse/daanse/sql/dialect/db/clickhouse/ClickHouseDialect.java b/dialect/db/clickhouse/src/main/java/org/eclipse/daanse/sql/dialect/db/clickhouse/ClickHouseDialect.java index 0da6b56..ea4e8eb 100644 --- a/dialect/db/clickhouse/src/main/java/org/eclipse/daanse/sql/dialect/db/clickhouse/ClickHouseDialect.java +++ b/dialect/db/clickhouse/src/main/java/org/eclipse/daanse/sql/dialect/db/clickhouse/ClickHouseDialect.java @@ -20,6 +20,7 @@ import java.util.List; +import org.eclipse.daanse.sql.model.schema.TableReference; import org.eclipse.daanse.sql.model.sql.BitOperation; import org.eclipse.daanse.sql.model.sql.OrderedColumn; import org.eclipse.daanse.sql.dialect.db.common.AbstractJdbcDialect; @@ -150,6 +151,18 @@ public boolean supportsDropConstraintIfExists() { return false; } + /** ClickHouse has no index rename — it has no b-tree index DDL at all (see {@link #supportsIndexDdl}). */ + @Override + public boolean supportsRenameIndex() { + return false; // skipping indexes: only DROP INDEX / ADD INDEX + } + + /** ClickHouse has no constraint rename. */ + @Override + public boolean supportsRenameConstraint() { + return false; + } + @Override public boolean supportsCreateOrReplaceView() { return false; @@ -236,4 +249,51 @@ public boolean supportsNthValue() { public boolean supportsListAgg() { return true; } + + /** ClickHouse: {@code RENAME TABLE a TO b} — there is no ALTER TABLE … RENAME TO. */ + @Override + public String renameTable(TableReference table, String newName) { + if (!supportsRenameTable()) { + return null; + } + return new StringBuilder("RENAME TABLE ").append(qualified(table)).append(" TO ") + .append(quoteIdentifier(newName)).toString(); + } + + /** + * ClickHouse views are table-like; {@code RENAME TABLE} covers them. Renders + * the statement directly rather than delegating to {@link #renameTable} so + * the two capability flags stay independent. + */ + @Override + public String renameView(TableReference view, String newName) { + if (!supportsRenameView()) { + return null; + } + return new StringBuilder("RENAME TABLE ").append(qualified(view)).append(" TO ") + .append(quoteIdentifier(newName)).toString(); + } + + /** {@code RENAME TABLE a TO b, c TO d} — one atomic statement. */ + @Override + public List renameTables(List renames) { + if (renames == null || renames.isEmpty()) { + return List.of(); + } + StringBuilder sb = new StringBuilder("RENAME TABLE "); + for (int i = 0; i < renames.size(); i++) { + if (i > 0) { + sb.append(", "); + } + TableRename r = renames.get(i); + sb.append(qualified(r.table())).append(" TO ").append(quoteIdentifier(r.newName())); + } + return List.of(sb.toString()); + } + + @Override + public boolean supportsAtomicMultiRenameTable() { + return true; + } + } diff --git a/dialect/db/clickhouse/src/test/java/org/eclipse/daanse/sql/dialect/db/clickhouse/sqlgen/ClickHouseAlterRenameOfflineTest.java b/dialect/db/clickhouse/src/test/java/org/eclipse/daanse/sql/dialect/db/clickhouse/sqlgen/ClickHouseAlterRenameOfflineTest.java new file mode 100644 index 0000000..21eaaae --- /dev/null +++ b/dialect/db/clickhouse/src/test/java/org/eclipse/daanse/sql/dialect/db/clickhouse/sqlgen/ClickHouseAlterRenameOfflineTest.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.daanse.sql.dialect.db.clickhouse.sqlgen; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import java.util.Optional; + +import org.eclipse.daanse.sql.model.schema.SchemaReference; +import org.eclipse.daanse.sql.model.schema.TableReference; +import org.eclipse.daanse.sql.dialect.api.generator.DdlGenerator.TableRename; +import org.eclipse.daanse.sql.dialect.db.clickhouse.ClickHouseDialect; +import org.junit.jupiter.api.Test; + +/** + * ClickHouse has no {@code ALTER TABLE ... RENAME TO} — table and view rename + * both go through the standalone {@code RENAME TABLE} statement, which also + * batches multiple pairs into one atomic statement. There is no b-tree index + * DDL and no constraint rename; column rename is the SQL-99 default. + */ +class ClickHouseAlterRenameOfflineTest { + + private static final SchemaReference S = new SchemaReference(Optional.empty(), "PUBLIC"); + private static final TableReference T = new TableReference(Optional.of(S), "EMPLOYEES", TableReference.TYPE_TABLE); + private static final TableReference V = new TableReference(Optional.of(S), "V_EMP", TableReference.TYPE_VIEW); + private static final TableReference TBL_A = new TableReference(Optional.of(S), "A", TableReference.TYPE_TABLE); + private static final TableReference TBL_B = new TableReference(Optional.of(S), "B", TableReference.TYPE_TABLE); + + private final ClickHouseDialect dialect = new ClickHouseDialect(); + + @Test + void renameTable_uses_RENAME_TABLE() { + assertThat(dialect.ddlGenerator().renameTable(T, "STAFF")) + .isEqualTo("RENAME TABLE \"PUBLIC\".\"EMPLOYEES\" TO \"STAFF\""); + } + + @Test + void renameView_uses_the_same_RENAME_TABLE_form() { + assertThat(dialect.ddlGenerator().renameView(V, "V_STAFF")) + .isEqualTo("RENAME TABLE \"PUBLIC\".\"V_EMP\" TO \"V_STAFF\""); + } + + @Test + void renameTables_emits_one_atomic_RENAME_TABLE_statement() { + assertThat(dialect.ddlGenerator().supportsAtomicMultiRenameTable()).isTrue(); + assertThat(dialect.ddlGenerator().renameTables(List.of( + new TableRename(TBL_A, "B"), + new TableRename(TBL_B, "C")))) + .containsExactly("RENAME TABLE \"PUBLIC\".\"A\" TO \"B\", \"PUBLIC\".\"B\" TO \"C\""); + } + + @Test + void renameColumn_inherits_ANSI_default() { + assertThat(dialect.ddlGenerator().renameColumn(T, "OLD", "NEW")) + .isEqualTo("ALTER TABLE \"PUBLIC\".\"EMPLOYEES\" RENAME COLUMN \"OLD\" TO \"NEW\""); + } + + @Test + void renameIndex_unsupported() { + assertThat(dialect.ddlGenerator().supportsRenameIndex()).isFalse(); + assertThat(dialect.ddlGenerator().renameIndex("IDX_OLD", "IDX_NEW", T)).isNull(); + } + + @Test + void renameConstraint_unsupported() { + assertThat(dialect.ddlGenerator().supportsRenameConstraint()).isFalse(); + assertThat(dialect.ddlGenerator().renameConstraint(T, "OLD_FK", "NEW_FK")).isNull(); + } +} diff --git a/dialect/db/consistency-test/pom.xml b/dialect/db/consistency-test/pom.xml new file mode 100644 index 0000000..fa2624f --- /dev/null +++ b/dialect/db/consistency-test/pom.xml @@ -0,0 +1,117 @@ + + + + 4.0.0 + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.db + ${revision} + ../pom.xml + + org.eclipse.daanse.sql.dialect.db.consistency-test + Eclipse Daanse JDBC DB Dialect Cross-Engine Consistency Tests + Test-only aggregator that depends on every concrete engine dialect + at once, to run checks across the whole matrix (e.g. rename capability + flags agreeing with their generators) in one parameterized test. The + per-engine modules depend on test-support, not on each other or on this + module, so this is the one place such a matrix test can live without + inverting that dependency direction. + + + + org.eclipse.daanse + org.eclipse.daanse.sql.model + 0.0.1-SNAPSHOT + test + + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.api + ${revision} + test + + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.db.test-support + ${revision} + test + + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.db.postgresql + ${revision} + test + + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.db.oracle + ${revision} + test + + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.db.mssqlserver + ${revision} + test + + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.db.mysql + ${revision} + test + + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.db.mariadb + ${revision} + test + + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.db.h2 + ${revision} + test + + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.db.sqlite + ${revision} + test + + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.db.duckdb + ${revision} + test + + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.db.derby + ${revision} + test + + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.db.clickhouse + ${revision} + test + + + org.assertj + assertj-core + test + + + diff --git a/dialect/db/consistency-test/src/test/java/org/eclipse/daanse/sql/dialect/db/consistencytest/MySqlFamilyRenameColumnVersionThresholdTest.java b/dialect/db/consistency-test/src/test/java/org/eclipse/daanse/sql/dialect/db/consistencytest/MySqlFamilyRenameColumnVersionThresholdTest.java new file mode 100644 index 0000000..4d0f362 --- /dev/null +++ b/dialect/db/consistency-test/src/test/java/org/eclipse/daanse/sql/dialect/db/consistencytest/MySqlFamilyRenameColumnVersionThresholdTest.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.daanse.sql.dialect.db.consistencytest; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Optional; +import java.util.stream.Stream; + +import org.eclipse.daanse.sql.dialect.api.Dialect; +import org.eclipse.daanse.sql.dialect.api.DialectInitData; +import org.eclipse.daanse.sql.dialect.api.generator.DdlGenerator; +import org.eclipse.daanse.sql.dialect.db.mariadb.MariaDBDialect; +import org.eclipse.daanse.sql.dialect.db.mysql.MySqlDialect; +import org.eclipse.daanse.sql.model.schema.SchemaReference; +import org.eclipse.daanse.sql.model.schema.TableReference; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * Locks the null/non-null threshold of the metadata-free 3-arg + * {@code renameColumn}: MySQL below 8.0, MariaDB below 10.5.2 return null. + * MariaDB needs its own gate — its 10.x numbering would otherwise satisfy + * MySQL's inherited "8.0+" check. + */ +class MySqlFamilyRenameColumnVersionThresholdTest { + + private static final SchemaReference SCHEMA = new SchemaReference(Optional.empty(), "appdb"); + private static final TableReference TABLE = new TableReference(Optional.of(SCHEMA), "EMPLOYEES", + TableReference.TYPE_TABLE); + + private static Dialect mysql(int major, int minor) { + return new MySqlDialect( + DialectInitData.ansiDefaults().withQuoteIdentifierString("`").withVersion(major, minor)); + } + + private static Dialect mariadb(int major, int minor) { + return new MariaDBDialect( + DialectInitData.ansiDefaults().withQuoteIdentifierString("`").withVersion(major, minor)); + } + + record VersionCase(String label, Dialect dialect, boolean nativeRenameColumnSupported) { + @Override + public String toString() { + return label; + } + } + + static Stream cases() { + return Stream.of( + new VersionCase("mysql unknown version", new MySqlDialect(), true), + new VersionCase("mysql 8.0", mysql(8, 0), true), + new VersionCase("mysql 5.7", mysql(5, 7), false), + new VersionCase("mariadb unknown version", new MariaDBDialect(), true), + new VersionCase("mariadb 10.6", mariadb(10, 6), true), + new VersionCase("mariadb 10.5", mariadb(10, 5), true), + new VersionCase("mariadb 10.4", mariadb(10, 4), false)); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("cases") + void renameColumn_without_metadata_follows_the_native_threshold(VersionCase c) { + DdlGenerator g = c.dialect().ddlGenerator(); + + // Flag describes the metadata overload, not this one — never moves with version. + assertThat(g.supportsRenameColumn()).as("%s: supportsRenameColumn stays true", c.label()).isTrue(); + + String sql = g.renameColumn(TABLE, "OLD", "NEW"); + if (c.nativeRenameColumnSupported()) { + assertThat(sql).as("%s: native RENAME COLUMN expected", c.label()).isNotBlank() + .contains("RENAME COLUMN"); + } else { + assertThat(sql).as("%s: below the native threshold, metadata-free rename must be null", c.label()) + .isNull(); + } + } +} diff --git a/dialect/db/consistency-test/src/test/java/org/eclipse/daanse/sql/dialect/db/consistencytest/RenameFlagsAgreementTest.java b/dialect/db/consistency-test/src/test/java/org/eclipse/daanse/sql/dialect/db/consistencytest/RenameFlagsAgreementTest.java new file mode 100644 index 0000000..4a1caff --- /dev/null +++ b/dialect/db/consistency-test/src/test/java/org/eclipse/daanse/sql/dialect/db/consistencytest/RenameFlagsAgreementTest.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.daanse.sql.dialect.db.consistencytest; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.sql.JDBCType; +import java.util.Optional; +import java.util.OptionalInt; +import java.util.stream.Stream; + +import org.eclipse.daanse.sql.dialect.api.Dialect; +import org.eclipse.daanse.sql.dialect.api.capability.DdlCapabilities; +import org.eclipse.daanse.sql.dialect.api.generator.DdlGenerator; +import org.eclipse.daanse.sql.dialect.db.clickhouse.ClickHouseDialect; +import org.eclipse.daanse.sql.dialect.db.derby.DerbyDialect; +import org.eclipse.daanse.sql.dialect.db.duckdb.DuckDbDialect; +import org.eclipse.daanse.sql.dialect.db.h2.H2Dialect; +import org.eclipse.daanse.sql.dialect.db.mariadb.MariaDBDialect; +import org.eclipse.daanse.sql.dialect.db.mssqlserver.MicrosoftSqlServerDialect; +import org.eclipse.daanse.sql.dialect.db.mysql.MySqlDialect; +import org.eclipse.daanse.sql.dialect.db.oracle.OracleDialect; +import org.eclipse.daanse.sql.dialect.db.postgresql.PostgreSqlDialect; +import org.eclipse.daanse.sql.dialect.db.sqlite.SqliteDialect; +import org.eclipse.daanse.sql.dialect.db.testsupport.GeneratorTestSupport; +import org.eclipse.daanse.sql.model.schema.ColumnMetaData; +import org.eclipse.daanse.sql.model.schema.SchemaReference; +import org.eclipse.daanse.sql.model.schema.TableReference; +import org.junit.jupiter.api.Named; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * For every dialect and rename operation, checks {@code supportsRename*()} + * agrees with the {@link DdlGenerator} method: flag true → non-blank SQL, + * flag false → null. Own module, not {@code test-support}, to avoid a + * reactor cycle (see this module's pom). + */ +class RenameFlagsAgreementTest { + + private static final SchemaReference SCHEMA = new SchemaReference(Optional.empty(), "PUBLIC"); + private static final TableReference TABLE = new TableReference(Optional.of(SCHEMA), "EMPLOYEES", + TableReference.TYPE_TABLE); + private static final TableReference VIEW = new TableReference(Optional.of(SCHEMA), "V_EMP", + TableReference.TYPE_VIEW); + private static final ColumnMetaData SOME_META = GeneratorTestSupport.columnMeta(JDBCType.DECIMAL, + OptionalInt.of(12), ColumnMetaData.Nullability.NULLABLE); + + /** Every concrete dialect, named by {@link Dialect#name()} for the test report. */ + static Stream> dialects() { + return Stream.of(new PostgreSqlDialect(), new OracleDialect(), new MicrosoftSqlServerDialect(), + new MySqlDialect(), new MariaDBDialect(), new H2Dialect(), new SqliteDialect(), new DuckDbDialect(), + new DerbyDialect(), new ClickHouseDialect()) + .map(d -> Named.of(d.name(), d)); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("dialects") + void renameFlagsAgreeWithGenerators(Dialect d) { + DdlGenerator g = d.ddlGenerator(); + DdlCapabilities caps = d.getDdlCapabilities(); + + assertAgreement(d, "renameTable", caps.renameTable(), g.renameTable(TABLE, "N")); + assertAgreement(d, "renameColumn", caps.renameColumn(), g.renameColumn(TABLE, "A", "B", SOME_META)); + assertAgreement(d, "renameIndex", caps.renameIndex(), g.renameIndex("I", "J", TABLE)); + assertAgreement(d, "renameConstraint", caps.renameConstraint(), g.renameConstraint(TABLE, "C", "D")); + assertAgreement(d, "renameView", caps.renameView(), g.renameView(VIEW, "N")); + assertAgreement(d, "renameTrigger", caps.renameTrigger(), g.renameTrigger("TR", TABLE, "N")); + assertAgreement(d, "renameSequence", caps.renameSequence() && caps.sequences(), + g.renameSequence("S", "Q", "N").orElse(null)); + } + + private static void assertAgreement(Dialect d, String operation, boolean flag, String sql) { + if (flag) { + assertThat(sql) + .as("%s on %s: flag says supported, so the generator must render something", operation, d.name()) + .isNotBlank(); + } else { + assertThat(sql) + .as("%s on %s: flag says unsupported, so the generator must return null", operation, d.name()) + .isNull(); + } + } +} diff --git a/dialect/db/derby/src/main/java/org/eclipse/daanse/sql/dialect/db/derby/DerbyDialect.java b/dialect/db/derby/src/main/java/org/eclipse/daanse/sql/dialect/db/derby/DerbyDialect.java index 4c29609..9fba3b0 100644 --- a/dialect/db/derby/src/main/java/org/eclipse/daanse/sql/dialect/db/derby/DerbyDialect.java +++ b/dialect/db/derby/src/main/java/org/eclipse/daanse/sql/dialect/db/derby/DerbyDialect.java @@ -23,6 +23,7 @@ import org.eclipse.daanse.sql.dialect.db.common.AbstractJdbcDialect; import org.eclipse.daanse.sql.dialect.db.common.DialectUtil; +import org.eclipse.daanse.sql.model.schema.TableReference; /** * @author jhyde @@ -85,6 +86,18 @@ public boolean supportsDropConstraintIfExists() { return false; } + /** Derby has no constraint rename — verified unsupported. */ + @Override + public boolean supportsRenameConstraint() { + return false; + } + + /** Derby has no {@code RENAME VIEW} — verified unsupported. */ + @Override + public boolean supportsRenameView() { + return false; + } + @Override public boolean supportsDropSchemaIfExists() { return false; @@ -157,4 +170,47 @@ public boolean allowsFieldAlias() { public String name() { return SUPPORTED_PRODUCT_NAME.toLowerCase(); } + + /** + * Derby: {@code RENAME TABLE "schema"."old" TO "new"} — its own statement + * shape, not the SQL-99 {@code ALTER TABLE ... RENAME TO ...} default. The + * new name is unqualified; only the source side carries the schema. + */ + @Override + public String renameTable(TableReference table, String newName) { + if (!supportsRenameTable()) { + return null; + } + return new StringBuilder("RENAME TABLE ").append(qualified(table)).append(" TO ") + .append(quoteIdentifier(newName)).toString(); + } + + /** + * Derby: {@code RENAME COLUMN "schema"."table"."old" TO "new"} — the column + * being renamed is addressed as a schema-qualified {@code table.column} path, + * not the SQL-99 {@code ALTER TABLE ... RENAME COLUMN ...} default. + */ + @Override + public String renameColumn(TableReference table, String oldName, String newName) { + if (!supportsRenameColumn()) { + return null; + } + return new StringBuilder("RENAME COLUMN ").append(qualified(table)).append('.') + .append(quoteIdentifier(oldName)).append(" TO ").append(quoteIdentifier(newName)).toString(); + } + + /** + * Derby: {@code RENAME INDEX "old" TO "new"} — unqualified and with no + * owning table, unlike the SQL-99 {@code ALTER INDEX ... RENAME TO ...} + * default. + */ + @Override + public String renameIndex(String oldName, String newName, TableReference table) { + if (!supportsRenameIndex()) { + return null; + } + return new StringBuilder("RENAME INDEX ").append(quoteIdentifier(oldName)).append(" TO ") + .append(quoteIdentifier(newName)).toString(); + } + } diff --git a/dialect/db/derby/src/test/java/org/eclipse/daanse/sql/dialect/db/derby/sqlgen/DerbyAlterRenameOfflineTest.java b/dialect/db/derby/src/test/java/org/eclipse/daanse/sql/dialect/db/derby/sqlgen/DerbyAlterRenameOfflineTest.java new file mode 100644 index 0000000..d553d61 --- /dev/null +++ b/dialect/db/derby/src/test/java/org/eclipse/daanse/sql/dialect/db/derby/sqlgen/DerbyAlterRenameOfflineTest.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.daanse.sql.dialect.db.derby.sqlgen; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Optional; + +import org.eclipse.daanse.sql.model.schema.SchemaReference; +import org.eclipse.daanse.sql.model.schema.TableReference; +import org.eclipse.daanse.sql.dialect.db.derby.DerbyDialect; +import org.junit.jupiter.api.Test; + +/** + * Derby spells table/column/index rename with its own statement shapes + * ({@code RENAME TABLE}, {@code RENAME COLUMN table.col TO new}, + * {@code RENAME INDEX}) rather than the SQL-99 {@code ALTER ... RENAME ...} + * default, and has no constraint/view/trigger/sequence rename at all. + */ +class DerbyAlterRenameOfflineTest { + + private static final SchemaReference S = new SchemaReference(Optional.empty(), "PUBLIC"); + private static final TableReference T = new TableReference(Optional.of(S), "EMPLOYEES", TableReference.TYPE_TABLE); + private static final TableReference V = new TableReference(Optional.of(S), "V_EMP", TableReference.TYPE_VIEW); + + private final DerbyDialect dialect = new DerbyDialect(); + + @Test + void renameTable_uses_RENAME_TABLE() { + assertThat(dialect.ddlGenerator().renameTable(T, "STAFF")) + .isEqualTo("RENAME TABLE \"PUBLIC\".\"EMPLOYEES\" TO \"STAFF\""); + } + + @Test + void renameColumn_uses_RENAME_COLUMN_with_qualified_table() { + assertThat(dialect.ddlGenerator().renameColumn(T, "OLD", "NEW")) + .isEqualTo("RENAME COLUMN \"PUBLIC\".\"EMPLOYEES\".\"OLD\" TO \"NEW\""); + } + + @Test + void renameIndex_uses_RENAME_INDEX_unqualified() { + assertThat(dialect.ddlGenerator().renameIndex("IDX_OLD", "IDX_NEW", T)) + .isEqualTo("RENAME INDEX \"IDX_OLD\" TO \"IDX_NEW\""); + } + + @Test + void renameConstraint_unsupported() { + assertThat(dialect.ddlGenerator().supportsRenameConstraint()).isFalse(); + assertThat(dialect.ddlGenerator().renameConstraint(T, "OLD_FK", "NEW_FK")).isNull(); + } + + @Test + void renameView_unsupported() { + assertThat(dialect.ddlGenerator().supportsRenameView()).isFalse(); + assertThat(dialect.ddlGenerator().renameView(V, "V_STAFF")).isNull(); + } + + @Test + void renameTrigger_unsupported() { + assertThat(dialect.ddlGenerator().supportsRenameTrigger()).isFalse(); + assertThat(dialect.ddlGenerator().renameTrigger("TRG_AUDIT", T, "TRG_LOG")).isNull(); + } + + @Test + void renameSequence_unsupported() { + assertThat(dialect.ddlGenerator().supportsRenameSequence()).isFalse(); + assertThat(dialect.ddlGenerator().renameSequence("PUBLIC", "SEQ_EMP", "SEQ_STAFF")).isEmpty(); + } +} diff --git a/dialect/db/duckdb/src/main/java/org/eclipse/daanse/sql/dialect/db/duckdb/DuckDbDialect.java b/dialect/db/duckdb/src/main/java/org/eclipse/daanse/sql/dialect/db/duckdb/DuckDbDialect.java index 6bf591c..956ac88 100644 --- a/dialect/db/duckdb/src/main/java/org/eclipse/daanse/sql/dialect/db/duckdb/DuckDbDialect.java +++ b/dialect/db/duckdb/src/main/java/org/eclipse/daanse/sql/dialect/db/duckdb/DuckDbDialect.java @@ -173,6 +173,40 @@ public Optional generateRegularExpression(String source, String javaRege return Optional.of(sb.toString()); } + /** DuckDB has no {@code ALTER INDEX ... RENAME} — verified unsupported. */ + @Override + public boolean supportsRenameIndex() { + return false; + } + + /** DuckDB has no constraint rename — verified unsupported. */ + @Override + public boolean supportsRenameConstraint() { + return false; + } + + /** + * True — DuckDB supports the ANSI {@code RENAME TO} syntax. It still rejects + * it at runtime with a "Dependency Error" if the table has a dependent + * object (e.g. an index); that's a runtime constraint, not a syntax gap, so + * the flag stays true and callers must handle the failure themselves. + */ + @Override + public boolean supportsRenameTable() { + return true; + } + + /** + * True — DuckDB supports the ANSI {@code RENAME COLUMN} syntax. Same runtime + * caveat as {@link #supportsRenameTable}: rejected with a "Dependency Error" + * when the column has a dependent index, not because the syntax is + * unsupported. + */ + @Override + public boolean supportsRenameColumn() { + return true; + } + /** DuckDB supports {@code GROUPING SETS}, {@code ROLLUP} and {@code CUBE}. */ @Override public boolean supportsGroupingSets() { @@ -302,7 +336,7 @@ public java.util.Optional generatePercentileDisc(double percentile, bool .of((buildPercentileFunction("quantile_disc", percentile, desc, tableName, columnName)).toString()); } - @Override + @Override public java.util.Optional generatePercentileCont(double percentile, boolean desc, String tableName, String columnName) { return java.util.Optional diff --git a/dialect/db/duckdb/src/test/java/org/eclipse/daanse/sql/dialect/db/duckdb/sqlgen/DuckDbAlterRenameOfflineTest.java b/dialect/db/duckdb/src/test/java/org/eclipse/daanse/sql/dialect/db/duckdb/sqlgen/DuckDbAlterRenameOfflineTest.java new file mode 100644 index 0000000..d50df33 --- /dev/null +++ b/dialect/db/duckdb/src/test/java/org/eclipse/daanse/sql/dialect/db/duckdb/sqlgen/DuckDbAlterRenameOfflineTest.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.daanse.sql.dialect.db.duckdb.sqlgen; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Optional; + +import org.eclipse.daanse.sql.model.schema.SchemaReference; +import org.eclipse.daanse.sql.model.schema.TableReference; +import org.eclipse.daanse.sql.dialect.db.duckdb.DuckDbDialect; +import org.junit.jupiter.api.Test; + +/** + * DuckDB tracks the SQL-99 default for table/column/view rename; index and + * constraint rename have no DuckDB equivalent, and sequence rename is left + * off despite DuckDB having sequences (verified unsupported). + */ +class DuckDbAlterRenameOfflineTest { + + private static final SchemaReference S = new SchemaReference(Optional.empty(), "PUBLIC"); + private static final TableReference T = new TableReference(Optional.of(S), "EMPLOYEES", TableReference.TYPE_TABLE); + private static final TableReference V = new TableReference(Optional.of(S), "V_EMP", TableReference.TYPE_VIEW); + + private final DuckDbDialect dialect = new DuckDbDialect(); + + @Test + void renameTable_inherits_ANSI_default() { + assertThat(dialect.ddlGenerator().supportsRenameTable()).isTrue(); + assertThat(dialect.ddlGenerator().renameTable(T, "STAFF")) + .isEqualTo("ALTER TABLE \"PUBLIC\".\"EMPLOYEES\" RENAME TO \"STAFF\""); + } + + @Test + void renameColumn_inherits_ANSI_default() { + assertThat(dialect.ddlGenerator().supportsRenameColumn()).isTrue(); + assertThat(dialect.ddlGenerator().renameColumn(T, "OLD", "NEW")) + .isEqualTo("ALTER TABLE \"PUBLIC\".\"EMPLOYEES\" RENAME COLUMN \"OLD\" TO \"NEW\""); + } + + @Test + void renameView_inherits_ANSI_default() { + assertThat(dialect.ddlGenerator().supportsRenameView()).isTrue(); + assertThat(dialect.ddlGenerator().renameView(V, "V_STAFF")) + .isEqualTo("ALTER VIEW \"PUBLIC\".\"V_EMP\" RENAME TO \"V_STAFF\""); + } + + @Test + void renameIndex_unsupported() { + assertThat(dialect.ddlGenerator().supportsRenameIndex()).isFalse(); + assertThat(dialect.ddlGenerator().renameIndex("IDX_OLD", "IDX_NEW", T)).isNull(); + } + + @Test + void renameConstraint_unsupported() { + assertThat(dialect.ddlGenerator().supportsRenameConstraint()).isFalse(); + assertThat(dialect.ddlGenerator().renameConstraint(T, "OLD_FK", "NEW_FK")).isNull(); + } + + @Test + void renameSequence_unsupported() { + assertThat(dialect.ddlGenerator().supportsRenameSequence()).isFalse(); + assertThat(dialect.ddlGenerator().renameSequence("PUBLIC", "SEQ_EMP", "SEQ_STAFF")).isEmpty(); + } +} diff --git a/dialect/db/h2/src/test/java/org/eclipse/daanse/sql/dialect/db/h2/sqlgen/H2AlterRenameOfflineTest.java b/dialect/db/h2/src/test/java/org/eclipse/daanse/sql/dialect/db/h2/sqlgen/H2AlterRenameOfflineTest.java new file mode 100644 index 0000000..b8acb62 --- /dev/null +++ b/dialect/db/h2/src/test/java/org/eclipse/daanse/sql/dialect/db/h2/sqlgen/H2AlterRenameOfflineTest.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.daanse.sql.dialect.db.h2.sqlgen; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Optional; + +import org.eclipse.daanse.sql.model.schema.SchemaReference; +import org.eclipse.daanse.sql.model.schema.TableReference; +import org.eclipse.daanse.sql.dialect.db.h2.H2Dialect; +import org.junit.jupiter.api.Test; + +/** + * H2 takes the SQL-99 default verbatim for table/column/index/constraint/view + * rename — no overrides in {@link H2Dialect} at all. Sequence rename is + * pinned off: H2 has no {@code ALTER SEQUENCE ... RENAME TO}, so this locks + * the flag at false rather than leaving it to be flipped on without checking. + */ +class H2AlterRenameOfflineTest { + + private static final SchemaReference S = new SchemaReference(Optional.empty(), "PUBLIC"); + private static final TableReference T = new TableReference(Optional.of(S), "EMPLOYEES", TableReference.TYPE_TABLE); + private static final TableReference V = new TableReference(Optional.of(S), "V_EMP", TableReference.TYPE_VIEW); + + private final H2Dialect dialect = new H2Dialect(); + + @Test + void renameTable_uses_ANSI_default() { + assertThat(dialect.ddlGenerator().renameTable(T, "STAFF")) + .isEqualTo("ALTER TABLE \"PUBLIC\".\"EMPLOYEES\" RENAME TO \"STAFF\""); + } + + @Test + void renameColumn_uses_ANSI_default() { + assertThat(dialect.ddlGenerator().renameColumn(T, "OLD", "NEW")) + .isEqualTo("ALTER TABLE \"PUBLIC\".\"EMPLOYEES\" RENAME COLUMN \"OLD\" TO \"NEW\""); + } + + @Test + void renameIndex_uses_ANSI_default() { + assertThat(dialect.ddlGenerator().renameIndex("IDX_OLD", "IDX_NEW", T)) + .isEqualTo("ALTER INDEX \"IDX_OLD\" RENAME TO \"IDX_NEW\""); + } + + @Test + void renameConstraint_uses_ANSI_default() { + assertThat(dialect.ddlGenerator().renameConstraint(T, "OLD_FK", "NEW_FK")) + .isEqualTo("ALTER TABLE \"PUBLIC\".\"EMPLOYEES\" RENAME CONSTRAINT \"OLD_FK\" TO \"NEW_FK\""); + } + + @Test + void renameView_uses_ANSI_default() { + assertThat(dialect.ddlGenerator().renameView(V, "V_STAFF")) + .isEqualTo("ALTER VIEW \"PUBLIC\".\"V_EMP\" RENAME TO \"V_STAFF\""); + } + + @Test + void renameSequence_pinned_off_no_ALTER_SEQUENCE_RENAME_in_H2() { + assertThat(dialect.ddlGenerator().supportsRenameSequence()).isFalse(); + assertThat(dialect.ddlGenerator().renameSequence("PUBLIC", "SEQ_EMP", "SEQ_STAFF")).isEmpty(); + } +} diff --git a/dialect/db/mariadb/src/main/java/org/eclipse/daanse/sql/dialect/db/mariadb/MariaDBDialect.java b/dialect/db/mariadb/src/main/java/org/eclipse/daanse/sql/dialect/db/mariadb/MariaDBDialect.java index bc37b14..bbe64d7 100644 --- a/dialect/db/mariadb/src/main/java/org/eclipse/daanse/sql/dialect/db/mariadb/MariaDBDialect.java +++ b/dialect/db/mariadb/src/main/java/org/eclipse/daanse/sql/dialect/db/mariadb/MariaDBDialect.java @@ -22,10 +22,8 @@ package org.eclipse.daanse.sql.dialect.db.mariadb; -import java.util.List; -import java.util.Optional; - import org.eclipse.daanse.sql.dialect.db.mysql.MySqlDialect; +import org.eclipse.daanse.sql.model.schema.TableReference; public class MariaDBDialect extends MySqlDialect { @@ -95,6 +93,37 @@ public boolean supportsDropConstraintIfExists() { return dialectVersion.isUnknownOrAtLeast(10, 5); } + /** + * MariaDB's native {@code RENAME COLUMN} landed in 10.5.2 — not comparable to + * MySQL's inherited 8.0 threshold, since MariaDB's own 10.x/11.x numbering + * would always satisfy {@code isUnknownOrAtLeast(8, 0)}. Below 10.5, + * {@link #renameColumn(TableReference, String, String)} falls back to null. + */ + @Override + protected boolean supportsNativeRenameColumn() { + return dialectVersion.isUnknownOrAtLeast(10, 5); + } + + @Override + public boolean supportsRenameSequence() { + return dialectVersion.isUnknownOrAtLeast(10, 5); + } + + /** + * MariaDB ≥ 10.5.2: {@code ALTER TABLE "schema"."seq" RENAME TO "new"} — + * sequences live in the table namespace, so there is no + * {@code ALTER SEQUENCE ... RENAME TO} (verified live). + */ + @Override + public java.util.Optional renameSequence(String schemaName, String name, String newName) { + if (!supportsSequences() || !supportsRenameSequence()) { + return java.util.Optional.empty(); + } + String qualified = schemaName != null && !schemaName.isBlank() ? quoteIdentifier(schemaName, name) + : quoteIdentifier(name); + return java.util.Optional.of("ALTER TABLE " + qualified + " RENAME TO " + quoteIdentifier(newName)); + } + @Override public org.eclipse.daanse.sql.dialect.api.generator.ReturningGenerator returningGenerator() { var local = cachedReturningGenerator; diff --git a/dialect/db/mariadb/src/test/java/org/eclipse/daanse/sql/dialect/db/mariadb/sqlgen/MariaDbAlterRenameOfflineTest.java b/dialect/db/mariadb/src/test/java/org/eclipse/daanse/sql/dialect/db/mariadb/sqlgen/MariaDbAlterRenameOfflineTest.java index a9a81c8..408513d 100644 --- a/dialect/db/mariadb/src/test/java/org/eclipse/daanse/sql/dialect/db/mariadb/sqlgen/MariaDbAlterRenameOfflineTest.java +++ b/dialect/db/mariadb/src/test/java/org/eclipse/daanse/sql/dialect/db/mariadb/sqlgen/MariaDbAlterRenameOfflineTest.java @@ -14,10 +14,13 @@ import java.sql.JDBCType; import java.util.Optional; import java.util.OptionalInt; +import java.util.Set; import org.eclipse.daanse.sql.model.schema.ColumnMetaData; import org.eclipse.daanse.sql.model.schema.SchemaReference; import org.eclipse.daanse.sql.model.schema.TableReference; +import org.eclipse.daanse.sql.dialect.api.DialectInitData; +import org.eclipse.daanse.sql.dialect.api.IdentifierQuotingPolicy; import org.eclipse.daanse.sql.dialect.db.mariadb.MariaDBDialect; import org.eclipse.daanse.sql.jdbc.record.schema.ColumnMetaDataRecord; import org.junit.jupiter.api.Test; @@ -32,6 +35,21 @@ class MariaDbAlterRenameOfflineTest { private final MariaDBDialect dialect = new MariaDBDialect(); + /** + * MariaDB 10.4.34 — below the 10.5.2 threshold where native + * {@code RENAME COLUMN} landed. Major 10 alone would satisfy the inherited + * MySQL parent's 8.0 gate, so this version only falls back to CHANGE if + * MariaDB's own 10.5 threshold is actually consulted. + */ + private final MariaDBDialect dialect104 = new MariaDBDialect( + new DialectInitData("`", "MariaDB", "10.4.34", 10, 4, Set.of(), true, 0, Set.of(), + IdentifierQuotingPolicy.ALWAYS)); + + /** MariaDB 10.6.5 — above the 10.5.2 threshold: native {@code RENAME COLUMN}. */ + private final MariaDBDialect dialect106 = new MariaDBDialect( + new DialectInitData("`", "MariaDB", "10.6.5", 10, 6, Set.of(), true, 0, Set.of(), + IdentifierQuotingPolicy.ALWAYS)); + private static ColumnMetaData meta(JDBCType jdbc, OptionalInt size) { return new ColumnMetaDataRecord(jdbc, jdbc.getName(), size, OptionalInt.empty(), OptionalInt.empty(), ColumnMetaData.Nullability.NULLABLE, OptionalInt.empty(), Optional.empty(), Optional.empty(), @@ -54,4 +72,49 @@ void inherits_table_scoped_renameIndex() { void inherits_renameConstraint_returning_null() { assertThat(dialect.ddlGenerator().renameConstraint(T, "OLD_FK", "NEW_FK")).isNull(); } + + @Test + void renameColumn_10_4_34_falls_back_to_CHANGE_not_MySQLs_8_0_gate() { + // Major 10 alone would satisfy the inherited MySQL parent's "8.0+" check, + // so this failing without metadata proves MariaDB's own 10.5 gate — not + // MySQL's — is what governs here. + assertThat(dialect104.ddlGenerator().supportsRenameColumn()).isTrue(); + assertThat(dialect104.ddlGenerator().renameColumn(T, "OLD", "NEW")).isNull(); + } + + @Test + void renameColumn_10_4_34_with_metadata_uses_CHANGE() { + assertThat(dialect104.ddlGenerator().renameColumn(T, "OLD", "NEW", + meta(JDBCType.DECIMAL, OptionalInt.of(12)))) + .startsWith("ALTER TABLE `appdb`.`EMPLOYEES` CHANGE `OLD` `NEW` ") + .doesNotContain("RENAME COLUMN"); + } + + @Test + void renameColumn_10_6_5_uses_native_RENAME_COLUMN() { + assertThat(dialect106.ddlGenerator().renameColumn(T, "OLD", "NEW")) + .isEqualTo("ALTER TABLE `appdb`.`EMPLOYEES` RENAME COLUMN `OLD` TO `NEW`"); + } + + @Test + void renameSequence_uses_ALTER_TABLE_not_ALTER_SEQUENCE() { + // MariaDB stores sequences in the table namespace — there is no + // ALTER SEQUENCE ... RENAME TO in its grammar; verified live. + assertThat(dialect.ddlGenerator().supportsRenameSequence()).isTrue(); + assertThat(dialect.ddlGenerator().renameSequence("appdb", "SEQ_EMP", "SEQ_STAFF")) + .contains("ALTER TABLE `appdb`.`SEQ_EMP` RENAME TO `SEQ_STAFF`"); + } + + @Test + void renameSequence_10_4_34_unsupported_below_the_10_5_2_threshold() { + assertThat(dialect104.ddlGenerator().supportsRenameSequence()).isFalse(); + assertThat(dialect104.ddlGenerator().renameSequence("appdb", "SEQ_EMP", "SEQ_STAFF")).isEmpty(); + } + + @Test + void renameSequence_10_6_5_uses_ALTER_TABLE_RENAME_TO() { + assertThat(dialect106.ddlGenerator().supportsRenameSequence()).isTrue(); + assertThat(dialect106.ddlGenerator().renameSequence("appdb", "SEQ_EMP", "SEQ_STAFF")) + .contains("ALTER TABLE `appdb`.`SEQ_EMP` RENAME TO `SEQ_STAFF`"); + } } diff --git a/dialect/db/mssqlserver/src/main/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/MicrosoftSqlServerDialect.java b/dialect/db/mssqlserver/src/main/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/MicrosoftSqlServerDialect.java index 7cf672f..e97ba1f 100644 --- a/dialect/db/mssqlserver/src/main/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/MicrosoftSqlServerDialect.java +++ b/dialect/db/mssqlserver/src/main/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/MicrosoftSqlServerDialect.java @@ -629,18 +629,27 @@ public String alterColumnDropDefault(TableReference table, String columnName) { @Override public String renameColumn(TableReference table, String oldName, String newName) { + if (!supportsRenameColumn()) { + return null; + } return spRename(qualified(table) + "." + quoteIdentifier(oldName), newName, "COLUMN"); } /** SQL Server: {@code EXEC sp_rename 'schema.table', 'new'}. */ @Override public String renameTable(TableReference table, String newName) { + if (!supportsRenameTable()) { + return null; + } return spRename(qualified(table), newName, null); } /** SQL Server: {@code EXEC sp_rename 'schema.table.idx', 'new', 'INDEX'}. */ @Override public String renameIndex(String oldName, String newName, TableReference table) { + if (!supportsRenameIndex()) { + return null; + } if (table == null) { throw new IllegalArgumentException("table must not be null for SQL Server sp_rename INDEX"); } @@ -653,6 +662,9 @@ public String renameIndex(String oldName, String newName, TableReference table) */ @Override public String renameConstraint(TableReference table, String oldName, String newName) { + if (!supportsRenameConstraint()) { + return null; + } return spRename(qualified(table) + "." + quoteIdentifier(oldName), newName, "OBJECT"); } @@ -670,6 +682,22 @@ private String spRename(String quotedSourcePath, String newName, String objectTy return sb.toString(); } + /** + * SQL Server: {@code sp_rename} technically renames a view, but it does not + * update the view's stored definition text or any dependent objects — + * treated as unsupported here rather than silently producing a broken view. + */ + @Override + public boolean supportsRenameView() { + return false; + } + + /** SQL Server: {@code EXEC sp_rename 'schema.seq', 'new'}. */ + @Override + public boolean supportsRenameSequence() { + return true; + } + /** SQL Server memberships are {@code ALTER ROLE}; there is no ADMIN OPTION — the flag is ignored. */ @Override public String grantRole(String roleName, String grantee, boolean withAdminOption) { @@ -699,4 +727,13 @@ public String revokeExecute(String schemaName, String routineName, boolean isFun return "REVOKE EXECUTE ON " + qualifiedRoutine(schemaName, routineName) + " FROM " + quoteIdentifier(grantee); } + + /** SQL Server: {@code EXEC sp_rename 'schema.seq', 'new'}. */ + @Override + public Optional renameSequence(String schemaName, String name, String newName) { + if (!supportsSequences() || !supportsRenameSequence()) { + return Optional.empty(); + } + return Optional.of(spRename(quoteIdentifier(schemaName, name), newName, "OBJECT")); + } } diff --git a/dialect/db/mysql/src/main/java/org/eclipse/daanse/sql/dialect/db/mysql/MySqlDialect.java b/dialect/db/mysql/src/main/java/org/eclipse/daanse/sql/dialect/db/mysql/MySqlDialect.java index 2a354d8..afb6c0b 100644 --- a/dialect/db/mysql/src/main/java/org/eclipse/daanse/sql/dialect/db/mysql/MySqlDialect.java +++ b/dialect/db/mysql/src/main/java/org/eclipse/daanse/sql/dialect/db/mysql/MySqlDialect.java @@ -621,12 +621,65 @@ public String alterColumnSetNullability(TableReference table, String columnName, // SET DEFAULT / DROP DEFAULT inherit the SQL-99 default — MySQL 8.0+ accepts // it. + /** + * MySQL 8.0+. Overridden by {@code MariaDBDialect}: its native rename landed + * at 10.5.2, not comparable to MySQL's 8.0 cut since MariaDB's own + * major/minor numbering (10.x/11.x) needs its own gate. + */ + protected boolean supportsNativeRenameColumn() { + return dialectVersion.isUnknownOrAtLeast(8, 0); + } + + /** + * MySQL 8.0+: ANSI {@code RENAME COLUMN}. Before 8.0, null — + * {@code supportsRenameColumn()} stays true because the capability is + * expressed through the {@code currentMeta} overload's {@code CHANGE} + * fallback instead. + */ + @Override + public String renameColumn(TableReference table, String oldName, String newName) { + if (!supportsRenameColumn() || !supportsNativeRenameColumn()) { + return null; + } + return new StringBuilder("ALTER TABLE ").append(qualified(table)).append(" RENAME COLUMN ") + .append(quoteIdentifier(oldName)).append(" TO ").append(quoteIdentifier(newName)).toString(); + } + + /** + * MySQL 8.0+: delegates to the metadata-free form. Before 8.0: + * {@code ALTER TABLE t CHANGE old new } — needs + * {@code currentMeta} to restate the column's full definition. + */ + @Override + public String renameColumn(TableReference table, String oldName, String newName, ColumnMetaData currentMeta) { + if (!supportsRenameColumn()) { + return null; + } + if (supportsNativeRenameColumn()) { + return renameColumn(table, oldName, newName); + } + if (currentMeta == null) { + throw new IllegalArgumentException("currentMeta must not be null for the CHANGE-based rename fallback"); + } + StringBuilder sb = new StringBuilder("ALTER TABLE ").append(qualified(table)).append(" CHANGE ") + .append(quoteIdentifier(oldName)).append(' ').append(quoteIdentifier(newName)).append(' ') + .append(nativeType(currentMeta)); + if (currentMeta.nullability() == ColumnMetaData.Nullability.NO_NULLS) { + sb.append(" NOT NULL"); + } + currentMeta.columnDefault().ifPresent(d -> sb.append(" DEFAULT ").append(d)); + return sb.toString(); + } + /** * MySQL: {@code ALTER TABLE x RENAME INDEX old TO new} — index names are * table-scoped. */ @Override public String renameIndex(String oldName, String newName, TableReference table) { + if (!supportsRenameIndex()) { + return null; + } if (table == null) { throw new IllegalArgumentException("table must not be null for MySQL RENAME INDEX"); } @@ -640,6 +693,43 @@ public String renameConstraint(TableReference table, String oldName, String newN return null; } + @Override + public boolean supportsRenameConstraint() { + return false; + } + + /** MySQL/MariaDB rename views with RENAME TABLE. */ + @Override + public String renameView(TableReference view, String newName) { + if (!supportsRenameView()) { + return null; + } + return new StringBuilder("RENAME TABLE ").append(qualified(view)).append(" TO ") + .append(quoteIdentifier(newName)).toString(); + } + + /** {@code RENAME TABLE a TO b, c TO d} — one atomic statement (table swap). */ + @Override + public List renameTables(List renames) { + if (renames == null || renames.isEmpty()) { + return List.of(); + } + StringBuilder sb = new StringBuilder("RENAME TABLE "); + for (int i = 0; i < renames.size(); i++) { + if (i > 0) { + sb.append(", "); + } + TableRename r = renames.get(i); + sb.append(qualified(r.table())).append(" TO ").append(quoteIdentifier(r.newName())); + } + return List.of(sb.toString()); + } + + @Override + public boolean supportsAtomicMultiRenameTable() { + return true; + } + private static org.eclipse.daanse.sql.dialect.api.DialectInitData initDataFor(java.sql.Connection c) { try { return org.eclipse.daanse.sql.dialect.api.DialectInitData.fromConnection(c); diff --git a/dialect/db/mysql/src/test/java/org/eclipse/daanse/sql/dialect/db/mysql/sqlgen/MySqlAlterRenameOfflineTest.java b/dialect/db/mysql/src/test/java/org/eclipse/daanse/sql/dialect/db/mysql/sqlgen/MySqlAlterRenameOfflineTest.java index e3953a4..8e5e7b2 100644 --- a/dialect/db/mysql/src/test/java/org/eclipse/daanse/sql/dialect/db/mysql/sqlgen/MySqlAlterRenameOfflineTest.java +++ b/dialect/db/mysql/src/test/java/org/eclipse/daanse/sql/dialect/db/mysql/sqlgen/MySqlAlterRenameOfflineTest.java @@ -13,12 +13,15 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.sql.JDBCType; +import java.util.List; import java.util.Optional; import java.util.OptionalInt; import org.eclipse.daanse.sql.model.schema.ColumnMetaData; import org.eclipse.daanse.sql.model.schema.SchemaReference; import org.eclipse.daanse.sql.model.schema.TableReference; +import org.eclipse.daanse.sql.dialect.api.DialectInitData; +import org.eclipse.daanse.sql.dialect.api.generator.DdlGenerator.TableRename; import org.eclipse.daanse.sql.dialect.db.mysql.MySqlDialect; import org.eclipse.daanse.sql.jdbc.record.schema.ColumnMetaDataRecord; import org.junit.jupiter.api.Test; @@ -31,9 +34,21 @@ class MySqlAlterRenameOfflineTest { private static final SchemaReference S = new SchemaReference(Optional.empty(), "appdb"); private static final TableReference T = new TableReference(Optional.of(S), "EMPLOYEES", TableReference.TYPE_TABLE); + private static final TableReference V = new TableReference(Optional.of(S), "V_EMP", TableReference.TYPE_VIEW); + private static final TableReference TBL_A = new TableReference(Optional.of(S), "A", TableReference.TYPE_TABLE); + private static final TableReference TBL_B = new TableReference(Optional.of(S), "B", TableReference.TYPE_TABLE); + private static final TableReference TBL_TMP = new TableReference(Optional.of(S), "TMP", TableReference.TYPE_TABLE); private final MySqlDialect dialect = new MySqlDialect(); + /** MySQL 8.0+ — the ANSI {@code RENAME COLUMN}/{@code RENAME TABLE} path. */ + private final MySqlDialect dialect80 = new MySqlDialect( + DialectInitData.ansiDefaults().withQuoteIdentifierString("`").withVersion(8, 0)); + + /** Pre-8.0 MySQL — the {@code CHANGE}-based rename fallback. */ + private final MySqlDialect dialect57 = new MySqlDialect( + DialectInitData.ansiDefaults().withQuoteIdentifierString("`").withVersion(5, 7)); + private static ColumnMetaData meta(JDBCType jdbc, OptionalInt size, ColumnMetaData.Nullability n) { return new ColumnMetaDataRecord(jdbc, jdbc.getName(), size, OptionalInt.empty(), OptionalInt.empty(), n, OptionalInt.empty(), Optional.empty(), Optional.empty(), ColumnMetaData.AutoIncrement.UNKNOWN, @@ -69,6 +84,7 @@ void renameIndex_uses_ALTER_TABLE_RENAME_INDEX() { @Test void renameConstraint_returns_null() { + assertThat(dialect.ddlGenerator().supportsRenameConstraint()).isFalse(); assertThat(dialect.ddlGenerator().renameConstraint(T, "OLD_FK", "NEW_FK")).isNull(); } @@ -79,4 +95,40 @@ void renameColumn_and_renameTable_inherit_ANSI_form() { assertThat(dialect.ddlGenerator().renameTable(T, "STAFF")) .isEqualTo("ALTER TABLE `appdb`.`EMPLOYEES` RENAME TO `STAFF`"); } + + @Test + void renameView_uses_RENAME_TABLE() { + assertThat(dialect.ddlGenerator().renameView(V, "V_STAFF")) + .isEqualTo("RENAME TABLE `appdb`.`V_EMP` TO `V_STAFF`"); + } + + @Test + void renameTables_swap_emits_one_atomic_RENAME_TABLE_statement() { + assertThat(dialect.ddlGenerator().supportsAtomicMultiRenameTable()).isTrue(); + assertThat(dialect.ddlGenerator().renameTables(List.of( + new TableRename(TBL_A, "TMP"), + new TableRename(TBL_B, "A"), + new TableRename(TBL_TMP, "B")))) + .containsExactly( + "RENAME TABLE `appdb`.`A` TO `TMP`, `appdb`.`B` TO `A`, `appdb`.`TMP` TO `B`"); + } + + @Test + void renameColumn_8_0_uses_ANSI_RENAME_COLUMN() { + assertThat(dialect80.ddlGenerator().renameColumn(T, "OLD", "NEW")) + .isEqualTo("ALTER TABLE `appdb`.`EMPLOYEES` RENAME COLUMN `OLD` TO `NEW`"); + } + + @Test + void renameColumn_pre_8_0_without_metadata_returns_null_but_capability_stays_true() { + assertThat(dialect57.ddlGenerator().supportsRenameColumn()).isTrue(); + assertThat(dialect57.ddlGenerator().renameColumn(T, "OLD", "NEW")).isNull(); + } + + @Test + void renameColumn_pre_8_0_with_metadata_uses_CHANGE() { + ColumnMetaData m = meta(JDBCType.DECIMAL, OptionalInt.of(12), ColumnMetaData.Nullability.NO_NULLS); + assertThat(dialect57.ddlGenerator().renameColumn(T, "OLD", "NEW", m)) + .isEqualTo("ALTER TABLE `appdb`.`EMPLOYEES` CHANGE `OLD` `NEW` DECIMAL(12) NOT NULL"); + } } diff --git a/dialect/db/oracle/src/main/java/org/eclipse/daanse/sql/dialect/db/oracle/OracleDialect.java b/dialect/db/oracle/src/main/java/org/eclipse/daanse/sql/dialect/db/oracle/OracleDialect.java index bf881b6..0dd66fb 100644 --- a/dialect/db/oracle/src/main/java/org/eclipse/daanse/sql/dialect/db/oracle/OracleDialect.java +++ b/dialect/db/oracle/src/main/java/org/eclipse/daanse/sql/dialect/db/oracle/OracleDialect.java @@ -656,6 +656,18 @@ public String alterColumnDropDefault(TableReference table, String columnName) { // RENAME COLUMN/TABLE/INDEX/CONSTRAINT inherit the SQL-99 default. + /** Oracle: {@code ALTER TRIGGER name RENAME TO newname}. */ + @Override + public boolean supportsRenameTrigger() { + return true; + } + + /** Oracle: {@code RENAME oldname TO newname} — also used for views and sequences. */ + @Override + public boolean supportsRenameSequence() { + return true; + } + /** Oracle takes no FUNCTION/PROCEDURE keyword in GRANT EXECUTE. */ @Override public String grantExecute(String schemaName, String routineName, boolean isFunction, String grantee, @@ -674,4 +686,37 @@ public String revokeExecute(String schemaName, String routineName, boolean isFun return "REVOKE EXECUTE ON " + qualifiedRoutine(schemaName, routineName) + " FROM " + quoteIdentifier(grantee); } + + /** + * Oracle renames views with the standalone {@code RENAME old TO new}, which + * only works in the current schema — no qualification of either side. + */ + @Override + public String renameView(TableReference view, String newName) { + if (!supportsRenameView()) { + return null; + } + return new StringBuilder("RENAME ").append(quoteIdentifier(view.name())).append(" TO ") + .append(quoteIdentifier(newName)).toString(); + } + + /** Oracle: {@code ALTER TRIGGER name RENAME TO new} — no table clause. */ + @Override + public String renameTrigger(String triggerName, TableReference table, String newName) { + if (!supportsRenameTrigger()) { + return null; + } + return new StringBuilder("ALTER TRIGGER ").append(quoteIdentifier(triggerName)).append(" RENAME TO ") + .append(quoteIdentifier(newName)).toString(); + } + + /** Oracle: {@code RENAME old TO new} — current schema only. */ + @Override + public Optional renameSequence(String schemaName, String name, String newName) { + if (!supportsSequences() || !supportsRenameSequence()) { + return Optional.empty(); + } + return Optional.of(new StringBuilder("RENAME ").append(quoteIdentifier(name)).append(" TO ") + .append(quoteIdentifier(newName)).toString()); + } } diff --git a/dialect/db/oracle/src/test/java/org/eclipse/daanse/sql/dialect/db/oracle/sqlgen/OracleAlterRenameOfflineTest.java b/dialect/db/oracle/src/test/java/org/eclipse/daanse/sql/dialect/db/oracle/sqlgen/OracleAlterRenameOfflineTest.java index be92674..1965f2e 100644 --- a/dialect/db/oracle/src/test/java/org/eclipse/daanse/sql/dialect/db/oracle/sqlgen/OracleAlterRenameOfflineTest.java +++ b/dialect/db/oracle/src/test/java/org/eclipse/daanse/sql/dialect/db/oracle/sqlgen/OracleAlterRenameOfflineTest.java @@ -30,6 +30,7 @@ class OracleAlterRenameOfflineTest { private static final SchemaReference S = new SchemaReference(Optional.empty(), "HR"); private static final TableReference T = new TableReference(Optional.of(S), "EMPLOYEES", TableReference.TYPE_TABLE); + private static final TableReference V = new TableReference(Optional.of(S), "V_EMP", TableReference.TYPE_VIEW); private final OracleDialect dialect = new OracleDialect(); @@ -76,4 +77,22 @@ void renames_inherit_ANSI_default() { assertThat(dialect.ddlGenerator().renameConstraint(T, "OLD_FK", "NEW_FK")) .isEqualTo("ALTER TABLE \"HR\".\"EMPLOYEES\" RENAME CONSTRAINT \"OLD_FK\" TO \"NEW_FK\""); } + + @Test + void renameView_uses_unqualified_RENAME() { + assertThat(dialect.ddlGenerator().renameView(V, "V_STAFF")) + .isEqualTo("RENAME \"V_EMP\" TO \"V_STAFF\""); + } + + @Test + void renameTrigger_uses_ALTER_TRIGGER_with_no_ON_clause() { + assertThat(dialect.ddlGenerator().renameTrigger("TRG_AUDIT", T, "TRG_LOG")) + .isEqualTo("ALTER TRIGGER \"TRG_AUDIT\" RENAME TO \"TRG_LOG\""); + } + + @Test + void renameSequence_uses_unqualified_RENAME() { + assertThat(dialect.ddlGenerator().renameSequence("HR", "SEQ_EMP", "SEQ_STAFF")) + .contains("RENAME \"SEQ_EMP\" TO \"SEQ_STAFF\""); + } } diff --git a/dialect/db/pom.xml b/dialect/db/pom.xml index 35a7ea1..01af8d3 100644 --- a/dialect/db/pom.xml +++ b/dialect/db/pom.xml @@ -37,5 +37,6 @@ oracle postgresql sqlite + consistency-test diff --git a/dialect/db/postgresql/src/main/java/org/eclipse/daanse/sql/dialect/db/postgresql/PostgreSqlDialect.java b/dialect/db/postgresql/src/main/java/org/eclipse/daanse/sql/dialect/db/postgresql/PostgreSqlDialect.java index 5adc3cf..4afac7f 100644 --- a/dialect/db/postgresql/src/main/java/org/eclipse/daanse/sql/dialect/db/postgresql/PostgreSqlDialect.java +++ b/dialect/db/postgresql/src/main/java/org/eclipse/daanse/sql/dialect/db/postgresql/PostgreSqlDialect.java @@ -242,6 +242,21 @@ public java.util.List dropTriggerOnTable(String triggerName, return java.util.List.of(sb.toString()); } + /** + * PostgreSQL: {@code ALTER TRIGGER name ON table RENAME TO newname} — unlike + * most objects, a trigger rename needs the owning table. + */ + @Override + public boolean supportsRenameTrigger() { + return true; + } + + /** PostgreSQL: {@code ALTER SEQUENCE name RENAME TO newname}. */ + @Override + public boolean supportsRenameSequence() { + return true; + } + @Override public StringBuilder generateOrderByNulls(CharSequence expr, boolean ascending, boolean collateNullsLast) { // Support for "ORDER BY ... NULLS LAST" was introduced in Postgres 8.3. diff --git a/dialect/db/postgresql/src/test/java/org/eclipse/daanse/sql/dialect/db/postgresql/sqlgen/PostgreSqlAlterRenameOfflineTest.java b/dialect/db/postgresql/src/test/java/org/eclipse/daanse/sql/dialect/db/postgresql/sqlgen/PostgreSqlAlterRenameOfflineTest.java index 44c7813..dc7a3dd 100644 --- a/dialect/db/postgresql/src/test/java/org/eclipse/daanse/sql/dialect/db/postgresql/sqlgen/PostgreSqlAlterRenameOfflineTest.java +++ b/dialect/db/postgresql/src/test/java/org/eclipse/daanse/sql/dialect/db/postgresql/sqlgen/PostgreSqlAlterRenameOfflineTest.java @@ -12,12 +12,14 @@ import static org.assertj.core.api.Assertions.assertThat; import java.sql.JDBCType; +import java.util.List; import java.util.Optional; import java.util.OptionalInt; import org.eclipse.daanse.sql.model.schema.ColumnMetaData; import org.eclipse.daanse.sql.model.schema.SchemaReference; import org.eclipse.daanse.sql.model.schema.TableReference; +import org.eclipse.daanse.sql.dialect.api.generator.DdlGenerator.TableRename; import org.eclipse.daanse.sql.dialect.db.postgresql.PostgreSqlDialect; import org.eclipse.daanse.sql.jdbc.record.schema.ColumnMetaDataRecord; import org.junit.jupiter.api.Test; @@ -26,6 +28,8 @@ class PostgreSqlAlterRenameOfflineTest { private static final SchemaReference S = new SchemaReference(Optional.empty(), "PUBLIC"); private static final TableReference T = new TableReference(Optional.of(S), "EMPLOYEES", TableReference.TYPE_TABLE); + private static final TableReference T2 = new TableReference(Optional.of(S), "DEPARTMENTS", TableReference.TYPE_TABLE); + private static final TableReference V = new TableReference(Optional.of(S), "V_EMP", TableReference.TYPE_VIEW); private final PostgreSqlDialect dialect = new PostgreSqlDialect(); @@ -84,4 +88,33 @@ void renameConstraint_emits_RENAME_CONSTRAINT() { assertThat(dialect.ddlGenerator().renameConstraint(T, "OLD_FK", "NEW_FK")) .isEqualTo("ALTER TABLE \"PUBLIC\".\"EMPLOYEES\" RENAME CONSTRAINT \"OLD_FK\" TO \"NEW_FK\""); } + + @Test + void renameView_emits_ALTER_VIEW_RENAME_TO() { + assertThat(dialect.ddlGenerator().renameView(V, "V_STAFF")) + .isEqualTo("ALTER VIEW \"PUBLIC\".\"V_EMP\" RENAME TO \"V_STAFF\""); + } + + @Test + void renameTrigger_emits_ALTER_TRIGGER_ON_table() { + assertThat(dialect.ddlGenerator().renameTrigger("TRG_AUDIT", T, "TRG_LOG")) + .isEqualTo("ALTER TRIGGER \"TRG_AUDIT\" ON \"PUBLIC\".\"EMPLOYEES\" RENAME TO \"TRG_LOG\""); + } + + @Test + void renameSequence_emits_ALTER_SEQUENCE_RENAME_TO() { + assertThat(dialect.ddlGenerator().renameSequence("PUBLIC", "SEQ_EMP", "SEQ_STAFF")) + .contains("ALTER SEQUENCE \"PUBLIC\".\"SEQ_EMP\" RENAME TO \"SEQ_STAFF\""); + } + + @Test + void renameTables_emits_one_ALTER_TABLE_per_step_in_order_not_atomic() { + assertThat(dialect.ddlGenerator().supportsAtomicMultiRenameTable()).isFalse(); + assertThat(dialect.ddlGenerator().renameTables(List.of( + new TableRename(T, "STAFF"), + new TableRename(T2, "DEPTS")))) + .containsExactly( + "ALTER TABLE \"PUBLIC\".\"EMPLOYEES\" RENAME TO \"STAFF\"", + "ALTER TABLE \"PUBLIC\".\"DEPARTMENTS\" RENAME TO \"DEPTS\""); + } } diff --git a/dialect/db/sqlite/src/main/java/org/eclipse/daanse/sql/dialect/db/sqlite/SqliteDialect.java b/dialect/db/sqlite/src/main/java/org/eclipse/daanse/sql/dialect/db/sqlite/SqliteDialect.java index 71b6aff..1fee61a 100644 --- a/dialect/db/sqlite/src/main/java/org/eclipse/daanse/sql/dialect/db/sqlite/SqliteDialect.java +++ b/dialect/db/sqlite/src/main/java/org/eclipse/daanse/sql/dialect/db/sqlite/SqliteDialect.java @@ -87,6 +87,24 @@ public boolean supportsCreateOrReplaceView() { return false; } + /** SQLite has no {@code ALTER INDEX ... RENAME}. */ + @Override + public boolean supportsRenameIndex() { + return false; + } + + /** SQLite can't rename constraints — same reason it can't drop them. */ + @Override + public boolean supportsRenameConstraint() { + return false; + } + + /** SQLite has no {@code ALTER VIEW ... RENAME}. */ + @Override + public boolean supportsRenameView() { + return false; + } + @Override public boolean supportsDropTableCascade() { return false; diff --git a/dialect/db/sqlite/src/test/java/org/eclipse/daanse/sql/dialect/db/sqlite/sqlgen/SqliteAlterRenameOfflineTest.java b/dialect/db/sqlite/src/test/java/org/eclipse/daanse/sql/dialect/db/sqlite/sqlgen/SqliteAlterRenameOfflineTest.java new file mode 100644 index 0000000..d6f5436 --- /dev/null +++ b/dialect/db/sqlite/src/test/java/org/eclipse/daanse/sql/dialect/db/sqlite/sqlgen/SqliteAlterRenameOfflineTest.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.daanse.sql.dialect.db.sqlite.sqlgen; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Optional; + +import org.eclipse.daanse.sql.model.schema.SchemaReference; +import org.eclipse.daanse.sql.model.schema.TableReference; +import org.eclipse.daanse.sql.dialect.db.sqlite.SqliteDialect; +import org.junit.jupiter.api.Test; + +/** + * SQLite gained {@code ALTER TABLE ... RENAME TO} and + * {@code ALTER TABLE ... RENAME COLUMN} in 3.25 — the SQL-99 default already + * renders both correctly, so no override is needed. Index, constraint and + * view rename have no SQLite equivalent at all. + */ +class SqliteAlterRenameOfflineTest { + + private static final SchemaReference S = new SchemaReference(Optional.empty(), "PUBLIC"); + private static final TableReference T = new TableReference(Optional.of(S), "EMPLOYEES", TableReference.TYPE_TABLE); + private static final TableReference V = new TableReference(Optional.of(S), "V_EMP", TableReference.TYPE_VIEW); + + private final SqliteDialect dialect = new SqliteDialect(); + + @Test + void renameTable_inherits_ANSI_default() { + assertThat(dialect.ddlGenerator().supportsRenameTable()).isTrue(); + assertThat(dialect.ddlGenerator().renameTable(T, "STAFF")) + .isEqualTo("ALTER TABLE \"PUBLIC\".\"EMPLOYEES\" RENAME TO \"STAFF\""); + } + + @Test + void renameColumn_inherits_ANSI_default() { + assertThat(dialect.ddlGenerator().supportsRenameColumn()).isTrue(); + assertThat(dialect.ddlGenerator().renameColumn(T, "OLD", "NEW")) + .isEqualTo("ALTER TABLE \"PUBLIC\".\"EMPLOYEES\" RENAME COLUMN \"OLD\" TO \"NEW\""); + } + + @Test + void renameIndex_unsupported() { + assertThat(dialect.ddlGenerator().supportsRenameIndex()).isFalse(); + assertThat(dialect.ddlGenerator().renameIndex("IDX_OLD", "IDX_NEW", T)).isNull(); + } + + @Test + void renameConstraint_unsupported() { + assertThat(dialect.ddlGenerator().supportsRenameConstraint()).isFalse(); + assertThat(dialect.ddlGenerator().renameConstraint(T, "OLD_FK", "NEW_FK")).isNull(); + } + + @Test + void renameView_unsupported() { + assertThat(dialect.ddlGenerator().supportsRenameView()).isFalse(); + assertThat(dialect.ddlGenerator().renameView(V, "V_STAFF")).isNull(); + } +} diff --git a/dialect/db/test-support/src/main/java/org/eclipse/daanse/sql/dialect/db/testsupport/GeneratorTestSupport.java b/dialect/db/test-support/src/main/java/org/eclipse/daanse/sql/dialect/db/testsupport/GeneratorTestSupport.java index 3f44f27..9f858ee 100644 --- a/dialect/db/test-support/src/main/java/org/eclipse/daanse/sql/dialect/db/testsupport/GeneratorTestSupport.java +++ b/dialect/db/test-support/src/main/java/org/eclipse/daanse/sql/dialect/db/testsupport/GeneratorTestSupport.java @@ -9,9 +9,12 @@ */ package org.eclipse.daanse.sql.dialect.db.testsupport; +import java.sql.JDBCType; import java.util.List; import java.util.Optional; +import java.util.OptionalInt; +import org.eclipse.daanse.sql.model.schema.ColumnMetaData; import org.eclipse.daanse.sql.model.schema.SchemaReference; import org.eclipse.daanse.sql.model.schema.TableReference; import org.eclipse.daanse.sql.dialect.api.generator.MergeGenerator; @@ -59,4 +62,18 @@ public static MergeGenerator.UpsertSpec upsertSpecDoNothing(TableReference targe } return new MergeGenerator.UpsertSpec(target, List.of(columns[0]), List.of(columns), List.of()); } + + /** A minimal {@link ColumnMetaData} for tests that just need some valid metadata. */ + public static ColumnMetaData columnMeta(JDBCType jdbc, OptionalInt size, ColumnMetaData.Nullability nullability) { + return new SimpleColumnMetaData(jdbc, jdbc.getName(), size, OptionalInt.empty(), OptionalInt.empty(), + nullability, OptionalInt.empty(), Optional.empty(), Optional.empty(), + ColumnMetaData.AutoIncrement.UNKNOWN, ColumnMetaData.GeneratedColumn.UNKNOWN); + } + + private record SimpleColumnMetaData(JDBCType dataType, String typeName, OptionalInt columnSize, + OptionalInt decimalDigits, OptionalInt numPrecRadix, ColumnMetaData.Nullability nullability, + OptionalInt charOctetLength, Optional remarks, Optional columnDefault, + ColumnMetaData.AutoIncrement autoIncrement, ColumnMetaData.GeneratedColumn generatedColumn) + implements ColumnMetaData { + } }