From 62940c9fd354d92cfd50cbc15a27469b5bf0983f Mon Sep 17 00:00:00 2001 From: Stefan Bischof Date: Tue, 4 Aug 2026 18:05:05 +0200 Subject: [PATCH] feat(dialect): render types from the JDBC type, not the model name nativeType asks the JDBC type instead of the model's type name, so a model may say BOOLEAN or CHARACTER VARYING and each database still gets a spelling it accepts. NUMERIC and DECIMAL stay distinct. New hooks: booleanTypeName, timestampTypeName, bigintTypeName, createTableSuffix, analyzeSchema, analyzeTable, supportsTransactions. Filled in: BOOLEAN as SMALLINT (duckdb, derby, oracle, sqlite, clickhouse), TINYINT(1) (mysql, mariadb), BIT (mssql); TIMESTAMP as DATETIME (mysql, mariadb, mssql); BIGINT as DECIMAL(15,0) with one-row inserts on oracle; MergeTree and Nullable(T) on clickhouse; ANALYZE on h2 and postgres. Signed-off-by: Stefan Bischof --- .../DialectCapabilitiesProvider.java | 7 + .../dialect/api/generator/DdlGenerator.java | 185 ++++++++++++++++-- .../db/clickhouse/ClickHouseDialect.java | 95 +++++++++ .../db/common/AbstractJdbcDialect.java | 5 + .../db/common/JdbcCapabilityFlags.java | 4 + .../sql/dialect/db/derby/DerbyDialect.java | 12 ++ .../sql/dialect/db/duckdb/DuckDbDialect.java | 15 ++ .../daanse/sql/dialect/db/h2/H2Dialect.java | 20 ++ .../dialect/db/mariadb/MariaDBDialect.java | 24 +++ .../MicrosoftSqlServerDialect.java | 23 +++ .../sql/dialect/db/mysql/MySqlDialect.java | 25 +++ .../sql/dialect/db/oracle/OracleDialect.java | 36 ++++ .../db/postgresql/PostgreSqlDialect.java | 19 ++ .../sql/dialect/db/sqlite/SqliteDialect.java | 25 +++ 14 files changed, 480 insertions(+), 15 deletions(-) diff --git a/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/capability/DialectCapabilitiesProvider.java b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/capability/DialectCapabilitiesProvider.java index 068dda1..fc5ebe2 100644 --- a/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/capability/DialectCapabilitiesProvider.java +++ b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/capability/DialectCapabilitiesProvider.java @@ -172,6 +172,13 @@ default boolean requiresDropSchemaRestrict() { /** @return true if parallel loading is supported */ boolean supportsParallelLoading(); + /** + * @return true if the database has transactions. ClickHouse has none: it + * refuses {@code setAutoCommit(false)} outright, so a writer that + * wraps a load in one transaction per table cannot even begin. + */ + boolean supportsTransactions(); + /** @return true if batch operations are supported */ boolean supportsBatchOperations(); 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 27fc332..d5598bc 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 @@ -84,27 +84,92 @@ default String createTable(TableReference table, List columns, sb.append(")"); } sb.append("\n)"); + sb.append(createTableSuffix(primaryKey == null ? List.of() + : primaryKey.columns().stream().map(c -> quoteIdentifier(c.name()).toString()).toList())); return sb.toString(); } + /** + * What has to follow the closing parenthesis of a {@code CREATE TABLE}, + * empty for every dialect that needs nothing there. + * + *

+ * ClickHouse is the one that does — it refuses a table without a storage + * engine and a sort order. Exposed separately from {@link #createTable} so + * that a caller assembling its own {@code CREATE TABLE} can obtain it too. + * + * @param quotedOrderByColumns the key columns, already quoted, or empty + * where the table has no key + */ + default String createTableSuffix(List quotedOrderByColumns) { + return ""; + } + // -------------------- DML -------------------- /** {@code INSERT INTO schema.table (col1, …) VALUES (?, …)} — parameterised. */ default String insertInto(TableReference table, List columns) { + return insertInto(table, columns, 1); + } + + /** + * How many value tuples one {@code INSERT} may carry on this dialect. + * Unlimited by default; a dialect whose grammar has no multi-row + * {@code VALUES} returns 1, and callers cap {@link #insertInto(TableReference, + * List, int)} by it rather than knowing dialects themselves. + */ + default int maxInsertRows() { + return Integer.MAX_VALUE; + } + + /** + * {@code INSERT INTO schema.table (col1, …) VALUES (?, …), (?, …), …} with + * {@code rows} value tuples — parameterised. Ask {@link #maxInsertRows()} + * first; this throws rather than emitting SQL the dialect cannot parse. + * + *

+ * One statement carrying many rows costs one execution instead of many. How + * much that is worth depends entirely on the driver: one that implements + * {@code executeBatch()} as a single protocol message gains nothing, while one + * that implements it as a loop over {@code execute()} gains the whole factor. + * + *

+ * Callers must cap {@code rows} so that {@code rows × columns} stays under + * what the driver accepts — 65535 bound parameters on PostgreSQL, 2100 on SQL + * Server. A dialect whose grammar has no multi-row {@code VALUES} (Oracle + * wants {@code INSERT ALL}) overrides this to reject anything but 1. + * + * @param rows number of value tuples, at least 1 + */ + default String insertInto(TableReference table, List columns, int rows) { if (columns.isEmpty()) { throw new IllegalArgumentException("columns must not be empty for INSERT"); } + if (rows < 1) { + throw new IllegalArgumentException("rows must be at least 1, was " + rows); + } + if (rows > maxInsertRows()) { + throw new IllegalArgumentException( + "this dialect takes at most " + maxInsertRows() + " value tuples per INSERT, was asked for " + rows); + } StringBuilder sb = new StringBuilder("INSERT INTO "); sb.append(qualified(table)); sb.append(" ("); appendColumnList(sb, columns); - sb.append(") VALUES ("); - for (int i = 0; i < columns.size(); i++) { - if (i > 0) + sb.append(") VALUES "); + for (int row = 0; row < rows; row++) { + if (row > 0) { sb.append(", "); - sb.append('?'); + } + sb.append('('); + for (int i = 0; i < columns.size(); i++) { + if (i > 0) { + sb.append(", "); + } + sb.append('?'); + } + sb.append(')'); } - sb.append(")"); return sb.toString(); } @@ -584,10 +649,94 @@ default String qualified(TableReference table) { return quoteIdentifier(schemaName, table.name()); } + /** + * How this dialect spells a boolean column. + * + *

+ * SQL-99 says {@code BOOLEAN} and that is the default, but the spelling is + * where the databases disagree most: SQL Server and Sybase have only + * {@code BIT}, MySQL and MariaDB read {@code BOOLEAN} as an alias for + * {@code TINYINT(1)}, and several engines are read back through + * {@code ResultSet.getInt}, which a native boolean column refuses. + */ + default String booleanTypeName() { + return "BOOLEAN"; + } + + /** + * The statement that gathers optimizer statistics for the whole database, + * or empty where the dialect has none. + * + *

+ * Worth running: measured over the legacy suite against PostgreSQL, 5:03 + * with against 7:18 without, and without it two queries ran into their + * timeout because the planner had nothing to go on. Gather them once the + * indexes exist — Derby's and PostgreSQL's statistics describe index + * cardinalities. + */ + default java.util.Optional analyzeSchema() { + return java.util.Optional.empty(); + } + + /** + * The statement that gathers statistics for one table, or empty where the + * dialect has none. The spelling differs even among those that have it: + * PostgreSQL takes the table straight after the keyword, H2 wants + * {@code TABLE} in between. + */ + default java.util.Optional analyzeTable(TableReference table) { + return java.util.Optional.empty(); + } + + /** + * How this dialect spells a timestamp column. + * + *

+ * SQL Server, MySQL, MariaDB and Sybase want {@code DATETIME}: their + * {@code TIMESTAMP} is something else entirely — on MySQL it only reaches + * back to 1970, so a birth date of 1946 is rejected outright. + */ + default String timestampTypeName() { + return "TIMESTAMP"; + } + + /** + * How this dialect spells a 64-bit integer column. + * + *

+ * SQL-99 has no {@code BIGINT} — it arrived with SQL:2003, which is why the + * CWM specification's catalogue of SQL-99 types does not list it either. + * Oracle and Firebird took that literally; Oracle answers + * {@code ORA-00902 invalid datatype}. Everything else in use accepts the + * name, so it stays the default and Oracle renders {@code DECIMAL(15,0)}, + * as the legacy loader did. + */ + default String bigintTypeName() { + return "BIGINT"; + } + + /** + * The physical type for a column, rendered from its JDBC type. + * + *

+ * Deliberately not from {@link ColumnMetaData#typeName()}. A model + * that is to be created on ten databases can only state the logical type; + * which physical type carries it is the dialect's to say, and they disagree + * far more than SQL-99 suggests. A model saying {@code CHARACTER VARYING} + * is legal SQL-99 and unknown to SQL Server; one saying {@code TIMESTAMP} + * means something different to MySQL than to PostgreSQL. + * + *

+ * The model's own name is used only when there is no JDBC type to render + * from — the escape hatch for a genuinely vendor-specific declaration. + */ default String nativeType(ColumnMetaData meta) { - String tn = meta.typeName(); - if (tn != null && !tn.isBlank() && !"UNKNOWN".equalsIgnoreCase(tn)) { - return applyLengthAndScale(tn, meta); + JDBCType jt = meta.dataType(); + if (jt == null || jt == JDBCType.OTHER) { + String tn = meta.typeName(); + if (tn != null && !tn.isBlank() && !"UNKNOWN".equalsIgnoreCase(tn)) { + return applyLengthAndScale(tn, meta); + } } return defaultTypeName(meta); } @@ -664,30 +813,36 @@ private static String applyLengthAndScale(String typeName, ColumnMetaData meta) }; } - private static String defaultTypeName(ColumnMetaData meta) { + // Not static: it asks the dialect how to spell the types the databases + // disagree about. + private String defaultTypeName(ColumnMetaData meta) { JDBCType jt = meta.dataType(); OptionalInt size = meta.columnSize(); OptionalInt scale = meta.decimalDigits(); return switch (jt) { - case BIT, BOOLEAN -> "BOOLEAN"; + case BIT, BOOLEAN -> booleanTypeName(); case TINYINT -> "TINYINT"; case SMALLINT -> "SMALLINT"; case INTEGER -> "INTEGER"; - case BIGINT -> "BIGINT"; + case BIGINT -> bigintTypeName(); case FLOAT, REAL -> "REAL"; case DOUBLE -> "DOUBLE PRECISION"; + // NUMERIC and DECIMAL are separate SQL types — NUMERIC has to hold + // exactly the declared precision, DECIMAL may hold more — and the model + // states which one it means, so it is not ours to collapse. case NUMERIC, DECIMAL -> { + String name = jt == JDBCType.NUMERIC ? "NUMERIC" : "DECIMAL"; if (size.isPresent() && scale.isPresent()) { - yield "DECIMAL(" + size.getAsInt() + ", " + scale.getAsInt() + ")"; + yield name + "(" + size.getAsInt() + ", " + scale.getAsInt() + ")"; } else if (size.isPresent()) { - yield "DECIMAL(" + size.getAsInt() + ")"; + yield name + "(" + size.getAsInt() + ")"; } else { - yield "DECIMAL"; + yield name; } } case DATE -> "DATE"; case TIME, TIME_WITH_TIMEZONE -> "TIME"; - case TIMESTAMP, TIMESTAMP_WITH_TIMEZONE -> "TIMESTAMP"; + case TIMESTAMP, TIMESTAMP_WITH_TIMEZONE -> timestampTypeName(); case CHAR -> size.isPresent() ? "CHAR(" + size.getAsInt() + ")" : "CHAR(1)"; case VARCHAR, LONGVARCHAR, NVARCHAR, LONGNVARCHAR, NCHAR -> size.isPresent() ? "VARCHAR(" + size.getAsInt() + ")" : "VARCHAR(255)"; 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 308368f..0da6b56 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 @@ -35,6 +35,101 @@ public boolean supportsIndexDdl() { private static final String SUPPORTED_PRODUCT_NAME = "CLICKHOUSE"; + /** + * {@code SMALLINT}. + * + *

+ * ClickHouse maps SMALLINT onto Int16 and reads it back as a number. + */ + @Override + public String booleanTypeName() { + return "SMALLINT"; + } + + /** + * No. + * + *

+ * ClickHouse refuses {@code setAutoCommit(false)} with a + * {@code SQLFeatureNotSupportedException} — there is nothing to turn off, + * every statement stands alone. A writer that wraps a table load in one + * transaction cannot even begin. + */ + @Override + public boolean supportsTransactions() { + return false; + } + + /** + * Wraps a nullable column's type in {@code Nullable(…)}. + * + *

+ * ClickHouse columns are not nullable by default and there is no + * {@code NULL} modifier to add afterwards — nullability is part of the type + * itself. A column declared {@code Int32} rejects every null; the same + * column declared {@code Nullable(Int32)} accepts them. + */ + @Override + public String nativeType(org.eclipse.daanse.sql.model.schema.ColumnMetaData meta) { + String type = super.nativeType(meta); + if (meta.nullability() == org.eclipse.daanse.sql.model.schema.ColumnMetaData.Nullability.NULLABLE) { + return "Nullable(" + type + ")"; + } + return type; + } + + /** + * Appends {@code ENGINE = MergeTree() ORDER BY (…)}. + * + *

+ * ClickHouse refuses a table without one: Code 42, ORDER BY or PRIMARY + * KEY clause is missing. MergeTree is the general-purpose engine, and + * it sorts by the primary key where the schema declares one. Where it does + * not, {@code ORDER BY tuple()} says "no ordering" — the legal way to + * express what every other database means by a table without a key. + * + *

+ * The {@code NOT NULL} the base implementation appends is left out here: + * nullability rides in the type (see {@link #nativeType}), and ClickHouse + * rejects the suffix. + */ + @Override + public String createTable(org.eclipse.daanse.sql.model.schema.TableReference table, + List columns, + org.eclipse.daanse.sql.model.schema.PrimaryKey primaryKey, boolean ifNotExists) { + StringBuilder sb = new StringBuilder(); + sb.append(ifNotExists && supportsCreateTableIfNotExists() ? "CREATE TABLE IF NOT EXISTS " : "CREATE TABLE "); + sb.append(qualified(table)).append(" (\n"); + boolean first = true; + for (org.eclipse.daanse.sql.model.schema.ColumnDefinition cd : columns) { + if (!first) { + sb.append(",\n"); + } + first = false; + sb.append(" ").append(quoteIdentifier(cd.column().name())); + sb.append(' ').append(nativeType(cd.columnMetaData())); + cd.columnMetaData().columnDefault().ifPresent(d -> sb.append(" DEFAULT ").append(d)); + } + sb.append("\n)"); + sb.append(createTableSuffix(primaryKey == null ? List.of() + : primaryKey.columns().stream().map(c -> quoteIdentifier(c.name()).toString()).toList())); + return sb.toString(); + } + + /** + * {@code ENGINE = MergeTree() ORDER BY (…)}. MergeTree is the + * general-purpose engine and sorts by the key where there is one; where + * there is none, {@code ORDER BY tuple()} is how ClickHouse spells "no + * ordering". + */ + @Override + public String createTableSuffix(List quotedOrderByColumns) { + if (quotedOrderByColumns == null || quotedOrderByColumns.isEmpty()) { + return " ENGINE = MergeTree() ORDER BY tuple()"; + } + return " ENGINE = MergeTree() ORDER BY (" + String.join(", ", quotedOrderByColumns) + ")"; + } + /** JDBC-free constructor for SQL generation. */ public ClickHouseDialect() { super(org.eclipse.daanse.sql.dialect.api.DialectInitData.ansiDefaults()); diff --git a/dialect/db/common/src/main/java/org/eclipse/daanse/sql/dialect/db/common/AbstractJdbcDialect.java b/dialect/db/common/src/main/java/org/eclipse/daanse/sql/dialect/db/common/AbstractJdbcDialect.java index 3e7b070..1d97343 100644 --- a/dialect/db/common/src/main/java/org/eclipse/daanse/sql/dialect/db/common/AbstractJdbcDialect.java +++ b/dialect/db/common/src/main/java/org/eclipse/daanse/sql/dialect/db/common/AbstractJdbcDialect.java @@ -523,6 +523,11 @@ public boolean supportsParallelLoading() { return caps.supportsParallelLoading(); } + @Override + public boolean supportsTransactions() { + return caps.supportsTransactions(); + } + @Override public boolean supportsBatchOperations() { return caps.supportsBatchOperations(); diff --git a/dialect/db/common/src/main/java/org/eclipse/daanse/sql/dialect/db/common/JdbcCapabilityFlags.java b/dialect/db/common/src/main/java/org/eclipse/daanse/sql/dialect/db/common/JdbcCapabilityFlags.java index 1f71f72..3592175 100644 --- a/dialect/db/common/src/main/java/org/eclipse/daanse/sql/dialect/db/common/JdbcCapabilityFlags.java +++ b/dialect/db/common/src/main/java/org/eclipse/daanse/sql/dialect/db/common/JdbcCapabilityFlags.java @@ -120,6 +120,10 @@ boolean supportsParallelLoading() { return true; } + boolean supportsTransactions() { + return true; + } + boolean supportsBatchOperations() { return true; } 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 e956045..4c29609 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 @@ -32,6 +32,18 @@ public class DerbyDialect extends AbstractJdbcDialect { private static final String SUPPORTED_PRODUCT_NAME = "DERBY"; + /** + * {@code SMALLINT}. + * + *

+ * Derby gained a native BOOLEAN in 10.7, but boolean levels are read back + * with {@code ResultSet.getInt}, which it refuses on that type. + */ + @Override + public String booleanTypeName() { + return "SMALLINT"; + } + /** JDBC-free constructor for SQL generation. */ public DerbyDialect() { super(org.eclipse.daanse.sql.dialect.api.DialectInitData.ansiDefaults()); 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 b3b909a..14b0c0f 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 @@ -53,6 +53,21 @@ public class DuckDbDialect extends AbstractJdbcDialect { private static final String SUPPORTED_PRODUCT_NAME = "DUCKDB"; + /** + * {@code SMALLINT}. + * + *

+ * DuckDB has a native BOOLEAN, and it is still the wrong choice here: the + * consumers read boolean levels back with {@code ResultSet.getInt}, and + * DuckDB answers a native boolean column with the string "true", which + * {@code getInt} rejects. SMALLINT is what the legacy loader used, for the + * same reason. + */ + @Override + public String booleanTypeName() { + return "SMALLINT"; + } + private volatile org.eclipse.daanse.sql.dialect.api.generator.PaginationGenerator cachedPaginationGenerator; /** JDBC-free constructor for SQL generation. */ 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 5fa5599..da249e1 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 @@ -28,6 +28,26 @@ public class H2Dialect extends AbstractJdbcDialect { private static final String SUPPORTED_PRODUCT_NAME = "H2"; + /** + * {@code ANALYZE}. + * + *

+ * H2 samples 10000 rows per table by default; {@code ANALYZE SAMPLE_SIZE 0} + * makes it read every row. + */ + @Override + public java.util.Optional analyzeSchema() { + return java.util.Optional.of("ANALYZE"); + } + + /** + * {@code ANALYZE TABLE }. + */ + @Override + public java.util.Optional analyzeTable(org.eclipse.daanse.sql.model.schema.TableReference table) { + return java.util.Optional.of("ANALYZE TABLE " + qualified(table)); + } + /** JDBC-free constructor for SQL generation. */ public H2Dialect() { super(org.eclipse.daanse.sql.dialect.api.DialectInitData.ansiDefaults()); diff --git a/dialect/db/mariadb/src/main/java/org/eclipse/daanse/sql/dialect/db/mariadb/MariaDBDialect.java b/dialect/db/mariadb/src/main/java/org/eclipse/daanse/sql/dialect/db/mariadb/MariaDBDialect.java index c8f406b..bc37b14 100644 --- a/dialect/db/mariadb/src/main/java/org/eclipse/daanse/sql/dialect/db/mariadb/MariaDBDialect.java +++ b/dialect/db/mariadb/src/main/java/org/eclipse/daanse/sql/dialect/db/mariadb/MariaDBDialect.java @@ -31,6 +31,30 @@ public class MariaDBDialect extends MySqlDialect { private static final String SUPPORTED_PRODUCT_NAME = "MARIADB"; + /** + * {@code TINYINT(1)}. + * + *

+ * MariaDB reads BOOLEAN as an alias for TINYINT(1); saying so outright keeps + * the emitted DDL the same as what the server stores. + */ + @Override + public String booleanTypeName() { + return "TINYINT(1)"; + } + + /** + * {@code DATETIME}. + * + *

+ * MariaDB's TIMESTAMP only reaches from 1970 to 2038, like MySQL's. DATETIME + * spans years 1000 to 9999. + */ + @Override + public String timestampTypeName() { + return "DATETIME"; + } + private volatile org.eclipse.daanse.sql.dialect.api.generator.ReturningGenerator cachedReturningGenerator; /** JDBC-free constructor for SQL generation. */ 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 8af6452..7cf672f 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 @@ -50,6 +50,29 @@ public class MicrosoftSqlServerDialect extends AbstractJdbcDialect { private static final String SUPPORTED_PRODUCT_NAME = "MSSQL"; + /** + * {@code BIT}. + * + *

+ * SQL Server has no BOOLEAN at all; BIT is the type that carries it. + */ + @Override + public String booleanTypeName() { + return "BIT"; + } + + /** + * {@code DATETIME}. + * + *

+ * SQL Server's TIMESTAMP is a row-version counter, not a point in time. DATETIME + * is the type that stores one. + */ + @Override + public String timestampTypeName() { + return "DATETIME"; + } + private volatile org.eclipse.daanse.sql.dialect.api.generator.ReturningGenerator cachedReturningGenerator; private volatile org.eclipse.daanse.sql.dialect.api.generator.MergeGenerator cachedMergeGenerator; private volatile org.eclipse.daanse.sql.dialect.api.generator.CteGenerator cachedCteGenerator; 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 c058b8d..0504714 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 @@ -50,6 +50,31 @@ public class MySqlDialect extends AbstractJdbcDialect { private static final String SUPPORTED_PRODUCT_NAME = "MYSQL"; + /** + * {@code TINYINT(1)}. + * + *

+ * MySQL reads BOOLEAN as an alias for TINYINT(1); saying so outright keeps + * the emitted DDL the same as what the server stores. + */ + @Override + public String booleanTypeName() { + return "TINYINT(1)"; + } + + /** + * {@code DATETIME}. + * + *

+ * MySQL's TIMESTAMP only reaches from 1970 to 2038 and is converted to UTC on the + * way in. DATETIME spans years 1000 to 9999 and stores what it is given - + * FoodMart holds birth dates from the 1940s. + */ + @Override + public String timestampTypeName() { + return "DATETIME"; + } + private volatile org.eclipse.daanse.sql.dialect.api.generator.PaginationGenerator cachedPaginationGenerator; private volatile org.eclipse.daanse.sql.dialect.api.generator.MergeGenerator cachedMergeGenerator; private volatile org.eclipse.daanse.sql.dialect.api.generator.CteGenerator cachedCteGenerator; 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 b6f5a09..bf881b6 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 @@ -51,6 +51,31 @@ public class OracleDialect extends AbstractJdbcDialect { private static final String SUPPORTED_PRODUCT_NAME = "ORACLE"; + /** + * {@code SMALLINT}. + * + *

+ * Oracle has no BOOLEAN in SQL before 23c. SMALLINT is an alias for + * {@code NUMBER(38)} and reads back as a number. + */ + @Override + public String booleanTypeName() { + return "SMALLINT"; + } + + /** + * {@code DECIMAL(15,0)}. + * + *

+ * Oracle has no BIGINT: {@code ORA-00902 invalid datatype}. Fifteen + * digits is what the legacy loader used, and it holds every value the + * datasets carry. + */ + @Override + public String bigintTypeName() { + return "DECIMAL(15,0)"; + } + private volatile org.eclipse.daanse.sql.dialect.api.generator.MergeGenerator cachedMergeGenerator; private volatile org.eclipse.daanse.sql.dialect.api.generator.ReturningGenerator cachedReturningGenerator; @@ -64,6 +89,17 @@ public OracleDialect(org.eclipse.daanse.sql.dialect.api.DialectInitData init) { super(init); } + /** + * One. Oracle's {@code VALUES} clause takes a single tuple; loading several + * rows in one statement is written {@code INSERT ALL INTO t VALUES (…) INTO t + * VALUES (…) SELECT 1 FROM dual}, which is a different statement shape, not a + * longer {@code VALUES} list. + */ + @Override + public int maxInsertRows() { + return 1; + } + /** * Oracle 9i+: SQL-2003 * {@code MERGE INTO target USING ... ON ... WHEN MATCHED ... WHEN NOT MATCHED ...}. 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 04e6007..3a0c60f 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 @@ -311,6 +311,25 @@ public String name() { return "postgres"; } + /** + * {@code ANALYZE}. + * + *

+ * Without arguments PostgreSQL analyses every table of the current + * database. Measured over the legacy suite: 5:03 with, 7:18 without, and + * two queries ran into their timeout without it. + */ + @Override + public java.util.Optional analyzeSchema() { + return java.util.Optional.of("ANALYZE"); + } + + /** {@code ANALYZE

}. */ + @Override + public java.util.Optional analyzeTable(org.eclipse.daanse.sql.model.schema.TableReference table) { + return java.util.Optional.of("ANALYZE " + qualified(table)); + } + @Override public org.eclipse.daanse.sql.dialect.api.IdentifierCaseFolding caseFolding() { return org.eclipse.daanse.sql.dialect.api.IdentifierCaseFolding.LOWER; 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 10f2926..648d119 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 @@ -32,6 +32,31 @@ public class SqliteDialect extends AbstractJdbcDialect { private static final String SUPPORTED_PRODUCT_NAME = "SQLITE"; + /** + * No. + * + *

+ * SQLite takes one writer at a time. Loading tables concurrently does not + * divide the work, it makes the connections starve each other on the shared + * cache — which is why the testkit used to compare against this dialect's + * name in two places. + */ + @Override + public boolean supportsParallelLoading() { + return false; + } + + /** + * {@code SMALLINT}. + * + *

+ * SQLite has no boolean type; it stores 0 and 1 in an INTEGER affinity. + */ + @Override + public String booleanTypeName() { + return "SMALLINT"; + } + private volatile org.eclipse.daanse.sql.dialect.api.generator.PaginationGenerator cachedPaginationGenerator; private volatile org.eclipse.daanse.sql.dialect.api.generator.ReturningGenerator cachedReturningGenerator; private volatile org.eclipse.daanse.sql.dialect.api.generator.MergeGenerator cachedMergeGenerator;