Skip to content

Commit e19a419

Browse files
authored
refactor(jdbc): extract MetadataSql query builders (#228)
Fixes #207 ## What changed Wave 5a of the `LibsqlDatabaseMetaData` split. New package-private `MetadataSql` holds `literal(String)` and one builder per metadata query: tables, columns, imported keys, exported keys, primary keys, table types, schemas, catalogs, and index info. Argument normalization (null/empty pattern to `%`) and result shaping stay in `LibsqlDatabaseMetaData`. No SQL text changed, no test edited, `pom.xml` untouched. Preserved deliberately: - `AND TABLE_TYPE IN ('TABLE','VIEW')` (no space after the comma). - `types[i].toUpperCase()` without a locale. - `getColumns` still passes a null `originSql`. - Empty `types` still behaves like null. ## Proof: generated SQL is byte-identical Same throwaway probe (`MetadataSqlBytesProbe`, untracked, deleted before commit) compiled in a worktree of `origin/main` and in this branch. It stubs `LibsqlConnection.executeStatement` to record `Stmt.sql()` for every builder over 8 patterns (`null`, ``, `%`, `t`, `O'Brien`, `t'x`, `TABLE`, `sqlite_%`) x 5 type sets (`null`, `{}`, `{"TABLE"}`, `{"table","View"}`, `{"it's","SYSTEM TABLE"}`), both `getIndexInfo` unique modes, and cross-reference with null/child/parent tables. ``` $ diff -u wt-main/libsql-jdbc/target/metadata-sql-probe.txt wt-207/libsql-jdbc/target/metadata-sql-probe.txt IDENTICAL $ sha256sum .../metadata-sql-probe.txt # both files 83b42bd7c72acdc23593e110c6da7c0948406c77b7f7331ae3bb4807e54be580 ``` 106,822 bytes of generated SQL per side, equal byte for byte. The existing assertions (`TABLE_NAME LIKE 'O''Brien'`, `UPPER(TYPE) IN ('TABLE', 'VIEW')`, `AND TABLE_TYPE IN ('TABLE','VIEW')`, `'sqlite_schema' AS TABLE_NAME`, `ORDER BY t.TABLE_RANK, t.TABLE_NAME, p.cid`) and the stub dispatch on `JOIN pragma_foreign_key_list` / `FROM pragma_foreign_key_list` / `FROM pragma_table_info` also pass unchanged. ## Probe: a changed SQL byte fails a test Mutated the types separator to `sql.append(", ")` in `MetadataSql.tables`: ``` [ERROR] Tests run: 1, Failures: 1, Errors: 0, Skipped: 0 ... in ...LibsqlDatabaseMetaDataTest java.lang.AssertionError: Expecting actual: "... AND TABLE_TYPE IN ('TABLE', 'VIEW') ORDER BY TABLE_TYPE, TABLE_NAME" to contain: "AND TABLE_TYPE IN ('TABLE','VIEW')" at ...LibsqlDatabaseMetaDataTest.getTablesWithMultipleTypesUppercasesThem(LibsqlDatabaseMetaDataTest.java:224) ``` Reverted; the lane below is on the reverted head. ## Line counts - `LibsqlDatabaseMetaData.java`: 1057 -> 949 (-108; acceptance: under 950). - `MetadataSql.java`: 133 new. ## Lanes - `./mvnw -pl libsql-jdbc -am test`: `Tests run: 396, Failures: 0, Errors: 0, Skipped: 0` — BUILD SUCCESS. - `./mvnw spotless:apply` then `./mvnw spotless:check`: BUILD SUCCESS. - `./mvnw -pl libsql-conformance -am verify`: `Tests run: 272, Failures: 0, Errors: 0, Skipped: 7` — BUILD SUCCESS.
1 parent 707deea commit e19a419

2 files changed

Lines changed: 150 additions & 125 deletions

File tree

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

Lines changed: 17 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -447,63 +447,18 @@ private ResultSet executeMetadataQuery(
447447
}
448448
}
449449

450-
private static String escapeLiteral(String value) {
451-
return value.replace("'", "''");
452-
}
453-
454-
@Override
455-
public ResultSet getTables(String catalog, String schemaPattern, String tableNamePattern, String[] types) throws SQLException {
456-
String pattern = tableNamePattern == null || tableNamePattern.isEmpty()
457-
? "%"
458-
: escapeLiteral(tableNamePattern);
459-
StringBuilder sql = new StringBuilder();
460-
sql.append("SELECT NULL AS TABLE_CAT, NULL AS TABLE_SCHEM, NAME AS TABLE_NAME, TYPE AS TABLE_TYPE, ")
461-
.append("NULL AS REMARKS, NULL AS TYPE_CAT, NULL AS TYPE_SCHEM, NULL AS TYPE_NAME, ")
462-
.append("NULL AS SELF_REFERENCING_COL_NAME, NULL AS REF_GENERATION FROM (")
463-
.append("SELECT 'sqlite_schema' AS NAME, 'SYSTEM TABLE' AS TYPE")
464-
.append(" UNION ALL SELECT NAME, UPPER(TYPE) AS TYPE FROM sqlite_schema")
465-
.append(" WHERE NAME NOT LIKE 'sqlite\\_%' ESCAPE '\\' AND UPPER(TYPE) IN ('TABLE', 'VIEW')")
466-
.append(" UNION ALL SELECT NAME, 'GLOBAL TEMPORARY' AS TYPE FROM sqlite_temp_master")
467-
.append(" UNION ALL SELECT NAME, 'SYSTEM TABLE' AS TYPE FROM sqlite_schema")
468-
.append(" WHERE NAME LIKE 'sqlite\\_%' ESCAPE '\\'")
469-
.append(") WHERE TABLE_NAME LIKE '").append(pattern).append("' ESCAPE '\\'");
470-
if (types != null && types.length != 0) {
471-
sql.append(" AND TABLE_TYPE IN (");
472-
for (int i = 0; i < types.length; i++) {
473-
if (i > 0) {
474-
sql.append(",");
475-
}
476-
sql.append("'").append(escapeLiteral(types[i].toUpperCase())).append("'");
477-
}
478-
sql.append(")");
479-
}
480-
sql.append(" ORDER BY TABLE_TYPE, TABLE_NAME");
481-
return executeMetadataQuery(sql.toString());
450+
@Override public ResultSet getTables(String catalog, String schemaPattern, String tableNamePattern, String[] types) throws SQLException {
451+
String pattern = tableNamePattern == null || tableNamePattern.isEmpty() ? "%" : tableNamePattern;
452+
return executeMetadataQuery(MetadataSql.tables(pattern, types));
482453
}
483454

484455
@Override
485456
public ResultSet getColumns(String catalog, String schemaPattern, String tableNamePattern, String columnNamePattern) throws SQLException {
486457
String tablePattern = tableNamePattern == null || tableNamePattern.isEmpty() ? "%" : tableNamePattern;
487458
String columnPattern = columnNamePattern == null || columnNamePattern.isEmpty() ? "%" : columnNamePattern;
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 '\\'"
502-
+ " AND p.name LIKE '" + escapeLiteral(columnPattern) + "' ESCAPE '\\'"
503-
+ " ORDER BY t.TABLE_RANK, t.TABLE_NAME, p.cid";
504459
// getColumns synthesizes its column labels, so the raw SQL must not drive column-origin
505460
// resolution for the result set.
506-
return executeMetadataQuery(sql, null, LibsqlDatabaseMetaData::columnsResult);
461+
return executeMetadataQuery(MetadataSql.columns(tablePattern, columnPattern), null, LibsqlDatabaseMetaData::columnsResult);
507462
}
508463

509464
private static StmtResult columnsResult(StmtResult raw) {
@@ -668,39 +623,6 @@ private static StmtResult primaryKeysResult(StmtResult raw, String table) {
668623
return new StmtResult(PRIMARY_KEY_COLUMNS, rows, 0, null);
669624
}
670625

671-
private static String importedKeysSql(String table) {
672-
String escapedTable = escapeLiteral(table == null ? "" : table);
673-
return "SELECT f.id AS FK_ID, f.seq AS KEY_SEQ, f.\"table\" AS PKTABLE_NAME, "
674-
+ "f.\"from\" AS FKCOLUMN_NAME, f.\"to\" AS PKCOLUMN_NAME, "
675-
+ "f.on_update AS UPDATE_RULE, f.on_delete AS DELETE_RULE, "
676-
+ "c.sql AS FK_CREATE_SQL, p.sql AS PK_CREATE_SQL, "
677-
+ "(SELECT pi.name FROM pragma_table_info(f.\"table\") pi WHERE pi.pk = 1) AS PK_FIRST_COLUMN "
678-
+ "FROM pragma_foreign_key_list('" + escapedTable + "') f "
679-
+ "LEFT JOIN sqlite_schema c ON lower(c.name) = lower('" + escapedTable + "') "
680-
+ "LEFT JOIN sqlite_schema p ON lower(p.name) = lower(f.\"table\") "
681-
+ "ORDER BY f.id, f.seq";
682-
}
683-
684-
private static String exportedKeysSql(String parentTable, String childTable) {
685-
String escapedParent = escapeLiteral(parentTable == null ? "" : parentTable);
686-
StringBuilder sql = new StringBuilder()
687-
.append("SELECT f.id AS FK_ID, f.seq AS KEY_SEQ, ")
688-
.append("COALESCE(p.name, f.\"table\") AS PKTABLE_NAME, f.\"from\" AS FKCOLUMN_NAME, ")
689-
.append("COALESCE(f.\"to\", (SELECT pi.name FROM pragma_table_info(f.\"table\") pi WHERE pi.pk = 1)) AS PKCOLUMN_NAME, ")
690-
.append("f.on_update AS UPDATE_RULE, f.on_delete AS DELETE_RULE, ")
691-
.append("m.name AS FKTABLE_NAME, m.sql AS FK_CREATE_SQL, p.sql AS PK_CREATE_SQL, ")
692-
.append("(SELECT group_concat(pi.name, ',') FROM pragma_table_info(f.\"table\") pi WHERE pi.pk > 0) AS PK_COLUMNS ")
693-
.append("FROM sqlite_schema m JOIN pragma_foreign_key_list(m.name) f ")
694-
.append("LEFT JOIN sqlite_schema p ON lower(p.name) = lower(f.\"table\") ")
695-
.append("WHERE m.type = 'table' AND lower(f.\"table\") = lower('")
696-
.append(escapedParent)
697-
.append("')");
698-
if (childTable != null) {
699-
sql.append(" AND lower(m.name) = lower('").append(escapeLiteral(childTable)).append("')");
700-
}
701-
return sql.append(" ORDER BY m.name, f.id, f.seq").toString();
702-
}
703-
704626
private static StmtResult importedKeysResult(
705627
StmtResult raw, String catalog, String schema, String table) {
706628
List<ForeignKeyRow> rows = new ArrayList<>();
@@ -905,24 +827,11 @@ private Row toRow() {
905827

906828
private record ColumnShape(int jdbcType, String typeName, int columnSize, int decimalDigits) {}
907829

908-
@Override
909-
public ResultSet getPrimaryKeys(String catalog, String schema, String table) throws SQLException {
910-
String escapedTable = escapeLiteral(table == null ? "" : table);
911-
String sql = "SELECT p.name AS COLUMN_NAME, p.pk AS KEY_SEQ, s.sql AS CREATE_SQL "
912-
+ "FROM pragma_table_info('" + escapedTable + "') p "
913-
+ "LEFT JOIN sqlite_schema s ON lower(s.name) = lower('" + escapedTable + "') "
914-
+ "WHERE p.pk > 0";
915-
return executeMetadataQuery(sql, null, raw -> primaryKeysResult(raw, table));
830+
@Override public ResultSet getPrimaryKeys(String catalog, String schema, String table) throws SQLException {
831+
return executeMetadataQuery(MetadataSql.primaryKeys(table), null, raw -> primaryKeysResult(raw, table));
916832
}
917833

918-
@Override
919-
public ResultSet getTableTypes() throws SQLException {
920-
return executeMetadataQuery(
921-
"SELECT 'TABLE' AS TABLE_TYPE "
922-
+ "UNION SELECT 'VIEW' AS TABLE_TYPE "
923-
+ "UNION SELECT 'SYSTEM TABLE' AS TABLE_TYPE "
924-
+ "UNION SELECT 'GLOBAL TEMPORARY' AS TABLE_TYPE");
925-
}
834+
@Override public ResultSet getTableTypes() throws SQLException { return executeMetadataQuery(MetadataSql.tableTypes()); }
926835

927836
@Override
928837
public Connection getConnection() throws SQLException {
@@ -931,22 +840,20 @@ public Connection getConnection() throws SQLException {
931840

932841
@Override public ResultSet getProcedures(String catalog, String schemaPattern, String procedureNamePattern) throws SQLException { return metadataResult(PROCEDURES_RESULT); }
933842
@Override public ResultSet getProcedureColumns(String catalog, String schemaPattern, String procedureNamePattern, String columnNamePattern) throws SQLException { return metadataResult(PROCEDURE_COLUMNS_RESULT); }
934-
@Override public ResultSet getSchemas() throws SQLException { return executeMetadataQuery("SELECT NULL AS TABLE_SCHEM, NULL AS TABLE_CATALOG WHERE 0"); }
935-
@Override public ResultSet getCatalogs() throws SQLException { return executeMetadataQuery("SELECT NULL AS TABLE_CAT WHERE 0"); }
843+
@Override public ResultSet getSchemas() throws SQLException { return executeMetadataQuery(MetadataSql.schemas()); }
844+
@Override public ResultSet getCatalogs() throws SQLException { return executeMetadataQuery(MetadataSql.catalogs()); }
936845
@Override public ResultSet getColumnPrivileges(String catalog, String schema, String table, String columnNamePattern) throws SQLException { return metadataResult(COLUMN_PRIVILEGES_RESULT); }
937846
@Override public ResultSet getTablePrivileges(String catalog, String schemaPattern, String tableNamePattern) throws SQLException { return metadataResult(TABLE_PRIVILEGES_RESULT); }
938847
@Override public ResultSet getBestRowIdentifier(String catalog, String schema, String table, int scope, boolean nullable) throws SQLException { return metadataResult(BEST_ROW_IDENTIFIER_RESULT); }
939848
@Override public ResultSet getVersionColumns(String catalog, String schema, String table) throws SQLException { return metadataResult(VERSION_COLUMNS_RESULT); }
940-
@Override
941-
public ResultSet getImportedKeys(String catalog, String schema, String table) throws SQLException {
942-
return executeMetadataQuery(
943-
importedKeysSql(table), null, raw -> importedKeysResult(raw, catalog, schema, table));
849+
@Override public ResultSet getImportedKeys(String catalog, String schema, String table) throws SQLException {
850+
return executeMetadataQuery(MetadataSql.importedKeys(table), null,
851+
raw -> importedKeysResult(raw, catalog, schema, table));
944852
}
945853

946-
@Override
947-
public ResultSet getExportedKeys(String catalog, String schema, String table) throws SQLException {
948-
return executeMetadataQuery(
949-
exportedKeysSql(table, null), null, raw -> exportedKeysResult(raw, catalog, schema, catalog, schema));
854+
@Override public ResultSet getExportedKeys(String catalog, String schema, String table) throws SQLException {
855+
return executeMetadataQuery(MetadataSql.exportedKeys(table, null), null,
856+
raw -> exportedKeysResult(raw, catalog, schema, catalog, schema));
950857
}
951858

952859
@Override
@@ -971,9 +878,7 @@ public ResultSet getCrossReference(
971878
if (foreignTable == null) {
972879
return getImportedKeys(parentCatalog, parentSchema, parentTable);
973880
}
974-
return executeMetadataQuery(
975-
exportedKeysSql(parentTable, foreignTable),
976-
null,
881+
return executeMetadataQuery(MetadataSql.exportedKeys(parentTable, foreignTable), null,
977882
raw -> exportedKeysResult(raw, parentCatalog, parentSchema, foreignCatalog, foreignSchema));
978883
}
979884

@@ -985,20 +890,7 @@ private static boolean requestsNamespace(String name) {
985890
public ResultSet getTypeInfo() throws SQLException {
986891
return metadataResult(TYPE_INFO_RESULT);
987892
}
988-
@Override public ResultSet getIndexInfo(String catalog, String schema, String table, boolean unique, boolean approximate) throws SQLException {
989-
String escapedTable = escapeLiteral(table == null ? "" : table);
990-
String sql = "SELECT NULL AS TABLE_CAT, NULL AS TABLE_SCHEM, '" + escapedTable + "' AS TABLE_NAME, " +
991-
"CASE il.\"unique\" WHEN 1 THEN 0 ELSE 1 END AS NON_UNIQUE, " +
992-
"NULL AS INDEX_QUALIFIER, il.name AS INDEX_NAME, 3 AS TYPE, " +
993-
"ii.seqno + 1 AS ORDINAL_POSITION, ii.name AS COLUMN_NAME, " +
994-
"'A' AS ASC_OR_DESC, 0 AS CARDINALITY, 0 AS PAGES, NULL AS FILTER_CONDITION " +
995-
"FROM pragma_index_list('" + escapedTable + "') il, pragma_index_info(il.name) ii";
996-
if (unique) {
997-
sql += " WHERE il.\"unique\" = 1";
998-
}
999-
sql += " ORDER BY NON_UNIQUE, TYPE, INDEX_NAME, ORDINAL_POSITION";
1000-
return executeMetadataQuery(sql);
1001-
}
893+
@Override public ResultSet getIndexInfo(String catalog, String schema, String table, boolean unique, boolean approximate) throws SQLException { return executeMetadataQuery(MetadataSql.indexInfo(table, unique)); }
1002894
@Override public boolean supportsResultSetType(int type) throws SQLException { return type == ResultSet.TYPE_FORWARD_ONLY; }
1003895
@Override public boolean supportsResultSetConcurrency(int type, int concurrency) throws SQLException { return type == ResultSet.TYPE_FORWARD_ONLY && concurrency == ResultSet.CONCUR_READ_ONLY; }
1004896
@Override public boolean ownUpdatesAreVisible(int type) throws SQLException { return false; }

0 commit comments

Comments
 (0)