@lunora/d1 1.0.0-alpha.3 → 1.0.0-alpha.31
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/LICENSE.md +6 -0
- package/__assets__/package-og.svg +1 -1
- package/dist/dialect.d.mts +14 -12
- package/dist/dialect.d.ts +14 -12
- package/dist/dialect.mjs +2 -1
- package/dist/index.d.mts +15 -6
- package/dist/index.d.ts +15 -6
- package/dist/index.mjs +4 -4
- package/dist/packem_shared/{D1Client-DA3flo1o.mjs → D1Client-DSj8p7c4.mjs} +24 -30
- package/dist/packem_shared/{MigrationRunner-BkEwQ-Ya.mjs → MigrationRunner-A-o1LTKj.mjs} +46 -26
- package/dist/packem_shared/{exportGlobalRows-BGCPm_nA.mjs → exportGlobalRows-q0RgCLPM.mjs} +13 -7
- package/dist/packem_shared/{facetGlobalColumn-C6u_WMIY.mjs → facetGlobalColumn-DnglIe4P.mjs} +9 -5
- package/dist/packem_shared/quoteIdentifier-B-ZeSe1V.mjs +3 -0
- package/package.json +5 -4
package/dist/dialect.d.mts
CHANGED
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* tables are physically shaped in D1.
|
|
2
|
+
* Canonical SQL identifier quoter shared by `@lunora/d1` and `@lunora/do`.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* stay "in lockstep"; this module makes the lockstep structural.
|
|
4
|
+
* Double-quotes a SQL identifier and escapes any embedded double quotes by
|
|
5
|
+
* doubling them (`"` → `""`) — the ANSI/SQLite/Postgres rule. This is a
|
|
6
|
+
* security-relevant primitive (it is the sole defense against identifier
|
|
7
|
+
* injection wherever a table/column name is spliced into raw SQL), so it must
|
|
8
|
+
* have exactly ONE definition rather than byte-identical copies that can drift.
|
|
11
9
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* Like `shared/stable-key.ts`, it is deliberately **not** a package: `@lunora/d1`
|
|
11
|
+
* and `@lunora/do` sit on the same tier with no lower-level package to host it,
|
|
12
|
+
* so each imports this file by relative path and the bundler (packem/rollup)
|
|
13
|
+
* inlines it — no runtime dependency edge, duplicated only in emitted output.
|
|
14
|
+
* Keep it genuinely zero-dependency (relative/built-in imports only) or inlining
|
|
15
|
+
* breaks. Consumers must drop `outDir`/`rootDir` from their `tsconfig.json` (a
|
|
16
|
+
* set `rootDir` raises TS6059 for this out-of-package file under `tsc --noEmit`).
|
|
14
17
|
*/
|
|
18
|
+
declare const quoteIdentifier: (name: string) => string;
|
|
15
19
|
/** SQLite column type affinities Lunora emits. */
|
|
16
20
|
type SqlAffinity = "BLOB" | "INTEGER" | "REAL" | "TEXT";
|
|
17
|
-
/** Double-quote (and escape) a SQL identifier. */
|
|
18
|
-
declare const quoteIdentifier: (name: string) => string;
|
|
19
21
|
/**
|
|
20
22
|
* SQLite affinity for a column by its validator `kind`, chosen so the value the
|
|
21
23
|
* D1 layer serializes round-trips intact:
|
package/dist/dialect.d.ts
CHANGED
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* tables are physically shaped in D1.
|
|
2
|
+
* Canonical SQL identifier quoter shared by `@lunora/d1` and `@lunora/do`.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* stay "in lockstep"; this module makes the lockstep structural.
|
|
4
|
+
* Double-quotes a SQL identifier and escapes any embedded double quotes by
|
|
5
|
+
* doubling them (`"` → `""`) — the ANSI/SQLite/Postgres rule. This is a
|
|
6
|
+
* security-relevant primitive (it is the sole defense against identifier
|
|
7
|
+
* injection wherever a table/column name is spliced into raw SQL), so it must
|
|
8
|
+
* have exactly ONE definition rather than byte-identical copies that can drift.
|
|
11
9
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* Like `shared/stable-key.ts`, it is deliberately **not** a package: `@lunora/d1`
|
|
11
|
+
* and `@lunora/do` sit on the same tier with no lower-level package to host it,
|
|
12
|
+
* so each imports this file by relative path and the bundler (packem/rollup)
|
|
13
|
+
* inlines it — no runtime dependency edge, duplicated only in emitted output.
|
|
14
|
+
* Keep it genuinely zero-dependency (relative/built-in imports only) or inlining
|
|
15
|
+
* breaks. Consumers must drop `outDir`/`rootDir` from their `tsconfig.json` (a
|
|
16
|
+
* set `rootDir` raises TS6059 for this out-of-package file under `tsc --noEmit`).
|
|
14
17
|
*/
|
|
18
|
+
declare const quoteIdentifier: (name: string) => string;
|
|
15
19
|
/** SQLite column type affinities Lunora emits. */
|
|
16
20
|
type SqlAffinity = "BLOB" | "INTEGER" | "REAL" | "TEXT";
|
|
17
|
-
/** Double-quote (and escape) a SQL identifier. */
|
|
18
|
-
declare const quoteIdentifier: (name: string) => string;
|
|
19
21
|
/**
|
|
20
22
|
* SQLite affinity for a column by its validator `kind`, chosen so the value the
|
|
21
23
|
* D1 layer serializes round-trips intact:
|
package/dist/dialect.mjs
CHANGED
package/dist/index.d.mts
CHANGED
|
@@ -52,11 +52,21 @@ interface ExportGlobalArgs {
|
|
|
52
52
|
tables?: ReadonlyArray<string>;
|
|
53
53
|
}
|
|
54
54
|
/**
|
|
55
|
-
* Yield rows from every requested `.global()` table in batches.
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
55
|
+
* Yield rows from every requested `.global()` table in batches.
|
|
56
|
+
*
|
|
57
|
+
* Keyset-paginates on the physical primary key (`id` — every `.global()` table
|
|
58
|
+
* carries it, see `frameworkColumnDdl`): `WHERE "id" > ? ORDER BY "id" LIMIT ?`,
|
|
59
|
+
* carrying the last id forward. A plain `LIMIT/OFFSET` scan would be wrong here —
|
|
60
|
+
* SQLite gives no ordering guarantee for an unordered SELECT and each page is a
|
|
61
|
+
* separate query, so pages could overlap or skip rows (and any concurrent
|
|
62
|
+
* insert/delete would shift offsets and silently drop/duplicate rows in the
|
|
63
|
+
* snapshot). Keyset paging is deterministic under concurrent writes and avoids
|
|
64
|
+
* O(n^2) OFFSET scans on large tables.
|
|
65
|
+
*
|
|
66
|
+
* Tables are provisioned first (idempotent `CREATE … IF NOT EXISTS`): `.global()`
|
|
67
|
+
* tables are created lazily on first write, so a fresh deployment — or any table
|
|
68
|
+
* never written — would otherwise abort the stream with a raw `no such table`
|
|
69
|
+
* instead of exporting it as empty.
|
|
60
70
|
*/
|
|
61
71
|
declare const exportGlobalRows: (exec: SqlCtxExec, schema: SchemaLike, args: ExportGlobalArgs) => AsyncGenerator<ExportRow, void, undefined>;
|
|
62
72
|
interface ImportGlobalArgs {
|
|
@@ -90,7 +100,6 @@ declare const importGlobalRows: (writer: DatabaseWriterLike, schema: SchemaLike,
|
|
|
90
100
|
*/
|
|
91
101
|
interface D1DatabaseLike {
|
|
92
102
|
batch?: (statements: D1PreparedStatementLike[]) => Promise<unknown[]>;
|
|
93
|
-
exec?: (sql: string) => Promise<unknown>;
|
|
94
103
|
prepare: (sql: string) => D1PreparedStatementLike;
|
|
95
104
|
withSession: (bookmark?: string) => D1SessionLike;
|
|
96
105
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -52,11 +52,21 @@ interface ExportGlobalArgs {
|
|
|
52
52
|
tables?: ReadonlyArray<string>;
|
|
53
53
|
}
|
|
54
54
|
/**
|
|
55
|
-
* Yield rows from every requested `.global()` table in batches.
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
55
|
+
* Yield rows from every requested `.global()` table in batches.
|
|
56
|
+
*
|
|
57
|
+
* Keyset-paginates on the physical primary key (`id` — every `.global()` table
|
|
58
|
+
* carries it, see `frameworkColumnDdl`): `WHERE "id" > ? ORDER BY "id" LIMIT ?`,
|
|
59
|
+
* carrying the last id forward. A plain `LIMIT/OFFSET` scan would be wrong here —
|
|
60
|
+
* SQLite gives no ordering guarantee for an unordered SELECT and each page is a
|
|
61
|
+
* separate query, so pages could overlap or skip rows (and any concurrent
|
|
62
|
+
* insert/delete would shift offsets and silently drop/duplicate rows in the
|
|
63
|
+
* snapshot). Keyset paging is deterministic under concurrent writes and avoids
|
|
64
|
+
* O(n^2) OFFSET scans on large tables.
|
|
65
|
+
*
|
|
66
|
+
* Tables are provisioned first (idempotent `CREATE … IF NOT EXISTS`): `.global()`
|
|
67
|
+
* tables are created lazily on first write, so a fresh deployment — or any table
|
|
68
|
+
* never written — would otherwise abort the stream with a raw `no such table`
|
|
69
|
+
* instead of exporting it as empty.
|
|
60
70
|
*/
|
|
61
71
|
declare const exportGlobalRows: (exec: SqlCtxExec, schema: SchemaLike, args: ExportGlobalArgs) => AsyncGenerator<ExportRow, void, undefined>;
|
|
62
72
|
interface ImportGlobalArgs {
|
|
@@ -90,7 +100,6 @@ declare const importGlobalRows: (writer: DatabaseWriterLike, schema: SchemaLike,
|
|
|
90
100
|
*/
|
|
91
101
|
interface D1DatabaseLike {
|
|
92
102
|
batch?: (statements: D1PreparedStatementLike[]) => Promise<unknown[]>;
|
|
93
|
-
exec?: (sql: string) => Promise<unknown>;
|
|
94
103
|
prepare: (sql: string) => D1PreparedStatementLike;
|
|
95
104
|
withSession: (bookmark?: string) => D1SessionLike;
|
|
96
105
|
}
|
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
export { exportGlobalRows, importGlobalRows, selectGlobalTables } from './packem_shared/exportGlobalRows-
|
|
2
|
-
export { D1Client, D1Session } from './packem_shared/D1Client-
|
|
1
|
+
export { exportGlobalRows, importGlobalRows, selectGlobalTables } from './packem_shared/exportGlobalRows-q0RgCLPM.mjs';
|
|
2
|
+
export { D1Client, D1Session } from './packem_shared/D1Client-DSj8p7c4.mjs';
|
|
3
3
|
export { createD1CtxDb, readD1CdcChanges, runD1AggregateMigrations, runD1CdcMigration, runD1GlobalTableMigrations, runD1RankMigrations, runD1SearchMigrations, trimD1CdcChanges } from './packem_shared/createD1CtxDb-BMR8J0dT.mjs';
|
|
4
|
-
export { facetGlobalColumn, listGlobalTables, readGlobalTablePage } from './packem_shared/facetGlobalColumn-
|
|
5
|
-
export { MigrationRunner } from './packem_shared/MigrationRunner-
|
|
4
|
+
export { facetGlobalColumn, listGlobalTables, readGlobalTablePage } from './packem_shared/facetGlobalColumn-DnglIe4P.mjs';
|
|
5
|
+
export { MigrationRunner } from './packem_shared/MigrationRunner-A-o1LTKj.mjs';
|
|
6
6
|
export { default as sqliteDialect } from './packem_shared/sqliteDialect-DqYnHPuu.mjs';
|
|
7
7
|
export { createSqlCtxDb } from '@lunora/sql-store';
|
|
@@ -1,6 +1,28 @@
|
|
|
1
1
|
import { drizzle } from 'drizzle-orm/d1';
|
|
2
2
|
|
|
3
|
+
const evictOldestEntry = (map, capacity) => {
|
|
4
|
+
if (map.size < capacity) {
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
const oldest = map.keys().next().value;
|
|
8
|
+
if (oldest !== void 0) {
|
|
9
|
+
map.delete(oldest);
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
|
|
3
13
|
const STMT_CACHE_CAPACITY = 256;
|
|
14
|
+
const prepareCached = (cache, prepare, sql) => {
|
|
15
|
+
const cached = cache.get(sql);
|
|
16
|
+
if (cached) {
|
|
17
|
+
cache.delete(sql);
|
|
18
|
+
cache.set(sql, cached);
|
|
19
|
+
return cached;
|
|
20
|
+
}
|
|
21
|
+
const stmt = prepare(sql);
|
|
22
|
+
evictOldestEntry(cache, STMT_CACHE_CAPACITY);
|
|
23
|
+
cache.set(sql, stmt);
|
|
24
|
+
return stmt;
|
|
25
|
+
};
|
|
4
26
|
const D1_FIRST_UNCONSTRAINED = "first-unconstrained";
|
|
5
27
|
class D1Session {
|
|
6
28
|
session;
|
|
@@ -10,21 +32,7 @@ class D1Session {
|
|
|
10
32
|
this.session = session;
|
|
11
33
|
}
|
|
12
34
|
prepare(sql) {
|
|
13
|
-
|
|
14
|
-
if (cached) {
|
|
15
|
-
this.stmtCache.delete(sql);
|
|
16
|
-
this.stmtCache.set(sql, cached);
|
|
17
|
-
return cached;
|
|
18
|
-
}
|
|
19
|
-
const stmt = this.session.prepare(sql);
|
|
20
|
-
if (this.stmtCache.size >= STMT_CACHE_CAPACITY) {
|
|
21
|
-
const oldest = this.stmtCache.keys().next().value;
|
|
22
|
-
if (oldest !== void 0) {
|
|
23
|
-
this.stmtCache.delete(oldest);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
this.stmtCache.set(sql, stmt);
|
|
27
|
-
return stmt;
|
|
35
|
+
return prepareCached(this.stmtCache, (text) => this.session.prepare(text), sql);
|
|
28
36
|
}
|
|
29
37
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters -- T types the result rows for the caller and is forwarded to the prepared statement.
|
|
30
38
|
async run(sql, ...binds) {
|
|
@@ -84,21 +92,7 @@ class D1Client {
|
|
|
84
92
|
* even when the previous caller already called `.bind(...).run()`.
|
|
85
93
|
*/
|
|
86
94
|
prepare(sql) {
|
|
87
|
-
|
|
88
|
-
if (cached) {
|
|
89
|
-
this.stmtCache.delete(sql);
|
|
90
|
-
this.stmtCache.set(sql, cached);
|
|
91
|
-
return cached;
|
|
92
|
-
}
|
|
93
|
-
const stmt = this.db.prepare(sql);
|
|
94
|
-
if (this.stmtCache.size >= STMT_CACHE_CAPACITY) {
|
|
95
|
-
const oldest = this.stmtCache.keys().next().value;
|
|
96
|
-
if (oldest !== void 0) {
|
|
97
|
-
this.stmtCache.delete(oldest);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
this.stmtCache.set(sql, stmt);
|
|
101
|
-
return stmt;
|
|
95
|
+
return prepareCached(this.stmtCache, (text) => this.db.prepare(text), sql);
|
|
102
96
|
}
|
|
103
97
|
/**
|
|
104
98
|
* Drizzle handle over the bare `env.DB` binding. Used for typed queries
|
|
@@ -1,18 +1,19 @@
|
|
|
1
|
+
import { LunoraError } from '@lunora/errors';
|
|
1
2
|
import { sql } from 'drizzle-orm';
|
|
2
|
-
import { D1Client } from './D1Client-
|
|
3
|
+
import { D1Client } from './D1Client-DSj8p7c4.mjs';
|
|
3
4
|
|
|
4
5
|
const TRACKING_TABLE_NAME = "__drizzle_migrations";
|
|
5
|
-
const TRACKING_TABLE_DDL = `CREATE TABLE IF NOT EXISTS ${TRACKING_TABLE_NAME} (id INTEGER PRIMARY KEY AUTOINCREMENT, hash TEXT NOT NULL, created_at NUMERIC)`;
|
|
6
|
+
const TRACKING_TABLE_DDL = `CREATE TABLE IF NOT EXISTS ${TRACKING_TABLE_NAME} (id INTEGER PRIMARY KEY AUTOINCREMENT, hash TEXT NOT NULL UNIQUE, created_at NUMERIC)`;
|
|
6
7
|
const WHITESPACE_RE = /\s/u;
|
|
7
|
-
const TRAILING_SEMICOLON_RE = /;\s*$/u;
|
|
8
8
|
const SHA256_HEX_RE = /^[0-9a-f]{64}$/u;
|
|
9
|
+
const TRACKING_HASH_UNIQUE_RE = new RegExp(String.raw`UNIQUE constraint failed:\s*${TRACKING_TABLE_NAME}\.hash`, "iu");
|
|
9
10
|
const assertSingleStatement = (migration) => {
|
|
10
11
|
const text = migration.sql;
|
|
11
12
|
let inSingle = false;
|
|
12
13
|
let inDouble = false;
|
|
13
14
|
let inLineComment = false;
|
|
14
15
|
let inBlockComment = false;
|
|
15
|
-
let
|
|
16
|
+
let terminatorIndex;
|
|
16
17
|
for (let index = 0; index < text.length; index += 1) {
|
|
17
18
|
const character = text[index];
|
|
18
19
|
const next = text[index + 1];
|
|
@@ -49,14 +50,6 @@ const assertSingleStatement = (migration) => {
|
|
|
49
50
|
}
|
|
50
51
|
continue;
|
|
51
52
|
}
|
|
52
|
-
if (character === "'") {
|
|
53
|
-
inSingle = true;
|
|
54
|
-
continue;
|
|
55
|
-
}
|
|
56
|
-
if (character === '"') {
|
|
57
|
-
inDouble = true;
|
|
58
|
-
continue;
|
|
59
|
-
}
|
|
60
53
|
if (character === "-" && next === "-") {
|
|
61
54
|
inLineComment = true;
|
|
62
55
|
index += 1;
|
|
@@ -67,16 +60,28 @@ const assertSingleStatement = (migration) => {
|
|
|
67
60
|
index += 1;
|
|
68
61
|
continue;
|
|
69
62
|
}
|
|
70
|
-
if (
|
|
71
|
-
|
|
63
|
+
if (terminatorIndex !== void 0) {
|
|
64
|
+
if (character !== void 0 && !WHITESPACE_RE.test(character)) {
|
|
65
|
+
throw new LunoraError(
|
|
66
|
+
"INTERNAL",
|
|
67
|
+
`Migration "${migration.name}" (v${String(migration.version)}) contains more than one SQL statement. Split it into separate migrations — batch() runs them atomically.`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
72
70
|
continue;
|
|
73
71
|
}
|
|
74
|
-
if (
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
72
|
+
if (character === "'") {
|
|
73
|
+
inSingle = true;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (character === '"') {
|
|
77
|
+
inDouble = true;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (character === ";") {
|
|
81
|
+
terminatorIndex = index;
|
|
78
82
|
}
|
|
79
83
|
}
|
|
84
|
+
return terminatorIndex;
|
|
80
85
|
};
|
|
81
86
|
const hashMigration = async (text) => {
|
|
82
87
|
const bytes = new TextEncoder().encode(text);
|
|
@@ -110,26 +115,38 @@ class MigrationRunner {
|
|
|
110
115
|
skipped.push({ name: migration.name, version: migration.version });
|
|
111
116
|
continue;
|
|
112
117
|
}
|
|
113
|
-
await this.applyOne(migration, hash);
|
|
114
|
-
|
|
118
|
+
const didApply = await this.applyOne(migration, hash);
|
|
119
|
+
if (didApply) {
|
|
120
|
+
applied.push({ name: migration.name, version: migration.version });
|
|
121
|
+
} else {
|
|
122
|
+
skipped.push({ name: migration.name, version: migration.version });
|
|
123
|
+
}
|
|
115
124
|
}
|
|
116
125
|
return { applied, skipped };
|
|
117
126
|
}
|
|
118
127
|
async applyOne(migration, hash) {
|
|
119
|
-
assertSingleStatement(migration);
|
|
120
|
-
const statementText = migration.sql.
|
|
128
|
+
const terminatorIndex = assertSingleStatement(migration);
|
|
129
|
+
const statementText = (terminatorIndex === void 0 ? migration.sql : migration.sql.slice(0, terminatorIndex)).trim();
|
|
121
130
|
if (!SHA256_HEX_RE.test(hash)) {
|
|
122
|
-
throw new
|
|
131
|
+
throw new LunoraError("INTERNAL", `migration "${migration.name}" produced a non-hex hash; refusing to inline into SQL`);
|
|
123
132
|
}
|
|
124
133
|
const trackingInsertSql = `INSERT INTO ${TRACKING_TABLE_NAME} (hash, created_at) VALUES ('${hash}', ${String(Date.now())})`;
|
|
125
134
|
const items = [this.client.drizzle.run(sql.raw(statementText)), this.client.drizzle.run(sql.raw(trackingInsertSql))];
|
|
126
|
-
|
|
135
|
+
try {
|
|
136
|
+
await this.client.batch(items);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
if (TRACKING_HASH_UNIQUE_RE.test(error instanceof Error ? error.message : String(error))) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
return true;
|
|
127
144
|
}
|
|
128
145
|
assertUniqueVersions() {
|
|
129
146
|
const seen = /* @__PURE__ */ new Set();
|
|
130
147
|
for (const m of this.migrations) {
|
|
131
148
|
if (seen.has(m.version)) {
|
|
132
|
-
throw new
|
|
149
|
+
throw new LunoraError("INTERNAL", `Duplicate migration version ${String(m.version)}`);
|
|
133
150
|
}
|
|
134
151
|
seen.add(m.version);
|
|
135
152
|
}
|
|
@@ -139,7 +156,10 @@ class MigrationRunner {
|
|
|
139
156
|
for (const m of this.migrations) {
|
|
140
157
|
const previousVersion = seen.get(m.sql);
|
|
141
158
|
if (previousVersion !== void 0) {
|
|
142
|
-
throw new
|
|
159
|
+
throw new LunoraError(
|
|
160
|
+
"INTERNAL",
|
|
161
|
+
`Migrations ${String(previousVersion)} and ${String(m.version)} have identical SQL — bump the content, not just the version.`
|
|
162
|
+
);
|
|
143
163
|
}
|
|
144
164
|
seen.set(m.sql, m.version);
|
|
145
165
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { toErrorBody } from '@lunora/errors';
|
|
2
|
+
import { runD1GlobalTableMigrations } from './createD1CtxDb-BMR8J0dT.mjs';
|
|
3
|
+
import { quoteIdentifier } from './quoteIdentifier-B-ZeSe1V.mjs';
|
|
1
4
|
import { decodeGlobalRow } from '@lunora/sql-store';
|
|
2
5
|
|
|
3
6
|
const DEFAULT_BATCH_SIZE = 200;
|
|
4
|
-
const quoteIdentifier = (name) => `"${name.replaceAll('"', '""')}"`;
|
|
5
7
|
const selectGlobalTables = (schema, requested) => {
|
|
6
8
|
const isGlobal = (table) => schema.tables[table]?.shardMode?.kind === "global";
|
|
7
9
|
if (requested && requested.length > 0) {
|
|
@@ -19,16 +21,21 @@ const decodeRow = (schema, table, row) => {
|
|
|
19
21
|
const exportGlobalRows = async function* (exec, schema, args) {
|
|
20
22
|
const tables = selectGlobalTables(schema, args.tables);
|
|
21
23
|
const batchSize = args.batchSize ?? DEFAULT_BATCH_SIZE;
|
|
24
|
+
await runD1GlobalTableMigrations(exec, schema);
|
|
22
25
|
for (const table of tables) {
|
|
23
|
-
|
|
26
|
+
const quoted = quoteIdentifier(table);
|
|
27
|
+
let lastId;
|
|
24
28
|
let hasMore = true;
|
|
25
29
|
while (hasMore) {
|
|
26
|
-
const rows = await exec.all(`SELECT * FROM ${
|
|
30
|
+
const rows = lastId === void 0 ? await exec.all(`SELECT * FROM ${quoted} ORDER BY "id" LIMIT ?`, [batchSize]) : await exec.all(`SELECT * FROM ${quoted} WHERE "id" > ? ORDER BY "id" LIMIT ?`, [lastId, batchSize]);
|
|
27
31
|
for (const row of rows) {
|
|
28
32
|
yield { doc: decodeRow(schema, table, row), table };
|
|
29
33
|
}
|
|
34
|
+
const last = rows.at(-1);
|
|
35
|
+
if (last !== void 0) {
|
|
36
|
+
lastId = String(last["id"]);
|
|
37
|
+
}
|
|
30
38
|
hasMore = rows.length === batchSize;
|
|
31
|
-
offset += rows.length;
|
|
32
39
|
}
|
|
33
40
|
}
|
|
34
41
|
};
|
|
@@ -88,9 +95,8 @@ const importOneRow = async (writer, schema, args, row, line) => {
|
|
|
88
95
|
await writer.insert(table, doc, { allowExplicitId: true });
|
|
89
96
|
return { inserted: table, kind: "inserted" };
|
|
90
97
|
} catch (error) {
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
return { error: { code, line, message, table }, kind: "error" };
|
|
98
|
+
const { body } = toErrorBody(error, { fallbackCode: "INSERT_FAILED" });
|
|
99
|
+
return { error: { code: body.code, line, message: body.message, table }, kind: "error" };
|
|
94
100
|
}
|
|
95
101
|
};
|
|
96
102
|
const importGlobalRows = async (writer, schema, args) => {
|
package/dist/packem_shared/{facetGlobalColumn-C6u_WMIY.mjs → facetGlobalColumn-DnglIe4P.mjs}
RENAMED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { LunoraError } from '@lunora/errors';
|
|
1
2
|
import { runD1GlobalTableMigrations } from './createD1CtxDb-BMR8J0dT.mjs';
|
|
3
|
+
import { quoteIdentifier } from './quoteIdentifier-B-ZeSe1V.mjs';
|
|
2
4
|
import { decodeGlobalRow } from '@lunora/sql-store';
|
|
3
5
|
|
|
4
6
|
const ensureGlobalTables = (exec, schema) => runD1GlobalTableMigrations(exec, schema);
|
|
@@ -6,7 +8,6 @@ const DEFAULT_PAGE_SIZE = 50;
|
|
|
6
8
|
const MAX_PAGE_SIZE = 500;
|
|
7
9
|
const DEFAULT_FACET_LIMIT = 30;
|
|
8
10
|
const MAX_FACET_LIMIT = 200;
|
|
9
|
-
const quoteIdentifier = (name) => `"${name.replaceAll('"', '""')}"`;
|
|
10
11
|
const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
|
|
11
12
|
const INTERNAL_TABLE = /^sqlite_|^_cf_|^d1_|^__cdc|__agg_|__rank_|__fts_/u;
|
|
12
13
|
const isInternalTable = (name) => INTERNAL_TABLE.test(name);
|
|
@@ -28,7 +29,10 @@ const buildEqPredicate = (schema, table, displayColumns, filters) => {
|
|
|
28
29
|
const params = [];
|
|
29
30
|
for (const filter of filters) {
|
|
30
31
|
if (!displayColumns.includes(filter.column)) {
|
|
31
|
-
throw
|
|
32
|
+
throw new LunoraError("UNKNOWN_COLUMN", `unknown column: ${filter.column}`, { status: 404 });
|
|
33
|
+
}
|
|
34
|
+
if (schema.tables[table] === void 0 && SENSITIVE_COLUMN.test(filter.column)) {
|
|
35
|
+
throw new LunoraError("FORBIDDEN", `cannot filter on a redacted column: ${filter.column}`, { status: 403 });
|
|
32
36
|
}
|
|
33
37
|
const quoted = quoteIdentifier(physicalColumnName(schema, table, filter.column));
|
|
34
38
|
if (filter.value === null || filter.value === void 0) {
|
|
@@ -89,7 +93,7 @@ const readGlobalTablePage = async (exec, schema, options) => {
|
|
|
89
93
|
await ensureGlobalTables(exec, schema);
|
|
90
94
|
const tableNames = await listTableNames(exec);
|
|
91
95
|
if (!tableNames.includes(table)) {
|
|
92
|
-
throw
|
|
96
|
+
throw new LunoraError("UNKNOWN_TABLE", `unknown table: ${table}`, { status: 404 });
|
|
93
97
|
}
|
|
94
98
|
const limit = clamp(Math.trunc(options.limit ?? DEFAULT_PAGE_SIZE), 1, MAX_PAGE_SIZE);
|
|
95
99
|
const offset = Math.max(0, Math.trunc(options.offset ?? 0));
|
|
@@ -109,11 +113,11 @@ const facetGlobalColumn = async (exec, schema, options) => {
|
|
|
109
113
|
await ensureGlobalTables(exec, schema);
|
|
110
114
|
const tableNames = await listTableNames(exec);
|
|
111
115
|
if (!tableNames.includes(table)) {
|
|
112
|
-
throw
|
|
116
|
+
throw new LunoraError("UNKNOWN_TABLE", `unknown table: ${table}`, { status: 404 });
|
|
113
117
|
}
|
|
114
118
|
const columns = await resolveColumns(exec, schema, table);
|
|
115
119
|
if (!columns.includes(column)) {
|
|
116
|
-
throw
|
|
120
|
+
throw new LunoraError("UNKNOWN_COLUMN", `unknown column: ${column}`, { status: 404 });
|
|
117
121
|
}
|
|
118
122
|
const quoted = quoteIdentifier(table);
|
|
119
123
|
const predicate = buildEqPredicate(schema, table, columns, options.filters);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lunora/d1",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.31",
|
|
4
4
|
"description": "D1 adapter for Lunora .global() tables, wrapping the Sessions API for read-your-writes",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cloudflare",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"directory": "packages/d1"
|
|
26
26
|
},
|
|
27
27
|
"files": [
|
|
28
|
-
"dist",
|
|
28
|
+
"./dist",
|
|
29
29
|
"README.md",
|
|
30
30
|
"LICENSE.md",
|
|
31
31
|
"__assets__"
|
|
@@ -50,8 +50,9 @@
|
|
|
50
50
|
"access": "public"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@lunora/do": "1.0.0-alpha.
|
|
54
|
-
"@lunora/
|
|
53
|
+
"@lunora/do": "1.0.0-alpha.32",
|
|
54
|
+
"@lunora/errors": "1.0.0-alpha.5",
|
|
55
|
+
"@lunora/sql-store": "1.0.0-alpha.32",
|
|
55
56
|
"drizzle-orm": "^0.45.2"
|
|
56
57
|
},
|
|
57
58
|
"engines": {
|