diff --git a/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/ColumnMetadata.java b/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/ColumnMetadata.java
new file mode 100644
index 0000000..4d61355
--- /dev/null
+++ b/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/ColumnMetadata.java
@@ -0,0 +1,177 @@
+package com.github.magnusp.libsql.jdbc;
+
+import com.github.magnusp.libsql.hrana.codec.Col;
+import com.github.magnusp.libsql.hrana.codec.Row;
+import com.github.magnusp.libsql.hrana.codec.StmtResult;
+import com.github.magnusp.libsql.hrana.codec.Value;
+
+import java.sql.DatabaseMetaData;
+import java.sql.Types;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.regex.Pattern;
+
+/**
+ * Shapes {@code pragma_table_xinfo} rows into the 24-column JDBC {@link DatabaseMetaData#getColumns}
+ * shape. The pragma select list is positional by contract; from {@link PragmaRow} onward every value
+ * has a name.
+ */
+final class ColumnMetadata {
+
+ private static final Pattern INTEGER_TYPE = Pattern.compile(".*(INT|BOOL).*");
+ 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 int UNKNOWN_COLUMN_SIZE = 2_000_000_000;
+ private static final int DEFAULT_DECIMAL_DIGITS = 10;
+
+ private ColumnMetadata() {}
+
+ static StmtResult columns(StmtResult raw, List
columns) {
+ List rows = new ArrayList<>();
+ if (raw != null && raw.rows() != null) {
+ for (Row row : raw.rows()) {
+ rows.add(JdbcRow.from(PragmaRow.fromRow(row)).toRow());
+ }
+ }
+ return new StmtResult(columns, rows, 0, null);
+ }
+
+ /** One {@code pragma_table_xinfo} row, in the order {@code MetadataSql.columns} selects it. */
+ private record PragmaRow(
+ String tableName,
+ String createSql,
+ long cid,
+ String columnName,
+ String declaredType,
+ boolean notNull,
+ String columnDefault,
+ boolean primaryKey,
+ long hidden) {
+
+ private static PragmaRow fromRow(Row row) {
+ List values = row.values();
+ return new PragmaRow(
+ text(values.get(0)),
+ text(values.get(1)),
+ integer(values.get(2)),
+ text(values.get(3)),
+ text(values.get(4)),
+ integer(values.get(5)) != 0,
+ text(values.get(6)),
+ integer(values.get(7)) != 0,
+ integer(values.get(8)));
+ }
+ }
+
+ private record JdbcRow(
+ String tableName,
+ String columnName,
+ ColumnShape shape,
+ long ordinalPosition,
+ boolean notNull,
+ String columnDefault,
+ boolean autoIncrement,
+ boolean generated) {
+
+ private static JdbcRow from(PragmaRow pragma) {
+ return new JdbcRow(
+ pragma.tableName(),
+ pragma.columnName(),
+ columnShape(pragma.declaredType()),
+ pragma.cid() + 1,
+ pragma.notNull(),
+ pragma.columnDefault(),
+ pragma.primaryKey()
+ && SqlAutoIncrement.isAutoIncrementColumn(
+ pragma.createSql(), pragma.columnName()),
+ pragma.hidden() == 2 || pragma.hidden() == 3);
+ }
+
+ private Row toRow() {
+ return new Row(List.of(
+ Value.ofNull(), // TABLE_CAT
+ Value.ofNull(), // TABLE_SCHEM
+ Value.of(tableName),
+ Value.of(columnName),
+ Value.of(shape.jdbcType()),
+ Value.of(shape.typeName()),
+ Value.of(shape.columnSize()),
+ Value.of(UNKNOWN_COLUMN_SIZE), // BUFFER_LENGTH
+ Value.of(shape.decimalDigits()),
+ Value.of(10), // NUM_PREC_RADIX is 10 by JDBC, not DECIMAL_DIGITS
+ Value.of(notNull ? DatabaseMetaData.columnNoNulls : DatabaseMetaData.columnNullable),
+ Value.ofNull(), // REMARKS stays null; libSQL has no column comments
+ Value.of(columnDefault),
+ Value.of(0), // SQL_DATA_TYPE
+ Value.of(0), // SQL_DATETIME_SUB
+ Value.of(UNKNOWN_COLUMN_SIZE), // CHAR_OCTET_LENGTH
+ Value.of(ordinalPosition),
+ Value.of(notNull ? "NO" : "YES"),
+ Value.ofNull(), // SCOPE_CATALOG
+ Value.ofNull(), // SCOPE_SCHEMA
+ Value.ofNull(), // SCOPE_TABLE
+ Value.ofNull(), // SOURCE_DATA_TYPE
+ Value.of(autoIncrement ? "YES" : "NO"),
+ Value.of(generated ? "YES" : "NO")));
+ }
+ }
+
+ private record ColumnShape(int jdbcType, String typeName, int columnSize, int decimalDigits) {}
+
+ private static ColumnShape columnShape(String declaredType) {
+ String typeName = declaredType == null ? "TEXT" : declaredType.toUpperCase(Locale.ENGLISH);
+ int jdbcType;
+ int decimalDigits = DEFAULT_DECIMAL_DIGITS;
+ if (INTEGER_TYPE.matcher(typeName).find()) {
+ jdbcType = Types.INTEGER;
+ decimalDigits = 0;
+ } else if (VARCHAR_TYPE.matcher(typeName).find()) {
+ jdbcType = Types.VARCHAR;
+ decimalDigits = 0;
+ } else if (FLOAT_TYPE.matcher(typeName).find()) {
+ jdbcType = Types.FLOAT;
+ } else {
+ jdbcType = Types.VARCHAR;
+ }
+
+ int columnSize = UNKNOWN_COLUMN_SIZE;
+ int startOfDimension = typeName.indexOf('(');
+ if (startOfDimension > 0) {
+ int endOfDimension = typeName.indexOf(')', startOfDimension);
+ if (endOfDimension > 0) {
+ int dimensionSeparator = typeName.indexOf(',', startOfDimension);
+ String integerPart;
+ String decimalPart;
+ if (dimensionSeparator > 0) {
+ integerPart = typeName.substring(startOfDimension + 1, dimensionSeparator);
+ decimalPart = typeName.substring(dimensionSeparator + 1, endOfDimension);
+ } else {
+ integerPart = typeName.substring(startOfDimension + 1, endOfDimension);
+ decimalPart = null;
+ }
+ try {
+ int integerValue = Integer.parseUnsignedInt(integerPart.trim());
+ if (decimalPart != null) {
+ decimalDigits = Integer.parseUnsignedInt(decimalPart.trim());
+ columnSize = integerValue + decimalDigits;
+ } else {
+ decimalDigits = 0;
+ columnSize = integerValue;
+ }
+ } catch (NumberFormatException ignored) {
+ }
+ }
+ typeName = typeName.substring(0, startOfDimension).trim();
+ }
+ return new ColumnShape(jdbcType, typeName, columnSize, decimalDigits);
+ }
+
+ private static String text(Value value) {
+ return value instanceof Value.Text text ? text.value() : null;
+ }
+
+ private static long integer(Value value) {
+ return value instanceof Value.Integer integer ? integer.value() : 0L;
+ }
+}
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 bd10b28..bd12a6a 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
@@ -23,17 +23,12 @@
public class LibsqlDatabaseMetaData implements DatabaseMetaData {
- private static final Pattern INTEGER_TYPE = Pattern.compile(".*(INT|BOOL).*");
- 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",
Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
private static final Pattern NAMED_FOREIGN_KEY = Pattern.compile(
"\\bCONSTRAINT\\s+(\"[^\"]+\"|`[^`]+`|\\[[^\\]]+\\]|'[^']+'|[^\\s]+)\\s+FOREIGN\\s+KEY\\b",
Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
- private static final int UNKNOWN_COLUMN_SIZE = 2_000_000_000;
- private static final int DEFAULT_DECIMAL_DIGITS = 10;
private static final List FOREIGN_KEY_COLUMNS = List.of(
new Col("PKTABLE_CAT", "TEXT"),
@@ -51,6 +46,32 @@ public class LibsqlDatabaseMetaData implements DatabaseMetaData {
new Col("PK_NAME", "TEXT"),
new Col("DEFERRABILITY", "INTEGER"));
+ private static final List COLUMN_COLUMNS = List.of(
+ new Col("TABLE_CAT", "TEXT"),
+ new Col("TABLE_SCHEM", "TEXT"),
+ new Col("TABLE_NAME", "TEXT"),
+ 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("NUM_PREC_RADIX", "INTEGER"),
+ new Col("NULLABLE", "INTEGER"),
+ new Col("REMARKS", "TEXT"),
+ new Col("COLUMN_DEF", "TEXT"),
+ new Col("SQL_DATA_TYPE", "INTEGER"),
+ new Col("SQL_DATETIME_SUB", "INTEGER"),
+ new Col("CHAR_OCTET_LENGTH", "INTEGER"),
+ new Col("ORDINAL_POSITION", "INTEGER"),
+ new Col("IS_NULLABLE", "TEXT"),
+ new Col("SCOPE_CATALOG", "TEXT"),
+ new Col("SCOPE_SCHEMA", "TEXT"),
+ new Col("SCOPE_TABLE", "TEXT"),
+ new Col("SOURCE_DATA_TYPE", "INTEGER"),
+ new Col("IS_AUTOINCREMENT", "TEXT"),
+ new Col("IS_GENERATEDCOLUMN", "TEXT"));
+
private static final List PRIMARY_KEY_COLUMNS = List.of(
new Col("TABLE_CAT", "TEXT"),
new Col("TABLE_SCHEM", "TEXT"),
@@ -458,130 +479,8 @@ public ResultSet getColumns(String catalog, String schemaPattern, String tableNa
String columnPattern = columnNamePattern == null || columnNamePattern.isEmpty() ? "%" : columnNamePattern;
// getColumns synthesizes its column labels, so the raw SQL must not drive column-origin
// resolution for the result set.
- return executeMetadataQuery(MetadataSql.columns(tablePattern, columnPattern), null, LibsqlDatabaseMetaData::columnsResult);
- }
-
- private static StmtResult columnsResult(StmtResult raw) {
- List columns = List.of(
- new Col("TABLE_CAT", "TEXT"),
- new Col("TABLE_SCHEM", "TEXT"),
- new Col("TABLE_NAME", "TEXT"),
- 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("NUM_PREC_RADIX", "INTEGER"),
- new Col("NULLABLE", "INTEGER"),
- new Col("REMARKS", "TEXT"),
- new Col("COLUMN_DEF", "TEXT"),
- new Col("SQL_DATA_TYPE", "INTEGER"),
- new Col("SQL_DATETIME_SUB", "INTEGER"),
- new Col("CHAR_OCTET_LENGTH", "INTEGER"),
- new Col("ORDINAL_POSITION", "INTEGER"),
- new Col("IS_NULLABLE", "TEXT"),
- new Col("SCOPE_CATALOG", "TEXT"),
- new Col("SCOPE_SCHEMA", "TEXT"),
- new Col("SCOPE_TABLE", "TEXT"),
- new Col("SOURCE_DATA_TYPE", "INTEGER"),
- new Col("IS_AUTOINCREMENT", "TEXT"),
- new Col("IS_GENERATEDCOLUMN", "TEXT"));
- List rows = new ArrayList<>();
- if (raw != null && raw.rows() != null) {
- for (Row row : raw.rows()) {
- List values = row.values();
- String tableName = text(values.get(0));
- String createSql = text(values.get(1));
- long cid = integer(values.get(2));
- String columnName = text(values.get(3));
- String declaredType = text(values.get(4));
- boolean notNull = integer(values.get(5)) != 0;
- String columnDefault = text(values.get(6));
- boolean primaryKey = integer(values.get(7)) != 0;
- long hidden = integer(values.get(8));
-
- ColumnShape shape = columnShape(declaredType);
- boolean autoIncrement = primaryKey
- && SqlAutoIncrement.isAutoIncrementColumn(createSql, columnName);
- boolean generated = hidden == 2 || hidden == 3;
-
- rows.add(new Row(List.of(
- Value.ofNull(),
- Value.ofNull(),
- Value.of(tableName),
- Value.of(columnName),
- Value.of(shape.jdbcType()),
- Value.of(shape.typeName()),
- Value.of(shape.columnSize()),
- Value.of(UNKNOWN_COLUMN_SIZE),
- Value.of(shape.decimalDigits()),
- Value.of(10),
- Value.of(notNull ? DatabaseMetaData.columnNoNulls : DatabaseMetaData.columnNullable),
- Value.ofNull(),
- Value.of(columnDefault),
- Value.of(0),
- Value.of(0),
- Value.of(UNKNOWN_COLUMN_SIZE),
- Value.of(cid + 1),
- Value.of(notNull ? "NO" : "YES"),
- Value.ofNull(),
- Value.ofNull(),
- Value.ofNull(),
- Value.ofNull(),
- Value.of(autoIncrement ? "YES" : "NO"),
- Value.of(generated ? "YES" : "NO"))));
- }
- }
- return new StmtResult(columns, rows, 0, null);
- }
-
- private static ColumnShape columnShape(String declaredType) {
- String typeName = declaredType == null ? "TEXT" : declaredType.toUpperCase(Locale.ENGLISH);
- int jdbcType;
- int decimalDigits = DEFAULT_DECIMAL_DIGITS;
- if (INTEGER_TYPE.matcher(typeName).find()) {
- jdbcType = Types.INTEGER;
- decimalDigits = 0;
- } else if (VARCHAR_TYPE.matcher(typeName).find()) {
- jdbcType = Types.VARCHAR;
- decimalDigits = 0;
- } else if (FLOAT_TYPE.matcher(typeName).find()) {
- jdbcType = Types.FLOAT;
- } else {
- jdbcType = Types.VARCHAR;
- }
-
- int columnSize = UNKNOWN_COLUMN_SIZE;
- int startOfDimension = typeName.indexOf('(');
- if (startOfDimension > 0) {
- int endOfDimension = typeName.indexOf(')', startOfDimension);
- if (endOfDimension > 0) {
- int dimensionSeparator = typeName.indexOf(',', startOfDimension);
- String integerPart;
- String decimalPart;
- if (dimensionSeparator > 0) {
- integerPart = typeName.substring(startOfDimension + 1, dimensionSeparator);
- decimalPart = typeName.substring(dimensionSeparator + 1, endOfDimension);
- } else {
- integerPart = typeName.substring(startOfDimension + 1, endOfDimension);
- decimalPart = null;
- }
- try {
- int integerValue = Integer.parseUnsignedInt(integerPart.trim());
- if (decimalPart != null) {
- decimalDigits = Integer.parseUnsignedInt(decimalPart.trim());
- columnSize = integerValue + decimalDigits;
- } else {
- decimalDigits = 0;
- columnSize = integerValue;
- }
- } catch (NumberFormatException ignored) {
- }
- }
- typeName = typeName.substring(0, startOfDimension).trim();
- }
- return new ColumnShape(jdbcType, typeName, columnSize, decimalDigits);
+ return executeMetadataQuery(MetadataSql.columns(tablePattern, columnPattern), null,
+ raw -> ColumnMetadata.columns(raw, COLUMN_COLUMNS));
}
private static String text(Value value) {
@@ -825,8 +724,6 @@ private Row toRow() {
}
}
- private record ColumnShape(int jdbcType, String typeName, int columnSize, int decimalDigits) {}
-
@Override public ResultSet getPrimaryKeys(String catalog, String schema, String table) throws SQLException {
return executeMetadataQuery(MetadataSql.primaryKeys(table), null, raw -> primaryKeysResult(raw, table));
}