@stacksjs/database 0.75.256 → 0.75.258

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.
@@ -27,7 +27,13 @@ export declare function getLikeableForeignKey(model: Model, tableName: string):
27
27
  export declare function prepareNumberColumnType(validator: NumberValidatorType, driver?: string): string;
28
28
  // Add new function for enum column types
29
29
  export declare function prepareEnumColumnType(validator: EnumValidatorType, driver?: string): string;
30
- export declare function mapFieldTypeToColumnType(validator: ValidationType, driver?: string): string;
30
+ /**
31
+ * @param declaredType the attribute's own `type`, which wins over the validator.
32
+ * `QueryLog` declares `type: 'text'` on its analysis columns, naming in a
33
+ * comment the very error Postgres raised without it, and nothing read it:
34
+ * the mapper saw only the validation rule (stacksjs/stacks#2965).
35
+ */
36
+ export declare function mapFieldTypeToColumnType(validator: ValidationType, driver?: string, declaredType?: string): string;
31
37
  export declare function checkIsRequired(rule: string): boolean;
32
38
  declare interface Range {
33
39
  min: number
@@ -1 +1 @@
1
- import{log}from"@stacksjs/logging";import{getTableName}from"@stacksjs/model-meta";import{path}from"@stacksjs/path";import{fs,globSync}from"@stacksjs/storage";import{plural,snakeCase}from"@stacksjs/strings";import{db}from"../utils";import{enumValidator,isBooleanValidator,isDatetimeValidator,isDateValidator,isFloatValidator,isNumberValidator,isStringValidator,isTimestampValidator,isUnixValidator}from"../validators";export async function deleteMigrationFiles(){const files=await fs.promises.readdir(path.userMigrationsPath());if(files.length){for(const file of files)if(file.endsWith(".ts")){const migrationPath=path.userMigrationsPath(`${file}`);if(fs.existsSync(migrationPath))await Bun.$`rm ${migrationPath}`}}}export async function deleteFrameworkModels(){const cacheDir=path.frameworkPath("cache/models");if(!fs.existsSync(cacheDir))return;const modelFiles=await fs.promises.readdir(cacheDir);if(modelFiles.length){for(const modelFile of modelFiles)if(modelFile.endsWith(".ts")){const modelPath=path.frameworkPath(`cache/models/${modelFile}`);if(fs.existsSync(modelPath))await Bun.$`rm ${modelPath}`}}}export async function getLastMigrationFields(modelName){const model=(await import(path.frameworkPath(`cache/models/${modelName}`))).default;let fields={};if(typeof model.attributes==="object")fields=model.attributes;else try{fields=JSON.parse(model.attributes||"{}")}catch{fields={}}return fields}export async function modelTableName(model){if(typeof model==="string")model=(await import(model)).default;return model.table??snakeCase(plural(model?.name||""))}export async function hasTableBeenMigrated(tableName){log.debug(`hasTableBeenMigrated for table: ${tableName}`);return(await getExecutedMigrations()).some((migration)=>migration.name.includes(tableName))}export async function hasMigrationBeenCreated(tableName){log.debug(`hasTableBeenMigrated for table: ${tableName}`);return globSync([path.userMigrationsPath("*.ts")],{absolute:!0}).some((path)=>path.includes(`create-${tableName}`))}export async function getExecutedMigrations(){try{return await db.selectFrom("migrations").select("name").execute()}catch(error){if(error?.message.includes("no such table: migrations")){console.warn("Migrations table does not exist, returning empty list.");return[]}return[]}}function findCharacterLength(validator){if("getRules"in validator){const maxLengthRule=validator.getRules().find((rule)=>rule.name==="max");return maxLengthRule?.params?.length||maxLengthRule?.params?.max||255}return 255}export function prepareTextColumnType(validator,driver="mysql"){if(driver==="sqlite")return"'text'";return`'varchar(${findCharacterLength(validator)})'`}export function prepareDateTimeColumnType(validator,driver="mysql"){if(driver==="sqlite")return"'text'";const name=validator.name;if(name==="unix")return"'bigint'";return name||"date"}export function compareRanges(range1,range2){return range1.min===range2.min&&range1.max===range2.max}export async function checkPivotMigration(dynamicPart){return(await fs.promises.readdir(path.userMigrationsPath())).some((migrationFile)=>{const escapedDynamicPart=dynamicPart.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");return new RegExp(`(-${escapedDynamicPart}-)`).test(migrationFile)})}export function pluckChanges(array1,array2){const removed=array1.filter((item)=>!array2.includes(item)),added=array2.filter((item)=>!array1.includes(item));if(removed.length===0&&added.length===0)return null;return{added,removed}}export function arrangeColumns(attributes){if(!attributes)return[];const entries=Object.entries(attributes);entries.sort(([_keyA,valueA],[_keyB,valueB])=>{const orderA=valueA.order??Number.POSITIVE_INFINITY,orderB=valueB.order??Number.POSITIVE_INFINITY;return orderA-orderB});return entries}export function isArrayEqual(arr1,arr2){if(!arr1||!arr2)return!1;if(arr1.length!==arr2.length)return!1;for(let i=0;i<arr1.length;i++)if(arr1[i]!==arr2[i])return!1;return!0}export function findDifferingKeys(obj1,obj2){const differingKeys=[];for(const key in obj1){const before=obj1[key],after=obj2[key];if(before&&after){const lastCharacterLength=findCharacterLength(before.validation.rule),latestCharacterLength=findCharacterLength(after.validation.rule);if(lastCharacterLength!==void 0&&latestCharacterLength!==void 0){if(lastCharacterLength!==latestCharacterLength)differingKeys.push({key,max:latestCharacterLength,min:latestCharacterLength})}}}return differingKeys}export async function fetchTables(){const modelFiles=globSync([path.userModelsPath("*.ts"),path.defaultsPath("app/Models/**/*.ts")],{absolute:!0}),tables=[];for(const modelPath of modelFiles){const model=(await import(modelPath)).default,tableName=getTableName(model,modelPath),upvoteTable=getUpvoteTableName(model,tableName);if(upvoteTable)tables.push(upvoteTable);tables.push(tableName)}return tables}export function getUpvoteTableName(model,tableName){const defaultTable=`${tableName}_likes`,traits=model.traits;if(!traits?.likeable)return;return typeof traits.likeable==="object"?traits.likeable.table||defaultTable:defaultTable}export function getLikeableForeignKey(model,tableName){const likeable=model.traits?.likeable;if(likeable&&typeof likeable==="object"&&!Array.isArray(likeable)&&likeable.foreignKey)return likeable.foreignKey;return`${tableName.replace(/s$/,"")}_id`}export function prepareNumberColumnType(validator,driver="mysql"){if(driver==="sqlite")return"'integer'";if("getRules"in validator){const minRule=validator.getRules().find((rule)=>rule.name==="min"),maxRule=validator.getRules().find((rule)=>rule.name==="max"),min=minRule?.params?.min??-2147483648,max=maxRule?.params?.max??2147483647;return min>=-2147483648&&max<=2147483647?"'integer'":"'bigint'"}return"'integer'"}export function prepareEnumColumnType(validator,driver="mysql"){const allowedValues=validator.getAllowedValues();if(!allowedValues)throw Error("Enum rule found but no allowedValues defined");const enumStructure=allowedValues.map((value)=>`'${value}'`).join(", ");if(driver==="postgres")return"'varchar(255)'";if(driver==="sqlite")return"'text'";return`sql\`enum(${enumStructure})\``}export function mapFieldTypeToColumnType(validator,driver="mysql"){if(enumValidator(validator))return prepareEnumColumnType(validator,driver);if(isStringValidator(validator))return prepareTextColumnType(validator,driver);if(isNumberValidator(validator))return prepareNumberColumnType(validator,driver);if(isBooleanValidator(validator))return"'boolean'";if(isDateValidator(validator))return"'date'";if(isDatetimeValidator(validator))return driver==="postgres"?"'timestamp'":"'datetime'";if(isUnixValidator(validator))return"'bigint'";if(isTimestampValidator(validator))return"'timestamp'";if(isFloatValidator(validator))return"'float4'";if(["array","object"].includes(validator.name))return driver==="sqlite"?"'text'":"'json'";if(driver==="sqlite")return"'text'";return driver==="mysql"?"'varchar(255)'":"'text'"}export function checkIsRequired(rule){return rule.includes(".required()")}
1
+ import{log}from"@stacksjs/logging";import{getTableName}from"@stacksjs/model-meta";import{path}from"@stacksjs/path";import{fs,globSync}from"@stacksjs/storage";import{plural,snakeCase}from"@stacksjs/strings";import{db}from"../utils";import{enumValidator,isBooleanValidator,isDatetimeValidator,isDateValidator,isFloatValidator,isNumberValidator,isStringValidator,isTimestampValidator,isUnixValidator}from"../validators";export async function deleteMigrationFiles(){const files=await fs.promises.readdir(path.userMigrationsPath());if(files.length){for(const file of files)if(file.endsWith(".ts")){const migrationPath=path.userMigrationsPath(`${file}`);if(fs.existsSync(migrationPath))await Bun.$`rm ${migrationPath}`}}}export async function deleteFrameworkModels(){const cacheDir=path.frameworkPath("cache/models");if(!fs.existsSync(cacheDir))return;const modelFiles=await fs.promises.readdir(cacheDir);if(modelFiles.length){for(const modelFile of modelFiles)if(modelFile.endsWith(".ts")){const modelPath=path.frameworkPath(`cache/models/${modelFile}`);if(fs.existsSync(modelPath))await Bun.$`rm ${modelPath}`}}}export async function getLastMigrationFields(modelName){const model=(await import(path.frameworkPath(`cache/models/${modelName}`))).default;let fields={};if(typeof model.attributes==="object")fields=model.attributes;else try{fields=JSON.parse(model.attributes||"{}")}catch{fields={}}return fields}export async function modelTableName(model){if(typeof model==="string")model=(await import(model)).default;return model.table??snakeCase(plural(model?.name||""))}export async function hasTableBeenMigrated(tableName){log.debug(`hasTableBeenMigrated for table: ${tableName}`);return(await getExecutedMigrations()).some((migration)=>migration.name.includes(tableName))}export async function hasMigrationBeenCreated(tableName){log.debug(`hasTableBeenMigrated for table: ${tableName}`);return globSync([path.userMigrationsPath("*.ts")],{absolute:!0}).some((path)=>path.includes(`create-${tableName}`))}export async function getExecutedMigrations(){try{return await db.selectFrom("migrations").select("name").execute()}catch(error){if(error?.message.includes("no such table: migrations")){console.warn("Migrations table does not exist, returning empty list.");return[]}return[]}}function declaredCharacterLength(validator){if("getRules"in validator){const maxLengthRule=validator.getRules().find((rule)=>rule.name==="max"),declared=maxLengthRule?.params?.length??maxLengthRule?.params?.max;return typeof declared==="number"?declared:null}return null}function findCharacterLength(validator){return declaredCharacterLength(validator)??255}export function prepareTextColumnType(validator,driver="mysql"){if(driver==="sqlite")return"'text'";const maxLength=declaredCharacterLength(validator);if(maxLength===null)return driver==="postgres"?"'text'":"'varchar(255)'";return`'varchar(${maxLength})'`}export function prepareDateTimeColumnType(validator,driver="mysql"){if(driver==="sqlite")return"'text'";const name=validator.name;if(name==="unix")return"'bigint'";return name||"date"}export function compareRanges(range1,range2){return range1.min===range2.min&&range1.max===range2.max}export async function checkPivotMigration(dynamicPart){return(await fs.promises.readdir(path.userMigrationsPath())).some((migrationFile)=>{const escapedDynamicPart=dynamicPart.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");return new RegExp(`(-${escapedDynamicPart}-)`).test(migrationFile)})}export function pluckChanges(array1,array2){const removed=array1.filter((item)=>!array2.includes(item)),added=array2.filter((item)=>!array1.includes(item));if(removed.length===0&&added.length===0)return null;return{added,removed}}export function arrangeColumns(attributes){if(!attributes)return[];const entries=Object.entries(attributes);entries.sort(([_keyA,valueA],[_keyB,valueB])=>{const orderA=valueA.order??Number.POSITIVE_INFINITY,orderB=valueB.order??Number.POSITIVE_INFINITY;return orderA-orderB});return entries}export function isArrayEqual(arr1,arr2){if(!arr1||!arr2)return!1;if(arr1.length!==arr2.length)return!1;for(let i=0;i<arr1.length;i++)if(arr1[i]!==arr2[i])return!1;return!0}export function findDifferingKeys(obj1,obj2){const differingKeys=[];for(const key in obj1){const before=obj1[key],after=obj2[key];if(before&&after){const lastCharacterLength=findCharacterLength(before.validation.rule),latestCharacterLength=findCharacterLength(after.validation.rule);if(lastCharacterLength!==void 0&&latestCharacterLength!==void 0){if(lastCharacterLength!==latestCharacterLength)differingKeys.push({key,max:latestCharacterLength,min:latestCharacterLength})}}}return differingKeys}export async function fetchTables(){const modelFiles=globSync([path.userModelsPath("*.ts"),path.defaultsPath("app/Models/**/*.ts")],{absolute:!0}),tables=[];for(const modelPath of modelFiles){const model=(await import(modelPath)).default,tableName=getTableName(model,modelPath),upvoteTable=getUpvoteTableName(model,tableName);if(upvoteTable)tables.push(upvoteTable);tables.push(tableName)}return tables}export function getUpvoteTableName(model,tableName){const defaultTable=`${tableName}_likes`,traits=model.traits;if(!traits?.likeable)return;return typeof traits.likeable==="object"?traits.likeable.table||defaultTable:defaultTable}export function getLikeableForeignKey(model,tableName){const likeable=model.traits?.likeable;if(likeable&&typeof likeable==="object"&&!Array.isArray(likeable)&&likeable.foreignKey)return likeable.foreignKey;return`${tableName.replace(/s$/,"")}_id`}export function prepareNumberColumnType(validator,driver="mysql"){if(driver==="sqlite")return"'integer'";if("getRules"in validator){const minRule=validator.getRules().find((rule)=>rule.name==="min"),maxRule=validator.getRules().find((rule)=>rule.name==="max"),min=minRule?.params?.min??-2147483648,max=maxRule?.params?.max??2147483647;return min>=-2147483648&&max<=2147483647?"'integer'":"'bigint'"}return"'integer'"}export function prepareEnumColumnType(validator,driver="mysql"){const allowedValues=validator.getAllowedValues();if(!allowedValues)throw Error("Enum rule found but no allowedValues defined");const enumStructure=allowedValues.map((value)=>`'${value}'`).join(", ");if(driver==="postgres")return"'varchar(255)'";if(driver==="sqlite")return"'text'";return`sql\`enum(${enumStructure})\``}const DECLARED_COLUMN_TYPES=new Set(["text","json","jsonb","uuid"]);export function mapFieldTypeToColumnType(validator,driver="mysql",declaredType){const declared=declaredType?.trim().toLowerCase();if(declared&&DECLARED_COLUMN_TYPES.has(declared))return`'${declared}'`;if(enumValidator(validator))return prepareEnumColumnType(validator,driver);if(isStringValidator(validator))return prepareTextColumnType(validator,driver);if(isNumberValidator(validator))return prepareNumberColumnType(validator,driver);if(isBooleanValidator(validator))return"'boolean'";if(isDateValidator(validator))return"'date'";if(isDatetimeValidator(validator))return driver==="postgres"?"'timestamp'":"'datetime'";if(isUnixValidator(validator))return"'bigint'";if(isTimestampValidator(validator))return"'timestamp'";if(isFloatValidator(validator))return"'float4'";if(["array","object"].includes(validator.name))return driver==="sqlite"?"'text'":"'json'";if(driver==="sqlite")return"'text'";return driver==="mysql"?"'varchar(255)'":"'text'"}export function checkIsRequired(rule){return rule.includes(".required()")}
@@ -11,7 +11,7 @@ import{log}from"@stacksjs/logging";function italic(str){return`\x1B[3m${str}\x1B
11
11
  `;if(socials.includes("apple"))migrationContent+=` .addColumn('apple_id', 'varchar(255)')
12
12
  `;if(socials.includes("twitter"))migrationContent+=` .addColumn('twitter_id', 'varchar(255)')
13
13
  `;if(socials.includes("facebook"))migrationContent+=` .addColumn('facebook_id', 'varchar(255)')
14
- `}for(const[fieldName,options]of arrangeColumns(model.attributes)){const fieldOptions=options,fieldNameFormatted=snakeCase(fieldName),columnType=mapFieldTypeToColumnType(fieldOptions.validation?.rule);migrationContent+=` .addColumn('${fieldNameFormatted}', ${columnType}`;const isRequired="isRequired"in(fieldOptions.validation?.rule??{})?(fieldOptions.validation?.rule).isRequired:!1;if(isRequired||fieldOptions.unique||fieldOptions.default!==void 0){migrationContent+=", col => col";if(isRequired)migrationContent+=".notNull()";if(fieldOptions.unique)migrationContent+=".unique()";if(fieldOptions.default!==void 0)if(typeof fieldOptions.default==="string")migrationContent+=`.defaultTo('${fieldOptions.default.replace(/'/g,"\\'")}')`;else if(fieldOptions.default===null)migrationContent+=".defaultTo(null)";else migrationContent+=`.defaultTo(${fieldOptions.default})`;migrationContent+=""}migrationContent+=`)
14
+ `}for(const[fieldName,options]of arrangeColumns(model.attributes)){const fieldOptions=options,fieldNameFormatted=snakeCase(fieldName),columnType=mapFieldTypeToColumnType(fieldOptions.validation?.rule,"mysql",fieldOptions.type);migrationContent+=` .addColumn('${fieldNameFormatted}', ${columnType}`;const isRequired="isRequired"in(fieldOptions.validation?.rule??{})?(fieldOptions.validation?.rule).isRequired:!1;if(isRequired||fieldOptions.unique||fieldOptions.default!==void 0){migrationContent+=", col => col";if(isRequired)migrationContent+=".notNull()";if(fieldOptions.unique)migrationContent+=".unique()";if(fieldOptions.default!==void 0)if(typeof fieldOptions.default==="string")migrationContent+=`.defaultTo('${fieldOptions.default.replace(/'/g,"\\'")}')`;else if(fieldOptions.default===null)migrationContent+=".defaultTo(null)";else migrationContent+=`.defaultTo(${fieldOptions.default})`;migrationContent+=""}migrationContent+=`)
15
15
  `}if(twoFactorEnabled!==!1&&twoFactorEnabled)migrationContent+=` .addColumn('two_factor_secret', 'varchar(255)')
16
16
  `;if(useBillable)migrationContent+=` .addColumn('stripe_id', 'varchar(255)')
17
17
  `;if(usePasskey)migrationContent+=` .addColumn('public_passkey', 'varchar(255)')
@@ -6,7 +6,7 @@ import{log}from"@stacksjs/logging";function italic(str){return`\x1B[3m${str}\x1B
6
6
  `;migrationContent+=` .createTable('${tableName}')
7
7
  `;migrationContent+=` .addColumn('id', 'serial', (col) => col.primaryKey())
8
8
  `;if(useUuid)migrationContent+=` .addColumn('uuid', 'uuid', (col) => col.defaultTo(sql.raw('gen_random_uuid()')))
9
- `;for(const[fieldName,options]of arrangeColumns(model.attributes)){const fieldOptions=options,fieldNameFormatted=snakeCase(fieldName),columnType=mapFieldTypeToColumnType(fieldOptions.validation?.rule,"postgres"),isRequired="isRequired"in(fieldOptions.validation?.rule??{})?(fieldOptions.validation?.rule).isRequired:!1;migrationContent+=` .addColumn('${fieldNameFormatted}', ${columnType}`;if(isRequired||fieldOptions.unique||fieldOptions.default!==void 0){migrationContent+=", col => col";if(isRequired)migrationContent+=".notNull()";if(fieldOptions.unique)migrationContent+=".unique()";if(fieldOptions.default!==void 0)if(typeof fieldOptions.default==="string")migrationContent+=`.defaultTo('${fieldOptions.default.replace(/'/g,"\\'")}')`;else if(fieldOptions.default===null)migrationContent+=".defaultTo(null)";else migrationContent+=`.defaultTo(${fieldOptions.default})`;migrationContent+=""}migrationContent+=`)
9
+ `;for(const[fieldName,options]of arrangeColumns(model.attributes)){const fieldOptions=options,fieldNameFormatted=snakeCase(fieldName),columnType=mapFieldTypeToColumnType(fieldOptions.validation?.rule,"postgres",fieldOptions.type),isRequired="isRequired"in(fieldOptions.validation?.rule??{})?(fieldOptions.validation?.rule).isRequired:!1;migrationContent+=` .addColumn('${fieldNameFormatted}', ${columnType}`;if(isRequired||fieldOptions.unique||fieldOptions.default!==void 0){migrationContent+=", col => col";if(isRequired)migrationContent+=".notNull()";if(fieldOptions.unique)migrationContent+=".unique()";if(fieldOptions.default!==void 0)if(typeof fieldOptions.default==="string")migrationContent+=`.defaultTo('${fieldOptions.default.replace(/'/g,"\\'")}')`;else if(fieldOptions.default===null)migrationContent+=".defaultTo(null)";else migrationContent+=`.defaultTo(${fieldOptions.default})`;migrationContent+=""}migrationContent+=`)
10
10
  `}if(useSocials){const socials=model.traits?.useSocials||[];if(socials.includes("google"))migrationContent+=` .addColumn('google_id', 'text')
11
11
  `;if(socials.includes("github"))migrationContent+=` .addColumn('github_id', 'text')
12
12
  `;if(socials.includes("apple"))migrationContent+=` .addColumn('apple_id', 'text')
@@ -75,7 +75,7 @@ import{log}from"@stacksjs/logging";function italic(str){return`\x1B[3m${str}\x1B
75
75
 
76
76
  `;migrationContent+=`export async function up(db: Database<any>) {
77
77
  `;if(fieldsToAdd.length||fieldsToRemove.length){hasChanged=!0;migrationContent+=` await db.schema.alterTable('${tableName}')
78
- `}for(const fieldName of fieldsToAdd){const options=currentFields[fieldName],columnType=mapFieldTypeToColumnType(options.validation?.rule,"postgres"),formattedFieldName=snakeCase(fieldName),isRequired="isRequired"in(options.validation?.rule??{})?(options.validation?.rule).isRequired:!1;migrationContent+=` .addColumn('${formattedFieldName}', '${columnType}'`;if(isRequired||options.unique||options.default!==void 0){migrationContent+=", col => col";if(isRequired)migrationContent+=".notNull()";if(options.unique)migrationContent+=".unique()";if(options.default!==void 0)if(typeof options.default==="string")migrationContent+=`.defaultTo('${options.default}')`;else if(options.default===null)migrationContent+=".defaultTo(null)";else migrationContent+=`.defaultTo(${options.default})`;migrationContent+=""}migrationContent+=`)
78
+ `}for(const fieldName of fieldsToAdd){const options=currentFields[fieldName],columnType=mapFieldTypeToColumnType(options.validation?.rule,"postgres",options.type),formattedFieldName=snakeCase(fieldName),isRequired="isRequired"in(options.validation?.rule??{})?(options.validation?.rule).isRequired:!1;migrationContent+=` .addColumn('${formattedFieldName}', '${columnType}'`;if(isRequired||options.unique||options.default!==void 0){migrationContent+=", col => col";if(isRequired)migrationContent+=".notNull()";if(options.unique)migrationContent+=".unique()";if(options.default!==void 0)if(typeof options.default==="string")migrationContent+=`.defaultTo('${options.default}')`;else if(options.default===null)migrationContent+=".defaultTo(null)";else migrationContent+=`.defaultTo(${options.default})`;migrationContent+=""}migrationContent+=`)
79
79
  `}for(const fieldName of fieldsToRemove)migrationContent+=` .dropColumn('${fieldName}')
80
80
  `;const fieldValidations=findDifferingKeys(lastFields,currentFields);for(const fieldValidation of fieldValidations){hasChanged=!0;const fieldNameFormatted=snakeCase(fieldValidation.key);migrationContent+=` .alterColumn('${fieldNameFormatted}', (col) => col.setDataType('text'))
81
81
  `}const lastFieldOrder=Object.values(lastFields).map((attr)=>attr.order),currentFieldOrder=Object.values(currentFields).map((attr)=>attr.order);if(!isArrayEqual(lastFieldOrder,currentFieldOrder))hasChanged=!0;if(hasChanged){migrationContent+=` .execute()
@@ -11,7 +11,7 @@ import{log}from"@stacksjs/logging";function italic(str){return`\x1B[3m${str}\x1B
11
11
  `;if(socials.includes("apple"))migrationContent+=` .addColumn('apple_id', 'text')
12
12
  `;if(socials.includes("twitter"))migrationContent+=` .addColumn('twitter_id', 'text')
13
13
  `;if(socials.includes("facebook"))migrationContent+=` .addColumn('facebook_id', 'text')
14
- `}for(const[fieldName,options]of arrangeColumns(model.attributes)){const fieldOptions=options,fieldNameFormatted=snakeCase(fieldName),columnType=mapFieldTypeToColumnType(fieldOptions.validation?.rule,"sqlite");migrationContent+=` .addColumn('${fieldNameFormatted}', ${columnType}`;const isRequired=fieldOptions.validation?.rule.isRequired;if(isRequired||fieldOptions.unique||fieldOptions.default!==void 0){migrationContent+=", col => col";if(isRequired)migrationContent+=".notNull()";if(fieldOptions.unique)migrationContent+=".unique()";if(fieldOptions.default!==void 0)if(typeof fieldOptions.default==="string")migrationContent+=`.defaultTo('${fieldOptions.default}')`;else if(fieldOptions.default===null)migrationContent+=".defaultTo(null)";else migrationContent+=`.defaultTo(${fieldOptions.default})`;migrationContent+=""}migrationContent+=`)
14
+ `}for(const[fieldName,options]of arrangeColumns(model.attributes)){const fieldOptions=options,fieldNameFormatted=snakeCase(fieldName),columnType=mapFieldTypeToColumnType(fieldOptions.validation?.rule,"sqlite",fieldOptions.type);migrationContent+=` .addColumn('${fieldNameFormatted}', ${columnType}`;const isRequired=fieldOptions.validation?.rule.isRequired;if(isRequired||fieldOptions.unique||fieldOptions.default!==void 0){migrationContent+=", col => col";if(isRequired)migrationContent+=".notNull()";if(fieldOptions.unique)migrationContent+=".unique()";if(fieldOptions.default!==void 0)if(typeof fieldOptions.default==="string")migrationContent+=`.defaultTo('${fieldOptions.default}')`;else if(fieldOptions.default===null)migrationContent+=".defaultTo(null)";else migrationContent+=`.defaultTo(${fieldOptions.default})`;migrationContent+=""}migrationContent+=`)
15
15
  `}if(twoFactorEnabled!==!1&&twoFactorEnabled)migrationContent+=` .addColumn('two_factor_secret', 'text')
16
16
  `;if(useBillable)migrationContent+=` .addColumn('stripe_id', 'text')
17
17
  `;if(useSoftDeletes)migrationContent+=` .addColumn('deleted_at', 'timestamp')
@@ -60,7 +60,7 @@ import{log}from"@stacksjs/logging";function italic(str){return`\x1B[3m${str}\x1B
60
60
  MODIFY COLUMN ${fieldNameFormatted} TEXT
61
61
  \`.execute(db)
62
62
 
63
- `}for(const fieldName of fieldsToAdd){const options=currentFields[fieldName],columnType=mapFieldTypeToColumnType(options.validation?.rule,"sqlite"),formattedFieldName=snakeCase(fieldName),isRequired=options.validation?.rule.isRequired;migrationContent+=` .addColumn('${formattedFieldName}', ${columnType}`;if(isRequired||options.unique||options.default!==void 0){migrationContent+=", col => col";if(isRequired)migrationContent+=".notNull()";if(options.unique)migrationContent+=".unique()";if(options.default!==void 0)if(typeof options.default==="string")migrationContent+=`.defaultTo('${options.default}')`;else if(options.default===null)migrationContent+=".defaultTo(null)";else migrationContent+=`.defaultTo(${options.default})`;migrationContent+=""}migrationContent+=`)
63
+ `}for(const fieldName of fieldsToAdd){const options=currentFields[fieldName],columnType=mapFieldTypeToColumnType(options.validation?.rule,"sqlite",options.type),formattedFieldName=snakeCase(fieldName),isRequired=options.validation?.rule.isRequired;migrationContent+=` .addColumn('${formattedFieldName}', ${columnType}`;if(isRequired||options.unique||options.default!==void 0){migrationContent+=", col => col";if(isRequired)migrationContent+=".notNull()";if(options.unique)migrationContent+=".unique()";if(options.default!==void 0)if(typeof options.default==="string")migrationContent+=`.defaultTo('${options.default}')`;else if(options.default===null)migrationContent+=".defaultTo(null)";else migrationContent+=`.defaultTo(${options.default})`;migrationContent+=""}migrationContent+=`)
64
64
 
65
65
  `}for(const fieldName of fieldsToRemove)migrationContent+=` .dropColumn('${fieldName}')
66
66
  `;if(fieldsToAdd.length||fieldsToRemove.length)migrationContent+=` .execute();
@@ -19,6 +19,8 @@ export declare function classifyMigrationSql(sql: string, file: string): Dialect
19
19
  export declare function auditMigrationCorpus(options: {
20
20
  dir: string
21
21
  target: MigrationDialect
22
+
23
+ only?: readonly string[]
22
24
  }): MigrationCorpusAudit;
23
25
  /**
24
26
  * The user-facing explanation. Long on purpose: this replaces a raw
@@ -1,4 +1,4 @@
1
1
  import{existsSync,readdirSync,readFileSync}from"node:fs";import{join}from"node:path";const MARKERS=[{dialect:"sqlite",pattern:/\bAUTOINCREMENT\b/i,label:"AUTOINCREMENT"},{dialect:"sqlite",pattern:/\bWITHOUT\s+ROWID\b/i,label:"WITHOUT ROWID"},{dialect:"sqlite",pattern:/\bPRAGMA\b/i,label:"PRAGMA"},{dialect:"mysql",pattern:/\bAUTO_INCREMENT\b/i,label:"AUTO_INCREMENT"},{dialect:"mysql",pattern:/\bENGINE\s*=/i,label:"ENGINE="},{dialect:"postgres",pattern:/\bBIGSERIAL\b/i,label:"BIGSERIAL"},{dialect:"postgres",pattern:/\bSERIAL\b/i,label:"SERIAL"},{dialect:"postgres",pattern:/\bCREATE\s+TYPE\b/i,label:"CREATE TYPE"},{dialect:"postgres",pattern:/\bGENERATED\s+(?:ALWAYS|BY\s+DEFAULT)\s+AS\s+IDENTITY\b/i,label:"GENERATED AS IDENTITY"}];export function stripSqlNoise(sql){let out="",i=0;const blank=(text)=>text.replace(/[^\n]/g," ");while(i<sql.length){const rest=sql.slice(i),line=rest.match(/^--[^\n]*/);if(line){out+=blank(line[0]);i+=line[0].length;continue}if(rest.startsWith("/*")){const end=rest.indexOf("*/"),chunk=end===-1?rest:rest.slice(0,end+2);out+=blank(chunk);i+=chunk.length;continue}const quote=rest[0];if(quote==='"'||quote==="'"||quote==="`"){let j=1;while(j<rest.length&&rest[j]!==quote)j++;const chunk=rest.slice(0,Math.min(j+1,rest.length));out+=blank(chunk);i+=chunk.length;continue}out+=sql[i];i+=1}return out}export function classifyMigrationSql(sql,file){const lines=stripSqlNoise(sql).split(`
2
2
  `),rawLines=sql.split(`
3
- `),found=[];for(let index=0;index<lines.length;index++)for(const{dialect,pattern,label}of MARKERS)if(pattern.test(lines[index]??""))found.push({dialect,marker:label,file,line:index+1,snippet:(rawLines[index]??"").trim().slice(0,120)});return found}export function auditMigrationCorpus(options){const{dir,target}=options;if(!existsSync(dir))return{total:0,inferred:null,incompatible:[],empty:!0};let files;try{files=readdirSync(dir).filter((f)=>f.endsWith(".sql")).sort()}catch{return{total:0,inferred:null,incompatible:[],empty:!0}}const all=[];for(const file of files){let sql;try{sql=readFileSync(join(dir,file),"utf8")}catch{continue}all.push(...classifyMigrationSql(sql,file))}const dialects=new Set(all.map((m)=>m.dialect));return{total:files.length,inferred:dialects.size===1?[...dialects][0]??null:null,incompatible:all.filter((m)=>m.dialect!==target),empty:files.length===0}}export const DIALECT_OVERRIDE_ENV="STACKS_ALLOW_DIALECT_MISMATCH";export function formatMigrationDialectError(audit,target,dir){const affected=new Set(audit.incompatible.map((m)=>m.file)),sample=audit.incompatible.slice(0,3),inferred=audit.inferred?`${audit.inferred}-flavoured`:"written for a different database";return[`The migration files in ${dir} are ${inferred} and cannot run on ${target}.`,`${affected.size} of ${audit.total} files use SQL that ${target} does not accept. For example:`,...sample.map((m)=>` ${m.file}:${m.line} ${m.marker}`),"","Nothing was migrated, so the database is unchanged.","","Stacks ships one set of migration files, and they are emitted for a single database.","Regenerate them from your models:",` ./buddy migrate:regenerate ${target}`,"or point DB_CONNECTION back at the database they were written for.","",`If you know this corpus is correct, re-run with ${DIALECT_OVERRIDE_ENV}=1 to proceed anyway.`].join(`
3
+ `),found=[];for(let index=0;index<lines.length;index++)for(const{dialect,pattern,label}of MARKERS)if(pattern.test(lines[index]??""))found.push({dialect,marker:label,file,line:index+1,snippet:(rawLines[index]??"").trim().slice(0,120)});return found}export function auditMigrationCorpus(options){const{dir,target}=options;if(!existsSync(dir))return{total:0,inferred:null,incompatible:[],empty:!0};let files;try{files=readdirSync(dir).filter((f)=>f.endsWith(".sql")).sort()}catch{return{total:0,inferred:null,incompatible:[],empty:!0}}if(options.only){const judged=new Set(options.only);files=files.filter((file)=>judged.has(file))}const all=[];for(const file of files){let sql;try{sql=readFileSync(join(dir,file),"utf8")}catch{continue}all.push(...classifyMigrationSql(sql,file))}const dialects=new Set(all.map((m)=>m.dialect));return{total:files.length,inferred:dialects.size===1?[...dialects][0]??null:null,incompatible:all.filter((m)=>m.dialect!==target),empty:files.length===0}}export const DIALECT_OVERRIDE_ENV="STACKS_ALLOW_DIALECT_MISMATCH";export function formatMigrationDialectError(audit,target,dir){const affected=new Set(audit.incompatible.map((m)=>m.file)),sample=audit.incompatible.slice(0,3),inferred=audit.inferred?`${audit.inferred}-flavoured`:"written for a different database";return[`The migration files in ${dir} are ${inferred} and cannot run on ${target}.`,`${affected.size} of ${audit.total} files use SQL that ${target} does not accept. For example:`,...sample.map((m)=>` ${m.file}:${m.line} ${m.marker}`),"","Nothing was migrated, so the database is unchanged.","","Stacks ships one set of migration files, and they are emitted for a single database.","Regenerate them from your models:",` ./buddy migrate:regenerate ${target}`,"or point DB_CONNECTION back at the database they were written for.","",`If you know this corpus is correct, re-run with ${DIALECT_OVERRIDE_ENV}=1 to proceed anyway.`].join(`
4
4
  `)}
@@ -39,6 +39,14 @@ export declare function readPostgresAlignSchema(run: AlignSqlRunner): Promise<Li
39
39
  * multi-statement string, which Postgres executes as a single implicit
40
40
  * transaction - all of it lands, or none of it does.
41
41
  */
42
+ /**
43
+ * Run each action's pre-flight check and sort the survivors from the refused.
44
+ *
45
+ * Separate from `alignSchema` so it can be exercised without a database: the
46
+ * decisions here are about what a probe's answer means, including the case
47
+ * where there is no answer to have.
48
+ */
49
+ export declare function evaluateAlignChecks(plan: AlignmentPlan, run: AlignSqlRunner): Promise<AlignAction[]>;
42
50
  export declare function alignSchema(options?: {
43
51
  apply?: boolean
44
52
  widenIntegers?: boolean
@@ -111,7 +119,7 @@ export declare interface AlignAction {
111
119
  name: string
112
120
  detail: string
113
121
  sql: string
114
- check?: { kind: 'duplicates', sql: string } | { kind: 'orphans', sql: string }
122
+ check?: ({ kind: 'duplicates', sql: string } | { kind: 'orphans', sql: string }) & { dependsOn?: string[] }
115
123
  }
116
124
  export declare interface AlignReport {
117
125
  kind: AlignReportKind
@@ -132,7 +140,7 @@ export declare interface AlignResult {
132
140
  }
133
141
  /** Runs one SQL string and returns its rows (see `migration-ledger.ts`). */
134
142
  export type AlignSqlRunner = (sql: string) => Promise<any[]>;
135
- export type AlignActionKind = 'add-enum-value' | 'add-column' | 'widen-column' | 'rename-index' | 'create-index' | 'add-foreign-key';
143
+ export type AlignActionKind = 'create-enum-type' | 'add-enum-value' | 'add-column' | 'widen-column' | 'rename-index' | 'create-index' | 'add-foreign-key';
136
144
  export type AlignReportKind = | 'missing-table'
137
145
  | 'missing-column'
138
146
  | 'extra-column'
@@ -144,4 +152,5 @@ export type AlignReportKind = | 'missing-table'
144
152
  | 'duplicate-index'
145
153
  | 'name-conflict'
146
154
  | 'enum'
147
- | 'foreign-key';
155
+ | 'foreign-key'
156
+ | 'unchecked';
@@ -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)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 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&&current.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&&current.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&&current.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)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`}}creates.push(action)}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);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)})`}})}}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";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&&current.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&&current.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&&current.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)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 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);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 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,typeSatisfies}),kept=[];for(const action of plan.actions){if(!action.check){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)}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(`;
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,typeSatisfies}),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}}
@@ -1 +1 @@
1
- export function isStringValidator(v){return v.name==="string"}export function isNumberValidator(v){return v.name==="number"}export function enumValidator(v){return v.name==="enum"}export function isBooleanValidator(v){return v.name==="boolean"}export function isDateValidator(v){return v.name==="date"}export function isUnixValidator(v){return v.name==="unix"}export function isFloatValidator(v){return v.name==="float"}export function isDatetimeValidator(v){return v.name==="datetime"}export function isTimestampValidator(v){return v.name==="timestamp"}export function isTimestampTzValidator(v){return v.name==="timestampTz"}export function isDecimalValidator(v){return v.name==="decimal"}export function isSmallintValidator(v){return v.name==="smallint"}export function isIntegerValidator(v){return v.name==="integer"}export function isBigintValidator(v){return v.name==="bigint"}export function isBinaryValidator(v){return v.name==="binary"}export function isBlobValidator(v){return v.name==="blob"}export function isJsonValidator(v){return v.name==="json"}export function checkValidator(validator,driver){if(enumValidator(validator))return prepareEnumColumnType(validator,driver);if(isStringValidator(validator))return prepareTextColumnType(validator,driver);if(isNumberValidator(validator))return prepareNumberColumnType(validator,driver);if(isBooleanValidator(validator))return"'boolean'";if(isDateValidator(validator))return"'date'";if(isDatetimeValidator(validator))return"'datetime'";if(isUnixValidator(validator))return"'bigint'";if(isTimestampValidator(validator))return"'timestamp'";if(isTimestampTzValidator(validator))return"'timestamp'";if(isFloatValidator(validator))return"'float'";if(isSmallintValidator(validator))return"'smallint'";if(isDecimalValidator(validator))return"'decimal'";if(isIntegerValidator(validator))return"'integer'";if(isBigintValidator(validator))return"'bigint'";if(isBinaryValidator(validator))return"'binary'";return""}export function prepareNumberColumnType(validator,driver="mysql"){if(driver==="sqlite")return"'integer'";if("getRules"in validator){const minRule=validator.getRules().find((rule)=>rule.name==="min"),maxRule=validator.getRules().find((rule)=>rule.name==="max"),min=minRule?.params?.min??-2147483648,max=maxRule?.params?.max??2147483647;return min>=-2147483648&&max<=2147483647?"'integer'":"'bigint'"}return"'integer'"}export function prepareEnumColumnType(validator,driver="mysql"){const allowedValues=validator.getAllowedValues();if(!allowedValues)throw Error("Enum rule found but no allowedValues defined");const enumStructure=allowedValues.map((value)=>`'${value}'`).join(", ");if(driver==="sqlite")return"'text'";return`sql\`enum(${enumStructure})\``}export function prepareTextColumnType(validator,driver="mysql"){if(driver==="sqlite")return"'text'";return`'varchar(${findCharacterLength(validator)})'`}export function prepareDateTimeColumnType(validator,driver="mysql"){if(driver==="sqlite")return"'text'";const name=validator.name;if(name==="unix")return"'bigint'";return name||"date"}export function findCharacterLength(validator){if("getRules"in validator){const maxLengthRule=validator.getRules().find((rule)=>rule.name==="max");return maxLengthRule?.params?.length||maxLengthRule?.params?.max||255}return 255}
1
+ export function isStringValidator(v){return v.name==="string"}export function isNumberValidator(v){return v.name==="number"}export function enumValidator(v){return v.name==="enum"}export function isBooleanValidator(v){return v.name==="boolean"}export function isDateValidator(v){return v.name==="date"}export function isUnixValidator(v){return v.name==="unix"}export function isFloatValidator(v){return v.name==="float"}export function isDatetimeValidator(v){return v.name==="datetime"}export function isTimestampValidator(v){return v.name==="timestamp"}export function isTimestampTzValidator(v){return v.name==="timestampTz"}export function isDecimalValidator(v){return v.name==="decimal"}export function isSmallintValidator(v){return v.name==="smallint"}export function isIntegerValidator(v){return v.name==="integer"}export function isBigintValidator(v){return v.name==="bigint"}export function isBinaryValidator(v){return v.name==="binary"}export function isBlobValidator(v){return v.name==="blob"}export function isJsonValidator(v){return v.name==="json"}export function checkValidator(validator,driver){if(enumValidator(validator))return prepareEnumColumnType(validator,driver);if(isStringValidator(validator))return prepareTextColumnType(validator,driver);if(isNumberValidator(validator))return prepareNumberColumnType(validator,driver);if(isBooleanValidator(validator))return"'boolean'";if(isDateValidator(validator))return"'date'";if(isDatetimeValidator(validator))return"'datetime'";if(isUnixValidator(validator))return"'bigint'";if(isTimestampValidator(validator))return"'timestamp'";if(isTimestampTzValidator(validator))return"'timestamp'";if(isFloatValidator(validator))return"'float'";if(isSmallintValidator(validator))return"'smallint'";if(isDecimalValidator(validator))return"'decimal'";if(isIntegerValidator(validator))return"'integer'";if(isBigintValidator(validator))return"'bigint'";if(isBinaryValidator(validator))return"'binary'";return""}export function prepareNumberColumnType(validator,driver="mysql"){if(driver==="sqlite")return"'integer'";if("getRules"in validator){const minRule=validator.getRules().find((rule)=>rule.name==="min"),maxRule=validator.getRules().find((rule)=>rule.name==="max"),min=minRule?.params?.min??-2147483648,max=maxRule?.params?.max??2147483647;return min>=-2147483648&&max<=2147483647?"'integer'":"'bigint'"}return"'integer'"}export function prepareEnumColumnType(validator,driver="mysql"){const allowedValues=validator.getAllowedValues();if(!allowedValues)throw Error("Enum rule found but no allowedValues defined");const enumStructure=allowedValues.map((value)=>`'${value}'`).join(", ");if(driver==="sqlite")return"'text'";return`sql\`enum(${enumStructure})\``}export function prepareTextColumnType(validator,driver="mysql"){if(driver==="sqlite")return"'text'";const maxLength=declaredCharacterLength(validator);if(maxLength===null)return driver==="postgres"?"'text'":"'varchar(255)'";return`'varchar(${maxLength})'`}export function prepareDateTimeColumnType(validator,driver="mysql"){if(driver==="sqlite")return"'text'";const name=validator.name;if(name==="unix")return"'bigint'";return name||"date"}function declaredCharacterLength(validator){if("getRules"in validator){const maxLengthRule=validator.getRules().find((rule)=>rule.name==="max"),declared=maxLengthRule?.params?.length??maxLengthRule?.params?.max;return typeof declared==="number"?declared:null}return null}export function findCharacterLength(validator){if("getRules"in validator){const maxLengthRule=validator.getRules().find((rule)=>rule.name==="max");return maxLengthRule?.params?.length||maxLengthRule?.params?.max||255}return 255}
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.256",
5
+ "version": "0.75.258",
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.256",
70
- "@stacksjs/env": "0.75.256",
71
- "@stacksjs/error-handling": "0.75.256",
72
- "@stacksjs/faker": "0.75.256",
73
- "@stacksjs/features": "0.75.256",
74
- "@stacksjs/logging": "0.75.256",
75
- "@stacksjs/model-meta": "0.75.256",
76
- "@stacksjs/path": "0.75.256",
77
- "@stacksjs/query-builder": "0.75.256",
78
- "@stacksjs/security": "0.75.256",
79
- "@stacksjs/storage": "0.75.256",
80
- "@stacksjs/strings": "0.75.256",
69
+ "@stacksjs/config": "0.75.258",
70
+ "@stacksjs/env": "0.75.258",
71
+ "@stacksjs/error-handling": "0.75.258",
72
+ "@stacksjs/faker": "0.75.258",
73
+ "@stacksjs/features": "0.75.258",
74
+ "@stacksjs/logging": "0.75.258",
75
+ "@stacksjs/model-meta": "0.75.258",
76
+ "@stacksjs/path": "0.75.258",
77
+ "@stacksjs/query-builder": "0.75.258",
78
+ "@stacksjs/security": "0.75.258",
79
+ "@stacksjs/storage": "0.75.258",
80
+ "@stacksjs/strings": "0.75.258",
81
81
  "@stacksjs/ts-validation": "^0.5.6",
82
82
  "bun-query-builder": "^0.3.9",
83
83
  "dynamodb-tooling": "^0.3.6"
84
84
  },
85
85
  "devDependencies": {
86
- "@stacksjs/cli": "0.75.256",
87
- "@stacksjs/router": "0.75.256",
88
- "@stacksjs/utils": "0.75.256",
86
+ "@stacksjs/cli": "0.75.258",
87
+ "@stacksjs/router": "0.75.258",
88
+ "@stacksjs/utils": "0.75.258",
89
89
  "better-dx": "^0.2.24"
90
90
  }
91
91
  }