From 6bc46e66452fe20d5562a3373484b1dabbcb8a86 Mon Sep 17 00:00:00 2001 From: Lukas Eder Date: Mon, 8 Sep 2025 11:44:52 +0200 Subject: [PATCH] [jOOQ/jOOQ#18988] Regression: Wrong DDL exported by MetaImpl for MySQL DEFAULT CURRENT_TIMESTAMP columns --- .../org/jooq/meta/ResultQueryDatabase.java | 20 ++++++++ .../meta/clickhouse/ClickHouseDatabase.java | 5 ++ .../org/jooq/meta/derby/DerbyDatabase.java | 5 ++ .../org/jooq/meta/duckdb/DuckDBDatabase.java | 5 ++ .../jooq/meta/firebird/FirebirdDatabase.java | 5 ++ .../java/org/jooq/meta/h2/H2Database.java | 5 ++ .../org/jooq/meta/hsqldb/HSQLDBDatabase.java | 5 ++ .../org/jooq/meta/mysql/MySQLDatabase.java | 23 ++++++++++ .../jooq/meta/postgres/PostgresDatabase.java | 5 ++ .../org/jooq/meta/sqlite/SQLiteDatabase.java | 5 ++ .../org/jooq/meta/trino/TrinoDatabase.java | 5 ++ .../src/main/java/org/jooq/impl/MetaImpl.java | 46 ++++++++++++++++--- jOOQ/src/main/java/org/jooq/impl/MetaSQL.java | 35 +++++++++++++- 13 files changed, 161 insertions(+), 8 deletions(-) diff --git a/jOOQ-meta/src/main/java/org/jooq/meta/ResultQueryDatabase.java b/jOOQ-meta/src/main/java/org/jooq/meta/ResultQueryDatabase.java index 998b87f520..1f7a0b28ac 100644 --- a/jOOQ-meta/src/main/java/org/jooq/meta/ResultQueryDatabase.java +++ b/jOOQ-meta/src/main/java/org/jooq/meta/ResultQueryDatabase.java @@ -232,4 +232,24 @@ public interface ResultQueryDatabase extends Database { + /** + * A query that produces generator expressions for computed columns for a + * set of input schemas. + *

+ * The resulting columns are: + *

    + *
  1. Catalog name
  2. + *
  3. Schema name
  4. + *
  5. Table name
  6. + *
  7. Column name
  8. + *
  9. Generator expression as in {@link DataType#generatedAlwaysAs()}
  10. + *
  11. Generation option as in {@link DataType#generationOption()}
  12. + *
+ * + * @return The query or null if this implementation doesn't + * support the query. + */ + @Internal + @Nullable + ResultQuery> generators(List schemas); } diff --git a/jOOQ-meta/src/main/java/org/jooq/meta/clickhouse/ClickHouseDatabase.java b/jOOQ-meta/src/main/java/org/jooq/meta/clickhouse/ClickHouseDatabase.java index 40440f09d8..2214d75169 100644 --- a/jOOQ-meta/src/main/java/org/jooq/meta/clickhouse/ClickHouseDatabase.java +++ b/jOOQ-meta/src/main/java/org/jooq/meta/clickhouse/ClickHouseDatabase.java @@ -366,6 +366,11 @@ public class ClickHouseDatabase extends AbstractDatabase implements ResultQueryD + @Override + public ResultQuery> generators(List schemas) { + return null; + } + @Override protected List getXMLSchemaCollections0() throws SQLException { List result = new ArrayList<>(); diff --git a/jOOQ-meta/src/main/java/org/jooq/meta/derby/DerbyDatabase.java b/jOOQ-meta/src/main/java/org/jooq/meta/derby/DerbyDatabase.java index 3bea131fc5..2766d1530f 100644 --- a/jOOQ-meta/src/main/java/org/jooq/meta/derby/DerbyDatabase.java +++ b/jOOQ-meta/src/main/java/org/jooq/meta/derby/DerbyDatabase.java @@ -658,6 +658,11 @@ public class DerbyDatabase extends AbstractDatabase implements ResultQueryDataba + + @Override + public ResultQuery> generators(List schemas) { + return null; + } @Override protected List getXMLSchemaCollections0() throws SQLException { diff --git a/jOOQ-meta/src/main/java/org/jooq/meta/duckdb/DuckDBDatabase.java b/jOOQ-meta/src/main/java/org/jooq/meta/duckdb/DuckDBDatabase.java index c037c723ce..dd131d1c74 100644 --- a/jOOQ-meta/src/main/java/org/jooq/meta/duckdb/DuckDBDatabase.java +++ b/jOOQ-meta/src/main/java/org/jooq/meta/duckdb/DuckDBDatabase.java @@ -418,6 +418,11 @@ public class DuckDBDatabase extends AbstractDatabase implements ResultQueryDatab + @Override + public ResultQuery> generators(List schemas) { + return null; + } + @Override public ResultQuery> sequences(List schemas) { return create() diff --git a/jOOQ-meta/src/main/java/org/jooq/meta/firebird/FirebirdDatabase.java b/jOOQ-meta/src/main/java/org/jooq/meta/firebird/FirebirdDatabase.java index e7229caac7..1d9d664452 100644 --- a/jOOQ-meta/src/main/java/org/jooq/meta/firebird/FirebirdDatabase.java +++ b/jOOQ-meta/src/main/java/org/jooq/meta/firebird/FirebirdDatabase.java @@ -709,6 +709,11 @@ public class FirebirdDatabase extends AbstractDatabase implements ResultQueryDat + + @Override + public ResultQuery> generators(List schemas) { + return null; + } @Override protected List getXMLSchemaCollections0() throws SQLException { diff --git a/jOOQ-meta/src/main/java/org/jooq/meta/h2/H2Database.java b/jOOQ-meta/src/main/java/org/jooq/meta/h2/H2Database.java index 228d58dc57..689eccbccb 100644 --- a/jOOQ-meta/src/main/java/org/jooq/meta/h2/H2Database.java +++ b/jOOQ-meta/src/main/java/org/jooq/meta/h2/H2Database.java @@ -774,6 +774,11 @@ public class H2Database extends AbstractDatabase implements ResultQueryDatabase + + @Override + public ResultQuery> generators(List schemas) { + return null; + } @Override protected List getSequences0() throws SQLException { diff --git a/jOOQ-meta/src/main/java/org/jooq/meta/hsqldb/HSQLDBDatabase.java b/jOOQ-meta/src/main/java/org/jooq/meta/hsqldb/HSQLDBDatabase.java index cd706e69f5..5f59192618 100644 --- a/jOOQ-meta/src/main/java/org/jooq/meta/hsqldb/HSQLDBDatabase.java +++ b/jOOQ-meta/src/main/java/org/jooq/meta/hsqldb/HSQLDBDatabase.java @@ -713,6 +713,11 @@ public class HSQLDBDatabase extends AbstractDatabase implements ResultQueryDatab + + @Override + public ResultQuery> generators(List schemas) { + return null; + } @Override protected List getXMLSchemaCollections0() throws SQLException { diff --git a/jOOQ-meta/src/main/java/org/jooq/meta/mysql/MySQLDatabase.java b/jOOQ-meta/src/main/java/org/jooq/meta/mysql/MySQLDatabase.java index fd979cc51d..f63a572430 100644 --- a/jOOQ-meta/src/main/java/org/jooq/meta/mysql/MySQLDatabase.java +++ b/jOOQ-meta/src/main/java/org/jooq/meta/mysql/MySQLDatabase.java @@ -112,6 +112,7 @@ import org.jooq.TableOptions.TableType; // ... import org.jooq.impl.DSL; import org.jooq.impl.QOM.ForeignKeyRule; +import org.jooq.impl.QOM.GenerationOption; import org.jooq.meta.AbstractDatabase; import org.jooq.meta.AbstractIndexDefinition; import org.jooq.meta.ArrayDefinition; @@ -716,6 +717,28 @@ public class MySQLDatabase extends AbstractDatabase implements ResultQueryDataba + + @Override + public ResultQuery> generators(List schemas) { + return create() + .select( + inline(null, VARCHAR).as("table_catalog"), + COLUMNS.TABLE_SCHEMA, + COLUMNS.TABLE_NAME, + COLUMNS.COLUMN_NAME, + generationExpression(COLUMNS.GENERATION_EXPRESSION), + when(COLUMNS.EXTRA.in(inline("VIRTUAL"), inline("VIRTUAL GENERATED")), inline(GenerationOption.VIRTUAL.name())) + .when(COLUMNS.EXTRA.in(inline("PERSISTENT"), inline("STORED GENERATED")), inline(GenerationOption.STORED.name())) + .as("generationOption")) + .from(COLUMNS) + .where(COLUMNS.TABLE_SCHEMA.in(schemas)) + .and(COLUMNS.EXTRA.in( + inline("VIRTUAL"), + inline("VIRTUAL GENERATED"), + inline("PERSISTENT"), + inline("STORED GENERATED"))) + .orderBy(COLUMNS.ORDINAL_POSITION); + } @Override protected List getXMLSchemaCollections0() throws SQLException { diff --git a/jOOQ-meta/src/main/java/org/jooq/meta/postgres/PostgresDatabase.java b/jOOQ-meta/src/main/java/org/jooq/meta/postgres/PostgresDatabase.java index 712390fe0f..5b7642e3c3 100644 --- a/jOOQ-meta/src/main/java/org/jooq/meta/postgres/PostgresDatabase.java +++ b/jOOQ-meta/src/main/java/org/jooq/meta/postgres/PostgresDatabase.java @@ -1082,6 +1082,11 @@ public class PostgresDatabase extends AbstractDatabase implements ResultQueryDat + + @Override + public ResultQuery> generators(List schemas) { + return null; + } @Override protected List getXMLSchemaCollections0() throws SQLException { diff --git a/jOOQ-meta/src/main/java/org/jooq/meta/sqlite/SQLiteDatabase.java b/jOOQ-meta/src/main/java/org/jooq/meta/sqlite/SQLiteDatabase.java index 92839324b3..0fbcb0309a 100644 --- a/jOOQ-meta/src/main/java/org/jooq/meta/sqlite/SQLiteDatabase.java +++ b/jOOQ-meta/src/main/java/org/jooq/meta/sqlite/SQLiteDatabase.java @@ -616,6 +616,11 @@ public class SQLiteDatabase extends AbstractDatabase implements ResultQueryDatab + + @Override + public ResultQuery> generators(List schemas) { + return null; + } @Override protected List getXMLSchemaCollections0() throws SQLException { diff --git a/jOOQ-meta/src/main/java/org/jooq/meta/trino/TrinoDatabase.java b/jOOQ-meta/src/main/java/org/jooq/meta/trino/TrinoDatabase.java index 224b237fd2..f82fbf275c 100644 --- a/jOOQ-meta/src/main/java/org/jooq/meta/trino/TrinoDatabase.java +++ b/jOOQ-meta/src/main/java/org/jooq/meta/trino/TrinoDatabase.java @@ -198,6 +198,11 @@ public class TrinoDatabase extends AbstractDatabase implements ResultQueryDataba + @Override + public ResultQuery> generators(List schemas) { + return null; + } + @Override protected List getCatalogs0() throws SQLException { List result = new ArrayList<>(); diff --git a/jOOQ/src/main/java/org/jooq/impl/MetaImpl.java b/jOOQ/src/main/java/org/jooq/impl/MetaImpl.java index f5dc114403..0d0f5535b4 100644 --- a/jOOQ/src/main/java/org/jooq/impl/MetaImpl.java +++ b/jOOQ/src/main/java/org/jooq/impl/MetaImpl.java @@ -90,7 +90,6 @@ import static org.jooq.impl.SQLDataType.SMALLINT; import static org.jooq.impl.SQLDataType.VARCHAR; import static org.jooq.impl.Tools.EMPTY_OBJECT; import static org.jooq.impl.Tools.EMPTY_SORTFIELD; -import static org.jooq.impl.Tools.anyMatch; import static org.jooq.impl.Tools.flatMap; import static org.jooq.impl.Tools.map; import static org.jooq.tools.StringUtils.defaultIfEmpty; @@ -111,7 +110,6 @@ import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Set; import java.util.function.Consumer; import java.util.function.Function; @@ -153,12 +151,9 @@ import org.jooq.exception.DataAccessException; import org.jooq.exception.DataDefinitionException; import org.jooq.exception.DataTypeException; import org.jooq.exception.SQLDialectNotSupportedException; -import org.jooq.impl.QOM.ForeignKeyRule; +import org.jooq.impl.QOM.GenerationOption; import org.jooq.tools.JooqLogger; import org.jooq.tools.StringUtils; -import org.jooq.tools.jdbc.JDBCUtils; - -import org.jetbrains.annotations.Nullable; /** * An implementation of the public {@link Meta} type. @@ -465,6 +460,8 @@ final class MetaImpl extends AbstractMeta { } } + static final record Generator(String expression, GenerationOption option) {} + private final class MetaSchema extends SchemaImpl { private final boolean empty; private transient volatile Map> columnCache; @@ -472,6 +469,7 @@ final class MetaImpl extends AbstractMeta { private transient volatile Map> sequenceCache; private transient volatile Map sourceCache; private transient volatile Map commentCache; + private transient volatile Map generatorCache; @@ -1001,6 +999,38 @@ final class MetaImpl extends AbstractMeta { return null; } + final Generator generator(String tableName, String columnName) { + if (generatorCache == null) { + String sql = MetaSQL.M_GENERATORS(dialect()); + + if (sql != null) { + Result result = meta(() -> "Error while fetching sources for schema " + this, meta -> + withCatalog(getCatalog(), ctx(meta), ctx -> + ctx.resultQuery(patchSchema(sql), MetaSchema.this.getName()).fetch() + ) + ); + + // TODO Support catalogs as well + generatorCache = new LinkedHashMap<>(); + for (Record r : result) { + String e = r.get(4, String.class); + + if (e != null) { + generatorCache.put( + name(r.get(1, String.class), r.get(2, String.class), r.get(3, String.class)), + new Generator(r.get(4, String.class), r.get(5, GenerationOption.class)) + ); + } + } + } + } + + if (generatorCache != null) + return generatorCache.get(name(MetaSchema.this.getName(), tableName, columnName)); + else + return null; + } + final String comment(String tableName) { return comment(tableName, null); } @@ -1705,6 +1735,10 @@ final class MetaImpl extends AbstractMeta { type = new DefaultDataType(family(), Object.class, typeName); } + // [#18988] MySQL JDBC reports DEFAULT CURRENT_TIMESTAMP columns as generated, not defaulted. + Generator g = schema.generator(getName(), columnName); + if (isGenerated && g == null && MYSQL == family()) + isGenerated = false; diff --git a/jOOQ/src/main/java/org/jooq/impl/MetaSQL.java b/jOOQ/src/main/java/org/jooq/impl/MetaSQL.java index 8af851be66..2c7b7eb774 100644 --- a/jOOQ/src/main/java/org/jooq/impl/MetaSQL.java +++ b/jOOQ/src/main/java/org/jooq/impl/MetaSQL.java @@ -14,6 +14,7 @@ final class MetaSQL { private static final EnumMap M_ENUMS = new EnumMap<>(SQLDialect.class); private static final EnumMap M_SOURCES = new EnumMap<>(SQLDialect.class); private static final EnumMap M_COMMENTS = new EnumMap<>(SQLDialect.class); + private static final EnumMap M_GENERATORS = new EnumMap<>(SQLDialect.class); private static final EnumMap M_TRIGGERS = new EnumMap<>(SQLDialect.class); private static final EnumMap M_SYNONYMS = new EnumMap<>(SQLDialect.class); @@ -47,6 +48,11 @@ final class MetaSQL { return result != null ? result : M_COMMENTS.get(dialect.family()); } + static final String M_GENERATORS(SQLDialect dialect) { + String result = M_GENERATORS.get(dialect); + return result != null ? result : M_GENERATORS.get(dialect.family()); + } + static final String M_TRIGGERS(SQLDialect dialect) { String result = M_TRIGGERS.get(dialect); return result != null ? result : M_TRIGGERS.get(dialect.family()); @@ -254,8 +260,8 @@ final class MetaSQL { - M_ENUMS.put(MARIADB, "with recursive e as (select information_schema.COLUMNS.TABLE_SCHEMA, information_schema.COLUMNS.TABLE_NAME, information_schema.COLUMNS.COLUMN_NAME, regexp_replace(information_schema.COLUMNS.COLUMN_TYPE, 'enum\\((.*)\\)', '$1') as e from information_schema.COLUMNS where information_schema.COLUMNS.DATA_TYPE = 'enum'), l as (select e.TABLE_SCHEMA, e.TABLE_NAME, e.COLUMN_NAME, e, cast('' as char(32767)) as l, 0 as p from e union all select e.TABLE_SCHEMA, e.TABLE_NAME, e.COLUMN_NAME, regexp_replace(e, '''.*?''(?:,|$)(.*)', '$1', 1, 1), replace(regexp_replace(e, '''(.*?)''(?:,|$).*', '$1', 1, 1), '''''', ''''), (p + 1) from l as e where char_length(e) > 0) select l.TABLE_SCHEMA, l.TABLE_NAME, l.COLUMN_NAME, null as DATA_TYPE, l.l, l.p from l where (p > 0 and l.TABLE_SCHEMA in (?)) order by l.TABLE_SCHEMA, l.TABLE_NAME, l.COLUMN_NAME, l.p"); - M_ENUMS.put(MYSQL, "with recursive e as (select information_schema.COLUMNS.TABLE_SCHEMA, information_schema.COLUMNS.TABLE_NAME, information_schema.COLUMNS.COLUMN_NAME, regexp_replace(information_schema.COLUMNS.COLUMN_TYPE, 'enum\\((.*)\\)', '$1') as e from information_schema.COLUMNS where information_schema.COLUMNS.DATA_TYPE = 'enum'), l as (select e.TABLE_SCHEMA, e.TABLE_NAME, e.COLUMN_NAME, e, cast('' as char(32767)) as l, 0 as p from e union all select e.TABLE_SCHEMA, e.TABLE_NAME, e.COLUMN_NAME, regexp_replace(e, '''.*?''(?:,|$)(.*)', '$1', 1, 1), replace(regexp_replace(e, '''(.*?)''(?:,|$).*', '$1', 1, 1), '''''', ''''), (p + 1) from l as e where char_length(e) > 0) select l.TABLE_SCHEMA, l.TABLE_NAME, l.COLUMN_NAME, null as DATA_TYPE, l.l, l.p from l where (p > 0 and l.TABLE_SCHEMA in (?)) order by l.TABLE_SCHEMA, l.TABLE_NAME, l.COLUMN_NAME, l.p"); + M_ENUMS.put(MARIADB, "with recursive e as (select information_schema.COLUMNS.TABLE_SCHEMA, information_schema.COLUMNS.TABLE_NAME, information_schema.COLUMNS.COLUMN_NAME, regexp_replace(information_schema.COLUMNS.COLUMN_TYPE, 'enum\\\\((.*)\\\\)', '$1') as e from information_schema.COLUMNS where information_schema.COLUMNS.DATA_TYPE = 'enum'), l as (select e.TABLE_SCHEMA, e.TABLE_NAME, e.COLUMN_NAME, e, cast('' as char(32767)) as l, 0 as p from e union all select e.TABLE_SCHEMA, e.TABLE_NAME, e.COLUMN_NAME, regexp_replace(e, '''.*?''(?:,|$)(.*)', '$1', 1, 1), replace(regexp_replace(e, '''(.*?)''(?:,|$).*', '$1', 1, 1), '''''', ''''), (p + 1) from l as e where char_length(e) > 0) select l.TABLE_SCHEMA, l.TABLE_NAME, l.COLUMN_NAME, null as DATA_TYPE, l.l, l.p from l where (p > 0 and l.TABLE_SCHEMA in (?)) order by l.TABLE_SCHEMA, l.TABLE_NAME, l.COLUMN_NAME, l.p"); + M_ENUMS.put(MYSQL, "with recursive e as (select information_schema.COLUMNS.TABLE_SCHEMA, information_schema.COLUMNS.TABLE_NAME, information_schema.COLUMNS.COLUMN_NAME, regexp_replace(information_schema.COLUMNS.COLUMN_TYPE, 'enum\\\\((.*)\\\\)', '$1') as e from information_schema.COLUMNS where information_schema.COLUMNS.DATA_TYPE = 'enum'), l as (select e.TABLE_SCHEMA, e.TABLE_NAME, e.COLUMN_NAME, e, cast('' as char(32767)) as l, 0 as p from e union all select e.TABLE_SCHEMA, e.TABLE_NAME, e.COLUMN_NAME, regexp_replace(e, '''.*?''(?:,|$)(.*)', '$1', 1, 1), replace(regexp_replace(e, '''(.*?)''(?:,|$).*', '$1', 1, 1), '''''', ''''), (p + 1) from l as e where char_length(e) > 0) select l.TABLE_SCHEMA, l.TABLE_NAME, l.COLUMN_NAME, null as DATA_TYPE, l.l, l.p from l where (p > 0 and l.TABLE_SCHEMA in (?)) order by l.TABLE_SCHEMA, l.TABLE_NAME, l.COLUMN_NAME, l.p"); @@ -425,6 +431,31 @@ final class MetaSQL { + + + + M_GENERATORS.put(MARIADB, "select null as table_catalog, information_schema.COLUMNS.TABLE_SCHEMA, information_schema.COLUMNS.TABLE_NAME, information_schema.COLUMNS.COLUMN_NAME, information_schema.COLUMNS.GENERATION_EXPRESSION, case when information_schema.COLUMNS.EXTRA in ('VIRTUAL', 'VIRTUAL GENERATED') then 'VIRTUAL' when information_schema.COLUMNS.EXTRA in ('PERSISTENT', 'STORED GENERATED') then 'STORED' end as generationOption from information_schema.COLUMNS where (information_schema.COLUMNS.TABLE_SCHEMA in (?) and information_schema.COLUMNS.EXTRA in ('VIRTUAL', 'VIRTUAL GENERATED', 'PERSISTENT', 'STORED GENERATED')) order by information_schema.COLUMNS.ORDINAL_POSITION"); + M_GENERATORS.put(MYSQL, "select null as table_catalog, information_schema.COLUMNS.TABLE_SCHEMA, information_schema.COLUMNS.TABLE_NAME, information_schema.COLUMNS.COLUMN_NAME, information_schema.COLUMNS.GENERATION_EXPRESSION, case when information_schema.COLUMNS.EXTRA in ('VIRTUAL', 'VIRTUAL GENERATED') then 'VIRTUAL' when information_schema.COLUMNS.EXTRA in ('PERSISTENT', 'STORED GENERATED') then 'STORED' end as generationOption from information_schema.COLUMNS where (information_schema.COLUMNS.TABLE_SCHEMA in (?) and information_schema.COLUMNS.EXTRA in ('VIRTUAL', 'VIRTUAL GENERATED', 'PERSISTENT', 'STORED GENERATED')) order by information_schema.COLUMNS.ORDINAL_POSITION"); + + + + + + + + + + + + + + + + + + + +