@gscdump/engine 0.32.11 → 0.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_chunks/engine.mjs +45 -22
- package/dist/_chunks/pg-adapter.d.mts +30 -4
- package/dist/_chunks/resolver.mjs +201 -37
- package/dist/_chunks/schema.d.mts +0 -120
- package/dist/_chunks/schema.mjs +4 -8
- package/dist/_chunks/schema2.mjs +8 -6
- package/dist/_chunks/sink.d.mts +18 -12
- package/dist/_chunks/types.d.mts +3 -0
- package/dist/adapters/hyparquet.mjs +7 -4
- package/dist/adapters/r2-manifest.mjs +40 -29
- package/dist/entities.d.mts +2 -2
- package/dist/iceberg/index.d.mts +2 -2
- package/dist/iceberg/index.mjs +9 -9
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/ingest.d.mts +1 -6
- package/dist/ingest.mjs +0 -7
- package/dist/resolver/index.d.mts +91 -36
- package/dist/resolver/index.mjs +2 -2
- package/dist/rollups.d.mts +4 -4
- package/dist/rollups.mjs +28 -19
- package/package.json +3 -3
package/dist/_chunks/engine.mjs
CHANGED
|
@@ -6,6 +6,20 @@ import { compileLogicalQueryPlan, substituteNamedFiles } from "./parquet-plan.mj
|
|
|
6
6
|
import { sqlEscape } from "../sql-bind.mjs";
|
|
7
7
|
import { buildLogicalPlan } from "gscdump/query/plan";
|
|
8
8
|
import { normalizeUrl } from "gscdump/normalize";
|
|
9
|
+
const BUFFER_READ_BATCH_SIZE = 8;
|
|
10
|
+
async function registerBufferedFiles(db, files, dataSource, registered, signal) {
|
|
11
|
+
for (let i = 0; i < files.length; i += BUFFER_READ_BATCH_SIZE) {
|
|
12
|
+
signal?.throwIfAborted();
|
|
13
|
+
const batch = files.slice(i, i + BUFFER_READ_BATCH_SIZE);
|
|
14
|
+
const buffers = await Promise.all(batch.map((file) => dataSource.read(file.key, void 0, signal)));
|
|
15
|
+
for (let j = 0; j < batch.length; j++) {
|
|
16
|
+
signal?.throwIfAborted();
|
|
17
|
+
const file = batch[j];
|
|
18
|
+
await db.registerFileBuffer(file.name, buffers[j]);
|
|
19
|
+
registered.push(file.name);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
9
23
|
async function encodeBytes(db, table, rows) {
|
|
10
24
|
const inName = db.makeTempPath("json");
|
|
11
25
|
const outName = db.makeTempPath("parquet");
|
|
@@ -72,17 +86,19 @@ function createDuckDBCodec(factory) {
|
|
|
72
86
|
await db.dropFiles([outName]);
|
|
73
87
|
}
|
|
74
88
|
}
|
|
75
|
-
const inputs = await Promise.all(inputKeys.map((k) => dataSource.read(k)));
|
|
76
89
|
const inNames = [];
|
|
77
90
|
const outName = db.makeTempPath("parquet");
|
|
78
91
|
const registered = [];
|
|
79
|
-
|
|
92
|
+
const bufferedInputs = inputKeys.map((key) => {
|
|
80
93
|
const name = db.makeTempPath("parquet");
|
|
81
|
-
await db.registerFileBuffer(name, inputs[i]);
|
|
82
94
|
inNames.push(name);
|
|
83
|
-
|
|
84
|
-
|
|
95
|
+
return {
|
|
96
|
+
key,
|
|
97
|
+
name
|
|
98
|
+
};
|
|
99
|
+
});
|
|
85
100
|
try {
|
|
101
|
+
await registerBufferedFiles(db, bufferedInputs, dataSource, registered);
|
|
86
102
|
const fileList = inNames.map((n) => `'${sqlEscape(n)}'`).join(", ");
|
|
87
103
|
await db.query(`COPY (${dedupedMergeSql(ctx.table, fileList)}) TO '${sqlEscape(outName)}' (FORMAT PARQUET, COMPRESSION ZSTD)`);
|
|
88
104
|
registered.push(outName);
|
|
@@ -127,23 +143,26 @@ function createDuckDBExecutor(factory) {
|
|
|
127
143
|
const registered = [];
|
|
128
144
|
const totalFiles = Object.values(fileKeys).reduce((n, keys) => n + keys.length, 0);
|
|
129
145
|
const endRegister = profiler?.start("files.register", { files: totalFiles });
|
|
130
|
-
await Promise.all(Object.entries(fileKeys).map(async ([name, keys]) => {
|
|
131
|
-
const uris = keys.map((key) => dataSource.uri?.(key));
|
|
132
|
-
const buffers = await Promise.all(keys.map((key, i) => uris[i] !== void 0 ? Promise.resolve(void 0) : dataSource.read(key, void 0, signal)));
|
|
133
|
-
const resolved = [];
|
|
134
|
-
for (let i = 0; i < keys.length; i++) {
|
|
135
|
-
const uri = uris[i];
|
|
136
|
-
if (uri !== void 0) resolved.push(uri);
|
|
137
|
-
else {
|
|
138
|
-
await db.registerFileBuffer(keys[i], buffers[i]);
|
|
139
|
-
registered.push(keys[i]);
|
|
140
|
-
resolved.push(keys[i]);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
placeholders[name] = resolved;
|
|
144
|
-
}));
|
|
145
|
-
endRegister?.({ buffered: registered.length });
|
|
146
146
|
try {
|
|
147
|
+
await Promise.all(Object.entries(fileKeys).map(async ([name, keys]) => {
|
|
148
|
+
const resolved = [];
|
|
149
|
+
const buffered = [];
|
|
150
|
+
for (let i = 0; i < keys.length; i++) {
|
|
151
|
+
const key = keys[i];
|
|
152
|
+
const uri = dataSource.uri?.(key);
|
|
153
|
+
if (uri !== void 0) resolved.push(uri);
|
|
154
|
+
else {
|
|
155
|
+
buffered.push({
|
|
156
|
+
key,
|
|
157
|
+
name: key
|
|
158
|
+
});
|
|
159
|
+
resolved.push(key);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
await registerBufferedFiles(db, buffered, dataSource, registered, signal);
|
|
163
|
+
placeholders[name] = resolved;
|
|
164
|
+
}));
|
|
165
|
+
endRegister?.({ buffered: registered.length });
|
|
147
166
|
signal?.throwIfAborted();
|
|
148
167
|
const finalSql = substituteNamedFiles(rewriteEmptyFileSets(sql, placeholders, table, placeholderTables), placeholders);
|
|
149
168
|
const endQuery = profiler?.start("query.run");
|
|
@@ -274,7 +293,11 @@ async function gcOrphansImpl(deps, now, graceMs, opts = {}) {
|
|
|
274
293
|
}
|
|
275
294
|
return { deleted: retired.length + sweptOrphans + hourlyDeleted };
|
|
276
295
|
}
|
|
277
|
-
const PUSHABLE_COLUMN = {
|
|
296
|
+
const PUSHABLE_COLUMN = {
|
|
297
|
+
country: "country",
|
|
298
|
+
query: "query",
|
|
299
|
+
searchAppearance: "searchAppearance"
|
|
300
|
+
};
|
|
278
301
|
function txLeaf(leaf, columns) {
|
|
279
302
|
if (leaf.operator !== "equals") return null;
|
|
280
303
|
const column = PUSHABLE_COLUMN[leaf.dimension];
|
|
@@ -15,11 +15,25 @@ declare const pgResolverAdapter: ResolverAdapter<PgTableKey>;
|
|
|
15
15
|
*/
|
|
16
16
|
interface ResolverAdapterOptions {
|
|
17
17
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
18
|
+
* Deprecated compatibility flag. Fact tables no longer carry
|
|
19
|
+
* `query_canonical`; canonical reads derive from `query_dim`, or from a
|
|
20
|
+
* canonical rollup relation when `queryCanonicalSource: 'column'` is set.
|
|
21
21
|
*/
|
|
22
22
|
canonicalFallback?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* `queryDim` reads canonical from a joined query dimension. `column` is only
|
|
25
|
+
* for derived canonical rollup relations whose primary relation already
|
|
26
|
+
* carries a null-free `query_canonical` output column.
|
|
27
|
+
*/
|
|
28
|
+
queryCanonicalSource?: 'queryDim' | 'column';
|
|
29
|
+
}
|
|
30
|
+
interface R2SqlResolverAdapterOptions extends ResolverAdapterOptions {
|
|
31
|
+
/**
|
|
32
|
+
* R2 SQL string partition equality can undercount on identity partitions;
|
|
33
|
+
* string-encoded catalogs use CONCAT(col, '') in partition predicates. Int
|
|
34
|
+
* catalogs do not need the workaround and keep bare equality for pruning.
|
|
35
|
+
*/
|
|
36
|
+
partitionKeyEncoding?: 'string' | 'int';
|
|
23
37
|
}
|
|
24
38
|
declare function createParquetResolverAdapter(options?: ResolverAdapterOptions): ResolverAdapter<PgTableKey>;
|
|
25
39
|
/**
|
|
@@ -33,4 +47,16 @@ declare function createParquetResolverAdapter(options?: ResolverAdapterOptions):
|
|
|
33
47
|
* `${namespace}.pages`) before sending to R2 SQL.
|
|
34
48
|
*/
|
|
35
49
|
declare function createIcebergResolverAdapter(options?: ResolverAdapterOptions): ResolverAdapter<PgTableKey>;
|
|
36
|
-
|
|
50
|
+
/**
|
|
51
|
+
* R2 SQL adapter for the Iceberg fact tables.
|
|
52
|
+
*
|
|
53
|
+
* It shares the multi-tenant Iceberg schema with `createIcebergResolverAdapter`
|
|
54
|
+
* but models R2 SQL's narrower execution surface: no window-total plans and no
|
|
55
|
+
* comparison joins. Int-partition catalogs are the default and emit bare
|
|
56
|
+
* equality predicates for pruning. Legacy string-partition catalogs must pass
|
|
57
|
+
* `partitionKeyEncoding: 'string'` to emit `CONCAT(partition_col, '') = ?`,
|
|
58
|
+
* working around R2 SQL's partition-string equality undercount while preserving
|
|
59
|
+
* bound params.
|
|
60
|
+
*/
|
|
61
|
+
declare function createR2SqlResolverAdapter(options?: R2SqlResolverAdapterOptions): ResolverAdapter<PgTableKey>;
|
|
62
|
+
export { PgTableKey, R2SqlResolverAdapterOptions, ResolverAdapterOptions, createIcebergResolverAdapter, createParquetResolverAdapter, createR2SqlResolverAdapter, pgResolverAdapter };
|
|
@@ -2,6 +2,7 @@ import { SCHEMAS, drizzleSchema } from "./schema.mjs";
|
|
|
2
2
|
import { enumeratePartitions } from "./compaction.mjs";
|
|
3
3
|
import { escapeLike } from "../sql-fragments.mjs";
|
|
4
4
|
import "../planner.mjs";
|
|
5
|
+
import "./schema2.mjs";
|
|
5
6
|
import { UnresolvableDatasetError, buildLogicalComparisonPlan, buildLogicalPlan, inferDataset as inferLogicalDataset, isDatasetResolvable } from "gscdump/query/plan";
|
|
6
7
|
import { PgDialect, pgTable, varchar } from "drizzle-orm/pg-core";
|
|
7
8
|
import { normalizeUrl } from "gscdump/normalize";
|
|
@@ -9,7 +10,7 @@ import { sql } from "drizzle-orm";
|
|
|
9
10
|
const DIMENSION_SURFACES = {
|
|
10
11
|
page: ["api", "stored"],
|
|
11
12
|
query: ["api", "stored"],
|
|
12
|
-
queryCanonical: ["
|
|
13
|
+
queryCanonical: ["derived"],
|
|
13
14
|
country: ["api", "stored"],
|
|
14
15
|
device: ["api", "stored"],
|
|
15
16
|
searchAppearance: ["api", "stored"],
|
|
@@ -34,7 +35,7 @@ const LOGICAL_DATASETS = {
|
|
|
34
35
|
},
|
|
35
36
|
queryCanonical: {
|
|
36
37
|
column: "query_canonical",
|
|
37
|
-
surfaces: ["
|
|
38
|
+
surfaces: ["derived"]
|
|
38
39
|
},
|
|
39
40
|
date: {
|
|
40
41
|
column: "date",
|
|
@@ -52,7 +53,7 @@ const LOGICAL_DATASETS = {
|
|
|
52
53
|
},
|
|
53
54
|
queryCanonical: {
|
|
54
55
|
column: "query_canonical",
|
|
55
|
-
surfaces: ["
|
|
56
|
+
surfaces: ["derived"]
|
|
56
57
|
},
|
|
57
58
|
date: {
|
|
58
59
|
column: "date",
|
|
@@ -108,7 +109,7 @@ const LOGICAL_DATASETS = {
|
|
|
108
109
|
},
|
|
109
110
|
queryCanonical: {
|
|
110
111
|
column: "query_canonical",
|
|
111
|
-
surfaces: ["
|
|
112
|
+
surfaces: ["derived"]
|
|
112
113
|
},
|
|
113
114
|
date: {
|
|
114
115
|
column: "date",
|
|
@@ -130,7 +131,7 @@ const LOGICAL_DATASETS = {
|
|
|
130
131
|
},
|
|
131
132
|
queryCanonical: {
|
|
132
133
|
column: "query_canonical",
|
|
133
|
-
surfaces: ["
|
|
134
|
+
surfaces: ["derived"]
|
|
134
135
|
},
|
|
135
136
|
date: {
|
|
136
137
|
column: "date",
|
|
@@ -169,6 +170,13 @@ const METRIC_NAMES = [
|
|
|
169
170
|
"ctr",
|
|
170
171
|
"position"
|
|
171
172
|
];
|
|
173
|
+
const QUERY_DIM_ALIAS = "query_dim";
|
|
174
|
+
function quoteIdent(id) {
|
|
175
|
+
return `"${id.replace(/"/g, "\"\"")}"`;
|
|
176
|
+
}
|
|
177
|
+
function qualifiedRaw(alias, column) {
|
|
178
|
+
return sql.raw(`${quoteIdent(alias)}.${quoteIdent(column)}`);
|
|
179
|
+
}
|
|
172
180
|
function defaultSqliteUrlToPathExpr(col) {
|
|
173
181
|
return `CASE WHEN ${col} LIKE 'http%' THEN CASE WHEN INSTR(SUBSTR(${col}, INSTR(${col}, '://') + 3), '/') > 0 THEN SUBSTR(${col}, INSTR(${col}, '://') + 2 + INSTR(SUBSTR(${col}, INSTR(${col}, '://') + 3), '/')) ELSE '/' END ELSE ${col} END`;
|
|
174
182
|
}
|
|
@@ -180,7 +188,7 @@ function buildDimensionColumnMap(datasetToTableKey) {
|
|
|
180
188
|
return Object.fromEntries(entries);
|
|
181
189
|
}
|
|
182
190
|
function createSqlFragments(config) {
|
|
183
|
-
const { schema, datasetToTableKey, metricCast, regexPredicate, tableLabel, includeSiteId, includeSearchType, urlToPathExpr: urlToPathExprOverride, tableRef: tableRefOverride,
|
|
191
|
+
const { schema, datasetToTableKey, metricCast, regexPredicate, tableLabel, includeSiteId, includeSearchType, urlToPathExpr: urlToPathExprOverride, tableRef: tableRefOverride, queryCanonicalSource = "queryDim", queryDimTableRef, queryDimSiteScoped = false } = config;
|
|
184
192
|
const DIM_COLUMN_MAP = buildDimensionColumnMap(datasetToTableKey);
|
|
185
193
|
function isMetricDimension(dim) {
|
|
186
194
|
return METRIC_NAMES.includes(dim);
|
|
@@ -205,6 +213,11 @@ function createSqlFragments(config) {
|
|
|
205
213
|
if (tableRefOverride) return tableRefOverride(tableKey);
|
|
206
214
|
return sql`${schema[tableKey]}`;
|
|
207
215
|
}
|
|
216
|
+
function fromSql(tableKey, options = {}) {
|
|
217
|
+
const base = tableRef(tableKey);
|
|
218
|
+
if (!options.queryCanonical || queryCanonicalSource !== "queryDim") return base;
|
|
219
|
+
return sql`${base} LEFT JOIN ${queryDimTableRef?.() ?? sql.raw(`${quoteIdent(QUERY_DIM_ALIAS)}`)} ON ${queryDimSiteScoped ? sql`${colRef(tableKey, "query")} = ${qualifiedRaw(QUERY_DIM_ALIAS, "query")} AND ${colRef(tableKey, "site_id")} = ${qualifiedRaw(QUERY_DIM_ALIAS, "site_id")}` : sql`${colRef(tableKey, "query")} = ${qualifiedRaw(QUERY_DIM_ALIAS, "query")}`}`;
|
|
220
|
+
}
|
|
208
221
|
function dateColRef(tableKey) {
|
|
209
222
|
return colRef(tableKey, "date");
|
|
210
223
|
}
|
|
@@ -217,7 +230,10 @@ function createSqlFragments(config) {
|
|
|
217
230
|
function dimExprSql(dim, tableKey) {
|
|
218
231
|
const colName = dimColumn(dim, tableKey);
|
|
219
232
|
if (dim === "page") return sql.raw(urlToPathExpr(colName));
|
|
220
|
-
if (
|
|
233
|
+
if (dim === "queryCanonical") {
|
|
234
|
+
if (queryCanonicalSource === "queryDim") return sql`COALESCE(${qualifiedRaw(QUERY_DIM_ALIAS, "query_canonical")}, ${colRef(tableKey, "query")})`;
|
|
235
|
+
return qualifiedRaw(String(tableKey), "query_canonical");
|
|
236
|
+
}
|
|
221
237
|
return colRef(tableKey, colName);
|
|
222
238
|
}
|
|
223
239
|
function metricSql(metric, tableKey) {
|
|
@@ -298,7 +314,7 @@ function createSqlFragments(config) {
|
|
|
298
314
|
if (f.dimension === "date") continue;
|
|
299
315
|
if (f.operator === "topLevel") continue;
|
|
300
316
|
const dim = f.dimension;
|
|
301
|
-
const cRef = colRef(tableKey, dimColumn(dim, tableKey));
|
|
317
|
+
const cRef = dim === "queryCanonical" ? void 0 : colRef(tableKey, dimColumn(dim, tableKey));
|
|
302
318
|
const matchExpr = dim === "page" || dim === "queryCanonical" ? dimExprSql(dim, tableKey) : cRef;
|
|
303
319
|
const patternExpr = dim === "queryCanonical" ? matchExpr : cRef;
|
|
304
320
|
switch (f.operator) {
|
|
@@ -339,6 +355,7 @@ function createSqlFragments(config) {
|
|
|
339
355
|
urlToPathExpr,
|
|
340
356
|
colRef,
|
|
341
357
|
tableRef,
|
|
358
|
+
fromSql,
|
|
342
359
|
dateColRef,
|
|
343
360
|
siteIdColRef: includeSiteId ? siteIdColRef : void 0,
|
|
344
361
|
searchTypeColRef: includeSearchType ? searchTypeColRef : void 0,
|
|
@@ -361,6 +378,7 @@ function createResolverAdapter(config) {
|
|
|
361
378
|
dimColumn: runtime.dimColumn,
|
|
362
379
|
isMetricDimension: runtime.isMetricDimension,
|
|
363
380
|
tableRef: runtime.tableRef,
|
|
381
|
+
fromSql: runtime.fromSql,
|
|
364
382
|
dateColRef: runtime.dateColRef,
|
|
365
383
|
urlToPathExpr: runtime.urlToPathExpr,
|
|
366
384
|
siteIdColRef: runtime.siteIdColRef,
|
|
@@ -439,7 +457,9 @@ function createParquetResolverAdapter(options = {}) {
|
|
|
439
457
|
...PG_BASE_CONFIG,
|
|
440
458
|
tableLabel: "parquet-resolver-adapter",
|
|
441
459
|
canonicalFallback: options.canonicalFallback ?? false,
|
|
442
|
-
|
|
460
|
+
queryCanonicalSource: options.queryCanonicalSource ?? "queryDim",
|
|
461
|
+
tableRef: (tk) => sql.raw(`read_parquet({{FILES}}, union_by_name = true) AS "${tk}"`),
|
|
462
|
+
queryDimTableRef: () => sql.raw("read_parquet({{QUERY_DIM}}, union_by_name = true) AS \"query_dim\"")
|
|
443
463
|
});
|
|
444
464
|
}
|
|
445
465
|
function createIcebergResolverAdapter(options = {}) {
|
|
@@ -450,9 +470,35 @@ function createIcebergResolverAdapter(options = {}) {
|
|
|
450
470
|
includeSearchType: true,
|
|
451
471
|
tableLabel: "iceberg-resolver-adapter",
|
|
452
472
|
canonicalFallback: options.canonicalFallback ?? false,
|
|
453
|
-
|
|
473
|
+
queryCanonicalSource: options.queryCanonicalSource ?? "queryDim",
|
|
474
|
+
tableRef: (tk) => sql.raw(`"${tk}"`),
|
|
475
|
+
queryDimTableRef: () => sql.raw("\"query_dim\"")
|
|
454
476
|
});
|
|
455
477
|
}
|
|
478
|
+
function createR2SqlResolverAdapter(options = {}) {
|
|
479
|
+
const adapter = createResolverAdapter({
|
|
480
|
+
...PG_BASE_CONFIG,
|
|
481
|
+
schema: icebergSchema,
|
|
482
|
+
includeSiteId: true,
|
|
483
|
+
includeSearchType: true,
|
|
484
|
+
tableLabel: "r2-sql-resolver-adapter",
|
|
485
|
+
canonicalFallback: options.canonicalFallback ?? false,
|
|
486
|
+
queryCanonicalSource: options.queryCanonicalSource ?? "queryDim",
|
|
487
|
+
capabilities: {
|
|
488
|
+
regex: false,
|
|
489
|
+
comparisonJoin: false,
|
|
490
|
+
windowTotals: false
|
|
491
|
+
},
|
|
492
|
+
tableRef: (tk) => sql.raw(`"${tk}"`),
|
|
493
|
+
queryDimTableRef: () => sql.raw("\"query_dim\"")
|
|
494
|
+
});
|
|
495
|
+
if ((options.partitionKeyEncoding ?? "int") === "int") return adapter;
|
|
496
|
+
return {
|
|
497
|
+
...adapter,
|
|
498
|
+
siteIdColRef: (tk) => sql`CONCAT(${adapter.siteIdColRef(tk)}, '')`,
|
|
499
|
+
searchTypeColRef: (tk) => sql`CONCAT(${adapter.searchTypeColRef(tk)}, '')`
|
|
500
|
+
};
|
|
501
|
+
}
|
|
456
502
|
const ALLOWED_FILTER_DIMS = /* @__PURE__ */ new Set(["date", "queryCanonical"]);
|
|
457
503
|
function planCoveredByCanonicalRollup(plan) {
|
|
458
504
|
if (plan.dataset !== "queries") return false;
|
|
@@ -548,6 +594,7 @@ function buildScope(state, options) {
|
|
|
548
594
|
const groupByDims = plan.groupByDimensions;
|
|
549
595
|
const hasDate = plan.hasDate;
|
|
550
596
|
const metrics = plan.metrics;
|
|
597
|
+
const queryCanonicalUsed = groupByDims.includes("queryCanonical") || plan.dimensionFilters.some((filter) => filter.dimension === "queryCanonical");
|
|
551
598
|
const wherePredicates = [];
|
|
552
599
|
if (adapter.siteIdColRef && siteId != null) wherePredicates.push(sql`${adapter.siteIdColRef(tableKey)} = ${siteId}`);
|
|
553
600
|
if (adapter.searchTypeColRef && searchType != null) wherePredicates.push(sql`${adapter.searchTypeColRef(tableKey)} = ${searchType}`);
|
|
@@ -569,7 +616,8 @@ function buildScope(state, options) {
|
|
|
569
616
|
having: adapter.havingPredicates(metricFilters, tableKey),
|
|
570
617
|
dimFilters,
|
|
571
618
|
startDate: plan.dateRange.startDate,
|
|
572
|
-
endDate: plan.dateRange.endDate
|
|
619
|
+
endDate: plan.dateRange.endDate,
|
|
620
|
+
queryCanonicalUsed
|
|
573
621
|
};
|
|
574
622
|
}
|
|
575
623
|
function buildComparisonPlan(current, previous, capabilities) {
|
|
@@ -584,8 +632,8 @@ function compileCollapsed(adapter, q) {
|
|
|
584
632
|
}
|
|
585
633
|
function resolveToSQLOptimized(state, options) {
|
|
586
634
|
const { adapter } = options;
|
|
587
|
-
const { tableKey, groupByDims, hasDate, metrics, wherePredicates, having } = buildScope(state, options);
|
|
588
|
-
const table = adapter.
|
|
635
|
+
const { tableKey, groupByDims, hasDate, metrics, wherePredicates, having, queryCanonicalUsed } = buildScope(state, options);
|
|
636
|
+
const table = adapter.fromSql(tableKey, { queryCanonical: queryCanonicalUsed });
|
|
589
637
|
const schema = adapter.schema;
|
|
590
638
|
const cteSelect = [];
|
|
591
639
|
for (const d of groupByDims) {
|
|
@@ -632,8 +680,8 @@ function resolveToSQLOptimized(state, options) {
|
|
|
632
680
|
}
|
|
633
681
|
function resolveToSQL(state, options) {
|
|
634
682
|
const { adapter } = options;
|
|
635
|
-
const { tableKey, groupByDims, hasDate, metrics, wherePredicates, having } = buildScope(state, options);
|
|
636
|
-
const table = adapter.
|
|
683
|
+
const { tableKey, groupByDims, hasDate, metrics, wherePredicates, having, queryCanonicalUsed } = buildScope(state, options);
|
|
684
|
+
const table = adapter.fromSql(tableKey, { queryCanonical: queryCanonicalUsed });
|
|
637
685
|
const selectExprs = [];
|
|
638
686
|
for (const d of groupByDims) {
|
|
639
687
|
const expr = adapter.dimExprSql(d, tableKey);
|
|
@@ -666,8 +714,8 @@ function resolveToSQL(state, options) {
|
|
|
666
714
|
}
|
|
667
715
|
function buildTotalsSql(state, options) {
|
|
668
716
|
const { adapter } = options;
|
|
669
|
-
const { tableKey, metrics, wherePredicates } = buildScope(state, options);
|
|
670
|
-
const table = adapter.
|
|
717
|
+
const { tableKey, metrics, wherePredicates, queryCanonicalUsed } = buildScope(state, options);
|
|
718
|
+
const table = adapter.fromSql(tableKey, { queryCanonical: queryCanonicalUsed });
|
|
671
719
|
const selectExprs = metrics.map((m) => sql`${adapter.metricSql(m, tableKey)} as ${aliasRaw(m)}`);
|
|
672
720
|
return compileCollapsed(adapter, wherePredicates.length > 0 ? sql`SELECT ${joinComma(selectExprs)} FROM ${table} WHERE ${joinAnd(wherePredicates)}` : sql`SELECT ${joinComma(selectExprs)} FROM ${table}`);
|
|
673
721
|
}
|
|
@@ -677,7 +725,7 @@ function resolveComparisonSQL(current, previous, options, comparisonFilter) {
|
|
|
677
725
|
const currentScope = buildScope(current, options);
|
|
678
726
|
const previousScope = buildScope(previous, options);
|
|
679
727
|
const { tableKey, groupByDims, metrics, wherePredicates: currentWhere, having } = currentScope;
|
|
680
|
-
const table = adapter.
|
|
728
|
+
const table = adapter.fromSql(tableKey, { queryCanonical: currentScope.queryCanonicalUsed || previousScope.queryCanonicalUsed });
|
|
681
729
|
const dimSelectExprs = [];
|
|
682
730
|
for (const d of groupByDims) {
|
|
683
731
|
const expr = adapter.dimExprSql(d, tableKey);
|
|
@@ -735,7 +783,7 @@ function buildExtrasQueries(state, options) {
|
|
|
735
783
|
if (!dims.includes("queryCanonical")) return extras;
|
|
736
784
|
const queriesKey = adapter.tableKeyForDataset("queries");
|
|
737
785
|
const t = adapter.schema[queriesKey];
|
|
738
|
-
const table = adapter.
|
|
786
|
+
const table = adapter.fromSql(queriesKey, { queryCanonical: true });
|
|
739
787
|
const whereParts = [];
|
|
740
788
|
if (adapter.siteIdColRef && siteId != null) whereParts.push(sql`${adapter.siteIdColRef(queriesKey)} = ${siteId}`);
|
|
741
789
|
if (adapter.searchTypeColRef && searchType != null) whereParts.push(sql`${adapter.searchTypeColRef(queriesKey)} = ${searchType}`);
|
|
@@ -743,7 +791,7 @@ function buildExtrasQueries(state, options) {
|
|
|
743
791
|
whereParts.push(sql`${adapter.dateColRef(queriesKey)} <= ${plan.dateRange.endDate}`);
|
|
744
792
|
const whereExpr = whereParts.length > 0 ? sql`WHERE ${joinAnd(whereParts)}` : sql``;
|
|
745
793
|
const outerQueryCol = sql.raw("query");
|
|
746
|
-
const canonKey =
|
|
794
|
+
const canonKey = adapter.dimExprSql("queryCanonical", queriesKey);
|
|
747
795
|
const compiled = compileCollapsed(adapter, sql`WITH per_variant AS (SELECT ${canonKey} as joinKey, ${t.query} as query, SUM(${t.clicks}) as clicks, SUM(${t.impressions}) as impressions, SUM(${t.sum_position}) as sum_pos, ROW_NUMBER() OVER (PARTITION BY ${canonKey} ORDER BY SUM(${t.clicks}) DESC) as rn, COUNT(*) OVER (PARTITION BY ${canonKey}) as variantCount FROM ${table} ${whereExpr} GROUP BY ${canonKey}, ${t.query}) SELECT joinKey, MAX(variantCount) as variantCount, MAX(CASE WHEN rn = 1 THEN ${outerQueryCol} END) as canonicalName, GROUP_CONCAT(CASE WHEN rn <= 10 THEN ${outerQueryCol} || ':::' || clicks || ':::' || impressions || ':::' || CAST(ROUND(CAST(sum_pos AS REAL) / NULLIF(impressions, 0) + 1, 1) AS TEXT) END, '||') as variants FROM per_variant GROUP BY joinKey`);
|
|
748
796
|
extras.push({
|
|
749
797
|
key: "canonicalExtras",
|
|
@@ -890,9 +938,80 @@ function matchesMetricFilter(row, filter) {
|
|
|
890
938
|
function matchesTopLevelPage(row) {
|
|
891
939
|
return (normalizeUrl(dimensionValue(row, "page")).match(/\//g)?.length ?? 0) <= 1;
|
|
892
940
|
}
|
|
941
|
+
var QuerySourceCoverageError = class extends Error {
|
|
942
|
+
fallback;
|
|
943
|
+
name = "QuerySourceCoverageError";
|
|
944
|
+
constructor(fallback) {
|
|
945
|
+
super(fallback.message);
|
|
946
|
+
this.fallback = fallback;
|
|
947
|
+
}
|
|
948
|
+
};
|
|
893
949
|
function canonicalSourceWithinCoverage(source, windowEnd) {
|
|
894
950
|
return source.coversThrough === void 0 || windowEnd <= source.coversThrough;
|
|
895
951
|
}
|
|
952
|
+
function querySourceDecision(kind, fallbacks = []) {
|
|
953
|
+
const first = fallbacks[0];
|
|
954
|
+
return {
|
|
955
|
+
kind,
|
|
956
|
+
...first ? {
|
|
957
|
+
fallback: first,
|
|
958
|
+
fallbacks
|
|
959
|
+
} : {}
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
function canonicalFeatureRequested(state, capabilities) {
|
|
963
|
+
const plan = buildLogicalPlan(state, capabilities);
|
|
964
|
+
return plan.groupByDimensions.includes("queryCanonical") || plan.dimensionFilters.some((f) => f.dimension === "queryCanonical");
|
|
965
|
+
}
|
|
966
|
+
function fallback(kind, message) {
|
|
967
|
+
return {
|
|
968
|
+
kind,
|
|
969
|
+
message
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
function staleQueryDim(source, requirements) {
|
|
973
|
+
if (requirements?.normalizerVersion !== void 0 && source.normalizerVersion !== requirements.normalizerVersion) return fallback("query-dim-stale-version", `canonical query dimension normalizer v${source.normalizerVersion} does not match required v${requirements.normalizerVersion}`);
|
|
974
|
+
if (requirements?.intentVersion !== void 0 && source.intentVersion !== requirements.intentVersion) return fallback("query-dim-stale-version", `canonical query dimension intent v${source.intentVersion ?? "missing"} does not match required v${requirements.intentVersion}`);
|
|
975
|
+
}
|
|
976
|
+
function canonicalQueryDim(options) {
|
|
977
|
+
return options.queryDim ?? options.canonicalSource?.queryDim;
|
|
978
|
+
}
|
|
979
|
+
function queryDimMiss(source, requirements) {
|
|
980
|
+
if (!source) return fallback("query-dim-missing", "canonical query requires queryDim; fact tables do not carry query_canonical");
|
|
981
|
+
if (source.keys.length === 0) return fallback("query-dim-empty", "queryDim has no parquet keys; refusing to run canonical query from an empty dimension");
|
|
982
|
+
return staleQueryDim(source, requirements);
|
|
983
|
+
}
|
|
984
|
+
function decideCanonicalSource(state, capabilities, options, windowEnd) {
|
|
985
|
+
if (!canonicalFeatureRequested(state, capabilities)) return querySourceDecision("raw-partitions");
|
|
986
|
+
const dimMiss = queryDimMiss(canonicalQueryDim(options), options.canonicalRequirements);
|
|
987
|
+
if (dimMiss) throw new QuerySourceCoverageError(dimMiss);
|
|
988
|
+
const source = options.canonicalSource;
|
|
989
|
+
const miss = (() => {
|
|
990
|
+
if (!source) return fallback("canonical-source-missing", "canonical rollup source missing; falling back to primary facts joined through query_dim");
|
|
991
|
+
if (source.keys.length === 0) return fallback("canonical-source-empty", "canonicalSource has no parquet keys; falling back to primary facts joined through query_dim");
|
|
992
|
+
if (!canonicalSourceWithinCoverage(source, windowEnd)) return fallback("canonical-source-stale-coverage", `canonicalSource covers through ${source.coversThrough}, but query needs ${windowEnd}`);
|
|
993
|
+
if (!canonicalRollupCovers(state, capabilities)) return fallback("canonical-source-not-coverable", "canonicalSource only covers queryCanonical/date reads without raw-grain filters");
|
|
994
|
+
})();
|
|
995
|
+
if (!miss) return querySourceDecision("canonical-rollup");
|
|
996
|
+
return querySourceDecision("raw-partitions", [miss]);
|
|
997
|
+
}
|
|
998
|
+
function primarySourceMiss(source, dateRange) {
|
|
999
|
+
if (!source) return fallback("primary-source-missing", "primary columnar source is required; pass primarySourceFallback: \"raw\" to use raw daily partitions explicitly");
|
|
1000
|
+
if (source.keys.length === 0) return fallback("primary-source-empty", "primarySource has no parquet keys; refusing to return partial or empty data implicitly");
|
|
1001
|
+
if (source.coversFrom !== void 0 && dateRange.startDate < source.coversFrom) return fallback("primary-source-missing-coverage", `primarySource covers from ${source.coversFrom}, but query starts ${dateRange.startDate}`);
|
|
1002
|
+
if (source.coversThrough !== void 0 && dateRange.endDate > source.coversThrough) return fallback("primary-source-stale-coverage", `primarySource covers through ${source.coversThrough}, but query needs ${dateRange.endDate}`);
|
|
1003
|
+
}
|
|
1004
|
+
function decidePrimarySource(options, dateRange, priorFallbacks = []) {
|
|
1005
|
+
const miss = primarySourceMiss(options.primarySource, dateRange);
|
|
1006
|
+
if (!miss) return querySourceDecision("primary-columnar", priorFallbacks);
|
|
1007
|
+
const fallbacks = [...priorFallbacks, miss];
|
|
1008
|
+
if (options.primarySourceFallback === "raw") return querySourceDecision("raw-partitions", fallbacks);
|
|
1009
|
+
throw new QuerySourceCoverageError(miss);
|
|
1010
|
+
}
|
|
1011
|
+
function sourceFallbacks(source) {
|
|
1012
|
+
if (source.fallbacks) return source.fallbacks;
|
|
1013
|
+
return source.fallback ? [source.fallback] : [];
|
|
1014
|
+
}
|
|
896
1015
|
function runArgs(ctx, partitions) {
|
|
897
1016
|
return {
|
|
898
1017
|
ctx: {
|
|
@@ -907,26 +1026,58 @@ function runArgs(ctx, partitions) {
|
|
|
907
1026
|
...ctx.searchType !== void 0 ? { searchType: ctx.searchType } : {}
|
|
908
1027
|
};
|
|
909
1028
|
}
|
|
1029
|
+
function primaryRunArgs(ctx, keys) {
|
|
1030
|
+
return {
|
|
1031
|
+
ctx: {
|
|
1032
|
+
userId: ctx.userId,
|
|
1033
|
+
siteId: ctx.siteId
|
|
1034
|
+
},
|
|
1035
|
+
table: ctx.table,
|
|
1036
|
+
fileSets: { FILES: {
|
|
1037
|
+
table: ctx.table,
|
|
1038
|
+
keys
|
|
1039
|
+
} },
|
|
1040
|
+
...ctx.searchType !== void 0 ? { searchType: ctx.searchType } : {}
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
function withQueryDimFileSet(args, queryDim, enabled) {
|
|
1044
|
+
if (!enabled || !queryDim) return args;
|
|
1045
|
+
return {
|
|
1046
|
+
...args,
|
|
1047
|
+
fileSets: {
|
|
1048
|
+
...args.fileSets,
|
|
1049
|
+
QUERY_DIM: {
|
|
1050
|
+
table: "queries",
|
|
1051
|
+
keys: queryDim.keys
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
910
1056
|
async function runOptimizedQuery(runSQL, ctx, state, dateRange, options = {}) {
|
|
911
1057
|
const base = runArgs(ctx, enumeratePartitions(dateRange.startDate, dateRange.endDate));
|
|
912
|
-
const probe = createParquetResolverAdapter(
|
|
913
|
-
const
|
|
914
|
-
const
|
|
1058
|
+
const probe = createParquetResolverAdapter();
|
|
1059
|
+
const canonicalRequested = canonicalFeatureRequested(state, probe.capabilities);
|
|
1060
|
+
const canonicalDecision = decideCanonicalSource(state, probe.capabilities, options, dateRange.endDate);
|
|
1061
|
+
const useCanonicalSource = canonicalDecision.kind === "canonical-rollup";
|
|
1062
|
+
const source = useCanonicalSource ? canonicalDecision : decidePrimarySource(options, dateRange, sourceFallbacks(canonicalDecision));
|
|
915
1063
|
const optimized = resolveToSQLOptimized(state, {
|
|
916
|
-
adapter,
|
|
1064
|
+
adapter: useCanonicalSource ? createParquetResolverAdapter({ queryCanonicalSource: "column" }) : probe,
|
|
917
1065
|
siteId: void 0
|
|
918
1066
|
});
|
|
919
1067
|
const extras = buildExtrasQueries(state, {
|
|
920
|
-
adapter,
|
|
1068
|
+
adapter: probe,
|
|
921
1069
|
siteId: void 0
|
|
922
1070
|
});
|
|
923
|
-
const
|
|
1071
|
+
const extraSource = extras.length > 0 ? useCanonicalSource ? decidePrimarySource(options, dateRange) : source : void 0;
|
|
1072
|
+
const queryDim = canonicalQueryDim(options);
|
|
1073
|
+
const mainArgs = withQueryDimFileSet(useCanonicalSource ? {
|
|
924
1074
|
...base,
|
|
925
1075
|
fileSets: { FILES: {
|
|
926
1076
|
table: ctx.table,
|
|
927
1077
|
keys: options.canonicalSource.keys
|
|
928
1078
|
} }
|
|
929
|
-
} : base;
|
|
1079
|
+
} : source.kind === "primary-columnar" ? primaryRunArgs(ctx, options.primarySource.keys) : base, queryDim, canonicalRequested && !useCanonicalSource);
|
|
1080
|
+
const extraArgs = withQueryDimFileSet(extraSource?.kind === "primary-columnar" ? primaryRunArgs(ctx, options.primarySource.keys) : base, queryDim, extras.length > 0);
|
|
930
1081
|
const resolveExtra = options.resolveExtra;
|
|
931
1082
|
const [optRes, ...extrasRows] = await Promise.all([runSQL({
|
|
932
1083
|
...mainArgs,
|
|
@@ -940,7 +1091,7 @@ async function runOptimizedQuery(runSQL, ctx, state, dateRange, options = {}) {
|
|
|
940
1091
|
dateRange
|
|
941
1092
|
}) : null;
|
|
942
1093
|
return overlaid !== null ? { rows: overlaid } : runSQL({
|
|
943
|
-
...
|
|
1094
|
+
...extraArgs,
|
|
944
1095
|
sql: e.sql,
|
|
945
1096
|
params: e.params
|
|
946
1097
|
});
|
|
@@ -963,13 +1114,25 @@ async function runOptimizedQuery(runSQL, ctx, state, dateRange, options = {}) {
|
|
|
963
1114
|
extras: extras.map((e, i) => ({
|
|
964
1115
|
key: e.key,
|
|
965
1116
|
rows: extrasRows[i].rows
|
|
966
|
-
}))
|
|
1117
|
+
})),
|
|
1118
|
+
source,
|
|
1119
|
+
...extraSource ? { extraSource } : {}
|
|
967
1120
|
};
|
|
968
1121
|
}
|
|
969
1122
|
async function runComparisonQuery(runSQL, ctx, current, previous, windows, filter, options = {}) {
|
|
970
|
-
const probe = createParquetResolverAdapter(
|
|
971
|
-
const
|
|
972
|
-
const
|
|
1123
|
+
const probe = createParquetResolverAdapter();
|
|
1124
|
+
const maxWindowEnd = windows.current.endDate > windows.previous.endDate ? windows.current.endDate : windows.previous.endDate;
|
|
1125
|
+
const currentSource = decideCanonicalSource(current, probe.capabilities, options, maxWindowEnd);
|
|
1126
|
+
const previousSource = decideCanonicalSource(previous, probe.capabilities, options, maxWindowEnd);
|
|
1127
|
+
const startDate = windows.current.startDate < windows.previous.startDate ? windows.current.startDate : windows.previous.startDate;
|
|
1128
|
+
const endDate = windows.current.endDate > windows.previous.endDate ? windows.current.endDate : windows.previous.endDate;
|
|
1129
|
+
const comparisonRange = {
|
|
1130
|
+
startDate,
|
|
1131
|
+
endDate
|
|
1132
|
+
};
|
|
1133
|
+
const source = currentSource.kind === "canonical-rollup" && previousSource.kind === "canonical-rollup" ? querySourceDecision("canonical-rollup") : decidePrimarySource(options, comparisonRange, [...sourceFallbacks(currentSource), ...sourceFallbacks(previousSource)]);
|
|
1134
|
+
const useCanonicalSource = source.kind === "canonical-rollup";
|
|
1135
|
+
const adapter = useCanonicalSource ? createParquetResolverAdapter({ queryCanonicalSource: "column" }) : probe;
|
|
973
1136
|
const comparison = resolveComparisonSQL(current, previous, {
|
|
974
1137
|
adapter,
|
|
975
1138
|
siteId: void 0
|
|
@@ -978,14 +1141,14 @@ async function runComparisonQuery(runSQL, ctx, current, previous, windows, filte
|
|
|
978
1141
|
adapter,
|
|
979
1142
|
siteId: void 0
|
|
980
1143
|
});
|
|
981
|
-
const partitions = enumeratePartitions(
|
|
982
|
-
const base = useCanonicalSource ? {
|
|
1144
|
+
const partitions = enumeratePartitions(startDate, endDate);
|
|
1145
|
+
const base = withQueryDimFileSet(useCanonicalSource ? {
|
|
983
1146
|
...runArgs(ctx, partitions),
|
|
984
1147
|
fileSets: { FILES: {
|
|
985
1148
|
table: ctx.table,
|
|
986
1149
|
keys: options.canonicalSource.keys
|
|
987
1150
|
} }
|
|
988
|
-
} : runArgs(ctx, partitions);
|
|
1151
|
+
} : source.kind === "primary-columnar" ? primaryRunArgs(ctx, options.primarySource.keys) : runArgs(ctx, partitions), canonicalQueryDim(options), !useCanonicalSource && (canonicalFeatureRequested(current, probe.capabilities) || canonicalFeatureRequested(previous, probe.capabilities)));
|
|
989
1152
|
const main = await runSQL({
|
|
990
1153
|
...base,
|
|
991
1154
|
sql: comparison.sql,
|
|
@@ -1004,7 +1167,8 @@ async function runComparisonQuery(runSQL, ctx, current, previous, windows, filte
|
|
|
1004
1167
|
return {
|
|
1005
1168
|
rows: main.rows,
|
|
1006
1169
|
totalCount: Number(count.rows[0]?.total ?? 0),
|
|
1007
|
-
totals: totalsRow.rows[0] ?? {}
|
|
1170
|
+
totals: totalsRow.rows[0] ?? {},
|
|
1171
|
+
source
|
|
1008
1172
|
};
|
|
1009
1173
|
}
|
|
1010
1174
|
function assertSchemaInSync(options) {
|
|
@@ -1017,4 +1181,4 @@ function assertSchemaInSync(options) {
|
|
|
1017
1181
|
if (missing.length > 0 || extra.length > 0) throw new Error(`${label} drizzle schema for '${key}' drifted from SCHEMAS. Missing: [${missing.join(", ")}]. Extra: [${extra.join(", ")}].`);
|
|
1018
1182
|
}
|
|
1019
1183
|
}
|
|
1020
|
-
export { DIMENSION_SURFACES, LOGICAL_DATASETS, UnresolvableDatasetError, assertDimensionsSupported, assertSchemaInSync, buildExtrasQueries, buildTotalsSql, canonicalRollupCovers, createIcebergResolverAdapter, createParquetResolverAdapter, createResolverAdapter, createRollupExtrasOverlay, createSqlFragments, dimensionColumn, dimensionValue, getDimensionFilters, getFilterDimensions, getInternalFilters, inferLogicalDataset, isDatasetResolvable, matchesDimensionFilter, matchesMetricFilter, matchesTopLevelPage, mergeExtras, metricValue, pgResolverAdapter, planCoveredByCanonicalRollup, resolveComparisonSQL, resolveToSQL, resolveToSQLOptimized, runComparisonQuery, runOptimizedQuery, supportsDimensionOnSurface };
|
|
1184
|
+
export { DIMENSION_SURFACES, LOGICAL_DATASETS, QuerySourceCoverageError, UnresolvableDatasetError, assertDimensionsSupported, assertSchemaInSync, buildExtrasQueries, buildTotalsSql, canonicalRollupCovers, createIcebergResolverAdapter, createParquetResolverAdapter, createR2SqlResolverAdapter, createResolverAdapter, createRollupExtrasOverlay, createSqlFragments, dimensionColumn, dimensionValue, getDimensionFilters, getFilterDimensions, getInternalFilters, inferLogicalDataset, isDatasetResolvable, matchesDimensionFilter, matchesMetricFilter, matchesTopLevelPage, mergeExtras, metricValue, pgResolverAdapter, planCoveredByCanonicalRollup, resolveComparisonSQL, resolveToSQL, resolveToSQLOptimized, runComparisonQuery, runOptimizedQuery, supportsDimensionOnSurface };
|