@hypequery/datasets 0.12.7 → 0.13.1
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/dist/cache/query-signature.d.ts.map +1 -1
- package/dist/cache/query-signature.js +22 -7
- package/dist/executor.d.ts.map +1 -1
- package/dist/executor.js +5 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/sql-portability.d.ts +42 -0
- package/dist/sql-portability.d.ts.map +1 -0
- package/dist/sql-portability.js +641 -0
- package/dist/utils/dataset-query-validation.d.ts.map +1 -1
- package/dist/utils/dataset-query-validation.js +5 -1
- package/dist/utils/relationship-validation.d.ts +9 -1
- package/dist/utils/relationship-validation.d.ts.map +1 -1
- package/dist/utils/relationship-validation.js +24 -2
- package/package.json +3 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"query-signature.d.ts","sourceRoot":"","sources":["../../src/cache/query-signature.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EACV,kBAAkB,EAClB,YAAY,EACZ,gBAAgB,EAChB,gBAAgB,EAGhB,WAAW,EACX,SAAS,EACV,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"query-signature.d.ts","sourceRoot":"","sources":["../../src/cache/query-signature.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EACV,kBAAkB,EAClB,YAAY,EACZ,gBAAgB,EAChB,gBAAgB,EAGhB,WAAW,EACX,SAAS,EACV,MAAM,aAAa,CAAC;AAIrB,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAE7D,OAAO,EAAE,eAAe,EAAE,CAAC;AAuE3B,wBAAgB,0BAA0B,CACxC,EAAE,EAAE,kBAAkB,EACtB,KAAK,EAAE,YAAY,EACnB,OAAO,CAAC,EAAE,gBAAgB,GACzB,MAAM,CAiBR;AAED,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,SAAS,GAAG,gBAAgB,EACpC,KAAK,EAAE,WAAW,EAClB,OAAO,CAAC,EAAE,gBAAgB,GACzB,MAAM,CAkBR"}
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { getMetricGrain, getMetricRef } from '../utils/metric-handle.js';
|
|
15
15
|
import { getRuntimeTenantPredicate } from '../utils/tenant-runtime.js';
|
|
16
|
+
import { buildRelationshipBuilderContext } from '../utils/relationship-builder-plan.js';
|
|
16
17
|
import { stableStringify } from '../utils/canonical-json.js';
|
|
17
18
|
export { stableStringify };
|
|
18
19
|
const SIGNATURE_VERSION = 1;
|
|
@@ -39,15 +40,29 @@ function orderBySignature(orderBy) {
|
|
|
39
40
|
}));
|
|
40
41
|
}
|
|
41
42
|
/**
|
|
42
|
-
*
|
|
43
|
-
* runtime tenant
|
|
44
|
-
*
|
|
43
|
+
* True when this query joins a tenant-scoped relationship target under an
|
|
44
|
+
* active runtime tenant, so the joined rows are filtered per tenant. Mirrors
|
|
45
|
+
* the executor's join planning (`buildRelationshipBuilderContext`).
|
|
45
46
|
*/
|
|
46
|
-
function
|
|
47
|
+
function activatesTenantScopedJoin(ds, query, context) {
|
|
48
|
+
const joinCtx = buildRelationshipBuilderContext(ds, query, context);
|
|
49
|
+
return !!joinCtx && joinCtx.joins.some((join) => join.tenant);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Tenant portion of the key. A dataset with a `tenantKey` is strictly
|
|
53
|
+
* partitioned per scope. A tenant-less dataset normally shares entries across
|
|
54
|
+
* callers — but if the query joins a tenant-scoped relationship target, those
|
|
55
|
+
* joined rows are still filtered by the runtime tenant, so the key must
|
|
56
|
+
* partition per tenant too or one tenant could be served another's cached rows.
|
|
57
|
+
*/
|
|
58
|
+
function tenantSignature(ds, query, context) {
|
|
59
|
+
const predicate = getRuntimeTenantPredicate(context);
|
|
47
60
|
if (!ds.tenantKey) {
|
|
61
|
+
if (predicate && activatesTenantScopedJoin(ds, query, context)) {
|
|
62
|
+
return { joinScoped: true, operator: predicate.operator, value: predicate.value };
|
|
63
|
+
}
|
|
48
64
|
return null;
|
|
49
65
|
}
|
|
50
|
-
const predicate = getRuntimeTenantPredicate(context);
|
|
51
66
|
if (!predicate) {
|
|
52
67
|
// Trusted cross-tenant scope (`scope: 'all'`) or no runtime tenancy.
|
|
53
68
|
return { key: ds.tenantKey, scope: 'all' };
|
|
@@ -68,7 +83,7 @@ export function buildDatasetQuerySignature(ds, query, context) {
|
|
|
68
83
|
by: query.by ?? null,
|
|
69
84
|
limit: query.limit ?? null,
|
|
70
85
|
offset: query.offset ?? null,
|
|
71
|
-
tenant: tenantSignature(ds, context),
|
|
86
|
+
tenant: tenantSignature(ds, query, context),
|
|
72
87
|
scope: scopeSignature(context),
|
|
73
88
|
});
|
|
74
89
|
}
|
|
@@ -87,7 +102,7 @@ export function buildMetricQuerySignature(metric, query, context) {
|
|
|
87
102
|
by: grain ?? null,
|
|
88
103
|
limit: query.limit ?? null,
|
|
89
104
|
offset: query.offset ?? null,
|
|
90
|
-
tenant: tenantSignature(ref.dataset, context),
|
|
105
|
+
tenant: tenantSignature(ref.dataset, query, context),
|
|
91
106
|
scope: scopeSignature(context),
|
|
92
107
|
});
|
|
93
108
|
}
|
package/dist/executor.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../src/executor.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EACV,SAAS,EACT,gBAAgB,EAChB,WAAW,EACX,cAAc,EACd,YAAY,EACZ,eAAe,EACf,YAAY,EACZ,eAAe,EACf,kBAAkB,EAClB,qBAAqB,EACrB,gBAAgB,EAGhB,kBAAkB,EAClB,eAAe,EAEhB,MAAM,YAAY,CAAC;AAEpB,OAAO,KAAK,EAEV,uBAAuB,EACvB,wBAAwB,EACzB,MAAM,6BAA6B,CAAC;AAErC,OAAO,KAAK,EACV,QAAQ,EACR,eAAe,EAChB,MAAM,oBAAoB,CAAC;AAI5B,OAAO,EAAuB,KAAK,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAgB7E,OAAO,EAIL,KAAK,4BAA4B,EAClC,MAAM,oBAAoB,CAAC;AAW5B,OAAO,EAEL,KAAK,oBAAoB,EACzB,KAAK,kBAAkB,EACxB,MAAM,iCAAiC,CAAC;
|
|
1
|
+
{"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../src/executor.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EACV,SAAS,EACT,gBAAgB,EAChB,WAAW,EACX,cAAc,EACd,YAAY,EACZ,eAAe,EACf,YAAY,EACZ,eAAe,EACf,kBAAkB,EAClB,qBAAqB,EACrB,gBAAgB,EAGhB,kBAAkB,EAClB,eAAe,EAEhB,MAAM,YAAY,CAAC;AAEpB,OAAO,KAAK,EAEV,uBAAuB,EACvB,wBAAwB,EACzB,MAAM,6BAA6B,CAAC;AAErC,OAAO,KAAK,EACV,QAAQ,EACR,eAAe,EAChB,MAAM,oBAAoB,CAAC;AAI5B,OAAO,EAAuB,KAAK,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAgB7E,OAAO,EAIL,KAAK,4BAA4B,EAClC,MAAM,oBAAoB,CAAC;AAW5B,OAAO,EAEL,KAAK,oBAAoB,EACzB,KAAK,kBAAkB,EACxB,MAAM,iCAAiC,CAAC;AA6JzC,MAAM,WAAW,wBAAwB;IACvC,mDAAmD;IACnD,cAAc,EAAE,wBAAwB,CAAC;CAC1C;AAED,MAAM,WAAW,0BAA0B;IACzC,+EAA+E;IAC/E,YAAY,CAAC,EAAE,wBAAwB,CAAC;IACxC;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,eAAe,CAAC;IAC1B;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,oBAAoB,CAAC;CAC9B;AAWD,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,gBAAgB,GAAG,kBAAkB,CAAC;AAE/E,MAAM,MAAM,aAAa,CAAC,OAAO,SAAS,cAAc,IACtD,OAAO,SAAS,kBAAkB,GAAG,YAAY,GAAG,WAAW,CAAC;AAElE,MAAM,MAAM,cAAc,CACxB,OAAO,SAAS,cAAc,EAC9B,IAAI,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAC5B,OAAO,SAAS,kBAAkB,GAClC,kBAAkB,CAAC,IAAI,CAAC,GACxB,YAAY,CAAC,IAAI,CAAC,CAAC;AAEvB,KAAK,YAAY,CAAC,YAAY,SAAS,MAAM,GAAG,MAAM,IACpD,eAAe,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,YAAY,CAAC,CAAC;AAE/C,KAAK,cAAc,CACjB,YAAY,SAAS,MAAM,EAC3B,WAAW,SAAS,MAAM,EAC1B,QAAQ,SAAS,YAAY,CAAC,YAAY,CAAC,IACzC,SAAS,CAAC,YAAY,EAAE,WAAW,EAAE,GAAG,EAAE,QAAQ,CAAC,CAAC;AAExD,KAAK,qBAAqB,CACxB,YAAY,SAAS,MAAM,EAC3B,WAAW,SAAS,MAAM,EAC1B,QAAQ,SAAS,YAAY,CAAC,YAAY,CAAC,IACzC,gBAAgB,CAAC,YAAY,EAAE,WAAW,EAAE,GAAG,EAAE,QAAQ,CAAC,CAAC;AAE/D,MAAM,WAAW,aAAa;IAC5B,OAAO,CACL,QAAQ,SAAS,YAAY,EAC7B,KAAK,CAAC,MAAM,SAAS,eAAe,CAAC,QAAQ,CAAC,GAAG,eAAe,CAAC,QAAQ,CAAC,EAE1E,MAAM,EAAE,QAAQ,EAChB,KAAK,CAAC,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,gBAAgB,GACzB,OAAO,CAAC,qBAAqB,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC;IACpD,OAAO,CACL,YAAY,SAAS,MAAM,EAC3B,WAAW,SAAS,MAAM,EAC1B,QAAQ,SAAS,YAAY,CAAC,YAAY,CAAC,EAC3C,KAAK,CAAC,MAAM,SAAS,cAAc,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,cAAc,CAAC,QAAQ,EAAE,WAAW,CAAC,EAElG,MAAM,EAAE,cAAc,CAAC,YAAY,EAAE,WAAW,EAAE,QAAQ,CAAC,EAC3D,KAAK,CAAC,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,gBAAgB,GACzB,OAAO,CAAC,eAAe,CAAC,QAAQ,EAAE,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC;IAC3D,OAAO,CACL,YAAY,SAAS,MAAM,EAC3B,WAAW,SAAS,MAAM,EAC1B,QAAQ,SAAS,YAAY,CAAC,YAAY,CAAC,EAC3C,KAAK,CAAC,MAAM,SAAS,cAAc,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,cAAc,CAAC,QAAQ,EAAE,WAAW,CAAC,EAElG,MAAM,EAAE,qBAAqB,CAAC,YAAY,EAAE,WAAW,EAAE,QAAQ,CAAC,EAClE,KAAK,CAAC,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,gBAAgB,GACzB,OAAO,CAAC,eAAe,CAAC,QAAQ,EAAE,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC;IAC3D,OAAO,CAAC,IAAI,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,SAAS,cAAc,GAAG,cAAc,EACrF,MAAM,EAAE,OAAO,EACf,KAAK,CAAC,EAAE,aAAa,CAAC,OAAO,CAAC,EAC9B,OAAO,CAAC,EAAE,gBAAgB,GACzB,OAAO,CAAC,cAAc,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC;IAC1C,KAAK,CAAC,OAAO,SAAS,cAAc,EAClC,MAAM,EAAE,OAAO,EACf,KAAK,CAAC,EAAE,aAAa,CAAC,OAAO,CAAC,EAC9B,OAAO,CAAC,EAAE,gBAAgB,GACzB,MAAM,CAAC;IACV,QAAQ,CAAC,OAAO,SAAS,cAAc,EACrC,MAAM,EAAE,OAAO,EACf,KAAK,CAAC,EAAE,aAAa,CAAC,OAAO,CAAC,EAC9B,OAAO,CAAC,EAAE,gBAAgB,GACzB,gBAAgB,CAAC;IACpB,+DAA+D;IAC/D,aAAa,IAAI,kBAAkB,CAAC;IACpC;;;OAGG;IACH,UAAU,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;CAChC;AAED,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,cAAc,CAA0B;gBAEpC,OAAO,EAAE,wBAAwB;IAI7C,SAAS,CAAC,iBAAiB,IAAI,uBAAuB;IAItD;;OAEG;IACG,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACnC,MAAM,EAAE,SAAS,GAAG,gBAAgB,EACpC,KAAK,GAAE,WAAgB,EACvB,OAAO,CAAC,EAAE,gBAAgB,GACzB,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IAW3B;;OAEG;IACH,KAAK,CACH,MAAM,EAAE,SAAS,GAAG,gBAAgB,EACpC,KAAK,GAAE,WAAgB,EACvB,OAAO,CAAC,EAAE,gBAAgB,GACzB,MAAM;IAmBT;;OAEG;IACH,QAAQ,CACN,MAAM,EAAE,SAAS,GAAG,gBAAgB,EACpC,KAAK,EAAE,WAAW,EAClB,OAAO,CAAC,EAAE,gBAAgB,GACzB,gBAAgB;YA+BL,aAAa;IAgC3B,OAAO,CAAC,cAAc;IAuDtB,OAAO,CAAC,yBAAyB;CA+GlC;AAED,qBAAa,iBAAkB,SAAQ,iBAAkB,YAAW,aAAa;IAC/E,OAAO,CAAC,OAAO,CAAC,CAAkB;IAClC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAqB;IAChD,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAU;IAChD,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAC,CAAS;gBAEhC,OAAO,EAAE,0BAA0B;IAoB/C,aAAa,IAAI,kBAAkB;IAInC,UAAU,IAAI,OAAO,CAAC,OAAO,CAAC;IAI9B;;;;OAIG;IACH,OAAO,CAAC,WAAW;IA0BnB;;;;OAIG;IACH,OAAO,CAAC,gBAAgB;IAUxB,UAAU,CACR,MAAM,EAAE,SAAS,GAAG,gBAAgB,EACpC,KAAK,GAAE,WAAgB,EACvB,OAAO,CAAC,EAAE,gBAAgB,GACzB,QAAQ;IASX,WAAW,CACT,EAAE,EAAE,kBAAkB,EACtB,KAAK,GAAE,YAAiB,EACxB,OAAO,CAAC,EAAE,gBAAgB,GACzB,QAAQ;IAIX;;OAEG;IACH,OAAO,CACL,QAAQ,SAAS,eAAe,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,EACpD,KAAK,CAAC,MAAM,SAAS,eAAe,CAAC,QAAQ,CAAC,GAAG,eAAe,CAAC,QAAQ,CAAC,EAE1E,MAAM,EAAE,QAAQ,EAChB,KAAK,CAAC,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,gBAAgB,GACzB,OAAO,CAAC,qBAAqB,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IACnD,OAAO,CACL,YAAY,SAAS,MAAM,EAC3B,WAAW,SAAS,MAAM,EAC1B,QAAQ,SAAS,eAAe,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,YAAY,CAAC,EAC7D,KAAK,CAAC,MAAM,SAAS,cAAc,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,cAAc,CAAC,QAAQ,EAAE,WAAW,CAAC,EAElG,MAAM,EAAE,SAAS,CAAC,YAAY,EAAE,WAAW,EAAE,GAAG,EAAE,QAAQ,CAAC,EAC3D,KAAK,CAAC,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,gBAAgB,GACzB,OAAO,CAAC,eAAe,CAAC,QAAQ,EAAE,WAAW,EAAE,MAAM,CAAC,CAAC;IAC1D,OAAO,CACL,YAAY,SAAS,MAAM,EAC3B,WAAW,SAAS,MAAM,EAC1B,QAAQ,SAAS,eAAe,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,YAAY,CAAC,EAC7D,KAAK,CAAC,MAAM,SAAS,cAAc,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,cAAc,CAAC,QAAQ,EAAE,WAAW,CAAC,EAElG,MAAM,EAAE,gBAAgB,CAAC,YAAY,EAAE,WAAW,EAAE,GAAG,EAAE,QAAQ,CAAC,EAClE,KAAK,CAAC,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,gBAAgB,GACzB,OAAO,CAAC,eAAe,CAAC,QAAQ,EAAE,WAAW,EAAE,MAAM,CAAC,CAAC;IAqB1D,KAAK,CAAC,OAAO,SAAS,cAAc,EAClC,MAAM,EAAE,OAAO,EACf,KAAK,GAAE,aAAa,CAAC,OAAO,CAAgC,EAC5D,OAAO,CAAC,EAAE,gBAAgB,GACzB,MAAM;IAQT,QAAQ,CAAC,OAAO,SAAS,cAAc,EACrC,MAAM,EAAE,OAAO,EACf,KAAK,GAAE,aAAa,CAAC,OAAO,CAAgC,EAC5D,OAAO,CAAC,EAAE,gBAAgB,GACzB,gBAAgB;IAQnB,OAAO,CAAC,aAAa;IAuCrB,OAAO,CAAC,cAAc;IAiCtB,OAAO,CAAC,YAAY;CAWrB;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,0BAA0B,GAAG,aAAa,CAEtF;AAED,YAAY,EAAE,4BAA4B,EAAE,CAAC"}
|
package/dist/executor.js
CHANGED
|
@@ -18,7 +18,7 @@ import { applyPagination, overfetchLimit } from './utils/pagination.js';
|
|
|
18
18
|
import { SemanticQueryCache, } from './cache/semantic-query-cache.js';
|
|
19
19
|
import { buildDatasetQuerySignature, buildMetricQuerySignature, } from './cache/query-signature.js';
|
|
20
20
|
import { isQualifiedField, resolveQualifiedField } from './utils/relationship-fields.js';
|
|
21
|
-
import { validateQualifiedFilter } from './utils/relationship-validation.js';
|
|
21
|
+
import { validateQualifiedFilter, validateRelationshipTenantRuntime, } from './utils/relationship-validation.js';
|
|
22
22
|
import { applyRelationshipJoins, buildRelationshipBuilderContext, qualifyBaseColumn, } from './utils/relationship-builder-plan.js';
|
|
23
23
|
function validateQuery(metric, query, context) {
|
|
24
24
|
const errors = [];
|
|
@@ -38,6 +38,10 @@ function validateQuery(metric, query, context) {
|
|
|
38
38
|
if (tenantRuntimeError) {
|
|
39
39
|
errors.push(tenantRuntimeError);
|
|
40
40
|
}
|
|
41
|
+
const relationshipTenantError = validateRelationshipTenantRuntime(ds, query, context);
|
|
42
|
+
if (relationshipTenantError) {
|
|
43
|
+
errors.push(relationshipTenantError);
|
|
44
|
+
}
|
|
41
45
|
if (metric.__type === 'grained_metric_ref' && query.by && query.by !== metric.grain) {
|
|
42
46
|
errors.push(`Metric "${ref.name}" is already grained by "${metric.grain}" and cannot be queried with by="${query.by}".`);
|
|
43
47
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ export type { DatasetCatalog, DatasetCatalogMap, DatasetCatalogSource, Dimension
|
|
|
12
12
|
export { serializeSemanticContract, contractToStableJson, hashContract, SEMANTIC_CONTRACT_VERSION, } from './contract.js';
|
|
13
13
|
export { buildProtocolDatasetContract } from './protocol-adapter.js';
|
|
14
14
|
export type { BuildProtocolDatasetContractOptions } from './protocol-adapter.js';
|
|
15
|
+
export { compilePortableSqlExpression, DEFAULT_SQL_PORTABILITY_LIMITS, } from './sql-portability.js';
|
|
16
|
+
export type { SqlPortabilityIssue, SqlPortabilityIssueCode, SqlPortabilityLimits, SqlPortabilityOptions, SqlPortabilityResult, } from './sql-portability.js';
|
|
15
17
|
export type { SemanticContract, SerializeSemanticContractOptions, ContractDataset, ContractDimension, ContractMeasure, ContractMetric, ContractFilter, ContractRelationship, } from './contract.js';
|
|
16
18
|
export { generateDatasetTools, toOpenAITools, toAISDKTools, toMcpTools, } from './tools.js';
|
|
17
19
|
export type { AISDKToolDefinition, DatasetToolAnalytics, DatasetToolMode, GenerateDatasetToolsOptions, JsonSchema, McpToolDefinition, OpenAIToolDefinition, SemanticToolDefinition, } from './tools.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAGvC,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAGvC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAGvC,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAGhE,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,aAAa,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAGnI,OAAO,EACL,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,EAC/B,UAAU,EAAE,QAAQ,EACpB,KAAK,EAAE,KAAK,EAAE,IAAI,GACnB,MAAM,eAAe,CAAC;AAGvB,OAAO,EACL,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE,GAAG,EACzB,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,EAChC,GAAG,EAAE,IAAI,EACT,MAAM,EAAE,KAAK,GACd,MAAM,oBAAoB,CAAC;AAG5B,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAGtD,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,8BAA8B,EAAE,MAAM,cAAc,CAAC;AACrG,OAAO,EAAE,+BAA+B,EAAE,MAAM,gCAAgC,CAAC;AACjF,YAAY,EACV,cAAc,EACd,iBAAiB,EACjB,oBAAoB,EACpB,qBAAqB,EACrB,mBAAmB,EACnB,kBAAkB,EAClB,kBAAkB,EAClB,wBAAwB,GACzB,MAAM,cAAc,CAAC;AAGtB,OAAO,EACL,yBAAyB,EACzB,oBAAoB,EACpB,YAAY,EACZ,yBAAyB,GAC1B,MAAM,eAAe,CAAC;AAGvB,OAAO,EAAE,4BAA4B,EAAE,MAAM,uBAAuB,CAAC;AACrE,YAAY,EAAE,mCAAmC,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAGvC,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAGvC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAGvC,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAGhE,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,aAAa,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAGnI,OAAO,EACL,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,EAC/B,UAAU,EAAE,QAAQ,EACpB,KAAK,EAAE,KAAK,EAAE,IAAI,GACnB,MAAM,eAAe,CAAC;AAGvB,OAAO,EACL,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE,GAAG,EACzB,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,EAChC,GAAG,EAAE,IAAI,EACT,MAAM,EAAE,KAAK,GACd,MAAM,oBAAoB,CAAC;AAG5B,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAGtD,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,8BAA8B,EAAE,MAAM,cAAc,CAAC;AACrG,OAAO,EAAE,+BAA+B,EAAE,MAAM,gCAAgC,CAAC;AACjF,YAAY,EACV,cAAc,EACd,iBAAiB,EACjB,oBAAoB,EACpB,qBAAqB,EACrB,mBAAmB,EACnB,kBAAkB,EAClB,kBAAkB,EAClB,wBAAwB,GACzB,MAAM,cAAc,CAAC;AAGtB,OAAO,EACL,yBAAyB,EACzB,oBAAoB,EACpB,YAAY,EACZ,yBAAyB,GAC1B,MAAM,eAAe,CAAC;AAGvB,OAAO,EAAE,4BAA4B,EAAE,MAAM,uBAAuB,CAAC;AACrE,YAAY,EAAE,mCAAmC,EAAE,MAAM,uBAAuB,CAAC;AAGjF,OAAO,EACL,4BAA4B,EAC5B,8BAA8B,GAC/B,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EACV,mBAAmB,EACnB,uBAAuB,EACvB,oBAAoB,EACpB,qBAAqB,EACrB,oBAAoB,GACrB,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EACV,gBAAgB,EAChB,gCAAgC,EAChC,eAAe,EACf,iBAAiB,EACjB,eAAe,EACf,cAAc,EACd,cAAc,EACd,oBAAoB,GACrB,MAAM,eAAe,CAAC;AAGvB,OAAO,EACL,oBAAoB,EACpB,aAAa,EACb,YAAY,EACZ,UAAU,GACX,MAAM,YAAY,CAAC;AACpB,YAAY,EACV,mBAAmB,EACnB,oBAAoB,EACpB,eAAe,EACf,2BAA2B,EAC3B,UAAU,EACV,iBAAiB,EACjB,oBAAoB,EACpB,sBAAsB,GACvB,MAAM,YAAY,CAAC;AAGpB,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACpD,YAAY,EAAE,aAAa,EAAE,0BAA0B,EAAE,MAAM,eAAe,CAAC;AAG/E,OAAO,EAAE,sBAAsB,EAAE,MAAM,iCAAiC,CAAC;AACzE,YAAY,EACV,kBAAkB,EAClB,qBAAqB,EACrB,oBAAoB,EACpB,oBAAoB,EACpB,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EACL,0BAA0B,EAC1B,yBAAyB,GAC1B,MAAM,4BAA4B,CAAC;AAIpC,OAAO,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAC5E,YAAY,EACV,QAAQ,EACR,eAAe,EACf,qBAAqB,EACrB,kBAAkB,EAClB,uBAAuB,EACvB,qBAAqB,EACrB,iBAAiB,GAClB,MAAM,oBAAoB,CAAC;AAG5B,YAAY,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACxD,OAAO,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAGxE,YAAY,EACV,gBAAgB,EAChB,uBAAuB,EACvB,6BAA6B,EAC7B,wBAAwB,GACzB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAE,qBAAqB,EAAE,MAAM,6BAA6B,CAAC;AAGpE,OAAO,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAGhG,OAAO,EAAE,eAAe,EAAE,yBAAyB,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AAGnG,YAAY,EACV,SAAS,EACT,aAAa,EACb,gBAAgB,EAChB,mBAAmB,EACnB,cAAc,EACd,iBAAiB,EACjB,kBAAkB,EAClB,gBAAgB,EAChB,sBAAsB,EACtB,eAAe,EACf,kBAAkB,EAClB,eAAe,EACf,WAAW,EACX,iBAAiB,EACjB,SAAS,EACT,SAAS,EACT,aAAa,EACb,gBAAgB,EAChB,gBAAgB,EAChB,cAAc,EACd,YAAY,EACZ,aAAa,EACb,WAAW,EACX,YAAY,EACZ,gBAAgB,EAChB,YAAY,EACZ,kBAAkB,EAClB,YAAY,EACZ,gBAAgB,EAChB,wBAAwB,EACxB,qBAAqB,EACrB,wBAAwB,EACxB,yBAAyB,EACzB,aAAa,EACb,aAAa,EACb,eAAe,EACf,kBAAkB,EAClB,gBAAgB,EAChB,mBAAmB,EACnB,uBAAuB,EACvB,iBAAiB,EACjB,qBAAqB,EACrB,0BAA0B,EAC1B,mBAAmB,EACnB,qBAAqB,EACrB,eAAe,EACf,UAAU,EACV,aAAa,EACb,qBAAqB,EACrB,cAAc,EACd,SAAS,EACT,YAAY,EACZ,eAAe,EACf,eAAe,GAChB,MAAM,YAAY,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -21,6 +21,8 @@ export { listQueryableRelationshipFields } from './utils/relationship-fields.js'
|
|
|
21
21
|
export { serializeSemanticContract, contractToStableJson, hashContract, SEMANTIC_CONTRACT_VERSION, } from './contract.js';
|
|
22
22
|
// Portable deployment contract adapter
|
|
23
23
|
export { buildProtocolDatasetContract } from './protocol-adapter.js';
|
|
24
|
+
// SQL portability compiler (R1A-07)
|
|
25
|
+
export { compilePortableSqlExpression, DEFAULT_SQL_PORTABILITY_LIMITS, } from './sql-portability.js';
|
|
24
26
|
// Agent/tool metadata
|
|
25
27
|
export { generateDatasetTools, toOpenAITools, toAISDKTools, toMcpTools, } from './tools.js';
|
|
26
28
|
// Dataset client
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type ProtocolExpression } from '@hypequery/protocol';
|
|
2
|
+
/**
|
|
3
|
+
* SQL portability compiler (R1A-07).
|
|
4
|
+
*
|
|
5
|
+
* Parses the supported ClickHouse SQL-expression subset used by SQL-backed
|
|
6
|
+
* dimensions and measures into an RFC 0003 expression AST. Unsupported syntax
|
|
7
|
+
* becomes a source-located incompatibility issue instead of executing or
|
|
8
|
+
* silently changing meaning.
|
|
9
|
+
*/
|
|
10
|
+
export type SqlPortabilityIssueCode = 'HQ_SQL_PORT_SYNTAX' | 'HQ_SQL_PORT_UNSUPPORTED_FUNCTION' | 'HQ_SQL_PORT_UNSUPPORTED_OPERATOR' | 'HQ_SQL_PORT_UNSUPPORTED_LITERAL' | 'HQ_SQL_PORT_UNSUPPORTED_SYNTAX' | 'HQ_SQL_PORT_TOO_COMPLEX' | 'HQ_SQL_PORT_TOO_LARGE';
|
|
11
|
+
export interface SqlPortabilityIssue {
|
|
12
|
+
readonly code: SqlPortabilityIssueCode;
|
|
13
|
+
readonly message: string;
|
|
14
|
+
readonly start: number;
|
|
15
|
+
readonly end: number;
|
|
16
|
+
}
|
|
17
|
+
export interface SqlPortabilityLimits {
|
|
18
|
+
readonly maxInputBytes: number;
|
|
19
|
+
readonly maxDepth: number;
|
|
20
|
+
readonly maxNodes: number;
|
|
21
|
+
}
|
|
22
|
+
export interface SqlPortabilityOptions {
|
|
23
|
+
readonly limits?: Partial<SqlPortabilityLimits>;
|
|
24
|
+
}
|
|
25
|
+
export interface SqlPortabilitySuccess {
|
|
26
|
+
readonly portable: true;
|
|
27
|
+
readonly expression: ProtocolExpression;
|
|
28
|
+
readonly dependencies: readonly string[];
|
|
29
|
+
}
|
|
30
|
+
export interface SqlPortabilityFailure {
|
|
31
|
+
readonly portable: false;
|
|
32
|
+
readonly issues: readonly SqlPortabilityIssue[];
|
|
33
|
+
}
|
|
34
|
+
export type SqlPortabilityResult = SqlPortabilitySuccess | SqlPortabilityFailure;
|
|
35
|
+
export declare const DEFAULT_SQL_PORTABILITY_LIMITS: Readonly<SqlPortabilityLimits>;
|
|
36
|
+
/**
|
|
37
|
+
* Compile a SQL expression fragment into an RFC 0003 expression. The result
|
|
38
|
+
* expression always passes RFC 0003 validation; dependencies are the sorted,
|
|
39
|
+
* deduplicated referenced identifiers.
|
|
40
|
+
*/
|
|
41
|
+
export declare function compilePortableSqlExpression(sql: string, options?: SqlPortabilityOptions): SqlPortabilityResult;
|
|
42
|
+
//# sourceMappingURL=sql-portability.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sql-portability.d.ts","sourceRoot":"","sources":["../src/sql-portability.ts"],"names":[],"mappings":"AAAA,OAAO,EAML,KAAK,kBAAkB,EAExB,MAAM,qBAAqB,CAAC;AAE7B;;;;;;;GAOG;AAEH,MAAM,MAAM,uBAAuB,GAC/B,oBAAoB,GACpB,kCAAkC,GAClC,kCAAkC,GAClC,iCAAiC,GACjC,gCAAgC,GAChC,yBAAyB,GACzB,uBAAuB,CAAC;AAE5B,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,IAAI,EAAE,uBAAuB,CAAC;IACvC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,oBAAoB,CAAC,CAAC;CACjD;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAC;IACxB,QAAQ,CAAC,UAAU,EAAE,kBAAkB,CAAC;IACxC,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;CAC1C;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,SAAS,mBAAmB,EAAE,CAAC;CACjD;AAED,MAAM,MAAM,oBAAoB,GAAG,qBAAqB,GAAG,qBAAqB,CAAC;AAEjF,eAAO,MAAM,8BAA8B,EAAE,QAAQ,CAAC,oBAAoB,CAKtE,CAAC;AAouBL;;;;GAIG;AACH,wBAAgB,4BAA4B,CAC1C,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,qBAA0B,GAClC,oBAAoB,CAyBtB"}
|
|
@@ -0,0 +1,641 @@
|
|
|
1
|
+
import { parseProtocolQualifiedIdentifier, ProtocolIdentifierError, validateProtocolExpression, } from '@hypequery/protocol';
|
|
2
|
+
export const DEFAULT_SQL_PORTABILITY_LIMITS = Object.freeze({
|
|
3
|
+
maxInputBytes: 65_536,
|
|
4
|
+
maxDepth: 16,
|
|
5
|
+
maxNodes: 1_000,
|
|
6
|
+
});
|
|
7
|
+
function resolveSqlPortabilityLimits(options = {}) {
|
|
8
|
+
const limits = { ...DEFAULT_SQL_PORTABILITY_LIMITS };
|
|
9
|
+
for (const key of Object.keys(limits)) {
|
|
10
|
+
const value = options.limits?.[key];
|
|
11
|
+
if (value === undefined)
|
|
12
|
+
continue;
|
|
13
|
+
const maximum = DEFAULT_SQL_PORTABILITY_LIMITS[key];
|
|
14
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > maximum) {
|
|
15
|
+
throw new RangeError(`${key} must be a positive safe integer no greater than ${maximum} `
|
|
16
|
+
+ '(the SQL portability v1 maximum)');
|
|
17
|
+
}
|
|
18
|
+
limits[key] = value;
|
|
19
|
+
}
|
|
20
|
+
return Object.freeze(limits);
|
|
21
|
+
}
|
|
22
|
+
class SqlPortabilityError extends Error {
|
|
23
|
+
issue;
|
|
24
|
+
constructor(issue) {
|
|
25
|
+
super(issue.message);
|
|
26
|
+
this.name = 'SqlPortabilityError';
|
|
27
|
+
this.issue = issue;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function fail(code, message, start, end) {
|
|
31
|
+
throw new SqlPortabilityError({ code, message, start, end });
|
|
32
|
+
}
|
|
33
|
+
const BARE_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*/;
|
|
34
|
+
/** Keywords with direct AST support, keyed by their uppercase spelling. */
|
|
35
|
+
const KEYWORDS = Object.freeze({
|
|
36
|
+
AND: 'and',
|
|
37
|
+
OR: 'or',
|
|
38
|
+
NOT: 'not',
|
|
39
|
+
IN: 'in',
|
|
40
|
+
BETWEEN: 'between',
|
|
41
|
+
LIKE: 'like',
|
|
42
|
+
TRUE: 'true',
|
|
43
|
+
FALSE: 'false',
|
|
44
|
+
NULL: 'null',
|
|
45
|
+
});
|
|
46
|
+
/** Reserved words that prove the input is not a portable expression. */
|
|
47
|
+
const UNSUPPORTED_KEYWORDS = new Set([
|
|
48
|
+
'ALTER', 'AS', 'CASE', 'CAST', 'CREATE', 'DATABASE', 'DELETE', 'DROP',
|
|
49
|
+
'ELSE', 'END', 'EXCEPT', 'EXISTS', 'FORMAT', 'FROM', 'GROUP', 'HAVING',
|
|
50
|
+
'INSERT', 'INTERSECT', 'INTO', 'JOIN', 'LAMBDA', 'LIMIT', 'ORDER', 'OVER',
|
|
51
|
+
'PARTITION', 'PREWHERE', 'SELECT', 'SETTINGS', 'TABLE', 'THEN', 'TRUNCATE',
|
|
52
|
+
'UNION', 'UPDATE', 'WHEN', 'WHERE', 'WINDOW', 'WITH',
|
|
53
|
+
]);
|
|
54
|
+
const BARE_IDENTIFIER_FULL = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
55
|
+
function tokenize(sql) {
|
|
56
|
+
const tokens = [];
|
|
57
|
+
let index = 0;
|
|
58
|
+
const push = (type, value, start, end) => {
|
|
59
|
+
tokens.push({ type, value, start, end });
|
|
60
|
+
};
|
|
61
|
+
while (index < sql.length) {
|
|
62
|
+
const start = index;
|
|
63
|
+
const char = sql[index];
|
|
64
|
+
if (char === ' ' || char === '\t' || char === '\n' || char === '\r') {
|
|
65
|
+
index += 1;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (char === '-' && sql[index + 1] === '-') {
|
|
69
|
+
fail('HQ_SQL_PORT_UNSUPPORTED_SYNTAX', 'Comments are not portable.', start, start + 2);
|
|
70
|
+
}
|
|
71
|
+
if (char === '/' && sql[index + 1] === '*') {
|
|
72
|
+
fail('HQ_SQL_PORT_UNSUPPORTED_SYNTAX', 'Comments are not portable.', start, start + 2);
|
|
73
|
+
}
|
|
74
|
+
if ('(),'.includes(char)) {
|
|
75
|
+
push('operator', char, start, start + 1);
|
|
76
|
+
index += 1;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (char === '+' || char === '-' || char === '*' || char === '/') {
|
|
80
|
+
push('operator', char, start, start + 1);
|
|
81
|
+
index += 1;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (char === '=' && sql[index + 1] === '=') {
|
|
85
|
+
fail('HQ_SQL_PORT_UNSUPPORTED_OPERATOR', 'Operator "==" is not portable; use "=" for equality.', start, start + 2);
|
|
86
|
+
}
|
|
87
|
+
if (char === '=') {
|
|
88
|
+
push('operator', '=', start, start + 1);
|
|
89
|
+
index += 1;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (char === '!' && sql[index + 1] === '=') {
|
|
93
|
+
push('operator', '!=', start, start + 2);
|
|
94
|
+
index += 2;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (char === '<' && sql[index + 1] === '>') {
|
|
98
|
+
push('operator', '!=', start, start + 2);
|
|
99
|
+
index += 2;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (char === '<' && sql[index + 1] === '=') {
|
|
103
|
+
push('operator', '<=', start, start + 2);
|
|
104
|
+
index += 2;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (char === '>' && sql[index + 1] === '=') {
|
|
108
|
+
push('operator', '>=', start, start + 2);
|
|
109
|
+
index += 2;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (char === '<' || char === '>') {
|
|
113
|
+
push('operator', char, start, start + 1);
|
|
114
|
+
index += 1;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (char === '%' || char === '^' || char === '&' || char === '|'
|
|
118
|
+
|| char === '~' || char === ':' || char === '?' || char === '@') {
|
|
119
|
+
fail('HQ_SQL_PORT_UNSUPPORTED_OPERATOR', `Operator "${char}" is not portable.`, start, start + 1);
|
|
120
|
+
}
|
|
121
|
+
if (char === ';' || char === '[' || char === ']' || char === '{' || char === '}'
|
|
122
|
+
|| char === '"' || char === '$' || char === '#') {
|
|
123
|
+
fail('HQ_SQL_PORT_UNSUPPORTED_SYNTAX', `Syntax "${char}" is not portable.`, start, start + 1);
|
|
124
|
+
}
|
|
125
|
+
if (char === '\'') {
|
|
126
|
+
index += 1;
|
|
127
|
+
let value = '';
|
|
128
|
+
let closed = false;
|
|
129
|
+
while (index < sql.length) {
|
|
130
|
+
const current = sql[index];
|
|
131
|
+
if (current === '\'') {
|
|
132
|
+
if (sql[index + 1] === '\'') {
|
|
133
|
+
value += '\'';
|
|
134
|
+
index += 2;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
closed = true;
|
|
138
|
+
index += 1;
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
if (current === '\\') {
|
|
142
|
+
fail('HQ_SQL_PORT_UNSUPPORTED_LITERAL', 'Backslash escapes are not portable; double the quote instead.', index, index + 1);
|
|
143
|
+
}
|
|
144
|
+
value += current;
|
|
145
|
+
index += 1;
|
|
146
|
+
}
|
|
147
|
+
if (!closed)
|
|
148
|
+
fail('HQ_SQL_PORT_SYNTAX', 'Unterminated string literal.', start, sql.length);
|
|
149
|
+
push('string', value, start, index);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (/[0-9]/.test(char) || (char === '.' && /[0-9]/.test(sql[index + 1] ?? ''))) {
|
|
153
|
+
const match = /^[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?|^\.[0-9]+(?:[eE][+-]?[0-9]+)?/
|
|
154
|
+
.exec(sql.slice(index));
|
|
155
|
+
const text = match[0];
|
|
156
|
+
push('number', text, start, start + text.length);
|
|
157
|
+
index += text.length;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (char === '`' || /[A-Za-z_]/.test(char)) {
|
|
161
|
+
const segments = [];
|
|
162
|
+
let bareOnly = true;
|
|
163
|
+
while (true) {
|
|
164
|
+
if (sql[index] === '`') {
|
|
165
|
+
bareOnly = false;
|
|
166
|
+
index += 1;
|
|
167
|
+
let segment = '';
|
|
168
|
+
let closed = false;
|
|
169
|
+
while (index < sql.length) {
|
|
170
|
+
if (sql[index] === '`') {
|
|
171
|
+
if (sql[index + 1] === '`') {
|
|
172
|
+
segment += '`';
|
|
173
|
+
index += 2;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
closed = true;
|
|
177
|
+
index += 1;
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
segment += sql[index];
|
|
181
|
+
index += 1;
|
|
182
|
+
}
|
|
183
|
+
if (!closed) {
|
|
184
|
+
fail('HQ_SQL_PORT_SYNTAX', 'Unterminated quoted identifier.', start, sql.length);
|
|
185
|
+
}
|
|
186
|
+
if (!BARE_IDENTIFIER_FULL.test(segment)) {
|
|
187
|
+
fail('HQ_SQL_PORT_SYNTAX', `Quoted identifier "${segment}" is not portable.`, start, index);
|
|
188
|
+
}
|
|
189
|
+
segments.push(segment);
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
const bare = BARE_IDENTIFIER.exec(sql.slice(index));
|
|
193
|
+
if (!bare)
|
|
194
|
+
break;
|
|
195
|
+
segments.push(bare[0]);
|
|
196
|
+
index += bare[0].length;
|
|
197
|
+
}
|
|
198
|
+
if (sql[index] === '.') {
|
|
199
|
+
if (sql[index + 1] === '`' || /[A-Za-z_]/.test(sql[index + 1] ?? '')) {
|
|
200
|
+
index += 1;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
fail('HQ_SQL_PORT_SYNTAX', 'Expected an identifier after ".".', index, index + 1);
|
|
204
|
+
}
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
const value = segments.join('.');
|
|
208
|
+
if (bareOnly && segments.length === 1) {
|
|
209
|
+
const keyword = KEYWORDS[value.toUpperCase()];
|
|
210
|
+
if (keyword !== undefined) {
|
|
211
|
+
push('keyword', keyword, start, index);
|
|
212
|
+
}
|
|
213
|
+
else if (UNSUPPORTED_KEYWORDS.has(value.toUpperCase())) {
|
|
214
|
+
fail('HQ_SQL_PORT_UNSUPPORTED_SYNTAX', `"${value}" is not part of the portable expression subset.`, start, index);
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
push('identifier', value, start, index);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
push('identifier', value, start, index);
|
|
222
|
+
}
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
fail('HQ_SQL_PORT_UNSUPPORTED_SYNTAX', `Unexpected character "${char}".`, start, start + 1);
|
|
226
|
+
}
|
|
227
|
+
return tokens;
|
|
228
|
+
}
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
230
|
+
// Parser
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
232
|
+
const CALL_ARITY = Object.freeze({
|
|
233
|
+
nullifzero: [1, 1],
|
|
234
|
+
coalesce: [2, 2],
|
|
235
|
+
round: [1, 2],
|
|
236
|
+
floor: [1, 1],
|
|
237
|
+
ceil: [1, 1],
|
|
238
|
+
});
|
|
239
|
+
const CALL_CANONICAL = Object.freeze({
|
|
240
|
+
nullifzero: 'nullIfZero',
|
|
241
|
+
coalesce: 'coalesce',
|
|
242
|
+
round: 'round',
|
|
243
|
+
floor: 'floor',
|
|
244
|
+
ceil: 'ceil',
|
|
245
|
+
});
|
|
246
|
+
const BINARY_OPERATORS = Object.freeze({
|
|
247
|
+
'+': 'add',
|
|
248
|
+
'-': 'subtract',
|
|
249
|
+
'*': 'multiply',
|
|
250
|
+
'/': 'divide',
|
|
251
|
+
});
|
|
252
|
+
const COMPARISON_OPERATORS = Object.freeze({
|
|
253
|
+
'=': 'eq',
|
|
254
|
+
'!=': 'neq',
|
|
255
|
+
'>': 'gt',
|
|
256
|
+
'>=': 'gte',
|
|
257
|
+
'<': 'lt',
|
|
258
|
+
'<=': 'lte',
|
|
259
|
+
});
|
|
260
|
+
function node(expression) {
|
|
261
|
+
return Object.freeze(expression);
|
|
262
|
+
}
|
|
263
|
+
class Parser {
|
|
264
|
+
tokens;
|
|
265
|
+
limits;
|
|
266
|
+
position = 0;
|
|
267
|
+
nodes = 0;
|
|
268
|
+
dependencies = new Set();
|
|
269
|
+
constructor(tokens, limits) {
|
|
270
|
+
this.tokens = tokens;
|
|
271
|
+
this.limits = limits;
|
|
272
|
+
}
|
|
273
|
+
parse() {
|
|
274
|
+
if (this.tokens.length === 0)
|
|
275
|
+
fail('HQ_SQL_PORT_SYNTAX', 'The expression is empty.', 0, 0);
|
|
276
|
+
const expression = this.parseOr(0);
|
|
277
|
+
const rest = this.peek();
|
|
278
|
+
if (rest)
|
|
279
|
+
fail('HQ_SQL_PORT_SYNTAX', 'Unexpected trailing input.', rest.start, rest.end);
|
|
280
|
+
return {
|
|
281
|
+
expression,
|
|
282
|
+
dependencies: Object.freeze([...this.dependencies].sort()),
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
peek() {
|
|
286
|
+
return this.tokens[this.position];
|
|
287
|
+
}
|
|
288
|
+
next() {
|
|
289
|
+
const token = this.tokens[this.position];
|
|
290
|
+
if (!token) {
|
|
291
|
+
const end = this.tokens.length === 0 ? 0 : this.tokens[this.tokens.length - 1].end;
|
|
292
|
+
fail('HQ_SQL_PORT_SYNTAX', 'Unexpected end of expression.', end, end);
|
|
293
|
+
}
|
|
294
|
+
this.position += 1;
|
|
295
|
+
return token;
|
|
296
|
+
}
|
|
297
|
+
enter(depth, token) {
|
|
298
|
+
this.nodes += 1;
|
|
299
|
+
if (this.nodes > this.limits.maxNodes) {
|
|
300
|
+
fail('HQ_SQL_PORT_TOO_COMPLEX', 'The expression exceeds its node limit.', token.start, token.end);
|
|
301
|
+
}
|
|
302
|
+
if (depth > this.limits.maxDepth) {
|
|
303
|
+
fail('HQ_SQL_PORT_TOO_COMPLEX', 'The expression exceeds its depth limit.', token.start, token.end);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
parseOr(depth) {
|
|
307
|
+
const operands = [this.parseAnd(depth)];
|
|
308
|
+
while (this.peek()?.type === 'keyword' && this.peek().value === 'or') {
|
|
309
|
+
this.next();
|
|
310
|
+
operands.push(this.parseAnd(depth));
|
|
311
|
+
}
|
|
312
|
+
if (operands.length === 1)
|
|
313
|
+
return operands[0];
|
|
314
|
+
this.enter(depth, this.tokens[this.position - 1]);
|
|
315
|
+
return node({ kind: 'logical', operator: 'or', operands: Object.freeze(operands) });
|
|
316
|
+
}
|
|
317
|
+
parseAnd(depth) {
|
|
318
|
+
const operands = [this.parseNot(depth)];
|
|
319
|
+
while (this.peek()?.type === 'keyword' && this.peek().value === 'and') {
|
|
320
|
+
this.next();
|
|
321
|
+
operands.push(this.parseNot(depth));
|
|
322
|
+
}
|
|
323
|
+
if (operands.length === 1)
|
|
324
|
+
return operands[0];
|
|
325
|
+
this.enter(depth, this.tokens[this.position - 1]);
|
|
326
|
+
return node({ kind: 'logical', operator: 'and', operands: Object.freeze(operands) });
|
|
327
|
+
}
|
|
328
|
+
parseNot(depth) {
|
|
329
|
+
const token = this.peek();
|
|
330
|
+
if (token?.type === 'keyword' && token.value === 'not') {
|
|
331
|
+
this.next();
|
|
332
|
+
this.enter(depth + 1, token);
|
|
333
|
+
return node({
|
|
334
|
+
kind: 'logical',
|
|
335
|
+
operator: 'not',
|
|
336
|
+
operand: this.parseNot(depth + 1),
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
return this.parseComparison(depth);
|
|
340
|
+
}
|
|
341
|
+
parseComparison(depth) {
|
|
342
|
+
const left = this.parseAdditive(depth);
|
|
343
|
+
const token = this.peek();
|
|
344
|
+
if (!token)
|
|
345
|
+
return left;
|
|
346
|
+
if (token.type === 'operator' && COMPARISON_OPERATORS[token.value] !== undefined) {
|
|
347
|
+
this.next();
|
|
348
|
+
const operator = COMPARISON_OPERATORS[token.value];
|
|
349
|
+
const right = this.parseAdditive(depth);
|
|
350
|
+
this.enter(depth + 1, token);
|
|
351
|
+
this.rejectChained();
|
|
352
|
+
return node({ kind: 'comparison', operator, left, right });
|
|
353
|
+
}
|
|
354
|
+
if (token.type === 'keyword' && token.value === 'like') {
|
|
355
|
+
this.next();
|
|
356
|
+
const right = this.parseAdditive(depth);
|
|
357
|
+
this.enter(depth + 1, token);
|
|
358
|
+
this.rejectChained();
|
|
359
|
+
return node({ kind: 'comparison', operator: 'like', left, right });
|
|
360
|
+
}
|
|
361
|
+
if (token.type === 'keyword' && token.value === 'between') {
|
|
362
|
+
this.next();
|
|
363
|
+
const lower = this.parseAdditive(depth);
|
|
364
|
+
const junction = this.peek();
|
|
365
|
+
if (junction?.type !== 'keyword' || junction.value !== 'and') {
|
|
366
|
+
fail('HQ_SQL_PORT_SYNTAX', 'BETWEEN requires an AND separator.', junction?.start ?? token.end, junction?.end ?? token.end);
|
|
367
|
+
}
|
|
368
|
+
this.next();
|
|
369
|
+
const upper = this.parseAdditive(depth);
|
|
370
|
+
this.enter(depth + 1, token);
|
|
371
|
+
this.rejectChained();
|
|
372
|
+
return node({
|
|
373
|
+
kind: 'comparison',
|
|
374
|
+
operator: 'between',
|
|
375
|
+
left,
|
|
376
|
+
right: this.tupleLiteral(lower, upper, token),
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
if (token.type === 'keyword' && (token.value === 'in' || token.value === 'not')) {
|
|
380
|
+
this.next();
|
|
381
|
+
if (token.value === 'not') {
|
|
382
|
+
const following = this.peek();
|
|
383
|
+
if (following?.type === 'keyword' && following.value === 'in') {
|
|
384
|
+
this.next();
|
|
385
|
+
return this.parseInList(left, 'notIn', token, depth);
|
|
386
|
+
}
|
|
387
|
+
fail('HQ_SQL_PORT_UNSUPPORTED_OPERATOR', 'Only NOT IN is portable; NOT LIKE and NOT BETWEEN are not.', token.start, (following ?? token).end);
|
|
388
|
+
}
|
|
389
|
+
return this.parseInList(left, 'in', token, depth);
|
|
390
|
+
}
|
|
391
|
+
return left;
|
|
392
|
+
}
|
|
393
|
+
rejectChained() {
|
|
394
|
+
const token = this.peek();
|
|
395
|
+
if (token && (token.type === 'operator' && COMPARISON_OPERATORS[token.value] !== undefined
|
|
396
|
+
|| token.type === 'keyword'
|
|
397
|
+
&& ['like', 'between', 'in'].includes(token.value))) {
|
|
398
|
+
fail('HQ_SQL_PORT_SYNTAX', 'Chained comparisons are not portable; combine them with AND/OR.', token.start, token.end);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
tupleLiteral(lower, upper, token) {
|
|
402
|
+
for (const bound of [lower, upper]) {
|
|
403
|
+
if (bound.kind !== 'literal') {
|
|
404
|
+
fail('HQ_SQL_PORT_UNSUPPORTED_SYNTAX', 'BETWEEN bounds must be literal values.', token.start, token.end);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
const bounds = [lower, upper];
|
|
408
|
+
return node({
|
|
409
|
+
kind: 'literal',
|
|
410
|
+
value: {
|
|
411
|
+
$hypequery: {
|
|
412
|
+
type: 'tuple',
|
|
413
|
+
version: 1,
|
|
414
|
+
values: [bounds[0].value, bounds[1].value],
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
parseInList(left, operator, token, depth) {
|
|
420
|
+
const open = this.peek();
|
|
421
|
+
if (open?.type !== 'operator' || open.value !== '(') {
|
|
422
|
+
fail('HQ_SQL_PORT_SYNTAX', 'IN requires a parenthesized literal list.', token.start, token.end);
|
|
423
|
+
}
|
|
424
|
+
this.next();
|
|
425
|
+
const values = [];
|
|
426
|
+
if (this.peek()?.type === 'operator' && this.peek().value === ')') {
|
|
427
|
+
fail('HQ_SQL_PORT_SYNTAX', 'IN requires at least one element.', open.start, open.end);
|
|
428
|
+
}
|
|
429
|
+
while (true) {
|
|
430
|
+
const element = this.parseUnary(depth + 1);
|
|
431
|
+
if (element.kind !== 'literal') {
|
|
432
|
+
fail('HQ_SQL_PORT_UNSUPPORTED_SYNTAX', 'IN list elements must be literal values.', token.start, token.end);
|
|
433
|
+
}
|
|
434
|
+
values.push(element.value);
|
|
435
|
+
const separator = this.peek();
|
|
436
|
+
if (separator?.type === 'operator' && separator.value === ',') {
|
|
437
|
+
this.next();
|
|
438
|
+
const following = this.peek();
|
|
439
|
+
if (following?.type === 'operator' && following.value === ')') {
|
|
440
|
+
fail('HQ_SQL_PORT_SYNTAX', 'IN lists do not allow a trailing comma.', separator.start, following.end);
|
|
441
|
+
}
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
if (separator?.type === 'operator' && separator.value === ')') {
|
|
445
|
+
this.next();
|
|
446
|
+
break;
|
|
447
|
+
}
|
|
448
|
+
fail('HQ_SQL_PORT_SYNTAX', 'Expected "," or ")" in the IN list.', separator?.start ?? token.end, separator?.end ?? token.end);
|
|
449
|
+
}
|
|
450
|
+
this.enter(depth + 1, token);
|
|
451
|
+
this.rejectChained();
|
|
452
|
+
return node({
|
|
453
|
+
kind: 'comparison',
|
|
454
|
+
operator,
|
|
455
|
+
left,
|
|
456
|
+
right: {
|
|
457
|
+
kind: 'literal',
|
|
458
|
+
value: {
|
|
459
|
+
$hypequery: {
|
|
460
|
+
type: 'array',
|
|
461
|
+
version: 1,
|
|
462
|
+
values,
|
|
463
|
+
},
|
|
464
|
+
},
|
|
465
|
+
},
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
parseAdditive(depth) {
|
|
469
|
+
let left = this.parseMultiplicative(depth);
|
|
470
|
+
while (true) {
|
|
471
|
+
const token = this.peek();
|
|
472
|
+
if (token?.type !== 'operator' || (token.value !== '+' && token.value !== '-'))
|
|
473
|
+
break;
|
|
474
|
+
this.next();
|
|
475
|
+
const right = this.parseMultiplicative(depth + 1);
|
|
476
|
+
this.enter(depth + 1, token);
|
|
477
|
+
left = node({
|
|
478
|
+
kind: 'binary',
|
|
479
|
+
operator: BINARY_OPERATORS[token.value],
|
|
480
|
+
left,
|
|
481
|
+
right,
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
return left;
|
|
485
|
+
}
|
|
486
|
+
parseMultiplicative(depth) {
|
|
487
|
+
let left = this.parseUnary(depth);
|
|
488
|
+
while (true) {
|
|
489
|
+
const token = this.peek();
|
|
490
|
+
if (token?.type !== 'operator' || (token.value !== '*' && token.value !== '/'))
|
|
491
|
+
break;
|
|
492
|
+
this.next();
|
|
493
|
+
const right = this.parseUnary(depth + 1);
|
|
494
|
+
this.enter(depth + 1, token);
|
|
495
|
+
left = node({
|
|
496
|
+
kind: 'binary',
|
|
497
|
+
operator: BINARY_OPERATORS[token.value],
|
|
498
|
+
left,
|
|
499
|
+
right,
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
return left;
|
|
503
|
+
}
|
|
504
|
+
parseUnary(depth) {
|
|
505
|
+
let token = this.peek();
|
|
506
|
+
while (token?.type === 'operator' && token.value === '+') {
|
|
507
|
+
this.next();
|
|
508
|
+
token = this.peek();
|
|
509
|
+
}
|
|
510
|
+
if (token?.type === 'operator' && token.value === '-') {
|
|
511
|
+
this.next();
|
|
512
|
+
const operand = this.peek();
|
|
513
|
+
if (operand?.type !== 'number') {
|
|
514
|
+
fail('HQ_SQL_PORT_UNSUPPORTED_OPERATOR', 'Negation is only portable on numeric literals.', token.start, token.end);
|
|
515
|
+
}
|
|
516
|
+
this.next();
|
|
517
|
+
return this.numericLiteral(operand, true);
|
|
518
|
+
}
|
|
519
|
+
return this.parsePrimary(depth);
|
|
520
|
+
}
|
|
521
|
+
parsePrimary(depth) {
|
|
522
|
+
const token = this.next();
|
|
523
|
+
this.enter(depth, token);
|
|
524
|
+
switch (token.type) {
|
|
525
|
+
case 'number':
|
|
526
|
+
return this.numericLiteral(token, false);
|
|
527
|
+
case 'string':
|
|
528
|
+
return node({ kind: 'literal', value: token.value });
|
|
529
|
+
case 'keyword':
|
|
530
|
+
if (token.value === 'true')
|
|
531
|
+
return node({ kind: 'literal', value: true });
|
|
532
|
+
if (token.value === 'false')
|
|
533
|
+
return node({ kind: 'literal', value: false });
|
|
534
|
+
if (token.value === 'null')
|
|
535
|
+
return node({ kind: 'literal', value: null });
|
|
536
|
+
break;
|
|
537
|
+
case 'identifier': {
|
|
538
|
+
const open = this.peek();
|
|
539
|
+
if (open?.type === 'operator' && open.value === '(') {
|
|
540
|
+
return this.parseCall(token, depth);
|
|
541
|
+
}
|
|
542
|
+
let name;
|
|
543
|
+
try {
|
|
544
|
+
name = parseProtocolQualifiedIdentifier(token.value);
|
|
545
|
+
}
|
|
546
|
+
catch (error) {
|
|
547
|
+
if (error instanceof ProtocolIdentifierError) {
|
|
548
|
+
fail('HQ_SQL_PORT_SYNTAX', `Identifier "${token.value}" is not portable.`, token.start, token.end);
|
|
549
|
+
}
|
|
550
|
+
throw error;
|
|
551
|
+
}
|
|
552
|
+
this.dependencies.add(token.value);
|
|
553
|
+
return node({ kind: 'reference', name });
|
|
554
|
+
}
|
|
555
|
+
case 'operator':
|
|
556
|
+
if (token.value === '(') {
|
|
557
|
+
const expression = this.parseOr(depth + 1);
|
|
558
|
+
const close = this.peek();
|
|
559
|
+
if (close?.type !== 'operator' || close.value !== ')') {
|
|
560
|
+
fail('HQ_SQL_PORT_SYNTAX', 'Expected ")".', close?.start ?? token.end, close?.end ?? token.end);
|
|
561
|
+
}
|
|
562
|
+
this.next();
|
|
563
|
+
return expression;
|
|
564
|
+
}
|
|
565
|
+
break;
|
|
566
|
+
}
|
|
567
|
+
fail('HQ_SQL_PORT_SYNTAX', `Unexpected "${token.value}".`, token.start, token.end);
|
|
568
|
+
}
|
|
569
|
+
parseCall(name, depth) {
|
|
570
|
+
const key = name.value.toLowerCase();
|
|
571
|
+
const canonical = CALL_CANONICAL[key];
|
|
572
|
+
const arity = CALL_ARITY[key];
|
|
573
|
+
if (canonical === undefined || arity === undefined) {
|
|
574
|
+
const open = this.peek();
|
|
575
|
+
fail('HQ_SQL_PORT_UNSUPPORTED_FUNCTION', `Function "${name.value}" is not in the portable allowlist.`, name.start, open.end);
|
|
576
|
+
}
|
|
577
|
+
this.next(); // consume '('
|
|
578
|
+
const args = [];
|
|
579
|
+
if (!(this.peek()?.type === 'operator' && this.peek().value === ')')) {
|
|
580
|
+
while (true) {
|
|
581
|
+
args.push(this.parseOr(depth + 1));
|
|
582
|
+
const separator = this.peek();
|
|
583
|
+
if (separator?.type === 'operator' && separator.value === ',') {
|
|
584
|
+
this.next();
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
break;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
const close = this.peek();
|
|
591
|
+
if (close?.type !== 'operator' || close.value !== ')') {
|
|
592
|
+
fail('HQ_SQL_PORT_SYNTAX', 'Expected ")".', close?.start ?? name.end, close?.end ?? name.end);
|
|
593
|
+
}
|
|
594
|
+
this.next();
|
|
595
|
+
if (args.length < arity[0] || args.length > arity[1]) {
|
|
596
|
+
fail('HQ_SQL_PORT_SYNTAX', `Function "${canonical}" expects ${arity[0] === arity[1] ? arity[0] : `${arity[0]} to ${arity[1]}`} arguments.`, name.start, close.end);
|
|
597
|
+
}
|
|
598
|
+
return node({
|
|
599
|
+
kind: 'call',
|
|
600
|
+
function: canonical,
|
|
601
|
+
args: Object.freeze(args),
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
numericLiteral(token, negated) {
|
|
605
|
+
const value = Number(token.value) * (negated ? -1 : 1);
|
|
606
|
+
if (!Number.isFinite(value)
|
|
607
|
+
|| Object.is(value, -0)
|
|
608
|
+
|| (Number.isInteger(value) && !Number.isSafeInteger(value))) {
|
|
609
|
+
fail('HQ_SQL_PORT_UNSUPPORTED_LITERAL', `Numeric literal "${token.value}" is outside the portable range.`, token.start, token.end);
|
|
610
|
+
}
|
|
611
|
+
return node({ kind: 'literal', value });
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
const textEncoder = new TextEncoder();
|
|
615
|
+
/**
|
|
616
|
+
* Compile a SQL expression fragment into an RFC 0003 expression. The result
|
|
617
|
+
* expression always passes RFC 0003 validation; dependencies are the sorted,
|
|
618
|
+
* deduplicated referenced identifiers.
|
|
619
|
+
*/
|
|
620
|
+
export function compilePortableSqlExpression(sql, options = {}) {
|
|
621
|
+
const limits = resolveSqlPortabilityLimits(options);
|
|
622
|
+
try {
|
|
623
|
+
if (typeof sql !== 'string' || sql.length > limits.maxInputBytes
|
|
624
|
+
|| textEncoder.encode(sql).byteLength > limits.maxInputBytes) {
|
|
625
|
+
fail('HQ_SQL_PORT_TOO_LARGE', 'The expression exceeds its byte limit.', 0, typeof sql === 'string' ? sql.length : 0);
|
|
626
|
+
}
|
|
627
|
+
const parsed = new Parser(tokenize(sql), limits).parse();
|
|
628
|
+
const expression = validateProtocolExpression(parsed.expression);
|
|
629
|
+
return Object.freeze({
|
|
630
|
+
portable: true,
|
|
631
|
+
expression,
|
|
632
|
+
dependencies: parsed.dependencies,
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
catch (error) {
|
|
636
|
+
if (error instanceof SqlPortabilityError) {
|
|
637
|
+
return Object.freeze({ portable: false, issues: Object.freeze([error.issue]) });
|
|
638
|
+
}
|
|
639
|
+
throw error;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dataset-query-validation.d.ts","sourceRoot":"","sources":["../../src/utils/dataset-query-validation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,kBAAkB,EAClB,YAAY,EACZ,gBAAgB,EACjB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAuB,KAAK,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"dataset-query-validation.d.ts","sourceRoot":"","sources":["../../src/utils/dataset-query-validation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,kBAAkB,EAClB,YAAY,EACZ,gBAAgB,EACjB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAuB,KAAK,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAe9E,wBAAgB,yBAAyB,CACvC,EAAE,EAAE,kBAAkB,EACtB,KAAK,EAAE,YAAY,EACnB,OAAO,CAAC,EAAE,gBAAgB,GACzB,gBAAgB,CA8JlB"}
|
|
@@ -2,7 +2,7 @@ import { validateFilterValue } from '../validation.js';
|
|
|
2
2
|
import { SUPPORTED_TIME_GRAINS, isSupportedTimeGrain } from '../constants.js';
|
|
3
3
|
import { getRuntimeTenantPredicate, validateTenantRuntime, } from './tenant-runtime.js';
|
|
4
4
|
import { isQualifiedField, resolveQualifiedField, } from './relationship-fields.js';
|
|
5
|
-
import { validateQualifiedFilter } from './relationship-validation.js';
|
|
5
|
+
import { validateQualifiedFilter, validateRelationshipTenantRuntime, } from './relationship-validation.js';
|
|
6
6
|
export function validateDatasetQueryInput(ds, query, context) {
|
|
7
7
|
const errors = [];
|
|
8
8
|
const dimensionNames = Object.keys(ds.dimensions);
|
|
@@ -19,6 +19,10 @@ export function validateDatasetQueryInput(ds, query, context) {
|
|
|
19
19
|
if (tenantRuntimeError) {
|
|
20
20
|
errors.push(tenantRuntimeError);
|
|
21
21
|
}
|
|
22
|
+
const relationshipTenantError = validateRelationshipTenantRuntime(ds, query, context);
|
|
23
|
+
if (relationshipTenantError) {
|
|
24
|
+
errors.push(relationshipTenantError);
|
|
25
|
+
}
|
|
22
26
|
if (selectedDimensions.length === 0 && selectedMeasures.length === 0) {
|
|
23
27
|
errors.push(`Dataset "${ds.name}" query must select at least one dimension or measure.`);
|
|
24
28
|
}
|
|
@@ -2,7 +2,14 @@
|
|
|
2
2
|
* Shared validation for relationship-qualified filters, used by both the
|
|
3
3
|
* dataset-query and metric-query validators.
|
|
4
4
|
*/
|
|
5
|
-
import type { AnyDatasetInstance, ExecutionContext, MetricFilter } from '../types.js';
|
|
5
|
+
import type { AnyDatasetInstance, DatasetQuery, ExecutionContext, MetricFilter, MetricQuery } from '../types.js';
|
|
6
|
+
type RelationshipQuery = Pick<DatasetQuery & MetricQuery, 'dimensions' | 'filters' | 'orderBy'>;
|
|
7
|
+
/**
|
|
8
|
+
* Tenant-less base datasets may still reach tenant-scoped data through a
|
|
9
|
+
* relationship. Require either a concrete runtime tenant or the trusted
|
|
10
|
+
* cross-tenant scope whenever a query activates such a join.
|
|
11
|
+
*/
|
|
12
|
+
export declare function validateRelationshipTenantRuntime(ds: AnyDatasetInstance, query: RelationshipQuery, context?: ExecutionContext): string | undefined;
|
|
6
13
|
/**
|
|
7
14
|
* Validates a relationship-qualified filter (`relationship.field`). Returns an
|
|
8
15
|
* error message string, or null when the filter is valid.
|
|
@@ -12,4 +19,5 @@ import type { AnyDatasetInstance, ExecutionContext, MetricFilter } from '../type
|
|
|
12
19
|
* that column is rejected (the planner injects the tenant predicate instead).
|
|
13
20
|
*/
|
|
14
21
|
export declare function validateQualifiedFilter(ds: AnyDatasetInstance, filter: MetricFilter, context?: ExecutionContext): string | null;
|
|
22
|
+
export {};
|
|
15
23
|
//# sourceMappingURL=relationship-validation.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"relationship-validation.d.ts","sourceRoot":"","sources":["../../src/utils/relationship-validation.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EACV,kBAAkB,EAClB,gBAAgB,EAChB,YAAY,
|
|
1
|
+
{"version":3,"file":"relationship-validation.d.ts","sourceRoot":"","sources":["../../src/utils/relationship-validation.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EACV,kBAAkB,EAClB,YAAY,EACZ,gBAAgB,EAChB,YAAY,EACZ,WAAW,EACZ,MAAM,aAAa,CAAC;AAWrB,KAAK,iBAAiB,GAAG,IAAI,CAC3B,YAAY,GAAG,WAAW,EAC1B,YAAY,GAAG,SAAS,GAAG,SAAS,CACrC,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,iCAAiC,CAC/C,EAAE,EAAE,kBAAkB,EACtB,KAAK,EAAE,iBAAiB,EACxB,OAAO,CAAC,EAAE,gBAAgB,GACzB,MAAM,GAAG,SAAS,CAmBpB;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CACrC,EAAE,EAAE,kBAAkB,EACtB,MAAM,EAAE,YAAY,EACpB,OAAO,CAAC,EAAE,gBAAgB,GACzB,MAAM,GAAG,IAAI,CAgBf"}
|
|
@@ -3,8 +3,30 @@
|
|
|
3
3
|
* dataset-query and metric-query validators.
|
|
4
4
|
*/
|
|
5
5
|
import { validateFilterValue } from '../validation.js';
|
|
6
|
-
import { getRuntimeTenantPredicate } from './tenant-runtime.js';
|
|
7
|
-
import { resolveQualifiedField } from './relationship-fields.js';
|
|
6
|
+
import { getRuntimeTenantPredicate, hasTenantRuntime, } from './tenant-runtime.js';
|
|
7
|
+
import { isQualifiedField, resolveQualifiedField, } from './relationship-fields.js';
|
|
8
|
+
/**
|
|
9
|
+
* Tenant-less base datasets may still reach tenant-scoped data through a
|
|
10
|
+
* relationship. Require either a concrete runtime tenant or the trusted
|
|
11
|
+
* cross-tenant scope whenever a query activates such a join.
|
|
12
|
+
*/
|
|
13
|
+
export function validateRelationshipTenantRuntime(ds, query, context) {
|
|
14
|
+
if (ds.tenantKey || hasTenantRuntime(context)) {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
const referenced = [
|
|
18
|
+
...(query.dimensions ?? []),
|
|
19
|
+
...(query.filters ?? []).map((filter) => filter.field),
|
|
20
|
+
...(query.orderBy ?? []).map((order) => order.field),
|
|
21
|
+
].filter(isQualifiedField);
|
|
22
|
+
for (const name of referenced) {
|
|
23
|
+
const resolution = resolveQualifiedField(ds, name);
|
|
24
|
+
if (resolution?.resolved?.target.tenantKey) {
|
|
25
|
+
return `Dataset "${ds.name}" requires runtime tenant scoping because relationship "${resolution.resolved.relationshipName}" targets a tenant-scoped dataset.`;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
8
30
|
/**
|
|
9
31
|
* Validates a relationship-qualified filter (`relationship.field`). Returns an
|
|
10
32
|
* error message string, or null when the filter is valid.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hypequery/datasets",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.1",
|
|
4
4
|
"description": "Semantic layer for defining datasets, metrics, and semantic queries",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -37,7 +37,8 @@
|
|
|
37
37
|
"@types/node": "^18.19.80",
|
|
38
38
|
"typescript": "^5.7.3",
|
|
39
39
|
"@vitest/coverage-v8": "^3.2.6",
|
|
40
|
-
"vitest": "^3.2.6"
|
|
40
|
+
"vitest": "^3.2.6",
|
|
41
|
+
"@hypequery/protocol-conformance": "^0.9.0"
|
|
41
42
|
},
|
|
42
43
|
"repository": {
|
|
43
44
|
"type": "git",
|