Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,13 +1,30 @@
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.Stmt;
import com.github.magnusp.libsql.hrana.codec.StmtResult;
import com.github.magnusp.libsql.hrana.codec.Value;

import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.RowIdLifetime;
import java.sql.SQLException;
import java.sql.Types;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.regex.Pattern;

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 int UNKNOWN_COLUMN_SIZE = 2_000_000_000;
private static final int DEFAULT_DECIMAL_DIGITS = 10;

private final LibsqlConnection connection;
private String cachedDatabaseProductVersion;

Expand Down Expand Up @@ -314,33 +331,151 @@ public ResultSet getTables(String catalog, String schemaPattern, String tableNam

@Override
public ResultSet getColumns(String catalog, String schemaPattern, String tableNamePattern, String columnNamePattern) throws SQLException {
String sql = "SELECT NULL AS TABLE_CAT, NULL AS TABLE_SCHEM, m.name AS TABLE_NAME, p.name AS COLUMN_NAME, " +
"CASE UPPER(p.type) " +
" WHEN 'INTEGER' THEN " + java.sql.Types.BIGINT + " " +
" WHEN 'INT' THEN " + java.sql.Types.BIGINT + " " +
" WHEN 'TEXT' THEN " + java.sql.Types.VARCHAR + " " +
" WHEN 'BLOB' THEN " + java.sql.Types.BLOB + " " +
" WHEN 'REAL' THEN " + java.sql.Types.DOUBLE + " " +
" ELSE " + java.sql.Types.OTHER + " " +
"END AS DATA_TYPE, " +
"p.type AS TYPE_NAME, 0 AS COLUMN_SIZE, 0 AS BUFFER_LENGTH, 0 AS DECIMAL_DIGITS, 10 AS NUM_PREC_RADIX, " +
"CASE p.\"notnull\" WHEN 1 THEN 0 ELSE 1 END AS NULLABLE, " +
"NULL AS REMARKS, p.dflt_value AS COLUMN_DEF, 0 AS SQL_DATA_TYPE, 0 AS SQL_DATETIME_SUB, 0 AS CHAR_OCTET_LENGTH, " +
"p.cid + 1 AS ORDINAL_POSITION, " +
"CASE p.\"notnull\" WHEN 1 THEN 'NO' ELSE 'YES' END AS IS_NULLABLE, " +
"NULL AS SCOPE_CATALOG, NULL AS SCOPE_SCHEMA, NULL AS SCOPE_TABLE, NULL AS SOURCE_DATA_TYPE, 'NO' AS IS_AUTOINCREMENT, 'NO' AS IS_GENERATEDCOLUMN " +
"FROM sqlite_master m JOIN pragma_table_info(m.name) p " +
"WHERE 1=1";
if (tableNamePattern != null && !tableNamePattern.equals("%")) {
sql += " AND m.name LIKE '" + escapeLiteral(tableNamePattern) + "'";
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 '\\'"
+ " AND p.name LIKE '" + escapeLiteral(columnPattern) + "' ESCAPE '\\'"
+ " ORDER BY m.name, p.cid";
return new LibsqlResultSet(null, columnsResult(connection.executeStatement(new Stmt(sql))));
}

private static StmtResult columnsResult(StmtResult raw) {
List<Col> 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<Row> rows = new ArrayList<>();
if (raw != null && raw.rows() != null) {
for (Row row : raw.rows()) {
List<Value> 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
&& LibsqlResultSetMetaData.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"))));
}
}
if (columnNamePattern != null && !columnNamePattern.equals("%")) {
sql += " AND p.name LIKE '" + escapeLiteral(columnNamePattern) + "'";
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;
}
sql += " ORDER BY TABLE_NAME, ORDINAL_POSITION";
return executeMetadataQuery(sql);

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;
}

private record ColumnShape(int jdbcType, String typeName, int columnSize, int decimalDigits) {}

@Override
public ResultSet getPrimaryKeys(String catalog, String schema, String table) throws SQLException {
String escapedTable = escapeLiteral(table == null ? "" : table);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
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.Stmt;
import com.github.magnusp.libsql.hrana.codec.StmtResult;
import com.github.magnusp.libsql.hrana.codec.Value;
import org.junit.jupiter.api.Test;

import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;
Expand Down Expand Up @@ -116,10 +120,109 @@ void getTablesWithEmptyTableNamePatternMatchesEverything() throws SQLException {
assertThat(connection.lastSql).contains("TABLE_NAME LIKE '%'");
}

@Test
void getColumnsReportsDeclaredTypesSizesNullabilityAndAutoIncrement() throws SQLException {
LibsqlDatabaseMetaData metadata = new LibsqlDatabaseMetaData(new StmtResultConnection(columnMetadataRows()));

try (ResultSet rs = metadata.getColumns(null, null, "c", null)) {
assertThat(rs.next()).isTrue();
assertThat(rs.getString("TABLE_NAME")).isEqualTo("c");
assertThat(rs.getString("COLUMN_NAME")).isEqualTo("i");
assertThat(rs.getInt("DATA_TYPE")).isEqualTo(Types.INTEGER);
assertThat(rs.getString("TYPE_NAME")).isEqualTo("INTEGER");
assertThat(rs.getInt("COLUMN_SIZE")).isEqualTo(2_000_000_000);
assertThat(rs.getInt("DECIMAL_DIGITS")).isZero();
assertThat(rs.getInt("NULLABLE")).isEqualTo(DatabaseMetaData.columnNullable);
assertThat(rs.getString("IS_NULLABLE")).isEqualTo("YES");
assertThat(rs.getString("IS_AUTOINCREMENT")).isEqualTo("NO");
assertThat(rs.getInt("ORDINAL_POSITION")).isEqualTo(1);

assertThat(rs.next()).isTrue();
assertThat(rs.getString("COLUMN_NAME")).isEqualTo("d");
assertThat(rs.getInt("DATA_TYPE")).isEqualTo(Types.FLOAT);
assertThat(rs.getString("TYPE_NAME")).isEqualTo("DECIMAL");
assertThat(rs.getInt("COLUMN_SIZE")).isEqualTo(15);
assertThat(rs.getInt("DECIMAL_DIGITS")).isEqualTo(5);
assertThat(rs.getInt("ORDINAL_POSITION")).isEqualTo(2);

assertThat(rs.next()).isTrue();
assertThat(rs.getString("COLUMN_NAME")).isEqualTo("k");
assertThat(rs.getInt("DATA_TYPE")).isEqualTo(Types.INTEGER);
assertThat(rs.getString("IS_AUTOINCREMENT")).isEqualTo("YES");

assertThat(rs.next()).isTrue();
assertThat(rs.getString("COLUMN_NAME")).isEqualTo("n");
assertThat(rs.getInt("NULLABLE")).isEqualTo(DatabaseMetaData.columnNoNulls);
assertThat(rs.getString("IS_NULLABLE")).isEqualTo("NO");
assertThat(rs.getString("IS_AUTOINCREMENT")).isEqualTo("NO");

assertThat(rs.next()).isFalse();
}
}

private static StmtResult columnMetadataRows() {
String createSql =
"CREATE TABLE c (i INTEGER, d DECIMAL(10,5), k INTEGER PRIMARY KEY AUTOINCREMENT, n TEXT NOT NULL)";
return new StmtResult(
List.of(
new Col("TABLE_NAME", "TEXT"),
new Col("CREATE_SQL", "TEXT"),
new Col("CID", "INTEGER"),
new Col("COLUMN_NAME", "TEXT"),
new Col("DECLARED_TYPE", "TEXT"),
new Col("NOT_NULL", "INTEGER"),
new Col("COLUMN_DEF", "TEXT"),
new Col("PK", "INTEGER"),
new Col("HIDDEN", "INTEGER")),
List.of(
columnRow("c", createSql, 0, "i", "INTEGER", 0, null, 0, 0),
columnRow("c", createSql, 1, "d", "DECIMAL(10,5)", 0, null, 0, 0),
columnRow("c", createSql, 2, "k", "INTEGER", 0, null, 1, 0),
columnRow("c", createSql, 3, "n", "TEXT", 1, null, 0, 0)),
0,
null);
}

private static Row columnRow(
String table,
String createSql,
int cid,
String column,
String declaredType,
int notNull,
String columnDefault,
int primaryKey,
int hidden) {
return new Row(List.of(
Value.of(table),
Value.of(createSql),
Value.of(cid),
Value.of(column),
Value.of(declaredType),
Value.of(notNull),
Value.of(columnDefault),
Value.of(primaryKey),
Value.of(hidden)));
}

private static StmtResult emptyResult() {
return new StmtResult(List.of(new Col("TABLE_NAME", "TEXT")), List.of(), 0, null);
}

private static final class StmtResultConnection extends LibsqlConnection {
private final StmtResult result;

private StmtResultConnection(StmtResult result) {
super(null, null);
this.result = result;
}

@Override
public StmtResult executeStatement(Stmt stmt) {
return result;
}
}

private static final class RecordingConnection extends LibsqlConnection {
private final StmtResult result;
private String lastSql;
Expand Down
Loading