99import java .sql .ResultSetMetaData ;
1010import java .sql .SQLException ;
1111import java .sql .Types ;
12+ import java .util .ArrayList ;
1213import java .util .HashMap ;
1314import java .util .List ;
1415import 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 ) {}
0 commit comments