squirreling 0.16.4 → 0.16.6

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.16.4",
3
+ "version": "0.16.6",
4
4
  "description": "Squirreling Async SQL Engine",
5
5
  "author": "Hyperparam",
6
6
  "homepage": "https://hyperparam.app",
@@ -40,12 +40,12 @@
40
40
  },
41
41
  "devDependencies": {
42
42
  "@eslint/js": "10.0.1",
43
- "@types/node": "26.4.1",
44
- "@vitest/coverage-v8": "5.0.0",
43
+ "@types/node": "26.5.0",
44
+ "@vitest/coverage-v8": "5.0.1",
45
45
  "eslint": "10.10.0",
46
- "eslint-plugin-jsdoc": "64.3.6",
46
+ "eslint-plugin-jsdoc": "64.5.2",
47
47
  "globals": "17.12.0",
48
48
  "typescript": "7.0.2",
49
- "vitest": "5.0.0"
49
+ "vitest": "5.0.1"
50
50
  }
51
51
  }
@@ -206,6 +206,31 @@ function compileValueKernel(node, state) {
206
206
  }
207
207
  }
208
208
 
209
+ if (node.type === 'in valuelist') {
210
+ // Later list entries are conditional reads. Keep those on the row path
211
+ // until they have a selection-aware evaluator, just like lazy functions.
212
+ if (node.values.some(readsIdentifier)) return undefined
213
+ const argument = compileValueKernel(node.expr, state)
214
+ if (!argument) return undefined
215
+ /** @type {ValueKernel[]} */
216
+ const values = []
217
+ for (const valueNode of node.values) {
218
+ const value = compileValueKernel(valueNode, state)
219
+ if (!value) return undefined
220
+ values.push(value)
221
+ }
222
+ return function inValue(vectors, rowIndex, streamRowIndex) {
223
+ const input = argument(vectors, rowIndex, streamRowIndex)
224
+ let sawNull = input == null
225
+ for (const value of values) {
226
+ const candidate = value(vectors, rowIndex, streamRowIndex)
227
+ if (candidate == null) sawNull = true
228
+ else if (input != null && sqlEquals(input, candidate)) return true
229
+ }
230
+ return sawNull ? null : false
231
+ }
232
+ }
233
+
209
234
  if (node.type === 'cast') {
210
235
  const argument = compileValueKernel(node.expr, state)
211
236
  if (!argument) return undefined
@@ -652,6 +677,7 @@ function readsIdentifier(node) {
652
677
  if (node.type === 'unary') return readsIdentifier(node.argument)
653
678
  if (node.type === 'binary') return readsIdentifier(node.left) || readsIdentifier(node.right)
654
679
  if (node.type === 'cast') return readsIdentifier(node.expr)
680
+ if (node.type === 'in valuelist') return readsIdentifier(node.expr) || node.values.some(readsIdentifier)
655
681
  if (node.type === 'function') return node.args.some(readsIdentifier)
656
682
  return false
657
683
  }
package/src/plan/plan.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { dataSourceColumns } from '../backend/dataSource.js'
1
2
  import { derivedAlias } from '../expression/alias.js'
2
3
  import { parseSql } from '../parse/parse.js'
3
4
  import { findAggregate } from '../validation/aggregates.js'
@@ -238,6 +239,7 @@ function planSelect({ select, ctePlans, cteColumns, tables, parentColumns, outer
238
239
  /** @type {QueryPlan} */
239
240
  let plan = planFrom({ select, ctePlans, cteColumns, hints, tables, outerScope })
240
241
  pruneAggregateColumns(plan, subqueryNeeds)
242
+ const directFrom = plan.type === 'Scan' && plan.hints === hints
241
243
 
242
244
  // Add JOINs
243
245
  if (select.joins.length) {
@@ -248,8 +250,13 @@ function planSelect({ select, ctePlans, cteColumns, tables, parentColumns, outer
248
250
  const isOwnScan = plan.type === 'Scan' && plan.hints === hints
249
251
 
250
252
  // Add WHERE filter when the scan didn't receive it
251
- if (select.where && !isOwnScan) {
252
- plan = { type: 'Filter', condition: select.where, child: plan }
253
+ let { where } = select
254
+ if (directFrom && select.joins.length === 1 && !outerScope?.length &&
255
+ !ctePlans?.has(select.joins[0].table?.toLowerCase())) {
256
+ where = pushJoinPredicates(plan, where, tables)
257
+ }
258
+ if (where && !isOwnScan) {
259
+ plan = { type: 'Filter', condition: where, child: plan }
253
260
  }
254
261
 
255
262
  if (useGrouping) {
@@ -826,6 +833,109 @@ function collectConjuncts(node, out) {
826
833
  out.push(node)
827
834
  }
828
835
 
836
+ /**
837
+ * Push total, table-local comparisons into fresh scans of a direct INNER
838
+ * equijoin. Scan execution already enforces hints a source cannot apply.
839
+ * Validate ON before moving anything; an unsafe ON also blocks WHERE
840
+ * pushdown. Safe ON terms can move even when WHERE must remain above the
841
+ * join, since ON already excludes those pairs before WHERE is evaluated.
842
+ *
843
+ * @param {QueryPlan} plan
844
+ * @param {ExprNode | undefined} where
845
+ * @param {Record<string, AsyncDataSource>} [tables]
846
+ * @returns {ExprNode | undefined}
847
+ */
848
+ function pushJoinPredicates(plan, where, tables) {
849
+ if (!tables || plan.type !== 'HashJoin' || plan.joinType !== 'INNER' ||
850
+ plan.left.type !== 'Scan' || plan.right.type !== 'Scan') return where
851
+ const scans = [plan.left, plan.right]
852
+ if (scans.some(scan => scan.hints.where || scan.hints.limit !== undefined || scan.hints.offset)) return where
853
+ const aliases = scans.map(scan => scan.alias ?? scan.table)
854
+ if (aliases[0] === aliases[1]) return where
855
+ const columns = scans.map(scan => dataSourceColumns(tables[scan.table]))
856
+ // A prefix that also names a struct column is not an unambiguous alias.
857
+ if (columns.some(names => aliases.some(alias => names.includes(alias)))) return where
858
+
859
+ /**
860
+ * @param {ExprNode} expr
861
+ * @returns {'left' | 'right' | 'literal' | 'unsafe'}
862
+ */
863
+ function owner(expr) {
864
+ if (expr.type === 'literal') return 'literal'
865
+ if (expr.type !== 'identifier' || !expr.prefix) return 'unsafe'
866
+ const index = aliases.indexOf(expr.prefix)
867
+ if (index < 0 || !columns[index].includes(expr.name)) return 'unsafe'
868
+ return index === 0 ? 'left' : 'right'
869
+ }
870
+
871
+ // Avoid changing the evaluation of unresolved or unqualified ON keys.
872
+ if (plan.leftKeys.some(key => owner(key) !== 'left') || plan.rightKeys.some(key => owner(key) !== 'right')) return where
873
+ /**
874
+ * @param {ExprNode[]} conjuncts
875
+ * @returns {Array<'left' | 'right' | 'residual'> | undefined}
876
+ */
877
+ function classify(conjuncts) {
878
+ /** @type {Array<'left' | 'right' | 'residual'>} */
879
+ const targets = []
880
+ for (const expr of conjuncts) {
881
+ if (expr.type !== 'binary' || !['=', '==', '!=', '<>', '<', '<=', '>', '>='].includes(expr.op)) return undefined
882
+ const left = owner(expr.left)
883
+ const right = owner(expr.right)
884
+ if (left === 'unsafe' || right === 'unsafe') return undefined
885
+ // Only column/literal terms move; cross-table comparisons remain residual.
886
+ targets.push(left !== 'literal' && right === 'literal' ? left : right !== 'literal' && left === 'literal' ? right : 'residual')
887
+ }
888
+ return targets
889
+ }
890
+
891
+ /**
892
+ * @param {ExprNode[]} conjuncts
893
+ * @param {Array<'left' | 'right' | 'residual'>} targets
894
+ * @returns {ExprNode | undefined}
895
+ */
896
+ function push(conjuncts, targets) {
897
+ /** @type {ExprNode | undefined} */
898
+ let residual
899
+ for (let i = 0; i < conjuncts.length; i++) {
900
+ const target = targets[i]
901
+ if (target === 'residual') {
902
+ residual = andPredicate(residual, conjuncts[i])
903
+ } else {
904
+ const scan = target === 'left' ? scans[0] : scans[1]
905
+ scan.hints = { ...scan.hints, where: andPredicate(scan.hints.where, conjuncts[i]) }
906
+ }
907
+ }
908
+ return residual
909
+ }
910
+
911
+ /** @type {ExprNode[]} */
912
+ const onConjuncts = []
913
+ if (plan.residual) collectConjuncts(plan.residual, onConjuncts)
914
+ const onTargets = classify(onConjuncts)
915
+ if (!onTargets) return where
916
+ const onResidual = push(onConjuncts, onTargets)
917
+ if (onResidual) plan.residual = onResidual
918
+ else delete plan.residual
919
+
920
+ if (!where) return undefined
921
+ /** @type {ExprNode[]} */
922
+ const whereConjuncts = []
923
+ collectConjuncts(where, whereConjuncts)
924
+ const whereTargets = classify(whereConjuncts)
925
+ return whereTargets ? push(whereConjuncts, whereTargets) : where
926
+ }
927
+
928
+ /**
929
+ * @param {ExprNode | undefined} left
930
+ * @param {ExprNode} right
931
+ * @returns {ExprNode}
932
+ */
933
+ function andPredicate(left, right) {
934
+ return left
935
+ ? { type: 'binary', op: 'AND', left, right, positionStart: left.positionStart, positionEnd: right.positionEnd }
936
+ : right
937
+ }
938
+
829
939
  /**
830
940
  * Returns the (leftKey, rightKey) pair for an equi conjunct, oriented so
831
941
  * leftKey references the left input and rightKey references the right input.