From d9f8cdd1829e37e2cd6cdf26c702a3b259205d93 Mon Sep 17 00:00:00 2001 From: Magnus Persson <1431685+magnusp@users.noreply.github.com> Date: Thu, 22 Oct 2026 02:10:00 +0200 Subject: [PATCH] fix(jdbc): escape quoted table names in getIndexInfo --- .../libsql/jdbc/LibsqlDatabaseMetaData.java | 5 +- .../jdbc/LibsqlDatabaseMetaDataTest.java | 89 +++++++++++++++++++ 2 files changed, 92 insertions(+), 2 deletions(-) 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 ec18aee..58ed213 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 @@ -532,12 +532,13 @@ public Connection getConnection() throws SQLException { return executeMetadataQuery(sql); } @Override public ResultSet getIndexInfo(String catalog, String schema, String table, boolean unique, boolean approximate) throws SQLException { - String sql = "SELECT NULL AS TABLE_CAT, NULL AS TABLE_SCHEM, '" + table + "' AS TABLE_NAME, " + + String escapedTable = escapeLiteral(table == null ? "" : table); + String sql = "SELECT NULL AS TABLE_CAT, NULL AS TABLE_SCHEM, '" + escapedTable + "' AS TABLE_NAME, " + "CASE il.\"unique\" WHEN 1 THEN 0 ELSE 1 END AS NON_UNIQUE, " + "NULL AS INDEX_QUALIFIER, il.name AS INDEX_NAME, 3 AS TYPE, " + "ii.seqno + 1 AS ORDINAL_POSITION, ii.name AS COLUMN_NAME, " + "'A' AS ASC_OR_DESC, 0 AS CARDINALITY, 0 AS PAGES, NULL AS FILTER_CONDITION " + - "FROM pragma_index_list('" + table + "') il, pragma_index_info(il.name) ii"; + "FROM pragma_index_list('" + escapedTable + "') il, pragma_index_info(il.name) ii"; if (unique) { sql += " WHERE il.\"unique\" = 1"; } 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 067a619..1ce242b 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 @@ -54,6 +54,18 @@ void escapesSingleQuoteInTableNamePattern() throws SQLException { assertThat(connection.lastSql).doesNotContain("TABLE_NAME LIKE 'O'Brien'"); } + @Test + void getIndexInfoRoundTripsQuotedTableName() throws SQLException { + LibsqlDatabaseMetaData metadata = new LibsqlDatabaseMetaData(new IndexInfoConnection()); + + try (ResultSet resultSet = metadata.getIndexInfo(null, null, "t'x", false, false)) { + assertThat(resultSet.next()).isTrue(); + assertThat(resultSet.getString("TABLE_NAME")).isEqualTo("t'x"); + assertThat(resultSet.getString("INDEX_NAME")).isEqualTo("idx_t"); + assertThat(resultSet.getString("COLUMN_NAME")).isEqualTo("col"); + } + } + @Test void getTablesWithNullTypesReturnsOnlyTablesViewsAndSystemAndTemporaryRows() throws SQLException { RecordingConnection connection = new RecordingConnection(emptyResult()); @@ -296,6 +308,83 @@ public StmtResult executeStatement(Stmt stmt) { } } + private static final class IndexInfoConnection extends LibsqlConnection { + private IndexInfoConnection() { + super(null, null); + } + + @Override + public StmtResult executeStatement(Stmt stmt) throws SQLException { + String sql = stmt.sql(); + String selectedTable = literalAfter(sql, "NULL AS TABLE_SCHEM, ", " AS TABLE_NAME"); + String pragmaTable = literalAfter(sql, "pragma_index_list(", ") il"); + if (!selectedTable.equals(pragmaTable)) { + throw new SQLException("TABLE_NAME literal does not match the pragma table name"); + } + return new StmtResult( + List.of( + new Col("TABLE_CAT", "TEXT"), + new Col("TABLE_SCHEM", "TEXT"), + new Col("TABLE_NAME", "TEXT"), + new Col("NON_UNIQUE", "INTEGER"), + new Col("INDEX_QUALIFIER", "TEXT"), + new Col("INDEX_NAME", "TEXT"), + new Col("TYPE", "INTEGER"), + new Col("ORDINAL_POSITION", "INTEGER"), + new Col("COLUMN_NAME", "TEXT"), + new Col("ASC_OR_DESC", "TEXT"), + new Col("CARDINALITY", "INTEGER"), + new Col("PAGES", "INTEGER"), + new Col("FILTER_CONDITION", "TEXT")), + List.of(new Row(List.of( + Value.ofNull(), + Value.ofNull(), + Value.of(selectedTable), + Value.of(1), + Value.ofNull(), + Value.of("idx_t"), + Value.of(3), + Value.of(1), + Value.of("col"), + Value.of("A"), + Value.of(0), + Value.of(0), + Value.ofNull()))), + 0, + null); + } + + private static String literalAfter(String sql, String prefix, String suffix) throws SQLException { + int prefixIndex = sql.indexOf(prefix); + if (prefixIndex < 0) { + throw new SQLException("missing " + prefix); + } + int quote = prefixIndex + prefix.length(); + if (quote >= sql.length() || sql.charAt(quote) != '\'') { + throw new SQLException("expected a quoted literal after " + prefix); + } + StringBuilder literal = new StringBuilder(); + int i = quote + 1; + while (i < sql.length()) { + char c = sql.charAt(i); + if (c == '\'') { + if (i + 1 < sql.length() && sql.charAt(i + 1) == '\'') { + literal.append('\''); + i += 2; + continue; + } + if (!sql.startsWith(suffix, i + 1)) { + throw new SQLException("syntax error after table-name literal"); + } + return literal.toString(); + } + literal.append(c); + i++; + } + throw new SQLException("unterminated table-name literal"); + } + } + private static final class RecordingConnection extends LibsqlConnection { private final StmtResult result; private String lastSql;