From f378aecfafb46f40ce82b1c18d5eae15a461b74d Mon Sep 17 00:00:00 2001 From: dbulahov Date: Tue, 20 Oct 2026 15:27:39 +0300 Subject: [PATCH 01/10] dialect(mysql): a case-sensitive regular expression says so in the pattern REGEXP follows the collation of its operand, case-insensitive by default, so "a.*" matched "A..." natively and not when interpreted. The case-sensitive pattern starts with (?-i), read by ICU (MySQL 8.0.4+) and PCRE (MariaDB). Signed-off-by: dbulahov --- .../eclipse/daanse/sql/dialect/db/mysql/MySqlDialect.java | 7 +++++-- .../daanse/sql/dialect/db/mysql/MySqlDialect3Test.java | 3 ++- 2 files changed, 7 insertions(+), 3 deletions(-) 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 b89e0a1..faaf99d 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 @@ -424,9 +424,12 @@ public Optional generateRegularExpression(String source, String javaRege } sb.append(" REGEXP "); // MDX MATCHES is anchored (Pattern.matches); REGEXP is contains — - // anchor AFTER the case fold, or the fold would turn \z into \Z + // anchor AFTER the case fold, or the fold would turn \z into \Z. + // REGEXP follows the collation of its operand, case-insensitive by default (utf8mb4_0900_ai_ci): a + // case-sensitive match says so in the pattern. (?-i) is read by ICU (MySQL 8.0.4+) and PCRE (MariaDB), the + // engines \A and \z already need; without it "a.*" matched "A..." natively, not when interpreted. if (caseSensitive) { - quoteStringLiteral(sb, "\\A(?:" + javaRegex + ")\\z"); + quoteStringLiteral(sb, "(?-i)\\A(?:" + javaRegex + ")\\z"); } else { quoteStringLiteral(sb, "\\A(?:" + javaRegex.toUpperCase() + ")\\z"); } diff --git a/dialect/db/mysql/src/test/java/org/eclipse/daanse/sql/dialect/db/mysql/MySqlDialect3Test.java b/dialect/db/mysql/src/test/java/org/eclipse/daanse/sql/dialect/db/mysql/MySqlDialect3Test.java index 4b613f5..33d2191 100644 --- a/dialect/db/mysql/src/test/java/org/eclipse/daanse/sql/dialect/db/mysql/MySqlDialect3Test.java +++ b/dialect/db/mysql/src/test/java/org/eclipse/daanse/sql/dialect/db/mysql/MySqlDialect3Test.java @@ -76,7 +76,8 @@ void testGenerateRegularExpression_CaseInsensitive() throws Exception { @Test void testGenerateRegularExpression_CaseSensitive() throws Exception { String sql = dialect.regexGenerator().generateRegularExpression("table.column", ".*a.*").get().toString(); - assertEquals("table.column IS NOT NULL AND table.column REGEXP '\\\\A(?:.*a.*)\\\\z'", sql); + // REGEXP follows the collation, case-insensitive by default: the pattern asks for case + assertEquals("table.column IS NOT NULL AND table.column REGEXP '(?-i)\\\\A(?:.*a.*)\\\\z'", sql); } @Test From 6cd84b8b79a70336384b54b475d2dfc23809221d Mon Sep 17 00:00:00 2001 From: dbulahov Date: Tue, 20 Oct 2026 15:39:54 +0300 Subject: [PATCH 02/10] dialect(mssql): the mean of an integer column is not a whole number SQL Server averages an INTEGER column to an INTEGER (31 for 31 and 32). The renderer asks the dialect for the operand of an avg aggregate (AggregationGenerator.averageOperand, the operand itself by default); SQL Server reads it as float. Signed-off-by: dbulahov --- .../api/generator/AggregationGenerator.java | 11 ++++ .../MicrosoftSqlServerDialect.java | 9 +++ .../MicrosoftSqlServerGeneratorsTest.java | 7 ++ .../statement/render/DialectSqlRenderer.java | 6 ++ .../render/AverageRenderingTest.java | 64 +++++++++++++++++++ 5 files changed, 97 insertions(+) create mode 100644 statement/impl/src/test/java/org/eclipse/daanse/sql/statement/render/AverageRenderingTest.java diff --git a/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/AggregationGenerator.java b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/AggregationGenerator.java index c9527b0..463ffb0 100644 --- a/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/AggregationGenerator.java +++ b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/AggregationGenerator.java @@ -27,6 +27,17 @@ public interface AggregationGenerator extends IdentifierQuoter { String IGNORE_NULLS = " IGNORE NULLS "; String RESPECT_NULLS = " RESPECT NULLS "; + // -------------------- AVG -------------------- + + /** + * The operand of an {@code avg} aggregate as this dialect needs it to compute the mean: the operand itself, unless + * the engine averages whole numbers to a whole number. SQL Server returns 31 for the mean of 31 and 32 over an + * INTEGER column, where every other engine, MDX and SSAS give 31.5 (database run 19./20.10., F-10.6 2). + */ + default CharSequence averageOperand(CharSequence operand) { + return operand; + } + // -------------------- list / string aggregation -------------------- default java.util.Optional generateListAgg(CharSequence operand, boolean distinct, String separator, 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 dcd31cd..01753ba 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 @@ -441,6 +441,15 @@ public boolean supportsPercentileCont() { return true; } + /** + * SQL Server averages an integer column to an integer (the result type of AVG is the operand's); the operand is + * read as float, the double way every measure value takes. + */ + @Override + public CharSequence averageOperand(CharSequence operand) { + return "CAST(" + operand + " AS float)"; + } + @Override public java.util.Optional generateListAgg(CharSequence operand, boolean distinct, String separator, String coalesce, String onOverflowTruncate, List columns) { 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 d682776..b97b5f3 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 @@ -27,6 +27,13 @@ class MicrosoftSqlServerGeneratorsTest { private final MicrosoftSqlServerDialect d = new MicrosoftSqlServerDialect(); + /** AVG over an INTEGER column is an integer on SQL Server; the operand is read as float. */ + @Test + void avg_reads_its_operand_as_float() { + assertThat(d.aggregationGenerator().averageOperand("\"t\".\"x\"")) + .hasToString("CAST(\"t\".\"x\" AS float)"); + } + @Test void output_clause() { assertThat(d.returningGenerator().supportsReturning()).isTrue(); diff --git a/statement/impl/src/main/java/org/eclipse/daanse/sql/statement/render/DialectSqlRenderer.java b/statement/impl/src/main/java/org/eclipse/daanse/sql/statement/render/DialectSqlRenderer.java index f98cfe1..8ae814f 100644 --- a/statement/impl/src/main/java/org/eclipse/daanse/sql/statement/render/DialectSqlRenderer.java +++ b/statement/impl/src/main/java/org/eclipse/daanse/sql/statement/render/DialectSqlRenderer.java @@ -809,6 +809,12 @@ public String renderExpression(SqlExpression e) { dialect.quote(b, l.value(), l.datatype()); return b.toString(); } + if (e instanceof SqlExpression.Aggregate a && a.arguments().size() == 1 && "avg".equalsIgnoreCase(a.name())) { + // the one dialect touch of a plain aggregate: an engine that averages whole numbers to a whole number + // reads the operand otherwise (SQL Server; F-10.6 2) + return a.name() + "(" + (a.distinct() ? "distinct " : "") + + dialect.aggregationGenerator().averageOperand(renderExpression(a.arguments().get(0))) + ")"; + } if (e instanceof SqlExpression.Call c) { boolean distinct = c instanceof SqlExpression.Aggregate a && a.distinct(); return c.name() + "(" + (distinct ? "distinct " : "") diff --git a/statement/impl/src/test/java/org/eclipse/daanse/sql/statement/render/AverageRenderingTest.java b/statement/impl/src/test/java/org/eclipse/daanse/sql/statement/render/AverageRenderingTest.java new file mode 100644 index 0000000..4986799 --- /dev/null +++ b/statement/impl/src/test/java/org/eclipse/daanse/sql/statement/render/AverageRenderingTest.java @@ -0,0 +1,64 @@ +/* + * 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 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.statement.render; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.eclipse.daanse.sql.dialect.db.common.AnsiDialect; +import org.eclipse.daanse.sql.statement.api.Expressions; +import org.eclipse.daanse.sql.statement.api.From; +import org.eclipse.daanse.sql.statement.api.SelectStatementBuilder; +import org.eclipse.daanse.sql.statement.api.model.TableAlias; +import org.junit.jupiter.api.Test; + +/** + * An {@code avg} aggregate asks the dialect for its operand: an engine that averages whole numbers to a whole number + * (SQL Server) reads it otherwise; every other aggregate and every other dialect render as written. + */ +class AverageRenderingTest { + + private static String avgOver(AnsiDialect dialect, boolean distinct) { + SelectStatementBuilder q = SelectStatementBuilder.create(); + var column = Expressions.column(TableAlias.of("f"), "x"); + q.project(distinct ? Expressions.aggregateDistinct("avg", column) : Expressions.aggregate("avg", column), null); + q.project(Expressions.aggregate("sum", column), null); + q.from(From.raw("select 1 as x", TableAlias.of("f"))); + return new DialectSqlRenderer(dialect).render(q.build()).sql(); + } + + private static final AnsiDialect CASTING = new AnsiDialect() { + @Override + public CharSequence averageOperand(CharSequence operand) { + return "CAST(" + operand + " AS float)"; + } + }; + + @Test + void anAverageRendersAsWrittenByDefault() { + assertEquals("select avg(\"f\".\"x\") as \"c0\", sum(\"f\".\"x\") as \"c1\" from (select 1 as x) as \"f\"", + avgOver(new AnsiDialect(), false)); + } + + @Test + void anAverageTakesTheOperandTheDialectNames() { + assertEquals("select avg(CAST(\"f\".\"x\" AS float)) as \"c0\", sum(\"f\".\"x\") as \"c1\" from (select 1 as x)" + + " as \"f\"", avgOver(CASTING, false)); + } + + @Test + void aDistinctAverageKeepsItsDistinct() { + assertEquals("select avg(distinct CAST(\"f\".\"x\" AS float)) as \"c0\", sum(\"f\".\"x\") as \"c1\" from" + + " (select 1 as x) as \"f\"", avgOver(CASTING, true)); + } +} From 15ab2a4d3de3802026c1a407289612256b1e1949 Mon Sep 17 00:00:00 2001 From: dbulahov Date: Tue, 20 Oct 2026 15:45:07 +0300 Subject: [PATCH 03/10] dialect(postgresql): a text aggregation is written with STRING_AGG PostgreSQL had no list aggregation, and every text aggregation measure failed: the SQL builder declined the segment and the recorder could not write it either. STRING_AGG takes text only, so the operand is cast; DISTINCT beside an ORDER BY over other columns is an error in PostgreSQL and is left out, as DuckDB does. Signed-off-by: dbulahov --- .../db/postgresql/PostgreSqlDialect.java | 28 +++++++++++++++++++ .../postgresql/PostgreSqlGeneratorsTest.java | 19 +++++++++++++ 2 files changed, 47 insertions(+) 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 55ab7aa..e69b162 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 @@ -394,6 +394,34 @@ protected boolean supportsNullsOrdering() { return productVersion != null && productVersion.compareTo("8.3") >= 0; } + /** + * {@code STRING_AGG(CAST(operand AS TEXT), 'separator' ORDER BY ...)}. PostgreSQL had no list aggregation here, and + * every text aggregation failed in the SQL builder and in the recorder alike (the writeback tutorials on + * PostgreSQL, database run 20.10.). The operand is cast, because STRING_AGG takes text only. DISTINCT with an ORDER BY + * over other columns is an error in PostgreSQL (the ORDER BY must name the arguments); DISTINCT is then left out, as + * DuckDB does, so that both write the same list. + */ + @Override + public java.util.Optional generateListAgg(CharSequence operand, boolean distinct, String separator, + String coalesce, String onOverflowTruncate, List columns) { + boolean ordered = columns != null && !columns.isEmpty(); + StringBuilder buf = new StringBuilder(64).append("STRING_AGG("); + if (distinct && !ordered) { + buf.append("DISTINCT "); + } + buf.append("CAST(").append(operand).append(" AS TEXT), '") + .append((separator != null ? separator : ",").replace("'", "''")).append('\''); + if (ordered) { + buf.append(" ORDER BY ").append(buildOrderedColumnsClause(columns)); + } + return java.util.Optional.of(buf.append(')').toString()); + } + + @Override + public boolean supportsListAgg() { + return true; + } + // Unified BitOperation methods @Override 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 815f655..2dcb5e0 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 @@ -35,6 +35,25 @@ class PostgreSqlGeneratorsTest { private final PostgreSqlDialect d = new PostgreSqlDialect(); + /** PostgreSQL lists text with STRING_AGG; the operand is cast, the separator quoted. */ + @Test + void list_aggregation_is_string_agg() { + assertThat(d.aggregationGenerator().supportsListAgg()).isTrue(); + assertThat(d.aggregationGenerator().generateListAgg("\"t\".\"c\"", true, " | ", null, null, List.of()) + .orElseThrow()).isEqualTo("STRING_AGG(DISTINCT CAST(\"t\".\"c\" AS TEXT), ' | ')"); + assertThat(d.aggregationGenerator().generateListAgg("x", false, "it's", null, null, + List.of(new org.eclipse.daanse.sql.model.sql.OrderedColumn("y", null))).orElseThrow()) + .isEqualTo("STRING_AGG(CAST(x AS TEXT), 'it''s' ORDER BY \"y\")"); + } + + /** DISTINCT beside an ORDER BY over other columns is an error in PostgreSQL; it is left out, as in DuckDB. */ + @Test + void a_distinct_ordered_list_is_written_without_distinct() { + assertThat(d.aggregationGenerator().generateListAgg("x", true, null, null, null, + List.of(new org.eclipse.daanse.sql.model.sql.OrderedColumn("y", null))).orElseThrow()) + .isEqualTo("STRING_AGG(CAST(x AS TEXT), ',' ORDER BY \"y\")"); + } + @Test void pagination_limit_offset() { assertThat(d.paginationGenerator().paginate(OptionalLong.of(20), OptionalLong.of(40))) From ade67eff04be6a9c130f8abfb28788257ffb8cc0 Mon Sep 17 00:00:00 2001 From: dbulahov Date: Tue, 20 Oct 2026 15:47:11 +0300 Subject: [PATCH 04/10] dialect(derby): loading writes one statement at a time Derby locks its system tables for DDL; tables and indexes created at once deadlocked while a test database was loaded. Signed-off-by: dbulahov --- .../daanse/sql/dialect/db/derby/DerbyDialect.java | 9 +++++++++ .../daanse/sql/dialect/db/derby/DerbyDialectTest.java | 6 ++++++ 2 files changed, 15 insertions(+) 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 1b33775..e645fcd 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 @@ -141,6 +141,15 @@ public boolean requiresAliasForFromQuery() { return true; } + /** + * One writer: Derby locks its system tables for DDL, and tables and indexes created at once deadlock ("A lock could + * not be obtained due to a deadlock", CREATE INDEX beside CREATE TABLE while FoodMart is loaded, database run 20.10.). + */ + @Override + public boolean supportsParallelLoading() { + return false; + } + @Override public boolean allowsMultipleCountDistinct() { // Derby allows at most one distinct-count per query. diff --git a/dialect/db/derby/src/test/java/org/eclipse/daanse/sql/dialect/db/derby/DerbyDialectTest.java b/dialect/db/derby/src/test/java/org/eclipse/daanse/sql/dialect/db/derby/DerbyDialectTest.java index 5b8bca5..54bcb13 100644 --- a/dialect/db/derby/src/test/java/org/eclipse/daanse/sql/dialect/db/derby/DerbyDialectTest.java +++ b/dialect/db/derby/src/test/java/org/eclipse/daanse/sql/dialect/db/derby/DerbyDialectTest.java @@ -39,6 +39,12 @@ protected void setUp() throws Exception { dialect = new DerbyDialect(org.eclipse.daanse.sql.dialect.api.DialectInitData.fromConnection(connection)); } + /** Derby deadlocks on DDL run at once; a loader writes one statement at a time. */ + @Test + void loadingIsOneWriterAtATime() { + org.junit.jupiter.api.Assertions.assertFalse(dialect.supportsParallelLoading()); + } + @Test void testGetDialectName() { assertEquals("derby", dialect.name()); From 21144fa5e6d2ad10e7d998c8e50acb0fbe6264f4 Mon Sep 17 00:00:00 2001 From: dbulahov Date: Tue, 20 Oct 2026 15:49:00 +0300 Subject: [PATCH 05/10] dialect(derby): the mean of an integer column is not a whole number Derby averages an INTEGER column to an INTEGER, like SQL Server; the operand of avg is read as DOUBLE. Signed-off-by: dbulahov --- .../daanse/sql/dialect/db/derby/DerbyDialect.java | 9 +++++++++ .../daanse/sql/dialect/db/derby/DerbyDialectTest.java | 7 +++++++ 2 files changed, 16 insertions(+) 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 e645fcd..6b236a1 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 @@ -141,6 +141,15 @@ public boolean requiresAliasForFromQuery() { return true; } + /** + * Derby averages an integer column to an integer (31 for 31 and 32, the Measure Aggregator Base tutorial, database + * run 20.10.); the operand is read as DOUBLE, as on SQL Server. + */ + @Override + public CharSequence averageOperand(CharSequence operand) { + return "CAST(" + operand + " AS DOUBLE)"; + } + /** * One writer: Derby locks its system tables for DDL, and tables and indexes created at once deadlock ("A lock could * not be obtained due to a deadlock", CREATE INDEX beside CREATE TABLE while FoodMart is loaded, database run 20.10.). diff --git a/dialect/db/derby/src/test/java/org/eclipse/daanse/sql/dialect/db/derby/DerbyDialectTest.java b/dialect/db/derby/src/test/java/org/eclipse/daanse/sql/dialect/db/derby/DerbyDialectTest.java index 54bcb13..b129e38 100644 --- a/dialect/db/derby/src/test/java/org/eclipse/daanse/sql/dialect/db/derby/DerbyDialectTest.java +++ b/dialect/db/derby/src/test/java/org/eclipse/daanse/sql/dialect/db/derby/DerbyDialectTest.java @@ -39,6 +39,13 @@ protected void setUp() throws Exception { dialect = new DerbyDialect(org.eclipse.daanse.sql.dialect.api.DialectInitData.fromConnection(connection)); } + /** AVG over an INTEGER column is an integer in Derby; the operand is read as DOUBLE. */ + @Test + void anAverageReadsItsOperandAsDouble() { + org.junit.jupiter.api.Assertions.assertEquals("CAST(x AS DOUBLE)", + dialect.aggregationGenerator().averageOperand("x").toString()); + } + /** Derby deadlocks on DDL run at once; a loader writes one statement at a time. */ @Test void loadingIsOneWriterAtATime() { From d3e3d0e7806a0e4f20e204f0d98aa7c2ba4b9ab4 Mon Sep 17 00:00:00 2001 From: dbulahov Date: Tue, 20 Oct 2026 15:51:19 +0300 Subject: [PATCH 06/10] importer(csv): a table is looked up before a plain DROP TABLE A dialect without DROP TABLE IF EXISTS (Derby, Oracle before 23) writes a plain DROP TABLE, which fails on a table that is not there; every fixture load on Derby failed so. The importer looks the table up by its exact name in its schema first, the name escaped for the metadata pattern. Signed-off-by: dbulahov --- .../importer/csv/impl/CsvDataImporter.java | 34 ++++++++++++- ...ATableIsLookedUpBeforeItIsDroppedTest.java | 50 +++++++++++++++++++ 2 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 jdbc/importer/csv/src/test/java/org/eclipse/daanse/sql/jdbc/importer/csv/impl/ATableIsLookedUpBeforeItIsDroppedTest.java diff --git a/jdbc/importer/csv/src/main/java/org/eclipse/daanse/sql/jdbc/importer/csv/impl/CsvDataImporter.java b/jdbc/importer/csv/src/main/java/org/eclipse/daanse/sql/jdbc/importer/csv/impl/CsvDataImporter.java index 9c9dd87..23a2ca3 100644 --- a/jdbc/importer/csv/src/main/java/org/eclipse/daanse/sql/jdbc/importer/csv/impl/CsvDataImporter.java +++ b/jdbc/importer/csv/src/main/java/org/eclipse/daanse/sql/jdbc/importer/csv/impl/CsvDataImporter.java @@ -225,7 +225,12 @@ private void insertTable(Connection connection, CloseableIterator headersTypeList, TableDefinition table) throws SQLException { try (Statement stmt = connection.createStatement();) { diff --git a/jdbc/importer/csv/src/test/java/org/eclipse/daanse/sql/jdbc/importer/csv/impl/ATableIsLookedUpBeforeItIsDroppedTest.java b/jdbc/importer/csv/src/test/java/org/eclipse/daanse/sql/jdbc/importer/csv/impl/ATableIsLookedUpBeforeItIsDroppedTest.java new file mode 100644 index 0000000..32d5dda --- /dev/null +++ b/jdbc/importer/csv/src/test/java/org/eclipse/daanse/sql/jdbc/importer/csv/impl/ATableIsLookedUpBeforeItIsDroppedTest.java @@ -0,0 +1,50 @@ +/* + * 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 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.jdbc.importer.csv.impl; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.Statement; +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; + +/** + * A dialect without DROP TABLE IF EXISTS (Derby) drops a table only after finding it: the lookup is by exact name in + * the table's schema, and an underscore in the name is no wildcard. + */ +class ATableIsLookedUpBeforeItIsDroppedTest { + + @Test + void aTableIsFoundByItsExactName() throws Exception { + try (Connection connection = DriverManager.getConnection("jdbc:h2:mem:lookup" + System.nanoTime()); + Statement statement = connection.createStatement()) { + statement.execute("CREATE SCHEMA \"shop\""); + statement.execute("CREATE TABLE \"shop\".\"store_csv\" (\"id\" INT)"); + statement.execute("CREATE TABLE \"line\" (\"id\" INT)"); + Optional shop = Optional.of(new SchemaReference("shop")); + + assertTrue(CsvDataImporter.exists(connection, new TableReference(shop, "store_csv"))); + assertTrue(CsvDataImporter.exists(connection, new TableReference("line")), "the current schema"); + assertFalse(CsvDataImporter.exists(connection, new TableReference(shop, "storeXcsv")), "_ is no wildcard"); + assertFalse(CsvDataImporter.exists(connection, new TableReference(shop, "line")), "another schema"); + assertFalse(CsvDataImporter.exists(connection, new TableReference(shop, "STORE_CSV")), "the exact name"); + } + } +} From 67f1e427d6706aab2db37517086de9d0594a7399 Mon Sep 17 00:00:00 2001 From: dbulahov Date: Tue, 20 Oct 2026 15:53:05 +0300 Subject: [PATCH 07/10] dialect(derby): a truth value is written as the number it is stored as The dialect stores a truth value as SMALLINT, and Derby does not compare SMALLINT with BOOLEAN; the literal is 1 or 0. Signed-off-by: dbulahov --- .../sql/dialect/db/derby/DerbyDialect.java | 17 +++++++++++++++++ .../sql/dialect/db/derby/DerbyDialectTest.java | 10 ++++++++++ 2 files changed, 27 insertions(+) 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 6b236a1..0565d31 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 @@ -45,6 +45,23 @@ public String booleanTypeName() { return "SMALLINT"; } + /** + * {@code 1} and {@code 0}: this dialect stores a truth value as SMALLINT ({@link #booleanTypeName()}), and Derby does + * not compare SMALLINT with BOOLEAN ("ERROR 42818: Comparisons between 'SMALLINT' and 'BOOLEAN' are not supported", + * a boolean level's key, database run 20.10.). + */ + @Override + public void quoteBooleanLiteral(StringBuilder buf, String value) { + String literal = value.trim(); + if (literal.equalsIgnoreCase("TRUE") || literal.equals("1")) { + buf.append('1'); + } else if (literal.equalsIgnoreCase("FALSE") || literal.equals("0")) { + buf.append('0'); + } else { + throw new NumberFormatException("Illegal BOOLEAN literal: " + value); + } + } + /** JDBC-free constructor for SQL generation. */ public DerbyDialect() { super(org.eclipse.daanse.sql.dialect.api.DialectInitData.ansiDefaults()); diff --git a/dialect/db/derby/src/test/java/org/eclipse/daanse/sql/dialect/db/derby/DerbyDialectTest.java b/dialect/db/derby/src/test/java/org/eclipse/daanse/sql/dialect/db/derby/DerbyDialectTest.java index b129e38..8fe7f83 100644 --- a/dialect/db/derby/src/test/java/org/eclipse/daanse/sql/dialect/db/derby/DerbyDialectTest.java +++ b/dialect/db/derby/src/test/java/org/eclipse/daanse/sql/dialect/db/derby/DerbyDialectTest.java @@ -39,6 +39,16 @@ protected void setUp() throws Exception { dialect = new DerbyDialect(org.eclipse.daanse.sql.dialect.api.DialectInitData.fromConnection(connection)); } + /** A truth value is stored as SMALLINT, so its literal is 1 or 0: Derby does not compare SMALLINT with BOOLEAN. */ + @Test + void aTruthValueIsWrittenAsTheNumberItIsStoredAs() { + for (String[] pair : new String[][] { { "true", "1" }, { "FALSE", "0" }, { "1", "1" }, { "0", "0" } }) { + StringBuilder buf = new StringBuilder(); + dialect.quoteBooleanLiteral(buf, pair[0]); + org.junit.jupiter.api.Assertions.assertEquals(pair[1], buf.toString(), pair[0]); + } + } + /** AVG over an INTEGER column is an integer in Derby; the operand is read as DOUBLE. */ @Test void anAverageReadsItsOperandAsDouble() { From 72d146d5e1fe98cbb4a84ab3819c74fdfad1d3ea Mon Sep 17 00:00:00 2001 From: dbulahov Date: Tue, 20 Oct 2026 15:56:14 +0300 Subject: [PATCH 08/10] dialect(derby): a quotient keeps its fraction Derby divides DECIMAL(31,4) by DECIMAL(31,4) at scale 0 and integers to an integer: a native ratio of 1.9 came out as 1. The renderer asks the dialect for the dividend of a division (FunctionGenerator.quotientDividend, the dividend itself by default); Derby reads it as DOUBLE. Signed-off-by: dbulahov --- .../api/generator/FunctionGenerator.java | 9 +++ .../sql/dialect/db/derby/DerbyDialect.java | 10 +++ .../dialect/db/derby/DerbyDialectTest.java | 7 +++ .../statement/render/DialectSqlRenderer.java | 8 ++- .../render/QuotientRenderingTest.java | 63 +++++++++++++++++++ 5 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 statement/impl/src/test/java/org/eclipse/daanse/sql/statement/render/QuotientRenderingTest.java diff --git a/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/FunctionGenerator.java b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/FunctionGenerator.java index e033957..05d1501 100644 --- a/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/FunctionGenerator.java +++ b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/FunctionGenerator.java @@ -11,6 +11,15 @@ public interface FunctionGenerator { + /** + * The dividend of a division as this dialect needs it to divide without losing the fraction: the dividend itself, + * unless the engine cuts a quotient's scale. Derby divides DECIMAL(31,4) by DECIMAL(31,4) at scale 0, so a native + * ratio of 1.9 came out as 1 (a native Filter over a calculated measure read no row, database run 20.10.). + */ + default CharSequence quotientDividend(CharSequence dividend) { + return dividend; + } + default StringBuilder wrapIntoSqlUpperCaseFunction(CharSequence sqlExpression) { return new StringBuilder("UPPER(").append(sqlExpression).append(")"); } 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 0565d31..b1f87f5 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 @@ -167,6 +167,16 @@ public CharSequence averageOperand(CharSequence operand) { return "CAST(" + operand + " AS DOUBLE)"; } + /** + * Derby divides decimals at a scale its precision leaves over: DECIMAL(31,4) by DECIMAL(31,4) at scale 0, so a + * ratio of 1.9 is 1; integers divide to an integer. The dividend is read as DOUBLE (a native Filter over a calculated + * measure, FilterTest#testCmNativeFilter, database run 20.10.). + */ + @Override + public CharSequence quotientDividend(CharSequence dividend) { + return "CAST(" + dividend + " AS DOUBLE)"; + } + /** * One writer: Derby locks its system tables for DDL, and tables and indexes created at once deadlock ("A lock could * not be obtained due to a deadlock", CREATE INDEX beside CREATE TABLE while FoodMart is loaded, database run 20.10.). diff --git a/dialect/db/derby/src/test/java/org/eclipse/daanse/sql/dialect/db/derby/DerbyDialectTest.java b/dialect/db/derby/src/test/java/org/eclipse/daanse/sql/dialect/db/derby/DerbyDialectTest.java index 8fe7f83..6d15a17 100644 --- a/dialect/db/derby/src/test/java/org/eclipse/daanse/sql/dialect/db/derby/DerbyDialectTest.java +++ b/dialect/db/derby/src/test/java/org/eclipse/daanse/sql/dialect/db/derby/DerbyDialectTest.java @@ -49,6 +49,13 @@ void aTruthValueIsWrittenAsTheNumberItIsStoredAs() { } } + /** Derby cuts a quotient's scale; the dividend is read as DOUBLE. */ + @Test + void aDivisionReadsItsDividendAsDouble() { + org.junit.jupiter.api.Assertions.assertEquals("CAST(x AS DOUBLE)", + dialect.functionGenerator().quotientDividend("x").toString()); + } + /** AVG over an INTEGER column is an integer in Derby; the operand is read as DOUBLE. */ @Test void anAverageReadsItsOperandAsDouble() { diff --git a/statement/impl/src/main/java/org/eclipse/daanse/sql/statement/render/DialectSqlRenderer.java b/statement/impl/src/main/java/org/eclipse/daanse/sql/statement/render/DialectSqlRenderer.java index 8ae814f..718ddea 100644 --- a/statement/impl/src/main/java/org/eclipse/daanse/sql/statement/render/DialectSqlRenderer.java +++ b/statement/impl/src/main/java/org/eclipse/daanse/sql/statement/render/DialectSqlRenderer.java @@ -821,8 +821,12 @@ public String renderExpression(SqlExpression e) { + c.arguments().stream().map(this::renderExpression).collect(Collectors.joining(", ")) + ")"; } if (e instanceof SqlExpression.Binary b) { - String rendered = renderExpression(b.left()) + " " + b.operator().symbol() + " " - + renderExpression(b.right()); + String left = renderExpression(b.left()); + if (b.operator() == org.eclipse.daanse.sql.statement.api.expression.ArithmeticOperator.DIVIDE) { + // the one dialect touch of arithmetic: an engine that cuts a quotient's scale divides a cast dividend + left = dialect.functionGenerator().quotientDividend(left).toString(); + } + String rendered = left + " " + b.operator().symbol() + " " + renderExpression(b.right()); return b.parenthesized() ? "(" + rendered + ")" : rendered; } if (e instanceof SqlExpression.Case c) { diff --git a/statement/impl/src/test/java/org/eclipse/daanse/sql/statement/render/QuotientRenderingTest.java b/statement/impl/src/test/java/org/eclipse/daanse/sql/statement/render/QuotientRenderingTest.java new file mode 100644 index 0000000..5c7d91d --- /dev/null +++ b/statement/impl/src/test/java/org/eclipse/daanse/sql/statement/render/QuotientRenderingTest.java @@ -0,0 +1,63 @@ +/* + * 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 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.statement.render; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.eclipse.daanse.sql.dialect.db.common.AnsiDialect; +import org.eclipse.daanse.sql.statement.api.Expressions; +import org.eclipse.daanse.sql.statement.api.From; +import org.eclipse.daanse.sql.statement.api.SelectStatementBuilder; +import org.eclipse.daanse.sql.statement.api.expression.ArithmeticOperator; +import org.eclipse.daanse.sql.statement.api.expression.SqlExpression; +import org.eclipse.daanse.sql.statement.api.model.TableAlias; +import org.junit.jupiter.api.Test; + +/** A division asks the dialect for its dividend; every other operator and the default dialect render as written. */ +class QuotientRenderingTest { + + private static String render(AnsiDialect dialect, ArithmeticOperator operator) { + SelectStatementBuilder q = SelectStatementBuilder.create(); + SqlExpression a = Expressions.column(TableAlias.of("f"), "a"); + SqlExpression b = Expressions.column(TableAlias.of("f"), "b"); + q.project(new SqlExpression.Binary(a, operator, b), null); + q.from(From.raw("select 1 as a, 2 as b", TableAlias.of("f"))); + return new DialectSqlRenderer(dialect).render(q.build()).sql(); + } + + private static final AnsiDialect CASTING = new AnsiDialect() { + @Override + public CharSequence quotientDividend(CharSequence dividend) { + return "CAST(" + dividend + " AS DOUBLE)"; + } + }; + + @Test + void aQuotientRendersAsWrittenByDefault() { + assertEquals("select (\"f\".\"a\" / \"f\".\"b\") as \"c0\" from (select 1 as a, 2 as b) as \"f\"", + render(new AnsiDialect(), ArithmeticOperator.DIVIDE)); + } + + @Test + void aQuotientTakesTheDividendTheDialectNames() { + assertEquals("select (CAST(\"f\".\"a\" AS DOUBLE) / \"f\".\"b\") as \"c0\" from (select 1 as a, 2 as b) as \"f\"", + render(CASTING, ArithmeticOperator.DIVIDE)); + } + + @Test + void otherOperatorsAreLeftAlone() { + assertEquals("select (\"f\".\"a\" - \"f\".\"b\") as \"c0\" from (select 1 as a, 2 as b) as \"f\"", + render(CASTING, ArithmeticOperator.SUBTRACT)); + } +} From ca4e2d63e7d1a407191b4816c978cf7299f5ba73 Mon Sep 17 00:00:00 2001 From: dbulahov Date: Tue, 20 Oct 2026 15:57:48 +0300 Subject: [PATCH 09/10] dialect(postgresql, mssql, h2): a quotient keeps its fraction PostgreSQL, SQL Server and H2 divide integers to an integer, and a native filter over sum(units_shipped) / sum(units_ordered) read 0 where MDX reads 0.9. The dividend is read as a floating type, as on Derby. Signed-off-by: dbulahov --- .../eclipse/daanse/sql/dialect/db/h2/H2Dialect.java | 10 ++++++++++ .../daanse/sql/dialect/db/h2/H2DialectTest.java | 7 +++++++ .../db/mssqlserver/MicrosoftSqlServerDialect.java | 9 +++++++++ .../mssqlserver/MicrosoftSqlServerGeneratorsTest.java | 6 ++++++ .../sql/dialect/db/postgresql/PostgreSqlDialect.java | 9 +++++++++ .../db/postgresql/PostgreSqlGeneratorsTest.java | 6 ++++++ 6 files changed, 47 insertions(+) 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 f6a8b3c..76e2812 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 @@ -109,6 +109,16 @@ public H2Dialect(org.eclipse.daanse.sql.dialect.api.DialectInitData init) { super(init); } + /** + * H2 divides integers to an integer. MDX divides with a fraction, and a native condition over a quotient has to as + * well: the dividend is read as DOUBLE (F-10.9, a native Filter over Units Shipped / Units Ordered, database run + * 20.10.). + */ + @Override + public CharSequence quotientDividend(CharSequence dividend) { + return "CAST(" + dividend + " AS DOUBLE)"; + } + @Override public String name() { return SUPPORTED_PRODUCT_NAME.toLowerCase(); diff --git a/dialect/db/h2/src/test/java/org/eclipse/daanse/sql/dialect/db/h2/H2DialectTest.java b/dialect/db/h2/src/test/java/org/eclipse/daanse/sql/dialect/db/h2/H2DialectTest.java index 95ce5b7..b8428da 100644 --- a/dialect/db/h2/src/test/java/org/eclipse/daanse/sql/dialect/db/h2/H2DialectTest.java +++ b/dialect/db/h2/src/test/java/org/eclipse/daanse/sql/dialect/db/h2/H2DialectTest.java @@ -48,6 +48,13 @@ protected void setUp() throws Exception { dialect = new H2Dialect(org.eclipse.daanse.sql.dialect.api.DialectInitData.fromConnection(connection)); } + /** H2 divides integers to an integer; the dividend is read as DOUBLE. */ + @Test + void aQuotientReadsItsDividendAsDouble() { + org.junit.jupiter.api.Assertions.assertEquals("CAST(x AS DOUBLE)", + dialect.functionGenerator().quotientDividend("x").toString()); + } + @Test void testGetDialectName() { assertEquals("h2", dialect.name()); 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 01753ba..95e81bf 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 @@ -441,6 +441,15 @@ public boolean supportsPercentileCont() { return true; } + /** + * SQL Server divides integers to an integer. MDX divides with a fraction, and a native condition over a quotient has to as well: the dividend is read as + * float (F-10.9, a native Filter over Units Shipped / Units Ordered, database run 20.10.). + */ + @Override + public CharSequence quotientDividend(CharSequence dividend) { + return "CAST(" + dividend + " AS float)"; + } + /** * SQL Server averages an integer column to an integer (the result type of AVG is the operand's); the operand is * read as float, the double way every measure value takes. 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 b97b5f3..131b6fc 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 @@ -27,6 +27,12 @@ class MicrosoftSqlServerGeneratorsTest { private final MicrosoftSqlServerDialect d = new MicrosoftSqlServerDialect(); + /** SQL Server divides integers to an integer; the dividend is read as float. */ + @Test + void a_quotient_reads_its_dividend_as_float() { + assertThat(d.functionGenerator().quotientDividend("x")).hasToString("CAST(x AS float)"); + } + /** AVG over an INTEGER column is an integer on SQL Server; the operand is read as float. */ @Test void avg_reads_its_operand_as_float() { 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 e69b162..681e7a8 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 @@ -394,6 +394,15 @@ protected boolean supportsNullsOrdering() { return productVersion != null && productVersion.compareTo("8.3") >= 0; } + /** + * PostgreSQL divides integers to an integer. MDX divides with a fraction, and a native condition over a quotient has to as well: the dividend is read as + * double precision (F-10.9, a native Filter over Units Shipped / Units Ordered, database run 20.10.). + */ + @Override + public CharSequence quotientDividend(CharSequence dividend) { + return "CAST(" + dividend + " AS double precision)"; + } + /** * {@code STRING_AGG(CAST(operand AS TEXT), 'separator' ORDER BY ...)}. PostgreSQL had no list aggregation here, and * every text aggregation failed in the SQL builder and in the recorder alike (the writeback tutorials on 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 2dcb5e0..3afdb7f 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 @@ -35,6 +35,12 @@ class PostgreSqlGeneratorsTest { private final PostgreSqlDialect d = new PostgreSqlDialect(); + /** PostgreSQL divides integers to an integer; the dividend is read as double precision. */ + @Test + void a_quotient_reads_its_dividend_as_double_precision() { + assertThat(d.functionGenerator().quotientDividend("x")).hasToString("CAST(x AS double precision)"); + } + /** PostgreSQL lists text with STRING_AGG; the operand is cast, the separator quoted. */ @Test void list_aggregation_is_string_agg() { From f99075c3d5c1015440d69877712534c7e01ecb01 Mon Sep 17 00:00:00 2001 From: dbulahov Date: Tue, 20 Oct 2026 15:59:58 +0300 Subject: [PATCH 10/10] dialect(postgresql, mysql): a negated bit aggregate is the bitwise complement NOT is logical in both. PostgreSQL refused NOT(bit_and(x)) on an integer column; MySQL and MariaDB answered 0 or 1 where DuckDB and the provider answer -2 for the NAND of 1. The complement is ~; MySQL's BIT_AND is unsigned 64-bit, so its complement is read as SIGNED. Signed-off-by: dbulahov --- .../daanse/sql/dialect/db/mysql/MySqlDialect.java | 9 ++++++--- .../sql/dialect/db/mysql/MySqlGeneratorsTest.java | 15 +++++++++++++++ .../dialect/db/postgresql/PostgreSqlDialect.java | 9 ++++++--- .../db/postgresql/PostgreSqlGeneratorsTest.java | 12 ++++++++++++ 4 files changed, 39 insertions(+), 6 deletions(-) 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 faaf99d..6bc5d7e 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 @@ -495,9 +495,12 @@ public java.util.Optional generateBitAggregation(BitOperation operation, case AND -> buf.append("BIT_AND(").append(operand).append(")"); case OR -> buf.append("BIT_OR(").append(operand).append(")"); case XOR -> buf.append("BIT_XOR(").append(operand).append(")"); - case NAND -> buf.append("NOT(BIT_AND(").append(operand).append("))"); - case NOR -> buf.append("NOT(BIT_OR(").append(operand).append("))"); - case NXOR -> buf.append("NOT(BIT_XOR(").append(operand).append("))"); + // NOT is logical in MySQL and MariaDB: NOT(BIT_AND(1)) is 0, which the tutorial oracle read silently as the + // NAND (database run 20.10.). ~ is bitwise, but on the unsigned 64-bit result of BIT_AND (~1 is + // 18446744073709551614); read as SIGNED it is -2, as DuckDB writes it and the provider computes it + case NAND -> buf.append("CAST(~BIT_AND(").append(operand).append(") AS SIGNED)"); + case NOR -> buf.append("CAST(~BIT_OR(").append(operand).append(") AS SIGNED)"); + case NXOR -> buf.append("CAST(~BIT_XOR(").append(operand).append(") AS SIGNED)"); }; return java.util.Optional.of(result.toString()); } 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 53d4a74..47f6252 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 @@ -32,6 +32,21 @@ class MySqlGeneratorsTest { private final MySqlDialect d = new MySqlDialect(); + /** + * A negated bit aggregate is the signed bitwise complement: MySQL's NOT is logical (NOT(BIT_AND(1)) is 0), and ~ on + * the unsigned 64-bit BIT_AND result is 18446744073709551614 where the other arts answer -2. + */ + @Test + void a_negated_bit_aggregate_is_the_signed_bitwise_complement() { + var g = d.aggregationGenerator(); + assertThat(g.generateBitAggregation(org.eclipse.daanse.sql.model.sql.BitOperation.NAND, "x").orElseThrow()) + .isEqualTo("CAST(~BIT_AND(x) AS SIGNED)"); + assertThat(g.generateBitAggregation(org.eclipse.daanse.sql.model.sql.BitOperation.NOR, "x").orElseThrow()) + .isEqualTo("CAST(~BIT_OR(x) AS SIGNED)"); + assertThat(g.generateBitAggregation(org.eclipse.daanse.sql.model.sql.BitOperation.NXOR, "x").orElseThrow()) + .isEqualTo("CAST(~BIT_XOR(x) AS SIGNED)"); + } + @Test void pagination_offset_limit() { assertThat(d.paginationGenerator().paginate(OptionalLong.of(20), OptionalLong.of(40))) 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 681e7a8..8ba34c9 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 @@ -440,9 +440,12 @@ public java.util.Optional generateBitAggregation(BitOperation operation, case AND -> buf.append("bit_and(").append(operand).append(")"); case OR -> buf.append("bit_or(").append(operand).append(")"); case XOR -> buf.append("bit_xor(").append(operand).append(")"); - case NAND -> buf.append("NOT(bit_and(").append(operand).append("))"); - case NOR -> buf.append("NOT(bit_or(").append(operand).append("))"); - case NXOR -> buf.append("NOT(bit_xor(").append(operand).append("))"); + // NOT is logical in PostgreSQL and takes a boolean, so NOT(bit_and(x)) failed on every integer column + // (tutorial oracle, database run 20.10.); the bitwise complement is ~, as DuckDB writes it and the provider + // computes it (the NAND of 1 is -2) + case NAND -> buf.append("~(bit_and(").append(operand).append("))"); + case NOR -> buf.append("~(bit_or(").append(operand).append("))"); + case NXOR -> buf.append("~(bit_xor(").append(operand).append("))"); }; return java.util.Optional.of(result.toString()); } 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 3afdb7f..10762bc 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 @@ -41,6 +41,18 @@ void a_quotient_reads_its_dividend_as_double_precision() { assertThat(d.functionGenerator().quotientDividend("x")).hasToString("CAST(x AS double precision)"); } + /** A negated bit aggregate is the bitwise complement: PostgreSQL's NOT is logical and takes no integer. */ + @Test + void a_negated_bit_aggregate_is_the_bitwise_complement() { + org.eclipse.daanse.sql.dialect.api.generator.AggregationGenerator g = d.aggregationGenerator(); + assertThat(g.generateBitAggregation(org.eclipse.daanse.sql.model.sql.BitOperation.NAND, "x").orElseThrow()) + .isEqualTo("~(bit_and(x))"); + assertThat(g.generateBitAggregation(org.eclipse.daanse.sql.model.sql.BitOperation.NOR, "x").orElseThrow()) + .isEqualTo("~(bit_or(x))"); + assertThat(g.generateBitAggregation(org.eclipse.daanse.sql.model.sql.BitOperation.NXOR, "x").orElseThrow()) + .isEqualTo("~(bit_xor(x))"); + } + /** PostgreSQL lists text with STRING_AGG; the operand is cast, the separator quoted. */ @Test void list_aggregation_is_string_agg() {