@@ -142,24 +142,29 @@ func (s *Lexer) Scan() *Token {
142142 case isWildcard (ch ):
143143 return s .scanWildcard ()
144144 case ch == '$' :
145- if isDigit (s .lookAhead (1 )) {
146- // if the dollar sign is followed by a digit, then it's a numbered parameter
147- return s .scanPositionalParameter ()
145+ nextCh := s .lookAhead (1 )
146+ if isDigit (nextCh ) {
147+ // Prefix length 2: consume '$' plus the first digit of SQLite bind parameters that use $VVV,
148+ // where V may be numeric (e.g. $1, $12). Refer to scanSQLiteBindParameter for details
149+ return s .scanNumericParameter (2 )
150+ }
151+ if s .config .DBMS == DBMSSQLite && isAlphaNumeric (nextCh ) {
152+ return s .scanBindParameter ()
148153 }
149- if s .config .DBMS == DBMSSQLServer && isLetter (s . lookAhead ( 1 ) ) {
154+ if s .config .DBMS == DBMSSQLServer && isLetter (nextCh ) {
150155 return s .scanIdentifier (ch )
151156 }
152157 return s .scanDollarQuotedString ()
153158 case ch == ':' :
154- if s .config .DBMS == DBMSOracle && isAlphaNumeric (s .lookAhead (1 )) {
159+ if ( s .config .DBMS == DBMSOracle || s . config . DBMS == DBMSSQLite ) && isAlphaNumeric (s .lookAhead (1 )) {
155160 return s .scanBindParameter ()
156161 }
157162 return s .scanOperator (ch )
158163 case ch == '`' :
159- if s .config .DBMS == DBMSMySQL {
164+ if s .config .DBMS == DBMSMySQL || s . config . DBMS == DBMSSQLite {
160165 return s .scanDoubleQuotedIdentifier ('`' )
161166 }
162- return s .scanUnknown () // backtick is only valid in mysql
167+ return s .scanUnknown () // backtick is only valid in mysql and sqlite
163168 case ch == '#' :
164169 if s .config .DBMS == DBMSSQLServer {
165170 return s .scanIdentifier (ch )
@@ -168,6 +173,13 @@ func (s *Lexer) Scan() *Token {
168173 return s .scanSingleLineComment (ch )
169174 }
170175 return s .scanOperator (ch )
176+ case ch == '?' :
177+ if s .config .DBMS == DBMSSQLite {
178+ // Prefix length 1: consume '?' before scanning optional digits of SQLite ?NNN parameters
179+ // SQLite treats bare '?' and '?NNN' as positional parameters (see scanSQLiteBindParameter)
180+ return s .scanNumericParameter (1 )
181+ }
182+ return s .scanOperator (ch )
171183 case ch == '@' :
172184 if s .lookAhead (1 ) == '@' {
173185 if isAlphaNumeric (s .lookAhead (2 )) {
@@ -192,7 +204,7 @@ func (s *Lexer) Scan() *Token {
192204 case isOperator (ch ):
193205 return s .scanOperator (ch )
194206 case isPunctuation (ch ):
195- if ch == '[' && s .config .DBMS == DBMSSQLServer {
207+ if ch == '[' && ( s .config .DBMS == DBMSSQLServer || s . config . DBMS == DBMSSQLite ) {
196208 return s .scanDoubleQuotedIdentifier ('[' )
197209 }
198210 return s .scanPunctuation ()
@@ -595,21 +607,22 @@ func (s *Lexer) scanDollarQuotedString() *Token {
595607 return s .emit (ERROR )
596608}
597609
598- func (s * Lexer ) scanPositionalParameter ( ) * Token {
610+ func (s * Lexer ) scanNumericParameter ( prefixLen int ) * Token {
599611 s .start = s .cursor
600- ch := s .nextBy (2 ) // consume the dollar sign and the number
601- for {
602- if ! isDigit (ch ) {
603- break
604- }
612+ ch := s .nextBy (prefixLen )
613+ for isDigit (ch ) {
605614 ch = s .next ()
606615 }
607616 return s .emit (POSITIONAL_PARAMETER )
608617}
609618
610619func (s * Lexer ) scanBindParameter () * Token {
611620 s .start = s .cursor
612- ch := s .nextBy (2 ) // consume the (colon|at sign) and the char
621+ if s .config .DBMS == DBMSSQLite {
622+ // SQLite allows named bind parameters prefixed with :, @, or $, so use the SQLite-specific scanner
623+ return s .scanSQLiteBindParameter ()
624+ }
625+ ch := s .nextBy (2 ) // consume the (colon|at sign|dollar sign) and the char
613626 for {
614627 if ! isAlphaNumeric (ch ) {
615628 break
@@ -619,6 +632,55 @@ func (s *Lexer) scanBindParameter() *Token {
619632 return s .emit (BIND_PARAMETER )
620633}
621634
635+ // https://sqlite.org/c3ref/bind_blob.html
636+ func (s * Lexer ) scanSQLiteBindParameter () * Token {
637+ s .next () // consume the prefix character (:, @, or $)
638+ s .consumeSQLiteIdentifier ()
639+
640+ for {
641+ if s .peek () == ':' && s .lookAhead (1 ) == ':' {
642+ s .nextBy (2 ) // consume '::'
643+ s .consumeSQLiteIdentifier ()
644+ continue
645+ }
646+ break
647+ }
648+
649+ if s .peek () == '(' {
650+ s .consumeSQLiteParameterSuffix ()
651+ }
652+
653+ return s .emit (BIND_PARAMETER )
654+ }
655+
656+ func (s * Lexer ) consumeSQLiteIdentifier () {
657+ for {
658+ ch := s .peek ()
659+ if ch == '_' || isAlphaNumeric (ch ) {
660+ s .next ()
661+ continue
662+ }
663+ break
664+ }
665+ }
666+
667+ func (s * Lexer ) consumeSQLiteParameterSuffix () {
668+ s .next () // consume '('
669+ depth := 1
670+ for depth > 0 {
671+ ch := s .peek ()
672+ if isEOF (ch ) {
673+ break
674+ }
675+ s .next ()
676+ if ch == '(' {
677+ depth ++
678+ } else if ch == ')' {
679+ depth --
680+ }
681+ }
682+ }
683+
622684func (s * Lexer ) scanSystemVariable () * Token {
623685 s .start = s .cursor
624686 ch := s .nextBy (2 ) // consume @@
0 commit comments