From b89ce02e0d92cecf19e2fc67e91179a8ce34d044 Mon Sep 17 00:00:00 2001
From: Magnus Persson <1431685+magnusp@users.noreply.github.com>
Date: Fri, 23 Oct 2026 05:37:00 +0200
Subject: [PATCH] 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`.
---
.../libsql/jdbc/LibsqlDatabaseMetaData.java | 130 ++++++++--
.../jdbc/LibsqlDatabaseMetaDataTest.java | 224 +++++++++++++++++-
2 files changed, 330 insertions(+), 24 deletions(-)
diff --git a/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlDatabaseMetaData.java b/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlDatabaseMetaData.java
index 48b4871..be714e2 100644
--- a/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlDatabaseMetaData.java
+++ b/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlDatabaseMetaData.java
@@ -27,7 +27,7 @@ public class LibsqlDatabaseMetaData implements DatabaseMetaData {
private static final Pattern VARCHAR_TYPE = Pattern.compile(".*(CHAR|CLOB|TEXT|BLOB).*");
private static final Pattern FLOAT_TYPE = Pattern.compile(".*(REAL|FLOA|DOUB|DEC|NUM).*");
private static final Pattern NAMED_PRIMARY_KEY = Pattern.compile(
- "\\bCONSTRAINT\\s+(\"[^\"]+\"|`[^`]+`|\\[[^\\]]+\\]|'[^']+'|[^\\s]+)\\s+PRIMARY\\s+KEY\\b",
+ "\\bCONSTRAINT\\s*(\"[^\"]+\"|`[^`]+`|\\[[^\\]]+\\]|'[^']+'|[^\\s]+)\\s*PRIMARY\\s+KEY\\b",
Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
private static final Pattern NAMED_FOREIGN_KEY = Pattern.compile(
"\\bCONSTRAINT\\s+(\"[^\"]+\"|`[^`]+`|\\[[^\\]]+\\]|'[^']+'|[^\\s]+)\\s+FOREIGN\\s+KEY\\b",
@@ -90,6 +90,83 @@ public class LibsqlDatabaseMetaData implements DatabaseMetaData {
0,
null);
+ private static final StmtResult PROCEDURES_RESULT = emptyMetadataResult(List.of(
+ new Col("PROCEDURE_CAT", "TEXT"),
+ new Col("PROCEDURE_SCHEM", "TEXT"),
+ new Col("PROCEDURE_NAME", "TEXT"),
+ new Col("UNDEF1", "TEXT"),
+ new Col("UNDEF2", "TEXT"),
+ new Col("UNDEF3", "TEXT"),
+ new Col("REMARKS", "TEXT"),
+ new Col("PROCEDURE_TYPE", "INTEGER")));
+
+ private static final StmtResult PROCEDURE_COLUMNS_RESULT = emptyMetadataResult(List.of(
+ new Col("PROCEDURE_CAT", "TEXT"),
+ new Col("PROCEDURE_SCHEM", "TEXT"),
+ new Col("PROCEDURE_NAME", "TEXT"),
+ new Col("COLUMN_NAME", "TEXT"),
+ new Col("COLUMN_TYPE", "INTEGER"),
+ new Col("DATA_TYPE", "INTEGER"),
+ new Col("TYPE_NAME", "TEXT"),
+ new Col("PRECISION", "INTEGER"),
+ new Col("LENGTH", "INTEGER"),
+ new Col("SCALE", "INTEGER"),
+ new Col("RADIX", "INTEGER"),
+ new Col("NULLABLE", "INTEGER"),
+ new Col("REMARKS", "TEXT")));
+
+ private static final StmtResult COLUMN_PRIVILEGES_RESULT = emptyMetadataResult(List.of(
+ new Col("TABLE_CAT", "TEXT"),
+ new Col("TABLE_SCHEM", "TEXT"),
+ new Col("TABLE_NAME", "TEXT"),
+ new Col("COLUMN_NAME", "TEXT"),
+ new Col("GRANTOR", "TEXT"),
+ new Col("GRANTEE", "TEXT"),
+ new Col("PRIVILEGE", "TEXT"),
+ new Col("IS_GRANTABLE", "TEXT")));
+
+ private static final StmtResult TABLE_PRIVILEGES_RESULT = emptyMetadataResult(List.of(
+ new Col("TABLE_CAT", "TEXT"),
+ new Col("TABLE_SCHEM", "TEXT"),
+ new Col("TABLE_NAME", "TEXT"),
+ new Col("GRANTOR", "TEXT"),
+ new Col("GRANTEE", "TEXT"),
+ new Col("PRIVILEGE", "TEXT"),
+ new Col("IS_GRANTABLE", "TEXT")));
+
+ private static final StmtResult BEST_ROW_IDENTIFIER_RESULT = emptyMetadataResult(List.of(
+ new Col("SCOPE", "INTEGER"),
+ new Col("COLUMN_NAME", "TEXT"),
+ new Col("DATA_TYPE", "INTEGER"),
+ new Col("TYPE_NAME", "TEXT"),
+ new Col("COLUMN_SIZE", "INTEGER"),
+ new Col("BUFFER_LENGTH", "INTEGER"),
+ new Col("DECIMAL_DIGITS", "INTEGER"),
+ new Col("PSEUDO_COLUMN", "INTEGER")));
+
+ private static final StmtResult VERSION_COLUMNS_RESULT = emptyMetadataResult(List.of(
+ new Col("SCOPE", "INTEGER"),
+ new Col("COLUMN_NAME", "TEXT"),
+ new Col("DATA_TYPE", "INTEGER"),
+ new Col("TYPE_NAME", "TEXT"),
+ new Col("COLUMN_SIZE", "INTEGER"),
+ new Col("BUFFER_LENGTH", "INTEGER"),
+ new Col("DECIMAL_DIGITS", "INTEGER"),
+ new Col("PSEUDO_COLUMN", "INTEGER")));
+
+ private static final StmtResult UDTS_RESULT = emptyMetadataResult(List.of(
+ new Col("TYPE_CAT", "TEXT"),
+ new Col("TYPE_SCHEM", "TEXT"),
+ new Col("TYPE_NAME", "TEXT"),
+ new Col("CLASS_NAME", "TEXT"),
+ new Col("DATA_TYPE", "INTEGER"),
+ new Col("REMARKS", "TEXT"),
+ new Col("BASE_TYPE", "INTEGER")));
+
+ private static StmtResult emptyMetadataResult(List
columns) {
+ return new StmtResult(columns, List.of(), 0, null);
+ }
+
private final LibsqlConnection connection;
private String cachedDatabaseProductVersion;
@@ -408,13 +485,22 @@ public ResultSet getTables(String catalog, String schemaPattern, String tableNam
public ResultSet getColumns(String catalog, String schemaPattern, String tableNamePattern, String columnNamePattern) throws SQLException {
String tablePattern = tableNamePattern == null || tableNamePattern.isEmpty() ? "%" : tableNamePattern;
String columnPattern = columnNamePattern == null || columnNamePattern.isEmpty() ? "%" : columnNamePattern;
- String sql = "SELECT m.name AS TABLE_NAME, m.sql AS CREATE_SQL, p.cid AS CID, p.name AS COLUMN_NAME, "
- + "p.type AS DECLARED_TYPE, p.\"notnull\" AS NOT_NULL, p.dflt_value AS COLUMN_DEF, p.pk AS PK, "
- + "p.hidden AS HIDDEN FROM sqlite_master m JOIN pragma_table_xinfo(m.name) p "
- + "WHERE m.type IN ('table', 'view')"
- + " AND m.name LIKE '" + escapeLiteral(tablePattern) + "' ESCAPE '\\'"
+ String sql = "WITH t(TABLE_NAME, CREATE_SQL, TABLE_RANK) AS ("
+ + " SELECT 'sqlite_schema' AS TABLE_NAME, NULL AS CREATE_SQL, 1 AS TABLE_RANK"
+ + " UNION ALL SELECT NAME, SQL, 1 FROM sqlite_schema"
+ + " WHERE NAME LIKE 'sqlite\\_%' ESCAPE '\\'"
+ + " UNION ALL SELECT NAME, SQL, 2 FROM sqlite_schema"
+ + " WHERE NAME NOT LIKE 'sqlite\\_%' ESCAPE '\\' AND UPPER(TYPE) = 'TABLE'"
+ + " UNION ALL SELECT NAME, SQL, 3 FROM sqlite_schema"
+ + " WHERE NAME NOT LIKE 'sqlite\\_%' ESCAPE '\\' AND UPPER(TYPE) = 'VIEW'"
+ + " UNION ALL SELECT NAME, NULL, 0 FROM sqlite_temp_master"
+ + ") SELECT t.TABLE_NAME AS TABLE_NAME, t.CREATE_SQL AS CREATE_SQL, p.cid AS CID, "
+ + "p.name AS COLUMN_NAME, p.type AS DECLARED_TYPE, p.\"notnull\" AS NOT_NULL, "
+ + "p.dflt_value AS COLUMN_DEF, p.pk AS PK, p.hidden AS HIDDEN "
+ + "FROM t JOIN pragma_table_xinfo(t.TABLE_NAME) p "
+ + "WHERE t.TABLE_NAME LIKE '" + escapeLiteral(tablePattern) + "' ESCAPE '\\'"
+ " AND p.name LIKE '" + escapeLiteral(columnPattern) + "' ESCAPE '\\'"
- + " ORDER BY m.name, p.cid";
+ + " ORDER BY t.TABLE_RANK, t.TABLE_NAME, p.cid";
// getColumns synthesizes its column labels, so the raw SQL must not drive column-origin
// resolution for the result set.
return executeMetadataQuery(sql, null, LibsqlDatabaseMetaData::columnsResult);
@@ -576,6 +662,9 @@ private static StmtResult primaryKeysResult(StmtResult raw, String table) {
Value.of(primaryKeyName(text(values.get(2)))))));
}
}
+ // xerial orders getPrimaryKeys by column name, not by the declared key sequence.
+ rows.sort(Comparator.comparing(
+ row -> text(row.values().get(3)), Comparator.nullsFirst(Comparator.naturalOrder())));
return new StmtResult(PRIMARY_KEY_COLUMNS, rows, 0, null);
}
@@ -822,7 +911,7 @@ public ResultSet getPrimaryKeys(String catalog, String schema, String table) thr
String sql = "SELECT p.name AS COLUMN_NAME, p.pk AS KEY_SEQ, s.sql AS CREATE_SQL "
+ "FROM pragma_table_info('" + escapedTable + "') p "
+ "LEFT JOIN sqlite_schema s ON lower(s.name) = lower('" + escapedTable + "') "
- + "WHERE p.pk > 0 ORDER BY p.pk";
+ + "WHERE p.pk > 0";
return executeMetadataQuery(sql, null, raw -> primaryKeysResult(raw, table));
}
@@ -840,14 +929,14 @@ public Connection getConnection() throws SQLException {
return connection;
}
- @Override public ResultSet getProcedures(String catalog, String schemaPattern, String procedureNamePattern) throws SQLException { throw new SQLFeatureNotSupportedException("not implemented by SQLite JDBC driver"); }
- @Override public ResultSet getProcedureColumns(String catalog, String schemaPattern, String procedureNamePattern, String columnNamePattern) throws SQLException { throw new SQLFeatureNotSupportedException("not implemented by SQLite JDBC driver"); }
+ @Override public ResultSet getProcedures(String catalog, String schemaPattern, String procedureNamePattern) throws SQLException { return metadataResult(PROCEDURES_RESULT); }
+ @Override public ResultSet getProcedureColumns(String catalog, String schemaPattern, String procedureNamePattern, String columnNamePattern) throws SQLException { return metadataResult(PROCEDURE_COLUMNS_RESULT); }
@Override public ResultSet getSchemas() throws SQLException { return executeMetadataQuery("SELECT NULL AS TABLE_SCHEM, NULL AS TABLE_CATALOG WHERE 0"); }
@Override public ResultSet getCatalogs() throws SQLException { return executeMetadataQuery("SELECT NULL AS TABLE_CAT WHERE 0"); }
- @Override public ResultSet getColumnPrivileges(String catalog, String schema, String table, String columnNamePattern) throws SQLException { throw new SQLFeatureNotSupportedException("not implemented by SQLite JDBC driver"); }
- @Override public ResultSet getTablePrivileges(String catalog, String schemaPattern, String tableNamePattern) throws SQLException { throw new SQLFeatureNotSupportedException("not implemented by SQLite JDBC driver"); }
- @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"); }
- @Override public ResultSet getVersionColumns(String catalog, String schema, String table) throws SQLException { throw new SQLFeatureNotSupportedException("not implemented by SQLite JDBC driver"); }
+ @Override public ResultSet getColumnPrivileges(String catalog, String schema, String table, String columnNamePattern) throws SQLException { return metadataResult(COLUMN_PRIVILEGES_RESULT); }
+ @Override public ResultSet getTablePrivileges(String catalog, String schemaPattern, String tableNamePattern) throws SQLException { return metadataResult(TABLE_PRIVILEGES_RESULT); }
+ @Override public ResultSet getBestRowIdentifier(String catalog, String schema, String table, int scope, boolean nullable) throws SQLException { return metadataResult(BEST_ROW_IDENTIFIER_RESULT); }
+ @Override public ResultSet getVersionColumns(String catalog, String schema, String table) throws SQLException { return metadataResult(VERSION_COLUMNS_RESULT); }
@Override
public ResultSet getImportedKeys(String catalog, String schema, String table) throws SQLException {
return executeMetadataQuery(
@@ -869,6 +958,13 @@ public ResultSet getCrossReference(
String foreignSchema,
String foreignTable)
throws SQLException {
+ if (requestsNamespace(parentCatalog)
+ || requestsNamespace(parentSchema)
+ || requestsNamespace(foreignCatalog)
+ || requestsNamespace(foreignSchema)) {
+ // The server reports no catalog and no schema, so a specific request matches nothing.
+ return metadataResult(emptyMetadataResult(FOREIGN_KEY_COLUMNS));
+ }
if (parentTable == null) {
return getExportedKeys(foreignCatalog, foreignSchema, foreignTable);
}
@@ -881,6 +977,10 @@ public ResultSet getCrossReference(
raw -> exportedKeysResult(raw, parentCatalog, parentSchema, foreignCatalog, foreignSchema));
}
+ private static boolean requestsNamespace(String name) {
+ return name != null && !name.isEmpty();
+ }
+
@Override
public ResultSet getTypeInfo() throws SQLException {
return metadataResult(TYPE_INFO_RESULT);
@@ -911,7 +1011,7 @@ public ResultSet getTypeInfo() throws SQLException {
@Override public boolean deletesAreDetected(int type) throws SQLException { return false; }
@Override public boolean insertsAreDetected(int type) throws SQLException { return false; }
@Override public boolean supportsBatchUpdates() throws SQLException { return true; }
- @Override public ResultSet getUDTs(String catalog, String schemaPattern, String typeNamePattern, int[] types) throws SQLException { throw new SQLFeatureNotSupportedException("not implemented by SQLite JDBC driver"); }
+ @Override public ResultSet getUDTs(String catalog, String schemaPattern, String typeNamePattern, int[] types) throws SQLException { return metadataResult(UDTS_RESULT); }
@Override public boolean supportsSavepoints() throws SQLException { return false; }
@Override public boolean supportsNamedParameters() throws SQLException { return false; }
@Override public boolean supportsMultipleOpenResults() throws SQLException { return false; }
diff --git a/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlDatabaseMetaDataTest.java b/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlDatabaseMetaDataTest.java
index ba6fa72..a0b5cac 100644
--- a/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlDatabaseMetaDataTest.java
+++ b/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlDatabaseMetaDataTest.java
@@ -121,6 +121,49 @@ void getIndexInfoRoundTripsQuotedTableName() throws SQLException {
}
}
+ @Test
+ void getColumnsIncludesSqliteSchemaBeforeUserTables() throws SQLException {
+ RecordingConnection connection = new RecordingConnection(emptyResult());
+
+ new LibsqlDatabaseMetaData(connection).getColumns(null, null, "%", "%").close();
+
+ assertThat(connection.lastSql)
+ .as("getColumns must synthesize the sqlite_schema system table")
+ .contains("'sqlite_schema' AS TABLE_NAME")
+ .as("getColumns must order system tables before user tables, like getTables")
+ .contains("ORDER BY t.TABLE_RANK, t.TABLE_NAME, p.cid");
+ }
+
+ @Test
+ void getCrossReferenceReturnsNoRowForAnotherCatalogOrSchema() throws SQLException {
+ LibsqlDatabaseMetaData metadata = new LibsqlDatabaseMetaData(new ForeignKeyConnection(false));
+
+ try (ResultSet rs =
+ metadata.getCrossReference("ca'talog", "sch'ema", "parent", null, null, "child1")) {
+ assertThat(columnNames(rs)).hasSize(14);
+ assertThat(rs.next())
+ .as("the database has no catalog or schema, so a specific request matches nothing")
+ .isFalse();
+ }
+ }
+
+ @Test
+ void getPrimaryKeysOrdersColumnsByNameAndKeepsTheDeclaredKeySequence() throws SQLException {
+ LibsqlDatabaseMetaData metadata = new LibsqlDatabaseMetaData(new CompositePrimaryKeysConnection());
+
+ try (ResultSet rs = metadata.getPrimaryKeys(null, null, "pk")) {
+ assertThat(rs.next()).isTrue();
+ assertThat(rs.getString("COLUMN_NAME")).isEqualTo("col2");
+ assertThat(rs.getInt("KEY_SEQ")).isEqualTo(2);
+
+ assertThat(rs.next()).isTrue();
+ assertThat(rs.getString("COLUMN_NAME")).isEqualTo("col3");
+ assertThat(rs.getInt("KEY_SEQ")).isEqualTo(1);
+
+ assertThat(rs.next()).isFalse();
+ }
+ }
+
@Test
void getTablesWithNullTypesReturnsOnlyTablesViewsAndSystemAndTemporaryRows() throws SQLException {
RecordingConnection connection = new RecordingConnection(emptyResult());
@@ -244,7 +287,26 @@ static Stream metadataQueries() {
m -> m.getCrossReference(null, null, "parent", null, null, "child")),
arguments(
"getIndexInfo",
- (MetadataCall) m -> m.getIndexInfo(null, null, "t", false, false)));
+ (MetadataCall) m -> m.getIndexInfo(null, null, "t", false, false)),
+ arguments("getProcedures", (MetadataCall) m -> m.getProcedures(null, null, null)),
+ arguments(
+ "getProcedureColumns",
+ (MetadataCall) m -> m.getProcedureColumns(null, null, null, null)),
+ arguments(
+ "getColumnPrivileges",
+ (MetadataCall) m -> m.getColumnPrivileges(null, null, null, null)),
+ arguments(
+ "getTablePrivileges",
+ (MetadataCall) m -> m.getTablePrivileges(null, null, null)),
+ arguments(
+ "getBestRowIdentifier",
+ (MetadataCall)
+ m -> m.getBestRowIdentifier(
+ null, null, null, DatabaseMetaData.bestRowSession, false)),
+ arguments(
+ "getVersionColumns",
+ (MetadataCall) m -> m.getVersionColumns(null, null, null)),
+ arguments("getUDTs", (MetadataCall) m -> m.getUDTs(null, null, null, null)));
}
@FunctionalInterface
@@ -479,6 +541,23 @@ void getPrimaryKeysLeavesPKNameNullForAnInlinePrimaryKey() throws SQLException {
}
}
+ @Test
+ void getPrimaryKeysReadsAConstraintNameGluedToTheKeywords() throws SQLException {
+ for (String createSql : List.of(
+ "CREATE TABLE `Pk10` (`Col1`, `Col2`, `Col3`, CONSTRAINT `NamedPk`PRIMARY KEY (`Col3`, `Col2`))",
+ "CREATE TABLE `Pk12` (`Col1`, `Col2`, `Col3`, CONSTRAINT`NamedPk`PRIMARY KEY(`Col3`,`Col2`))")) {
+ LibsqlDatabaseMetaData metadata =
+ new LibsqlDatabaseMetaData(new PrimaryKeysConnection(createSql));
+
+ try (ResultSet rs = metadata.getPrimaryKeys(null, null, "pk")) {
+ assertThat(rs.next()).isTrue();
+ assertThat(rs.getString("PK_NAME"))
+ .as(createSql)
+ .isEqualTo("NamedPk");
+ }
+ }
+ }
+
@Test
void getTypeInfoReturnsTheXerialTypeTable() throws SQLException {
LibsqlDatabaseMetaData metadata = new LibsqlDatabaseMetaData(new RecordingConnection(emptyResult()));
@@ -525,14 +604,6 @@ void everyUnsupportedMethodThrowsSqlFeatureNotSupportedException() {
LibsqlDatabaseMetaData metadata = new LibsqlDatabaseMetaData(new RecordingConnection(emptyResult()));
List calls = List.of(
- new UnsupportedCall("getProcedures", () -> metadata.getProcedures(null, null, null)),
- new UnsupportedCall("getProcedureColumns", () -> metadata.getProcedureColumns(null, null, null, null)),
- new UnsupportedCall("getColumnPrivileges", () -> metadata.getColumnPrivileges(null, null, null, null)),
- new UnsupportedCall("getTablePrivileges", () -> metadata.getTablePrivileges(null, null, null)),
- new UnsupportedCall("getBestRowIdentifier", () -> metadata.getBestRowIdentifier(
- null, null, null, DatabaseMetaData.bestRowSession, false)),
- new UnsupportedCall("getVersionColumns", () -> metadata.getVersionColumns(null, null, null)),
- new UnsupportedCall("getUDTs", () -> metadata.getUDTs(null, null, null, null)),
new UnsupportedCall("getSuperTypes", () -> metadata.getSuperTypes(null, null, null)),
new UnsupportedCall("getSuperTables", () -> metadata.getSuperTables(null, null, null)),
new UnsupportedCall("getAttributes", () -> metadata.getAttributes(null, null, null, null)),
@@ -548,6 +619,115 @@ void everyUnsupportedMethodThrowsSqlFeatureNotSupportedException() {
}
}
+ @ParameterizedTest(name = "{0}")
+ @MethodSource("emptyMetadataShapes")
+ void metadataMethodsWithoutSqliteSupportReturnJavaSqlShapedEmptyResultSets(
+ String method, List columns, MetadataCall call) throws SQLException {
+ LibsqlDatabaseMetaData metadata = new LibsqlDatabaseMetaData(new RecordingConnection(emptyResult()));
+
+ try (ResultSet resultSet = call.execute(metadata)) {
+ assertThat(resultSet.next())
+ .as("%s must return an empty result set", method)
+ .isFalse();
+ assertThat(columnNames(resultSet))
+ .as("%s must return the java.sql column list", method)
+ .containsExactlyElementsOf(columns);
+ }
+ }
+
+ static Stream emptyMetadataShapes() {
+ return Stream.of(
+ arguments(
+ "getProcedures",
+ List.of(
+ "PROCEDURE_CAT",
+ "PROCEDURE_SCHEM",
+ "PROCEDURE_NAME",
+ "UNDEF1",
+ "UNDEF2",
+ "UNDEF3",
+ "REMARKS",
+ "PROCEDURE_TYPE"),
+ (MetadataCall) m -> m.getProcedures(null, null, null)),
+ arguments(
+ "getProcedureColumns",
+ List.of(
+ "PROCEDURE_CAT",
+ "PROCEDURE_SCHEM",
+ "PROCEDURE_NAME",
+ "COLUMN_NAME",
+ "COLUMN_TYPE",
+ "DATA_TYPE",
+ "TYPE_NAME",
+ "PRECISION",
+ "LENGTH",
+ "SCALE",
+ "RADIX",
+ "NULLABLE",
+ "REMARKS"),
+ (MetadataCall) m -> m.getProcedureColumns(null, null, null, null)),
+ arguments(
+ "getColumnPrivileges",
+ List.of(
+ "TABLE_CAT",
+ "TABLE_SCHEM",
+ "TABLE_NAME",
+ "COLUMN_NAME",
+ "GRANTOR",
+ "GRANTEE",
+ "PRIVILEGE",
+ "IS_GRANTABLE"),
+ (MetadataCall) m -> m.getColumnPrivileges(null, null, null, null)),
+ arguments(
+ "getTablePrivileges",
+ List.of(
+ "TABLE_CAT",
+ "TABLE_SCHEM",
+ "TABLE_NAME",
+ "GRANTOR",
+ "GRANTEE",
+ "PRIVILEGE",
+ "IS_GRANTABLE"),
+ (MetadataCall) m -> m.getTablePrivileges(null, null, null)),
+ arguments(
+ "getBestRowIdentifier",
+ List.of(
+ "SCOPE",
+ "COLUMN_NAME",
+ "DATA_TYPE",
+ "TYPE_NAME",
+ "COLUMN_SIZE",
+ "BUFFER_LENGTH",
+ "DECIMAL_DIGITS",
+ "PSEUDO_COLUMN"),
+ (MetadataCall)
+ m -> m.getBestRowIdentifier(
+ null, null, null, DatabaseMetaData.bestRowSession, false)),
+ arguments(
+ "getVersionColumns",
+ List.of(
+ "SCOPE",
+ "COLUMN_NAME",
+ "DATA_TYPE",
+ "TYPE_NAME",
+ "COLUMN_SIZE",
+ "BUFFER_LENGTH",
+ "DECIMAL_DIGITS",
+ "PSEUDO_COLUMN"),
+ (MetadataCall) m -> m.getVersionColumns(null, null, null)),
+ arguments(
+ "getUDTs",
+ List.of(
+ "TYPE_CAT",
+ "TYPE_SCHEM",
+ "TYPE_NAME",
+ "CLASS_NAME",
+ "DATA_TYPE",
+ "REMARKS",
+ "BASE_TYPE"),
+ (MetadataCall) m -> m.getUDTs(null, null, null, null)));
+ }
+
private record UnsupportedCall(String name, org.assertj.core.api.ThrowableAssert.ThrowingCallable call) {}
private static List columnNames(ResultSet rs) throws SQLException {
@@ -674,6 +854,32 @@ public StmtResult executeStatement(Stmt stmt) {
}
}
+ private static final class CompositePrimaryKeysConnection extends LibsqlConnection {
+ private CompositePrimaryKeysConnection() {
+ super(null, null);
+ }
+
+ @Override
+ public StmtResult executeStatement(Stmt stmt) {
+ return new StmtResult(
+ List.of(
+ new Col("COLUMN_NAME", "TEXT"),
+ new Col("KEY_SEQ", "INTEGER"),
+ new Col("CREATE_SQL", "TEXT")),
+ List.of(
+ new Row(List.of(
+ Value.of("col3"),
+ Value.of(1),
+ Value.of("CREATE TABLE pk (col1, col2, col3, col4, PRIMARY KEY (col3, col2))"))),
+ new Row(List.of(
+ Value.of("col2"),
+ Value.of(2),
+ Value.of("CREATE TABLE pk (col1, col2, col3, col4, PRIMARY KEY (col3, col2))")))),
+ 0,
+ null);
+ }
+ }
+
private static StmtResult versionResult(String version) {
return new StmtResult(
List.of(new Col("sqlite_version()", "TEXT")),