Skip to content

Commit e61ca82

Browse files
committed
refactor(jdbc): shape getColumns rows through named records
1 parent e19a419 commit e61ca82

2 files changed

Lines changed: 205 additions & 131 deletions

File tree

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
package com.github.magnusp.libsql.jdbc;
2+
3+
import com.github.magnusp.libsql.hrana.codec.Col;
4+
import com.github.magnusp.libsql.hrana.codec.Row;
5+
import com.github.magnusp.libsql.hrana.codec.StmtResult;
6+
import com.github.magnusp.libsql.hrana.codec.Value;
7+
8+
import java.sql.DatabaseMetaData;
9+
import java.sql.Types;
10+
import java.util.ArrayList;
11+
import java.util.List;
12+
import java.util.Locale;
13+
import java.util.regex.Pattern;
14+
15+
/**
16+
* Shapes {@code pragma_table_xinfo} rows into the 24-column JDBC {@link DatabaseMetaData#getColumns}
17+
* shape. The pragma select list is positional by contract; from {@link PragmaRow} onward every value
18+
* has a name.
19+
*/
20+
final class ColumnMetadata {
21+
22+
private static final Pattern INTEGER_TYPE = Pattern.compile(".*(INT|BOOL).*");
23+
private static final Pattern VARCHAR_TYPE = Pattern.compile(".*(CHAR|CLOB|TEXT|BLOB).*");
24+
private static final Pattern FLOAT_TYPE = Pattern.compile(".*(REAL|FLOA|DOUB|DEC|NUM).*");
25+
private static final int UNKNOWN_COLUMN_SIZE = 2_000_000_000;
26+
private static final int DEFAULT_DECIMAL_DIGITS = 10;
27+
28+
private ColumnMetadata() {}
29+
30+
static StmtResult columns(StmtResult raw, List<Col> columns) {
31+
List<Row> rows = new ArrayList<>();
32+
if (raw != null && raw.rows() != null) {
33+
for (Row row : raw.rows()) {
34+
rows.add(JdbcRow.from(PragmaRow.fromRow(row)).toRow());
35+
}
36+
}
37+
return new StmtResult(columns, rows, 0, null);
38+
}
39+
40+
/** One {@code pragma_table_xinfo} row, in the order {@code MetadataSql.columns} selects it. */
41+
private record PragmaRow(
42+
String tableName,
43+
String createSql,
44+
long cid,
45+
String columnName,
46+
String declaredType,
47+
boolean notNull,
48+
String columnDefault,
49+
boolean primaryKey,
50+
long hidden) {
51+
52+
private static PragmaRow fromRow(Row row) {
53+
List<Value> values = row.values();
54+
return new PragmaRow(
55+
text(values.get(0)),
56+
text(values.get(1)),
57+
integer(values.get(2)),
58+
text(values.get(3)),
59+
text(values.get(4)),
60+
integer(values.get(5)) != 0,
61+
text(values.get(6)),
62+
integer(values.get(7)) != 0,
63+
integer(values.get(8)));
64+
}
65+
}
66+
67+
private record JdbcRow(
68+
String tableName,
69+
String columnName,
70+
ColumnShape shape,
71+
long ordinalPosition,
72+
boolean notNull,
73+
String columnDefault,
74+
boolean autoIncrement,
75+
boolean generated) {
76+
77+
private static JdbcRow from(PragmaRow pragma) {
78+
return new JdbcRow(
79+
pragma.tableName(),
80+
pragma.columnName(),
81+
columnShape(pragma.declaredType()),
82+
pragma.cid() + 1,
83+
pragma.notNull(),
84+
pragma.columnDefault(),
85+
pragma.primaryKey()
86+
&& SqlAutoIncrement.isAutoIncrementColumn(
87+
pragma.createSql(), pragma.columnName()),
88+
pragma.hidden() == 2 || pragma.hidden() == 3);
89+
}
90+
91+
private Row toRow() {
92+
return new Row(List.of(
93+
Value.ofNull(), // TABLE_CAT
94+
Value.ofNull(), // TABLE_SCHEM
95+
Value.of(tableName),
96+
Value.of(columnName),
97+
Value.of(shape.jdbcType()),
98+
Value.of(shape.typeName()),
99+
Value.of(shape.columnSize()),
100+
Value.of(UNKNOWN_COLUMN_SIZE), // BUFFER_LENGTH
101+
Value.of(shape.decimalDigits()),
102+
Value.of(10), // NUM_PREC_RADIX is 10 by JDBC, not DECIMAL_DIGITS
103+
Value.of(notNull ? DatabaseMetaData.columnNoNulls : DatabaseMetaData.columnNullable),
104+
Value.ofNull(), // REMARKS stays null; libSQL has no column comments
105+
Value.of(columnDefault),
106+
Value.of(0), // SQL_DATA_TYPE
107+
Value.of(0), // SQL_DATETIME_SUB
108+
Value.of(UNKNOWN_COLUMN_SIZE), // CHAR_OCTET_LENGTH
109+
Value.of(ordinalPosition),
110+
Value.of(notNull ? "NO" : "YES"),
111+
Value.ofNull(), // SCOPE_CATALOG
112+
Value.ofNull(), // SCOPE_SCHEMA
113+
Value.ofNull(), // SCOPE_TABLE
114+
Value.ofNull(), // SOURCE_DATA_TYPE
115+
Value.of(autoIncrement ? "YES" : "NO"),
116+
Value.of(generated ? "YES" : "NO")));
117+
}
118+
}
119+
120+
private record ColumnShape(int jdbcType, String typeName, int columnSize, int decimalDigits) {}
121+
122+
private static ColumnShape columnShape(String declaredType) {
123+
String typeName = declaredType == null ? "TEXT" : declaredType.toUpperCase(Locale.ENGLISH);
124+
int jdbcType;
125+
int decimalDigits = DEFAULT_DECIMAL_DIGITS;
126+
if (INTEGER_TYPE.matcher(typeName).find()) {
127+
jdbcType = Types.INTEGER;
128+
decimalDigits = 0;
129+
} else if (VARCHAR_TYPE.matcher(typeName).find()) {
130+
jdbcType = Types.VARCHAR;
131+
decimalDigits = 0;
132+
} else if (FLOAT_TYPE.matcher(typeName).find()) {
133+
jdbcType = Types.FLOAT;
134+
} else {
135+
jdbcType = Types.VARCHAR;
136+
}
137+
138+
int columnSize = UNKNOWN_COLUMN_SIZE;
139+
int startOfDimension = typeName.indexOf('(');
140+
if (startOfDimension > 0) {
141+
int endOfDimension = typeName.indexOf(')', startOfDimension);
142+
if (endOfDimension > 0) {
143+
int dimensionSeparator = typeName.indexOf(',', startOfDimension);
144+
String integerPart;
145+
String decimalPart;
146+
if (dimensionSeparator > 0) {
147+
integerPart = typeName.substring(startOfDimension + 1, dimensionSeparator);
148+
decimalPart = typeName.substring(dimensionSeparator + 1, endOfDimension);
149+
} else {
150+
integerPart = typeName.substring(startOfDimension + 1, endOfDimension);
151+
decimalPart = null;
152+
}
153+
try {
154+
int integerValue = Integer.parseUnsignedInt(integerPart.trim());
155+
if (decimalPart != null) {
156+
decimalDigits = Integer.parseUnsignedInt(decimalPart.trim());
157+
columnSize = integerValue + decimalDigits;
158+
} else {
159+
decimalDigits = 0;
160+
columnSize = integerValue;
161+
}
162+
} catch (NumberFormatException ignored) {
163+
}
164+
}
165+
typeName = typeName.substring(0, startOfDimension).trim();
166+
}
167+
return new ColumnShape(jdbcType, typeName, columnSize, decimalDigits);
168+
}
169+
170+
private static String text(Value value) {
171+
return value instanceof Value.Text text ? text.value() : null;
172+
}
173+
174+
private static long integer(Value value) {
175+
return value instanceof Value.Integer integer ? integer.value() : 0L;
176+
}
177+
}

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

Lines changed: 28 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,12 @@
2323

2424
public class LibsqlDatabaseMetaData implements DatabaseMetaData {
2525

26-
private static final Pattern INTEGER_TYPE = Pattern.compile(".*(INT|BOOL).*");
27-
private static final Pattern VARCHAR_TYPE = Pattern.compile(".*(CHAR|CLOB|TEXT|BLOB).*");
28-
private static final Pattern FLOAT_TYPE = Pattern.compile(".*(REAL|FLOA|DOUB|DEC|NUM).*");
2926
private static final Pattern NAMED_PRIMARY_KEY = Pattern.compile(
3027
"\\bCONSTRAINT\\s*(\"[^\"]+\"|`[^`]+`|\\[[^\\]]+\\]|'[^']+'|[^\\s]+)\\s*PRIMARY\\s+KEY\\b",
3128
Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
3229
private static final Pattern NAMED_FOREIGN_KEY = Pattern.compile(
3330
"\\bCONSTRAINT\\s+(\"[^\"]+\"|`[^`]+`|\\[[^\\]]+\\]|'[^']+'|[^\\s]+)\\s+FOREIGN\\s+KEY\\b",
3431
Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
35-
private static final int UNKNOWN_COLUMN_SIZE = 2_000_000_000;
36-
private static final int DEFAULT_DECIMAL_DIGITS = 10;
3732

3833
private static final List<Col> FOREIGN_KEY_COLUMNS = List.of(
3934
new Col("PKTABLE_CAT", "TEXT"),
@@ -51,6 +46,32 @@ public class LibsqlDatabaseMetaData implements DatabaseMetaData {
5146
new Col("PK_NAME", "TEXT"),
5247
new Col("DEFERRABILITY", "INTEGER"));
5348

49+
private static final List<Col> COLUMN_COLUMNS = List.of(
50+
new Col("TABLE_CAT", "TEXT"),
51+
new Col("TABLE_SCHEM", "TEXT"),
52+
new Col("TABLE_NAME", "TEXT"),
53+
new Col("COLUMN_NAME", "TEXT"),
54+
new Col("DATA_TYPE", "INTEGER"),
55+
new Col("TYPE_NAME", "TEXT"),
56+
new Col("COLUMN_SIZE", "INTEGER"),
57+
new Col("BUFFER_LENGTH", "INTEGER"),
58+
new Col("DECIMAL_DIGITS", "INTEGER"),
59+
new Col("NUM_PREC_RADIX", "INTEGER"),
60+
new Col("NULLABLE", "INTEGER"),
61+
new Col("REMARKS", "TEXT"),
62+
new Col("COLUMN_DEF", "TEXT"),
63+
new Col("SQL_DATA_TYPE", "INTEGER"),
64+
new Col("SQL_DATETIME_SUB", "INTEGER"),
65+
new Col("CHAR_OCTET_LENGTH", "INTEGER"),
66+
new Col("ORDINAL_POSITION", "INTEGER"),
67+
new Col("IS_NULLABLE", "TEXT"),
68+
new Col("SCOPE_CATALOG", "TEXT"),
69+
new Col("SCOPE_SCHEMA", "TEXT"),
70+
new Col("SCOPE_TABLE", "TEXT"),
71+
new Col("SOURCE_DATA_TYPE", "INTEGER"),
72+
new Col("IS_AUTOINCREMENT", "TEXT"),
73+
new Col("IS_GENERATEDCOLUMN", "TEXT"));
74+
5475
private static final List<Col> PRIMARY_KEY_COLUMNS = List.of(
5576
new Col("TABLE_CAT", "TEXT"),
5677
new Col("TABLE_SCHEM", "TEXT"),
@@ -458,130 +479,8 @@ public ResultSet getColumns(String catalog, String schemaPattern, String tableNa
458479
String columnPattern = columnNamePattern == null || columnNamePattern.isEmpty() ? "%" : columnNamePattern;
459480
// getColumns synthesizes its column labels, so the raw SQL must not drive column-origin
460481
// resolution for the result set.
461-
return executeMetadataQuery(MetadataSql.columns(tablePattern, columnPattern), null, LibsqlDatabaseMetaData::columnsResult);
462-
}
463-
464-
private static StmtResult columnsResult(StmtResult raw) {
465-
List<Col> columns = List.of(
466-
new Col("TABLE_CAT", "TEXT"),
467-
new Col("TABLE_SCHEM", "TEXT"),
468-
new Col("TABLE_NAME", "TEXT"),
469-
new Col("COLUMN_NAME", "TEXT"),
470-
new Col("DATA_TYPE", "INTEGER"),
471-
new Col("TYPE_NAME", "TEXT"),
472-
new Col("COLUMN_SIZE", "INTEGER"),
473-
new Col("BUFFER_LENGTH", "INTEGER"),
474-
new Col("DECIMAL_DIGITS", "INTEGER"),
475-
new Col("NUM_PREC_RADIX", "INTEGER"),
476-
new Col("NULLABLE", "INTEGER"),
477-
new Col("REMARKS", "TEXT"),
478-
new Col("COLUMN_DEF", "TEXT"),
479-
new Col("SQL_DATA_TYPE", "INTEGER"),
480-
new Col("SQL_DATETIME_SUB", "INTEGER"),
481-
new Col("CHAR_OCTET_LENGTH", "INTEGER"),
482-
new Col("ORDINAL_POSITION", "INTEGER"),
483-
new Col("IS_NULLABLE", "TEXT"),
484-
new Col("SCOPE_CATALOG", "TEXT"),
485-
new Col("SCOPE_SCHEMA", "TEXT"),
486-
new Col("SCOPE_TABLE", "TEXT"),
487-
new Col("SOURCE_DATA_TYPE", "INTEGER"),
488-
new Col("IS_AUTOINCREMENT", "TEXT"),
489-
new Col("IS_GENERATEDCOLUMN", "TEXT"));
490-
List<Row> rows = new ArrayList<>();
491-
if (raw != null && raw.rows() != null) {
492-
for (Row row : raw.rows()) {
493-
List<Value> values = row.values();
494-
String tableName = text(values.get(0));
495-
String createSql = text(values.get(1));
496-
long cid = integer(values.get(2));
497-
String columnName = text(values.get(3));
498-
String declaredType = text(values.get(4));
499-
boolean notNull = integer(values.get(5)) != 0;
500-
String columnDefault = text(values.get(6));
501-
boolean primaryKey = integer(values.get(7)) != 0;
502-
long hidden = integer(values.get(8));
503-
504-
ColumnShape shape = columnShape(declaredType);
505-
boolean autoIncrement = primaryKey
506-
&& SqlAutoIncrement.isAutoIncrementColumn(createSql, columnName);
507-
boolean generated = hidden == 2 || hidden == 3;
508-
509-
rows.add(new Row(List.of(
510-
Value.ofNull(),
511-
Value.ofNull(),
512-
Value.of(tableName),
513-
Value.of(columnName),
514-
Value.of(shape.jdbcType()),
515-
Value.of(shape.typeName()),
516-
Value.of(shape.columnSize()),
517-
Value.of(UNKNOWN_COLUMN_SIZE),
518-
Value.of(shape.decimalDigits()),
519-
Value.of(10),
520-
Value.of(notNull ? DatabaseMetaData.columnNoNulls : DatabaseMetaData.columnNullable),
521-
Value.ofNull(),
522-
Value.of(columnDefault),
523-
Value.of(0),
524-
Value.of(0),
525-
Value.of(UNKNOWN_COLUMN_SIZE),
526-
Value.of(cid + 1),
527-
Value.of(notNull ? "NO" : "YES"),
528-
Value.ofNull(),
529-
Value.ofNull(),
530-
Value.ofNull(),
531-
Value.ofNull(),
532-
Value.of(autoIncrement ? "YES" : "NO"),
533-
Value.of(generated ? "YES" : "NO"))));
534-
}
535-
}
536-
return new StmtResult(columns, rows, 0, null);
537-
}
538-
539-
private static ColumnShape columnShape(String declaredType) {
540-
String typeName = declaredType == null ? "TEXT" : declaredType.toUpperCase(Locale.ENGLISH);
541-
int jdbcType;
542-
int decimalDigits = DEFAULT_DECIMAL_DIGITS;
543-
if (INTEGER_TYPE.matcher(typeName).find()) {
544-
jdbcType = Types.INTEGER;
545-
decimalDigits = 0;
546-
} else if (VARCHAR_TYPE.matcher(typeName).find()) {
547-
jdbcType = Types.VARCHAR;
548-
decimalDigits = 0;
549-
} else if (FLOAT_TYPE.matcher(typeName).find()) {
550-
jdbcType = Types.FLOAT;
551-
} else {
552-
jdbcType = Types.VARCHAR;
553-
}
554-
555-
int columnSize = UNKNOWN_COLUMN_SIZE;
556-
int startOfDimension = typeName.indexOf('(');
557-
if (startOfDimension > 0) {
558-
int endOfDimension = typeName.indexOf(')', startOfDimension);
559-
if (endOfDimension > 0) {
560-
int dimensionSeparator = typeName.indexOf(',', startOfDimension);
561-
String integerPart;
562-
String decimalPart;
563-
if (dimensionSeparator > 0) {
564-
integerPart = typeName.substring(startOfDimension + 1, dimensionSeparator);
565-
decimalPart = typeName.substring(dimensionSeparator + 1, endOfDimension);
566-
} else {
567-
integerPart = typeName.substring(startOfDimension + 1, endOfDimension);
568-
decimalPart = null;
569-
}
570-
try {
571-
int integerValue = Integer.parseUnsignedInt(integerPart.trim());
572-
if (decimalPart != null) {
573-
decimalDigits = Integer.parseUnsignedInt(decimalPart.trim());
574-
columnSize = integerValue + decimalDigits;
575-
} else {
576-
decimalDigits = 0;
577-
columnSize = integerValue;
578-
}
579-
} catch (NumberFormatException ignored) {
580-
}
581-
}
582-
typeName = typeName.substring(0, startOfDimension).trim();
583-
}
584-
return new ColumnShape(jdbcType, typeName, columnSize, decimalDigits);
482+
return executeMetadataQuery(MetadataSql.columns(tablePattern, columnPattern), null,
483+
raw -> ColumnMetadata.columns(raw, COLUMN_COLUMNS));
585484
}
586485

587486
private static String text(Value value) {
@@ -825,8 +724,6 @@ private Row toRow() {
825724
}
826725
}
827726

828-
private record ColumnShape(int jdbcType, String typeName, int columnSize, int decimalDigits) {}
829-
830727
@Override public ResultSet getPrimaryKeys(String catalog, String schema, String table) throws SQLException {
831728
return executeMetadataQuery(MetadataSql.primaryKeys(table), null, raw -> primaryKeysResult(raw, table));
832729
}

0 commit comments

Comments
 (0)