squirreling 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "squirreling",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "Squirreling SQL Engine",
5
5
  "author": "Hyperparam",
6
6
  "homepage": "https://hyperparam.app",
@@ -1,13 +1,13 @@
1
- /**
2
- * @import { DataSource, ExecuteSqlOptions, FunctionColumn, FunctionNode, OrderByItem, RowSource, SelectStatement, SqlPrimitive } from '../types.js'
3
- */
4
-
5
1
  import { defaultAggregateAlias, evaluateAggregate } from './aggregates.js'
6
2
  import { evaluateExpr } from './expression.js'
7
3
  import { evaluateHavingExpr } from './having.js'
8
4
  import { parseSql } from '../parse/parse.js'
9
5
  import { createMemorySource, createRowAccessor } from '../backend/memory.js'
10
6
 
7
+ /**
8
+ * @import { DataSource, ExecuteSqlOptions, ExprNode, OrderByItem, RowSource, SelectStatement, SqlPrimitive } from '../types.js'
9
+ */
10
+
11
11
  /**
12
12
  * Executes a SQL SELECT query against a data source
13
13
  *
@@ -21,21 +21,31 @@ export function executeSql({ source, query }) {
21
21
  }
22
22
 
23
23
  /**
24
- * Generates a default alias name for a string function
24
+ * Generates a default alias for a derived column expression
25
25
  *
26
- * @param {FunctionColumn} col - the function column definition
27
- * @returns {string} the generated alias (e.g., "upper_name", "concat_a_b")
26
+ * @param {ExprNode} expr - the expression node
27
+ * @returns {string} the generated alias
28
28
  */
29
- function defaultFunctionAlias(col) {
30
- const base = col.func.toLowerCase()
31
- // Try to extract column names from identifier arguments
32
- const columnNames = col.args
33
- .filter(arg => arg.type === 'identifier')
34
- .map(arg => arg.name)
35
- if (columnNames.length > 0) {
36
- return base + '_' + columnNames.join('_')
29
+ function defaultDerivedAlias(expr) {
30
+ if (expr.type === 'identifier') {
31
+ return expr.name
37
32
  }
38
- return base
33
+ if (expr.type === 'function') {
34
+ const base = expr.name.toLowerCase()
35
+ // Try to extract column names from identifier arguments
36
+ const columnNames = expr.args
37
+ .filter(arg => arg.type === 'identifier')
38
+ .map(arg => arg.name)
39
+ if (columnNames.length > 0) {
40
+ return base + '_' + columnNames.join('_')
41
+ }
42
+ return base
43
+ }
44
+ if (expr.type === 'cast') return 'cast_expr'
45
+ if (expr.type === 'unary' && expr.argument.type === 'identifier') {
46
+ return expr.op === '-' ? 'neg_' + expr.argument.name : 'expr'
47
+ }
48
+ return 'expr'
39
49
  }
40
50
 
41
51
  /**
@@ -118,6 +128,24 @@ function applyOrderBy(rows, orderBy) {
118
128
  const dir = term.direction
119
129
  const av = evaluateExpr(term.expr, createRowAccessor(a))
120
130
  const bv = evaluateExpr(term.expr, createRowAccessor(b))
131
+
132
+ // Handle NULLS FIRST / NULLS LAST
133
+ const aIsNull = av == null
134
+ const bIsNull = bv == null
135
+
136
+ if (aIsNull || bIsNull) {
137
+ if (aIsNull && bIsNull) continue // both null, try next sort term
138
+
139
+ // Determine null ordering
140
+ const nullsFirst = term.nulls === 'LAST' ? false : true // default is NULLS FIRST
141
+
142
+ if (aIsNull) {
143
+ return nullsFirst ? -1 : 1
144
+ } else {
145
+ return nullsFirst ? 1 : -1
146
+ }
147
+ }
148
+
121
149
  const cmp = compareValues(av, bv)
122
150
  if (cmp !== 0) {
123
151
  return dir === 'DESC' ? -cmp : cmp
@@ -214,20 +242,9 @@ function evaluateSelectAst(select, dataSource) {
214
242
  continue
215
243
  }
216
244
 
217
- if (col.kind === 'column') {
218
- const name = col.column
219
- const alias = col.alias ?? name
220
- // Evaluate on first row of group (all rows have same value for GROUP BY columns)
221
- resultRow[alias] = group[0]?.getCell(name)
222
- continue
223
- }
224
-
225
- if (col.kind === 'function') {
226
- // Evaluate function on the first row of the group
227
- /** @type {FunctionNode} */
228
- const funcNode = { type: 'function', name: col.func, args: col.args }
229
- const alias = col.alias ?? defaultFunctionAlias(col)
230
- const value = group.length > 0 ? evaluateExpr(funcNode, group[0]) : undefined
245
+ if (col.kind === 'derived') {
246
+ const alias = col.alias ?? defaultDerivedAlias(col.expr)
247
+ const value = group.length > 0 ? evaluateExpr(col.expr, group[0]) : undefined
231
248
  resultRow[alias] = value
232
249
  continue
233
250
  }
@@ -238,13 +255,6 @@ function evaluateSelectAst(select, dataSource) {
238
255
  resultRow[alias] = value
239
256
  continue
240
257
  }
241
-
242
- if (col.kind === 'operation') {
243
- const alias = col.alias ?? 'expr'
244
- const value = group.length > 0 ? evaluateExpr(col.expr, group[0]) : undefined
245
- resultRow[alias] = value
246
- continue
247
- }
248
258
  }
249
259
 
250
260
  // Apply HAVING filter before adding to projected results
@@ -269,18 +279,8 @@ function evaluateSelectAst(select, dataSource) {
269
279
  for (const key of keys) {
270
280
  outRow[key] = row.getCell(key)
271
281
  }
272
- } else if (col.kind === 'column') {
273
- const name = col.column
274
- const alias = col.alias ?? name
275
- outRow[alias] = row.getCell(name)
276
- } else if (col.kind === 'function') {
277
- /** @type {FunctionNode} */
278
- const funcNode = { type: 'function', name: col.func, args: col.args }
279
- const value = evaluateExpr(funcNode, row)
280
- const alias = col.alias ?? defaultFunctionAlias(col)
281
- outRow[alias] = value
282
- } else if (col.kind === 'operation') {
283
- const alias = col.alias ?? 'expr'
282
+ } else if (col.kind === 'derived') {
283
+ const alias = col.alias ?? defaultDerivedAlias(col.expr)
284
284
  const value = evaluateExpr(col.expr, row)
285
285
  outRow[alias] = value
286
286
  } else if (col.kind === 'aggregate') {
@@ -131,23 +131,23 @@ export function evaluateExpr(node, row) {
131
131
  return String(val).length
132
132
  }
133
133
 
134
- if (funcName === 'SUBSTRING') {
134
+ if (funcName === 'SUBSTRING' || funcName === 'SUBSTR') {
135
135
  if (args.length < 2 || args.length > 3) {
136
- throw new Error('SUBSTRING requires 2 or 3 arguments')
136
+ throw new Error(`${funcName} requires 2 or 3 arguments`)
137
137
  }
138
138
  const str = args[0]
139
139
  if (str == null) return null
140
140
  const strVal = String(str)
141
141
  const start = Number(args[1])
142
142
  if (!Number.isInteger(start) || start < 1) {
143
- throw new Error('SUBSTRING start position must be a positive integer')
143
+ throw new Error(`${funcName} start position must be a positive integer`)
144
144
  }
145
145
  // SQL uses 1-based indexing
146
146
  const startIdx = start - 1
147
147
  if (args.length === 3) {
148
148
  const len = Number(args[2])
149
149
  if (!Number.isInteger(len) || len < 0) {
150
- throw new Error('SUBSTRING length must be a non-negative integer')
150
+ throw new Error(`${funcName} length must be a non-negative integer`)
151
151
  }
152
152
  return strVal.substring(startIdx, startIdx + len)
153
153
  }
@@ -190,6 +190,24 @@ export function evaluateExpr(node, row) {
190
190
  throw new Error('Unsupported CAST to type ' + node.toType)
191
191
  }
192
192
 
193
+ // IN and NOT IN with value lists
194
+ if (node.type === 'in valuelist') {
195
+ const exprVal = evaluateExpr(node.expr, row)
196
+ for (const valueNode of node.values) {
197
+ const val = evaluateExpr(valueNode, row)
198
+ if (exprVal === val) return true
199
+ }
200
+ return false
201
+ }
202
+ if (node.type === 'not in valuelist') {
203
+ const exprVal = evaluateExpr(node.expr, row)
204
+ for (const valueNode of node.values) {
205
+ const val = evaluateExpr(valueNode, row)
206
+ if (exprVal === val) return false
207
+ }
208
+ return true
209
+ }
210
+
193
211
  // IN and NOT IN with subqueries
194
212
  if (node.type === 'in') {
195
213
  throw new Error('WHERE IN with subqueries is not yet supported.')
@@ -1,5 +1,7 @@
1
+ import { isAggregateFunc, isStringFunc } from '../validation.js'
2
+
1
3
  /**
2
- * @import { ExprCursor, ExprNode, BinaryOp, SelectStatement } from '../types.js'
4
+ * @import { ExprCursor, ExprNode, BinaryOp } from '../types.js'
3
5
  */
4
6
 
5
7
  /**
@@ -11,13 +13,10 @@ export function parseExpression(c) {
11
13
  }
12
14
 
13
15
  /**
14
- * Exposed so SELECT list parsing can reuse the same notion of "primary"
15
- * for function arguments, etc.
16
- *
17
16
  * @param {ExprCursor} c
18
17
  * @returns {ExprNode}
19
18
  */
20
- export function parsePrimary(c) {
19
+ function parsePrimary(c) {
21
20
  const tok = c.current()
22
21
 
23
22
  if (tok.type === 'paren' && tok.value === '(') {
@@ -30,10 +29,30 @@ export function parsePrimary(c) {
30
29
  if (tok.type === 'identifier') {
31
30
  const next = c.peek(1)
32
31
 
32
+ // CAST expression
33
+ if (tok.value === 'CAST' && next.type === 'paren' && next.value === '(') {
34
+ c.consume() // CAST
35
+ c.consume() // '('
36
+ const expr = parseExpression(c)
37
+ c.expect('keyword', 'AS')
38
+ const typeTok = c.expectIdentifier()
39
+ c.expect('paren', ')')
40
+ return {
41
+ type: 'cast',
42
+ expr,
43
+ toType: typeTok.value,
44
+ }
45
+ }
46
+
33
47
  // function call
34
48
  if (next.type === 'paren' && next.value === '(') {
35
49
  const funcName = tok.value
36
- // TODO: validate function name
50
+
51
+ // validate function names
52
+ if (!isStringFunc(funcName) && !isAggregateFunc(funcName)) {
53
+ throw new Error(`Unknown function "${funcName}" at position ${tok.position}`)
54
+ }
55
+
37
56
  c.consume() // function name
38
57
  c.consume() // '('
39
58
 
@@ -281,28 +300,80 @@ function parseComparison(c) {
281
300
  if (nextTok.type === 'keyword' && nextTok.value === 'IN') {
282
301
  c.consume() // NOT
283
302
  c.consume() // IN
284
- if (!c.parseSubquery) {
285
- throw new Error('Subquery parsing not available in this context')
303
+
304
+ // Check if it's a subquery or a list of values by peeking ahead
305
+ // parseSubquery expects to consume the opening paren itself
306
+ const parenTok = c.current()
307
+ if (parenTok.type !== 'paren' || parenTok.value !== '(') {
308
+ throw new Error('Expected ( after IN')
286
309
  }
287
- const subquery = c.parseSubquery()
288
- return {
289
- type: 'not in',
290
- expr: left,
291
- subquery,
310
+ const peekTok = c.peek(1)
311
+ if (peekTok.type === 'keyword' && peekTok.value === 'SELECT') {
312
+ // Subquery - let parseSubquery handle the parens
313
+ if (!c.parseSubquery) {
314
+ throw new Error('Subquery parsing not available in this context')
315
+ }
316
+ const subquery = c.parseSubquery()
317
+ return {
318
+ type: 'not in',
319
+ expr: left,
320
+ subquery,
321
+ }
322
+ } else {
323
+ // Parse list of values - we handle the parens
324
+ c.consume() // '('
325
+ /** @type {ExprNode[]} */
326
+ const values = []
327
+ while (true) {
328
+ values.push(parseExpression(c))
329
+ if (!c.match('comma')) break
330
+ }
331
+ c.expect('paren', ')')
332
+ return {
333
+ type: 'not in valuelist',
334
+ expr: left,
335
+ values,
336
+ }
292
337
  }
293
338
  }
294
339
  }
295
340
 
296
341
  if (tok.type === 'keyword' && tok.value === 'IN') {
297
342
  c.consume() // IN
298
- if (!c.parseSubquery) {
299
- throw new Error('Subquery parsing not available in this context')
343
+
344
+ // Check if it's a subquery or a list of values by peeking ahead
345
+ // parseSubquery expects to consume the opening paren itself
346
+ const parenTok = c.current()
347
+ if (parenTok.type !== 'paren' || parenTok.value !== '(') {
348
+ throw new Error('Expected ( after IN')
300
349
  }
301
- const subquery = c.parseSubquery()
302
- return {
303
- type: 'in',
304
- expr: left,
305
- subquery,
350
+ const peekTok = c.peek(1)
351
+ if (peekTok.type === 'keyword' && peekTok.value === 'SELECT') {
352
+ // Subquery - let parseSubquery handle the parens
353
+ if (!c.parseSubquery) {
354
+ throw new Error('Subquery parsing not available in this context')
355
+ }
356
+ const subquery = c.parseSubquery()
357
+ return {
358
+ type: 'in',
359
+ expr: left,
360
+ subquery,
361
+ }
362
+ } else {
363
+ // Parse list of values - we handle the parens
364
+ c.consume() // '('
365
+ /** @type {ExprNode[]} */
366
+ const values = []
367
+ while (true) {
368
+ values.push(parseExpression(c))
369
+ if (!c.match('comma')) break
370
+ }
371
+ c.expect('paren', ')')
372
+ return {
373
+ type: 'in valuelist',
374
+ expr: left,
375
+ values,
376
+ }
306
377
  }
307
378
  }
308
379
 
@@ -1,11 +1,11 @@
1
+ import { tokenize } from './tokenize.js'
2
+ import { parseExpression } from './expression.js'
3
+ import { isAggregateFunc } from '../validation.js'
4
+
1
5
  /**
2
- * @import { AggregateColumn, AggregateArg, AggregateFunc, ExprCursor, ExprNode, FromSubquery, JoinClause, JoinType, OrderByItem, ParserState, SelectStatement, SelectColumn, StringFunc, Token, TokenType } from '../types.js'
6
+ * @import { AggregateColumn, AggregateArg, AggregateFunc, ExprCursor, ExprNode, FromSubquery, JoinClause, JoinType, OrderByItem, ParserState, SelectStatement, SelectColumn, Token, TokenType } from '../types.js'
3
7
  */
4
8
 
5
- import { tokenize } from './tokenize.js'
6
- import { parseExpression, parsePrimary } from './expression.js'
7
- import { isAggregateFunc, isStringFunc } from '../validation.js'
8
-
9
9
  // Keywords that cannot be used as implicit aliases after a column
10
10
  const RESERVED_AFTER_COLUMN = new Set([
11
11
  'FROM',
@@ -112,6 +112,7 @@ function expectIdentifier(state) {
112
112
 
113
113
  /**
114
114
  * Creates an ExprCursor adapter for the ParserState.
115
+ *
115
116
  * @param {ParserState} state
116
117
  * @returns {ExprCursor}
117
118
  */
@@ -141,6 +142,20 @@ function parseSelectList(state) {
141
142
  const cols = []
142
143
  const tok = current(state)
143
144
 
145
+ // Check for qualified asterisk (table.*)
146
+ if (tok.type === 'identifier') {
147
+ const next = peekToken(state, 1)
148
+ const nextNext = peekToken(state, 2)
149
+ if (next.type === 'dot' && nextNext.type === 'operator' && nextNext.value === '*') {
150
+ const tableTok = consume(state) // consume table name
151
+ consume(state) // consume dot
152
+ consume(state) // consume asterisk
153
+ cols.push({ kind: 'star', table: tableTok.value })
154
+ return cols
155
+ }
156
+ }
157
+
158
+ // Check for unqualified asterisk (*)
144
159
  if (tok.type === 'operator' && tok.value === '*') {
145
160
  consume(state)
146
161
  cols.push({ kind: 'star' })
@@ -166,56 +181,20 @@ function parseSelectItem(state) {
166
181
  throw parseError(state, 'column name or expression')
167
182
  }
168
183
 
169
- if (tok.type === 'identifier' && tok.value === 'CAST') {
170
- expectIdentifier(state) // consume CAST
171
- expect(state, 'paren', '(')
172
- const cursor = createExprCursor(state)
173
- const expr = parseExpression(cursor)
174
- expect(state, 'keyword', 'AS')
175
- const typeTok = expectIdentifier(state)
176
- expect(state, 'paren', ')')
177
- const alias = parseAs(state)
178
- return {
179
- kind: 'operation',
180
- expr: { type: 'cast', expr, toType: typeTok.value },
181
- alias,
182
- }
183
- }
184
-
185
- if (tok.type === 'operator') {
186
- // Handle SELECT expression AS alias
187
- const cursor = createExprCursor(state)
188
- const expr = parseExpression(cursor)
189
- const alias = parseAs(state)
190
- return { kind: 'operation', expr, alias }
191
- }
192
-
193
184
  const next = peekToken(state, 1)
194
- const upper = tok.value.toUpperCase()
195
-
196
185
  if (next.type === 'paren' && next.value === '(') {
197
- expectIdentifier(state) // consume function name
186
+ const upper = tok.value.toUpperCase()
198
187
  if (isAggregateFunc(upper)) {
188
+ expectIdentifier(state) // consume function name
199
189
  return parseAggregateItem(state, upper)
200
190
  }
201
- if (isStringFunc(upper)) {
202
- return parseStringFunctionItem(state, upper)
203
- }
204
- }
205
-
206
- consume(state)
207
- let column = tok.value
208
-
209
- // Handle dot notation (table.column)
210
- if (current(state).type === 'dot') {
211
- consume(state) // consume the dot
212
- const columnTok = expectIdentifier(state)
213
- column += '.' + columnTok.value
214
191
  }
215
192
 
193
+ // Delegate to expression parser
194
+ const cursor = createExprCursor(state)
195
+ const expr = parseExpression(cursor)
216
196
  const alias = parseAs(state)
217
-
218
- return { kind: 'column', column, alias }
197
+ return { kind: 'derived', expr, alias }
219
198
  }
220
199
 
221
200
  /**
@@ -261,34 +240,6 @@ function parseAggregateItem(state, func) {
261
240
  return { kind: 'aggregate', func, arg, alias }
262
241
  }
263
242
 
264
- /**
265
- * @param {ParserState} state
266
- * @param {StringFunc} func
267
- * @returns {SelectColumn}
268
- */
269
- function parseStringFunctionItem(state, func) {
270
- expect(state, 'paren', '(')
271
-
272
- /** @type {ExprNode[]} */
273
- const args = []
274
-
275
- // Parse comma-separated arguments
276
- if (current(state).type !== 'paren' || current(state).value !== ')') {
277
- const cursor = createExprCursor(state)
278
- while (true) {
279
- const arg = parsePrimary(cursor)
280
- args.push(arg)
281
- if (!match(state, 'comma')) break
282
- }
283
- }
284
-
285
- expect(state, 'paren', ')')
286
-
287
- const alias = parseAs(state)
288
-
289
- return { kind: 'function', func, args, alias }
290
- }
291
-
292
243
  /**
293
244
  * @param {ParserState} state
294
245
  * @returns {string | undefined}
@@ -481,9 +432,24 @@ function parseSelectInternal(state) {
481
432
  } else if (match(state, 'keyword', 'DESC')) {
482
433
  direction = 'DESC'
483
434
  }
435
+ /** @type {'FIRST' | 'LAST' | undefined} */
436
+ let nulls
437
+ if (match(state, 'keyword', 'NULLS')) {
438
+ const tok = current(state)
439
+ if (tok.type === 'identifier' && tok.value.toUpperCase() === 'FIRST') {
440
+ consume(state)
441
+ nulls = 'FIRST'
442
+ } else if (tok.type === 'identifier' && tok.value.toUpperCase() === 'LAST') {
443
+ consume(state)
444
+ nulls = 'LAST'
445
+ } else {
446
+ throw parseError(state, 'FIRST or LAST after NULLS')
447
+ }
448
+ }
484
449
  orderBy.push({
485
450
  expr,
486
451
  direction,
452
+ nulls,
487
453
  })
488
454
  if (!match(state, 'comma')) break
489
455
  }
@@ -16,6 +16,7 @@ const KEYWORDS = new Set([
16
16
  'ORDER',
17
17
  'ASC',
18
18
  'DESC',
19
+ 'NULLS',
19
20
  'LIMIT',
20
21
  'OFFSET',
21
22
  'AS',
package/src/types.d.ts CHANGED
@@ -34,26 +34,6 @@ export interface SelectStatement {
34
34
  offset?: number
35
35
  }
36
36
 
37
- export type TokenType =
38
- | 'keyword'
39
- | 'identifier'
40
- | 'number'
41
- | 'string'
42
- | 'operator'
43
- | 'comma'
44
- | 'dot'
45
- | 'paren'
46
- | 'semicolon'
47
- | 'eof'
48
-
49
- export interface Token {
50
- type: TokenType
51
- value: string
52
- position: number
53
- numericValue?: number
54
- originalValue?: string
55
- }
56
-
57
37
  export type BinaryOp =
58
38
  | 'AND'
59
39
  | 'OR'
@@ -108,12 +88,18 @@ export interface BetweenNode {
108
88
  upper: ExprNode
109
89
  }
110
90
 
111
- export interface InNode {
91
+ export interface InSubqueryNode {
112
92
  type: 'in' | 'not in'
113
93
  expr: ExprNode
114
94
  subquery: SelectStatement
115
95
  }
116
96
 
97
+ export interface InValuesNode {
98
+ type: 'in valuelist' | 'not in valuelist'
99
+ expr: ExprNode
100
+ values: ExprNode[]
101
+ }
102
+
117
103
  export interface ExistsNode {
118
104
  type: 'exists' | 'not exists'
119
105
  subquery: SelectStatement
@@ -127,23 +113,19 @@ export type ExprNode =
127
113
  | FunctionNode
128
114
  | CastNode
129
115
  | BetweenNode
130
- | InNode
116
+ | InSubqueryNode
117
+ | InValuesNode
131
118
  | ExistsNode
132
119
 
133
120
  export interface StarColumn {
134
121
  kind: 'star'
135
- alias?: string
136
- }
137
-
138
- export interface SimpleColumn {
139
- kind: 'column'
140
- column: string
122
+ table?: string
141
123
  alias?: string
142
124
  }
143
125
 
144
126
  export type AggregateFunc = 'COUNT' | 'SUM' | 'AVG' | 'MIN' | 'MAX'
145
127
 
146
- export type StringFunc = 'UPPER' | 'LOWER' | 'CONCAT' | 'LENGTH' | 'SUBSTRING' | 'TRIM'
128
+ export type StringFunc = 'UPPER' | 'LOWER' | 'CONCAT' | 'LENGTH' | 'SUBSTRING' | 'SUBSTR' | 'TRIM'
147
129
 
148
130
  export interface AggregateArgStar {
149
131
  kind: 'star'
@@ -163,24 +145,18 @@ export interface AggregateColumn {
163
145
  alias?: string
164
146
  }
165
147
 
166
- export interface FunctionColumn {
167
- kind: 'function'
168
- func: StringFunc
169
- args: ExprNode[]
170
- alias?: string
171
- }
172
-
173
- export interface OperationColumn {
174
- kind: 'operation'
148
+ export interface DerivedColumn {
149
+ kind: 'derived'
175
150
  expr: ExprNode
176
151
  alias?: string
177
152
  }
178
153
 
179
- export type SelectColumn = StarColumn | SimpleColumn | AggregateColumn | FunctionColumn | OperationColumn
154
+ export type SelectColumn = StarColumn | AggregateColumn | DerivedColumn
180
155
 
181
156
  export interface OrderByItem {
182
157
  expr: ExprNode
183
158
  direction: 'ASC' | 'DESC'
159
+ nulls?: 'FIRST' | 'LAST'
184
160
  }
185
161
 
186
162
  export type JoinType = 'INNER' | 'LEFT' | 'RIGHT' | 'FULL' | 'CROSS'
@@ -205,3 +181,24 @@ export interface ExprCursor {
205
181
  expectIdentifier(): Token
206
182
  parseSubquery?: () => SelectStatement
207
183
  }
184
+
185
+ // Tokenizer types
186
+ export type TokenType =
187
+ | 'keyword'
188
+ | 'identifier'
189
+ | 'number'
190
+ | 'string'
191
+ | 'operator'
192
+ | 'comma'
193
+ | 'dot'
194
+ | 'paren'
195
+ | 'semicolon'
196
+ | 'eof'
197
+
198
+ export interface Token {
199
+ type: TokenType
200
+ value: string
201
+ position: number
202
+ numericValue?: number
203
+ originalValue?: string
204
+ }
package/src/validation.js CHANGED
@@ -13,5 +13,5 @@ export function isAggregateFunc(name) {
13
13
  * @returns {name is StringFunc}
14
14
  */
15
15
  export function isStringFunc(name) {
16
- return ['UPPER', 'LOWER', 'CONCAT', 'LENGTH', 'SUBSTRING', 'TRIM'].includes(name)
16
+ return ['UPPER', 'LOWER', 'CONCAT', 'LENGTH', 'SUBSTRING', 'SUBSTR', 'TRIM'].includes(name)
17
17
  }