mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-21 06:00:39 +00:00
fix(jdbc): load ordered Sybase view source from system catalogs
Closes #10737
This commit is contained in:
@@ -4366,6 +4366,10 @@ public final class DbxJdbcPlugin {
|
||||
throw new SQLException("Object source not found");
|
||||
}
|
||||
|
||||
if (isSybaseConnection(connection) && "VIEW".equals(normalizeObjectType(objectType))) {
|
||||
return sybaseViewObjectSource(connection, conn, database, schema, name, objectType);
|
||||
}
|
||||
|
||||
if ("TABLE".equals(normalizeObjectType(objectType))) {
|
||||
return genericTableObjectSource(connection, database, schema, name);
|
||||
}
|
||||
@@ -4373,6 +4377,43 @@ public final class DbxJdbcPlugin {
|
||||
throw new SQLException("Object source is not supported by this JDBC driver");
|
||||
}
|
||||
|
||||
private static JsonNode sybaseViewObjectSource(
|
||||
JsonNode connection,
|
||||
Connection conn,
|
||||
String database,
|
||||
String schema,
|
||||
String name,
|
||||
String objectType
|
||||
) throws SQLException {
|
||||
applyExecutionContext(connection, conn, database, schema);
|
||||
String sql = "SELECT sc.text FROM sysobjects so, syscomments sc "
|
||||
+ "WHERE user_name(so.uid) = ? AND so.name = ? AND sc.id = so.id ORDER BY sc.colid";
|
||||
StringBuilder source = new StringBuilder();
|
||||
try (PreparedStatement ps = conn.prepareStatement(sql)) {
|
||||
ps.setString(1, schema);
|
||||
ps.setString(2, name);
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
while (rs.next()) {
|
||||
String fragment = rs.getString(1);
|
||||
if (fragment != null) {
|
||||
source.append(fragment);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
String ddl = source.toString();
|
||||
if (ddl.isBlank()) {
|
||||
throw new SQLException("Object source not found");
|
||||
}
|
||||
|
||||
ObjectNode item = MAPPER.createObjectNode();
|
||||
item.put("name", name);
|
||||
item.put("object_type", objectType);
|
||||
putNullable(item, "schema", emptyToNull(schema));
|
||||
item.put("source", ddl);
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Table source for generic external JDBC drivers (JDBCX wrappers, custom
|
||||
* protocol drivers) that expose no vendor DDL statement: assemble CREATE
|
||||
|
||||
@@ -4477,6 +4477,96 @@ final class DbxJdbcPluginTest {
|
||||
);
|
||||
}
|
||||
|
||||
private static final class SybaseViewSourceDriver implements Driver {
|
||||
private final String url;
|
||||
private final List<String> calls;
|
||||
private final String[] fragments;
|
||||
|
||||
private SybaseViewSourceDriver(String url, List<String> calls, String... fragments) {
|
||||
this.url = url;
|
||||
this.calls = calls;
|
||||
this.fragments = fragments;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Connection connect(String candidateUrl, Properties info) {
|
||||
return acceptsURL(candidateUrl) ? sybaseViewSourceConnection(calls, fragments) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean acceptsURL(String candidateUrl) {
|
||||
return url.equals(candidateUrl);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DriverPropertyInfo[] getPropertyInfo(String candidateUrl, Properties info) {
|
||||
return new DriverPropertyInfo[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMajorVersion() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMinorVersion() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean jdbcCompliant() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.util.logging.Logger getParentLogger() {
|
||||
return java.util.logging.Logger.getGlobal();
|
||||
}
|
||||
}
|
||||
|
||||
private static Connection sybaseViewSourceConnection(List<String> calls, String[] fragments) {
|
||||
return (Connection) Proxy.newProxyInstance(
|
||||
DbxJdbcPluginTest.class.getClassLoader(),
|
||||
new Class<?>[] { Connection.class },
|
||||
(proxy, method, args) -> switch (method.getName()) {
|
||||
case "prepareStatement" -> {
|
||||
calls.add("sql:" + args[0]);
|
||||
ResultSet rows = rowsResultSet(
|
||||
new String[] { "text" },
|
||||
java.util.Arrays.stream(fragments)
|
||||
.map(fragment -> new Object[] { fragment })
|
||||
.toArray(Object[][]::new)
|
||||
);
|
||||
yield (PreparedStatement) Proxy.newProxyInstance(
|
||||
DbxJdbcPluginTest.class.getClassLoader(),
|
||||
new Class<?>[] { PreparedStatement.class },
|
||||
(statementProxy, statementMethod, statementArgs) -> switch (statementMethod.getName()) {
|
||||
case "setString" -> {
|
||||
calls.add("bind:" + statementArgs[0] + ":" + statementArgs[1]);
|
||||
yield null;
|
||||
}
|
||||
case "executeQuery" -> rows;
|
||||
case "close" -> null;
|
||||
default -> defaultValue(statementMethod.getReturnType());
|
||||
}
|
||||
);
|
||||
}
|
||||
case "setCatalog" -> {
|
||||
calls.add("catalog:" + args[0]);
|
||||
yield null;
|
||||
}
|
||||
case "setSchema" -> {
|
||||
calls.add("schema:" + args[0]);
|
||||
yield null;
|
||||
}
|
||||
case "getAutoCommit", "isValid" -> true;
|
||||
case "isClosed" -> false;
|
||||
case "close" -> null;
|
||||
default -> defaultValue(method.getReturnType());
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private static final class SybaseMetadataDriver implements Driver {
|
||||
private final List<String> calls;
|
||||
|
||||
@@ -5449,6 +5539,121 @@ final class DbxJdbcPluginTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void getObjectSourceReturnsOrderedSybaseViewSourceWithBoundIdentity() throws Exception {
|
||||
List<String> calls = new ArrayList<>();
|
||||
String url = "jdbc:sybase:Tds:sybase-view-source-test:5000";
|
||||
Driver driver = new SybaseViewSourceDriver(
|
||||
url,
|
||||
calls,
|
||||
"CREATE VIEW active_users",
|
||||
" AS SELECT id",
|
||||
" FROM users WHERE active = 1"
|
||||
);
|
||||
DriverManager.registerDriver(driver);
|
||||
String connection = """
|
||||
{
|
||||
"connection_string": "%s",
|
||||
"connect_timeout_secs": 30
|
||||
}
|
||||
""".formatted(url);
|
||||
try {
|
||||
JsonNode response = request("getObjectSource", """
|
||||
{
|
||||
"connection": %s,
|
||||
"database": "appdb",
|
||||
"schema": "reporting'owner",
|
||||
"name": "active_'users",
|
||||
"object_type": "VIEW"
|
||||
}
|
||||
""".formatted(connection));
|
||||
|
||||
assertFalse(response.has("error"), response.toString());
|
||||
assertEquals(
|
||||
"CREATE VIEW active_users AS SELECT id FROM users WHERE active = 1",
|
||||
response.path("result").path("source").asText()
|
||||
);
|
||||
assertEquals("reporting'owner", response.path("result").path("schema").asText());
|
||||
assertEquals("VIEW", response.path("result").path("object_type").asText());
|
||||
assertEquals(List.of(
|
||||
"catalog:appdb",
|
||||
"schema:reporting'owner",
|
||||
"sql:SELECT sc.text FROM sysobjects so, syscomments sc "
|
||||
+ "WHERE user_name(so.uid) = ? AND so.name = ? AND sc.id = so.id ORDER BY sc.colid",
|
||||
"bind:1:reporting'owner",
|
||||
"bind:2:active_'users"
|
||||
), calls);
|
||||
} finally {
|
||||
closeAndDeregister(connection, driver);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void getObjectSourceSupportsJtdsSybaseViewSource() throws Exception {
|
||||
List<String> calls = new ArrayList<>();
|
||||
String url = "jdbc:jtds:sybase://sybase-view-source-test:5000/appdb";
|
||||
Driver driver = new SybaseViewSourceDriver(url, calls, "CREATE VIEW audit_log AS SELECT 1");
|
||||
DriverManager.registerDriver(driver);
|
||||
String connection = """
|
||||
{ "connection_string": "%s", "connect_timeout_secs": 30 }
|
||||
""".formatted(url);
|
||||
try {
|
||||
JsonNode response = request("getObjectSource", """
|
||||
{
|
||||
"connection": %s,
|
||||
"database": "appdb",
|
||||
"schema": "dbo",
|
||||
"name": "audit_log",
|
||||
"object_type": "VIEW"
|
||||
}
|
||||
""".formatted(connection));
|
||||
|
||||
assertFalse(response.has("error"), response.toString());
|
||||
assertEquals(
|
||||
"CREATE VIEW audit_log AS SELECT 1",
|
||||
response.path("result").path("source").asText()
|
||||
);
|
||||
assertTrue(calls.contains("bind:1:dbo"), calls.toString());
|
||||
assertTrue(calls.contains("bind:2:audit_log"), calls.toString());
|
||||
} finally {
|
||||
closeAndDeregister(connection, driver);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void getObjectSourceReportsMissingOrEmptySybaseViewSource() throws Exception {
|
||||
assertSybaseViewSourceNotFound("jdbc:sybase:Tds:sybase-view-missing-test:5000");
|
||||
assertSybaseViewSourceNotFound("jdbc:sybase:Tds:sybase-view-empty-test:5000", null, "");
|
||||
}
|
||||
|
||||
private static void assertSybaseViewSourceNotFound(String url, String... fragments) throws Exception {
|
||||
List<String> calls = new ArrayList<>();
|
||||
Driver driver = new SybaseViewSourceDriver(url, calls, fragments);
|
||||
DriverManager.registerDriver(driver);
|
||||
String connection = """
|
||||
{ "connection_string": "%s", "connect_timeout_secs": 30 }
|
||||
""".formatted(url);
|
||||
try {
|
||||
JsonNode response = request("getObjectSource", """
|
||||
{
|
||||
"connection": %s,
|
||||
"database": "appdb",
|
||||
"schema": "dbo",
|
||||
"name": "missing_view",
|
||||
"object_type": "VIEW"
|
||||
}
|
||||
""".formatted(connection));
|
||||
|
||||
assertTrue(response.has("error"), response.toString());
|
||||
assertEquals(
|
||||
"Object source not found",
|
||||
response.path("error").path("message").asText()
|
||||
);
|
||||
} finally {
|
||||
closeAndDeregister(connection, driver);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void getObjectSourceBuildsExecutableTableDdlForPlainJdbcDrivers() throws Exception {
|
||||
String sourceDb = "jdbc:h2:mem:dbx_ddl_src;DB_CLOSE_DELAY=-1";
|
||||
|
||||
Reference in New Issue
Block a user