From 6e5e2250b01cf8528e9f5e2c1dc6af3068403677 Mon Sep 17 00:00:00 2001 From: Stefan Bischof Date: Mon, 20 Jul 2026 17:54:48 +0200 Subject: [PATCH] feat(jdbc): expose privileges and richer trigger metadata Add ObjectPrivilege for non-table grants (schema, routine, sequence) and surface table/column/object privileges through StructureInfo, with compatibility constructors keeping the plain-JDBC path unchanged. Trigger now carries all DML events plus the WHEN condition; event() still returns the first event. Oracle and PostgreSQL providers query the new data from their catalog views. Signed-off-by: Stefan Bischof --- .../daanse/sql/jdbc/api/MetadataProvider.java | 9 + .../sql/jdbc/api/meta/StructureInfo.java | 15 ++ .../sql/jdbc/api/schema/ObjectPrivilege.java | 48 ++++++ .../sql/jdbc/impl/DatabaseServiceImpl.java | 16 +- .../jdbc/metadata/OracleMetadataProvider.java | 91 +++++++--- .../metadata/PostgreSqlMetadataProvider.java | 156 ++++++++++++++++-- .../jdbc/record/meta/StructureInfoRecord.java | 35 +++- .../record/schema/ObjectPrivilegeRecord.java | 29 ++++ .../sql/jdbc/record/schema/TriggerRecord.java | 21 ++- .../daanse/sql/model/schema/Trigger.java | 21 +++ 10 files changed, 405 insertions(+), 36 deletions(-) create mode 100644 jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/ObjectPrivilege.java create mode 100644 jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/ObjectPrivilegeRecord.java diff --git a/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/MetadataProvider.java b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/MetadataProvider.java index f14d4c4..d341ef1 100644 --- a/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/MetadataProvider.java +++ b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/MetadataProvider.java @@ -359,6 +359,15 @@ default Optional> getAllTablePrivileges(Connection connecti return Optional.empty(); } + /** + * Privileges on non-table objects (schema, database, routine, sequence, ...); + * no JDBC equivalent exists. + */ + default Optional> getAllObjectPrivileges( + Connection connection, String catalog, String schema) throws SQLException { + return Optional.empty(); + } + /** * Per-table alternative to * {@link java.sql.DatabaseMetaData#getColumnPrivileges(String, String, String, String)}. diff --git a/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/meta/StructureInfo.java b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/meta/StructureInfo.java index b824ae2..335ab39 100644 --- a/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/meta/StructureInfo.java +++ b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/meta/StructureInfo.java @@ -95,4 +95,19 @@ default List partitions() { return List.of(); } + /** @return the table privileges, empty list if not available */ + default List tablePrivileges() { + return List.of(); + } + + /** @return the column privileges, empty list if not available */ + default List columnPrivileges() { + return List.of(); + } + + /** @return privileges on non-table objects (schema, database, routine, ...), empty list if not available */ + default List objectPrivileges() { + return List.of(); + } + } diff --git a/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/ObjectPrivilege.java b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/ObjectPrivilege.java new file mode 100644 index 0000000..0b3c64c --- /dev/null +++ b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/ObjectPrivilege.java @@ -0,0 +1,48 @@ +/* +* 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.api.schema; + +import java.util.Optional; + +/** + * A privilege granted on a non-table securable object — schema, database, + * function, procedure, package, sequence, directory, ... Table and column + * privileges stay in {@link TablePrivilege}/{@link ColumnPrivilege}. + */ +public interface ObjectPrivilege { + + /** Raw object kind as the dialect reports it: SCHEMA, DATABASE, FUNCTION, SEQUENCE, ... */ + String objectKind(); + + /** @return the catalog/database qualifier, or empty */ + Optional catalogName(); + + /** @return the schema qualifier, or empty (schema- and database-level rows) */ + Optional schemaName(); + + /** The object's name. */ + String objectName(); + + /** Principal that granted the privilege (or empty if unknown). */ + Optional grantor(); + + /** Principal that received the privilege. */ + String grantee(); + + /** Privilege kind: USAGE, CREATE, EXECUTE, CONNECT, ... */ + String privilege(); + + /** "YES", "NO", or empty if unknown. */ + Optional isGrantable(); +} diff --git a/jdbc/impl/src/main/java/org/eclipse/daanse/sql/jdbc/impl/DatabaseServiceImpl.java b/jdbc/impl/src/main/java/org/eclipse/daanse/sql/jdbc/impl/DatabaseServiceImpl.java index 31c64d4..ed65d05 100644 --- a/jdbc/impl/src/main/java/org/eclipse/daanse/sql/jdbc/impl/DatabaseServiceImpl.java +++ b/jdbc/impl/src/main/java/org/eclipse/daanse/sql/jdbc/impl/DatabaseServiceImpl.java @@ -256,9 +256,23 @@ protected MetaInfo readMetaInfoWithProvider(Connection connection, DatabaseMetaD viewDefinitions = filteredViews; } + // Privileges — only via dialect providers; the plain-JDBC path stays without them. + List tablePrivileges = + provider.getAllTablePrivileges(connection, null, null, null).orElse(List.of()); + List columnPrivileges = new ArrayList<>(); + for (TableDefinition td : tables) { + String tpSchema = td.table().schema().map(SchemaReference::name).orElse(null); + provider.getColumnPrivileges(connection, null, tpSchema, td.table().name(), null) + .ifPresent(columnPrivileges::addAll); + } + + List objectPrivileges = + provider.getAllObjectPrivileges(connection, null, null).orElse(List.of()); + StructureInfo structureInfo = new StructureInfoRecord(catalogs, schemas, tables, columns, importedKeys, primaryKeys, triggers, sequences, checkConstraints, uniqueConstraints, - userDefinedTypes, viewDefinitions, procedures, functions, materializedViews, partitions); + userDefinedTypes, viewDefinitions, procedures, functions, materializedViews, partitions, + tablePrivileges, List.copyOf(columnPrivileges), objectPrivileges); return new MetaInfoRecord(databaseInfo, structureInfo, identifierInfo, typeInfos, indexInfos); } diff --git a/jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/OracleMetadataProvider.java b/jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/OracleMetadataProvider.java index 8ec33a2..7d05c3e 100644 --- a/jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/OracleMetadataProvider.java +++ b/jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/OracleMetadataProvider.java @@ -94,7 +94,7 @@ public class OracleMetadataProvider implements MetadataProvider { @Override public List getAllTriggers(Connection connection, String catalog, String schema) throws SQLException { String sql = """ - SELECT TRIGGER_NAME, TABLE_NAME, TRIGGER_TYPE, TRIGGERING_EVENT, TRIGGER_BODY + SELECT TRIGGER_NAME, TABLE_NAME, TRIGGER_TYPE, TRIGGERING_EVENT, WHEN_CLAUSE, TRIGGER_BODY FROM ALL_TRIGGERS WHERE OWNER = ? ORDER BY TABLE_NAME, TRIGGER_NAME """; String schemaName = resolveSchema(schema, connection); @@ -115,7 +115,7 @@ public List getAllTriggers(Connection connection, String catalog, Strin public List getTriggers(Connection connection, String catalog, String schema, String tableName) throws SQLException { String sql = """ - SELECT TRIGGER_NAME, TABLE_NAME, TRIGGER_TYPE, TRIGGERING_EVENT, TRIGGER_BODY + SELECT TRIGGER_NAME, TABLE_NAME, TRIGGER_TYPE, TRIGGERING_EVENT, WHEN_CLAUSE, TRIGGER_BODY FROM ALL_TRIGGERS WHERE OWNER = ? AND TABLE_NAME = ? ORDER BY TRIGGER_NAME """; @@ -876,6 +876,7 @@ private Trigger readTrigger(ResultSet rs, String schemaName) throws SQLException String tableName = rs.getString("TABLE_NAME"); String triggerType = rs.getString("TRIGGER_TYPE"); String triggeringEvent = rs.getString("TRIGGERING_EVENT"); + String whenClause = rs.getString("WHEN_CLAUSE"); // TRIGGER_BODY is LONG type, wrap in try-catch String triggerBody; @@ -890,10 +891,11 @@ private Trigger readTrigger(ResultSet rs, String schemaName) throws SQLException TableReference tableRef = new TableReference(oSchema, tableName); TriggerTiming timing = mapOracleTriggerTiming(triggerType); - TriggerEvent event = mapOracleTriggerEvent(triggeringEvent); + List events = mapOracleTriggerEvents(triggeringEvent); String orientation = parseOracleOrientation(triggerType); - return new TriggerRecord(new TriggerReference(tableRef, triggerName), timing, event, + return new TriggerRecord(new TriggerReference(tableRef, triggerName), timing, events, + Optional.ofNullable(whenClause).map(String::strip).filter(w -> !w.isEmpty()), Optional.ofNullable(triggerBody), Optional.empty(), Optional.ofNullable(orientation)); } @@ -1088,17 +1090,21 @@ private static TriggerTiming mapOracleTriggerTiming(String triggerType) { } - private static TriggerEvent mapOracleTriggerEvent(String triggeringEvent) { + private static List mapOracleTriggerEvents(String triggeringEvent) { if (triggeringEvent == null) { - return TriggerEvent.INSERT; + return List.of(TriggerEvent.INSERT); } - // Take the first word: 'INSERT OR UPDATE' -> 'INSERT' - String firstWord = triggeringEvent.trim().split("\\s+")[0].toUpperCase(); - return switch (firstWord) { - case "UPDATE" -> TriggerEvent.UPDATE; - case "DELETE" -> TriggerEvent.DELETE; - default -> TriggerEvent.INSERT; - }; + // 'INSERT OR UPDATE OR DELETE' -> [INSERT, UPDATE, DELETE] + List events = new ArrayList<>(); + for (String word : triggeringEvent.trim().toUpperCase().split("\\s+OR\\s+")) { + switch (word.trim()) { + case "INSERT" -> events.add(TriggerEvent.INSERT); + case "UPDATE" -> events.add(TriggerEvent.UPDATE); + case "DELETE" -> events.add(TriggerEvent.DELETE); + default -> LOGGER.debug("Ignoring unsupported Oracle trigger event {}", word); + } + } + return events.isEmpty() ? List.of(TriggerEvent.INSERT) : List.copyOf(events); } @@ -1189,10 +1195,15 @@ public Optional> getAllTablePrivileges(Connection connectio // grants, // and grants made by others are all returned. JDBC's getTablePrivileges only // returns direct grants to the connected user. + // ALL_TAB_PRIVS covers every object type — scope to table-like objects; + // everything else is reported via getAllObjectPrivileges. StringBuilder sql = new StringBuilder(""" - SELECT OWNER, TABLE_NAME, GRANTOR, GRANTEE, PRIVILEGE, GRANTABLE - FROM ALL_TAB_PRIVS - WHERE OWNER = ? + SELECT TABLE_SCHEMA, TABLE_NAME, GRANTOR, GRANTEE, PRIVILEGE, GRANTABLE + FROM ALL_TAB_PRIVS t + WHERE TABLE_SCHEMA = ? + AND EXISTS (SELECT 1 FROM ALL_OBJECTS o WHERE o.OWNER = t.TABLE_SCHEMA + AND o.OBJECT_NAME = t.TABLE_NAME + AND o.OBJECT_TYPE IN ('TABLE', 'VIEW', 'MATERIALIZED VIEW')) """); boolean hasTableFilter = tableNamePattern != null && !tableNamePattern.isBlank() && !"%".equals(tableNamePattern); @@ -1231,6 +1242,39 @@ public Optional> getAllTablePrivileges(Connection connectio } + @Override + public Optional> getAllObjectPrivileges( + Connection connection, String catalog, String schema) throws SQLException { + // Non-table objects from ALL_TAB_PRIVS: procedures, functions, packages, + // sequences, types, directories, ... (bodies excluded). + String sql = """ + SELECT DISTINCT t.TABLE_NAME AS OBJECT_NAME, o.OBJECT_TYPE, t.GRANTOR, t.GRANTEE, + t.PRIVILEGE, t.GRANTABLE + FROM ALL_TAB_PRIVS t + JOIN ALL_OBJECTS o ON o.OWNER = t.TABLE_SCHEMA AND o.OBJECT_NAME = t.TABLE_NAME + WHERE t.TABLE_SCHEMA = ? + AND o.OBJECT_TYPE NOT IN ('TABLE', 'VIEW', 'MATERIALIZED VIEW') + AND o.OBJECT_TYPE NOT LIKE '%BODY' + ORDER BY o.OBJECT_TYPE, OBJECT_NAME, t.PRIVILEGE, t.GRANTEE + """; + String schemaName = resolveSchema(schema, connection); + List result = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + result.add(new org.eclipse.daanse.sql.jdbc.record.schema.ObjectPrivilegeRecord( + rs.getString("OBJECT_TYPE"), Optional.empty(), Optional.of(schemaName), + rs.getString("OBJECT_NAME"), Optional.ofNullable(rs.getString("GRANTOR")), + rs.getString("GRANTEE"), rs.getString("PRIVILEGE"), + Optional.ofNullable(rs.getString("GRANTABLE")))); + } + } + } + return Optional.of(List.copyOf(result)); + } + + @Override public Optional> getColumnPrivileges(Connection connection, String catalog, String schema, String tableName, String columnNamePattern) throws SQLException { @@ -1410,11 +1454,13 @@ public Optional> getA Connection connection, String catalog, String schemaPattern, String tableNamePattern, String columnNamePattern) throws SQLException { String sql = """ - SELECT OWNER, TABLE_NAME, COLUMN_NAME, DATA_TYPE, DATA_LENGTH, DATA_PRECISION, - DATA_SCALE, NULLABLE, DATA_DEFAULT, COLUMN_ID - FROM ALL_TAB_COLS - WHERE OWNER = ? AND HIDDEN_COLUMN = 'NO' - ORDER BY OWNER, TABLE_NAME, COLUMN_ID + SELECT t.OWNER, t.TABLE_NAME, t.COLUMN_NAME, t.DATA_TYPE, t.DATA_LENGTH, t.DATA_PRECISION, + t.DATA_SCALE, t.NULLABLE, t.DATA_DEFAULT, t.COLUMN_ID, c.COMMENTS + FROM ALL_TAB_COLS t + LEFT JOIN ALL_COL_COMMENTS c + ON c.OWNER = t.OWNER AND c.TABLE_NAME = t.TABLE_NAME AND c.COLUMN_NAME = t.COLUMN_NAME + WHERE t.OWNER = ? AND t.HIDDEN_COLUMN = 'NO' + ORDER BY t.OWNER, t.TABLE_NAME, t.COLUMN_ID """; String schemaName = resolveSchema(schemaPattern, connection); List out = new ArrayList<>(); @@ -1433,6 +1479,7 @@ public Optional> getA boolean scaleNull = rs.wasNull(); String nullable = rs.getString("NULLABLE"); String columnDefault = rs.getString("DATA_DEFAULT"); // bufferable here + String comments = rs.getString("COMMENTS"); java.sql.JDBCType jdbcType = mapOracleType(dataType); java.util.OptionalInt size; @@ -1457,7 +1504,7 @@ public Optional> getA org.eclipse.daanse.sql.model.schema.ColumnMetaData meta = new org.eclipse.daanse.sql.jdbc.record.schema.ColumnMetaDataRecord( jdbcType, dataType, size, scale, java.util.OptionalInt.empty(), n, - java.util.OptionalInt.empty(), Optional.empty(), + java.util.OptionalInt.empty(), Optional.ofNullable(comments), Optional.ofNullable(columnDefault).map(String::trim).filter(s -> !s.isEmpty()), org.eclipse.daanse.sql.model.schema.ColumnMetaData.AutoIncrement.UNKNOWN, org.eclipse.daanse.sql.model.schema.ColumnMetaData.GeneratedColumn.UNKNOWN); diff --git a/jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/PostgreSqlMetadataProvider.java b/jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/PostgreSqlMetadataProvider.java index 14dfefb..53bbe7c 100644 --- a/jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/PostgreSqlMetadataProvider.java +++ b/jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/PostgreSqlMetadataProvider.java @@ -81,6 +81,7 @@ public List getAllTriggers(Connection connection, String catalog, Strin String sql = """ SELECT t.tgname AS trigger_name, c.relname AS table_name, n.nspname AS schema_name, pg_get_triggerdef(t.oid) AS definition, + t.tgtype AS tgtype, p.prosrc AS proc_body FROM pg_trigger t JOIN pg_class c ON c.oid = t.tgrelid @@ -109,6 +110,7 @@ public List getTriggers(Connection connection, String catalog, String s String sql = """ SELECT t.tgname AS trigger_name, c.relname AS table_name, n.nspname AS schema_name, pg_get_triggerdef(t.oid) AS definition, + t.tgtype AS tgtype, p.prosrc AS proc_body FROM pg_trigger t JOIN pg_class c ON c.oid = t.tgrelid @@ -459,7 +461,9 @@ public Optional> getAllIndexInfo(Connection connection, String c SELECT c.relname AS table_name, i_class.relname AS index_name, a.attname AS column_name, array_position(ix.indkey, a.attnum) AS ordinal, ix.indisunique AS is_unique, - am.amname AS index_type + am.amname AS index_type, + (ix.indoption[array_position(ix.indkey, a.attnum)] & 1) = 1 AS is_desc, + pg_get_expr(ix.indpred, ix.indrelid) AS filter_condition FROM pg_index ix JOIN pg_class c ON c.oid = ix.indrelid JOIN pg_class i_class ON i_class.oid = ix.indexrelid @@ -482,8 +486,14 @@ a.attname AS column_name, array_position(ix.indkey, a.attnum) AS ordinal, int ordinal = rs.getInt("ordinal"); boolean isUnique = rs.getBoolean("is_unique"); String indexType = rs.getString("index_type"); + boolean isDesc = rs.getBoolean("is_desc"); + boolean noSortOrder = rs.wasNull(); + String filterCondition = rs.getString("filter_condition"); IndexInfoItem.IndexType mappedType = mapPgIndexType(indexType); + // Only ordered access methods (btree) have a sort direction. + Optional ascending = "btree".equals(indexType) && !noSortOrder ? Optional.of(!isDesc) + : Optional.empty(); TableReference tableRef = tableRefs.computeIfAbsent(tableName, k -> { Optional oSchema = Optional @@ -495,7 +505,7 @@ a.attname AS column_name, array_position(ix.indkey, a.attnum) AS ordinal, .map(cn -> new ColumnReference(Optional.of(tableRef), cn)); IndexInfoItem item = new IndexInfoItemRecord(Optional.ofNullable(indexName), mappedType, colRef, - ordinal, Optional.empty(), 0L, 0L, Optional.empty(), isUnique); + ordinal, ascending, 0L, 0L, Optional.ofNullable(filterCondition), isUnique); tableIndexes.computeIfAbsent(tableName, k -> new ArrayList<>()).add(item); } @@ -735,6 +745,10 @@ private Trigger readTrigger(ResultSet rs, String schemaName) throws SQLException String triggerName = rs.getString("trigger_name"); String tableName = rs.getString("table_name"); String definition = rs.getString("definition"); + int tgtype = rs.getInt("tgtype"); + // pg_get_expr(tgqual) refuses trigger WHEN clauses (OLD/NEW span two + // relations), so the guard is carved out of pg_get_triggerdef instead. + String whenClause = parseTriggerWhen(rs.getString("definition")); // pg_proc.prosrc is the procedural source of the function the trigger // calls — this is what callers need to reconstruct the trigger. String procBody = rs.getString("proc_body"); @@ -743,14 +757,44 @@ private Trigger readTrigger(ResultSet rs, String schemaName) throws SQLException TableReference tableRef = new TableReference(oSchema, tableName); TriggerTiming timing = parseTriggerTiming(definition); - TriggerEvent event = parseTriggerEvent(definition); + List events = mapPgTriggerEvents(tgtype, definition); Optional orientation = parseTriggerOrientation(definition); - return new TriggerRecord(new TriggerReference(tableRef, triggerName), timing, event, + return new TriggerRecord(new TriggerReference(tableRef, triggerName), timing, events, + Optional.ofNullable(whenClause).map(String::strip).filter(w -> !w.isEmpty()), Optional.ofNullable(procBody), Optional.ofNullable(definition), orientation); } + /** + * Extract the WHEN guard from a {@code pg_get_triggerdef} definition: + * {@code ... WHEN ((expr)) EXECUTE FUNCTION ...} → {@code (expr)} without + * the outermost parentheses; {@code null} when the trigger has no guard. + */ + private static String parseTriggerWhen(String definition) { + if (definition == null) { + return null; + } + int when = definition.indexOf(" WHEN ("); + if (when < 0) { + return null; + } + int start = when + " WHEN (".length(); + int depth = 1; + for (int i = start; i < definition.length(); i++) { + char c = definition.charAt(i); + if (c == '(') { + depth++; + } else if (c == ')') { + depth--; + if (depth == 0) { + return definition.substring(start, i).strip(); + } + } + } + return null; + } + private static Optional parseTriggerOrientation(String definition) { if (definition == null) return Optional.empty(); @@ -840,18 +884,32 @@ private static TriggerTiming parseTriggerTiming(String definition) { } - private static TriggerEvent parseTriggerEvent(String definition) { - if (definition == null) { - return TriggerEvent.INSERT; + /** + * Decode the {@code pg_trigger.tgtype} event bits (2 = INSERT, 8 = DELETE, + * 16 = UPDATE); falls back to scanning the definition when no bit is set. + */ + private static List mapPgTriggerEvents(int tgtype, String definition) { + List events = new ArrayList<>(); + if ((tgtype & (1 << 2)) != 0) { + events.add(TriggerEvent.INSERT); } - String upper = definition.toUpperCase(); + if ((tgtype & (1 << 4)) != 0) { + events.add(TriggerEvent.UPDATE); + } + if ((tgtype & (1 << 3)) != 0) { + events.add(TriggerEvent.DELETE); + } + if (!events.isEmpty()) { + return List.copyOf(events); + } + String upper = definition == null ? "" : definition.toUpperCase(); if (upper.contains("DELETE")) { - return TriggerEvent.DELETE; + return List.of(TriggerEvent.DELETE); } if (upper.contains("UPDATE")) { - return TriggerEvent.UPDATE; + return List.of(TriggerEvent.UPDATE); } - return TriggerEvent.INSERT; + return List.of(TriggerEvent.INSERT); } @@ -915,6 +973,82 @@ public Optional> getAllTablePrivileges(Connection connectio } + @Override + public Optional> getAllObjectPrivileges( + Connection connection, String catalog, String schema) throws SQLException { + // ACL-based privileges on non-table objects. aclexplode() skips NULL ACLs + // (implicit owner-only default) — matching information_schema behaviour is + // not required here; explicit grants are what matters. + String schemaName = resolveSchema(schema, connection); + List result = new ArrayList<>(); + + String schemaSql = """ + SELECT n.nspname AS object_name, 'SCHEMA' AS object_kind, + pg_get_userbyid(a.grantor) AS grantor, + CASE WHEN a.grantee = 0 THEN 'PUBLIC' ELSE pg_get_userbyid(a.grantee) END AS grantee, + a.privilege_type, a.is_grantable + FROM pg_namespace n, aclexplode(n.nspacl) a + WHERE n.nspname = ? + """; + readObjectPrivileges(connection, schemaSql, schemaName, null, result); + + String databaseSql = """ + SELECT d.datname AS object_name, 'DATABASE' AS object_kind, + pg_get_userbyid(a.grantor) AS grantor, + CASE WHEN a.grantee = 0 THEN 'PUBLIC' ELSE pg_get_userbyid(a.grantee) END AS grantee, + a.privilege_type, a.is_grantable + FROM pg_database d, aclexplode(d.datacl) a + WHERE d.datname = current_database() + """; + readObjectPrivileges(connection, databaseSql, null, null, result); + + String routineSql = """ + SELECT p.proname AS object_name, + CASE WHEN p.prokind = 'p' THEN 'PROCEDURE' ELSE 'FUNCTION' END AS object_kind, + pg_get_userbyid(a.grantor) AS grantor, + CASE WHEN a.grantee = 0 THEN 'PUBLIC' ELSE pg_get_userbyid(a.grantee) END AS grantee, + a.privilege_type, a.is_grantable + FROM pg_proc p + JOIN pg_namespace n ON n.oid = p.pronamespace, aclexplode(p.proacl) a + WHERE n.nspname = ? + """; + readObjectPrivileges(connection, routineSql, schemaName, schemaName, result); + + String sequenceSql = """ + SELECT c.relname AS object_name, 'SEQUENCE' AS object_kind, + pg_get_userbyid(a.grantor) AS grantor, + CASE WHEN a.grantee = 0 THEN 'PUBLIC' ELSE pg_get_userbyid(a.grantee) END AS grantee, + a.privilege_type, a.is_grantable + FROM pg_class c + JOIN pg_namespace n ON n.oid = c.relnamespace, aclexplode(c.relacl) a + WHERE c.relkind = 'S' AND n.nspname = ? + """; + readObjectPrivileges(connection, sequenceSql, schemaName, schemaName, result); + + return Optional.of(List.copyOf(result)); + } + + + private static void readObjectPrivileges(Connection connection, String sql, String parameter, + String schemaName, List result) + throws SQLException { + try (PreparedStatement ps = connection.prepareStatement(sql)) { + if (parameter != null) { + ps.setString(1, parameter); + } + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + result.add(new org.eclipse.daanse.sql.jdbc.record.schema.ObjectPrivilegeRecord( + rs.getString("object_kind"), Optional.empty(), Optional.ofNullable(schemaName), + rs.getString("object_name"), Optional.ofNullable(rs.getString("grantor")), + rs.getString("grantee"), rs.getString("privilege_type"), + Optional.of(rs.getBoolean("is_grantable") ? "YES" : "NO"))); + } + } + } + } + + @Override public Optional> getColumnPrivileges(Connection connection, String catalog, String schema, String tableName, String columnNamePattern) throws SQLException { diff --git a/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/meta/StructureInfoRecord.java b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/meta/StructureInfoRecord.java index fae111c..fd6eabf 100644 --- a/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/meta/StructureInfoRecord.java +++ b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/meta/StructureInfoRecord.java @@ -18,16 +18,19 @@ import org.eclipse.daanse.sql.jdbc.api.meta.StructureInfo; import org.eclipse.daanse.sql.model.schema.CatalogReference; import org.eclipse.daanse.sql.jdbc.api.schema.CheckConstraint; +import org.eclipse.daanse.sql.jdbc.api.schema.ColumnPrivilege; import org.eclipse.daanse.sql.model.schema.ColumnDefinition; import org.eclipse.daanse.sql.jdbc.api.schema.Function; import org.eclipse.daanse.sql.jdbc.api.schema.ImportedKey; import org.eclipse.daanse.sql.jdbc.api.schema.MaterializedView; +import org.eclipse.daanse.sql.jdbc.api.schema.ObjectPrivilege; import org.eclipse.daanse.sql.jdbc.api.schema.Partition; import org.eclipse.daanse.sql.model.schema.PrimaryKey; import org.eclipse.daanse.sql.jdbc.api.schema.Procedure; import org.eclipse.daanse.sql.model.schema.SchemaReference; import org.eclipse.daanse.sql.jdbc.api.schema.Sequence; import org.eclipse.daanse.sql.jdbc.api.schema.TableDefinition; +import org.eclipse.daanse.sql.jdbc.api.schema.TablePrivilege; import org.eclipse.daanse.sql.model.schema.Trigger; import org.eclipse.daanse.sql.jdbc.api.schema.UniqueConstraint; import org.eclipse.daanse.sql.jdbc.api.schema.UserDefinedType; @@ -49,5 +52,35 @@ public record StructureInfoRecord( List procedures, List functions, List materializedViews, - List partitions) implements StructureInfo { + List partitions, + List tablePrivileges, + List columnPrivileges, + List objectPrivileges) implements StructureInfo { + + /** Compatibility constructor without object privileges. */ + public StructureInfoRecord(List catalogs, List schemas, + List tables, List columns, List importedKeys, + List primaryKeys, List triggers, List sequences, + List checkConstraints, List uniqueConstraints, + List userDefinedTypes, List viewDefinitions, + List procedures, List functions, List materializedViews, + List partitions, List tablePrivileges, + List columnPrivileges) { + this(catalogs, schemas, tables, columns, importedKeys, primaryKeys, triggers, sequences, checkConstraints, + uniqueConstraints, userDefinedTypes, viewDefinitions, procedures, functions, materializedViews, + partitions, tablePrivileges, columnPrivileges, List.of()); + } + + /** Compatibility constructor without privileges (plain-JDBC path). */ + public StructureInfoRecord(List catalogs, List schemas, + List tables, List columns, List importedKeys, + List primaryKeys, List triggers, List sequences, + List checkConstraints, List uniqueConstraints, + List userDefinedTypes, List viewDefinitions, + List procedures, List functions, List materializedViews, + List partitions) { + this(catalogs, schemas, tables, columns, importedKeys, primaryKeys, triggers, sequences, checkConstraints, + uniqueConstraints, userDefinedTypes, viewDefinitions, procedures, functions, materializedViews, + partitions, List.of(), List.of()); + } } diff --git a/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/ObjectPrivilegeRecord.java b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/ObjectPrivilegeRecord.java new file mode 100644 index 0000000..df1e3b6 --- /dev/null +++ b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/ObjectPrivilegeRecord.java @@ -0,0 +1,29 @@ +/* +* 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.record.schema; + +import java.util.Optional; + +import org.eclipse.daanse.sql.jdbc.api.schema.ObjectPrivilege; + +public record ObjectPrivilegeRecord( + String objectKind, + Optional catalogName, + Optional schemaName, + String objectName, + Optional grantor, + String grantee, + String privilege, + Optional isGrantable) implements ObjectPrivilege { +} diff --git a/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/TriggerRecord.java b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/TriggerRecord.java index 58d4fcb..038bdc7 100644 --- a/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/TriggerRecord.java +++ b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/TriggerRecord.java @@ -13,6 +13,7 @@ */ package org.eclipse.daanse.sql.jdbc.record.schema; +import java.util.List; import java.util.Optional; import org.eclipse.daanse.sql.model.schema.Trigger; @@ -23,9 +24,27 @@ public record TriggerRecord( TriggerReference reference, TriggerTiming timing, - TriggerEvent event, + List events, + Optional whenCondition, Optional body, Optional fullDefinition, Optional orientation) implements Trigger { + public TriggerRecord { + if (events == null || events.isEmpty()) { + throw new IllegalArgumentException("events must not be empty"); + } + events = List.copyOf(events); + } + + /** Single-event trigger without a {@code WHEN} condition. */ + public TriggerRecord(TriggerReference reference, TriggerTiming timing, TriggerEvent event, Optional body, + Optional fullDefinition, Optional orientation) { + this(reference, timing, List.of(event), Optional.empty(), body, fullDefinition, orientation); + } + + @Override + public TriggerEvent event() { + return events.get(0); + } } diff --git a/model/src/main/java/org/eclipse/daanse/sql/model/schema/Trigger.java b/model/src/main/java/org/eclipse/daanse/sql/model/schema/Trigger.java index a8b11cc..c872621 100644 --- a/model/src/main/java/org/eclipse/daanse/sql/model/schema/Trigger.java +++ b/model/src/main/java/org/eclipse/daanse/sql/model/schema/Trigger.java @@ -13,6 +13,7 @@ */ package org.eclipse.daanse.sql.model.schema; +import java.util.List; import java.util.Optional; public interface Trigger extends Named { @@ -33,6 +34,26 @@ default TableReference table() { TriggerEvent event(); + /** + * All DML events this trigger fires on, in catalog order. Multi-event + * triggers ({@code BEFORE INSERT OR UPDATE}) list every event here while + * {@link #event()} keeps returning the first one. + * + * @return the events, never empty + */ + default List events() { + return List.of(event()); + } + + /** + * @return the {@code WHEN} condition guarding the trigger action (without + * the {@code WHEN} keyword and outer parentheses), or empty if the + * trigger is unconditional or the provider cannot read it + */ + default Optional whenCondition() { + return Optional.empty(); + } + /** @return the trigger body, or empty if not available */ Optional body();