From ef9bbe2405bd7b51b37b193712402a235441c8de Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Fri, 2 Oct 2026 21:31:15 +0000 Subject: [PATCH] fix(jdbc): load ordered Sybase view source from system catalogs Closes #10737 --- .../main/java/app/dbx/jdbc/DbxJdbcPlugin.java | 41 ++++ .../java/app/dbx/jdbc/DbxJdbcPluginTest.java | 205 ++++++++++++++++++ 2 files changed, 246 insertions(+) diff --git a/plugins/jdbc/src/main/java/app/dbx/jdbc/DbxJdbcPlugin.java b/plugins/jdbc/src/main/java/app/dbx/jdbc/DbxJdbcPlugin.java index 0cd2ca1b93..fade6bb10a 100644 --- a/plugins/jdbc/src/main/java/app/dbx/jdbc/DbxJdbcPlugin.java +++ b/plugins/jdbc/src/main/java/app/dbx/jdbc/DbxJdbcPlugin.java @@ -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 diff --git a/plugins/jdbc/src/test/java/app/dbx/jdbc/DbxJdbcPluginTest.java b/plugins/jdbc/src/test/java/app/dbx/jdbc/DbxJdbcPluginTest.java index 560bea7728..751d1aa4b0 100644 --- a/plugins/jdbc/src/test/java/app/dbx/jdbc/DbxJdbcPluginTest.java +++ b/plugins/jdbc/src/test/java/app/dbx/jdbc/DbxJdbcPluginTest.java @@ -4477,6 +4477,96 @@ final class DbxJdbcPluginTest { ); } + private static final class SybaseViewSourceDriver implements Driver { + private final String url; + private final List calls; + private final String[] fragments; + + private SybaseViewSourceDriver(String url, List 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 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 calls; @@ -5449,6 +5539,121 @@ final class DbxJdbcPluginTest { } } + @Test + void getObjectSourceReturnsOrderedSybaseViewSourceWithBoundIdentity() throws Exception { + List 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 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 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";