2
2
* Tokenizer
3
3
*/
4
4
5
- import type { Token , State , Dialect } from './defines' ;
5
+ import type { Token , State , Dialect , ParamTypes } from './defines' ;
6
6
7
7
type Char = string | null ;
8
8
@@ -76,7 +76,7 @@ const ENDTOKENS: Record<string, Char> = {
76
76
'[' : ']' ,
77
77
} ;
78
78
79
- export function scanToken ( state : State , dialect : Dialect = 'generic' ) : Token {
79
+ export function scanToken ( state : State , dialect : Dialect = 'generic' , paramTypes ?: ParamTypes ) : Token {
80
80
const ch = read ( state ) ;
81
81
82
82
if ( isWhitespace ( ch ) ) {
@@ -95,8 +95,8 @@ export function scanToken(state: State, dialect: Dialect = 'generic'): Token {
95
95
return scanString ( state , ENDTOKENS [ ch ] ) ;
96
96
}
97
97
98
- if ( isParameter ( ch , state , dialect ) ) {
99
- return scanParameter ( state , dialect ) ;
98
+ if ( isParameter ( ch , state , dialect , paramTypes ) ) {
99
+ return scanParameter ( state , dialect , paramTypes ) ;
100
100
}
101
101
102
102
if ( isDollarQuotedString ( state ) ) {
@@ -253,7 +253,88 @@ function scanString(state: State, endToken: Char): Token {
253
253
} ;
254
254
}
255
255
256
- function scanParameter ( state : State , dialect : Dialect ) : Token {
256
+ function getCustomParam ( state : State , paramTypes : ParamTypes ) : string | null | undefined {
257
+ const matches = paramTypes ?. custom ?. map ( ( { regex } ) => {
258
+ const reg = new RegExp ( `(?:${ regex } )` , 'u' ) ;
259
+ return reg . exec ( state . input ) ;
260
+ } ) . filter ( ( value ) => ! ! value ) [ 0 ] ;
261
+
262
+ return matches ? matches [ 0 ] : null ;
263
+ }
264
+
265
+ function scanParameter ( state : State , dialect : Dialect , paramTypes ?: ParamTypes ) : Token {
266
+ // user has defined wanted param types, so we only evaluate them
267
+ if ( paramTypes ) {
268
+ const curCh : any = state . input [ 0 ] ;
269
+ let nextChar = peek ( state ) ;
270
+ let matched = false
271
+
272
+ // this could be a named parameter that just starts with a number (ugh)
273
+ if ( paramTypes . numbered && paramTypes . numbered . length && paramTypes . numbered . includes ( curCh ) ) {
274
+ const maybeNumbers = state . input . slice ( 1 , state . input . length ) ;
275
+ if ( nextChar !== null && ! isNaN ( Number ( nextChar ) ) && / ^ \d + $ / . test ( maybeNumbers ) ) {
276
+ do {
277
+ nextChar = read ( state ) ;
278
+ } while ( nextChar !== null && ! isNaN ( Number ( nextChar ) ) && ! isWhitespace ( nextChar ) ) ;
279
+
280
+ if ( nextChar !== null ) unread ( state ) ;
281
+ matched = true ;
282
+ }
283
+ }
284
+
285
+ if ( ! matched && paramTypes . named && paramTypes . named . length && paramTypes . named . includes ( curCh ) ) {
286
+ if ( ! isQuotedIdentifier ( nextChar , dialect ) ) {
287
+ while ( isAlphaNumeric ( peek ( state ) ) ) read ( state ) ;
288
+ matched = true ;
289
+ }
290
+ }
291
+
292
+ if ( ! matched && paramTypes . quoted && paramTypes . quoted . length && paramTypes . quoted . includes ( curCh ) ) {
293
+ if ( isQuotedIdentifier ( nextChar , dialect ) ) {
294
+ const endChars = new Map < string , string > ( [
295
+ [ '"' , '"' ] ,
296
+ [ '[' , ']' ] ,
297
+ [ '`' , '`' ]
298
+ ] ) ;
299
+ const quoteChar = read ( state ) as string ;
300
+ const end = endChars . get ( quoteChar ) ;
301
+ // end when we reach the end quote
302
+ while ( ( isAlphaNumeric ( peek ( state ) ) || peek ( state ) === ' ' ) && peek ( state ) != end ) read ( state ) ;
303
+
304
+ // read the end quote
305
+ read ( state ) ;
306
+
307
+ matched = true ;
308
+ }
309
+ }
310
+
311
+ if ( ! matched && paramTypes . custom && paramTypes . custom . length ) {
312
+ const custom = getCustomParam ( state , paramTypes ) ;
313
+
314
+ if ( custom ) {
315
+ read ( state , custom . length ) ;
316
+ matched = true ;
317
+ }
318
+ }
319
+
320
+ if ( ! matched && curCh !== '?' && nextChar !== null ) { // not positional, panic
321
+ return {
322
+ type : 'parameter' ,
323
+ value : 'unknown' ,
324
+ start : state . start ,
325
+ end : state . end
326
+ }
327
+ }
328
+
329
+ const value = state . input . slice ( state . start , state . position + 1 ) ;
330
+ return {
331
+ type : 'parameter' ,
332
+ value,
333
+ start : state . start ,
334
+ end : state . start + value . length - 1 ,
335
+ } ;
336
+ }
337
+
257
338
if ( [ 'mysql' , 'generic' , 'sqlite' ] . includes ( dialect ) ) {
258
339
return {
259
340
type : 'parameter' ,
@@ -413,7 +494,37 @@ function isString(ch: Char, dialect: Dialect): boolean {
413
494
return stringStart . includes ( ch ) ;
414
495
}
415
496
416
- function isParameter ( ch : Char , state : State , dialect : Dialect ) : boolean {
497
+ function isCustomParam ( state : State , paramTypes : ParamTypes ) : boolean | undefined {
498
+ return paramTypes ?. custom ?. some ( ( { regex } ) => {
499
+ const reg = new RegExp ( `(?:${ regex } )` , 'uy' ) ;
500
+ return reg . test ( state . input ) ;
501
+ } )
502
+ }
503
+
504
+ function isParameter ( ch : Char , state : State , dialect : Dialect , paramTypes ?: ParamTypes ) : boolean {
505
+ if ( paramTypes && ch !== null ) {
506
+ const curCh : any = ch ;
507
+ const nextChar = peek ( state ) ;
508
+ if ( paramTypes . positional && ch === '?' && nextChar === null ) return true ;
509
+
510
+ if ( paramTypes . numbered && paramTypes . numbered . length && paramTypes . numbered . includes ( curCh ) ) {
511
+ if ( nextChar !== null && ! isNaN ( Number ( nextChar ) ) ) {
512
+ return true ;
513
+ }
514
+ }
515
+
516
+ if ( ( paramTypes . named && paramTypes . named . length && paramTypes . named . includes ( curCh ) ) ||
517
+ ( paramTypes . quoted && paramTypes . quoted . length && paramTypes . quoted . includes ( curCh ) ) ) {
518
+ return true ;
519
+ }
520
+
521
+ if ( ( paramTypes . custom && paramTypes . custom . length && isCustomParam ( state , paramTypes ) ) ) {
522
+ return true
523
+ }
524
+
525
+ return false ;
526
+ }
527
+
417
528
let pStart = '?' ; // ansi standard - sqlite, mysql
418
529
if ( dialect === 'psql' ) {
419
530
pStart = '$' ;
0 commit comments