squirreling 0.4.7 → 0.4.8

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/README.md CHANGED
@@ -76,5 +76,6 @@ console.log(allUsers)
76
76
  - `GROUP BY` and `HAVING` clauses
77
77
  - Aggregate functions: `COUNT`, `SUM`, `AVG`, `MIN`, `MAX`, `JSON_ARRAYAGG`
78
78
  - String functions: `CONCAT`, `SUBSTRING`, `LENGTH`, `UPPER`, `LOWER`
79
+ - Date functions: `CURRENT_DATE`, `CURRENT_TIME`, `CURRENT_TIMESTAMP`, `INTERVAL`
79
80
  - Json functions: `JSON_VALUE`, `JSON_QUERY`, `JSON_OBJECT`
80
81
  - Basic expressions and arithmetic operations
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "squirreling",
3
- "version": "0.4.7",
3
+ "version": "0.4.8",
4
4
  "description": "Squirreling SQL Engine",
5
5
  "author": "Hyperparam",
6
6
  "homepage": "https://hyperparam.app",
@@ -40,7 +40,7 @@
40
40
  "@types/node": "24.10.1",
41
41
  "@vitest/coverage-v8": "4.0.15",
42
42
  "eslint": "9.39.1",
43
- "eslint-plugin-jsdoc": "61.4.1",
43
+ "eslint-plugin-jsdoc": "61.4.2",
44
44
  "typescript": "5.9.3",
45
45
  "vitest": "4.0.15"
46
46
  }
package/src/errors.js ADDED
@@ -0,0 +1,230 @@
1
+ // ============================================================================
2
+ // PARSE ERRORS - Issues during SQL tokenization and parsing
3
+ // ============================================================================
4
+
5
+ /**
6
+ * General syntax error for unexpected tokens.
7
+ *
8
+ * @param {Object} options
9
+ * @param {string} options.expected - Description of what was expected
10
+ * @param {string} options.received - What was actually found
11
+ * @param {number} options.position - Character position in query
12
+ * @param {string} [options.after] - What token came before (for context)
13
+ * @returns {Error}
14
+ */
15
+ export function syntaxError({ expected, received, position, after }) {
16
+ const afterClause = after ? ` after "${after}"` : ''
17
+ return new Error(`Expected ${expected}${afterClause} but found ${received} at position ${position}`)
18
+ }
19
+
20
+ /**
21
+ * Error for unterminated literals (strings, identifiers).
22
+ *
23
+ * @param {'string' | 'identifier'} type - Type of unterminated literal
24
+ * @param {number} position - Starting position
25
+ * @returns {Error}
26
+ */
27
+ export function unterminatedError(type, position) {
28
+ const name = type === 'string' ? 'string literal' : 'identifier'
29
+ return new Error(`Unterminated ${name} starting at position ${position}`)
30
+ }
31
+
32
+ /**
33
+ * Error for invalid literals (numbers, intervals, etc).
34
+ *
35
+ * @param {Object} options
36
+ * @param {string} options.type - Type of invalid literal (e.g., 'number', 'interval value', 'interval unit')
37
+ * @param {string} options.value - The invalid value
38
+ * @param {number} options.position - Position in query
39
+ * @param {string} [options.validValues] - List of valid values (for enums like interval units)
40
+ * @returns {Error}
41
+ */
42
+ export function invalidLiteralError({ type, value, position, validValues }) {
43
+ const suffix = validValues ? `. Valid values: ${validValues}` : ''
44
+ return new Error(`Invalid ${type} ${value} at position ${position}${suffix}`)
45
+ }
46
+
47
+ /**
48
+ * Error for unexpected characters during tokenization.
49
+ *
50
+ * @param {string} char - The unexpected character
51
+ * @param {number} position - Position in query
52
+ * @param {boolean} [expectsSelect=false] - Whether SELECT was expected (first token)
53
+ * @returns {Error}
54
+ */
55
+ export function unexpectedCharError(char, position, expectsSelect = false) {
56
+ if (expectsSelect) {
57
+ return new Error(`Expected SELECT but found "${char}" at position ${position}. Queries must start with SELECT.`)
58
+ }
59
+ return new Error(`Unexpected character "${char}" at position ${position}`)
60
+ }
61
+
62
+ /**
63
+ * Error for unknown/unsupported functions.
64
+ *
65
+ * @param {string} funcName - The unknown function name
66
+ * @param {number} [position] - Position in query (for parse errors)
67
+ * @param {string} [validFunctions] - List of valid functions
68
+ * @returns {Error}
69
+ */
70
+ export function unknownFunctionError(funcName, position, validFunctions) {
71
+ const supported = validFunctions ||
72
+ 'COUNT, SUM, AVG, MIN, MAX, UPPER, LOWER, CONCAT, LENGTH, SUBSTRING, TRIM, REPLACE, JSON_OBJECT, JSON_VALUE, JSON_QUERY, JSON_ARRAYAGG'
73
+
74
+ if (position !== undefined) {
75
+ return new Error(`Unknown function "${funcName}" at position ${position}. Supported: ${supported}`)
76
+ }
77
+ return new Error(`Unsupported function: ${funcName}. Supported: ${supported}`)
78
+ }
79
+
80
+ /**
81
+ * Error for missing required clause or structure.
82
+ *
83
+ * @param {Object} options
84
+ * @param {string} options.missing - What is missing (e.g., 'WHEN clause', 'FROM clause', 'ON condition')
85
+ * @param {string} options.context - Where it's missing from (e.g., 'CASE expression', 'SELECT statement', 'JOIN')
86
+ * @returns {Error}
87
+ */
88
+ export function missingClauseError({ missing, context }) {
89
+ return new Error(`${context} requires ${missing}`)
90
+ }
91
+
92
+ // ============================================================================
93
+ // EXECUTION ERRORS - Issues during query execution
94
+ // ============================================================================
95
+
96
+ /**
97
+ * Error for missing table.
98
+ *
99
+ * @param {string} tableName - The missing table name
100
+ * @returns {Error}
101
+ */
102
+ export function tableNotFoundError(tableName) {
103
+ return new Error(`Table "${tableName}" not found. Check spelling or add it to the tables parameter.`)
104
+ }
105
+
106
+ /**
107
+ * Error for invalid context (e.g., INTERVAL without date arithmetic).
108
+ *
109
+ * @param {Object} options
110
+ * @param {string} options.item - What was used incorrectly
111
+ * @param {string} options.validContext - Where it can be used
112
+ * @returns {Error}
113
+ */
114
+ export function invalidContextError({ item, validContext }) {
115
+ return new Error(`${item} can only be used with ${validContext}`)
116
+ }
117
+
118
+ /**
119
+ * Error for unsupported operation combinations.
120
+ *
121
+ * @param {string} operation - The unsupported operation
122
+ * @param {string} [hint] - How to fix it
123
+ * @returns {Error}
124
+ */
125
+ export function unsupportedOperationError(operation, hint) {
126
+ const suffix = hint ? `. ${hint}` : ''
127
+ return new Error(`${operation}${suffix}`)
128
+ }
129
+
130
+ // ============================================================================
131
+ // VALIDATION ERRORS - Function argument and type validation
132
+ // ============================================================================
133
+
134
+ /**
135
+ * Function signatures for helpful error messages.
136
+ * Maps function name to its parameter signature.
137
+ * @type {Record<string, string>}
138
+ */
139
+ const FUNCTION_SIGNATURES = {
140
+ // String functions
141
+ UPPER: 'string',
142
+ LOWER: 'string',
143
+ LENGTH: 'string',
144
+ TRIM: 'string',
145
+ REPLACE: 'string, search, replacement',
146
+ SUBSTRING: 'string, start[, length]',
147
+ SUBSTR: 'string, start[, length]',
148
+ CONCAT: 'value1, value2[, ...]',
149
+
150
+ // Date/time functions
151
+ RANDOM: '',
152
+ RAND: '',
153
+ CURRENT_DATE: '',
154
+ CURRENT_TIME: '',
155
+ CURRENT_TIMESTAMP: '',
156
+
157
+ // JSON functions
158
+ JSON_VALUE: 'expression, path',
159
+ JSON_QUERY: 'expression, path',
160
+ JSON_OBJECT: 'key1, value1[, ...]',
161
+ JSON_ARRAYAGG: 'expression',
162
+
163
+ // Aggregate functions
164
+ COUNT: 'expression',
165
+ SUM: 'expression',
166
+ AVG: 'expression',
167
+ MIN: 'expression',
168
+ MAX: 'expression',
169
+ }
170
+
171
+ /**
172
+ * Error for wrong number of function arguments.
173
+ *
174
+ * @param {string} funcName - The function name
175
+ * @param {number | string} expected - Expected count (number or range like "2 or 3")
176
+ * @param {number} received - Actual argument count
177
+ * @returns {Error}
178
+ */
179
+ export function argCountError(funcName, expected, received) {
180
+ const signature = FUNCTION_SIGNATURES[funcName] ?? ''
181
+ let expectedStr = `${expected} arguments`
182
+ if (expected === 0) expectedStr = 'no arguments'
183
+ if (expected === 1) expectedStr = '1 argument'
184
+ if (typeof expected === 'string' && expected.endsWith(' 1')) {
185
+ expectedStr = `${expected} argument`
186
+ }
187
+
188
+ return new Error(`${funcName}(${signature}) function requires ${expectedStr}, got ${received}`)
189
+ }
190
+
191
+ /**
192
+ * Error for invalid argument type or value.
193
+ *
194
+ * @param {Object} options
195
+ * @param {string} options.funcName - The function name
196
+ * @param {string} options.message - Specific error message
197
+ * @param {string} [options.hint] - Recovery hint
198
+ * @returns {Error}
199
+ */
200
+ export function argValueError({ funcName, message, hint }) {
201
+ const signature = FUNCTION_SIGNATURES[funcName] ?? ''
202
+ const suffix = hint ? `. ${hint}` : ''
203
+ return new Error(`${funcName}(${signature}): ${message}${suffix}`)
204
+ }
205
+
206
+ /**
207
+ * Error for aggregate function misuse (e.g., SUM(*)).
208
+ *
209
+ * @param {string} funcName - The aggregate function
210
+ * @param {string} issue - What's wrong (e.g., "(*) is not supported")
211
+ * @returns {Error}
212
+ */
213
+ export function aggregateError(funcName, issue) {
214
+ return new Error(`${funcName}${issue}. Only COUNT supports *. Use a column name for ${funcName}.`)
215
+ }
216
+
217
+ /**
218
+ * Error for unsupported CAST type.
219
+ *
220
+ * @param {string} toType - The unsupported target type
221
+ * @param {string} [fromType] - The source type (optional)
222
+ * @returns {Error}
223
+ */
224
+ export function castError(toType, fromType) {
225
+ const message = fromType
226
+ ? `Cannot CAST ${fromType} to ${toType}`
227
+ : `Unsupported CAST to type ${toType}`
228
+
229
+ return new Error(`${message}. Supported types: TEXT, VARCHAR, INTEGER, INT, BIGINT, FLOAT, REAL, DOUBLE, BOOLEAN`)
230
+ }
@@ -1,3 +1,4 @@
1
+ import { aggregateError, unknownFunctionError } from '../errors.js'
1
2
  import { evaluateExpr } from './expression.js'
2
3
  import { defaultDerivedAlias, stringify } from './utils.js'
3
4
 
@@ -20,7 +21,7 @@ export async function evaluateAggregate({ col, rows, tables }) {
20
21
  const seen = new Set()
21
22
  for (const row of rows) {
22
23
  const v = await evaluateExpr({ node: arg.expr, row, tables })
23
- if (v !== null && v !== undefined) {
24
+ if (v != null) {
24
25
  seen.add(v)
25
26
  }
26
27
  }
@@ -29,7 +30,7 @@ export async function evaluateAggregate({ col, rows, tables }) {
29
30
  let count = 0
30
31
  for (const row of rows) {
31
32
  const v = await evaluateExpr({ node: arg.expr, row, tables })
32
- if (v !== null && v !== undefined) {
33
+ if (v != null) {
33
34
  count += 1
34
35
  }
35
36
  }
@@ -38,7 +39,7 @@ export async function evaluateAggregate({ col, rows, tables }) {
38
39
 
39
40
  if (func === 'SUM' || func === 'AVG' || func === 'MIN' || func === 'MAX') {
40
41
  if (arg.kind === 'star') {
41
- throw new Error(func + '(*) is not supported, use a column name')
42
+ throw aggregateError(func, '(*) is not supported, use a column name')
42
43
  }
43
44
  let sum = 0
44
45
  let count = 0
@@ -72,7 +73,7 @@ export async function evaluateAggregate({ col, rows, tables }) {
72
73
 
73
74
  if (func === 'JSON_ARRAYAGG') {
74
75
  if (arg.kind === 'star') {
75
- throw new Error('JSON_ARRAYAGG(*) is not supported, use a column name or expression')
76
+ throw aggregateError('JSON_ARRAYAGG', '(*) is not supported, use a column name or expression')
76
77
  }
77
78
  /** @type {SqlPrimitive[]} */
78
79
  const values = []
@@ -95,7 +96,7 @@ export async function evaluateAggregate({ col, rows, tables }) {
95
96
  return values
96
97
  }
97
98
 
98
- throw new Error('Unsupported aggregate function ' + func)
99
+ throw unknownFunctionError(func, undefined, 'COUNT, SUM, AVG, MIN, MAX, JSON_ARRAYAGG')
99
100
  }
100
101
 
101
102
  /**
@@ -0,0 +1,57 @@
1
+ /**
2
+ * @import { SqlPrimitive, IntervalUnit } from '../types.js'
3
+ */
4
+
5
+ /**
6
+ * @param {SqlPrimitive} val
7
+ * @returns {Date | null}
8
+ */
9
+ function toDate(val) {
10
+ if (val instanceof Date) return val
11
+ const dateOrTime = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2})?/
12
+ if (typeof val === 'string' && dateOrTime.test(val)) {
13
+ const date = new Date(val)
14
+ if (!isNaN(date.getTime())) {
15
+ return date
16
+ }
17
+ }
18
+ return null
19
+ }
20
+
21
+ /**
22
+ * Apply an interval to a date
23
+ * @param {SqlPrimitive} dateVal
24
+ * @param {number} value
25
+ * @param {IntervalUnit} unit
26
+ * @param {'+' | '-'} op
27
+ * @returns {string | null}
28
+ */
29
+ export function applyIntervalToDate(dateVal, value, unit, op) {
30
+ const date = toDate(dateVal)
31
+ if (date == null) return null
32
+
33
+ const multiplier = op === '+' ? 1 : -1
34
+ const adjusted = value * multiplier
35
+
36
+ if (unit === 'SECOND') {
37
+ date.setUTCSeconds(date.getUTCSeconds() + adjusted)
38
+ } else if (unit === 'MINUTE') {
39
+ date.setUTCMinutes(date.getUTCMinutes() + adjusted)
40
+ } else if (unit === 'HOUR') {
41
+ date.setUTCHours(date.getUTCHours() + adjusted)
42
+ } else if (unit === 'DAY') {
43
+ date.setUTCDate(date.getUTCDate() + adjusted)
44
+ } else if (unit === 'MONTH') {
45
+ date.setUTCMonth(date.getUTCMonth() + adjusted)
46
+ } else if (unit === 'YEAR') {
47
+ date.setUTCFullYear(date.getUTCFullYear() + adjusted)
48
+ }
49
+
50
+ // Return in same format as input
51
+ if (dateVal instanceof Date) return date.toISOString()
52
+ if (String(dateVal).includes('T')) {
53
+ return date.toISOString()
54
+ } else {
55
+ return date.toISOString().split('T')[0]
56
+ }
57
+ }
@@ -1,3 +1,4 @@
1
+ import { missingClauseError, tableNotFoundError, unsupportedOperationError } from '../errors.js'
1
2
  import { generatorSource, memorySource } from '../backend/dataSource.js'
2
3
  import { parseSql } from '../parse/parse.js'
3
4
  import { defaultAggregateAlias, evaluateAggregate } from './aggregates.js'
@@ -22,7 +23,10 @@ export async function* executeSql({ tables, query }) {
22
23
 
23
24
  // Check for unsupported operations
24
25
  if (!select.from) {
25
- throw new Error('FROM clause is required')
26
+ throw missingClauseError({
27
+ missing: 'FROM clause',
28
+ context: 'SELECT statement',
29
+ })
26
30
  }
27
31
 
28
32
  // Normalize tables: convert arrays to AsyncDataSource
@@ -57,7 +61,7 @@ export async function* executeSelect(select, tables) {
57
61
  fromTableName = select.from.alias ?? select.from.table
58
62
  dataSource = tables[select.from.table]
59
63
  if (dataSource === undefined) {
60
- throw new Error(`Table "${select.from.table}" not found`)
64
+ throw tableNotFoundError(select.from.table)
61
65
  }
62
66
  } else {
63
67
  // Nested subquery - recursively resolve
@@ -375,7 +379,10 @@ async function* evaluateBuffered(select, dataSource, tables, hasAggregate, useGr
375
379
 
376
380
  const hasStar = select.columns.some(col => col.kind === 'star')
377
381
  if (hasStar && hasAggregate) {
378
- throw new Error('SELECT * with aggregate functions is not supported in this implementation')
382
+ throw unsupportedOperationError(
383
+ 'SELECT * with aggregate functions is not supported',
384
+ 'Replace * with specific column names when using aggregate functions.'
385
+ )
379
386
  }
380
387
 
381
388
  for (const group of groups) {
@@ -1,8 +1,16 @@
1
+ import {
2
+ argCountError,
3
+ argValueError,
4
+ castError,
5
+ invalidContextError,
6
+ unknownFunctionError,
7
+ } from '../errors.js'
8
+ import { applyIntervalToDate } from './date.js'
1
9
  import { executeSelect } from './execute.js'
2
10
  import { applyBinaryOp, stringify } from './utils.js'
3
11
 
4
12
  /**
5
- * @import { ExprNode, AsyncRow, SqlPrimitive, AsyncDataSource } from '../types.js'
13
+ * @import { ExprNode, AsyncRow, SqlPrimitive, AsyncDataSource, IntervalUnit } from '../types.js'
6
14
  */
7
15
 
8
16
  /**
@@ -31,7 +39,7 @@ export async function evaluateExpr({ node, row, tables }) {
31
39
  return row[colName]()
32
40
  }
33
41
  }
34
- return undefined
42
+ return null
35
43
  }
36
44
 
37
45
  // Scalar subquery - returns a single value
@@ -66,6 +74,16 @@ export async function evaluateExpr({ node, row, tables }) {
66
74
 
67
75
  // Binary operators
68
76
  if (node.type === 'binary') {
77
+ // Handle date +/- interval at AST level
78
+ if ((node.op === '+' || node.op === '-') && node.right.type === 'interval') {
79
+ const dateVal = await evaluateExpr({ node: node.left, row, tables })
80
+ return applyIntervalToDate(dateVal, node.right.value, node.right.unit, node.op)
81
+ }
82
+ if (node.op === '+' && node.left.type === 'interval') {
83
+ const dateVal = await evaluateExpr({ node: node.right, row, tables })
84
+ return applyIntervalToDate(dateVal, node.left.value, node.left.unit, '+')
85
+ }
86
+
69
87
  const left = await evaluateExpr({ node: node.left, row, tables })
70
88
 
71
89
  // Short-circuit evaluation for AND and OR
@@ -87,31 +105,35 @@ export async function evaluateExpr({ node, row, tables }) {
87
105
  const args = await Promise.all(node.args.map(arg => evaluateExpr({ node: arg, row, tables })))
88
106
 
89
107
  if (funcName === 'UPPER') {
90
- if (args.length !== 1) throw new Error('UPPER requires exactly 1 argument')
108
+ if (args.length !== 1) throw argCountError('UPPER', 1, args.length)
91
109
  const val = args[0]
92
110
  if (val == null) return null
93
111
  return String(val).toUpperCase()
94
112
  }
95
113
 
96
114
  if (funcName === 'LOWER') {
97
- if (args.length !== 1) throw new Error('LOWER requires exactly 1 argument')
115
+ if (args.length !== 1) throw argCountError('LOWER', 1, args.length)
98
116
  const val = args[0]
99
117
  if (val == null) return null
100
118
  return String(val).toLowerCase()
101
119
  }
102
120
 
103
121
  if (funcName === 'CONCAT') {
104
- if (args.length < 1) throw new Error('CONCAT requires at least 1 argument')
122
+ if (args.length < 1) throw argCountError('CONCAT', 'at least 1', args.length)
105
123
  // SQL CONCAT returns NULL if any argument is NULL
106
124
  if (args.some(a => a == null)) return null
107
125
  if (args.some(a => typeof a === 'object')) {
108
- throw new Error('CONCAT does not support object arguments')
126
+ throw argValueError({
127
+ funcName: 'CONCAT',
128
+ message: 'does not support object arguments',
129
+ hint: 'Use CAST to convert objects to strings first.',
130
+ })
109
131
  }
110
132
  return args.map(a => String(a)).join('')
111
133
  }
112
134
 
113
135
  if (funcName === 'LENGTH') {
114
- if (args.length !== 1) throw new Error('LENGTH requires exactly 1 argument')
136
+ if (args.length !== 1) throw argCountError('LENGTH', 1, args.length)
115
137
  const val = args[0]
116
138
  if (val == null) return null
117
139
  return String(val).length
@@ -119,21 +141,28 @@ export async function evaluateExpr({ node, row, tables }) {
119
141
 
120
142
  if (funcName === 'SUBSTRING' || funcName === 'SUBSTR') {
121
143
  if (args.length < 2 || args.length > 3) {
122
- throw new Error(`${funcName} requires 2 or 3 arguments`)
144
+ throw argCountError(funcName, '2 or 3', args.length)
123
145
  }
124
146
  const str = args[0]
125
147
  if (str == null) return null
126
148
  const strVal = String(str)
127
149
  const start = Number(args[1])
128
150
  if (!Number.isInteger(start) || start < 1) {
129
- throw new Error(`${funcName} start position must be a positive integer`)
151
+ throw argValueError({
152
+ funcName,
153
+ message: `start position must be a positive integer, got ${args[1]}`,
154
+ hint: 'SQL uses 1-based indexing.',
155
+ })
130
156
  }
131
157
  // SQL uses 1-based indexing
132
158
  const startIdx = start - 1
133
159
  if (args.length === 3) {
134
160
  const len = Number(args[2])
135
161
  if (!Number.isInteger(len) || len < 0) {
136
- throw new Error(`${funcName} length must be a non-negative integer`)
162
+ throw argValueError({
163
+ funcName,
164
+ message: `length must be a non-negative integer, got ${args[2]}`,
165
+ })
137
166
  }
138
167
  return strVal.substring(startIdx, startIdx + len)
139
168
  }
@@ -141,14 +170,14 @@ export async function evaluateExpr({ node, row, tables }) {
141
170
  }
142
171
 
143
172
  if (funcName === 'TRIM') {
144
- if (args.length !== 1) throw new Error('TRIM requires exactly 1 argument')
173
+ if (args.length !== 1) throw argCountError('TRIM', 1, args.length)
145
174
  const val = args[0]
146
175
  if (val == null) return null
147
176
  return String(val).trim()
148
177
  }
149
178
 
150
179
  if (funcName === 'REPLACE') {
151
- if (args.length !== 3) throw new Error('REPLACE requires exactly 3 arguments')
180
+ if (args.length !== 3) throw argCountError('REPLACE', 3, args.length)
152
181
  const str = args[0]
153
182
  const searchStr = args[1]
154
183
  const replaceStr = args[2]
@@ -158,13 +187,28 @@ export async function evaluateExpr({ node, row, tables }) {
158
187
  }
159
188
 
160
189
  if (funcName === 'RANDOM' || funcName === 'RAND') {
161
- if (args.length !== 0) throw new Error(`${funcName} takes no arguments`)
190
+ if (args.length !== 0) throw argCountError(funcName, 0, args.length)
162
191
  return Math.random()
163
192
  }
164
193
 
194
+ if (funcName === 'CURRENT_DATE') {
195
+ if (args.length !== 0) throw argCountError('CURRENT_DATE', 0, args.length)
196
+ return new Date().toISOString().split('T')[0]
197
+ }
198
+
199
+ if (funcName === 'CURRENT_TIME') {
200
+ if (args.length !== 0) throw argCountError('CURRENT_TIME', 0, args.length)
201
+ return new Date().toISOString().split('T')[1].replace('Z', '')
202
+ }
203
+
204
+ if (funcName === 'CURRENT_TIMESTAMP') {
205
+ if (args.length !== 0) throw argCountError('CURRENT_TIMESTAMP', 0, args.length)
206
+ return new Date().toISOString()
207
+ }
208
+
165
209
  if (funcName === 'JSON_OBJECT') {
166
210
  if (args.length % 2 !== 0) {
167
- throw new Error('JSON_OBJECT requires an even number of arguments (key-value pairs)')
211
+ throw argCountError('JSON_OBJECT', 'even number', args.length)
168
212
  }
169
213
  /** @type {Record<string, SqlPrimitive>} */
170
214
  const result = {}
@@ -172,7 +216,11 @@ export async function evaluateExpr({ node, row, tables }) {
172
216
  const key = args[i]
173
217
  const value = args[i + 1]
174
218
  if (key == null) {
175
- throw new Error('JSON_OBJECT: key cannot be null')
219
+ throw argValueError({
220
+ funcName: 'JSON_OBJECT',
221
+ message: 'key cannot be null',
222
+ hint: 'All keys must be non-null values.',
223
+ })
176
224
  }
177
225
  result[String(key)] = value
178
226
  }
@@ -180,7 +228,7 @@ export async function evaluateExpr({ node, row, tables }) {
180
228
  }
181
229
 
182
230
  if (funcName === 'JSON_VALUE' || funcName === 'JSON_QUERY') {
183
- if (args.length !== 2) throw new Error(`${funcName} requires exactly 2 arguments`)
231
+ if (args.length !== 2) throw argCountError(funcName, 2, args.length)
184
232
  let jsonArg = args[0]
185
233
  const pathArg = args[1]
186
234
  if (jsonArg == null || pathArg == null) return null
@@ -190,11 +238,18 @@ export async function evaluateExpr({ node, row, tables }) {
190
238
  try {
191
239
  jsonArg = JSON.parse(jsonArg)
192
240
  } catch {
193
- throw new Error(`${funcName}: invalid JSON string`)
241
+ throw argValueError({
242
+ funcName,
243
+ message: 'invalid JSON string',
244
+ hint: 'First argument must be valid JSON.',
245
+ })
194
246
  }
195
247
  }
196
- if (typeof jsonArg !== 'object') {
197
- throw new Error(`${funcName}: first argument must be JSON string or object`)
248
+ if (typeof jsonArg !== 'object' || jsonArg instanceof Date) {
249
+ throw argValueError({
250
+ funcName,
251
+ message: `first argument must be JSON string or object, got ${typeof jsonArg}`,
252
+ })
198
253
  }
199
254
 
200
255
  // Parse path ("$.foo.bar[0].baz" or "foo.bar[0]")
@@ -223,7 +278,7 @@ export async function evaluateExpr({ node, row, tables }) {
223
278
  return current
224
279
  }
225
280
 
226
- throw new Error('Unsupported function ' + funcName)
281
+ throw unknownFunctionError(funcName)
227
282
  }
228
283
 
229
284
  if (node.type === 'cast') {
@@ -235,7 +290,7 @@ export async function evaluateExpr({ node, row, tables }) {
235
290
  return String(val)
236
291
  }
237
292
  // Can only cast primitives to other primitive types
238
- if (typeof val === 'object') throw new Error(`Cannot CAST object to type ${node.toType}`)
293
+ if (typeof val === 'object') throw castError(node.toType, 'object')
239
294
  if (toType === 'INTEGER' || toType === 'INT') {
240
295
  const num = Number(val)
241
296
  if (isNaN(num)) return null
@@ -252,7 +307,7 @@ export async function evaluateExpr({ node, row, tables }) {
252
307
  if (toType === 'BOOLEAN' || toType === 'BOOL') {
253
308
  return Boolean(val)
254
309
  }
255
- throw new Error('Unsupported CAST to type ' + node.toType)
310
+ throw castError(node.toType)
256
311
  }
257
312
 
258
313
  // IN and NOT IN with value lists
@@ -317,5 +372,14 @@ export async function evaluateExpr({ node, row, tables }) {
317
372
  return null
318
373
  }
319
374
 
320
- throw new Error('Unknown expression node type ' + node.type)
375
+ // INTERVAL expressions should only appear as part of binary +/- operations
376
+ // which are handled above. A standalone interval is an error.
377
+ if (node.type === 'interval') {
378
+ throw invalidContextError({
379
+ item: 'INTERVAL',
380
+ validContext: 'date arithmetic (+ or -)',
381
+ })
382
+ }
383
+
384
+ throw new Error(`Unknown expression node type: ${node.type}. This is an internal error - the query may contain unsupported syntax.`)
321
385
  }
@@ -1,3 +1,4 @@
1
+ import { unknownFunctionError } from '../errors.js'
1
2
  import { isAggregateFunc } from '../validation.js'
2
3
  import { evaluateExpr } from './expression.js'
3
4
  import { applyBinaryOp } from './utils.js'
@@ -152,5 +153,5 @@ async function evaluateAggregateFunction(funcName, args, group, tables) {
152
153
  return max
153
154
  }
154
155
 
155
- throw new Error('Unsupported aggregate function: ' + funcName)
156
+ throw unknownFunctionError(funcName, undefined, 'COUNT, SUM, AVG, MIN, MAX')
156
157
  }
@@ -1,3 +1,4 @@
1
+ import { missingClauseError, tableNotFoundError } from '../errors.js'
1
2
  import { evaluateExpr } from './expression.js'
2
3
  import { stringify } from './utils.js'
3
4
 
@@ -22,7 +23,7 @@ export async function executeJoins(leftSource, joins, leftTableName, tables) {
22
23
  const join = joins[0]
23
24
  const rightSource = tables[join.table]
24
25
  if (rightSource === undefined) {
25
- throw new Error(`Table "${join.table}" not found`)
26
+ throw tableNotFoundError(join.table)
26
27
  }
27
28
 
28
29
  // Buffer right rows for hash index (required for hash join)
@@ -62,7 +63,7 @@ export async function executeJoins(leftSource, joins, leftTableName, tables) {
62
63
  const join = joins[i]
63
64
  const rightSource = tables[join.table]
64
65
  if (rightSource === undefined) {
65
- throw new Error(`Table "${join.table}" not found`)
66
+ throw tableNotFoundError(join.table)
66
67
  }
67
68
 
68
69
  /** @type {AsyncRow[]} */
@@ -98,7 +99,7 @@ export async function executeJoins(leftSource, joins, leftTableName, tables) {
98
99
  const lastJoin = joins[joins.length - 1]
99
100
  const rightSource = tables[lastJoin.table]
100
101
  if (rightSource === undefined) {
101
- throw new Error(`Table "${lastJoin.table}" not found`)
102
+ throw tableNotFoundError(lastJoin.table)
102
103
  }
103
104
 
104
105
  /** @type {AsyncRow[]} */
@@ -234,7 +235,10 @@ async function* hashJoin({ leftRows, rightRows, join, leftTable, rightTable, tab
234
235
  const { joinType, on: onCondition } = join
235
236
 
236
237
  if (!onCondition) {
237
- throw new Error('JOIN requires ON condition')
238
+ throw missingClauseError({
239
+ missing: 'ON condition',
240
+ context: 'JOIN',
241
+ })
238
242
  }
239
243
 
240
244
  const keys = extractJoinKeys(onCondition, leftTable, rightTable)
@@ -134,6 +134,9 @@ export function defaultDerivedAlias(expr) {
134
134
  if (expr.type === 'function') {
135
135
  return expr.name.toLowerCase() + '_' + expr.args.map(defaultDerivedAlias).join('_')
136
136
  }
137
+ if (expr.type === 'interval') {
138
+ return `interval_${expr.value}_${expr.unit.toLowerCase()}`
139
+ }
137
140
  return 'expr'
138
141
  }
139
142
 
@@ -1,3 +1,4 @@
1
+ import { syntaxError } from '../errors.js'
1
2
  import { isBinaryOp } from '../validation.js'
2
3
  import { parseAdditive, parseExpression, parseSubquery } from './expression.js'
3
4
  import { consume, current, expect, match, peekToken } from './state.js'
@@ -110,7 +111,7 @@ export function parseComparison(state) {
110
111
  // parseSubquery expects to consume the opening paren itself
111
112
  const parenTok = current(state)
112
113
  if (parenTok.type !== 'paren' || parenTok.value !== '(') {
113
- throw new Error('Expected ( after IN')
114
+ throw syntaxError({ expected: '(', received: `"${parenTok.value}"`, position: parenTok.position, after: 'IN' })
114
115
  }
115
116
  const peekTok = peekToken(state, 1)
116
117
  if (peekTok.type === 'keyword' && peekTok.value === 'SELECT') {
@@ -155,7 +156,7 @@ export function parseComparison(state) {
155
156
  // parseSubquery expects to consume the opening paren itself
156
157
  const parenTok = current(state)
157
158
  if (parenTok.type !== 'paren' || parenTok.value !== '(') {
158
- throw new Error('Expected ( after IN')
159
+ throw syntaxError({ expected: '(', received: `"${parenTok.value}"`, position: parenTok.position, after: 'IN' })
159
160
  }
160
161
  const peekTok = peekToken(state, 1)
161
162
  if (peekTok.type === 'keyword' && peekTok.value === 'SELECT') {
@@ -1,12 +1,66 @@
1
- import { isAggregateFunc, isStringFunc } from '../validation.js'
1
+ import {
2
+ invalidLiteralError,
3
+ missingClauseError,
4
+ syntaxError,
5
+ unknownFunctionError,
6
+ } from '../errors.js'
7
+ import { isAggregateFunc, isIntervalUnit, isStringFunc } from '../validation.js'
2
8
  import { parseComparison } from './comparison.js'
3
9
  import { parseSelectInternal } from './parse.js'
4
10
  import { consume, current, expect, expectIdentifier, match, peekToken } from './state.js'
5
11
 
6
12
  /**
7
- * @import { ExprNode, ParserState, SelectStatement, WhenClause } from '../types.js'
13
+ * @import { ExprNode, IntervalNode, ParserState, SelectStatement, WhenClause } from '../types.js'
8
14
  */
9
15
 
16
+ /**
17
+ * @param {ParserState} state
18
+ * @returns {IntervalNode}
19
+ */
20
+ function parseInterval(state) {
21
+ consume(state) // INTERVAL
22
+
23
+ // Handle optional negative sign
24
+ let sign = 1
25
+ const signTok = current(state)
26
+ if (signTok.type === 'operator' && signTok.value === '-') {
27
+ consume(state)
28
+ sign = -1
29
+ }
30
+
31
+ // Get value (number or quoted string)
32
+ const valueTok = current(state)
33
+ /** @type {number} */
34
+ let value
35
+ if (valueTok.type === 'number') {
36
+ consume(state)
37
+ value = sign * Number(valueTok.numericValue)
38
+ } else if (valueTok.type === 'string') {
39
+ consume(state)
40
+ const parsed = parseFloat(valueTok.value)
41
+ if (isNaN(parsed)) {
42
+ throw invalidLiteralError({ type: 'interval value', value: valueTok.value, position: valueTok.position })
43
+ }
44
+ value = sign * parsed
45
+ } else {
46
+ throw syntaxError({ expected: 'interval value (number)', received: `"${valueTok.value}"`, position: valueTok.position })
47
+ }
48
+
49
+ // Get unit keyword
50
+ const unitTok = current(state)
51
+ if (unitTok.type !== 'keyword' || !isIntervalUnit(unitTok.value)) {
52
+ throw invalidLiteralError({
53
+ type: 'interval unit',
54
+ value: unitTok.value,
55
+ position: unitTok.position,
56
+ validValues: 'DAY, MONTH, YEAR, HOUR, MINUTE, SECOND',
57
+ })
58
+ }
59
+ consume(state)
60
+
61
+ return { type: 'interval', value, unit: unitTok.value }
62
+ }
63
+
10
64
  /**
11
65
  * @param {ParserState} state
12
66
  * @returns {ExprNode}
@@ -64,7 +118,7 @@ export function parsePrimary(state) {
64
118
 
65
119
  // validate function names
66
120
  if (!isStringFunc(funcName) && !isAggregateFunc(funcName)) {
67
- throw new Error(`Unknown function "${funcName}" at position ${tok.position}`)
121
+ throw unknownFunctionError(funcName, tok.position)
68
122
  }
69
123
 
70
124
  consume(state) // function name
@@ -99,6 +153,17 @@ export function parsePrimary(state) {
99
153
  }
100
154
  }
101
155
 
156
+ // Niladic datetime functions (no parentheses required per ANSI SQL)
157
+ const niladicFuncs = ['CURRENT_DATE', 'CURRENT_TIME', 'CURRENT_TIMESTAMP']
158
+ if (niladicFuncs.includes(tok.value)) {
159
+ consume(state)
160
+ return {
161
+ type: 'function',
162
+ name: tok.value,
163
+ args: [],
164
+ }
165
+ }
166
+
102
167
  consume(state)
103
168
  let name = tok.value
104
169
 
@@ -175,7 +240,10 @@ export function parsePrimary(state) {
175
240
  }
176
241
 
177
242
  if (whenClauses.length === 0) {
178
- throw new Error('CASE expression must have at least one WHEN clause')
243
+ throw missingClauseError({
244
+ missing: 'at least one WHEN clause',
245
+ context: 'CASE expression',
246
+ })
179
247
  }
180
248
 
181
249
  // Parse optional ELSE clause
@@ -194,6 +262,9 @@ export function parsePrimary(state) {
194
262
  elseResult,
195
263
  }
196
264
  }
265
+ if (tok.value === 'INTERVAL') {
266
+ return parseInterval(state)
267
+ }
197
268
  }
198
269
 
199
270
  if (tok.type === 'operator' && tok.value === '-') {
@@ -207,7 +278,7 @@ export function parsePrimary(state) {
207
278
  }
208
279
 
209
280
  const found = tok.type === 'eof' ? 'end of query' : `"${tok.originalValue ?? tok.value}"`
210
- throw new Error(`Expected expression but found ${found} at position ${tok.position}`)
281
+ throw syntaxError({ expected: 'expression', received: found, position: tok.position })
211
282
  }
212
283
 
213
284
  /**
@@ -63,6 +63,11 @@ function parseSelectList(state) {
63
63
  return cols
64
64
  }
65
65
 
66
+ // Keywords that can start a valid expression in SELECT
67
+ const EXPRESSION_START_KEYWORDS = new Set([
68
+ 'CASE', 'TRUE', 'FALSE', 'NULL', 'EXISTS', 'NOT', 'INTERVAL',
69
+ ])
70
+
66
71
  /**
67
72
  * @param {ParserState} state
68
73
  * @returns {SelectColumn}
@@ -70,7 +75,7 @@ function parseSelectList(state) {
70
75
  function parseSelectItem(state) {
71
76
  const tok = current(state)
72
77
 
73
- if (tok.type === 'keyword' && tok.value !== 'CASE' || tok.type === 'eof') {
78
+ if (tok.type === 'keyword' && !EXPRESSION_START_KEYWORDS.has(tok.value) || tok.type === 'eof') {
74
79
  throw parseError(state, 'column name or expression')
75
80
  }
76
81
 
@@ -1,3 +1,5 @@
1
+ import { syntaxError } from '../errors.js'
2
+
1
3
  /**
2
4
  * @import { ParserState, Token, TokenType } from '../types.js'
3
5
  */
@@ -86,7 +88,7 @@ export function expectIdentifier(state) {
86
88
  export function parseError(state, expected) {
87
89
  const tok = current(state)
88
90
  const prevToken = state.tokens[state.pos - 1]
89
- const after = prevToken ? ` after "${prevToken.originalValue ?? prevToken.value}"` : ''
90
- const found = tok.type === 'eof' ? 'end of query' : `"${tok.originalValue ?? tok.value}"`
91
- return new Error(`Expected ${expected}${after} but found ${found} at position ${tok.position}`)
91
+ const after = prevToken ? prevToken.originalValue ?? prevToken.value : undefined
92
+ const received = tok.type === 'eof' ? 'end of query' : `"${tok.originalValue ?? tok.value}"`
93
+ return syntaxError({ expected, received, position: tok.position, after })
92
94
  }
@@ -1,3 +1,9 @@
1
+ import {
2
+ invalidLiteralError,
3
+ unexpectedCharError,
4
+ unterminatedError,
5
+ } from '../errors.js'
6
+
1
7
  /**
2
8
  * @import { Token } from '../types.d.ts'
3
9
  */
@@ -41,6 +47,13 @@ const KEYWORDS = new Set([
41
47
  'FULL',
42
48
  'OUTER',
43
49
  'ON',
50
+ 'INTERVAL',
51
+ 'DAY',
52
+ 'MONTH',
53
+ 'YEAR',
54
+ 'HOUR',
55
+ 'MINUTE',
56
+ 'SECOND',
44
57
  ])
45
58
 
46
59
  /**
@@ -71,6 +84,61 @@ export function tokenize(sql) {
71
84
  return ch
72
85
  }
73
86
 
87
+ /**
88
+ * @param {number} startPos
89
+ * @param {string} prefix
90
+ * @returns {Token}
91
+ */
92
+ function parseNumber(startPos, prefix = '') {
93
+ let text = prefix
94
+ while (isDigit(peek())) {
95
+ text += nextChar()
96
+ }
97
+ if (peek() === '.') {
98
+ text += nextChar()
99
+ while (isDigit(peek())) {
100
+ text += nextChar()
101
+ }
102
+ }
103
+ // exponent
104
+ if (peek() === 'e' || peek() === 'E') {
105
+ text += nextChar()
106
+ if (peek() === '+' || peek() === '-') {
107
+ text += nextChar()
108
+ }
109
+ while (isDigit(peek())) {
110
+ text += nextChar()
111
+ }
112
+ }
113
+ // bigint suffix
114
+ if (peek() === 'n') {
115
+ text += nextChar()
116
+ try {
117
+ return {
118
+ type: 'number',
119
+ value: text,
120
+ position: startPos,
121
+ numericValue: BigInt(text.slice(0, -1)),
122
+ }
123
+ } catch {
124
+ throw invalidLiteralError({ type: 'bigint', value: text.slice(0, -1), position: startPos })
125
+ }
126
+ }
127
+ if (isAlpha(peek())) {
128
+ throw invalidLiteralError({ type: 'number', value: text + peek(), position: startPos })
129
+ }
130
+ const num = parseFloat(text)
131
+ if (isNaN(num)) {
132
+ throw invalidLiteralError({ type: 'number', value: text, position: startPos })
133
+ }
134
+ return {
135
+ type: 'number',
136
+ value: text,
137
+ position: startPos,
138
+ numericValue: num,
139
+ }
140
+ }
141
+
74
142
  while (i < length) {
75
143
  const ch = peek()
76
144
 
@@ -102,41 +170,25 @@ export function tokenize(sql) {
102
170
 
103
171
  const pos = i
104
172
 
173
+ // negative numbers (when not subtraction)
174
+ if (ch === '-' && i + 1 < length && isDigit(sql[i + 1])) {
175
+ const lastToken = tokens[tokens.length - 1]
176
+ const isValueBefore = lastToken && (
177
+ lastToken.type === 'identifier' ||
178
+ lastToken.type === 'number' ||
179
+ lastToken.type === 'string' ||
180
+ lastToken.type === 'paren' && lastToken.value === ')'
181
+ )
182
+ if (!isValueBefore) {
183
+ nextChar() // consume '-'
184
+ tokens.push(parseNumber(pos, '-'))
185
+ continue
186
+ }
187
+ }
188
+
105
189
  // numbers
106
190
  if (isDigit(ch)) {
107
- let text = ''
108
- while (isDigit(peek())) {
109
- text += nextChar()
110
- }
111
- if (peek() === '.') {
112
- text += nextChar()
113
- while (isDigit(peek())) {
114
- text += nextChar()
115
- }
116
- }
117
- // exponent
118
- if (peek() === 'e' || peek() === 'E') {
119
- text += nextChar()
120
- if (peek() === '+' || peek() === '-') {
121
- text += nextChar()
122
- }
123
- while (isDigit(peek())) {
124
- text += nextChar()
125
- }
126
- }
127
- if (isAlpha(peek())) {
128
- throw new Error(`Invalid number at position ${pos}: ${text}${peek()}`)
129
- }
130
- const num = parseFloat(text)
131
- if (isNaN(num)) {
132
- throw new Error(`Invalid number at position ${pos}: ${text}`)
133
- }
134
- tokens.push({
135
- type: 'number',
136
- value: text,
137
- position: pos,
138
- numericValue: num,
139
- })
191
+ tokens.push(parseNumber(pos))
140
192
  continue
141
193
  }
142
194
 
@@ -170,7 +222,7 @@ export function tokenize(sql) {
170
222
  let text = ''
171
223
  while (i <= length) {
172
224
  if (i === length) {
173
- throw new Error(`Unterminated string literal starting at position ${pos}`)
225
+ throw unterminatedError('string', pos)
174
226
  }
175
227
  const c = nextChar()
176
228
  if (c === quote) {
@@ -198,7 +250,7 @@ export function tokenize(sql) {
198
250
  let text = ''
199
251
  while (i <= length) {
200
252
  if (i === length) {
201
- throw new Error(`Unterminated identifier starting at position ${pos}`)
253
+ throw unterminatedError('identifier', pos)
202
254
  }
203
255
  const c = nextChar()
204
256
  if (c === quote) {
@@ -288,9 +340,9 @@ export function tokenize(sql) {
288
340
  }
289
341
 
290
342
  if (tokens.length === 0) {
291
- throw new Error(`Expected SELECT but found "${ch}" at position ${pos}`)
343
+ throw unexpectedCharError(ch, pos, true)
292
344
  }
293
- throw new Error(`Unexpected character "${ch}" at position ${pos}`)
345
+ throw unexpectedCharError(ch, pos)
294
346
  }
295
347
 
296
348
  tokens.push({
package/src/types.d.ts CHANGED
@@ -27,7 +27,15 @@ export interface ExecuteSqlOptions {
27
27
  query: string
28
28
  }
29
29
 
30
- export type SqlPrimitive = string | number | bigint | boolean | SqlPrimitive[] | Record<string, any> | null
30
+ export type SqlPrimitive =
31
+ | string
32
+ | number
33
+ | bigint
34
+ | boolean
35
+ | Date
36
+ | null
37
+ | SqlPrimitive[]
38
+ | Record<string, any>
31
39
 
32
40
  export interface SelectStatement {
33
41
  distinct: boolean
@@ -129,6 +137,14 @@ export interface SubqueryNode {
129
137
  subquery: SelectStatement
130
138
  }
131
139
 
140
+ export type IntervalUnit = 'DAY' | 'MONTH' | 'YEAR' | 'HOUR' | 'MINUTE' | 'SECOND'
141
+
142
+ export interface IntervalNode {
143
+ type: 'interval'
144
+ value: number
145
+ unit: IntervalUnit
146
+ }
147
+
132
148
  export type ExprNode =
133
149
  | LiteralNode
134
150
  | IdentifierNode
@@ -141,6 +157,7 @@ export type ExprNode =
141
157
  | ExistsNode
142
158
  | CaseNode
143
159
  | SubqueryNode
160
+ | IntervalNode
144
161
 
145
162
  export interface StarColumn {
146
163
  kind: 'star'
@@ -162,6 +179,9 @@ export type StringFunc =
162
179
  | 'JSON_VALUE'
163
180
  | 'JSON_QUERY'
164
181
  | 'JSON_OBJECT'
182
+ | 'CURRENT_DATE'
183
+ | 'CURRENT_TIME'
184
+ | 'CURRENT_TIMESTAMP'
165
185
 
166
186
  export interface AggregateArgStar {
167
187
  kind: 'star'
@@ -227,6 +247,6 @@ export interface Token {
227
247
  type: TokenType
228
248
  value: string
229
249
  position: number
230
- numericValue?: number
250
+ numericValue?: number | bigint
231
251
  originalValue?: string
232
252
  }
package/src/validation.js CHANGED
@@ -1,6 +1,6 @@
1
1
 
2
2
  /**
3
- * @import {AggregateFunc, BinaryOp, ComparisonOp, StringFunc} from './types.js'
3
+ * @import {AggregateFunc, BinaryOp, ComparisonOp, IntervalUnit, StringFunc} from './types.js'
4
4
  * @param {string} name
5
5
  * @returns {name is AggregateFunc}
6
6
  */
@@ -8,6 +8,14 @@ export function isAggregateFunc(name) {
8
8
  return ['COUNT', 'SUM', 'AVG', 'MIN', 'MAX', 'JSON_ARRAYAGG'].includes(name)
9
9
  }
10
10
 
11
+ /**
12
+ * @param {string} name
13
+ * @returns {name is IntervalUnit}
14
+ */
15
+ export function isIntervalUnit(name) {
16
+ return ['DAY', 'MONTH', 'YEAR', 'HOUR', 'MINUTE', 'SECOND'].includes(name)
17
+ }
18
+
11
19
  /**
12
20
  * @param {string} name
13
21
  * @returns {name is StringFunc}
@@ -27,6 +35,9 @@ export function isStringFunc(name) {
27
35
  'JSON_VALUE',
28
36
  'JSON_QUERY',
29
37
  'JSON_OBJECT',
38
+ 'CURRENT_DATE',
39
+ 'CURRENT_TIME',
40
+ 'CURRENT_TIMESTAMP',
30
41
  ].includes(name)
31
42
  }
32
43