@stacksjs/database 0.75.361 → 0.75.366
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/migrations.js +1 -1
- package/package.json +16 -16
package/dist/migrations.js
CHANGED
|
@@ -29,7 +29,7 @@ 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();if(!options.dialect||options.dialect===attachedDriver())return generateMigrationsForDialect(options);if(!MIGRATION_DIALECTS.includes(options.dialect))return err(Error(`Unknown migration dialect "${options.dialect}". Use one of: ${MIGRATION_DIALECTS.join(", ")}.`));if(resolveGenerateOptions(options).fromDb)return err(Error(`--from-db diffs against the attached ${attachedDriver()} database, so it cannot generate ${options.dialect} migrations.`));return withGenerationDialect(options.dialect,()=>generateMigrationsForDialect(options))}function withAttachedDialect(work){const previous=generationDialect;generationDialect=void 0;try{return work()}finally{generationDialect=previous}}async function withGenerationDialect(dialect,work){const previous=generationDialect;generationDialect=dialect;try{return await work()}finally{generationDialect=previous;configureQueryBuilder()}}export async function siblingMigrationDialects(){await ensureDatabaseConfigLoaded();configureQueryBuilder();let files;try{files=readdirSync(resolveSnapshotDir())}catch{return[]}return MIGRATION_DIALECTS.filter((dialect)=>dialect!==attachedDriver()&&files.includes(`model-snapshot.${dialect}.json`))}export async function dialectsBehindModels(){const behind=[];for(const dialect of await siblingMigrationDialects())if(await withGenerationDialect(dialect,async()=>{configureQueryBuilder();const{modelsDir,skip}=prepareMigrationModelsDir();if(skip)return!1;const result=await qbGenerateMigration(modelsDir,{dialect:getQbDialect(),dryRun:!0});return Boolean(result.hasChanges)}).catch(()=>!1))behind.push(dialect);return behind}async function generateMigrationsForDialect(options){try{log.debug("Generating migrations...");configureQueryBuilder();const dialect=getDialect(),otherDialect=generationDialect!==void 0&&generationDialect!==attachedDriver();if(otherDialect){const attachedDir=withAttachedDialect(()=>migrationDirectory());if(migrationDirectory(dialect)===attachedDir)return err(Error(`Refusing to generate ${dialect} migrations into ${relativeMigrationDirectory(attachedDir)}, which is the ${attachedDriver()} corpus. Create ${relativeMigrationDirectory(join(attachedDir,dialect))}/ to give ${dialect} a corpus of its own, then retry.`))}const 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(),full=otherDialect&&!existsSync(snapshotPathFor(qbDialect)),result=await qbGenerateMigration(modelsDir,{dialect:qbDialect,vitessSharded:qbDialect==="vitess"?isVitessSharded(dbConfig.connections.vitess.sharded):void 0,dryRun:!0,applyRenames,fromDb,...full?{full:!0}:{}});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(`;
|
|
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();if(!options.dialect||options.dialect===attachedDriver())return pendingMigrationOperationsForDialect(options);if(!MIGRATION_DIALECTS.includes(options.dialect))throw Error(`Unknown migration dialect "${options.dialect}". Use one of: ${MIGRATION_DIALECTS.join(", ")}.`);if(resolveGenerateOptions(options).fromDb)throw Error(`--from-db diffs against the attached ${attachedDriver()} database, so it cannot inspect ${options.dialect} migrations.`);return withGenerationDialect(options.dialect,()=>pendingMigrationOperationsForDialect(options))}async function pendingMigrationOperationsForDialect(options){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();if(!options.dialect||options.dialect===attachedDriver())return generateMigrationsForDialect(options);if(!MIGRATION_DIALECTS.includes(options.dialect))return err(Error(`Unknown migration dialect "${options.dialect}". Use one of: ${MIGRATION_DIALECTS.join(", ")}.`));if(resolveGenerateOptions(options).fromDb)return err(Error(`--from-db diffs against the attached ${attachedDriver()} database, so it cannot generate ${options.dialect} migrations.`));return withGenerationDialect(options.dialect,()=>generateMigrationsForDialect(options))}function withAttachedDialect(work){const previous=generationDialect;generationDialect=void 0;try{return work()}finally{generationDialect=previous}}async function withGenerationDialect(dialect,work){const previous=generationDialect;generationDialect=dialect;try{return await work()}finally{generationDialect=previous;configureQueryBuilder()}}export async function siblingMigrationDialects(){await ensureDatabaseConfigLoaded();configureQueryBuilder();let files;try{files=readdirSync(resolveSnapshotDir())}catch{return[]}return MIGRATION_DIALECTS.filter((dialect)=>dialect!==attachedDriver()&&files.includes(`model-snapshot.${dialect}.json`))}export async function dialectsBehindModels(){const behind=[];for(const dialect of await siblingMigrationDialects())if(await withGenerationDialect(dialect,async()=>{configureQueryBuilder();const{modelsDir,skip}=prepareMigrationModelsDir();if(skip)return!1;const result=await qbGenerateMigration(modelsDir,{dialect:getQbDialect(),dryRun:!0});return Boolean(result.hasChanges)}).catch(()=>!1))behind.push(dialect);return behind}async function generateMigrationsForDialect(options){try{log.debug("Generating migrations...");configureQueryBuilder();const dialect=getDialect(),otherDialect=generationDialect!==void 0&&generationDialect!==attachedDriver();if(otherDialect){const attachedDir=withAttachedDialect(()=>migrationDirectory());if(migrationDirectory(dialect)===attachedDir)return err(Error(`Refusing to generate ${dialect} migrations into ${relativeMigrationDirectory(attachedDir)}, which is the ${attachedDriver()} corpus. Create ${relativeMigrationDirectory(join(attachedDir,dialect))}/ to give ${dialect} a corpus of its own, then retry.`))}const 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(),full=otherDialect&&!existsSync(snapshotPathFor(qbDialect)),result=await qbGenerateMigration(modelsDir,{dialect:qbDialect,vitessSharded:qbDialect==="vitess"?isVitessSharded(dbConfig.connections.vitess.sharded):void 0,dryRun:!0,applyRenames,fromDb,...full?{full:!0}:{}});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(`
|
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.
|
|
5
|
+
"version": "0.75.366",
|
|
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.
|
|
70
|
-
"@stacksjs/env": "0.75.
|
|
71
|
-
"@stacksjs/error-handling": "0.75.
|
|
72
|
-
"@stacksjs/faker": "0.75.
|
|
73
|
-
"@stacksjs/features": "0.75.
|
|
74
|
-
"@stacksjs/logging": "0.75.
|
|
75
|
-
"@stacksjs/model-meta": "0.75.
|
|
76
|
-
"@stacksjs/path": "0.75.
|
|
77
|
-
"@stacksjs/query-builder": "0.75.
|
|
78
|
-
"@stacksjs/security": "0.75.
|
|
79
|
-
"@stacksjs/storage": "0.75.
|
|
80
|
-
"@stacksjs/strings": "0.75.
|
|
69
|
+
"@stacksjs/config": "0.75.366",
|
|
70
|
+
"@stacksjs/env": "0.75.366",
|
|
71
|
+
"@stacksjs/error-handling": "0.75.366",
|
|
72
|
+
"@stacksjs/faker": "0.75.366",
|
|
73
|
+
"@stacksjs/features": "0.75.366",
|
|
74
|
+
"@stacksjs/logging": "0.75.366",
|
|
75
|
+
"@stacksjs/model-meta": "0.75.366",
|
|
76
|
+
"@stacksjs/path": "0.75.366",
|
|
77
|
+
"@stacksjs/query-builder": "0.75.366",
|
|
78
|
+
"@stacksjs/security": "0.75.366",
|
|
79
|
+
"@stacksjs/storage": "0.75.366",
|
|
80
|
+
"@stacksjs/strings": "0.75.366",
|
|
81
81
|
"@stacksjs/ts-validation": "^0.5.6",
|
|
82
82
|
"bun-query-builder": "^0.3.28",
|
|
83
83
|
"dynamodb-tooling": "^0.3.6"
|
|
84
84
|
},
|
|
85
85
|
"devDependencies": {
|
|
86
|
-
"@stacksjs/cli": "0.75.
|
|
87
|
-
"@stacksjs/router": "0.75.
|
|
88
|
-
"@stacksjs/utils": "0.75.
|
|
86
|
+
"@stacksjs/cli": "0.75.366",
|
|
87
|
+
"@stacksjs/router": "0.75.366",
|
|
88
|
+
"@stacksjs/utils": "0.75.366",
|
|
89
89
|
"better-dx": "^0.2.24"
|
|
90
90
|
}
|
|
91
91
|
}
|