@syncular/typegen 0.15.27 → 0.15.29
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 +31 -10
- package/dist/emit-queries-rust.js +4 -2
- package/dist/query.d.ts +2 -0
- package/dist/query.js +196 -31
- package/dist/syql-validator.js +166 -44
- package/package.json +3 -3
- package/src/emit-queries-rust.ts +9 -7
- package/src/query.ts +224 -29
- package/src/syql-validator.ts +233 -58
package/dist/syql-validator.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { analyzeStatement, inferParamTypeEvidence, scanTableRefs, stripCommentsAndStrings, } from './query.js';
|
|
2
2
|
import { isSyqlTrivia, lexSyqlSqlSource, SyqlFrontendError, } from './syql-lexer.js';
|
|
3
3
|
import { syqlRangeEndBind, syqlRangeStartBind } from './syql-semantics.js';
|
|
4
|
+
const IDENT_SOURCE = '[A-Za-z_][A-Za-z0-9_]*';
|
|
4
5
|
const IDENT_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
5
6
|
const NONDETERMINISTIC_FUNCTIONS = new Set([
|
|
6
7
|
'random',
|
|
@@ -694,12 +695,67 @@ class Validator {
|
|
|
694
695
|
}
|
|
695
696
|
return true;
|
|
696
697
|
};
|
|
698
|
+
const requiredJoinEqualityAt = (index) => {
|
|
699
|
+
const prefix = cleaned.slice(0, index);
|
|
700
|
+
const clauses = [
|
|
701
|
+
...prefix.matchAll(/\b(ON|JOIN|WHERE|GROUP\s+BY|ORDER\s+BY|HAVING|LIMIT|UNION|EXCEPT|INTERSECT)\b/gi),
|
|
702
|
+
];
|
|
703
|
+
const last = clauses.at(-1);
|
|
704
|
+
if (last?.[1]?.toUpperCase() !== 'ON')
|
|
705
|
+
return false;
|
|
706
|
+
const start = (last.index ?? 0) + last[0].length;
|
|
707
|
+
const next = /\b(?:JOIN|WHERE|GROUP\s+BY|ORDER\s+BY|HAVING|LIMIT|UNION|EXCEPT|INTERSECT)\b/i.exec(cleaned.slice(index));
|
|
708
|
+
const end = next === null ? cleaned.length : index + next.index;
|
|
709
|
+
const clause = cleaned.slice(start, end);
|
|
710
|
+
return !/\b(?:OR|NOT|SELECT|WITH)\b/i.test(clause);
|
|
711
|
+
};
|
|
712
|
+
const refByAlias = new Map(refs.map((ref) => [ref.alias.toLowerCase(), ref]));
|
|
713
|
+
const scopeNode = (alias, column) => `${alias.toLowerCase()}\0${column.toLowerCase()}`;
|
|
714
|
+
const parents = new Map();
|
|
715
|
+
const find = (node) => {
|
|
716
|
+
const parent = parents.get(node);
|
|
717
|
+
if (parent === undefined) {
|
|
718
|
+
parents.set(node, node);
|
|
719
|
+
return node;
|
|
720
|
+
}
|
|
721
|
+
if (parent === node)
|
|
722
|
+
return node;
|
|
723
|
+
const root = find(parent);
|
|
724
|
+
parents.set(node, root);
|
|
725
|
+
return root;
|
|
726
|
+
};
|
|
727
|
+
const union = (left, right) => {
|
|
728
|
+
const leftRoot = find(left);
|
|
729
|
+
const rightRoot = find(right);
|
|
730
|
+
if (leftRoot !== rightRoot)
|
|
731
|
+
parents.set(rightRoot, leftRoot);
|
|
732
|
+
};
|
|
733
|
+
const scopeColumn = (alias, column) => {
|
|
734
|
+
const ref = refByAlias.get(alias.toLowerCase());
|
|
735
|
+
const table = this.#ir.tables.find((item) => item.name === ref?.table);
|
|
736
|
+
return (table?.scopes.some((scope) => scope.column.toLowerCase() === column.toLowerCase()) === true);
|
|
737
|
+
};
|
|
738
|
+
const qualifiedEquality = new RegExp(`(${IDENT_SOURCE})\\.(${IDENT_SOURCE})\\s*(?:=|==|\\bIS\\b)\\s*(${IDENT_SOURCE})\\.(${IDENT_SOURCE})`, 'gi');
|
|
739
|
+
for (const match of cleaned.matchAll(qualifiedEquality)) {
|
|
740
|
+
const leftAlias = match[1];
|
|
741
|
+
const leftColumn = match[2];
|
|
742
|
+
const rightAlias = match[3];
|
|
743
|
+
const rightColumn = match[4];
|
|
744
|
+
if (!scopeColumn(leftAlias, leftColumn) ||
|
|
745
|
+
!scopeColumn(rightAlias, rightColumn) ||
|
|
746
|
+
(!requiredAt(match.index ?? 0) &&
|
|
747
|
+
!requiredJoinEqualityAt(match.index ?? 0))) {
|
|
748
|
+
continue;
|
|
749
|
+
}
|
|
750
|
+
union(scopeNode(leftAlias, leftColumn), scopeNode(rightAlias, rightColumn));
|
|
751
|
+
}
|
|
697
752
|
const required = new Set([...symbols.values()].flatMap((symbol) => symbol.parameter.kind === 'value' &&
|
|
698
753
|
!symbol.parameter.optional &&
|
|
699
754
|
symbol.authoredType?.nullable !== true
|
|
700
755
|
? [symbol.name]
|
|
701
756
|
: []));
|
|
702
757
|
const candidates = [];
|
|
758
|
+
const directScopeEvidence = new Map();
|
|
703
759
|
for (const ref of refs) {
|
|
704
760
|
const table = this.#ir.tables.find((item) => item.name === ref.table);
|
|
705
761
|
if (table === undefined)
|
|
@@ -747,7 +803,7 @@ class Validator {
|
|
|
747
803
|
}
|
|
748
804
|
const params = [...new Set(found.map((item) => item.name))];
|
|
749
805
|
if (params.length > 0) {
|
|
750
|
-
|
|
806
|
+
const evidence = {
|
|
751
807
|
binding: {
|
|
752
808
|
table: table.name,
|
|
753
809
|
variable: scope.variable,
|
|
@@ -757,19 +813,56 @@ class Validator {
|
|
|
757
813
|
operator: found.some((item) => item.operator === 'in')
|
|
758
814
|
? 'in'
|
|
759
815
|
: 'equal',
|
|
760
|
-
}
|
|
816
|
+
};
|
|
817
|
+
inferred.push(evidence);
|
|
818
|
+
directScopeEvidence.set(scopeNode(ref.alias, scope.column), evidence);
|
|
761
819
|
}
|
|
762
820
|
}
|
|
763
821
|
candidates.push({ ref, scopes: inferred });
|
|
764
822
|
}
|
|
823
|
+
const resolvedCandidates = candidates.map((candidate) => {
|
|
824
|
+
const table = this.#ir.tables.find((item) => item.name === candidate.ref.table);
|
|
825
|
+
if (table === undefined)
|
|
826
|
+
return candidate;
|
|
827
|
+
const directByVariable = new Map(candidate.scopes.map((scope) => [scope.binding.variable, scope]));
|
|
828
|
+
const scopes = table.scopes.flatMap((scope) => {
|
|
829
|
+
const direct = directByVariable.get(scope.variable);
|
|
830
|
+
if (direct !== undefined)
|
|
831
|
+
return [direct];
|
|
832
|
+
const root = find(scopeNode(candidate.ref.alias, scope.column));
|
|
833
|
+
const inherited = [...directScopeEvidence.entries()]
|
|
834
|
+
.filter(([node]) => find(node) === root)
|
|
835
|
+
.map(([, evidence]) => evidence);
|
|
836
|
+
const params = [
|
|
837
|
+
...new Set(inherited.flatMap((evidence) => evidence.binding.params)),
|
|
838
|
+
];
|
|
839
|
+
if (params.length === 0)
|
|
840
|
+
return [];
|
|
841
|
+
return [
|
|
842
|
+
{
|
|
843
|
+
binding: {
|
|
844
|
+
table: table.name,
|
|
845
|
+
variable: scope.variable,
|
|
846
|
+
pattern: scope.pattern,
|
|
847
|
+
params,
|
|
848
|
+
},
|
|
849
|
+
operator: inherited.some((evidence) => evidence.operator === 'in')
|
|
850
|
+
? 'in'
|
|
851
|
+
: 'equal',
|
|
852
|
+
},
|
|
853
|
+
];
|
|
854
|
+
});
|
|
855
|
+
return { ...candidate, scopes };
|
|
856
|
+
});
|
|
765
857
|
const ftsOwners = new Map(this.#ir.tables.flatMap((table) => table.ftsIndexes.map((index) => [index.name, table.name])));
|
|
766
858
|
const dependencies = [
|
|
767
859
|
...new Set(refs.map((ref) => ftsOwners.get(ref.table) ?? ref.table)),
|
|
768
860
|
]
|
|
769
861
|
.sort()
|
|
770
862
|
.map((table) => {
|
|
771
|
-
const instances =
|
|
772
|
-
if (instances.
|
|
863
|
+
const instances = resolvedCandidates.filter((item) => item.ref.table === table);
|
|
864
|
+
if (instances.length !== 1 ||
|
|
865
|
+
instances.some((item) => item.scopes.length === 0)) {
|
|
773
866
|
return { table, scopes: [] };
|
|
774
867
|
}
|
|
775
868
|
const scopes = instances
|
|
@@ -790,56 +883,85 @@ class Validator {
|
|
|
790
883
|
});
|
|
791
884
|
if (!logical.declaration.sync)
|
|
792
885
|
return { dependencies, coverage: [] };
|
|
793
|
-
|
|
886
|
+
const syncedCandidates = resolvedCandidates.filter((candidate) => this.#ir.tables.some((table) => table.name === candidate.ref.table));
|
|
887
|
+
const eligible = syncedCandidates.filter((candidate) => {
|
|
794
888
|
const table = this.#ir.tables.find((item) => item.name === candidate.ref.table);
|
|
795
889
|
return (table !== undefined &&
|
|
796
|
-
|
|
797
|
-
.length === 1 &&
|
|
890
|
+
syncedCandidates.filter((item) => item.ref.table === candidate.ref.table).length === 1 &&
|
|
798
891
|
table.scopes.length > 0 &&
|
|
799
892
|
candidate.scopes.length === table.scopes.length);
|
|
800
893
|
});
|
|
801
894
|
const syncBy = logical.declaration.syncBy;
|
|
802
|
-
if (
|
|
803
|
-
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
if (
|
|
820
|
-
this.#
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
.filter((scope) => scope.variable !== dimension.variable)
|
|
826
|
-
.map((scope) => {
|
|
827
|
-
const fixed = byVariable.get(scope.variable);
|
|
828
|
-
if (fixed.operator !== 'equal' || fixed.binding.params.length !== 1) {
|
|
829
|
-
this.#fail('SYQL6005_INVALID_SYNC_QUERY', syncBy?.span ?? logical.declaration.nameSpan, `fixed sync scope ${table.name}.${scope.column} must use one required equality bind`);
|
|
895
|
+
if (eligible.length === 0 || eligible.length !== syncedCandidates.length) {
|
|
896
|
+
this.#fail('SYQL6005_INVALID_SYNC_QUERY', logical.declaration.syncBy?.span ?? logical.declaration.nameSpan, 'sync query coverage cannot be proven for every read table from required equality/IN predicates over every declared scope; split the query or add an exact compatible scope proof');
|
|
897
|
+
}
|
|
898
|
+
const byCandidate = syncBy === undefined
|
|
899
|
+
? undefined
|
|
900
|
+
: eligible.find((candidate) => candidate.ref.alias === syncBy.qualifier);
|
|
901
|
+
if (syncBy !== undefined && byCandidate === undefined) {
|
|
902
|
+
this.#fail('SYQL6005_INVALID_SYNC_QUERY', syncBy.span, `sync query \`by ${syncBy.qualifier}.${syncBy.column}\` does not name one fully coverable table instance`);
|
|
903
|
+
}
|
|
904
|
+
if (syncBy === undefined &&
|
|
905
|
+
eligible.some((candidate) => {
|
|
906
|
+
const table = this.#ir.tables.find((item) => item.name === candidate.ref.table);
|
|
907
|
+
return (table?.scopes.length ?? 0) > 1;
|
|
908
|
+
})) {
|
|
909
|
+
this.#fail('SYQL6005_INVALID_SYNC_QUERY', logical.declaration.nameSpan, 'a sync query reading any multi-scope table must select an anchor unit dimension with `by alias.scope_column`');
|
|
910
|
+
}
|
|
911
|
+
let anchorUnit;
|
|
912
|
+
if (byCandidate !== undefined && syncBy !== undefined) {
|
|
913
|
+
const table = this.#ir.tables.find((item) => item.name === byCandidate.ref.table);
|
|
914
|
+
const dimension = table?.scopes.find((scope) => scope.column === syncBy.column);
|
|
915
|
+
anchorUnit = byCandidate.scopes.find((scope) => scope.binding.variable === dimension?.variable);
|
|
916
|
+
if (dimension === undefined || anchorUnit === undefined) {
|
|
917
|
+
this.#fail('SYQL6005_INVALID_SYNC_QUERY', syncBy.span, `${byCandidate.ref.alias}.${syncBy.column} is not a proven declared scope`);
|
|
830
918
|
}
|
|
919
|
+
}
|
|
920
|
+
const sameParams = (left, right) => left.length === right.length &&
|
|
921
|
+
left.every((value) => right.includes(value));
|
|
922
|
+
const coverage = eligible.map((candidate) => {
|
|
923
|
+
const table = this.#ir.tables.find((item) => item.name === candidate.ref.table);
|
|
924
|
+
if (table === undefined)
|
|
925
|
+
throw new Error('eligible table disappeared');
|
|
926
|
+
const byVariable = new Map(candidate.scopes.map((scope) => [scope.binding.variable, scope]));
|
|
927
|
+
let unit;
|
|
928
|
+
if (candidate === byCandidate) {
|
|
929
|
+
unit = anchorUnit;
|
|
930
|
+
}
|
|
931
|
+
else if (table.scopes.length === 1) {
|
|
932
|
+
unit = candidate.scopes[0];
|
|
933
|
+
}
|
|
934
|
+
else if (anchorUnit !== undefined) {
|
|
935
|
+
const matching = candidate.scopes.filter((scope) => sameParams(scope.binding.params, anchorUnit.binding.params));
|
|
936
|
+
if (matching.length === 1)
|
|
937
|
+
unit = matching[0];
|
|
938
|
+
}
|
|
939
|
+
if (unit === undefined) {
|
|
940
|
+
this.#fail('SYQL6005_INVALID_SYNC_QUERY', syncBy?.span ?? logical.declaration.nameSpan, `joined multi-scope table ${table.name} has no unambiguous unit dimension compatible with the selected anchor; split the query`);
|
|
941
|
+
}
|
|
942
|
+
const fixedScopes = table.scopes
|
|
943
|
+
.filter((scope) => scope.variable !== unit.binding.variable)
|
|
944
|
+
.map((scope) => {
|
|
945
|
+
const fixed = byVariable.get(scope.variable);
|
|
946
|
+
if (fixed === undefined ||
|
|
947
|
+
fixed.operator !== 'equal' ||
|
|
948
|
+
fixed.binding.params.length !== 1) {
|
|
949
|
+
this.#fail('SYQL6005_INVALID_SYNC_QUERY', syncBy?.span ?? logical.declaration.nameSpan, `fixed sync scope ${table.name}.${scope.column} must use one required equality bind`);
|
|
950
|
+
}
|
|
951
|
+
return {
|
|
952
|
+
variable: fixed.binding.variable,
|
|
953
|
+
params: fixed.binding.params,
|
|
954
|
+
};
|
|
955
|
+
});
|
|
831
956
|
return {
|
|
832
|
-
|
|
833
|
-
|
|
957
|
+
table: table.name,
|
|
958
|
+
variable: unit.binding.variable,
|
|
959
|
+
units: unit.binding.params,
|
|
960
|
+
fixedScopes,
|
|
834
961
|
};
|
|
835
962
|
});
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
variable: unit.binding.variable,
|
|
839
|
-
units: unit.binding.params,
|
|
840
|
-
fixedScopes,
|
|
841
|
-
};
|
|
842
|
-
return { dependencies, coverage: [coverage] };
|
|
963
|
+
coverage.sort((left, right) => left.table.localeCompare(right.table));
|
|
964
|
+
return { dependencies, coverage };
|
|
843
965
|
}
|
|
844
966
|
#validateSort(query, structure, location) {
|
|
845
967
|
if (query.sort === undefined)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@syncular/typegen",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.29",
|
|
4
4
|
"description": "Syncular schema-to-TypeScript type generator and the `syncular` CLI",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Benjamin Kniffler",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"!dist/**/*.test.d.ts"
|
|
49
49
|
],
|
|
50
50
|
"devDependencies": {
|
|
51
|
-
"@syncular/core": "0.15.
|
|
52
|
-
"@syncular/server": "0.15.
|
|
51
|
+
"@syncular/core": "0.15.29",
|
|
52
|
+
"@syncular/server": "0.15.29"
|
|
53
53
|
}
|
|
54
54
|
}
|
package/src/emit-queries-rust.ts
CHANGED
|
@@ -401,7 +401,10 @@ function emitSelect(query: AnalyzedQuery): string[] {
|
|
|
401
401
|
' }',
|
|
402
402
|
);
|
|
403
403
|
}
|
|
404
|
-
|
|
404
|
+
const usesActivationMask =
|
|
405
|
+
metadata.plan.backend === 'variants' &&
|
|
406
|
+
metadata.plan.activationControls.length > 0;
|
|
407
|
+
if (usesActivationMask) {
|
|
405
408
|
lines.push(' let mut activation_mask = 0usize;');
|
|
406
409
|
metadata.plan.activationControls.forEach((control, index) => {
|
|
407
410
|
lines.push(
|
|
@@ -425,12 +428,11 @@ function emitSelect(query: AnalyzedQuery): string[] {
|
|
|
425
428
|
}
|
|
426
429
|
const sortIndex = sort?.kind === 'sort' ? 'sort_index' : '0usize';
|
|
427
430
|
const profileCount = sort?.kind === 'sort' ? sort.profiles.length : 1;
|
|
428
|
-
const index =
|
|
429
|
-
|
|
430
|
-
?
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
: sortIndex;
|
|
431
|
+
const index = usesActivationMask
|
|
432
|
+
? profileCount === 1
|
|
433
|
+
? 'activation_mask'
|
|
434
|
+
: `activation_mask * ${profileCount} + ${sortIndex}`
|
|
435
|
+
: sortIndex;
|
|
434
436
|
lines.push(
|
|
435
437
|
` let statement_index = ${index};`,
|
|
436
438
|
' match statement_index {',
|