feat(oceanbase-oracle): append object grants in table DDL

This commit is contained in:
Mo-Moore
2026-09-14 17:52:13 +08:00
committed by GitHub
parent 2e3b7da533
commit a7a2e2f434
5 changed files with 555 additions and 2 deletions
@@ -219,6 +219,118 @@ public final class DdlBuilder {
return ddl.toString();
}
/**
* Appends trailing SQL (e.g. GRANT statements) after a CREATE TABLE DDL script.
* Returns the original DDL unchanged when {@code trailingSql} is blank.
*/
public static String appendTrailingSql(String ddl, String trailingSql) {
if (!notBlank(trailingSql)) {
return ddl == null ? "" : ddl;
}
String base = ddl == null ? "" : ddl.trim();
if (base.isEmpty()) {
return trailingSql.trim();
}
if (!base.endsWith(";")) {
base = base + ";";
}
String grants = trailingSql.trim();
return base + "\n\n" + grants + (grants.endsWith(";") ? "" : ";");
}
/**
* Builds Oracle-style object GRANT statements for table and column privileges.
* Privileges are grouped by grantee / grantable / privilege(+columns).
*/
public static String buildOracleObjectGrantSql(
String schema,
String table,
List<OracleObjectPrivilege> privileges
) {
if (privileges == null || privileges.isEmpty() || !notBlank(table)) {
return "";
}
String tableRef = qualifiedName(schema, table, false);
Map<String, List<String>> tablePrivileges = new LinkedHashMap<>();
Map<String, List<String>> columnPrivileges = new LinkedHashMap<>();
for (OracleObjectPrivilege privilege : privileges) {
if (privilege == null || !notBlank(privilege.grantee()) || !notBlank(privilege.privilege())) {
continue;
}
String privilegeName = privilege.privilege().trim().toUpperCase(Locale.ROOT);
String grantableKey = privilege.grantable() ? "1" : "0";
if (notBlank(privilege.columnName())) {
String key = privilege.grantee() + "\u0000" + privilegeName + "\u0000" + grantableKey;
columnPrivileges
.computeIfAbsent(key, ignored -> new ArrayList<>())
.add(privilege.columnName().trim());
} else {
String key = privilege.grantee() + "\u0000" + grantableKey;
List<String> names = tablePrivileges.computeIfAbsent(key, ignored -> new ArrayList<>());
if (!names.contains(privilegeName)) {
names.add(privilegeName);
}
}
}
List<String> statements = new ArrayList<>();
for (Map.Entry<String, List<String>> entry : tablePrivileges.entrySet()) {
String[] parts = entry.getKey().split("\u0000", -1);
String grantee = parts[0];
boolean grantable = "1".equals(parts[1]);
statements.add(formatOracleGrant(
join(entry.getValue(), ", "),
null,
tableRef,
grantee,
grantable
));
}
for (Map.Entry<String, List<String>> entry : columnPrivileges.entrySet()) {
String[] parts = entry.getKey().split("\u0000", -1);
String grantee = parts[0];
String privilegeName = parts[1];
boolean grantable = "1".equals(parts[2]);
List<String> columns = new ArrayList<>();
for (String column : entry.getValue()) {
String quoted = quoteIdent(column, false);
if (!columns.contains(quoted)) {
columns.add(quoted);
}
}
statements.add(formatOracleGrant(
privilegeName,
join(columns, ", "),
tableRef,
grantee,
grantable
));
}
return join(statements, "\n");
}
private static String formatOracleGrant(
String privilegeList,
String columnList,
String tableRef,
String grantee,
boolean grantable
) {
StringBuilder statement = new StringBuilder("GRANT ");
statement.append(privilegeList);
if (notBlank(columnList)) {
statement.append(" (").append(columnList).append(")");
}
statement.append(" ON ").append(tableRef);
statement.append(" TO ").append(quoteIdent(grantee, false));
if (grantable) {
statement.append(" WITH GRANT OPTION");
}
statement.append(";");
return statement.toString();
}
private static List<ForeignKeyGroup> groupForeignKeys(List<ForeignKeyInfo> foreignKeys) {
List<ForeignKeyGroup> result = new ArrayList<>();
Map<String, ForeignKeyGroup> namedGroups = new LinkedHashMap<>();
@@ -0,0 +1,38 @@
package com.dbx.agent;
/**
* Oracle-compatible object privilege row used when reconstructing GRANT DDL.
*/
public final class OracleObjectPrivilege {
private final String grantee;
private final String privilege;
private final boolean grantable;
private final String columnName;
public OracleObjectPrivilege(String grantee, String privilege, boolean grantable) {
this(grantee, privilege, grantable, null);
}
public OracleObjectPrivilege(String grantee, String privilege, boolean grantable, String columnName) {
this.grantee = grantee;
this.privilege = privilege;
this.grantable = grantable;
this.columnName = columnName;
}
public String grantee() {
return grantee;
}
public String privilege() {
return privilege;
}
public boolean grantable() {
return grantable;
}
public String columnName() {
return columnName;
}
}
@@ -750,6 +750,36 @@ class CommonJavaCompatibilityTest {
assertTrue(ddl.contains("\"NAME\" NVARCHAR(100) NOT NULL"));
}
@Test
void appendsOracleObjectGrantSqlAfterTableDdl() {
String ddl = DdlBuilder.buildTableDdl(
"APP",
"USERS",
Collections.singletonList(new ColumnInfo("ID", "NUMBER", false, null, true)),
Collections.emptyList(),
Collections.emptyList()
);
String grants = DdlBuilder.buildOracleObjectGrantSql(
"APP",
"USERS",
Arrays.asList(
new OracleObjectPrivilege("READER", "SELECT", false),
new OracleObjectPrivilege("READER", "INSERT", false),
new OracleObjectPrivilege("ADMIN", "SELECT", true),
new OracleObjectPrivilege("ANALYST", "UPDATE", false, "NAME")
)
);
String combined = DdlBuilder.appendTrailingSql(ddl, grants);
assertTrue(combined.contains("CREATE TABLE \"APP\".\"USERS\""));
assertTrue(combined.contains("GRANT SELECT, INSERT ON \"APP\".\"USERS\" TO \"READER\";"));
assertTrue(combined.contains("GRANT SELECT ON \"APP\".\"USERS\" TO \"ADMIN\" WITH GRANT OPTION;"));
assertTrue(combined.contains("GRANT UPDATE (\"NAME\") ON \"APP\".\"USERS\" TO \"ANALYST\";"));
assertEquals("", DdlBuilder.buildOracleObjectGrantSql("APP", "USERS", Collections.emptyList()));
assertEquals(ddl, DdlBuilder.appendTrailingSql(ddl, " "));
}
private static class MinimalAgent implements DatabaseAgent {
@Override
public void connect(ConnectParams params) {
@@ -14,6 +14,7 @@ import com.dbx.agent.MultiSessionJsonRpcServer;
import com.dbx.agent.MetadataListConstraints;
import com.dbx.agent.ObjectInfo;
import com.dbx.agent.ObjectSource;
import com.dbx.agent.OracleObjectPrivilege;
import com.dbx.agent.TableInfo;
import com.dbx.agent.TriggerInfo;
@@ -556,7 +557,141 @@ public final class OceanBaseOracleAgent extends ConfiguredJdbcAgent {
// Table comment is optional; DDL generation should still succeed without it.
}
return DdlBuilder.buildTableDdl(schema, table, getColumns(schema, table), indexes, foreignKeys, java.util.Collections.emptyList(), false, true, tableComment);
String ddl = DdlBuilder.buildTableDdl(
schema,
table,
getColumns(schema, table),
indexes,
foreignKeys,
java.util.Collections.emptyList(),
false,
true,
tableComment
);
try {
String owner = normalizeSchema(schema);
return DdlBuilder.appendTrailingSql(ddl, queryObjectGrantSql(owner, table));
} catch (RuntimeException | SQLException ignored) {
// Privilege metadata is optional; CREATE TABLE DDL should still succeed without it.
return ddl;
}
}
private String queryObjectGrantSql(String owner, String table) throws SQLException {
List<OracleObjectPrivilege> privileges = new ArrayList<>();
privileges.addAll(queryTablePrivileges(owner, table));
try {
privileges.addAll(queryColumnPrivileges(owner, table));
} catch (SQLException ignored) {
// Column privileges are optional when table-level grants are available.
}
return DdlBuilder.buildOracleObjectGrantSql(owner, table, privileges);
}
private List<OracleObjectPrivilege> queryTablePrivileges(String owner, String table) throws SQLException {
// DBA_TAB_PRIVS shows every object grant (OWNER column). ALL_TAB_PRIVS is limited to
// grants where the session user is owner/grantor/grantee (or PUBLIC), so admins
// browsing another schema often see nothing unless we prefer the DBA view.
return queryFirstAvailablePrivileges(tablePrivilegeQueries(), owner, table, null);
}
private List<OracleObjectPrivilege> queryColumnPrivileges(String owner, String table) throws SQLException {
return queryFirstAvailablePrivileges(columnPrivilegeQueries(), owner, table, "COLUMN_NAME");
}
private List<OracleObjectPrivilege> queryFirstAvailablePrivileges(
List<String> queries,
String owner,
String table,
String columnNameField
) throws SQLException {
SQLException firstError = null;
for (String sql : queries) {
try {
return queryPrivileges(sql, owner, table, columnNameField);
} catch (SQLException error) {
if (firstError == null) {
firstError = error;
} else {
firstError.addSuppressed(error);
}
}
}
throw firstError == null ? new SQLException("No privilege views available") : firstError;
}
private static List<String> tablePrivilegeQueries() {
return List.of(
"""
SELECT GRANTEE, PRIVILEGE, GRANTABLE
FROM DBA_TAB_PRIVS
WHERE OWNER = ? AND TABLE_NAME = ?
ORDER BY GRANTEE, PRIVILEGE
""".stripIndent().trim(),
"""
SELECT GRANTEE, PRIVILEGE, GRANTABLE
FROM SYS.DBA_TAB_PRIVS
WHERE OWNER = ? AND TABLE_NAME = ?
ORDER BY GRANTEE, PRIVILEGE
""".stripIndent().trim(),
"""
SELECT GRANTEE, PRIVILEGE, GRANTABLE
FROM ALL_TAB_PRIVS
WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?
ORDER BY GRANTEE, PRIVILEGE
""".stripIndent().trim()
);
}
private static List<String> columnPrivilegeQueries() {
return List.of(
"""
SELECT GRANTEE, PRIVILEGE, GRANTABLE, COLUMN_NAME
FROM DBA_COL_PRIVS
WHERE OWNER = ? AND TABLE_NAME = ?
ORDER BY GRANTEE, COLUMN_NAME, PRIVILEGE
""".stripIndent().trim(),
"""
SELECT GRANTEE, PRIVILEGE, GRANTABLE, COLUMN_NAME
FROM SYS.DBA_COL_PRIVS
WHERE OWNER = ? AND TABLE_NAME = ?
ORDER BY GRANTEE, COLUMN_NAME, PRIVILEGE
""".stripIndent().trim(),
"""
SELECT GRANTEE, PRIVILEGE, GRANTABLE, COLUMN_NAME
FROM ALL_COL_PRIVS
WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?
ORDER BY GRANTEE, COLUMN_NAME, PRIVILEGE
""".stripIndent().trim()
);
}
private List<OracleObjectPrivilege> queryPrivileges(
String sql,
String owner,
String table,
String columnNameField
) throws SQLException {
List<OracleObjectPrivilege> result = new ArrayList<>();
try (var stmt = requireConnection().prepareStatement(sql)) {
stmt.setString(1, owner);
stmt.setString(2, table);
try (ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
result.add(new OracleObjectPrivilege(
rs.getString("GRANTEE"),
rs.getString("PRIVILEGE"),
isYes(rs.getString("GRANTABLE")),
columnNameField == null ? null : rs.getString(columnNameField)
));
}
}
}
return result;
}
private static boolean isYes(String value) {
return value != null && "YES".equalsIgnoreCase(value.trim());
}
@Override
@@ -439,7 +439,15 @@ class OceanBaseOracleAgentTest {
columnResultSet(new Object[][]{
{"CREATED_AT", "TIMESTAMP", "N", null, null, null, null, "SYSDATE", "Created timestamp", 0},
{"INTERNAL_NOTE", "VARCHAR2", "Y", null, null, 100, 100, null, " ", 0}
})
}),
resultSet(
new String[]{"GRANTEE", "PRIVILEGE", "GRANTABLE"},
new Object[][]{}
),
resultSet(
new String[]{"GRANTEE", "COLUMN_NAME", "PRIVILEGE", "GRANTABLE"},
new Object[][]{}
)
));
String ddl = agent.getTableDdl("APP", "AUDIT_LOG");
@@ -451,6 +459,147 @@ class OceanBaseOracleAgentTest {
);
Assertions.assertTrue(ddl.contains("\"INTERNAL_NOTE\" VARCHAR2(100)"), ddl);
Assertions.assertFalse(ddl.contains("\"INTERNAL_NOTE\" IS"), ddl);
Assertions.assertFalse(ddl.contains("GRANT "), ddl);
}
@Test
void tableDdlAppendsObjectGrantsFromDictionaryViews() {
List<String> sql = new ArrayList<>();
OceanBaseOracleAgent agent = new OceanBaseOracleAgent();
TestSupport.setPrivateConnection(agent, preparedConnection(sql,
resultSet(
new String[]{"INDEX_NAME", "COLUMN_NAME", "COLUMN_POSITION", "UNIQUENESS", "CONSTRAINT_TYPE", "INDEX_TYPE"},
new Object[][]{}
),
resultSet(
new String[]{"CONSTRAINT_NAME", "COLUMN_NAME", "TABLE_NAME", "REF_COLUMN_NAME"},
new Object[][]{}
),
resultSet(
new String[]{"COMMENTS"},
new Object[][]{{null}}
),
columnResultSet(new Object[][]{
{"ID", "NUMBER", "N", 19, 0, null, null, null, null, 1},
{"NAME", "VARCHAR2", "Y", null, null, 100, 100, null, null, 0}
}),
resultSet(
new String[]{"GRANTEE", "PRIVILEGE", "GRANTABLE"},
new Object[][]{
{"READER", "SELECT", "NO"},
{"READER", "INSERT", "NO"},
{"ADMIN", "SELECT", "YES"}
}
),
resultSet(
new String[]{"GRANTEE", "COLUMN_NAME", "PRIVILEGE", "GRANTABLE"},
new Object[][]{
{"ANALYST", "NAME", "UPDATE", "NO"}
}
)
));
String ddl = agent.getTableDdl("APP", "USERS");
Assertions.assertTrue(ddl.contains("CREATE TABLE \"APP\".\"USERS\""), ddl);
Assertions.assertTrue(ddl.contains("GRANT SELECT, INSERT ON \"APP\".\"USERS\" TO \"READER\";"), ddl);
Assertions.assertTrue(
ddl.contains("GRANT SELECT ON \"APP\".\"USERS\" TO \"ADMIN\" WITH GRANT OPTION;"),
ddl
);
Assertions.assertTrue(
ddl.contains("GRANT UPDATE (\"NAME\") ON \"APP\".\"USERS\" TO \"ANALYST\";"),
ddl
);
Assertions.assertTrue(
sql.stream().anyMatch(statement -> statement.toUpperCase(Locale.ROOT).contains("FROM DBA_TAB_PRIVS")),
String.valueOf(sql)
);
Assertions.assertTrue(
sql.stream().anyMatch(statement -> statement.toUpperCase(Locale.ROOT).contains("FROM DBA_COL_PRIVS")),
String.valueOf(sql)
);
}
@Test
void tableDdlFallsBackToAllTabPrivsWhenDbaViewUnavailable() {
List<String> sql = new ArrayList<>();
OceanBaseOracleAgent agent = new OceanBaseOracleAgent();
TestSupport.setPrivateConnection(agent, dbaFallbackPrivilegeConnection(sql,
resultSet(
new String[]{"INDEX_NAME", "COLUMN_NAME", "COLUMN_POSITION", "UNIQUENESS", "CONSTRAINT_TYPE", "INDEX_TYPE"},
new Object[][]{}
),
resultSet(
new String[]{"CONSTRAINT_NAME", "COLUMN_NAME", "TABLE_NAME", "REF_COLUMN_NAME"},
new Object[][]{}
),
resultSet(
new String[]{"COMMENTS"},
new Object[][]{{null}}
),
columnResultSet(new Object[][]{
{"ID", "NUMBER", "N", 19, 0, null, null, null, null, 1}
}),
resultSet(
new String[]{"GRANTEE", "PRIVILEGE", "GRANTABLE"},
new Object[][]{
{"READER", "SELECT", "NO"}
}
),
resultSet(
new String[]{"GRANTEE", "COLUMN_NAME", "PRIVILEGE", "GRANTABLE"},
new Object[][]{}
)
));
String ddl = agent.getTableDdl("APP", "USERS");
Assertions.assertTrue(ddl.contains("GRANT SELECT ON \"APP\".\"USERS\" TO \"READER\";"), ddl);
Assertions.assertTrue(
sql.stream().anyMatch(statement -> statement.toUpperCase(Locale.ROOT).contains("FROM DBA_TAB_PRIVS")),
String.valueOf(sql)
);
Assertions.assertTrue(
sql.stream().anyMatch(statement -> statement.toUpperCase(Locale.ROOT).contains("FROM ALL_TAB_PRIVS")),
String.valueOf(sql)
);
Assertions.assertTrue(
sql.stream().anyMatch(statement -> statement.toUpperCase(Locale.ROOT).contains("FROM SYS.DBA_TAB_PRIVS")),
String.valueOf(sql)
);
}
@Test
void tableDdlKeepsCreateTableWhenPrivilegeQueriesFail() {
List<String> sql = new ArrayList<>();
OceanBaseOracleAgent agent = new OceanBaseOracleAgent();
TestSupport.setPrivateConnection(agent, privilegeFailingDdlConnection(sql,
resultSet(
new String[]{"INDEX_NAME", "COLUMN_NAME", "COLUMN_POSITION", "UNIQUENESS", "CONSTRAINT_TYPE", "INDEX_TYPE"},
new Object[][]{}
),
resultSet(
new String[]{"CONSTRAINT_NAME", "COLUMN_NAME", "TABLE_NAME", "REF_COLUMN_NAME"},
new Object[][]{}
),
resultSet(
new String[]{"COMMENTS"},
new Object[][]{{null}}
),
columnResultSet(new Object[][]{
{"ID", "NUMBER", "N", 19, 0, null, null, null, null, 1}
})
));
String ddl = agent.getTableDdl("APP", "USERS");
Assertions.assertTrue(ddl.contains("CREATE TABLE \"APP\".\"USERS\""), ddl);
Assertions.assertFalse(ddl.contains("GRANT "), ddl);
Assertions.assertTrue(
sql.stream().anyMatch(statement -> statement.toUpperCase(Locale.ROOT).contains("FROM DBA_TAB_PRIVS")),
String.valueOf(sql)
);
}
private static ResultSet columnResultSet(Object[][] rows) {
@@ -496,6 +645,95 @@ class OceanBaseOracleAgentTest {
});
}
private static Connection privilegeFailingDdlConnection(List<String> sql, ResultSet... resultSets) {
int[] resultSetIndex = {0};
return proxy(Connection.class, (method, args) -> {
if ("prepareStatement".equals(method.getName())) {
String statementSql = String.valueOf(args[0]);
sql.add(statementSql);
String normalized = statementSql.toUpperCase(Locale.ROOT);
if (normalized.contains("DBA_TAB_PRIVS")
|| normalized.contains("ALL_TAB_PRIVS")
|| normalized.contains("DBA_COL_PRIVS")
|| normalized.contains("ALL_COL_PRIVS")) {
PreparedStatement failing = proxy(PreparedStatement.class, (statementMethod, statementArgs) -> {
if ("executeQuery".equals(statementMethod.getName())) {
throw new SQLException("privilege view unavailable");
}
if ("setString".equals(statementMethod.getName())
|| "setInt".equals(statementMethod.getName())
|| "close".equals(statementMethod.getName())) {
return null;
}
return defaultValue(statementMethod.getReturnType());
});
return failing;
}
PreparedStatement statement = proxy(PreparedStatement.class, (statementMethod, statementArgs) -> {
if ("executeQuery".equals(statementMethod.getName())) {
int current = Math.min(resultSetIndex[0], resultSets.length - 1);
resultSetIndex[0] += 1;
return resultSets[current];
}
if ("setString".equals(statementMethod.getName())
|| "setInt".equals(statementMethod.getName())
|| "close".equals(statementMethod.getName())) {
return null;
}
return defaultValue(statementMethod.getReturnType());
});
return statement;
}
if ("isClosed".equals(method.getName())) {
return false;
}
return defaultValue(method.getReturnType());
});
}
private static Connection dbaFallbackPrivilegeConnection(List<String> sql, ResultSet... resultSets) {
int[] resultSetIndex = {0};
return proxy(Connection.class, (method, args) -> {
if ("prepareStatement".equals(method.getName())) {
String statementSql = String.valueOf(args[0]);
sql.add(statementSql);
String normalized = statementSql.toUpperCase(Locale.ROOT);
if (normalized.contains("DBA_TAB_PRIVS") || normalized.contains("DBA_COL_PRIVS")) {
PreparedStatement failing = proxy(PreparedStatement.class, (statementMethod, statementArgs) -> {
if ("executeQuery".equals(statementMethod.getName())) {
throw new SQLException("DBA privilege view unavailable");
}
if ("setString".equals(statementMethod.getName())
|| "setInt".equals(statementMethod.getName())
|| "close".equals(statementMethod.getName())) {
return null;
}
return defaultValue(statementMethod.getReturnType());
});
return failing;
}
PreparedStatement statement = proxy(PreparedStatement.class, (statementMethod, statementArgs) -> {
if ("executeQuery".equals(statementMethod.getName())) {
int current = Math.min(resultSetIndex[0], resultSets.length - 1);
resultSetIndex[0] += 1;
return resultSets[current];
}
if ("setString".equals(statementMethod.getName())
|| "setInt".equals(statementMethod.getName())
|| "close".equals(statementMethod.getName())) {
return null;
}
return defaultValue(statementMethod.getReturnType());
});
return statement;
}
if ("isClosed".equals(method.getName())) {
return false;
}
return defaultValue(method.getReturnType());
});
}
private static Connection schemaConnection(List<String> sql, ResultSet resultSet) {
Statement statement = proxy(Statement.class, (method, args) -> {
if ("executeQuery".equals(method.getName())) {