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")),