Skip to content

Commit b89ce02

Browse files
committed
fix(jdbc): return java.sql-shaped DatabaseMetaData results (#156)
Fixes #156 Hrana 3 is the source of truth for transport, values, metadata, batching, transactions, and error reporting. `java.sql` is the source of truth for the JDBC surface. xerial/sqlite-jdbc 3.53.4.0 is the behavioral reference and conformance foundation, never a copy target. ## What changed - `getProcedures`, `getProcedureColumns`, `getColumnPrivileges`, `getTablePrivileges`, `getBestRowIdentifier`, `getVersionColumns`, and `getUDTs` return an empty result set with the `java.sql` column list through the existing synthesized-metadata path, instead of throwing `SQLFeatureNotSupportedException`. - `getColumns` synthesizes the `sqlite_schema` system table and orders rows the way `getTables` does: temporary, system, table, then view, by name. - `getCrossReference` returns no row when a non-empty catalog or schema is requested, because the server reports neither. - `getPrimaryKeys` sorts rows by `COLUMN_NAME`, matching xerial's `order by cn`, while `KEY_SEQ` keeps the declared key position. - `NAMED_PRIMARY_KEY` also matches a constraint name glued to the keywords, as in ``CONSTRAINT`NamedPk`PRIMARY KEY(...)``. ## Red New unit cases at base `3de2ad5` with only the test diff applied: ``` ./mvnw -pl libsql-jdbc -Dtest=LibsqlDatabaseMetaDataTest test Tests run: 53, Failures: 3, Errors: 14 ``` Vendored `DBMetaDataTest` at the same base, with a temporary copy selected: ``` ./mvnw -pl libsql-conformance -am -Dit.test=DBMetaDataTest verify Tests run: 56, Failures: 3, Errors: 8, Skipped: 1 getColumns, getCrossReferenceCatalogSchemaWithQuote, columnOrderOfgetPrimaryKeys, columnOrderOfgetProcedures, columnOrderOfgetProcedurColumns, columnOrderOfgetColumnPrivileges, columnOrderOfgetTablePrivileges, columnOrderOfgetBestRowIdentifier, columnOrderOfgetVersionColumns, columnOrderOfgetUDTs, columnOrderOfgetTables ``` ## Green At head, with the same temporary copy: ``` ./mvnw -pl libsql-conformance -am -Dit.test=DBMetaDataTest verify Tests run: 56, Failures: 0, Errors: 1, Skipped: 1 ``` The single remaining error is `columnOrderOfgetTables` at line 1113: sqld rejects `CREATE TEMP TABLE` with `SQL_PARSE_ERROR`. The xerial control run passes all 56 on the same copy, so this is remote-model divergence, not a driver gap. The driver does not fake TEMP support; the later selection PR records the divergence or excludes the case. `version` is skipped by its compile-option assumption. Full lanes: ``` ./mvnw -pl libsql-jdbc test Tests run: 336, Failures: 0, Errors: 0 ./mvnw -pl libsql-conformance -am verify Tests run: 123, Failures: 0, Errors: 0, Skipped: 1 ./mvnw -pl libsql-conformance -am -Pxerial-control verify Tests run: 100, Failures: 0, Errors: 0, Skipped: 1 ./mvnw test BUILD SUCCESS ./mvnw spotless:check BUILD SUCCESS ./mvnw -pl libsql-jdbc -am package -DskipTests BUILD SUCCESS tools/export-upstream --verify verified ``` Perf, three interleaved runs of `./mvnw -pl libsql-jdbc test`: base median 2.47 s, head median 2.50 s (+1.2%). Logs are in `/tmp/xerial-parity-i156/`. The vendored tree is unchanged. ## Files `libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlDatabaseMetaData.java`, `libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlDatabaseMetaDataTest.java`.
1 parent 3de2ad5 commit b89ce02

2 files changed

Lines changed: 330 additions & 24 deletions

File tree

‎libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlDatabaseMetaData.java‎

Lines changed: 115 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public class LibsqlDatabaseMetaData implements DatabaseMetaData {
2727
private static final Pattern VARCHAR_TYPE = Pattern.compile(".*(CHAR|CLOB|TEXT|BLOB).*");
2828
private static final Pattern FLOAT_TYPE = Pattern.compile(".*(REAL|FLOA|DOUB|DEC|NUM).*");
2929
private static final Pattern NAMED_PRIMARY_KEY = Pattern.compile(
30-
"\\bCONSTRAINT\\s+(\"[^\"]+\"|`[^`]+`|\\[[^\\]]+\\]|'[^']+'|[^\\s]+)\\s+PRIMARY\\s+KEY\\b",
30+
"\\bCONSTRAINT\\s*(\"[^\"]+\"|`[^`]+`|\\[[^\\]]+\\]|'[^']+'|[^\\s]+)\\s*PRIMARY\\s+KEY\\b",
3131
Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
3232
private static final Pattern NAMED_FOREIGN_KEY = Pattern.compile(
3333
"\\bCONSTRAINT\\s+(\"[^\"]+\"|`[^`]+`|\\[[^\\]]+\\]|'[^']+'|[^\\s]+)\\s+FOREIGN\\s+KEY\\b",
@@ -90,6 +90,83 @@ public class LibsqlDatabaseMetaData implements DatabaseMetaData {
9090
0,
9191
null);
9292

93+
private static final StmtResult PROCEDURES_RESULT = emptyMetadataResult(List.of(
94+
new Col("PROCEDURE_CAT", "TEXT"),
95+
new Col("PROCEDURE_SCHEM", "TEXT"),
96+
new Col("PROCEDURE_NAME", "TEXT"),
97+
new Col("UNDEF1", "TEXT"),
98+
new Col("UNDEF2", "TEXT"),
99+
new Col("UNDEF3", "TEXT"),
100+
new Col("REMARKS", "TEXT"),
101+
new Col("PROCEDURE_TYPE", "INTEGER")));
102+
103+
private static final StmtResult PROCEDURE_COLUMNS_RESULT = emptyMetadataResult(List.of(
104+
new Col("PROCEDURE_CAT", "TEXT"),
105+
new Col("PROCEDURE_SCHEM", "TEXT"),
106+
new Col("PROCEDURE_NAME", "TEXT"),
107+
new Col("COLUMN_NAME", "TEXT"),
108+
new Col("COLUMN_TYPE", "INTEGER"),
109+
new Col("DATA_TYPE", "INTEGER"),
110+
new Col("TYPE_NAME", "TEXT"),
111+
new Col("PRECISION", "INTEGER"),
112+
new Col("LENGTH", "INTEGER"),
113+
new Col("SCALE", "INTEGER"),
114+
new Col("RADIX", "INTEGER"),
115+
new Col("NULLABLE", "INTEGER"),
116+
new Col("REMARKS", "TEXT")));
117+
118+
private static final StmtResult COLUMN_PRIVILEGES_RESULT = emptyMetadataResult(List.of(
119+
new Col("TABLE_CAT", "TEXT"),
120+
new Col("TABLE_SCHEM", "TEXT"),
121+
new Col("TABLE_NAME", "TEXT"),
122+
new Col("COLUMN_NAME", "TEXT"),
123+
new Col("GRANTOR", "TEXT"),
124+
new Col("GRANTEE", "TEXT"),
125+
new Col("PRIVILEGE", "TEXT"),
126+
new Col("IS_GRANTABLE", "TEXT")));
127+
128+
private static final StmtResult TABLE_PRIVILEGES_RESULT = emptyMetadataResult(List.of(
129+
new Col("TABLE_CAT", "TEXT"),
130+
new Col("TABLE_SCHEM", "TEXT"),
131+
new Col("TABLE_NAME", "TEXT"),
132+
new Col("GRANTOR", "TEXT"),
133+
new Col("GRANTEE", "TEXT"),
134+
new Col("PRIVILEGE", "TEXT"),
135+
new Col("IS_GRANTABLE", "TEXT")));
136+
137+
private static final StmtResult BEST_ROW_IDENTIFIER_RESULT = emptyMetadataResult(List.of(
138+
new Col("SCOPE", "INTEGER"),
139+
new Col("COLUMN_NAME", "TEXT"),
140+
new Col("DATA_TYPE", "INTEGER"),
141+
new Col("TYPE_NAME", "TEXT"),
142+
new Col("COLUMN_SIZE", "INTEGER"),
143+
new Col("BUFFER_LENGTH", "INTEGER"),
144+
new Col("DECIMAL_DIGITS", "INTEGER"),
145+
new Col("PSEUDO_COLUMN", "INTEGER")));
146+
147+
private static final StmtResult VERSION_COLUMNS_RESULT = emptyMetadataResult(List.of(
148+
new Col("SCOPE", "INTEGER"),
149+
new Col("COLUMN_NAME", "TEXT"),
150+
new Col("DATA_TYPE", "INTEGER"),
151+
new Col("TYPE_NAME", "TEXT"),
152+
new Col("COLUMN_SIZE", "INTEGER"),
153+
new Col("BUFFER_LENGTH", "INTEGER"),
154+
new Col("DECIMAL_DIGITS", "INTEGER"),
155+
new Col("PSEUDO_COLUMN", "INTEGER")));
156+
157+
private static final StmtResult UDTS_RESULT = emptyMetadataResult(List.of(
158+
new Col("TYPE_CAT", "TEXT"),
159+
new Col("TYPE_SCHEM", "TEXT"),
160+
new Col("TYPE_NAME", "TEXT"),
161+
new Col("CLASS_NAME", "TEXT"),
162+
new Col("DATA_TYPE", "INTEGER"),
163+
new Col("REMARKS", "TEXT"),
164+
new Col("BASE_TYPE", "INTEGER")));
165+
166+
private static StmtResult emptyMetadataResult(List<Col> columns) {
167+
return new StmtResult(columns, List.of(), 0, null);
168+
}
169+
93170
private final LibsqlConnection connection;
94171
private String cachedDatabaseProductVersion;
95172

@@ -408,13 +485,22 @@ public ResultSet getTables(String catalog, String schemaPattern, String tableNam
408485
public ResultSet getColumns(String catalog, String schemaPattern, String tableNamePattern, String columnNamePattern) throws SQLException {
409486
String tablePattern = tableNamePattern == null || tableNamePattern.isEmpty() ? "%" : tableNamePattern;
410487
String columnPattern = columnNamePattern == null || columnNamePattern.isEmpty() ? "%" : columnNamePattern;
411-
String sql = "SELECT m.name AS TABLE_NAME, m.sql AS CREATE_SQL, p.cid AS CID, p.name AS COLUMN_NAME, "
412-
+ "p.type AS DECLARED_TYPE, p.\"notnull\" AS NOT_NULL, p.dflt_value AS COLUMN_DEF, p.pk AS PK, "
413-
+ "p.hidden AS HIDDEN FROM sqlite_master m JOIN pragma_table_xinfo(m.name) p "
414-
+ "WHERE m.type IN ('table', 'view')"
415-
+ " AND m.name LIKE '" + escapeLiteral(tablePattern) + "' ESCAPE '\\'"
488+
String sql = "WITH t(TABLE_NAME, CREATE_SQL, TABLE_RANK) AS ("
489+
+ " SELECT 'sqlite_schema' AS TABLE_NAME, NULL AS CREATE_SQL, 1 AS TABLE_RANK"
490+
+ " UNION ALL SELECT NAME, SQL, 1 FROM sqlite_schema"
491+
+ " WHERE NAME LIKE 'sqlite\\_%' ESCAPE '\\'"
492+
+ " UNION ALL SELECT NAME, SQL, 2 FROM sqlite_schema"
493+
+ " WHERE NAME NOT LIKE 'sqlite\\_%' ESCAPE '\\' AND UPPER(TYPE) = 'TABLE'"
494+
+ " UNION ALL SELECT NAME, SQL, 3 FROM sqlite_schema"
495+
+ " WHERE NAME NOT LIKE 'sqlite\\_%' ESCAPE '\\' AND UPPER(TYPE) = 'VIEW'"
496+
+ " UNION ALL SELECT NAME, NULL, 0 FROM sqlite_temp_master"
497+
+ ") SELECT t.TABLE_NAME AS TABLE_NAME, t.CREATE_SQL AS CREATE_SQL, p.cid AS CID, "
498+
+ "p.name AS COLUMN_NAME, p.type AS DECLARED_TYPE, p.\"notnull\" AS NOT_NULL, "
499+
+ "p.dflt_value AS COLUMN_DEF, p.pk AS PK, p.hidden AS HIDDEN "
500+
+ "FROM t JOIN pragma_table_xinfo(t.TABLE_NAME) p "
501+
+ "WHERE t.TABLE_NAME LIKE '" + escapeLiteral(tablePattern) + "' ESCAPE '\\'"
416502
+ " AND p.name LIKE '" + escapeLiteral(columnPattern) + "' ESCAPE '\\'"
417-
+ " ORDER BY m.name, p.cid";
503+
+ " ORDER BY t.TABLE_RANK, t.TABLE_NAME, p.cid";
418504
// getColumns synthesizes its column labels, so the raw SQL must not drive column-origin
419505
// resolution for the result set.
420506
return executeMetadataQuery(sql, null, LibsqlDatabaseMetaData::columnsResult);
@@ -576,6 +662,9 @@ private static StmtResult primaryKeysResult(StmtResult raw, String table) {
576662
Value.of(primaryKeyName(text(values.get(2)))))));
577663
}
578664
}
665+
// xerial orders getPrimaryKeys by column name, not by the declared key sequence.
666+
rows.sort(Comparator.comparing(
667+
row -> text(row.values().get(3)), Comparator.nullsFirst(Comparator.naturalOrder())));
579668
return new StmtResult(PRIMARY_KEY_COLUMNS, rows, 0, null);
580669
}
581670

@@ -822,7 +911,7 @@ public ResultSet getPrimaryKeys(String catalog, String schema, String table) thr
822911
String sql = "SELECT p.name AS COLUMN_NAME, p.pk AS KEY_SEQ, s.sql AS CREATE_SQL "
823912
+ "FROM pragma_table_info('" + escapedTable + "') p "
824913
+ "LEFT JOIN sqlite_schema s ON lower(s.name) = lower('" + escapedTable + "') "
825-
+ "WHERE p.pk > 0 ORDER BY p.pk";
914+
+ "WHERE p.pk > 0";
826915
return executeMetadataQuery(sql, null, raw -> primaryKeysResult(raw, table));
827916
}
828917

@@ -840,14 +929,14 @@ public Connection getConnection() throws SQLException {
840929
return connection;
841930
}
842931

843-
@Override public ResultSet getProcedures(String catalog, String schemaPattern, String procedureNamePattern) throws SQLException { throw new SQLFeatureNotSupportedException("not implemented by SQLite JDBC driver"); }
844-
@Override public ResultSet getProcedureColumns(String catalog, String schemaPattern, String procedureNamePattern, String columnNamePattern) throws SQLException { throw new SQLFeatureNotSupportedException("not implemented by SQLite JDBC driver"); }
932+
@Override public ResultSet getProcedures(String catalog, String schemaPattern, String procedureNamePattern) throws SQLException { return metadataResult(PROCEDURES_RESULT); }
933+
@Override public ResultSet getProcedureColumns(String catalog, String schemaPattern, String procedureNamePattern, String columnNamePattern) throws SQLException { return metadataResult(PROCEDURE_COLUMNS_RESULT); }
845934
@Override public ResultSet getSchemas() throws SQLException { return executeMetadataQuery("SELECT NULL AS TABLE_SCHEM, NULL AS TABLE_CATALOG WHERE 0"); }
846935
@Override public ResultSet getCatalogs() throws SQLException { return executeMetadataQuery("SELECT NULL AS TABLE_CAT WHERE 0"); }
847-
@Override public ResultSet getColumnPrivileges(String catalog, String schema, String table, String columnNamePattern) throws SQLException { throw new SQLFeatureNotSupportedException("not implemented by SQLite JDBC driver"); }
848-
@Override public ResultSet getTablePrivileges(String catalog, String schemaPattern, String tableNamePattern) throws SQLException { throw new SQLFeatureNotSupportedException("not implemented by SQLite JDBC driver"); }
849-
@Override public ResultSet getBestRowIdentifier(String catalog, String schema, String table, int scope, boolean nullable) throws SQLException { throw new SQLFeatureNotSupportedException("not implemented by SQLite JDBC driver"); }
850-
@Override public ResultSet getVersionColumns(String catalog, String schema, String table) throws SQLException { throw new SQLFeatureNotSupportedException("not implemented by SQLite JDBC driver"); }
936+
@Override public ResultSet getColumnPrivileges(String catalog, String schema, String table, String columnNamePattern) throws SQLException { return metadataResult(COLUMN_PRIVILEGES_RESULT); }
937+
@Override public ResultSet getTablePrivileges(String catalog, String schemaPattern, String tableNamePattern) throws SQLException { return metadataResult(TABLE_PRIVILEGES_RESULT); }
938+
@Override public ResultSet getBestRowIdentifier(String catalog, String schema, String table, int scope, boolean nullable) throws SQLException { return metadataResult(BEST_ROW_IDENTIFIER_RESULT); }
939+
@Override public ResultSet getVersionColumns(String catalog, String schema, String table) throws SQLException { return metadataResult(VERSION_COLUMNS_RESULT); }
851940
@Override
852941
public ResultSet getImportedKeys(String catalog, String schema, String table) throws SQLException {
853942
return executeMetadataQuery(
@@ -869,6 +958,13 @@ public ResultSet getCrossReference(
869958
String foreignSchema,
870959
String foreignTable)
871960
throws SQLException {
961+
if (requestsNamespace(parentCatalog)
962+
|| requestsNamespace(parentSchema)
963+
|| requestsNamespace(foreignCatalog)
964+
|| requestsNamespace(foreignSchema)) {
965+
// The server reports no catalog and no schema, so a specific request matches nothing.
966+
return metadataResult(emptyMetadataResult(FOREIGN_KEY_COLUMNS));
967+
}
872968
if (parentTable == null) {
873969
return getExportedKeys(foreignCatalog, foreignSchema, foreignTable);
874970
}
@@ -881,6 +977,10 @@ public ResultSet getCrossReference(
881977
raw -> exportedKeysResult(raw, parentCatalog, parentSchema, foreignCatalog, foreignSchema));
882978
}
883979

980+
private static boolean requestsNamespace(String name) {
981+
return name != null && !name.isEmpty();
982+
}
983+
884984
@Override
885985
public ResultSet getTypeInfo() throws SQLException {
886986
return metadataResult(TYPE_INFO_RESULT);
@@ -911,7 +1011,7 @@ public ResultSet getTypeInfo() throws SQLException {
9111011
@Override public boolean deletesAreDetected(int type) throws SQLException { return false; }
9121012
@Override public boolean insertsAreDetected(int type) throws SQLException { return false; }
9131013
@Override public boolean supportsBatchUpdates() throws SQLException { return true; }
914-
@Override public ResultSet getUDTs(String catalog, String schemaPattern, String typeNamePattern, int[] types) throws SQLException { throw new SQLFeatureNotSupportedException("not implemented by SQLite JDBC driver"); }
1014+
@Override public ResultSet getUDTs(String catalog, String schemaPattern, String typeNamePattern, int[] types) throws SQLException { return metadataResult(UDTS_RESULT); }
9151015
@Override public boolean supportsSavepoints() throws SQLException { return false; }
9161016
@Override public boolean supportsNamedParameters() throws SQLException { return false; }
9171017
@Override public boolean supportsMultipleOpenResults() throws SQLException { return false; }

0 commit comments

Comments
 (0)