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,11 @@ 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 (
80
+ state : State ,
81
+ dialect : Dialect = 'generic' ,
82
+ paramTypes : ParamTypes = { positional : true } ,
83
+ ) : Token {
80
84
const ch = read ( state ) ;
81
85
82
86
if ( isWhitespace ( ch ) ) {
@@ -95,8 +99,8 @@ export function scanToken(state: State, dialect: Dialect = 'generic'): Token {
95
99
return scanString ( state , ENDTOKENS [ ch ] ) ;
96
100
}
97
101
98
- if ( isParameter ( ch , state , dialect ) ) {
99
- return scanParameter ( state , dialect ) ;
102
+ if ( isParameter ( ch , state , paramTypes ) ) {
103
+ return scanParameter ( state , dialect , paramTypes ) ;
100
104
}
101
105
102
106
if ( isDollarQuotedString ( state ) ) {
@@ -253,52 +257,92 @@ function scanString(state: State, endToken: Char): Token {
253
257
} ;
254
258
}
255
259
256
- function scanParameter ( state : State , dialect : Dialect ) : Token {
257
- if ( [ 'mysql' , 'generic' , 'sqlite' ] . includes ( dialect ) ) {
258
- return {
259
- type : 'parameter' ,
260
- value : state . input . slice ( state . start , state . position + 1 ) ,
261
- start : state . start ,
262
- end : state . start ,
263
- } ;
264
- }
260
+ function getCustomParam ( state : State , paramTypes : ParamTypes ) : string | null | undefined {
261
+ const matches = paramTypes ?. custom
262
+ ?. map ( ( regex ) => {
263
+ const reg = new RegExp ( `^(?:${ regex } )` , 'u' ) ;
264
+ return reg . exec ( state . input . slice ( state . start ) ) ;
265
+ } )
266
+ . filter ( ( value ) => ! ! value ) [ 0 ] ;
265
267
266
- if ( dialect === 'psql' ) {
267
- let nextChar : Char ;
268
+ return matches ? matches [ 0 ] : null ;
269
+ }
268
270
269
- do {
270
- nextChar = read ( state ) ;
271
- } while ( nextChar !== null && ! isNaN ( Number ( nextChar ) ) && ! isWhitespace ( nextChar ) ) ;
271
+ function scanParameter ( state : State , dialect : Dialect , paramTypes : ParamTypes ) : Token {
272
+ const curCh = state . input [ state . start ] ;
273
+ const nextChar = peek ( state ) ;
274
+ let matched = false ;
275
+
276
+ if ( paramTypes . numbered ?. length && paramTypes . numbered . some ( ( type ) => type === curCh ) ) {
277
+ const endIndex = state . input
278
+ . slice ( state . start + 1 )
279
+ . split ( '' )
280
+ . findIndex ( ( val ) => / ^ \W + / . test ( val ) ) ;
281
+ const maybeNumbers = state . input . slice (
282
+ state . start + 1 ,
283
+ endIndex > 0 ? state . start + endIndex + 1 : state . end + 1 ,
284
+ ) ;
285
+ if ( nextChar !== null && ! isNaN ( Number ( nextChar ) ) && / ^ \d + $ / . test ( maybeNumbers ) ) {
286
+ let nextChar : Char = null ;
287
+ do {
288
+ nextChar = read ( state ) ;
289
+ } while ( nextChar !== null && ! isNaN ( Number ( nextChar ) ) && ! isWhitespace ( nextChar ) ) ;
290
+
291
+ if ( nextChar !== null ) unread ( state ) ;
292
+ matched = true ;
293
+ }
294
+ }
272
295
273
- if ( nextChar !== null ) unread ( state ) ;
296
+ if ( ! matched && paramTypes . named ?. length && paramTypes . named . some ( ( type ) => type === curCh ) ) {
297
+ if ( ! isQuotedIdentifier ( nextChar , dialect ) ) {
298
+ while ( isAlphaNumeric ( peek ( state ) ) ) read ( state ) ;
299
+ matched = true ;
300
+ }
301
+ }
274
302
275
- const value = state . input . slice ( state . start , state . position + 1 ) ;
303
+ if ( ! matched && paramTypes . quoted ?. length && paramTypes . quoted . some ( ( type ) => type === curCh ) ) {
304
+ if ( isQuotedIdentifier ( nextChar , dialect ) ) {
305
+ const quoteChar = read ( state ) as string ;
306
+ // end when we reach the end quote
307
+ while (
308
+ ( isAlphaNumeric ( peek ( state ) ) || peek ( state ) === ' ' ) &&
309
+ peek ( state ) != ENDTOKENS [ quoteChar ]
310
+ ) {
311
+ read ( state ) ;
312
+ }
276
313
277
- return {
278
- type : 'parameter' ,
279
- value,
280
- start : state . start ,
281
- end : state . start + value . length - 1 ,
282
- } ;
314
+ // read the end quote
315
+ read ( state ) ;
316
+
317
+ matched = true ;
318
+ }
283
319
}
284
320
285
- if ( dialect === 'mssql' ) {
286
- while ( isAlphaNumeric ( peek ( state ) ) ) read ( state ) ;
321
+ if ( ! matched && paramTypes . custom && paramTypes . custom . length ) {
322
+ const custom = getCustomParam ( state , paramTypes ) ;
323
+
324
+ if ( custom ) {
325
+ read ( state , custom . length ) ;
326
+ matched = true ;
327
+ }
328
+ }
329
+ const value = state . input . slice ( state . start , state . position + 1 ) ;
287
330
288
- const value = state . input . slice ( state . start , state . position + 1 ) ;
331
+ if ( ! matched && ! paramTypes . positional && curCh !== '?' ) {
332
+ // not positional, panic
289
333
return {
290
- type : 'parameter ' ,
291
- value,
334
+ type : 'unknown ' ,
335
+ value : value ,
292
336
start : state . start ,
293
337
end : state . start + value . length - 1 ,
294
338
} ;
295
339
}
296
340
297
341
return {
298
342
type : 'parameter' ,
299
- value : 'unknown' ,
343
+ value,
300
344
start : state . start ,
301
- end : state . end ,
345
+ end : state . start + value . length - 1 ,
302
346
} ;
303
347
}
304
348
@@ -413,18 +457,38 @@ function isString(ch: Char, dialect: Dialect): boolean {
413
457
return stringStart . includes ( ch ) ;
414
458
}
415
459
416
- function isParameter ( ch : Char , state : State , dialect : Dialect ) : boolean {
417
- let pStart = '?' ; // ansi standard - sqlite, mysql
418
- if ( dialect === 'psql' ) {
419
- pStart = '$' ;
420
- const nextChar = peek ( state ) ;
421
- if ( nextChar === null || isNaN ( Number ( nextChar ) ) ) {
422
- return false ;
460
+ function isCustomParam ( state : State , customParamType : NonNullable < ParamTypes [ 'custom' ] > ) : boolean {
461
+ return customParamType . some ( ( regex ) => {
462
+ const reg = new RegExp ( `^(?:${ regex } )` , 'uy' ) ;
463
+ return reg . test ( state . input . slice ( state . start ) ) ;
464
+ } ) ;
465
+ }
466
+
467
+ function isParameter ( ch : Char , state : State , paramTypes : ParamTypes ) : boolean {
468
+ if ( ! ch ) {
469
+ return false ;
470
+ }
471
+ const nextChar = peek ( state ) ;
472
+ if ( paramTypes . positional && ch === '?' ) return true ;
473
+
474
+ if ( paramTypes . numbered ?. length && paramTypes . numbered . some ( ( type ) => ch === type ) ) {
475
+ if ( nextChar !== null && ! isNaN ( Number ( nextChar ) ) ) {
476
+ return true ;
423
477
}
424
478
}
425
- if ( dialect === 'mssql' ) pStart = ':' ;
426
479
427
- return ch === pStart ;
480
+ if (
481
+ ( paramTypes . named ?. length && paramTypes . named . some ( ( type ) => type === ch ) ) ||
482
+ ( paramTypes . quoted ?. length && paramTypes . quoted . some ( ( type ) => type === ch ) )
483
+ ) {
484
+ return true ;
485
+ }
486
+
487
+ if ( paramTypes . custom ?. length && isCustomParam ( state , paramTypes . custom ) ) {
488
+ return true ;
489
+ }
490
+
491
+ return false ;
428
492
}
429
493
430
494
function isDollarQuotedString ( state : State ) : boolean {
0 commit comments