@metaobjectsdev/cli 0.8.1 → 0.9.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/README.md +40 -1
- package/dist/src/commands/migrate.d.ts +16 -0
- package/dist/src/commands/migrate.d.ts.map +1 -1
- package/dist/src/commands/migrate.js +252 -39
- package/dist/src/commands/migrate.js.map +1 -1
- package/dist/src/commands/verify.d.ts.map +1 -1
- package/dist/src/commands/verify.js +162 -53
- package/dist/src/commands/verify.js.map +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +6 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/lib/allow.d.ts +10 -0
- package/dist/src/lib/allow.d.ts.map +1 -0
- package/dist/src/lib/allow.js +50 -0
- package/dist/src/lib/allow.js.map +1 -0
- package/dist/src/lib/args.d.ts +22 -4
- package/dist/src/lib/args.d.ts.map +1 -1
- package/dist/src/lib/args.js +47 -11
- package/dist/src/lib/args.js.map +1 -1
- package/dist/src/lib/bun-sqlite-dialect.d.ts +13 -0
- package/dist/src/lib/bun-sqlite-dialect.d.ts.map +1 -0
- package/dist/src/lib/bun-sqlite-dialect.js +119 -0
- package/dist/src/lib/bun-sqlite-dialect.js.map +1 -0
- package/dist/src/lib/config.d.ts +11 -0
- package/dist/src/lib/config.d.ts.map +1 -1
- package/dist/src/lib/config.js +4 -0
- package/dist/src/lib/config.js.map +1 -1
- package/dist/src/lib/kysely.d.ts.map +1 -1
- package/dist/src/lib/kysely.js +21 -7
- package/dist/src/lib/kysely.js.map +1 -1
- package/dist/src/lib/projection-migrations.d.ts.map +1 -1
- package/dist/src/lib/projection-migrations.js +2 -6
- package/dist/src/lib/projection-migrations.js.map +1 -1
- package/package.json +11 -10
- package/src/commands/migrate.ts +277 -42
- package/src/commands/verify.ts +172 -61
- package/src/index.ts +6 -0
- package/src/lib/allow.ts +54 -0
- package/src/lib/args.ts +77 -15
- package/src/lib/bun-sqlite-dialect.ts +146 -0
- package/src/lib/config.ts +15 -0
- package/src/lib/kysely.ts +23 -10
- package/src/lib/projection-migrations.ts +2 -6
package/src/index.ts
CHANGED
|
@@ -58,6 +58,12 @@ EXPORT FLAGS:
|
|
|
58
58
|
|
|
59
59
|
VERIFY FLAGS:
|
|
60
60
|
--prompts <dir> Directory of provider-resolved template text (default: prompts)
|
|
61
|
+
--db <url> Live DB URL — enables the schema-drift gate (exit 1 on drift).
|
|
62
|
+
Supports: file:, libsql:, postgres:, postgresql:. Omit to skip.
|
|
63
|
+
--dialect sqlite|postgres Optional override (auto-detected from --db URL scheme)
|
|
64
|
+
--allow <csv> Accepted for parity with 'migrate'; does NOT affect the
|
|
65
|
+
verify drift gate (the gate fails on ANY detected change)
|
|
66
|
+
--skip-schema Skip the schema-drift gate even when --db is present
|
|
61
67
|
|
|
62
68
|
PROMPT-SNAPSHOT FLAGS:
|
|
63
69
|
--check Compare against committed snapshots; exit 1 on drift (CI gate)
|
package/src/lib/allow.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Shared destructive-change-permission parsing + change description, used by
|
|
2
|
+
// both `meta migrate` and `meta verify --db`. Keeping a single copy avoids the
|
|
3
|
+
// two commands drifting on which `--allow` tokens exist or how a change reads.
|
|
4
|
+
|
|
5
|
+
import type { AllowOptions, Change } from "@metaobjectsdev/migrate-ts";
|
|
6
|
+
|
|
7
|
+
// Map CLI allow tokens → migrate-ts AllowOptions field names.
|
|
8
|
+
const ALLOW_TOKEN_MAP: Record<string, keyof AllowOptions> = {
|
|
9
|
+
"drop-column": "dropColumn",
|
|
10
|
+
"drop-table": "dropTable",
|
|
11
|
+
"type-change": "typeChange",
|
|
12
|
+
"drop-index": "dropIndex",
|
|
13
|
+
"drop-fk": "dropFk",
|
|
14
|
+
"nullable-to-not-null": "nullableToNotNull",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/** Translate parsed `--allow` tokens into the migrate-ts `AllowOptions` shape. */
|
|
18
|
+
export function tokensToAllowOptions(tokens: string[]): AllowOptions {
|
|
19
|
+
const opts: AllowOptions = {};
|
|
20
|
+
for (const tok of tokens) {
|
|
21
|
+
const field = ALLOW_TOKEN_MAP[tok];
|
|
22
|
+
if (field !== undefined) {
|
|
23
|
+
opts[field] = true;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return opts;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* One-line, human-readable detail for a single change (table/column/index/fk/
|
|
31
|
+
* view). This is the shared core: `migrate` prints it as-is; `verify` prefixes
|
|
32
|
+
* a +/-/~ glyph and a noun (`table`/`column`/…) on top.
|
|
33
|
+
*/
|
|
34
|
+
export function describeChange(c: Change): string {
|
|
35
|
+
switch (c.kind) {
|
|
36
|
+
case "create-table": return c.table.name;
|
|
37
|
+
case "drop-table": return c.table;
|
|
38
|
+
case "rename-table": return `${c.from} → ${c.to}`;
|
|
39
|
+
case "add-column": return `${c.table}.${c.column.name}`;
|
|
40
|
+
case "drop-column": return `${c.table}.${c.column}`;
|
|
41
|
+
case "rename-column": return `${c.table}.${c.from} → ${c.table}.${c.to}`;
|
|
42
|
+
case "change-column-type": return `${c.table}.${c.column} (${c.from.kind} → ${c.to.kind})`;
|
|
43
|
+
case "change-column-nullable": return `${c.table}.${c.column} (${c.from ? "NULL" : "NOT NULL"} → ${c.to ? "NULL" : "NOT NULL"})`;
|
|
44
|
+
case "change-column-default": return `${c.table}.${c.column}`;
|
|
45
|
+
case "add-index": return `${c.table} idx ${c.index.name}`;
|
|
46
|
+
case "drop-index": return `${c.table} idx ${c.index}`;
|
|
47
|
+
case "add-fk": return `${c.table} fk ${c.fk.name}`;
|
|
48
|
+
case "drop-fk": return `${c.table} fk ${c.fk}`;
|
|
49
|
+
case "create-view": return c.view.name;
|
|
50
|
+
case "replace-view": return c.view.name;
|
|
51
|
+
case "drop-view": return c.view;
|
|
52
|
+
default: return JSON.stringify(c);
|
|
53
|
+
}
|
|
54
|
+
}
|
package/src/lib/args.ts
CHANGED
|
@@ -92,6 +92,23 @@ export function parseExportArgs(argv: string[]): ExportFlags {
|
|
|
92
92
|
};
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// shared DB-connection vocab (used by both verify --db and migrate)
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
const DIALECTS = ["sqlite", "postgres", "d1"] as const;
|
|
100
|
+
type Dialect = (typeof DIALECTS)[number];
|
|
101
|
+
|
|
102
|
+
const ALLOW_TOKENS = [
|
|
103
|
+
"drop-column",
|
|
104
|
+
"drop-table",
|
|
105
|
+
"type-change",
|
|
106
|
+
"drop-index",
|
|
107
|
+
"drop-fk",
|
|
108
|
+
"nullable-to-not-null",
|
|
109
|
+
] as const;
|
|
110
|
+
type AllowToken = (typeof ALLOW_TOKENS)[number];
|
|
111
|
+
|
|
95
112
|
// ---------------------------------------------------------------------------
|
|
96
113
|
// verify flags
|
|
97
114
|
// ---------------------------------------------------------------------------
|
|
@@ -99,6 +116,14 @@ export function parseExportArgs(argv: string[]): ExportFlags {
|
|
|
99
116
|
export interface VerifyFlags {
|
|
100
117
|
/** Directory (relative to cwd) holding provider-resolved template text. */
|
|
101
118
|
prompts: string | undefined;
|
|
119
|
+
/** Live DB connection URL; when present, enables the schema-drift gate. */
|
|
120
|
+
db: string | undefined;
|
|
121
|
+
/** Optional dialect override (auto-detected from --db URL scheme otherwise). */
|
|
122
|
+
dialect: Dialect | undefined;
|
|
123
|
+
/** Destructive-change permissions; only affects how drift is described. */
|
|
124
|
+
allow: AllowToken[];
|
|
125
|
+
/** Skip the schema-drift gate even when --db is present. */
|
|
126
|
+
skipSchema: boolean;
|
|
102
127
|
}
|
|
103
128
|
|
|
104
129
|
export function parseVerifyArgs(argv: string[]): VerifyFlags {
|
|
@@ -106,12 +131,38 @@ export function parseVerifyArgs(argv: string[]): VerifyFlags {
|
|
|
106
131
|
args: argv,
|
|
107
132
|
options: {
|
|
108
133
|
prompts: { type: "string" },
|
|
134
|
+
db: { type: "string" },
|
|
135
|
+
dialect: { type: "string" },
|
|
136
|
+
allow: { type: "string" },
|
|
137
|
+
"skip-schema": { type: "boolean", default: false },
|
|
109
138
|
},
|
|
110
139
|
strict: true,
|
|
111
140
|
allowPositionals: false,
|
|
112
141
|
});
|
|
142
|
+
|
|
143
|
+
const dialect = values.dialect as string | undefined;
|
|
144
|
+
if (dialect !== undefined && !DIALECTS.includes(dialect as Dialect)) {
|
|
145
|
+
throw new Error(`invalid --dialect '${dialect}'; expected: ${DIALECTS.join(", ")}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const allowRaw = (values.allow as string | undefined) ?? "";
|
|
149
|
+
const allowTokens = allowRaw.length === 0
|
|
150
|
+
? []
|
|
151
|
+
: allowRaw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
152
|
+
for (const tok of allowTokens) {
|
|
153
|
+
if (!ALLOW_TOKENS.includes(tok as AllowToken)) {
|
|
154
|
+
throw new Error(
|
|
155
|
+
`invalid --allow token '${tok}'; expected one of: ${ALLOW_TOKENS.join(", ")}`,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
113
160
|
return {
|
|
114
161
|
prompts: values.prompts,
|
|
162
|
+
db: values.db as string | undefined,
|
|
163
|
+
dialect: dialect as Dialect | undefined,
|
|
164
|
+
allow: allowTokens as AllowToken[],
|
|
165
|
+
skipSchema: !!values["skip-schema"],
|
|
115
166
|
};
|
|
116
167
|
}
|
|
117
168
|
|
|
@@ -146,19 +197,6 @@ export function parsePromptSnapshotArgs(argv: string[]): PromptSnapshotFlags {
|
|
|
146
197
|
// migrate flags
|
|
147
198
|
// ---------------------------------------------------------------------------
|
|
148
199
|
|
|
149
|
-
const DIALECTS = ["sqlite", "postgres", "d1"] as const;
|
|
150
|
-
type Dialect = (typeof DIALECTS)[number];
|
|
151
|
-
|
|
152
|
-
const ALLOW_TOKENS = [
|
|
153
|
-
"drop-column",
|
|
154
|
-
"drop-table",
|
|
155
|
-
"type-change",
|
|
156
|
-
"drop-index",
|
|
157
|
-
"drop-fk",
|
|
158
|
-
"nullable-to-not-null",
|
|
159
|
-
] as const;
|
|
160
|
-
type AllowToken = (typeof ALLOW_TOKENS)[number];
|
|
161
|
-
|
|
162
200
|
const ON_AMBIGUOUS = ["abort", "rename", "drop-add"] as const;
|
|
163
201
|
type OnAmbiguous = (typeof ON_AMBIGUOUS)[number];
|
|
164
202
|
|
|
@@ -174,11 +212,21 @@ export interface MigrateFlags {
|
|
|
174
212
|
d1Binding: string | undefined;
|
|
175
213
|
remote: boolean;
|
|
176
214
|
apply: boolean;
|
|
215
|
+
/**
|
|
216
|
+
* Roll back all applied migrations NEWER than this target (the target itself
|
|
217
|
+
* is retained), running each migration's down.sql in reverse order. Mutually
|
|
218
|
+
* exclusive with --apply. postgres/sqlite only (not d1).
|
|
219
|
+
*/
|
|
220
|
+
rollback: string | undefined;
|
|
177
221
|
yes: boolean;
|
|
222
|
+
/** Use live-DB introspection instead of the committed snapshot (legacy/adoption). */
|
|
223
|
+
fromDb: boolean;
|
|
224
|
+
/** `migrate baseline` subcommand: seed the snapshot, emit no migration. */
|
|
225
|
+
baseline: boolean;
|
|
178
226
|
}
|
|
179
227
|
|
|
180
228
|
export function parseMigrateArgs(argv: string[]): MigrateFlags {
|
|
181
|
-
const { values } = parseArgs({
|
|
229
|
+
const { values, positionals } = parseArgs({
|
|
182
230
|
args: argv,
|
|
183
231
|
options: {
|
|
184
232
|
"db": { type: "string" },
|
|
@@ -188,15 +236,26 @@ export function parseMigrateArgs(argv: string[]): MigrateFlags {
|
|
|
188
236
|
"allow": { type: "string" },
|
|
189
237
|
"on-ambiguous": { type: "string" },
|
|
190
238
|
"dry-run": { type: "boolean", default: false },
|
|
239
|
+
"from-db": { type: "boolean", default: false },
|
|
191
240
|
"d1": { type: "string" },
|
|
192
241
|
"remote": { type: "boolean", default: false },
|
|
193
242
|
"apply": { type: "boolean", default: false },
|
|
243
|
+
"rollback": { type: "string" },
|
|
194
244
|
"yes": { type: "boolean", default: false },
|
|
195
245
|
},
|
|
196
246
|
strict: true,
|
|
197
|
-
allowPositionals:
|
|
247
|
+
allowPositionals: true,
|
|
198
248
|
});
|
|
199
249
|
|
|
250
|
+
const baseline = positionals[0] === "baseline";
|
|
251
|
+
if (positionals.length > 0 && !baseline) {
|
|
252
|
+
throw new Error(`unknown migrate subcommand '${positionals[0]}'; expected 'baseline' or no subcommand`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (values.rollback !== undefined && values.apply === true) {
|
|
256
|
+
throw new Error(`--rollback and --apply are mutually exclusive`);
|
|
257
|
+
}
|
|
258
|
+
|
|
200
259
|
const dialect = values.dialect as string | undefined;
|
|
201
260
|
if (dialect !== undefined && !DIALECTS.includes(dialect as Dialect)) {
|
|
202
261
|
throw new Error(`invalid --dialect '${dialect}'; expected: ${DIALECTS.join(", ")}`);
|
|
@@ -230,6 +289,9 @@ export function parseMigrateArgs(argv: string[]): MigrateFlags {
|
|
|
230
289
|
d1Binding: values.d1 as string | undefined,
|
|
231
290
|
remote: !!values.remote,
|
|
232
291
|
apply: !!values.apply,
|
|
292
|
+
rollback: values.rollback as string | undefined,
|
|
233
293
|
yes: !!values.yes,
|
|
294
|
+
fromDb: !!values["from-db"],
|
|
295
|
+
baseline,
|
|
234
296
|
};
|
|
235
297
|
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// A Kysely SQLite dialect backed by Bun's built-in `bun:sqlite`.
|
|
2
|
+
//
|
|
3
|
+
// WHY: the standalone `meta` binary is produced with `bun build --compile`,
|
|
4
|
+
// which embeds the Bun runtime but CANNOT bundle native node addons. The
|
|
5
|
+
// default sqlite driver (`@libsql/kysely-libsql`) loads a platform-native
|
|
6
|
+
// `.node` addon (`@libsql/linux-x64-gnu` etc.) via a runtime require that the
|
|
7
|
+
// single-file binary's virtual filesystem can't resolve — so libsql fails
|
|
8
|
+
// inside the compiled binary with `Cannot find module '@libsql/linux-x64-gnu'`.
|
|
9
|
+
//
|
|
10
|
+
// `bun:sqlite` ships *inside* the Bun runtime that `--compile` embeds, so it
|
|
11
|
+
// needs no on-disk addon. This dialect is therefore the sqlite driver the
|
|
12
|
+
// standalone binary uses. The npm/Node distribution keeps using libsql (this
|
|
13
|
+
// module is only loaded when running under Bun — see buildKyselyFromUrl).
|
|
14
|
+
//
|
|
15
|
+
// The `bun:sqlite` import is dynamic + typed loosely so the tsc → Node build
|
|
16
|
+
// (which has no `bun:sqlite` module) never tries to resolve it statically.
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
type DatabaseConnection,
|
|
20
|
+
type Dialect,
|
|
21
|
+
type Driver,
|
|
22
|
+
type QueryResult,
|
|
23
|
+
SqliteAdapter,
|
|
24
|
+
SqliteIntrospector,
|
|
25
|
+
SqliteQueryCompiler,
|
|
26
|
+
type CompiledQuery,
|
|
27
|
+
type DatabaseIntrospector,
|
|
28
|
+
type Kysely,
|
|
29
|
+
type DialectAdapter,
|
|
30
|
+
type QueryCompiler,
|
|
31
|
+
} from "kysely";
|
|
32
|
+
|
|
33
|
+
/** Minimal structural view of a `bun:sqlite` Database — only what we use. */
|
|
34
|
+
interface BunSqliteDatabase {
|
|
35
|
+
query(sql: string): BunSqliteStatement;
|
|
36
|
+
run(sql: string): void;
|
|
37
|
+
close(): void;
|
|
38
|
+
}
|
|
39
|
+
interface BunSqliteStatement {
|
|
40
|
+
all(...params: readonly unknown[]): unknown[];
|
|
41
|
+
run(...params: readonly unknown[]): { changes: number; lastInsertRowid: number | bigint };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Open a `bun:sqlite` database for a `file:`/`libsql:` URL (or a bare path).
|
|
46
|
+
* Throws when not running under Bun (no `bun:sqlite` module).
|
|
47
|
+
*/
|
|
48
|
+
async function openBunSqlite(url: string): Promise<BunSqliteDatabase> {
|
|
49
|
+
// `bun:sqlite` is a Bun built-in; the dynamic specifier keeps Node/tsc from
|
|
50
|
+
// resolving it. The cast is necessary because there's no ambient type here.
|
|
51
|
+
const mod = (await import("bun:sqlite")) as unknown as {
|
|
52
|
+
Database: new (filename: string) => BunSqliteDatabase;
|
|
53
|
+
};
|
|
54
|
+
// Strip a file:/libsql: scheme to a plain filesystem path; `:memory:` stays.
|
|
55
|
+
let filename = url;
|
|
56
|
+
const schemeMatch = /^(file|libsql):\/\/(.*)$/i.exec(url) ?? /^(file|libsql):(.*)$/i.exec(url);
|
|
57
|
+
if (schemeMatch) filename = schemeMatch[2] ?? "";
|
|
58
|
+
if (filename === "") filename = ":memory:";
|
|
59
|
+
return new mod.Database(filename);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
class BunSqliteConnection implements DatabaseConnection {
|
|
63
|
+
constructor(private readonly db: BunSqliteDatabase) {}
|
|
64
|
+
|
|
65
|
+
async executeQuery<R>(compiledQuery: CompiledQuery): Promise<QueryResult<R>> {
|
|
66
|
+
const { sql, parameters } = compiledQuery;
|
|
67
|
+
const stmt = this.db.query(sql);
|
|
68
|
+
const params = parameters as readonly unknown[];
|
|
69
|
+
// SELECT-shaped statements return rows; everything else reports counts.
|
|
70
|
+
const isReturningRows = /^\s*(select|with|pragma)\b/i.test(sql);
|
|
71
|
+
if (isReturningRows) {
|
|
72
|
+
const rows = stmt.all(...params) as R[];
|
|
73
|
+
return { rows };
|
|
74
|
+
}
|
|
75
|
+
const info = stmt.run(...params);
|
|
76
|
+
return {
|
|
77
|
+
numAffectedRows: BigInt(info.changes),
|
|
78
|
+
insertId: BigInt(info.lastInsertRowid),
|
|
79
|
+
rows: [],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Async generator that never yields by design — streaming is unsupported.
|
|
84
|
+
async *streamQuery<R>(): AsyncIterableIterator<QueryResult<R>> {
|
|
85
|
+
throw new Error("bun:sqlite dialect does not support streaming");
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
class BunSqliteDriver implements Driver {
|
|
90
|
+
private db: BunSqliteDatabase | undefined;
|
|
91
|
+
private connection: BunSqliteConnection | undefined;
|
|
92
|
+
|
|
93
|
+
constructor(private readonly url: string) {}
|
|
94
|
+
|
|
95
|
+
async init(): Promise<void> {
|
|
96
|
+
this.db = await openBunSqlite(this.url);
|
|
97
|
+
this.connection = new BunSqliteConnection(this.db);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async acquireConnection(): Promise<DatabaseConnection> {
|
|
101
|
+
if (this.connection === undefined) throw new Error("bun:sqlite driver not initialized");
|
|
102
|
+
return this.connection;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async beginTransaction(conn: DatabaseConnection): Promise<void> {
|
|
106
|
+
await conn.executeQuery({ sql: "begin", parameters: [], query: { kind: "RawNode" } as never });
|
|
107
|
+
}
|
|
108
|
+
async commitTransaction(conn: DatabaseConnection): Promise<void> {
|
|
109
|
+
await conn.executeQuery({ sql: "commit", parameters: [], query: { kind: "RawNode" } as never });
|
|
110
|
+
}
|
|
111
|
+
async rollbackTransaction(conn: DatabaseConnection): Promise<void> {
|
|
112
|
+
await conn.executeQuery({ sql: "rollback", parameters: [], query: { kind: "RawNode" } as never });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async releaseConnection(): Promise<void> {
|
|
116
|
+
/* single shared connection — nothing to release */
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async destroy(): Promise<void> {
|
|
120
|
+
this.db?.close();
|
|
121
|
+
this.db = undefined;
|
|
122
|
+
this.connection = undefined;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Kysely Dialect backed by `bun:sqlite`. */
|
|
127
|
+
export class BunSqliteDialect implements Dialect {
|
|
128
|
+
constructor(private readonly url: string) {}
|
|
129
|
+
createDriver(): Driver {
|
|
130
|
+
return new BunSqliteDriver(this.url);
|
|
131
|
+
}
|
|
132
|
+
createQueryCompiler(): QueryCompiler {
|
|
133
|
+
return new SqliteQueryCompiler();
|
|
134
|
+
}
|
|
135
|
+
createAdapter(): DialectAdapter {
|
|
136
|
+
return new SqliteAdapter();
|
|
137
|
+
}
|
|
138
|
+
createIntrospector(db: Kysely<Record<string, unknown>>): DatabaseIntrospector {
|
|
139
|
+
return new SqliteIntrospector(db);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** True when running under the Bun runtime (where `bun:sqlite` is available). */
|
|
144
|
+
export function isBun(): boolean {
|
|
145
|
+
return typeof (globalThis as { Bun?: unknown }).Bun !== "undefined";
|
|
146
|
+
}
|
package/src/lib/config.ts
CHANGED
|
@@ -42,7 +42,18 @@ export interface ResolvedMigrateConfig {
|
|
|
42
42
|
allow: string[];
|
|
43
43
|
slug: string | undefined;
|
|
44
44
|
dryRun: boolean;
|
|
45
|
+
/** Run pending migration files against the DB (postgres/sqlite, ledger-backed). */
|
|
46
|
+
apply: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Roll back all applied migrations newer than this target (target retained),
|
|
49
|
+
* postgres/sqlite only. Mutually exclusive with apply.
|
|
50
|
+
*/
|
|
51
|
+
rollback: string | undefined;
|
|
45
52
|
yes: boolean;
|
|
53
|
+
/** Use live-DB introspection instead of the committed snapshot. */
|
|
54
|
+
fromDb: boolean;
|
|
55
|
+
/** Seed the snapshot and exit (no migration). */
|
|
56
|
+
baseline: boolean;
|
|
46
57
|
d1: ResolvedD1Config;
|
|
47
58
|
}
|
|
48
59
|
|
|
@@ -87,7 +98,11 @@ export async function resolveMigrateConfig(
|
|
|
87
98
|
: (cfgBlock.allow ?? MIGRATE_DEFAULTS.allow),
|
|
88
99
|
slug: flags.slug,
|
|
89
100
|
dryRun: flags.dryRun,
|
|
101
|
+
apply: flags.apply,
|
|
102
|
+
rollback: flags.rollback,
|
|
90
103
|
yes: flags.yes,
|
|
104
|
+
fromDb: flags.fromDb,
|
|
105
|
+
baseline: flags.baseline,
|
|
91
106
|
d1: {
|
|
92
107
|
binding: flags.d1Binding ?? d1Block.binding,
|
|
93
108
|
remote: flags.remote || (d1Block.remote ?? false),
|
package/src/lib/kysely.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Kysely } from "kysely";
|
|
2
|
+
import { BunSqliteDialect, isBun } from "./bun-sqlite-dialect.js";
|
|
2
3
|
|
|
3
4
|
export type Dialect = "sqlite" | "postgres" | "d1";
|
|
4
5
|
|
|
@@ -63,17 +64,29 @@ export async function buildKyselyFromUrl(
|
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
if (dialect === "sqlite") {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
);
|
|
67
|
+
// Under Bun (notably the `bun build --compile` standalone binary), use the
|
|
68
|
+
// built-in `bun:sqlite` driver. It ships inside the embedded Bun runtime,
|
|
69
|
+
// so — unlike `@libsql/kysely-libsql`, whose platform-native `.node` addon
|
|
70
|
+
// can't be bundled into a single-file binary — it works in the compiled
|
|
71
|
+
// `meta` binary with no on-disk dependency. The Node/npm distribution falls
|
|
72
|
+
// through to libsql below.
|
|
73
|
+
let sqliteDialect: ConstructorParameters<typeof Kysely<Record<string, unknown>>>[0]["dialect"];
|
|
74
|
+
if (isBun()) {
|
|
75
|
+
sqliteDialect = new BunSqliteDialect(url);
|
|
76
|
+
} else {
|
|
77
|
+
type LibsqlDialectCtor = new (opts: { url: string }) => ConstructorParameters<typeof Kysely<Record<string, unknown>>>[0]["dialect"];
|
|
78
|
+
let LibsqlDialect: LibsqlDialectCtor;
|
|
79
|
+
try {
|
|
80
|
+
const mod = await import("@libsql/kysely-libsql");
|
|
81
|
+
LibsqlDialect = mod.LibsqlDialect as unknown as LibsqlDialectCtor;
|
|
82
|
+
} catch {
|
|
83
|
+
throw new Error(
|
|
84
|
+
`dialect 'sqlite' requires '@libsql/kysely-libsql'; install it: 'bun add @libsql/kysely-libsql'`,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
sqliteDialect = new LibsqlDialect({ url });
|
|
75
88
|
}
|
|
76
|
-
const db = new Kysely<Record<string, unknown>>({ dialect:
|
|
89
|
+
const db = new Kysely<Record<string, unknown>>({ dialect: sqliteDialect });
|
|
77
90
|
let closed = false;
|
|
78
91
|
return {
|
|
79
92
|
db,
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from "@metaobjectsdev/codegen-ts";
|
|
11
11
|
import {
|
|
12
12
|
computeViewMigrations,
|
|
13
|
+
viewSqlEquals,
|
|
13
14
|
type ViewMigrationInput,
|
|
14
15
|
type ViewMigrationsResult,
|
|
15
16
|
} from "@metaobjectsdev/migrate-ts";
|
|
@@ -33,11 +34,6 @@ export interface ProjectionMigrationsOpts {
|
|
|
33
34
|
readonly existingViewSql?: ReadonlyMap<string, string>;
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
/** Collapse whitespace + strip trailing ";" for textual view-SQL comparison. */
|
|
37
|
-
function normalizeViewSql(sql: string): string {
|
|
38
|
-
return sql.replace(/\s+/g, " ").replace(/;\s*$/, "").trim();
|
|
39
|
-
}
|
|
40
|
-
|
|
41
37
|
/**
|
|
42
38
|
* Walk all projection entities in metadata, extract their ViewSpec, emit CREATE
|
|
43
39
|
* VIEW DDL, and compute view migration SQL via computeViewMigrations.
|
|
@@ -96,7 +92,7 @@ export function computeProjectionMigrations(
|
|
|
96
92
|
// Avoids the "every migration re-creates every view" noise when nothing
|
|
97
93
|
// about the view's body actually changed.
|
|
98
94
|
const existing = opts.existingViewSql?.get(spec.viewName);
|
|
99
|
-
if (existing !== undefined &&
|
|
95
|
+
if (existing !== undefined && viewSqlEquals(existing, createSql)) {
|
|
100
96
|
continue;
|
|
101
97
|
}
|
|
102
98
|
|