@uwdata/mosaic-sql 0.16.2 → 0.17.0

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.
Files changed (97) hide show
  1. package/package.json +5 -9
  2. package/src/ast/aggregate.js +50 -8
  3. package/src/ast/collate.js +33 -0
  4. package/src/ast/from.js +13 -4
  5. package/src/ast/node.js +13 -0
  6. package/src/ast/query.js +54 -22
  7. package/src/ast/sample.js +3 -3
  8. package/src/ast/subquery.js +23 -0
  9. package/src/ast/verbatim.js +8 -1
  10. package/src/ast/window-frame.js +107 -0
  11. package/src/ast/window.js +65 -99
  12. package/src/constants.js +5 -6
  13. package/src/functions/collate.js +16 -0
  14. package/src/functions/datetime.js +1 -11
  15. package/src/functions/interval.js +83 -0
  16. package/src/functions/literal.js +3 -2
  17. package/src/functions/window-frame.js +61 -0
  18. package/src/index.js +12 -4
  19. package/src/load/load.js +1 -1
  20. package/src/transforms/bin-date.js +2 -1
  21. package/src/transforms/filter-query.js +44 -0
  22. package/src/types.ts +9 -0
  23. package/src/visit/clone.js +53 -0
  24. package/src/visit/recurse.js +17 -5
  25. package/src/visit/rewrite.js +3 -11
  26. package/src/visit/walk.js +0 -1
  27. package/tsconfig.json +3 -7
  28. package/LICENSE +0 -47
  29. package/dist/types/ast/aggregate.d.ts +0 -71
  30. package/dist/types/ast/between-op.d.ts +0 -46
  31. package/dist/types/ast/binary-op.d.ts +0 -28
  32. package/dist/types/ast/case.d.ts +0 -67
  33. package/dist/types/ast/cast.d.ts +0 -21
  34. package/dist/types/ast/column-param.d.ts +0 -23
  35. package/dist/types/ast/column-ref.d.ts +0 -40
  36. package/dist/types/ast/fragment.d.ts +0 -14
  37. package/dist/types/ast/from.d.ts +0 -21
  38. package/dist/types/ast/function.d.ts +0 -21
  39. package/dist/types/ast/in-op.d.ts +0 -21
  40. package/dist/types/ast/interval.d.ts +0 -21
  41. package/dist/types/ast/literal.d.ts +0 -15
  42. package/dist/types/ast/logical-op.d.ts +0 -46
  43. package/dist/types/ast/node.d.ts +0 -24
  44. package/dist/types/ast/order-by.d.ts +0 -29
  45. package/dist/types/ast/param.d.ts +0 -20
  46. package/dist/types/ast/query.d.ts +0 -320
  47. package/dist/types/ast/sample.d.ts +0 -42
  48. package/dist/types/ast/select.d.ts +0 -22
  49. package/dist/types/ast/table-ref.d.ts +0 -25
  50. package/dist/types/ast/unary-op.d.ts +0 -39
  51. package/dist/types/ast/verbatim.d.ts +0 -9
  52. package/dist/types/ast/window.d.ts +0 -180
  53. package/dist/types/ast/with.d.ts +0 -32
  54. package/dist/types/constants.d.ts +0 -38
  55. package/dist/types/functions/aggregate.d.ts +0 -236
  56. package/dist/types/functions/case.d.ts +0 -13
  57. package/dist/types/functions/cast.d.ts +0 -26
  58. package/dist/types/functions/column.d.ts +0 -11
  59. package/dist/types/functions/cte.d.ts +0 -13
  60. package/dist/types/functions/datetime.d.ts +0 -45
  61. package/dist/types/functions/literal.d.ts +0 -16
  62. package/dist/types/functions/numeric.d.ts +0 -95
  63. package/dist/types/functions/operators.d.ts +0 -200
  64. package/dist/types/functions/order-by.d.ts +0 -18
  65. package/dist/types/functions/spatial.d.ts +0 -38
  66. package/dist/types/functions/sql-template-tag.d.ts +0 -15
  67. package/dist/types/functions/string.d.ts +0 -57
  68. package/dist/types/functions/table-ref.d.ts +0 -9
  69. package/dist/types/functions/util.d.ts +0 -8
  70. package/dist/types/functions/window.d.ts +0 -89
  71. package/dist/types/index-types.d.ts +0 -2
  72. package/dist/types/index.d.ts +0 -59
  73. package/dist/types/load/create.d.ts +0 -8
  74. package/dist/types/load/extension.d.ts +0 -1
  75. package/dist/types/load/load.d.ts +0 -12
  76. package/dist/types/load/sql-from.d.ts +0 -11
  77. package/dist/types/transforms/bin-1d.d.ts +0 -15
  78. package/dist/types/transforms/bin-2d.d.ts +0 -19
  79. package/dist/types/transforms/bin-date.d.ts +0 -44
  80. package/dist/types/transforms/bin-histogram.d.ts +0 -51
  81. package/dist/types/transforms/bin-linear-1d.d.ts +0 -12
  82. package/dist/types/transforms/bin-linear-2d.d.ts +0 -19
  83. package/dist/types/transforms/line-density.d.ts +0 -24
  84. package/dist/types/transforms/m4.d.ts +0 -21
  85. package/dist/types/transforms/scales.d.ts +0 -1
  86. package/dist/types/transforms/util/bin-step.d.ts +0 -61
  87. package/dist/types/transforms/util/time-interval.d.ts +0 -13
  88. package/dist/types/types.d.ts +0 -62
  89. package/dist/types/util/ast.d.ts +0 -61
  90. package/dist/types/util/function.d.ts +0 -56
  91. package/dist/types/util/string.d.ts +0 -3
  92. package/dist/types/util/type-check.d.ts +0 -22
  93. package/dist/types/visit/recurse.d.ts +0 -28
  94. package/dist/types/visit/rewrite.d.ts +0 -10
  95. package/dist/types/visit/visitors.d.ts +0 -34
  96. package/dist/types/visit/walk.d.ts +0 -10
  97. package/jsconfig.json +0 -11
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uwdata/mosaic-sql",
3
- "version": "0.16.2",
3
+ "version": "0.17.0",
4
4
  "description": "SQL query construction and analysis.",
5
5
  "keywords": [
6
6
  "sql",
@@ -11,7 +11,7 @@
11
11
  "author": "Jeffrey Heer (https://idl.uw.edu)",
12
12
  "type": "module",
13
13
  "exports": {
14
- "types": "./dist/types/index-types.d.ts",
14
+ "types": "./dist/src/index-types.d.ts",
15
15
  "default": "./src/index.js"
16
16
  },
17
17
  "repository": {
@@ -20,12 +20,8 @@
20
20
  },
21
21
  "scripts": {
22
22
  "prebuild": "rimraf dist && mkdir dist",
23
- "build": "npm run types",
24
- "types": "tsc",
25
23
  "lint": "eslint src test",
26
- "test": "vitest run && npm run tsc",
27
- "tsc": "tsc -p jsconfig.json",
28
- "prepublishOnly": "npm run test && npm run lint && npm run build"
29
- },
30
- "gitHead": "26d2719f4bcab471d2831145e1f03f39f3509869"
24
+ "test": "vitest run && tsc",
25
+ "prepublishOnly": "npm run test && npm run lint && tsc"
26
+ }
31
27
  }
@@ -1,8 +1,10 @@
1
1
  /**
2
- * @import { ExprVarArgs } from '../types.js'
2
+ * @import { ExprVarArgs, OrderByExpr } from '../types.js'
3
+ * @import { WindowFrameNode } from './window-frame.js'
3
4
  */
4
5
  import { AGGREGATE } from '../constants.js';
5
6
  import { asVerbatim } from '../util/ast.js';
7
+ import { exprList } from '../util/function.js';
6
8
  import { isString } from '../util/type-check.js';
7
9
  import { ExprNode } from './node.js';
8
10
  import { WindowNode } from './window.js';
@@ -14,8 +16,9 @@ export class AggregateNode extends ExprNode {
14
16
  * @param {ExprNode[]} args The aggregate function arguments.
15
17
  * @param {boolean} [distinct] The distinct flag.
16
18
  * @param {ExprNode} [filter] Filter expression.
19
+ * @param {OrderByExpr} [argOrder] Order by expression.
17
20
  */
18
- constructor(name, args, distinct, filter) {
21
+ constructor(name, args, distinct = false, filter = null, argOrder = []) {
19
22
  super(AGGREGATE);
20
23
  /**
21
24
  * The aggregate function name.
@@ -41,6 +44,12 @@ export class AggregateNode extends ExprNode {
41
44
  * @readonly
42
45
  */
43
46
  this.filter = filter;
47
+ /**
48
+ * Order by expression for order-sensitive aggregates.
49
+ * @type {ExprNode[]}
50
+ * @readonly
51
+ */
52
+ this.order = exprList([argOrder]);
44
53
  }
45
54
 
46
55
  /**
@@ -49,7 +58,7 @@ export class AggregateNode extends ExprNode {
49
58
  * @returns {AggregateNode} A new aggregate node.
50
59
  */
51
60
  distinct(isDistinct = true) {
52
- return new AggregateNode(this.name, this.args, isDistinct, this.filter);
61
+ return new AggregateNode(this.name, this.args, isDistinct, this.filter, this.order);
53
62
  }
54
63
 
55
64
  /**
@@ -59,7 +68,16 @@ export class AggregateNode extends ExprNode {
59
68
  */
60
69
  where(filter) {
61
70
  if (isString(filter)) filter = asVerbatim(filter);
62
- return new AggregateNode(this.name, this.args, this.isDistinct, filter);
71
+ return new AggregateNode(this.name, this.args, this.isDistinct, filter, this.order);
72
+ }
73
+
74
+ /**
75
+ * Return a new derived aggregate function that sorts values prior to aggregation.
76
+ * @param {OrderByExpr} order The order by expression.
77
+ * @returns {AggregateNode} A new aggregate node.
78
+ */
79
+ argOrder(order) {
80
+ return new AggregateNode(this.name, this.args, this.isDistinct, this.filter, order);
63
81
  }
64
82
 
65
83
  /**
@@ -88,19 +106,42 @@ export class AggregateNode extends ExprNode {
88
106
  return this.window().orderby(...expr);
89
107
  }
90
108
 
109
+ /**
110
+ * Return a new window function over this aggregate with the given frame.
111
+ * @param {WindowFrameNode} framedef The window frame definition.
112
+ * @returns {WindowNode} A new window node.
113
+ */
114
+ frame(framedef) {
115
+ return this.window().frame(framedef);
116
+ }
117
+
91
118
  /**
92
119
  * Generate a SQL query string for this node.
93
120
  * @returns {string}
94
121
  */
95
122
  toString() {
96
- const { name, args, isDistinct, filter } = this;
97
- const dist = isDistinct ? 'DISTINCT ' : '';
98
- const arg = args?.length ? args.join(', ') : '*';
123
+ const { name, args, isDistinct, filter, order } = this;
124
+ const arg = [
125
+ isDistinct ? 'DISTINCT' : '',
126
+ args?.length ? args.join(', ')
127
+ : name.toLowerCase() === 'count' ? '*'
128
+ : '',
129
+ order.length ? `ORDER BY ${order.join(', ')}` : ''
130
+ ].filter(x => x).join(' ');
99
131
  const filt = filter ? ` FILTER (WHERE ${filter})` : '';
100
- return `${name}(${dist}${arg})${filt}`;
132
+ return `${name}(${arg})${filt}`;
101
133
  }
102
134
  }
103
135
 
136
+ /**
137
+ * Check if a function name corresponds to an aggregate function.
138
+ * @param {string} name The function name to check
139
+ * @returns {boolean} True if a known aggregate function, false otherwise.
140
+ */
141
+ export function isAggregateFunction(name) {
142
+ return aggregateNames.includes(name.toLowerCase());
143
+ }
144
+
104
145
  /**
105
146
  * An array of known aggregate function names.
106
147
  * From https://duckdb.org/docs/sql/functions/aggregates.html.
@@ -124,6 +165,7 @@ export const aggregateNames = [
124
165
  'bool_or',
125
166
  'corr',
126
167
  'count',
168
+ 'count_star',
127
169
  'covar_pop',
128
170
  'covar_samp',
129
171
  'entropy',
@@ -0,0 +1,33 @@
1
+ import { COLLATE } from '../constants.js';
2
+ import { ExprNode } from './node.js';
3
+
4
+ export class CollateNode extends ExprNode {
5
+ /**
6
+ * Instantiate a collate node.
7
+ * @param {ExprNode} expr The expression to collate.
8
+ * @param {string} collation The collation type.
9
+ */
10
+ constructor(expr, collation) {
11
+ super(COLLATE);
12
+ /**
13
+ * The expression to collate.
14
+ * @type {ExprNode}
15
+ * @readonly
16
+ */
17
+ this.expr = expr;
18
+ /**
19
+ * The collation type.
20
+ * @type {string}
21
+ * @readonly
22
+ */
23
+ this.collation = collation;
24
+ }
25
+
26
+ /**
27
+ * Generate a SQL query string for this node.
28
+ * @returns {string}
29
+ */
30
+ toString() {
31
+ return `${this.expr} ${COLLATE} ${this.collation}`;
32
+ }
33
+ }
package/src/ast/from.js CHANGED
@@ -1,3 +1,4 @@
1
+ /** @import { SampleClauseNode } from './sample.js' */
1
2
  import { FROM_CLAUSE } from '../constants.js';
2
3
  import { quoteIdentifier } from '../util/string.js';
3
4
  import { SQLNode } from './node.js';
@@ -8,9 +9,10 @@ export class FromClauseNode extends SQLNode {
8
9
  /**
9
10
  * Instantiate a from node.
10
11
  * @param {SQLNode} expr The from expression.
11
- * @param {string} alias The output name.
12
+ * @param {string} [alias] The output name.
13
+ * @param {SampleClauseNode} [sample] The table sample.
12
14
  */
13
- constructor(expr, alias) {
15
+ constructor(expr, alias, sample) {
14
16
  super(FROM_CLAUSE);
15
17
  /**
16
18
  * The from expression.
@@ -24,6 +26,12 @@ export class FromClauseNode extends SQLNode {
24
26
  * @readonly
25
27
  */
26
28
  this.alias = alias;
29
+ /**
30
+ * The table sample.
31
+ * @type {SampleClauseNode}
32
+ * @readonly
33
+ */
34
+ this.sample = sample;
27
35
  }
28
36
 
29
37
  /**
@@ -31,10 +39,11 @@ export class FromClauseNode extends SQLNode {
31
39
  * @returns {string}
32
40
  */
33
41
  toString() {
34
- const { expr, alias } = this;
42
+ const { expr, alias, sample } = this;
35
43
  const ref = isQuery(expr) ? `(${expr})` : `${expr}`;
36
- return alias && !(isTableRef(expr) && expr.table.join('.') === alias)
44
+ const from = alias && !(isTableRef(expr) && expr.table.join('.') === alias)
37
45
  ? `${ref} AS ${quoteIdentifier(alias)}`
38
46
  : `${ref}`;
47
+ return `${from}${sample ? ` TABLESAMPLE ${sample}` : ''}`;
39
48
  }
40
49
  }
package/src/ast/node.js CHANGED
@@ -20,6 +20,19 @@ export class SQLNode {
20
20
  */
21
21
  this.type = type;
22
22
  }
23
+
24
+ /**
25
+ * Create a shallow clone of this SQL AST node.
26
+ * @returns {this} The shallow clone node.
27
+ */
28
+ clone() {
29
+ // @ts-expect-error
30
+ const clone = new this.constructor();
31
+ for (const key in this) {
32
+ clone[key] = this[key];
33
+ }
34
+ return clone;
35
+ }
23
36
  }
24
37
 
25
38
  /**
package/src/ast/query.js CHANGED
@@ -125,9 +125,11 @@ export class Query extends ExprNode {
125
125
  this._with = [];
126
126
  /** @type {ExprNode[]} */
127
127
  this._orderby = [];
128
- /** @type {number} */
128
+ /** @type {boolean} */
129
+ this._limitPerc = false;
130
+ /** @type {ExprNode} */
129
131
  this._limit = undefined;
130
- /** @type {number} */
132
+ /** @type {ExprNode} */
131
133
  this._offset = undefined;
132
134
  /** @type {Query | null} */
133
135
  this.cteFor = null;
@@ -143,12 +145,20 @@ export class Query extends ExprNode {
143
145
 
144
146
  /**
145
147
  * Clone this query.
146
- * @returns {Query}
148
+ * @returns {this}
147
149
  */
148
150
  clone() {
149
151
  return this;
150
152
  }
151
153
 
154
+ /**
155
+ * Add a pointer to the query for which this query is a CTE.
156
+ * @param {Query | null} query
157
+ */
158
+ setCteFor(query) {
159
+ this.cteFor = query;
160
+ }
161
+
152
162
  /**
153
163
  * Add WITH common table expressions (CTEs).
154
164
  * @param {...WithExpr} expr Expressions to add.
@@ -159,7 +169,7 @@ export class Query extends ExprNode {
159
169
  const list = [];
160
170
  const add = (name, q) => {
161
171
  const query = q.clone();
162
- query.cteFor = this;
172
+ query.setCteFor(this);
163
173
  list.push(new WithClauseNode(name, query));
164
174
  };
165
175
  expr.flat().forEach(e => {
@@ -180,23 +190,35 @@ export class Query extends ExprNode {
180
190
  return this;
181
191
  }
182
192
 
193
+ /**
194
+ * Set the query result LIMIT as a percentage value.
195
+ * @param {number | ExprNode} value The limit percentage value.
196
+ * @returns {this}
197
+ */
198
+ limitPercent(value) {
199
+ this._limitPerc = true;
200
+ this._limit = asNode(value);
201
+ return this;
202
+ }
203
+
183
204
  /**
184
205
  * Set the query result LIMIT.
185
- * @param {number} value The limit value.
206
+ * @param {number | ExprNode} value The limit value.
186
207
  * @returns {this}
187
208
  */
188
209
  limit(value) {
189
- this._limit = Number.isFinite(value) ? value : undefined;
210
+ this._limitPerc = false;
211
+ this._limit = asNode(value);
190
212
  return this;
191
213
  }
192
214
 
193
215
  /**
194
216
  * Set the query result OFFSET.
195
- * @param {number} value The offset value.
217
+ * @param {number | ExprNode} value The offset value.
196
218
  * @returns {this}
197
219
  */
198
220
  offset(value) {
199
- this._offset = Number.isFinite(value) ? value : undefined;
221
+ this._offset = asNode(value);
200
222
  return this;
201
223
  }
202
224
  }
@@ -251,7 +273,7 @@ export class SelectQuery extends Query {
251
273
 
252
274
  /**
253
275
  * Clone this query.
254
- * @returns {SelectQuery}
276
+ * @returns {this}
255
277
  */
256
278
  clone() {
257
279
  return Object.assign(new SelectQuery(), this);
@@ -278,7 +300,7 @@ export class SelectQuery extends Query {
278
300
 
279
301
  const keys = new Set(list.map(x => x.alias));
280
302
  this._select = this._select
281
- .filter(x => !keys.has(x.alias))
303
+ .filter(x => x.alias && !keys.has(x.alias))
282
304
  .concat(list.filter(x => x.expr));
283
305
  return this;
284
306
  }
@@ -313,6 +335,7 @@ export class SelectQuery extends Query {
313
335
  const add = (v, as) => list.push(new FromClauseNode(asTableRef(v), unquote(as)));
314
336
  expr.flat().forEach(e => {
315
337
  if (e == null) return;
338
+ else if (e instanceof FromClauseNode) list.push(e);
316
339
  else if (isString(e)) add(e, e);
317
340
  else if (isTableRef(e)) add(e, e.name);
318
341
  else if (isNode(e)) add(e);
@@ -438,7 +461,7 @@ export class SelectQuery extends Query {
438
461
  toString() {
439
462
  const {
440
463
  _with, _select, _distinct, _from, _sample, _where, _groupby,
441
- _having, _window, _qualify, _orderby, _limit, _offset
464
+ _having, _window, _qualify, _orderby, _limitPerc, _limit, _offset
442
465
  } = this;
443
466
  const sql = [];
444
467
 
@@ -484,10 +507,10 @@ export class SelectQuery extends Query {
484
507
  if (_orderby.length) sql.push(`ORDER BY ${_orderby.join(', ')}`);
485
508
 
486
509
  // LIMIT
487
- if (Number.isFinite(_limit)) sql.push(`LIMIT ${_limit}`);
510
+ if (_limit) sql.push(`LIMIT ${_limit}${_limitPerc ? '%' : ''}`);
488
511
 
489
512
  // OFFSET
490
- if (Number.isFinite(_offset)) sql.push(`OFFSET ${_offset}`);
513
+ if (_offset) sql.push(`OFFSET ${_offset}`);
491
514
 
492
515
  return sql.join(' ');
493
516
  }
@@ -504,9 +527,10 @@ export class DescribeQuery extends SQLNode {
504
527
 
505
528
  /**
506
529
  * Clone this describe query.
507
- * @returns {DescribeQuery}
530
+ * @returns {this}
508
531
  */
509
532
  clone() {
533
+ // @ts-expect-error
510
534
  return new DescribeQuery(this.query.clone());
511
535
  }
512
536
 
@@ -539,23 +563,31 @@ export class SetOperation extends Query {
539
563
  this.queries = queries;
540
564
  }
541
565
 
566
+ /**
567
+ * Add a pointer to the query for which this query is a CTE.
568
+ * @param {Query | null} query
569
+ */
570
+ setCteFor(query) {
571
+ super.setCteFor(query);
572
+ const { queries, cteFor } = this;
573
+ if (cteFor) queries.forEach(q => q.setCteFor(cteFor));
574
+ }
575
+
542
576
  /**
543
577
  * Return a list of subqueries.
544
578
  * @returns {Query[]}
545
579
  */
546
580
  get subqueries() {
547
- const { queries, cteFor } = this;
548
- // TODO: revisit this?
549
- if (cteFor) queries.forEach(q => q.cteFor = cteFor);
550
- return queries;
581
+ return this.queries;
551
582
  }
552
583
 
553
584
  /**
554
585
  * Clone this set operation.
555
- * @returns {SetOperation}
586
+ * @returns {this}
556
587
  */
557
588
  clone() {
558
589
  const { op, queries, ...rest } = this;
590
+ // @ts-expect-error
559
591
  return Object.assign(new SetOperation(op, queries), rest);
560
592
  }
561
593
 
@@ -564,7 +596,7 @@ export class SetOperation extends Query {
564
596
  * @returns {string}
565
597
  */
566
598
  toString() {
567
- const { op, queries, _with, _orderby, _limit, _offset } = this;
599
+ const { op, queries, _with, _orderby, _limitPerc, _limit, _offset } = this;
568
600
  const sql = [];
569
601
 
570
602
  // WITH
@@ -577,10 +609,10 @@ export class SetOperation extends Query {
577
609
  if (_orderby.length) sql.push(`ORDER BY ${_orderby.join(', ')}`);
578
610
 
579
611
  // LIMIT
580
- if (Number.isFinite(_limit)) sql.push(`LIMIT ${_limit}`);
612
+ if (_limit) sql.push(`LIMIT ${_limit}${_limitPerc ? '%' : ''}`);
581
613
 
582
614
  // OFFSET
583
- if (Number.isFinite(_offset)) sql.push(`OFFSET ${_offset}`);
615
+ if (_offset) sql.push(`OFFSET ${_offset}`);
584
616
 
585
617
  return sql.join(' ');
586
618
  }
package/src/ast/sample.js CHANGED
@@ -46,8 +46,8 @@ export class SampleClauseNode extends SQLNode {
46
46
 
47
47
  toString() {
48
48
  const { size, perc, method, seed } = this;
49
- const unit = perc ? '%' : ' ROWS';
50
- const s = seed != null ? `, ${seed}` : '';
51
- return `${size}${unit}${method ? ` (${method}${s})` : ''}`;
49
+ const m = method ? `${method} ` : '';
50
+ const s = seed != null ? ` REPEATABLE (${seed})` : '';
51
+ return `${m}(${size}${perc ? '%' : ' ROWS'})${s}`;
52
52
  }
53
53
  }
@@ -0,0 +1,23 @@
1
+ /** @import { Query } from './query.js' */
2
+ import { SCALAR_SUBQUERY } from '../constants.js';
3
+ import { ExprNode } from './node.js';
4
+
5
+ export class ScalarSubqueryNode extends ExprNode {
6
+ /**
7
+ * Instantiate a scalar subquery node.
8
+ * @param {Query} subquery The scalar subquery.
9
+ */
10
+ constructor(subquery) {
11
+ super(SCALAR_SUBQUERY);
12
+ /**
13
+ * The scalar subquery.
14
+ * @type {Query}
15
+ * @readonly
16
+ */
17
+ this.subquery = subquery;
18
+ }
19
+
20
+ toString() {
21
+ return `(${this.subquery})`;
22
+ }
23
+ }
@@ -5,8 +5,9 @@ export class VerbatimNode extends ExprNode {
5
5
  /**
6
6
  * Instantiate a raw node with verbatim content.
7
7
  * @param {string} value The verbatim content to include.
8
+ * @param {string} [hint] A type hint for analyzing verbatim content.
8
9
  */
9
- constructor(value) {
10
+ constructor(value, hint) {
10
11
  super(VERBATIM);
11
12
  /**
12
13
  * The verbatim content to include.
@@ -14,6 +15,12 @@ export class VerbatimNode extends ExprNode {
14
15
  * @readonly
15
16
  */
16
17
  this.value = value;
18
+ /**
19
+ * A type hint for analyzing verbatim content.
20
+ * @type {string}
21
+ * @readonly
22
+ */
23
+ this.hint = hint;
17
24
  }
18
25
 
19
26
  /**
@@ -0,0 +1,107 @@
1
+ /**
2
+ * @import { FrameExtent, FrameScope, FrameValue } from '../types.js'
3
+ */
4
+ import { WINDOW_EXTENT_EXPR, WINDOW_FRAME } from '../constants.js';
5
+ import { isParamLike } from '../util/type-check.js';
6
+ import { isNode, SQLNode } from './node.js';
7
+ import { ParamNode } from './param.js';
8
+
9
+ /**
10
+ * @typedef {ROWS | RANGE | GROUPS} FrameType
11
+ * @typedef {'NO OTHERS' | 'CURRENT ROW' | 'TIES' | 'GROUP'} FrameExclude
12
+ */
13
+
14
+ export const ROWS = 'ROWS';
15
+ export const RANGE = 'RANGE';
16
+ export const GROUPS = 'GROUPS';
17
+ export const PRECEDING = 'PRECEDING';
18
+ export const FOLLOWING = 'FOLLOWING';
19
+ export const CURRENT_ROW = 'CURRENT ROW';
20
+ export const UNBOUNDED = 'UNBOUNDED';
21
+
22
+ export class WindowFrameNode extends SQLNode {
23
+ /**
24
+ * Instantiate a window frame definition node.
25
+ * @param {FrameType} frameType The frame type, one of ROWS, RANGE, or GROUPS.
26
+ * @param {FrameExtent} extent The window frame extent.
27
+ * @param {FrameExclude} [exclude] The window frame exclusion criteria.
28
+ */
29
+ constructor(frameType, extent, exclude = undefined) {
30
+ super(WINDOW_FRAME);
31
+ /**
32
+ * The frame type, one of ROWS, RANGE, or GROUPS.
33
+ * @type {FrameType}
34
+ * @readonly
35
+ */
36
+ this.frameType = frameType;
37
+ /**
38
+ * The window frame extent.
39
+ * @type {[any, any] | ParamNode}
40
+ * @readonly
41
+ */
42
+ this.extent = isParamLike(extent) ? new ParamNode(extent) : extent;
43
+ /**
44
+ * The window frame exclusion criteria.
45
+ * @type {FrameExclude}
46
+ * @readonly
47
+ */
48
+ this.exclude = exclude;
49
+ }
50
+
51
+ /**
52
+ * Generate a SQL query string for this node.
53
+ * @returns {string}
54
+ */
55
+ toString() {
56
+ const { frameType, exclude, extent } = this;
57
+ const [prev, next] = isNode(extent) ? extent.value : extent;
58
+ const a = asFrameExpr(prev, PRECEDING);
59
+ const b = asFrameExpr(next, FOLLOWING);
60
+ return `${frameType} BETWEEN ${a} AND ${b}${exclude ? ` ${exclude}` : ''}`;
61
+ }
62
+ }
63
+
64
+ function asFrameExpr(value, scope) {
65
+ return value instanceof WindowFrameExprNode ? value
66
+ : value != null && typeof value !== 'number' ? `${value} ${scope}`
67
+ : value === 0 ? CURRENT_ROW
68
+ : !(value && Number.isFinite(value)) ? `${UNBOUNDED} ${scope}`
69
+ : `${Math.abs(value)} ${scope}`;
70
+ }
71
+
72
+ export class WindowFrameExprNode extends SQLNode {
73
+ /**
74
+ * Instantiate a window frame definition node.
75
+ * @param {FrameScope} scope The frame scope, one of PRECEDING, FOLLOWING, or CURRENT ROW.
76
+ * @param {FrameValue | null} [expr] The window frame extent expression.
77
+ */
78
+ constructor(scope, expr = null) {
79
+ super(WINDOW_EXTENT_EXPR);
80
+
81
+ /**
82
+ * The window frame extent.
83
+ * @type {FrameScope}
84
+ * @readonly
85
+ */
86
+ this.scope = scope;
87
+
88
+ /**
89
+ * The window frame extent expression. This value should be null
90
+ * in the case of current row or unbounded extent values.
91
+ * @type {FrameValue | null}
92
+ * @readonly
93
+ */
94
+ this.expr = expr;
95
+ }
96
+
97
+ /**
98
+ * Generate a SQL query string for this node.
99
+ * @returns {string}
100
+ */
101
+ toString() {
102
+ const { scope, expr } = this;
103
+ return scope === CURRENT_ROW
104
+ ? scope
105
+ : `${expr ?? UNBOUNDED} ${scope}`;
106
+ }
107
+ }