mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-19 16:16:45 +00:00
fix(oceanbase-oracle): include package bodies in object metadata
Closes #10617
This commit is contained in:
+12
-5
@@ -307,8 +307,9 @@ public final class OceanBaseOracleAgent extends ConfiguredJdbcAgent {
|
||||
WHEN 'PROCEDURE' THEN 2
|
||||
WHEN 'FUNCTION' THEN 3
|
||||
WHEN 'PACKAGE' THEN 4
|
||||
WHEN 'SEQUENCE' THEN 5
|
||||
ELSE 6
|
||||
WHEN 'PACKAGE BODY' THEN 5
|
||||
WHEN 'SEQUENCE' THEN 6
|
||||
ELSE 7
|
||||
END, OBJECT_NAME
|
||||
""".stripIndent().trim(),
|
||||
owner,
|
||||
@@ -317,11 +318,17 @@ public final class OceanBaseOracleAgent extends ConfiguredJdbcAgent {
|
||||
);
|
||||
|
||||
List<ObjectInfo> result = new ArrayList<>();
|
||||
try (var stmt = requireConnection().prepareStatement(query.sql)) {
|
||||
String sql = query.sql;
|
||||
if (constraints.hasLimit() || constraints.hasOffset()) {
|
||||
sql += "\nORDER BY DBX_RN";
|
||||
}
|
||||
try (var stmt = requireConnection().prepareStatement(sql)) {
|
||||
bind(stmt, query.args);
|
||||
try (ResultSet rs = stmt.executeQuery()) {
|
||||
while (rs.next()) {
|
||||
result.add(new ObjectInfo(rs.getString(1), rs.getString(2), owner, null));
|
||||
String objectType = rs.getString(2);
|
||||
result.add(new ObjectInfo(rs.getString(1),
|
||||
"PACKAGE BODY".equals(objectType) ? "PACKAGE_BODY" : objectType, owner, null));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -703,7 +710,7 @@ public final class OceanBaseOracleAgent extends ConfiguredJdbcAgent {
|
||||
}
|
||||
|
||||
private static List<String> oceanBaseObjectTypes(MetadataListConstraints constraints) {
|
||||
List<String> supported = List.of("TABLE", "VIEW", "PROCEDURE", "FUNCTION", "PACKAGE", "SEQUENCE", "SYNONYM");
|
||||
List<String> supported = List.of("TABLE", "VIEW", "PROCEDURE", "FUNCTION", "PACKAGE", "PACKAGE BODY", "SEQUENCE", "SYNONYM");
|
||||
if (!constraints.hasObjectTypes()) {
|
||||
return supported;
|
||||
}
|
||||
|
||||
+376
@@ -0,0 +1,376 @@
|
||||
package com.dbx.agent.oceanbaseoracle;
|
||||
|
||||
import com.dbx.agent.MetadataListConstraints;
|
||||
import com.dbx.agent.ObjectInfo;
|
||||
import com.dbx.agent.ObjectSource;
|
||||
import com.dbx.agent.test.TestSupport;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import java.lang.reflect.InvocationHandler;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Deque;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class OceanBaseOracleObjectListTest {
|
||||
@Test
|
||||
void defaultListsIncludeBothPackageTypesAndKeepExistingTypeOrder() {
|
||||
JdbcFixture jdbc = new JdbcFixture();
|
||||
for (int request = 0; request < 4; request++) {
|
||||
JdbcCall call = jdbc.rows(
|
||||
row("T", "TABLE"), row("V", "VIEW"), row("P", "PROCEDURE"),
|
||||
row("F", "FUNCTION"), row("PKG", "PACKAGE"), row("PKG", "PACKAGE BODY"),
|
||||
row("SEQ", "SEQUENCE"), row("SYN", "SYNONYM")
|
||||
);
|
||||
List<ObjectInfo> objects = switch (request) {
|
||||
case 0 -> jdbc.agent.listObjects("APP");
|
||||
case 1 -> jdbc.agent.listObjects("APP", null);
|
||||
case 2 -> jdbc.agent.listObjects("APP", MetadataListConstraints.NONE);
|
||||
default -> jdbc.agent.listObjects("APP", new MetadataListConstraints("", 0, -1, List.of()));
|
||||
};
|
||||
|
||||
assertEquals(List.of("APP", "TABLE", "VIEW", "PROCEDURE", "FUNCTION", "PACKAGE",
|
||||
"PACKAGE BODY", "SEQUENCE", "SYNONYM"), call.args);
|
||||
assertEquals(List.of("TABLE", "VIEW", "PROCEDURE", "FUNCTION", "PACKAGE",
|
||||
"PACKAGE_BODY", "SEQUENCE", "SYNONYM"), objects.stream().map(ObjectInfo::getObject_type).toList());
|
||||
assertEquals(8, objects.size());
|
||||
assertTrue(objects.stream().allMatch(object -> "APP".equals(object.getSchema())));
|
||||
assertTrue(objects.stream().allMatch(object -> object.getValid() == null));
|
||||
assertTrue(call.sql.contains("OBJECT_TYPE IN (?, ?, ?, ?, ?, ?, ?, ?)"), call.sql);
|
||||
assertFalse(call.sql.contains("ROWNUM"), call.sql);
|
||||
assertObjectOrder(call.sql);
|
||||
call.assertClosed();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void packageGroupRetainsSameNameSpecAndBodyWithDistinctProtocolTypes() {
|
||||
JdbcFixture jdbc = new JdbcFixture();
|
||||
JdbcCall call = jdbc.rows(row("Pkg.With\"Quote", "PACKAGE"), row("Pkg.With\"Quote", "PACKAGE BODY"));
|
||||
|
||||
List<ObjectInfo> objects = jdbc.agent.listObjects("Mixed.Owner\"Name",
|
||||
constraints(null, null, null, " package_body ", "package", "PACKAGE BODY"));
|
||||
|
||||
assertEquals(List.of("Mixed.Owner\"Name", "PACKAGE", "PACKAGE BODY"), call.args);
|
||||
assertEquals(List.of(
|
||||
new ObjectInfo("Pkg.With\"Quote", "PACKAGE", "Mixed.Owner\"Name", null),
|
||||
new ObjectInfo("Pkg.With\"Quote", "PACKAGE_BODY", "Mixed.Owner\"Name", null)
|
||||
), objects);
|
||||
assertNotEquals(objects.get(0), objects.get(1));
|
||||
assertFalse(call.sql.contains("Mixed.Owner"), call.sql);
|
||||
assertFalse(call.sql.contains("STATUS"), call.sql);
|
||||
call.assertClosed();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"PACKAGE_BODY", "package body", " package_body "})
|
||||
void bodyOnlyBindsDatabaseSpellingAndReturnsProtocolSpelling(String requestedType) {
|
||||
JdbcFixture jdbc = new JdbcFixture();
|
||||
JdbcCall call = jdbc.rows(row("OnlyBody", "PACKAGE BODY"));
|
||||
|
||||
assertEquals(List.of(new ObjectInfo("OnlyBody", "PACKAGE_BODY", "APP", null)),
|
||||
jdbc.agent.listObjects("APP", constraints(null, null, null, requestedType)));
|
||||
assertEquals(List.of("APP", "PACKAGE BODY"), call.args);
|
||||
assertTrue(call.sql.contains("OBJECT_TYPE IN (?)"), call.sql);
|
||||
call.assertClosed();
|
||||
}
|
||||
|
||||
@Test
|
||||
void specOnlyDoesNotRequestBodies() {
|
||||
JdbcFixture jdbc = new JdbcFixture();
|
||||
JdbcCall call = jdbc.rows(row("OnlySpec", "PACKAGE"));
|
||||
|
||||
assertEquals(List.of(new ObjectInfo("OnlySpec", "PACKAGE", "APP", null)),
|
||||
jdbc.agent.listObjects("APP", constraints(null, null, null, "PACKAGE")));
|
||||
assertEquals(List.of("APP", "PACKAGE"), call.args);
|
||||
call.assertClosed();
|
||||
}
|
||||
|
||||
@Test
|
||||
void filteredPagesKeepSameNameSpecAndBodyAndDoNotApplyOffsetTwice() {
|
||||
JdbcFixture jdbc = new JdbcFixture();
|
||||
List<ObjectInfo> pages = new ArrayList<>();
|
||||
for (int offset = 0; offset < 2; offset++) {
|
||||
JdbcCall call = jdbc.rows(row("Pkg_'%", offset == 0 ? "PACKAGE" : "PACKAGE BODY"));
|
||||
pages.addAll(jdbc.agent.listObjects("APP",
|
||||
constraints("k_'%", 1, offset, "PACKAGE", "PACKAGE_BODY")));
|
||||
|
||||
assertEquals(List.of("APP", "PACKAGE", "PACKAGE BODY", "%K%\\_%'%\\%%", offset + 1, offset), call.args);
|
||||
assertTrue(call.sql.contains("UPPER(OBJECT_NAME) LIKE ? ESCAPE '\\'"), call.sql);
|
||||
assertTrue(call.sql.contains("ROWNUM <= ?"), call.sql);
|
||||
assertTrue(call.sql.contains("WHERE DBX_RN > ?"), call.sql);
|
||||
assertTrue(call.sql.endsWith("ORDER BY DBX_RN"), call.sql);
|
||||
assertTrue(call.sql.indexOf("ORDER BY CASE OBJECT_TYPE") < call.sql.indexOf("WHERE ROWNUM <= ?"), call.sql);
|
||||
assertObjectOrder(call.sql);
|
||||
call.assertClosed();
|
||||
}
|
||||
assertEquals(List.of(new ObjectInfo("Pkg_'%", "PACKAGE", "APP", null),
|
||||
new ObjectInfo("Pkg_'%", "PACKAGE_BODY", "APP", null)), pages);
|
||||
}
|
||||
|
||||
@Test
|
||||
void offsetWithoutLimitKeepsTheOrderedRemainingBody() {
|
||||
JdbcFixture jdbc = new JdbcFixture();
|
||||
JdbcCall call = jdbc.rows(row("PKG", "PACKAGE BODY"));
|
||||
|
||||
assertEquals(List.of(new ObjectInfo("PKG", "PACKAGE_BODY", "APP", null)),
|
||||
jdbc.agent.listObjects("APP", constraints(null, null, 1, "PACKAGE", "PACKAGE_BODY")));
|
||||
assertEquals(List.of("APP", "PACKAGE", "PACKAGE BODY", 1), call.args);
|
||||
assertFalse(call.sql.contains("ROWNUM <= ?"), call.sql);
|
||||
assertTrue(call.sql.endsWith("WHERE DBX_RN > ?\nORDER BY DBX_RN"), call.sql);
|
||||
call.assertClosed();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"TABLE", "VIEW", "PROCEDURE", "FUNCTION", "SEQUENCE", "SYNONYM"})
|
||||
void unrelatedTypeRequestsKeepTheirParametersAndMapping(String type) {
|
||||
JdbcFixture jdbc = new JdbcFixture();
|
||||
JdbcCall call = jdbc.rows(row("Existing", type));
|
||||
|
||||
assertEquals(List.of(new ObjectInfo("Existing", type, "APP", null)),
|
||||
jdbc.agent.listObjects("APP", constraints(null, null, null, type)));
|
||||
assertEquals(List.of("APP", type), call.args);
|
||||
call.assertClosed();
|
||||
}
|
||||
|
||||
@Test
|
||||
void unsupportedTypesDoNotQueryTheDatabase() {
|
||||
JdbcFixture jdbc = new JdbcFixture();
|
||||
|
||||
assertEquals(List.of(), jdbc.agent.listObjects("APP", constraints(null, null, null, "TRIGGER", "TYPE_BODY")));
|
||||
assertEquals(0, jdbc.prepared);
|
||||
}
|
||||
|
||||
@Test
|
||||
void refreshDoesNotRetainPreviousObjectsOrSchema() {
|
||||
JdbcFixture jdbc = new JdbcFixture();
|
||||
JdbcCall first = jdbc.rows(row("PKG", "PACKAGE BODY"));
|
||||
JdbcCall second = jdbc.rows();
|
||||
JdbcCall third = jdbc.rows(row("PKG", "PACKAGE BODY"));
|
||||
|
||||
assertEquals(List.of(new ObjectInfo("PKG", "PACKAGE_BODY", "APP", null)),
|
||||
jdbc.agent.listObjects("APP", constraints(null, null, null, "PACKAGE_BODY")));
|
||||
assertEquals(List.of(), jdbc.agent.listObjects("APP", constraints(null, null, null, "PACKAGE_BODY")));
|
||||
assertEquals(List.of(new ObjectInfo("PKG", "PACKAGE_BODY", "OtherOwner", null)),
|
||||
jdbc.agent.listObjects("OtherOwner", constraints(null, null, null, "PACKAGE_BODY")));
|
||||
assertEquals(List.of("APP", "PACKAGE BODY"), second.args);
|
||||
assertEquals(List.of("OtherOwner", "PACKAGE BODY"), third.args);
|
||||
first.assertClosed();
|
||||
second.assertClosed();
|
||||
third.assertClosed();
|
||||
}
|
||||
|
||||
@Test
|
||||
void permissionFailureRemainsAnErrorAndFollowingRequestCanSucceed() {
|
||||
JdbcFixture jdbc = new JdbcFixture();
|
||||
SQLException denied = new SQLException("ORA-01031: insufficient privileges", "42000", 1031);
|
||||
JdbcCall failed = jdbc.rows();
|
||||
failed.executeFailure = denied;
|
||||
JdbcCall next = jdbc.rows(row("PKG", "PACKAGE BODY"));
|
||||
|
||||
RuntimeException error = assertThrows(RuntimeException.class,
|
||||
() -> jdbc.agent.listObjects("APP", constraints(null, null, null, "PACKAGE_BODY")));
|
||||
assertSame(denied, error.getCause());
|
||||
assertTrue(failed.statementClosed);
|
||||
assertEquals(List.of(new ObjectInfo("PKG", "PACKAGE_BODY", "APP", null)),
|
||||
jdbc.agent.listObjects("APP", constraints(null, null, null, "PACKAGE_BODY")));
|
||||
next.assertClosed();
|
||||
}
|
||||
|
||||
@Test
|
||||
void cancelledResultReadClosesResourcesAndDoesNotReturnPartialObjects() {
|
||||
JdbcFixture jdbc = new JdbcFixture();
|
||||
SQLException cancelled = new SQLException("ORA-01013: user requested cancel of current operation", "72000", 1013);
|
||||
JdbcCall call = jdbc.rows(row("PKG", "PACKAGE"));
|
||||
call.readFailure = cancelled;
|
||||
|
||||
RuntimeException error = assertThrows(RuntimeException.class,
|
||||
() -> jdbc.agent.listObjects("APP", constraints(null, null, null, "PACKAGE", "PACKAGE_BODY")));
|
||||
|
||||
assertSame(cancelled, error.getCause());
|
||||
call.assertClosed();
|
||||
}
|
||||
|
||||
@Test
|
||||
void listedSpecAndBodyOpenTheirSeparateDictionarySources() {
|
||||
JdbcFixture jdbc = new JdbcFixture();
|
||||
jdbc.rows(row("Mixed.Pkg", "PACKAGE"), row("Mixed.Pkg", "PACKAGE BODY"));
|
||||
List<ObjectInfo> objects = jdbc.agent.listObjects("Mixed.Owner", constraints(null, null, null, "PACKAGE", "PACKAGE_BODY"));
|
||||
assertEquals(2, objects.size());
|
||||
for (ObjectInfo object : objects) {
|
||||
String databaseType = object.getObject_type().equals("PACKAGE_BODY") ? "PACKAGE BODY" : "PACKAGE";
|
||||
JdbcCall call = jdbc.rows(row(databaseType + " \"Mixed.Pkg\" AS\n"), row("END;\n"));
|
||||
|
||||
ObjectSource source = jdbc.agent.getObjectSource(object.getSchema(), object.getName(), object.getObject_type());
|
||||
|
||||
assertEquals("CREATE OR REPLACE " + databaseType + " \"Mixed.Pkg\" AS\nEND;", source.getSource());
|
||||
assertEquals(object.getObject_type(), source.getObject_type());
|
||||
assertEquals(List.of("Mixed.Owner", "Mixed.Pkg", databaseType), call.args);
|
||||
assertEquals("SELECT TEXT FROM ALL_SOURCE WHERE OWNER = ? AND NAME = ? AND TYPE = ? ORDER BY LINE", call.sql);
|
||||
call.assertClosed();
|
||||
}
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"PACKAGE", "PACKAGE_BODY"})
|
||||
void emptyDictionarySourceFallsBackToMetadata(String type) {
|
||||
JdbcFixture jdbc = new JdbcFixture();
|
||||
JdbcCall dictionary = jdbc.rows();
|
||||
String ddl = "CREATE OR REPLACE " + type.replace('_', ' ') + " PKG AS END;";
|
||||
JdbcCall metadata = jdbc.rows(row(ddl));
|
||||
|
||||
assertEquals(ddl, jdbc.agent.getObjectSource("APP", "PKG", type).getSource());
|
||||
assertEquals(List.of("APP", "PKG", type.replace('_', ' ')), dictionary.args);
|
||||
assertEquals(List.of(type, "PKG", "APP"), metadata.args);
|
||||
assertTrue(metadata.sql.contains("DBMS_METADATA.GET_DDL"), metadata.sql);
|
||||
dictionary.assertClosed();
|
||||
metadata.assertClosed();
|
||||
}
|
||||
|
||||
@Test
|
||||
void missingBodySourceStaysEmptyWhenBothSourcesAreEmpty() {
|
||||
JdbcFixture jdbc = new JdbcFixture();
|
||||
JdbcCall dictionary = jdbc.rows();
|
||||
JdbcCall metadata = jdbc.rows();
|
||||
|
||||
assertEquals("", jdbc.agent.getObjectSource("APP", "Missing", "PACKAGE_BODY").getSource());
|
||||
dictionary.assertClosed();
|
||||
metadata.assertClosed();
|
||||
}
|
||||
|
||||
@Test
|
||||
void bodySourceErrorsPreserveBothDictionaryAndMetadataFailures() {
|
||||
JdbcFixture jdbc = new JdbcFixture();
|
||||
SQLException denied = new SQLException("ORA-01031: insufficient privileges", "42000", 1031);
|
||||
SQLException unavailable = new SQLException("DBMS_METADATA is unavailable", "42000");
|
||||
JdbcCall dictionary = jdbc.rows();
|
||||
dictionary.executeFailure = denied;
|
||||
JdbcCall metadata = jdbc.rows();
|
||||
metadata.executeFailure = unavailable;
|
||||
|
||||
RuntimeException error = assertThrows(RuntimeException.class,
|
||||
() -> jdbc.agent.getObjectSource("APP", "PKG", "PACKAGE_BODY"));
|
||||
|
||||
assertSame(denied, error.getCause());
|
||||
assertArrayEquals(new Throwable[]{unavailable}, denied.getSuppressed());
|
||||
assertTrue(dictionary.statementClosed);
|
||||
assertTrue(metadata.statementClosed);
|
||||
}
|
||||
|
||||
private static MetadataListConstraints constraints(String filter, Integer limit, Integer offset, String... types) {
|
||||
return new MetadataListConstraints(filter, limit, offset, Arrays.asList(types));
|
||||
}
|
||||
|
||||
private static String[] row(String... columns) {
|
||||
return columns;
|
||||
}
|
||||
|
||||
private static void assertObjectOrder(String sql) {
|
||||
List<String> types = List.of("TABLE", "VIEW", "PROCEDURE", "FUNCTION", "PACKAGE", "PACKAGE BODY", "SEQUENCE");
|
||||
for (int rank = 0; rank < types.size(); rank++) {
|
||||
assertTrue(sql.contains("WHEN '" + types.get(rank) + "' THEN " + rank), sql);
|
||||
}
|
||||
assertTrue(sql.contains("ELSE 7\nEND, OBJECT_NAME"), sql);
|
||||
}
|
||||
|
||||
private static final class JdbcFixture {
|
||||
final OceanBaseOracleAgent agent = new OceanBaseOracleAgent();
|
||||
final Deque<JdbcCall> replies = new ArrayDeque<>();
|
||||
int prepared;
|
||||
|
||||
JdbcFixture() {
|
||||
TestSupport.setPrivateConnection(agent, proxy(Connection.class, (connection, method, args) -> {
|
||||
if (method.getName().equals("prepareStatement")) {
|
||||
prepared++;
|
||||
JdbcCall call = replies.removeFirst();
|
||||
call.sql = (String) args[0];
|
||||
return call.statement();
|
||||
}
|
||||
if (method.getName().equals("isClosed")) {
|
||||
return false;
|
||||
}
|
||||
throw new AssertionError("Unexpected connection method: " + method.getName());
|
||||
}));
|
||||
}
|
||||
|
||||
JdbcCall rows(String[]... rows) {
|
||||
JdbcCall call = new JdbcCall(rows);
|
||||
replies.add(call);
|
||||
return call;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class JdbcCall {
|
||||
final String[][] rows;
|
||||
final List<Object> args = new ArrayList<>();
|
||||
String sql;
|
||||
SQLException executeFailure;
|
||||
SQLException readFailure;
|
||||
boolean statementClosed;
|
||||
boolean resultClosed;
|
||||
|
||||
JdbcCall(String[][] rows) {
|
||||
this.rows = rows;
|
||||
}
|
||||
|
||||
PreparedStatement statement() {
|
||||
return proxy(PreparedStatement.class, (statement, method, values) -> switch (method.getName()) {
|
||||
case "setString", "setInt", "setObject" -> {
|
||||
assertEquals(args.size() + 1, values[0]);
|
||||
args.add(values[1]);
|
||||
yield null;
|
||||
}
|
||||
case "setFetchSize" -> null;
|
||||
case "executeQuery" -> {
|
||||
if (executeFailure != null) {
|
||||
throw executeFailure;
|
||||
}
|
||||
yield resultSet();
|
||||
}
|
||||
case "close" -> {
|
||||
statementClosed = true;
|
||||
yield null;
|
||||
}
|
||||
default -> throw new AssertionError("Unexpected statement method: " + method.getName());
|
||||
});
|
||||
}
|
||||
|
||||
ResultSet resultSet() {
|
||||
int[] cursor = {-1};
|
||||
return proxy(ResultSet.class, (result, method, values) -> switch (method.getName()) {
|
||||
case "next" -> {
|
||||
cursor[0]++;
|
||||
if (cursor[0] >= rows.length && readFailure != null) {
|
||||
throw readFailure;
|
||||
}
|
||||
yield cursor[0] < rows.length;
|
||||
}
|
||||
case "getString" -> rows[cursor[0]][(Integer) values[0] - 1];
|
||||
case "close" -> {
|
||||
resultClosed = true;
|
||||
yield null;
|
||||
}
|
||||
default -> throw new AssertionError("Unexpected result method: " + method.getName());
|
||||
});
|
||||
}
|
||||
|
||||
void assertClosed() {
|
||||
assertTrue(statementClosed, "statement must close");
|
||||
assertTrue(resultClosed, "result set must close");
|
||||
}
|
||||
}
|
||||
|
||||
private static <T> T proxy(Class<T> type, InvocationHandler handler) {
|
||||
return type.cast(Proxy.newProxyInstance(type.getClassLoader(), new Class<?>[]{type}, handler));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user