@gscdump/engine-sqlite 1.3.2 → 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.
@@ -0,0 +1,81 @@
1
+ import { sql } from "drizzle-orm";
2
+ import { index, integer, primaryKey, sqliteTable, text, unique } from "drizzle-orm/sqlite-core";
3
+ const r2Manifest = sqliteTable("r2_manifest", {
4
+ id: text("id").primaryKey(),
5
+ userId: integer("user_id").notNull(),
6
+ siteId: text("site_id"),
7
+ table: text("table").notNull(),
8
+ partition: text("partition").notNull(),
9
+ objectKey: text("object_key").notNull(),
10
+ rowCount: integer("row_count").notNull().default(0),
11
+ bytes: integer("bytes").notNull().default(0),
12
+ createdAt: integer("created_at").notNull(),
13
+ retiredAt: integer("retired_at"),
14
+ tier: text("tier"),
15
+ searchType: text("search_type"),
16
+ schemaVersion: integer("schema_version")
17
+ }, (t) => [
18
+ index("idx_r2_manifest_live").on(t.userId, t.siteId, t.table, t.partition, t.retiredAt),
19
+ index("idx_r2_manifest_lookup").on(t.userId, t.siteId, t.table, t.searchType, t.tier, t.partition, t.retiredAt),
20
+ index("idx_r2_manifest_retired").on(t.retiredAt),
21
+ index("idx_r2_manifest_tier").on(t.userId, t.siteId, t.table, t.tier, t.retiredAt),
22
+ unique("r2_manifest_object_key_unique").on(t.objectKey)
23
+ ]);
24
+ const r2WriteErrors = sqliteTable("r2_write_errors", {
25
+ id: text("id").primaryKey(),
26
+ userId: integer("user_id").notNull(),
27
+ siteId: text("site_id"),
28
+ table: text("table"),
29
+ date: text("date"),
30
+ error: text("error").notNull(),
31
+ createdAt: integer("created_at").notNull().default(sql`(unixepoch())`)
32
+ }, (t) => [index("idx_r2_write_errors_user").on(t.userId, t.createdAt), index("idx_r2_write_errors_created").on(t.createdAt)]);
33
+ const r2ShadowDiffs = sqliteTable("r2_shadow_diffs", {
34
+ id: text("id").primaryKey(),
35
+ userId: integer("user_id").notNull(),
36
+ siteId: text("site_id"),
37
+ endpoint: text("endpoint").notNull(),
38
+ diff: text("diff").notNull(),
39
+ createdAt: integer("created_at").notNull().default(sql`(unixepoch())`)
40
+ }, (t) => [index("idx_r2_shadow_diffs_user").on(t.userId, t.createdAt), index("idx_r2_shadow_diffs_endpoint").on(t.endpoint, t.createdAt)]);
41
+ const r2Locks = sqliteTable("r2_locks", {
42
+ scope: text("scope").primaryKey(),
43
+ holderId: text("holder_id").notNull(),
44
+ acquiredAt: integer("acquired_at").notNull(),
45
+ expiresAt: integer("expires_at").notNull()
46
+ }, (t) => [index("idx_r2_locks_expires").on(t.expiresAt)]);
47
+ const r2Watermarks = sqliteTable("r2_watermarks", {
48
+ userId: integer("user_id").notNull(),
49
+ siteId: text("site_id").notNull().default(""),
50
+ table: text("table").notNull(),
51
+ newestDateSynced: text("newest_date_synced").notNull(),
52
+ oldestDateSynced: text("oldest_date_synced").notNull(),
53
+ lastSyncAt: integer("last_sync_at").notNull()
54
+ }, (t) => [primaryKey({ columns: [
55
+ t.userId,
56
+ t.siteId,
57
+ t.table
58
+ ] })]);
59
+ const r2SyncStates = sqliteTable("r2_sync_states", {
60
+ userId: integer("user_id").notNull(),
61
+ siteId: text("site_id").notNull().default(""),
62
+ table: text("table").notNull(),
63
+ date: text("date").notNull(),
64
+ searchType: text("search_type").notNull().default(""),
65
+ state: text("state", { enum: [
66
+ "pending",
67
+ "inflight",
68
+ "done",
69
+ "failed"
70
+ ] }).notNull(),
71
+ updatedAt: integer("updated_at").notNull(),
72
+ attempts: integer("attempts").notNull().default(0),
73
+ error: text("error")
74
+ }, (t) => [primaryKey({ columns: [
75
+ t.userId,
76
+ t.siteId,
77
+ t.table,
78
+ t.date,
79
+ t.searchType
80
+ ] }), index("idx_r2_sync_states_state").on(t.state)]);
81
+ export { r2Locks, r2Manifest, r2ShadowDiffs, r2SyncStates, r2Watermarks, r2WriteErrors };
@@ -0,0 +1,11 @@
1
+ import { r2Locks, r2Manifest, r2SyncStates, r2Watermarks } from "./r2-manifest-schema.mjs";
2
+ import { ManifestStore } from "@gscdump/engine";
3
+ import { DrizzleD1Database } from "drizzle-orm/d1";
4
+ type AnalyticsManifestDb = DrizzleD1Database<{
5
+ r2Manifest: typeof r2Manifest;
6
+ r2Locks: typeof r2Locks;
7
+ r2SyncStates: typeof r2SyncStates;
8
+ r2Watermarks: typeof r2Watermarks;
9
+ }>;
10
+ declare function createD1ManifestStore(db: AnalyticsManifestDb): ManifestStore;
11
+ export { AnalyticsManifestDb, createD1ManifestStore };
@@ -0,0 +1,282 @@
1
+ import { r2Locks, r2Manifest, r2SyncStates, r2Watermarks } from "./r2-manifest-schema.mjs";
2
+ import { and, eq, inArray, isNotNull, isNull, lt, lte, or, sql } from "drizzle-orm";
3
+ import { inferSearchType } from "@gscdump/engine";
4
+ import { engineErrorToException, engineErrors } from "@gscdump/engine/errors";
5
+ function toRow(e) {
6
+ return {
7
+ id: e.objectKey,
8
+ userId: Number(e.userId),
9
+ siteId: e.siteId ?? null,
10
+ table: e.table,
11
+ partition: e.partition,
12
+ objectKey: e.objectKey,
13
+ rowCount: e.rowCount,
14
+ bytes: e.bytes,
15
+ createdAt: e.createdAt,
16
+ retiredAt: e.retiredAt ?? null,
17
+ tier: e.tier ?? null,
18
+ searchType: e.searchType ?? null,
19
+ schemaVersion: e.schemaVersion ?? null
20
+ };
21
+ }
22
+ function fromRow(r) {
23
+ return {
24
+ userId: String(r.userId),
25
+ siteId: r.siteId ?? void 0,
26
+ table: r.table,
27
+ partition: r.partition,
28
+ objectKey: r.objectKey,
29
+ rowCount: r.rowCount,
30
+ bytes: r.bytes,
31
+ createdAt: r.createdAt,
32
+ retiredAt: r.retiredAt ?? void 0,
33
+ ...r.tier !== null ? { tier: r.tier } : {},
34
+ ...r.searchType !== null ? { searchType: r.searchType } : {},
35
+ ...r.schemaVersion !== null ? { schemaVersion: r.schemaVersion } : {}
36
+ };
37
+ }
38
+ const siteIdOf = (s) => s ?? "";
39
+ const searchTypeOf = (s) => s === void 0 || s === "web" ? "" : s;
40
+ const LOCK_TTL_MS = 3e4;
41
+ const LOCK_ACQUIRE_TIMEOUT_MS = 5e3;
42
+ const LOCK_RETRY_MIN_MS = 25;
43
+ const LOCK_RETRY_MAX_MS = 150;
44
+ const D1_BATCH_LIMIT = 95;
45
+ const D1_IN_CLAUSE_CHUNK = 90;
46
+ function lockScopeKey(scope) {
47
+ return `${scope.userId}|${siteIdOf(scope.siteId)}|${scope.table}|${scope.partition}`;
48
+ }
49
+ function jitterDelay() {
50
+ return LOCK_RETRY_MIN_MS + Math.floor(Math.random() * (LOCK_RETRY_MAX_MS - LOCK_RETRY_MIN_MS));
51
+ }
52
+ function tierMatchCond(target) {
53
+ const explicit = eq(r2Manifest.tier, target);
54
+ if (target === "raw") return or(explicit, and(isNull(r2Manifest.tier), sql`${r2Manifest.partition} LIKE 'daily/%'`));
55
+ if (target === "d30") return or(explicit, and(isNull(r2Manifest.tier), sql`${r2Manifest.partition} LIKE 'monthly/%'`));
56
+ return explicit;
57
+ }
58
+ function createD1ManifestStore(db) {
59
+ async function listByFilter(filter, liveOnly) {
60
+ const baseConds = [eq(r2Manifest.userId, Number(filter.userId))];
61
+ if (liveOnly) baseConds.push(isNull(r2Manifest.retiredAt));
62
+ if (filter.siteId !== void 0) baseConds.push(eq(r2Manifest.siteId, filter.siteId));
63
+ if (filter.table !== void 0) baseConds.push(eq(r2Manifest.table, filter.table));
64
+ if (filter.tier !== void 0) {
65
+ const cond = tierMatchCond(filter.tier);
66
+ if (cond) baseConds.push(cond);
67
+ }
68
+ if (filter.searchType !== void 0) baseConds.push(filter.searchType === "web" ? or(eq(r2Manifest.searchType, "web"), isNull(r2Manifest.searchType)) : eq(r2Manifest.searchType, filter.searchType));
69
+ const PARTITION_CHUNK = 80;
70
+ if (filter.partitions) {
71
+ if (filter.partitions.length === 0) return [];
72
+ const statements = [];
73
+ for (let i = 0; i < filter.partitions.length; i += PARTITION_CHUNK) {
74
+ const slice = filter.partitions.slice(i, i + PARTITION_CHUNK);
75
+ const conds = [...baseConds, inArray(r2Manifest.partition, slice)];
76
+ statements.push(db.select().from(r2Manifest).where(and(...conds)));
77
+ }
78
+ return (await db.batch(statements)).flat().map((r) => fromRow(r));
79
+ }
80
+ return (await db.select().from(r2Manifest).where(and(...baseConds))).map(fromRow);
81
+ }
82
+ const listLive = (filter) => listByFilter(filter, true);
83
+ const listAll = (filter) => listByFilter(filter, false);
84
+ async function registerVersions(newEntries, superseding) {
85
+ const supersededAt = newEntries[0]?.createdAt ?? Date.now();
86
+ const statements = [];
87
+ if (superseding && superseding.length > 0) {
88
+ const keys = superseding.map((s) => s.objectKey);
89
+ for (let i = 0; i < keys.length; i += D1_IN_CLAUSE_CHUNK) {
90
+ const slice = keys.slice(i, i + D1_IN_CLAUSE_CHUNK);
91
+ statements.push(db.update(r2Manifest).set({ retiredAt: supersededAt }).where(and(inArray(r2Manifest.objectKey, slice), isNull(r2Manifest.retiredAt))));
92
+ }
93
+ }
94
+ for (const e of newEntries) statements.push(db.insert(r2Manifest).values(toRow(e)).onConflictDoUpdate({
95
+ target: r2Manifest.objectKey,
96
+ set: {
97
+ userId: sql`excluded.user_id`,
98
+ siteId: sql`excluded.site_id`,
99
+ table: sql`excluded."table"`,
100
+ partition: sql`excluded.partition`,
101
+ rowCount: sql`excluded.row_count`,
102
+ bytes: sql`excluded.bytes`,
103
+ createdAt: sql`excluded.created_at`,
104
+ retiredAt: sql`excluded.retired_at`,
105
+ tier: sql`excluded.tier`,
106
+ searchType: sql`excluded.search_type`,
107
+ schemaVersion: sql`excluded.schema_version`
108
+ }
109
+ }));
110
+ if (statements.length === 0) return;
111
+ for (let i = 0; i < statements.length; i += D1_BATCH_LIMIT) {
112
+ const chunk = statements.slice(i, i + D1_BATCH_LIMIT);
113
+ await db.batch(chunk);
114
+ }
115
+ }
116
+ async function listRetired(olderThan) {
117
+ return (await db.select().from(r2Manifest).where(and(isNotNull(r2Manifest.retiredAt), lte(r2Manifest.retiredAt, olderThan)))).map(fromRow);
118
+ }
119
+ async function deleteEntries(entries) {
120
+ if (entries.length === 0) return;
121
+ const keys = entries.map((e) => e.objectKey);
122
+ const statements = [];
123
+ 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))));
124
+ for (let i = 0; i < statements.length; i += D1_BATCH_LIMIT) await db.batch(statements.slice(i, i + D1_BATCH_LIMIT));
125
+ }
126
+ async function getWatermarks(filter) {
127
+ const conds = [eq(r2Watermarks.userId, Number(filter.userId))];
128
+ if (filter.siteId !== void 0) conds.push(eq(r2Watermarks.siteId, filter.siteId));
129
+ if (filter.table !== void 0) conds.push(eq(r2Watermarks.table, filter.table));
130
+ return (await db.select().from(r2Watermarks).where(and(...conds))).map((r) => ({
131
+ userId: String(r.userId),
132
+ siteId: r.siteId === "" ? void 0 : r.siteId,
133
+ table: r.table,
134
+ newestDateSynced: r.newestDateSynced,
135
+ oldestDateSynced: r.oldestDateSynced,
136
+ lastSyncAt: r.lastSyncAt
137
+ }));
138
+ }
139
+ async function bumpWatermark(scope, date, at) {
140
+ const lastSyncAt = at ?? Date.now();
141
+ await db.insert(r2Watermarks).values({
142
+ userId: Number(scope.userId),
143
+ siteId: siteIdOf(scope.siteId),
144
+ table: scope.table,
145
+ newestDateSynced: date,
146
+ oldestDateSynced: date,
147
+ lastSyncAt
148
+ }).onConflictDoUpdate({
149
+ target: [
150
+ r2Watermarks.userId,
151
+ r2Watermarks.siteId,
152
+ r2Watermarks.table
153
+ ],
154
+ set: {
155
+ newestDateSynced: sql`CASE WHEN excluded.newest_date_synced > newest_date_synced THEN excluded.newest_date_synced ELSE newest_date_synced END`,
156
+ oldestDateSynced: sql`CASE WHEN excluded.oldest_date_synced < oldest_date_synced THEN excluded.oldest_date_synced ELSE oldest_date_synced END`,
157
+ lastSyncAt: sql`CASE WHEN excluded.last_sync_at > last_sync_at THEN excluded.last_sync_at ELSE last_sync_at END`
158
+ }
159
+ }).run();
160
+ }
161
+ async function getSyncStates(filter) {
162
+ const conds = [eq(r2SyncStates.userId, Number(filter.userId))];
163
+ if (filter.siteId !== void 0) conds.push(eq(r2SyncStates.siteId, filter.siteId));
164
+ if (filter.table !== void 0) conds.push(eq(r2SyncStates.table, filter.table));
165
+ if (filter.state !== void 0) conds.push(eq(r2SyncStates.state, filter.state));
166
+ if (filter.searchType !== void 0) conds.push(eq(r2SyncStates.searchType, searchTypeOf(filter.searchType)));
167
+ return (await db.select().from(r2SyncStates).where(and(...conds))).map((r) => ({
168
+ userId: String(r.userId),
169
+ siteId: r.siteId === "" ? void 0 : r.siteId,
170
+ table: r.table,
171
+ date: r.date,
172
+ searchType: inferSearchType({ searchType: r.searchType === "" ? void 0 : r.searchType }),
173
+ state: r.state,
174
+ updatedAt: r.updatedAt,
175
+ attempts: r.attempts,
176
+ error: r.error ?? void 0
177
+ }));
178
+ }
179
+ async function setSyncState(scope, state, detail) {
180
+ const updatedAt = detail?.at ?? Date.now();
181
+ const errorText = detail?.error ?? null;
182
+ await db.insert(r2SyncStates).values({
183
+ userId: Number(scope.userId),
184
+ siteId: siteIdOf(scope.siteId),
185
+ table: scope.table,
186
+ date: scope.date,
187
+ searchType: searchTypeOf(scope.searchType),
188
+ state,
189
+ updatedAt,
190
+ attempts: state === "inflight" ? 1 : 0,
191
+ error: errorText
192
+ }).onConflictDoUpdate({
193
+ target: [
194
+ r2SyncStates.userId,
195
+ r2SyncStates.siteId,
196
+ r2SyncStates.table,
197
+ r2SyncStates.date,
198
+ r2SyncStates.searchType
199
+ ],
200
+ set: {
201
+ state: sql`excluded.state`,
202
+ updatedAt: sql`excluded.updated_at`,
203
+ attempts: sql`CASE WHEN excluded.state = 'inflight' THEN attempts + 1 ELSE attempts END`,
204
+ error: sql`CASE
205
+ WHEN excluded.state = 'done' THEN NULL
206
+ WHEN excluded.state = 'inflight' THEN error
207
+ ELSE excluded.error
208
+ END`
209
+ }
210
+ }).run();
211
+ }
212
+ async function withLock(scope, fn) {
213
+ const key = lockScopeKey(scope);
214
+ const holderId = crypto.randomUUID();
215
+ const deadline = Date.now() + LOCK_ACQUIRE_TIMEOUT_MS;
216
+ while (true) {
217
+ const now = Date.now();
218
+ const expiresAt = now + LOCK_TTL_MS;
219
+ await db.insert(r2Locks).values({
220
+ scope: key,
221
+ holderId,
222
+ acquiredAt: now,
223
+ expiresAt
224
+ }).onConflictDoUpdate({
225
+ target: r2Locks.scope,
226
+ set: {
227
+ holderId: sql`excluded.holder_id`,
228
+ acquiredAt: sql`excluded.acquired_at`,
229
+ expiresAt: sql`excluded.expires_at`
230
+ },
231
+ setWhere: lt(r2Locks.expiresAt, now)
232
+ }).run();
233
+ if ((await db.select({ holderId: r2Locks.holderId }).from(r2Locks).where(eq(r2Locks.scope, key)).get())?.holderId === holderId) break;
234
+ if (Date.now() >= deadline) throw engineErrorToException(engineErrors.lockAcquireTimeout(key, LOCK_ACQUIRE_TIMEOUT_MS));
235
+ await new Promise((resolve) => setTimeout(resolve, jitterDelay()));
236
+ }
237
+ return await fn().finally(async () => {
238
+ await Promise.resolve(db.delete(r2Locks).where(and(eq(r2Locks.scope, key), eq(r2Locks.holderId, holderId))).run()).catch((error) => {
239
+ console.warn(`[gscdump/engine-sqlite] failed to release manifest lock ${key}; lease will expire`, error);
240
+ });
241
+ });
242
+ }
243
+ async function purgeTenant(filter) {
244
+ const userIdNum = Number(filter.userId);
245
+ const entriesCond = filter.siteId !== void 0 ? and(eq(r2Manifest.userId, userIdNum), eq(r2Manifest.siteId, filter.siteId)) : eq(r2Manifest.userId, userIdNum);
246
+ const watermarksCond = filter.siteId !== void 0 ? and(eq(r2Watermarks.userId, userIdNum), eq(r2Watermarks.siteId, filter.siteId)) : eq(r2Watermarks.userId, userIdNum);
247
+ const syncStatesCond = filter.siteId !== void 0 ? and(eq(r2SyncStates.userId, userIdNum), eq(r2SyncStates.siteId, filter.siteId)) : eq(r2SyncStates.userId, userIdNum);
248
+ const [entriesRows, watermarkRows, syncStateRows] = await Promise.all([
249
+ db.select({ n: sql`count(*)` }).from(r2Manifest).where(entriesCond).all(),
250
+ db.select({ n: sql`count(*)` }).from(r2Watermarks).where(watermarksCond).all(),
251
+ db.select({ n: sql`count(*)` }).from(r2SyncStates).where(syncStatesCond).all()
252
+ ]);
253
+ const entriesRemoved = Number(entriesRows[0]?.n ?? 0);
254
+ const watermarksRemoved = Number(watermarkRows[0]?.n ?? 0);
255
+ const syncStatesRemoved = Number(syncStateRows[0]?.n ?? 0);
256
+ await db.batch([
257
+ db.delete(r2Manifest).where(entriesCond),
258
+ db.delete(r2Watermarks).where(watermarksCond),
259
+ db.delete(r2SyncStates).where(syncStatesCond)
260
+ ]);
261
+ return {
262
+ entriesRemoved,
263
+ watermarksRemoved,
264
+ syncStatesRemoved
265
+ };
266
+ }
267
+ return {
268
+ listLive,
269
+ listAll,
270
+ registerVersion: (entry, superseding) => registerVersions([entry], superseding),
271
+ registerVersions,
272
+ listRetired,
273
+ delete: deleteEntries,
274
+ getWatermarks,
275
+ bumpWatermark,
276
+ getSyncStates,
277
+ setSyncState,
278
+ withLock,
279
+ purgeTenant
280
+ };
281
+ }
282
+ export { createD1ManifestStore };
@@ -0,0 +1,27 @@
1
+ import { schema } from "./schema.mjs";
2
+ import { SqliteRowExecutor } from "./runner.mjs";
3
+ import { ResolverAdapter } from "@gscdump/engine/resolver";
4
+ type TableKey = keyof typeof schema;
5
+ interface CreateSqliteResolverAdapterOptions {
6
+ regex?: boolean;
7
+ }
8
+ declare function createSqliteResolverAdapter(options?: CreateSqliteResolverAdapterOptions): ResolverAdapter<TableKey>;
9
+ declare const sqliteResolverAdapter: ResolverAdapter<"gsc_countries" | "gsc_devices" | "gsc_hourly_pages" | "gsc_keywords" | "gsc_page_keywords" | "gsc_pages" | "gsc_query_dim" | "gsc_search_appearance" | "gsc_search_appearance_page_queries" | "gsc_search_appearance_pages" | "gsc_search_appearance_queries">;
10
+ /**
11
+ * One-shot probe that detects whether the host SQLite has a working REGEXP
12
+ * function. SQLite ships REGEXP as a no-op stub by default; D1, libsql, and
13
+ * sqlite3 with the regexp extension register a real implementation. Probe
14
+ * once at adapter construction so per-query plan-time gating is honest.
15
+ */
16
+ declare function probeSqliteRegex(executor: SqliteRowExecutor): Promise<boolean>;
17
+ interface CreateSqliteResolverAdapterFromExecutorOptions {
18
+ executor: SqliteRowExecutor;
19
+ }
20
+ /**
21
+ * Async factory that probes REGEXP availability on `executor` and returns
22
+ * a resolver adapter with `capabilities.regex` set accordingly. Prefer over
23
+ * the manual `createSqliteResolverAdapter({ regex })` whenever you have
24
+ * an executor handy.
25
+ */
26
+ declare function createSqliteResolverAdapterFromExecutor(options: CreateSqliteResolverAdapterFromExecutorOptions): Promise<ResolverAdapter<TableKey>>;
27
+ export { CreateSqliteResolverAdapterFromExecutorOptions, CreateSqliteResolverAdapterOptions, TableKey, createSqliteResolverAdapter, createSqliteResolverAdapterFromExecutor, probeSqliteRegex, sqliteResolverAdapter };
@@ -0,0 +1,41 @@
1
+ import { schema } from "./schema.mjs";
2
+ import { compileSqlite } from "./runner.mjs";
3
+ import { sql } from "drizzle-orm";
4
+ import { createResolverAdapter } from "@gscdump/engine/resolver";
5
+ function createSqliteResolverAdapter(options = {}) {
6
+ return createResolverAdapter({
7
+ schema,
8
+ datasetToTableKey: {
9
+ pages: "gsc_pages",
10
+ queries: "gsc_keywords",
11
+ page_queries: "gsc_page_keywords",
12
+ countries: "gsc_countries",
13
+ dates: "gsc_devices",
14
+ search_appearance: "gsc_search_appearance",
15
+ search_appearance_pages: "gsc_search_appearance_pages",
16
+ search_appearance_queries: "gsc_search_appearance_queries",
17
+ search_appearance_page_queries: "gsc_search_appearance_page_queries",
18
+ hourly_pages: "gsc_hourly_pages"
19
+ },
20
+ metricCast: "REAL",
21
+ regexPredicate: (expr, pattern, negate) => negate ? sql`NOT (${expr} REGEXP ${pattern})` : sql`${expr} REGEXP ${pattern}`,
22
+ tableLabel: "sqlite/resolver-adapter",
23
+ includeSiteId: true,
24
+ queryDimTableRef: () => sql.raw("\"gsc_query_dim\" AS \"query_dim\""),
25
+ queryDimSiteScoped: true,
26
+ compile: compileSqlite,
27
+ capabilities: {
28
+ regex: options.regex ?? false,
29
+ comparisonJoin: true,
30
+ windowTotals: true
31
+ }
32
+ });
33
+ }
34
+ const sqliteResolverAdapter = createSqliteResolverAdapter();
35
+ async function probeSqliteRegex(executor) {
36
+ return executor(`SELECT 'a' REGEXP 'a' AS r`, [], "all").then(({ rows }) => rows.length > 0).catch(() => false);
37
+ }
38
+ async function createSqliteResolverAdapterFromExecutor(options) {
39
+ return createSqliteResolverAdapter({ regex: await probeSqliteRegex(options.executor) });
40
+ }
41
+ export { createSqliteResolverAdapter, createSqliteResolverAdapterFromExecutor, probeSqliteRegex, sqliteResolverAdapter };
@@ -0,0 +1,42 @@
1
+ import { Schema } from "./schema.mjs";
2
+ import { SQL } from "drizzle-orm";
3
+ import { ScopedRunnerOptions, TableScope } from "@gscdump/engine/scope";
4
+ import { SqliteRemoteDatabase } from "drizzle-orm/sqlite-proxy";
5
+ declare function compileSqlite(query: SQL): {
6
+ sql: string;
7
+ params: unknown[];
8
+ };
9
+ type SqliteRowExecutor = (sql: string, params: unknown[], method: 'run' | 'all' | 'values' | 'get') => Promise<{
10
+ rows: unknown[];
11
+ }>;
12
+ interface SqliteInsightRunnerOptions<TSchema extends Record<string, unknown> = Schema> {
13
+ executor: SqliteRowExecutor;
14
+ logger?: boolean;
15
+ /**
16
+ * Convert object rows to positional arrays before handing to drizzle.
17
+ * D1 HTTP + most REST-style drivers return objects; set true when the
18
+ * executor yields `{ col: value }` rows.
19
+ */
20
+ rowsAsArrays?: boolean;
21
+ /**
22
+ * Override the bundled gsc_* schema. Pass an extended schema (superset
23
+ * of the upstream tables) when the consumer DB carries additional tables
24
+ * like sitemaps or indexing state. Defaults to the bundled schema.
25
+ */
26
+ schema?: TSchema;
27
+ }
28
+ interface SqliteInsightRunner<TSchema extends Record<string, unknown> = Schema> {
29
+ db: SqliteRemoteDatabase<TSchema>;
30
+ }
31
+ declare function createSqliteInsightRunner<TSchema extends Record<string, unknown> = Schema>(opts: SqliteInsightRunnerOptions<TSchema>): SqliteInsightRunner<TSchema>;
32
+ /**
33
+ * Per-table predicate set from {siteId, window}. Unlike the /browser
34
+ * variant, every sqlite table has `site_id` so the siteId predicate is
35
+ * always emitted when supplied.
36
+ *
37
+ * Date bounds come from either a resolved window or bare startDate/endDate
38
+ * (one or both). This lets callers pass unbounded query-string params
39
+ * through without first normalizing them.
40
+ */
41
+ declare const scopeFor: (table: "gsc_countries" | "gsc_devices" | "gsc_hourly_pages" | "gsc_keywords" | "gsc_page_keywords" | "gsc_pages" | "gsc_query_dim" | "gsc_search_appearance" | "gsc_search_appearance_page_queries" | "gsc_search_appearance_pages" | "gsc_search_appearance_queries", opts: ScopedRunnerOptions) => TableScope, mergeScope: typeof import("@gscdump/engine/scope").mergeScope;
42
+ export { type ScopedRunnerOptions, SqliteInsightRunner, SqliteInsightRunnerOptions, SqliteRowExecutor, type TableScope, compileSqlite, createSqliteInsightRunner, mergeScope, scopeFor };
@@ -0,0 +1,30 @@
1
+ import { schema } from "./schema.mjs";
2
+ import { createScopedHelpers } from "@gscdump/engine/scope";
3
+ import { SQLiteAsyncDialect } from "drizzle-orm/sqlite-core";
4
+ import { drizzle } from "drizzle-orm/sqlite-proxy";
5
+ const sqliteDialect = new SQLiteAsyncDialect();
6
+ function compileSqlite(query) {
7
+ const compiled = sqliteDialect.sqlToQuery(query);
8
+ return {
9
+ sql: compiled.sql,
10
+ params: compiled.params
11
+ };
12
+ }
13
+ function createSqliteInsightRunner(opts) {
14
+ const { executor, logger, rowsAsArrays, schema: schemaOverride } = opts;
15
+ const callback = async (sql, params, method) => {
16
+ const result = await executor(sql, params, method);
17
+ if (!rowsAsArrays) return { rows: result.rows };
18
+ return { rows: result.rows.map((r) => {
19
+ if (Array.isArray(r)) return r;
20
+ if (r && typeof r === "object") return Object.values(r);
21
+ return r;
22
+ }) };
23
+ };
24
+ return { db: drizzle(callback, {
25
+ schema: schemaOverride ?? schema,
26
+ logger
27
+ }) };
28
+ }
29
+ const { scopeFor, mergeScope } = createScopedHelpers(schema);
30
+ export { compileSqlite, createSqliteInsightRunner, mergeScope, scopeFor };