@stacksjs/database 0.70.88 → 0.70.91

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.
Files changed (83) hide show
  1. package/dist/auth-tables.d.ts +60 -0
  2. package/dist/auth-tables.js +220 -0
  3. package/dist/class-seeder.d.ts +65 -0
  4. package/dist/class-seeder.js +116 -0
  5. package/dist/column.d.ts +17 -0
  6. package/dist/column.js +26 -0
  7. package/dist/custom/audits.d.ts +16 -0
  8. package/dist/custom/audits.js +57 -0
  9. package/dist/custom/errors.d.ts +1 -0
  10. package/dist/custom/errors.js +48 -0
  11. package/dist/custom/index.d.ts +3 -0
  12. package/dist/custom/index.js +3 -0
  13. package/dist/custom/jobs.d.ts +3 -0
  14. package/dist/custom/jobs.js +449 -0
  15. package/dist/database.d.ts +89 -0
  16. package/dist/database.js +178 -0
  17. package/dist/defaults.d.ts +48 -0
  18. package/dist/defaults.js +48 -0
  19. package/dist/driver-config.d.ts +149 -0
  20. package/dist/driver-config.js +144 -0
  21. package/dist/drivers/defaults/index.d.ts +2 -0
  22. package/dist/drivers/defaults/index.js +2 -0
  23. package/dist/drivers/defaults/passwords.d.ts +4 -0
  24. package/dist/drivers/defaults/passwords.js +106 -0
  25. package/dist/drivers/defaults/traits.d.ts +33 -0
  26. package/dist/drivers/defaults/traits.js +1125 -0
  27. package/dist/drivers/dynamodb.d.ts +200 -0
  28. package/dist/drivers/dynamodb.js +607 -0
  29. package/dist/drivers/helpers.d.ts +35 -0
  30. package/dist/drivers/helpers.js +206 -0
  31. package/dist/drivers/index.d.ts +16 -0
  32. package/dist/drivers/index.js +9 -0
  33. package/dist/drivers/mysql.d.ts +7 -0
  34. package/dist/drivers/mysql.js +322 -0
  35. package/dist/drivers/postgres.d.ts +7 -0
  36. package/dist/drivers/postgres.js +411 -0
  37. package/dist/drivers/sqlite.d.ts +20 -0
  38. package/dist/drivers/sqlite.js +397 -0
  39. package/dist/factory.d.ts +41 -0
  40. package/dist/factory.js +51 -0
  41. package/dist/fk-audit.d.ts +101 -0
  42. package/dist/fk-audit.js +181 -0
  43. package/dist/index.d.ts +149 -0
  44. package/dist/index.js +55 -0
  45. package/dist/migration-lock.d.ts +23 -0
  46. package/dist/migration-lock.js +143 -0
  47. package/dist/migrations.d.ts +76 -0
  48. package/dist/migrations.js +549 -0
  49. package/dist/notification-tables.d.ts +20 -0
  50. package/dist/notification-tables.js +54 -0
  51. package/dist/query-logger.d.ts +26 -0
  52. package/dist/query-logger.js +213 -0
  53. package/dist/query-parser.d.ts +4 -0
  54. package/dist/query-parser.js +93 -0
  55. package/dist/rbac-tables.d.ts +17 -0
  56. package/dist/rbac-tables.js +84 -0
  57. package/dist/safe-migrations.d.ts +72 -0
  58. package/dist/safe-migrations.js +59 -0
  59. package/dist/schema.d.ts +4 -0
  60. package/dist/schema.js +10 -0
  61. package/dist/seed-scaffold.d.ts +34 -0
  62. package/dist/seed-scaffold.js +144 -0
  63. package/dist/seeder.d.ts +116 -0
  64. package/dist/seeder.js +363 -0
  65. package/dist/sql-helpers.d.ts +33 -0
  66. package/dist/sql-helpers.js +24 -0
  67. package/dist/table.d.ts +7 -0
  68. package/dist/table.js +26 -0
  69. package/dist/tools/setup.d.ts +1 -0
  70. package/dist/tools/setup.js +6 -0
  71. package/dist/transaction-context.d.ts +52 -0
  72. package/dist/transaction-context.js +62 -0
  73. package/dist/types.d.ts +151 -0
  74. package/dist/types.js +23 -0
  75. package/dist/unique-audit.d.ts +60 -0
  76. package/dist/unique-audit.js +174 -0
  77. package/dist/utils.d.ts +189 -0
  78. package/dist/utils.js +163 -0
  79. package/dist/uuid-columns.d.ts +22 -0
  80. package/dist/uuid-columns.js +68 -0
  81. package/dist/validators.d.ts +26 -0
  82. package/dist/validators.js +122 -0
  83. package/package.json +11 -11
@@ -0,0 +1,181 @@
1
+ import { existsSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ import { plural, singular, snakeCase } from "@stacksjs/strings";
4
+ import { path } from "@stacksjs/path";
5
+ import { globSync } from "@stacksjs/storage";
6
+ export function safeGlob(pattern) {
7
+ const metaIdx = pattern.search(/[*?[]/), root = metaIdx === -1 ? dirname(pattern) : dirname(pattern.slice(0, metaIdx));
8
+ if (!existsSync(root))
9
+ return [];
10
+ try {
11
+ return globSync(pattern, { absolute: !0 });
12
+ } catch {
13
+ return [];
14
+ }
15
+ }
16
+ export async function getDeclaredFKs() {
17
+ const modelFiles = [
18
+ ...safeGlob(path.userModelsPath("*.ts")),
19
+ ...safeGlob(path.storagePath("framework/defaults/app/Models/**/*.ts"))
20
+ ], declared = [];
21
+ for (const modelFile of modelFiles) {
22
+ let model;
23
+ try {
24
+ model = (await import(modelFile)).default;
25
+ } catch {
26
+ continue;
27
+ }
28
+ if (!model || typeof model !== "object")
29
+ continue;
30
+ const fromTable = model.table || plural(snakeCase(model.name || "")), belongsTo = model.belongsTo;
31
+ if (Array.isArray(belongsTo))
32
+ for (const entry of belongsTo) {
33
+ const related = typeof entry === "string" ? entry : entry?.model ?? "";
34
+ if (!related)
35
+ continue;
36
+ const fromColumn = `${snakeCase(singular(related))}_id`, toTable = plural(snakeCase(related));
37
+ declared.push({
38
+ fromTable,
39
+ fromColumn,
40
+ toTable,
41
+ toColumn: "id",
42
+ model: model.name || ""
43
+ });
44
+ }
45
+ else if (belongsTo && typeof belongsTo === "object")
46
+ for (const related of Object.keys(belongsTo)) {
47
+ const fromColumn = `${snakeCase(singular(related))}_id`, toTable = plural(snakeCase(related));
48
+ declared.push({
49
+ fromTable,
50
+ fromColumn,
51
+ toTable,
52
+ toColumn: "id",
53
+ model: model.name || ""
54
+ });
55
+ }
56
+ }
57
+ return declared;
58
+ }
59
+ export async function getLiveFKs() {
60
+ const { db } = await import("./utils"), dialect = await currentDialect();
61
+ if (dialect === "sqlite")
62
+ return getSqliteLiveFKs(db);
63
+ if (dialect === "mysql")
64
+ return getMysqlLiveFKs(db);
65
+ if (dialect === "postgres")
66
+ return getPostgresLiveFKs(db);
67
+ return [];
68
+ }
69
+ export async function auditForeignKeys() {
70
+ const declared = await getDeclaredFKs(), live = await getLiveFKs(), liveKeys = new Set(live.map((fk) => `${fk.fromTable.toLowerCase()}.${fk.fromColumn.toLowerCase()}\u2192${fk.toTable.toLowerCase()}.${fk.toColumn.toLowerCase()}`)), missing = declared.filter((d) => {
71
+ const key = `${d.fromTable.toLowerCase()}.${d.fromColumn.toLowerCase()}\u2192${d.toTable.toLowerCase()}.${d.toColumn.toLowerCase()}`;
72
+ return !liveKeys.has(key);
73
+ });
74
+ return { declared, live, missing };
75
+ }
76
+ export async function findFkOrphans(dialect) {
77
+ if ((dialect ?? await currentDialect()) !== "sqlite")
78
+ return { supported: !1, total: 0, orphans: [] };
79
+ const { db } = await import("./utils"), rows = await db.unsafe("PRAGMA foreign_key_check").execute(), checkRows = Array.isArray(rows) ? rows : [], fkListCache = new Map;
80
+ async function fkListFor(table) {
81
+ if (fkListCache.has(table))
82
+ return fkListCache.get(table);
83
+ if (!/^[a-z_]\w*$/i.test(table)) {
84
+ fkListCache.set(table, []);
85
+ return [];
86
+ }
87
+ const list = await db.unsafe(`PRAGMA foreign_key_list("${table}")`).execute(), arr = Array.isArray(list) ? list : [];
88
+ fkListCache.set(table, arr);
89
+ return arr;
90
+ }
91
+ const grouped = new Map;
92
+ for (const raw of checkRows) {
93
+ const r = raw, table = String(r.table ?? ""), parent = String(r.parent ?? "");
94
+ if (!table || !parent)
95
+ continue;
96
+ const fkid = Number(r.fkid ?? 0);
97
+ let column = "";
98
+ const fkList = await fkListFor(table);
99
+ for (const fk of fkList) {
100
+ const f = fk;
101
+ if (Number(f.id) === fkid && f.from) {
102
+ column = String(f.from);
103
+ break;
104
+ }
105
+ }
106
+ const key = `${table}\x00${parent}\x00${fkid}`;
107
+ let entry = grouped.get(key);
108
+ if (!entry) {
109
+ entry = { table, column, parent, count: 0, sampleRowids: [] };
110
+ grouped.set(key, entry);
111
+ }
112
+ entry.count++;
113
+ if (entry.sampleRowids.length < 5 && typeof r.rowid === "number")
114
+ entry.sampleRowids.push(r.rowid);
115
+ }
116
+ const orphans = [...grouped.values()];
117
+ return { supported: !0, total: orphans.reduce((sum, o) => sum + o.count, 0), orphans };
118
+ }
119
+ async function currentDialect() {
120
+ const driver = ((await import("@stacksjs/env")).env?.DB_CONNECTION ?? "sqlite").toLowerCase();
121
+ if (driver === "sqlite" || driver === "mysql" || driver === "postgres")
122
+ return driver;
123
+ return "other";
124
+ }
125
+ async function getSqliteLiveFKs(db) {
126
+ const tables = await db.unsafe("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"), rows = Array.isArray(tables) ? tables : [], fks = [];
127
+ for (const row of rows) {
128
+ const fromTable = row.name;
129
+ if (!fromTable)
130
+ continue;
131
+ if (!/^[a-z_][\w]*$/i.test(fromTable))
132
+ continue;
133
+ const fkRows = await db.unsafe(`PRAGMA foreign_key_list("${fromTable}")`);
134
+ for (const fk of Array.isArray(fkRows) ? fkRows : []) {
135
+ const r = fk;
136
+ if ((r.seq ?? 0) !== 0)
137
+ continue;
138
+ if (!r.from || !r.to || !r.table)
139
+ continue;
140
+ fks.push({ fromTable, fromColumn: r.from, toTable: r.table, toColumn: r.to });
141
+ }
142
+ }
143
+ return fks;
144
+ }
145
+ async function getMysqlLiveFKs(db) {
146
+ const rows = await db.unsafe(`
147
+ SELECT TABLE_NAME, COLUMN_NAME, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME
148
+ FROM information_schema.KEY_COLUMN_USAGE
149
+ WHERE TABLE_SCHEMA = DATABASE()
150
+ AND REFERENCED_TABLE_NAME IS NOT NULL
151
+ `);
152
+ return (Array.isArray(rows) ? rows : []).map((row) => ({
153
+ fromTable: String(row.TABLE_NAME ?? row.table_name ?? ""),
154
+ fromColumn: String(row.COLUMN_NAME ?? row.column_name ?? ""),
155
+ toTable: String(row.REFERENCED_TABLE_NAME ?? row.referenced_table_name ?? ""),
156
+ toColumn: String(row.REFERENCED_COLUMN_NAME ?? row.referenced_column_name ?? "")
157
+ })).filter((fk) => fk.fromTable && fk.fromColumn && fk.toTable && fk.toColumn);
158
+ }
159
+ async function getPostgresLiveFKs(db) {
160
+ const rows = await db.unsafe(`
161
+ SELECT
162
+ kcu.table_name AS from_table,
163
+ kcu.column_name AS from_column,
164
+ ccu.table_name AS to_table,
165
+ ccu.column_name AS to_column
166
+ FROM information_schema.referential_constraints rc
167
+ JOIN information_schema.key_column_usage kcu
168
+ ON kcu.constraint_name = rc.constraint_name
169
+ AND kcu.constraint_schema = rc.constraint_schema
170
+ JOIN information_schema.constraint_column_usage ccu
171
+ ON ccu.constraint_name = rc.constraint_name
172
+ AND ccu.constraint_schema = rc.constraint_schema
173
+ WHERE rc.constraint_schema = 'public'
174
+ `);
175
+ return (Array.isArray(rows) ? rows : []).map((row) => ({
176
+ fromTable: String(row.from_table ?? ""),
177
+ fromColumn: String(row.from_column ?? ""),
178
+ toTable: String(row.to_table ?? ""),
179
+ toColumn: String(row.to_column ?? "")
180
+ })).filter((fk) => fk.fromTable && fk.fromColumn && fk.toTable && fk.toColumn);
181
+ }
@@ -0,0 +1,149 @@
1
+ export type {
2
+ DatabaseConnectionConfig,
3
+ DatabaseOptions,
4
+ } from './database';
5
+ export type {
6
+ DatabaseConnections,
7
+ DynamoDbConfig,
8
+ FullDatabaseConfig,
9
+ MysqlConfig,
10
+ PostgresConfig,
11
+ SqliteConfig,
12
+ } from './driver-config';
13
+ export type { GenerateOptions } from './factory';
14
+ export type { ScaffoldOptions, ScaffoldResult } from './seed-scaffold';
15
+ export type { DeclaredFK, FkAuditResult, FkOrphan, FkOrphanReport, LiveFK } from './fk-audit';
16
+ export type { DeclaredUnique, LiveUniqueIndex, UniqueAuditResult } from './unique-audit';
17
+ export type {
18
+ QueryBuilder,
19
+ QueryBuilderConfig,
20
+ Seeder as QueryBuilderSeeder,
21
+ SupportedDialect,
22
+ } from '@stacksjs/query-builder';
23
+ export type {
24
+ DynamoConnectionConfig,
25
+ SingleTableEntityMapping,
26
+ SortKeyBuilder,
27
+ BatchWriteOperation,
28
+ TransactWriteOperation,
29
+ QueryResult,
30
+ } from './drivers/dynamodb';
31
+ /**
32
+ * @stacksjs/database
33
+ *
34
+ * Database module powered by bun-query-builder.
35
+ * Provides database initialization, driver configuration, migrations,
36
+ * seeding, and a fluent query builder interface.
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * import { Database, db, createSqliteDatabase } from '@stacksjs/database'
41
+ *
42
+ * // Use the default db instance (configured from environment)
43
+ * const users = await db.selectFrom('users').where('active', '=', true).get()
44
+ *
45
+ * // Or create a custom database instance
46
+ * const customDb = new Database({
47
+ * driver: 'postgres',
48
+ * connection: {
49
+ * database: 'myapp',
50
+ * host: 'localhost',
51
+ * port: 5432,
52
+ * username: 'postgres',
53
+ * password: 'secret'
54
+ * }
55
+ * })
56
+ *
57
+ * // Helper functions for quick setup
58
+ * const sqliteDb = createSqliteDatabase('database/app.sqlite')
59
+ * ```
60
+ */
61
+ // Database initialization and management
62
+ export {
63
+ Database,
64
+ createDatabase,
65
+ createMysqlDatabase,
66
+ createPostgresDatabase,
67
+ createSqliteDatabase,
68
+ } from './database';
69
+ // Driver configuration
70
+ export {
71
+ detectDriver,
72
+ driverDefaults,
73
+ getConfigFromEnv,
74
+ getConnectionString,
75
+ mergeWithDefaults,
76
+ validateDriverConfig,
77
+ } from './driver-config';
78
+ // Core database utilities and default instance
79
+ export * from './utils';
80
+ // Types (compatibility layer for Kysely types)
81
+ export * from './types';
82
+ // Migrations
83
+ export * from './migrations';
84
+ // Query logger DI hook (router calls setQueryTracker on init)
85
+ export { setQueryTracker, logQuery } from './query-logger';
86
+ // Class-based seeders (supplements the model-attribute auto-seeder)
87
+ export { Seeder, runClassSeeders } from './class-seeder';
88
+ // Zero-downtime migration helpers
89
+ export { addColumnSafely, backfillInBatches, renameColumnSafely } from './safe-migrations';
90
+ // Seeding
91
+ export * from './seeder';
92
+ // stacksjs/stacks#1919 — public factory API. The canonical replacement
93
+ // for the legacy `useSeeder` trait + auto-walker. Class seeders call
94
+ // `factory.generate(Model, opts)` explicitly so there's one
95
+ // orchestration layer per table, no double-fire on tables that have
96
+ // both a `useSeeder` trait and a class seeder file.
97
+ export { factory, generate as factoryGenerate } from './factory';
98
+ // `buddy seed:scaffold` codemod — generates class-seeder files for
99
+ // every model with a `useSeeder` trait, easing the migration off the
100
+ // auto-walker.
101
+ export { scaffoldClassSeedersFromModels, renderSeederFile } from './seed-scaffold';
102
+ // Driver utilities
103
+ export * from './drivers/index';
104
+ // Custom migrations (jobs, errors, etc.)
105
+ export * from './custom/index';
106
+ // Auth tables migration
107
+ export * from './auth-tables';
108
+ // uuid column guarantee for `useUuid` models (stacksjs/status#1 Phase 9)
109
+ export * from './uuid-columns';
110
+ // Notification tables migration (stacksjs/stacks#1937)
111
+ export { migrateNotificationTables } from './notification-tables';
112
+ // RBAC tables migration (stacksjs/stacks#1941 Phase A)
113
+ export { migrateRbacTables } from './rbac-tables';
114
+ // SQL dialect helpers & connection defaults
115
+ export * from './sql-helpers';
116
+ export * from './defaults';
117
+ // Foreign-key audit (stacksjs/stacks#1916) — compare declared
118
+ // `belongsTo` relationships against live FKs.
119
+ export { auditForeignKeys, findFkOrphans, getDeclaredFKs, getLiveFKs } from './fk-audit';
120
+ // Unique-index drift audit (stacksjs/stacks#1952) — compare declared
121
+ // `unique: true` attributes / indexes against live UNIQUE indexes.
122
+ export { auditUniqueIndexes, getDeclaredUniques, getLiveUniqueIndexes } from './unique-audit';
123
+ // Transaction context: AsyncLocalStorage-based scope so side-effect
124
+ // emitters (queue dispatch, mailer send) can buffer themselves
125
+ // until the surrounding `db.transaction(...)` commits
126
+ // (stacksjs/stacks#1882).
127
+ export {
128
+ __flushAfterCommitNow,
129
+ __pendingAfterCommitCount,
130
+ enqueueAfterCommit,
131
+ isInTransaction,
132
+ runInTransactionScope,
133
+ } from './transaction-context';
134
+ // Re-export bun-query-builder functions and types
135
+ export {
136
+ createQueryBuilder,
137
+ setConfig,
138
+ } from '@stacksjs/query-builder';
139
+ // DynamoDB entity-centric API
140
+ export {
141
+ createDynamo,
142
+ dynamo,
143
+ EntityQueryBuilder,
144
+ generateKeyPattern,
145
+ parseKeyPattern,
146
+ buildKey,
147
+ marshall,
148
+ unmarshall,
149
+ } from './drivers/dynamodb';
package/dist/index.js ADDED
@@ -0,0 +1,55 @@
1
+ export {
2
+ Database,
3
+ createDatabase,
4
+ createMysqlDatabase,
5
+ createPostgresDatabase,
6
+ createSqliteDatabase
7
+ } from "./database";
8
+ export {
9
+ detectDriver,
10
+ driverDefaults,
11
+ getConfigFromEnv,
12
+ getConnectionString,
13
+ mergeWithDefaults,
14
+ validateDriverConfig
15
+ } from "./driver-config";
16
+ export * from "./utils";
17
+ export * from "./types";
18
+ export * from "./migrations";
19
+ export { setQueryTracker, logQuery } from "./query-logger";
20
+ export { Seeder, runClassSeeders } from "./class-seeder";
21
+ export { addColumnSafely, backfillInBatches, renameColumnSafely } from "./safe-migrations";
22
+ export * from "./seeder";
23
+ export { factory, generate as factoryGenerate } from "./factory";
24
+ export { scaffoldClassSeedersFromModels, renderSeederFile } from "./seed-scaffold";
25
+ export * from "./drivers";
26
+ export * from "./custom";
27
+ export * from "./auth-tables";
28
+ export * from "./uuid-columns";
29
+ export { migrateNotificationTables } from "./notification-tables";
30
+ export { migrateRbacTables } from "./rbac-tables";
31
+ export * from "./sql-helpers";
32
+ export * from "./defaults";
33
+ export { auditForeignKeys, findFkOrphans, getDeclaredFKs, getLiveFKs } from "./fk-audit";
34
+ export { auditUniqueIndexes, getDeclaredUniques, getLiveUniqueIndexes } from "./unique-audit";
35
+ export {
36
+ __flushAfterCommitNow,
37
+ __pendingAfterCommitCount,
38
+ enqueueAfterCommit,
39
+ isInTransaction,
40
+ runInTransactionScope
41
+ } from "./transaction-context";
42
+ export {
43
+ createQueryBuilder,
44
+ setConfig
45
+ } from "@stacksjs/query-builder";
46
+ export {
47
+ createDynamo,
48
+ dynamo,
49
+ EntityQueryBuilder,
50
+ generateKeyPattern,
51
+ parseKeyPattern,
52
+ buildKey,
53
+ marshall,
54
+ unmarshall
55
+ } from "./drivers/dynamodb";
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Acquire the distributed migration lock for the given dialect.
3
+ * Returns a handle whose `release()` method MUST be called in a
4
+ * `finally` to free the lock — even on error paths.
5
+ *
6
+ * @param dialect - which database driver is being migrated against
7
+ * @param adminDb - the bun-query-builder connection to issue lock SQL
8
+ * through (PG / MySQL). Ignored for SQLite.
9
+ * @param opts.timeoutMs - max time to wait for an existing holder
10
+ * @param opts.sqliteLockPath - override the file path SQLite uses
11
+ *
12
+ * Throws an error if the lock can't be acquired within `timeoutMs`.
13
+ */
14
+ export declare function acquireMigrationLock(dialect: Dialect, adminDb: { unsafe: (sql: string) => Promise<unknown> } | null, opts?: { timeoutMs?: number, sqliteLockPath?: string }): Promise<MigrationLockHandle>;
15
+ /**
16
+ * Returned by `acquireMigrationLock()`. Callers MUST invoke `release`
17
+ * in a finally block; the lock is process-external (file, advisory,
18
+ * or named) so leaking it strands future migration runs.
19
+ */
20
+ export declare interface MigrationLockHandle {
21
+ release: () => Promise<void>
22
+ }
23
+ export type Dialect = 'sqlite' | 'mysql' | 'postgres';
@@ -0,0 +1,143 @@
1
+ import { Buffer } from "node:buffer";
2
+ import { createHash } from "node:crypto";
3
+ import { closeSync, openSync, readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs";
4
+ import process from "node:process";
5
+ import { userDatabasePath } from "@stacksjs/path";
6
+ const DEFAULT_TIMEOUT_MS = 30000, INITIAL_BACKOFF_MS = 100, MAX_BACKOFF_MS = 2000, STALE_LOCK_MS = 60000, LOCK_NAME = "stacks_migrations";
7
+ export async function acquireMigrationLock(dialect, adminDb, opts = {}) {
8
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
9
+ if (dialect !== "sqlite" && dialect !== "postgres" && dialect !== "mysql")
10
+ throw Error(`[migration-lock] unknown dialect: ${String(dialect)}`);
11
+ if (dialect === "sqlite")
12
+ return acquireSqliteLock(opts.sqliteLockPath, timeoutMs);
13
+ if (!adminDb)
14
+ throw Error(`[migration-lock] ${dialect} requires a database connection to acquire the lock`);
15
+ if (dialect === "postgres")
16
+ return acquirePostgresLock(adminDb, timeoutMs);
17
+ return acquireMySqlLock(adminDb, timeoutMs);
18
+ }
19
+ function lockKeysForPostgres() {
20
+ const hash = createHash("sha256").update(LOCK_NAME).digest(), key1 = hash.readInt32BE(0), key2 = hash.readInt32BE(4);
21
+ return { key1, key2 };
22
+ }
23
+ async function acquirePostgresLock(adminDb, timeoutMs) {
24
+ const { key1, key2 } = lockKeysForPostgres(), start = Date.now();
25
+ let backoff = INITIAL_BACKOFF_MS;
26
+ while (!0) {
27
+ const result = await adminDb.unsafe(`SELECT pg_try_advisory_lock(${key1}, ${key2}) AS acquired`);
28
+ if (extractFirstBool(result, "acquired"))
29
+ return {
30
+ release: async () => {
31
+ try {
32
+ await adminDb.unsafe(`SELECT pg_advisory_unlock(${key1}, ${key2})`);
33
+ } catch {}
34
+ }
35
+ };
36
+ if (Date.now() - start >= timeoutMs)
37
+ throw Error("[migration-lock] another migration is in progress \u2014 could not acquire postgres advisory lock within timeout");
38
+ await sleepWithJitter(backoff);
39
+ backoff = Math.min(backoff * 2, MAX_BACKOFF_MS);
40
+ }
41
+ }
42
+ async function acquireMySqlLock(adminDb, timeoutMs) {
43
+ const start = Date.now();
44
+ let backoff = INITIAL_BACKOFF_MS;
45
+ while (!0) {
46
+ const result = await adminDb.unsafe(`SELECT GET_LOCK('${LOCK_NAME}', 0) AS acquired`);
47
+ if (extractFirstInt(result, "acquired") === 1)
48
+ return {
49
+ release: async () => {
50
+ try {
51
+ await adminDb.unsafe(`SELECT RELEASE_LOCK('${LOCK_NAME}')`);
52
+ } catch {}
53
+ }
54
+ };
55
+ if (Date.now() - start >= timeoutMs)
56
+ throw Error("[migration-lock] another migration is in progress \u2014 could not acquire MySQL named lock within timeout");
57
+ await sleepWithJitter(backoff);
58
+ backoff = Math.min(backoff * 2, MAX_BACKOFF_MS);
59
+ }
60
+ }
61
+ function defaultSqliteLockPath() {
62
+ return userDatabasePath(".migration.lock");
63
+ }
64
+ async function acquireSqliteLock(lockPath, timeoutMs) {
65
+ const path = lockPath ?? defaultSqliteLockPath(), start = Date.now();
66
+ let backoff = INITIAL_BACKOFF_MS;
67
+ while (!0) {
68
+ if (tryCreateLockFile(path)) {
69
+ let released = !1;
70
+ return {
71
+ release: async () => {
72
+ if (released)
73
+ return;
74
+ released = !0;
75
+ try {
76
+ unlinkSync(path);
77
+ } catch {}
78
+ }
79
+ };
80
+ }
81
+ reclaimIfStale(path);
82
+ if (Date.now() - start >= timeoutMs)
83
+ throw Error(`[migration-lock] another migration is in progress \u2014 lock file ${path} held within timeout`);
84
+ await sleepWithJitter(backoff);
85
+ backoff = Math.min(backoff * 2, MAX_BACKOFF_MS);
86
+ }
87
+ }
88
+ function tryCreateLockFile(path) {
89
+ try {
90
+ const fd = openSync(path, "wx");
91
+ try {
92
+ const payload = JSON.stringify({ pid: process.pid, startedAt: new Date().toISOString() });
93
+ writeFileSync(fd, Buffer.from(payload, "utf8"));
94
+ } finally {
95
+ closeSync(fd);
96
+ }
97
+ return !0;
98
+ } catch (e) {
99
+ if (e.code === "EEXIST")
100
+ return !1;
101
+ throw e;
102
+ }
103
+ }
104
+ function reclaimIfStale(path) {
105
+ try {
106
+ const st = statSync(path);
107
+ if (Date.now() - st.mtimeMs > STALE_LOCK_MS)
108
+ try {
109
+ unlinkSync(path);
110
+ } catch {}
111
+ } catch {}
112
+ }
113
+ function sleepWithJitter(ms) {
114
+ const jittered = ms * (1 + Math.random() * 0.25);
115
+ return new Promise((resolve) => setTimeout(resolve, jittered));
116
+ }
117
+ function extractFirstBool(result, column) {
118
+ const row = pluckFirstRow(result);
119
+ if (!row)
120
+ return !1;
121
+ const value = row[column];
122
+ return value === !0 || value === 1 || value === "1" || value === "t";
123
+ }
124
+ function extractFirstInt(result, column) {
125
+ const row = pluckFirstRow(result);
126
+ if (!row)
127
+ return null;
128
+ const value = row[column];
129
+ if (typeof value === "number")
130
+ return value;
131
+ if (typeof value === "string" && /^-?\d+$/.test(value))
132
+ return Number.parseInt(value, 10);
133
+ return null;
134
+ }
135
+ function pluckFirstRow(result) {
136
+ if (!result)
137
+ return null;
138
+ if (Array.isArray(result))
139
+ return result[0];
140
+ if (typeof result === "object" && "rows" in result && Array.isArray(result.rows))
141
+ return result.rows[0];
142
+ return null;
143
+ }
@@ -0,0 +1,76 @@
1
+ import type { MigrationOperation } from '@stacksjs/query-builder';
2
+ import type { Result } from '@stacksjs/error-handling';
3
+ export type { MigrationResult as MigrationResultType };
4
+ /**
5
+ * SQLite compatibility preprocessing for migrations.
6
+ *
7
+ * SQLite does not support:
8
+ * - ALTER TABLE ADD CONSTRAINT (foreign keys must be defined at table creation)
9
+ * - CREATE TYPE ... AS ENUM (SQLite has no user-defined types; enum columns
10
+ * are plain TEXT, with the allowed values enforced at the validation layer)
11
+ *
12
+ * Note: CREATE UNIQUE INDEX files are deliberately NOT skipped — the SQLite
13
+ * dialect driver never renders inline UNIQUE in CREATE TABLE, so the
14
+ * standalone index file is the only uniqueness enforcement on SQLite
15
+ * (stacksjs/stacks#1952).
16
+ *
17
+ * Two flavours of "no-op on SQLite" need different handling:
18
+ *
19
+ * - **Skip-and-keep** (`skipMigration`): the file is portable — it would
20
+ * run cleanly on MySQL/Postgres — but doesn't apply to SQLite. Record
21
+ * it as executed in the migrations tracking table so it doesn't replay,
22
+ * but **leave the file on disk** so a future `DB_CONNECTION` flip can
23
+ * pick it up. This is the right path for FK constraint files.
24
+ * (stacksjs/stacks#1916)
25
+ *
26
+ * - **Drop-and-delete** (`deleteMigration`): the file is genuinely dead
27
+ * — a duplicate CREATE TABLE created by `buddy generate:migrations`
28
+ * regenerating against an already-modeled table, or a DROP COLUMN
29
+ * migration whose target column never existed. Removing it keeps the
30
+ * directory clean and prevents future runs from re-discovering it.
31
+ */
32
+ export declare function preprocessSqliteMigrations(): void;
33
+ /**
34
+ * Run database migrations
35
+ */
36
+ export declare function runDatabaseMigration(): Promise<Result<string, Error>>;
37
+ /**
38
+ * Reset the database (drop all tables)
39
+ */
40
+ export declare function resetDatabase(): Promise<Result<string, Error>>;
41
+ /**
42
+ * Preview the pending migration as a list of structured operations WITHOUT
43
+ * writing any files or advancing the snapshot. The `buddy migrate` command
44
+ * uses this (in the interactive parent process) to gate destructive changes
45
+ * behind confirmation before spawning the non-interactive migrate action.
46
+ */
47
+ export declare function previewPendingMigrations(options?: GenerateMigrationsOptions): Promise<MigrationOperation[]>;
48
+ export declare function generateMigrations(options?: GenerateMigrationsOptions): Promise<Result<string, Error>>;
49
+ /**
50
+ * Generate fresh migrations (full regeneration, ignoring previous state)
51
+ */
52
+ export declare function generateMigrations2(): Promise<Result<string, Error>>;
53
+ /*` definitions to the stored snapshot
54
+ * (`.qb/model-snapshot.<dialect>.json`) via bun-query-builder, then — if
55
+ * there are changes — writes the resulting ALTER/CREATE/DROP statements
56
+ * out to a fresh file in `database/migrations/`. Each statement is
57
+ * grouped by table + DDL verb and lands in its own file using the
58
+ * runner's existing naming convention so it picks them up the same way
59
+ * as a hand-written migration.
60
+ *
61
+ * Without this write step the qb generator stages the diff in memory but
62
+ * the runner never sees it, so model edits silently no-op'd — defeating
63
+ * the "models are the source of truth" promise.
64
+ */
65
+ export declare interface GenerateMigrationsOptions {
66
+ applyRenames?: boolean
67
+ fromDb?: boolean
68
+ }
69
+ /**
70
+ * Migration result type for compatibility
71
+ */
72
+ export declare interface MigrationResult {
73
+ migrationName: string
74
+ direction: 'Up' | 'Down'
75
+ status: 'Success' | 'Error' | 'NotExecuted'
76
+ }