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 499960b..11c10e4 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 @@ -78,7 +78,10 @@ default String createTable(TableReference table, List columns, cd.columnMetaData().columnDefault().ifPresent(d -> sb.append(" DEFAULT ").append(d)); } if (primaryKey != null && !primaryKey.columns().isEmpty()) { - sb.append(",\n PRIMARY KEY ("); + sb.append(",\n "); + primaryKey.constraintName().filter(n -> supportsNamedPrimaryKey()) + .ifPresent(n -> sb.append("CONSTRAINT ").append(quoteIdentifier(n)).append(' ')); + sb.append("PRIMARY KEY ("); sb.append(String.join(", ", primaryKey.columns().stream().map(c -> quoteIdentifier(c.name()).toString()).toList())); sb.append(")"); @@ -307,11 +310,19 @@ default String renameTable(TableReference table, String newName) { .append(quoteIdentifier(newName)).toString(); } + /** + * {@code ALTER INDEX schema.old RENAME TO new}. Index names are schema-scoped + * (PostgreSQL, H2, Oracle), so the old name is qualified with the table's + * schema — unqualified it would only be found on the search path. + */ default String renameIndex(String oldName, String newName, TableReference table) { if (!supportsRenameIndex()) { return null; } - return new StringBuilder("ALTER INDEX ").append(quoteIdentifier(oldName)).append(" RENAME TO ") + String old = table != null && table.schema().isPresent() + ? quoteIdentifier(table.schema().get().name(), oldName).toString() + : quoteIdentifier(oldName); + return new StringBuilder("ALTER INDEX ").append(old).append(" RENAME TO ") .append(quoteIdentifier(newName)).toString(); } @@ -648,6 +659,38 @@ default Optional dropProcedure(String procedureName, String schemaName, return Optional.empty(); } + // -------------------- comments -------------------- + + /** + * {@code COMMENT ON TABLE schema.table IS '…'}; a {@code null} comment + * removes it ({@code IS NULL}). + * + * @return empty when the dialect cannot store table comments + */ + default Optional commentOnTable(TableReference table, String comment) { + return Optional.of("COMMENT ON TABLE " + qualified(table) + " IS " + commentLiteral(comment)); + } + + /** + * {@code COMMENT ON COLUMN schema.table.column IS '…'}; a {@code null} + * comment removes it. + * + * @param currentMeta the column's current definition — required by dialects + * that can only set a column comment by restating the + * whole column (MySQL/MariaDB); may be {@code null} otherwise + * @return empty when the dialect cannot store column comments + */ + default Optional commentOnColumn(TableReference table, String columnName, String comment, + ColumnMetaData currentMeta) { + return Optional.of("COMMENT ON COLUMN " + qualified(table) + "." + quoteIdentifier(columnName) + " IS " + + commentLiteral(comment)); + } + + /** ANSI string literal for a comment ({@code '} doubled), or {@code NULL}. */ + private static String commentLiteral(String comment) { + return comment == null ? "NULL" : "'" + comment.replace("'", "''") + "'"; + } + default java.util.List dropTriggerOnTable(String triggerName, TableReference table, boolean ifExists) { return java.util.List.of(dropTrigger(triggerName, ifExists)); } @@ -1000,6 +1043,15 @@ default boolean supportsRenameConstraint() { return true; } + /** + * @return true if {@code createTable} may name the primary key + * ({@code CONSTRAINT name PRIMARY KEY (…)}); without it the + * database picks the name and a later rename or drop by name misses + */ + default boolean supportsNamedPrimaryKey() { + 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}, diff --git a/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/PaginationGenerator.java b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/PaginationGenerator.java index d06b774..3e18d66 100644 --- a/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/PaginationGenerator.java +++ b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/PaginationGenerator.java @@ -9,6 +9,7 @@ */ package org.eclipse.daanse.sql.dialect.api.generator; +import java.util.List; import java.util.Optional; import java.util.OptionalLong; @@ -61,4 +62,29 @@ default boolean supportsOffset() { default Optional paginate(long limit) { return Optional.of(paginate(OptionalLong.of(limit), OptionalLong.empty())); } + + /** Paging clause with placeholders; the returned order lists which parameter comes first. */ + default PreparedPaging paginatePrepared(boolean withLimit, boolean withOffset) { + if (withLimit && withOffset) { + return new PreparedPaging( + "OFFSET ? ROWS FETCH NEXT ? ROWS ONLY", + List.of(PagingParam.OFFSET, PagingParam.LIMIT) + ); + } else if (withLimit) { + return new PreparedPaging( + "FETCH NEXT ? ROWS ONLY", + List.of(PagingParam.LIMIT) + ); + } else if (withOffset) { + return new PreparedPaging( + "OFFSET ? ROWS", + List.of(PagingParam.OFFSET) + ); + } else { + return new PreparedPaging("", List.of()); + } + } + + record PreparedPaging(String clause, List order) {} + enum PagingParam { LIMIT, OFFSET } } 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 ea4e8eb..8df4577 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 @@ -296,4 +296,28 @@ public boolean supportsAtomicMultiRenameTable() { return true; } + /** {@code ALTER TABLE t MODIFY COMMENT '…'}; {@code null} clears it. */ + @Override + public java.util.Optional commentOnTable(TableReference table, String comment) { + return java.util.Optional.of("ALTER TABLE " + qualified(table) + " MODIFY COMMENT " + chLiteral(comment)); + } + + /** {@code ALTER TABLE t COMMENT COLUMN c '…'}; {@code null} clears it. */ + @Override + public java.util.Optional commentOnColumn(TableReference table, String columnName, String comment, + org.eclipse.daanse.sql.model.schema.ColumnMetaData currentMeta) { + return java.util.Optional.of("ALTER TABLE " + qualified(table) + " COMMENT COLUMN " + + quoteIdentifier(columnName) + " " + chLiteral(comment)); + } + + private static String chLiteral(String comment) { + String s = comment == null ? "" : comment; + return "'" + s.replace("\\", "\\\\").replace("'", "\\'") + "'"; + } + + /** ClickHouse has no named table constraints; the primary key stays unnamed. */ + @Override + public boolean supportsNamedPrimaryKey() { + return false; + } } diff --git a/dialect/db/clickhouse/src/test/java/org/eclipse/daanse/sql/dialect/db/clickhouse/ClickHouseDialectCommentTest.java b/dialect/db/clickhouse/src/test/java/org/eclipse/daanse/sql/dialect/db/clickhouse/ClickHouseDialectCommentTest.java new file mode 100644 index 0000000..936c559 --- /dev/null +++ b/dialect/db/clickhouse/src/test/java/org/eclipse/daanse/sql/dialect/db/clickhouse/ClickHouseDialectCommentTest.java @@ -0,0 +1,31 @@ +/* + * 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; + +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.junit.jupiter.api.Test; + +class ClickHouseDialectCommentTest { + + private final ClickHouseDialect d = new ClickHouseDialect(); + private final TableReference t = new TableReference(Optional.of(new SchemaReference("s")), "t", + TableReference.TYPE_TABLE); + + @Test + void comments() { + assertThat(d.commentOnTable(t, "it's").orElseThrow()).endsWith(" MODIFY COMMENT 'it\\'s'"); + assertThat(d.commentOnColumn(t, "c", "x", null).orElseThrow()).endsWith(" COMMENT COLUMN \"c\" 'x'"); + } +} diff --git a/dialect/db/clickhouse/src/test/java/org/eclipse/daanse/sql/dialect/db/clickhouse/ClickHouseDialectTest.java b/dialect/db/clickhouse/src/test/java/org/eclipse/daanse/sql/dialect/db/clickhouse/ClickHouseDialectTest.java index 8547891..e9ee7f6 100644 --- a/dialect/db/clickhouse/src/test/java/org/eclipse/daanse/sql/dialect/db/clickhouse/ClickHouseDialectTest.java +++ b/dialect/db/clickhouse/src/test/java/org/eclipse/daanse/sql/dialect/db/clickhouse/ClickHouseDialectTest.java @@ -166,4 +166,9 @@ void testGenerateListAgg() { assertEquals("groupArrayArray( column1)", result); } } + + @Test + void primaryKeyStaysUnnamed() { + assertFalse(dialect.supportsNamedPrimaryKey()); + } } 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 9fba3b0..1b33775 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 @@ -213,4 +213,15 @@ public String renameIndex(String oldName, String newName, TableReference table) .append(quoteIdentifier(newName)).toString(); } + /** Derby cannot store comments on tables or columns. */ + @Override + public java.util.Optional commentOnTable(TableReference table, String comment) { + return java.util.Optional.empty(); + } + + @Override + public java.util.Optional commentOnColumn(TableReference table, String columnName, String comment, + org.eclipse.daanse.sql.model.schema.ColumnMetaData currentMeta) { + return java.util.Optional.empty(); + } } diff --git a/dialect/db/derby/src/test/java/org/eclipse/daanse/sql/dialect/db/derby/DerbyDialectCommentTest.java b/dialect/db/derby/src/test/java/org/eclipse/daanse/sql/dialect/db/derby/DerbyDialectCommentTest.java new file mode 100644 index 0000000..e935d70 --- /dev/null +++ b/dialect/db/derby/src/test/java/org/eclipse/daanse/sql/dialect/db/derby/DerbyDialectCommentTest.java @@ -0,0 +1,31 @@ +/* + * 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; + +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.junit.jupiter.api.Test; + +class DerbyDialectCommentTest { + + private final DerbyDialect d = new DerbyDialect(); + private final TableReference t = new TableReference(Optional.of(new SchemaReference("s")), "t", + TableReference.TYPE_TABLE); + + @Test + void comments() { + assertThat(d.commentOnTable(t, "x")).isEmpty(); + assertThat(d.commentOnColumn(t, "c", "x", null)).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 e20049e..ddc1749 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 @@ -18,6 +18,8 @@ import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; +import org.eclipse.daanse.sql.dialect.api.generator.PaginationGenerator.PagingParam; +import org.eclipse.daanse.sql.dialect.api.generator.PaginationGenerator.PreparedPaging; import org.eclipse.daanse.sql.dialect.db.common.AbstractJdbcDialect; import org.eclipse.daanse.sql.dialect.db.common.DialectUtil; import org.eclipse.daanse.sql.model.sql.BitOperation; @@ -251,6 +253,28 @@ public String paginate(java.util.OptionalLong limit, java.util.OptionalLong offs }); return sb.toString(); } + + @Override + public PreparedPaging paginatePrepared(boolean withLimit, boolean withOffset) { + if (withLimit && withOffset) { + return new PreparedPaging( + "LIMIT ? OFFSET ?", + List.of(PagingParam.LIMIT, PagingParam.OFFSET) + ); + } else if (withLimit) { + return new PreparedPaging( + "LIMIT ?", + List.of(PagingParam.LIMIT) + ); + } else if (withOffset) { + return new PreparedPaging( + "OFFSET ?", + List.of(PagingParam.OFFSET) + ); + } else { + return new PreparedPaging("", List.of()); + } + } }; cachedPaginationGenerator = local; return local; diff --git a/dialect/db/h2/src/main/java/org/eclipse/daanse/sql/dialect/db/h2/H2Dialect.java b/dialect/db/h2/src/main/java/org/eclipse/daanse/sql/dialect/db/h2/H2Dialect.java index 98815c7..80ed811 100644 --- a/dialect/db/h2/src/main/java/org/eclipse/daanse/sql/dialect/db/h2/H2Dialect.java +++ b/dialect/db/h2/src/main/java/org/eclipse/daanse/sql/dialect/db/h2/H2Dialect.java @@ -18,12 +18,15 @@ import org.eclipse.daanse.sql.model.sql.BitOperation; import org.eclipse.daanse.sql.model.sql.OrderedColumn; +import org.eclipse.daanse.sql.dialect.api.generator.PaginationGenerator.PagingParam; +import org.eclipse.daanse.sql.dialect.api.generator.PaginationGenerator.PreparedPaging; import org.eclipse.daanse.sql.dialect.db.common.AbstractJdbcDialect; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class H2Dialect extends AbstractJdbcDialect { + private volatile org.eclipse.daanse.sql.dialect.api.generator.PaginationGenerator cachedPaginationGenerator; private static final Logger LOGGER = LoggerFactory.getLogger(H2Dialect.class); private static final String SUPPORTED_PRODUCT_NAME = "H2"; @@ -221,4 +224,59 @@ public boolean supportsListAgg() { public String grantRole(String roleName, String grantee, boolean withAdminOption) { return "GRANT " + quoteIdentifier(roleName) + " TO " + quoteIdentifier(grantee); } + + @Override + public org.eclipse.daanse.sql.dialect.api.generator.PaginationGenerator paginationGenerator() { + var local = cachedPaginationGenerator; + if (local != null) + return local; + local = new org.eclipse.daanse.sql.dialect.api.generator.PaginationGenerator() { + @Override + public String paginate(java.util.OptionalLong limit, java.util.OptionalLong offset) { + if (limit.isEmpty() && offset.isEmpty()) + return ""; + StringBuilder sb = new StringBuilder(); + if (limit.isPresent()) { + long l = limit.getAsLong(); + if (l < 0) + throw new IllegalArgumentException("limit must be >= 0"); + sb.append(" LIMIT ").append(l); + } else { + sb.append(" LIMIT -1"); + } + if (offset.isPresent()) { + long o = offset.getAsLong(); + if (o < 0) + throw new IllegalArgumentException("offset must be >= 0"); + sb.append(" OFFSET ").append(o); + } + return sb.toString(); + } + + @Override + public PreparedPaging paginatePrepared(boolean withLimit, boolean withOffset) { + if (withLimit && withOffset) { + return new PreparedPaging( + "LIMIT ? OFFSET ?", + List.of(PagingParam.LIMIT, PagingParam.OFFSET) + ); + } else if (withLimit) { + return new PreparedPaging( + "LIMIT ?", + List.of(PagingParam.LIMIT) + ); + } else if (withOffset) { + return new PreparedPaging( + "OFFSET ?", + List.of(PagingParam.OFFSET) + ); + } else { + return new PreparedPaging("", List.of()); + } + } + }; + cachedPaginationGenerator = local; + return local; + } + } 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 index b8acb62..2bd49d0 100644 --- 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 @@ -47,7 +47,7 @@ void renameColumn_uses_ANSI_default() { @Test void renameIndex_uses_ANSI_default() { assertThat(dialect.ddlGenerator().renameIndex("IDX_OLD", "IDX_NEW", T)) - .isEqualTo("ALTER INDEX \"IDX_OLD\" RENAME TO \"IDX_NEW\""); + .isEqualTo("ALTER INDEX \"PUBLIC\".\"IDX_OLD\" RENAME TO \"IDX_NEW\""); } @Test diff --git a/dialect/db/hive/src/main/java/org/eclipse/daanse/sql/dialect/db/hive/HiveDialect.java b/dialect/db/hive/src/main/java/org/eclipse/daanse/sql/dialect/db/hive/HiveDialect.java index b2b940a..6b3924b 100644 --- a/dialect/db/hive/src/main/java/org/eclipse/daanse/sql/dialect/db/hive/HiveDialect.java +++ b/dialect/db/hive/src/main/java/org/eclipse/daanse/sql/dialect/db/hive/HiveDialect.java @@ -13,6 +13,7 @@ */ package org.eclipse.daanse.sql.dialect.db.hive; +import org.eclipse.daanse.sql.model.schema.TableReference; import java.sql.Date; import java.sql.Timestamp; import java.util.List; @@ -120,4 +121,27 @@ public String name() { return SUPPORTED_PRODUCT_NAME.toLowerCase(); } + /** {@code ALTER TABLE t SET TBLPROPERTIES ('comment' = '…')}; {@code null} clears it. */ + @Override + public java.util.Optional commentOnTable(TableReference table, String comment) { + String s = comment == null ? "" : comment; + return java.util.Optional.of("ALTER TABLE " + qualified(table) + " SET TBLPROPERTIES ('comment' = '" + + s.replace("\\", "\\\\").replace("'", "\\'") + "')"); + } + + /** + * Hive can only change a column comment through {@code CHANGE COLUMN} with + * the full column type — not supported here. + */ + @Override + public java.util.Optional commentOnColumn(TableReference table, String columnName, String comment, + org.eclipse.daanse.sql.model.schema.ColumnMetaData currentMeta) { + return java.util.Optional.empty(); + } + + /** Hive has no named table constraints; the primary key stays unnamed. */ + @Override + public boolean supportsNamedPrimaryKey() { + return false; + } } diff --git a/dialect/db/hive/src/test/java/org/eclipse/daanse/sql/dialect/db/hive/HiveDialectCommentTest.java b/dialect/db/hive/src/test/java/org/eclipse/daanse/sql/dialect/db/hive/HiveDialectCommentTest.java new file mode 100644 index 0000000..4531dd2 --- /dev/null +++ b/dialect/db/hive/src/test/java/org/eclipse/daanse/sql/dialect/db/hive/HiveDialectCommentTest.java @@ -0,0 +1,31 @@ +/* + * 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.hive; + +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.junit.jupiter.api.Test; + +class HiveDialectCommentTest { + + private final HiveDialect d = new HiveDialect(); + private final TableReference t = new TableReference(Optional.of(new SchemaReference("s")), "t", + TableReference.TYPE_TABLE); + + @Test + void comments() { + assertThat(d.commentOnTable(t, "x").orElseThrow()).endsWith(" SET TBLPROPERTIES ('comment' = 'x')"); + assertThat(d.commentOnColumn(t, "c", "x", null)).isEmpty(); + } +} diff --git a/dialect/db/mariadb/src/test/java/org/eclipse/daanse/sql/dialect/db/mariadb/MariaDBGeneratorsRoundTripTest.java b/dialect/db/mariadb/src/test/java/org/eclipse/daanse/sql/dialect/db/mariadb/MariaDBGeneratorsRoundTripTest.java index f8b1bba..c680c71 100644 --- a/dialect/db/mariadb/src/test/java/org/eclipse/daanse/sql/dialect/db/mariadb/MariaDBGeneratorsRoundTripTest.java +++ b/dialect/db/mariadb/src/test/java/org/eclipse/daanse/sql/dialect/db/mariadb/MariaDBGeneratorsRoundTripTest.java @@ -14,6 +14,7 @@ import static org.eclipse.daanse.sql.dialect.db.testsupport.RoundTripAssertions.assertFirstIntEquals; import static org.eclipse.daanse.sql.dialect.db.testsupport.RoundTripAssertions.assertFirstStringEquals; import static org.eclipse.daanse.sql.dialect.db.testsupport.RoundTripAssertions.assertSelectIdRowCountAndFirst; +import static org.eclipse.daanse.sql.dialect.db.testsupport.RoundTripAssertions.assertSelectIdRowCountAndFirstWithParams; import java.sql.Connection; import java.sql.DriverManager; @@ -27,6 +28,7 @@ import org.eclipse.daanse.sql.model.schema.TableReference; import org.eclipse.daanse.sql.dialect.api.DialectInitData; import org.eclipse.daanse.sql.dialect.api.generator.MergeGenerator; +import org.eclipse.daanse.sql.dialect.api.generator.PaginationGenerator.PreparedPaging; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -77,6 +79,19 @@ void pagination_limit_offset_executes() throws SQLException { assertSelectIdRowCountAndFirst(conn, "SELECT id FROM `rt`.`users` ORDER BY id" + tail, 2, 2); } + @Test + void pagination_limit_offset_executesPlaceholders() throws SQLException { + PreparedPaging preparedPaging = dialect.paginationGenerator().paginatePrepared(false, false); + assertThat(preparedPaging.clause()).isEqualTo(""); + preparedPaging = dialect.paginationGenerator().paginatePrepared(true, false); + assertThat(preparedPaging.clause()).isEqualTo("LIMIT ?"); + preparedPaging = dialect.paginationGenerator().paginatePrepared(false, true); + assertThat(preparedPaging.clause()).isEqualTo("OFFSET ?"); + preparedPaging = dialect.paginationGenerator().paginatePrepared(true, true); + assertThat(preparedPaging.clause()).isEqualTo("LIMIT ? OFFSET ?"); + assertSelectIdRowCountAndFirstWithParams(conn, "SELECT id FROM `rt`.`users` ORDER BY id" + preparedPaging.clause(), List.of(2, 1), 2, 2); + } + @Test void returning_clause_returns_inserted_id() throws SQLException { // MariaDB 10.5+ supports RETURNING (unlike MySQL); mariadb:11 reports >= 10.5. 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 e97ba1f..83655ed 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 @@ -665,7 +665,11 @@ public String renameConstraint(TableReference table, String oldName, String newN if (!supportsRenameConstraint()) { return null; } - return spRename(qualified(table) + "." + quoteIdentifier(oldName), newName, "OBJECT"); + // constraints are schema-scoped objects: sp_rename wants schema.constraint, not table.constraint + String old = table.schema().isPresent() + ? quoteIdentifier(table.schema().get().name(), oldName).toString() + : quoteIdentifier(oldName); + return spRename(old, newName, "OBJECT"); } /** @@ -736,4 +740,42 @@ public Optional renameSequence(String schemaName, String name, String ne } return Optional.of(spRename(quoteIdentifier(schemaName, name), newName, "OBJECT")); } + + /** + * SQL Server has no COMMENT ON — comments are the {@code MS_Description} + * extended property. Update when present, add otherwise; {@code null} + * drops it. The schema defaults to {@code dbo}. + */ + @Override + public Optional commentOnTable(TableReference table, String comment) { + return Optional.of(msDescription(table, null, comment)); + } + + @Override + public Optional commentOnColumn(TableReference table, String columnName, String comment, + ColumnMetaData currentMeta) { + return Optional.of(msDescription(table, columnName, comment)); + } + + private String msDescription(TableReference table, String columnName, String comment) { + String schema = table.schema().map(s -> s.name()).orElse("dbo"); + String levels = "@level0type = N'SCHEMA', @level0name = " + nLiteral(schema) + + ", @level1type = N'TABLE', @level1name = " + nLiteral(table.name()) + + (columnName == null ? "" : ", @level2type = N'COLUMN', @level2name = " + nLiteral(columnName)); + String object = "OBJECT_ID(" + nLiteral(quoteIdentifier(schema, table.name()).toString()) + ")"; + String minor = columnName == null ? "0" + : "COLUMNPROPERTY(" + object + ", " + nLiteral(columnName) + ", 'ColumnId')"; + String exists = "EXISTS (SELECT 1 FROM sys.extended_properties WHERE major_id = " + object + + " AND minor_id = " + minor + " AND name = N'MS_Description')"; + if (comment == null) { + return "IF " + exists + " EXEC sys.sp_dropextendedproperty @name = N'MS_Description', " + levels; + } + String value = "@name = N'MS_Description', @value = " + nLiteral(comment) + ", " + levels; + return "IF " + exists + " EXEC sys.sp_updateextendedproperty " + value + + " ELSE EXEC sys.sp_addextendedproperty " + value; + } + + private static String nLiteral(String s) { + return "N'" + s.replace("'", "''") + "'"; + } } diff --git a/dialect/db/mssqlserver/src/test/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/MicrosoftSqlServerGeneratorsRoundTripTest.java b/dialect/db/mssqlserver/src/test/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/MicrosoftSqlServerGeneratorsRoundTripTest.java index 4de8882..a72907f 100644 --- a/dialect/db/mssqlserver/src/test/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/MicrosoftSqlServerGeneratorsRoundTripTest.java +++ b/dialect/db/mssqlserver/src/test/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/MicrosoftSqlServerGeneratorsRoundTripTest.java @@ -14,6 +14,7 @@ import static org.eclipse.daanse.sql.dialect.db.testsupport.RoundTripAssertions.assertFirstIntEquals; import static org.eclipse.daanse.sql.dialect.db.testsupport.RoundTripAssertions.assertFirstStringEquals; import static org.eclipse.daanse.sql.dialect.db.testsupport.RoundTripAssertions.assertSelectIdRowCountAndFirst; +import static org.eclipse.daanse.sql.dialect.db.testsupport.RoundTripAssertions.assertSelectIdRowCountAndFirstWithParams; import java.sql.Connection; import java.sql.DriverManager; @@ -27,6 +28,7 @@ import org.eclipse.daanse.sql.model.schema.TableReference; import org.eclipse.daanse.sql.dialect.api.DialectInitData; import org.eclipse.daanse.sql.dialect.api.generator.MergeGenerator; +import org.eclipse.daanse.sql.dialect.api.generator.PaginationGenerator.PreparedPaging; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -76,6 +78,19 @@ void pagination_offset_fetch_next_executes() throws SQLException { assertSelectIdRowCountAndFirst(conn, "SELECT id FROM [dbo].[users] ORDER BY id" + tail, 2, 2); } + @Test + void pagination_limit_offset_executesPlaceholders() throws SQLException { + PreparedPaging preparedPaging = dialect.paginationGenerator().paginatePrepared(false, false); + assertThat(preparedPaging.clause()).isEqualTo(""); + preparedPaging = dialect.paginationGenerator().paginatePrepared(true, false); + assertThat(preparedPaging.clause()).isEqualTo("OFFSET ? ROWS"); + preparedPaging = dialect.paginationGenerator().paginatePrepared(false, true); + assertThat(preparedPaging.clause()).isEqualTo("FETCH NEXT ? ROWS ONLY"); + preparedPaging = dialect.paginationGenerator().paginatePrepared(true, true); + assertThat(preparedPaging.clause()).isEqualTo("OFFSET ? ROWS FETCH NEXT ? ROWS ONLY"); + assertSelectIdRowCountAndFirstWithParams(conn, "SELECT id FROM [dbo].[users] ORDER BY id " + preparedPaging.clause(), List.of(1, 2), 2, 2); + } + @Test void merge_upsert_inserts_then_updates() throws SQLException { MergeGenerator.UpsertSpec spec = new MergeGenerator.UpsertSpec(users, List.of("id"), List.of("id", "name"), diff --git a/dialect/db/mssqlserver/src/test/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/MicrosoftSqlServerGeneratorsTest.java b/dialect/db/mssqlserver/src/test/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/MicrosoftSqlServerGeneratorsTest.java index 74db5cd..d682776 100644 --- a/dialect/db/mssqlserver/src/test/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/MicrosoftSqlServerGeneratorsTest.java +++ b/dialect/db/mssqlserver/src/test/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/MicrosoftSqlServerGeneratorsTest.java @@ -133,4 +133,24 @@ void known_function_override_guards_arity() { assertThatThrownBy(() -> d.functionGenerator().generateKnownFunction(KnownFunction.NOW, List.of("x"))) .isInstanceOf(IllegalArgumentException.class).hasMessageContaining("NOW"); } + + @Test + void comment_uses_ms_description_extended_property() { + String table = d.commentOnTable(table("dbo", "USERS"), "it's").orElseThrow(); + assertThat(table).startsWith("IF EXISTS (SELECT 1 FROM sys.extended_properties") + .contains("EXEC sys.sp_updateextendedproperty @name = N'MS_Description', @value = N'it''s'") + .contains("ELSE EXEC sys.sp_addextendedproperty") + .contains("@level1type = N'TABLE', @level1name = N'USERS'") + .doesNotContain("@level2type"); + String column = d.commentOnColumn(table("dbo", "USERS"), "NAME", "x", null).orElseThrow(); + assertThat(column).contains("COLUMNPROPERTY(").contains("@level2type = N'COLUMN', @level2name = N'NAME'"); + assertThat(d.commentOnTable(table("dbo", "USERS"), null).orElseThrow()) + .contains("sp_dropextendedproperty").doesNotContain("@value"); + } + + @Test + void rename_constraint_uses_the_schema_scoped_name() { + assertThat(d.renameConstraint(table("dbo", "T"), "PK_OLD", "PK_NEW")) + .isEqualTo("EXEC sp_rename '\"dbo\".\"PK_OLD\"', 'PK_NEW', 'OBJECT'"); + } } diff --git a/dialect/db/mssqlserver/src/test/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/MicrosoftSqlServerQuotingPolicyTest.java b/dialect/db/mssqlserver/src/test/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/MicrosoftSqlServerQuotingPolicyTest.java index 85a58aa..3b50c54 100644 --- a/dialect/db/mssqlserver/src/test/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/MicrosoftSqlServerQuotingPolicyTest.java +++ b/dialect/db/mssqlserver/src/test/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/MicrosoftSqlServerQuotingPolicyTest.java @@ -50,7 +50,9 @@ void renameIndex_via_spRename_unquoted() { @Test void renameConstraint_via_spRename_unquoted() { assertThat(dialectNever().ddlGenerator().renameConstraint(EMP, "PK_OLD", "PK_NEW")) - .doesNotContainPattern("\\[[^]]*\\]").contains("EMPLOYEES").contains("PK_OLD").contains("PK_NEW"); + // constraints are schema-scoped: schema.constraint, not table.constraint + .doesNotContainPattern("\\[[^]]*\\]").contains("dbo.PK_OLD").doesNotContain("EMPLOYEES") + .contains("PK_NEW"); } @Test 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 a2744ab..e29e012 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 @@ -39,6 +39,8 @@ import org.eclipse.daanse.sql.model.sql.BitOperation; import org.eclipse.daanse.sql.dialect.api.generator.KnownFunction; import org.eclipse.daanse.sql.dialect.api.generator.StatementHint; +import org.eclipse.daanse.sql.dialect.api.generator.PaginationGenerator.PagingParam; +import org.eclipse.daanse.sql.dialect.api.generator.PaginationGenerator.PreparedPaging; import org.eclipse.daanse.sql.model.sql.OrderedColumn; import org.eclipse.daanse.sql.dialect.db.common.AbstractJdbcDialect; import org.eclipse.daanse.sql.dialect.db.common.DialectUtil; @@ -123,6 +125,28 @@ public String paginate(java.util.OptionalLong limit, java.util.OptionalLong offs } return sb.toString(); } + + @Override + public PreparedPaging paginatePrepared(boolean withLimit, boolean withOffset) { + if (withLimit && withOffset) { + return new PreparedPaging( + "LIMIT ? OFFSET ?", + List.of(PagingParam.LIMIT, PagingParam.OFFSET) + ); + } else if (withLimit) { + return new PreparedPaging( + "LIMIT ?", + List.of(PagingParam.LIMIT) + ); + } else if (withOffset) { + return new PreparedPaging( + "OFFSET ?", + List.of(PagingParam.OFFSET) + ); + } else { + return new PreparedPaging("", List.of()); + } + } }; cachedPaginationGenerator = local; return local; @@ -620,6 +644,39 @@ public String alterColumnSetNullability(TableReference table, String columnName, .append(nullable ? " NULL" : " NOT NULL").toString(); } + /** {@code ALTER TABLE t COMMENT = '…'} — MySQL has no COMMENT ON; {@code null} clears it. */ + @Override + public Optional commentOnTable(TableReference table, String comment) { + return Optional.of("ALTER TABLE " + qualified(table) + " COMMENT = " + mysqlLiteral(comment)); + } + + /** + * {@code ALTER TABLE t MODIFY COLUMN c [NOT NULL] [DEFAULT d] COMMENT '…'} + * — MySQL can only set a column comment by restating the whole column, so + * type, nullability and default come from {@code currentMeta}. + */ + @Override + public Optional commentOnColumn(TableReference table, String columnName, String comment, + ColumnMetaData currentMeta) { + if (currentMeta == null) { + throw new IllegalArgumentException("currentMeta must not be null for a MySQL column comment"); + } + StringBuilder sb = new StringBuilder("ALTER TABLE ").append(qualified(table)).append(" MODIFY COLUMN ") + .append(quoteIdentifier(columnName)).append(' ').append(nativeType(currentMeta)); + if (currentMeta.nullability() == ColumnMetaData.Nullability.NO_NULLS) { + sb.append(" NOT NULL"); + } + currentMeta.columnDefault().filter(d -> !d.isBlank()).ifPresent(d -> sb.append(" DEFAULT ").append(d)); + sb.append(" COMMENT ").append(mysqlLiteral(comment)); + return Optional.of(sb.toString()); + } + + /** MySQL string literal: {@code '} and {@code \} escaped; {@code null} becomes the empty comment. */ + private static String mysqlLiteral(String comment) { + String s = comment == null ? "" : comment; + return "'" + s.replace("\\", "\\\\").replace("'", "''") + "'"; + } + // SET DEFAULT / DROP DEFAULT inherit the SQL-99 default — MySQL 8.0+ accepts // it. diff --git a/dialect/db/mysql/src/test/java/org/eclipse/daanse/sql/dialect/db/mysql/MySqlGeneratorsRoundTripTest.java b/dialect/db/mysql/src/test/java/org/eclipse/daanse/sql/dialect/db/mysql/MySqlGeneratorsRoundTripTest.java index fb0073b..20c286f 100644 --- a/dialect/db/mysql/src/test/java/org/eclipse/daanse/sql/dialect/db/mysql/MySqlGeneratorsRoundTripTest.java +++ b/dialect/db/mysql/src/test/java/org/eclipse/daanse/sql/dialect/db/mysql/MySqlGeneratorsRoundTripTest.java @@ -13,6 +13,7 @@ import static org.eclipse.daanse.sql.dialect.db.testsupport.RoundTripAssertions.assertExecuteUpdateAffected; import static org.eclipse.daanse.sql.dialect.db.testsupport.RoundTripAssertions.assertFirstStringEquals; import static org.eclipse.daanse.sql.dialect.db.testsupport.RoundTripAssertions.assertSelectIdRowCountAndFirst; +import static org.eclipse.daanse.sql.dialect.db.testsupport.RoundTripAssertions.assertSelectIdRowCountAndFirstWithParams; import java.sql.Connection; import java.sql.SQLException; @@ -25,6 +26,7 @@ import org.eclipse.daanse.sql.model.schema.TableReference; import org.eclipse.daanse.sql.dialect.api.DialectInitData; import org.eclipse.daanse.sql.dialect.api.generator.MergeGenerator; +import org.eclipse.daanse.sql.dialect.api.generator.PaginationGenerator.PreparedPaging; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -76,6 +78,19 @@ void pagination_limit_offset_executes() throws SQLException { assertSelectIdRowCountAndFirst(conn, "SELECT id FROM `rt`.`users` ORDER BY id" + tail, 2, 2); } + @Test + void pagination_limit_offset_executesPlaceholders() throws SQLException { + PreparedPaging preparedPaging = dialect.paginationGenerator().paginatePrepared(false, false); + assertThat(preparedPaging.clause()).isEqualTo(""); + preparedPaging = dialect.paginationGenerator().paginatePrepared(true, false); + assertThat(preparedPaging.clause()).isEqualTo("LIMIT ?"); + preparedPaging = dialect.paginationGenerator().paginatePrepared(false, true); + assertThat(preparedPaging.clause()).isEqualTo("OFFSET ?"); + preparedPaging = dialect.paginationGenerator().paginatePrepared(true, true); + assertThat(preparedPaging.clause()).isEqualTo("LIMIT ? OFFSET ?"); + assertSelectIdRowCountAndFirstWithParams(conn, "SELECT id FROM `rt`.`users` ORDER BY id" + preparedPaging.clause(), List.of(2, 1), 2, 2); + } + @Test void pagination_offset_only_uses_huge_limit_trick() throws SQLException { // MySQL has no offset-only form, so the generator emits LIMIT off, MAX_UINT64. diff --git a/dialect/db/mysql/src/test/java/org/eclipse/daanse/sql/dialect/db/mysql/MySqlGeneratorsTest.java b/dialect/db/mysql/src/test/java/org/eclipse/daanse/sql/dialect/db/mysql/MySqlGeneratorsTest.java index 26654c1..53d4a74 100644 --- a/dialect/db/mysql/src/test/java/org/eclipse/daanse/sql/dialect/db/mysql/MySqlGeneratorsTest.java +++ b/dialect/db/mysql/src/test/java/org/eclipse/daanse/sql/dialect/db/mysql/MySqlGeneratorsTest.java @@ -15,6 +15,10 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; +import org.eclipse.daanse.sql.dialect.db.testsupport.GeneratorTestSupport; +import org.eclipse.daanse.sql.model.schema.ColumnMetaData; +import java.util.OptionalInt; +import java.sql.JDBCType; import java.util.List; import java.util.OptionalLong; @@ -34,6 +38,18 @@ void pagination_offset_limit() { .isEqualTo(" LIMIT 40, 20"); } + @Test + void pagination_offset_limit_placeholders() { + assertThat(d.paginationGenerator().paginatePrepared(false, false).clause()) + .isEqualTo(""); + assertThat(d.paginationGenerator().paginatePrepared(true, true).clause()) + .isEqualTo("LIMIT ? OFFSET ?"); + assertThat(d.paginationGenerator().paginatePrepared(true, false).clause()) + .isEqualTo("LIMIT ?"); + assertThat(d.paginationGenerator().paginatePrepared(false, true).clause()) + .isEqualTo("OFFSET ?"); + } + @Test void pagination_limit_only() { assertThat(d.paginationGenerator().paginate(OptionalLong.of(20), OptionalLong.empty())).isEqualTo(" LIMIT 20"); @@ -117,4 +133,26 @@ void known_function_override_guards_arity() { assertThatThrownBy(() -> d.functionGenerator().generateKnownFunction(KnownFunction.INDEX_OF, List.of("n"))) .isInstanceOf(IllegalArgumentException.class).hasMessageContaining("INDEX_OF"); } + + @Test + void comment_on_table_uses_alter_table() { + assertThat(d.commentOnTable(table("app", "users"), "it's").orElseThrow()) + .isEqualTo("ALTER TABLE `app`.`users` COMMENT = 'it''s'"); + assertThat(d.commentOnTable(table("app", "users"), null).orElseThrow()).endsWith("COMMENT = ''"); + } + + @Test + void comment_on_column_restates_the_column() { + String sql = d.commentOnColumn(table("app", "users"), "name", "full\\name", + GeneratorTestSupport.columnMeta(JDBCType.VARCHAR, OptionalInt.of(50), + ColumnMetaData.Nullability.NO_NULLS)).orElseThrow(); + assertThat(sql).startsWith("ALTER TABLE `app`.`users` MODIFY COLUMN `name` VARCHAR(50) NOT NULL") + .endsWith(" COMMENT 'full\\\\name'"); + } + + @Test + void comment_on_column_requires_meta() { + assertThatThrownBy(() -> d.commentOnColumn(table("app", "users"), "name", "x", null)) + .isInstanceOf(IllegalArgumentException.class); + } } 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 01316d8..8e590bd 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 @@ -599,8 +599,10 @@ public String addForeignKeyConstraint(TableReference table, String constraintNam sb.append(") REFERENCES ").append(qualified(referencedTable)).append(" ("); appendQuotedCsv(sb, referencedColumns); sb.append(")"); - if (onDelete != null && !onDelete.isBlank()) - sb.append(" ON DELETE ").append(onDelete); + // Oracle only knows ON DELETE CASCADE / SET NULL; NO ACTION is the implicit + // behavior and RESTRICT / SET DEFAULT do not exist — writing them is ORA-02000. + if (onDelete != null && (onDelete.equalsIgnoreCase("CASCADE") || onDelete.equalsIgnoreCase("SET NULL"))) + sb.append(" ON DELETE ").append(onDelete.toUpperCase()); // Oracle: no ON UPDATE clause — silently drop {@code onUpdate}. return sb.toString(); } @@ -726,4 +728,21 @@ public Optional renameSequence(String schemaName, String name, String ne return Optional.of(new StringBuilder("RENAME ").append(quoteIdentifier(name)).append(" TO ") .append(quoteIdentifier(newName)).toString()); } + + /** Oracle has no {@code IS NULL} form — an empty string literal removes the comment. */ + @Override + public Optional commentOnTable(TableReference table, String comment) { + return Optional.of("COMMENT ON TABLE " + qualified(table) + " IS " + oracleLiteral(comment)); + } + + @Override + public Optional commentOnColumn(TableReference table, String columnName, String comment, + ColumnMetaData currentMeta) { + return Optional.of("COMMENT ON COLUMN " + qualified(table) + "." + quoteIdentifier(columnName) + " IS " + + oracleLiteral(comment)); + } + + private static String oracleLiteral(String comment) { + return "'" + (comment == null ? "" : comment.replace("'", "''")) + "'"; + } } diff --git a/dialect/db/oracle/src/test/java/org/eclipse/daanse/sql/dialect/db/oracle/OracleGeneratorsRoundTripTest.java b/dialect/db/oracle/src/test/java/org/eclipse/daanse/sql/dialect/db/oracle/OracleGeneratorsRoundTripTest.java index 8c793c3..6ef715f 100644 --- a/dialect/db/oracle/src/test/java/org/eclipse/daanse/sql/dialect/db/oracle/OracleGeneratorsRoundTripTest.java +++ b/dialect/db/oracle/src/test/java/org/eclipse/daanse/sql/dialect/db/oracle/OracleGeneratorsRoundTripTest.java @@ -13,6 +13,7 @@ import static org.eclipse.daanse.sql.dialect.db.testsupport.RoundTripAssertions.assertExecuteUpdateAffected; import static org.eclipse.daanse.sql.dialect.db.testsupport.RoundTripAssertions.assertFirstStringEquals; import static org.eclipse.daanse.sql.dialect.db.testsupport.RoundTripAssertions.assertSelectIdRowCountAndFirst; +import static org.eclipse.daanse.sql.dialect.db.testsupport.RoundTripAssertions.assertSelectIdRowCountAndFirstWithParams; import java.sql.Connection; import java.sql.DriverManager; @@ -26,6 +27,7 @@ import org.eclipse.daanse.sql.model.schema.TableReference; import org.eclipse.daanse.sql.dialect.api.DialectInitData; import org.eclipse.daanse.sql.dialect.api.generator.MergeGenerator; +import org.eclipse.daanse.sql.dialect.api.generator.PaginationGenerator.PreparedPaging; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -84,6 +86,20 @@ void pagination_offset_fetch_next_executes() throws SQLException { assertSelectIdRowCountAndFirst(conn, "SELECT id FROM \"" + schema + "\".\"USERS\" ORDER BY id" + tail, 2, 2); } + @Test + void pagination_limit_offset_executesPlaceholders() throws SQLException { + PreparedPaging preparedPaging = dialect.paginationGenerator().paginatePrepared(false, false); + assertThat(preparedPaging.clause()).isEqualTo(""); + preparedPaging = dialect.paginationGenerator().paginatePrepared(true, false); + assertThat(preparedPaging.clause()).isEqualTo("OFFSET ? ROWS"); + preparedPaging = dialect.paginationGenerator().paginatePrepared(false, true); + assertThat(preparedPaging.clause()).isEqualTo("FETCH NEXT ? ROWS ONLY"); + preparedPaging = dialect.paginationGenerator().paginatePrepared(true, true); + assertThat(preparedPaging.clause()).isEqualTo("OFFSET ? ROWS FETCH NEXT ? ROWS ONLY"); + String schema = CONTAINER.getUsername().toUpperCase(); + assertSelectIdRowCountAndFirstWithParams(conn, "SELECT id FROM \"" + schema + "\".\"USERS\" ORDER BY id" + preparedPaging.clause(), List.of(1, 2), 2, 2); + } + @Test void merge_into_using_dual_inserts_then_updates() throws SQLException { MergeGenerator.UpsertSpec spec = new MergeGenerator.UpsertSpec(users, List.of("ID"), List.of("ID", "NAME"), diff --git a/dialect/db/oracle/src/test/java/org/eclipse/daanse/sql/dialect/db/oracle/OracleGeneratorsTest.java b/dialect/db/oracle/src/test/java/org/eclipse/daanse/sql/dialect/db/oracle/OracleGeneratorsTest.java index ee0b7eb..da8b182 100644 --- a/dialect/db/oracle/src/test/java/org/eclipse/daanse/sql/dialect/db/oracle/OracleGeneratorsTest.java +++ b/dialect/db/oracle/src/test/java/org/eclipse/daanse/sql/dialect/db/oracle/OracleGeneratorsTest.java @@ -93,4 +93,22 @@ void known_function_override_guards_arity() { () -> d.functionGenerator().generateKnownFunction(KnownFunction.INDEX_OF, List.of("n", "h", "z"))) .isInstanceOf(IllegalArgumentException.class).hasMessageContaining("INDEX_OF"); } + + @Test + void comment_null_becomes_empty_literal() { + assertThat(d.commentOnTable(table("APP", "USERS"), null).orElseThrow()) + .isEqualTo("COMMENT ON TABLE \"APP\".\"USERS\" IS ''"); + assertThat(d.commentOnColumn(table("APP", "USERS"), "NAME", "it's", null).orElseThrow()) + .isEqualTo("COMMENT ON COLUMN \"APP\".\"USERS\".\"NAME\" IS 'it''s'"); + } + + @Test + void foreign_key_writes_only_actions_oracle_knows() { + String noAction = d.addForeignKeyConstraint(table("APP", "O"), "FK", List.of("C"), + table("APP", "P"), List.of("ID"), "NO ACTION", "NO ACTION"); + assertThat(noAction).doesNotContain(" ON DELETE").doesNotContain(" ON UPDATE"); + String cascade = d.addForeignKeyConstraint(table("APP", "O"), "FK", List.of("C"), + table("APP", "P"), List.of("ID"), "cascade", null); + assertThat(cascade).endsWith(" ON DELETE CASCADE"); + } } 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 1965f2e..c78af1b 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 @@ -73,7 +73,7 @@ void renames_inherit_ANSI_default() { assertThat(dialect.ddlGenerator().renameTable(T, "STAFF")) .isEqualTo("ALTER TABLE \"HR\".\"EMPLOYEES\" RENAME TO \"STAFF\""); assertThat(dialect.ddlGenerator().renameIndex("IDX_OLD", "IDX_NEW", T)) - .isEqualTo("ALTER INDEX \"IDX_OLD\" RENAME TO \"IDX_NEW\""); + .isEqualTo("ALTER INDEX \"HR\".\"IDX_OLD\" RENAME TO \"IDX_NEW\""); assertThat(dialect.ddlGenerator().renameConstraint(T, "OLD_FK", "NEW_FK")) .isEqualTo("ALTER TABLE \"HR\".\"EMPLOYEES\" RENAME CONSTRAINT \"OLD_FK\" TO \"NEW_FK\""); } 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 c3c06f2..f342438 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 @@ -38,6 +38,8 @@ import org.eclipse.daanse.sql.model.sql.BitOperation; import org.eclipse.daanse.sql.model.sql.OrderedColumn; import org.eclipse.daanse.sql.model.type.BestFitColumnType; +import org.eclipse.daanse.sql.dialect.api.generator.PaginationGenerator.PagingParam; +import org.eclipse.daanse.sql.dialect.api.generator.PaginationGenerator.PreparedPaging; import org.eclipse.daanse.sql.dialect.db.common.AbstractJdbcDialect; import org.eclipse.daanse.sql.dialect.db.common.DialectUtil; @@ -101,6 +103,28 @@ public String paginate(java.util.OptionalLong limit, java.util.OptionalLong offs }); return sb.toString(); } + + @Override + public PreparedPaging paginatePrepared(boolean withLimit, boolean withOffset) { + if (withLimit && withOffset) { + return new PreparedPaging( + "LIMIT ? OFFSET ?", + List.of(PagingParam.LIMIT, PagingParam.OFFSET) + ); + } else if (withLimit) { + return new PreparedPaging( + "LIMIT ?", + List.of(PagingParam.LIMIT) + ); + } else if (withOffset) { + return new PreparedPaging( + "OFFSET ?", + List.of(PagingParam.OFFSET) + ); + } else { + return new PreparedPaging("", List.of()); + } + } }; cachedPaginationGenerator = local; return local; diff --git a/dialect/db/postgresql/src/test/java/org/eclipse/daanse/sql/dialect/db/postgresql/PostgreSqlGeneratorsRoundTripTest.java b/dialect/db/postgresql/src/test/java/org/eclipse/daanse/sql/dialect/db/postgresql/PostgreSqlGeneratorsRoundTripTest.java index c829a48..af6b546 100644 --- a/dialect/db/postgresql/src/test/java/org/eclipse/daanse/sql/dialect/db/postgresql/PostgreSqlGeneratorsRoundTripTest.java +++ b/dialect/db/postgresql/src/test/java/org/eclipse/daanse/sql/dialect/db/postgresql/PostgreSqlGeneratorsRoundTripTest.java @@ -9,10 +9,12 @@ */ package org.eclipse.daanse.sql.dialect.db.postgresql; +import static org.assertj.core.api.Assertions.assertThat; import static org.eclipse.daanse.sql.dialect.db.testsupport.RoundTripAssertions.assertExecuteUpdateAffected; import static org.eclipse.daanse.sql.dialect.db.testsupport.RoundTripAssertions.assertFirstIntEquals; import static org.eclipse.daanse.sql.dialect.db.testsupport.RoundTripAssertions.assertFirstStringEquals; import static org.eclipse.daanse.sql.dialect.db.testsupport.RoundTripAssertions.assertSelectIdRowCountAndFirst; +import static org.eclipse.daanse.sql.dialect.db.testsupport.RoundTripAssertions.assertSelectIdRowCountAndFirstWithParams; import java.sql.Connection; import java.sql.DriverManager; @@ -27,6 +29,7 @@ import org.eclipse.daanse.sql.model.schema.TableReference; import org.eclipse.daanse.sql.dialect.api.DialectInitData; import org.eclipse.daanse.sql.dialect.api.generator.MergeGenerator; +import org.eclipse.daanse.sql.dialect.api.generator.PaginationGenerator.PreparedPaging; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -79,6 +82,19 @@ void pagination_limit_offset_executes() throws SQLException { assertSelectIdRowCountAndFirst(conn, "SELECT id FROM \"public\".\"users\" ORDER BY id" + tail, 2, 2); } + @Test + void pagination_limit_offset_executesPlaceholders() throws SQLException { + PreparedPaging preparedPaging = dialect.paginationGenerator().paginatePrepared(false, false); + assertThat(preparedPaging.clause()).isEqualTo(""); + preparedPaging = dialect.paginationGenerator().paginatePrepared(true, false); + assertThat(preparedPaging.clause()).isEqualTo("LIMIT ?"); + preparedPaging = dialect.paginationGenerator().paginatePrepared(false, true); + assertThat(preparedPaging.clause()).isEqualTo("OFFSET ?"); + preparedPaging = dialect.paginationGenerator().paginatePrepared(true, true); + assertThat(preparedPaging.clause()).isEqualTo("LIMIT ? OFFSET ?"); + assertSelectIdRowCountAndFirstWithParams(conn, "SELECT id FROM `rt`.`users` ORDER BY id" + preparedPaging.clause(), List.of(2, 1), 2, 2); + } + @Test void upsert_on_conflict_inserts_then_updates() throws SQLException { MergeGenerator.UpsertSpec spec = new MergeGenerator.UpsertSpec(USERS, List.of("id"), List.of("id", "name"), diff --git a/dialect/db/postgresql/src/test/java/org/eclipse/daanse/sql/dialect/db/postgresql/PostgreSqlGeneratorsTest.java b/dialect/db/postgresql/src/test/java/org/eclipse/daanse/sql/dialect/db/postgresql/PostgreSqlGeneratorsTest.java index b0d387a..815f655 100644 --- a/dialect/db/postgresql/src/test/java/org/eclipse/daanse/sql/dialect/db/postgresql/PostgreSqlGeneratorsTest.java +++ b/dialect/db/postgresql/src/test/java/org/eclipse/daanse/sql/dialect/db/postgresql/PostgreSqlGeneratorsTest.java @@ -14,10 +14,20 @@ import static org.eclipse.daanse.sql.dialect.db.testsupport.GeneratorTestSupport.upsertSpec; import static org.eclipse.daanse.sql.dialect.db.testsupport.GeneratorTestSupport.upsertSpecDoNothing; +import java.sql.JDBCType; import java.util.List; +import java.util.Optional; +import java.util.OptionalInt; import java.util.OptionalLong; import org.eclipse.daanse.sql.dialect.api.generator.MergeGenerator; +import org.eclipse.daanse.sql.jdbc.record.schema.ColumnDefinitionRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.ColumnMetaDataRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.PrimaryKeyRecord; +import org.eclipse.daanse.sql.model.schema.ColumnDefinition; +import org.eclipse.daanse.sql.model.schema.ColumnMetaData; +import org.eclipse.daanse.sql.model.schema.ColumnReference; +import org.eclipse.daanse.sql.model.schema.TableReference; import org.junit.jupiter.api.Test; /** Smoke tests for PostgreSQL's engine-specific overrides of new generators. */ @@ -31,6 +41,19 @@ void pagination_limit_offset() { .isEqualTo(" LIMIT 20 OFFSET 40"); } + @Test + void pagination_offset_limit_placeholders() { + assertThat(d.paginationGenerator().paginatePrepared(false, false).clause()) + .isEqualTo(""); + assertThat(d.paginationGenerator().paginatePrepared(true, true).clause()) + .isEqualTo("LIMIT ? OFFSET ?"); + assertThat(d.paginationGenerator().paginatePrepared(true, false).clause()) + .isEqualTo("LIMIT ?"); + assertThat(d.paginationGenerator().paginatePrepared(false, true).clause()) + .isEqualTo("OFFSET ?"); + } + + @Test void pagination_limit_only() { assertThat(d.paginationGenerator().paginate(OptionalLong.of(20), OptionalLong.empty())).isEqualTo(" LIMIT 20"); @@ -62,4 +85,37 @@ void upsert_on_conflict_do_nothing() { String sql = d.mergeGenerator().upsert(spec, List.of("1", "'foo'")).orElseThrow(); assertThat(sql).contains("ON CONFLICT (\"ID\") DO NOTHING"); } + + @Test + void comment_on_table_and_column_standard_form() { + assertThat(d.commentOnTable(table("public", "USERS"), "it's the users").orElseThrow()) + .isEqualTo("COMMENT ON TABLE \"public\".\"USERS\" IS 'it''s the users'"); + assertThat(d.commentOnColumn(table("public", "USERS"), "NAME", "full name", null).orElseThrow()) + .isEqualTo("COMMENT ON COLUMN \"public\".\"USERS\".\"NAME\" IS 'full name'"); + } + + @Test + void comment_null_removes_it() { + assertThat(d.commentOnTable(table("public", "USERS"), null).orElseThrow()).endsWith(" IS NULL"); + } + + @Test + void rename_index_is_schema_qualified() { + assertThat(d.renameIndex("ix_old", "ix_new", table("sales", "t"))) + .isEqualTo("ALTER INDEX \"sales\".\"ix_old\" RENAME TO \"ix_new\""); + } + + @Test + void create_table_names_the_primary_key() { + TableReference t = table("sales", "t"); + ColumnDefinition id = new ColumnDefinitionRecord(new ColumnReference(Optional.of(t), "id"), + new ColumnMetaDataRecord(JDBCType.INTEGER, "INTEGER", OptionalInt.empty(), OptionalInt.empty(), + OptionalInt.empty(), ColumnMetaData.Nullability.NO_NULLS, OptionalInt.empty(), + Optional.empty(), Optional.empty(), ColumnMetaData.AutoIncrement.UNKNOWN, + ColumnMetaData.GeneratedColumn.UNKNOWN)); + assertThat(d.createTable(t, List.of(id), new PrimaryKeyRecord(t, List.of(id.column()), Optional.of("pk_t")), + false)).contains("CONSTRAINT \"pk_t\" PRIMARY KEY (\"id\")"); + assertThat(d.createTable(t, List.of(id), new PrimaryKeyRecord(t, List.of(id.column()), Optional.empty()), + false)).contains(",\n PRIMARY KEY (\"id\")").doesNotContain("CONSTRAINT"); + } } 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 dc7a3dd..78e5124 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 @@ -80,7 +80,7 @@ void renameTable_emits_RENAME_TO() { @Test void renameIndex_emits_ALTER_INDEX() { assertThat(dialect.ddlGenerator().renameIndex("IDX_OLD", "IDX_NEW", T)) - .isEqualTo("ALTER INDEX \"IDX_OLD\" RENAME TO \"IDX_NEW\""); + .isEqualTo("ALTER INDEX \"PUBLIC\".\"IDX_OLD\" RENAME TO \"IDX_NEW\""); } @Test 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 1fee61a..528f7f4 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 @@ -21,11 +21,15 @@ */ package org.eclipse.daanse.sql.dialect.db.sqlite; +import org.eclipse.daanse.sql.model.schema.TableReference; import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.sql.Types; +import java.util.List; import org.eclipse.daanse.sql.model.type.BestFitColumnType; +import org.eclipse.daanse.sql.dialect.api.generator.PaginationGenerator.PagingParam; +import org.eclipse.daanse.sql.dialect.api.generator.PaginationGenerator.PreparedPaging; import org.eclipse.daanse.sql.dialect.db.common.AbstractJdbcDialect; public class SqliteDialect extends AbstractJdbcDialect { @@ -196,6 +200,28 @@ public String paginate(java.util.OptionalLong limit, java.util.OptionalLong offs } return sb.toString(); } + + @Override + public PreparedPaging paginatePrepared(boolean withLimit, boolean withOffset) { + if (withLimit && withOffset) { + return new PreparedPaging( + "LIMIT ? OFFSET ?", + List.of(PagingParam.LIMIT, PagingParam.OFFSET) + ); + } else if (withLimit) { + return new PreparedPaging( + "LIMIT ?", + List.of(PagingParam.LIMIT) + ); + } else if (withOffset) { + return new PreparedPaging( + "OFFSET ?", + List.of(PagingParam.OFFSET) + ); + } else { + return new PreparedPaging("", List.of()); + } + } }; cachedPaginationGenerator = local; return local; @@ -283,4 +309,16 @@ public java.util.Optional upsert(UpsertSpec spec, java.util.List cachedMergeGenerator = local; return local; } + + /** SQLite cannot store comments on tables or columns. */ + @Override + public java.util.Optional commentOnTable(TableReference table, String comment) { + return java.util.Optional.empty(); + } + + @Override + public java.util.Optional commentOnColumn(TableReference table, String columnName, String comment, + org.eclipse.daanse.sql.model.schema.ColumnMetaData currentMeta) { + return java.util.Optional.empty(); + } } diff --git a/dialect/db/sqlite/src/test/java/org/eclipse/daanse/sql/dialect/db/sqlite/SqliteDialectTest.java b/dialect/db/sqlite/src/test/java/org/eclipse/daanse/sql/dialect/db/sqlite/SqliteDialectTest.java index 34e50f1..a3c095a 100644 --- a/dialect/db/sqlite/src/test/java/org/eclipse/daanse/sql/dialect/db/sqlite/SqliteDialectTest.java +++ b/dialect/db/sqlite/src/test/java/org/eclipse/daanse/sql/dialect/db/sqlite/SqliteDialectTest.java @@ -89,4 +89,13 @@ void merge_unsupported_when_version_below_3_24() { private static SqliteDialect withVersion(int major, int minor) { return new SqliteDialect(DialectInitData.ansiDefaults().withVersion(major, minor)); } + + @Test + void comments_are_not_supported() { + SqliteDialect d = new SqliteDialect(); + org.eclipse.daanse.sql.model.schema.TableReference t = new org.eclipse.daanse.sql.model.schema.TableReference( + java.util.Optional.empty(), "t", org.eclipse.daanse.sql.model.schema.TableReference.TYPE_TABLE); + assertThat(d.commentOnTable(t, "x")).isEmpty(); + assertThat(d.commentOnColumn(t, "c", "x", null)).isEmpty(); + } } diff --git a/dialect/db/sqlite/src/test/java/org/eclipse/daanse/sql/dialect/db/sqlite/SqliteGeneratorsRoundTripTest.java b/dialect/db/sqlite/src/test/java/org/eclipse/daanse/sql/dialect/db/sqlite/SqliteGeneratorsRoundTripTest.java index 4044361..544735e 100644 --- a/dialect/db/sqlite/src/test/java/org/eclipse/daanse/sql/dialect/db/sqlite/SqliteGeneratorsRoundTripTest.java +++ b/dialect/db/sqlite/src/test/java/org/eclipse/daanse/sql/dialect/db/sqlite/SqliteGeneratorsRoundTripTest.java @@ -9,10 +9,12 @@ */ package org.eclipse.daanse.sql.dialect.db.sqlite; +import static org.assertj.core.api.Assertions.assertThat; import static org.eclipse.daanse.sql.dialect.db.testsupport.RoundTripAssertions.assertExecuteUpdateAffected; import static org.eclipse.daanse.sql.dialect.db.testsupport.RoundTripAssertions.assertFirstIntEquals; import static org.eclipse.daanse.sql.dialect.db.testsupport.RoundTripAssertions.assertFirstStringEquals; import static org.eclipse.daanse.sql.dialect.db.testsupport.RoundTripAssertions.assertSelectIdRowCountAndFirst; +import static org.eclipse.daanse.sql.dialect.db.testsupport.RoundTripAssertions.assertSelectIdRowCountAndFirstWithParams; import java.sql.Connection; import java.sql.DriverManager; @@ -26,6 +28,7 @@ import org.eclipse.daanse.sql.model.schema.TableReference; import org.eclipse.daanse.sql.dialect.api.DialectInitData; import org.eclipse.daanse.sql.dialect.api.generator.MergeGenerator; +import org.eclipse.daanse.sql.dialect.api.generator.PaginationGenerator.PreparedPaging; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -68,6 +71,19 @@ void pagination_limit_offset_executes() throws SQLException { assertSelectIdRowCountAndFirst(conn, "SELECT id FROM \"users\" ORDER BY id" + tail, 2, 2); } + @Test + void pagination_limit_offset_executesPlaceholders() throws SQLException { + PreparedPaging preparedPaging = dialect.paginationGenerator().paginatePrepared(false, false); + assertThat(preparedPaging.clause()).isEqualTo(""); + preparedPaging = dialect.paginationGenerator().paginatePrepared(true, false); + assertThat(preparedPaging.clause()).isEqualTo("LIMIT ?"); + preparedPaging = dialect.paginationGenerator().paginatePrepared(false, true); + assertThat(preparedPaging.clause()).isEqualTo("OFFSET ?"); + preparedPaging = dialect.paginationGenerator().paginatePrepared(true, true); + assertThat(preparedPaging.clause()).isEqualTo("LIMIT ? OFFSET ?"); + assertSelectIdRowCountAndFirstWithParams(conn, "SELECT id FROM \"users\" ORDER BY id " + preparedPaging.clause(), List.of(2, 1), 2, 2); + } + @Test void upsert_on_conflict_inserts_then_updates() throws SQLException { MergeGenerator.UpsertSpec spec = new MergeGenerator.UpsertSpec(users, List.of("id"), List.of("id", "name"), diff --git a/dialect/db/test-support/src/main/java/org/eclipse/daanse/sql/dialect/db/testsupport/RoundTripAssertions.java b/dialect/db/test-support/src/main/java/org/eclipse/daanse/sql/dialect/db/testsupport/RoundTripAssertions.java index 3b0726e..3593c60 100644 --- a/dialect/db/test-support/src/main/java/org/eclipse/daanse/sql/dialect/db/testsupport/RoundTripAssertions.java +++ b/dialect/db/test-support/src/main/java/org/eclipse/daanse/sql/dialect/db/testsupport/RoundTripAssertions.java @@ -12,9 +12,11 @@ import static org.assertj.core.api.Assertions.assertThat; import java.sql.Connection; +import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; +import java.util.List; /** * ResultSet assertion helpers shared by per-engine generator round-trip tests. @@ -51,6 +53,27 @@ public static void assertSelectIdRowCountAndFirst(Connection conn, String select } } + public static void assertSelectIdRowCountAndFirstWithParams(Connection conn, String selectIdSql, List params, int expectedCount, + int expectedFirstId) throws SQLException { + try (PreparedStatement ps = conn.prepareStatement(selectIdSql);) { + int index = 1; + for (Integer param : params) { + ps.setInt(index++, param); + } + int count = 0; + int firstId = -1; + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + if (count == 0) + firstId = rs.getInt(1); + count++; + } + } + assertThat(count).as("row count").isEqualTo(expectedCount); + assertThat(firstId).as("first id").isEqualTo(expectedFirstId); + } + } + /** * Execute {@code dml} and assert the returned update count. *