@stacksjs/database 0.75.35 → 0.75.37

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.
@@ -28,6 +28,22 @@ export declare function renameIdentifierPart(identifier: string, from: string, t
28
28
  * leftover old entry is only ever read back as a table to drop.
29
29
  */
30
30
  export declare function applyFrameworkRenamesToPlan(plan: MigrationPlan, renames?: readonly FrameworkRename[]): PlanRenameResult;
31
+ /**
32
+ * The double-quoted names in a stored `CREATE INDEX` key list that are not
33
+ * columns of the indexed table.
34
+ *
35
+ * SQLite reads a double-quoted name that matches no column as a string
36
+ * literal, so `CREATE UNIQUE INDEX users_users_uuid_unique ON users ("uuid")`
37
+ * run before `users.uuid` exists succeeds, and indexes the constant 'uuid'.
38
+ * Every `ALTER TABLE ... RENAME` then re-checks the whole schema with that
39
+ * leniency off and fails on it - "error in index users_users_uuid_unique
40
+ * after rename: no such column: uuid" - however unrelated the renamed table
41
+ * is. As a unique index on a constant it also lets the table hold one row.
42
+ *
43
+ * Returns [] for anything it cannot read as a plain key list, so an index it
44
+ * does not understand is left alone.
45
+ */
46
+ export declare function sqliteLiteralIndexColumns(createSql: string, tableColumns: Iterable<string>): string[];
31
47
  /**
32
48
  * Bring a live database up to the renamed schema. Idempotent: a second run on
33
49
  * the same database applies nothing.
@@ -1 +1 @@
1
- export const FRAMEWORK_RENAMES=[{id:"driver-to-courier",reference:"stacksjs/stacks#2382",tables:[{from:"drivers",to:"couriers"},{from:"driver_pings",to:"courier_pings"}],columns:[{table:"courier_pings",from:"driver_id",to:"courier_id"},{table:"delivery_routes",from:"driver",to:"courier"},{table:"delivery_routes",from:"driver_id",to:"courier_id"}]}];export function applicableFrameworkRenames(declaredTables,renames=FRAMEWORK_RENAMES){const declared=new Set([...declaredTables].map((table)=>table.toLowerCase()));return renames.filter((group)=>!group.tables.some((rename)=>declared.has(rename.from.toLowerCase())))}export function renameIdentifierPart(identifier,from,to){const escaped=from.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");return identifier.replace(new RegExp(`(^|_)${escaped}(?=_|$)`,"g"),(_match,lead)=>`${lead}${to}`)}export function applyFrameworkRenamesToPlan(plan,renames=FRAMEWORK_RENAMES){const next=structuredClone(plan),changes=[];for(const group of renames){for(const{from,to}of group.tables){const oldIndex=next.tables.findIndex((table)=>table.table===from);if(oldIndex!==-1)if(next.tables.some((table)=>table.table===to)){next.tables.splice(oldIndex,1);changes.push(`removed stale ${from} (the snapshot already describes ${to})`)}else{const entry=next.tables[oldIndex];entry.table=to;for(const index of entry.indexes??[])index.name=renameIdentifierPart(index.name,from,to);changes.push(`${from} -> ${to}`)}for(const table of next.tables)for(const column of table.columns)if(column.references?.table===from){column.references.table=to;changes.push(`${table.table}.${column.name} references ${to}`)}}for(const{table:tableName,from,to}of group.columns){const table=next.tables.find((entry)=>entry.table===tableName);if(!table)continue;const oldIndex=table.columns.findIndex((column)=>column.name===from);if(oldIndex===-1)continue;if(table.columns.some((column)=>column.name===to)){table.columns.splice(oldIndex,1);changes.push(`removed stale ${tableName}.${from} (the snapshot already describes ${to})`)}else{table.columns[oldIndex].name=to;changes.push(`${tableName}.${from} -> ${to}`)}for(const index of table.indexes??[])index.columns=index.columns.map((column)=>column===from?to:column)}}return{plan:next,changes}}export class FrameworkRenameConflictError extends Error{constructor(message){super(message);this.name="FrameworkRenameConflictError"}}function quote(dialect,identifier){return dialect==="mysql"?`\`${identifier.replace(/`/g,"``")}\``:`"${identifier.replace(/"/g,'""')}"`}function firstValue(row){if(!row||typeof row!=="object")return;return Object.values(row)[0]}function pickString(row,...keys){for(const key of keys){const value=row?.[key]??row?.[key.toLowerCase()]??row?.[key.toUpperCase()];if(typeof value==="string")return value}return""}function catalog(dialect,run){const literal=(value)=>`'${value.replace(/'/g,"''")}'`;async function count(sql){const rows=await run(sql);return Number(firstValue(rows[0])??0)}return{async tableExists(table){if(dialect==="sqlite")return await count(`SELECT COUNT(*) AS n FROM sqlite_master WHERE type = 'table' AND name = ${literal(table)}`)>0;if(dialect==="mysql")return await count(`SELECT COUNT(*) AS n FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ${literal(table)}`)>0;return await count(`SELECT COUNT(*) AS n FROM information_schema.tables WHERE table_schema = current_schema() AND table_name = ${literal(table)}`)>0},async columnExists(table,column){if(dialect==="sqlite")return await count(`SELECT COUNT(*) AS n FROM pragma_table_info(${literal(table)}) WHERE name = ${literal(column)}`)>0;if(dialect==="mysql")return await count(`SELECT COUNT(*) AS n FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ${literal(table)} AND COLUMN_NAME = ${literal(column)}`)>0;return await count(`SELECT COUNT(*) AS n FROM information_schema.columns WHERE table_schema = current_schema() AND table_name = ${literal(table)} AND column_name = ${literal(column)}`)>0},async rowCount(table){return count(`SELECT COUNT(*) AS n FROM ${quote(dialect,table)}`)},async sqliteIndexes(table){return(await run(`SELECT name, sql FROM sqlite_master WHERE type = 'index' AND tbl_name = ${literal(table)} AND sql IS NOT NULL`)).map((row)=>({name:pickString(row,"name"),sql:pickString(row,"sql")})).filter((index)=>index.name&&index.sql)}}}export async function applyFrameworkRenamesToDatabase(run,dialect,renames=FRAMEWORK_RENAMES){const result={applied:[],skipped:[]},db=catalog(dialect,run),q=(identifier)=>quote(dialect,identifier);let foreignKeysChecked=!1;const ensureSqliteForeignKeys=async()=>{if(dialect!=="sqlite"||foreignKeysChecked)return;await run("PRAGMA foreign_keys = ON");if(Number(firstValue((await run("PRAGMA foreign_keys"))[0])??0)!==1)throw Error("Cannot apply framework table renames: SQLite foreign keys could not be enabled on this connection, and renaming with them off leaves other tables referencing the old table name.");foreignKeysChecked=!0};for(const group of renames){const plans=[];for(const{from,to}of group.tables){if(!await db.tableExists(from)){plans.push({from,to,action:"skip"});continue}if(!await db.tableExists(to)){plans.push({from,to,action:"rename"});continue}const oldRows=await db.rowCount(from),newRows=await db.rowCount(to);if(oldRows>0&&newRows>0)throw new FrameworkRenameConflictError(`${from} (${oldRows} row${oldRows===1?"":"s"}) and ${to} (${newRows} row${newRows===1?"":"s"}) both hold data, so the ${group.id} rename (${group.reference}) cannot tell which is real. Merge them by hand, leaving the rows in ${to} and ${from} empty or dropped, then run migrate again.`);if(dialect!=="sqlite"){plans.push({from,to,action:"skip",reason:`${from} and ${to} both exist; ${newRows>0?`drop the empty ${from}`:`drop the empty ${to} and rename ${from} to ${to}`} by hand (${group.reference})`});continue}plans.push({from,to,action:newRows>0?"drop-empty-old":"replace-empty-new"})}for(const plan of plans){if(plan.action==="skip"){if(plan.reason)result.skipped.push(plan.reason);continue}await ensureSqliteForeignKeys();if(plan.action==="drop-empty-old"){await run(`DROP TABLE ${q(plan.from)}`);result.applied.push(`dropped empty legacy ${plan.from} (${plan.to} holds the rows)`);continue}if(plan.action==="replace-empty-new"){await run(`DROP TABLE ${q(plan.to)}`);result.applied.push(`dropped empty ${plan.to} to make way for ${plan.from}`)}await run(`ALTER TABLE ${q(plan.from)} RENAME TO ${q(plan.to)}`);result.applied.push(`renamed ${plan.from} -> ${plan.to}`)}if(dialect==="sqlite")for(const{from,to}of group.tables){if(!await db.tableExists(to))continue;for(const index of await db.sqliteIndexes(to)){const renamed=renameIdentifierPart(index.name,from,to);if(renamed===index.name)continue;if((await db.sqliteIndexes(to)).some((existing)=>existing.name===renamed)){result.skipped.push(`index ${index.name} left as is: ${renamed} already exists`);continue}const createSql=replaceIndexName(index.sql,index.name,renamed);if(!createSql){result.skipped.push(`index ${index.name} left as is: could not read its definition`);continue}await ensureSqliteForeignKeys();await run(createSql);await run(`DROP INDEX ${q(index.name)}`);result.applied.push(`renamed index ${index.name} -> ${renamed}`)}}for(const{table,from,to}of group.columns){if(!await db.tableExists(table)||!await db.columnExists(table,from))continue;if(await db.columnExists(table,to)){result.skipped.push(`${table} has both ${from} and ${to}; ${from} is left in place, move its values by hand if it holds any (${group.reference})`);continue}await ensureSqliteForeignKeys();await run(`ALTER TABLE ${q(table)} RENAME COLUMN ${q(from)} TO ${q(to)}`);result.applied.push(`renamed ${table}.${from} -> ${to}`)}}return result}export function replaceIndexName(createSql,from,to){const escaped=from.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),pattern=new RegExp(`^(\\s*CREATE\\s+(?:UNIQUE\\s+)?INDEX\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?)(["\`\\[]?)${escaped}(["\`\\]]?)(\\s)`,"i");if(!pattern.test(createSql))return;return createSql.replace(pattern,(_match,head,open,close,space)=>`${head}${open}${to}${close}${space}`)}
1
+ export const FRAMEWORK_RENAMES=[{id:"driver-to-courier",reference:"stacksjs/stacks#2382",tables:[{from:"drivers",to:"couriers"},{from:"driver_pings",to:"courier_pings"}],columns:[{table:"courier_pings",from:"driver_id",to:"courier_id"},{table:"delivery_routes",from:"driver",to:"courier"},{table:"delivery_routes",from:"driver_id",to:"courier_id"}]}];export function applicableFrameworkRenames(declaredTables,renames=FRAMEWORK_RENAMES){const declared=new Set([...declaredTables].map((table)=>table.toLowerCase()));return renames.filter((group)=>!group.tables.some((rename)=>declared.has(rename.from.toLowerCase())))}export function renameIdentifierPart(identifier,from,to){const escaped=from.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");return identifier.replace(new RegExp(`(^|_)${escaped}(?=_|$)`,"g"),(_match,lead)=>`${lead}${to}`)}export function applyFrameworkRenamesToPlan(plan,renames=FRAMEWORK_RENAMES){const next=structuredClone(plan),changes=[];for(const group of renames){for(const{from,to}of group.tables){const oldIndex=next.tables.findIndex((table)=>table.table===from);if(oldIndex!==-1)if(next.tables.some((table)=>table.table===to)){next.tables.splice(oldIndex,1);changes.push(`removed stale ${from} (the snapshot already describes ${to})`)}else{const entry=next.tables[oldIndex];entry.table=to;for(const index of entry.indexes??[])index.name=renameIdentifierPart(index.name,from,to);changes.push(`${from} -> ${to}`)}for(const table of next.tables)for(const column of table.columns)if(column.references?.table===from){column.references.table=to;changes.push(`${table.table}.${column.name} references ${to}`)}}for(const{table:tableName,from,to}of group.columns){const table=next.tables.find((entry)=>entry.table===tableName);if(!table)continue;const oldIndex=table.columns.findIndex((column)=>column.name===from);if(oldIndex===-1)continue;if(table.columns.some((column)=>column.name===to)){table.columns.splice(oldIndex,1);changes.push(`removed stale ${tableName}.${from} (the snapshot already describes ${to})`)}else{table.columns[oldIndex].name=to;changes.push(`${tableName}.${from} -> ${to}`)}for(const index of table.indexes??[])index.columns=index.columns.map((column)=>column===from?to:column)}}return{plan:next,changes}}export class FrameworkRenameConflictError extends Error{constructor(message){super(message);this.name="FrameworkRenameConflictError"}}function quote(dialect,identifier){return dialect==="mysql"?`\`${identifier.replace(/`/g,"``")}\``:`"${identifier.replace(/"/g,'""')}"`}function firstValue(row){if(!row||typeof row!=="object")return;return Object.values(row)[0]}function pickString(row,...keys){for(const key of keys){const value=row?.[key]??row?.[key.toLowerCase()]??row?.[key.toUpperCase()];if(typeof value==="string")return value}return""}function catalog(dialect,run){const literal=(value)=>`'${value.replace(/'/g,"''")}'`;async function count(sql){const rows=await run(sql);return Number(firstValue(rows[0])??0)}return{async tableExists(table){if(dialect==="sqlite")return await count(`SELECT COUNT(*) AS n FROM sqlite_master WHERE type = 'table' AND name = ${literal(table)}`)>0;if(dialect==="mysql")return await count(`SELECT COUNT(*) AS n FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ${literal(table)}`)>0;return await count(`SELECT COUNT(*) AS n FROM information_schema.tables WHERE table_schema = current_schema() AND table_name = ${literal(table)}`)>0},async columnExists(table,column){if(dialect==="sqlite")return await count(`SELECT COUNT(*) AS n FROM pragma_table_info(${literal(table)}) WHERE name = ${literal(column)}`)>0;if(dialect==="mysql")return await count(`SELECT COUNT(*) AS n FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ${literal(table)} AND COLUMN_NAME = ${literal(column)}`)>0;return await count(`SELECT COUNT(*) AS n FROM information_schema.columns WHERE table_schema = current_schema() AND table_name = ${literal(table)} AND column_name = ${literal(column)}`)>0},async rowCount(table){return count(`SELECT COUNT(*) AS n FROM ${quote(dialect,table)}`)},async sqliteIndexes(table){return(await run(`SELECT name, sql FROM sqlite_master WHERE type = 'index' AND tbl_name = ${literal(table)} AND sql IS NOT NULL`)).map((row)=>({name:pickString(row,"name"),sql:pickString(row,"sql")})).filter((index)=>index.name&&index.sql)},async sqliteAllIndexes(){return(await run("SELECT name, tbl_name, sql FROM sqlite_master WHERE type = 'index' AND sql IS NOT NULL")).map((row)=>({name:pickString(row,"name"),table:pickString(row,"tbl_name"),sql:pickString(row,"sql")})).filter((index)=>index.name&&index.table&&index.sql)},async sqliteColumns(table){return(await run(`SELECT name FROM pragma_table_info(${literal(table)})`)).map((row)=>pickString(row,"name")).filter(Boolean)}}}export function sqliteLiteralIndexColumns(createSql,tableColumns){const on=/\bON\s+(?:"(?:[^"]|"")+"|`[^`]+`|\[[^\]]+\]|[\w$]+)\s*\(/i.exec(createSql);if(!on)return[];const terms=[];let depth=1,quote,term="";for(let i=on.index+on[0].length;i<createSql.length;i++){const char=createSql[i];if(quote){term+=char;if(char===quote)quote=void 0;continue}if(char==='"'||char==="'"||char==="`"){quote=char;term+=char;continue}if(char==="(")depth++;if(char===")"&&--depth===0){terms.push(term);break}if(char===","&&depth===1){terms.push(term);term="";continue}term+=char}if(depth!==0)return[];const known=new Set([...tableColumns].map((column)=>column.toLowerCase())),missing=[];for(const raw of terms){const match=/^\s*"((?:[^"]|"")+)"(?:\s+COLLATE\s+(?:"[^"]*"|\w+))?(?:\s+(?:ASC|DESC))?\s*$/i.exec(raw);if(!match)continue;const name=match[1].replace(/""/g,'"');if(!known.has(name.toLowerCase()))missing.push(name)}return missing}export async function applyFrameworkRenamesToDatabase(run,dialect,renames=FRAMEWORK_RENAMES){const result={applied:[],skipped:[]},db=catalog(dialect,run),q=(identifier)=>quote(dialect,identifier);let foreignKeysChecked=!1;const ensureSqliteForeignKeys=async()=>{if(dialect!=="sqlite"||foreignKeysChecked)return;await run("PRAGMA foreign_keys = ON");if(Number(firstValue((await run("PRAGMA foreign_keys"))[0])??0)!==1)throw Error("Cannot apply framework table renames: SQLite foreign keys could not be enabled on this connection, and renaming with them off leaves other tables referencing the old table name.");for(const index of await db.sqliteAllIndexes()){const missing=sqliteLiteralIndexColumns(index.sql,await db.sqliteColumns(index.table));if(missing.length===0)continue;await run(`DROP INDEX ${q(index.name)}`);result.applied.push(`dropped index ${index.name}: ${index.table} has no column ${missing.join(", ")}, so SQLite had indexed ${missing.length===1?"that name":"those names"} as a string constant, which blocks every rename`)}foreignKeysChecked=!0};for(const group of renames){const plans=[];for(const{from,to}of group.tables){if(!await db.tableExists(from)){plans.push({from,to,action:"skip"});continue}if(!await db.tableExists(to)){plans.push({from,to,action:"rename"});continue}const oldRows=await db.rowCount(from),newRows=await db.rowCount(to);if(oldRows>0&&newRows>0)throw new FrameworkRenameConflictError(`${from} (${oldRows} row${oldRows===1?"":"s"}) and ${to} (${newRows} row${newRows===1?"":"s"}) both hold data, so the ${group.id} rename (${group.reference}) cannot tell which is real. Merge them by hand, leaving the rows in ${to} and ${from} empty or dropped, then run migrate again.`);if(dialect!=="sqlite"){plans.push({from,to,action:"skip",reason:`${from} and ${to} both exist; ${newRows>0?`drop the empty ${from}`:`drop the empty ${to} and rename ${from} to ${to}`} by hand (${group.reference})`});continue}plans.push({from,to,action:newRows>0?"drop-empty-old":"replace-empty-new"})}for(const plan of plans){if(plan.action==="skip"){if(plan.reason)result.skipped.push(plan.reason);continue}await ensureSqliteForeignKeys();if(plan.action==="drop-empty-old"){await run(`DROP TABLE ${q(plan.from)}`);result.applied.push(`dropped empty legacy ${plan.from} (${plan.to} holds the rows)`);continue}if(plan.action==="replace-empty-new"){await run(`DROP TABLE ${q(plan.to)}`);result.applied.push(`dropped empty ${plan.to} to make way for ${plan.from}`)}await run(`ALTER TABLE ${q(plan.from)} RENAME TO ${q(plan.to)}`);result.applied.push(`renamed ${plan.from} -> ${plan.to}`)}if(dialect==="sqlite")for(const{from,to}of group.tables){if(!await db.tableExists(to))continue;for(const index of await db.sqliteIndexes(to)){const renamed=renameIdentifierPart(index.name,from,to);if(renamed===index.name)continue;if((await db.sqliteIndexes(to)).some((existing)=>existing.name===renamed)){result.skipped.push(`index ${index.name} left as is: ${renamed} already exists`);continue}const createSql=replaceIndexName(index.sql,index.name,renamed);if(!createSql){result.skipped.push(`index ${index.name} left as is: could not read its definition`);continue}await ensureSqliteForeignKeys();await run(createSql);await run(`DROP INDEX ${q(index.name)}`);result.applied.push(`renamed index ${index.name} -> ${renamed}`)}}for(const{table,from,to}of group.columns){if(!await db.tableExists(table)||!await db.columnExists(table,from))continue;if(await db.columnExists(table,to)){result.skipped.push(`${table} has both ${from} and ${to}; ${from} is left in place, move its values by hand if it holds any (${group.reference})`);continue}await ensureSqliteForeignKeys();await run(`ALTER TABLE ${q(table)} RENAME COLUMN ${q(from)} TO ${q(to)}`);result.applied.push(`renamed ${table}.${from} -> ${to}`)}}return result}export function replaceIndexName(createSql,from,to){const escaped=from.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),pattern=new RegExp(`^(\\s*CREATE\\s+(?:UNIQUE\\s+)?INDEX\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?)(["\`\\[]?)${escaped}(["\`\\]]?)(\\s)`,"i");if(!pattern.test(createSql))return;return createSql.replace(pattern,(_match,head,open,close,space)=>`${head}${open}${to}${close}${space}`)}
package/dist/types.d.ts CHANGED
@@ -2,9 +2,28 @@
2
2
  * SQL template tag function.
3
3
  * Creates parameterized SQL queries from template literals.
4
4
  *
5
+ * Every interpolated value becomes a literal `?`, whatever the connection is.
6
+ * That is correct only because the query builder re-renders placeholders for
7
+ * the dialect when it compiles the statement, so a fragment handed to
8
+ * `.set()`, `.whereRaw()` or any other builder method comes out as `$1` on
9
+ * Postgres.
10
+ *
11
+ * **Do not read `.sql` and `.parameters` off one of these and run it through
12
+ * `unsafe()`.** Nothing re-renders it on that path, so Postgres receives the
13
+ * `?`, parses it as an operator, and fails at the following token - which is
14
+ * why the whole Postgres auth suite once failed with
15
+ * `syntax error at or near "LIMIT"` (stacksjs/stacks#2842). Build the string
16
+ * with `sqlHelpers(getDatabaseDialect()).param(n)` and pass the values as an
17
+ * array instead.
18
+ *
5
19
  * @example
6
20
  * ```ts
7
- * const query = sql`SELECT * FROM users WHERE id = ${userId}`
21
+ * // Safe: the builder compiles this for the connection's dialect.
22
+ * query.whereRaw(sql`created_at > ${cutoff}`)
23
+ *
24
+ * // Broken on Postgres: `?` reaches the server verbatim.
25
+ * const q = sql`SELECT * FROM users WHERE id = ${userId}`
26
+ * await db.primary.unsafe(q.sql, q.parameters)
8
27
  * ```
9
28
  */
10
29
  export declare function sql(strings: TemplateStringsArray, ...values: unknown[]): Sql;
@@ -1 +1 @@
1
- import process from"node:process";import{log}from"@stacksjs/logging";import{path}from"@stacksjs/path";import{getTableName}from"@stacksjs/model-meta";import{fs}from"@stacksjs/storage";import{db}from"./utils";import{sqlHelpers}from"./sql-helpers";function getDbDriver(){return process.env.DB_CONNECTION||"sqlite"}function uuidColumnType(sql){if(sql.isPostgres)return"UUID";if(sql.isMysql)return"VARCHAR(255)";return"TEXT"}export function uuidColumnSql(table,sql){return`ALTER TABLE ${table} ADD COLUMN uuid ${uuidColumnType(sql)}`}async function loadModelsFrom(dir){const out=[];if(!fs.existsSync(dir))return out;const entries=fs.readdirSync(dir,{withFileTypes:!0});for(const entry of entries){const fullPath=path.join(dir,entry.name);if(entry.isDirectory()){out.push(...await loadModelsFrom(fullPath));continue}if(!entry.name.endsWith(".ts"))continue;if(entry.name.startsWith("_")||entry.name.startsWith("index"))continue;try{const imported=(await import(fullPath)).default;if(imported?.name||imported?.table)out.push({filePath:fullPath,model:imported})}catch{}}return out}export async function findUuidTables(){const dirs=[path.userModelsPath(),path.frameworkPath("defaults/app/Models")],tables=new Set;for(const dir of dirs)for(const{filePath,model}of await loadModelsFrom(dir)){if(!model.traits?.useUuid)continue;tables.add(getTableName(model,filePath))}return[...tables]}export async function ensureUuidColumns(sql,options={}){const tables=await findUuidTables();for(const table of tables)try{await db.unsafe(uuidColumnSql(table,sql)).execute();if(options.verbose)log.debug(`[uuid-columns] Added uuid column to ${table}`)}catch{if(options.verbose)log.debug(`[uuid-columns] Skipped (already applied or ${table} missing): uuid column`)}}export async function ensureUuidColumnsForCurrentDriver(options={}){await ensureUuidColumns(sqlHelpers(getDbDriver()),options)}
1
+ import process from"node:process";import{log}from"@stacksjs/logging";import{path}from"@stacksjs/path";import{getTableName}from"@stacksjs/model-meta";import{fs}from"@stacksjs/storage";import{db}from"./utils";import{sqlHelpers}from"./sql-helpers";function getDbDriver(){return process.env.DB_CONNECTION||"sqlite"}function uuidColumnType(sql){if(sql.isPostgres)return"UUID";if(sql.isMysql)return"VARCHAR(255)";return"TEXT"}export function uuidColumnSql(table,sql){return`ALTER TABLE ${table} ADD COLUMN uuid ${uuidColumnType(sql)}`}async function loadModelsFrom(dir){const out=[];if(!fs.existsSync(dir))return out;const entries=fs.readdirSync(dir,{withFileTypes:!0});for(const entry of entries){const fullPath=path.join(dir,entry.name);if(entry.isDirectory()){out.push(...await loadModelsFrom(fullPath));continue}if(!entry.name.endsWith(".ts"))continue;if(entry.name.startsWith("_")||entry.name.startsWith("index"))continue;try{const imported=(await import(fullPath)).default;if(imported?.name||imported?.table)out.push({filePath:fullPath,model:imported})}catch{}}return out}export async function findUuidTables(){const dirs=[path.userModelsPath(),path.frameworkPath("defaults/app/Models")],tables=new Set;for(const dir of dirs)for(const{filePath,model}of await loadModelsFrom(dir)){if(!model.traits?.useUuid)continue;tables.add(getTableName(model,filePath))}return[...tables]}export async function ensureUuidColumns(sql,options={}){const tables=await findUuidTables();for(const table of tables){try{await db.unsafe(uuidColumnSql(table,sql)).execute();if(options.verbose)log.debug(`[uuid-columns] Added uuid column to ${table}`)}catch{if(options.verbose)log.debug(`[uuid-columns] Skipped (already applied or ${table} missing): uuid column`);continue}if(sql.isSqlite)await reindexAfterAddingUuid(table)}}async function reindexAfterAddingUuid(table){try{await db.unsafe(`REINDEX "${table.replace(/"/g,'""')}"`).execute()}catch(error){log.warn(`[uuid-columns] Added uuid to ${table} but could not rebuild its indexes: ${error instanceof Error?error.message:String(error)}. Run REINDEX "${table}" before writing to it.`)}}export async function ensureUuidColumnsForCurrentDriver(options={}){await ensureUuidColumns(sqlHelpers(getDbDriver()),options)}
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.35",
5
+ "version": "0.75.37",
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.35",
70
- "@stacksjs/env": "0.75.35",
71
- "@stacksjs/error-handling": "0.75.35",
72
- "@stacksjs/faker": "^0.75.35",
73
- "@stacksjs/features": "0.75.35",
74
- "@stacksjs/logging": "0.75.35",
75
- "@stacksjs/model-meta": "0.75.35",
76
- "@stacksjs/path": "0.75.35",
77
- "@stacksjs/query-builder": "^0.75.35",
78
- "@stacksjs/security": "0.75.35",
79
- "@stacksjs/storage": "0.75.35",
80
- "@stacksjs/strings": "0.75.35",
69
+ "@stacksjs/config": "0.75.37",
70
+ "@stacksjs/env": "0.75.37",
71
+ "@stacksjs/error-handling": "0.75.37",
72
+ "@stacksjs/faker": "^0.75.37",
73
+ "@stacksjs/features": "0.75.37",
74
+ "@stacksjs/logging": "0.75.37",
75
+ "@stacksjs/model-meta": "0.75.37",
76
+ "@stacksjs/path": "0.75.37",
77
+ "@stacksjs/query-builder": "^0.75.37",
78
+ "@stacksjs/security": "0.75.37",
79
+ "@stacksjs/storage": "0.75.37",
80
+ "@stacksjs/strings": "0.75.37",
81
81
  "@stacksjs/ts-validation": "^0.5.6",
82
82
  "bun-query-builder": "^0.3.0",
83
83
  "dynamodb-tooling": "^0.3.2"
84
84
  },
85
85
  "devDependencies": {
86
- "@stacksjs/cli": "0.75.35",
87
- "@stacksjs/router": "0.75.35",
88
- "@stacksjs/utils": "0.75.35",
86
+ "@stacksjs/cli": "0.75.37",
87
+ "@stacksjs/router": "0.75.37",
88
+ "@stacksjs/utils": "0.75.37",
89
89
  "better-dx": "^0.2.24"
90
90
  }
91
91
  }