@stacksjs/database 0.75.167 → 0.75.169

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -152,6 +152,8 @@ export * from './migration-path';
152
152
  export * from './migration-ledger';
153
153
  export * from './schema-align';
154
154
  export * from './hand-written-migrations';
155
+ // Generated names never reuse one a ledger or the corpus's history holds.
156
+ export * from './migration-names';
155
157
  export { comparableIndexKey, postgresIndexStatement } from './index-shape';
156
158
  // Model resolution for the generator: userland + framework defaults, flattened
157
159
  // because bun-query-builder's loadModels reads only the top level of a dir.
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- export{Database,createDatabase,createMysqlDatabase,createPostgresDatabase,createSqliteDatabase,createTursoDatabase}from"./database";export{detectDriver,driverDefaults,getConfigFromEnv,getConnectionString,mergeWithDefaults,validateDriverConfig}from"./driver-config";export*from"./utils";export*from"./row-lock";export{BUN_BROKEN_MYSQL_POOL_MESSAGE,BUN_BROKEN_POSTGRES_POOL_MESSAGE,getBrokenDatabasePools,isBrokenBunPoolError}from"./broken-pool";export*from"./types";export*from"./migrations";export*from"./framework-renames";export{setQueryTracker,logQuery}from"./query-logger";export{addColumnSafely,backfillInBatches,renameColumnSafely}from"./safe-migrations";export{matchedRows,mutationCount}from"./affected-rows";export*from"./seeder";export*from"./drivers";export*from"./custom";export*from"./auth-tables";export*from"./utc-defaults";export*from"./uuid-columns";export*from"./managed-columns";export*from"./relation-columns";export{ensureNotificationForeignKeys,migrateNotificationTables}from"./notification-tables";export{migrateRbacTables}from"./rbac-tables";export*from"./trait-tables";export*from"./datetime-columns";export*from"./dialect";export*from"./replicas";export*from"./ddl-constraints";export*from"./erd";export*from"./vschema";export*from"./sql-helpers";export*from"./defaults";export*from"./migration-dialect";export*from"./migration-path";export*from"./migration-ledger";export*from"./schema-align";export*from"./hand-written-migrations";export{comparableIndexKey,postgresIndexStatement}from"./index-shape";export*from"./model-sources";export*from"./package-migrations";export*from"./package-models";export*from"./shadowed-models";export*from"./ensure-database";export{auditForeignKeys,classifyDeclaredFKs,findFkOrphans,fkKey,getDeclaredFKs,getLiveFKs,getLiveTables}from"./fk-audit";export{auditSchemaDrift,formatSchemaDrift}from"./schema-drift";export{describeIgnoreRule,findIgnoredMigrations,formatIgnoredMigrations,trackIgnoredGeneratedMigrations}from"./ignored-migrations";export{auditUniqueIndexes,getDeclaredUniques,getLiveUniqueIndexes}from"./unique-audit";export{__flushAfterCommitNow,__pendingAfterCommitCount,enqueueAfterCommit,isInTransaction,runInTransactionScope}from"./transaction-context";export{createQueryBuilder,setConfig}from"@stacksjs/query-builder";export{createDynamo,dynamo,EntityQueryBuilder,generateKeyPattern,parseKeyPattern,buildKey,marshall,unmarshall}from"./drivers/dynamodb";
1
+ export{Database,createDatabase,createMysqlDatabase,createPostgresDatabase,createSqliteDatabase,createTursoDatabase}from"./database";export{detectDriver,driverDefaults,getConfigFromEnv,getConnectionString,mergeWithDefaults,validateDriverConfig}from"./driver-config";export*from"./utils";export*from"./row-lock";export{BUN_BROKEN_MYSQL_POOL_MESSAGE,BUN_BROKEN_POSTGRES_POOL_MESSAGE,getBrokenDatabasePools,isBrokenBunPoolError}from"./broken-pool";export*from"./types";export*from"./migrations";export*from"./framework-renames";export{setQueryTracker,logQuery}from"./query-logger";export{addColumnSafely,backfillInBatches,renameColumnSafely}from"./safe-migrations";export{matchedRows,mutationCount}from"./affected-rows";export*from"./seeder";export*from"./drivers";export*from"./custom";export*from"./auth-tables";export*from"./utc-defaults";export*from"./uuid-columns";export*from"./managed-columns";export*from"./relation-columns";export{ensureNotificationForeignKeys,migrateNotificationTables}from"./notification-tables";export{migrateRbacTables}from"./rbac-tables";export*from"./trait-tables";export*from"./datetime-columns";export*from"./dialect";export*from"./replicas";export*from"./ddl-constraints";export*from"./erd";export*from"./vschema";export*from"./sql-helpers";export*from"./defaults";export*from"./migration-dialect";export*from"./migration-path";export*from"./migration-ledger";export*from"./schema-align";export*from"./hand-written-migrations";export*from"./migration-names";export{comparableIndexKey,postgresIndexStatement}from"./index-shape";export*from"./model-sources";export*from"./package-migrations";export*from"./package-models";export*from"./shadowed-models";export*from"./ensure-database";export{auditForeignKeys,classifyDeclaredFKs,findFkOrphans,fkKey,getDeclaredFKs,getLiveFKs,getLiveTables}from"./fk-audit";export{auditSchemaDrift,formatSchemaDrift}from"./schema-drift";export{describeIgnoreRule,findIgnoredMigrations,formatIgnoredMigrations,trackIgnoredGeneratedMigrations}from"./ignored-migrations";export{auditUniqueIndexes,getDeclaredUniques,getLiveUniqueIndexes}from"./unique-audit";export{__flushAfterCommitNow,__pendingAfterCommitCount,enqueueAfterCommit,isInTransaction,runInTransactionScope}from"./transaction-context";export{createQueryBuilder,setConfig}from"@stacksjs/query-builder";export{createDynamo,dynamo,EntityQueryBuilder,generateKeyPattern,parseKeyPattern,buildKey,marshall,unmarshall}from"./drivers/dynamodb";
@@ -0,0 +1,56 @@
1
+ /** The leading ordinal of a migration filename, or undefined when it has none. */
2
+ export declare function migrationNameOrdinal(file: string): number | undefined;
3
+ /**
4
+ * Names the corpus directory has had in git history and no longer has.
5
+ *
6
+ * Renames are read as a delete plus an add (`--no-renames`), because a
7
+ * renumbered file's OLD name is the one an older ledger recorded. Outside a
8
+ * git checkout, without git, or in a shallow clone that has no history, this
9
+ * answers with whatever git can see, which may be nothing.
10
+ */
11
+ export declare function retiredMigrationNames(dir: string, options?: { timeoutMs?: number }): string[];
12
+ /**
13
+ * The ordinal the next generated migration should take.
14
+ *
15
+ * One past the highest application ordinal on disk and the highest sequential
16
+ * ordinal in `claimed` (ledger rows and retired names), then past any ordinal
17
+ * `claimed` uses. A timestamp ordinal on disk still counts - that corpus is
18
+ * numbered by time, and the next file has to sort after it - but one that only
19
+ * the ledger or the history holds does not raise the floor. Package migrations
20
+ * live in a reserved high band and are ignored on both sides, as they always
21
+ * have been.
22
+ */
23
+ export declare function nextMigrationOrdinal(diskFiles: readonly string[], claimed?: readonly string[]): number;
24
+ /** The ordinals the claimed names use, minus the package band. */
25
+ export declare function reservedMigrationOrdinals(claimed: readonly string[]): Set<number>;
26
+ /** The first ordinal at or after `from` that nothing has reserved. */
27
+ export declare function nextFreeMigrationOrdinal(from: number, reserved: ReadonlySet<number>): number;
28
+ /**
29
+ * Ordinals more than one migration file carries.
30
+ *
31
+ * Two files with one number still run in a fixed order (the runner sorts by
32
+ * full filename), but that order is the alphabet's, not the author's: two
33
+ * branches that each generated `0000000129-*` merge into a corpus where an
34
+ * `alter-` can run before the `create-` it depends on. It is also the
35
+ * fingerprint of the generator having numbered without seeing everything.
36
+ */
37
+ export declare function duplicateMigrationOrdinals(files: readonly string[]): DuplicateMigrationOrdinal[];
38
+ /**
39
+ * {@link duplicateMigrationOrdinals} for a corpus directory, with whether a
40
+ * generator wrote any of each group's files.
41
+ *
42
+ * A group with a generated file is the generator numbering over something it
43
+ * could not see - two branches that each generated the same ordinal - and is
44
+ * worth a warning on every `migrate`. A group of hand-written files is history
45
+ * (the framework's own default corpus has one) and only `--strict` refuses it.
46
+ */
47
+ export declare function duplicateMigrationOrdinalsIn(dir: string): Array<DuplicateMigrationOrdinal & { generated: boolean }>;
48
+ /**
49
+ * Ordinals at or above this are `make:migration` timestamps (seconds or
50
+ * milliseconds since 1970), not positions in a sequence.
51
+ */
52
+ export declare const TIMESTAMP_MIGRATION_ORDINAL: unknown;
53
+ export declare interface DuplicateMigrationOrdinal {
54
+ ordinal: string
55
+ files: string[]
56
+ }
@@ -0,0 +1,2 @@
1
+ import{spawnSync}from"node:child_process";import{readdirSync,readFileSync}from"node:fs";import{basename,join}from"node:path";import{carriesGeneratedMarker}from"./hand-written-migrations";import{isPackageMigration}from"./package-migrations";export function migrationNameOrdinal(file){const match=/^(\d+)[-_]/.exec(file);return match?Number.parseInt(match[1],10):void 0}export function retiredMigrationNames(dir,options={}){try{const result=spawnSync("git",["log","--no-renames","--diff-filter=D","--name-only","--pretty=format:","--","."],{cwd:dir,encoding:"utf8",timeout:options.timeoutMs??1e4,maxBuffer:67108864});if(result.status!==0||typeof result.stdout!=="string")return[];const names=new Set;for(const line of result.stdout.split(`
2
+ `)){const name=basename(line.trim());if(name.toLowerCase().endsWith(".sql"))names.add(name)}return[...names].sort()}catch{return[]}}export const TIMESTAMP_MIGRATION_ORDINAL=1e9;export function nextMigrationOrdinal(diskFiles,claimed=[]){let max=0;for(const file of diskFiles){if(isPackageMigration(file))continue;const ordinal=migrationNameOrdinal(file);if(ordinal!==void 0)max=Math.max(max,ordinal)}const reserved=reservedMigrationOrdinals(claimed);for(const ordinal of reserved)if(ordinal<TIMESTAMP_MIGRATION_ORDINAL)max=Math.max(max,ordinal);return nextFreeMigrationOrdinal(max+1,reserved)}export function reservedMigrationOrdinals(claimed){const reserved=new Set;for(const name of claimed){if(isPackageMigration(name))continue;const ordinal=migrationNameOrdinal(name);if(ordinal!==void 0)reserved.add(ordinal)}return reserved}export function nextFreeMigrationOrdinal(from,reserved){let cursor=from;while(reserved.has(cursor))cursor+=1;return cursor}export function duplicateMigrationOrdinals(files){const byOrdinal=new Map;for(const file of files){if(!file.toLowerCase().endsWith(".sql"))continue;const ordinal=/^(\d+)[-_]/.exec(file)?.[1];if(ordinal===void 0)continue;const key=String(Number.parseInt(ordinal,10));if(!byOrdinal.has(key))byOrdinal.set(key,[]);byOrdinal.get(key).push(file)}return[...byOrdinal.values()].filter((group)=>group.length>1).map((group)=>{const sorted=[...group].sort();return{ordinal:/^(\d+)/.exec(sorted[0])[1],files:sorted}}).sort((a,b)=>a.files[0].localeCompare(b.files[0]))}export function duplicateMigrationOrdinalsIn(dir){let files;try{files=readdirSync(dir)}catch{return[]}return duplicateMigrationOrdinals(files).map((group)=>({...group,generated:group.files.some((file)=>{try{return carriesGeneratedMarker(readFileSync(join(dir,file),"utf8"))}catch{return!1}})}))}
@@ -592,6 +592,27 @@ export declare function generatedStatementIsRedundant(statement: string, index:
592
592
  * treats a later `create-<table>-table.sql` as a duplicate of the first.
593
593
  */
594
594
  export declare function persistedGroupLabel(label: string, statements: readonly string[]): string;
595
+ /**
596
+ * Every migration filename a database may already have recorded, beyond the
597
+ * files on disk: the ledger of the database at hand, when it can be read, and
598
+ * the names the corpus has dropped over its git history. See
599
+ * `migration-names.ts` for why both, and why the generator must avoid them.
600
+ *
601
+ * Never throws and never blocks for long. With no database (a fresh clone, CI
602
+ * before `migrate`, a server that is down) the git history still covers every
603
+ * name that could have reached a deployed ledger; with neither, numbering
604
+ * falls back to the files on disk, which is what it always was.
605
+ */
606
+ export declare function claimedMigrationNames(dir?: string): Promise<string[]>;
607
+ /**
608
+ * Write generated SQL to the corpus, one file per group.
609
+ *
610
+ * `claimed` is every name some database may already have recorded (see
611
+ * {@link claimedMigrationNames}). No file is written under one of those names,
612
+ * or under one of their ordinals: the ledger keys on the filename alone, so a
613
+ * reused name is a migration `migrate` silently skips.
614
+ */
615
+ export declare function persistGeneratedMigrations(sqlStatements: string[], previousPlan?: MigrationPlan, claimed?: readonly string[]): number;
595
616
  /**
596
617
  * Group generated SQL by the migration filename style the runner already
597
618
  * uses for hand-written files: `create-<table>-table`,
@@ -1,4 +1,4 @@
1
- var {require}=import.meta;import{existsSync,mkdirSync,readdirSync,readFileSync,renameSync,unlinkSync,writeFileSync}from"node:fs";import{isPackageMigration,stagePackageMigrations}from"./package-migrations";import{basename,dirname,isAbsolute,join,resolve}from"node:path";import{log as _log}from"@stacksjs/logging";const log={info:(...args)=>typeof _log?.info==="function"?_log.info(...args):console.log(...args),success:(msg)=>typeof _log?.success==="function"?_log.success(msg):console.log(msg),warn:(msg)=>typeof _log?.warn==="function"?_log.warn(msg):console.warn(msg),error:(...args)=>typeof _log?.error==="function"?_log.error(...args):console.error(...args),debug:(...args)=>typeof _log?.debug==="function"?_log.debug(...args):console.debug(...args)};import{err,handleError,ok}from"@stacksjs/error-handling";import{path}from"@stacksjs/path";import{defaultModelsPath}from"./seeder";import{createQueryBuilder,executeMigrationWithReport as qbExecuteMigrationWithReport,generateDiffOperations,generateMigration as qbGenerateMigration,resetDatabase as qbResetDatabase,config as qbConfig,saveMigrationSnapshot,setConfig}from"@stacksjs/query-builder";import{db,ensureDatabaseConfigLoaded,qbSnapshotDir,resetDatabaseConnection,resolvedDatabaseConfig}from"./utils";import{classifyConnectionError,createDatabase,describeTarget,manualCreateHint,probeTargetDatabase,resolveConnectionTarget}from"./ensure-database";import{declaredModelTables,resolveModelSources}from"./model-sources";import{applicableFrameworkRenames,applyFrameworkRenamesToDatabase,applyFrameworkRenamesToPlan}from"./framework-renames";import{findShadowedColumnDrops,shadowDropsAllowed,shadowedDropMessage}from"./shadowed-models";import{frameworkManagedColumns,withoutManagedColumnDrops,withoutManagedColumnDropSql}from"./managed-columns";import{acquireLibsqlMigrationLock,acquireMigrationLock}from"./migration-lock";import{auditMissingTables}from"./migration-ledger";import{relativeMigrationDirectory,resolveMigrationDirectory}from"./migration-path";import{ensureNotificationForeignKeys,migrateNotificationTables,notificationTablePreflight}from"./notification-tables";import{traitTableNames}from"./trait-tables";import{AUTH_TABLES}from"./auth-tables";import{enumTypesCreatedBy,guardedBlockStatements,SQL_IDENTIFIER,sqlStatementsOf}from"./sql-statements";import{indexShape}from"./index-shape";import{corpusUniqueIndexes,hasIndex,restorableMissingUniqueIndexes,restoreDroppedUniqueIndexes,uniqueIndexIsRestorable}from"./declared-unique-indexes";import{ensureUuidColumns}from"./uuid-columns";import{sqlHelpers}from"./sql-helpers";import{GENERATED_MIGRATION_MARKER,isGeneratedMigration}from"./generated-migration-marker";import{trackIgnoredGeneratedMigrations}from"./ignored-migrations";export{enumTypesCreatedBy,sqlStatementsOf}from"./sql-statements";import{isLibsqlDriver,isVitessSharded,toSqlIntrospectionDialect}from"./dialect";const dbConfig={get default(){return resolvedDatabaseConfig().driver},get connections(){const{connections}=resolvedDatabaseConfig();return{...connections,turso:connections.turso??{url:"",authToken:""}}}};function usesLibsql(){return isLibsqlDriver(getDriver())}function sqliteDatabasePath(){const configured=dbConfig.connections.sqlite.database||"stacks.db";return isAbsolute(configured)?configured:join(process.cwd(),configured)}function getDriver(){return dbConfig.default||"sqlite"}function getDialect(){const driver=getDriver();if(driver==="sqlite"||driver==="mysql"||driver==="vitess"||driver==="postgres")return driver;if(driver==="turso")return"sqlite";if(driver==="singlestore")return"mysql";if(driver==="dynamodb")throw Error("[database] DB_CONNECTION=dynamodb is not compatible with the SQL migration runner. DynamoDB has no schema-migration concept - use the entity-style `dynamo.entity(...)` API from @stacksjs/database directly. To run SQL migrations, set DB_CONNECTION to one of: sqlite, turso, mysql, singlestore, vitess, postgres.");throw Error(`[database] Unknown DB_CONNECTION "${driver}". Allowed values: sqlite, turso, mysql, singlestore, vitess, postgres, dynamodb.`)}function getQbDialect(){return getDriver()==="singlestore"?"singlestore":getDialect()}function migrationDirectory(dialect=getQbDialect()){return resolveMigrationDirectory(dialect,{configured:qbConfig.migrationDir,snapshotDir:qbSnapshotDir()})}export async function activeMigrationDirectory(){await ensureDatabaseConfigLoaded();return migrationDirectory(getDialect())}function configureQueryBuilder(targetDialect=getQbDialect(),vitessSharded){const connectionConfig=dbConfig.connections[targetDialect],libsql=targetDialect==="sqlite"&&usesLibsql()?{url:dbConfig.connections.turso.url||void 0,authToken:dbConfig.connections.turso.authToken||void 0}:{url:void 0,authToken:void 0};if(targetDialect==="sqlite"&&usesLibsql()&&!libsql.url)throw Error("[database] DB_CONNECTION=turso needs TURSO_DATABASE_URL (libsql://<db>-<org>.turso.io, or http://127.0.0.1:8080 for `turso dev`).");setConfig({dialect:targetDialect,vitess:{sharded:isVitessSharded(vitessSharded??connectionConfig?.sharded)},verbose:!1,snapshotDir:qbSnapshotDir(),migrationDir:relativeMigrationDirectory(migrationDirectory(targetDialect)),database:{database:connectionConfig?.name||connectionConfig?.database||"stacks",host:connectionConfig?.host||"localhost",port:connectionConfig?.port||(targetDialect==="postgres"?5432:targetDialect==="vitess"?15306:targetDialect==="mysql"||targetDialect==="singlestore"?3306:0),username:connectionConfig?.username||"",password:connectionConfig?.password||"",...libsql}});resetDatabaseConnection()}export function prepareMigrationModelsDir(){const sources=resolveModelSources(),excludedTables=sources?.excludedTables??[];return{modelsDir:sources?.dir??path.userModelsPath(),skip:!sources,excludedTables,protectedTables:[...new Set([...excludedTables,...traitTableNames()])]}}function generatedStatementTables(statement){return statement.split(";").map((part)=>part.trim()).filter(Boolean).flatMap((part)=>{const table=rebuiltTable(part)??statementTable(part),index=/^\s*CREATE\s+(?:UNIQUE\s+)?INDEX\s+(?:IF\s+NOT\s+EXISTS\s+)?["`]?\w+["`]?\s+ON\s+["`]?(\w+)["`]?/i.exec(part)?.[1],dropped=DROP_TABLE_RE.exec(part)?.[1];return[table,index,dropped].filter((name)=>Boolean(name)).map((name)=>name.toLowerCase())})}export function withoutRuntimeOwnedTableSql(statements,owned=AUTH_TABLES){const ownedSet=new Set(owned.map((table)=>table.toLowerCase())),removed=[];return{statements:statements.filter((statement)=>{if(!generatedStatementTables(statement).some((table)=>ownedSet.has(table)))return!0;removed.push(statement);return!1}),removed}}const DROP_TABLE_RE=/^\s*DROP\s+TABLE\s+(?:IF\s+EXISTS\s+)?["'`]?(\w+)["'`]?/i;export function withoutProtectedTableDropSql(statements,protectedTables,operations){if(protectedTables.length===0)return{statements,removed:[]};const excluded=new Set(protectedTables.map((table)=>table.toLowerCase())),normalize=(sql)=>sql.replace(/\s+/g," ").trim().replace(/;$/,""),dropped=new Set(operations.filter((op)=>op.kind==="drop_table"&&excluded.has(op.table.toLowerCase())).filter((op)=>Boolean(op.sql)).map((op)=>normalize(op.sql))),removed=[];return{statements:statements.filter((statement)=>{if(dropped.has(normalize(statement))){removed.push(statement);return!1}const match=statement.match(DROP_TABLE_RE);if(match?.[1]&&excluded.has(match[1].toLowerCase())){removed.push(statement);return!1}return!0}),removed}}export function orderPostgresColumnTypeChanges(sql){const lines=sql.split(`
1
+ var {require}=import.meta;import{existsSync,mkdirSync,readdirSync,readFileSync,renameSync,unlinkSync,writeFileSync}from"node:fs";import{isPackageMigration,stagePackageMigrations}from"./package-migrations";import{nextFreeMigrationOrdinal,nextMigrationOrdinal,reservedMigrationOrdinals,retiredMigrationNames}from"./migration-names";import{basename,dirname,isAbsolute,join,resolve}from"node:path";import{log as _log}from"@stacksjs/logging";const log={info:(...args)=>typeof _log?.info==="function"?_log.info(...args):console.log(...args),success:(msg)=>typeof _log?.success==="function"?_log.success(msg):console.log(msg),warn:(msg)=>typeof _log?.warn==="function"?_log.warn(msg):console.warn(msg),error:(...args)=>typeof _log?.error==="function"?_log.error(...args):console.error(...args),debug:(...args)=>typeof _log?.debug==="function"?_log.debug(...args):console.debug(...args)};import{err,handleError,ok}from"@stacksjs/error-handling";import{path}from"@stacksjs/path";import{defaultModelsPath}from"./seeder";import{createQueryBuilder,executeMigrationWithReport as qbExecuteMigrationWithReport,generateDiffOperations,generateMigration as qbGenerateMigration,resetDatabase as qbResetDatabase,config as qbConfig,saveMigrationSnapshot,setConfig}from"@stacksjs/query-builder";import{db,ensureDatabaseConfigLoaded,qbSnapshotDir,resetDatabaseConnection,resolvedDatabaseConfig}from"./utils";import{classifyConnectionError,createDatabase,describeTarget,manualCreateHint,probeTargetDatabase,resolveConnectionTarget}from"./ensure-database";import{declaredModelTables,resolveModelSources}from"./model-sources";import{applicableFrameworkRenames,applyFrameworkRenamesToDatabase,applyFrameworkRenamesToPlan}from"./framework-renames";import{findShadowedColumnDrops,shadowDropsAllowed,shadowedDropMessage}from"./shadowed-models";import{frameworkManagedColumns,withoutManagedColumnDrops,withoutManagedColumnDropSql}from"./managed-columns";import{acquireLibsqlMigrationLock,acquireMigrationLock}from"./migration-lock";import{auditMissingTables}from"./migration-ledger";import{relativeMigrationDirectory,resolveMigrationDirectory}from"./migration-path";import{ensureNotificationForeignKeys,migrateNotificationTables,notificationTablePreflight}from"./notification-tables";import{traitTableNames}from"./trait-tables";import{AUTH_TABLES}from"./auth-tables";import{enumTypesCreatedBy,guardedBlockStatements,SQL_IDENTIFIER,sqlStatementsOf}from"./sql-statements";import{indexShape}from"./index-shape";import{corpusUniqueIndexes,hasIndex,restorableMissingUniqueIndexes,restoreDroppedUniqueIndexes,uniqueIndexIsRestorable}from"./declared-unique-indexes";import{ensureUuidColumns}from"./uuid-columns";import{sqlHelpers}from"./sql-helpers";import{GENERATED_MIGRATION_MARKER,isGeneratedMigration}from"./generated-migration-marker";import{trackIgnoredGeneratedMigrations}from"./ignored-migrations";export{enumTypesCreatedBy,sqlStatementsOf}from"./sql-statements";import{isLibsqlDriver,isVitessSharded,toSqlIntrospectionDialect}from"./dialect";const dbConfig={get default(){return resolvedDatabaseConfig().driver},get connections(){const{connections}=resolvedDatabaseConfig();return{...connections,turso:connections.turso??{url:"",authToken:""}}}};function usesLibsql(){return isLibsqlDriver(getDriver())}function sqliteDatabasePath(){const configured=dbConfig.connections.sqlite.database||"stacks.db";return isAbsolute(configured)?configured:join(process.cwd(),configured)}function getDriver(){return dbConfig.default||"sqlite"}function getDialect(){const driver=getDriver();if(driver==="sqlite"||driver==="mysql"||driver==="vitess"||driver==="postgres")return driver;if(driver==="turso")return"sqlite";if(driver==="singlestore")return"mysql";if(driver==="dynamodb")throw Error("[database] DB_CONNECTION=dynamodb is not compatible with the SQL migration runner. DynamoDB has no schema-migration concept - use the entity-style `dynamo.entity(...)` API from @stacksjs/database directly. To run SQL migrations, set DB_CONNECTION to one of: sqlite, turso, mysql, singlestore, vitess, postgres.");throw Error(`[database] Unknown DB_CONNECTION "${driver}". Allowed values: sqlite, turso, mysql, singlestore, vitess, postgres, dynamodb.`)}function getQbDialect(){return getDriver()==="singlestore"?"singlestore":getDialect()}function migrationDirectory(dialect=getQbDialect()){return resolveMigrationDirectory(dialect,{configured:qbConfig.migrationDir,snapshotDir:qbSnapshotDir()})}export async function activeMigrationDirectory(){await ensureDatabaseConfigLoaded();return migrationDirectory(getDialect())}function configureQueryBuilder(targetDialect=getQbDialect(),vitessSharded){const connectionConfig=dbConfig.connections[targetDialect],libsql=targetDialect==="sqlite"&&usesLibsql()?{url:dbConfig.connections.turso.url||void 0,authToken:dbConfig.connections.turso.authToken||void 0}:{url:void 0,authToken:void 0};if(targetDialect==="sqlite"&&usesLibsql()&&!libsql.url)throw Error("[database] DB_CONNECTION=turso needs TURSO_DATABASE_URL (libsql://<db>-<org>.turso.io, or http://127.0.0.1:8080 for `turso dev`).");setConfig({dialect:targetDialect,vitess:{sharded:isVitessSharded(vitessSharded??connectionConfig?.sharded)},verbose:!1,snapshotDir:qbSnapshotDir(),migrationDir:relativeMigrationDirectory(migrationDirectory(targetDialect)),database:{database:connectionConfig?.name||connectionConfig?.database||"stacks",host:connectionConfig?.host||"localhost",port:connectionConfig?.port||(targetDialect==="postgres"?5432:targetDialect==="vitess"?15306:targetDialect==="mysql"||targetDialect==="singlestore"?3306:0),username:connectionConfig?.username||"",password:connectionConfig?.password||"",...libsql}});resetDatabaseConnection()}export function prepareMigrationModelsDir(){const sources=resolveModelSources(),excludedTables=sources?.excludedTables??[];return{modelsDir:sources?.dir??path.userModelsPath(),skip:!sources,excludedTables,protectedTables:[...new Set([...excludedTables,...traitTableNames()])]}}function generatedStatementTables(statement){return statement.split(";").map((part)=>part.trim()).filter(Boolean).flatMap((part)=>{const table=rebuiltTable(part)??statementTable(part),index=/^\s*CREATE\s+(?:UNIQUE\s+)?INDEX\s+(?:IF\s+NOT\s+EXISTS\s+)?["`]?\w+["`]?\s+ON\s+["`]?(\w+)["`]?/i.exec(part)?.[1],dropped=DROP_TABLE_RE.exec(part)?.[1];return[table,index,dropped].filter((name)=>Boolean(name)).map((name)=>name.toLowerCase())})}export function withoutRuntimeOwnedTableSql(statements,owned=AUTH_TABLES){const ownedSet=new Set(owned.map((table)=>table.toLowerCase())),removed=[];return{statements:statements.filter((statement)=>{if(!generatedStatementTables(statement).some((table)=>ownedSet.has(table)))return!0;removed.push(statement);return!1}),removed}}const DROP_TABLE_RE=/^\s*DROP\s+TABLE\s+(?:IF\s+EXISTS\s+)?["'`]?(\w+)["'`]?/i;export function withoutProtectedTableDropSql(statements,protectedTables,operations){if(protectedTables.length===0)return{statements,removed:[]};const excluded=new Set(protectedTables.map((table)=>table.toLowerCase())),normalize=(sql)=>sql.replace(/\s+/g," ").trim().replace(/;$/,""),dropped=new Set(operations.filter((op)=>op.kind==="drop_table"&&excluded.has(op.table.toLowerCase())).filter((op)=>Boolean(op.sql)).map((op)=>normalize(op.sql))),removed=[];return{statements:statements.filter((statement)=>{if(dropped.has(normalize(statement))){removed.push(statement);return!1}const match=statement.match(DROP_TABLE_RE);if(match?.[1]&&excluded.has(match[1].toLowerCase())){removed.push(statement);return!1}return!0}),removed}}export function orderPostgresColumnTypeChanges(sql){const lines=sql.split(`
2
2
  `),output=[];for(const line of lines){const match=/^(\s*)ALTER\s+TABLE\s+("?[\w.]+"?)\s+ALTER\s+COLUMN\s+("?[\w]+"?)\s+TYPE\s/i.exec(line);if(match){const[,indent,table,column]=match,drop=`${indent}ALTER TABLE ${table} ALTER COLUMN ${column} DROP DEFAULT;`;if((output.length>0?output[output.length-1].trim():"")!==drop.trim())output.push(drop)}output.push(line)}return output.join(`
3
3
  `)}export function guardPostgresEnumTypes(sql){return assertPostgresEnumMembers(wrapPostgresEnumTypes(sql))}function wrapPostgresEnumTypes(sql){return sql.replace(/CREATE\s+TYPE\s+("?[\w.]+"?)\s+AS\s+ENUM\s*\(([^)]*)\)/gi,(match,name,members,offset,whole)=>{if(/\bBEGIN\s*$/i.test(whole.slice(Math.max(0,offset-40),offset)))return match;return`DO $stacks$ BEGIN CREATE TYPE ${name} AS ENUM (${members}); EXCEPTION WHEN duplicate_object THEN null; END $stacks$`})}function assertPostgresEnumMembers(sql){return sql.replace(/DO \$stacks\$[\s\S]*?END \$stacks\$;?/g,(block)=>{const created=/CREATE\s+TYPE\s+("?[\w.]+"?)\s+AS\s+ENUM\s*\(([^)]*)\)/i.exec(block);if(!created)return block;const[,name,members]=created,missing=enumMembers(members).map((member)=>`ALTER TYPE ${name} ADD VALUE IF NOT EXISTS ${member};`).filter((statement)=>!sql.includes(statement));if(missing.length===0)return block;return`${block.endsWith(";")?block:`${block};`}
4
4
  ${missing.join(`
@@ -29,14 +29,14 @@ Create it yourself with: ${manualCreateHint(target)}`);if(result.created)log.su
29
29
  JOIN pg_class c ON c.oid = a.attrelid
30
30
  WHERE a.atttypid = t.oid AND c.relkind = 'r' AND NOT a.attisdropped
31
31
  )
32
- `).execute(),names=(Array.isArray(raw)?raw:raw?.rows??[]).map((row)=>String(row.name??row.typname??"")).filter(Boolean);for(const name of names)try{await db.unsafe(`DROP TYPE IF EXISTS "${name}" CASCADE`).execute();log.debug(`Dropped orphaned enum type: ${name}`)}catch(error){log.warn(`Could not drop enum type ${name}: ${error instanceof Error?error.message:String(error)}`)}}catch(error){log.warn(`Could not list enum types to drop: ${error instanceof Error?error.message:String(error)}`)}}async function dropFrameworkTables(dialect){if(dialect==="mysql"||dialect==="vitess")try{await db.unsafe("SET FOREIGN_KEY_CHECKS = 0").execute()}catch(error){log.warn(`Could not disable foreign key checks: ${error instanceof Error?error.message:String(error)}`)}if(dialect==="sqlite")try{await db.unsafe("PRAGMA foreign_keys = OFF").execute()}catch(error){log.warn(`Could not disable foreign key checks: ${error instanceof Error?error.message:String(error)}`)}for(const tableName of FRAMEWORK_TABLES)try{let dropSql;if(dialect==="postgres")dropSql=`DROP TABLE IF EXISTS "${tableName}" CASCADE`;else if(dialect==="mysql"||dialect==="vitess")dropSql=`DROP TABLE IF EXISTS \`${tableName}\``;else dropSql=`DROP TABLE IF EXISTS "${tableName}"`;log.info(`Dropping framework table: ${tableName}`);await db.unsafe(dropSql).execute();log.info(`Dropped framework table: ${tableName}`)}catch(error){const kind=classifyConnectionError(error);if(kind==="missing-database"||kind==="missing-role"||kind==="auth-failed"||kind==="server-unreachable"||kind==="timeout")throw error;log.warn(`Could not drop table ${tableName}: ${error instanceof Error?error.message:String(error)}`)}if(dialect==="mysql"||dialect==="vitess")try{await db.unsafe("SET FOREIGN_KEY_CHECKS = 1").execute()}catch(error){log.warn(`Could not re-enable foreign key checks: ${error instanceof Error?error.message:String(error)}`)}if(dialect==="sqlite")try{await db.unsafe("PRAGMA foreign_keys = ON").execute()}catch(error){log.warn(`Could not re-enable foreign key checks: ${error instanceof Error?error.message:String(error)}`)}}function frameworkRenamesForThisApp(){return applicableFrameworkRenames(declaredModelTables())}function describeRenames(renames){return renames.map((rename)=>`${rename.id}, ${rename.reference}`).join("; ")}export function catchUpSnapshotFrameworkRenames(options){configureQueryBuilder();const dialect=getQbDialect(),stored=readStoredMigrationPlan(dialect);if(!stored)return{changes:[]};const renames=frameworkRenamesForThisApp();if(renames.length===0)return{changes:[]};const{plan,changes}=applyFrameworkRenamesToPlan(stored,renames);if(changes.length===0)return{changes};if(options.persist){saveMigrationSnapshot(plan,{dialect});log.info(`[migration] Updated the model snapshot for framework renames (${describeRenames(renames)}): ${changes.join(", ")}`)}else log.debug(`[migration] Previewing against the snapshot with framework renames applied: ${changes.join(", ")}`);return{changes,plan}}export async function catchUpFrameworkRenames(options={}){await ensureDatabaseConfigLoaded();const dialect=toSqlIntrospectionDialect(getDialect());if(dialect==="other")return{applied:[],skipped:[]};const renames=frameworkRenamesForThisApp();if(renames.length===0)return{applied:[],skipped:[]};const runner=options.runner??(async(sql)=>{const result=await db.unsafe(sql).execute();return Array.isArray(result)?result:[]}),result=await applyFrameworkRenamesToDatabase(runner,dialect,renames);if(result.applied.length>0)log.info(`[migration] Applied framework renames to the database (${describeRenames(renames)}): ${result.applied.join(", ")}`);for(const skipped of result.skipped)log.warn(`[migration] Framework rename not applied: ${skipped}`);return result}function resolveGenerateOptions(options){const applyRenames=options.applyRenames??(process.env.STACKS_MIGRATE_NO_RENAME==="1"?!1:void 0),fromDb=options.fromDb??(process.env.STACKS_MIGRATE_FROM_DB==="1"?!0:void 0);return{applyRenames,fromDb}}export async function previewPendingMigrations(options={}){try{return await pendingMigrationOperations(options)}catch(error){log.debug(`[migration] preview failed: ${error instanceof Error?error.message:String(error)}`);return[]}}export async function pendingMigrationOperations(options={}){await ensureDatabaseConfigLoaded();configureQueryBuilder();const dialect=getDialect(),{modelsDir,skip,protectedTables}=prepareMigrationModelsDir();if(skip)return[];const{applyRenames,fromDb}=resolveGenerateOptions(options),qbDialect=getQbDialect(),result=await qbGenerateMigration(modelsDir,{dialect:qbDialect,vitessSharded:qbDialect==="vitess"?isVitessSharded(dbConfig.connections.vitess.sharded):void 0,dryRun:!0,applyRenames,fromDb}),corrected=fromDb?void 0:catchUpSnapshotFrameworkRenames({persist:!1}).plan;if(corrected&&result.plan)result.operations=generateDiffOperations(corrected,result.plan,{applyRenames,dryRun:!0}).operations;const authOwned=new Set(AUTH_TABLES);let operations=(result.operations??[]).filter((op)=>!authOwned.has(op.table.toLowerCase()));if(protectedTables.length>0){const excluded=new Set(protectedTables.map((table)=>table.toLowerCase()));operations=operations.filter((op)=>!(op.kind==="drop_table"&&excluded.has(op.table.toLowerCase())))}if(!operations.some((op)=>op.kind==="drop_column"))return operations;return withoutManagedColumnDrops(operations,await frameworkManagedColumns())}function snapshotDirLabel(){return qbConfig?.snapshotDir||qbSnapshotDir()}function resolveSnapshotDir(){const label=snapshotDirLabel();return isAbsolute(label)?label:resolve(process.cwd(),label)}function snapshotPathFor(dialect){return join(resolveSnapshotDir(),`model-snapshot.${dialect}.json`)}function readStoredMigrationPlan(dialect){const snapshotPath=snapshotPathFor(dialect);if(!existsSync(snapshotPath))return;let parsed;try{parsed=JSON.parse(readFileSync(snapshotPath,"utf8"))}catch(error){throw Error(`The migration snapshot is not valid JSON: ${snapshotPath}. Repair or regenerate it before creating migrations.`,{cause:error})}const candidate=parsed&&typeof parsed==="object"&&"plan"in parsed?parsed.plan:parsed;if(!candidate||typeof candidate!=="object"||!Array.isArray(candidate.tables))throw TypeError(`The migration snapshot has an invalid structure: ${snapshotPath}. Repair or regenerate it before creating migrations.`);return candidate}export function preserveMigrationPlanTableOrder(next,previous){const orderedByPrevious=(values,previousValues,keyFor)=>{if(!previousValues)return[...values];const previousOrder=new Map(previousValues.map((value,index)=>[keyFor(value),index])),nextOrder=new Map(values.map((value,index)=>[keyFor(value),index])),previousCount=previousValues.length;return[...values].sort((left,right)=>{const leftKey=keyFor(left),rightKey=keyFor(right),leftOrder=previousOrder.get(leftKey)??previousCount+nextOrder.get(leftKey),rightOrder=previousOrder.get(rightKey)??previousCount+nextOrder.get(rightKey);return leftOrder-rightOrder})},previousTables=new Map(previous?.tables.map((table)=>[table.table,table])??[]),tables=next.tables.map((table)=>{const previousTable=previousTables.get(table.table),previousColumns=new Map(previousTable?.columns.map((column)=>[column.name,column])??[]);return{...table,columns:orderedByPrevious(table.columns.map((column)=>{if(next.dialect!=="sqlite"||!column.enumTypeName||previousColumns.get(column.name)?.enumTypeName)return column;const portableColumn={...column};delete portableColumn.enumTypeName;return portableColumn}),previousTable?.columns,(column)=>column.name),indexes:orderedByPrevious(table.indexes,previousTable?.indexes,(index)=>index.name)}});return{...next,tables:orderedByPrevious(tables,previous?.tables,(table)=>table.table)}}function detectSnapshotDialectMismatch(dialect){const qbDir=resolveSnapshotDir();let files;try{files=readdirSync(qbDir)}catch{return null}const snapshotFor=(d)=>`model-snapshot.${d}.json`;if(files.includes(snapshotFor(dialect)))return null;for(const f of files){const m=/^model-snapshot\.(\w+)\.json$/.exec(f);if(m?.[1]&&m[1]!==dialect)return m[1]}return null}export async function generateMigrations(options={}){await ensureDatabaseConfigLoaded();try{log.debug("Generating migrations...");configureQueryBuilder();const dialect=getDialect(),mismatch=detectSnapshotDialectMismatch(dialect),flatMigrationDir=join(process.cwd(),"database","migrations");if(mismatch&&migrationDirectory(dialect)===flatMigrationDir){const snapshotDir=snapshotDirLabel();return err(Error(`Refusing to generate migrations: resolved dialect "${dialect}" has no snapshot in ${snapshotDir}/, but "${mismatch}" does. DB_CONNECTION is likely unset or wrong (missing .env?) - generating now would write a full duplicate migration set in the wrong dialect. Set DB_CONNECTION=${mismatch} (or your intended dialect) and retry. To intentionally start a new dialect from scratch, remove ${snapshotDir}/model-snapshot.${mismatch}.json first.`))}catchUpSnapshotFrameworkRenames({persist:!0});const storedPlan=readStoredMigrationPlan(getQbDialect()),{modelsDir,skip,excludedTables,protectedTables}=prepareMigrationModelsDir();if(skip){trackIgnoredGeneratedMigrations(migrationDirectory());log.debug("No app/Models directory found; using committed framework migrations");return ok("Migrations generated")}const{applyRenames,fromDb}=resolveGenerateOptions(options);log.debug(`[migration] Generating migrations for dialect: ${dialect}, models: ${modelsDir}`);if(excludedTables.length>0)log.debug(`[migration] ${excludedTables.length} framework default model(s) out of scope because app/Models defines this app's schema. Enable database.models.includeFrameworkDefaults (or STACKS_INCLUDE_FRAMEWORK_MODELS=1) to generate them too.`);const qbDialect=getQbDialect(),result=await qbGenerateMigration(modelsDir,{dialect:qbDialect,vitessSharded:qbDialect==="vitess"?isVitessSharded(dbConfig.connections.vitess.sharded):void 0,dryRun:!0,applyRenames,fromDb});let sqlStatements=result.sqlStatements??[];if(dialect==="sqlite")sqlStatements=inlineSqliteAddedColumnReferences(sqlStatements,result.plan);{const runtimeOwned=withoutRuntimeOwnedTableSql(sqlStatements);if(runtimeOwned.removed.length>0)log.debug(`[migration] Left out ${runtimeOwned.removed.length} generated statement(s) for tables the auth layer creates itself`);sqlStatements=runtimeOwned.statements}if(result.hasChanges&&sqlStatements.length>0){const filtered=withoutManagedColumnDropSql(sqlStatements,await frameworkManagedColumns(),result.operations??[]);if(filtered.removed.length>0)log.debug(`[migration] Skipped ${filtered.removed.length} generated drop(s) of framework-managed column(s) (stacksjs/stacks#2075)`);sqlStatements=filtered.statements}if(result.hasChanges&&sqlStatements.length>0&&protectedTables.length>0){const filtered=withoutProtectedTableDropSql(sqlStatements,protectedTables,result.operations??[]);if(filtered.removed.length>0)log.info(`[migration] Left ${filtered.removed.length} framework-owned table(s) in place rather than dropping them. They are no longer generated because app/Models defines this app's schema; the tables and their data are untouched. Set database.models.includeFrameworkDefaults to keep generating them (stacksjs/stacks#2220).`);sqlStatements=filtered.statements}if(result.hasChanges&&sqlStatements.length>0){const dangling=findDanglingTypeReferences(sqlStatements);if(dangling.length>0){const created=createMissingEnumTypes(dangling,result.plan);if(created.statements.length>0){sqlStatements=[...created.statements,...sqlStatements];log.debug(`[migration] Created ${created.statements.length} enum type(s) an ALTER needed: ${created.defined.join(", ")}`)}const unresolved=dangling.filter((name)=>!created.defined.includes(name));if(unresolved.length>0){const before=sqlStatements.length;sqlStatements=sqlStatements.filter((statement)=>!referencesUndefinedType(statement,unresolved));log.warn(`[migration] Skipped ${before-sqlStatements.length} generated statement(s) referencing enum type(s) nothing creates and no model defines values for (${unresolved.slice(0,3).join(", ")}${unresolved.length>3?", \u2026":""}).`)}}}if(result.hasChanges&&sqlStatements.length>0&&!shadowDropsAllowed()){const shadowed=resolveModelSources()?.shadowed??[],drops=findShadowedColumnDrops(sqlStatements,shadowed);if(drops.length>0)return err(Error(shadowedDropMessage(drops)))}if(result.hasChanges){const written=persistGeneratedMigrations(sqlStatements,storedPlan);if(written>0)log.success(`Generated ${written} migration file${written===1?"":"s"}`);else log.debug("Migration generation produced no new files (already up to date)")}else log.debug("No changes detected");trackIgnoredGeneratedMigrations(migrationDirectory());const stablePlan=preserveMigrationPlanTableOrder(result.plan,storedPlan);if(!storedPlan||JSON.stringify(stablePlan)!==JSON.stringify(storedPlan))saveMigrationSnapshot(stablePlan,{dialect:getQbDialect()});else log.debug("Model snapshot unchanged");return ok("Migrations generated")}catch(error){const because=error instanceof Error?error.message:String(error);return err(handleError(`Migration generation failed: ${because}`,error))}}export function referencesUndefinedType(statement,dangling){return dangling.some((name)=>statement.includes(`"${name}"`))}export function findDanglingTypeReferences(statements){const defined=new Set,referenced=new Set;for(const statement of statements){for(const match of statement.matchAll(/CREATE\s+TYPE\s+"([^"]+)"/gi))defined.add(match[1]);for(const match of statement.matchAll(/\bTYPE\s+"([^"]+)"/gi))referenced.add(match[1])}return[...referenced].filter((name)=>!defined.has(name)).sort()}export function createMissingEnumTypes(dangling,plan){if(dangling.length===0)return{statements:[],defined:[]};const values=new Map;for(const table of plan?.tables??[]){if(!table.table)continue;for(const column of table.columns??[]){if(!column.name||!column.enumValues?.length)continue;values.set(`${table.table}_${column.name}_type`,column.enumValues)}}const statements=[],defined=[];for(const name of dangling){const members=values.get(name);if(!members?.length)continue;const literals=members.map((member)=>`'${String(member).replaceAll("'","''")}'`).join(", "),guarded=guardPostgresEnumTypes(`CREATE TYPE "${name}" AS ENUM (${literals})`);statements.push(guarded.endsWith(";")?guarded:`${guarded};`);defined.push(name)}return{statements,defined}}export function inlineSqliteAddedColumnReferences(statements,plan){const references=new Map;for(const table of plan?.tables??[]){if(!table.table)continue;for(const column of table.columns??[]){const reference=column.references;if(!column.name||!reference?.table||!reference.column)continue;references.set(`${table.table}.${column.name}`,{table:reference.table,column:reference.column})}}if(references.size===0)return statements;return statements.map((statement)=>{if(/\bREFERENCES\b/i.test(statement))return statement;const match=statement.match(/^(\s*ALTER\s+TABLE\s+["`]?(\w+)["`]?\s+ADD\s+COLUMN\s+["`]?(\w+)["`]?\s+)([\s\S]*?)(;?\s*)$/i),reference=match?.[2]&&match[3]?references.get(`${match[2]}.${match[3]}`):void 0;if(!match||!reference)return statement;return`${match[1]}${match[4].trimEnd()} REFERENCES "${reference.table}"("${reference.column}")${match[5]}`})}export{GENERATED_MIGRATION_MARKER,isGeneratedMigration}from"./generated-migration-marker";export function tablesOperatedOn(sql){const tables=new Set;for(const statement of sqlStatementsOf(sql)){const stmt=statement.trim(),direct=stmt.match(/^(?:CREATE\s+TABLE(?:\s+IF\s+NOT\s+EXISTS)?|ALTER\s+TABLE|DROP\s+TABLE(?:\s+IF\s+EXISTS)?|TRUNCATE\s+TABLE)\s+["'`]?(\w+)["'`]?/i);if(direct?.[1]){tables.add(direct[1].toLowerCase());continue}const index=stmt.match(/^CREATE\s+(?:UNIQUE\s+)?INDEX(?:\s+IF\s+NOT\s+EXISTS)?\s+\S+\s+ON\s+["'`]?(\w+)["'`]?/i);if(index?.[1])tables.add(index[1].toLowerCase())}return[...tables]}const IDENTIFIER=SQL_IDENTIFIER,NEUTRAL_STATEMENT=/^(?:BEGIN|COMMIT|END|PRAGMA|SET|SELECT\s+1\s*$|COMMENT\s+ON)\b/i;export function describeMigrationContent(sql){const content={types:[],droppedTypes:[],droppedIndexes:[],createdIndexes:[],dataTables:[],seedTables:[],createdTables:[],other:!1},add=(list,name)=>{const lower=name?.toLowerCase();if(lower&&!list.includes(lower))list.push(lower)},visit=(statement)=>{const stmt=statement.trim().replace(/;\s*$/,"");if(!stmt||NEUTRAL_STATEMENT.test(stmt))return;const guarded=guardedBlockStatements(stmt);if(guarded){for(const inner of guarded)visit(inner);return}let m=new RegExp(String.raw`^CREATE\s+TYPE\s+${IDENTIFIER}\s+AS\s+ENUM\b`,"i").exec(stmt)??new RegExp(String.raw`^ALTER\s+TYPE\s+${IDENTIFIER}\s+ADD\s+VALUE\b`,"i").exec(stmt);if(m){add(content.types,m[1]);return}m=new RegExp(String.raw`^DROP\s+TYPE\s+(?:IF\s+EXISTS\s+)?${IDENTIFIER}`,"i").exec(stmt);if(m){add(content.droppedTypes,m[1]);return}m=new RegExp(String.raw`^DROP\s+INDEX\s+(?:CONCURRENTLY\s+)?(?:IF\s+EXISTS\s+)?${IDENTIFIER}`,"i").exec(stmt);if(m){add(content.droppedIndexes,m[1]);return}m=new RegExp(String.raw`^(?:DELETE\s+FROM|UPDATE|TRUNCATE(?:\s+TABLE)?)\s+(?:ONLY\s+)?${IDENTIFIER}`,"i").exec(stmt);if(m){add(content.dataTables,m[1]);return}m=new RegExp(String.raw`^COPY\s+${IDENTIFIER}[\s\S]*\bFROM\b`,"i").exec(stmt);if(m){add(content.seedTables,m[1]);return}m=new RegExp(String.raw`^INSERT\s+(?:OR\s+\w+\s+)?INTO\s+${IDENTIFIER}`,"i").exec(stmt);if(m){if(/\bSELECT\b[\s\S]*\bFROM\b/i.test(stmt)&&!/\bVALUES\b/i.test(stmt))content.other=!0;else add(content.seedTables,m[1]);return}const shape=indexShape(stmt);if(shape){add(content.createdIndexes,shape.name);content.other=!0;return}const created=createdTableName(stmt);if(created)add(content.createdTables,created);content.other=!0};for(const statement of sqlStatementsOf(sql))visit(statement);return content}export function obsoleteMigrationReason(content,context){if(content.other||content.seedTables.length>0)return;const hasTypes=content.types.length>0||content.droppedTypes.length>0,hasDrops=content.droppedIndexes.length>0,hasData=content.dataTables.length>0;if([hasTypes,hasDrops,hasData].filter(Boolean).length!==1)return;if(hasTypes)return content.types.every((type)=>context.emittedTypes.has(type))?"types":void 0;if(hasDrops)return content.droppedIndexes.every((index)=>!context.keptIndexes.has(index))?"drop-index":void 0;return content.dataTables.every((table)=>!context.keptSeedTables.has(table))?"data":void 0}export function columnsDefinedByCreate(statement){const body=statement.match(/^\s*CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["'`]?\w+["'`]?\s*\(([\s\S]*)\)\s*;?\s*$/i)?.[1];if(!body)return[];const parts=[];let depth=0,current="";for(const char of body){if(char==="(")depth++;if(char===")")depth--;if(char===","&&depth===0){parts.push(current);current="";continue}current+=char}parts.push(current);const constraint=/^\s*(?:PRIMARY\s+KEY|FOREIGN\s+KEY|UNIQUE|CHECK|CONSTRAINT|KEY|INDEX)\b/i;return parts.map((part)=>part.trim()).filter((part)=>part&&!constraint.test(part)).flatMap((part)=>part.match(/^["'`]?(\w+)["'`]?/)?.[1]??[])}export function columnsProducedByMigrations(dir,files,table){const columns=new Set,target=table.toLowerCase();for(const file of[...files].sort()){let content;try{content=readFileSync(join(dir,file),"utf8")}catch{continue}for(const statement of sqlStatementsOf(content)){const stmt=statement.trim();if(stmt.match(/^CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["'`]?(\w+)["'`]?/i)?.[1]?.toLowerCase()===target){for(const column of columnsDefinedByCreate(stmt))columns.add(column.toLowerCase());continue}const added=stmt.match(/^ALTER\s+TABLE\s+["'`]?(\w+)["'`]?\s+ADD\s+(?:COLUMN\s+)?["'`]?(\w+)["'`]?/i);if(added?.[1]?.toLowerCase()===target&&added[2])columns.add(added[2].toLowerCase());const dropped=stmt.match(/^ALTER\s+TABLE\s+["'`]?(\w+)["'`]?\s+DROP\s+(?:COLUMN\s+)?["'`]?(\w+)["'`]?/i);if(dropped?.[1]?.toLowerCase()===target&&dropped[2])columns.delete(dropped[2].toLowerCase());const rebuilt=stmt.match(/^ALTER\s+TABLE\s+["'`]?_qb_tmp_(\w+)["'`]?\s+RENAME\s+TO\s+["'`]?(\w+)["'`]?/i);if(rebuilt?.[2]?.toLowerCase()===target){const temp=sqlStatementsOf(content).find((s)=>new RegExp(`^CREATE\\s+TABLE\\s+["'\`]?_qb_tmp_${rebuilt[1]}["'\`]?`,"i").test(s.trim()));if(temp){columns.clear();for(const column of columnsDefinedByCreate(temp))columns.add(column.toLowerCase())}}}}return columns}export function rootedTableCatchUpStatements(createStatement,existingColumns){const table=createStatement.match(/^\s*CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["'`]?(\w+)["'`]?/i)?.[1];if(!table)return[];const body=createStatement.match(/\(([\s\S]*)\)\s*;?\s*$/)?.[1];if(!body)return[];const definitions=new Map;let depth=0,current="";const parts=[];for(const char of body){if(char==="(")depth++;if(char===")")depth--;if(char===","&&depth===0){parts.push(current);current="";continue}current+=char}parts.push(current);const constraint=/^\s*(?:PRIMARY\s+KEY|FOREIGN\s+KEY|UNIQUE|CHECK|CONSTRAINT|KEY|INDEX)\b/i;for(const raw of parts){const part=raw.trim();if(!part||constraint.test(part))continue;const name=part.match(/^["'`]?(\w+)["'`]?/)?.[1];if(name)definitions.set(name.toLowerCase(),part)}const statements=[];for(const[name,definition]of definitions){if(existingColumns.has(name))continue;if(/\b(?:PRIMARY\s+KEY|UNIQUE|AUTOINCREMENT)\b/i.test(definition))continue;const nullable=/\bNOT\s+NULL\b/i.test(definition)&&!/\bDEFAULT\b/i.test(definition)?definition.replace(/\s*\bNOT\s+NULL\b/i,""):definition;statements.push(`ALTER TABLE "${table}" ADD COLUMN ${nullable.trim()}`)}return statements}export function createdTablesOf(statements){const tables=new Set;for(const statement of statements){const match=statement.trim().match(/^CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["'`]?(\w+)["'`]?/i);if(match?.[1])tables.add(match[1].toLowerCase())}return[...tables]}export function historicallyRootedTables(dir,files){const tables=new Set;for(const file of files){if(isGeneratedMigration(dir,file))continue;try{for(const statement of sqlStatementsOf(readFileSync(join(dir,file),"utf8"))){const match=statement.match(/^\s*CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["'`]?(\w+)["'`]?/i);if(match?.[1])tables.add(match[1].toLowerCase())}}catch{}}return[...tables]}export function tablesDefinedByCorpus(dir,files){const tables=new Set;for(const file of files)try{for(const statement of sqlStatementsOf(readFileSync(join(dir,file),"utf8"))){const match=statement.match(/^\s*CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["'`]?(\w+)["'`]?/i);if(match?.[1])tables.add(match[1].toLowerCase())}}catch{}return[...tables]}export function migrationTouchesRootedTable(dir,file,rootedTables){try{return tablesOperatedOn(readFileSync(join(dir,file),"utf8")).some((table)=>rootedTables.has(table.toLowerCase()))}catch{return!1}}export function allocateMigrationOrdinals(count,startAt,reserved){const ordinals=[];let cursor=startAt;while(ordinals.length<count){if(!reserved.has(cursor))ordinals.push(cursor);cursor+=1}return ordinals}export function migrationsOutsideCorpus(dir,files,corpusTables){const rebuilt=new Set(corpusTables.map((table)=>table.toLowerCase()));return files.filter((file)=>{let contents;try{contents=readFileSync(join(dir,file),"utf8")}catch{return!0}const touched=tablesOperatedOn(contents);if(touched.length===0)return!0;return touched.some((table)=>!rebuilt.has(table))})}function migrationOrdinal(file){const match=file.match(/^(\d+)/);return match?Number(match[1]):0}export async function generateFullModelMigration(options={}){await ensureDatabaseConfigLoaded();try{const dialect=options.dialect??getQbDialect();let requestedVitessSharded;if(dialect==="vitess")try{const{config,overridesReady}=await import("@stacksjs/config");await overridesReady;requestedVitessSharded=isVitessSharded(config?.database?.connections?.vitess?.sharded)}catch{requestedVitessSharded=isVitessSharded(dbConfig.connections.vitess.sharded)}configureQueryBuilder(dialect,requestedVitessSharded);const sources=resolveModelSources({forceStage:!0,...options.onlyExistingTables?{includeFrameworkDefaults:!0}:{}});if(!sources)return err(Error("No models found. Define models in app/Models, or ensure the framework defaults at storage/framework/defaults/app/Models are present."));try{writeFileSync(join(sources.dir,`.qb-migrations.${dialect}.json`),JSON.stringify({plan:{dialect,tables:[]}}))}catch{}const snapshotPath=join(resolveSnapshotDir(),`model-snapshot.${dialect}.json`),parkedSnapshot=`${snapshotPath}.regenerating`;let snapshotParked=!1;if(existsSync(snapshotPath))try{renameSync(snapshotPath,parkedSnapshot);snapshotParked=!0}catch{}let result;try{result=await qbGenerateMigration(sources.dir,{dialect,vitessSharded:requestedVitessSharded,dryRun:!0,full:!0})}finally{if(snapshotParked)try{renameSync(parkedSnapshot,snapshotPath)}catch{}}const statements=result.sqlStatements??[];if(statements.length===0)return err(Error(`The generator produced no SQL for dialect "${dialect}".`));const dangling=findDanglingTypeReferences(statements);if(dangling.length>0)return err(Error(`The generator emitted ${dangling.length} reference(s) to enum type(s) it never creates: ${dangling.slice(0,5).join(", ")}${dangling.length>5?`, +${dangling.length-5} more`:""}. Writing this corpus would fail partway through a migration. This is a bug in the migration generator, not in your models.`));return ok({dialect,statements,plan:result.plan,sources})}catch(error){return err(handleError("Migration generation failed",error))}}export async function regenerateMigrationCorpus(options={}){try{const generated=await generateFullModelMigration({dialect:options.dialect,onlyExistingTables:options.onlyExistingTables});if(!generated.isOk)return err(generated.error);const{dialect,statements,plan,sources}=generated.value,dir=options.dir??migrationDirectory(dialect);let groups=groupGeneratedStatements(statements),existing=[];try{existing=readdirSync(dir).filter((f)=>f.endsWith(".sql")).sort()}catch{}let unrebuildable=[];if(options.onlyExistingTables){const corpusTables=new Set(tablesDefinedByCorpus(dir,existing));if(corpusTables.size===0)return err(Error(`No CREATE TABLE statements found in ${dir}, so there is nothing to regenerate in place. Run \`buddy migrate:regenerate <dialect>\` without --only-existing-tables to write a corpus from your models.`));const emitted=new Set(createdTablesOf(statements).map((table)=>table.toLowerCase()));unrebuildable=[...corpusTables].filter((table)=>!emitted.has(table)).sort();groups=groups.map((group)=>({...group,statements:group.statements.filter((statement)=>{const table=statementTable(statement);return table?corpusTables.has(table.toLowerCase()):!1})})).filter((group)=>group.statements.length>0);if(groups.length===0)return err(Error(`None of the ${corpusTables.size} table(s) in ${dir} have a model behind them, so none can be regenerated. Declare the models, or publish the framework ones with \`buddy publish model <Name>\`.`))}const contents=new Map,contentOf=(file)=>{let content=contents.get(file);if(!content){try{content=describeMigrationContent(readFileSync(join(dir,file),"utf8"))}catch{content={types:[],droppedTypes:[],droppedIndexes:[],createdIndexes:[],dataTables:[],seedTables:[],createdTables:[],other:!0}}contents.set(file,content)}return content},namesNoTable=new Set(existing.filter((file)=>{try{return tablesOperatedOn(readFileSync(join(dir,file),"utf8")).length===0}catch{return!1}})),seedFiles=new Set(existing.filter((file)=>contentOf(file).seedTables.length>0)),seedRootedTables=options.replaceUnmarked&&!options.onlyExistingTables?[...seedFiles].flatMap((file)=>contentOf(file).createdTables):[],rootedTables=new Set(options.replaceUnmarked||options.onlyExistingTables?seedRootedTables:historicallyRootedTables(dir,existing)),outOfScopeByTable=new Set(migrationsOutsideCorpus(dir,existing,createdTablesOf(statements))),markerDeletable=(options.replaceUnmarked||options.onlyExistingTables?existing:existing.filter((file)=>isGeneratedMigration(dir,file))).filter((file)=>!isPackageMigration(file)),certainlyKept=existing.filter((file)=>!markerDeletable.includes(file)||seedFiles.has(file)||outOfScopeByTable.has(file)&&!namesNoTable.has(file)),emittedTypes=enumTypesCreatedBy(statements),keptIndexes=new Set(certainlyKept.flatMap((file)=>contentOf(file).createdIndexes)),obsolete=new Map;for(const file of namesNoTable){const keptSeedTables=new Set(certainlyKept.filter((kept)=>migrationOrdinal(kept)<migrationOrdinal(file)).flatMap((kept)=>contentOf(kept).seedTables)),reason=obsoleteMigrationReason(contentOf(file),{emittedTypes,keptIndexes,keptSeedTables});if(reason)obsolete.set(file,reason)}const outOfScope=new Set([...outOfScopeByTable].filter((file)=>!obsolete.has(file)&&!seedFiles.has(file))),removed=markerDeletable.filter((file)=>!seedFiles.has(file)).filter((file)=>{return!outOfScope.has(file)&&!migrationTouchesRootedTable(dir,file,rootedTables)}),preserved=existing.filter((file)=>!removed.includes(file)),preservedOutOfScope=preserved.filter((file)=>outOfScope.has(file)),preservedData=preserved.filter((file)=>seedFiles.has(file)),removedObsolete=removed.filter((file)=>obsolete.get(file)==="types"||obsolete.get(file)==="drop-index"),removedDataOnly=removed.filter((file)=>obsolete.get(file)==="data"),catchUp=[];for(const table of rootedTables){const create=groups.flatMap((group)=>group.statements).find((statement)=>statementTable(statement)===table&&/^\s*CREATE\s+TABLE\b/i.test(statement));if(!create)continue;const produced=columnsProducedByMigrations(dir,preserved,table);if(produced.size===0)continue;catchUp.push(...rootedTableCatchUpStatements(create,produced))}const writableGroups=groups.map((group)=>({...group,statements:group.statements.filter((statement)=>{const table=statementTable(statement);return!table||!rootedTables.has(table)})})).filter((group)=>group.statements.length>0).concat(catchUp.length>0?[{label:"alter-rooted-tables-columns",statements:catchUp}]:[]),writtenTables=new Set(createdTablesOf(writableGroups.flatMap((group)=>group.statements))),relocating=preservedData.filter((file)=>{const content=contentOf(file);return content.seedTables.some((table)=>writtenTables.has(table)&&!content.createdTables.includes(table))}),staying=preserved.filter((file)=>!relocating.includes(file)),historicalBoundary=existing.filter((file)=>!isGeneratedMigration(dir,file)&&!isPackageMigration(file)&&!relocating.includes(file)).reduce((max,file)=>Math.max(max,migrationOrdinal(file)),0),startAt=rootedTables.size>0?historicalBoundary+1:staying.filter((file)=>!isPackageMigration(file)).reduce((max,file)=>Math.max(max,migrationOrdinal(file)),0)+1,reservedOrdinals=new Set(staying.map(migrationOrdinal)),ordinals=allocateMigrationOrdinals(writableGroups.length+relocating.length,startAt,reservedOrdinals),files=writableGroups.map((group,index)=>({name:`${String(ordinals[index]).padStart(10,"0")}-${group.label}.sql`,statements:group.statements.length})),relocated=relocating.map((file,index)=>({from:file,to:`${String(ordinals[writableGroups.length+index]).padStart(10,"0")}-${file.replace(/^\d+[-_]/,"")}`})),corpus={dialect,models:sources.models.length,modelRoots:sources.roots,files,removed,removedObsolete,removedDataOnly,preserved,preservedOutOfScope,preservedData,relocated,unrebuildable,dir};if(options.dryRun)return ok(corpus);mkdirSync(dir,{recursive:!0});for(const file of removed)unlinkSync(join(dir,file));writableGroups.forEach((group,index)=>{const body=`${group.statements.map((s)=>s.trim().replace(/;\s*$/,"")).join(`;
32
+ `).execute(),names=(Array.isArray(raw)?raw:raw?.rows??[]).map((row)=>String(row.name??row.typname??"")).filter(Boolean);for(const name of names)try{await db.unsafe(`DROP TYPE IF EXISTS "${name}" CASCADE`).execute();log.debug(`Dropped orphaned enum type: ${name}`)}catch(error){log.warn(`Could not drop enum type ${name}: ${error instanceof Error?error.message:String(error)}`)}}catch(error){log.warn(`Could not list enum types to drop: ${error instanceof Error?error.message:String(error)}`)}}async function dropFrameworkTables(dialect){if(dialect==="mysql"||dialect==="vitess")try{await db.unsafe("SET FOREIGN_KEY_CHECKS = 0").execute()}catch(error){log.warn(`Could not disable foreign key checks: ${error instanceof Error?error.message:String(error)}`)}if(dialect==="sqlite")try{await db.unsafe("PRAGMA foreign_keys = OFF").execute()}catch(error){log.warn(`Could not disable foreign key checks: ${error instanceof Error?error.message:String(error)}`)}for(const tableName of FRAMEWORK_TABLES)try{let dropSql;if(dialect==="postgres")dropSql=`DROP TABLE IF EXISTS "${tableName}" CASCADE`;else if(dialect==="mysql"||dialect==="vitess")dropSql=`DROP TABLE IF EXISTS \`${tableName}\``;else dropSql=`DROP TABLE IF EXISTS "${tableName}"`;log.info(`Dropping framework table: ${tableName}`);await db.unsafe(dropSql).execute();log.info(`Dropped framework table: ${tableName}`)}catch(error){const kind=classifyConnectionError(error);if(kind==="missing-database"||kind==="missing-role"||kind==="auth-failed"||kind==="server-unreachable"||kind==="timeout")throw error;log.warn(`Could not drop table ${tableName}: ${error instanceof Error?error.message:String(error)}`)}if(dialect==="mysql"||dialect==="vitess")try{await db.unsafe("SET FOREIGN_KEY_CHECKS = 1").execute()}catch(error){log.warn(`Could not re-enable foreign key checks: ${error instanceof Error?error.message:String(error)}`)}if(dialect==="sqlite")try{await db.unsafe("PRAGMA foreign_keys = ON").execute()}catch(error){log.warn(`Could not re-enable foreign key checks: ${error instanceof Error?error.message:String(error)}`)}}function frameworkRenamesForThisApp(){return applicableFrameworkRenames(declaredModelTables())}function describeRenames(renames){return renames.map((rename)=>`${rename.id}, ${rename.reference}`).join("; ")}export function catchUpSnapshotFrameworkRenames(options){configureQueryBuilder();const dialect=getQbDialect(),stored=readStoredMigrationPlan(dialect);if(!stored)return{changes:[]};const renames=frameworkRenamesForThisApp();if(renames.length===0)return{changes:[]};const{plan,changes}=applyFrameworkRenamesToPlan(stored,renames);if(changes.length===0)return{changes};if(options.persist){saveMigrationSnapshot(plan,{dialect});log.info(`[migration] Updated the model snapshot for framework renames (${describeRenames(renames)}): ${changes.join(", ")}`)}else log.debug(`[migration] Previewing against the snapshot with framework renames applied: ${changes.join(", ")}`);return{changes,plan}}export async function catchUpFrameworkRenames(options={}){await ensureDatabaseConfigLoaded();const dialect=toSqlIntrospectionDialect(getDialect());if(dialect==="other")return{applied:[],skipped:[]};const renames=frameworkRenamesForThisApp();if(renames.length===0)return{applied:[],skipped:[]};const runner=options.runner??(async(sql)=>{const result=await db.unsafe(sql).execute();return Array.isArray(result)?result:[]}),result=await applyFrameworkRenamesToDatabase(runner,dialect,renames);if(result.applied.length>0)log.info(`[migration] Applied framework renames to the database (${describeRenames(renames)}): ${result.applied.join(", ")}`);for(const skipped of result.skipped)log.warn(`[migration] Framework rename not applied: ${skipped}`);return result}function resolveGenerateOptions(options){const applyRenames=options.applyRenames??(process.env.STACKS_MIGRATE_NO_RENAME==="1"?!1:void 0),fromDb=options.fromDb??(process.env.STACKS_MIGRATE_FROM_DB==="1"?!0:void 0);return{applyRenames,fromDb}}export async function previewPendingMigrations(options={}){try{return await pendingMigrationOperations(options)}catch(error){log.debug(`[migration] preview failed: ${error instanceof Error?error.message:String(error)}`);return[]}}export async function pendingMigrationOperations(options={}){await ensureDatabaseConfigLoaded();configureQueryBuilder();const dialect=getDialect(),{modelsDir,skip,protectedTables}=prepareMigrationModelsDir();if(skip)return[];const{applyRenames,fromDb}=resolveGenerateOptions(options),qbDialect=getQbDialect(),result=await qbGenerateMigration(modelsDir,{dialect:qbDialect,vitessSharded:qbDialect==="vitess"?isVitessSharded(dbConfig.connections.vitess.sharded):void 0,dryRun:!0,applyRenames,fromDb}),corrected=fromDb?void 0:catchUpSnapshotFrameworkRenames({persist:!1}).plan;if(corrected&&result.plan)result.operations=generateDiffOperations(corrected,result.plan,{applyRenames,dryRun:!0}).operations;const authOwned=new Set(AUTH_TABLES);let operations=(result.operations??[]).filter((op)=>!authOwned.has(op.table.toLowerCase()));if(protectedTables.length>0){const excluded=new Set(protectedTables.map((table)=>table.toLowerCase()));operations=operations.filter((op)=>!(op.kind==="drop_table"&&excluded.has(op.table.toLowerCase())))}if(!operations.some((op)=>op.kind==="drop_column"))return operations;return withoutManagedColumnDrops(operations,await frameworkManagedColumns())}function snapshotDirLabel(){return qbConfig?.snapshotDir||qbSnapshotDir()}function resolveSnapshotDir(){const label=snapshotDirLabel();return isAbsolute(label)?label:resolve(process.cwd(),label)}function snapshotPathFor(dialect){return join(resolveSnapshotDir(),`model-snapshot.${dialect}.json`)}function readStoredMigrationPlan(dialect){const snapshotPath=snapshotPathFor(dialect);if(!existsSync(snapshotPath))return;let parsed;try{parsed=JSON.parse(readFileSync(snapshotPath,"utf8"))}catch(error){throw Error(`The migration snapshot is not valid JSON: ${snapshotPath}. Repair or regenerate it before creating migrations.`,{cause:error})}const candidate=parsed&&typeof parsed==="object"&&"plan"in parsed?parsed.plan:parsed;if(!candidate||typeof candidate!=="object"||!Array.isArray(candidate.tables))throw TypeError(`The migration snapshot has an invalid structure: ${snapshotPath}. Repair or regenerate it before creating migrations.`);return candidate}export function preserveMigrationPlanTableOrder(next,previous){const orderedByPrevious=(values,previousValues,keyFor)=>{if(!previousValues)return[...values];const previousOrder=new Map(previousValues.map((value,index)=>[keyFor(value),index])),nextOrder=new Map(values.map((value,index)=>[keyFor(value),index])),previousCount=previousValues.length;return[...values].sort((left,right)=>{const leftKey=keyFor(left),rightKey=keyFor(right),leftOrder=previousOrder.get(leftKey)??previousCount+nextOrder.get(leftKey),rightOrder=previousOrder.get(rightKey)??previousCount+nextOrder.get(rightKey);return leftOrder-rightOrder})},previousTables=new Map(previous?.tables.map((table)=>[table.table,table])??[]),tables=next.tables.map((table)=>{const previousTable=previousTables.get(table.table),previousColumns=new Map(previousTable?.columns.map((column)=>[column.name,column])??[]);return{...table,columns:orderedByPrevious(table.columns.map((column)=>{if(next.dialect!=="sqlite"||!column.enumTypeName||previousColumns.get(column.name)?.enumTypeName)return column;const portableColumn={...column};delete portableColumn.enumTypeName;return portableColumn}),previousTable?.columns,(column)=>column.name),indexes:orderedByPrevious(table.indexes,previousTable?.indexes,(index)=>index.name)}});return{...next,tables:orderedByPrevious(tables,previous?.tables,(table)=>table.table)}}function detectSnapshotDialectMismatch(dialect){const qbDir=resolveSnapshotDir();let files;try{files=readdirSync(qbDir)}catch{return null}const snapshotFor=(d)=>`model-snapshot.${d}.json`;if(files.includes(snapshotFor(dialect)))return null;for(const f of files){const m=/^model-snapshot\.(\w+)\.json$/.exec(f);if(m?.[1]&&m[1]!==dialect)return m[1]}return null}export async function generateMigrations(options={}){await ensureDatabaseConfigLoaded();try{log.debug("Generating migrations...");configureQueryBuilder();const dialect=getDialect(),mismatch=detectSnapshotDialectMismatch(dialect),flatMigrationDir=join(process.cwd(),"database","migrations");if(mismatch&&migrationDirectory(dialect)===flatMigrationDir){const snapshotDir=snapshotDirLabel();return err(Error(`Refusing to generate migrations: resolved dialect "${dialect}" has no snapshot in ${snapshotDir}/, but "${mismatch}" does. DB_CONNECTION is likely unset or wrong (missing .env?) - generating now would write a full duplicate migration set in the wrong dialect. Set DB_CONNECTION=${mismatch} (or your intended dialect) and retry. To intentionally start a new dialect from scratch, remove ${snapshotDir}/model-snapshot.${mismatch}.json first.`))}catchUpSnapshotFrameworkRenames({persist:!0});const storedPlan=readStoredMigrationPlan(getQbDialect()),{modelsDir,skip,excludedTables,protectedTables}=prepareMigrationModelsDir();if(skip){trackIgnoredGeneratedMigrations(migrationDirectory());log.debug("No app/Models directory found; using committed framework migrations");return ok("Migrations generated")}const{applyRenames,fromDb}=resolveGenerateOptions(options);log.debug(`[migration] Generating migrations for dialect: ${dialect}, models: ${modelsDir}`);if(excludedTables.length>0)log.debug(`[migration] ${excludedTables.length} framework default model(s) out of scope because app/Models defines this app's schema. Enable database.models.includeFrameworkDefaults (or STACKS_INCLUDE_FRAMEWORK_MODELS=1) to generate them too.`);const qbDialect=getQbDialect(),result=await qbGenerateMigration(modelsDir,{dialect:qbDialect,vitessSharded:qbDialect==="vitess"?isVitessSharded(dbConfig.connections.vitess.sharded):void 0,dryRun:!0,applyRenames,fromDb});let sqlStatements=result.sqlStatements??[];if(dialect==="sqlite")sqlStatements=inlineSqliteAddedColumnReferences(sqlStatements,result.plan);{const runtimeOwned=withoutRuntimeOwnedTableSql(sqlStatements);if(runtimeOwned.removed.length>0)log.debug(`[migration] Left out ${runtimeOwned.removed.length} generated statement(s) for tables the auth layer creates itself`);sqlStatements=runtimeOwned.statements}if(result.hasChanges&&sqlStatements.length>0){const filtered=withoutManagedColumnDropSql(sqlStatements,await frameworkManagedColumns(),result.operations??[]);if(filtered.removed.length>0)log.debug(`[migration] Skipped ${filtered.removed.length} generated drop(s) of framework-managed column(s) (stacksjs/stacks#2075)`);sqlStatements=filtered.statements}if(result.hasChanges&&sqlStatements.length>0&&protectedTables.length>0){const filtered=withoutProtectedTableDropSql(sqlStatements,protectedTables,result.operations??[]);if(filtered.removed.length>0)log.info(`[migration] Left ${filtered.removed.length} framework-owned table(s) in place rather than dropping them. They are no longer generated because app/Models defines this app's schema; the tables and their data are untouched. Set database.models.includeFrameworkDefaults to keep generating them (stacksjs/stacks#2220).`);sqlStatements=filtered.statements}if(result.hasChanges&&sqlStatements.length>0){const dangling=findDanglingTypeReferences(sqlStatements);if(dangling.length>0){const created=createMissingEnumTypes(dangling,result.plan);if(created.statements.length>0){sqlStatements=[...created.statements,...sqlStatements];log.debug(`[migration] Created ${created.statements.length} enum type(s) an ALTER needed: ${created.defined.join(", ")}`)}const unresolved=dangling.filter((name)=>!created.defined.includes(name));if(unresolved.length>0){const before=sqlStatements.length;sqlStatements=sqlStatements.filter((statement)=>!referencesUndefinedType(statement,unresolved));log.warn(`[migration] Skipped ${before-sqlStatements.length} generated statement(s) referencing enum type(s) nothing creates and no model defines values for (${unresolved.slice(0,3).join(", ")}${unresolved.length>3?", \u2026":""}).`)}}}if(result.hasChanges&&sqlStatements.length>0&&!shadowDropsAllowed()){const shadowed=resolveModelSources()?.shadowed??[],drops=findShadowedColumnDrops(sqlStatements,shadowed);if(drops.length>0)return err(Error(shadowedDropMessage(drops)))}if(result.hasChanges){const written=persistGeneratedMigrations(sqlStatements,storedPlan,await claimedMigrationNames(migrationDirectory()));if(written>0)log.success(`Generated ${written} migration file${written===1?"":"s"}`);else log.debug("Migration generation produced no new files (already up to date)")}else log.debug("No changes detected");trackIgnoredGeneratedMigrations(migrationDirectory());const stablePlan=preserveMigrationPlanTableOrder(result.plan,storedPlan);if(!storedPlan||JSON.stringify(stablePlan)!==JSON.stringify(storedPlan))saveMigrationSnapshot(stablePlan,{dialect:getQbDialect()});else log.debug("Model snapshot unchanged");return ok("Migrations generated")}catch(error){const because=error instanceof Error?error.message:String(error);return err(handleError(`Migration generation failed: ${because}`,error))}}export function referencesUndefinedType(statement,dangling){return dangling.some((name)=>statement.includes(`"${name}"`))}export function findDanglingTypeReferences(statements){const defined=new Set,referenced=new Set;for(const statement of statements){for(const match of statement.matchAll(/CREATE\s+TYPE\s+"([^"]+)"/gi))defined.add(match[1]);for(const match of statement.matchAll(/\bTYPE\s+"([^"]+)"/gi))referenced.add(match[1])}return[...referenced].filter((name)=>!defined.has(name)).sort()}export function createMissingEnumTypes(dangling,plan){if(dangling.length===0)return{statements:[],defined:[]};const values=new Map;for(const table of plan?.tables??[]){if(!table.table)continue;for(const column of table.columns??[]){if(!column.name||!column.enumValues?.length)continue;values.set(`${table.table}_${column.name}_type`,column.enumValues)}}const statements=[],defined=[];for(const name of dangling){const members=values.get(name);if(!members?.length)continue;const literals=members.map((member)=>`'${String(member).replaceAll("'","''")}'`).join(", "),guarded=guardPostgresEnumTypes(`CREATE TYPE "${name}" AS ENUM (${literals})`);statements.push(guarded.endsWith(";")?guarded:`${guarded};`);defined.push(name)}return{statements,defined}}export function inlineSqliteAddedColumnReferences(statements,plan){const references=new Map;for(const table of plan?.tables??[]){if(!table.table)continue;for(const column of table.columns??[]){const reference=column.references;if(!column.name||!reference?.table||!reference.column)continue;references.set(`${table.table}.${column.name}`,{table:reference.table,column:reference.column})}}if(references.size===0)return statements;return statements.map((statement)=>{if(/\bREFERENCES\b/i.test(statement))return statement;const match=statement.match(/^(\s*ALTER\s+TABLE\s+["`]?(\w+)["`]?\s+ADD\s+COLUMN\s+["`]?(\w+)["`]?\s+)([\s\S]*?)(;?\s*)$/i),reference=match?.[2]&&match[3]?references.get(`${match[2]}.${match[3]}`):void 0;if(!match||!reference)return statement;return`${match[1]}${match[4].trimEnd()} REFERENCES "${reference.table}"("${reference.column}")${match[5]}`})}export{GENERATED_MIGRATION_MARKER,isGeneratedMigration}from"./generated-migration-marker";export function tablesOperatedOn(sql){const tables=new Set;for(const statement of sqlStatementsOf(sql)){const stmt=statement.trim(),direct=stmt.match(/^(?:CREATE\s+TABLE(?:\s+IF\s+NOT\s+EXISTS)?|ALTER\s+TABLE|DROP\s+TABLE(?:\s+IF\s+EXISTS)?|TRUNCATE\s+TABLE)\s+["'`]?(\w+)["'`]?/i);if(direct?.[1]){tables.add(direct[1].toLowerCase());continue}const index=stmt.match(/^CREATE\s+(?:UNIQUE\s+)?INDEX(?:\s+IF\s+NOT\s+EXISTS)?\s+\S+\s+ON\s+["'`]?(\w+)["'`]?/i);if(index?.[1])tables.add(index[1].toLowerCase())}return[...tables]}const IDENTIFIER=SQL_IDENTIFIER,NEUTRAL_STATEMENT=/^(?:BEGIN|COMMIT|END|PRAGMA|SET|SELECT\s+1\s*$|COMMENT\s+ON)\b/i;export function describeMigrationContent(sql){const content={types:[],droppedTypes:[],droppedIndexes:[],createdIndexes:[],dataTables:[],seedTables:[],createdTables:[],other:!1},add=(list,name)=>{const lower=name?.toLowerCase();if(lower&&!list.includes(lower))list.push(lower)},visit=(statement)=>{const stmt=statement.trim().replace(/;\s*$/,"");if(!stmt||NEUTRAL_STATEMENT.test(stmt))return;const guarded=guardedBlockStatements(stmt);if(guarded){for(const inner of guarded)visit(inner);return}let m=new RegExp(String.raw`^CREATE\s+TYPE\s+${IDENTIFIER}\s+AS\s+ENUM\b`,"i").exec(stmt)??new RegExp(String.raw`^ALTER\s+TYPE\s+${IDENTIFIER}\s+ADD\s+VALUE\b`,"i").exec(stmt);if(m){add(content.types,m[1]);return}m=new RegExp(String.raw`^DROP\s+TYPE\s+(?:IF\s+EXISTS\s+)?${IDENTIFIER}`,"i").exec(stmt);if(m){add(content.droppedTypes,m[1]);return}m=new RegExp(String.raw`^DROP\s+INDEX\s+(?:CONCURRENTLY\s+)?(?:IF\s+EXISTS\s+)?${IDENTIFIER}`,"i").exec(stmt);if(m){add(content.droppedIndexes,m[1]);return}m=new RegExp(String.raw`^(?:DELETE\s+FROM|UPDATE|TRUNCATE(?:\s+TABLE)?)\s+(?:ONLY\s+)?${IDENTIFIER}`,"i").exec(stmt);if(m){add(content.dataTables,m[1]);return}m=new RegExp(String.raw`^COPY\s+${IDENTIFIER}[\s\S]*\bFROM\b`,"i").exec(stmt);if(m){add(content.seedTables,m[1]);return}m=new RegExp(String.raw`^INSERT\s+(?:OR\s+\w+\s+)?INTO\s+${IDENTIFIER}`,"i").exec(stmt);if(m){if(/\bSELECT\b[\s\S]*\bFROM\b/i.test(stmt)&&!/\bVALUES\b/i.test(stmt))content.other=!0;else add(content.seedTables,m[1]);return}const shape=indexShape(stmt);if(shape){add(content.createdIndexes,shape.name);content.other=!0;return}const created=createdTableName(stmt);if(created)add(content.createdTables,created);content.other=!0};for(const statement of sqlStatementsOf(sql))visit(statement);return content}export function obsoleteMigrationReason(content,context){if(content.other||content.seedTables.length>0)return;const hasTypes=content.types.length>0||content.droppedTypes.length>0,hasDrops=content.droppedIndexes.length>0,hasData=content.dataTables.length>0;if([hasTypes,hasDrops,hasData].filter(Boolean).length!==1)return;if(hasTypes)return content.types.every((type)=>context.emittedTypes.has(type))?"types":void 0;if(hasDrops)return content.droppedIndexes.every((index)=>!context.keptIndexes.has(index))?"drop-index":void 0;return content.dataTables.every((table)=>!context.keptSeedTables.has(table))?"data":void 0}export function columnsDefinedByCreate(statement){const body=statement.match(/^\s*CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["'`]?\w+["'`]?\s*\(([\s\S]*)\)\s*;?\s*$/i)?.[1];if(!body)return[];const parts=[];let depth=0,current="";for(const char of body){if(char==="(")depth++;if(char===")")depth--;if(char===","&&depth===0){parts.push(current);current="";continue}current+=char}parts.push(current);const constraint=/^\s*(?:PRIMARY\s+KEY|FOREIGN\s+KEY|UNIQUE|CHECK|CONSTRAINT|KEY|INDEX)\b/i;return parts.map((part)=>part.trim()).filter((part)=>part&&!constraint.test(part)).flatMap((part)=>part.match(/^["'`]?(\w+)["'`]?/)?.[1]??[])}export function columnsProducedByMigrations(dir,files,table){const columns=new Set,target=table.toLowerCase();for(const file of[...files].sort()){let content;try{content=readFileSync(join(dir,file),"utf8")}catch{continue}for(const statement of sqlStatementsOf(content)){const stmt=statement.trim();if(stmt.match(/^CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["'`]?(\w+)["'`]?/i)?.[1]?.toLowerCase()===target){for(const column of columnsDefinedByCreate(stmt))columns.add(column.toLowerCase());continue}const added=stmt.match(/^ALTER\s+TABLE\s+["'`]?(\w+)["'`]?\s+ADD\s+(?:COLUMN\s+)?["'`]?(\w+)["'`]?/i);if(added?.[1]?.toLowerCase()===target&&added[2])columns.add(added[2].toLowerCase());const dropped=stmt.match(/^ALTER\s+TABLE\s+["'`]?(\w+)["'`]?\s+DROP\s+(?:COLUMN\s+)?["'`]?(\w+)["'`]?/i);if(dropped?.[1]?.toLowerCase()===target&&dropped[2])columns.delete(dropped[2].toLowerCase());const rebuilt=stmt.match(/^ALTER\s+TABLE\s+["'`]?_qb_tmp_(\w+)["'`]?\s+RENAME\s+TO\s+["'`]?(\w+)["'`]?/i);if(rebuilt?.[2]?.toLowerCase()===target){const temp=sqlStatementsOf(content).find((s)=>new RegExp(`^CREATE\\s+TABLE\\s+["'\`]?_qb_tmp_${rebuilt[1]}["'\`]?`,"i").test(s.trim()));if(temp){columns.clear();for(const column of columnsDefinedByCreate(temp))columns.add(column.toLowerCase())}}}}return columns}export function rootedTableCatchUpStatements(createStatement,existingColumns){const table=createStatement.match(/^\s*CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["'`]?(\w+)["'`]?/i)?.[1];if(!table)return[];const body=createStatement.match(/\(([\s\S]*)\)\s*;?\s*$/)?.[1];if(!body)return[];const definitions=new Map;let depth=0,current="";const parts=[];for(const char of body){if(char==="(")depth++;if(char===")")depth--;if(char===","&&depth===0){parts.push(current);current="";continue}current+=char}parts.push(current);const constraint=/^\s*(?:PRIMARY\s+KEY|FOREIGN\s+KEY|UNIQUE|CHECK|CONSTRAINT|KEY|INDEX)\b/i;for(const raw of parts){const part=raw.trim();if(!part||constraint.test(part))continue;const name=part.match(/^["'`]?(\w+)["'`]?/)?.[1];if(name)definitions.set(name.toLowerCase(),part)}const statements=[];for(const[name,definition]of definitions){if(existingColumns.has(name))continue;if(/\b(?:PRIMARY\s+KEY|UNIQUE|AUTOINCREMENT)\b/i.test(definition))continue;const nullable=/\bNOT\s+NULL\b/i.test(definition)&&!/\bDEFAULT\b/i.test(definition)?definition.replace(/\s*\bNOT\s+NULL\b/i,""):definition;statements.push(`ALTER TABLE "${table}" ADD COLUMN ${nullable.trim()}`)}return statements}export function createdTablesOf(statements){const tables=new Set;for(const statement of statements){const match=statement.trim().match(/^CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["'`]?(\w+)["'`]?/i);if(match?.[1])tables.add(match[1].toLowerCase())}return[...tables]}export function historicallyRootedTables(dir,files){const tables=new Set;for(const file of files){if(isGeneratedMigration(dir,file))continue;try{for(const statement of sqlStatementsOf(readFileSync(join(dir,file),"utf8"))){const match=statement.match(/^\s*CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["'`]?(\w+)["'`]?/i);if(match?.[1])tables.add(match[1].toLowerCase())}}catch{}}return[...tables]}export function tablesDefinedByCorpus(dir,files){const tables=new Set;for(const file of files)try{for(const statement of sqlStatementsOf(readFileSync(join(dir,file),"utf8"))){const match=statement.match(/^\s*CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["'`]?(\w+)["'`]?/i);if(match?.[1])tables.add(match[1].toLowerCase())}}catch{}return[...tables]}export function migrationTouchesRootedTable(dir,file,rootedTables){try{return tablesOperatedOn(readFileSync(join(dir,file),"utf8")).some((table)=>rootedTables.has(table.toLowerCase()))}catch{return!1}}export function allocateMigrationOrdinals(count,startAt,reserved){const ordinals=[];let cursor=startAt;while(ordinals.length<count){if(!reserved.has(cursor))ordinals.push(cursor);cursor+=1}return ordinals}export function migrationsOutsideCorpus(dir,files,corpusTables){const rebuilt=new Set(corpusTables.map((table)=>table.toLowerCase()));return files.filter((file)=>{let contents;try{contents=readFileSync(join(dir,file),"utf8")}catch{return!0}const touched=tablesOperatedOn(contents);if(touched.length===0)return!0;return touched.some((table)=>!rebuilt.has(table))})}function migrationOrdinal(file){const match=file.match(/^(\d+)/);return match?Number(match[1]):0}export async function generateFullModelMigration(options={}){await ensureDatabaseConfigLoaded();try{const dialect=options.dialect??getQbDialect();let requestedVitessSharded;if(dialect==="vitess")try{const{config,overridesReady}=await import("@stacksjs/config");await overridesReady;requestedVitessSharded=isVitessSharded(config?.database?.connections?.vitess?.sharded)}catch{requestedVitessSharded=isVitessSharded(dbConfig.connections.vitess.sharded)}configureQueryBuilder(dialect,requestedVitessSharded);const sources=resolveModelSources({forceStage:!0,...options.onlyExistingTables?{includeFrameworkDefaults:!0}:{}});if(!sources)return err(Error("No models found. Define models in app/Models, or ensure the framework defaults at storage/framework/defaults/app/Models are present."));try{writeFileSync(join(sources.dir,`.qb-migrations.${dialect}.json`),JSON.stringify({plan:{dialect,tables:[]}}))}catch{}const snapshotPath=join(resolveSnapshotDir(),`model-snapshot.${dialect}.json`),parkedSnapshot=`${snapshotPath}.regenerating`;let snapshotParked=!1;if(existsSync(snapshotPath))try{renameSync(snapshotPath,parkedSnapshot);snapshotParked=!0}catch{}let result;try{result=await qbGenerateMigration(sources.dir,{dialect,vitessSharded:requestedVitessSharded,dryRun:!0,full:!0})}finally{if(snapshotParked)try{renameSync(parkedSnapshot,snapshotPath)}catch{}}const statements=result.sqlStatements??[];if(statements.length===0)return err(Error(`The generator produced no SQL for dialect "${dialect}".`));const dangling=findDanglingTypeReferences(statements);if(dangling.length>0)return err(Error(`The generator emitted ${dangling.length} reference(s) to enum type(s) it never creates: ${dangling.slice(0,5).join(", ")}${dangling.length>5?`, +${dangling.length-5} more`:""}. Writing this corpus would fail partway through a migration. This is a bug in the migration generator, not in your models.`));return ok({dialect,statements,plan:result.plan,sources})}catch(error){return err(handleError("Migration generation failed",error))}}export async function regenerateMigrationCorpus(options={}){try{const generated=await generateFullModelMigration({dialect:options.dialect,onlyExistingTables:options.onlyExistingTables});if(!generated.isOk)return err(generated.error);const{dialect,statements,plan,sources}=generated.value,dir=options.dir??migrationDirectory(dialect);let groups=groupGeneratedStatements(statements),existing=[];try{existing=readdirSync(dir).filter((f)=>f.endsWith(".sql")).sort()}catch{}let unrebuildable=[];if(options.onlyExistingTables){const corpusTables=new Set(tablesDefinedByCorpus(dir,existing));if(corpusTables.size===0)return err(Error(`No CREATE TABLE statements found in ${dir}, so there is nothing to regenerate in place. Run \`buddy migrate:regenerate <dialect>\` without --only-existing-tables to write a corpus from your models.`));const emitted=new Set(createdTablesOf(statements).map((table)=>table.toLowerCase()));unrebuildable=[...corpusTables].filter((table)=>!emitted.has(table)).sort();groups=groups.map((group)=>({...group,statements:group.statements.filter((statement)=>{const table=statementTable(statement);return table?corpusTables.has(table.toLowerCase()):!1})})).filter((group)=>group.statements.length>0);if(groups.length===0)return err(Error(`None of the ${corpusTables.size} table(s) in ${dir} have a model behind them, so none can be regenerated. Declare the models, or publish the framework ones with \`buddy publish model <Name>\`.`))}const contents=new Map,contentOf=(file)=>{let content=contents.get(file);if(!content){try{content=describeMigrationContent(readFileSync(join(dir,file),"utf8"))}catch{content={types:[],droppedTypes:[],droppedIndexes:[],createdIndexes:[],dataTables:[],seedTables:[],createdTables:[],other:!0}}contents.set(file,content)}return content},namesNoTable=new Set(existing.filter((file)=>{try{return tablesOperatedOn(readFileSync(join(dir,file),"utf8")).length===0}catch{return!1}})),seedFiles=new Set(existing.filter((file)=>contentOf(file).seedTables.length>0)),seedRootedTables=options.replaceUnmarked&&!options.onlyExistingTables?[...seedFiles].flatMap((file)=>contentOf(file).createdTables):[],rootedTables=new Set(options.replaceUnmarked||options.onlyExistingTables?seedRootedTables:historicallyRootedTables(dir,existing)),outOfScopeByTable=new Set(migrationsOutsideCorpus(dir,existing,createdTablesOf(statements))),markerDeletable=(options.replaceUnmarked||options.onlyExistingTables?existing:existing.filter((file)=>isGeneratedMigration(dir,file))).filter((file)=>!isPackageMigration(file)),certainlyKept=existing.filter((file)=>!markerDeletable.includes(file)||seedFiles.has(file)||outOfScopeByTable.has(file)&&!namesNoTable.has(file)),emittedTypes=enumTypesCreatedBy(statements),keptIndexes=new Set(certainlyKept.flatMap((file)=>contentOf(file).createdIndexes)),obsolete=new Map;for(const file of namesNoTable){const keptSeedTables=new Set(certainlyKept.filter((kept)=>migrationOrdinal(kept)<migrationOrdinal(file)).flatMap((kept)=>contentOf(kept).seedTables)),reason=obsoleteMigrationReason(contentOf(file),{emittedTypes,keptIndexes,keptSeedTables});if(reason)obsolete.set(file,reason)}const outOfScope=new Set([...outOfScopeByTable].filter((file)=>!obsolete.has(file)&&!seedFiles.has(file))),removed=markerDeletable.filter((file)=>!seedFiles.has(file)).filter((file)=>{return!outOfScope.has(file)&&!migrationTouchesRootedTable(dir,file,rootedTables)}),preserved=existing.filter((file)=>!removed.includes(file)),preservedOutOfScope=preserved.filter((file)=>outOfScope.has(file)),preservedData=preserved.filter((file)=>seedFiles.has(file)),removedObsolete=removed.filter((file)=>obsolete.get(file)==="types"||obsolete.get(file)==="drop-index"),removedDataOnly=removed.filter((file)=>obsolete.get(file)==="data"),catchUp=[];for(const table of rootedTables){const create=groups.flatMap((group)=>group.statements).find((statement)=>statementTable(statement)===table&&/^\s*CREATE\s+TABLE\b/i.test(statement));if(!create)continue;const produced=columnsProducedByMigrations(dir,preserved,table);if(produced.size===0)continue;catchUp.push(...rootedTableCatchUpStatements(create,produced))}const writableGroups=groups.map((group)=>({...group,statements:group.statements.filter((statement)=>{const table=statementTable(statement);return!table||!rootedTables.has(table)})})).filter((group)=>group.statements.length>0).concat(catchUp.length>0?[{label:"alter-rooted-tables-columns",statements:catchUp}]:[]),writtenTables=new Set(createdTablesOf(writableGroups.flatMap((group)=>group.statements))),relocating=preservedData.filter((file)=>{const content=contentOf(file);return content.seedTables.some((table)=>writtenTables.has(table)&&!content.createdTables.includes(table))}),staying=preserved.filter((file)=>!relocating.includes(file)),historicalBoundary=existing.filter((file)=>!isGeneratedMigration(dir,file)&&!isPackageMigration(file)&&!relocating.includes(file)).reduce((max,file)=>Math.max(max,migrationOrdinal(file)),0),startAt=rootedTables.size>0?historicalBoundary+1:staying.filter((file)=>!isPackageMigration(file)).reduce((max,file)=>Math.max(max,migrationOrdinal(file)),0)+1,reservedOrdinals=new Set(staying.map(migrationOrdinal)),ordinals=allocateMigrationOrdinals(writableGroups.length+relocating.length,startAt,reservedOrdinals),files=writableGroups.map((group,index)=>({name:`${String(ordinals[index]).padStart(10,"0")}-${group.label}.sql`,statements:group.statements.length})),relocated=relocating.map((file,index)=>({from:file,to:`${String(ordinals[writableGroups.length+index]).padStart(10,"0")}-${file.replace(/^\d+[-_]/,"")}`})),corpus={dialect,models:sources.models.length,modelRoots:sources.roots,files,removed,removedObsolete,removedDataOnly,preserved,preservedOutOfScope,preservedData,relocated,unrebuildable,dir};if(options.dryRun)return ok(corpus);mkdirSync(dir,{recursive:!0});for(const file of removed)unlinkSync(join(dir,file));writableGroups.forEach((group,index)=>{const body=`${group.statements.map((s)=>s.trim().replace(/;\s*$/,"")).join(`;
33
33
  `)};
34
34
  `,content=`${GENERATED_MIGRATION_MARKER}
35
35
  ${body}`;writeFileSync(join(dir,files[index].name),(options.dialect??getDialect())==="postgres"?normalizePostgresMigrationSql(content):content)});for(const{from,to}of relocated)renameSync(join(dir,from),join(dir,to));trackIgnoredGeneratedMigrations(dir);saveMigrationSnapshot(plan,{dialect});return ok(corpus)}catch(error){return err(handleError("Migration regeneration failed",error))}}export function createdTableName(statement){return/^\s*CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["'`[]?([\w$]+)["'`\]]?/i.exec(statement)?.[1]?.toLowerCase()}export{indexShape};export function droppedIndexName(statement){return/^\s*DROP\s+INDEX\s+(?:IF\s+EXISTS\s+)?(?:["'`[]?[\w$]+["'`\]]?\.)?["'`[]?([\w$]+)["'`\]]?/i.exec(statement)?.[1]?.toLowerCase()}export function indexCommittedMigrations(fileContents){const createdTables=new Set,indexes=new Map;for(const contents of fileContents)for(const statement of sqlStatementsOf(contents)){const table=createdTableName(statement);if(table){createdTables.add(table);continue}const shape=indexShape(statement);if(shape){if(!indexes.has(shape.name))indexes.set(shape.name,shape.key);continue}const dropped=droppedIndexName(statement);if(dropped)indexes.delete(dropped)}return{sql:normalizeSqlForComparison(fileContents.join(`
36
- `)),createdTables,indexShapes:new Set(indexes.values())}}function normalizeSqlForComparison(sql){return sql.replace(/\s+/g," ").trim()}export function generatedStatementIsRedundant(statement,index){const table=createdTableName(statement);if(table)return index.createdTables.has(table);const shape=indexShape(statement);if(shape&&index.indexShapes.has(shape.key))return!0;return index.sql.includes(normalizeSqlForComparison(statement))}export function persistedGroupLabel(label,statements){const table=/^create-(\w+)-table$/.exec(label)?.[1];if(!table||statements.some((statement)=>createdTableName(statement)))return label;const only=statements.length===1?indexShape(statements[0]):void 0;return only?`create-${only.name}-index-in-${table}`:`create-${table}-indexes`}function persistGeneratedMigrations(sqlStatements,previousPlan){if(!sqlStatements?.length)return 0;const migrationsDir=migrationDirectory();try{require("node:fs").mkdirSync(migrationsDir,{recursive:!0})}catch{}const committed=[];try{for(const f of readdirSync(migrationsDir).filter((f)=>f.endsWith(".sql")).sort())committed.push(readFileSync(join(migrationsDir,f),"utf8"))}catch{}const index=indexCommittedMigrations(committed),groups=groupGeneratedStatements(sqlStatements,previousPlan);let written=0,cursor=nextMigrationNumber(migrationsDir);for(const group of groups){const fresh=group.statements.filter((stmt)=>{if(!generatedStatementIsRedundant(stmt,index))return!0;const table=createdTableName(stmt);if(table)log.debug(`[migration] Skipping generated CREATE for "${table}" - the committed corpus already creates it.`);const shape=indexShape(stmt);if(shape)log.debug(`[migration] Skipping generated index "${shape.name}" - the committed corpus already builds the same index on "${shape.table}".`);return!1});if(fresh.length===0)continue;const filename=`${String(cursor).padStart(10,"0")}-${persistedGroupLabel(group.label,fresh)}.sql`,filePath=join(migrationsDir,filename),body=`${fresh.map((s)=>s.trim().replace(/;\s*$/,"")).join(`;
36
+ `)),createdTables,indexShapes:new Set(indexes.values())}}function normalizeSqlForComparison(sql){return sql.replace(/\s+/g," ").trim()}export function generatedStatementIsRedundant(statement,index){const table=createdTableName(statement);if(table)return index.createdTables.has(table);const shape=indexShape(statement);if(shape&&index.indexShapes.has(shape.key))return!0;return index.sql.includes(normalizeSqlForComparison(statement))}export function persistedGroupLabel(label,statements){const table=/^create-(\w+)-table$/.exec(label)?.[1];if(!table||statements.some((statement)=>createdTableName(statement)))return label;const only=statements.length===1?indexShape(statements[0]):void 0;return only?`create-${only.name}-index-in-${table}`:`create-${table}-indexes`}export async function claimedMigrationNames(dir=migrationDirectory()){const ledger=await ledgerNamesForNumbering();if(ledger===null)log.debug("[migration] Could not read the migrations table; numbering from the corpus and its git history");return[...new Set([...ledger??[],...retiredMigrationNames(dir)])].sort()}async function ledgerNamesForNumbering(timeoutMs=5000){try{if(getDialect()==="sqlite"&&!usesLibsql()){const file=sqliteDatabasePath();if(file.includes(":memory:")||!existsSync(file))return null}}catch{return null}let timer;try{const query=Promise.resolve(db.unsafe("SELECT migration FROM migrations").execute()),timeout=new Promise((done)=>{timer=setTimeout(()=>done("timeout"),timeoutMs)}),rows=await Promise.race([query,timeout]);if(rows==="timeout"){query.catch(()=>{});return null}return(Array.isArray(rows)?rows:[]).map((row)=>String(row.migration??"")).filter((name)=>name.length>0)}catch(error){const message=error instanceof Error?error.message:String(error);return/no such table|does not exist|doesn't exist/i.test(message)?[]:null}finally{if(timer)clearTimeout(timer)}}export function persistGeneratedMigrations(sqlStatements,previousPlan,claimed=[]){if(!sqlStatements?.length)return 0;const migrationsDir=migrationDirectory();try{require("node:fs").mkdirSync(migrationsDir,{recursive:!0})}catch{}const committed=[];try{for(const f of readdirSync(migrationsDir).filter((f)=>f.endsWith(".sql")).sort())committed.push(readFileSync(join(migrationsDir,f),"utf8"))}catch{}const index=indexCommittedMigrations(committed),groups=groupGeneratedStatements(sqlStatements,previousPlan);let written=0,diskFiles=[];try{diskFiles=readdirSync(migrationsDir)}catch{}const reserved=reservedMigrationOrdinals(claimed),taken=new Set([...diskFiles,...claimed]);let cursor=nextMigrationOrdinal(diskFiles,claimed);for(const group of groups){const fresh=group.statements.filter((stmt)=>{if(!generatedStatementIsRedundant(stmt,index))return!0;const table=createdTableName(stmt);if(table)log.debug(`[migration] Skipping generated CREATE for "${table}" - the committed corpus already creates it.`);const shape=indexShape(stmt);if(shape)log.debug(`[migration] Skipping generated index "${shape.name}" - the committed corpus already builds the same index on "${shape.table}".`);return!1});if(fresh.length===0)continue;const label=persistedGroupLabel(group.label,fresh);while(taken.has(`${String(cursor).padStart(10,"0")}-${label}.sql`))cursor=nextFreeMigrationOrdinal(cursor+1,reserved);const filename=`${String(cursor).padStart(10,"0")}-${label}.sql`,filePath=join(migrationsDir,filename),body=`${fresh.map((s)=>s.trim().replace(/;\s*$/,"")).join(`;
37
37
  `)};
38
38
  `,content=`${GENERATED_MIGRATION_MARKER}
39
- ${body}`;writeFileSync(filePath,getDialect()==="postgres"?normalizePostgresMigrationSql(content):content);log.debug(`[migration] Wrote ${filename} (${fresh.length} stmt${fresh.length===1?"":"s"})`);written+=1;cursor+=1}return written}function normalizeCreateStatements(sqlStatements){const creates=[],constraints=[],passthrough=[];for(const raw of sqlStatements){const statement=raw.trim();if(!statement)continue;const create=statement.match(/^CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["`]?(\w+)["`]?/i);if(create?.[1]){creates.push({statement,table:create[1]});continue}const constraint=statement.match(/^ALTER\s+TABLE\s+["`]?(\w+)["`]?\s+ADD\s+CONSTRAINT\s+([\s\S]+?);?$/i);if(constraint?.[1]&&constraint[2]){const references=[...constraint[2].matchAll(/REFERENCES\s+["`]?(\w+)["`]?/gi)].flatMap((match)=>match[1]?[match[1]]:[]);constraints.push({body:`CONSTRAINT ${constraint[2].replace(/;\s*$/,"")}`,references,statement,table:constraint[1]});continue}passthrough.push(statement)}if(creates.length===0)return sqlStatements.map((statement)=>statement.trim()).filter(Boolean);const createdTables=new Set(creates.map((create)=>create.table)),createOrder=new Map(creates.map((create,index)=>[create.table,index])),relevantConstraints=constraints.filter((constraint)=>createdTables.has(constraint.table)),unrelatedConstraints=constraints.filter((constraint)=>!createdTables.has(constraint.table)),dependencies=new Map(creates.map((create)=>[create.table,new Set(relevantConstraints.filter((constraint)=>constraint.table===create.table).flatMap((constraint)=>constraint.references).filter((reference)=>reference!==create.table&&createdTables.has(reference)))])),sortTables=(ignoredEdges=new Set)=>{const remaining=new Set(createdTables),sorted=[];while(remaining.size>0){const ready=[...remaining].filter((table)=>[...dependencies.get(table)??[]].every((dependency)=>{return!remaining.has(dependency)||ignoredEdges.has(`${table}->${dependency}`)})).sort((a,b)=>(createOrder.get(a)??0)-(createOrder.get(b)??0));if(ready.length===0)break;for(const table of ready){remaining.delete(table);sorted.push(table)}}return sorted},initiallySorted=sortTables(),cyclicTables=new Set([...createdTables].filter((table)=>!initiallySorted.includes(table))),deferred=relevantConstraints.filter((constraint)=>constraint.references.some((reference)=>{return reference!==constraint.table&&cyclicTables.has(constraint.table)&&cyclicTables.has(reference)})),deferredStatements=new Set(deferred.map((constraint)=>constraint.statement)),ignoredEdges=new Set(deferred.flatMap((constraint)=>constraint.references.map((reference)=>`${constraint.table}->${reference}`))),orderedTables=sortTables(ignoredEdges),byTable=new Map(creates.map((create)=>[create.table,create]));return[...orderedTables.map((table)=>{const create=byTable.get(table),inline=relevantConstraints.filter((constraint)=>constraint.table===table&&!deferredStatements.has(constraint.statement));if(inline.length===0)return create.statement;const closing=create.statement.lastIndexOf(")");if(closing<0)return create.statement;const before=create.statement.slice(0,closing).trimEnd(),after=create.statement.slice(closing);return`${before},
39
+ ${body}`;writeFileSync(filePath,getDialect()==="postgres"?normalizePostgresMigrationSql(content):content);log.debug(`[migration] Wrote ${filename} (${fresh.length} stmt${fresh.length===1?"":"s"})`);taken.add(filename);written+=1;cursor=nextFreeMigrationOrdinal(cursor+1,reserved)}return written}function normalizeCreateStatements(sqlStatements){const creates=[],constraints=[],passthrough=[];for(const raw of sqlStatements){const statement=raw.trim();if(!statement)continue;const create=statement.match(/^CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["`]?(\w+)["`]?/i);if(create?.[1]){creates.push({statement,table:create[1]});continue}const constraint=statement.match(/^ALTER\s+TABLE\s+["`]?(\w+)["`]?\s+ADD\s+CONSTRAINT\s+([\s\S]+?);?$/i);if(constraint?.[1]&&constraint[2]){const references=[...constraint[2].matchAll(/REFERENCES\s+["`]?(\w+)["`]?/gi)].flatMap((match)=>match[1]?[match[1]]:[]);constraints.push({body:`CONSTRAINT ${constraint[2].replace(/;\s*$/,"")}`,references,statement,table:constraint[1]});continue}passthrough.push(statement)}if(creates.length===0)return sqlStatements.map((statement)=>statement.trim()).filter(Boolean);const createdTables=new Set(creates.map((create)=>create.table)),createOrder=new Map(creates.map((create,index)=>[create.table,index])),relevantConstraints=constraints.filter((constraint)=>createdTables.has(constraint.table)),unrelatedConstraints=constraints.filter((constraint)=>!createdTables.has(constraint.table)),dependencies=new Map(creates.map((create)=>[create.table,new Set(relevantConstraints.filter((constraint)=>constraint.table===create.table).flatMap((constraint)=>constraint.references).filter((reference)=>reference!==create.table&&createdTables.has(reference)))])),sortTables=(ignoredEdges=new Set)=>{const remaining=new Set(createdTables),sorted=[];while(remaining.size>0){const ready=[...remaining].filter((table)=>[...dependencies.get(table)??[]].every((dependency)=>{return!remaining.has(dependency)||ignoredEdges.has(`${table}->${dependency}`)})).sort((a,b)=>(createOrder.get(a)??0)-(createOrder.get(b)??0));if(ready.length===0)break;for(const table of ready){remaining.delete(table);sorted.push(table)}}return sorted},initiallySorted=sortTables(),cyclicTables=new Set([...createdTables].filter((table)=>!initiallySorted.includes(table))),deferred=relevantConstraints.filter((constraint)=>constraint.references.some((reference)=>{return reference!==constraint.table&&cyclicTables.has(constraint.table)&&cyclicTables.has(reference)})),deferredStatements=new Set(deferred.map((constraint)=>constraint.statement)),ignoredEdges=new Set(deferred.flatMap((constraint)=>constraint.references.map((reference)=>`${constraint.table}->${reference}`))),orderedTables=sortTables(ignoredEdges),byTable=new Map(creates.map((create)=>[create.table,create]));return[...orderedTables.map((table)=>{const create=byTable.get(table),inline=relevantConstraints.filter((constraint)=>constraint.table===table&&!deferredStatements.has(constraint.statement));if(inline.length===0)return create.statement;const closing=create.statement.lastIndexOf(")");if(closing<0)return create.statement;const before=create.statement.slice(0,closing).trimEnd(),after=create.statement.slice(closing);return`${before},
40
40
  ${inline.map((constraint)=>constraint.body).join(`,
41
41
  `)}
42
- ${after}`}),...passthrough,...unrelatedConstraints.map((constraint)=>constraint.statement),...deferred.map((constraint)=>constraint.statement)]}export function groupGeneratedStatements(sqlStatements,previousPlan){const normalizedStatements=normalizeCreateStatements(sqlStatements),groups=new Map,push=(label,stmt)=>{const list=groups.get(label)??[];list.push(stmt);groups.set(label,list)},createdTables=new Set(normalizedStatements.flatMap((raw)=>{const match=raw.trim().match(/^\s*CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["`]?(\w+)["`]?/i);return match?.[1]?[match[1]]:[]}));for(const raw of normalizedStatements){const stmt=raw.trim();if(!stmt)continue;const create=stmt.match(/^\s*CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["`]?(\w+)["`]?/i);if(create){push(`create-${create[1]}-table`,stmt);continue}if(stmt.match(/^\s*CREATE\s+TYPE\s+/i)){push("create-database-types",stmt);continue}if(/^\s*ALTER\s+TYPE\s+\S+\s+ADD\s+VALUE\b/i.test(stmt)){push("add-enum-values",stmt);continue}const alter=stmt.match(/^\s*ALTER\s+TABLE\s+["`]?(\w+)["`]?\s+(?:ADD\s+COLUMN\s+["`]?(\w+)["`]?|DROP\s+COLUMN\s+["`]?(\w+)["`]?|ADD\s+CONSTRAINT)/i),alterTable=alter?.[1];if(alter&&alterTable){const isCreateTimeConstraint=createdTables.has(alterTable)&&!alter[2]&&!alter[3];push(isCreateTimeConstraint?"create-foreign-key-constraints":`alter-${alterTable}-columns`,stmt);continue}const idx=stmt.match(/^\s*CREATE\s+(?:UNIQUE\s+)?INDEX\s+(?:IF\s+NOT\s+EXISTS\s+)?["`]?(\w+)["`]?\s+ON\s+["`]?(\w+)["`]?/i),idxName=idx?.[1],idxTable=idx?.[2];if(idxName&&idxTable){push(createdTables.has(idxTable)?`create-${idxTable}-table`:`create-${idxName}-index-in-${idxTable}`,stmt);continue}const drop=stmt.match(/^\s*DROP\s+TABLE\s+(?:IF\s+EXISTS\s+)?["`]?(\w+)["`]?/i);if(drop){push(`drop-${drop[1]}-table`,stmt);continue}push("auto-misc",stmt)}const priority=(label)=>label==="create-database-types"?0:label==="add-enum-values"?1:2,ordered=[...groups.entries()].map(([label,statements])=>({label,statements})).sort((a,b)=>priority(a.label)-priority(b.label));return withTableDropsLast(ordered,previousPlan)}const DROP_TABLE_GROUP_RE=/^drop-(\w+)-table$/;export function withTableDropsLast(groups,previousPlan){const dropOf=(group)=>group.label.match(DROP_TABLE_GROUP_RE)?.[1],recreated=new Set(groups.flatMap((group)=>group.statements.flatMap((statement)=>{const table=createdTableName(statement);return table?[table]:[]}))),movable=groups.filter((group)=>{const table=dropOf(group);return table!==void 0&&!recreated.has(table.toLowerCase())});if(movable.length===0)return groups;const rest=groups.filter((group)=>!movable.includes(group)),drops=[...movable].reverse(),references=new Map;for(const table of previousPlan?.tables??[])references.set(table.table,new Set(table.columns.flatMap((column)=>column.references?.table&&column.references.table!==table.table?[column.references.table]:[])));const sorted=[],visiting=new Set,visit=(group)=>{if(sorted.includes(group)||visiting.has(group))return;visiting.add(group);for(const other of drops)if(other!==group&&references.get(dropOf(other))?.has(dropOf(group)))visit(other);visiting.delete(group);sorted.push(group)};for(const group of drops)visit(group);return[...rest,...sorted]}function nextMigrationNumber(migrationsDir){let max=0;try{for(const f of readdirSync(migrationsDir)){if(isPackageMigration(f))continue;const m=f.match(/^(\d+)-/);if(m?.[1])max=Math.max(max,Number.parseInt(m[1],10))}}catch{}return max+1}export async function generateMigrations2(){await ensureDatabaseConfigLoaded();try{log.info("Generating fresh migrations...");configureQueryBuilder();const dialect=getDialect(),{modelsDir,skip}=prepareMigrationModelsDir();if(skip){log.info("No app/Models directory found; using committed framework migrations");return ok("Migrations generated")}const qbDialect=getQbDialect();await qbGenerateMigration(modelsDir,{dialect:qbDialect,vitessSharded:qbDialect==="vitess"?isVitessSharded(dbConfig.connections.vitess.sharded):void 0,full:!0,dryRun:!0});log.success("Migrations generated");return ok("Migrations generated")}catch(error){return err(handleError("Fresh migration generation failed",error))}}
42
+ ${after}`}),...passthrough,...unrelatedConstraints.map((constraint)=>constraint.statement),...deferred.map((constraint)=>constraint.statement)]}export function groupGeneratedStatements(sqlStatements,previousPlan){const normalizedStatements=normalizeCreateStatements(sqlStatements),groups=new Map,push=(label,stmt)=>{const list=groups.get(label)??[];list.push(stmt);groups.set(label,list)},createdTables=new Set(normalizedStatements.flatMap((raw)=>{const match=raw.trim().match(/^\s*CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["`]?(\w+)["`]?/i);return match?.[1]?[match[1]]:[]}));for(const raw of normalizedStatements){const stmt=raw.trim();if(!stmt)continue;const create=stmt.match(/^\s*CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["`]?(\w+)["`]?/i);if(create){push(`create-${create[1]}-table`,stmt);continue}if(stmt.match(/^\s*CREATE\s+TYPE\s+/i)){push("create-database-types",stmt);continue}if(/^\s*ALTER\s+TYPE\s+\S+\s+ADD\s+VALUE\b/i.test(stmt)){push("add-enum-values",stmt);continue}const alter=stmt.match(/^\s*ALTER\s+TABLE\s+["`]?(\w+)["`]?\s+(?:ADD\s+COLUMN\s+["`]?(\w+)["`]?|DROP\s+COLUMN\s+["`]?(\w+)["`]?|ADD\s+CONSTRAINT)/i),alterTable=alter?.[1];if(alter&&alterTable){const isCreateTimeConstraint=createdTables.has(alterTable)&&!alter[2]&&!alter[3];push(isCreateTimeConstraint?"create-foreign-key-constraints":`alter-${alterTable}-columns`,stmt);continue}const idx=stmt.match(/^\s*CREATE\s+(?:UNIQUE\s+)?INDEX\s+(?:IF\s+NOT\s+EXISTS\s+)?["`]?(\w+)["`]?\s+ON\s+["`]?(\w+)["`]?/i),idxName=idx?.[1],idxTable=idx?.[2];if(idxName&&idxTable){push(createdTables.has(idxTable)?`create-${idxTable}-table`:`create-${idxName}-index-in-${idxTable}`,stmt);continue}const drop=stmt.match(/^\s*DROP\s+TABLE\s+(?:IF\s+EXISTS\s+)?["`]?(\w+)["`]?/i);if(drop){push(`drop-${drop[1]}-table`,stmt);continue}push("auto-misc",stmt)}const priority=(label)=>label==="create-database-types"?0:label==="add-enum-values"?1:2,ordered=[...groups.entries()].map(([label,statements])=>({label,statements})).sort((a,b)=>priority(a.label)-priority(b.label));return withTableDropsLast(ordered,previousPlan)}const DROP_TABLE_GROUP_RE=/^drop-(\w+)-table$/;export function withTableDropsLast(groups,previousPlan){const dropOf=(group)=>group.label.match(DROP_TABLE_GROUP_RE)?.[1],recreated=new Set(groups.flatMap((group)=>group.statements.flatMap((statement)=>{const table=createdTableName(statement);return table?[table]:[]}))),movable=groups.filter((group)=>{const table=dropOf(group);return table!==void 0&&!recreated.has(table.toLowerCase())});if(movable.length===0)return groups;const rest=groups.filter((group)=>!movable.includes(group)),drops=[...movable].reverse(),references=new Map;for(const table of previousPlan?.tables??[])references.set(table.table,new Set(table.columns.flatMap((column)=>column.references?.table&&column.references.table!==table.table?[column.references.table]:[])));const sorted=[],visiting=new Set,visit=(group)=>{if(sorted.includes(group)||visiting.has(group))return;visiting.add(group);for(const other of drops)if(other!==group&&references.get(dropOf(other))?.has(dropOf(group)))visit(other);visiting.delete(group);sorted.push(group)};for(const group of drops)visit(group);return[...rest,...sorted]}export async function generateMigrations2(){await ensureDatabaseConfigLoaded();try{log.info("Generating fresh migrations...");configureQueryBuilder();const dialect=getDialect(),{modelsDir,skip}=prepareMigrationModelsDir();if(skip){log.info("No app/Models directory found; using committed framework migrations");return ok("Migrations generated")}const qbDialect=getQbDialect();await qbGenerateMigration(modelsDir,{dialect:qbDialect,vitessSharded:qbDialect==="vitess"?isVitessSharded(dbConfig.connections.vitess.sharded):void 0,full:!0,dryRun:!0});log.success("Migrations generated");return ok("Migrations generated")}catch(error){return err(handleError("Fresh migration generation failed",error))}}
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/database",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.75.167",
5
+ "version": "0.75.169",
6
6
  "description": "The Stacks database integration.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -66,26 +66,26 @@
66
66
  "prepublishOnly": "bun run build"
67
67
  },
68
68
  "dependencies": {
69
- "@stacksjs/config": "0.75.167",
70
- "@stacksjs/env": "0.75.167",
71
- "@stacksjs/error-handling": "0.75.167",
72
- "@stacksjs/faker": "0.75.167",
73
- "@stacksjs/features": "0.75.167",
74
- "@stacksjs/logging": "0.75.167",
75
- "@stacksjs/model-meta": "0.75.167",
76
- "@stacksjs/path": "0.75.167",
77
- "@stacksjs/query-builder": "0.75.167",
78
- "@stacksjs/security": "0.75.167",
79
- "@stacksjs/storage": "0.75.167",
80
- "@stacksjs/strings": "0.75.167",
69
+ "@stacksjs/config": "0.75.169",
70
+ "@stacksjs/env": "0.75.169",
71
+ "@stacksjs/error-handling": "0.75.169",
72
+ "@stacksjs/faker": "0.75.169",
73
+ "@stacksjs/features": "0.75.169",
74
+ "@stacksjs/logging": "0.75.169",
75
+ "@stacksjs/model-meta": "0.75.169",
76
+ "@stacksjs/path": "0.75.169",
77
+ "@stacksjs/query-builder": "0.75.169",
78
+ "@stacksjs/security": "0.75.169",
79
+ "@stacksjs/storage": "0.75.169",
80
+ "@stacksjs/strings": "0.75.169",
81
81
  "@stacksjs/ts-validation": "^0.5.6",
82
82
  "bun-query-builder": "^0.3.9",
83
83
  "dynamodb-tooling": "^0.3.6"
84
84
  },
85
85
  "devDependencies": {
86
- "@stacksjs/cli": "0.75.167",
87
- "@stacksjs/router": "0.75.167",
88
- "@stacksjs/utils": "0.75.167",
86
+ "@stacksjs/cli": "0.75.169",
87
+ "@stacksjs/router": "0.75.169",
88
+ "@stacksjs/utils": "0.75.169",
89
89
  "better-dx": "^0.2.24"
90
90
  }
91
91
  }