flowquery 1.0.69 → 1.0.70
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 +244 -1
- package/dist/compute/runner.d.ts +6 -0
- package/dist/compute/runner.d.ts.map +1 -1
- package/dist/compute/runner.js +26 -0
- package/dist/compute/runner.js.map +1 -1
- package/dist/flowquery.min.js +1 -1
- package/dist/graph/bindings.d.ts +78 -0
- package/dist/graph/bindings.d.ts.map +1 -0
- package/dist/graph/bindings.js +210 -0
- package/dist/graph/bindings.js.map +1 -0
- package/dist/graph/database.d.ts +4 -2
- package/dist/graph/database.d.ts.map +1 -1
- package/dist/graph/database.js +45 -9
- package/dist/graph/database.js.map +1 -1
- package/dist/graph/physical_node.d.ts +9 -0
- package/dist/graph/physical_node.d.ts.map +1 -1
- package/dist/graph/physical_node.js +70 -3
- package/dist/graph/physical_node.js.map +1 -1
- package/dist/graph/physical_relationship.d.ts +9 -0
- package/dist/graph/physical_relationship.d.ts.map +1 -1
- package/dist/graph/physical_relationship.js +70 -3
- package/dist/graph/physical_relationship.js.map +1 -1
- package/dist/parsing/ast_node.d.ts +9 -0
- package/dist/parsing/ast_node.d.ts.map +1 -1
- package/dist/parsing/ast_node.js +21 -4
- package/dist/parsing/ast_node.js.map +1 -1
- package/dist/parsing/expressions/binding_reference.d.ts +16 -0
- package/dist/parsing/expressions/binding_reference.d.ts.map +1 -0
- package/dist/parsing/expressions/binding_reference.js +34 -0
- package/dist/parsing/expressions/binding_reference.js.map +1 -0
- package/dist/parsing/operations/create_node.d.ts +5 -1
- package/dist/parsing/operations/create_node.d.ts.map +1 -1
- package/dist/parsing/operations/create_node.js +12 -2
- package/dist/parsing/operations/create_node.js.map +1 -1
- package/dist/parsing/operations/create_relationship.d.ts +5 -1
- package/dist/parsing/operations/create_relationship.d.ts.map +1 -1
- package/dist/parsing/operations/create_relationship.js +12 -2
- package/dist/parsing/operations/create_relationship.js.map +1 -1
- package/dist/parsing/operations/drop_binding.d.ts +15 -0
- package/dist/parsing/operations/drop_binding.d.ts.map +1 -0
- package/dist/parsing/operations/drop_binding.js +42 -0
- package/dist/parsing/operations/drop_binding.js.map +1 -0
- package/dist/parsing/operations/let.d.ts +36 -0
- package/dist/parsing/operations/let.d.ts.map +1 -0
- package/dist/parsing/operations/let.js +101 -0
- package/dist/parsing/operations/let.js.map +1 -0
- package/dist/parsing/operations/load.d.ts +11 -0
- package/dist/parsing/operations/load.d.ts.map +1 -1
- package/dist/parsing/operations/load.js +31 -2
- package/dist/parsing/operations/load.js.map +1 -1
- package/dist/parsing/operations/merge.d.ts +158 -0
- package/dist/parsing/operations/merge.d.ts.map +1 -0
- package/dist/parsing/operations/merge.js +338 -0
- package/dist/parsing/operations/merge.js.map +1 -0
- package/dist/parsing/operations/refresh_binding.d.ts +15 -0
- package/dist/parsing/operations/refresh_binding.d.ts.map +1 -0
- package/dist/parsing/operations/refresh_binding.js +42 -0
- package/dist/parsing/operations/refresh_binding.js.map +1 -0
- package/dist/parsing/operations/refresh_node.d.ts +11 -0
- package/dist/parsing/operations/refresh_node.d.ts.map +1 -0
- package/dist/parsing/operations/refresh_node.js +46 -0
- package/dist/parsing/operations/refresh_node.js.map +1 -0
- package/dist/parsing/operations/refresh_relationship.d.ts +11 -0
- package/dist/parsing/operations/refresh_relationship.d.ts.map +1 -0
- package/dist/parsing/operations/refresh_relationship.js +46 -0
- package/dist/parsing/operations/refresh_relationship.js.map +1 -0
- package/dist/parsing/operations/return.d.ts.map +1 -1
- package/dist/parsing/operations/return.js +7 -1
- package/dist/parsing/operations/return.js.map +1 -1
- package/dist/parsing/operations/update.d.ts +27 -0
- package/dist/parsing/operations/update.d.ts.map +1 -0
- package/dist/parsing/operations/update.js +88 -0
- package/dist/parsing/operations/update.js.map +1 -0
- package/dist/parsing/operations/update_delete.d.ts +43 -0
- package/dist/parsing/operations/update_delete.d.ts.map +1 -0
- package/dist/parsing/operations/update_delete.js +105 -0
- package/dist/parsing/operations/update_delete.js.map +1 -0
- package/dist/parsing/parser.d.ts +70 -1
- package/dist/parsing/parser.d.ts.map +1 -1
- package/dist/parsing/parser.js +704 -10
- package/dist/parsing/parser.js.map +1 -1
- package/dist/tokenization/keyword.d.ts +21 -1
- package/dist/tokenization/keyword.d.ts.map +1 -1
- package/dist/tokenization/keyword.js +20 -0
- package/dist/tokenization/keyword.js.map +1 -1
- package/dist/tokenization/token.d.ts +24 -0
- package/dist/tokenization/token.d.ts.map +1 -1
- package/dist/tokenization/token.js +73 -0
- package/dist/tokenization/token.js.map +1 -1
- package/package.json +1 -1
package/dist/parsing/parser.js
CHANGED
|
@@ -58,6 +58,7 @@ const key_value_pair_1 = __importDefault(require("./data_structures/key_value_pa
|
|
|
58
58
|
const list_comprehension_1 = __importDefault(require("./data_structures/list_comprehension"));
|
|
59
59
|
const lookup_1 = __importDefault(require("./data_structures/lookup"));
|
|
60
60
|
const range_lookup_1 = __importDefault(require("./data_structures/range_lookup"));
|
|
61
|
+
const binding_reference_1 = __importDefault(require("./expressions/binding_reference"));
|
|
61
62
|
const expression_1 = __importDefault(require("./expressions/expression"));
|
|
62
63
|
const f_string_1 = __importDefault(require("./expressions/f_string"));
|
|
63
64
|
const identifier_1 = __importDefault(require("./expressions/identifier"));
|
|
@@ -80,14 +81,22 @@ const create_node_1 = __importDefault(require("./operations/create_node"));
|
|
|
80
81
|
const create_relationship_1 = __importDefault(require("./operations/create_relationship"));
|
|
81
82
|
const delete_node_1 = __importDefault(require("./operations/delete_node"));
|
|
82
83
|
const delete_relationship_1 = __importDefault(require("./operations/delete_relationship"));
|
|
84
|
+
const drop_binding_1 = __importDefault(require("./operations/drop_binding"));
|
|
85
|
+
const let_1 = __importDefault(require("./operations/let"));
|
|
83
86
|
const limit_1 = __importDefault(require("./operations/limit"));
|
|
84
87
|
const load_1 = __importDefault(require("./operations/load"));
|
|
85
88
|
const match_1 = __importDefault(require("./operations/match"));
|
|
89
|
+
const merge_1 = __importStar(require("./operations/merge"));
|
|
86
90
|
const order_by_1 = __importDefault(require("./operations/order_by"));
|
|
91
|
+
const refresh_binding_1 = __importDefault(require("./operations/refresh_binding"));
|
|
92
|
+
const refresh_node_1 = __importDefault(require("./operations/refresh_node"));
|
|
93
|
+
const refresh_relationship_1 = __importDefault(require("./operations/refresh_relationship"));
|
|
87
94
|
const return_1 = __importDefault(require("./operations/return"));
|
|
88
95
|
const union_1 = __importDefault(require("./operations/union"));
|
|
89
96
|
const union_all_1 = __importDefault(require("./operations/union_all"));
|
|
90
97
|
const unwind_1 = __importDefault(require("./operations/unwind"));
|
|
98
|
+
const update_1 = __importDefault(require("./operations/update"));
|
|
99
|
+
const update_delete_1 = __importDefault(require("./operations/update_delete"));
|
|
91
100
|
const where_1 = __importDefault(require("./operations/where"));
|
|
92
101
|
const with_1 = __importDefault(require("./operations/with"));
|
|
93
102
|
const parser_state_1 = __importDefault(require("./parser_state"));
|
|
@@ -175,7 +184,9 @@ class Parser extends base_parser_1.default {
|
|
|
175
184
|
}
|
|
176
185
|
}
|
|
177
186
|
/**
|
|
178
|
-
* Validates that all operations in a statement are CREATE
|
|
187
|
+
* Validates that all operations in a statement are CREATE, DELETE,
|
|
188
|
+
* REFRESH, LET, or UPDATE — the declaration kinds that may precede the
|
|
189
|
+
* terminal statement in a multi-statement query.
|
|
179
190
|
*/
|
|
180
191
|
validateIsCreateOrDelete(root) {
|
|
181
192
|
let op = root.firstChild();
|
|
@@ -183,8 +194,16 @@ class Parser extends base_parser_1.default {
|
|
|
183
194
|
if (!(op instanceof create_node_1.default) &&
|
|
184
195
|
!(op instanceof create_relationship_1.default) &&
|
|
185
196
|
!(op instanceof delete_node_1.default) &&
|
|
186
|
-
!(op instanceof delete_relationship_1.default)
|
|
187
|
-
|
|
197
|
+
!(op instanceof delete_relationship_1.default) &&
|
|
198
|
+
!(op instanceof drop_binding_1.default) &&
|
|
199
|
+
!(op instanceof refresh_node_1.default) &&
|
|
200
|
+
!(op instanceof refresh_relationship_1.default) &&
|
|
201
|
+
!(op instanceof refresh_binding_1.default) &&
|
|
202
|
+
!(op instanceof let_1.default) &&
|
|
203
|
+
!(op instanceof update_1.default) &&
|
|
204
|
+
!(op instanceof update_delete_1.default) &&
|
|
205
|
+
!(op instanceof merge_1.default)) {
|
|
206
|
+
throw new Error("Only CREATE, DELETE, DROP, REFRESH, LET, UPDATE, and MERGE statements can appear before the last statement in a multi-statement query");
|
|
188
207
|
}
|
|
189
208
|
op = op.next;
|
|
190
209
|
}
|
|
@@ -198,6 +217,9 @@ class Parser extends base_parser_1.default {
|
|
|
198
217
|
if (this.token.isSemicolon()) {
|
|
199
218
|
break;
|
|
200
219
|
}
|
|
220
|
+
if (isSubQuery && this.token.isClosingBrace()) {
|
|
221
|
+
return root;
|
|
222
|
+
}
|
|
201
223
|
this.expectAndSkipWhitespaceAndComments();
|
|
202
224
|
}
|
|
203
225
|
else {
|
|
@@ -292,8 +314,16 @@ class Parser extends base_parser_1.default {
|
|
|
292
314
|
!(operation instanceof create_node_1.default) &&
|
|
293
315
|
!(operation instanceof create_relationship_1.default) &&
|
|
294
316
|
!(operation instanceof delete_node_1.default) &&
|
|
295
|
-
!(operation instanceof delete_relationship_1.default)
|
|
296
|
-
|
|
317
|
+
!(operation instanceof delete_relationship_1.default) &&
|
|
318
|
+
!(operation instanceof drop_binding_1.default) &&
|
|
319
|
+
!(operation instanceof refresh_node_1.default) &&
|
|
320
|
+
!(operation instanceof refresh_relationship_1.default) &&
|
|
321
|
+
!(operation instanceof refresh_binding_1.default) &&
|
|
322
|
+
!(operation instanceof let_1.default) &&
|
|
323
|
+
!(operation instanceof update_1.default) &&
|
|
324
|
+
!(operation instanceof update_delete_1.default) &&
|
|
325
|
+
!(operation instanceof merge_1.default)) {
|
|
326
|
+
throw new Error("Last statement must be a RETURN, WHERE, CALL, CREATE, DELETE, DROP, REFRESH, LET, UPDATE, or MERGE statement");
|
|
297
327
|
}
|
|
298
328
|
return root;
|
|
299
329
|
}
|
|
@@ -305,6 +335,10 @@ class Parser extends base_parser_1.default {
|
|
|
305
335
|
this.parseCall() ||
|
|
306
336
|
this.parseCreate() ||
|
|
307
337
|
this.parseDelete() ||
|
|
338
|
+
this.parseRefresh() ||
|
|
339
|
+
this.parseLet() ||
|
|
340
|
+
this.parseUpdate() ||
|
|
341
|
+
this.parseMerge() ||
|
|
308
342
|
this.parseMatch());
|
|
309
343
|
}
|
|
310
344
|
parseWith() {
|
|
@@ -434,6 +468,15 @@ class Parser extends base_parser_1.default {
|
|
|
434
468
|
if (expression === null) {
|
|
435
469
|
throw new Error("Expected expression or async function");
|
|
436
470
|
}
|
|
471
|
+
// A bare unresolved identifier in `LOAD JSON FROM <name>` is
|
|
472
|
+
// treated as a reference to a LET-bound value, resolved at
|
|
473
|
+
// execution time against the global Bindings store.
|
|
474
|
+
const inner = expression.firstChild();
|
|
475
|
+
if (inner instanceof reference_1.default &&
|
|
476
|
+
inner.referred === undefined &&
|
|
477
|
+
!inner.identifier.startsWith("$")) {
|
|
478
|
+
expression.replaceChild(inner, new binding_reference_1.default(inner.identifier));
|
|
479
|
+
}
|
|
437
480
|
from.addChild(expression);
|
|
438
481
|
}
|
|
439
482
|
this.expectAndSkipWhitespaceAndComments();
|
|
@@ -503,6 +546,12 @@ class Parser extends base_parser_1.default {
|
|
|
503
546
|
}
|
|
504
547
|
this.setNextToken();
|
|
505
548
|
this.expectAndSkipWhitespaceAndComments();
|
|
549
|
+
let isStatic = false;
|
|
550
|
+
if (this.token.isStatic()) {
|
|
551
|
+
isStatic = true;
|
|
552
|
+
this.setNextToken();
|
|
553
|
+
this.expectAndSkipWhitespaceAndComments();
|
|
554
|
+
}
|
|
506
555
|
if (!this.token.isVirtual()) {
|
|
507
556
|
throw new Error("Expected VIRTUAL");
|
|
508
557
|
}
|
|
@@ -554,24 +603,91 @@ class Parser extends base_parser_1.default {
|
|
|
554
603
|
if (query === null) {
|
|
555
604
|
throw new Error("Expected sub-query");
|
|
556
605
|
}
|
|
606
|
+
// Optional trailing REFRESH EVERY <n> <unit> clause. Only allowed
|
|
607
|
+
// for STATIC virtual entities (caching must be enabled to refresh).
|
|
608
|
+
// We deliberately peek past REFRESH to confirm EVERY follows: a bare
|
|
609
|
+
// `REFRESH VIRTUAL ...` is a separate top-level statement.
|
|
610
|
+
let refreshEveryMs = null;
|
|
611
|
+
const savedIndex = this.tokenIndex;
|
|
612
|
+
this.skipWhitespaceAndComments();
|
|
613
|
+
let consumedRefreshClause = false;
|
|
614
|
+
if (this.token.isRefresh()) {
|
|
615
|
+
// Look ahead past whitespace for EVERY.
|
|
616
|
+
const afterRefreshIndex = this.tokenIndex;
|
|
617
|
+
this.setNextToken();
|
|
618
|
+
this.skipWhitespaceAndComments();
|
|
619
|
+
if (this.token.isEvery()) {
|
|
620
|
+
if (!isStatic) {
|
|
621
|
+
throw new Error("REFRESH EVERY requires STATIC (caching must be enabled)");
|
|
622
|
+
}
|
|
623
|
+
this.setNextToken();
|
|
624
|
+
this.expectAndSkipWhitespaceAndComments();
|
|
625
|
+
if (!this.token.isNumber()) {
|
|
626
|
+
throw new Error("Expected number after REFRESH EVERY");
|
|
627
|
+
}
|
|
628
|
+
const amount = Number(this.token.value);
|
|
629
|
+
if (!Number.isFinite(amount) || amount <= 0) {
|
|
630
|
+
throw new Error("REFRESH EVERY interval must be a positive number");
|
|
631
|
+
}
|
|
632
|
+
this.setNextToken();
|
|
633
|
+
this.expectAndSkipWhitespaceAndComments();
|
|
634
|
+
const unit = (this.token.value || "").toUpperCase();
|
|
635
|
+
const unitMs = {
|
|
636
|
+
SECOND: 1000,
|
|
637
|
+
SECONDS: 1000,
|
|
638
|
+
MINUTE: 60000,
|
|
639
|
+
MINUTES: 60000,
|
|
640
|
+
HOUR: 3600000,
|
|
641
|
+
HOURS: 3600000,
|
|
642
|
+
DAY: 86400000,
|
|
643
|
+
DAYS: 86400000,
|
|
644
|
+
};
|
|
645
|
+
if (!(unit in unitMs)) {
|
|
646
|
+
throw new Error("Expected time unit (SECOND[S], MINUTE[S], HOUR[S], DAY[S]) after REFRESH EVERY interval");
|
|
647
|
+
}
|
|
648
|
+
refreshEveryMs = amount * unitMs[unit];
|
|
649
|
+
this.setNextToken();
|
|
650
|
+
consumedRefreshClause = true;
|
|
651
|
+
}
|
|
652
|
+
else {
|
|
653
|
+
// Not REFRESH EVERY — must be the start of a separate
|
|
654
|
+
// REFRESH VIRTUAL statement. Rewind to before REFRESH.
|
|
655
|
+
this.tokenIndex = afterRefreshIndex;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
if (!consumedRefreshClause) {
|
|
659
|
+
// No REFRESH clause present; restore token position so the
|
|
660
|
+
// outer loop sees the original whitespace/next-clause boundary.
|
|
661
|
+
this.tokenIndex = savedIndex;
|
|
662
|
+
}
|
|
557
663
|
let create;
|
|
558
664
|
if (relationship !== null) {
|
|
559
|
-
create = new create_relationship_1.default(relationship, query);
|
|
665
|
+
create = new create_relationship_1.default(relationship, query, isStatic, refreshEveryMs);
|
|
560
666
|
}
|
|
561
667
|
else {
|
|
562
|
-
create = new create_node_1.default(node, query);
|
|
668
|
+
create = new create_node_1.default(node, query, isStatic, refreshEveryMs);
|
|
563
669
|
}
|
|
564
670
|
return create;
|
|
565
671
|
}
|
|
566
672
|
parseDelete() {
|
|
567
673
|
var _a;
|
|
568
|
-
if (!this.token.isDelete()) {
|
|
674
|
+
if (!this.token.isDelete() && !this.token.isDrop()) {
|
|
569
675
|
return null;
|
|
570
676
|
}
|
|
571
677
|
this.setNextToken();
|
|
572
678
|
this.expectAndSkipWhitespaceAndComments();
|
|
679
|
+
if (this.token.isBinding()) {
|
|
680
|
+
this.setNextToken();
|
|
681
|
+
this.expectAndSkipWhitespaceAndComments();
|
|
682
|
+
if (!this.token.isIdentifierOrKeyword()) {
|
|
683
|
+
throw new Error("Expected binding name");
|
|
684
|
+
}
|
|
685
|
+
const name = this.token.value || "";
|
|
686
|
+
this.setNextToken();
|
|
687
|
+
return new drop_binding_1.default(name);
|
|
688
|
+
}
|
|
573
689
|
if (!this.token.isVirtual()) {
|
|
574
|
-
throw new Error("Expected VIRTUAL");
|
|
690
|
+
throw new Error("Expected VIRTUAL or BINDING");
|
|
575
691
|
}
|
|
576
692
|
this.setNextToken();
|
|
577
693
|
this.expectAndSkipWhitespaceAndComments();
|
|
@@ -618,6 +734,584 @@ class Parser extends base_parser_1.default {
|
|
|
618
734
|
}
|
|
619
735
|
return result;
|
|
620
736
|
}
|
|
737
|
+
parseRefresh() {
|
|
738
|
+
var _a;
|
|
739
|
+
if (!this.token.isRefresh()) {
|
|
740
|
+
return null;
|
|
741
|
+
}
|
|
742
|
+
this.setNextToken();
|
|
743
|
+
this.expectAndSkipWhitespaceAndComments();
|
|
744
|
+
if (this.token.isBinding()) {
|
|
745
|
+
this.setNextToken();
|
|
746
|
+
this.expectAndSkipWhitespaceAndComments();
|
|
747
|
+
if (!this.token.isIdentifierOrKeyword()) {
|
|
748
|
+
throw new Error("Expected binding name");
|
|
749
|
+
}
|
|
750
|
+
const name = this.token.value || "";
|
|
751
|
+
this.setNextToken();
|
|
752
|
+
return new refresh_binding_1.default(name);
|
|
753
|
+
}
|
|
754
|
+
if (!this.token.isVirtual()) {
|
|
755
|
+
throw new Error("Expected VIRTUAL or BINDING");
|
|
756
|
+
}
|
|
757
|
+
this.setNextToken();
|
|
758
|
+
this.expectAndSkipWhitespaceAndComments();
|
|
759
|
+
const node = this.parseNode();
|
|
760
|
+
if (node === null) {
|
|
761
|
+
throw new Error("Expected node definition");
|
|
762
|
+
}
|
|
763
|
+
let relationship = null;
|
|
764
|
+
if (this.token.isSubtract() && ((_a = this.peek()) === null || _a === void 0 ? void 0 : _a.isOpeningBracket())) {
|
|
765
|
+
this.setNextToken();
|
|
766
|
+
this.setNextToken();
|
|
767
|
+
if (!this.token.isColon()) {
|
|
768
|
+
throw new Error("Expected ':' for relationship type");
|
|
769
|
+
}
|
|
770
|
+
this.setNextToken();
|
|
771
|
+
if (!this.token.isIdentifierOrKeyword()) {
|
|
772
|
+
throw new Error("Expected relationship type identifier");
|
|
773
|
+
}
|
|
774
|
+
const type = this.token.value || "";
|
|
775
|
+
this.setNextToken();
|
|
776
|
+
if (!this.token.isClosingBracket()) {
|
|
777
|
+
throw new Error("Expected closing bracket for relationship definition");
|
|
778
|
+
}
|
|
779
|
+
this.setNextToken();
|
|
780
|
+
if (!this.token.isSubtract()) {
|
|
781
|
+
throw new Error("Expected '-' for relationship definition");
|
|
782
|
+
}
|
|
783
|
+
this.setNextToken();
|
|
784
|
+
const target = this.parseNode();
|
|
785
|
+
if (target === null) {
|
|
786
|
+
throw new Error("Expected target node definition");
|
|
787
|
+
}
|
|
788
|
+
relationship = new relationship_1.default();
|
|
789
|
+
relationship.type = type;
|
|
790
|
+
relationship.source = node;
|
|
791
|
+
relationship.target = target;
|
|
792
|
+
}
|
|
793
|
+
if (relationship !== null) {
|
|
794
|
+
return new refresh_relationship_1.default(relationship);
|
|
795
|
+
}
|
|
796
|
+
return new refresh_node_1.default(node);
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* Returns true when the current token is a clause keyword that
|
|
800
|
+
* starts a query pipeline (the right-hand side of a `LET name =`
|
|
801
|
+
* or `UPDATE name =`). Used to disambiguate between binding a
|
|
802
|
+
* plain expression and binding the rows produced by a sub-query.
|
|
803
|
+
*/
|
|
804
|
+
looksLikePipelineStart() {
|
|
805
|
+
return (this.token.isWith() ||
|
|
806
|
+
this.token.isUnwind() ||
|
|
807
|
+
this.token.isLoad() ||
|
|
808
|
+
this.token.isCall() ||
|
|
809
|
+
this.token.isMatch() ||
|
|
810
|
+
this.token.isOptional() ||
|
|
811
|
+
this.token.isReturn());
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* Peeks past `{` to decide whether the brace opens a sub-query
|
|
815
|
+
* (first significant token is a clause keyword) or a map literal
|
|
816
|
+
* (anything else, e.g. identifier+colon or string key).
|
|
817
|
+
*/
|
|
818
|
+
braceOpensSubQuery() {
|
|
819
|
+
if (!this.token.isOpeningBrace()) {
|
|
820
|
+
return false;
|
|
821
|
+
}
|
|
822
|
+
const savedIndex = this.tokenIndex;
|
|
823
|
+
this.setNextToken();
|
|
824
|
+
this.skipWhitespaceAndComments();
|
|
825
|
+
const result = this.token.isWith() ||
|
|
826
|
+
this.token.isUnwind() ||
|
|
827
|
+
this.token.isLoad() ||
|
|
828
|
+
this.token.isCall() ||
|
|
829
|
+
this.token.isMatch() ||
|
|
830
|
+
this.token.isOptional() ||
|
|
831
|
+
this.token.isReturn();
|
|
832
|
+
this.tokenIndex = savedIndex;
|
|
833
|
+
return result;
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* Parses the right-hand side of a `LET` / `UPDATE`. Returns the
|
|
837
|
+
* expression *or* the sub-query AST that was parsed.
|
|
838
|
+
*/
|
|
839
|
+
parseLetUpdateRhs() {
|
|
840
|
+
this.expectAndSkipWhitespaceAndComments();
|
|
841
|
+
// Explicit braces wrap a sub-query only when followed by a
|
|
842
|
+
// clause keyword; otherwise `{ ... }` is a map literal.
|
|
843
|
+
if (this.braceOpensSubQuery()) {
|
|
844
|
+
const subQuery = this.parseSubQuery();
|
|
845
|
+
if (subQuery === null) {
|
|
846
|
+
throw new Error("Expected sub-query");
|
|
847
|
+
}
|
|
848
|
+
return { expression: null, subQuery };
|
|
849
|
+
}
|
|
850
|
+
if (this.looksLikePipelineStart()) {
|
|
851
|
+
// Inline sub-query: parse a pipeline up to ; / EOF.
|
|
852
|
+
const savedState = this._state;
|
|
853
|
+
this._state = new parser_state_1.default();
|
|
854
|
+
const subQuery = this._parseTokenized(true);
|
|
855
|
+
this._state = savedState;
|
|
856
|
+
if (subQuery.firstChild() === null) {
|
|
857
|
+
throw new Error("Expected expression or sub-query");
|
|
858
|
+
}
|
|
859
|
+
return { expression: null, subQuery };
|
|
860
|
+
}
|
|
861
|
+
const expression = this.parseExpression();
|
|
862
|
+
if (expression === null) {
|
|
863
|
+
throw new Error("Expected expression or sub-query");
|
|
864
|
+
}
|
|
865
|
+
return { expression, subQuery: null };
|
|
866
|
+
}
|
|
867
|
+
parseLet() {
|
|
868
|
+
if (!this.token.isLet()) {
|
|
869
|
+
return null;
|
|
870
|
+
}
|
|
871
|
+
this.setNextToken();
|
|
872
|
+
this.expectAndSkipWhitespaceAndComments();
|
|
873
|
+
if (!this.token.isIdentifierOrKeyword() || this.token.value === null) {
|
|
874
|
+
throw new Error("Expected identifier after LET");
|
|
875
|
+
}
|
|
876
|
+
const name = this.token.value;
|
|
877
|
+
this.setNextToken();
|
|
878
|
+
this.skipWhitespaceAndComments();
|
|
879
|
+
if (!this.token.isEquals()) {
|
|
880
|
+
throw new Error("Expected '=' after LET identifier");
|
|
881
|
+
}
|
|
882
|
+
this.setNextToken();
|
|
883
|
+
const { expression, subQuery } = this.parseLetUpdateRhs();
|
|
884
|
+
// Optional trailing REFRESH EVERY <n> <unit> clause turns the
|
|
885
|
+
// binding into a refreshable one. Requires a sub-query RHS
|
|
886
|
+
// (there is nothing to re-evaluate for a literal expression).
|
|
887
|
+
// Peek past REFRESH to confirm EVERY follows: a bare
|
|
888
|
+
// `REFRESH BINDING` is a separate top-level statement.
|
|
889
|
+
let refreshEveryMs = null;
|
|
890
|
+
const savedIndex = this.tokenIndex;
|
|
891
|
+
this.skipWhitespaceAndComments();
|
|
892
|
+
let consumedRefreshClause = false;
|
|
893
|
+
if (this.token.isRefresh()) {
|
|
894
|
+
const afterRefreshIndex = this.tokenIndex;
|
|
895
|
+
this.setNextToken();
|
|
896
|
+
this.skipWhitespaceAndComments();
|
|
897
|
+
if (this.token.isEvery()) {
|
|
898
|
+
if (subQuery === null) {
|
|
899
|
+
throw new Error("LET REFRESH EVERY requires a sub-query right-hand side");
|
|
900
|
+
}
|
|
901
|
+
this.setNextToken();
|
|
902
|
+
this.expectAndSkipWhitespaceAndComments();
|
|
903
|
+
if (!this.token.isNumber()) {
|
|
904
|
+
throw new Error("Expected number after REFRESH EVERY");
|
|
905
|
+
}
|
|
906
|
+
const amount = Number(this.token.value);
|
|
907
|
+
if (!Number.isFinite(amount) || amount <= 0) {
|
|
908
|
+
throw new Error("REFRESH EVERY interval must be a positive number");
|
|
909
|
+
}
|
|
910
|
+
this.setNextToken();
|
|
911
|
+
this.expectAndSkipWhitespaceAndComments();
|
|
912
|
+
const unit = (this.token.value || "").toUpperCase();
|
|
913
|
+
const unitMs = {
|
|
914
|
+
SECOND: 1000,
|
|
915
|
+
SECONDS: 1000,
|
|
916
|
+
MINUTE: 60000,
|
|
917
|
+
MINUTES: 60000,
|
|
918
|
+
HOUR: 3600000,
|
|
919
|
+
HOURS: 3600000,
|
|
920
|
+
DAY: 86400000,
|
|
921
|
+
DAYS: 86400000,
|
|
922
|
+
};
|
|
923
|
+
if (!(unit in unitMs)) {
|
|
924
|
+
throw new Error("Expected time unit (SECOND[S], MINUTE[S], HOUR[S], DAY[S]) after REFRESH EVERY interval");
|
|
925
|
+
}
|
|
926
|
+
refreshEveryMs = amount * unitMs[unit];
|
|
927
|
+
this.setNextToken();
|
|
928
|
+
consumedRefreshClause = true;
|
|
929
|
+
}
|
|
930
|
+
else {
|
|
931
|
+
// Not REFRESH EVERY; must be the start of a separate
|
|
932
|
+
// REFRESH statement. Rewind to before REFRESH.
|
|
933
|
+
this.tokenIndex = afterRefreshIndex;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
if (!consumedRefreshClause) {
|
|
937
|
+
this.tokenIndex = savedIndex;
|
|
938
|
+
}
|
|
939
|
+
return new let_1.default(name, expression, subQuery, refreshEveryMs);
|
|
940
|
+
}
|
|
941
|
+
parseUpdate() {
|
|
942
|
+
if (!this.token.isUpdate()) {
|
|
943
|
+
return null;
|
|
944
|
+
}
|
|
945
|
+
this.setNextToken();
|
|
946
|
+
this.expectAndSkipWhitespaceAndComments();
|
|
947
|
+
if (!this.token.isIdentifierOrKeyword() || this.token.value === null) {
|
|
948
|
+
throw new Error("Expected identifier after UPDATE");
|
|
949
|
+
}
|
|
950
|
+
const name = this.token.value;
|
|
951
|
+
this.setNextToken();
|
|
952
|
+
this.skipWhitespaceAndComments();
|
|
953
|
+
// `UPDATE name AS alias DELETE WHERE <pred>` — row-filtering branch.
|
|
954
|
+
if (this.token.isAs()) {
|
|
955
|
+
return this.parseUpdateDeleteTail(name);
|
|
956
|
+
}
|
|
957
|
+
// `UPDATE name = <rhs>` — assign branch.
|
|
958
|
+
if (!this.token.isEquals()) {
|
|
959
|
+
throw new Error("Expected '=' in UPDATE");
|
|
960
|
+
}
|
|
961
|
+
this.setNextToken();
|
|
962
|
+
const { expression, subQuery } = this.parseLetUpdateRhs();
|
|
963
|
+
return new update_1.default(name, expression, subQuery);
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
966
|
+
* Parses a `MERGE INTO <name> [AS <target>] USING <rhs>
|
|
967
|
+
* [AS <source>] ON <on-clause> <when-clauses>` statement.
|
|
968
|
+
*/
|
|
969
|
+
parseMerge() {
|
|
970
|
+
if (!this.token.isMerge()) {
|
|
971
|
+
return null;
|
|
972
|
+
}
|
|
973
|
+
this.setNextToken();
|
|
974
|
+
this.expectAndSkipWhitespaceAndComments();
|
|
975
|
+
if (!this.token.isInto()) {
|
|
976
|
+
throw new Error("Expected INTO after MERGE");
|
|
977
|
+
}
|
|
978
|
+
this.setNextToken();
|
|
979
|
+
this.expectAndSkipWhitespaceAndComments();
|
|
980
|
+
if (!this.token.isIdentifierOrKeyword() || this.token.value === null) {
|
|
981
|
+
throw new Error("Expected binding name after MERGE INTO");
|
|
982
|
+
}
|
|
983
|
+
const name = this.token.value;
|
|
984
|
+
this.setNextToken();
|
|
985
|
+
this.skipWhitespaceAndComments();
|
|
986
|
+
// Optional `AS <target-alias>`.
|
|
987
|
+
let targetAlias = null;
|
|
988
|
+
if (this.token.isAs()) {
|
|
989
|
+
this.setNextToken();
|
|
990
|
+
this.expectAndSkipWhitespaceAndComments();
|
|
991
|
+
if (!this.token.isIdentifierOrKeyword() || this.token.value === null) {
|
|
992
|
+
throw new Error("Expected alias after AS in MERGE INTO");
|
|
993
|
+
}
|
|
994
|
+
targetAlias = this.token.value;
|
|
995
|
+
this.setNextToken();
|
|
996
|
+
this.skipWhitespaceAndComments();
|
|
997
|
+
}
|
|
998
|
+
if (!this.token.isUsing()) {
|
|
999
|
+
throw new Error("Expected USING after MERGE INTO target");
|
|
1000
|
+
}
|
|
1001
|
+
this.setNextToken();
|
|
1002
|
+
const { expression: sourceExpression, subQuery: sourceSubQuery } = this.parseLetUpdateRhs();
|
|
1003
|
+
if (sourceExpression !== null) {
|
|
1004
|
+
// The source RHS lives outside the LET / UPDATE binding-RHS
|
|
1005
|
+
// scope, so a bare identifier here refers to an existing
|
|
1006
|
+
// binding (e.g. `USING incoming AS s`) rather than a
|
|
1007
|
+
// declaration-site name. Convert unresolved references so
|
|
1008
|
+
// they resolve against the global Bindings store at runtime.
|
|
1009
|
+
this.convertUnresolvedReferencesToBindings(sourceExpression);
|
|
1010
|
+
}
|
|
1011
|
+
this.skipWhitespaceAndComments();
|
|
1012
|
+
// Optional `AS <source-alias>`.
|
|
1013
|
+
let sourceAlias = null;
|
|
1014
|
+
if (this.token.isAs()) {
|
|
1015
|
+
this.setNextToken();
|
|
1016
|
+
this.expectAndSkipWhitespaceAndComments();
|
|
1017
|
+
if (!this.token.isIdentifierOrKeyword() || this.token.value === null) {
|
|
1018
|
+
throw new Error("Expected alias after AS in MERGE INTO … USING");
|
|
1019
|
+
}
|
|
1020
|
+
sourceAlias = this.token.value;
|
|
1021
|
+
this.setNextToken();
|
|
1022
|
+
this.skipWhitespaceAndComments();
|
|
1023
|
+
}
|
|
1024
|
+
if (!this.token.isOn()) {
|
|
1025
|
+
throw new Error("Expected ON in MERGE INTO");
|
|
1026
|
+
}
|
|
1027
|
+
this.setNextToken();
|
|
1028
|
+
this.expectAndSkipWhitespaceAndComments();
|
|
1029
|
+
// Pre-create the Merge with placeholders so that aliases can
|
|
1030
|
+
// resolve to the per-row context while we parse the ON clause
|
|
1031
|
+
// and the WHEN expressions. The proxy aliases delegate to
|
|
1032
|
+
// `targetValue()` / `sourceValue()` on this instance.
|
|
1033
|
+
const placeholderOn = { type: "keys", keys: [] };
|
|
1034
|
+
const merge = new merge_1.default(name, targetAlias, sourceAlias, sourceExpression, sourceSubQuery, placeholderOn, null, null);
|
|
1035
|
+
if (targetAlias !== null) {
|
|
1036
|
+
this._state.variables.set(targetAlias, new merge_1.MergeTargetAlias(merge));
|
|
1037
|
+
}
|
|
1038
|
+
if (sourceAlias !== null) {
|
|
1039
|
+
this._state.variables.set(sourceAlias, new merge_1.MergeSourceAlias(merge));
|
|
1040
|
+
}
|
|
1041
|
+
let onClause;
|
|
1042
|
+
try {
|
|
1043
|
+
onClause = this.parseMergeOn();
|
|
1044
|
+
this.skipWhitespaceAndComments();
|
|
1045
|
+
// Build the matched / not-matched actions from one or more
|
|
1046
|
+
// WHEN clauses. Track whether we've already seen each kind
|
|
1047
|
+
// to forbid duplicates.
|
|
1048
|
+
let matched = null;
|
|
1049
|
+
let notMatched = null;
|
|
1050
|
+
while (this.token.isWhen()) {
|
|
1051
|
+
const parsed = this.parseMergeWhenClause();
|
|
1052
|
+
if (parsed.kind === "matched") {
|
|
1053
|
+
if (matched !== null) {
|
|
1054
|
+
throw new Error("Duplicate WHEN MATCHED clause in MERGE INTO");
|
|
1055
|
+
}
|
|
1056
|
+
matched = parsed.action;
|
|
1057
|
+
}
|
|
1058
|
+
else {
|
|
1059
|
+
if (notMatched !== null) {
|
|
1060
|
+
throw new Error("Duplicate WHEN NOT MATCHED clause in MERGE INTO");
|
|
1061
|
+
}
|
|
1062
|
+
notMatched = parsed.action;
|
|
1063
|
+
}
|
|
1064
|
+
this.skipWhitespaceAndComments();
|
|
1065
|
+
}
|
|
1066
|
+
if (matched === null && notMatched === null) {
|
|
1067
|
+
throw new Error("MERGE INTO requires at least one WHEN clause");
|
|
1068
|
+
}
|
|
1069
|
+
merge.setClauses(onClause, matched, notMatched);
|
|
1070
|
+
}
|
|
1071
|
+
finally {
|
|
1072
|
+
if (targetAlias !== null) {
|
|
1073
|
+
this._state.variables.delete(targetAlias);
|
|
1074
|
+
}
|
|
1075
|
+
if (sourceAlias !== null) {
|
|
1076
|
+
this._state.variables.delete(sourceAlias);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
return merge;
|
|
1080
|
+
}
|
|
1081
|
+
/**
|
|
1082
|
+
* Parses the body of the `ON` clause. Distinguishes a short-form
|
|
1083
|
+
* key list (`ON id`, `ON (a, b, c)`) from a full predicate
|
|
1084
|
+
* expression by looking ahead: a bare identifier (or parenthesised
|
|
1085
|
+
* list of identifiers) followed by `WHEN` is the key form;
|
|
1086
|
+
* anything else is parsed as an expression.
|
|
1087
|
+
*/
|
|
1088
|
+
parseMergeOn() {
|
|
1089
|
+
// Snapshot tokenizer position so we can rewind after a failed
|
|
1090
|
+
// key-list probe.
|
|
1091
|
+
const savedIndex = this.tokenIndex;
|
|
1092
|
+
const keys = this.tryParseMergeKeyList();
|
|
1093
|
+
if (keys !== null) {
|
|
1094
|
+
return { type: "keys", keys };
|
|
1095
|
+
}
|
|
1096
|
+
// Not a key list — rewind and parse as a predicate expression.
|
|
1097
|
+
this.tokenIndex = savedIndex;
|
|
1098
|
+
const predicate = this.parseExpression();
|
|
1099
|
+
if (predicate === null) {
|
|
1100
|
+
throw new Error("Expected predicate or key list after MERGE INTO … ON");
|
|
1101
|
+
}
|
|
1102
|
+
this.convertUnresolvedReferencesToBindings(predicate);
|
|
1103
|
+
return { type: "predicate", predicate };
|
|
1104
|
+
}
|
|
1105
|
+
/**
|
|
1106
|
+
* Speculatively parses `<id>` or `(<id> [, <id>]*)` followed by
|
|
1107
|
+
* `WHEN`. Returns the key list on success, or `null` if the
|
|
1108
|
+
* lookahead doesn't match (leaving the caller to rewind and try
|
|
1109
|
+
* an expression).
|
|
1110
|
+
*/
|
|
1111
|
+
tryParseMergeKeyList() {
|
|
1112
|
+
const keys = [];
|
|
1113
|
+
if (this.token.isLeftParenthesis()) {
|
|
1114
|
+
this.setNextToken();
|
|
1115
|
+
this.skipWhitespaceAndComments();
|
|
1116
|
+
while (!this.token.isRightParenthesis()) {
|
|
1117
|
+
if (!this.token.isIdentifierOrKeyword() || this.token.value === null) {
|
|
1118
|
+
return null;
|
|
1119
|
+
}
|
|
1120
|
+
keys.push(this.token.value);
|
|
1121
|
+
this.setNextToken();
|
|
1122
|
+
this.skipWhitespaceAndComments();
|
|
1123
|
+
if (this.token.isComma()) {
|
|
1124
|
+
this.setNextToken();
|
|
1125
|
+
this.skipWhitespaceAndComments();
|
|
1126
|
+
}
|
|
1127
|
+
else if (!this.token.isRightParenthesis()) {
|
|
1128
|
+
return null;
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
this.setNextToken();
|
|
1132
|
+
}
|
|
1133
|
+
else {
|
|
1134
|
+
if (!this.token.isIdentifierOrKeyword() || this.token.value === null) {
|
|
1135
|
+
return null;
|
|
1136
|
+
}
|
|
1137
|
+
keys.push(this.token.value);
|
|
1138
|
+
this.setNextToken();
|
|
1139
|
+
}
|
|
1140
|
+
// Must be followed by `WHEN` (with any whitespace) to be a key list.
|
|
1141
|
+
this.skipWhitespaceAndComments();
|
|
1142
|
+
if (!this.token.isWhen()) {
|
|
1143
|
+
return null;
|
|
1144
|
+
}
|
|
1145
|
+
if (keys.length === 0) {
|
|
1146
|
+
return null;
|
|
1147
|
+
}
|
|
1148
|
+
return keys;
|
|
1149
|
+
}
|
|
1150
|
+
/**
|
|
1151
|
+
* Parses a single `WHEN [NOT] MATCHED THEN UPDATE SET … | DELETE
|
|
1152
|
+
* | INSERT [<expr>]` clause. Caller is positioned on `WHEN`.
|
|
1153
|
+
*/
|
|
1154
|
+
parseMergeWhenClause() {
|
|
1155
|
+
this.setNextToken(); // consume WHEN
|
|
1156
|
+
this.expectAndSkipWhitespaceAndComments();
|
|
1157
|
+
let negated = false;
|
|
1158
|
+
if (this.token.isNot()) {
|
|
1159
|
+
negated = true;
|
|
1160
|
+
this.setNextToken();
|
|
1161
|
+
this.expectAndSkipWhitespaceAndComments();
|
|
1162
|
+
}
|
|
1163
|
+
if (!this.token.isMatched()) {
|
|
1164
|
+
throw new Error("Expected MATCHED after WHEN" + (negated ? " NOT" : ""));
|
|
1165
|
+
}
|
|
1166
|
+
this.setNextToken();
|
|
1167
|
+
this.expectAndSkipWhitespaceAndComments();
|
|
1168
|
+
if (!this.token.isThen()) {
|
|
1169
|
+
throw new Error("Expected THEN after WHEN" + (negated ? " NOT" : "") + " MATCHED");
|
|
1170
|
+
}
|
|
1171
|
+
this.setNextToken();
|
|
1172
|
+
this.expectAndSkipWhitespaceAndComments();
|
|
1173
|
+
if (negated) {
|
|
1174
|
+
// WHEN NOT MATCHED — only INSERT is meaningful.
|
|
1175
|
+
if (!this.token.isInsert()) {
|
|
1176
|
+
throw new Error("Expected INSERT after WHEN NOT MATCHED THEN");
|
|
1177
|
+
}
|
|
1178
|
+
this.setNextToken();
|
|
1179
|
+
this.skipWhitespaceAndComments();
|
|
1180
|
+
// Optional expression: anything that isn't a WHEN / EOF /
|
|
1181
|
+
// semicolon / closing brace is the row-builder expression.
|
|
1182
|
+
let expression = null;
|
|
1183
|
+
if (!this.token.isWhen() &&
|
|
1184
|
+
!this.token.isEOF() &&
|
|
1185
|
+
!this.token.isSemicolon() &&
|
|
1186
|
+
!this.token.isClosingBrace()) {
|
|
1187
|
+
expression = this.parseExpression();
|
|
1188
|
+
if (expression === null) {
|
|
1189
|
+
throw new Error("Expected expression after WHEN NOT MATCHED THEN INSERT");
|
|
1190
|
+
}
|
|
1191
|
+
this.convertUnresolvedReferencesToBindings(expression);
|
|
1192
|
+
}
|
|
1193
|
+
return { kind: "notMatched", action: { type: "insert", expression } };
|
|
1194
|
+
}
|
|
1195
|
+
// WHEN MATCHED — accepts UPDATE SET … or DELETE.
|
|
1196
|
+
if (this.token.isUpdate()) {
|
|
1197
|
+
this.setNextToken();
|
|
1198
|
+
this.expectAndSkipWhitespaceAndComments();
|
|
1199
|
+
if (!this.token.isSet()) {
|
|
1200
|
+
throw new Error("Expected SET after WHEN MATCHED THEN UPDATE");
|
|
1201
|
+
}
|
|
1202
|
+
this.setNextToken();
|
|
1203
|
+
this.expectAndSkipWhitespaceAndComments();
|
|
1204
|
+
const setItems = this.parseMergeSetList();
|
|
1205
|
+
return { kind: "matched", action: { type: "update", setItems } };
|
|
1206
|
+
}
|
|
1207
|
+
if (this.token.isDelete()) {
|
|
1208
|
+
this.setNextToken();
|
|
1209
|
+
return { kind: "matched", action: { type: "delete" } };
|
|
1210
|
+
}
|
|
1211
|
+
throw new Error("Expected UPDATE or DELETE after WHEN MATCHED THEN");
|
|
1212
|
+
}
|
|
1213
|
+
/**
|
|
1214
|
+
* Parses `.field [= <expr>] [, .field [= <expr>]]*` — the body of a
|
|
1215
|
+
* `WHEN MATCHED THEN UPDATE SET` clause. An item without an
|
|
1216
|
+
* explicit `= <expr>` defaults to `source.<field>` at run time.
|
|
1217
|
+
*/
|
|
1218
|
+
parseMergeSetList() {
|
|
1219
|
+
const items = [];
|
|
1220
|
+
while (true) {
|
|
1221
|
+
if (!this.token.isDot()) {
|
|
1222
|
+
throw new Error("Expected '.' before SET field name");
|
|
1223
|
+
}
|
|
1224
|
+
this.setNextToken();
|
|
1225
|
+
if (!this.token.isIdentifierOrKeyword() || this.token.value === null) {
|
|
1226
|
+
throw new Error("Expected field name after '.'");
|
|
1227
|
+
}
|
|
1228
|
+
const field = this.token.value;
|
|
1229
|
+
this.setNextToken();
|
|
1230
|
+
this.skipWhitespaceAndComments();
|
|
1231
|
+
let expression = null;
|
|
1232
|
+
if (this.token.isEquals()) {
|
|
1233
|
+
this.setNextToken();
|
|
1234
|
+
this.expectAndSkipWhitespaceAndComments();
|
|
1235
|
+
expression = this.parseExpression();
|
|
1236
|
+
if (expression === null) {
|
|
1237
|
+
throw new Error(`Expected expression after SET .${field} =`);
|
|
1238
|
+
}
|
|
1239
|
+
this.convertUnresolvedReferencesToBindings(expression);
|
|
1240
|
+
this.skipWhitespaceAndComments();
|
|
1241
|
+
}
|
|
1242
|
+
items.push({ field, expression });
|
|
1243
|
+
if (!this.token.isComma()) {
|
|
1244
|
+
break;
|
|
1245
|
+
}
|
|
1246
|
+
this.setNextToken();
|
|
1247
|
+
this.skipWhitespaceAndComments();
|
|
1248
|
+
}
|
|
1249
|
+
if (items.length === 0) {
|
|
1250
|
+
throw new Error("Expected at least one SET field");
|
|
1251
|
+
}
|
|
1252
|
+
return items;
|
|
1253
|
+
}
|
|
1254
|
+
/**
|
|
1255
|
+
* Parses the tail of `UPDATE name AS alias DELETE WHERE <pred>`,
|
|
1256
|
+
* with `AS` already consumed by the caller's lookahead.
|
|
1257
|
+
*/
|
|
1258
|
+
parseUpdateDeleteTail(name) {
|
|
1259
|
+
this.setNextToken(); // consume AS
|
|
1260
|
+
this.expectAndSkipWhitespaceAndComments();
|
|
1261
|
+
if (!this.token.isIdentifierOrKeyword() || this.token.value === null) {
|
|
1262
|
+
throw new Error("Expected alias after AS");
|
|
1263
|
+
}
|
|
1264
|
+
const alias = this.token.value;
|
|
1265
|
+
this.setNextToken();
|
|
1266
|
+
this.expectAndSkipWhitespaceAndComments();
|
|
1267
|
+
if (!this.token.isDelete()) {
|
|
1268
|
+
throw new Error("Expected DELETE after UPDATE alias");
|
|
1269
|
+
}
|
|
1270
|
+
this.setNextToken();
|
|
1271
|
+
this.expectAndSkipWhitespaceAndComments();
|
|
1272
|
+
if (!this.token.isWhere()) {
|
|
1273
|
+
throw new Error("Expected WHERE after UPDATE … DELETE");
|
|
1274
|
+
}
|
|
1275
|
+
this.setNextToken();
|
|
1276
|
+
this.expectAndSkipWhitespaceAndComments();
|
|
1277
|
+
// Inject the alias into the parser's variable scope while we
|
|
1278
|
+
// parse the predicate so that `<alias>` / `<alias>.field`
|
|
1279
|
+
// references resolve to the UpdateDelete's per-row value.
|
|
1280
|
+
const updateDelete = new update_delete_1.default(name, alias, new expression_1.default());
|
|
1281
|
+
this._state.variables.set(alias, updateDelete);
|
|
1282
|
+
const predicate = this.parseExpression();
|
|
1283
|
+
this._state.variables.delete(alias);
|
|
1284
|
+
if (predicate === null) {
|
|
1285
|
+
throw new Error("Expected predicate expression after WHERE");
|
|
1286
|
+
}
|
|
1287
|
+
// Any other unresolved bare identifiers (e.g. `IN banned`) refer
|
|
1288
|
+
// to LET-bound bindings, which are resolved at run-time against
|
|
1289
|
+
// the global Bindings store.
|
|
1290
|
+
this.convertUnresolvedReferencesToBindings(predicate);
|
|
1291
|
+
updateDelete.setPredicate(predicate);
|
|
1292
|
+
return updateDelete;
|
|
1293
|
+
}
|
|
1294
|
+
/**
|
|
1295
|
+
* Recursively replaces any unresolved {@link Reference} node in
|
|
1296
|
+
* the given subtree with a {@link BindingReference}, which looks up
|
|
1297
|
+
* the value from the global {@link Bindings} store at run-time.
|
|
1298
|
+
*
|
|
1299
|
+
* Used by `UPDATE … AS u DELETE WHERE …` so that predicates can
|
|
1300
|
+
* reference other bindings by name, and to keep `LOAD JSON FROM …`
|
|
1301
|
+
* behaviour consistent at arbitrary depth.
|
|
1302
|
+
*/
|
|
1303
|
+
convertUnresolvedReferencesToBindings(node) {
|
|
1304
|
+
for (const child of node.getChildren().slice()) {
|
|
1305
|
+
if (child instanceof reference_1.default &&
|
|
1306
|
+
child.referred === undefined &&
|
|
1307
|
+
!child.identifier.startsWith("$")) {
|
|
1308
|
+
node.replaceChild(child, new binding_reference_1.default(child.identifier));
|
|
1309
|
+
}
|
|
1310
|
+
else {
|
|
1311
|
+
this.convertUnresolvedReferencesToBindings(child);
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
621
1315
|
parseMatch() {
|
|
622
1316
|
let optional = false;
|
|
623
1317
|
if (this.token.isOptional()) {
|
|
@@ -956,7 +1650,7 @@ class Parser extends base_parser_1.default {
|
|
|
956
1650
|
return null;
|
|
957
1651
|
}
|
|
958
1652
|
this.setNextToken();
|
|
959
|
-
this.
|
|
1653
|
+
this.skipWhitespaceAndComments();
|
|
960
1654
|
const query = this._parseTokenized(true);
|
|
961
1655
|
this.skipWhitespaceAndComments();
|
|
962
1656
|
if (!this.token.isClosingBrace()) {
|