Skip to content

Commit a5ed7c5

Browse files
committed
fix(jdbc): detect AUTOINCREMENT from matching column definition
1 parent 194ebdc commit a5ed7c5

2 files changed

Lines changed: 254 additions & 18 deletions

File tree

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

Lines changed: 217 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import java.sql.ResultSetMetaData;
1010
import java.sql.SQLException;
1111
import java.sql.Types;
12+
import java.util.ArrayList;
1213
import java.util.HashMap;
1314
import java.util.List;
1415
import java.util.Locale;
@@ -58,12 +59,24 @@ public boolean isAutoIncrement(int column) throws SQLException {
5859
getCol(column);
5960
TableInfo table = resolveTableContext();
6061
ColumnInfo info = table != null ? table.columns().get(columnName(column)) : null;
61-
return info != null
62-
&& info.primaryKey()
63-
&& table.createSql() != null
64-
&& stripSqlCommentsAndLiterals(table.createSql())
65-
.toUpperCase(Locale.ENGLISH)
66-
.contains("AUTOINCREMENT");
62+
return info != null && info.primaryKey() && isAutoIncrementColumn(table.createSql(), columnName(column));
63+
}
64+
65+
static boolean isAutoIncrementColumn(String createSql, String columnName) {
66+
if (createSql == null || columnName == null) {
67+
return false;
68+
}
69+
List<List<Token>> definitions = tableDefinitions(createSql);
70+
if (definitions == null) {
71+
return false;
72+
}
73+
for (List<Token> definition : definitions) {
74+
if (columnDefinitionIsAutoIncrement(definition, columnName)
75+
|| tableConstraintIsAutoIncrement(definition, columnName)) {
76+
return true;
77+
}
78+
}
79+
return false;
6780
}
6881

6982
@Override
@@ -515,50 +528,219 @@ private static TableInfo toTableInfo(StmtResult result, String fallbackName) {
515528
return new TableInfo(tableName != null ? tableName : fallbackName, createSql, columns);
516529
}
517530

518-
private static String stripSqlCommentsAndLiterals(String sql) {
519-
StringBuilder result = new StringBuilder(sql.length());
531+
private static List<List<Token>> tableDefinitions(String createSql) {
532+
List<Token> tokens = tokenize(createSql);
533+
if (tokens == null) {
534+
return null;
535+
}
536+
int open = -1;
537+
for (int i = 0; i < tokens.size(); i++) {
538+
if (isSymbol(tokens.get(i), "(")) {
539+
open = i;
540+
break;
541+
}
542+
}
543+
if (open < 0) {
544+
return null;
545+
}
546+
547+
List<List<Token>> definitions = new ArrayList<>();
548+
List<Token> current = new ArrayList<>();
549+
for (int i = open + 1; i < tokens.size(); i++) {
550+
Token token = tokens.get(i);
551+
if (isSymbol(token, ")") && token.depth() == 0) {
552+
if (!current.isEmpty()) {
553+
definitions.add(current);
554+
}
555+
return definitions;
556+
}
557+
if (isSymbol(token, ",") && token.depth() == 1) {
558+
definitions.add(current);
559+
current = new ArrayList<>();
560+
continue;
561+
}
562+
current.add(token);
563+
}
564+
return null;
565+
}
566+
567+
private static boolean columnDefinitionIsAutoIncrement(List<Token> definition, String columnName) {
568+
int start = skipConstraintName(definition);
569+
if (start >= definition.size()) {
570+
return false;
571+
}
572+
if (isConstraintKeyword(definition.get(start))) {
573+
return false;
574+
}
575+
String name = identifier(definition.get(start));
576+
if (name == null || !name.equalsIgnoreCase(columnName)) {
577+
return false;
578+
}
579+
return containsKeyword(definition, start + 1, definition.size(), "AUTOINCREMENT", 1);
580+
}
581+
582+
private static boolean tableConstraintIsAutoIncrement(List<Token> definition, String columnName) {
583+
int start = skipConstraintName(definition);
584+
if (start + 1 >= definition.size()
585+
|| !isWord(definition.get(start), "PRIMARY")
586+
|| !isWord(definition.get(start + 1), "KEY")) {
587+
return false;
588+
}
589+
int open = -1;
590+
for (int i = start + 2; i < definition.size(); i++) {
591+
if (isSymbol(definition.get(i), "(")) {
592+
open = i;
593+
break;
594+
}
595+
}
596+
if (open < 0) {
597+
return false;
598+
}
599+
600+
int partStart = open + 1;
601+
for (int i = open + 1; i < definition.size(); i++) {
602+
Token token = definition.get(i);
603+
if ((isSymbol(token, ",") && token.depth() == 2) || (isSymbol(token, ")") && token.depth() == 1)) {
604+
if (indexedColumnIsAutoIncrement(definition, partStart, i, columnName)) {
605+
return true;
606+
}
607+
partStart = i + 1;
608+
}
609+
}
610+
return false;
611+
}
612+
613+
private static boolean indexedColumnIsAutoIncrement(
614+
List<Token> definition, int start, int end, String columnName) {
615+
if (start >= end) {
616+
return false;
617+
}
618+
String name = identifier(definition.get(start));
619+
return name != null
620+
&& name.equalsIgnoreCase(columnName)
621+
&& containsKeyword(definition, start + 1, end, "AUTOINCREMENT", 2);
622+
}
623+
624+
private static int skipConstraintName(List<Token> definition) {
625+
if (definition.size() >= 2 && isWord(definition.get(0), "CONSTRAINT")) {
626+
return 2;
627+
}
628+
return 0;
629+
}
630+
631+
private static boolean isConstraintKeyword(Token token) {
632+
return isWord(token, "PRIMARY")
633+
|| isWord(token, "UNIQUE")
634+
|| isWord(token, "CHECK")
635+
|| isWord(token, "FOREIGN");
636+
}
637+
638+
private static boolean containsKeyword(
639+
List<Token> tokens, int start, int end, String keyword, int depth) {
640+
for (int i = start; i < end; i++) {
641+
Token token = tokens.get(i);
642+
if (token.depth() == depth && isWord(token, keyword)) {
643+
return true;
644+
}
645+
}
646+
return false;
647+
}
648+
649+
private static boolean isWord(Token token, String word) {
650+
return token.type() == TokenType.WORD && token.text().equalsIgnoreCase(word);
651+
}
652+
653+
private static boolean isSymbol(Token token, String symbol) {
654+
return token.type() == TokenType.SYMBOL && token.text().equals(symbol);
655+
}
656+
657+
private static String identifier(Token token) {
658+
return switch (token.type()) {
659+
case IDENT, WORD -> token.text();
660+
case SYMBOL -> null;
661+
};
662+
}
663+
664+
private static List<Token> tokenize(String sql) {
665+
List<Token> tokens = new ArrayList<>();
666+
int depth = 0;
520667
int i = 0;
521668
while (i < sql.length()) {
522669
char c = sql.charAt(i);
670+
if (Character.isWhitespace(c)) {
671+
i++;
672+
continue;
673+
}
523674
if (c == '-' && i + 1 < sql.length() && sql.charAt(i + 1) == '-') {
524675
i += 2;
525676
while (i < sql.length() && sql.charAt(i) != '\n' && sql.charAt(i) != '\r') {
526677
i++;
527678
}
528-
result.append(' ');
529679
continue;
530680
}
531681
if (c == '/' && i + 1 < sql.length() && sql.charAt(i + 1) == '*') {
532682
int end = sql.indexOf("*/", i + 2);
533683
if (end < 0) {
534-
break;
684+
return null;
535685
}
536-
result.append(' ');
537686
i = end + 2;
538687
continue;
539688
}
540-
if (c == '\'' || c == '"' || c == '`') {
689+
if (c == '\'') {
690+
i = skipQuoted(sql, i, c);
691+
if (i < 0) {
692+
return null;
693+
}
694+
continue;
695+
}
696+
if (c == '"' || c == '`') {
541697
int end = skipQuoted(sql, i, c);
542698
if (end < 0) {
543-
break;
699+
return null;
544700
}
545-
result.append(' ');
701+
tokens.add(new Token(TokenType.IDENT, unescape(sql.substring(i + 1, end - 1), c), depth));
546702
i = end;
547703
continue;
548704
}
549705
if (c == '[') {
550706
int end = sql.indexOf(']', i + 1);
551707
if (end < 0) {
552-
break;
708+
return null;
553709
}
554-
result.append(' ');
710+
tokens.add(new Token(TokenType.IDENT, sql.substring(i + 1, end), depth));
555711
i = end + 1;
556712
continue;
557713
}
558-
result.append(c);
714+
if (c == '(') {
715+
tokens.add(new Token(TokenType.SYMBOL, "(", depth));
716+
depth++;
717+
i++;
718+
continue;
719+
}
720+
if (c == ')') {
721+
if (depth == 0) {
722+
return null;
723+
}
724+
depth--;
725+
tokens.add(new Token(TokenType.SYMBOL, ")", depth));
726+
i++;
727+
continue;
728+
}
729+
if (isWordChar(c)) {
730+
int start = i;
731+
while (i < sql.length() && isWordChar(sql.charAt(i))) {
732+
i++;
733+
}
734+
tokens.add(new Token(TokenType.WORD, sql.substring(start, i), depth));
735+
continue;
736+
}
737+
tokens.add(new Token(TokenType.SYMBOL, String.valueOf(c), depth));
559738
i++;
560739
}
561-
return result.toString();
740+
if (depth != 0) {
741+
return null;
742+
}
743+
return tokens;
562744
}
563745

564746
private static int skipQuoted(String sql, int start, char quote) {
@@ -576,6 +758,15 @@ private static int skipQuoted(String sql, int start, char quote) {
576758
return -1;
577759
}
578760

761+
private static String unescape(String content, char quote) {
762+
String doubled = new String(new char[] {quote, quote});
763+
return content.replace(doubled, String.valueOf(quote));
764+
}
765+
766+
private static boolean isWordChar(char c) {
767+
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_' || c == '$';
768+
}
769+
579770
private static String text(Value value) {
580771
return value instanceof Value.Text t ? t.value() : null;
581772
}
@@ -592,6 +783,14 @@ private enum SampleType {
592783
NULL
593784
}
594785

786+
private enum TokenType {
787+
WORD,
788+
IDENT,
789+
SYMBOL
790+
}
791+
792+
private record Token(TokenType type, String text, int depth) {}
793+
595794
private record ColumnInfo(boolean notNull, boolean primaryKey) {}
596795

597796
private record TableInfo(String name, String createSql, Map<String, ColumnInfo> columns) {}

‎libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlResultSetMetaDataTest.java‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,30 @@ void ignoresAutoIncrementInsideStringLiterals() throws SQLException {
9393
assertThat(metadata.isAutoIncrement(1)).isFalse();
9494
}
9595

96+
@Test
97+
void doesNotReportAutoIncrementForColumnNamedAutoincrement() throws SQLException {
98+
String createSql = "CREATE TABLE T (myautoincrement INTEGER PRIMARY KEY)";
99+
LibsqlResultSetMetaData metadata = metadataForTable(createSql, List.of(new Col("myautoincrement", "INTEGER")));
100+
101+
assertThat(metadata.isAutoIncrement(1)).isFalse();
102+
}
103+
104+
@Test
105+
void reportsAutoIncrementForTableLevelAutoincrementPrimaryKey() throws SQLException {
106+
String createSql = "CREATE TABLE T (id INTEGER, PRIMARY KEY (id AUTOINCREMENT))";
107+
LibsqlResultSetMetaData metadata = metadataForTable(createSql, List.of(new Col("id", "INTEGER")));
108+
109+
assertThat(metadata.isAutoIncrement(1)).isTrue();
110+
}
111+
112+
@Test
113+
void ignoresQuotedIdentifierNamedAutoincrement() throws SQLException {
114+
String createSql = "CREATE TABLE T (\"AUTOINCREMENT\" INTEGER PRIMARY KEY)";
115+
LibsqlResultSetMetaData metadata = metadataForTable(createSql, List.of(new Col("AUTOINCREMENT", "INTEGER")));
116+
117+
assertThat(metadata.isAutoIncrement(1)).isFalse();
118+
}
119+
96120
@Test
97121
void usesFirstNonNullValueForIntegerTypeAndClass() throws SQLException {
98122
LibsqlResultSetMetaData metadata = new LibsqlResultSetMetaData(
@@ -136,6 +160,19 @@ void leavesAmbiguousTableContextUnknown() throws SQLException {
136160
assertThat(metadata.isAutoIncrement(1)).isFalse();
137161
}
138162

163+
private static LibsqlResultSetMetaData metadataForTable(String createSql, List<Col> cols) throws SQLException {
164+
List<Row> tableRows = cols.stream()
165+
.map(col -> new Row(List.of(
166+
Value.of("T"),
167+
Value.of(createSql),
168+
Value.of(col.name()),
169+
Value.of(0L),
170+
Value.of(1L))))
171+
.toList();
172+
StmtResult result = new StmtResult(List.of(), tableRows, 0, null);
173+
return new LibsqlResultSetMetaData(cols, List.of(), "SELECT * FROM T", connectionReturning(result));
174+
}
175+
139176
private static LibsqlConnection connectionReturning(StmtResult result) {
140177
return new LibsqlConnection(null, null) {
141178
@Override

0 commit comments

Comments
 (0)