@gscdump/engine-sqlite 1.4.0 → 1.4.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/index.mjs CHANGED
@@ -1,563 +1,10 @@
1
- import { and, and as and$1, eq, eq as eq$1, gte, inArray, isNotNull, isNull, lt, lte, lte as lte$1, or, sql, sql as sql$1 } from "drizzle-orm";
2
- import { createSqlQuerySource } from "@gscdump/engine/source";
3
- import { assertSchemaInSync, createResolverAdapter } from "@gscdump/engine/resolver";
4
- import { createScopedHelpers } from "@gscdump/engine/scope";
5
- import { SQLiteAsyncDialect, index, integer, primaryKey, real, sqliteTable, text, unique } from "drizzle-orm/sqlite-core";
6
- import { drizzle } from "drizzle-orm/sqlite-proxy";
7
- import { inferSearchType } from "@gscdump/engine";
8
- import { engineErrorToException, engineErrors } from "@gscdump/engine/errors";
1
+ import { aggClicks, aggCtr, aggImpressions, aggPosition } from "./metrics.mjs";
2
+ import { gsc_countries, gsc_devices, gsc_keywords, gsc_page_keywords, gsc_pages, gsc_query_dim, schema } from "./schema.mjs";
3
+ import { compileSqlite, createSqliteInsightRunner, mergeScope, scopeFor } from "./runner.mjs";
4
+ import { createSqliteResolverAdapter, createSqliteResolverAdapterFromExecutor, probeSqliteRegex, sqliteResolverAdapter } from "./resolver-adapter.mjs";
5
+ import { createSqliteQuerySource } from "./query-source.mjs";
6
+ import { r2Locks, r2Manifest, r2ShadowDiffs, r2SyncStates, r2Watermarks, r2WriteErrors } from "./r2-manifest-schema.mjs";
7
+ import { createD1ManifestStore } from "./r2-manifest-store.mjs";
8
+ import { and, eq, gte, lte, sql } from "drizzle-orm";
9
9
  import { resolveWindow } from "@gscdump/engine/period";
10
- function aggClicks(t) {
11
- return sql$1`SUM(${t.clicks})`;
12
- }
13
- function aggImpressions(t) {
14
- return sql$1`SUM(${t.impressions})`;
15
- }
16
- function aggCtr(t) {
17
- return sql$1`CAST(SUM(${t.clicks}) AS REAL) / NULLIF(SUM(${t.impressions}), 0)`;
18
- }
19
- function aggPosition(t) {
20
- return sql$1`SUM(${t.sum_position}) / NULLIF(SUM(${t.impressions}), 0) + 1`;
21
- }
22
- function metricCols() {
23
- return {
24
- clicks: integer("clicks").notNull().default(0),
25
- impressions: integer("impressions").notNull().default(0),
26
- ctr: real("ctr").notNull().default(0),
27
- position: real("position").notNull().default(0),
28
- sum_position: real("sum_position").notNull().default(0)
29
- };
30
- }
31
- function baseCols() {
32
- return {
33
- id: text("id").primaryKey(),
34
- site_id: text("site_id").notNull(),
35
- date: text("date").notNull(),
36
- created_at: integer("created_at").default(sql$1`(unixepoch())`)
37
- };
38
- }
39
- const gsc_pages = sqliteTable("gsc_pages", {
40
- ...baseCols(),
41
- url: text("url").notNull(),
42
- ...metricCols()
43
- }, (t) => [index("idx_gsc_pages_site_date").on(t.site_id, t.date)]);
44
- const gsc_keywords = sqliteTable("gsc_keywords", {
45
- ...baseCols(),
46
- query: text("query").notNull(),
47
- ...metricCols()
48
- }, (t) => [index("idx_gsc_keywords_site_date").on(t.site_id, t.date)]);
49
- const gsc_countries = sqliteTable("gsc_countries", {
50
- ...baseCols(),
51
- country: text("country").notNull(),
52
- ...metricCols()
53
- }, (t) => [index("idx_gsc_countries_site_date").on(t.site_id, t.date)]);
54
- const gsc_devices = sqliteTable("gsc_devices", {
55
- ...baseCols(),
56
- device: text("device").notNull(),
57
- ...metricCols()
58
- }, (t) => [index("idx_gsc_devices_site_date").on(t.site_id, t.date)]);
59
- const gsc_page_keywords = sqliteTable("gsc_page_keywords", {
60
- ...baseCols(),
61
- url: text("url").notNull(),
62
- query: text("query").notNull(),
63
- ...metricCols()
64
- }, (t) => [index("idx_gsc_page_keywords_site_date").on(t.site_id, t.date)]);
65
- const gsc_search_appearance = sqliteTable("gsc_search_appearance", {
66
- ...baseCols(),
67
- searchAppearance: text("searchAppearance").notNull(),
68
- ...metricCols()
69
- }, (t) => [index("idx_gsc_search_appearance_site_date").on(t.site_id, t.date)]);
70
- const gsc_search_appearance_pages = sqliteTable("gsc_search_appearance_pages", {
71
- ...baseCols(),
72
- searchAppearance: text("searchAppearance").notNull(),
73
- url: text("url").notNull(),
74
- ...metricCols()
75
- }, (t) => [index("idx_gsc_search_appearance_pages_site_date").on(t.site_id, t.date)]);
76
- const gsc_search_appearance_queries = sqliteTable("gsc_search_appearance_queries", {
77
- ...baseCols(),
78
- searchAppearance: text("searchAppearance").notNull(),
79
- query: text("query").notNull(),
80
- ...metricCols()
81
- }, (t) => [index("idx_gsc_search_appearance_queries_site_date").on(t.site_id, t.date)]);
82
- const gsc_search_appearance_page_queries = sqliteTable("gsc_search_appearance_page_queries", {
83
- ...baseCols(),
84
- searchAppearance: text("searchAppearance").notNull(),
85
- url: text("url").notNull(),
86
- query: text("query").notNull(),
87
- ...metricCols()
88
- }, (t) => [index("idx_gsc_search_appearance_page_queries_site_date").on(t.site_id, t.date)]);
89
- const gsc_query_dim = sqliteTable("gsc_query_dim", {
90
- site_id: text("site_id").notNull(),
91
- query: text("query").notNull(),
92
- query_canonical: text("query_canonical").notNull(),
93
- normalizer_version: integer("normalizer_version").notNull(),
94
- intent_code: integer("intent_code"),
95
- intent_version: integer("intent_version"),
96
- built_at: integer("built_at")
97
- }, (t) => [index("idx_gsc_query_dim_site_query").on(t.site_id, t.query)]);
98
- const schema = {
99
- gsc_pages,
100
- gsc_keywords,
101
- gsc_countries,
102
- gsc_devices,
103
- gsc_page_keywords,
104
- gsc_search_appearance,
105
- gsc_search_appearance_pages,
106
- gsc_search_appearance_queries,
107
- gsc_search_appearance_page_queries,
108
- gsc_hourly_pages: sqliteTable("gsc_hourly_pages", {
109
- ...baseCols(),
110
- url: text("url").notNull(),
111
- hour: integer("hour").notNull(),
112
- ...metricCols()
113
- }, (t) => [index("idx_gsc_hourly_pages_site_date").on(t.site_id, t.date)]),
114
- gsc_query_dim
115
- };
116
- const GSC_TABLE_TO_LOGICAL = {
117
- gsc_pages: "pages",
118
- gsc_keywords: "queries",
119
- gsc_countries: "countries",
120
- gsc_devices: null,
121
- gsc_page_keywords: "page_queries",
122
- gsc_search_appearance: "search_appearance",
123
- gsc_search_appearance_pages: "search_appearance_pages",
124
- gsc_search_appearance_queries: "search_appearance_queries",
125
- gsc_search_appearance_page_queries: "search_appearance_page_queries",
126
- gsc_hourly_pages: "hourly_pages",
127
- gsc_query_dim: null
128
- };
129
- assertSchemaInSync({
130
- label: "sqlite",
131
- schema: Object.fromEntries(Object.entries(schema).filter(([k]) => GSC_TABLE_TO_LOGICAL[k] !== null)),
132
- tableKeyToName: (key) => GSC_TABLE_TO_LOGICAL[key],
133
- mode: "superset"
134
- });
135
- const sqliteDialect = new SQLiteAsyncDialect();
136
- function compileSqlite(query) {
137
- const compiled = sqliteDialect.sqlToQuery(query);
138
- return {
139
- sql: compiled.sql,
140
- params: compiled.params
141
- };
142
- }
143
- function createSqliteInsightRunner(opts) {
144
- const { executor, logger, rowsAsArrays, schema: schemaOverride } = opts;
145
- const callback = async (sql, params, method) => {
146
- const result = await executor(sql, params, method);
147
- if (!rowsAsArrays) return { rows: result.rows };
148
- return { rows: result.rows.map((r) => {
149
- if (Array.isArray(r)) return r;
150
- if (r && typeof r === "object") return Object.values(r);
151
- return r;
152
- }) };
153
- };
154
- return { db: drizzle(callback, {
155
- schema: schemaOverride ?? schema,
156
- logger
157
- }) };
158
- }
159
- const { scopeFor, mergeScope } = createScopedHelpers(schema);
160
- function createSqliteResolverAdapter(options = {}) {
161
- return createResolverAdapter({
162
- schema,
163
- datasetToTableKey: {
164
- pages: "gsc_pages",
165
- queries: "gsc_keywords",
166
- page_queries: "gsc_page_keywords",
167
- countries: "gsc_countries",
168
- dates: "gsc_devices",
169
- search_appearance: "gsc_search_appearance",
170
- search_appearance_pages: "gsc_search_appearance_pages",
171
- search_appearance_queries: "gsc_search_appearance_queries",
172
- search_appearance_page_queries: "gsc_search_appearance_page_queries",
173
- hourly_pages: "gsc_hourly_pages"
174
- },
175
- metricCast: "REAL",
176
- regexPredicate: (expr, pattern, negate) => negate ? sql$1`NOT (${expr} REGEXP ${pattern})` : sql$1`${expr} REGEXP ${pattern}`,
177
- tableLabel: "sqlite/resolver-adapter",
178
- includeSiteId: true,
179
- queryDimTableRef: () => sql$1.raw("\"gsc_query_dim\" AS \"query_dim\""),
180
- queryDimSiteScoped: true,
181
- compile: compileSqlite,
182
- capabilities: {
183
- regex: options.regex ?? false,
184
- comparisonJoin: true,
185
- windowTotals: true
186
- }
187
- });
188
- }
189
- const sqliteResolverAdapter = createSqliteResolverAdapter();
190
- async function probeSqliteRegex(executor) {
191
- return executor(`SELECT 'a' REGEXP 'a' AS r`, [], "all").then(({ rows }) => rows.length > 0).catch(() => false);
192
- }
193
- async function createSqliteResolverAdapterFromExecutor(options) {
194
- return createSqliteResolverAdapter({ regex: await probeSqliteRegex(options.executor) });
195
- }
196
- function createSqliteQuerySource(config) {
197
- const { executor, siteId, regex } = config;
198
- return createSqlQuerySource({
199
- name: "sqlite",
200
- kind: "local",
201
- adapter: regex === void 0 ? sqliteResolverAdapter : createSqliteResolverAdapter({ regex }),
202
- execute: async (sql, params) => {
203
- return (await executor(sql, params, "all")).rows;
204
- },
205
- siteId
206
- });
207
- }
208
- const r2Manifest = sqliteTable("r2_manifest", {
209
- id: text("id").primaryKey(),
210
- userId: integer("user_id").notNull(),
211
- siteId: text("site_id"),
212
- table: text("table").notNull(),
213
- partition: text("partition").notNull(),
214
- objectKey: text("object_key").notNull(),
215
- rowCount: integer("row_count").notNull().default(0),
216
- bytes: integer("bytes").notNull().default(0),
217
- createdAt: integer("created_at").notNull(),
218
- retiredAt: integer("retired_at"),
219
- tier: text("tier"),
220
- searchType: text("search_type"),
221
- schemaVersion: integer("schema_version")
222
- }, (t) => [
223
- index("idx_r2_manifest_live").on(t.userId, t.siteId, t.table, t.partition, t.retiredAt),
224
- index("idx_r2_manifest_lookup").on(t.userId, t.siteId, t.table, t.searchType, t.tier, t.partition, t.retiredAt),
225
- index("idx_r2_manifest_retired").on(t.retiredAt),
226
- index("idx_r2_manifest_tier").on(t.userId, t.siteId, t.table, t.tier, t.retiredAt),
227
- unique("r2_manifest_object_key_unique").on(t.objectKey)
228
- ]);
229
- const r2WriteErrors = sqliteTable("r2_write_errors", {
230
- id: text("id").primaryKey(),
231
- userId: integer("user_id").notNull(),
232
- siteId: text("site_id"),
233
- table: text("table"),
234
- date: text("date"),
235
- error: text("error").notNull(),
236
- createdAt: integer("created_at").notNull().default(sql$1`(unixepoch())`)
237
- }, (t) => [index("idx_r2_write_errors_user").on(t.userId, t.createdAt), index("idx_r2_write_errors_created").on(t.createdAt)]);
238
- const r2ShadowDiffs = sqliteTable("r2_shadow_diffs", {
239
- id: text("id").primaryKey(),
240
- userId: integer("user_id").notNull(),
241
- siteId: text("site_id"),
242
- endpoint: text("endpoint").notNull(),
243
- diff: text("diff").notNull(),
244
- createdAt: integer("created_at").notNull().default(sql$1`(unixepoch())`)
245
- }, (t) => [index("idx_r2_shadow_diffs_user").on(t.userId, t.createdAt), index("idx_r2_shadow_diffs_endpoint").on(t.endpoint, t.createdAt)]);
246
- const r2Locks = sqliteTable("r2_locks", {
247
- scope: text("scope").primaryKey(),
248
- holderId: text("holder_id").notNull(),
249
- acquiredAt: integer("acquired_at").notNull(),
250
- expiresAt: integer("expires_at").notNull()
251
- }, (t) => [index("idx_r2_locks_expires").on(t.expiresAt)]);
252
- const r2Watermarks = sqliteTable("r2_watermarks", {
253
- userId: integer("user_id").notNull(),
254
- siteId: text("site_id").notNull().default(""),
255
- table: text("table").notNull(),
256
- newestDateSynced: text("newest_date_synced").notNull(),
257
- oldestDateSynced: text("oldest_date_synced").notNull(),
258
- lastSyncAt: integer("last_sync_at").notNull()
259
- }, (t) => [primaryKey({ columns: [
260
- t.userId,
261
- t.siteId,
262
- t.table
263
- ] })]);
264
- const r2SyncStates = sqliteTable("r2_sync_states", {
265
- userId: integer("user_id").notNull(),
266
- siteId: text("site_id").notNull().default(""),
267
- table: text("table").notNull(),
268
- date: text("date").notNull(),
269
- searchType: text("search_type").notNull().default(""),
270
- state: text("state", { enum: [
271
- "pending",
272
- "inflight",
273
- "done",
274
- "failed"
275
- ] }).notNull(),
276
- updatedAt: integer("updated_at").notNull(),
277
- attempts: integer("attempts").notNull().default(0),
278
- error: text("error")
279
- }, (t) => [primaryKey({ columns: [
280
- t.userId,
281
- t.siteId,
282
- t.table,
283
- t.date,
284
- t.searchType
285
- ] }), index("idx_r2_sync_states_state").on(t.state)]);
286
- function toRow(e) {
287
- return {
288
- id: e.objectKey,
289
- userId: Number(e.userId),
290
- siteId: e.siteId ?? null,
291
- table: e.table,
292
- partition: e.partition,
293
- objectKey: e.objectKey,
294
- rowCount: e.rowCount,
295
- bytes: e.bytes,
296
- createdAt: e.createdAt,
297
- retiredAt: e.retiredAt ?? null,
298
- tier: e.tier ?? null,
299
- searchType: e.searchType ?? null,
300
- schemaVersion: e.schemaVersion ?? null
301
- };
302
- }
303
- function fromRow(r) {
304
- return {
305
- userId: String(r.userId),
306
- siteId: r.siteId ?? void 0,
307
- table: r.table,
308
- partition: r.partition,
309
- objectKey: r.objectKey,
310
- rowCount: r.rowCount,
311
- bytes: r.bytes,
312
- createdAt: r.createdAt,
313
- retiredAt: r.retiredAt ?? void 0,
314
- ...r.tier !== null ? { tier: r.tier } : {},
315
- ...r.searchType !== null ? { searchType: r.searchType } : {},
316
- ...r.schemaVersion !== null ? { schemaVersion: r.schemaVersion } : {}
317
- };
318
- }
319
- const siteIdOf = (s) => s ?? "";
320
- const searchTypeOf = (s) => s === void 0 || s === "web" ? "" : s;
321
- const LOCK_TTL_MS = 3e4;
322
- const LOCK_ACQUIRE_TIMEOUT_MS = 5e3;
323
- const LOCK_RETRY_MIN_MS = 25;
324
- const LOCK_RETRY_MAX_MS = 150;
325
- const D1_BATCH_LIMIT = 95;
326
- const D1_IN_CLAUSE_CHUNK = 90;
327
- function lockScopeKey(scope) {
328
- return `${scope.userId}|${siteIdOf(scope.siteId)}|${scope.table}|${scope.partition}`;
329
- }
330
- function jitterDelay() {
331
- return LOCK_RETRY_MIN_MS + Math.floor(Math.random() * (LOCK_RETRY_MAX_MS - LOCK_RETRY_MIN_MS));
332
- }
333
- function tierMatchCond(target) {
334
- const explicit = eq$1(r2Manifest.tier, target);
335
- if (target === "raw") return or(explicit, and$1(isNull(r2Manifest.tier), sql$1`${r2Manifest.partition} LIKE 'daily/%'`));
336
- if (target === "d30") return or(explicit, and$1(isNull(r2Manifest.tier), sql$1`${r2Manifest.partition} LIKE 'monthly/%'`));
337
- return explicit;
338
- }
339
- function createD1ManifestStore(db) {
340
- async function listByFilter(filter, liveOnly) {
341
- const baseConds = [eq$1(r2Manifest.userId, Number(filter.userId))];
342
- if (liveOnly) baseConds.push(isNull(r2Manifest.retiredAt));
343
- if (filter.siteId !== void 0) baseConds.push(eq$1(r2Manifest.siteId, filter.siteId));
344
- if (filter.table !== void 0) baseConds.push(eq$1(r2Manifest.table, filter.table));
345
- if (filter.tier !== void 0) {
346
- const cond = tierMatchCond(filter.tier);
347
- if (cond) baseConds.push(cond);
348
- }
349
- if (filter.searchType !== void 0) baseConds.push(filter.searchType === "web" ? or(eq$1(r2Manifest.searchType, "web"), isNull(r2Manifest.searchType)) : eq$1(r2Manifest.searchType, filter.searchType));
350
- const PARTITION_CHUNK = 80;
351
- if (filter.partitions) {
352
- if (filter.partitions.length === 0) return [];
353
- const statements = [];
354
- for (let i = 0; i < filter.partitions.length; i += PARTITION_CHUNK) {
355
- const slice = filter.partitions.slice(i, i + PARTITION_CHUNK);
356
- const conds = [...baseConds, inArray(r2Manifest.partition, slice)];
357
- statements.push(db.select().from(r2Manifest).where(and$1(...conds)));
358
- }
359
- return (await db.batch(statements)).flat().map((r) => fromRow(r));
360
- }
361
- return (await db.select().from(r2Manifest).where(and$1(...baseConds))).map(fromRow);
362
- }
363
- const listLive = (filter) => listByFilter(filter, true);
364
- const listAll = (filter) => listByFilter(filter, false);
365
- async function registerVersions(newEntries, superseding) {
366
- const supersededAt = newEntries[0]?.createdAt ?? Date.now();
367
- const statements = [];
368
- if (superseding && superseding.length > 0) {
369
- const keys = superseding.map((s) => s.objectKey);
370
- for (let i = 0; i < keys.length; i += D1_IN_CLAUSE_CHUNK) {
371
- const slice = keys.slice(i, i + D1_IN_CLAUSE_CHUNK);
372
- statements.push(db.update(r2Manifest).set({ retiredAt: supersededAt }).where(and$1(inArray(r2Manifest.objectKey, slice), isNull(r2Manifest.retiredAt))));
373
- }
374
- }
375
- for (const e of newEntries) statements.push(db.insert(r2Manifest).values(toRow(e)).onConflictDoUpdate({
376
- target: r2Manifest.objectKey,
377
- set: {
378
- userId: sql$1`excluded.user_id`,
379
- siteId: sql$1`excluded.site_id`,
380
- table: sql$1`excluded."table"`,
381
- partition: sql$1`excluded.partition`,
382
- rowCount: sql$1`excluded.row_count`,
383
- bytes: sql$1`excluded.bytes`,
384
- createdAt: sql$1`excluded.created_at`,
385
- retiredAt: sql$1`excluded.retired_at`,
386
- tier: sql$1`excluded.tier`,
387
- searchType: sql$1`excluded.search_type`,
388
- schemaVersion: sql$1`excluded.schema_version`
389
- }
390
- }));
391
- if (statements.length === 0) return;
392
- for (let i = 0; i < statements.length; i += D1_BATCH_LIMIT) {
393
- const chunk = statements.slice(i, i + D1_BATCH_LIMIT);
394
- await db.batch(chunk);
395
- }
396
- }
397
- async function listRetired(olderThan) {
398
- return (await db.select().from(r2Manifest).where(and$1(isNotNull(r2Manifest.retiredAt), lte$1(r2Manifest.retiredAt, olderThan)))).map(fromRow);
399
- }
400
- async function deleteEntries(entries) {
401
- if (entries.length === 0) return;
402
- const keys = entries.map((e) => e.objectKey);
403
- const statements = [];
404
- for (let i = 0; i < keys.length; i += D1_IN_CLAUSE_CHUNK) statements.push(db.delete(r2Manifest).where(inArray(r2Manifest.objectKey, keys.slice(i, i + D1_IN_CLAUSE_CHUNK))));
405
- for (let i = 0; i < statements.length; i += D1_BATCH_LIMIT) await db.batch(statements.slice(i, i + D1_BATCH_LIMIT));
406
- }
407
- async function getWatermarks(filter) {
408
- const conds = [eq$1(r2Watermarks.userId, Number(filter.userId))];
409
- if (filter.siteId !== void 0) conds.push(eq$1(r2Watermarks.siteId, filter.siteId));
410
- if (filter.table !== void 0) conds.push(eq$1(r2Watermarks.table, filter.table));
411
- return (await db.select().from(r2Watermarks).where(and$1(...conds))).map((r) => ({
412
- userId: String(r.userId),
413
- siteId: r.siteId === "" ? void 0 : r.siteId,
414
- table: r.table,
415
- newestDateSynced: r.newestDateSynced,
416
- oldestDateSynced: r.oldestDateSynced,
417
- lastSyncAt: r.lastSyncAt
418
- }));
419
- }
420
- async function bumpWatermark(scope, date, at) {
421
- const lastSyncAt = at ?? Date.now();
422
- await db.insert(r2Watermarks).values({
423
- userId: Number(scope.userId),
424
- siteId: siteIdOf(scope.siteId),
425
- table: scope.table,
426
- newestDateSynced: date,
427
- oldestDateSynced: date,
428
- lastSyncAt
429
- }).onConflictDoUpdate({
430
- target: [
431
- r2Watermarks.userId,
432
- r2Watermarks.siteId,
433
- r2Watermarks.table
434
- ],
435
- set: {
436
- newestDateSynced: sql$1`CASE WHEN excluded.newest_date_synced > newest_date_synced THEN excluded.newest_date_synced ELSE newest_date_synced END`,
437
- oldestDateSynced: sql$1`CASE WHEN excluded.oldest_date_synced < oldest_date_synced THEN excluded.oldest_date_synced ELSE oldest_date_synced END`,
438
- lastSyncAt: sql$1`CASE WHEN excluded.last_sync_at > last_sync_at THEN excluded.last_sync_at ELSE last_sync_at END`
439
- }
440
- }).run();
441
- }
442
- async function getSyncStates(filter) {
443
- const conds = [eq$1(r2SyncStates.userId, Number(filter.userId))];
444
- if (filter.siteId !== void 0) conds.push(eq$1(r2SyncStates.siteId, filter.siteId));
445
- if (filter.table !== void 0) conds.push(eq$1(r2SyncStates.table, filter.table));
446
- if (filter.state !== void 0) conds.push(eq$1(r2SyncStates.state, filter.state));
447
- if (filter.searchType !== void 0) conds.push(eq$1(r2SyncStates.searchType, searchTypeOf(filter.searchType)));
448
- return (await db.select().from(r2SyncStates).where(and$1(...conds))).map((r) => ({
449
- userId: String(r.userId),
450
- siteId: r.siteId === "" ? void 0 : r.siteId,
451
- table: r.table,
452
- date: r.date,
453
- searchType: inferSearchType({ searchType: r.searchType === "" ? void 0 : r.searchType }),
454
- state: r.state,
455
- updatedAt: r.updatedAt,
456
- attempts: r.attempts,
457
- error: r.error ?? void 0
458
- }));
459
- }
460
- async function setSyncState(scope, state, detail) {
461
- const updatedAt = detail?.at ?? Date.now();
462
- const errorText = detail?.error ?? null;
463
- await db.insert(r2SyncStates).values({
464
- userId: Number(scope.userId),
465
- siteId: siteIdOf(scope.siteId),
466
- table: scope.table,
467
- date: scope.date,
468
- searchType: searchTypeOf(scope.searchType),
469
- state,
470
- updatedAt,
471
- attempts: state === "inflight" ? 1 : 0,
472
- error: errorText
473
- }).onConflictDoUpdate({
474
- target: [
475
- r2SyncStates.userId,
476
- r2SyncStates.siteId,
477
- r2SyncStates.table,
478
- r2SyncStates.date,
479
- r2SyncStates.searchType
480
- ],
481
- set: {
482
- state: sql$1`excluded.state`,
483
- updatedAt: sql$1`excluded.updated_at`,
484
- attempts: sql$1`CASE WHEN excluded.state = 'inflight' THEN attempts + 1 ELSE attempts END`,
485
- error: sql$1`CASE
486
- WHEN excluded.state = 'done' THEN NULL
487
- WHEN excluded.state = 'inflight' THEN error
488
- ELSE excluded.error
489
- END`
490
- }
491
- }).run();
492
- }
493
- async function withLock(scope, fn) {
494
- const key = lockScopeKey(scope);
495
- const holderId = crypto.randomUUID();
496
- const deadline = Date.now() + LOCK_ACQUIRE_TIMEOUT_MS;
497
- while (true) {
498
- const now = Date.now();
499
- const expiresAt = now + LOCK_TTL_MS;
500
- await db.insert(r2Locks).values({
501
- scope: key,
502
- holderId,
503
- acquiredAt: now,
504
- expiresAt
505
- }).onConflictDoUpdate({
506
- target: r2Locks.scope,
507
- set: {
508
- holderId: sql$1`excluded.holder_id`,
509
- acquiredAt: sql$1`excluded.acquired_at`,
510
- expiresAt: sql$1`excluded.expires_at`
511
- },
512
- setWhere: lt(r2Locks.expiresAt, now)
513
- }).run();
514
- if ((await db.select({ holderId: r2Locks.holderId }).from(r2Locks).where(eq$1(r2Locks.scope, key)).get())?.holderId === holderId) break;
515
- if (Date.now() >= deadline) throw engineErrorToException(engineErrors.lockAcquireTimeout(key, LOCK_ACQUIRE_TIMEOUT_MS));
516
- await new Promise((resolve) => setTimeout(resolve, jitterDelay()));
517
- }
518
- return await fn().finally(async () => {
519
- await Promise.resolve(db.delete(r2Locks).where(and$1(eq$1(r2Locks.scope, key), eq$1(r2Locks.holderId, holderId))).run()).catch((error) => {
520
- console.warn(`[gscdump/engine-sqlite] failed to release manifest lock ${key}; lease will expire`, error);
521
- });
522
- });
523
- }
524
- async function purgeTenant(filter) {
525
- const userIdNum = Number(filter.userId);
526
- const entriesCond = filter.siteId !== void 0 ? and$1(eq$1(r2Manifest.userId, userIdNum), eq$1(r2Manifest.siteId, filter.siteId)) : eq$1(r2Manifest.userId, userIdNum);
527
- const watermarksCond = filter.siteId !== void 0 ? and$1(eq$1(r2Watermarks.userId, userIdNum), eq$1(r2Watermarks.siteId, filter.siteId)) : eq$1(r2Watermarks.userId, userIdNum);
528
- const syncStatesCond = filter.siteId !== void 0 ? and$1(eq$1(r2SyncStates.userId, userIdNum), eq$1(r2SyncStates.siteId, filter.siteId)) : eq$1(r2SyncStates.userId, userIdNum);
529
- const [entriesRows, watermarkRows, syncStateRows] = await Promise.all([
530
- db.select({ n: sql$1`count(*)` }).from(r2Manifest).where(entriesCond).all(),
531
- db.select({ n: sql$1`count(*)` }).from(r2Watermarks).where(watermarksCond).all(),
532
- db.select({ n: sql$1`count(*)` }).from(r2SyncStates).where(syncStatesCond).all()
533
- ]);
534
- const entriesRemoved = Number(entriesRows[0]?.n ?? 0);
535
- const watermarksRemoved = Number(watermarkRows[0]?.n ?? 0);
536
- const syncStatesRemoved = Number(syncStateRows[0]?.n ?? 0);
537
- await db.batch([
538
- db.delete(r2Manifest).where(entriesCond),
539
- db.delete(r2Watermarks).where(watermarksCond),
540
- db.delete(r2SyncStates).where(syncStatesCond)
541
- ]);
542
- return {
543
- entriesRemoved,
544
- watermarksRemoved,
545
- syncStatesRemoved
546
- };
547
- }
548
- return {
549
- listLive,
550
- listAll,
551
- registerVersion: (entry, superseding) => registerVersions([entry], superseding),
552
- registerVersions,
553
- listRetired,
554
- delete: deleteEntries,
555
- getWatermarks,
556
- bumpWatermark,
557
- getSyncStates,
558
- setSyncState,
559
- withLock,
560
- purgeTenant
561
- };
562
- }
563
10
  export { aggClicks, aggCtr, aggImpressions, aggPosition, and, compileSqlite, createD1ManifestStore, createSqliteInsightRunner, createSqliteQuerySource, createSqliteResolverAdapter, createSqliteResolverAdapterFromExecutor, eq, gsc_countries, gsc_devices, gsc_keywords, gsc_page_keywords, gsc_pages, gsc_query_dim, gte, lte, mergeScope, probeSqliteRegex, r2Locks, r2Manifest, r2ShadowDiffs, r2SyncStates, r2Watermarks, r2WriteErrors, resolveWindow, schema, scopeFor, sql, sqliteResolverAdapter };
@@ -0,0 +1,8 @@
1
+ import { gsc_countries, gsc_devices, gsc_keywords, gsc_page_keywords, gsc_pages } from "./schema.mjs";
2
+ import { SQL } from "drizzle-orm";
3
+ type MetricTable = typeof gsc_pages | typeof gsc_keywords | typeof gsc_countries | typeof gsc_devices | typeof gsc_page_keywords;
4
+ declare function aggClicks(t: MetricTable): SQL;
5
+ declare function aggImpressions(t: MetricTable): SQL;
6
+ declare function aggCtr(t: MetricTable): SQL;
7
+ declare function aggPosition(t: MetricTable): SQL;
8
+ export { aggClicks, aggCtr, aggImpressions, aggPosition };
@@ -0,0 +1,14 @@
1
+ import { sql } from "drizzle-orm";
2
+ function aggClicks(t) {
3
+ return sql`SUM(${t.clicks})`;
4
+ }
5
+ function aggImpressions(t) {
6
+ return sql`SUM(${t.impressions})`;
7
+ }
8
+ function aggCtr(t) {
9
+ return sql`CAST(SUM(${t.clicks}) AS REAL) / NULLIF(SUM(${t.impressions}), 0)`;
10
+ }
11
+ function aggPosition(t) {
12
+ return sql`SUM(${t.sum_position}) / NULLIF(SUM(${t.impressions}), 0) + 1`;
13
+ }
14
+ export { aggClicks, aggCtr, aggImpressions, aggPosition };
@@ -0,0 +1,11 @@
1
+ import { SqliteRowExecutor } from "./runner.mjs";
2
+ import { AnalysisQuerySource } from "@gscdump/engine/source";
3
+ type SqliteQueryExecutor = SqliteRowExecutor;
4
+ interface SqliteQuerySourceOptions {
5
+ executor: SqliteQueryExecutor;
6
+ siteId: string | number;
7
+ /** Override for hosts that expose REGEXP (D1, libsql, sqlite3+regexp). */
8
+ regex?: boolean;
9
+ }
10
+ declare function createSqliteQuerySource(config: SqliteQuerySourceOptions): AnalysisQuerySource;
11
+ export { SqliteQueryExecutor, SqliteQuerySourceOptions, createSqliteQuerySource };
@@ -0,0 +1,15 @@
1
+ import { createSqliteResolverAdapter, sqliteResolverAdapter } from "./resolver-adapter.mjs";
2
+ import { createSqlQuerySource } from "@gscdump/engine/source";
3
+ function createSqliteQuerySource(config) {
4
+ const { executor, siteId, regex } = config;
5
+ return createSqlQuerySource({
6
+ name: "sqlite",
7
+ kind: "local",
8
+ adapter: regex === void 0 ? sqliteResolverAdapter : createSqliteResolverAdapter({ regex }),
9
+ execute: async (sql, params) => {
10
+ return (await executor(sql, params, "all")).rows;
11
+ },
12
+ siteId
13
+ });
14
+ }
15
+ export { createSqliteQuerySource };