Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -702,4 +702,106 @@ private static String schemaKey(TableReference tbl) {
String c = tbl.schema().flatMap(SchemaReference::catalog).map(CatalogReference::name).orElse("");
return c + "" + s;
}

// -------------------- Privileges (GRANT/REVOKE) --------------------
// The grantor of a privilege is never rendered — it is implied by the
// session executing the statement.

/** {@code CREATE ROLE "x"}; users are not creatable (no credentials). */
default String createRole(String roleName) {
return "CREATE ROLE " + quoteIdentifier(roleName);
}

/** {@code DROP ROLE "x"}. */
default String dropRole(String roleName) {
return "DROP ROLE " + quoteIdentifier(roleName);
}

/** {@code GRANT <action> ON "s"."t" TO "g" [WITH GRANT OPTION]}. */
default String grantTablePrivilege(String action, TableReference table, String grantee,
boolean withGrantOption) {
return grantTablePrivilege(action, table, List.of(), grantee, withGrantOption);
}

/** {@code GRANT <action> ("c1", "c2") ON "s"."t" TO "g"} — empty columns = whole table. */
default String grantTablePrivilege(String action, TableReference table, List<String> columns,
String grantee, boolean withGrantOption) {
StringBuilder sb = new StringBuilder("GRANT ").append(action);
appendColumnRestriction(sb, columns);
sb.append(" ON ").append(qualified(table)).append(" TO ").append(quoteIdentifier(grantee));
if (withGrantOption) {
sb.append(" WITH GRANT OPTION");
}
return sb.toString();
}

/** {@code REVOKE <action> ON "s"."t" FROM "g"}. */
default String revokeTablePrivilege(String action, TableReference table, String grantee) {
return revokeTablePrivilege(action, table, List.of(), grantee);
}

/** {@code REVOKE <action> ("c1", "c2") ON "s"."t" FROM "g"}. */
default String revokeTablePrivilege(String action, TableReference table, List<String> columns,
String grantee) {
StringBuilder sb = new StringBuilder("REVOKE ").append(action);
appendColumnRestriction(sb, columns);
sb.append(" ON ").append(qualified(table)).append(" FROM ").append(quoteIdentifier(grantee));
return sb.toString();
}

private void appendColumnRestriction(StringBuilder sb, List<String> columns) {
if (columns != null && !columns.isEmpty()) {
sb.append(" (");
for (int i = 0; i < columns.size(); i++) {
if (i > 0) {
sb.append(", ");
}
sb.append(quoteIdentifier(columns.get(i)));
}
sb.append(')');
}
}

/**
* {@code GRANT EXECUTE ON FUNCTION|PROCEDURE "s"."f" TO "g"}. No signature is
* rendered — PG accepts the bare name while it is unambiguous.
*/
default String grantExecute(String schemaName, String routineName, boolean isFunction, String grantee,
boolean withGrantOption) {
StringBuilder sb = new StringBuilder("GRANT EXECUTE ON ")
.append(isFunction ? "FUNCTION " : "PROCEDURE ")
.append(qualifiedRoutine(schemaName, routineName))
.append(" TO ").append(quoteIdentifier(grantee));
if (withGrantOption) {
sb.append(" WITH GRANT OPTION");
}
return sb.toString();
}

/** {@code REVOKE EXECUTE ON FUNCTION|PROCEDURE "s"."f" FROM "g"}. */
default String revokeExecute(String schemaName, String routineName, boolean isFunction, String grantee) {
return "REVOKE EXECUTE ON " + (isFunction ? "FUNCTION " : "PROCEDURE ")
+ qualifiedRoutine(schemaName, routineName) + " FROM " + quoteIdentifier(grantee);
}

/** {@code GRANT "role" TO "g" [WITH ADMIN OPTION]} — role membership. */
default String grantRole(String roleName, String grantee, boolean withAdminOption) {
StringBuilder sb = new StringBuilder("GRANT ").append(quoteIdentifier(roleName))
.append(" TO ").append(quoteIdentifier(grantee));
if (withAdminOption) {
sb.append(" WITH ADMIN OPTION");
}
return sb.toString();
}

/** {@code REVOKE "role" FROM "g"} — removes a role membership. */
default String revokeRole(String roleName, String grantee) {
return "REVOKE " + quoteIdentifier(roleName) + " FROM " + quoteIdentifier(grantee);
}

/** Schema-qualified routine name; the bare name when no schema is given. */
default String qualifiedRoutine(String schemaName, String routineName) {
return schemaName == null || schemaName.isBlank() ? quoteIdentifier(routineName)
: quoteIdentifier(schemaName, routineName);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -148,4 +148,10 @@ public boolean supportsNthValueIgnoreNulls() {
public boolean supportsListAgg() {
return true;
}

/** H2 has no ADMIN OPTION on role grants — the flag is ignored. */
@Override
public String grantRole(String roleName, String grantee, boolean withAdminOption) {
return "GRANT " + quoteIdentifier(roleName) + " TO " + quoteIdentifier(grantee);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -646,4 +646,34 @@ private String spRename(String quotedSourcePath, String newName, String objectTy
sb.append(", '").append(objectType).append("'");
return sb.toString();
}

/** SQL Server memberships are {@code ALTER ROLE}; there is no ADMIN OPTION — the flag is ignored. */
@Override
public String grantRole(String roleName, String grantee, boolean withAdminOption) {
return "ALTER ROLE " + quoteIdentifier(roleName) + " ADD MEMBER " + quoteIdentifier(grantee);
}

@Override
public String revokeRole(String roleName, String grantee) {
return "ALTER ROLE " + quoteIdentifier(roleName) + " DROP MEMBER " + quoteIdentifier(grantee);
}

/** SQL Server takes no FUNCTION/PROCEDURE keyword in GRANT EXECUTE. */
@Override
public String grantExecute(String schemaName, String routineName, boolean isFunction, String grantee,
boolean withGrantOption) {
StringBuilder sb = new StringBuilder("GRANT EXECUTE ON ")
.append(qualifiedRoutine(schemaName, routineName))
.append(" TO ").append(quoteIdentifier(grantee));
if (withGrantOption) {
sb.append(" WITH GRANT OPTION");
}
return sb.toString();
}

@Override
public String revokeExecute(String schemaName, String routineName, boolean isFunction, String grantee) {
return "REVOKE EXECUTE ON " + qualifiedRoutine(schemaName, routineName) + " FROM "
+ quoteIdentifier(grantee);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -619,4 +619,23 @@ public String alterColumnDropDefault(TableReference table, String columnName) {
}

// RENAME COLUMN/TABLE/INDEX/CONSTRAINT inherit the SQL-99 default.

/** Oracle takes no FUNCTION/PROCEDURE keyword in GRANT EXECUTE. */
@Override
public String grantExecute(String schemaName, String routineName, boolean isFunction, String grantee,
boolean withGrantOption) {
StringBuilder sb = new StringBuilder("GRANT EXECUTE ON ")
.append(qualifiedRoutine(schemaName, routineName))
.append(" TO ").append(quoteIdentifier(grantee));
if (withGrantOption) {
sb.append(" WITH GRANT OPTION");
}
return sb.toString();
}

@Override
public String revokeExecute(String schemaName, String routineName, boolean isFunction, String grantee) {
return "REVOKE EXECUTE ON " + qualifiedRoutine(schemaName, routineName) + " FROM "
+ quoteIdentifier(grantee);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,24 @@ default Optional<List<org.eclipse.daanse.sql.jdbc.api.schema.ObjectPrivilege>> g
return Optional.empty();
}

/**
* Role membership edges (GRANT role TO grantee); no JDBC equivalent.
* Empty when the dialect keeps none or the current user may not read them.
*/
default Optional<List<org.eclipse.daanse.sql.jdbc.api.schema.RoleMembership>> getAllRoleMemberships(
Connection connection) throws SQLException {
return Optional.empty();
}

/**
* Database principals (users and roles), built-ins included; no JDBC
* equivalent. Empty when unreadable for the current user.
*/
default Optional<List<org.eclipse.daanse.sql.jdbc.api.schema.DatabasePrincipal>> getAllPrincipals(
Connection connection) throws SQLException {
return Optional.empty();
}

/**
* Per-table alternative to
* {@link java.sql.DatabaseMetaData#getColumnPrivileges(String, String, String, String)}.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,14 @@ default List<org.eclipse.daanse.sql.jdbc.api.schema.ObjectPrivilege> objectPrivi
return List.of();
}

/** @return role membership edges (GRANT role TO grantee), empty list if not available */
default List<org.eclipse.daanse.sql.jdbc.api.schema.RoleMembership> roleMemberships() {
return List.of();
}

/** @return database principals (users and roles), empty list if not available */
default List<org.eclipse.daanse.sql.jdbc.api.schema.DatabasePrincipal> principals() {
return List.of();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* 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;

/**
* A database principal — user or role — from the dialect's principal catalog;
* no JDBC equivalent exists. Built-ins are not filtered.
*/
public interface DatabasePrincipal {

/** Canonical {@link #kind()} for login-capable principals. */
String KIND_USER = "USER";

/** Canonical {@link #kind()} for grantable/joinable principals (roles, groups). */
String KIND_ROLE = "ROLE";

/**
* Canonical {@link #kind()} when the dialect does not separate users from
* roles structurally (MySQL: "little to distinguish them") — no basis for
* provisioning decisions.
*/
String KIND_UNKNOWN = "UNKNOWN";

/** The principal's name. */
String name();

/** Canonical kind: {@link #KIND_USER}, {@link #KIND_ROLE} or {@link #KIND_UNKNOWN}. */
String kind();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* 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 role membership edge — {@code GRANT role TO grantee} — from the dialect's
* membership catalog; no JDBC equivalent exists.
*/
public interface RoleMembership {

/** Principal (user or role) that received the role. */
String grantee();

/** The granted role. */
String role();

/** Principal that granted the membership (or empty if unknown). */
Optional<String> grantor();

/** WITH ADMIN OPTION: "YES", "NO", or empty if unknown. */
Optional<String> adminOption();
}
Original file line number Diff line number Diff line change
Expand Up @@ -269,10 +269,16 @@ protected MetaInfo readMetaInfoWithProvider(Connection connection, DatabaseMetaD
List<org.eclipse.daanse.sql.jdbc.api.schema.ObjectPrivilege> objectPrivileges =
provider.getAllObjectPrivileges(connection, null, null).orElse(List.of());

List<org.eclipse.daanse.sql.jdbc.api.schema.RoleMembership> roleMemberships =
provider.getAllRoleMemberships(connection).orElse(List.of());

List<org.eclipse.daanse.sql.jdbc.api.schema.DatabasePrincipal> principals =
provider.getAllPrincipals(connection).orElse(List.of());

StructureInfo structureInfo = new StructureInfoRecord(catalogs, schemas, tables, columns,
importedKeys, primaryKeys, triggers, sequences, checkConstraints, uniqueConstraints,
userDefinedTypes, viewDefinitions, procedures, functions, materializedViews, partitions,
tablePrivileges, List.copyOf(columnPrivileges), objectPrivileges);
tablePrivileges, List.copyOf(columnPrivileges), objectPrivileges, roleMemberships, principals);
return new MetaInfoRecord(databaseInfo, structureInfo, identifierInfo, typeInfos, indexInfos);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -932,4 +932,47 @@ UniqueConstraint build() {
return new UniqueConstraintRecord(constraintName, tableRef, colRefs);
}
}

@Override
public Optional<List<org.eclipse.daanse.sql.jdbc.api.schema.RoleMembership>> getAllRoleMemberships(Connection connection)
throws SQLException {
// mysql.roles_mapping; needs privileges on the mysql schema.
String sql = """
SELECT User AS grantee, Role AS role_name, Admin_option
FROM mysql.roles_mapping
ORDER BY grantee, role_name
""";
List<org.eclipse.daanse.sql.jdbc.api.schema.RoleMembership> result = new ArrayList<>();
try (PreparedStatement ps = connection.prepareStatement(sql);
ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
result.add(new org.eclipse.daanse.sql.jdbc.record.schema.RoleMembershipRecord(
rs.getString("grantee"), rs.getString("role_name"),
Optional.empty(),
Optional.of("Y".equalsIgnoreCase(rs.getString("Admin_option")) ? "YES" : "NO")));
}
} catch (SQLException e) {
return Optional.empty();
}
return Optional.of(List.copyOf(result));
}

@Override
public Optional<List<org.eclipse.daanse.sql.jdbc.api.schema.DatabasePrincipal>> getAllPrincipals(Connection connection)
throws SQLException {
// MariaDB flags roles first-class (Is_role).
String sql = "SELECT User, Is_role FROM mysql.user ORDER BY User";
List<org.eclipse.daanse.sql.jdbc.api.schema.DatabasePrincipal> result = new ArrayList<>();
try (PreparedStatement ps = connection.prepareStatement(sql);
ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
result.add(new org.eclipse.daanse.sql.jdbc.record.schema.DatabasePrincipalRecord(rs.getString("User"),
"Y".equalsIgnoreCase(rs.getString("Is_role")) ? org.eclipse.daanse.sql.jdbc.api.schema.DatabasePrincipal.KIND_ROLE
: org.eclipse.daanse.sql.jdbc.api.schema.DatabasePrincipal.KIND_USER));
}
} catch (SQLException e) {
return Optional.empty();
}
return Optional.of(List.copyOf(result));
}
}
Loading
Loading