squirreling 0.15.1 → 0.15.2

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
@@ -146,7 +146,7 @@ Squirreling mostly follows the SQL standard. The following features are supporte
146
146
  - `JOIN` operations: `INNER JOIN`, `LEFT JOIN`, `RIGHT JOIN`, `FULL JOIN`, `CROSS JOIN`, `POSITIONAL JOIN`, `LATERAL VIEW [OUTER] EXPLODE(...)`, with `ON` or `USING (col, ...)` conditions
147
147
  - `GROUP BY` and `HAVING` clauses
148
148
  - Set operations: `UNION`, `UNION ALL`, `INTERSECT`, `INTERSECT ALL`, `EXCEPT`, `EXCEPT ALL`
149
- - Expressions: `CASE`, `CAST`, `BETWEEN`, `IN`, `LIKE`, `IS NULL`, `IS NOT NULL`, string concatenation `||`
149
+ - Expressions: `CASE`, `CAST`, `TRY_CAST`, `BETWEEN`, `IN`, `LIKE`, `IS NULL`, `IS NOT NULL`, string concatenation `||`
150
150
  - Subscript access: zero-based array indexing `col[0]`, struct field access `col['field']`, and chains like `col[0].field`
151
151
 
152
152
  ### Quoting
@@ -157,16 +157,16 @@ Squirreling mostly follows the SQL standard. The following features are supporte
157
157
 
158
158
  ### Functions
159
159
 
160
- - Aggregate: `COUNT`, `COUNTIF`, `SUM`, `AVG`, `MIN`, `MAX`, `MEDIAN`, `PERCENTILE_CONT`, `APPROX_QUANTILE`, `STDDEV_POP`, `STDDEV_SAMP`, `ARRAY_AGG`, `JSON_ARRAYAGG`, `STRING_AGG`
160
+ - Aggregate: `COUNT`, `COUNTIF`, `SUM`, `AVG`, `MIN`, `MAX`, `MIN_BY`, `MAX_BY`, `ANY_VALUE`, `MEDIAN`, `PERCENTILE_CONT`, `APPROX_QUANTILE`, `STDDEV_POP`, `STDDEV_SAMP`, `ARRAY_AGG`, `JSON_ARRAYAGG`, `STRING_AGG`
161
161
  - Window: `ROW_NUMBER`, `LAG`, `LEAD`
162
162
  - String: `CONCAT`, `SUBSTRING`, `REPLACE`, `LENGTH`, `OCTET_LENGTH`, `UPPER`, `LOWER`, `TRIM`, `LEFT`, `RIGHT`, `INSTR`, `POSITION`, `STRPOS`, `SPLIT_PART`, `STRING_SPLIT`
163
163
  - Math: `ABS`, `SIGN`, `CEIL`, `FLOOR`, `ROUND`, `MOD`, `RAND`, `RANDOM`, `LN`, `LOG10`, `EXP`, `POWER`, `SQRT`
164
164
  - Trig: `SIN`, `COS`, `TAN`, `COT`, `ASIN`, `ACOS`, `ATAN`, `ATAN2`, `DEGREES`, `RADIANS`, `PI`
165
165
  - Date: `CURRENT_DATE`, `CURRENT_TIME`, `CURRENT_TIMESTAMP`, `DATE_DIFF`, `DATEDIFF`, `DATE_PART`, `DATE_TRUNC`, `EPOCH`, `EXTRACT`, `INTERVAL`
166
- - Json: `JSON_VALUE`, `JSON_QUERY`, `JSON_EXTRACT`, `JSON_OBJECT`, `JSON_ARRAY_LENGTH`, `JSON_VALID`, `JSON_TYPE`, `JSON_KEYS`
166
+ - Json: `JSON_VALUE`, `JSON_QUERY`, `JSON_EXTRACT`, `JSON_EXTRACT_STRING`, `JSON_OBJECT`, `JSON_ARRAY_LENGTH`, `JSON_VALID`, `JSON_TYPE`, `JSON_KEYS`
167
167
  - Array: `ARRAY_LENGTH`, `ARRAY_POSITION`, `ARRAY_CONTAINS`, `ARRAY_SORT`, `ARRAY_APPEND`, `ARRAY_CONCAT`, `LEN`, `CARDINALITY`, `SIZE`
168
168
  - Table functions: `UNNEST`, `EXPLODE`, `JSON_EACH`
169
- - Regex: `REGEXP_SUBSTR`, `REGEXP_EXTRACT`, `REGEXP_REPLACE`, `REGEXP_MATCHES`
169
+ - Regex: `REGEXP_SUBSTR`, `REGEXP_EXTRACT`, `REGEXP_REPLACE`, `REGEXP_MATCHES`, `REGEXP_LIKE`
170
170
  - Spatial: `ST_GeomFromText`, `ST_MakeEnvelope`, `ST_AsText`, `ST_Intersects`, `ST_Contains`, `ST_ContainsProperly`, `ST_Within`, `ST_Overlaps`, `ST_Touches`, `ST_Equals`, `ST_Crosses`, `ST_Covers`, `ST_CoveredBy`, `ST_DWithin`
171
171
  - Conditional: `COALESCE`, `NULLIF`, `GREATEST`, `LEAST`
172
172
  - User-defined functions (UDFs)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "squirreling",
3
- "version": "0.15.1",
3
+ "version": "0.15.2",
4
4
  "description": "Squirreling Async SQL Engine",
5
5
  "author": "Hyperparam",
6
6
  "homepage": "https://hyperparam.app",
package/src/ast.d.ts CHANGED
@@ -120,6 +120,8 @@ export interface CastNode extends AstBase {
120
120
  type: 'cast'
121
121
  expr: ExprNode
122
122
  toType: CastType
123
+ /** TRY_CAST returns null instead of throwing when the value cannot be cast */
124
+ tryCast?: boolean
123
125
  }
124
126
 
125
127
  export interface InSubqueryNode extends AstBase {
@@ -289,6 +289,37 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
289
289
  if (funcName === 'MAX') return max
290
290
  }
291
291
 
292
+ if (funcName === 'MIN_BY' || funcName === 'ARG_MIN' || funcName === 'MAX_BY' || funcName === 'ARG_MAX') {
293
+ // Returns the value at the row with the smallest (or largest) key.
294
+ // Rows with a null value or key are ignored, and ties keep the first row.
295
+ const isMin = funcName === 'MIN_BY' || funcName === 'ARG_MIN'
296
+ const values = await evaluateAll(argNode, filteredRows, context)
297
+ const keys = await evaluateAll(node.args[1], filteredRows, context)
298
+ /** @type {SqlPrimitive} */
299
+ let best = null
300
+ /** @type {SqlPrimitive} */
301
+ let bestKey = null
302
+ for (let i = 0; i < values.length; i++) {
303
+ const value = values[i]
304
+ const key = keys[i]
305
+ if (value == null || key == null) continue
306
+ if (bestKey === null || (isMin ? key < bestKey : key > bestKey)) {
307
+ best = value
308
+ bestKey = key
309
+ }
310
+ }
311
+ return best
312
+ }
313
+
314
+ if (funcName === 'ANY_VALUE') {
315
+ // Returns an arbitrary non-null value from the group; we pick the first
316
+ const values = await evaluateAll(argNode, filteredRows, context)
317
+ for (const v of values) {
318
+ if (v != null) return v
319
+ }
320
+ return null
321
+ }
322
+
292
323
  if (funcName === 'STDDEV_SAMP' || funcName === 'STDDEV_POP') {
293
324
  const rawValues = await evaluateAll(argNode, filteredRows, context)
294
325
  let sum = 0
@@ -633,7 +664,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
633
664
  })
634
665
  }
635
666
 
636
- if (funcName === 'JSON_VALUE' || funcName === 'JSON_QUERY' || funcName === 'JSON_EXTRACT') {
667
+ if (funcName === 'JSON_VALUE' || funcName === 'JSON_QUERY' || funcName === 'JSON_EXTRACT' || funcName === 'JSON_EXTRACT_STRING') {
637
668
  let jsonArg = args[0]
638
669
  const pathArg = args[1]
639
670
  if (jsonArg == null || pathArg == null) return null
@@ -682,6 +713,11 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
682
713
  }
683
714
 
684
715
  if (current == null) return null
716
+ // JSON_EXTRACT_STRING returns text: unquoted scalars, JSON text for objects and arrays
717
+ if (funcName === 'JSON_EXTRACT_STRING') {
718
+ if (typeof current === 'object') return JSON.stringify(current)
719
+ return String(current)
720
+ }
685
721
  return current
686
722
  }
687
723
 
@@ -707,6 +743,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
707
743
  }
708
744
  // Can only cast primitives (and Dates) to other primitive types
709
745
  if (typeof val === 'object' && !(val instanceof Date)) {
746
+ if (node.tryCast) return null
710
747
  throw new ExecutionError({ message: `Cannot CAST object to ${toType}`, rowIndex, ...node })
711
748
  }
712
749
  if (toType === 'INTEGER' || toType === 'INT') {
@@ -717,7 +754,8 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
717
754
  if (toType === 'BIGINT') {
718
755
  if (typeof val === 'bigint') return val
719
756
  const num = Number(val)
720
- if (isNaN(num)) return null
757
+ // NaN and Infinity have no bigint representation
758
+ if (!isFinite(num)) return null
721
759
  return BigInt(Math.trunc(num))
722
760
  }
723
761
  if (toType === 'FLOAT' || toType === 'REAL' || toType === 'DOUBLE') {
@@ -78,7 +78,7 @@ export function evaluateRegexpFunc({ funcName, node, args, rowIndex }) {
78
78
  return null
79
79
  }
80
80
 
81
- if (funcName === 'REGEXP_MATCHES') {
81
+ if (funcName === 'REGEXP_MATCHES' || funcName === 'REGEXP_LIKE') {
82
82
  const str = args[0]
83
83
  const pattern = args[1]
84
84
  if (str == null || pattern == null) return null
@@ -125,8 +125,8 @@ function parsePrimaryBase(state) {
125
125
  const next = peekToken(state, 1)
126
126
  const funcNameUpper = tok.value.toUpperCase()
127
127
 
128
- // CAST(expr AS type)
129
- if (funcNameUpper === 'CAST' && next.type === 'paren' && next.value === '(') {
128
+ // CAST(expr AS type) and TRY_CAST(expr AS type)
129
+ if ((funcNameUpper === 'CAST' || funcNameUpper === 'TRY_CAST') && next.type === 'paren' && next.value === '(') {
130
130
  consume(state) // CAST
131
131
  consume(state) // '('
132
132
  const expr = parseExpression(state)
@@ -145,6 +145,7 @@ function parsePrimaryBase(state) {
145
145
  type: 'cast',
146
146
  expr,
147
147
  toType,
148
+ tryCast: funcNameUpper === 'TRY_CAST' ? true : undefined,
148
149
  positionStart,
149
150
  positionEnd: state.lastPos,
150
151
  }
package/src/types.d.ts CHANGED
@@ -137,9 +137,9 @@ export interface UserDefinedFunction {
137
137
  arguments: FunctionSignature
138
138
  }
139
139
 
140
- export type AggregateFunc = 'COUNT' | 'COUNTIF' | 'SUM' | 'AVG' | 'MIN' | 'MAX' | 'ARRAY_AGG' | 'LIST' | 'JSON_ARRAYAGG' | 'STDDEV_SAMP' | 'STDDEV_POP' | 'MEDIAN' | 'PERCENTILE_CONT' | 'APPROX_QUANTILE' | 'STRING_AGG'
140
+ export type AggregateFunc = 'COUNT' | 'COUNTIF' | 'SUM' | 'AVG' | 'MIN' | 'MAX' | 'MIN_BY' | 'ARG_MIN' | 'MAX_BY' | 'ARG_MAX' | 'ANY_VALUE' | 'ARRAY_AGG' | 'LIST' | 'JSON_ARRAYAGG' | 'STDDEV_SAMP' | 'STDDEV_POP' | 'MEDIAN' | 'PERCENTILE_CONT' | 'APPROX_QUANTILE' | 'STRING_AGG'
141
141
 
142
- export type RegExpFunction = 'REGEXP_SUBSTR' | 'REGEXP_EXTRACT' | 'REGEXP_REPLACE' | 'REGEXP_MATCHES'
142
+ export type RegExpFunction = 'REGEXP_SUBSTR' | 'REGEXP_EXTRACT' | 'REGEXP_REPLACE' | 'REGEXP_MATCHES' | 'REGEXP_LIKE'
143
143
 
144
144
  export type MathFunc =
145
145
  | 'FLOOR'
@@ -11,7 +11,7 @@ export const niladicFuncs = ['CURRENT_DATE', 'CURRENT_TIME', 'CURRENT_TIMESTAMP'
11
11
  * @returns {name is AggregateFunc}
12
12
  */
13
13
  export function isAggregateFunc(name) {
14
- return ['COUNT', 'COUNTIF', 'SUM', 'AVG', 'MIN', 'MAX', 'ARRAY_AGG', 'LIST', 'JSON_ARRAYAGG', 'STDDEV_SAMP', 'STDDEV_POP', 'MEDIAN', 'PERCENTILE_CONT', 'APPROX_QUANTILE', 'STRING_AGG'].includes(name)
14
+ return ['COUNT', 'COUNTIF', 'SUM', 'AVG', 'MIN', 'MAX', 'MIN_BY', 'ARG_MIN', 'MAX_BY', 'ARG_MAX', 'ANY_VALUE', 'ARRAY_AGG', 'LIST', 'JSON_ARRAYAGG', 'STDDEV_SAMP', 'STDDEV_POP', 'MEDIAN', 'PERCENTILE_CONT', 'APPROX_QUANTILE', 'STRING_AGG'].includes(name)
15
15
  }
16
16
 
17
17
  /**
@@ -39,7 +39,7 @@ export function isWindowFunc(name) {
39
39
  * @returns {name is RegExpFunction}
40
40
  */
41
41
  export function isRegexpFunc(name) {
42
- return ['REGEXP_SUBSTR', 'REGEXP_EXTRACT', 'REGEXP_REPLACE', 'REGEXP_MATCHES'].includes(name)
42
+ return ['REGEXP_SUBSTR', 'REGEXP_EXTRACT', 'REGEXP_REPLACE', 'REGEXP_MATCHES', 'REGEXP_LIKE'].includes(name)
43
43
  }
44
44
 
45
45
  /**
@@ -134,6 +134,7 @@ export const FUNCTION_SIGNATURES = {
134
134
  REGEXP_EXTRACT: { min: 2, max: 4, signature: 'string, pattern[, position[, occurrence]]' },
135
135
  REGEXP_REPLACE: { min: 3, max: 5, signature: 'string, pattern, replacement[, position[, occurrence]]' },
136
136
  REGEXP_MATCHES: { min: 2, max: 2, signature: 'string, pattern' },
137
+ REGEXP_LIKE: { min: 2, max: 2, signature: 'string, pattern' },
137
138
 
138
139
  // Date/time functions
139
140
  RANDOM: { min: 0, max: 0, signature: '' },
@@ -178,6 +179,7 @@ export const FUNCTION_SIGNATURES = {
178
179
  JSON_VALUE: { min: 2, max: 2, signature: 'expression, path' },
179
180
  JSON_QUERY: { min: 2, max: 2, signature: 'expression, path' },
180
181
  JSON_EXTRACT: { min: 2, max: 2, signature: 'expression, path' },
182
+ JSON_EXTRACT_STRING: { min: 2, max: 2, signature: 'expression, path' },
181
183
  JSON_OBJECT: { min: 0, signature: 'key1, value1[, ...]' },
182
184
  JSON_ARRAY_LENGTH: { min: 1, max: 1, signature: 'array' },
183
185
  JSON_VALID: { min: 1, max: 1, signature: 'value' },
@@ -221,6 +223,11 @@ export const FUNCTION_SIGNATURES = {
221
223
  AVG: { min: 1, max: 1, signature: 'expression' },
222
224
  MIN: { min: 1, max: 1, signature: 'expression' },
223
225
  MAX: { min: 1, max: 1, signature: 'expression' },
226
+ MIN_BY: { min: 2, max: 2, signature: 'value, key' },
227
+ ARG_MIN: { min: 2, max: 2, signature: 'value, key' },
228
+ MAX_BY: { min: 2, max: 2, signature: 'value, key' },
229
+ ARG_MAX: { min: 2, max: 2, signature: 'value, key' },
230
+ ANY_VALUE: { min: 1, max: 1, signature: 'expression' },
224
231
  STDDEV_SAMP: { min: 1, max: 1, signature: 'expression' },
225
232
  STDDEV_POP: { min: 1, max: 1, signature: 'expression' },
226
233
  MEDIAN: { min: 1, max: 1, signature: 'expression' },