Skip to content

Commit db5cd60

Browse files
authored
fix(jdbc): honor table types in getTables metadata (#84)
Closes #41. Part of #54. ## What changed - `LibsqlDatabaseMetaData.getTables` now builds the xerial-shaped union instead of selecting every `sqlite_master` row: - a `sqlite_schema` `SYSTEM TABLE` row, - user rows filtered to `UPPER(TYPE) IN ('TABLE', 'VIEW')`, - `sqlite_temp_master` rows as `GLOBAL TEMPORARY`, - `sqlite_%` rows as `SYSTEM TABLE`. - The `types` argument is honored. `null` and an empty array add no filter; a non-empty array is uppercased and emitted as `TABLE_TYPE IN (...)`. - A `null` or empty table-name pattern maps to `%`, matching xerial. Single quotes are still escaped, and the pattern uses `LIKE ... ESCAPE '\'`. - Tests cover `null` types, an empty array, `{"TABLE"}`, `{"VIEW"}`, mixed-case multiple types, an empty name pattern, and the existing quote-escape case. ## Contract decision Parity with xerial 3.53.4.0 `JDBC3DatabaseMetaData.getTables` (lines 1682-1750). The union, the `TABLE`/`VIEW` user-row filter, the system and temporary rows, the `types` filter behavior, the `LIKE ... ESCAPE '\'` pattern handling, and the `ORDER BY TABLE_TYPE, TABLE_NAME` ordering all match. No transport limitation required a different shape. ## Evidence Tests were added first and failed on the pre-fix code: ``` ./mvnw -pl libsql-jdbc -am test -Dtest=LibsqlDatabaseMetaDataTest -Dsurefire.failIfNoSpecifiedTests=false [ERROR] Tests run: 9, Failures: 7, Errors: 0, Skipped: 0 ``` Representative failures: the pre-fix SQL had no `sqlite_schema`/`SYSTEM TABLE` branch, no `sqlite_temp_master`/`GLOBAL TEMPORARY` branch, no `UPPER(TYPE) IN ('TABLE', 'VIEW')` filter, no `TABLE_TYPE IN (...)` filter for `{"TABLE"}`/`{"VIEW"}`, and turned an empty pattern into `LIKE ''`. After the fix: ``` ./mvnw -pl libsql-jdbc -am test Tests run: 154, Failures: 0, Errors: 0, Skipped: 0 BUILD SUCCESS ./mvnw -pl libsql-conformance -am verify Tests run: 115, Failures: 0, Errors: 0, Skipped: 1 BUILD SUCCESS ``` The conformance run exercises real sqld and includes `MetadataLeakTest`, which calls `getTables(null, null, null, null)`. I also executed the generated union against a real SQLite engine (xerial 3.53.4.0) with a table, a view, an index, a trigger, and a temp table. Null types returned `tmp` (GLOBAL TEMPORARY), `sqlite_schema` (SYSTEM TABLE), `t` (TABLE), and `v` (VIEW); `{"TABLE"}` returned only `t`; the index and trigger never appeared. `./mvnw spotless:apply` completed successfully before the final commit. ## Deviations none
1 parent 8b7f967 commit db5cd60

2 files changed

Lines changed: 94 additions & 5 deletions

File tree

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

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -284,9 +284,29 @@ private static String escapeLiteral(String value) {
284284

285285
@Override
286286
public ResultSet getTables(String catalog, String schemaPattern, String tableNamePattern, String[] types) throws SQLException {
287-
StringBuilder sql = new StringBuilder("SELECT NULL AS TABLE_CAT, NULL AS TABLE_SCHEM, name AS TABLE_NAME, UPPER(type) AS TABLE_TYPE, NULL AS REMARKS, NULL AS TYPE_CAT, NULL AS TYPE_SCHEM, NULL AS TYPE_NAME, NULL AS SELF_REFERENCING_COL_NAME, NULL AS REF_GENERATION FROM sqlite_master WHERE 1=1");
288-
if (tableNamePattern != null && !tableNamePattern.equals("%")) {
289-
sql.append(" AND name LIKE '").append(escapeLiteral(tableNamePattern)).append("'");
287+
String pattern = tableNamePattern == null || tableNamePattern.isEmpty()
288+
? "%"
289+
: escapeLiteral(tableNamePattern);
290+
StringBuilder sql = new StringBuilder();
291+
sql.append("SELECT NULL AS TABLE_CAT, NULL AS TABLE_SCHEM, NAME AS TABLE_NAME, TYPE AS TABLE_TYPE, ")
292+
.append("NULL AS REMARKS, NULL AS TYPE_CAT, NULL AS TYPE_SCHEM, NULL AS TYPE_NAME, ")
293+
.append("NULL AS SELF_REFERENCING_COL_NAME, NULL AS REF_GENERATION FROM (")
294+
.append("SELECT 'sqlite_schema' AS NAME, 'SYSTEM TABLE' AS TYPE")
295+
.append(" UNION ALL SELECT NAME, UPPER(TYPE) AS TYPE FROM sqlite_schema")
296+
.append(" WHERE NAME NOT LIKE 'sqlite\\_%' ESCAPE '\\' AND UPPER(TYPE) IN ('TABLE', 'VIEW')")
297+
.append(" UNION ALL SELECT NAME, 'GLOBAL TEMPORARY' AS TYPE FROM sqlite_temp_master")
298+
.append(" UNION ALL SELECT NAME, 'SYSTEM TABLE' AS TYPE FROM sqlite_schema")
299+
.append(" WHERE NAME LIKE 'sqlite\\_%' ESCAPE '\\'")
300+
.append(") WHERE TABLE_NAME LIKE '").append(pattern).append("' ESCAPE '\\'");
301+
if (types != null && types.length != 0) {
302+
sql.append(" AND TABLE_TYPE IN (");
303+
for (int i = 0; i < types.length; i++) {
304+
if (i > 0) {
305+
sql.append(",");
306+
}
307+
sql.append("'").append(escapeLiteral(types[i].toUpperCase())).append("'");
308+
}
309+
sql.append(")");
290310
}
291311
sql.append(" ORDER BY TABLE_TYPE, TABLE_NAME");
292312
return executeMetadataQuery(sql.toString());

‎libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlDatabaseMetaDataTest.java‎

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,77 @@ void escapesSingleQuoteInTableNamePattern() throws SQLException {
4343
ResultSet resultSet = metadata.getTables(null, null, "O'Brien", null);
4444
resultSet.close();
4545

46-
assertThat(connection.lastSql).contains("name LIKE 'O''Brien'");
47-
assertThat(connection.lastSql).doesNotContain("name LIKE 'O'Brien'");
46+
assertThat(connection.lastSql).contains("TABLE_NAME LIKE 'O''Brien'");
47+
assertThat(connection.lastSql).doesNotContain("TABLE_NAME LIKE 'O'Brien'");
48+
}
49+
50+
@Test
51+
void getTablesWithNullTypesReturnsOnlyTablesViewsAndSystemAndTemporaryRows() throws SQLException {
52+
RecordingConnection connection = new RecordingConnection(emptyResult());
53+
54+
new LibsqlDatabaseMetaData(connection).getTables(null, null, null, null).close();
55+
56+
assertThat(connection.lastSql)
57+
.contains("'sqlite_schema' AS NAME")
58+
.contains("'SYSTEM TABLE' AS TYPE")
59+
.contains("'GLOBAL TEMPORARY' AS TYPE")
60+
.contains("FROM sqlite_temp_master")
61+
.contains("UPPER(TYPE) IN ('TABLE', 'VIEW')")
62+
.contains("TABLE_NAME LIKE '%'")
63+
.doesNotContain("TABLE_TYPE IN")
64+
.doesNotContain("INDEX")
65+
.doesNotContain("TRIGGER");
66+
}
67+
68+
@Test
69+
void getTablesWithEmptyTypesBehavesLikeNullTypes() throws SQLException {
70+
RecordingConnection connection = new RecordingConnection(emptyResult());
71+
72+
new LibsqlDatabaseMetaData(connection).getTables(null, null, null, new String[0]).close();
73+
74+
assertThat(connection.lastSql)
75+
.contains("'SYSTEM TABLE' AS TYPE")
76+
.contains("'GLOBAL TEMPORARY' AS TYPE")
77+
.contains("UPPER(TYPE) IN ('TABLE', 'VIEW')")
78+
.doesNotContain("TABLE_TYPE IN");
79+
}
80+
81+
@Test
82+
void getTablesWithTableTypeFiltersToTableRows() throws SQLException {
83+
RecordingConnection connection = new RecordingConnection(emptyResult());
84+
85+
new LibsqlDatabaseMetaData(connection).getTables(null, null, null, new String[] {"TABLE"}).close();
86+
87+
assertThat(connection.lastSql)
88+
.contains("UPPER(TYPE) IN ('TABLE', 'VIEW')")
89+
.contains("AND TABLE_TYPE IN ('TABLE')");
90+
}
91+
92+
@Test
93+
void getTablesWithViewTypeFiltersToViewRows() throws SQLException {
94+
RecordingConnection connection = new RecordingConnection(emptyResult());
95+
96+
new LibsqlDatabaseMetaData(connection).getTables(null, null, null, new String[] {"VIEW"}).close();
97+
98+
assertThat(connection.lastSql).contains("AND TABLE_TYPE IN ('VIEW')");
99+
}
100+
101+
@Test
102+
void getTablesWithMultipleTypesUppercasesThem() throws SQLException {
103+
RecordingConnection connection = new RecordingConnection(emptyResult());
104+
105+
new LibsqlDatabaseMetaData(connection).getTables(null, null, null, new String[] {"table", "View"}).close();
106+
107+
assertThat(connection.lastSql).contains("AND TABLE_TYPE IN ('TABLE','VIEW')");
108+
}
109+
110+
@Test
111+
void getTablesWithEmptyTableNamePatternMatchesEverything() throws SQLException {
112+
RecordingConnection connection = new RecordingConnection(emptyResult());
113+
114+
new LibsqlDatabaseMetaData(connection).getTables(null, null, "", null).close();
115+
116+
assertThat(connection.lastSql).contains("TABLE_NAME LIKE '%'");
48117
}
49118

50119
private static StmtResult emptyResult() {

0 commit comments

Comments
 (0)