diff --git a/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlResultSetMetaData.java b/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlResultSetMetaData.java index 42f5f15..afc57d0 100644 --- a/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlResultSetMetaData.java +++ b/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlResultSetMetaData.java @@ -2,17 +2,12 @@ import com.github.magnusp.libsql.hrana.codec.Col; import com.github.magnusp.libsql.hrana.codec.Row; -import com.github.magnusp.libsql.hrana.codec.Stmt; -import com.github.magnusp.libsql.hrana.codec.StmtResult; import com.github.magnusp.libsql.hrana.codec.Value; import java.sql.ResultSetMetaData; import java.sql.SQLException; -import java.util.ArrayList; -import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; -import java.util.Map; public class LibsqlResultSetMetaData implements ResultSetMetaData { @@ -21,10 +16,7 @@ public class LibsqlResultSetMetaData implements ResultSetMetaData { private final String sql; private final LibsqlConnection connection; private final LibsqlResultSet resultSet; - - private boolean tableContextResolved; - private SqlTableReference.Resolution columnOrigins; - private final Map tableInfoCache = new LinkedHashMap<>(); + private final TableMetadataResolver tableMetadata; public LibsqlResultSetMetaData(List cols) { this(cols, List.of(), null, null, null); @@ -41,6 +33,7 @@ public LibsqlResultSetMetaData(List cols) { this.sql = sql; this.connection = connection; this.resultSet = resultSet; + this.tableMetadata = new TableMetadataResolver(connection, sql, this.cols.size()); } @Override @@ -57,8 +50,9 @@ private Col getCol(int column) throws SQLException { @Override public boolean isAutoIncrement(int column) throws SQLException { - TableInfo table = tableForColumn(column); - ColumnInfo info = table != null ? table.columns().get(columnName(column)) : null; + TableMetadataResolver.TableInfo table = tableMetadata.tableForColumn(column); + TableMetadataResolver.ColumnInfo info = + table != null ? table.columns().get(columnName(column)) : null; return info != null && info.primaryKey() && SqlAutoIncrement.isAutoIncrementColumn(table.createSql(), columnName(column)); @@ -84,11 +78,11 @@ public boolean isCurrency(int column) throws SQLException { @Override public int isNullable(int column) throws SQLException { - TableInfo table = tableForColumn(column); + TableMetadataResolver.TableInfo table = tableMetadata.tableForColumn(column); if (table == null) { return columnNullableUnknown; } - ColumnInfo info = table.columns().get(columnName(column)); + TableMetadataResolver.ColumnInfo info = table.columns().get(columnName(column)); if (info == null) { return columnNullable; } @@ -147,7 +141,7 @@ public int getScale(int column) throws SQLException { @Override public String getTableName(int column) throws SQLException { - TableInfo table = tableForColumn(column); + TableMetadataResolver.TableInfo table = tableMetadata.tableForColumn(column); return table != null ? table.name() : ""; } @@ -220,148 +214,6 @@ private Value sampleValue(int column) { return null; } - private TableInfo tableForColumn(int column) throws SQLException { - getCol(column); - SqlTableReference.TableRef reference = originTable(column); - return reference != null ? tableInfo(reference) : null; - } - - private SqlTableReference.TableRef originTable(int column) throws SQLException { - SqlTableReference.Resolution resolution = resolveColumnOrigins(); - return resolution != null ? resolution.table(column - 1) : null; - } - - private SqlTableReference.Resolution resolveColumnOrigins() throws SQLException { - if (!tableContextResolved) { - SqlTableReference.Resolution resolved = null; - if (connection != null && sql != null) { - resolved = SqlTableReference.resolve(sql, reference -> { - TableInfo info = tableInfo(reference); - if (info == null) { - return null; - } - return new SqlTableReference.TableDefinition( - info.createSql(), new ArrayList<>(info.columns().keySet())); - }, cols.size()); - } - columnOrigins = resolved; - tableContextResolved = true; - } - return columnOrigins; - } - - private TableInfo tableInfo(SqlTableReference.TableRef reference) throws SQLException { - String schema = reference.schema() != null ? reference.schema().toLowerCase(Locale.ROOT) : ""; - String table = reference.table() != null ? reference.table().toLowerCase(Locale.ROOT) : ""; - String key = schema + "." + table; - if (tableInfoCache.containsKey(key)) { - return tableInfoCache.get(key); - } - TableInfo info = loadTableInfo(reference); - tableInfoCache.put(key, info); - return info; - } - - private TableInfo loadTableInfo(SqlTableReference.TableRef reference) throws SQLException { - String schema = reference.schema(); - String table = reference.table(); - if (schema == null) { - TableInfo temp = loadTableInfo("temp", table); - if (temp != null) { - return temp; - } - return loadTableInfo("main", table); - } - return switch (schema.toLowerCase(Locale.ROOT)) { - case "main" -> loadTableInfo("main", table); - case "temp" -> loadTableInfo("temp", table); - default -> loadAttachedTableInfo(schema, table); - }; - } - - private TableInfo loadTableInfo(String schema, String table) throws SQLException { - String master = schema.equals("temp") ? "sqlite_temp_master" : "sqlite_master"; - String query = "SELECT m.name, m.sql, p.name, p.\"notnull\", p.pk " - + "FROM " + master + " m JOIN pragma_table_xinfo(?, ?) p " - + "WHERE m.type IN ('table', 'view') AND m.name COLLATE NOCASE = ? " - + "AND p.hidden != 1 " - + "ORDER BY p.cid"; - StmtResult result = connection.executeStatement( - new Stmt(query, List.of(Value.of(table), Value.of(schema), Value.of(table)))); - return toTableInfo(result, table); - } - - private TableInfo loadAttachedTableInfo(String schema, String table) throws SQLException { - String actualSchema = findSchema(schema); - if (actualSchema == null) { - return null; - } - String quotedSchema = "\"" + actualSchema.replace("\"", "\"\"") + "\""; - String query = "SELECT m.name, m.sql, p.name, p.\"notnull\", p.pk " - + "FROM " + quotedSchema + ".sqlite_master m JOIN pragma_table_xinfo(?, ?) p " - + "WHERE m.type IN ('table', 'view') AND m.name COLLATE NOCASE = ? " - + "AND p.hidden != 1 " - + "ORDER BY p.cid"; - StmtResult result = connection.executeStatement(new Stmt( - query, List.of(Value.of(table), Value.of(actualSchema), Value.of(table)))); - return toTableInfo(result, table); - } - - private String findSchema(String schema) throws SQLException { - StmtResult databases = - connection.executeStatement(new Stmt("SELECT name FROM pragma_database_list")); - if (databases == null || databases.rows() == null) { - return null; - } - for (Row row : databases.rows()) { - List values = row.values(); - if (values == null || values.isEmpty()) { - continue; - } - String name = text(values.get(0)); - if (name != null && name.equalsIgnoreCase(schema)) { - return name; - } - } - return null; - } - - private static TableInfo toTableInfo(StmtResult result, String fallbackName) { - if (result == null || result.rows() == null || result.rows().isEmpty()) { - return null; - } - String tableName = null; - String createSql = null; - Map columns = new LinkedHashMap<>(); - for (Row row : result.rows()) { - List values = row.values(); - if (values == null || values.size() < 5) { - continue; - } - if (tableName == null) { - tableName = text(values.get(0)); - createSql = text(values.get(1)); - } - String columnName = text(values.get(2)); - if (columnName != null) { - columns.put( - columnName.toLowerCase(Locale.ROOT), - new ColumnInfo( - XerialColumnType.integer(values.get(3)) != 0, - XerialColumnType.integer(values.get(4)) != 0)); - } - } - return new TableInfo(tableName != null ? tableName : fallbackName, createSql, columns); - } - - private static String text(Value value) { - return value instanceof Value.Text t ? t.value() : null; - } - - private record ColumnInfo(boolean notNull, boolean primaryKey) {} - - private record TableInfo(String name, String createSql, Map columns) {} - @Override public T unwrap(Class iface) throws SQLException { if (iface.isInstance(this)) return iface.cast(this); diff --git a/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/TableMetadataResolver.java b/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/TableMetadataResolver.java new file mode 100644 index 0000000..3e7c124 --- /dev/null +++ b/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/TableMetadataResolver.java @@ -0,0 +1,174 @@ +package com.github.magnusp.libsql.jdbc; + +import com.github.magnusp.libsql.hrana.codec.Row; +import com.github.magnusp.libsql.hrana.codec.Stmt; +import com.github.magnusp.libsql.hrana.codec.StmtResult; +import com.github.magnusp.libsql.hrana.codec.Value; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +class TableMetadataResolver { + + private final LibsqlConnection connection; + private final String sql; + private final int columnCount; + private final Map tableInfoCache = new LinkedHashMap<>(); + + private boolean tableContextResolved; + private SqlTableReference.Resolution columnOrigins; + + TableMetadataResolver(LibsqlConnection connection, String sql, int columnCount) { + this.connection = connection; + this.sql = sql; + this.columnCount = columnCount; + } + + TableInfo tableForColumn(int column) throws SQLException { + if (column < 1 || column > columnCount) { + throw new SQLException("Invalid column index: " + column + ", must be between 1 and " + columnCount); + } + SqlTableReference.TableRef reference = originTable(column); + return reference != null ? tableInfo(reference) : null; + } + + private SqlTableReference.TableRef originTable(int column) throws SQLException { + SqlTableReference.Resolution resolution = resolveColumnOrigins(); + return resolution != null ? resolution.table(column - 1) : null; + } + + private SqlTableReference.Resolution resolveColumnOrigins() throws SQLException { + if (!tableContextResolved) { + SqlTableReference.Resolution resolved = null; + if (connection != null && sql != null) { + resolved = SqlTableReference.resolve(sql, reference -> { + TableInfo info = tableInfo(reference); + if (info == null) { + return null; + } + return new SqlTableReference.TableDefinition( + info.createSql(), new ArrayList<>(info.columns().keySet())); + }, columnCount); + } + columnOrigins = resolved; + tableContextResolved = true; + } + return columnOrigins; + } + + private TableInfo tableInfo(SqlTableReference.TableRef reference) throws SQLException { + String schema = reference.schema() != null ? reference.schema().toLowerCase(Locale.ROOT) : ""; + String table = reference.table() != null ? reference.table().toLowerCase(Locale.ROOT) : ""; + String key = schema + "." + table; + if (tableInfoCache.containsKey(key)) { + return tableInfoCache.get(key); + } + TableInfo info = loadTableInfo(reference); + tableInfoCache.put(key, info); + return info; + } + + private TableInfo loadTableInfo(SqlTableReference.TableRef reference) throws SQLException { + String schema = reference.schema(); + String table = reference.table(); + if (schema == null) { + TableInfo temp = loadTableInfo("temp", table); + if (temp != null) { + return temp; + } + return loadTableInfo("main", table); + } + return switch (schema.toLowerCase(Locale.ROOT)) { + case "main" -> loadTableInfo("main", table); + case "temp" -> loadTableInfo("temp", table); + default -> loadAttachedTableInfo(schema, table); + }; + } + + private TableInfo loadTableInfo(String schema, String table) throws SQLException { + String master = schema.equals("temp") ? "sqlite_temp_master" : "sqlite_master"; + String query = "SELECT m.name, m.sql, p.name, p.\"notnull\", p.pk " + + "FROM " + master + " m JOIN pragma_table_xinfo(?, ?) p " + + "WHERE m.type IN ('table', 'view') AND m.name COLLATE NOCASE = ? " + + "AND p.hidden != 1 " + + "ORDER BY p.cid"; + StmtResult result = connection.executeStatement( + new Stmt(query, List.of(Value.of(table), Value.of(schema), Value.of(table)))); + return toTableInfo(result, table); + } + + private TableInfo loadAttachedTableInfo(String schema, String table) throws SQLException { + String actualSchema = findSchema(schema); + if (actualSchema == null) { + return null; + } + String quotedSchema = "\"" + actualSchema.replace("\"", "\"\"") + "\""; + String query = "SELECT m.name, m.sql, p.name, p.\"notnull\", p.pk " + + "FROM " + quotedSchema + ".sqlite_master m JOIN pragma_table_xinfo(?, ?) p " + + "WHERE m.type IN ('table', 'view') AND m.name COLLATE NOCASE = ? " + + "AND p.hidden != 1 " + + "ORDER BY p.cid"; + StmtResult result = connection.executeStatement(new Stmt( + query, List.of(Value.of(table), Value.of(actualSchema), Value.of(table)))); + return toTableInfo(result, table); + } + + private String findSchema(String schema) throws SQLException { + StmtResult databases = + connection.executeStatement(new Stmt("SELECT name FROM pragma_database_list")); + if (databases == null || databases.rows() == null) { + return null; + } + for (Row row : databases.rows()) { + List values = row.values(); + if (values == null || values.isEmpty()) { + continue; + } + String name = text(values.get(0)); + if (name != null && name.equalsIgnoreCase(schema)) { + return name; + } + } + return null; + } + + private static TableInfo toTableInfo(StmtResult result, String fallbackName) { + if (result == null || result.rows() == null || result.rows().isEmpty()) { + return null; + } + String tableName = null; + String createSql = null; + Map columns = new LinkedHashMap<>(); + for (Row row : result.rows()) { + List values = row.values(); + if (values == null || values.size() < 5) { + continue; + } + if (tableName == null) { + tableName = text(values.get(0)); + createSql = text(values.get(1)); + } + String columnName = text(values.get(2)); + if (columnName != null) { + columns.put( + columnName.toLowerCase(Locale.ROOT), + new ColumnInfo( + XerialColumnType.integer(values.get(3)) != 0, + XerialColumnType.integer(values.get(4)) != 0)); + } + } + return new TableInfo(tableName != null ? tableName : fallbackName, createSql, columns); + } + + private static String text(Value value) { + return value instanceof Value.Text t ? t.value() : null; + } + + record ColumnInfo(boolean notNull, boolean primaryKey) {} + + record TableInfo(String name, String createSql, Map columns) {} +}