@stacksjs/database 0.75.399 → 0.75.400
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/schema-align.d.ts +13 -0
- package/dist/schema-align.js +2 -2
- package/package.json +16 -16
package/dist/schema-align.d.ts
CHANGED
|
@@ -55,8 +55,21 @@ export declare function alignSchema(options?: {
|
|
|
55
55
|
relaxConstraints?: boolean
|
|
56
56
|
dialect?: string
|
|
57
57
|
|
|
58
|
+
only?: readonly AlignActionKind[]
|
|
59
|
+
|
|
58
60
|
run?: AlignSqlRunner
|
|
59
61
|
}): Promise<AlignResult>;
|
|
62
|
+
/**
|
|
63
|
+
* The repairs `migrate` makes by itself: an enum label the model declares and
|
|
64
|
+
* the database lacks.
|
|
65
|
+
*
|
|
66
|
+
* Nothing else in the plan is this clear-cut. A label only adds a value the
|
|
67
|
+
* column may hold, so it cannot lose data, and without it every write of that
|
|
68
|
+
* value fails, and so does every query that compares against it. The commonest
|
|
69
|
+
* way to get here is a migration edited after it ran: `migrate` keys on file
|
|
70
|
+
* names, so the edit never reaches a database that already ran the file.
|
|
71
|
+
*/
|
|
72
|
+
export declare const ENUM_ALIGN_KINDS: readonly AlignActionKind[];
|
|
60
73
|
export declare interface DesiredIndex {
|
|
61
74
|
name: string
|
|
62
75
|
table: string
|
package/dist/schema-align.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{comparableIndexKey,indexShape,postgresIndexStatement}from"./index-shape";import{sqlStatementsOf}from"./sql-statements";const quote=(name)=>`"${name.replace(/"/g,'""')}"`,sqlString=(value)=>`'${value.replace(/'/g,"''")}'`;export function postgresIdentifier(name,max=63){if(name.length<=max)return name;let hash=2166136261;for(let i=0;i<name.length;i++){hash^=name.charCodeAt(i);hash=Math.imul(hash,16777619)>>>0}const suffix=`_${hash.toString(36).padStart(7,"0").slice(-7)}`;return name.slice(0,max-suffix.length)+suffix}function topLevelParts(body){const parts=[];let depth=0,current="",quoted=null;for(const char of body){if(quoted){current+=char;if(char===quoted)quoted=null;continue}if(char==="'"||char==='"'){quoted=char;current+=char;continue}if(char==="(")depth++;if(char===")")depth--;if(char===","&&depth===0){parts.push(current);current="";continue}current+=char}parts.push(current);return parts.map((part)=>part.trim()).filter(Boolean)}export function columnDefinitionsOf(createStatement){const definitions=new Map,body=createStatement.match(/\(([\s\S]*)\)\s*;?\s*$/)?.[1];if(!body)return definitions;const constraint=/^\s*(?:PRIMARY\s+KEY|FOREIGN\s+KEY|UNIQUE|CHECK|CONSTRAINT|KEY|INDEX)\b/i;for(const part of topLevelParts(body)){if(constraint.test(part))continue;const name=part.match(/^["'`]?(\w+)["'`]?/)?.[1];if(name)definitions.set(name.toLowerCase(),part)}return definitions}function indexColumnsAndPredicate(statement){const open=/\bON\s+\S+\s*\(/i.exec(statement);if(!open)return;const start=open.index+open[0].length;let depth=1,end=start;for(;end<statement.length&&depth>0;end++)if(statement[end]==="(")depth++;else if(statement[end]===")")depth--;if(depth>0)return;return[statement,statement.slice(start,end-1),statement.slice(end).trim()]}export function desiredSchemaFrom(plan,statements){const creates=new Map,indexes=[],enums=new Map,visit=(statement)=>{const table=/^\s*CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["'`]?(\w+)["'`]?/i.exec(statement)?.[1];if(table){creates.set(table.toLowerCase(),statement);return}const enumType=/^\s*CREATE\s+TYPE\s+["]?([\w$]+)["]?\s+AS\s+ENUM\s*\(([\s\S]*)\)\s*;?\s*$/i.exec(statement);if(enumType?.[1]){const labels=[...(enumType[2]??"").matchAll(/'((?:[^']|'')*)'/g)].map((m)=>(m[1]??"").replace(/''/g,"'"));enums.set(enumType[1].toLowerCase(),labels);return}const guarded=/^\s*DO\s+\$(\w*)\$\s*BEGIN\s+([\s\S]*?);\s*EXCEPTION\b/i.exec(statement);if(guarded?.[2]){visit(guarded[2]);return}const shape=indexShape(statement),key=comparableIndexKey(statement);if(shape&&key){const head=indexColumnsAndPredicate(statement),parts=topLevelParts(head?.[1]??""),plain=parts.every((part)=>/^["`]?\w+["`]?$/.test(part));indexes.push({name:shape.name,table:shape.table,unique:/^\s*CREATE\s+UNIQUE\b/i.test(statement),...plain?{columns:parts.map((part)=>part.replace(/["`]/g,"").toLowerCase())}:{},key,where:(head?.[2]??"").replace(/;\s*$/,"").trim(),statement:statement.trim().replace(/;\s*$/,"")})}};for(const statement of statements)for(const part of sqlStatementsOf(statement.trim().endsWith(";")?statement:`${statement};`))visit(part);const tables=new Map;for(const table of plan.tables){const name=table.table.toLowerCase(),definitions=creates.has(name)?columnDefinitionsOf(creates.get(name)):new Map;tables.set(name,table.columns.map((column)=>({name:column.name.toLowerCase(),type:column.type,...column.maxLength!==void 0?{maxLength:column.maxLength}:{},isPrimaryKey:column.isPrimaryKey,isNullable:column.isNullable,hasDefault:column.hasDefault,...column.defaultValue!==void 0?{defaultValue:column.defaultValue}:{},...definitions.get(column.name.toLowerCase())?{definition:definitions.get(column.name.toLowerCase())}:{},...column.references?{references:{...column.references}}:{}})))}return{tables,indexes,enums}}const INTEGER_RANK={smallint:1,integer:2,bigint:3};function declaredIntegerRank(type){if(type==="integer")return INTEGER_RANK.integer;if(type==="bigint")return INTEGER_RANK.bigint;return}function liveIntegerRank(column){return INTEGER_RANK[column.dataType]}export function comparableDefault(value){if(value===void 0||value===null)return;let text=String(value).trim();if(/^nextval\(/i.test(text))return;if(/\bnow\(\)|\b(?:current_timestamp|utc_timestamp|localtimestamp)\b/i.test(text))return"clock";text=text.replace(/::\s*(?:"[^"]+"|[a-z_]\w*)(?:\s+varying|\s+precision|\s+with(?:out)?\s+time\s+zone)?(?:\[\])?/gi,"");while(/^\(.*\)$/s.test(text))text=text.slice(1,-1).trim();text=text.replace(/^'(.*)'$/s,"$1").replace(/''/g,"'");return text.toLowerCase()}function declaredDefault(column){if(!column.hasDefault||column.defaultValue===void 0||column.defaultValue===null)return;const value=column.defaultValue;if(value instanceof Date)return comparableDefault(value.toISOString());return comparableDefault(typeof value==="string"?value:String(value))}function foreignKeyAction(action){return(action??"no action").toUpperCase()}const PG_ACTION={a:"NO ACTION",r:"RESTRICT",c:"CASCADE",n:"SET NULL",d:"SET DEFAULT"};export function planSchemaAlignment(desired,live,options={}){const actions=[],reports=[],added=new Set;for(const[type,labels]of desired.enums){const existing=live.enums.get(type);if(!existing){actions.push({kind:"create-enum-type",table:type,name:type,detail:`create enum type ${type} (${labels.join(", ")})`,sql:`CREATE TYPE ${quote(type)} AS ENUM (${labels.map(sqlString).join(", ")})`});continue}for(const label of labels.filter((label)=>!existing.includes(label)))actions.push({kind:"add-enum-value",table:type,name:label,detail:`add '${label}' to enum type ${type}`,sql:`ALTER TYPE ${quote(type)} ADD VALUE IF NOT EXISTS ${sqlString(label)}`});const extra=existing.filter((label)=>!labels.includes(label));if(extra.length>0)reports.push({kind:"enum",table:type,detail:`enum type ${type} has label(s) the model does not declare: ${extra.join(", ")}`})}for(const[table,columns]of desired.tables){const liveColumns=live.tables.get(table);if(!liveColumns){reports.push({kind:"missing-table",table,detail:`table ${table} does not exist - \`buddy migrate\` creates it`});continue}const declared=new Set(columns.map((column)=>column.name));for(const name of liveColumns.keys())if(!declared.has(name))reports.push({kind:"extra-column",table,name,detail:`${table}.${name} exists but no model declares it`});for(const column of columns){const current=liveColumns.get(column.name);if(!current){if(!column.definition||/\bPRIMARY\s+KEY\b|\bUNIQUE\b/i.test(column.definition)){reports.push({kind:"missing-column",table,name:column.name,detail:`${table}.${column.name} is missing and cannot be added in place`});continue}const inlineReference=/\bREFERENCES\s+"([^"]+)"/i.exec(column.definition)?.[1]?.toLowerCase();if(inlineReference&&!live.tables.has(inlineReference)){reports.push({kind:"foreign-key",table,name:column.name,detail:`${table}.${column.name} references ${inlineReference}, which does not exist - \`buddy migrate\` creates that table`});continue}const strict=/\bNOT\s+NULL\b/i.test(column.definition)&&!/\bDEFAULT\b/i.test(column.definition),populated=live.populated?.has(table)??!0,definition=strict&&populated?column.definition.replace(/\s*\bNOT\s+NULL\b/i,""):column.definition;actions.push({kind:"add-column",table,name:column.name,detail:`add ${table}.${column.name}${definition!==column.definition?" (nullable: the table has rows and the column has no default)":""}`,sql:`ALTER TABLE ${quote(table)} ADD COLUMN IF NOT EXISTS ${definition}`});added.add(`${table}.${column.name}`);if(definition!==column.definition)reports.push({kind:"nullability",table,name:column.name,detail:`${table}.${column.name} is added nullable; backfill it, then make it NOT NULL`});continue}if(column.isPrimaryKey)continue;const liveSql=current.maxLength?`${current.dataType}(${current.maxLength})`:current.dataType;if(column.type==="string"||column.type==="text"){const wanted=column.type==="text"?void 0:column.maxLength??255,isVarchar=current.dataType==="character varying",isText=current.dataType==="text"||isVarchar&&!current.maxLength;if(isVarchar&¤t.maxLength&&(wanted===void 0||wanted>current.maxLength)){const to=wanted===void 0?"text":`varchar(${wanted})`;actions.push({kind:"widen-column",table,name:column.name,detail:`widen ${table}.${column.name} from varchar(${current.maxLength}) to ${to}`,sql:`ALTER TABLE ${quote(table)} ALTER COLUMN ${quote(column.name)} TYPE ${to}`})}else if(isVarchar&¤t.maxLength&&wanted!==void 0&&wanted<current.maxLength)reports.push({kind:"type",table,name:column.name,detail:`${table}.${column.name} is varchar(${current.maxLength}); the model declares varchar(${wanted}) - narrowing could truncate data`});else if(isText&&wanted!==void 0)reports.push({kind:"type",table,name:column.name,detail:`${table}.${column.name} is unbounded text; the model declares varchar(${wanted}) - narrowing could truncate data`});else if(!isVarchar&&!isText&¤t.dataType!=="USER-DEFINED")reports.push({kind:"type",table,name:column.name,detail:`${table}.${column.name} is ${liveSql}; the model declares ${column.type==="text"?"text":`varchar(${wanted})`}`})}else if(declaredIntegerRank(column.type)!==void 0&&liveIntegerRank(current)!==void 0){const want=declaredIntegerRank(column.type),have=liveIntegerRank(current);if(want>have)if(options.widenIntegers)actions.push({kind:"widen-column",table,name:column.name,detail:`widen ${table}.${column.name} from ${current.dataType} to bigint (rewrites the table)`,sql:`ALTER TABLE ${quote(table)} ALTER COLUMN ${quote(column.name)} TYPE bigint`});else reports.push({kind:"optional-widening",table,name:column.name,detail:`${table}.${column.name} is ${current.dataType}; the model declares bigint - pass --widen-integers to convert it (rewrites the table)`});else if(want<have)reports.push({kind:"type",table,name:column.name,detail:`${table}.${column.name} is ${current.dataType}; the model declares ${column.type} - narrowing could overflow`})}else if(column.type!=="enum"&&options.typeSatisfies&&!options.typeSatisfies(column.type,current.dataType))reports.push({kind:"type",table,name:column.name,detail:`${table}.${column.name} is ${liveSql}; the model declares ${column.type}`});const wantNullable=column.isNullable;if(wantNullable!==current.nullable)if(options.relaxConstraints&&wantNullable&&!column.isPrimaryKey)actions.push({kind:"relax-nullability",table,name:column.name,detail:`allow NULL in ${table}.${column.name}, as the model declares`,sql:`ALTER TABLE ${quote(table)} ALTER COLUMN ${quote(column.name)} DROP NOT NULL`});else reports.push({kind:"nullability",table,name:column.name,detail:wantNullable?`${table}.${column.name} is NOT NULL; the model allows NULL`:`${table}.${column.name} allows NULL; the model declares NOT NULL`});const want=declaredDefault(column),have=comparableDefault(current.default);if((want??"")!==(have??"")&&!(want===void 0&&have==="null"))reports.push({kind:"default",table,name:column.name,detail:`${table}.${column.name} defaults to ${current.default??"nothing"}; the model declares ${want===void 0?"no default":String(column.defaultValue)}`})}}const desiredNames=new Set(desired.indexes.map((index)=>index.name)),liveByName=new Map(live.indexes.map((index)=>[index.name,index])),claimed=new Set,renames=[],creates=[];for(const index of desired.indexes){const liveColumns=live.tables.get(index.table);if(!liveColumns)continue;const existing=liveByName.get(index.name);if(existing){claimed.add(existing.name);if(existing.table!==index.table||existing.key&&existing.key!==index.key)reports.push({kind:"index-definition",table:index.table,name:index.name,detail:`index ${index.name} exists but differs from the model (${existing.definition}) - drop and recreate it by hand if the model is right`});continue}const twins=live.indexes.filter((candidate)=>candidate.table===index.table&&!candidate.primary&&candidate.key===index.key&&!desiredNames.has(candidate.name)&&!claimed.has(candidate.name));if(twins.length>0){const[twin,...extra]=[...twins].sort((a,b)=>Number(b.name.includes(index.name))-Number(a.name.includes(index.name)));claimed.add(twin.name);if(live.names.has(index.name))reports.push({kind:"name-conflict",table:index.table,name:index.name,detail:`cannot rename ${twin.name} to ${index.name}: that name is taken by another object`});else renames.push({kind:"rename-index",table:index.table,name:index.name,detail:`rename index ${twin.name} to ${index.name}${twin.constraint?" (and the constraint it backs)":""}`,sql:`ALTER INDEX ${quote(twin.name)} RENAME TO ${quote(index.name)}`});for(const duplicate of extra){claimed.add(duplicate.name);reports.push({kind:"duplicate-index",table:index.table,name:duplicate.name,detail:`${duplicate.name} duplicates ${index.name} - drop it by hand once nothing depends on it`})}continue}const missingColumn=index.columns?.find((column)=>!liveColumns.has(column)&&!added.has(`${index.table}.${column}`));if(missingColumn){reports.push({kind:"missing-column",table:index.table,name:missingColumn,detail:`index ${index.name} needs ${index.table}.${missingColumn}, which cannot be added here`});continue}const statement=index.statement.replace(/^\s*CREATE\s+(UNIQUE\s+)?INDEX\s+(?!IF\s+NOT\s+EXISTS)/i,(_m,unique)=>`CREATE ${unique??""}INDEX IF NOT EXISTS `),action={kind:"create-index",table:index.table,name:index.name,detail:`create ${index.unique?"unique ":""}index ${index.name}`,sql:statement};if(index.unique&&index.columns){const columns=index.columns.map(quote).join(", "),where=[...index.columns.map((column)=>`${quote(column)} IS NOT NULL`),...index.where?[`(${index.where.replace(/^WHERE\s+/i,"")})`]:[]].join(" AND ");action.check={kind:"duplicates",sql:`SELECT COUNT(*) AS n FROM (SELECT 1 FROM ${quote(index.table)} WHERE ${where} GROUP BY ${columns} HAVING COUNT(*) > 1) AS duplicates`,dependsOn:[...index.columns]}}creates.push(action)}const obsolete=new Set;for(const foreignKey of live.foreignKeys){const columns=desired.tables.get(foreignKey.table),column=columns?.find((value)=>value.name===foreignKey.column),typeColumn=foreignKey.column.replace(/_id$/,"_type"),key=`${foreignKey.table}:${foreignKey.name}`;if(!column||column.references||typeColumn===foreignKey.column||!columns?.some((value)=>value.name===typeColumn)||obsolete.has(key))continue;obsolete.add(key);if(options.relaxConstraints)actions.push({kind:"drop-polymorphic-foreign-key",table:foreignKey.table,name:foreignKey.name,detail:`remove ${foreignKey.name}: ${foreignKey.column} has a polymorphic type column`,sql:`ALTER TABLE ${quote(foreignKey.table)} DROP CONSTRAINT ${quote(foreignKey.name)}`});else reports.push({kind:"foreign-key",table:foreignKey.table,name:foreignKey.name,detail:`${foreignKey.name} constrains a polymorphic reference to one table; use --relax-constraints to remove it`})}const foreignKeys=[];for(const[table,columns]of desired.tables){const liveColumns=live.tables.get(table);if(!liveColumns)continue;for(const column of columns){const ref=column.references;if(!ref)continue;const refTable=ref.table.toLowerCase(),refColumn=ref.column.toLowerCase();if(added.has(`${table}.${column.name}`)||!liveColumns.has(column.name))continue;const present=live.foreignKeys.find((fk)=>fk.table===table&&fk.column===column.name&&fk.refTable===refTable&&fk.refColumn===refColumn),conflicts=live.foreignKeys.filter((fk)=>fk.table===table&&fk.column===column.name&&(fk.refTable!==refTable||fk.refColumn!==refColumn));for(const conflict of conflicts)if(present&&options.relaxConstraints)actions.push({kind:"drop-conflicting-foreign-key",table,name:conflict.name,detail:`remove ${conflict.name}: ${table}.${column.name} already references the model's ${refTable}.${refColumn}`,sql:`ALTER TABLE ${quote(table)} DROP CONSTRAINT ${quote(conflict.name)}`});else reports.push({kind:"foreign-key",table,name:conflict.name,detail:`${conflict.name} references ${conflict.refTable}.${conflict.refColumn} instead of ${refTable}.${refColumn}; removal requires --relax-constraints and an enforced model reference`});if(present){const wantDelete=foreignKeyAction(ref.onDelete),wantUpdate=foreignKeyAction(ref.onUpdate);if(present.onDelete!==wantDelete||present.onUpdate!==wantUpdate)reports.push({kind:"foreign-key",table,name:present.name,detail:`${present.name} is ON DELETE ${present.onDelete} ON UPDATE ${present.onUpdate}; the model declares ON DELETE ${wantDelete} ON UPDATE ${wantUpdate}`});continue}if(!live.tables.get(refTable)?.has(refColumn)){reports.push({kind:"foreign-key",table,name:column.name,detail:`${table}.${column.name} references ${refTable}.${refColumn}, which does not exist`});continue}const name=postgresIdentifier(ref.constraintNames?.[0]??`${table}_${column.name}_fkey`);if(live.names.has(name)){reports.push({kind:"name-conflict",table,name,detail:`cannot add the foreign key for ${table}.${column.name} as ${name}: that name is taken`});continue}const actionsSql=[ref.onDelete?` ON DELETE ${foreignKeyAction(ref.onDelete)}`:"",ref.onUpdate?` ON UPDATE ${foreignKeyAction(ref.onUpdate)}`:""].join("");foreignKeys.push({kind:"add-foreign-key",table,name,detail:`add foreign key ${table}.${column.name} -> ${refTable}.${refColumn}`,sql:`ALTER TABLE ${quote(table)} ADD CONSTRAINT ${quote(name)} FOREIGN KEY (${quote(column.name)}) REFERENCES ${quote(refTable)} (${quote(refColumn)})${actionsSql}`,check:{kind:"orphans",sql:`SELECT COUNT(*) AS n FROM ${quote(table)} AS child WHERE child.${quote(column.name)} IS NOT NULL AND NOT EXISTS (SELECT 1 FROM ${quote(refTable)} AS parent WHERE parent.${quote(refColumn)} = child.${quote(column.name)})`,dependsOn:[column.name]}})}}return{actions:[...actions,...renames,...creates,...foreignKeys],reports,refused:[]}}function rows(result){return Array.isArray(result)?result:[]}function text(row,key){const value=row?.[key];return value===null||value===void 0?"":String(value)}export async function readPostgresAlignSchema(run){const tables=new Map;for(const row of rows(await run("SELECT table_name FROM information_schema.tables WHERE table_schema = current_schema() AND table_type = 'BASE TABLE'")))tables.set(text(row,"table_name").toLowerCase(),new Map);for(const row of rows(await run("SELECT table_name, column_name, data_type, udt_name, character_maximum_length, is_nullable, column_default FROM information_schema.columns WHERE table_schema = current_schema()"))){const columns=tables.get(text(row,"table_name").toLowerCase());if(!columns)continue;const length=Number(row.character_maximum_length);columns.set(text(row,"column_name").toLowerCase(),{name:text(row,"column_name").toLowerCase(),dataType:text(row,"data_type").toLowerCase()==="user-defined"?"USER-DEFINED":text(row,"data_type").toLowerCase(),udtName:text(row,"udt_name").toLowerCase(),...Number.isFinite(length)&&length>0?{maxLength:length}:{},nullable:text(row,"is_nullable").toUpperCase()==="YES",...row.column_default!==null&&row.column_default!==void 0?{default:String(row.column_default)}:{}})}const indexes=[];for(const row of rows(await run(`
|
|
1
|
+
import{comparableIndexKey,indexShape,postgresIndexStatement}from"./index-shape";import{sqlStatementsOf}from"./sql-statements";export const ENUM_ALIGN_KINDS=["create-enum-type","add-enum-value"];const quote=(name)=>`"${name.replace(/"/g,'""')}"`,sqlString=(value)=>`'${value.replace(/'/g,"''")}'`;export function postgresIdentifier(name,max=63){if(name.length<=max)return name;let hash=2166136261;for(let i=0;i<name.length;i++){hash^=name.charCodeAt(i);hash=Math.imul(hash,16777619)>>>0}const suffix=`_${hash.toString(36).padStart(7,"0").slice(-7)}`;return name.slice(0,max-suffix.length)+suffix}function topLevelParts(body){const parts=[];let depth=0,current="",quoted=null;for(const char of body){if(quoted){current+=char;if(char===quoted)quoted=null;continue}if(char==="'"||char==='"'){quoted=char;current+=char;continue}if(char==="(")depth++;if(char===")")depth--;if(char===","&&depth===0){parts.push(current);current="";continue}current+=char}parts.push(current);return parts.map((part)=>part.trim()).filter(Boolean)}export function columnDefinitionsOf(createStatement){const definitions=new Map,body=createStatement.match(/\(([\s\S]*)\)\s*;?\s*$/)?.[1];if(!body)return definitions;const constraint=/^\s*(?:PRIMARY\s+KEY|FOREIGN\s+KEY|UNIQUE|CHECK|CONSTRAINT|KEY|INDEX)\b/i;for(const part of topLevelParts(body)){if(constraint.test(part))continue;const name=part.match(/^["'`]?(\w+)["'`]?/)?.[1];if(name)definitions.set(name.toLowerCase(),part)}return definitions}function indexColumnsAndPredicate(statement){const open=/\bON\s+\S+\s*\(/i.exec(statement);if(!open)return;const start=open.index+open[0].length;let depth=1,end=start;for(;end<statement.length&&depth>0;end++)if(statement[end]==="(")depth++;else if(statement[end]===")")depth--;if(depth>0)return;return[statement,statement.slice(start,end-1),statement.slice(end).trim()]}export function desiredSchemaFrom(plan,statements){const creates=new Map,indexes=[],enums=new Map,visit=(statement)=>{const table=/^\s*CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["'`]?(\w+)["'`]?/i.exec(statement)?.[1];if(table){creates.set(table.toLowerCase(),statement);return}const enumType=/^\s*CREATE\s+TYPE\s+["]?([\w$]+)["]?\s+AS\s+ENUM\s*\(([\s\S]*)\)\s*;?\s*$/i.exec(statement);if(enumType?.[1]){const labels=[...(enumType[2]??"").matchAll(/'((?:[^']|'')*)'/g)].map((m)=>(m[1]??"").replace(/''/g,"'"));enums.set(enumType[1].toLowerCase(),labels);return}const guarded=/^\s*DO\s+\$(\w*)\$\s*BEGIN\s+([\s\S]*?);\s*EXCEPTION\b/i.exec(statement);if(guarded?.[2]){visit(guarded[2]);return}const shape=indexShape(statement),key=comparableIndexKey(statement);if(shape&&key){const head=indexColumnsAndPredicate(statement),parts=topLevelParts(head?.[1]??""),plain=parts.every((part)=>/^["`]?\w+["`]?$/.test(part));indexes.push({name:shape.name,table:shape.table,unique:/^\s*CREATE\s+UNIQUE\b/i.test(statement),...plain?{columns:parts.map((part)=>part.replace(/["`]/g,"").toLowerCase())}:{},key,where:(head?.[2]??"").replace(/;\s*$/,"").trim(),statement:statement.trim().replace(/;\s*$/,"")})}};for(const statement of statements)for(const part of sqlStatementsOf(statement.trim().endsWith(";")?statement:`${statement};`))visit(part);const tables=new Map;for(const table of plan.tables){const name=table.table.toLowerCase(),definitions=creates.has(name)?columnDefinitionsOf(creates.get(name)):new Map;tables.set(name,table.columns.map((column)=>({name:column.name.toLowerCase(),type:column.type,...column.maxLength!==void 0?{maxLength:column.maxLength}:{},isPrimaryKey:column.isPrimaryKey,isNullable:column.isNullable,hasDefault:column.hasDefault,...column.defaultValue!==void 0?{defaultValue:column.defaultValue}:{},...definitions.get(column.name.toLowerCase())?{definition:definitions.get(column.name.toLowerCase())}:{},...column.references?{references:{...column.references}}:{}})))}return{tables,indexes,enums}}const INTEGER_RANK={smallint:1,integer:2,bigint:3};function declaredIntegerRank(type){if(type==="integer")return INTEGER_RANK.integer;if(type==="bigint")return INTEGER_RANK.bigint;return}function liveIntegerRank(column){return INTEGER_RANK[column.dataType]}export function comparableDefault(value){if(value===void 0||value===null)return;let text=String(value).trim();if(/^nextval\(/i.test(text))return;if(/\bnow\(\)|\b(?:current_timestamp|utc_timestamp|localtimestamp)\b/i.test(text))return"clock";text=text.replace(/::\s*(?:"[^"]+"|[a-z_]\w*)(?:\s+varying|\s+precision|\s+with(?:out)?\s+time\s+zone)?(?:\[\])?/gi,"");while(/^\(.*\)$/s.test(text))text=text.slice(1,-1).trim();text=text.replace(/^'(.*)'$/s,"$1").replace(/''/g,"'");return text.toLowerCase()}function declaredDefault(column){if(!column.hasDefault||column.defaultValue===void 0||column.defaultValue===null)return;const value=column.defaultValue;if(value instanceof Date)return comparableDefault(value.toISOString());return comparableDefault(typeof value==="string"?value:String(value))}function foreignKeyAction(action){return(action??"no action").toUpperCase()}const PG_ACTION={a:"NO ACTION",r:"RESTRICT",c:"CASCADE",n:"SET NULL",d:"SET DEFAULT"};export function planSchemaAlignment(desired,live,options={}){const actions=[],reports=[],added=new Set;for(const[type,labels]of desired.enums){const existing=live.enums.get(type);if(!existing){actions.push({kind:"create-enum-type",table:type,name:type,detail:`create enum type ${type} (${labels.join(", ")})`,sql:`CREATE TYPE ${quote(type)} AS ENUM (${labels.map(sqlString).join(", ")})`});continue}for(const label of labels.filter((label)=>!existing.includes(label)))actions.push({kind:"add-enum-value",table:type,name:label,detail:`add '${label}' to enum type ${type}`,sql:`ALTER TYPE ${quote(type)} ADD VALUE IF NOT EXISTS ${sqlString(label)}`});const extra=existing.filter((label)=>!labels.includes(label));if(extra.length>0)reports.push({kind:"enum",table:type,detail:`enum type ${type} has label(s) the model does not declare: ${extra.join(", ")}`})}for(const[table,columns]of desired.tables){const liveColumns=live.tables.get(table);if(!liveColumns){reports.push({kind:"missing-table",table,detail:`table ${table} does not exist - \`buddy migrate\` creates it`});continue}const declared=new Set(columns.map((column)=>column.name));for(const name of liveColumns.keys())if(!declared.has(name))reports.push({kind:"extra-column",table,name,detail:`${table}.${name} exists but no model declares it`});for(const column of columns){const current=liveColumns.get(column.name);if(!current){if(!column.definition||/\bPRIMARY\s+KEY\b|\bUNIQUE\b/i.test(column.definition)){reports.push({kind:"missing-column",table,name:column.name,detail:`${table}.${column.name} is missing and cannot be added in place`});continue}const inlineReference=/\bREFERENCES\s+"([^"]+)"/i.exec(column.definition)?.[1]?.toLowerCase();if(inlineReference&&!live.tables.has(inlineReference)){reports.push({kind:"foreign-key",table,name:column.name,detail:`${table}.${column.name} references ${inlineReference}, which does not exist - \`buddy migrate\` creates that table`});continue}const strict=/\bNOT\s+NULL\b/i.test(column.definition)&&!/\bDEFAULT\b/i.test(column.definition),populated=live.populated?.has(table)??!0,definition=strict&&populated?column.definition.replace(/\s*\bNOT\s+NULL\b/i,""):column.definition;actions.push({kind:"add-column",table,name:column.name,detail:`add ${table}.${column.name}${definition!==column.definition?" (nullable: the table has rows and the column has no default)":""}`,sql:`ALTER TABLE ${quote(table)} ADD COLUMN IF NOT EXISTS ${definition}`});added.add(`${table}.${column.name}`);if(definition!==column.definition)reports.push({kind:"nullability",table,name:column.name,detail:`${table}.${column.name} is added nullable; backfill it, then make it NOT NULL`});continue}if(column.isPrimaryKey)continue;const liveSql=current.maxLength?`${current.dataType}(${current.maxLength})`:current.dataType;if(column.type==="string"||column.type==="text"){const wanted=column.type==="text"?void 0:column.maxLength??255,isVarchar=current.dataType==="character varying",isText=current.dataType==="text"||isVarchar&&!current.maxLength;if(isVarchar&¤t.maxLength&&(wanted===void 0||wanted>current.maxLength)){const to=wanted===void 0?"text":`varchar(${wanted})`;actions.push({kind:"widen-column",table,name:column.name,detail:`widen ${table}.${column.name} from varchar(${current.maxLength}) to ${to}`,sql:`ALTER TABLE ${quote(table)} ALTER COLUMN ${quote(column.name)} TYPE ${to}`})}else if(isVarchar&¤t.maxLength&&wanted!==void 0&&wanted<current.maxLength)reports.push({kind:"type",table,name:column.name,detail:`${table}.${column.name} is varchar(${current.maxLength}); the model declares varchar(${wanted}) - narrowing could truncate data`});else if(isText&&wanted!==void 0)reports.push({kind:"type",table,name:column.name,detail:`${table}.${column.name} is unbounded text; the model declares varchar(${wanted}) - narrowing could truncate data`});else if(!isVarchar&&!isText&¤t.dataType!=="USER-DEFINED")reports.push({kind:"type",table,name:column.name,detail:`${table}.${column.name} is ${liveSql}; the model declares ${column.type==="text"?"text":`varchar(${wanted})`}`})}else if(declaredIntegerRank(column.type)!==void 0&&liveIntegerRank(current)!==void 0){const want=declaredIntegerRank(column.type),have=liveIntegerRank(current);if(want>have)if(options.widenIntegers)actions.push({kind:"widen-column",table,name:column.name,detail:`widen ${table}.${column.name} from ${current.dataType} to bigint (rewrites the table)`,sql:`ALTER TABLE ${quote(table)} ALTER COLUMN ${quote(column.name)} TYPE bigint`});else reports.push({kind:"optional-widening",table,name:column.name,detail:`${table}.${column.name} is ${current.dataType}; the model declares bigint - pass --widen-integers to convert it (rewrites the table)`});else if(want<have)reports.push({kind:"type",table,name:column.name,detail:`${table}.${column.name} is ${current.dataType}; the model declares ${column.type} - narrowing could overflow`})}else if(column.type!=="enum"&&options.typeSatisfies&&!options.typeSatisfies(column.type,current.dataType))reports.push({kind:"type",table,name:column.name,detail:`${table}.${column.name} is ${liveSql}; the model declares ${column.type}`});const wantNullable=column.isNullable;if(wantNullable!==current.nullable)if(options.relaxConstraints&&wantNullable&&!column.isPrimaryKey)actions.push({kind:"relax-nullability",table,name:column.name,detail:`allow NULL in ${table}.${column.name}, as the model declares`,sql:`ALTER TABLE ${quote(table)} ALTER COLUMN ${quote(column.name)} DROP NOT NULL`});else reports.push({kind:"nullability",table,name:column.name,detail:wantNullable?`${table}.${column.name} is NOT NULL; the model allows NULL`:`${table}.${column.name} allows NULL; the model declares NOT NULL`});const want=declaredDefault(column),have=comparableDefault(current.default);if((want??"")!==(have??"")&&!(want===void 0&&have==="null"))reports.push({kind:"default",table,name:column.name,detail:`${table}.${column.name} defaults to ${current.default??"nothing"}; the model declares ${want===void 0?"no default":String(column.defaultValue)}`})}}const desiredNames=new Set(desired.indexes.map((index)=>index.name)),liveByName=new Map(live.indexes.map((index)=>[index.name,index])),claimed=new Set,renames=[],creates=[];for(const index of desired.indexes){const liveColumns=live.tables.get(index.table);if(!liveColumns)continue;const existing=liveByName.get(index.name);if(existing){claimed.add(existing.name);if(existing.table!==index.table||existing.key&&existing.key!==index.key)reports.push({kind:"index-definition",table:index.table,name:index.name,detail:`index ${index.name} exists but differs from the model (${existing.definition}) - drop and recreate it by hand if the model is right`});continue}const twins=live.indexes.filter((candidate)=>candidate.table===index.table&&!candidate.primary&&candidate.key===index.key&&!desiredNames.has(candidate.name)&&!claimed.has(candidate.name));if(twins.length>0){const[twin,...extra]=[...twins].sort((a,b)=>Number(b.name.includes(index.name))-Number(a.name.includes(index.name)));claimed.add(twin.name);if(live.names.has(index.name))reports.push({kind:"name-conflict",table:index.table,name:index.name,detail:`cannot rename ${twin.name} to ${index.name}: that name is taken by another object`});else renames.push({kind:"rename-index",table:index.table,name:index.name,detail:`rename index ${twin.name} to ${index.name}${twin.constraint?" (and the constraint it backs)":""}`,sql:`ALTER INDEX ${quote(twin.name)} RENAME TO ${quote(index.name)}`});for(const duplicate of extra){claimed.add(duplicate.name);reports.push({kind:"duplicate-index",table:index.table,name:duplicate.name,detail:`${duplicate.name} duplicates ${index.name} - drop it by hand once nothing depends on it`})}continue}const missingColumn=index.columns?.find((column)=>!liveColumns.has(column)&&!added.has(`${index.table}.${column}`));if(missingColumn){reports.push({kind:"missing-column",table:index.table,name:missingColumn,detail:`index ${index.name} needs ${index.table}.${missingColumn}, which cannot be added here`});continue}const statement=index.statement.replace(/^\s*CREATE\s+(UNIQUE\s+)?INDEX\s+(?!IF\s+NOT\s+EXISTS)/i,(_m,unique)=>`CREATE ${unique??""}INDEX IF NOT EXISTS `),action={kind:"create-index",table:index.table,name:index.name,detail:`create ${index.unique?"unique ":""}index ${index.name}`,sql:statement};if(index.unique&&index.columns){const columns=index.columns.map(quote).join(", "),where=[...index.columns.map((column)=>`${quote(column)} IS NOT NULL`),...index.where?[`(${index.where.replace(/^WHERE\s+/i,"")})`]:[]].join(" AND ");action.check={kind:"duplicates",sql:`SELECT COUNT(*) AS n FROM (SELECT 1 FROM ${quote(index.table)} WHERE ${where} GROUP BY ${columns} HAVING COUNT(*) > 1) AS duplicates`,dependsOn:[...index.columns]}}creates.push(action)}const obsolete=new Set;for(const foreignKey of live.foreignKeys){const columns=desired.tables.get(foreignKey.table),column=columns?.find((value)=>value.name===foreignKey.column),typeColumn=foreignKey.column.replace(/_id$/,"_type"),key=`${foreignKey.table}:${foreignKey.name}`;if(!column||column.references||typeColumn===foreignKey.column||!columns?.some((value)=>value.name===typeColumn)||obsolete.has(key))continue;obsolete.add(key);if(options.relaxConstraints)actions.push({kind:"drop-polymorphic-foreign-key",table:foreignKey.table,name:foreignKey.name,detail:`remove ${foreignKey.name}: ${foreignKey.column} has a polymorphic type column`,sql:`ALTER TABLE ${quote(foreignKey.table)} DROP CONSTRAINT ${quote(foreignKey.name)}`});else reports.push({kind:"foreign-key",table:foreignKey.table,name:foreignKey.name,detail:`${foreignKey.name} constrains a polymorphic reference to one table; use --relax-constraints to remove it`})}const foreignKeys=[];for(const[table,columns]of desired.tables){const liveColumns=live.tables.get(table);if(!liveColumns)continue;for(const column of columns){const ref=column.references;if(!ref)continue;const refTable=ref.table.toLowerCase(),refColumn=ref.column.toLowerCase();if(added.has(`${table}.${column.name}`)||!liveColumns.has(column.name))continue;const present=live.foreignKeys.find((fk)=>fk.table===table&&fk.column===column.name&&fk.refTable===refTable&&fk.refColumn===refColumn),conflicts=live.foreignKeys.filter((fk)=>fk.table===table&&fk.column===column.name&&(fk.refTable!==refTable||fk.refColumn!==refColumn));for(const conflict of conflicts)if(present&&options.relaxConstraints)actions.push({kind:"drop-conflicting-foreign-key",table,name:conflict.name,detail:`remove ${conflict.name}: ${table}.${column.name} already references the model's ${refTable}.${refColumn}`,sql:`ALTER TABLE ${quote(table)} DROP CONSTRAINT ${quote(conflict.name)}`});else reports.push({kind:"foreign-key",table,name:conflict.name,detail:`${conflict.name} references ${conflict.refTable}.${conflict.refColumn} instead of ${refTable}.${refColumn}; removal requires --relax-constraints and an enforced model reference`});if(present){const wantDelete=foreignKeyAction(ref.onDelete),wantUpdate=foreignKeyAction(ref.onUpdate);if(present.onDelete!==wantDelete||present.onUpdate!==wantUpdate)reports.push({kind:"foreign-key",table,name:present.name,detail:`${present.name} is ON DELETE ${present.onDelete} ON UPDATE ${present.onUpdate}; the model declares ON DELETE ${wantDelete} ON UPDATE ${wantUpdate}`});continue}if(!live.tables.get(refTable)?.has(refColumn)){reports.push({kind:"foreign-key",table,name:column.name,detail:`${table}.${column.name} references ${refTable}.${refColumn}, which does not exist`});continue}const name=postgresIdentifier(ref.constraintNames?.[0]??`${table}_${column.name}_fkey`);if(live.names.has(name)){reports.push({kind:"name-conflict",table,name,detail:`cannot add the foreign key for ${table}.${column.name} as ${name}: that name is taken`});continue}const actionsSql=[ref.onDelete?` ON DELETE ${foreignKeyAction(ref.onDelete)}`:"",ref.onUpdate?` ON UPDATE ${foreignKeyAction(ref.onUpdate)}`:""].join("");foreignKeys.push({kind:"add-foreign-key",table,name,detail:`add foreign key ${table}.${column.name} -> ${refTable}.${refColumn}`,sql:`ALTER TABLE ${quote(table)} ADD CONSTRAINT ${quote(name)} FOREIGN KEY (${quote(column.name)}) REFERENCES ${quote(refTable)} (${quote(refColumn)})${actionsSql}`,check:{kind:"orphans",sql:`SELECT COUNT(*) AS n FROM ${quote(table)} AS child WHERE child.${quote(column.name)} IS NOT NULL AND NOT EXISTS (SELECT 1 FROM ${quote(refTable)} AS parent WHERE parent.${quote(refColumn)} = child.${quote(column.name)})`,dependsOn:[column.name]}})}}return{actions:[...actions,...renames,...creates,...foreignKeys],reports,refused:[]}}function rows(result){return Array.isArray(result)?result:[]}function text(row,key){const value=row?.[key];return value===null||value===void 0?"":String(value)}export async function readPostgresAlignSchema(run){const tables=new Map;for(const row of rows(await run("SELECT table_name FROM information_schema.tables WHERE table_schema = current_schema() AND table_type = 'BASE TABLE'")))tables.set(text(row,"table_name").toLowerCase(),new Map);for(const row of rows(await run("SELECT table_name, column_name, data_type, udt_name, character_maximum_length, is_nullable, column_default FROM information_schema.columns WHERE table_schema = current_schema()"))){const columns=tables.get(text(row,"table_name").toLowerCase());if(!columns)continue;const length=Number(row.character_maximum_length);columns.set(text(row,"column_name").toLowerCase(),{name:text(row,"column_name").toLowerCase(),dataType:text(row,"data_type").toLowerCase()==="user-defined"?"USER-DEFINED":text(row,"data_type").toLowerCase(),udtName:text(row,"udt_name").toLowerCase(),...Number.isFinite(length)&&length>0?{maxLength:length}:{},nullable:text(row,"is_nullable").toUpperCase()==="YES",...row.column_default!==null&&row.column_default!==void 0?{default:String(row.column_default)}:{}})}const indexes=[];for(const row of rows(await run(`
|
|
2
2
|
SELECT t.relname AS table_name, i.relname AS index_name, ix.indisunique AS is_unique, ix.indisprimary AS is_primary,
|
|
3
3
|
pg_get_indexdef(ix.indexrelid) AS definition, con.conname AS constraint_name
|
|
4
4
|
FROM pg_index ix
|
|
@@ -20,5 +20,5 @@ import{comparableIndexKey,indexShape,postgresIndexStatement}from"./index-shape";
|
|
|
20
20
|
JOIN pg_enum e ON e.enumtypid = t.oid
|
|
21
21
|
JOIN pg_namespace n ON n.oid = t.typnamespace
|
|
22
22
|
WHERE n.nspname = current_schema()
|
|
23
|
-
ORDER BY t.typname, e.enumsortorder`))){const type=text(row,"typname").toLowerCase();if(!enums.has(type))enums.set(type,[]);enums.get(type).push(text(row,"enumlabel"))}const names=new Set;for(const row of rows(await run("SELECT c.relname AS name FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname = current_schema()")))names.add(text(row,"name").toLowerCase());for(const row of rows(await run("SELECT con.conname AS name FROM pg_constraint con JOIN pg_namespace n ON n.oid = con.connamespace WHERE n.nspname = current_schema()")))names.add(text(row,"name").toLowerCase());return{tables,indexes,foreignKeys,enums,names}}async function populatedTables(run,tables){const populated=new Set;for(const table of tables)if(rows(await run(`SELECT 1 AS present FROM ${quote(table)} LIMIT 1`)).length>0)populated.add(table);return populated}export async function evaluateAlignChecks(plan,run){const pending=new Set(plan.actions.filter((action)=>action.kind==="add-column").map((action)=>`${action.table}.${action.name}`)),kept=[];for(const action of plan.actions){if(!action.check){kept.push(action);continue}const unmet=action.check.dependsOn?.filter((column)=>pending.has(`${action.table}.${column}`))??[];if(unmet.length>0){plan.reports.push({kind:"unchecked",table:action.table,name:action.name,detail:`${action.detail}: not checked for ${action.check.kind==="orphans"?"orphan rows":"duplicate values"} first, because the probe reads ${unmet.map((column)=>`${action.table}.${column}`).join(", ")}, which this plan adds`});kept.push(action);continue}const count=Number(rows(await run(action.check.sql))[0]?.n??0);if(count>0){plan.refused.push({...action,reason:action.check.kind==="orphans"?`${count} row(s) in ${action.table} reference a missing parent - fix or delete them, then re-run`:`${count} duplicate value(s) in ${action.table} would violate it - de-duplicate them, then re-run`});continue}kept.push(action)}return kept}export async function alignSchema(options={}){const{ensureDatabaseConfigLoaded,resolvedDatabaseConfig}=await import("./utils");await ensureDatabaseConfigLoaded();const dialect=options.dialect??resolvedDatabaseConfig().driver,empty={actions:[],reports:[],refused:[]};if(dialect!=="postgres")return{supported:!1,dialect,plan:empty,applied:[]};const{generateFullModelMigration}=await import("./migrations"),generated=await generateFullModelMigration({dialect});if(!generated.isOk)throw generated.error;const run=options.run??(async(sql)=>{const{db}=await import("./utils");return rows(await db.unsafe(sql).execute())}),desired=desiredSchemaFrom(generated.value.plan,generated.value.statements),live=await readPostgresAlignSchema(run),candidates=[...desired.tables.keys()].filter((table)=>live.tables.has(table));live.populated=await populatedTables(run,candidates);let typeSatisfies;try{const qb=await import("bun-query-builder");if(qb.familyOfDeclared&&qb.familyOfSqlType&&qb.satisfies){const{familyOfDeclared,familyOfSqlType,satisfies}=qb;typeSatisfies=(declared,sqlType)=>satisfies(familyOfDeclared(declared),familyOfSqlType(sqlType),"postgres")}}catch{}const plan=planSchemaAlignment(desired,live,{widenIntegers:options.widenIntegers,relaxConstraints:options.relaxConstraints,typeSatisfies})
|
|
23
|
+
ORDER BY t.typname, e.enumsortorder`))){const type=text(row,"typname").toLowerCase();if(!enums.has(type))enums.set(type,[]);enums.get(type).push(text(row,"enumlabel"))}const names=new Set;for(const row of rows(await run("SELECT c.relname AS name FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname = current_schema()")))names.add(text(row,"name").toLowerCase());for(const row of rows(await run("SELECT con.conname AS name FROM pg_constraint con JOIN pg_namespace n ON n.oid = con.connamespace WHERE n.nspname = current_schema()")))names.add(text(row,"name").toLowerCase());return{tables,indexes,foreignKeys,enums,names}}async function populatedTables(run,tables){const populated=new Set;for(const table of tables)if(rows(await run(`SELECT 1 AS present FROM ${quote(table)} LIMIT 1`)).length>0)populated.add(table);return populated}export async function evaluateAlignChecks(plan,run){const pending=new Set(plan.actions.filter((action)=>action.kind==="add-column").map((action)=>`${action.table}.${action.name}`)),kept=[];for(const action of plan.actions){if(!action.check){kept.push(action);continue}const unmet=action.check.dependsOn?.filter((column)=>pending.has(`${action.table}.${column}`))??[];if(unmet.length>0){plan.reports.push({kind:"unchecked",table:action.table,name:action.name,detail:`${action.detail}: not checked for ${action.check.kind==="orphans"?"orphan rows":"duplicate values"} first, because the probe reads ${unmet.map((column)=>`${action.table}.${column}`).join(", ")}, which this plan adds`});kept.push(action);continue}const count=Number(rows(await run(action.check.sql))[0]?.n??0);if(count>0){plan.refused.push({...action,reason:action.check.kind==="orphans"?`${count} row(s) in ${action.table} reference a missing parent - fix or delete them, then re-run`:`${count} duplicate value(s) in ${action.table} would violate it - de-duplicate them, then re-run`});continue}kept.push(action)}return kept}export async function alignSchema(options={}){const{ensureDatabaseConfigLoaded,resolvedDatabaseConfig}=await import("./utils");await ensureDatabaseConfigLoaded();const dialect=options.dialect??resolvedDatabaseConfig().driver,empty={actions:[],reports:[],refused:[]};if(dialect!=="postgres")return{supported:!1,dialect,plan:empty,applied:[]};const{generateFullModelMigration}=await import("./migrations"),generated=await generateFullModelMigration({dialect});if(!generated.isOk)throw generated.error;const run=options.run??(async(sql)=>{const{db}=await import("./utils");return rows(await db.unsafe(sql).execute())}),desired=desiredSchemaFrom(generated.value.plan,generated.value.statements),live=await readPostgresAlignSchema(run),candidates=[...desired.tables.keys()].filter((table)=>live.tables.has(table));live.populated=await populatedTables(run,candidates);let typeSatisfies;try{const qb=await import("bun-query-builder");if(qb.familyOfDeclared&&qb.familyOfSqlType&&qb.satisfies){const{familyOfDeclared,familyOfSqlType,satisfies}=qb;typeSatisfies=(declared,sqlType)=>satisfies(familyOfDeclared(declared),familyOfSqlType(sqlType),"postgres")}}catch{}const plan=planSchemaAlignment(desired,live,{widenIntegers:options.widenIntegers,relaxConstraints:options.relaxConstraints,typeSatisfies});if(options.only)plan.actions=plan.actions.filter((action)=>options.only.includes(action.kind));const kept=await evaluateAlignChecks(plan,run);plan.actions=kept;if(!options.apply||kept.length===0)return{supported:!0,dialect,plan,applied:[]};const statements=kept.map((action)=>action.sql);await run(`${statements.join(`;
|
|
24
24
|
`)};`);return{supported:!0,dialect,plan,applied:statements}}
|
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.400",
|
|
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.400",
|
|
70
|
+
"@stacksjs/env": "0.75.400",
|
|
71
|
+
"@stacksjs/error-handling": "0.75.400",
|
|
72
|
+
"@stacksjs/faker": "0.75.400",
|
|
73
|
+
"@stacksjs/features": "0.75.400",
|
|
74
|
+
"@stacksjs/logging": "0.75.400",
|
|
75
|
+
"@stacksjs/model-meta": "0.75.400",
|
|
76
|
+
"@stacksjs/path": "0.75.400",
|
|
77
|
+
"@stacksjs/query-builder": "0.75.400",
|
|
78
|
+
"@stacksjs/security": "0.75.400",
|
|
79
|
+
"@stacksjs/storage": "0.75.400",
|
|
80
|
+
"@stacksjs/strings": "0.75.400",
|
|
81
81
|
"@stacksjs/ts-validation": "^0.5.6",
|
|
82
82
|
"bun-query-builder": "^0.3.32",
|
|
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.400",
|
|
87
|
+
"@stacksjs/router": "0.75.400",
|
|
88
|
+
"@stacksjs/utils": "0.75.400",
|
|
89
89
|
"better-dx": "^0.2.24"
|
|
90
90
|
}
|
|
91
91
|
}
|