@stacksjs/database 0.75.41 → 0.75.43

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.
@@ -8,6 +8,13 @@ export declare function createDatabase(options: DatabaseOptions): Database;
8
8
  * Create a SQLite database connection
9
9
  */
10
10
  export declare function createSqliteDatabase(database: string, options?: Partial<Omit<DatabaseOptions, 'driver' | 'connection'>>): Database;
11
+ /**
12
+ * Create a Turso / libSQL database connection.
13
+ *
14
+ * SQLite's SQL over the network: `url` is `libsql://<db>-<org>.turso.io` (or
15
+ * `http://127.0.0.1:8080` for `turso dev`), and `authToken` the database token.
16
+ */
17
+ export declare function createTursoDatabase(url: string, authToken?: string, options?: Partial<Omit<DatabaseOptions, 'driver' | 'connection'>>): Database;
11
18
  /**
12
19
  * Create a PostgreSQL database connection
13
20
  */
@@ -23,6 +30,7 @@ export declare interface DatabaseConnectionConfig {
23
30
  username?: string
24
31
  password?: string
25
32
  url?: string
33
+ authToken?: string
26
34
  sharded?: boolean
27
35
  }
28
36
  export declare interface DatabaseOptions {
@@ -84,6 +92,7 @@ export declare class Database {
84
92
  mysql?: { name: string, host?: string, port?: number, username?: string, password?: string }
85
93
  singlestore?: { name: string, host?: string, port?: number, username?: string, password?: string }
86
94
  postgres?: { name: string, host?: string, port?: number, username?: string, password?: string }
95
+ turso?: { url?: string, authToken?: string }
87
96
  }
88
97
  }, env?: string): Database;
89
98
  static fromEnv(): Database;
package/dist/database.js CHANGED
@@ -1 +1 @@
1
- import{isVitessSharded,toQueryBuilderDialect}from"./dialect";import{qbSnapshotDir,resetDatabaseConnection}from"./utils";import{createQueryBuilder,setConfig}from"@stacksjs/query-builder";import{env as stacksEnv}from"@stacksjs/env";export class Database{_queryBuilder=null;_options;_initialized=!1;constructor(options){this._options={verbose:!1,timestamps:{createdAt:"created_at",updatedAt:"updated_at",defaultOrderColumn:"created_at"},softDeletes:{enabled:!1,column:"deleted_at",defaultFilter:!0},...options}}get driver(){return this._options.driver}get connection(){return this._options.connection}get isInitialized(){return this._initialized}get query(){if(!this._queryBuilder)this.initialize();return this._queryBuilder}initialize(){if(this._initialized)return;setConfig({snapshotDir:qbSnapshotDir(),dialect:toQueryBuilderDialect(this._options.driver),vitess:{sharded:isVitessSharded(this._options.connection.sharded)},database:this._options.connection,verbose:this._options.verbose,timestamps:this._options.timestamps,softDeletes:this._options.softDeletes,hooks:this._options.hooks});this._queryBuilder=createQueryBuilder();this._initialized=!0}switchDriver(driver,connection){this.close();this._options.driver=driver;this._options.connection=connection;this._initialized=!1;this.initialize()}async close(){if(this._queryBuilder&&typeof this._queryBuilder.close==="function")await this._queryBuilder.close();this._queryBuilder=null;this._initialized=!1;resetDatabaseConnection()}static fromConfig(config,env){const driver=config.default;let connection;switch(driver){case"sqlite":{connection={database:env==="testing"?config.connections.sqlite?.database?.replace(".sqlite","_testing.sqlite")||"database/stacks_testing.sqlite":config.connections.sqlite?.database||"database/stacks.sqlite"};break}case"mysql":{const mysql=config.connections.mysql;connection={database:mysql?.name||"stacks",host:mysql?.host||"127.0.0.1",port:mysql?.port||3306,username:mysql?.username||"root",password:mysql?.password||""};break}case"singlestore":{const singlestore=config.connections.singlestore;connection={database:singlestore?.name||"stacks",host:singlestore?.host||"127.0.0.1",port:singlestore?.port||3306,username:singlestore?.username||"root",password:singlestore?.password||""};break}case"postgres":{const postgres=config.connections.postgres,dbName=postgres?.name||"stacks";connection={database:env==="testing"?`${dbName}_testing`:dbName,host:postgres?.host||"127.0.0.1",port:postgres?.port||5432,username:postgres?.username||"",password:postgres?.password||""};break}case"dynamodb":throw Error("[database] DB_CONNECTION=dynamodb is not a SQL driver. Use the entity-style `dynamo.entity(...)` API for DynamoDB access, or set DB_CONNECTION to sqlite/mysql/postgres for SQL workloads.");default:throw Error(`[database] Unknown DB_CONNECTION "${String(driver)}". Allowed values: sqlite, mysql, postgres.`)}return new Database({driver,connection,verbose:env!=="production"})}static fromEnv(){const driver=stacksEnv.DB_CONNECTION||"sqlite";let connection;switch(driver){case"sqlite":connection={database:stacksEnv.DB_DATABASE||"database/stacks.sqlite"};break;case"mysql":case"singlestore":connection={database:stacksEnv.DB_DATABASE||"stacks",host:stacksEnv.DB_HOST||"127.0.0.1",port:stacksEnv.DB_PORT||3306,username:stacksEnv.DB_USERNAME||"root",password:stacksEnv.DB_PASSWORD||""};break;case"postgres":connection={database:stacksEnv.DB_DATABASE||"stacks",host:stacksEnv.DB_HOST||"127.0.0.1",port:stacksEnv.DB_PORT||5432,username:stacksEnv.DB_USERNAME||"",password:stacksEnv.DB_PASSWORD||""};break;default:connection={database:":memory:"}}return new Database({driver,connection,verbose:stacksEnv.APP_ENV!=="prod"})}}export function createDatabase(options){return new Database(options)}export function createSqliteDatabase(database,options){return new Database({driver:"sqlite",connection:{database},...options})}export function createPostgresDatabase(connection,options){return new Database({driver:"postgres",connection,...options})}export function createMysqlDatabase(connection,options){return new Database({driver:"mysql",connection,...options})}
1
+ import{isVitessSharded,toQueryBuilderDialect}from"./dialect";import{qbSnapshotDir,resetDatabaseConnection}from"./utils";import{createQueryBuilder,setConfig}from"@stacksjs/query-builder";import{env as stacksEnv}from"@stacksjs/env";export class Database{_queryBuilder=null;_options;_initialized=!1;constructor(options){this._options={verbose:!1,timestamps:{createdAt:"created_at",updatedAt:"updated_at",defaultOrderColumn:"created_at"},softDeletes:{enabled:!1,column:"deleted_at",defaultFilter:!0},...options}}get driver(){return this._options.driver}get connection(){return this._options.connection}get isInitialized(){return this._initialized}get query(){if(!this._queryBuilder)this.initialize();return this._queryBuilder}initialize(){if(this._initialized)return;setConfig({snapshotDir:qbSnapshotDir(),dialect:toQueryBuilderDialect(this._options.driver),vitess:{sharded:isVitessSharded(this._options.connection.sharded)},database:{url:void 0,authToken:void 0,...this._options.connection},verbose:this._options.verbose,timestamps:this._options.timestamps,softDeletes:this._options.softDeletes,hooks:this._options.hooks});this._queryBuilder=createQueryBuilder();this._initialized=!0}switchDriver(driver,connection){this.close();this._options.driver=driver;this._options.connection=connection;this._initialized=!1;this.initialize()}async close(){if(this._queryBuilder&&typeof this._queryBuilder.close==="function")await this._queryBuilder.close();this._queryBuilder=null;this._initialized=!1;resetDatabaseConnection()}static fromConfig(config,env){const driver=config.default;let connection;switch(driver){case"sqlite":{connection={database:env==="testing"?config.connections.sqlite?.database?.replace(".sqlite","_testing.sqlite")||"database/stacks_testing.sqlite":config.connections.sqlite?.database||"database/stacks.sqlite"};break}case"mysql":{const mysql=config.connections.mysql;connection={database:mysql?.name||"stacks",host:mysql?.host||"127.0.0.1",port:mysql?.port||3306,username:mysql?.username||"root",password:mysql?.password||""};break}case"singlestore":{const singlestore=config.connections.singlestore;connection={database:singlestore?.name||"stacks",host:singlestore?.host||"127.0.0.1",port:singlestore?.port||3306,username:singlestore?.username||"root",password:singlestore?.password||""};break}case"postgres":{const postgres=config.connections.postgres,dbName=postgres?.name||"stacks";connection={database:env==="testing"?`${dbName}_testing`:dbName,host:postgres?.host||"127.0.0.1",port:postgres?.port||5432,username:postgres?.username||"",password:postgres?.password||""};break}case"turso":{const turso=config.connections.turso;if(!turso?.url)throw Error("[database] DB_CONNECTION=turso needs connections.turso.url (TURSO_DATABASE_URL).");connection={database:turso.url,url:turso.url,authToken:turso.authToken||void 0};break}case"dynamodb":throw Error("[database] DB_CONNECTION=dynamodb is not a SQL driver. Use the entity-style `dynamo.entity(...)` API for DynamoDB access, or set DB_CONNECTION to sqlite/mysql/postgres for SQL workloads.");default:throw Error(`[database] Unknown DB_CONNECTION "${String(driver)}". Allowed values: sqlite, mysql, postgres, singlestore, vitess, turso.`)}return new Database({driver,connection,verbose:env!=="production"})}static fromEnv(){const configured=String(stacksEnv.DB_CONNECTION||"sqlite"),driver=configured==="libsql"?"turso":configured;let connection;switch(driver){case"sqlite":connection={database:stacksEnv.DB_DATABASE||"database/stacks.sqlite"};break;case"mysql":case"singlestore":connection={database:stacksEnv.DB_DATABASE||"stacks",host:stacksEnv.DB_HOST||"127.0.0.1",port:stacksEnv.DB_PORT||3306,username:stacksEnv.DB_USERNAME||"root",password:stacksEnv.DB_PASSWORD||""};break;case"postgres":connection={database:stacksEnv.DB_DATABASE||"stacks",host:stacksEnv.DB_HOST||"127.0.0.1",port:stacksEnv.DB_PORT||5432,username:stacksEnv.DB_USERNAME||"",password:stacksEnv.DB_PASSWORD||""};break;case"turso":{const url=stacksEnv.TURSO_DATABASE_URL||"";if(!url)throw Error("[database] DB_CONNECTION=turso needs TURSO_DATABASE_URL.");connection={database:url,url,authToken:stacksEnv.TURSO_AUTH_TOKEN||void 0};break}default:connection={database:":memory:"}}return new Database({driver,connection,verbose:stacksEnv.APP_ENV!=="prod"})}}export function createDatabase(options){return new Database(options)}export function createSqliteDatabase(database,options){return new Database({driver:"sqlite",connection:{database},...options})}export function createTursoDatabase(url,authToken,options){return new Database({driver:"turso",connection:{database:url,url,authToken},...options})}export function createPostgresDatabase(connection,options){return new Database({driver:"postgres",connection,...options})}export function createMysqlDatabase(connection,options){return new Database({driver:"mysql",connection,...options})}
@@ -33,6 +33,13 @@ export declare function uniqueIndexIsRestorable(db: Database, index: DeclaredUni
33
33
  * duplicate rows got in while the index was missing, and the migrate must say
34
34
  * so rather than finish green on a table that no longer enforces the rule.
35
35
  */
36
+ /**
37
+ * The declared unique indexes a later rebuild dropped and that can still be
38
+ * put back, in corpus order. What {@link restoreDroppedUniqueIndexes}
39
+ * restores, for a caller that has to execute the statements elsewhere - a
40
+ * libSQL server, read through a schema replica.
41
+ */
42
+ export declare function restorableMissingUniqueIndexes(db: Database, migrationsDir: string): DeclaredUniqueIndex[];
36
43
  export declare function restoreDroppedUniqueIndexes(db: Database, migrationsDir: string): DeclaredUniqueIndex[];
37
44
  export declare interface DeclaredUniqueIndex {
38
45
  file: string
@@ -1 +1 @@
1
- import{readdirSync,readFileSync}from"node:fs";import{join}from"node:path";import{sqlStatementsOf}from"./sql-statements";const quoted="[\"'`]?(\\w+)[\"'`]?",CREATE_UNIQUE_INDEX=new RegExp(`^\\s*CREATE\\s+UNIQUE\\s+INDEX\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?${quoted}\\s+ON\\s+${quoted}\\s*\\(`,"i"),DROP_INDEX=new RegExp(`^\\s*DROP\\s+INDEX\\s+(?:IF\\s+EXISTS\\s+)?${quoted}`,"i"),PLAIN_COLUMN=new RegExp(`^${quoted}(?:\\s+COLLATE\\s+\\w+)?(?:\\s+(?:ASC|DESC))?$`,"i");function columnTerms(statement,start){const terms=[];let depth=0,current="";for(let i=start;i<statement.length;i++){const char=statement[i];if(char==="("){depth++;if(depth===1)continue}else if(char===")"){depth--;if(depth===0){terms.push(current.trim());return terms}}else if(char===","&&depth===1){terms.push(current.trim());current="";continue}current+=char}return terms}export function parseUniqueIndex(statement,file){const match=CREATE_UNIQUE_INDEX.exec(statement);if(!match?.[1]||!match[2])return null;const columns=columnTerms(statement,match[0].length-1).map((term)=>PLAIN_COLUMN.exec(term)?.[1]).filter((column)=>Boolean(column));return{file,name:match[1],table:match[2],columns,statement}}export function uniqueIndexOnlyFile(content,file){const statements=sqlStatementsOf(content);if(statements.length===0)return null;const declared=statements.map((statement)=>parseUniqueIndex(statement,file));return declared.every((index)=>index!==null)?declared:null}export function corpusUniqueIndexes(corpus){const ordered=[...corpus].sort((a,b)=>a.file.localeCompare(b.file)),declared=new Map,lastEvent=new Map;for(const{file,content}of ordered){const only=uniqueIndexOnlyFile(content,file);if(only)declared.set(file,only);for(const statement of sqlStatementsOf(content)){const created=CREATE_UNIQUE_INDEX.exec(statement)?.[1];if(created){lastEvent.set(created.toLowerCase(),"create");continue}const dropped=DROP_INDEX.exec(statement)?.[1];if(dropped)lastEvent.set(dropped.toLowerCase(),"drop")}}for(const[file,indexes]of declared)declared.set(file,indexes.filter((index)=>lastEvent.get(index.name.toLowerCase())!=="drop"));return declared}export function readCorpus(migrationsDir){let files;try{files=readdirSync(migrationsDir).filter((file)=>file.endsWith(".sql"))}catch{return[]}return files.map((file)=>({file,content:readFileSync(join(migrationsDir,file),"utf8")}))}export function hasIndex(db,name){return Boolean(db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'index' AND lower(name) = lower(?)").get(name))}export function uniqueIndexIsRestorable(db,index){const table=db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND lower(name) = lower(?)").get(index.table);if(!table)return!1;const present=new Set(db.prepare("SELECT name FROM pragma_table_info(?)").all(table.name).map((column)=>column.name.toLowerCase()));return index.columns.every((column)=>present.has(column.toLowerCase()))}export function restoreDroppedUniqueIndexes(db,migrationsDir){const restored=[];for(const indexes of corpusUniqueIndexes(readCorpus(migrationsDir)).values())for(const index of indexes){if(hasIndex(db,index.name)||!uniqueIndexIsRestorable(db,index))continue;try{db.exec(index.statement)}catch(error){const detail=error instanceof Error?error.message:String(error);throw Error(`Could not restore unique index "${index.name}" on "${index.table}" (declared in ${index.file}), which a later table rebuild dropped: ${detail}`)}restored.push(index)}return restored}
1
+ import{readdirSync,readFileSync}from"node:fs";import{join}from"node:path";import{sqlStatementsOf}from"./sql-statements";const quoted="[\"'`]?(\\w+)[\"'`]?",CREATE_UNIQUE_INDEX=new RegExp(`^\\s*CREATE\\s+UNIQUE\\s+INDEX\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?${quoted}\\s+ON\\s+${quoted}\\s*\\(`,"i"),DROP_INDEX=new RegExp(`^\\s*DROP\\s+INDEX\\s+(?:IF\\s+EXISTS\\s+)?${quoted}`,"i"),PLAIN_COLUMN=new RegExp(`^${quoted}(?:\\s+COLLATE\\s+\\w+)?(?:\\s+(?:ASC|DESC))?$`,"i");function columnTerms(statement,start){const terms=[];let depth=0,current="";for(let i=start;i<statement.length;i++){const char=statement[i];if(char==="("){depth++;if(depth===1)continue}else if(char===")"){depth--;if(depth===0){terms.push(current.trim());return terms}}else if(char===","&&depth===1){terms.push(current.trim());current="";continue}current+=char}return terms}export function parseUniqueIndex(statement,file){const match=CREATE_UNIQUE_INDEX.exec(statement);if(!match?.[1]||!match[2])return null;const columns=columnTerms(statement,match[0].length-1).map((term)=>PLAIN_COLUMN.exec(term)?.[1]).filter((column)=>Boolean(column));return{file,name:match[1],table:match[2],columns,statement}}export function uniqueIndexOnlyFile(content,file){const statements=sqlStatementsOf(content);if(statements.length===0)return null;const declared=statements.map((statement)=>parseUniqueIndex(statement,file));return declared.every((index)=>index!==null)?declared:null}export function corpusUniqueIndexes(corpus){const ordered=[...corpus].sort((a,b)=>a.file.localeCompare(b.file)),declared=new Map,lastEvent=new Map;for(const{file,content}of ordered){const only=uniqueIndexOnlyFile(content,file);if(only)declared.set(file,only);for(const statement of sqlStatementsOf(content)){const created=CREATE_UNIQUE_INDEX.exec(statement)?.[1];if(created){lastEvent.set(created.toLowerCase(),"create");continue}const dropped=DROP_INDEX.exec(statement)?.[1];if(dropped)lastEvent.set(dropped.toLowerCase(),"drop")}}for(const[file,indexes]of declared)declared.set(file,indexes.filter((index)=>lastEvent.get(index.name.toLowerCase())!=="drop"));return declared}export function readCorpus(migrationsDir){let files;try{files=readdirSync(migrationsDir).filter((file)=>file.endsWith(".sql"))}catch{return[]}return files.map((file)=>({file,content:readFileSync(join(migrationsDir,file),"utf8")}))}export function hasIndex(db,name){return Boolean(db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'index' AND lower(name) = lower(?)").get(name))}export function uniqueIndexIsRestorable(db,index){const table=db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND lower(name) = lower(?)").get(index.table);if(!table)return!1;const present=new Set(db.prepare("SELECT name FROM pragma_table_info(?)").all(table.name).map((column)=>column.name.toLowerCase()));return index.columns.every((column)=>present.has(column.toLowerCase()))}export function restorableMissingUniqueIndexes(db,migrationsDir){const missing=[],seen=new Set;for(const indexes of corpusUniqueIndexes(readCorpus(migrationsDir)).values())for(const index of indexes){const key=index.name.toLowerCase();if(seen.has(key)||hasIndex(db,index.name)||!uniqueIndexIsRestorable(db,index))continue;seen.add(key);missing.push(index)}return missing}export function restoreDroppedUniqueIndexes(db,migrationsDir){const restored=[];for(const indexes of corpusUniqueIndexes(readCorpus(migrationsDir)).values())for(const index of indexes){if(hasIndex(db,index.name)||!uniqueIndexIsRestorable(db,index))continue;try{db.exec(index.statement)}catch(error){const detail=error instanceof Error?error.message:String(error);throw Error(`Could not restore unique index "${index.name}" on "${index.table}" (declared in ${index.file}), which a later table rebuild dropped: ${detail}`)}restored.push(index)}return restored}
package/dist/dialect.d.ts CHANGED
@@ -16,6 +16,22 @@ export declare function isVitessSharded(explicit?: boolean | string): boolean;
16
16
  export declare function dialectCapabilities(dialect: string, options?: DialectCapabilityOptions): DialectCapabilities;
17
17
  /** Whether the framework has an explicit capability row for this dialect. */
18
18
  export declare function isKnownDialect(dialect: string): boolean;
19
+ /**
20
+ * The canonical name of a configured database driver.
21
+ *
22
+ * `libsql` is the protocol and `turso` the service most people know it by;
23
+ * both select the same driver, recorded as `turso`. Anything else is returned
24
+ * as written.
25
+ */
26
+ export declare function normalizeDatabaseDriver(driver: string): string;
27
+ /**
28
+ * Whether a driver is SQLite reached over the network (Turso / libSQL).
29
+ *
30
+ * Its SQL is SQLite's, so dialect checks treat it as `sqlite`. This is the
31
+ * question for the code that instead opens the database FILE - there is no
32
+ * file - or relies on SQLite being embedded and synchronous.
33
+ */
34
+ export declare function isLibsqlDriver(driver: string | undefined | null): boolean;
19
35
  /**
20
36
  * Collapse a configured database driver to the information-schema family it
21
37
  * exposes. Unknown drivers must not inherit SQLite's compatibility fallback:
package/dist/dialect.js CHANGED
@@ -1 +1 @@
1
- import process from"node:process";const CAPABILITIES={sqlite:{dialect:"sqlite",wire:"sqlite",queryBuilderDialect:"sqlite",identifierQuote:'"',supportsForeignKeys:!0,supportsAutoIncrement:!0,supportsAtomicMultiTableTransactions:!0,requiresOnlineDdl:!1,supportsCreateIndexIfNotExists:!0},mysql:{dialect:"mysql",wire:"mysql",queryBuilderDialect:"mysql",defaultPort:3306,identifierQuote:"`",supportsForeignKeys:!0,supportsAutoIncrement:!0,supportsAtomicMultiTableTransactions:!0,requiresOnlineDdl:!1,supportsCreateIndexIfNotExists:!1},singlestore:{dialect:"singlestore",wire:"mysql",queryBuilderDialect:"singlestore",defaultPort:3306,identifierQuote:"`",supportsForeignKeys:!1,supportsAutoIncrement:!0,supportsAtomicMultiTableTransactions:!1,requiresOnlineDdl:!1,supportsCreateIndexIfNotExists:!1},vitess:{dialect:"vitess",wire:"mysql",queryBuilderDialect:"vitess",defaultPort:15306,identifierQuote:"`",supportsForeignKeys:!1,supportsAutoIncrement:!1,supportsAtomicMultiTableTransactions:!1,requiresOnlineDdl:!0,supportsCreateIndexIfNotExists:!1},postgres:{dialect:"postgres",wire:"postgres",queryBuilderDialect:"postgres",defaultPort:5432,identifierQuote:'"',supportsForeignKeys:!0,supportsAutoIncrement:!0,supportsAtomicMultiTableTransactions:!0,requiresOnlineDdl:!1,supportsCreateIndexIfNotExists:!0}};export function isVitessSharded(explicit){if(typeof explicit==="boolean")return explicit;const raw=(typeof explicit==="string"?explicit:process.env.DB_VITESS_SHARDED)?.trim().toLowerCase();if(raw===void 0||raw==="")return!0;return!["0","false","no","off"].includes(raw)}export function dialectCapabilities(dialect,options={}){const caps=CAPABILITIES[dialect]??CAPABILITIES.sqlite;if(dialect!=="vitess"||isVitessSharded(options.vitessSharded))return caps;return{...CAPABILITIES.mysql,dialect:"vitess",queryBuilderDialect:"vitess",defaultPort:15306}}export function isKnownDialect(dialect){return dialect in CAPABILITIES}export function toSqlIntrospectionDialect(dialect){if(!isKnownDialect(dialect))return"other";return dialectCapabilities(dialect).wire}export function indexSqlForDialect(statement,dialect){if(dialectCapabilities(dialect).supportsCreateIndexIfNotExists)return statement;return statement.replace(/^(\s*CREATE\s+(?:UNIQUE\s+)?INDEX)\s+IF\s+NOT\s+EXISTS\s+/i,"$1 ")}export function isDuplicateIndexError(error){const message=error instanceof Error?error.message:String(error);return/duplicate key name|already exists/i.test(message)}export function isDuplicateColumnError(error){const message=error instanceof Error?error.message:String(error);return/duplicate column|already exists/i.test(message)}export function knownDialects(){return Object.keys(CAPABILITIES)}export function isMysqlWire(dialect){return dialectCapabilities(dialect).wire==="mysql"}export function isPostgresWire(dialect){return dialectCapabilities(dialect).wire==="postgres"}export function toQueryBuilderDialect(dialect){return dialectCapabilities(dialect).queryBuilderDialect}
1
+ import process from"node:process";const CAPABILITIES={sqlite:{dialect:"sqlite",wire:"sqlite",queryBuilderDialect:"sqlite",identifierQuote:'"',supportsForeignKeys:!0,supportsAutoIncrement:!0,supportsAtomicMultiTableTransactions:!0,requiresOnlineDdl:!1,supportsCreateIndexIfNotExists:!0},mysql:{dialect:"mysql",wire:"mysql",queryBuilderDialect:"mysql",defaultPort:3306,identifierQuote:"`",supportsForeignKeys:!0,supportsAutoIncrement:!0,supportsAtomicMultiTableTransactions:!0,requiresOnlineDdl:!1,supportsCreateIndexIfNotExists:!1},singlestore:{dialect:"singlestore",wire:"mysql",queryBuilderDialect:"singlestore",defaultPort:3306,identifierQuote:"`",supportsForeignKeys:!1,supportsAutoIncrement:!0,supportsAtomicMultiTableTransactions:!1,requiresOnlineDdl:!1,supportsCreateIndexIfNotExists:!1},vitess:{dialect:"vitess",wire:"mysql",queryBuilderDialect:"vitess",defaultPort:15306,identifierQuote:"`",supportsForeignKeys:!1,supportsAutoIncrement:!1,supportsAtomicMultiTableTransactions:!1,requiresOnlineDdl:!0,supportsCreateIndexIfNotExists:!1},turso:{dialect:"turso",wire:"sqlite",queryBuilderDialect:"sqlite",identifierQuote:'"',supportsForeignKeys:!0,supportsAutoIncrement:!0,supportsAtomicMultiTableTransactions:!0,requiresOnlineDdl:!1,supportsCreateIndexIfNotExists:!0},postgres:{dialect:"postgres",wire:"postgres",queryBuilderDialect:"postgres",defaultPort:5432,identifierQuote:'"',supportsForeignKeys:!0,supportsAutoIncrement:!0,supportsAtomicMultiTableTransactions:!0,requiresOnlineDdl:!1,supportsCreateIndexIfNotExists:!0}};export function isVitessSharded(explicit){if(typeof explicit==="boolean")return explicit;const raw=(typeof explicit==="string"?explicit:process.env.DB_VITESS_SHARDED)?.trim().toLowerCase();if(raw===void 0||raw==="")return!0;return!["0","false","no","off"].includes(raw)}export function dialectCapabilities(dialect,options={}){dialect=normalizeDatabaseDriver(dialect);const caps=CAPABILITIES[dialect]??CAPABILITIES.sqlite;if(dialect!=="vitess"||isVitessSharded(options.vitessSharded))return caps;return{...CAPABILITIES.mysql,dialect:"vitess",queryBuilderDialect:"vitess",defaultPort:15306}}export function isKnownDialect(dialect){return normalizeDatabaseDriver(dialect)in CAPABILITIES}export function normalizeDatabaseDriver(driver){return driver==="libsql"?"turso":driver}export function isLibsqlDriver(driver){return normalizeDatabaseDriver(String(driver??""))==="turso"}export function toSqlIntrospectionDialect(dialect){if(!isKnownDialect(dialect))return"other";return dialectCapabilities(dialect).wire}export function indexSqlForDialect(statement,dialect){if(dialectCapabilities(dialect).supportsCreateIndexIfNotExists)return statement;return statement.replace(/^(\s*CREATE\s+(?:UNIQUE\s+)?INDEX)\s+IF\s+NOT\s+EXISTS\s+/i,"$1 ")}export function isDuplicateIndexError(error){const message=error instanceof Error?error.message:String(error);return/duplicate key name|already exists/i.test(message)}export function isDuplicateColumnError(error){const message=error instanceof Error?error.message:String(error);return/duplicate column|already exists/i.test(message)}export function knownDialects(){return Object.keys(CAPABILITIES)}export function isMysqlWire(dialect){return dialectCapabilities(dialect).wire==="mysql"}export function isPostgresWire(dialect){return dialectCapabilities(dialect).wire==="postgres"}export function toQueryBuilderDialect(dialect){return dialectCapabilities(dialect).queryBuilderDialect}
@@ -56,6 +56,7 @@ export declare function detectDriver(): StacksDialect;
56
56
  * ssl: false,
57
57
  * sharded: true
58
58
  * },
59
+ * turso: { url: 'http://127.0.0.1:8080', prefix: '' },
59
60
  * postgres: {
60
61
  * name: 'stacks',
61
62
  * host: '127.0.0.1',
@@ -68,7 +69,7 @@ export declare function detectDriver(): StacksDialect;
68
69
  * }
69
70
  * ```
70
71
  */
71
- export declare const driverDefaults: Record<StacksDialect, Partial<SqliteConfig | MysqlConfig | SinglestoreConfig | VitessConfig | PostgresConfig | DynamoDbConfig>>;
72
+ export declare const driverDefaults: Record<StacksDialect, Partial<SqliteConfig | MysqlConfig | SinglestoreConfig | VitessConfig | TursoConfig | PostgresConfig | DynamoDbConfig>>;
72
73
  /**
73
74
  * SQLite specific configuration
74
75
  */
@@ -135,6 +136,18 @@ export declare interface VitessConfig extends NetworkedConnectionConfig {
135
136
  tabletType?: 'primary' | 'replica' | 'rdonly'
136
137
  sharded?: boolean
137
138
  }
139
+ /**
140
+ * Turso / libSQL configuration.
141
+ *
142
+ * SQLite's SQL behind a network server (Hrana over HTTP). It renders and
143
+ * migrates exactly as SQLite does; only the connection is different, so the
144
+ * shape is a URL and a token rather than a file path.
145
+ */
146
+ export declare interface TursoConfig {
147
+ url: string
148
+ authToken?: string
149
+ prefix?: string
150
+ }
138
151
  /**
139
152
  * PostgreSQL specific configuration
140
153
  */
@@ -175,6 +188,7 @@ export declare interface DatabaseConnections {
175
188
  mysql?: MysqlConfig
176
189
  singlestore?: SinglestoreConfig
177
190
  vitess?: VitessConfig
191
+ turso?: TursoConfig
178
192
  postgres?: PostgresConfig
179
193
  dynamodb?: DynamoDbConfig
180
194
  }
@@ -1 +1 @@
1
- import{env}from"@stacksjs/env";export const driverDefaults={sqlite:{database:"database/stacks.sqlite",prefix:""},mysql:{name:"stacks",host:"127.0.0.1",port:3306,username:"root",password:"",prefix:"",charset:"utf8mb4",collation:"utf8mb4_unicode_ci"},singlestore:{name:"stacks",host:"127.0.0.1",port:3306,username:"root",password:"",prefix:"",charset:"utf8mb4",ssl:!1},vitess:{name:"stacks",host:"127.0.0.1",port:15306,username:"root",password:"",prefix:"",ssl:!1,sharded:!0},postgres:{name:"stacks",host:"127.0.0.1",port:5432,username:"postgres",password:"",prefix:"",schema:"public"},browser:{}};export function getConnectionString(driver,config){switch(driver){case"sqlite":{const sqliteConfig=config;if(sqliteConfig.database===":memory:")return":memory:";return`sqlite://${sqliteConfig.database}`}case"mysql":{const mysqlConfig=config,{name,host="127.0.0.1",port=3306,username="root",password=""}=mysqlConfig;return`mysql://${username}:${password}@${host}:${port}/${name}`}case"singlestore":{const ssConfig=config,{name,host="127.0.0.1",port=3306,username="root",password=""}=ssConfig;return`mysql://${username}:${password}@${host}:${port}/${name}`}case"vitess":{const vtConfig=config,{name,host="127.0.0.1",port=15306,username="root",password="",tabletType}=vtConfig,keyspace=tabletType?`${name}@${tabletType}`:name;return`mysql://${username}:${password}@${host}:${port}/${keyspace}`}case"postgres":{const pgConfig=config,{name,host="127.0.0.1",port=5432,username="postgres",password=""}=pgConfig;return`postgres://${username}:${password}@${host}:${port}/${name}`}default:throw Error(`Unsupported driver: ${driver}`)}}export function validateDriverConfig(driver,config){const errors=[];switch(driver){case"sqlite":{if(!config.database)errors.push("SQLite requires a database path");break}case"mysql":{if(!config.name)errors.push("MySQL requires a database name");break}case"singlestore":{if(!config.name)errors.push("SingleStore requires a database name");break}case"vitess":{if(!config.name)errors.push("Vitess requires a keyspace name");break}case"postgres":{if(!config.name)errors.push("PostgreSQL requires a database name");break}default:errors.push(`Unsupported driver: ${driver}`)}return{valid:errors.length===0,errors}}export function mergeWithDefaults(driver,config){return{...driverDefaults[driver],...config}}export function getConfigFromEnv(driver){switch(driver){case"sqlite":return{database:env.DB_DATABASE||"database/stacks.sqlite",prefix:env.DB_PREFIX||""};case"mysql":return{name:env.DB_DATABASE||"stacks",host:env.DB_HOST||"127.0.0.1",port:env.DB_PORT??3306,username:env.DB_USERNAME||"root",password:env.DB_PASSWORD||"",prefix:env.DB_PREFIX||""};case"singlestore":return{name:env.DB_DATABASE||"stacks",host:env.DB_HOST||"127.0.0.1",port:env.DB_PORT??3306,username:env.DB_USERNAME||"root",password:env.DB_PASSWORD||"",prefix:env.DB_PREFIX||"",ssl:env.DB_SSL==="true"||env.DB_SSL==="1"};case"vitess":return{name:env.DB_DATABASE||"stacks",host:env.DB_HOST||"127.0.0.1",port:env.DB_PORT??15306,username:env.DB_USERNAME||"root",password:env.DB_PASSWORD||"",prefix:env.DB_PREFIX||"",ssl:env.DB_SSL==="true"||env.DB_SSL==="1",sharded:!["0","false","no","off"].includes(String(env.DB_VITESS_SHARDED??"true").toLowerCase())};case"postgres":return{name:env.DB_DATABASE||"stacks",host:env.DB_HOST||"127.0.0.1",port:env.DB_PORT??5432,username:env.DB_USERNAME||"postgres",password:env.DB_PASSWORD||"",prefix:env.DB_PREFIX||"",schema:env.DB_SCHEMA||"public"};default:throw Error(`Unsupported driver: ${driver}`)}}export function detectDriver(){if(env.DB_CONNECTION)return env.DB_CONNECTION;if(env.DATABASE_URL?.startsWith("postgres"))return"postgres";if(env.DATABASE_URL?.startsWith("mysql"))return"mysql";return"sqlite"}
1
+ import{env}from"@stacksjs/env";export const driverDefaults={sqlite:{database:"database/stacks.sqlite",prefix:""},mysql:{name:"stacks",host:"127.0.0.1",port:3306,username:"root",password:"",prefix:"",charset:"utf8mb4",collation:"utf8mb4_unicode_ci"},singlestore:{name:"stacks",host:"127.0.0.1",port:3306,username:"root",password:"",prefix:"",charset:"utf8mb4",ssl:!1},vitess:{name:"stacks",host:"127.0.0.1",port:15306,username:"root",password:"",prefix:"",ssl:!1,sharded:!0},turso:{url:"http://127.0.0.1:8080",prefix:""},postgres:{name:"stacks",host:"127.0.0.1",port:5432,username:"postgres",password:"",prefix:"",schema:"public"},browser:{}};export function getConnectionString(driver,config){switch(driver){case"sqlite":{const sqliteConfig=config;if(sqliteConfig.database===":memory:")return":memory:";return`sqlite://${sqliteConfig.database}`}case"mysql":{const mysqlConfig=config,{name,host="127.0.0.1",port=3306,username="root",password=""}=mysqlConfig;return`mysql://${username}:${password}@${host}:${port}/${name}`}case"singlestore":{const ssConfig=config,{name,host="127.0.0.1",port=3306,username="root",password=""}=ssConfig;return`mysql://${username}:${password}@${host}:${port}/${name}`}case"vitess":{const vtConfig=config,{name,host="127.0.0.1",port=15306,username="root",password="",tabletType}=vtConfig,keyspace=tabletType?`${name}@${tabletType}`:name;return`mysql://${username}:${password}@${host}:${port}/${keyspace}`}case"postgres":{const pgConfig=config,{name,host="127.0.0.1",port=5432,username="postgres",password=""}=pgConfig;return`postgres://${username}:${password}@${host}:${port}/${name}`}case"turso":return config.url;default:throw Error(`Unsupported driver: ${driver}`)}}export function validateDriverConfig(driver,config){const errors=[];switch(driver){case"sqlite":{if(!config.database)errors.push("SQLite requires a database path");break}case"mysql":{if(!config.name)errors.push("MySQL requires a database name");break}case"singlestore":{if(!config.name)errors.push("SingleStore requires a database name");break}case"vitess":{if(!config.name)errors.push("Vitess requires a keyspace name");break}case"postgres":{if(!config.name)errors.push("PostgreSQL requires a database name");break}case"turso":{const tursoConfig=config;if(!tursoConfig.url)errors.push("Turso requires a database URL (TURSO_DATABASE_URL)");else if(!/^(?:libsql|https?|wss?):\/\//i.test(tursoConfig.url))errors.push("Turso URLs start with libsql://, https://, http://, ws:// or wss://");break}default:errors.push(`Unsupported driver: ${driver}`)}return{valid:errors.length===0,errors}}export function mergeWithDefaults(driver,config){return{...driverDefaults[driver],...config}}export function getConfigFromEnv(driver){switch(driver){case"sqlite":return{database:env.DB_DATABASE||"database/stacks.sqlite",prefix:env.DB_PREFIX||""};case"mysql":return{name:env.DB_DATABASE||"stacks",host:env.DB_HOST||"127.0.0.1",port:env.DB_PORT??3306,username:env.DB_USERNAME||"root",password:env.DB_PASSWORD||"",prefix:env.DB_PREFIX||""};case"singlestore":return{name:env.DB_DATABASE||"stacks",host:env.DB_HOST||"127.0.0.1",port:env.DB_PORT??3306,username:env.DB_USERNAME||"root",password:env.DB_PASSWORD||"",prefix:env.DB_PREFIX||"",ssl:env.DB_SSL==="true"||env.DB_SSL==="1"};case"vitess":return{name:env.DB_DATABASE||"stacks",host:env.DB_HOST||"127.0.0.1",port:env.DB_PORT??15306,username:env.DB_USERNAME||"root",password:env.DB_PASSWORD||"",prefix:env.DB_PREFIX||"",ssl:env.DB_SSL==="true"||env.DB_SSL==="1",sharded:!["0","false","no","off"].includes(String(env.DB_VITESS_SHARDED??"true").toLowerCase())};case"turso":return{url:env.TURSO_DATABASE_URL||"",authToken:env.TURSO_AUTH_TOKEN||void 0,prefix:env.DB_PREFIX||""};case"postgres":return{name:env.DB_DATABASE||"stacks",host:env.DB_HOST||"127.0.0.1",port:env.DB_PORT??5432,username:env.DB_USERNAME||"postgres",password:env.DB_PASSWORD||"",prefix:env.DB_PREFIX||"",schema:env.DB_SCHEMA||"public"};default:throw Error(`Unsupported driver: ${driver}`)}}export function detectDriver(){if(env.DB_CONNECTION)return env.DB_CONNECTION==="libsql"?"turso":env.DB_CONNECTION;if(env.TURSO_DATABASE_URL)return"turso";if(env.DATABASE_URL?.startsWith("postgres"))return"postgres";if(env.DATABASE_URL?.startsWith("mysql"))return"mysql";return"sqlite"}
package/dist/index.d.ts CHANGED
@@ -9,6 +9,7 @@ export type {
9
9
  MysqlConfig,
10
10
  PostgresConfig,
11
11
  SqliteConfig,
12
+ TursoConfig,
12
13
  } from './driver-config';
13
14
  // Core database utilities and default instance
14
15
  export type { FrameworkSchema } from './framework-schema';
@@ -67,6 +68,7 @@ export {
67
68
  createMysqlDatabase,
68
69
  createPostgresDatabase,
69
70
  createSqliteDatabase,
71
+ createTursoDatabase,
70
72
  } from './database';
71
73
  // Driver configuration
72
74
  export {
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- export{Database,createDatabase,createMysqlDatabase,createPostgresDatabase,createSqliteDatabase}from"./database";export{detectDriver,driverDefaults,getConfigFromEnv,getConnectionString,mergeWithDefaults,validateDriverConfig}from"./driver-config";export*from"./utils";export{BUN_BROKEN_MYSQL_POOL_MESSAGE,BUN_BROKEN_POSTGRES_POOL_MESSAGE,getBrokenDatabasePools,isBrokenBunPoolError}from"./broken-pool";export*from"./types";export*from"./migrations";export*from"./framework-renames";export{setQueryTracker,logQuery}from"./query-logger";export{addColumnSafely,backfillInBatches,renameColumnSafely}from"./safe-migrations";export{matchedRows,mutationCount}from"./affected-rows";export*from"./seeder";export*from"./drivers";export*from"./custom";export*from"./auth-tables";export*from"./utc-defaults";export*from"./uuid-columns";export*from"./managed-columns";export*from"./relation-columns";export{ensureNotificationForeignKeys,migrateNotificationTables}from"./notification-tables";export{migrateRbacTables}from"./rbac-tables";export*from"./trait-tables";export*from"./datetime-columns";export*from"./dialect";export*from"./replicas";export*from"./ddl-constraints";export*from"./erd";export*from"./vschema";export*from"./sql-helpers";export*from"./defaults";export*from"./migration-dialect";export*from"./migration-path";export*from"./migration-ledger";export*from"./model-sources";export*from"./package-migrations";export*from"./package-models";export*from"./shadowed-models";export*from"./ensure-database";export{auditForeignKeys,classifyDeclaredFKs,findFkOrphans,fkKey,getDeclaredFKs,getLiveFKs,getLiveTables}from"./fk-audit";export{auditSchemaDrift,formatSchemaDrift}from"./schema-drift";export{describeIgnoreRule,findIgnoredMigrations,formatIgnoredMigrations}from"./ignored-migrations";export{auditUniqueIndexes,getDeclaredUniques,getLiveUniqueIndexes}from"./unique-audit";export{__flushAfterCommitNow,__pendingAfterCommitCount,enqueueAfterCommit,isInTransaction,runInTransactionScope}from"./transaction-context";export{createQueryBuilder,setConfig}from"@stacksjs/query-builder";export{createDynamo,dynamo,EntityQueryBuilder,generateKeyPattern,parseKeyPattern,buildKey,marshall,unmarshall}from"./drivers/dynamodb";
1
+ export{Database,createDatabase,createMysqlDatabase,createPostgresDatabase,createSqliteDatabase,createTursoDatabase}from"./database";export{detectDriver,driverDefaults,getConfigFromEnv,getConnectionString,mergeWithDefaults,validateDriverConfig}from"./driver-config";export*from"./utils";export{BUN_BROKEN_MYSQL_POOL_MESSAGE,BUN_BROKEN_POSTGRES_POOL_MESSAGE,getBrokenDatabasePools,isBrokenBunPoolError}from"./broken-pool";export*from"./types";export*from"./migrations";export*from"./framework-renames";export{setQueryTracker,logQuery}from"./query-logger";export{addColumnSafely,backfillInBatches,renameColumnSafely}from"./safe-migrations";export{matchedRows,mutationCount}from"./affected-rows";export*from"./seeder";export*from"./drivers";export*from"./custom";export*from"./auth-tables";export*from"./utc-defaults";export*from"./uuid-columns";export*from"./managed-columns";export*from"./relation-columns";export{ensureNotificationForeignKeys,migrateNotificationTables}from"./notification-tables";export{migrateRbacTables}from"./rbac-tables";export*from"./trait-tables";export*from"./datetime-columns";export*from"./dialect";export*from"./replicas";export*from"./ddl-constraints";export*from"./erd";export*from"./vschema";export*from"./sql-helpers";export*from"./defaults";export*from"./migration-dialect";export*from"./migration-path";export*from"./migration-ledger";export*from"./model-sources";export*from"./package-migrations";export*from"./package-models";export*from"./shadowed-models";export*from"./ensure-database";export{auditForeignKeys,classifyDeclaredFKs,findFkOrphans,fkKey,getDeclaredFKs,getLiveFKs,getLiveTables}from"./fk-audit";export{auditSchemaDrift,formatSchemaDrift}from"./schema-drift";export{describeIgnoreRule,findIgnoredMigrations,formatIgnoredMigrations}from"./ignored-migrations";export{auditUniqueIndexes,getDeclaredUniques,getLiveUniqueIndexes}from"./unique-audit";export{__flushAfterCommitNow,__pendingAfterCommitCount,enqueueAfterCommit,isInTransaction,runInTransactionScope}from"./transaction-context";export{createQueryBuilder,setConfig}from"@stacksjs/query-builder";export{createDynamo,dynamo,EntityQueryBuilder,generateKeyPattern,parseKeyPattern,buildKey,marshall,unmarshall}from"./drivers/dynamodb";
@@ -1 +1 @@
1
- import{existsSync,readdirSync,readFileSync}from"node:fs";import process from"node:process";import{join}from"node:path";import{resolveMigrationDirectory}from"./migration-path";const SAFE_MIGRATION_FILE=/^[\w.-]+\.sql$/,IDENT=String.raw`["\`\[]?([A-Za-z_]\w*)["\`\]]?`;export function stripForEffects(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}if(rest[0]==="'"){let j=1;while(j<rest.length&&rest[j]!=="'")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}function statementsOf(sql){return stripForEffects(sql).split(";").map((s)=>s.trim()).filter((s)=>s.length>0)}export function logicalName(file){return file.replace(/^\d+[-_]/,"").replace(/\.sql$/i,"")}export function migrationRemovals(sql){const removals=[],seen=new Set,push=(effect)=>{const key=effectKey(effect);if(seen.has(key))return;seen.add(key);removals.push(effect)};for(const statement of statementsOf(sql)){const index=new RegExp(String.raw`^DROP\s+INDEX\s+(?:CONCURRENTLY\s+)?(?:IF\s+EXISTS\s+)?${IDENT}`,"i").exec(statement);if(index?.[1]){push({kind:"index",name:index[1]});continue}const table=new RegExp(String.raw`^DROP\s+TABLE\s+(?:IF\s+EXISTS\s+)?${IDENT}`,"i").exec(statement);if(table?.[1]){push({kind:"table",name:table[1]});continue}const enumType=new RegExp(String.raw`^DROP\s+TYPE\s+(?:IF\s+EXISTS\s+)?${IDENT}`,"i").exec(statement);if(enumType?.[1]){push({kind:"enum",name:enumType[1]});continue}const alter=new RegExp(String.raw`^ALTER\s+TABLE\s+(?:IF\s+EXISTS\s+)?${IDENT}\s+(.*)$`,"is").exec(statement);if(!alter?.[1])continue;const owner=alter[1];for(const clause of(alter[2]??"").split(",")){const constraint=new RegExp(String.raw`^\s*DROP\s+CONSTRAINT\s+(?:IF\s+EXISTS\s+)?${IDENT}`,"i").exec(clause);if(constraint?.[1]){push({kind:"constraint",table:owner,name:constraint[1]});continue}const column=new RegExp(String.raw`^\s*DROP\s+(?:COLUMN\s+)?(?:IF\s+EXISTS\s+)?${IDENT}`,"i").exec(clause);if(column?.[1]&&!/^(?:constraint|index|key|primary|foreign|unique|check|default|partition)$/i.test(column[1]))push({kind:"column",table:owner,name:column[1]})}}return removals}export function migrationEffects(sql){const effects=[],seen=new Set,renamedAway=new Set,push=(effect)=>{const key=effectKey(effect);if(seen.has(key))return;seen.add(key);effects.push(effect)};for(const statement of statementsOf(sql)){const create=new RegExp(String.raw`^CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?${IDENT}`,"i").exec(statement);if(create?.[1]){push({kind:"table",name:create[1]});continue}const rename=new RegExp(String.raw`^ALTER\s+TABLE\s+${IDENT}\s+RENAME\s+TO\s+${IDENT}`,"i").exec(statement);if(rename?.[2]){if(rename[1])renamedAway.add(rename[1].toLowerCase());push({kind:"table",name:rename[2]});continue}const index=new RegExp(String.raw`^CREATE\s+(?:UNIQUE\s+)?INDEX\s+(?:IF\s+NOT\s+EXISTS\s+)?${IDENT}(?:\s+ON\s+${IDENT})?`,"i").exec(statement);if(index?.[1]){push(index[2]?{kind:"index",name:index[1],table:index[2]}:{kind:"index",name:index[1]});continue}const enumType=new RegExp(String.raw`^CREATE\s+TYPE\s+${IDENT}\s+AS\s+ENUM`,"i").exec(statement);if(enumType?.[1]){push({kind:"enum",name:enumType[1]});continue}const alter=new RegExp(String.raw`^ALTER\s+TABLE\s+(?:IF\s+EXISTS\s+)?${IDENT}\s+(.*)$`,"is").exec(statement);if(!alter?.[1]||!alter[2])continue;const table=alter[1],addColumn=new RegExp(String.raw`\bADD\s+COLUMN\s+(?:IF\s+NOT\s+EXISTS\s+)?${IDENT}`,"gi");for(const m of alter[2].matchAll(addColumn))if(m[1])push({kind:"column",table,name:m[1]});const addConstraint=new RegExp(String.raw`\bADD\s+CONSTRAINT\s+(?:IF\s+NOT\s+EXISTS\s+)?${IDENT}`,"gi");for(const m of alter[2].matchAll(addConstraint))if(m[1])push({kind:"constraint",table,name:m[1]});const addBare=new RegExp(String.raw`\bADD\s+(?!COLUMN\b|CONSTRAINT\b|INDEX\b|KEY\b|PRIMARY\b|UNIQUE\b|FOREIGN\b|FULLTEXT\b|SPATIAL\b|CHECK\b)${IDENT}\s+\w`,"gi");for(const m of alter[2].matchAll(addBare))if(m[1])push({kind:"column",table,name:m[1]})}if(renamedAway.size===0)return effects;return effects.filter((effect)=>{const owner=(effect.kind==="table"?effect.name:effect.table??"").toLowerCase();return!renamedAway.has(owner)})}function effectKey(effect){return`${effect.kind}:${(effect.table??"").toLowerCase()}.${effect.name.toLowerCase()}`}function removalKey(effect){if(effect.kind==="column"||effect.kind==="constraint")return effectKey(effect);return`${effect.kind}:${effect.name.toLowerCase()}`}export function verifiableEffects(effects,dialect){if(dialect==="postgres")return effects;if(dialect==="mysql")return effects.filter((e)=>e.kind!=="enum");return effects.filter((e)=>e.kind!=="constraint"&&e.kind!=="enum")}function emptySchema(){return{tables:new Set,columns:new Map,indexes:new Set,constraints:new Set,enums:new Set}}function rowsOf(result){return Array.isArray(result)?result:[]}async function defaultRunner(){const{db}=await import("./utils");return async(sql)=>rowsOf(await db.unsafe(sql).execute())}function pick(row,...keys){for(const key of keys){const value=row?.[key]??row?.[key.toLowerCase()]??row?.[key.toUpperCase()];if(typeof value==="string"&&value.length>0)return value}return""}export async function readLiveSchema(dialect,runner){const schema=emptySchema(),run=runner??await defaultRunner();if(dialect==="sqlite"){for(const row of await run("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")){const name=pick(row,"name");if(name)schema.tables.add(name.toLowerCase())}for(const row of await run("SELECT name FROM sqlite_master WHERE type='index' AND name NOT LIKE 'sqlite_%'")){const name=pick(row,"name");if(name)schema.indexes.add(name.toLowerCase())}for(const table of schema.tables){if(!/^[a-z_]\w*$/i.test(table))continue;const cols=new Set;for(const row of await run(`PRAGMA table_info("${table}")`)){const name=pick(row,"name");if(name)cols.add(name.toLowerCase())}schema.columns.set(table,cols)}return schema}if(dialect==="mysql"){for(const row of await run("SELECT TABLE_NAME AS name FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE()")){const name=pick(row,"name","TABLE_NAME");if(name)schema.tables.add(name.toLowerCase())}for(const row of await run("SELECT TABLE_NAME, COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE()")){const table=pick(row,"TABLE_NAME").toLowerCase(),column=pick(row,"COLUMN_NAME").toLowerCase();if(!table||!column)continue;if(!schema.columns.has(table))schema.columns.set(table,new Set);schema.columns.get(table).add(column)}for(const row of await run("SELECT DISTINCT INDEX_NAME AS name FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE()")){const name=pick(row,"name","INDEX_NAME");if(name)schema.indexes.add(name.toLowerCase())}for(const row of await run("SELECT CONSTRAINT_NAME AS name FROM information_schema.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE()")){const name=pick(row,"name","CONSTRAINT_NAME");if(name)schema.constraints.add(name.toLowerCase())}return schema}for(const row of await run("SELECT tablename AS name FROM pg_tables WHERE schemaname = 'public'")){const name=pick(row,"name","tablename");if(name)schema.tables.add(name.toLowerCase())}for(const row of await run("SELECT table_name, column_name FROM information_schema.columns WHERE table_schema = 'public'")){const table=pick(row,"table_name").toLowerCase(),column=pick(row,"column_name").toLowerCase();if(!table||!column)continue;if(!schema.columns.has(table))schema.columns.set(table,new Set);schema.columns.get(table).add(column)}for(const row of await run("SELECT indexname AS name FROM pg_indexes WHERE schemaname = 'public'")){const name=pick(row,"name","indexname");if(name)schema.indexes.add(name.toLowerCase())}for(const row of await run("SELECT c.conname AS name FROM pg_constraint c JOIN pg_namespace n ON n.oid = c.connamespace WHERE n.nspname = 'public'")){const name=pick(row,"name","conname");if(name)schema.constraints.add(name.toLowerCase())}for(const row of await run("SELECT t.typname AS name FROM pg_type t JOIN pg_namespace n ON n.oid = t.typnamespace WHERE t.typtype = 'e' AND n.nspname = 'public'")){const name=pick(row,"name","typname");if(name)schema.enums.add(name.toLowerCase())}return schema}export function effectPresent(effect,schema){const name=effect.name.toLowerCase();switch(effect.kind){case"table":return schema.tables.has(name);case"column":return schema.columns.get((effect.table??"").toLowerCase())?.has(name)??!1;case"index":{if(schema.indexes.has(name))return!0;const table=(effect.table??"").toLowerCase();if(!table)return!1;return schema.indexes.has(`${table}_${name}`)}case"constraint":return schema.constraints.has(name);case"enum":return schema.enums.has(name)}}export function classifyMigration(recorded,present,absent){const verifiable=present.length+absent.length;if(recorded){if(verifiable===0||absent.length===0)return"applied";return"reverted"}if(verifiable===0)return"unverifiable";if(absent.length===0)return"stranded";if(present.length===0)return"pending";return"partial"}function migrationsDir(dir){if(dir)return dir;const driver=String(process.env.DB_CONNECTION||"sqlite").toLowerCase();return resolveMigrationDirectory(driver==="singlestore"?"singlestore":driver)}function listMigrationFiles(dir){if(!existsSync(dir))return[];try{return readdirSync(dir).filter((f)=>f.toLowerCase().endsWith(".sql")).sort()}catch{return[]}}async function currentDialect(){const driver=((await import("@stacksjs/env")).env?.DB_CONNECTION??"sqlite").toLowerCase();if(driver==="sqlite"||driver==="mysql"||driver==="postgres")return driver;if(driver==="vitess"||driver==="singlestore")return"mysql";return"other"}export async function readLedger(runner){try{return(await(runner??await defaultRunner())("SELECT migration FROM migrations")).map((row)=>pick(row,"migration")).filter((name)=>name.length>0).sort()}catch{return[]}}export function planLedgerRemap(ledger,diskFiles){const onDisk=new Set(diskFiles),byLogical=new Map;for(const file of diskFiles){const key=logicalName(file);if(!byLogical.has(key))byLogical.set(key,[]);byLogical.get(key).push(file)}const claimed=new Set(ledger.filter((row)=>onDisk.has(row))),remap=[],ambiguous=[],dropped=[],superseded=[],targets=new Map;for(const row of ledger){if(onDisk.has(row))continue;const sameLogical=byLogical.get(logicalName(row))??[],candidates=sameLogical.filter((f)=>!claimed.has(f));if(candidates.length===0){if(sameLogical.length>0)superseded.push(row);else dropped.push(row);continue}if(candidates.length>1){ambiguous.push(row);continue}const to=candidates[0];if(!targets.has(to))targets.set(to,[]);targets.get(to).push(row);remap.push({from:row,to})}const contested=new Set([...targets.entries()].filter(([,rows])=>rows.length>1).flatMap(([,rows])=>rows));if(contested.size===0)return{remap,ambiguous,dropped,superseded};return{remap:remap.filter((r)=>!contested.has(r.from)),ambiguous:[...ambiguous,...contested].sort(),dropped,superseded}}export async function auditMigrationLedger(options={}){const dir=migrationsDir(options.dir),dialect=options.dialect??await currentDialect(),files=listMigrationFiles(dir),counts={applied:0,stranded:0,pending:0,partial:0,unverifiable:0,reverted:0},emptyPlan={remap:[],ambiguous:[],dropped:[],superseded:[]};if(dialect==="other")return{supported:!1,dialect,dir,entries:[],orphans:[],counts,recordedCount:0,remapPlan:emptyPlan,drift:!1};const run=options.run??await defaultRunner(),ledger=await readLedger(run),recorded=new Set(ledger),schema=await readLiveSchema(dialect,run),sources=new Map;for(const file of files)try{sources.set(file,readFileSync(join(dir,file),"utf8"))}catch{continue}const readFiles=[...sources.keys()],removedLater=new Map,tablesDroppedLater=new Map;for(let i=0;i<readFiles.length;i++){const laterKeys=new Set,laterTables=new Set;for(let j=i+1;j<readFiles.length;j++)for(const removal of migrationRemovals(sources.get(readFiles[j]))){laterKeys.add(removalKey(removal));if(removal.kind==="table")laterTables.add(removal.name.toLowerCase())}removedLater.set(readFiles[i],laterKeys);tablesDroppedLater.set(readFiles[i],laterTables)}const entries=[];for(const file of readFiles){const sql=sources.get(file),effects=verifiableEffects(migrationEffects(sql),dialect),dropped=removedLater.get(file)??new Set,droppedTables=tablesDroppedLater.get(file)??new Set,survives=(effect)=>effectPresent(effect,schema)||dropped.has(removalKey(effect))||effect.table!==void 0&&droppedTables.has(effect.table.toLowerCase()),present=effects.filter(survives),absent=effects.filter((effect)=>!survives(effect)),isRecorded=recorded.has(file),status=classifyMigration(isRecorded,present,absent);counts[status]+=1;entries.push({file,logical:logicalName(file),recorded:isRecorded,status,effects,present,absent})}const readable=entries.map((entry)=>entry.file),remapPlan=planLedgerRemap(ledger,readable),renamedTo=new Map(remapPlan.remap.map((r)=>[r.from,r.to])),orphans=ledger.filter((row)=>!readable.includes(row)).map((row)=>({migration:row,renamedTo:renamedTo.get(row)})),drift=counts.stranded>0||counts.partial>0||counts.reverted>0||orphans.length>0;return{supported:!0,dialect,dir,entries,orphans,counts,recordedCount:ledger.length,remapPlan,drift}}async function ensureLedgerTable(dialect,run){const id=dialect==="postgres"?"id SERIAL PRIMARY KEY":dialect==="mysql"?"id INT AUTO_INCREMENT PRIMARY KEY":"id INTEGER PRIMARY KEY AUTOINCREMENT",timestamp=dialect==="postgres"?"TIMESTAMP":"DATETIME",utcNow=dialect==="postgres"?"(now() AT TIME ZONE 'utc')":dialect==="mysql"?"(UTC_TIMESTAMP)":"CURRENT_TIMESTAMP";await run(`CREATE TABLE IF NOT EXISTS migrations (${id}, migration VARCHAR(255) NOT NULL UNIQUE, executed_at ${timestamp} DEFAULT ${utcNow})`);if(dialect!=="sqlite")try{await run(`ALTER TABLE migrations ALTER COLUMN executed_at SET DEFAULT ${utcNow}`)}catch{}}function byLogicalOnDisk(row,diskFiles){const key=logicalName(row),matches=diskFiles.filter((f)=>logicalName(f)===key);return matches.length===1?matches[0]:void 0}export async function reconcileMigrationLedger(options={}){const run=options.run??await defaultRunner(),audit=await auditMigrationLedger({dir:options.dir,dialect:options.dialect,run}),result={remapped:[],recorded:[],pruned:[],skipped:[]};if(!audit.supported){result.skipped.push({file:"*",reason:`dialect "${audit.dialect}" is not audited`});return result}const plan=audit.remapPlan;for(const row of plan.ambiguous)result.skipped.push({file:row,reason:"ledger row matches more than one file by logical name"});for(const row of plan.dropped)result.skipped.push({file:row,reason:"recorded migration no longer exists on disk"});const prunable=plan.superseded.filter((row)=>SAFE_MIGRATION_FILE.test(row));for(const row of plan.superseded.filter((row)=>!SAFE_MIGRATION_FILE.test(row)))result.skipped.push({file:row,reason:"ledger row is not safe to write to the ledger"});const toRecord=[];for(const entry of audit.entries){if(entry.status==="stranded"){toRecord.push(entry.file);continue}if(entry.status==="partial"){if(options.includePartial){toRecord.push(entry.file);continue}result.skipped.push({file:entry.file,reason:`${entry.present.length}/${entry.effects.length} effects present - resolve by hand, or pass --include-partial`});continue}if(entry.status==="reverted")result.skipped.push({file:entry.file,reason:`recorded, but ${entry.absent.length} effect(s) are missing from the schema`})}const unsafe=(file)=>!SAFE_MIGRATION_FILE.test(file);for(const{from,to}of plan.remap.filter((r)=>unsafe(r.from)||unsafe(r.to)))result.skipped.push({file:unsafe(from)?from:to,reason:"migration filename is not safe to write to the ledger"});for(const file of toRecord.filter(unsafe))result.skipped.push({file,reason:"migration filename is not safe to write to the ledger"});const remapped=plan.remap.filter((r)=>!unsafe(r.from)&&!unsafe(r.to)),recordable=toRecord.filter((file)=>!unsafe(file)&&!remapped.some((r)=>r.to===file));if(options.dryRun){result.remapped=remapped;result.recorded=recordable;result.pruned=prunable;return result}await ensureLedgerTable(audit.dialect,run);for(const{from,to}of remapped){await run(`UPDATE migrations SET migration = '${to}' WHERE migration = '${from}'`);result.remapped.push({from,to})}for(const file of recordable){if((await run(`SELECT migration FROM migrations WHERE migration = '${file}'`)).length>0)continue;await run(`INSERT INTO migrations (migration) VALUES ('${file}')`);result.recorded.push(file)}for(const row of prunable){const survivor=byLogicalOnDisk(row,audit.entries.map((e)=>e.file));if(!survivor)continue;if((await run(`SELECT migration FROM migrations WHERE migration = '${survivor}'`)).length===0){result.skipped.push({file:row,reason:`would leave ${survivor} unrecorded`});continue}await run(`DELETE FROM migrations WHERE migration = '${row}'`);result.pruned.push(row)}return result}
1
+ import{existsSync,readdirSync,readFileSync}from"node:fs";import process from"node:process";import{join}from"node:path";import{resolveMigrationDirectory}from"./migration-path";const SAFE_MIGRATION_FILE=/^[\w.-]+\.sql$/,IDENT=String.raw`["\`\[]?([A-Za-z_]\w*)["\`\]]?`;export function stripForEffects(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}if(rest[0]==="'"){let j=1;while(j<rest.length&&rest[j]!=="'")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}function statementsOf(sql){return stripForEffects(sql).split(";").map((s)=>s.trim()).filter((s)=>s.length>0)}export function logicalName(file){return file.replace(/^\d+[-_]/,"").replace(/\.sql$/i,"")}export function migrationRemovals(sql){const removals=[],seen=new Set,push=(effect)=>{const key=effectKey(effect);if(seen.has(key))return;seen.add(key);removals.push(effect)};for(const statement of statementsOf(sql)){const index=new RegExp(String.raw`^DROP\s+INDEX\s+(?:CONCURRENTLY\s+)?(?:IF\s+EXISTS\s+)?${IDENT}`,"i").exec(statement);if(index?.[1]){push({kind:"index",name:index[1]});continue}const table=new RegExp(String.raw`^DROP\s+TABLE\s+(?:IF\s+EXISTS\s+)?${IDENT}`,"i").exec(statement);if(table?.[1]){push({kind:"table",name:table[1]});continue}const enumType=new RegExp(String.raw`^DROP\s+TYPE\s+(?:IF\s+EXISTS\s+)?${IDENT}`,"i").exec(statement);if(enumType?.[1]){push({kind:"enum",name:enumType[1]});continue}const alter=new RegExp(String.raw`^ALTER\s+TABLE\s+(?:IF\s+EXISTS\s+)?${IDENT}\s+(.*)$`,"is").exec(statement);if(!alter?.[1])continue;const owner=alter[1];for(const clause of(alter[2]??"").split(",")){const constraint=new RegExp(String.raw`^\s*DROP\s+CONSTRAINT\s+(?:IF\s+EXISTS\s+)?${IDENT}`,"i").exec(clause);if(constraint?.[1]){push({kind:"constraint",table:owner,name:constraint[1]});continue}const column=new RegExp(String.raw`^\s*DROP\s+(?:COLUMN\s+)?(?:IF\s+EXISTS\s+)?${IDENT}`,"i").exec(clause);if(column?.[1]&&!/^(?:constraint|index|key|primary|foreign|unique|check|default|partition)$/i.test(column[1]))push({kind:"column",table:owner,name:column[1]})}}return removals}export function migrationEffects(sql){const effects=[],seen=new Set,renamedAway=new Set,push=(effect)=>{const key=effectKey(effect);if(seen.has(key))return;seen.add(key);effects.push(effect)};for(const statement of statementsOf(sql)){const create=new RegExp(String.raw`^CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?${IDENT}`,"i").exec(statement);if(create?.[1]){push({kind:"table",name:create[1]});continue}const rename=new RegExp(String.raw`^ALTER\s+TABLE\s+${IDENT}\s+RENAME\s+TO\s+${IDENT}`,"i").exec(statement);if(rename?.[2]){if(rename[1])renamedAway.add(rename[1].toLowerCase());push({kind:"table",name:rename[2]});continue}const index=new RegExp(String.raw`^CREATE\s+(?:UNIQUE\s+)?INDEX\s+(?:IF\s+NOT\s+EXISTS\s+)?${IDENT}(?:\s+ON\s+${IDENT})?`,"i").exec(statement);if(index?.[1]){push(index[2]?{kind:"index",name:index[1],table:index[2]}:{kind:"index",name:index[1]});continue}const enumType=new RegExp(String.raw`^CREATE\s+TYPE\s+${IDENT}\s+AS\s+ENUM`,"i").exec(statement);if(enumType?.[1]){push({kind:"enum",name:enumType[1]});continue}const alter=new RegExp(String.raw`^ALTER\s+TABLE\s+(?:IF\s+EXISTS\s+)?${IDENT}\s+(.*)$`,"is").exec(statement);if(!alter?.[1]||!alter[2])continue;const table=alter[1],addColumn=new RegExp(String.raw`\bADD\s+COLUMN\s+(?:IF\s+NOT\s+EXISTS\s+)?${IDENT}`,"gi");for(const m of alter[2].matchAll(addColumn))if(m[1])push({kind:"column",table,name:m[1]});const addConstraint=new RegExp(String.raw`\bADD\s+CONSTRAINT\s+(?:IF\s+NOT\s+EXISTS\s+)?${IDENT}`,"gi");for(const m of alter[2].matchAll(addConstraint))if(m[1])push({kind:"constraint",table,name:m[1]});const addBare=new RegExp(String.raw`\bADD\s+(?!COLUMN\b|CONSTRAINT\b|INDEX\b|KEY\b|PRIMARY\b|UNIQUE\b|FOREIGN\b|FULLTEXT\b|SPATIAL\b|CHECK\b)${IDENT}\s+\w`,"gi");for(const m of alter[2].matchAll(addBare))if(m[1])push({kind:"column",table,name:m[1]})}if(renamedAway.size===0)return effects;return effects.filter((effect)=>{const owner=(effect.kind==="table"?effect.name:effect.table??"").toLowerCase();return!renamedAway.has(owner)})}function effectKey(effect){return`${effect.kind}:${(effect.table??"").toLowerCase()}.${effect.name.toLowerCase()}`}function removalKey(effect){if(effect.kind==="column"||effect.kind==="constraint")return effectKey(effect);return`${effect.kind}:${effect.name.toLowerCase()}`}export function verifiableEffects(effects,dialect){if(dialect==="postgres")return effects;if(dialect==="mysql")return effects.filter((e)=>e.kind!=="enum");return effects.filter((e)=>e.kind!=="constraint"&&e.kind!=="enum")}function emptySchema(){return{tables:new Set,columns:new Map,indexes:new Set,constraints:new Set,enums:new Set}}function rowsOf(result){return Array.isArray(result)?result:[]}async function defaultRunner(){const{db}=await import("./utils");return async(sql)=>rowsOf(await db.unsafe(sql).execute())}function pick(row,...keys){for(const key of keys){const value=row?.[key]??row?.[key.toLowerCase()]??row?.[key.toUpperCase()];if(typeof value==="string"&&value.length>0)return value}return""}export async function readLiveSchema(dialect,runner){const schema=emptySchema(),run=runner??await defaultRunner();if(dialect==="sqlite"){for(const row of await run("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")){const name=pick(row,"name");if(name)schema.tables.add(name.toLowerCase())}for(const row of await run("SELECT name FROM sqlite_master WHERE type='index' AND name NOT LIKE 'sqlite_%'")){const name=pick(row,"name");if(name)schema.indexes.add(name.toLowerCase())}for(const table of schema.tables){if(!/^[a-z_]\w*$/i.test(table))continue;const cols=new Set;for(const row of await run(`PRAGMA table_info("${table}")`)){const name=pick(row,"name");if(name)cols.add(name.toLowerCase())}schema.columns.set(table,cols)}return schema}if(dialect==="mysql"){for(const row of await run("SELECT TABLE_NAME AS name FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE()")){const name=pick(row,"name","TABLE_NAME");if(name)schema.tables.add(name.toLowerCase())}for(const row of await run("SELECT TABLE_NAME, COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE()")){const table=pick(row,"TABLE_NAME").toLowerCase(),column=pick(row,"COLUMN_NAME").toLowerCase();if(!table||!column)continue;if(!schema.columns.has(table))schema.columns.set(table,new Set);schema.columns.get(table).add(column)}for(const row of await run("SELECT DISTINCT INDEX_NAME AS name FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE()")){const name=pick(row,"name","INDEX_NAME");if(name)schema.indexes.add(name.toLowerCase())}for(const row of await run("SELECT CONSTRAINT_NAME AS name FROM information_schema.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE()")){const name=pick(row,"name","CONSTRAINT_NAME");if(name)schema.constraints.add(name.toLowerCase())}return schema}for(const row of await run("SELECT tablename AS name FROM pg_tables WHERE schemaname = 'public'")){const name=pick(row,"name","tablename");if(name)schema.tables.add(name.toLowerCase())}for(const row of await run("SELECT table_name, column_name FROM information_schema.columns WHERE table_schema = 'public'")){const table=pick(row,"table_name").toLowerCase(),column=pick(row,"column_name").toLowerCase();if(!table||!column)continue;if(!schema.columns.has(table))schema.columns.set(table,new Set);schema.columns.get(table).add(column)}for(const row of await run("SELECT indexname AS name FROM pg_indexes WHERE schemaname = 'public'")){const name=pick(row,"name","indexname");if(name)schema.indexes.add(name.toLowerCase())}for(const row of await run("SELECT c.conname AS name FROM pg_constraint c JOIN pg_namespace n ON n.oid = c.connamespace WHERE n.nspname = 'public'")){const name=pick(row,"name","conname");if(name)schema.constraints.add(name.toLowerCase())}for(const row of await run("SELECT t.typname AS name FROM pg_type t JOIN pg_namespace n ON n.oid = t.typnamespace WHERE t.typtype = 'e' AND n.nspname = 'public'")){const name=pick(row,"name","typname");if(name)schema.enums.add(name.toLowerCase())}return schema}export function effectPresent(effect,schema){const name=effect.name.toLowerCase();switch(effect.kind){case"table":return schema.tables.has(name);case"column":return schema.columns.get((effect.table??"").toLowerCase())?.has(name)??!1;case"index":{if(schema.indexes.has(name))return!0;const table=(effect.table??"").toLowerCase();if(!table)return!1;return schema.indexes.has(`${table}_${name}`)}case"constraint":return schema.constraints.has(name);case"enum":return schema.enums.has(name)}}export function classifyMigration(recorded,present,absent){const verifiable=present.length+absent.length;if(recorded){if(verifiable===0||absent.length===0)return"applied";return"reverted"}if(verifiable===0)return"unverifiable";if(absent.length===0)return"stranded";if(present.length===0)return"pending";return"partial"}function migrationsDir(dir){if(dir)return dir;const driver=String(process.env.DB_CONNECTION||"sqlite").toLowerCase();return resolveMigrationDirectory(driver==="singlestore"?"singlestore":driver)}function listMigrationFiles(dir){if(!existsSync(dir))return[];try{return readdirSync(dir).filter((f)=>f.toLowerCase().endsWith(".sql")).sort()}catch{return[]}}async function currentDialect(){const driver=((await import("@stacksjs/env")).env?.DB_CONNECTION??"sqlite").toLowerCase();if(driver==="sqlite"||driver==="mysql"||driver==="postgres")return driver;if(driver==="vitess"||driver==="singlestore")return"mysql";if(driver==="turso"||driver==="libsql")return"sqlite";return"other"}export async function readLedger(runner){try{return(await(runner??await defaultRunner())("SELECT migration FROM migrations")).map((row)=>pick(row,"migration")).filter((name)=>name.length>0).sort()}catch{return[]}}export function planLedgerRemap(ledger,diskFiles){const onDisk=new Set(diskFiles),byLogical=new Map;for(const file of diskFiles){const key=logicalName(file);if(!byLogical.has(key))byLogical.set(key,[]);byLogical.get(key).push(file)}const claimed=new Set(ledger.filter((row)=>onDisk.has(row))),remap=[],ambiguous=[],dropped=[],superseded=[],targets=new Map;for(const row of ledger){if(onDisk.has(row))continue;const sameLogical=byLogical.get(logicalName(row))??[],candidates=sameLogical.filter((f)=>!claimed.has(f));if(candidates.length===0){if(sameLogical.length>0)superseded.push(row);else dropped.push(row);continue}if(candidates.length>1){ambiguous.push(row);continue}const to=candidates[0];if(!targets.has(to))targets.set(to,[]);targets.get(to).push(row);remap.push({from:row,to})}const contested=new Set([...targets.entries()].filter(([,rows])=>rows.length>1).flatMap(([,rows])=>rows));if(contested.size===0)return{remap,ambiguous,dropped,superseded};return{remap:remap.filter((r)=>!contested.has(r.from)),ambiguous:[...ambiguous,...contested].sort(),dropped,superseded}}export async function auditMigrationLedger(options={}){const dir=migrationsDir(options.dir),dialect=options.dialect??await currentDialect(),files=listMigrationFiles(dir),counts={applied:0,stranded:0,pending:0,partial:0,unverifiable:0,reverted:0},emptyPlan={remap:[],ambiguous:[],dropped:[],superseded:[]};if(dialect==="other")return{supported:!1,dialect,dir,entries:[],orphans:[],counts,recordedCount:0,remapPlan:emptyPlan,drift:!1};const run=options.run??await defaultRunner(),ledger=await readLedger(run),recorded=new Set(ledger),schema=await readLiveSchema(dialect,run),sources=new Map;for(const file of files)try{sources.set(file,readFileSync(join(dir,file),"utf8"))}catch{continue}const readFiles=[...sources.keys()],removedLater=new Map,tablesDroppedLater=new Map;for(let i=0;i<readFiles.length;i++){const laterKeys=new Set,laterTables=new Set;for(let j=i+1;j<readFiles.length;j++)for(const removal of migrationRemovals(sources.get(readFiles[j]))){laterKeys.add(removalKey(removal));if(removal.kind==="table")laterTables.add(removal.name.toLowerCase())}removedLater.set(readFiles[i],laterKeys);tablesDroppedLater.set(readFiles[i],laterTables)}const entries=[];for(const file of readFiles){const sql=sources.get(file),effects=verifiableEffects(migrationEffects(sql),dialect),dropped=removedLater.get(file)??new Set,droppedTables=tablesDroppedLater.get(file)??new Set,survives=(effect)=>effectPresent(effect,schema)||dropped.has(removalKey(effect))||effect.table!==void 0&&droppedTables.has(effect.table.toLowerCase()),present=effects.filter(survives),absent=effects.filter((effect)=>!survives(effect)),isRecorded=recorded.has(file),status=classifyMigration(isRecorded,present,absent);counts[status]+=1;entries.push({file,logical:logicalName(file),recorded:isRecorded,status,effects,present,absent})}const readable=entries.map((entry)=>entry.file),remapPlan=planLedgerRemap(ledger,readable),renamedTo=new Map(remapPlan.remap.map((r)=>[r.from,r.to])),orphans=ledger.filter((row)=>!readable.includes(row)).map((row)=>({migration:row,renamedTo:renamedTo.get(row)})),drift=counts.stranded>0||counts.partial>0||counts.reverted>0||orphans.length>0;return{supported:!0,dialect,dir,entries,orphans,counts,recordedCount:ledger.length,remapPlan,drift}}async function ensureLedgerTable(dialect,run){const id=dialect==="postgres"?"id SERIAL PRIMARY KEY":dialect==="mysql"?"id INT AUTO_INCREMENT PRIMARY KEY":"id INTEGER PRIMARY KEY AUTOINCREMENT",timestamp=dialect==="postgres"?"TIMESTAMP":"DATETIME",utcNow=dialect==="postgres"?"(now() AT TIME ZONE 'utc')":dialect==="mysql"?"(UTC_TIMESTAMP)":"CURRENT_TIMESTAMP";await run(`CREATE TABLE IF NOT EXISTS migrations (${id}, migration VARCHAR(255) NOT NULL UNIQUE, executed_at ${timestamp} DEFAULT ${utcNow})`);if(dialect!=="sqlite")try{await run(`ALTER TABLE migrations ALTER COLUMN executed_at SET DEFAULT ${utcNow}`)}catch{}}function byLogicalOnDisk(row,diskFiles){const key=logicalName(row),matches=diskFiles.filter((f)=>logicalName(f)===key);return matches.length===1?matches[0]:void 0}export async function reconcileMigrationLedger(options={}){const run=options.run??await defaultRunner(),audit=await auditMigrationLedger({dir:options.dir,dialect:options.dialect,run}),result={remapped:[],recorded:[],pruned:[],skipped:[]};if(!audit.supported){result.skipped.push({file:"*",reason:`dialect "${audit.dialect}" is not audited`});return result}const plan=audit.remapPlan;for(const row of plan.ambiguous)result.skipped.push({file:row,reason:"ledger row matches more than one file by logical name"});for(const row of plan.dropped)result.skipped.push({file:row,reason:"recorded migration no longer exists on disk"});const prunable=plan.superseded.filter((row)=>SAFE_MIGRATION_FILE.test(row));for(const row of plan.superseded.filter((row)=>!SAFE_MIGRATION_FILE.test(row)))result.skipped.push({file:row,reason:"ledger row is not safe to write to the ledger"});const toRecord=[];for(const entry of audit.entries){if(entry.status==="stranded"){toRecord.push(entry.file);continue}if(entry.status==="partial"){if(options.includePartial){toRecord.push(entry.file);continue}result.skipped.push({file:entry.file,reason:`${entry.present.length}/${entry.effects.length} effects present - resolve by hand, or pass --include-partial`});continue}if(entry.status==="reverted")result.skipped.push({file:entry.file,reason:`recorded, but ${entry.absent.length} effect(s) are missing from the schema`})}const unsafe=(file)=>!SAFE_MIGRATION_FILE.test(file);for(const{from,to}of plan.remap.filter((r)=>unsafe(r.from)||unsafe(r.to)))result.skipped.push({file:unsafe(from)?from:to,reason:"migration filename is not safe to write to the ledger"});for(const file of toRecord.filter(unsafe))result.skipped.push({file,reason:"migration filename is not safe to write to the ledger"});const remapped=plan.remap.filter((r)=>!unsafe(r.from)&&!unsafe(r.to)),recordable=toRecord.filter((file)=>!unsafe(file)&&!remapped.some((r)=>r.to===file));if(options.dryRun){result.remapped=remapped;result.recorded=recordable;result.pruned=prunable;return result}await ensureLedgerTable(audit.dialect,run);for(const{from,to}of remapped){await run(`UPDATE migrations SET migration = '${to}' WHERE migration = '${from}'`);result.remapped.push({from,to})}for(const file of recordable){if((await run(`SELECT migration FROM migrations WHERE migration = '${file}'`)).length>0)continue;await run(`INSERT INTO migrations (migration) VALUES ('${file}')`);result.recorded.push(file)}for(const row of prunable){const survivor=byLogicalOnDisk(row,audit.entries.map((e)=>e.file));if(!survivor)continue;if((await run(`SELECT migration FROM migrations WHERE migration = '${survivor}'`)).length===0){result.skipped.push({file:row,reason:`would leave ${survivor} unrecorded`});continue}await run(`DELETE FROM migrations WHERE migration = '${row}'`);result.pruned.push(row)}return result}
@@ -12,6 +12,21 @@
12
12
  * Throws an error if the lock can't be acquired within `timeoutMs`.
13
13
  */
14
14
  export declare function acquireMigrationLock(dialect: Dialect, adminDb: { unsafe: (sql: string) => Promise<unknown> } | null, opts?: { timeoutMs?: number, sqliteLockPath?: string }): Promise<MigrationLockHandle>;
15
+ /**
16
+ * The migration lock for a libSQL (Turso) database.
17
+ *
18
+ * A lock FILE only serialises processes on one machine, and a Turso database
19
+ * is reached from many: two deploy hosts migrating at once would both get a
20
+ * local file and both run the batch. There is no advisory-lock primitive to
21
+ * borrow either, so the lock is a row in the database itself - the one thing
22
+ * every migrating process shares. Inserting it is atomic (the name is the
23
+ * primary key), the holder refreshes it while working, and a row nobody has
24
+ * refreshed for {@link STALE_LOCK_MS} belongs to a process that died and is
25
+ * cleared by the next one to try.
26
+ */
27
+ export declare function acquireLibsqlMigrationLock(adminDb: LockStatementRunner, opts?: { timeoutMs?: number, staleMs?: number, heartbeatMs?: number }): Promise<MigrationLockHandle>;
28
+ /** The table the libSQL lock lives in. */
29
+ export declare const LIBSQL_LOCK_TABLE: 'stacks_migration_lock';
15
30
  /**
16
31
  * Returned by `acquireMigrationLock()`. Callers MUST invoke `release`
17
32
  * in a finally block; the lock is process-external (file, advisory,
@@ -20,4 +35,7 @@ export declare function acquireMigrationLock(dialect: Dialect, adminDb: { unsafe
20
35
  export declare interface MigrationLockHandle {
21
36
  release: () => Promise<void>
22
37
  }
38
+ declare interface LockStatementRunner {
39
+ unsafe: (sql: string, params?: unknown[]) => PromiseLike<unknown> | unknown
40
+ }
23
41
  export type Dialect = 'sqlite' | 'mysql' | 'vitess' | 'postgres';
@@ -1 +1 @@
1
- import{Buffer}from"node:buffer";import{createHash}from"node:crypto";import{closeSync,openSync,readFileSync,statSync,unlinkSync,writeFileSync}from"node:fs";import process from"node:process";import{userDatabasePath}from"@stacksjs/path";const DEFAULT_TIMEOUT_MS=30000,INITIAL_BACKOFF_MS=100,MAX_BACKOFF_MS=2000,STALE_LOCK_MS=60000,LOCK_NAME="stacks_migrations";export async function acquireMigrationLock(dialect,adminDb,opts={}){const timeoutMs=opts.timeoutMs??DEFAULT_TIMEOUT_MS;if(dialect!=="sqlite"&&dialect!=="postgres"&&dialect!=="mysql"&&dialect!=="vitess")throw Error(`[migration-lock] unknown dialect: ${String(dialect)}`);if(dialect==="sqlite")return acquireSqliteLock(opts.sqliteLockPath,timeoutMs);if(!adminDb)throw Error(`[migration-lock] ${dialect} requires a database connection to acquire the lock`);if(dialect==="postgres")return acquirePostgresLock(adminDb,timeoutMs);return acquireMySqlLock(adminDb,timeoutMs)}function lockKeysForPostgres(){const hash=createHash("sha256").update(LOCK_NAME).digest(),key1=hash.readInt32BE(0),key2=hash.readInt32BE(4);return{key1,key2}}async function acquirePostgresLock(adminDb,timeoutMs){const{key1,key2}=lockKeysForPostgres(),start=Date.now();let backoff=INITIAL_BACKOFF_MS;while(!0){const result=await adminDb.unsafe(`SELECT pg_try_advisory_lock(${key1}, ${key2}) AS acquired`);if(extractFirstBool(result,"acquired"))return{release:async()=>{try{await adminDb.unsafe(`SELECT pg_advisory_unlock(${key1}, ${key2})`)}catch{}}};if(Date.now()-start>=timeoutMs)throw Error("[migration-lock] another migration is in progress - could not acquire postgres advisory lock within timeout");await sleepWithJitter(backoff);backoff=Math.min(backoff*2,MAX_BACKOFF_MS)}}async function acquireMySqlLock(adminDb,timeoutMs){const start=Date.now();let backoff=INITIAL_BACKOFF_MS;while(!0){const result=await adminDb.unsafe(`SELECT GET_LOCK('${LOCK_NAME}', 0) AS acquired`);if(extractFirstInt(result,"acquired")===1)return{release:async()=>{try{await adminDb.unsafe(`SELECT RELEASE_LOCK('${LOCK_NAME}')`)}catch{}}};if(Date.now()-start>=timeoutMs)throw Error("[migration-lock] another migration is in progress - could not acquire MySQL named lock within timeout");await sleepWithJitter(backoff);backoff=Math.min(backoff*2,MAX_BACKOFF_MS)}}function defaultSqliteLockPath(){return userDatabasePath(".migration.lock")}async function acquireSqliteLock(lockPath,timeoutMs){const path=lockPath??defaultSqliteLockPath(),start=Date.now();let backoff=INITIAL_BACKOFF_MS;while(!0){if(tryCreateLockFile(path)){let released=!1;return{release:async()=>{if(released)return;released=!0;try{unlinkSync(path)}catch{}}}}reclaimIfStale(path);if(Date.now()-start>=timeoutMs)throw Error(`[migration-lock] another migration is in progress - lock file ${path} held within timeout`);await sleepWithJitter(backoff);backoff=Math.min(backoff*2,MAX_BACKOFF_MS)}}function tryCreateLockFile(path){try{const fd=openSync(path,"wx");try{const payload=JSON.stringify({pid:process.pid,startedAt:new Date().toISOString()});writeFileSync(fd,Buffer.from(payload,"utf8"))}finally{closeSync(fd)}return!0}catch(e){if(e.code==="EEXIST")return!1;throw e}}function reclaimIfStale(path){try{const st=statSync(path);if(Date.now()-st.mtimeMs<=STALE_LOCK_MS||lockOwnerIsAlive(path))return;try{unlinkSync(path)}catch{}}catch{}}function lockOwnerIsAlive(path){try{const pid=JSON.parse(readFileSync(path,"utf8")).pid;if(typeof pid!=="number"||!Number.isInteger(pid)||pid<=0)return!1;try{process.kill(pid,0);return!0}catch(error){return error.code==="EPERM"}}catch{return!1}}function sleepWithJitter(ms){const jittered=ms*(1+Math.random()*0.25);return new Promise((resolve)=>setTimeout(resolve,jittered))}function extractFirstBool(result,column){const row=pluckFirstRow(result);if(!row)return!1;const value=firstColumnValue(row,column);return value===!0||value===1||value==="1"||value==="t"}function extractFirstInt(result,column){const row=pluckFirstRow(result);if(!row)return null;const value=firstColumnValue(row,column);if(typeof value==="number")return value;if(typeof value==="string"&&/^-?\d+$/.test(value))return Number.parseInt(value,10);return null}function firstColumnValue(row,column){if(!row||typeof row!=="object")return;const record=row;if(column in record)return record[column];const values=Object.values(record);return values.length===1?values[0]:void 0}function pluckFirstRow(result){if(!result)return null;if(Array.isArray(result))return result[0];if(typeof result==="object"&&"rows"in result&&Array.isArray(result.rows))return result.rows[0];return null}
1
+ import{Buffer}from"node:buffer";import{createHash,randomUUID}from"node:crypto";import{closeSync,openSync,readFileSync,statSync,unlinkSync,writeFileSync}from"node:fs";import{hostname}from"node:os";import process from"node:process";import{userDatabasePath}from"@stacksjs/path";const DEFAULT_TIMEOUT_MS=30000,INITIAL_BACKOFF_MS=100,MAX_BACKOFF_MS=2000,STALE_LOCK_MS=60000,LOCK_NAME="stacks_migrations";export async function acquireMigrationLock(dialect,adminDb,opts={}){const timeoutMs=opts.timeoutMs??DEFAULT_TIMEOUT_MS;if(dialect!=="sqlite"&&dialect!=="postgres"&&dialect!=="mysql"&&dialect!=="vitess")throw Error(`[migration-lock] unknown dialect: ${String(dialect)}`);if(dialect==="sqlite")return acquireSqliteLock(opts.sqliteLockPath,timeoutMs);if(!adminDb)throw Error(`[migration-lock] ${dialect} requires a database connection to acquire the lock`);if(dialect==="postgres")return acquirePostgresLock(adminDb,timeoutMs);return acquireMySqlLock(adminDb,timeoutMs)}function lockKeysForPostgres(){const hash=createHash("sha256").update(LOCK_NAME).digest(),key1=hash.readInt32BE(0),key2=hash.readInt32BE(4);return{key1,key2}}async function acquirePostgresLock(adminDb,timeoutMs){const{key1,key2}=lockKeysForPostgres(),start=Date.now();let backoff=INITIAL_BACKOFF_MS;while(!0){const result=await adminDb.unsafe(`SELECT pg_try_advisory_lock(${key1}, ${key2}) AS acquired`);if(extractFirstBool(result,"acquired"))return{release:async()=>{try{await adminDb.unsafe(`SELECT pg_advisory_unlock(${key1}, ${key2})`)}catch{}}};if(Date.now()-start>=timeoutMs)throw Error("[migration-lock] another migration is in progress - could not acquire postgres advisory lock within timeout");await sleepWithJitter(backoff);backoff=Math.min(backoff*2,MAX_BACKOFF_MS)}}async function acquireMySqlLock(adminDb,timeoutMs){const start=Date.now();let backoff=INITIAL_BACKOFF_MS;while(!0){const result=await adminDb.unsafe(`SELECT GET_LOCK('${LOCK_NAME}', 0) AS acquired`);if(extractFirstInt(result,"acquired")===1)return{release:async()=>{try{await adminDb.unsafe(`SELECT RELEASE_LOCK('${LOCK_NAME}')`)}catch{}}};if(Date.now()-start>=timeoutMs)throw Error("[migration-lock] another migration is in progress - could not acquire MySQL named lock within timeout");await sleepWithJitter(backoff);backoff=Math.min(backoff*2,MAX_BACKOFF_MS)}}function defaultSqliteLockPath(){return userDatabasePath(".migration.lock")}async function acquireSqliteLock(lockPath,timeoutMs){const path=lockPath??defaultSqliteLockPath(),start=Date.now();let backoff=INITIAL_BACKOFF_MS;while(!0){if(tryCreateLockFile(path)){let released=!1;return{release:async()=>{if(released)return;released=!0;try{unlinkSync(path)}catch{}}}}reclaimIfStale(path);if(Date.now()-start>=timeoutMs)throw Error(`[migration-lock] another migration is in progress - lock file ${path} held within timeout`);await sleepWithJitter(backoff);backoff=Math.min(backoff*2,MAX_BACKOFF_MS)}}function tryCreateLockFile(path){try{const fd=openSync(path,"wx");try{const payload=JSON.stringify({pid:process.pid,startedAt:new Date().toISOString()});writeFileSync(fd,Buffer.from(payload,"utf8"))}finally{closeSync(fd)}return!0}catch(e){if(e.code==="EEXIST")return!1;throw e}}function reclaimIfStale(path){try{const st=statSync(path);if(Date.now()-st.mtimeMs<=STALE_LOCK_MS||lockOwnerIsAlive(path))return;try{unlinkSync(path)}catch{}}catch{}}function lockOwnerIsAlive(path){try{const pid=JSON.parse(readFileSync(path,"utf8")).pid;if(typeof pid!=="number"||!Number.isInteger(pid)||pid<=0)return!1;try{process.kill(pid,0);return!0}catch(error){return error.code==="EPERM"}}catch{return!1}}function sleepWithJitter(ms){const jittered=ms*(1+Math.random()*0.25);return new Promise((resolve)=>setTimeout(resolve,jittered))}function extractFirstBool(result,column){const row=pluckFirstRow(result);if(!row)return!1;const value=firstColumnValue(row,column);return value===!0||value===1||value==="1"||value==="t"}function extractFirstInt(result,column){const row=pluckFirstRow(result);if(!row)return null;const value=firstColumnValue(row,column);if(typeof value==="number")return value;if(typeof value==="string"&&/^-?\d+$/.test(value))return Number.parseInt(value,10);return null}function firstColumnValue(row,column){if(!row||typeof row!=="object")return;const record=row;if(column in record)return record[column];const values=Object.values(record);return values.length===1?values[0]:void 0}function pluckFirstRow(result){if(!result)return null;if(Array.isArray(result))return result[0];if(typeof result==="object"&&"rows"in result&&Array.isArray(result.rows))return result.rows[0];return null}export const LIBSQL_LOCK_TABLE="stacks_migration_lock";const LIBSQL_HEARTBEAT_MS=15000;export async function acquireLibsqlMigrationLock(adminDb,opts={}){const timeoutMs=opts.timeoutMs??DEFAULT_TIMEOUT_MS,staleMs=opts.staleMs??STALE_LOCK_MS,heartbeatMs=opts.heartbeatMs??LIBSQL_HEARTBEAT_MS,holder=`${hostname()}:${process.pid}:${randomUUID()}`,run=async(sql,params=[])=>await adminDb.unsafe(sql,params);await run(`CREATE TABLE IF NOT EXISTS "${LIBSQL_LOCK_TABLE}" (name TEXT PRIMARY KEY, holder TEXT NOT NULL, heartbeat_at INTEGER NOT NULL)`);const start=Date.now();let backoff=INITIAL_BACKOFF_MS;while(!0){await run(`DELETE FROM "${LIBSQL_LOCK_TABLE}" WHERE name = ? AND heartbeat_at < ?`,[LOCK_NAME,Date.now()-staleMs]);await run(`INSERT INTO "${LIBSQL_LOCK_TABLE}" (name, holder, heartbeat_at) VALUES (?, ?, ?) ON CONFLICT (name) DO NOTHING`,[LOCK_NAME,holder,Date.now()]);const rows=await run(`SELECT holder FROM "${LIBSQL_LOCK_TABLE}" WHERE name = ?`,[LOCK_NAME]);if((Array.isArray(rows)?rows[0]?.holder:void 0)===holder){const heartbeat=setInterval(()=>{Promise.resolve(run(`UPDATE "${LIBSQL_LOCK_TABLE}" SET heartbeat_at = ? WHERE name = ? AND holder = ?`,[Date.now(),LOCK_NAME,holder])).catch(()=>{})},heartbeatMs);heartbeat.unref?.();let released=!1;return{release:async()=>{if(released)return;released=!0;clearInterval(heartbeat);try{await run(`DELETE FROM "${LIBSQL_LOCK_TABLE}" WHERE name = ? AND holder = ?`,[LOCK_NAME,holder])}catch{}}}}if(Date.now()-start>=timeoutMs)throw Error(`[migration-lock] another migration is in progress - the libSQL lock row in "${LIBSQL_LOCK_TABLE}" was held within timeout`);await sleepWithJitter(backoff);backoff=Math.min(backoff*2,MAX_BACKOFF_MS)}}
@@ -1 +1 @@
1
- import{existsSync,readdirSync,readFileSync}from"node:fs";import{isAbsolute,join,relative}from"node:path";import process from"node:process";const DEFAULT_MIGRATION_DIR="database/migrations",DEFAULT_SNAPSHOT_DIR="storage/framework/database";export function resolveMigrationDirectory(dialect,options={}){const cwd=options.cwd??process.cwd(),configured=process.env.DB_MIGRATIONS_PATH||options.configured||DEFAULT_MIGRATION_DIR,absoluteConfigured=isAbsolute(configured)?configured:join(cwd,configured);if(configured!==DEFAULT_MIGRATION_DIR||isAbsolute(configured))return absoluteConfigured;const dialectDir=join(absoluteConfigured,dialect);if(existsSync(dialectDir))return dialectDir;if(dialect==="sqlite")return absoluteConfigured;const snapshotDir=isAbsolute(options.snapshotDir||"")?options.snapshotDir:join(cwd,options.snapshotDir||DEFAULT_SNAPSHOT_DIR);let snapshots=[];try{snapshots=readdirSync(snapshotDir)}catch{}const wanted=`model-snapshot.${dialect}.json`,hasOtherDialect=snapshots.some((file)=>/^model-snapshot\.\w+\.json$/.test(file)&&file!==wanted);if(hasOtherDialect&&ownerOfFlatCorpus(absoluteConfigured)===wireOf(dialect))return absoluteConfigured;return hasOtherDialect?dialectDir:absoluteConfigured}function ownerOfFlatCorpus(directory){let files=[];try{files=readdirSync(directory).filter((file)=>file.endsWith(".sql")).sort()}catch{return null}for(const file of files){let sql;try{sql=readFileSync(join(directory,file),"utf8")}catch{continue}const create=/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?(["`])/i.exec(sql);if(create)return create[1]==="`"?"mysql":"postgres"}return null}function wireOf(dialect){return dialect==="mysql"||dialect==="mariadb"||dialect==="singlestore"||dialect==="vitess"||dialect==="planetscale"?"mysql":"postgres"}export function relativeMigrationDirectory(directory,cwd=process.cwd()){const prefix=`${cwd}/`;return directory.startsWith(prefix)?directory.slice(prefix.length):directory}export function resolveSnapshotDirectory(cwd=process.cwd()){const configured=process.env.DB_SNAPSHOT_PATH||DEFAULT_SNAPSHOT_DIR;return isAbsolute(configured)?configured:join(cwd,configured)}export function snapshotDirForQueryBuilder(cwd=process.cwd()){return relative(cwd,resolveSnapshotDirectory(cwd))||"."}export function snapshotDirectoryIsShared(){return Boolean(process.env.DB_SNAPSHOT_PATH)}
1
+ import{existsSync,readdirSync,readFileSync}from"node:fs";import{isAbsolute,join,relative}from"node:path";import process from"node:process";const DEFAULT_MIGRATION_DIR="database/migrations",DEFAULT_SNAPSHOT_DIR="storage/framework/database";export function resolveMigrationDirectory(dialect,options={}){if(dialect==="turso"||dialect==="libsql")dialect="sqlite";const cwd=options.cwd??process.cwd(),configured=process.env.DB_MIGRATIONS_PATH||options.configured||DEFAULT_MIGRATION_DIR,absoluteConfigured=isAbsolute(configured)?configured:join(cwd,configured);if(configured!==DEFAULT_MIGRATION_DIR||isAbsolute(configured))return absoluteConfigured;const dialectDir=join(absoluteConfigured,dialect);if(existsSync(dialectDir))return dialectDir;if(dialect==="sqlite")return absoluteConfigured;const snapshotDir=isAbsolute(options.snapshotDir||"")?options.snapshotDir:join(cwd,options.snapshotDir||DEFAULT_SNAPSHOT_DIR);let snapshots=[];try{snapshots=readdirSync(snapshotDir)}catch{}const wanted=`model-snapshot.${dialect}.json`,hasOtherDialect=snapshots.some((file)=>/^model-snapshot\.\w+\.json$/.test(file)&&file!==wanted);if(hasOtherDialect&&ownerOfFlatCorpus(absoluteConfigured)===wireOf(dialect))return absoluteConfigured;return hasOtherDialect?dialectDir:absoluteConfigured}function ownerOfFlatCorpus(directory){let files=[];try{files=readdirSync(directory).filter((file)=>file.endsWith(".sql")).sort()}catch{return null}for(const file of files){let sql;try{sql=readFileSync(join(directory,file),"utf8")}catch{continue}const create=/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?(["`])/i.exec(sql);if(create)return create[1]==="`"?"mysql":"postgres"}return null}function wireOf(dialect){return dialect==="mysql"||dialect==="mariadb"||dialect==="singlestore"||dialect==="vitess"||dialect==="planetscale"?"mysql":"postgres"}export function relativeMigrationDirectory(directory,cwd=process.cwd()){const prefix=`${cwd}/`;return directory.startsWith(prefix)?directory.slice(prefix.length):directory}export function resolveSnapshotDirectory(cwd=process.cwd()){const configured=process.env.DB_SNAPSHOT_PATH||DEFAULT_SNAPSHOT_DIR;return isAbsolute(configured)?configured:join(cwd,configured)}export function snapshotDirForQueryBuilder(cwd=process.cwd()){return relative(cwd,resolveSnapshotDirectory(cwd))||"."}export function snapshotDirectoryIsShared(){return Boolean(process.env.DB_SNAPSHOT_PATH)}
@@ -132,7 +132,7 @@ export declare function withoutProtectedTableDropSql(statements: string[], prote
132
132
  */
133
133
  export declare function orderPostgresColumnTypeChanges(sql: string): string;
134
134
  export declare function guardPostgresEnumTypes(sql: string): string;
135
- export declare function preprocessSqliteMigrations(): Array<{ original: string, hidden: string, feature: string }>;
135
+ export declare function preprocessSqliteMigrations(options?: PreprocessSqliteOptions): Array<{ original: string, hidden: string, feature: string }>;
136
136
  /**
137
137
  * Public bootstrap entry point.
138
138
  *
@@ -499,6 +499,10 @@ export declare function generateMigrations2(): Promise<Result<string, Error>>;
499
499
  * not glue it onto the first statement.
500
500
  */
501
501
  export declare const GENERATED_MIGRATION_MARKER: unknown;
502
+ export declare interface PreprocessSqliteOptions {
503
+ schema?: import('bun:sqlite').Database | null
504
+ recordMigrations?: (record: string[], replay: string[]) => void
505
+ }
502
506
  /*` definitions to the stored snapshot
503
507
  * (`.qb/model-snapshot.<dialect>.json`) via bun-query-builder, then — if
504
508
  * there are changes — writes the resulting ALTER/CREATE/DROP statements
@@ -1,10 +1,10 @@
1
- var {require}=import.meta;import{existsSync,mkdirSync,readdirSync,readFileSync,renameSync,unlinkSync,writeFileSync}from"node:fs";import{isPackageMigration,stagePackageMigrations}from"./package-migrations";import{basename,dirname,isAbsolute,join,resolve}from"node:path";import{log as _log}from"@stacksjs/logging";const log={info:(...args)=>typeof _log?.info==="function"?_log.info(...args):console.log(...args),success:(msg)=>typeof _log?.success==="function"?_log.success(msg):console.log(msg),warn:(msg)=>typeof _log?.warn==="function"?_log.warn(msg):console.warn(msg),error:(...args)=>typeof _log?.error==="function"?_log.error(...args):console.error(...args),debug:(...args)=>typeof _log?.debug==="function"?_log.debug(...args):console.debug(...args)};import{err,handleError,ok}from"@stacksjs/error-handling";import{path}from"@stacksjs/path";import{defaultModelsPath}from"./seeder";import{createQueryBuilder,executeMigration as qbExecuteMigration,generateDiffOperations,generateMigration as qbGenerateMigration,resetDatabase as qbResetDatabase,config as qbConfig,saveMigrationSnapshot,setConfig}from"@stacksjs/query-builder";import{db,qbSnapshotDir,resetDatabaseConnection}from"./utils";import{classifyConnectionError,createDatabase,describeTarget,manualCreateHint,probeTargetDatabase,resolveConnectionTarget}from"./ensure-database";import{declaredModelTables,resolveModelSources}from"./model-sources";import{applicableFrameworkRenames,applyFrameworkRenamesToDatabase,applyFrameworkRenamesToPlan}from"./framework-renames";import{findShadowedColumnDrops,shadowDropsAllowed,shadowedDropMessage}from"./shadowed-models";import{frameworkManagedColumns,withoutManagedColumnDrops,withoutManagedColumnDropSql}from"./managed-columns";import{acquireMigrationLock}from"./migration-lock";import{relativeMigrationDirectory,resolveMigrationDirectory}from"./migration-path";import{ensureNotificationForeignKeys,migrateNotificationTables,notificationTablesMissingCreateStatements}from"./notification-tables";import{traitTableNames}from"./trait-tables";import{AUTH_TABLES}from"./auth-tables";import{sqlStatementsOf}from"./sql-statements";import{corpusUniqueIndexes,hasIndex,restoreDroppedUniqueIndexes,uniqueIndexIsRestorable}from"./declared-unique-indexes";import{ensureUuidColumns}from"./uuid-columns";import{sqlHelpers}from"./sql-helpers";export{sqlStatementsOf}from"./sql-statements";import{env as envVars}from"@stacksjs/env";import{getConnectionDefaults}from"./defaults";import{isVitessSharded,toSqlIntrospectionDialect}from"./dialect";const databaseEnv={DB_CONNECTION:process.env.DB_CONNECTION||envVars.DB_CONNECTION,DB_DATABASE_PATH:process.env.DB_DATABASE_PATH||envVars.DB_DATABASE_PATH,DB_DATABASE:process.env.DB_DATABASE||envVars.DB_DATABASE,DB_HOST:process.env.DB_HOST||envVars.DB_HOST,DB_PORT:process.env.DB_PORT?Number(process.env.DB_PORT):envVars.DB_PORT,DB_USERNAME:process.env.DB_USERNAME||envVars.DB_USERNAME,DB_PASSWORD:process.env.DB_PASSWORD||envVars.DB_PASSWORD,DB_VITESS_SHARDED:process.env.DB_VITESS_SHARDED||envVars.DB_VITESS_SHARDED},dbDriver=databaseEnv.DB_CONNECTION||"sqlite",sqliteDefaults=getConnectionDefaults("sqlite",databaseEnv),mysqlDefaults=getConnectionDefaults("mysql",databaseEnv),singlestoreDefaults=getConnectionDefaults("singlestore",databaseEnv),vitessDefaults=getConnectionDefaults("vitess",databaseEnv),postgresDefaults=getConnectionDefaults("postgres",databaseEnv),dbConfig={default:dbDriver,connections:{sqlite:{database:sqliteDefaults.database,prefix:""},mysql:{name:mysqlDefaults.database,host:mysqlDefaults.host,username:mysqlDefaults.username,password:mysqlDefaults.password,port:mysqlDefaults.port,prefix:""},singlestore:{name:singlestoreDefaults.database,host:singlestoreDefaults.host,username:singlestoreDefaults.username,password:singlestoreDefaults.password,port:singlestoreDefaults.port,prefix:""},vitess:{name:vitessDefaults.database,host:vitessDefaults.host,username:vitessDefaults.username,password:vitessDefaults.password,port:vitessDefaults.port,prefix:"",sharded:isVitessSharded(databaseEnv.DB_VITESS_SHARDED)},postgres:{name:postgresDefaults.database,host:postgresDefaults.host,username:postgresDefaults.username,password:postgresDefaults.password,port:postgresDefaults.port,prefix:""}}};function sqliteDatabasePath(){const configured=dbConfig.connections.sqlite.database||"stacks.db";return isAbsolute(configured)?configured:join(process.cwd(),configured)}function getDriver(){return dbConfig.default||"sqlite"}function getDialect(){const driver=getDriver();if(driver==="sqlite"||driver==="mysql"||driver==="vitess"||driver==="postgres")return driver;if(driver==="singlestore")return"mysql";if(driver==="dynamodb")throw Error("[database] DB_CONNECTION=dynamodb is not compatible with the SQL migration runner. DynamoDB has no schema-migration concept - use the entity-style `dynamo.entity(...)` API from @stacksjs/database directly. To run SQL migrations, set DB_CONNECTION to one of: sqlite, mysql, singlestore, vitess, postgres.");throw Error(`[database] Unknown DB_CONNECTION "${driver}". Allowed values: sqlite, mysql, singlestore, vitess, postgres, dynamodb.`)}function getQbDialect(){return getDriver()==="singlestore"?"singlestore":getDialect()}function migrationDirectory(dialect=getQbDialect()){return resolveMigrationDirectory(dialect,{configured:qbConfig.migrationDir,snapshotDir:qbSnapshotDir()})}function configureQueryBuilder(targetDialect=getQbDialect(),vitessSharded){const connectionConfig=dbConfig.connections[targetDialect];setConfig({dialect:targetDialect,vitess:{sharded:isVitessSharded(vitessSharded??connectionConfig?.sharded)},verbose:!1,snapshotDir:qbSnapshotDir(),migrationDir:relativeMigrationDirectory(migrationDirectory(targetDialect)),database:{database:connectionConfig?.name||connectionConfig?.database||"stacks",host:connectionConfig?.host||"localhost",port:connectionConfig?.port||(targetDialect==="postgres"?5432:targetDialect==="vitess"?15306:targetDialect==="mysql"||targetDialect==="singlestore"?3306:0),username:connectionConfig?.username||"",password:connectionConfig?.password||""}});resetDatabaseConnection()}export function prepareMigrationModelsDir(){const sources=resolveModelSources(),excludedTables=sources?.excludedTables??[];return{modelsDir:sources?.dir??path.userModelsPath(),skip:!sources,excludedTables,protectedTables:[...new Set([...excludedTables,...traitTableNames()])]}}function generatedStatementTables(statement){return statement.split(";").map((part)=>part.trim()).filter(Boolean).flatMap((part)=>{const table=rebuiltTable(part)??statementTable(part),index=/^\s*CREATE\s+(?:UNIQUE\s+)?INDEX\s+(?:IF\s+NOT\s+EXISTS\s+)?["`]?\w+["`]?\s+ON\s+["`]?(\w+)["`]?/i.exec(part)?.[1],dropped=DROP_TABLE_RE.exec(part)?.[1];return[table,index,dropped].filter((name)=>Boolean(name)).map((name)=>name.toLowerCase())})}export function withoutRuntimeOwnedTableSql(statements,owned=AUTH_TABLES){const ownedSet=new Set(owned.map((table)=>table.toLowerCase())),removed=[];return{statements:statements.filter((statement)=>{if(!generatedStatementTables(statement).some((table)=>ownedSet.has(table)))return!0;removed.push(statement);return!1}),removed}}const DROP_TABLE_RE=/^\s*DROP\s+TABLE\s+(?:IF\s+EXISTS\s+)?["'`]?(\w+)["'`]?/i;export function withoutProtectedTableDropSql(statements,protectedTables,operations){if(protectedTables.length===0)return{statements,removed:[]};const excluded=new Set(protectedTables.map((table)=>table.toLowerCase())),normalize=(sql)=>sql.replace(/\s+/g," ").trim().replace(/;$/,""),dropped=new Set(operations.filter((op)=>op.kind==="drop_table"&&excluded.has(op.table.toLowerCase())).filter((op)=>Boolean(op.sql)).map((op)=>normalize(op.sql))),removed=[];return{statements:statements.filter((statement)=>{if(dropped.has(normalize(statement))){removed.push(statement);return!1}const match=statement.match(DROP_TABLE_RE);if(match?.[1]&&excluded.has(match[1].toLowerCase())){removed.push(statement);return!1}return!0}),removed}}export function orderPostgresColumnTypeChanges(sql){const lines=sql.split(`
1
+ var {require}=import.meta;import{existsSync,mkdirSync,readdirSync,readFileSync,renameSync,unlinkSync,writeFileSync}from"node:fs";import{isPackageMigration,stagePackageMigrations}from"./package-migrations";import{basename,dirname,isAbsolute,join,resolve}from"node:path";import{log as _log}from"@stacksjs/logging";const log={info:(...args)=>typeof _log?.info==="function"?_log.info(...args):console.log(...args),success:(msg)=>typeof _log?.success==="function"?_log.success(msg):console.log(msg),warn:(msg)=>typeof _log?.warn==="function"?_log.warn(msg):console.warn(msg),error:(...args)=>typeof _log?.error==="function"?_log.error(...args):console.error(...args),debug:(...args)=>typeof _log?.debug==="function"?_log.debug(...args):console.debug(...args)};import{err,handleError,ok}from"@stacksjs/error-handling";import{path}from"@stacksjs/path";import{defaultModelsPath}from"./seeder";import{createQueryBuilder,executeMigration as qbExecuteMigration,generateDiffOperations,generateMigration as qbGenerateMigration,resetDatabase as qbResetDatabase,config as qbConfig,saveMigrationSnapshot,setConfig}from"@stacksjs/query-builder";import{db,qbSnapshotDir,resetDatabaseConnection}from"./utils";import{classifyConnectionError,createDatabase,describeTarget,manualCreateHint,probeTargetDatabase,resolveConnectionTarget}from"./ensure-database";import{declaredModelTables,resolveModelSources}from"./model-sources";import{applicableFrameworkRenames,applyFrameworkRenamesToDatabase,applyFrameworkRenamesToPlan}from"./framework-renames";import{findShadowedColumnDrops,shadowDropsAllowed,shadowedDropMessage}from"./shadowed-models";import{frameworkManagedColumns,withoutManagedColumnDrops,withoutManagedColumnDropSql}from"./managed-columns";import{acquireLibsqlMigrationLock,acquireMigrationLock}from"./migration-lock";import{relativeMigrationDirectory,resolveMigrationDirectory}from"./migration-path";import{ensureNotificationForeignKeys,migrateNotificationTables,notificationTablesMissingCreateStatements}from"./notification-tables";import{traitTableNames}from"./trait-tables";import{AUTH_TABLES}from"./auth-tables";import{sqlStatementsOf}from"./sql-statements";import{corpusUniqueIndexes,hasIndex,restorableMissingUniqueIndexes,restoreDroppedUniqueIndexes,uniqueIndexIsRestorable}from"./declared-unique-indexes";import{ensureUuidColumns}from"./uuid-columns";import{sqlHelpers}from"./sql-helpers";export{sqlStatementsOf}from"./sql-statements";import{env as envVars}from"@stacksjs/env";import{getConnectionDefaults}from"./defaults";import{isLibsqlDriver,isVitessSharded,normalizeDatabaseDriver,toSqlIntrospectionDialect}from"./dialect";const databaseEnv={DB_CONNECTION:process.env.DB_CONNECTION||envVars.DB_CONNECTION,DB_DATABASE_PATH:process.env.DB_DATABASE_PATH||envVars.DB_DATABASE_PATH,DB_DATABASE:process.env.DB_DATABASE||envVars.DB_DATABASE,DB_HOST:process.env.DB_HOST||envVars.DB_HOST,DB_PORT:process.env.DB_PORT?Number(process.env.DB_PORT):envVars.DB_PORT,DB_USERNAME:process.env.DB_USERNAME||envVars.DB_USERNAME,DB_PASSWORD:process.env.DB_PASSWORD||envVars.DB_PASSWORD,DB_VITESS_SHARDED:process.env.DB_VITESS_SHARDED||envVars.DB_VITESS_SHARDED,TURSO_DATABASE_URL:process.env.TURSO_DATABASE_URL||envVars.TURSO_DATABASE_URL,TURSO_AUTH_TOKEN:process.env.TURSO_AUTH_TOKEN||envVars.TURSO_AUTH_TOKEN},dbDriver=normalizeDatabaseDriver(databaseEnv.DB_CONNECTION||"sqlite"),sqliteDefaults=getConnectionDefaults("sqlite",databaseEnv),mysqlDefaults=getConnectionDefaults("mysql",databaseEnv),singlestoreDefaults=getConnectionDefaults("singlestore",databaseEnv),vitessDefaults=getConnectionDefaults("vitess",databaseEnv),postgresDefaults=getConnectionDefaults("postgres",databaseEnv),dbConfig={default:dbDriver,connections:{sqlite:{database:sqliteDefaults.database,prefix:""},mysql:{name:mysqlDefaults.database,host:mysqlDefaults.host,username:mysqlDefaults.username,password:mysqlDefaults.password,port:mysqlDefaults.port,prefix:""},singlestore:{name:singlestoreDefaults.database,host:singlestoreDefaults.host,username:singlestoreDefaults.username,password:singlestoreDefaults.password,port:singlestoreDefaults.port,prefix:""},vitess:{name:vitessDefaults.database,host:vitessDefaults.host,username:vitessDefaults.username,password:vitessDefaults.password,port:vitessDefaults.port,prefix:"",sharded:isVitessSharded(databaseEnv.DB_VITESS_SHARDED)},postgres:{name:postgresDefaults.database,host:postgresDefaults.host,username:postgresDefaults.username,password:postgresDefaults.password,port:postgresDefaults.port,prefix:""},turso:{url:databaseEnv.TURSO_DATABASE_URL||"",authToken:databaseEnv.TURSO_AUTH_TOKEN||""}}};function usesLibsql(){return isLibsqlDriver(getDriver())}function sqliteDatabasePath(){const configured=dbConfig.connections.sqlite.database||"stacks.db";return isAbsolute(configured)?configured:join(process.cwd(),configured)}function getDriver(){return dbConfig.default||"sqlite"}function getDialect(){const driver=getDriver();if(driver==="sqlite"||driver==="mysql"||driver==="vitess"||driver==="postgres")return driver;if(driver==="turso")return"sqlite";if(driver==="singlestore")return"mysql";if(driver==="dynamodb")throw Error("[database] DB_CONNECTION=dynamodb is not compatible with the SQL migration runner. DynamoDB has no schema-migration concept - use the entity-style `dynamo.entity(...)` API from @stacksjs/database directly. To run SQL migrations, set DB_CONNECTION to one of: sqlite, turso, mysql, singlestore, vitess, postgres.");throw Error(`[database] Unknown DB_CONNECTION "${driver}". Allowed values: sqlite, turso, mysql, singlestore, vitess, postgres, dynamodb.`)}function getQbDialect(){return getDriver()==="singlestore"?"singlestore":getDialect()}function migrationDirectory(dialect=getQbDialect()){return resolveMigrationDirectory(dialect,{configured:qbConfig.migrationDir,snapshotDir:qbSnapshotDir()})}function configureQueryBuilder(targetDialect=getQbDialect(),vitessSharded){const connectionConfig=dbConfig.connections[targetDialect],libsql=targetDialect==="sqlite"&&usesLibsql()?{url:dbConfig.connections.turso.url||void 0,authToken:dbConfig.connections.turso.authToken||void 0}:{url:void 0,authToken:void 0};if(targetDialect==="sqlite"&&usesLibsql()&&!libsql.url)throw Error("[database] DB_CONNECTION=turso needs TURSO_DATABASE_URL (libsql://<db>-<org>.turso.io, or http://127.0.0.1:8080 for `turso dev`).");setConfig({dialect:targetDialect,vitess:{sharded:isVitessSharded(vitessSharded??connectionConfig?.sharded)},verbose:!1,snapshotDir:qbSnapshotDir(),migrationDir:relativeMigrationDirectory(migrationDirectory(targetDialect)),database:{database:connectionConfig?.name||connectionConfig?.database||"stacks",host:connectionConfig?.host||"localhost",port:connectionConfig?.port||(targetDialect==="postgres"?5432:targetDialect==="vitess"?15306:targetDialect==="mysql"||targetDialect==="singlestore"?3306:0),username:connectionConfig?.username||"",password:connectionConfig?.password||"",...libsql}});resetDatabaseConnection()}export function prepareMigrationModelsDir(){const sources=resolveModelSources(),excludedTables=sources?.excludedTables??[];return{modelsDir:sources?.dir??path.userModelsPath(),skip:!sources,excludedTables,protectedTables:[...new Set([...excludedTables,...traitTableNames()])]}}function generatedStatementTables(statement){return statement.split(";").map((part)=>part.trim()).filter(Boolean).flatMap((part)=>{const table=rebuiltTable(part)??statementTable(part),index=/^\s*CREATE\s+(?:UNIQUE\s+)?INDEX\s+(?:IF\s+NOT\s+EXISTS\s+)?["`]?\w+["`]?\s+ON\s+["`]?(\w+)["`]?/i.exec(part)?.[1],dropped=DROP_TABLE_RE.exec(part)?.[1];return[table,index,dropped].filter((name)=>Boolean(name)).map((name)=>name.toLowerCase())})}export function withoutRuntimeOwnedTableSql(statements,owned=AUTH_TABLES){const ownedSet=new Set(owned.map((table)=>table.toLowerCase())),removed=[];return{statements:statements.filter((statement)=>{if(!generatedStatementTables(statement).some((table)=>ownedSet.has(table)))return!0;removed.push(statement);return!1}),removed}}const DROP_TABLE_RE=/^\s*DROP\s+TABLE\s+(?:IF\s+EXISTS\s+)?["'`]?(\w+)["'`]?/i;export function withoutProtectedTableDropSql(statements,protectedTables,operations){if(protectedTables.length===0)return{statements,removed:[]};const excluded=new Set(protectedTables.map((table)=>table.toLowerCase())),normalize=(sql)=>sql.replace(/\s+/g," ").trim().replace(/;$/,""),dropped=new Set(operations.filter((op)=>op.kind==="drop_table"&&excluded.has(op.table.toLowerCase())).filter((op)=>Boolean(op.sql)).map((op)=>normalize(op.sql))),removed=[];return{statements:statements.filter((statement)=>{if(dropped.has(normalize(statement))){removed.push(statement);return!1}const match=statement.match(DROP_TABLE_RE);if(match?.[1]&&excluded.has(match[1].toLowerCase())){removed.push(statement);return!1}return!0}),removed}}export function orderPostgresColumnTypeChanges(sql){const lines=sql.split(`
2
2
  `),output=[];for(const line of lines){const match=/^(\s*)ALTER\s+TABLE\s+("?[\w.]+"?)\s+ALTER\s+COLUMN\s+("?[\w]+"?)\s+TYPE\s/i.exec(line);if(match){const[,indent,table,column]=match,drop=`${indent}ALTER TABLE ${table} ALTER COLUMN ${column} DROP DEFAULT;`;if((output.length>0?output[output.length-1].trim():"")!==drop.trim())output.push(drop)}output.push(line)}return output.join(`
3
3
  `)}export function guardPostgresEnumTypes(sql){return assertPostgresEnumMembers(wrapPostgresEnumTypes(sql))}function wrapPostgresEnumTypes(sql){return sql.replace(/CREATE\s+TYPE\s+("?[\w.]+"?)\s+AS\s+ENUM\s*\(([^)]*)\)/gi,(match,name,members,offset,whole)=>{if(/\bBEGIN\s*$/i.test(whole.slice(Math.max(0,offset-40),offset)))return match;return`DO $stacks$ BEGIN CREATE TYPE ${name} AS ENUM (${members}); EXCEPTION WHEN duplicate_object THEN null; END $stacks$`})}function assertPostgresEnumMembers(sql){return sql.replace(/DO \$stacks\$[\s\S]*?END \$stacks\$;?/g,(block)=>{const created=/CREATE\s+TYPE\s+("?[\w.]+"?)\s+AS\s+ENUM\s*\(([^)]*)\)/i.exec(block);if(!created)return block;const[,name,members]=created,missing=enumMembers(members).map((member)=>`ALTER TYPE ${name} ADD VALUE IF NOT EXISTS ${member};`).filter((statement)=>!sql.includes(statement));if(missing.length===0)return block;return`${block.endsWith(";")?block:`${block};`}
4
4
  ${missing.join(`
5
- `)}`})}function enumMembers(list){return[...list.matchAll(/'(?:[^']|'')*'/g)].map((match)=>match[0])}export function preprocessSqliteMigrations(){const migrationsDir=migrationDirectory("sqlite"),quarantined=[];try{for(const stale of readdirSync(migrationsDir).filter((f)=>f.endsWith(".sql.unsupported"))){const original=join(migrationsDir,stale.slice(0,-12));try{if(!existsSync(original))renameSync(join(migrationsDir,stale),original);else unlinkSync(join(migrationsDir,stale))}catch{}}}catch{}let files;try{files=readdirSync(migrationsDir).filter((f)=>f.endsWith(".sql"))}catch{return quarantined}const droppedMigrations=[],skipMigration=(file,reason)=>{log.info(`Skipping migration on SQLite (${reason}): ${file}`);droppedMigrations.push(file)},partitionUnsupported=(file,filePath,statements,pattern,feature)=>{const unsupported=statements.filter((s)=>pattern.test(s));if(unsupported.length===statements.length){skipMigration(file,`SQLite does not support ${feature}`);return"skipped"}const hiddenPath=`${filePath}.unsupported`;try{renameSync(filePath,hiddenPath)}catch(error){log.error(`[migration] Could not quarantine ${file}: ${error instanceof Error?error.message:String(error)}`);return"runnable"}quarantined.push({original:filePath,hidden:hiddenPath,feature});log.warn(`[migration] ${file} mixes ${unsupported.length} ${feature} statement(s) SQLite cannot run with ${statements.length-unsupported.length} it can, so none of it was applied. Split them into separate migrations, or run this app on MySQL/Postgres. The rest of the corpus was applied.`);return"quarantined"},retireMigration=(file,reason)=>{log.info(`Not running no-op migration (${reason}): ${file}`);droppedMigrations.push(file)},replayMigrations=[],addConstraintPattern=/^\s*ALTER\s+TABLE\s+.+\s+ADD\s+CONSTRAINT\s+/i,createTypePattern=/^\s*CREATE\s+TYPE\s+/i,uniqueIndexFiles=corpusUniqueIndexes(files.map((file)=>({file,content:readFileSync(join(migrationsDir,file),"utf-8")}))),dropColumnPattern=/^\s*ALTER\s+TABLE\s+["']?(\w+)["']?\s+DROP\s+COLUMN\s+["']?(\w+)["']?\s*$/i,addColumnPattern=/^\s*ALTER\s+TABLE\s+["']?(\w+)["']?\s+ADD\s+COLUMN\s+["'`]?(\w+)["'`]?/i,createTablePattern=/^\s*CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["']?(\w+)["']?/i,createTableEarliest=new Map;for(const file of files){const m=file.match(/^\d+-create-(\w+)-table\.sql$/);if(!m||!m[1])continue;const tableName=m[1],existing=createTableEarliest.get(tableName);if(!existing||file<existing)createTableEarliest.set(tableName,file)}const earlierCreateDefinesColumn=(migrationFile,table,column)=>{const createFile=createTableEarliest.get(table);if(!createFile||createFile>=migrationFile)return!1;try{const createContent=readFileSync(join(migrationsDir,createFile),"utf8"),createStatement=sqlStatementsOf(createContent).find((statement)=>statement.match(createTablePattern)?.[1]===table);if(!createStatement)return!1;const escapedColumn=column.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");return new RegExp(`(?:^|[,(])\\s*["'\`]?${escapedColumn}["'\`]?\\s+`,"i").test(createStatement)}catch{return!1}},sqliteDbPath=sqliteDatabasePath();let sqliteDb=null;if(existsSync(sqliteDbPath))try{const{Database}=require("bun:sqlite");sqliteDb=new Database(sqliteDbPath,{readonly:!0})}catch{}const migrationWasRecorded=(file)=>{if(!sqliteDb)return!1;try{return Boolean(sqliteDb.prepare("SELECT 1 FROM migrations WHERE migration = ? LIMIT 1").get(file))}catch{return!1}};for(const file of files){log.debug(`[migration] Running: ${file}`);const filePath=join(migrationsDir,file),content=readFileSync(filePath,"utf-8"),statements=sqlStatementsOf(content);if(statements.length===0)continue;const declaredUniques=uniqueIndexFiles.get(file);if(sqliteDb&&declaredUniques){const liveDb=sqliteDb;if(declaredUniques.filter((index)=>!hasIndex(liveDb,index.name)&&uniqueIndexIsRestorable(liveDb,index)).length>0){log.info(`Re-queueing unique-index migration (index missing from database): ${file}`);replayMigrations.push(file)}continue}if(migrationWasRecorded(file))continue;const firstStatement=statements[0],createTableMatch=firstStatement?firstStatement.match(createTablePattern):null;if(createTableMatch&&createTableMatch[1]){const tableName=createTableMatch[1],earliest=createTableEarliest.get(tableName);if(earliest&&earliest!==file&&!isPackageMigration(file)){retireMigration(file,`duplicate create-table for "${tableName}" (kept ${earliest})`);continue}}if(statements.some((s)=>addConstraintPattern.test(s))){if(partitionUnsupported(file,filePath,statements,addConstraintPattern,"ALTER TABLE ADD CONSTRAINT")!=="runnable")continue}if(statements.some((s)=>createTypePattern.test(s))){if(partitionUnsupported(file,filePath,statements,createTypePattern,"CREATE TYPE (enum types)")!=="runnable")continue}const addColumnTargets=statements.map((s)=>s.match(addColumnPattern)).filter((m)=>Boolean(m?.[1]&&m[2])).map((m)=>({table:m[1],column:m[2]}));if(addColumnTargets.length>0&&addColumnTargets.length===statements.length){const satisfied=addColumnTargets.filter(({table,column})=>{try{if(sqliteDb){const safeTableName=table.replace(/[^a-zA-Z0-9_]/g,""),columns=sqliteDb.prepare(`PRAGMA table_info("${safeTableName}")`).all();if(columns.some((col)=>col.name===column))return!0;if(columns.length>0)return!1}return earlierCreateDefinesColumn(file,table,column)}catch{return!1}});if(satisfied.length===addColumnTargets.length){skipMigration(file,"every column it adds already exists or is defined by an earlier create-table migration");continue}if(satisfied.length>0)log.warn(`[migration] ${file} is partially applied: ${satisfied.map((p)=>`${p.table}.${p.column}`).join(", ")} already exist${satisfied.length===1?"s":""} or will be created earlier, the rest do not. SQLite cannot skip a single ADD COLUMN, so this file will fail. Add the remaining columns by hand, or split the file so the applied statements sit in their own migration.`)}if(statements.some((s)=>dropColumnPattern.test(s))){let modified=!1;const filteredStatements=[];for(const stmt of statements){const dropColMatch=stmt.match(dropColumnPattern);if(dropColMatch&&dropColMatch[1]&&dropColMatch[2]){const tableName=dropColMatch[1],columnName=dropColMatch[2];if(!sqliteDb){if(earlierCreateDefinesColumn(file,tableName,columnName)){filteredStatements.push(stmt);continue}log.info(`Skipping DROP COLUMN "${columnName}" - no database exists yet: ${file}`);modified=!0;continue}try{const safeTableName=tableName.replace(/[^a-zA-Z0-9_]/g,""),columns=sqliteDb.prepare(`PRAGMA table_info("${safeTableName}")`).all();if(columns.length===0){if(earlierCreateDefinesColumn(file,tableName,columnName)){filteredStatements.push(stmt);continue}log.info(`Skipping DROP COLUMN "${columnName}" - table "${tableName}" does not exist yet: ${file}`);modified=!0;continue}if(!columns.some((col)=>col.name===columnName)){log.info(`Skipping DROP COLUMN "${columnName}" from "${tableName}" - column does not exist: ${file}`);modified=!0;continue}}catch{if(earlierCreateDefinesColumn(file,tableName,columnName)){filteredStatements.push(stmt);continue}log.info(`Skipping DROP COLUMN "${columnName}" - table "${tableName}" not found: ${file}`);modified=!0;continue}}filteredStatements.push(stmt)}if(modified){if(filteredStatements.length===0)retireMigration(file,"columns already absent from table");else writeFileSync(filePath,`${filteredStatements.join(`;
5
+ `)}`})}function enumMembers(list){return[...list.matchAll(/'(?:[^']|'')*'/g)].map((match)=>match[0])}export function preprocessSqliteMigrations(options={}){const migrationsDir=migrationDirectory("sqlite"),quarantined=[];try{for(const stale of readdirSync(migrationsDir).filter((f)=>f.endsWith(".sql.unsupported"))){const original=join(migrationsDir,stale.slice(0,-12));try{if(!existsSync(original))renameSync(join(migrationsDir,stale),original);else unlinkSync(join(migrationsDir,stale))}catch{}}}catch{}let files;try{files=readdirSync(migrationsDir).filter((f)=>f.endsWith(".sql"))}catch{return quarantined}const droppedMigrations=[],skipMigration=(file,reason)=>{log.info(`Skipping migration on SQLite (${reason}): ${file}`);droppedMigrations.push(file)},partitionUnsupported=(file,filePath,statements,pattern,feature)=>{const unsupported=statements.filter((s)=>pattern.test(s));if(unsupported.length===statements.length){skipMigration(file,`SQLite does not support ${feature}`);return"skipped"}const hiddenPath=`${filePath}.unsupported`;try{renameSync(filePath,hiddenPath)}catch(error){log.error(`[migration] Could not quarantine ${file}: ${error instanceof Error?error.message:String(error)}`);return"runnable"}quarantined.push({original:filePath,hidden:hiddenPath,feature});log.warn(`[migration] ${file} mixes ${unsupported.length} ${feature} statement(s) SQLite cannot run with ${statements.length-unsupported.length} it can, so none of it was applied. Split them into separate migrations, or run this app on MySQL/Postgres. The rest of the corpus was applied.`);return"quarantined"},retireMigration=(file,reason)=>{log.info(`Not running no-op migration (${reason}): ${file}`);droppedMigrations.push(file)},replayMigrations=[],addConstraintPattern=/^\s*ALTER\s+TABLE\s+.+\s+ADD\s+CONSTRAINT\s+/i,createTypePattern=/^\s*CREATE\s+TYPE\s+/i,uniqueIndexFiles=corpusUniqueIndexes(files.map((file)=>({file,content:readFileSync(join(migrationsDir,file),"utf-8")}))),dropColumnPattern=/^\s*ALTER\s+TABLE\s+["']?(\w+)["']?\s+DROP\s+COLUMN\s+["']?(\w+)["']?\s*$/i,addColumnPattern=/^\s*ALTER\s+TABLE\s+["']?(\w+)["']?\s+ADD\s+COLUMN\s+["'`]?(\w+)["'`]?/i,createTablePattern=/^\s*CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["']?(\w+)["']?/i,createTableEarliest=new Map;for(const file of files){const m=file.match(/^\d+-create-(\w+)-table\.sql$/);if(!m||!m[1])continue;const tableName=m[1],existing=createTableEarliest.get(tableName);if(!existing||file<existing)createTableEarliest.set(tableName,file)}const earlierCreateDefinesColumn=(migrationFile,table,column)=>{const createFile=createTableEarliest.get(table);if(!createFile||createFile>=migrationFile)return!1;try{const createContent=readFileSync(join(migrationsDir,createFile),"utf8"),createStatement=sqlStatementsOf(createContent).find((statement)=>statement.match(createTablePattern)?.[1]===table);if(!createStatement)return!1;const escapedColumn=column.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");return new RegExp(`(?:^|[,(])\\s*["'\`]?${escapedColumn}["'\`]?\\s+`,"i").test(createStatement)}catch{return!1}},sqliteDbPath=sqliteDatabasePath(),borrowedSchema=options.schema!==void 0;let sqliteDb=borrowedSchema?options.schema??null:null;if(!borrowedSchema&&existsSync(sqliteDbPath))try{const{Database}=require("bun:sqlite");sqliteDb=new Database(sqliteDbPath,{readonly:!0})}catch{}const migrationWasRecorded=(file)=>{if(!sqliteDb)return!1;try{return Boolean(sqliteDb.prepare("SELECT 1 FROM migrations WHERE migration = ? LIMIT 1").get(file))}catch{return!1}};for(const file of files){log.debug(`[migration] Running: ${file}`);const filePath=join(migrationsDir,file),content=readFileSync(filePath,"utf-8"),statements=sqlStatementsOf(content);if(statements.length===0)continue;const declaredUniques=uniqueIndexFiles.get(file);if(sqliteDb&&declaredUniques){const liveDb=sqliteDb;if(declaredUniques.filter((index)=>!hasIndex(liveDb,index.name)&&uniqueIndexIsRestorable(liveDb,index)).length>0){log.info(`Re-queueing unique-index migration (index missing from database): ${file}`);replayMigrations.push(file)}continue}if(migrationWasRecorded(file))continue;const firstStatement=statements[0],createTableMatch=firstStatement?firstStatement.match(createTablePattern):null;if(createTableMatch&&createTableMatch[1]){const tableName=createTableMatch[1],earliest=createTableEarliest.get(tableName);if(earliest&&earliest!==file&&!isPackageMigration(file)){retireMigration(file,`duplicate create-table for "${tableName}" (kept ${earliest})`);continue}}if(statements.some((s)=>addConstraintPattern.test(s))){if(partitionUnsupported(file,filePath,statements,addConstraintPattern,"ALTER TABLE ADD CONSTRAINT")!=="runnable")continue}if(statements.some((s)=>createTypePattern.test(s))){if(partitionUnsupported(file,filePath,statements,createTypePattern,"CREATE TYPE (enum types)")!=="runnable")continue}const addColumnTargets=statements.map((s)=>s.match(addColumnPattern)).filter((m)=>Boolean(m?.[1]&&m[2])).map((m)=>({table:m[1],column:m[2]}));if(addColumnTargets.length>0&&addColumnTargets.length===statements.length){const satisfied=addColumnTargets.filter(({table,column})=>{try{if(sqliteDb){const safeTableName=table.replace(/[^a-zA-Z0-9_]/g,""),columns=sqliteDb.prepare(`PRAGMA table_info("${safeTableName}")`).all();if(columns.some((col)=>col.name===column))return!0;if(columns.length>0)return!1}return earlierCreateDefinesColumn(file,table,column)}catch{return!1}});if(satisfied.length===addColumnTargets.length){skipMigration(file,"every column it adds already exists or is defined by an earlier create-table migration");continue}if(satisfied.length>0)log.warn(`[migration] ${file} is partially applied: ${satisfied.map((p)=>`${p.table}.${p.column}`).join(", ")} already exist${satisfied.length===1?"s":""} or will be created earlier, the rest do not. SQLite cannot skip a single ADD COLUMN, so this file will fail. Add the remaining columns by hand, or split the file so the applied statements sit in their own migration.`)}if(statements.some((s)=>dropColumnPattern.test(s))){let modified=!1;const filteredStatements=[];for(const stmt of statements){const dropColMatch=stmt.match(dropColumnPattern);if(dropColMatch&&dropColMatch[1]&&dropColMatch[2]){const tableName=dropColMatch[1],columnName=dropColMatch[2];if(!sqliteDb){if(earlierCreateDefinesColumn(file,tableName,columnName)){filteredStatements.push(stmt);continue}log.info(`Skipping DROP COLUMN "${columnName}" - no database exists yet: ${file}`);modified=!0;continue}try{const safeTableName=tableName.replace(/[^a-zA-Z0-9_]/g,""),columns=sqliteDb.prepare(`PRAGMA table_info("${safeTableName}")`).all();if(columns.length===0){if(earlierCreateDefinesColumn(file,tableName,columnName)){filteredStatements.push(stmt);continue}log.info(`Skipping DROP COLUMN "${columnName}" - table "${tableName}" does not exist yet: ${file}`);modified=!0;continue}if(!columns.some((col)=>col.name===columnName)){log.info(`Skipping DROP COLUMN "${columnName}" from "${tableName}" - column does not exist: ${file}`);modified=!0;continue}}catch{if(earlierCreateDefinesColumn(file,tableName,columnName)){filteredStatements.push(stmt);continue}log.info(`Skipping DROP COLUMN "${columnName}" - table "${tableName}" not found: ${file}`);modified=!0;continue}}filteredStatements.push(stmt)}if(modified){if(filteredStatements.length===0)retireMigration(file,"columns already absent from table");else writeFileSync(filePath,`${filteredStatements.join(`;
6
6
  `)};
7
- `);continue}}}if(sqliteDb)try{sqliteDb.close()}catch{}if(droppedMigrations.length>0||replayMigrations.length>0)try{const dbPath=sqliteDatabasePath();mkdirSync(dirname(dbPath),{recursive:!0});const{Database}=require("bun:sqlite"),writeDb=new Database(dbPath);try{writeDb.exec(`CREATE TABLE IF NOT EXISTS migrations (
7
+ `);continue}}}if(sqliteDb&&!borrowedSchema)try{sqliteDb.close()}catch{}if(options.recordMigrations){options.recordMigrations(droppedMigrations,replayMigrations);return quarantined}if(droppedMigrations.length>0||replayMigrations.length>0)try{const dbPath=sqliteDatabasePath();mkdirSync(dirname(dbPath),{recursive:!0});const{Database}=require("bun:sqlite"),writeDb=new Database(dbPath);try{writeDb.exec(`CREATE TABLE IF NOT EXISTS migrations (
8
8
  id INTEGER PRIMARY KEY AUTOINCREMENT,
9
9
  migration TEXT NOT NULL UNIQUE,
10
10
  executed_at DATETIME DEFAULT CURRENT_TIMESTAMP
@@ -13,8 +13,12 @@ Create it yourself with: ${manualCreateHint(target)}`);if(result.created)log.su
13
13
  `)};
14
14
  `}async function restoreHiddenMigrations(hidden){const fs=await import("node:fs/promises");for(const{original,hidden:h}of hidden)try{if(h.endsWith(".ungated"))await fs.rm(original,{force:!0});await fs.rename(h,original)}catch{}}export async function countAppliedMigrations(){try{const rows=await db.unsafe("SELECT COUNT(*) AS n FROM migrations").execute(),row=Array.isArray(rows)?rows[0]:void 0;if(!row)return 0;const n=Number(row.n??row.N??0);return Number.isFinite(n)?n:0}catch{return 0}}async function writeMigrateMarker(appliedCount){try{const fs=await import("node:fs/promises"),dir=path.frameworkRuntimePath();await fs.mkdir(dir,{recursive:!0});const file=`${dir}/last-migrate-result.json`,body=JSON.stringify({appliedCount,completedAt:new Date().toISOString()});await fs.writeFile(file,body,"utf8")}catch{}}export function idempotentSql(sql){const header=/^(?:[^\S\n]*--[^\n]*\n)+/.exec(sql)?.[0]??"",stmts=sql.slice(header.length).split(";").map((s)=>s.trim()).filter(Boolean);if(stmts.length===0)return sql;const out=[];for(const raw of stmts){const stmt=raw.replace(/\bADD\s+COLUMN\s+(?!IF\s+NOT\s+EXISTS\b)/gi,"ADD COLUMN IF NOT EXISTS "),m=/^ALTER\s+TABLE\s+("?\w+"?)\s+ADD\s+CONSTRAINT\s+("?\w+"?)/i.exec(stmt);if(m){const drops=[`ALTER TABLE ${m[1]} DROP CONSTRAINT IF EXISTS ${m[2]}`],fk=/\bFOREIGN\s+KEY\s*\(\s*"?(\w+)"?\s*\)/i.exec(stmt);if(fk){const table=m[1].replace(/"/g,"");drops.push(`ALTER TABLE ${m[1]} DROP CONSTRAINT IF EXISTS "${table}_${fk[1]}_fkey"`)}const already=new Set;for(let i=out.length-1;i>=0;i--){const previous=out[i];if(!/^ALTER\s+TABLE\s+"?\w+"?\s+DROP\s+CONSTRAINT\b/i.test(previous))break;already.add(previous.toUpperCase())}for(const drop of drops)if(!already.has(drop.toUpperCase()))out.push(drop)}out.push(stmt)}return`${header}${out.join(`;
15
15
  `)};
16
- `}function makeMigrationsIdempotent(){const rewritten=[],migrationsDir=migrationDirectory("postgres");let files;try{files=readdirSync(migrationsDir).filter((f)=>f.endsWith(".sql"))}catch{return}for(const f of files){const p=join(migrationsDir,f);let sql;try{sql=readFileSync(p,"utf8")}catch{continue}if(!(/\bADD\s+(?:COLUMN|CONSTRAINT)\b/i.test(sql)||/\bCREATE\s+TYPE\b/i.test(sql)||/\bALTER\s+COLUMN\b[^\n]*\bTYPE\b/i.test(sql)))continue;const next=orderPostgresColumnTypeChanges(guardPostgresEnumTypes(idempotentSql(sql)));if(next!==sql)try{writeFileSync(p,next);rewritten.push(f)}catch{}}if(rewritten.length>0)log.warn(`[migration] Rewrote ${rewritten.length} migration file(s) on disk to be idempotent: ${rewritten.slice(0,3).join(", ")}${rewritten.length>3?`, +${rewritten.length-3} more`:""}. These files are tracked in git, so this shows up as a working-tree change.`)}function restoreUniqueIndexesAfterBatch(corpusDir){const dbPath=sqliteDatabasePath();if(!existsSync(dbPath))return;const{Database}=require("bun:sqlite"),liveDb=new Database(dbPath);try{liveDb.exec("PRAGMA busy_timeout = 5000");for(const index of restoreDroppedUniqueIndexes(liveDb,corpusDir))log.info(`Restored unique index ${index.name} on ${index.table} (declared in ${index.file}, dropped by a later table rebuild)`)}finally{liveDb.close()}}export async function runDatabaseMigration(){const startedAt=Date.now(),hidden=await hideDisabledFeatureMigrations();let quarantinedMigrations=[],lockHandle=null;try{log.debug("Migrating database...");await ensureDatabaseReady();configureQueryBuilder();const dialect=getDialect(),lockDb=dialect==="sqlite"?null:createQueryBuilder();lockHandle=await acquireMigrationLock(dialect,lockDb);try{const{packageMigrationRoots}=await import("@stacksjs/config"),staged=stagePackageMigrations({roots:packageMigrationRoots(),corpusDir:migrationDirectory(dialect)});if(staged.length>0){const packages=[...new Set(staged.map((s)=>s.package))].join(", ");log.info(`Staged ${staged.length} migration(s) from ${packages}`)}}catch(error){log.warn(`Could not stage package migrations: ${error instanceof Error?error.message:String(error)}`)}if(dialect==="sqlite"){quarantinedMigrations=preprocessSqliteMigrations();hidden.push(...quarantinedMigrations)}else if(dialect==="postgres")makeMigrationsIdempotent();const corpusDir=migrationDirectory(dialect),migrationsDir=migrationDirectory();let migrationSql="";try{migrationSql=readdirSync(migrationsDir).filter((file)=>file.endsWith(".sql")).sort().map((file)=>readFileSync(join(migrationsDir,file),"utf8")).join(`
17
- `)}catch{}if(migrationSql){const preflightTables=notificationTablesMissingCreateStatements(migrationSql);if(preflightTables.length>0){const preflight=await migrateNotificationTables({tables:preflightTables});if(!preflight.success)throw Error(preflight.error||"Failed to prepare notification tables before migrations")}}const appliedBefore=await countAppliedMigrations();if(dialect==="sqlite"&&appliedBefore>0)await ensureUuidColumns(sqlHelpers(dialect));await catchUpFrameworkRenames();log.debug(`[migration] Running migrations from: ${corpusDir}`);await qbExecuteMigration(corpusDir);const appliedAfter=await countAppliedMigrations();if(dialect==="sqlite"&&appliedAfter>appliedBefore)await ensureUuidColumns(sqlHelpers(dialect));await catchUpFrameworkRenames();if(dialect==="sqlite")restoreUniqueIndexesAfterBatch(corpusDir);const notificationTables=await migrateNotificationTables();if(!notificationTables.success)throw Error(notificationTables.error||"Failed to prepare notification tables");await ensureNotificationForeignKeys();const appliedCount=Math.max(0,appliedAfter-appliedBefore);await writeMigrateMarker(appliedCount);log.debug(`Database migration completed in ${Date.now()-startedAt}ms (applied ${appliedCount}).`);if(quarantinedMigrations.length>0&&process.env.STACKS_ALLOW_PENDING_MIGRATIONS!=="1"){const names=quarantinedMigrations.map((q)=>basename(q.original));return err(Error(`Applied ${appliedCount} migration${appliedCount===1?"":"s"}, but ${names.length} could not run on SQLite and ${names.length===1?"is":"are"} still pending: ${names.join(", ")}. ${names.length===1?"It mixes":"They mix"} statements SQLite cannot execute with statements it can, so splitting them into separate migrations lets the runnable half apply. Set STACKS_ALLOW_PENDING_MIGRATIONS=1 to proceed anyway.`))}return ok(appliedCount===0?"Nothing to migrate.":`Applied ${appliedCount} migration${appliedCount===1?"":"s"}.`)}catch(error){const detail=error instanceof Error?error.message:String(error);log.error(`[migration] Failed after ${Date.now()-startedAt}ms: ${detail}`);if(/UNIQUE constraint failed/i.test(detail)){const viaIndex=/index\s+'/i.test(detail);log.info("[migration] This is a data conflict, not a schema one - `migrate:fresh` replays the same statements against the same rows and fails the same way. Clear or de-duplicate the offending rows first.");if(viaIndex)log.info("[migration] The error names an index rather than a column, which SQLite only does for an expression index or a table rebuild - so the conflict is arising while rows are being copied, not from a bare CREATE UNIQUE INDEX.")}else log.info("[migration] Run `./buddy migrate:fresh` to drop and recreate the schema if state is partial.");return err(handleError("Migration failed",error))}finally{if(lockHandle)try{await lockHandle.release()}catch{}await restoreHiddenMigrations(hidden)}}const FRAMEWORK_TABLES=["oauth_refresh_tokens","oauth_access_tokens","oauth_clients","passkeys","failed_jobs","jobs","notifications","password_reset_tokens"];export async function resetDatabase(){try{await ensureDatabaseReady();configureQueryBuilder();const modelsDir=path.userModelsPath(),dialect=getDialect();await dropFrameworkTables(dialect);if(existsSync(modelsDir))await qbResetDatabase(modelsDir,{dialect,preserveMigrationState:!0});else log.debug(`No models directory at ${modelsDir}; skipping model table drops.`);const defaultsDir=defaultModelsPath();if(existsSync(defaultsDir))await qbResetDatabase(defaultsDir,{dialect,preserveMigrationState:!0});else log.debug(`No framework default models directory at ${defaultsDir}; skipping.`);if(dialect==="postgres")await dropOrphanedEnumTypes();return ok("All tables dropped successfully!")}catch(error){return err(handleError("Database reset failed",error))}}async function dropOrphanedEnumTypes(){try{const raw=await db.unsafe(`
16
+ `}function makeMigrationsIdempotent(){const rewritten=[],migrationsDir=migrationDirectory("postgres");let files;try{files=readdirSync(migrationsDir).filter((f)=>f.endsWith(".sql"))}catch{return}for(const f of files){const p=join(migrationsDir,f);let sql;try{sql=readFileSync(p,"utf8")}catch{continue}if(!(/\bADD\s+(?:COLUMN|CONSTRAINT)\b/i.test(sql)||/\bCREATE\s+TYPE\b/i.test(sql)||/\bALTER\s+COLUMN\b[^\n]*\bTYPE\b/i.test(sql)))continue;const next=orderPostgresColumnTypeChanges(guardPostgresEnumTypes(idempotentSql(sql)));if(next!==sql)try{writeFileSync(p,next);rewritten.push(f)}catch{}}if(rewritten.length>0)log.warn(`[migration] Rewrote ${rewritten.length} migration file(s) on disk to be idempotent: ${rewritten.slice(0,3).join(", ")}${rewritten.length>3?`, +${rewritten.length-3} more`:""}. These files are tracked in git, so this shows up as a working-tree change.`)}async function loadLibsqlSchemaMirror(){const{Database}=require("bun:sqlite"),mirror=new Database(":memory:");mirror.exec("PRAGMA foreign_keys = OFF");const objects=await db.unsafe("SELECT type, name, sql FROM sqlite_master WHERE sql IS NOT NULL AND name NOT LIKE 'sqlite_%' ORDER BY CASE type WHEN 'table' THEN 0 WHEN 'index' THEN 1 WHEN 'view' THEN 2 ELSE 3 END, rowid").execute();for(const object of objects)try{mirror.exec(object.sql)}catch(error){log.debug(`[migration] libSQL schema mirror skipped ${object.type} ${object.name}: ${error instanceof Error?error.message:String(error)}`)}if(objects.some((object)=>object.type==="table"&&object.name==="migrations")){const recorded=await db.unsafe("SELECT migration FROM migrations").execute(),insert=mirror.prepare("INSERT OR IGNORE INTO migrations (migration) VALUES (?)");for(const row of recorded)try{insert.run(row.migration)}catch{}}return mirror}async function recordLibsqlMigrations(record,replay){if(record.length===0&&replay.length===0)return;try{await db.unsafe(`CREATE TABLE IF NOT EXISTS migrations (
17
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
18
+ migration TEXT NOT NULL UNIQUE,
19
+ executed_at DATETIME DEFAULT CURRENT_TIMESTAMP
20
+ )`).execute();for(const migration of record)await db.unsafe("INSERT OR IGNORE INTO migrations (migration) VALUES (?)",[migration]).execute();for(const migration of replay)await db.unsafe("DELETE FROM migrations WHERE migration = ?",[migration]).execute()}catch(e){log.debug(`[migration] Could not record dropped migrations as executed: ${e}`)}}async function preprocessLibsqlMigrations(){const mirror=await loadLibsqlSchemaMirror();let ledger={record:[],replay:[]};try{const quarantined=preprocessSqliteMigrations({schema:mirror,recordMigrations:(record,replay)=>{ledger={record,replay}}});await recordLibsqlMigrations(ledger.record,ledger.replay);return quarantined}finally{mirror.close()}}async function restoreUniqueIndexesAfterBatch(corpusDir){if(usesLibsql()){const mirror=await loadLibsqlSchemaMirror();let missing;try{missing=restorableMissingUniqueIndexes(mirror,corpusDir)}finally{mirror.close()}for(const index of missing){try{await db.unsafe(index.statement).execute()}catch(error){const detail=error instanceof Error?error.message:String(error);throw Error(`Could not restore unique index "${index.name}" on "${index.table}" (declared in ${index.file}), which a later table rebuild dropped: ${detail}`)}log.info(`Restored unique index ${index.name} on ${index.table} (declared in ${index.file}, dropped by a later table rebuild)`)}return}const dbPath=sqliteDatabasePath();if(!existsSync(dbPath))return;const{Database}=require("bun:sqlite"),liveDb=new Database(dbPath);try{liveDb.exec("PRAGMA busy_timeout = 5000");for(const index of restoreDroppedUniqueIndexes(liveDb,corpusDir))log.info(`Restored unique index ${index.name} on ${index.table} (declared in ${index.file}, dropped by a later table rebuild)`)}finally{liveDb.close()}}export async function runDatabaseMigration(){const startedAt=Date.now(),hidden=await hideDisabledFeatureMigrations();let quarantinedMigrations=[],lockHandle=null;try{log.debug("Migrating database...");await ensureDatabaseReady();configureQueryBuilder();const dialect=getDialect();if(usesLibsql())lockHandle=await acquireLibsqlMigrationLock(db);else{const lockDb=dialect==="sqlite"?null:createQueryBuilder();lockHandle=await acquireMigrationLock(dialect,lockDb)}try{const{packageMigrationRoots}=await import("@stacksjs/config"),staged=stagePackageMigrations({roots:packageMigrationRoots(),corpusDir:migrationDirectory(dialect)});if(staged.length>0){const packages=[...new Set(staged.map((s)=>s.package))].join(", ");log.info(`Staged ${staged.length} migration(s) from ${packages}`)}}catch(error){log.warn(`Could not stage package migrations: ${error instanceof Error?error.message:String(error)}`)}if(dialect==="sqlite"){quarantinedMigrations=usesLibsql()?await preprocessLibsqlMigrations():preprocessSqliteMigrations();hidden.push(...quarantinedMigrations)}else if(dialect==="postgres")makeMigrationsIdempotent();const corpusDir=migrationDirectory(dialect),migrationsDir=migrationDirectory();let migrationSql="";try{migrationSql=readdirSync(migrationsDir).filter((file)=>file.endsWith(".sql")).sort().map((file)=>readFileSync(join(migrationsDir,file),"utf8")).join(`
21
+ `)}catch{}if(migrationSql){const preflightTables=notificationTablesMissingCreateStatements(migrationSql);if(preflightTables.length>0){const preflight=await migrateNotificationTables({tables:preflightTables});if(!preflight.success)throw Error(preflight.error||"Failed to prepare notification tables before migrations")}}const appliedBefore=await countAppliedMigrations();if(dialect==="sqlite"&&appliedBefore>0)await ensureUuidColumns(sqlHelpers(dialect));await catchUpFrameworkRenames();log.debug(`[migration] Running migrations from: ${corpusDir}`);await qbExecuteMigration(corpusDir);const appliedAfter=await countAppliedMigrations();if(dialect==="sqlite"&&appliedAfter>appliedBefore)await ensureUuidColumns(sqlHelpers(dialect));await catchUpFrameworkRenames();if(dialect==="sqlite")await restoreUniqueIndexesAfterBatch(corpusDir);const notificationTables=await migrateNotificationTables();if(!notificationTables.success)throw Error(notificationTables.error||"Failed to prepare notification tables");await ensureNotificationForeignKeys();const appliedCount=Math.max(0,appliedAfter-appliedBefore);await writeMigrateMarker(appliedCount);log.debug(`Database migration completed in ${Date.now()-startedAt}ms (applied ${appliedCount}).`);if(quarantinedMigrations.length>0&&process.env.STACKS_ALLOW_PENDING_MIGRATIONS!=="1"){const names=quarantinedMigrations.map((q)=>basename(q.original));return err(Error(`Applied ${appliedCount} migration${appliedCount===1?"":"s"}, but ${names.length} could not run on SQLite and ${names.length===1?"is":"are"} still pending: ${names.join(", ")}. ${names.length===1?"It mixes":"They mix"} statements SQLite cannot execute with statements it can, so splitting them into separate migrations lets the runnable half apply. Set STACKS_ALLOW_PENDING_MIGRATIONS=1 to proceed anyway.`))}return ok(appliedCount===0?"Nothing to migrate.":`Applied ${appliedCount} migration${appliedCount===1?"":"s"}.`)}catch(error){const detail=error instanceof Error?error.message:String(error);log.error(`[migration] Failed after ${Date.now()-startedAt}ms: ${detail}`);if(/UNIQUE constraint failed/i.test(detail)){const viaIndex=/index\s+'/i.test(detail);log.info("[migration] This is a data conflict, not a schema one - `migrate:fresh` replays the same statements against the same rows and fails the same way. Clear or de-duplicate the offending rows first.");if(viaIndex)log.info("[migration] The error names an index rather than a column, which SQLite only does for an expression index or a table rebuild - so the conflict is arising while rows are being copied, not from a bare CREATE UNIQUE INDEX.")}else log.info("[migration] Run `./buddy migrate:fresh` to drop and recreate the schema if state is partial.");return err(handleError("Migration failed",error))}finally{if(lockHandle)try{await lockHandle.release()}catch{}await restoreHiddenMigrations(hidden)}}const FRAMEWORK_TABLES=["oauth_refresh_tokens","oauth_access_tokens","oauth_clients","passkeys","failed_jobs","jobs","notifications","password_reset_tokens"];export async function resetDatabase(){try{await ensureDatabaseReady();configureQueryBuilder();const modelsDir=path.userModelsPath(),dialect=getDialect();await dropFrameworkTables(dialect);if(existsSync(modelsDir))await qbResetDatabase(modelsDir,{dialect,preserveMigrationState:!0});else log.debug(`No models directory at ${modelsDir}; skipping model table drops.`);const defaultsDir=defaultModelsPath();if(existsSync(defaultsDir))await qbResetDatabase(defaultsDir,{dialect,preserveMigrationState:!0});else log.debug(`No framework default models directory at ${defaultsDir}; skipping.`);if(dialect==="postgres")await dropOrphanedEnumTypes();return ok("All tables dropped successfully!")}catch(error){return err(handleError("Database reset failed",error))}}async function dropOrphanedEnumTypes(){try{const raw=await db.unsafe(`
18
22
  SELECT t.typname AS name
19
23
  FROM pg_type t
20
24
  JOIN pg_namespace n ON n.oid = t.typnamespace
@@ -1,4 +1,4 @@
1
- import{log}from"@stacksjs/logging";import{env as envVars}from"@stacksjs/env";import{db}from"./utils";import{sqlHelpers}from"./sql-helpers";import{indexSqlForDialect,isDuplicateIndexError}from"./dialect";function getDbDriver(){return process.env.DB_CONNECTION||envVars.DB_CONNECTION||"sqlite"}export function notificationsTableSql(sql){const{bigPkColumn,bigInteger,nullableTimestamp,datetime,utcNow}=sql;return`CREATE TABLE IF NOT EXISTS notifications (
1
+ import{log}from"@stacksjs/logging";import{env as envVars}from"@stacksjs/env";import{db}from"./utils";import{sqlHelpers}from"./sql-helpers";import{dialectCapabilities,indexSqlForDialect,isDuplicateIndexError}from"./dialect";function getDbDriver(){return process.env.DB_CONNECTION||envVars.DB_CONNECTION||"sqlite"}export function notificationsTableSql(sql){const{bigPkColumn,bigInteger,nullableTimestamp,datetime,utcNow}=sql;return`CREATE TABLE IF NOT EXISTS notifications (
2
2
  ${bigPkColumn},
3
3
  user_id ${bigInteger},
4
4
  type VARCHAR(255) NOT NULL,
@@ -29,4 +29,4 @@ import{log}from"@stacksjs/logging";import{env as envVars}from"@stacksjs/env";imp
29
29
  sent_at ${nullableTimestamp},
30
30
  created_at ${datetime} DEFAULT ${utcNow},
31
31
  updated_at ${nullableTimestamp}
32
- )`}async function createIndex(statement,dialect){try{await db.unsafe(indexSqlForDialect(statement,dialect)).execute()}catch(error){if(!isDuplicateIndexError(error))throw error}}const REQUIRED_COLUMNS={notifications:["id","user_id","type","data","read_at"],notification_preferences:["id","user_id","channel","enabled","category"],notification_deliveries:["id","user_id","channel","recipient","body","status"]};export const notificationTableNames=["notifications","notification_preferences","notification_deliveries"];export function notificationTablesMissingCreateStatements(sql){return notificationTableNames.filter((table)=>{const escaped=table.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),create=new RegExp(`\\bCREATE\\s+TABLE\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?["\\x60\\[]?${escaped}["\\x60\\]]?\\s*\\(`,"i").exec(sql);if(!create)return!0;const referencePattern=new RegExp(`\\b${escaped}\\b`,"i"),beforeCreate=sql.slice(0,create.index);return referencePattern.test(beforeCreate)})}async function warnOnShapeMismatch(table){const required=REQUIRED_COLUMNS[table];if(!required)return;try{const rows=await db.unsafe(`SELECT * FROM ${table} WHERE 1 = 0`).execute(),known=Array.isArray(rows)&&rows.length>0?Object.keys(rows[0]):null,missing=[];for(const column of required){if(known){if(!known.includes(column))missing.push(column);continue}try{await db.unsafe(`SELECT ${column} FROM ${table} WHERE 1 = 0`).execute()}catch{missing.push(column)}}if(missing.length>0)log.warn(`[notifications] "${table}" already exists without ${missing.join(", ")}. CREATE TABLE IF NOT EXISTS left it untouched, so the notification driver will fail on those columns. Something else owns this table - either rename it, or stop using the framework's notification tables.`)}catch{}}export async function migrateNotificationTables(options={}){const dbDriver=getDbDriver(),sql=sqlHelpers(dbDriver),tables=new Set(options.tables??notificationTableNames);if(options.verbose)log.info(`Creating notification tables for ${dbDriver}...`);try{if(tables.has("notifications")){if(options.verbose)log.info("Creating notifications table...");await warnOnShapeMismatch("notifications");await db.unsafe(notificationsTableSql(sql)).execute();await createIndex("CREATE INDEX IF NOT EXISTS idx_notifications_user ON notifications (user_id)",dbDriver)}if(tables.has("notification_preferences")){if(options.verbose)log.info("Creating notification_preferences table...");await warnOnShapeMismatch("notification_preferences");await db.unsafe(notificationPreferencesTableSql(sql)).execute();await createIndex("CREATE INDEX IF NOT EXISTS idx_notification_preferences_user ON notification_preferences (user_id)",dbDriver)}if(tables.has("notification_deliveries")){if(options.verbose)log.info("Creating notification deliveries table...");await warnOnShapeMismatch("notification_deliveries");await db.unsafe(notificationDeliveriesTableSql(sql)).execute();await createIndex("CREATE INDEX IF NOT EXISTS idx_notification_deliveries_channel ON notification_deliveries (channel)",dbDriver);await createIndex("CREATE INDEX IF NOT EXISTS idx_notification_deliveries_status ON notification_deliveries (status)",dbDriver)}if(options.verbose)log.success("Notification tables created");return{success:!0}}catch(error){const message=error instanceof Error?error.message:String(error);log.error(`Failed to create notification tables: ${message}`);return{success:!1,error:message}}}export async function ensureNotificationForeignKeys(options={}){if(getDbDriver()==="sqlite")return;for(const table of["notifications","notification_deliveries"]){const statements=[`ALTER TABLE ${table} DROP CONSTRAINT IF EXISTS "${table}_user_id_fk"`,`ALTER TABLE ${table} DROP CONSTRAINT IF EXISTS "${table}_user_id_fkey"`,`ALTER TABLE ${table} ADD CONSTRAINT "${table}_user_id_fk" FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE`];for(const statement of statements)try{await db.unsafe(statement).execute()}catch(error){if(options.verbose)log.debug(`[notification-tables] Skipped: ${statement} (${error instanceof Error?error.message:String(error)})`)}}}
32
+ )`}async function createIndex(statement,dialect){try{await db.unsafe(indexSqlForDialect(statement,dialect)).execute()}catch(error){if(!isDuplicateIndexError(error))throw error}}const REQUIRED_COLUMNS={notifications:["id","user_id","type","data","read_at"],notification_preferences:["id","user_id","channel","enabled","category"],notification_deliveries:["id","user_id","channel","recipient","body","status"]};export const notificationTableNames=["notifications","notification_preferences","notification_deliveries"];export function notificationTablesMissingCreateStatements(sql){return notificationTableNames.filter((table)=>{const escaped=table.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),create=new RegExp(`\\bCREATE\\s+TABLE\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?["\\x60\\[]?${escaped}["\\x60\\]]?\\s*\\(`,"i").exec(sql);if(!create)return!0;const referencePattern=new RegExp(`\\b${escaped}\\b`,"i"),beforeCreate=sql.slice(0,create.index);return referencePattern.test(beforeCreate)})}async function warnOnShapeMismatch(table){const required=REQUIRED_COLUMNS[table];if(!required)return;try{const rows=await db.unsafe(`SELECT * FROM ${table} WHERE 1 = 0`).execute(),known=Array.isArray(rows)&&rows.length>0?Object.keys(rows[0]):null,missing=[];for(const column of required){if(known){if(!known.includes(column))missing.push(column);continue}try{await db.unsafe(`SELECT ${column} FROM ${table} WHERE 1 = 0`).execute()}catch{missing.push(column)}}if(missing.length>0)log.warn(`[notifications] "${table}" already exists without ${missing.join(", ")}. CREATE TABLE IF NOT EXISTS left it untouched, so the notification driver will fail on those columns. Something else owns this table - either rename it, or stop using the framework's notification tables.`)}catch{}}export async function migrateNotificationTables(options={}){const dbDriver=getDbDriver(),sql=sqlHelpers(dbDriver),tables=new Set(options.tables??notificationTableNames);if(options.verbose)log.info(`Creating notification tables for ${dbDriver}...`);try{if(tables.has("notifications")){if(options.verbose)log.info("Creating notifications table...");await warnOnShapeMismatch("notifications");await db.unsafe(notificationsTableSql(sql)).execute();await createIndex("CREATE INDEX IF NOT EXISTS idx_notifications_user ON notifications (user_id)",dbDriver)}if(tables.has("notification_preferences")){if(options.verbose)log.info("Creating notification_preferences table...");await warnOnShapeMismatch("notification_preferences");await db.unsafe(notificationPreferencesTableSql(sql)).execute();await createIndex("CREATE INDEX IF NOT EXISTS idx_notification_preferences_user ON notification_preferences (user_id)",dbDriver)}if(tables.has("notification_deliveries")){if(options.verbose)log.info("Creating notification deliveries table...");await warnOnShapeMismatch("notification_deliveries");await db.unsafe(notificationDeliveriesTableSql(sql)).execute();await createIndex("CREATE INDEX IF NOT EXISTS idx_notification_deliveries_channel ON notification_deliveries (channel)",dbDriver);await createIndex("CREATE INDEX IF NOT EXISTS idx_notification_deliveries_status ON notification_deliveries (status)",dbDriver)}if(options.verbose)log.success("Notification tables created");return{success:!0}}catch(error){const message=error instanceof Error?error.message:String(error);log.error(`Failed to create notification tables: ${message}`);return{success:!1,error:message}}}export async function ensureNotificationForeignKeys(options={}){const dbDriver=getDbDriver();if(dialectCapabilities(dbDriver).wire==="sqlite")return;for(const table of["notifications","notification_deliveries"]){const statements=[`ALTER TABLE ${table} DROP CONSTRAINT IF EXISTS "${table}_user_id_fk"`,`ALTER TABLE ${table} DROP CONSTRAINT IF EXISTS "${table}_user_id_fkey"`,`ALTER TABLE ${table} ADD CONSTRAINT "${table}_user_id_fk" FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE`];for(const statement of statements)try{await db.unsafe(statement).execute()}catch(error){if(options.verbose)log.debug(`[notification-tables] Skipped: ${statement} (${error instanceof Error?error.message:String(error)})`)}}}
@@ -1,2 +1,2 @@
1
- import{sqlDateTime}from"./sql-helpers";import{memoryUsage}from"node:process";import{AsyncLocalStorage}from"node:async_hooks";import{config}from"@stacksjs/config";import{env as envVars}from"@stacksjs/env";import{log}from"@stacksjs/logging";import{parseCaptureBindings,parseSensitiveColumn,persistQueryLogValues,queryLogError}from"./query-log-bindings";import{normalizeQuery,parseQuery}from"./query-parser";import{db,getDatabaseDialect}from"./utils";const QUERY_TRACKER_KEY=Symbol.for("stacks.database.queryTracker");let configuredQueryTracker=()=>{};export function setQueryTracker(fn){configuredQueryTracker=fn}function trackQuery(query,durationMs,connection){const shared=globalThis[QUERY_TRACKER_KEY];(typeof shared==="function"?shared:configuredQueryTracker)(query,durationMs,connection)}const queryLogContext=new AsyncLocalStorage,QUERY_LOG_BATCH_SIZE=100,QUERY_LOG_BATCH_DELAY_MS=5,pendingQueryLogs=[];let queryLogFlushScheduled=!1,queryLogFlushInFlight=!1;function enqueueQueryLog(record){const settled=new Promise((resolve)=>{pendingQueryLogs.push({record,resolve})});if(pendingQueryLogs.length>=QUERY_LOG_BATCH_SIZE)flushQueuedQueryLogs();else if(!queryLogFlushScheduled){queryLogFlushScheduled=!0;setTimeout(()=>{queryLogFlushScheduled=!1;flushQueuedQueryLogs()},QUERY_LOG_BATCH_DELAY_MS)}return settled}async function flushQueuedQueryLogs(){if(queryLogFlushInFlight)return;queryLogFlushInFlight=!0;try{while(pendingQueryLogs.length>0){const batch=pendingQueryLogs.splice(0,QUERY_LOG_BATCH_SIZE),recordsByShape=Map.groupBy(batch.map((item)=>item.record),(record)=>Object.keys(record).join("\x00"));for(const records of recordsByShape.values())if(!await queryLogContext.run(!0,()=>storeQueryLogs(records,records.length===1)))for(const record of records)await queryLogContext.run(!0,()=>storeQueryLogs([record]));for(const item of batch)item.resolve()}}finally{queryLogFlushInFlight=!1;if(pendingQueryLogs.length>0&&!queryLogFlushScheduled){queryLogFlushScheduled=!0;setTimeout(()=>{queryLogFlushScheduled=!1;flushQueuedQueryLogs()},QUERY_LOG_BATCH_DELAY_MS)}}}export async function logQuery(event){if(queryLogContext.getStore())return;try{const{query,durationMs,error,parameters}=extractQueryInfo(event);if(query)try{trackQuery(query,durationMs,config.database?.default||"unknown")}catch{}if(!config.database?.queryLogging?.enabled)return;const excludedQueries=config.database?.queryLogging?.excludedQueries;if(!query.trim()||isExcludedQuery(query,Array.isArray(excludedQueries)?excludedQueries:[]))return;const status=determineQueryStatus(durationMs,error),logRecord=await createQueryLogRecord(query,durationMs,status,error,parameters);if(config.database?.queryLogging?.analysis?.enabled&&(status==="slow"||config.database.queryLogging.analysis.analyzeAll))await enhanceWithQueryAnalysis(logRecord);await enqueueQueryLog(logRecord);if(status!=="completed")log[status==="failed"?"error":"warn"](`Query ${status}:`,{query:logRecord.query,duration:logRecord.duration,connection:logRecord.connection,...error&&{error}})}catch(err){log.error("Failed to log query:",err)}}export function isExcludedQuery(query,excludedQueries){const normalizedQuery=query.toLowerCase();return excludedQueries.some((pattern)=>{const normalizedPattern=pattern.trim().toLowerCase();return normalizedPattern.length>0&&normalizedQuery.includes(normalizedPattern)})}function queryText(sql){if(typeof sql==="string")return usableQueryText(sql);if(sql&&typeof sql==="object"&&typeof sql.then==="function")return"";if(sql&&typeof sql.toString==="function")return usableQueryText(String(sql));return""}function usableQueryText(text){return text.startsWith("[object ")?"":text}function extractQueryInfo(event){const query=queryText(event.query?.sql),durationMs=event.queryDurationMillis||0,error=event.error,parameters=event.query?.parameters;return{query,durationMs,error,parameters}}function determineQueryStatus(durationMs,error){const slowThreshold=config.database?.queryLogging?.slowThreshold||100;if(error)return"failed";if(durationMs>slowThreshold)return"slow";return"completed"}const warnedCaptureBindings=new Set;function capturesBindingValues(){const setting=config.database?.queryLogging?.captureBindings??envVars.DB_QUERY_LOGGING_CAPTURE_BINDINGS,capture=parseCaptureBindings(setting);if(capture===null){const shown=String(setting);if(!warnedCaptureBindings.has(shown)){warnedCaptureBindings.add(shown);console.warn(`[database] queryLogging.captureBindings (DB_QUERY_LOGGING_CAPTURE_BINDINGS) is "${shown}", which is not true, false, 1, 0, yes, no, on or off; query logs keep only the type of each binding.`)}return!1}if(capture!==void 0)return capture;const appEnv=config.app?.env;return appEnv!=="production"&&appEnv!=="prod"}const warnedSensitiveColumns=new Set;function sensitiveColumns(){const setting=config.database?.queryLogging?.sensitiveColumns;if(setting===void 0||setting===null)return[];const entries=Array.isArray(setting)?setting:[setting];for(const entry of entries){const shown=String(entry);if(parseSensitiveColumn(entry)===void 0&&!warnedSensitiveColumns.has(shown)){warnedSensitiveColumns.add(shown);console.warn(`[database] queryLogging.sensitiveColumns has "${shown}", which is not a column or table.column; it marks nothing as sensitive.`)}}return entries}async function createQueryLogRecord(query,durationMs,status,error,parameters){const connection=config.database.default||"unknown",normalizedQuery=normalizeQuery(query)||query,traceInfo=status==="completed"&&!config.database?.queryLogging?.captureAllTraces?void 0:extractTraceInfo(),errorText=error?String(error):void 0,persisted=parameters?persistQueryLogValues(query,parameters,errorText,{captureValues:capturesBindingValues(),sensitiveColumns:sensitiveColumns()}):{bindings:void 0,error:errorText===void 0?void 0:queryLogError(errorText,[],[])};return{query,normalized_query:normalizedQuery,duration:durationMs,connection,status,error:persisted.error,executed_at:sqlDateTime(),bindings:persisted.bindings,trace:traceInfo?.trace,...traceInfo?.caller??{},memory_usage:memoryUsage().heapUsed/1024/1024}}const SECRET_PATTERNS=[/\b(?:sk|pk|rk|api|secret|token|bearer|password|pwd|key)[_-]?[A-Za-z0-9]{12,}/gi,/\bsk_(?:live|test)_[A-Za-z0-9]{20,}/g,/\bAKIA[0-9A-Z]{16}/g,/\b[A-Za-z0-9_-]{40,}\b(?=\s|$|['"])/g];function sanitizeStackTrace(stack){let out=stack;for(const pattern of SECRET_PATTERNS)out=out.replace(pattern,"<redacted>");return out}let lastTraceInfo;function extractTraceInfo(){try{const stack=Error("Stack trace capture").stack||"";if(lastTraceInfo?.trace===stack)return lastTraceInfo;const callerLine=stack.split(`
2
- `).slice(1).find((line)=>!line.includes("query-logger.ts"));let caller={};if(callerLine){const methodMatch=callerLine.match(/at (.+?) \(/),fileMatch=callerLine.match(/\((.+?):(\d+):(\d+)\)/);if(methodMatch&&methodMatch[1]){const methodParts=methodMatch[1].split(".");caller={model:methodParts.length>1?methodParts[0]:void 0,method:methodParts.length>1?methodParts[1]:methodParts[0]}}if(fileMatch&&fileMatch[1]&&fileMatch[2])caller={...caller,file:fileMatch[1],line:Number.parseInt(fileMatch[2],10)}}const result={trace:sanitizeStackTrace(stack),caller};if(result.trace===stack&&stack.length<=8192)lastTraceInfo=result;return result}catch{return{trace:"",caller:{}}}}async function enhanceWithQueryAnalysis(logRecord){try{const{tables,type}=parseQuery(logRecord.query);logRecord.affected_tables=JSON.stringify(tables||[]);if(type==="SELECT"&&config.database?.queryLogging?.analysis?.explainPlan){const explainResult=await getExplainPlan(logRecord.query);if(explainResult){logRecord.explain_plan=explainResult.plan;logRecord.indexes_used=JSON.stringify(explainResult.indexesUsed||[]);logRecord.missing_indexes=JSON.stringify(explainResult.missingIndexes||[]);if(config.database?.queryLogging?.analysis?.suggestions)logRecord.optimization_suggestions=JSON.stringify(generateOptimizationSuggestions(explainResult,logRecord))}}const tags=[type];if(tables&&tables.length>0)tags.push(...tables.map((table)=>`table:${table}`));logRecord.tags=JSON.stringify(tags)}catch(error){log.debug("Error during query analysis:",error)}}const EXPLAIN_DIALECTS=new Set(["sqlite","mysql","postgres"]);async function getExplainPlan(query){try{const rawDriver=(await import("@stacksjs/config")).config?.database?.default,driver=typeof rawDriver==="string"?rawDriver:"sqlite";if(!EXPLAIN_DIALECTS.has(driver))return null;let sqlText;if(driver==="mysql")sqlText=`EXPLAIN FORMAT=JSON ${query}`;else if(driver==="postgres")sqlText=`EXPLAIN (FORMAT JSON) ${query}`;else sqlText=`EXPLAIN QUERY PLAN ${query}`;const result=await db.unsafe?.(sqlText);if(!result)return null;const raw=result,rows=Array.isArray(raw)?raw:raw?.rows??[],planText=JSON.stringify(rows),indexesUsed=[],missingIndexes=[];if(driver==="sqlite")for(const row of rows){const detail=row?.detail||"",idxMatch=detail.match(/USING (?:COVERING )?INDEX (\w+)/i);if(idxMatch&&idxMatch[1])indexesUsed.push(idxMatch[1]);else{const scanMatch=detail.match(/^SCAN\s+(\w+)/i);if(scanMatch&&scanMatch[1])missingIndexes.push(scanMatch[1])}}else if(driver==="mysql"){const text=planText;for(const m of text.matchAll(/"key"\s*:\s*"([^"]+)"/g))if(m[1])indexesUsed.push(m[1]);for(const m of text.matchAll(/"table_name"\s*:\s*"([^"]+)"[^}]*"access_type"\s*:\s*"ALL"/g))if(m[1])missingIndexes.push(m[1])}else if(driver==="postgres"){const text=planText;for(const m of text.matchAll(/"Index Name"\s*:\s*"([^"]+)"/g))if(m[1])indexesUsed.push(m[1]);for(const m of text.matchAll(/"Node Type"\s*:\s*"Seq Scan"[^}]*"Relation Name"\s*:\s*"([^"]+)"/g))if(m[1])missingIndexes.push(m[1])}return{plan:planText.length>4000?`${planText.slice(0,4000)}\u2026`:planText,indexesUsed:Array.from(new Set(indexesUsed)),missingIndexes:Array.from(new Set(missingIndexes))}}catch(err){log.debug("[query-logger] EXPLAIN failed (non-fatal):",err);return null}}function generateOptimizationSuggestions(explainResult,logRecord){const suggestions=[];if(explainResult.missingIndexes&&explainResult.missingIndexes.length>0)suggestions.push(`Consider adding index on ${explainResult.missingIndexes.join(", ")}`);if(logRecord.status==="slow"){suggestions.push("Consider optimizing this query to reduce execution time");if(logRecord.query.toLowerCase().includes("select *"))suggestions.push("Specify only needed columns instead of using SELECT *");if(!logRecord.query.toLowerCase().includes("limit"))suggestions.push("Consider adding LIMIT clause to reduce result set size")}return suggestions}async function storeQueryLogs(logRecords,reportFailure=!0){try{const values=logRecords;if(values.length>1&&getDatabaseDialect()==="sqlite"){const columns=Object.keys(values[0]),identifiers=columns.map((column)=>`"${column.replaceAll('"','""')}"`).join(", "),row=`(${columns.map(()=>"?").join(", ")})`,statement=`INSERT INTO query_logs (${identifiers}) VALUES ${values.map(()=>row).join(", ")}`,bindings=values.flatMap((value)=>columns.map((column)=>value[column]??null));db.unsafe("SAVEPOINT stacks_query_log_batch").executeSync();try{db.unsafe(statement,bindings).executeSync();db.unsafe("RELEASE SAVEPOINT stacks_query_log_batch").executeSync()}catch(error){db.unsafe("ROLLBACK TO SAVEPOINT stacks_query_log_batch").executeSync();db.unsafe("RELEASE SAVEPOINT stacks_query_log_batch").executeSync();throw error}}else await db.insertInto("query_logs").values(values).execute();return!0}catch(error){if(reportFailure){const message=error instanceof Error?error.message:String(error);if(/no such table|does not exist|doesn't exist/i.test(message)&&/query_logs/i.test(message))log.debug("Query logging will start after the query_logs table is migrated.");else log.error("Failed to store query log:",error)}return!1}}
1
+ import{sqlDateTime}from"./sql-helpers";import{memoryUsage}from"node:process";import{AsyncLocalStorage}from"node:async_hooks";import{config}from"@stacksjs/config";import{env as envVars}from"@stacksjs/env";import{log}from"@stacksjs/logging";import{parseCaptureBindings,parseSensitiveColumn,persistQueryLogValues,queryLogError}from"./query-log-bindings";import{normalizeQuery,parseQuery}from"./query-parser";import{db,getDatabaseDialect}from"./utils";import{isLibsqlDriver}from"./dialect";const QUERY_TRACKER_KEY=Symbol.for("stacks.database.queryTracker");let configuredQueryTracker=()=>{};export function setQueryTracker(fn){configuredQueryTracker=fn}function trackQuery(query,durationMs,connection){const shared=globalThis[QUERY_TRACKER_KEY];(typeof shared==="function"?shared:configuredQueryTracker)(query,durationMs,connection)}const queryLogContext=new AsyncLocalStorage,QUERY_LOG_BATCH_SIZE=100,QUERY_LOG_BATCH_DELAY_MS=5,pendingQueryLogs=[];let queryLogFlushScheduled=!1,queryLogFlushInFlight=!1;function enqueueQueryLog(record){const settled=new Promise((resolve)=>{pendingQueryLogs.push({record,resolve})});if(pendingQueryLogs.length>=QUERY_LOG_BATCH_SIZE)flushQueuedQueryLogs();else if(!queryLogFlushScheduled){queryLogFlushScheduled=!0;setTimeout(()=>{queryLogFlushScheduled=!1;flushQueuedQueryLogs()},QUERY_LOG_BATCH_DELAY_MS)}return settled}async function flushQueuedQueryLogs(){if(queryLogFlushInFlight)return;queryLogFlushInFlight=!0;try{while(pendingQueryLogs.length>0){const batch=pendingQueryLogs.splice(0,QUERY_LOG_BATCH_SIZE),recordsByShape=Map.groupBy(batch.map((item)=>item.record),(record)=>Object.keys(record).join("\x00"));for(const records of recordsByShape.values())if(!await queryLogContext.run(!0,()=>storeQueryLogs(records,records.length===1)))for(const record of records)await queryLogContext.run(!0,()=>storeQueryLogs([record]));for(const item of batch)item.resolve()}}finally{queryLogFlushInFlight=!1;if(pendingQueryLogs.length>0&&!queryLogFlushScheduled){queryLogFlushScheduled=!0;setTimeout(()=>{queryLogFlushScheduled=!1;flushQueuedQueryLogs()},QUERY_LOG_BATCH_DELAY_MS)}}}export async function logQuery(event){if(queryLogContext.getStore())return;try{const{query,durationMs,error,parameters}=extractQueryInfo(event);if(query)try{trackQuery(query,durationMs,config.database?.default||"unknown")}catch{}if(!config.database?.queryLogging?.enabled)return;const excludedQueries=config.database?.queryLogging?.excludedQueries;if(!query.trim()||isExcludedQuery(query,Array.isArray(excludedQueries)?excludedQueries:[]))return;const status=determineQueryStatus(durationMs,error),logRecord=await createQueryLogRecord(query,durationMs,status,error,parameters);if(config.database?.queryLogging?.analysis?.enabled&&(status==="slow"||config.database.queryLogging.analysis.analyzeAll))await enhanceWithQueryAnalysis(logRecord);await enqueueQueryLog(logRecord);if(status!=="completed")log[status==="failed"?"error":"warn"](`Query ${status}:`,{query:logRecord.query,duration:logRecord.duration,connection:logRecord.connection,...error&&{error}})}catch(err){log.error("Failed to log query:",err)}}export function isExcludedQuery(query,excludedQueries){const normalizedQuery=query.toLowerCase();return excludedQueries.some((pattern)=>{const normalizedPattern=pattern.trim().toLowerCase();return normalizedPattern.length>0&&normalizedQuery.includes(normalizedPattern)})}function queryText(sql){if(typeof sql==="string")return usableQueryText(sql);if(sql&&typeof sql==="object"&&typeof sql.then==="function")return"";if(sql&&typeof sql.toString==="function")return usableQueryText(String(sql));return""}function usableQueryText(text){return text.startsWith("[object ")?"":text}function extractQueryInfo(event){const query=queryText(event.query?.sql),durationMs=event.queryDurationMillis||0,error=event.error,parameters=event.query?.parameters;return{query,durationMs,error,parameters}}function determineQueryStatus(durationMs,error){const slowThreshold=config.database?.queryLogging?.slowThreshold||100;if(error)return"failed";if(durationMs>slowThreshold)return"slow";return"completed"}const warnedCaptureBindings=new Set;function capturesBindingValues(){const setting=config.database?.queryLogging?.captureBindings??envVars.DB_QUERY_LOGGING_CAPTURE_BINDINGS,capture=parseCaptureBindings(setting);if(capture===null){const shown=String(setting);if(!warnedCaptureBindings.has(shown)){warnedCaptureBindings.add(shown);console.warn(`[database] queryLogging.captureBindings (DB_QUERY_LOGGING_CAPTURE_BINDINGS) is "${shown}", which is not true, false, 1, 0, yes, no, on or off; query logs keep only the type of each binding.`)}return!1}if(capture!==void 0)return capture;const appEnv=config.app?.env;return appEnv!=="production"&&appEnv!=="prod"}const warnedSensitiveColumns=new Set;function sensitiveColumns(){const setting=config.database?.queryLogging?.sensitiveColumns;if(setting===void 0||setting===null)return[];const entries=Array.isArray(setting)?setting:[setting];for(const entry of entries){const shown=String(entry);if(parseSensitiveColumn(entry)===void 0&&!warnedSensitiveColumns.has(shown)){warnedSensitiveColumns.add(shown);console.warn(`[database] queryLogging.sensitiveColumns has "${shown}", which is not a column or table.column; it marks nothing as sensitive.`)}}return entries}async function createQueryLogRecord(query,durationMs,status,error,parameters){const connection=config.database.default||"unknown",normalizedQuery=normalizeQuery(query)||query,traceInfo=status==="completed"&&!config.database?.queryLogging?.captureAllTraces?void 0:extractTraceInfo(),errorText=error?String(error):void 0,persisted=parameters?persistQueryLogValues(query,parameters,errorText,{captureValues:capturesBindingValues(),sensitiveColumns:sensitiveColumns()}):{bindings:void 0,error:errorText===void 0?void 0:queryLogError(errorText,[],[])};return{query,normalized_query:normalizedQuery,duration:durationMs,connection,status,error:persisted.error,executed_at:sqlDateTime(),bindings:persisted.bindings,trace:traceInfo?.trace,...traceInfo?.caller??{},memory_usage:memoryUsage().heapUsed/1024/1024}}const SECRET_PATTERNS=[/\b(?:sk|pk|rk|api|secret|token|bearer|password|pwd|key)[_-]?[A-Za-z0-9]{12,}/gi,/\bsk_(?:live|test)_[A-Za-z0-9]{20,}/g,/\bAKIA[0-9A-Z]{16}/g,/\b[A-Za-z0-9_-]{40,}\b(?=\s|$|['"])/g];function sanitizeStackTrace(stack){let out=stack;for(const pattern of SECRET_PATTERNS)out=out.replace(pattern,"<redacted>");return out}let lastTraceInfo;function extractTraceInfo(){try{const stack=Error("Stack trace capture").stack||"";if(lastTraceInfo?.trace===stack)return lastTraceInfo;const callerLine=stack.split(`
2
+ `).slice(1).find((line)=>!line.includes("query-logger.ts"));let caller={};if(callerLine){const methodMatch=callerLine.match(/at (.+?) \(/),fileMatch=callerLine.match(/\((.+?):(\d+):(\d+)\)/);if(methodMatch&&methodMatch[1]){const methodParts=methodMatch[1].split(".");caller={model:methodParts.length>1?methodParts[0]:void 0,method:methodParts.length>1?methodParts[1]:methodParts[0]}}if(fileMatch&&fileMatch[1]&&fileMatch[2])caller={...caller,file:fileMatch[1],line:Number.parseInt(fileMatch[2],10)}}const result={trace:sanitizeStackTrace(stack),caller};if(result.trace===stack&&stack.length<=8192)lastTraceInfo=result;return result}catch{return{trace:"",caller:{}}}}async function enhanceWithQueryAnalysis(logRecord){try{const{tables,type}=parseQuery(logRecord.query);logRecord.affected_tables=JSON.stringify(tables||[]);if(type==="SELECT"&&config.database?.queryLogging?.analysis?.explainPlan){const explainResult=await getExplainPlan(logRecord.query);if(explainResult){logRecord.explain_plan=explainResult.plan;logRecord.indexes_used=JSON.stringify(explainResult.indexesUsed||[]);logRecord.missing_indexes=JSON.stringify(explainResult.missingIndexes||[]);if(config.database?.queryLogging?.analysis?.suggestions)logRecord.optimization_suggestions=JSON.stringify(generateOptimizationSuggestions(explainResult,logRecord))}}const tags=[type];if(tables&&tables.length>0)tags.push(...tables.map((table)=>`table:${table}`));logRecord.tags=JSON.stringify(tags)}catch(error){log.debug("Error during query analysis:",error)}}const EXPLAIN_DIALECTS=new Set(["sqlite","turso","mysql","postgres"]);async function getExplainPlan(query){try{const rawDriver=(await import("@stacksjs/config")).config?.database?.default,driver=typeof rawDriver==="string"?isLibsqlDriver(rawDriver)?"sqlite":rawDriver:"sqlite";if(!EXPLAIN_DIALECTS.has(driver))return null;let sqlText;if(driver==="mysql")sqlText=`EXPLAIN FORMAT=JSON ${query}`;else if(driver==="postgres")sqlText=`EXPLAIN (FORMAT JSON) ${query}`;else sqlText=`EXPLAIN QUERY PLAN ${query}`;const result=await db.unsafe?.(sqlText);if(!result)return null;const raw=result,rows=Array.isArray(raw)?raw:raw?.rows??[],planText=JSON.stringify(rows),indexesUsed=[],missingIndexes=[];if(driver==="sqlite")for(const row of rows){const detail=row?.detail||"",idxMatch=detail.match(/USING (?:COVERING )?INDEX (\w+)/i);if(idxMatch&&idxMatch[1])indexesUsed.push(idxMatch[1]);else{const scanMatch=detail.match(/^SCAN\s+(\w+)/i);if(scanMatch&&scanMatch[1])missingIndexes.push(scanMatch[1])}}else if(driver==="mysql"){const text=planText;for(const m of text.matchAll(/"key"\s*:\s*"([^"]+)"/g))if(m[1])indexesUsed.push(m[1]);for(const m of text.matchAll(/"table_name"\s*:\s*"([^"]+)"[^}]*"access_type"\s*:\s*"ALL"/g))if(m[1])missingIndexes.push(m[1])}else if(driver==="postgres"){const text=planText;for(const m of text.matchAll(/"Index Name"\s*:\s*"([^"]+)"/g))if(m[1])indexesUsed.push(m[1]);for(const m of text.matchAll(/"Node Type"\s*:\s*"Seq Scan"[^}]*"Relation Name"\s*:\s*"([^"]+)"/g))if(m[1])missingIndexes.push(m[1])}return{plan:planText.length>4000?`${planText.slice(0,4000)}\u2026`:planText,indexesUsed:Array.from(new Set(indexesUsed)),missingIndexes:Array.from(new Set(missingIndexes))}}catch(err){log.debug("[query-logger] EXPLAIN failed (non-fatal):",err);return null}}function generateOptimizationSuggestions(explainResult,logRecord){const suggestions=[];if(explainResult.missingIndexes&&explainResult.missingIndexes.length>0)suggestions.push(`Consider adding index on ${explainResult.missingIndexes.join(", ")}`);if(logRecord.status==="slow"){suggestions.push("Consider optimizing this query to reduce execution time");if(logRecord.query.toLowerCase().includes("select *"))suggestions.push("Specify only needed columns instead of using SELECT *");if(!logRecord.query.toLowerCase().includes("limit"))suggestions.push("Consider adding LIMIT clause to reduce result set size")}return suggestions}async function storeQueryLogs(logRecords,reportFailure=!0){try{const values=logRecords,libsql=isLibsqlDriver(config.database?.default),batch=libsql?db.sql?.batch:void 0;if(values.length>1&&getDatabaseDialect()==="sqlite"&&batch){const columns=Object.keys(values[0]),identifiers=columns.map((column)=>`"${column.replaceAll('"','""')}"`).join(", "),row=`(${columns.map(()=>"?").join(", ")})`,statement=`INSERT INTO query_logs (${identifiers}) VALUES ${values.map(()=>row).join(", ")}`,bindings=values.flatMap((value)=>columns.map((column)=>value[column]??null));await batch([{sql:statement,args:bindings}])}else if(values.length>1&&getDatabaseDialect()==="sqlite"&&!libsql){const columns=Object.keys(values[0]),identifiers=columns.map((column)=>`"${column.replaceAll('"','""')}"`).join(", "),row=`(${columns.map(()=>"?").join(", ")})`,statement=`INSERT INTO query_logs (${identifiers}) VALUES ${values.map(()=>row).join(", ")}`,bindings=values.flatMap((value)=>columns.map((column)=>value[column]??null));db.unsafe("SAVEPOINT stacks_query_log_batch").executeSync();try{db.unsafe(statement,bindings).executeSync();db.unsafe("RELEASE SAVEPOINT stacks_query_log_batch").executeSync()}catch(error){db.unsafe("ROLLBACK TO SAVEPOINT stacks_query_log_batch").executeSync();db.unsafe("RELEASE SAVEPOINT stacks_query_log_batch").executeSync();throw error}}else await db.insertInto("query_logs").values(values).execute();return!0}catch(error){if(reportFailure){const message=error instanceof Error?error.message:String(error);if(/no such table|does not exist|doesn't exist/i.test(message)&&/query_logs/i.test(message))log.debug("Query logging will start after the query_logs table is migrated.");else log.error("Failed to store query log:",error)}return!1}}
package/dist/utils.d.ts CHANGED
@@ -129,6 +129,8 @@ export declare const db: Db;
129
129
  export declare const readDb: Omit<Db, 'read' | 'primary'>;
130
130
  declare interface DbConnectionConfig {
131
131
  database?: string
132
+ url?: string
133
+ authToken?: string
132
134
  name?: string
133
135
  host?: string
134
136
  username?: string
@@ -147,6 +149,7 @@ declare interface DbConfig {
147
149
  singlestore: DbConnectionConfig
148
150
  vitess: DbConnectionConfig
149
151
  postgres: DbConnectionConfig
152
+ turso?: DbConnectionConfig
150
153
  }
151
154
  reads?: ReadPolicyConfig
152
155
  }
package/dist/utils.js CHANGED
@@ -1 +1 @@
1
- import{AsyncLocalStorage}from"node:async_hooks";import process from"node:process";import{isPromise,isProxy}from"node:util/types";import{config as queryBuilderConfig,createQueryBuilder,registerPersistentQueryHooks,resetConnection as resetQueryBuilderConnection,setConfig}from"@stacksjs/query-builder";import{SQL}from"bun";import{recordDatabaseQueryError}from"./broken-pool";import{closeDatabaseConnectionsAndPending,retireDatabaseConnections}from"./connection-lifecycle";import{runInTransactionScope}from"./transaction-context";import{env as envVars}from"@stacksjs/env/runtime";import{getConnectionDefaults}from"./defaults";import{isMysqlWire,isPostgresWire,isVitessSharded,toQueryBuilderDialect}from"./dialect";import{relativeMigrationDirectory,resolveMigrationDirectory,snapshotDirForQueryBuilder}from"./migration-path";import{configureDatabaseRoutingContext,contextInTransaction,markContextWrote,replicaConnectionKey,resolveReplicaConnection,runInDatabaseRoutingContext,selectReplica,shouldRouteToReplica,withDatabaseRoutingContext,withTransactionContext}from"./replicas";import{aggregateFunctions}from"./types";const sqliteDefaults=getConnectionDefaults("sqlite",envVars),mysqlDefaults=getConnectionDefaults("mysql",envVars),postgresDefaults=getConnectionDefaults("postgres",envVars);let appEnv=envVars.APP_ENV||"local",dbDriver=envVars.DB_CONNECTION||"sqlite",queryBuilderDialect=toQueryBuilderDialect(dbDriver),queryLoggingEnabled=envVars.DB_QUERY_LOGGING_ENABLED??!isProductionEnvironment(appEnv),dbConfig={connections:{sqlite:{database:sqliteDefaults.database,prefix:""},mysql:{name:mysqlDefaults.database,host:mysqlDefaults.host,username:mysqlDefaults.username,password:mysqlDefaults.password,port:mysqlDefaults.port,prefix:""},singlestore:{name:mysqlDefaults.database,host:mysqlDefaults.host,username:mysqlDefaults.username,password:mysqlDefaults.password,port:mysqlDefaults.port,prefix:""},vitess:{name:mysqlDefaults.database,host:mysqlDefaults.host,username:mysqlDefaults.username,password:mysqlDefaults.password,port:15306,prefix:"",sharded:isVitessSharded()},postgres:{name:postgresDefaults.database,host:postgresDefaults.host,username:postgresDefaults.username,password:postgresDefaults.password,port:postgresDefaults.port,prefix:""}}},dbConfigLockTail=Promise.resolve();const DB_CONFIG_LOCK_MAX_HOLD_MS=60000;export function acquireDbConfigLock(){let release=()=>{};const held=new Promise((resolve)=>{release=resolve}),acquired=dbConfigLockTail.then(()=>{const watchdog=setTimeout(()=>{console.warn(`[database] the db config lock was held for over ${DB_CONFIG_LOCK_MAX_HOLD_MS/1000}s. Releasing it so the queue can proceed - a test file most likely failed before its afterAll ran.`);release()},DB_CONFIG_LOCK_MAX_HOLD_MS);watchdog.unref?.();held.then(()=>clearTimeout(watchdog));return release});dbConfigLockTail=dbConfigLockTail.then(()=>held).catch(()=>{});return acquired}export function initializeDbConfig(config){if(config?.app?.env)appEnv=config.app.env;if(config?.database?.default)dbDriver=config.database.default;queryBuilderDialect=toQueryBuilderDialect(dbDriver);if(config?.database?.connections)dbConfig=config.database;configureDatabaseRoutingContext(getReplicas().length>0);queryLoggingEnabled=config?.database?.queryLogging?.enabled??envVars.DB_QUERY_LOGGING_ENABLED??!isProductionEnvironment(appEnv);syncDatabaseQueryHooks();syncBrokenPoolDetection();updateQueryBuilderConfig();_dbInstance=null;_replicaInstances=new Map}function getEnv(){return appEnv}function isProductionEnvironment(value){return value==="production"||value==="prod"}function getDriver(){return dbDriver}function getDatabaseConfig(){return dbConfig}function getDialect(){return queryBuilderDialect}export{getDialect as getDatabaseDialect};function getDbConfig(){const driver=getDriver(),database=getDatabaseConfig(),env=getEnv();if(driver==="sqlite"){const defaultName=env!=="testing"?"database/stacks.sqlite":"database/stacks_testing.sqlite";return{database:database.connections?.sqlite?.database??defaultName}}if(driver==="mysql")return{database:database.connections?.mysql?.name||"stacks",host:database.connections?.mysql?.host??"127.0.0.1",username:database.connections?.mysql?.username??"root",password:database.connections?.mysql?.password??"",port:database.connections?.mysql?.port??3306};if(driver==="singlestore")return{database:database.connections?.singlestore?.name||"stacks",host:database.connections?.singlestore?.host??"127.0.0.1",username:database.connections?.singlestore?.username??"root",password:database.connections?.singlestore?.password??"",port:database.connections?.singlestore?.port??3306};if(driver==="vitess")return{database:database.connections?.vitess?.name||"stacks",host:database.connections?.vitess?.host??"127.0.0.1",username:database.connections?.vitess?.username??"root",password:database.connections?.vitess?.password??"",port:database.connections?.vitess?.port??15306};if(driver==="postgres"){const dbName=database.connections?.postgres?.name??"stacks";return{database:env==="testing"?`${dbName}_testing`:dbName,host:database.connections?.postgres?.host??"127.0.0.1",username:database.connections?.postgres?.username??"",password:database.connections?.postgres?.password??"",port:database.connections?.postgres?.port??5432}}return{database:":memory:"}}export const QB_SNAPSHOT_DIR="storage/framework/database";export function qbSnapshotDir(){return snapshotDirForQueryBuilder()}export const RAW_QUERY_SOFT_DELETE_CONFIG={enabled:!1,column:"deleted_at",defaultFilter:!0};export function createDatabaseQueryHooks(dispatch){function forward(event){try{Promise.resolve(dispatch(event)).catch(()=>{})}catch{}}return{onQueryEnd:(event)=>forward({query:{sql:event.sql,parameters:event.params},queryDurationMillis:event.durationMs}),onQueryError:(event)=>forward({query:{sql:event.sql,parameters:event.params},queryDurationMillis:event.durationMs,error:event.error})}}let queryLoggerModule;function forwardDatabaseQuery(event){transactionConnection.exit(()=>{queryLoggerModule??=import("./query-logger").catch((error)=>{queryLoggerModule=void 0;throw error});queryLoggerModule.then(({logQuery})=>logQuery(event)).catch(()=>{})})}let unregisterDatabaseQueryHooks;function syncDatabaseQueryHooks(){const shouldInstall=!isProductionEnvironment(appEnv)||queryLoggingEnabled;if(shouldInstall&&!unregisterDatabaseQueryHooks)unregisterDatabaseQueryHooks=registerPersistentQueryHooks(createDatabaseQueryHooks(forwardDatabaseQuery));else if(!shouldInstall&&unregisterDatabaseQueryHooks){unregisterDatabaseQueryHooks();unregisterDatabaseQueryHooks=void 0}}syncDatabaseQueryHooks();function primaryPoolIdentity(){const dialect=String(queryBuilderConfig.dialect),configured=queryBuilderConfig.database??{},env=process.env.DB_CONNECTION===dialect?process.env:{};return{driver:dialect,host:env.DB_HOST||configured.host||"localhost",port:Number(env.DB_PORT||configured.port)||void 0,database:String(env.DB_DATABASE||configured.database||"").replace(/^['"]|['"]$/g,"")}}export function recordPrimaryDatabaseQueryError(error){recordDatabaseQueryError(error,primaryPoolIdentity())}let unregisterBrokenPoolDetection;function detectsBrokenPools(){return isPostgresWire(getDriver())||isMysqlWire(getDriver())}function syncBrokenPoolDetection(){const shouldInstall=detectsBrokenPools();if(shouldInstall&&!unregisterBrokenPoolDetection)unregisterBrokenPoolDetection=registerPersistentQueryHooks({onQueryError:(event)=>recordPrimaryDatabaseQueryError(event.error)});else if(!shouldInstall&&unregisterBrokenPoolDetection){unregisterBrokenPoolDetection();unregisterBrokenPoolDetection=void 0}}syncBrokenPoolDetection();function replicaQueryHooks(pool){return{onQueryError:(event)=>{try{recordDatabaseQueryError(event.error,pool);queryBuilderConfig.hooks?.onQueryError?.(event)}catch{}}}}function getPoolConfig(){const driver=getDriver();if(driver==="sqlite")return;return getDatabaseConfig().connections?.[driver]?.pool}const EMPTY_REPLICAS=[];function getReplicas(){const driver=getDriver();if(driver==="sqlite")return EMPTY_REPLICAS;return getDatabaseConfig().connections?.[driver]?.replicas??EMPTY_REPLICAS}function getReadPolicy(){return getDatabaseConfig().reads??{}}export{runInDatabaseRoutingContext,withDatabaseRoutingContext};function toBunPoolOptions(pool){if(!pool)return{};const options={};if(pool.max!==void 0)options.max=pool.max;if(pool.idleTimeoutMs!==void 0)options.idleTimeout=Math.round(pool.idleTimeoutMs/1000);if(pool.acquireTimeoutMs!==void 0)options.connectionTimeout=Math.round(pool.acquireTimeoutMs/1000);if(pool.maxLifetimeMs!==void 0)options.maxLifetime=Math.round(pool.maxLifetimeMs/1000);return options}function updateQueryBuilderConfig(){const dialect=getDialect(),dbConfigForQb=getDbConfig(),pool=getPoolConfig();setConfig({dialect,vitess:{sharded:isVitessSharded(dbConfig.connections.vitess?.sharded)},database:pool?{...dbConfigForQb,pool}:dbConfigForQb,verbose:getEnv()!=="production",snapshotDir:qbSnapshotDir(),migrationDir:relativeMigrationDirectory(resolveMigrationDirectory(toQueryBuilderDialect(dialect),{snapshotDir:qbSnapshotDir()})),timestamps:{createdAt:"created_at",updatedAt:"updated_at",defaultOrderColumn:"created_at"},softDeletes:RAW_QUERY_SOFT_DELETE_CONFIG})}updateQueryBuilderConfig();let _dbInstance=null,_configInitPromise=null;function ensureConfigLoaded(){if(!_configInitPromise)_configInitPromise=(async()=>{try{const{config,overridesReady}=await import("@stacksjs/config/runtime");await overridesReady;if(config){initializeDbConfig(config);_dbInstance=null}}catch{}})();return _configInitPromise}export async function ensureDatabaseConfigLoaded(){await ensureConfigLoaded()}export{applySqlitePragmas,SQLITE_BOOTSTRAP_PRAGMAS}from"@stacksjs/query-builder";const sqliteTxOwner=new AsyncLocalStorage;let sqliteTxTail=Promise.resolve();function runOwnedSqliteTransaction(run,parent){const owner={parent,accepting:!0,transactions:0,completionTail:Promise.resolve()};return sqliteTxOwner.run(owner,async()=>{try{return await run()}finally{owner.accepting=!1;await owner.completionTail}})}function serializeSqliteTransaction(run){let owner=sqliteTxOwner.getStore();while(owner&&!owner.accepting)owner=owner.parent;if(owner?.accepting&&owner.transactions>0)return sqliteTxOwner.run(owner,run);const completionOwner=owner?.accepting?owner:void 0,result=(completionOwner?.completionTail??sqliteTxTail).then(()=>runOwnedSqliteTransaction(run,completionOwner)),tail=result.then(()=>{return},()=>{return});if(completionOwner)completionOwner.completionTail=tail;else sqliteTxTail=tail;return result}function applySqliteTransactionSerialization(instance){const original=instance.transaction.bind(instance);instance.transaction=(...args)=>serializeSqliteTransaction(()=>original(...args))}const dispatchScopedBuilders=new WeakSet,transactionConnection=new AsyncLocalStorage;function activeTransactionConnection(){const scope=transactionConnection.getStore();if(!scope)return;assertTransactionConnection(scope);return scope.connection}function assertTransactionConnection(scope){for(let owner=scope;owner;owner=owner.parent)if(!owner.active)throw Error("Database work cannot continue from a closed transaction callback.");if(scope.nested)throw Error("Await the active nested transaction before issuing more database work.")}function guardTransactionQuery(value){const scope=transactionConnection.getStore();if(!scope||!value||typeof value!=="object"||typeof value.execute!=="function")return value;const wrappers=new WeakMap,wrap=(target)=>{const cached=wrappers.get(target);if(cached)return cached;const proxy=new Proxy(target,{get(object,property){const member=Reflect.get(object,property,object);if(typeof member!=="function")return member;return(...args)=>{assertTransactionConnection(scope);if(transactionConnection.getStore()!==scope)throw Error("A transaction query must execute in the callback that created it.");const guardedArgs=args.map((arg)=>typeof arg==="function"?function(...values){return Reflect.apply(arg,this,values.map((value)=>value&&typeof value==="object"&&typeof Reflect.get(value,"execute")==="function"?wrap(value):value))}:arg),result=Reflect.apply(member,object,guardedArgs);if(result===object)return proxy;if(result&&typeof result==="object"&&!isPromise(result)&&(typeof Reflect.get(result,"execute")==="function"||typeof Reflect.get(result,"next")==="function"))return wrap(result);return result}}});wrappers.set(target,proxy);return proxy};return wrap(value)}function applyTransactionDispatchScope(instance){if(dispatchScopedBuilders.has(instance))return;dispatchScopedBuilders.add(instance);let defaultOptions={};const setDefaults=instance.setTransactionDefaults.bind(instance);instance.setTransactionDefaults=(defaults)=>{setDefaults(defaults);defaultOptions={...defaultOptions,...defaults}};for(const method of["transaction","savepoint"]){const original=instance[method].bind(instance);instance[method]=async(callback,options)=>{const active=activeTransactionConnection();if(active&&active!==instance)return active[method](callback,options);const parent=transactionConnection.getStore(),effectiveOptions={...defaultOptions,...options},afterCommit=method==="transaction"?effectiveOptions.afterCommit:void 0,parentObserver=(listener)=>listener?(...args)=>{const nested=parent?.nested;if(parent)parent.nested=!1;try{listener(...args)}finally{if(parent)parent.nested=nested}}:void 0,result=await runInTransactionScope(async(runAttempt)=>{const owner=sqliteTxOwner.getStore();if(owner)owner.transactions++;if(parent)parent.nested=!0;try{const runCallback=AsyncLocalStorage.bind((tx)=>runAttempt(async()=>{applyTransactionDispatchScope(tx);const scope={connection:tx,parent,active:!0,nested:!1};return transactionConnection.run(scope,async()=>{try{return await callback(tx)}finally{scope.active=!1}})}));return await original(runCallback,method==="transaction"?{...options,afterCommit:void 0,onRollback:parentObserver(effectiveOptions.onRollback),afterRollback:parentObserver(effectiveOptions.afterRollback),onRetry:parentObserver(effectiveOptions.onRetry)}:options)}finally{if(parent)parent.nested=!1;if(owner)owner.transactions--}});await afterCommit?.();return result}}}function applyTransactionRoutingContext(instance){const original=instance.transaction.bind(instance);instance.transaction=(...args)=>withTransactionContext(()=>original(...args))}function getDb(){if(_databaseClosePromise)throw Error("Database connections are shutting down");const active=activeTransactionConnection();if(active)return active;if(!_dbInstance){updateQueryBuilderConfig();_dbInstance=createQueryBuilder();applyTransactionDispatchScope(_dbInstance);if(getDialect()==="sqlite")applySqliteTransactionSerialization(_dbInstance);applyTransactionRoutingContext(_dbInstance)}return _dbInstance}let _replicaInstances=new Map,_databaseClosePromise=null;const _retiredConnectionClosures=new Set;export function resetDatabaseConnection(){const connections=[..._dbInstance?[_dbInstance]:[],..._replicaInstances.values()];if(connections.length>0)retireDatabaseConnections(_retiredConnectionClosures,connections,(error)=>console.error(`[database] Failed to close retired connection: ${error.message}`));resetQueryBuilderConnection();lastGeneralSqliteStatement=void 0;_dbInstance=null;_replicaInstances=new Map}export function closeDatabaseConnection(){if(_databaseClosePromise)return _databaseClosePromise;const connections=[..._dbInstance?[_dbInstance]:[],..._replicaInstances.values()];_dbInstance=null;_replicaInstances=new Map;lastGeneralSqliteStatement=void 0;const closing=(async()=>{try{await closeDatabaseConnectionsAndPending(connections,_retiredConnectionClosures)}finally{resetQueryBuilderConnection()}})();_databaseClosePromise=closing;closing.then(()=>{if(_databaseClosePromise===closing)_databaseClosePromise=null},()=>{if(_databaseClosePromise===closing)_databaseClosePromise=null});return closing}function getReplicaDb(replica){const primary=getDbConfig(),resolved=resolveReplicaConnection(replica,primary),key=replicaConnectionKey(resolved),cached=_replicaInstances.get(key);if(cached)return cached;const scheme=isMysqlWire(getDriver())?"mysql":"postgres",auth=resolved.username?`${encodeURIComponent(resolved.username)}:${encodeURIComponent(resolved.password??"")}@`:"",url=`${scheme}://${auth}${resolved.host}:${resolved.port}/${resolved.database}`,sql=new SQL({url,...toBunPoolOptions(getPoolConfig())}),instance=detectsBrokenPools()?createQueryBuilder({sql,hooks:replicaQueryHooks({driver:getDriver(),host:resolved.host,port:resolved.port,database:resolved.database})}):createQueryBuilder({sql});_replicaInstances.set(key,instance);return instance}function getReadDb(){const active=activeTransactionConnection();if(active)return active;const replicas=getReplicas();if(replicas.length===0)return getDb();const policy=getReadPolicy();if(!shouldRouteToReplica({policy,replicas}))return getDb();const replica=selectReplica(replicas,policy.strategy);return replica?getReplicaDb(replica):getDb()}function getExplicitReadDb(){const active=activeTransactionConnection();if(active)return active;const replicas=getReplicas();if(!replicas.length||contextInTransaction())return getDb();const replica=selectReplica(replicas,getReadPolicy().strategy);return replica?getReplicaDb(replica):getDb()}const WRITE_ENTRY_POINTS=new Set(["insertInto","updateTable","deleteFrom","create","createMany","insertOrIgnore","insertGetId","updateOrInsert","upsert"]),READ_ENTRY_POINTS=new Set(["selectFrom","selectFromSub","select"]);ensureConfigLoaded();export function asRows(rows){return rows}export function asRow(row){return row}const SIMPLE_SQLITE_TABLE=/^[A-Z_][A-Z0-9_]*$/i,SIMPLE_SQLITE_COLUMN=/^[A-Z_][A-Z0-9_]*(?:\.[A-Z_][A-Z0-9_]*)?$/i,SIMPLE_SQLITE_SELECTION=/^[A-Z_][A-Z0-9_]*(?:\.[A-Z_][A-Z0-9_]*)?(?:\s+AS\s+[A-Z_][A-Z0-9_]*)?$/i,SIMPLE_SQLITE_OPERATORS=new Set(["=","!=","<>","<","<=",">",">=","like","not like"]),SQLITE_IDENTIFIER_CACHE_LIMIT=512;function memoizeSqliteIdentifier(pattern){const valid=new Set;return(value)=>{if(valid.has(value))return!0;if(!pattern.test(value))return!1;if(valid.size<SQLITE_IDENTIFIER_CACHE_LIMIT)valid.add(value);return!0}}const isSimpleSqliteTable=memoizeSqliteIdentifier(SIMPLE_SQLITE_TABLE),isSimpleSqliteColumn=memoizeSqliteIdentifier(SIMPLE_SQLITE_COLUMN),isSimpleSqliteSelection=memoizeSqliteIdentifier(SIMPLE_SQLITE_SELECTION);let lastParameterizedSqliteSelect,lastUnparameterizedSqliteSelect,lastSqliteSelection;const GUARDED_SQLITE_SELECT_METHODS=new Set(["select","where","limit","execute","executeSync","executeTakeFirstSync"]);function hasActiveQueryBuilderHooks(){return Boolean(queryBuilderConfig.hooks&&Object.values(queryBuilderConfig.hooks).some((value)=>value!==void 0))}function resolveDeferredSqliteTerminal(target,property,executeStatement,executeFirstStatement,materialize,proxy){if(property==="get"){target.get=target.execute;return target.execute}if(property==="selectAll"){const selectAll=()=>proxy;target.selectAll=selectAll;return selectAll}if(property==="first"){const first=async()=>executeFirstStatement();target.first=first;return first}if(property==="firstOrFail"||property==="executeTakeFirstOrThrow"){const firstOrFail=async()=>{const row=executeFirstStatement();if(row===void 0)throw Error("Record not found");return row};target.firstOrFail=firstOrFail;target.executeTakeFirstOrThrow=firstOrFail;return firstOrFail}if(property==="exists"||property==="doesntExist"){const findsRow=property==="exists",check=async()=>executeFirstStatement()!==void 0===findsRow;target[property]=check;return check}if(property==="value"){const value=async(column)=>executeFirstStatement()?.[column];target.value=value;return value}if(property==="count"||property==="sum"||property==="avg"||property==="min"||property==="max"){const emptyValue=property==="min"||property==="max"?null:0,aggregate=(...args)=>runDeferredSqliteAggregate(property,args,emptyValue,materialize,executeStatement);target[property]=aggregate;return aggregate}if(property==="pluck"){const pluck=(...args)=>{if(args.length!==1){const builder=materialize();return builder.pluck.call(builder,...args)}try{const column=args[0],rows=executeStatement(),values=Array(rows.length);for(let index=0;index<rows.length;index++)values[index]=rows[index]?.[column];return Promise.resolve(values)}catch(error){return Promise.reject(error)}};target.pluck=pluck;return pluck}}function runDeferredSqliteAggregate(name,args,emptyValue,materialize,executeStatement){const column=args[0],acceptsNoColumn=name==="count"&&args.length===0;if(!acceptsNoColumn&&(args.length!==1||typeof column!=="string"||!isSimpleSqliteColumn(column))){const builder=materialize();return builder[name].call(builder,...args)}try{const expression=`${name.toUpperCase()}(${acceptsNoColumn?"*":column}) AS aggregate`,value=executeStatement(!1,expression)[0]?.aggregate??emptyValue;return Promise.resolve(name==="count"||name==="sum"||name==="avg"?Number(value):value)}catch(error){return Promise.reject(error)}}const fastSqliteDatabaseCache=new WeakMap;let lastGeneralSqliteStatement;function fastSqliteDatabase(instance){if(fastSqliteDatabaseCache.has(instance))return fastSqliteDatabaseCache.get(instance)??void 0;const database=instance.sql?._wrapper?.database,resolved=database&&typeof database.query==="function"?database:null;fastSqliteDatabaseCache.set(instance,resolved);return resolved??void 0}function runFastSqliteSql(instance,sqliteDatabase,sql,params){if(sqliteDatabase){let cached=lastGeneralSqliteStatement;if(!cached||cached.database!==sqliteDatabase||cached.sql!==sql){cached={database:sqliteDatabase,sql,statement:sqliteDatabase.query(sql)};lastGeneralSqliteStatement=cached}return params?cached.statement.all(...params):cached.statement.all()}return instance.unsafe(sql,params).executeSync()}let lastSqlitePlaceholders;function renderSqlitePlaceholders(length){const cached=lastSqlitePlaceholders;if(cached&&cached.length===length)return cached.sql;const sql=Array(length).fill("?").join(", ");if(typeof length==="number")lastSqlitePlaceholders={length,sql};return sql}function snapshotSimpleSqliteMembershipValues(values){if(isProxy(values)||Object.getPrototypeOf(values)!==Array.prototype||Object.hasOwn(values,Symbol.iterator))return;const snapshot=Array(values.length);for(let index=0;index<values.length;index++){const descriptor=Object.getOwnPropertyDescriptor(values,index);if(!descriptor||!("value"in descriptor))return;snapshot[index]=descriptor.value}return snapshot}function createDeferredSqliteSelect(instance,table){const sqliteDatabase=fastSqliteDatabase(instance);let selectKeyword="SELECT",columns,selectedColumnsSql,predicateColumn,predicateOperator,predicateValue,predicateParameterized=!0,predicateValues,additionalPredicates,orderings,rowLimit,rowOffset,materialized;const materialize=()=>{if(materialized)return materialized;let builder=instance.selectFrom(table);if(columns)builder=builder.select.call(builder,columns);if(selectKeyword==="SELECT DISTINCT")builder=builder.distinct.call(builder);if(predicateColumn!==void 0){if(predicateValues)if(predicateOperator==="LIKE LOWER"||predicateOperator==="NOT LIKE LOWER")builder=builder[predicateOperator==="LIKE LOWER"?"whereILike":"whereNotILike"].call(builder,predicateColumn.slice(6,-1),predicateValues[0]);else builder=builder[predicateOperator==="IN"?"whereIn":"whereNotIn"].call(builder,predicateColumn,predicateValues);else if(predicateParameterized)builder=builder.where.call(builder,predicateColumn,predicateOperator,predicateValue);else builder=builder[predicateOperator==="IS NULL"?"whereNull":"whereNotNull"].call(builder,predicateColumn);if(additionalPredicates)for(const predicate of additionalPredicates)if(predicate.values)if(predicate.operator==="LIKE LOWER"||predicate.operator==="NOT LIKE LOWER"){const method=predicate.operator==="LIKE LOWER"?"whereILike":"whereNotILike";builder=builder[method].call(builder,predicate.column.slice(6,-1),predicate.values[0])}else{const method=predicate.operator==="IN"?"whereIn":"whereNotIn";builder=builder[method].call(builder,predicate.column,predicate.values)}else if(predicate.parameterized)builder=builder.where.call(builder,predicate.column,predicate.operator,predicate.value);else{const method=predicate.operator==="IS NULL"?"whereNull":"whereNotNull";builder=builder[method].call(builder,predicate.column)}}if(orderings){const apply=builder.orderBy;for(const ordering of orderings)builder=apply.call(builder,ordering.column,ordering.direction)}if(rowLimit!==void 0)builder=builder.limit.call(builder,rowLimit);if(rowOffset!==void 0)builder=builder.offset.call(builder,rowOffset);materialized=builder;return builder};let proxy;const executeStatement=(firstOnly=!1,selection)=>{const selected=selection??selectedColumnsSql??"*",effectiveLimit=rowLimit??(firstOnly&&rowOffset===void 0?1:void 0);if(predicateColumn===void 0&&orderings===void 0&&rowOffset===void 0){const cached=lastUnparameterizedSqliteSelect;if(cached&&cached.instance===instance&&cached.selectKeyword===selectKeyword&&cached.selected===selected&&cached.table===table&&cached.limit===effectiveLimit){if(sqliteDatabase){const statement=cached.statement??=sqliteDatabase.query(cached.sql);if(effectiveLimit===1){const row=statement.get();return row===null?[]:[row]}return statement.all()}return runFastSqliteSql(instance,sqliteDatabase,cached.sql)}const limit=effectiveLimit===void 0?"":` LIMIT ${effectiveLimit}`,sql=`${selectKeyword} ${selected} FROM ${table}${limit}`;lastUnparameterizedSqliteSelect={instance,selectKeyword,selected,table,limit:effectiveLimit,sql,statement:sqliteDatabase?.query(sql)};if(lastUnparameterizedSqliteSelect.statement){if(effectiveLimit===1){const row=lastUnparameterizedSqliteSelect.statement.get();return row===null?[]:[row]}return lastUnparameterizedSqliteSelect.statement.all()}return runFastSqliteSql(instance,sqliteDatabase,sql)}if(predicateColumn!==void 0&&predicateValues===void 0&&predicateParameterized&&additionalPredicates===void 0&&orderings===void 0&&rowOffset===void 0){const cached=lastParameterizedSqliteSelect;if(cached&&cached.instance===instance&&cached.selectKeyword===selectKeyword&&cached.selected===selected&&cached.table===table&&cached.predicateColumn===predicateColumn&&cached.predicateOperator===predicateOperator&&cached.limit===effectiveLimit){if(sqliteDatabase){const statement=cached.statement??=sqliteDatabase.query(cached.sql);if(effectiveLimit===1){const row=statement.get(predicateValue);return row===null?[]:[row]}return statement.all(predicateValue)}return runFastSqliteSql(instance,sqliteDatabase,cached.sql,[predicateValue])}const sql=`${selectKeyword} ${selected} FROM ${table} WHERE ${predicateColumn} ${predicateOperator} ?${effectiveLimit===void 0?"":` LIMIT ${effectiveLimit}`}`;lastParameterizedSqliteSelect={instance,selectKeyword,selected,table,predicateColumn,predicateOperator,limit:effectiveLimit,sql,statement:sqliteDatabase?.query(sql)};if(lastParameterizedSqliteSelect.statement){if(effectiveLimit===1){const row=lastParameterizedSqliteSelect.statement.get(predicateValue);return row===null?[]:[row]}return lastParameterizedSqliteSelect.statement.all(predicateValue)}return runFastSqliteSql(instance,sqliteDatabase,sql,[predicateValue])}let query=`${selectKeyword} ${selected} FROM ${table}`;const params=[];if(predicateColumn!==void 0){query+=` WHERE ${predicateColumn} ${predicateOperator}`;if(predicateValues){query+=` (${renderSqlitePlaceholders(predicateValues.length)})`;params.push(...predicateValues)}else if(predicateParameterized){query+=" ?";params.push(predicateValue)}if(additionalPredicates){const additionalCount=additionalPredicates.length;query+=" AND ";for(let index=0;index<additionalCount;index++){if(index>0)query+=" AND ";const predicate=additionalPredicates[index];if(predicate.values){params.push(...predicate.values);query+=`${predicate.column} ${predicate.operator} (${renderSqlitePlaceholders(predicate.values.length)})`;continue}if(predicate.parameterized){params.push(predicate.value);query+=`${predicate.column} ${predicate.operator} ?`;continue}query+=`${predicate.column} ${predicate.operator}`}}}if(orderings)query+=` ORDER BY ${orderings.map((ordering)=>`${ordering.column} ${ordering.direction.toUpperCase()}`).join(", ")}`;if(effectiveLimit!==void 0)query+=` LIMIT ${effectiveLimit}`;if(rowOffset!==void 0)query+=` OFFSET ${rowOffset}`;return runFastSqliteSql(instance,sqliteDatabase,query,params)},executeFirstStatement=()=>{if(rowLimit===0)return;const selected=selectedColumnsSql??"*";if(predicateColumn===void 0&&orderings===void 0&&rowOffset===void 0){const cached=lastUnparameterizedSqliteSelect;if(cached&&cached.instance===instance&&cached.selectKeyword===selectKeyword&&cached.selected===selected&&cached.table===table&&cached.limit===1){if(sqliteDatabase)return(cached.statement??=sqliteDatabase.query(cached.sql)).get()??void 0;return runFastSqliteSql(instance,sqliteDatabase,cached.sql)[0]}const sql=`${selectKeyword} ${selected} FROM ${table} LIMIT 1`,statement=sqliteDatabase?.query(sql);lastUnparameterizedSqliteSelect={instance,selectKeyword,selected,table,limit:1,sql,statement};return statement?statement.get()??void 0:runFastSqliteSql(instance,sqliteDatabase,sql)[0]}if(predicateColumn!==void 0&&predicateValues===void 0&&predicateParameterized&&additionalPredicates===void 0&&orderings===void 0&&rowOffset===void 0){const cached=lastParameterizedSqliteSelect;if(cached&&cached.instance===instance&&cached.selectKeyword===selectKeyword&&cached.selected===selected&&cached.table===table&&cached.predicateColumn===predicateColumn&&cached.predicateOperator===predicateOperator&&cached.limit===1){if(sqliteDatabase)return(cached.statement??=sqliteDatabase.query(cached.sql)).get(predicateValue)??void 0;return runFastSqliteSql(instance,sqliteDatabase,cached.sql,[predicateValue])[0]}const sql=`${selectKeyword} ${selected} FROM ${table} WHERE ${predicateColumn} ${predicateOperator} ? LIMIT 1`,statement=sqliteDatabase?.query(sql);lastParameterizedSqliteSelect={instance,selectKeyword,selected,table,predicateColumn,predicateOperator,limit:1,sql,statement};return statement?statement.get(predicateValue)??void 0:runFastSqliteSql(instance,sqliteDatabase,sql,[predicateValue])[0]}return executeStatement(!0)[0]},createExtensions=()=>({distinct(){if(selectKeyword==="SELECT DISTINCT")return materialize().distinct();selectKeyword="SELECT DISTINCT";return proxy},where(column,operator,value){if(column!==null&&typeof column==="object"&&!Array.isArray(column)&&operator===void 0&&value===void 0){const prototype=Object.getPrototypeOf(column),entries=prototype===Object.prototype||prototype===null?Object.entries(column):[];if(entries.length>0&&entries.every(([key])=>isSimpleSqliteColumn(key))){for(const[key,entryValue]of entries)if(predicateColumn===void 0){predicateColumn=key;predicateOperator="=";predicateValue=entryValue;predicateParameterized=!0;predicateValues=void 0}else(additionalPredicates??=[]).push({column:key,operator:"=",value:entryValue,parameterized:!0});return proxy}}const builder=materialize();return builder.where.call(builder,column,operator,value)},whereNull(column){if(typeof column!=="string"||!isSimpleSqliteColumn(column)){const builder=materialize();return builder.whereNull.call(builder,column)}if(predicateColumn===void 0){predicateColumn=column;predicateOperator="IS NULL";predicateValue=void 0;predicateParameterized=!1;predicateValues=void 0}else(additionalPredicates??=[]).push({column,operator:"IS NULL",value:void 0,parameterized:!1});return proxy},whereNotNull(column){if(typeof column!=="string"||!isSimpleSqliteColumn(column)){const builder=materialize();return builder.whereNotNull.call(builder,column)}if(predicateColumn===void 0){predicateColumn=column;predicateOperator="IS NOT NULL";predicateValue=void 0;predicateParameterized=!1;predicateValues=void 0}else(additionalPredicates??=[]).push({column,operator:"IS NOT NULL",value:void 0,parameterized:!1});return proxy},whereLike(column,value,caseSensitive=!1){if(typeof column!=="string"||!isSimpleSqliteColumn(column)){const builder=materialize();return builder.whereLike.call(builder,column,value,caseSensitive)}if(!caseSensitive)return proxy.whereILike(column,value);if(predicateColumn===void 0){predicateColumn=column;predicateOperator="LIKE";predicateValue=value;predicateParameterized=!0;predicateValues=void 0}else(additionalPredicates??=[]).push({column,operator:"LIKE",value,parameterized:!0});return proxy},whereNotLike(column,value,caseSensitive=!1){if(typeof column!=="string"||!isSimpleSqliteColumn(column)){const builder=materialize();return builder.whereNotLike.call(builder,column,value,caseSensitive)}if(!caseSensitive)return proxy.whereNotILike(column,value);if(predicateColumn===void 0){predicateColumn=column;predicateOperator="NOT LIKE";predicateValue=value;predicateParameterized=!0;predicateValues=void 0}else(additionalPredicates??=[]).push({column,operator:"NOT LIKE",value,parameterized:!0});return proxy},whereILike(column,value){if(typeof column!=="string"||!isSimpleSqliteColumn(column)){const builder=materialize();return builder.whereILike.call(builder,column,value)}const sqlColumn=`LOWER(${column})`;if(predicateColumn===void 0){predicateColumn=sqlColumn;predicateOperator="LIKE LOWER";predicateValue=void 0;predicateParameterized=!1;predicateValues=[value]}else(additionalPredicates??=[]).push({column:sqlColumn,operator:"LIKE LOWER",value:void 0,parameterized:!1,values:[value]});return proxy},whereNotILike(column,value){if(typeof column!=="string"||!isSimpleSqliteColumn(column)){const builder=materialize();return builder.whereNotILike.call(builder,column,value)}const sqlColumn=`LOWER(${column})`;if(predicateColumn===void 0){predicateColumn=sqlColumn;predicateOperator="NOT LIKE LOWER";predicateValue=void 0;predicateParameterized=!1;predicateValues=[value]}else(additionalPredicates??=[]).push({column:sqlColumn,operator:"NOT LIKE LOWER",value:void 0,parameterized:!1,values:[value]});return proxy},whereIn(column,values){if(typeof column!=="string"||!isSimpleSqliteColumn(column)||!Array.isArray(values)){const builder=materialize();return builder.whereIn.call(builder,column,values)}const snapshot=values.slice();if(predicateColumn===void 0){predicateColumn=column;predicateOperator="IN";predicateValue=void 0;predicateParameterized=!1;predicateValues=snapshot}else(additionalPredicates??=[]).push({column,operator:"IN",value:void 0,parameterized:!1,values:snapshot});return proxy},whereNotIn(column,values){if(typeof column!=="string"||!isSimpleSqliteColumn(column)||!Array.isArray(values)){const builder=materialize();return builder.whereNotIn.call(builder,column,values)}const snapshot=values.slice();if(predicateColumn===void 0){predicateColumn=column;predicateOperator="NOT IN";predicateValue=void 0;predicateParameterized=!1;predicateValues=snapshot}else(additionalPredicates??=[]).push({column,operator:"NOT IN",value:void 0,parameterized:!1,values:snapshot});return proxy},whereBetween(...args){const[column,startOrValues,end]=args,values=Array.isArray(startOrValues)?startOrValues:args.length>=3?[startOrValues,end]:void 0;if(typeof column!=="string"||!isSimpleSqliteColumn(column)||!values||values.length<2){const builder=materialize();return builder.whereBetween.call(builder,...args)}const lower=values[0],upper=values[1];if(predicateColumn===void 0){predicateColumn=column;predicateOperator=">=";predicateValue=lower;predicateParameterized=!0;predicateValues=void 0}else(additionalPredicates??=[]).push({column,operator:">=",value:lower,parameterized:!0});(additionalPredicates??=[]).push({column,operator:"<=",value:upper,parameterized:!0});return proxy},whereDate(...args){const[column,operator,date]=args;if(typeof column!=="string"||!isSimpleSqliteColumn(column)||typeof operator!=="string"||!SIMPLE_SQLITE_OPERATORS.has(operator.toLowerCase())||typeof date!=="string"&&!(date instanceof Date)){const builder=materialize();return builder.whereDate.call(builder,...args)}const normalizedDate=date instanceof Date?date.toISOString():date;if(predicateColumn===void 0){predicateColumn=column;predicateOperator=operator;predicateValue=normalizedDate;predicateParameterized=!0;predicateValues=void 0}else(additionalPredicates??=[]).push({column,operator,value:normalizedDate,parameterized:!0});return proxy},orderBy(column,direction="asc"){if(typeof column!=="string"||!isSimpleSqliteColumn(column)){const builder=materialize();return builder.orderBy.call(builder,column,direction)}(orderings??=[]).push({column,direction:direction==="asc"?"asc":"desc"});return proxy},orderByDesc(column){if(typeof column!=="string"||!isSimpleSqliteColumn(column)){const builder=materialize();return builder.orderByDesc.call(builder,column)}(orderings??=[]).push({column,direction:"desc"});return proxy},latest(column){const resolvedColumn=column??queryBuilderConfig.timestamps.defaultOrderColumn;if(typeof resolvedColumn!=="string"||!isSimpleSqliteColumn(resolvedColumn)){const builder=materialize();return builder.latest.call(builder,column)}(orderings??=[]).push({column:resolvedColumn,direction:"desc"});return proxy},oldest(column){const resolvedColumn=column??queryBuilderConfig.timestamps.defaultOrderColumn;if(typeof resolvedColumn!=="string"||!isSimpleSqliteColumn(resolvedColumn)){const builder=materialize();return builder.oldest.call(builder,column)}(orderings??=[]).push({column:resolvedColumn,direction:"asc"});return proxy},offset(value){if(typeof value!=="number"||value<0||!Number.isInteger(value)){const builder=materialize();return builder.offset.call(builder,value)}rowOffset=value;return proxy}});let extensions;const base={select(value){if(materialized)return Reflect.apply(materialized.select,materialized,arguments);if(typeof value==="string"){if(value!=="*"&&!isSimpleSqliteSelection(value)){const builder=materialize();return builder.select.call(builder,value)}columns=value;selectedColumnsSql=value;return proxy}const selected=Array.isArray(value)?value:[value];let simple=!1,selection;const cached=lastSqliteSelection;if(cached&&cached.columns.length===selected.length){simple=!0;for(let index=0;simple&&index<selected.length;index++)simple=cached.columns[index]===selected[index];if(simple)selection=cached.sql}if(!simple){simple=selected.length>0;selection="";for(let index=0;simple&&index<selected.length;index++){const column=selected[index];simple=typeof column==="string"&&(column==="*"||isSimpleSqliteSelection(column));if(simple)selection+=index===0?column:`, ${column}`}if(simple)lastSqliteSelection={columns:selected.slice(),sql:selection}}if(!simple){const builder=materialize();return builder.select.call(builder,value)}columns=selected;selectedColumnsSql=selection;return proxy},where(column,operator,value){if(materialized)return Reflect.apply(materialized.where,materialized,arguments);if(typeof column!=="string"||!isSimpleSqliteColumn(column)||typeof operator!=="string"||operator!=="="&&!SIMPLE_SQLITE_OPERATORS.has(operator)&&!SIMPLE_SQLITE_OPERATORS.has(operator.toLowerCase())){if(typeof column==="string"&&isSimpleSqliteColumn(column)&&typeof operator==="string"){const normalized=operator.toLowerCase();if(normalized==="in"||normalized==="not in"){const values=Array.isArray(value)?snapshotSimpleSqliteMembershipValues(value):[value];if(values!==void 0){const membershipOperator=normalized==="in"?"IN":"NOT IN";if(predicateColumn===void 0){predicateColumn=column;predicateOperator=membershipOperator;predicateValue=void 0;predicateParameterized=!1;predicateValues=values}else(additionalPredicates??=[]).push({column,operator:membershipOperator,value:void 0,parameterized:!1,values});return proxy}}}return(extensions??=createExtensions()).where(column,operator,value)}if(predicateColumn===void 0){predicateColumn=column;predicateOperator=operator;predicateValue=value;predicateParameterized=!0;predicateValues=void 0}else(additionalPredicates??=[]).push({column,operator,value,parameterized:!0});return proxy},limit(value){if(materialized)return Reflect.apply(materialized.limit,materialized,arguments);if(typeof value!=="number"||value<0||!Number.isInteger(value)){const builder=materialize();return builder.limit.call(builder,value)}rowLimit=value;return proxy},execute(){if(materialized)return Reflect.apply(materialized.execute,materialized,arguments);try{return Promise.resolve(executeStatement())}catch(error){return Promise.reject(error)}},executeSync(){if(materialized)return Reflect.apply(materialized.executeSync,materialized,arguments);return executeStatement()},executeTakeFirstSync(){if(materialized)return Reflect.apply(materialized.executeTakeFirstSync,materialized,arguments);return executeFirstStatement()}};let forwardedMethods;proxy=new Proxy(base,{get(target,property){if(materialized){const builder=materialized,value=builder[property];return typeof value==="function"?value.bind(builder):value}if(property==="executeTakeFirst")return target.executeTakeFirst??=function(){if(materialized)return Reflect.apply(materialized.executeTakeFirst,materialized,arguments);try{return Promise.resolve(executeFirstStatement())}catch(error){return Promise.reject(error)}};if(GUARDED_SQLITE_SELECT_METHODS.has(property))return target[property];const cached=forwardedMethods?.[property];if(cached!==void 0)return cached;const value=target[property]??resolveDeferredSqliteTerminal(target,property,executeStatement,executeFirstStatement,materialize,proxy)??(extensions??=createExtensions())[property];if(value===void 0){const builder=materialize(),fallback=builder[property];return typeof fallback==="function"?fallback.bind(builder):fallback}if(typeof value!=="function")return value;const forward=(...args)=>{const builder=materialized;return builder?Reflect.apply(builder[property],builder,args):Reflect.apply(value,proxy,args)};(forwardedMethods??=Object.create(null))[property]=forward;return forward}});return proxy}function selectFromDatabase(table){const dialect=getDialect(),instance=dialect==="sqlite"?getDb():getReadDb();if(dialect==="sqlite"&&!queryBuilderConfig.softDeletes?.enabled&&!hasActiveQueryBuilderHooks()&&isSimpleSqliteTable(table))return guardTransactionQuery(createDeferredSqliteSelect(instance,table));return guardTransactionQuery(instance.selectFrom(table))}function selectFromExplicitReadDatabase(table){const dialect=getDialect(),instance=getExplicitReadDb();if(dialect==="sqlite"&&!queryBuilderConfig.softDeletes?.enabled&&!hasActiveQueryBuilderHooks()&&isSimpleSqliteTable(table))return guardTransactionQuery(createDeferredSqliteSelect(instance,table));return guardTransactionQuery(instance.selectFrom(table))}function unsafeDatabase(query,params){return guardTransactionQuery(getDb().unsafe(query,params))}function unsafeExplicitReadDatabase(query,params){return guardTransactionQuery(getExplicitReadDb().unsafe(query,params))}function insertIntoDatabase(table){markContextWrote();return guardTransactionQuery(getDb().insertInto(table))}function updateTableDatabase(table){markContextWrote();return guardTransactionQuery(getDb().updateTable(table))}function deleteFromDatabase(table){markContextWrote();return guardTransactionQuery(getDb().deleteFrom(table))}function tableDatabase(table){return guardTransactionQuery(getDb().table(table))}function selectDatabase(table,...columns){return guardTransactionQuery(getReadDb().select(table,...columns))}function selectFromSubDatabase(subquery,alias){return guardTransactionQuery(getReadDb().selectFromSub(subquery,alias))}function tableExplicitReadDatabase(table){return guardTransactionQuery(getExplicitReadDb().table(table))}function selectExplicitReadDatabase(table,...columns){return guardTransactionQuery(getExplicitReadDb().select(table,...columns))}function selectFromSubExplicitReadDatabase(subquery,alias){return guardTransactionQuery(getExplicitReadDb().selectFromSub(subquery,alias))}function transactionalDatabase(callback,options){return(...args)=>getDb().transaction((tx)=>callback(tx,...args),options)}const dbFallback=new Proxy({},{get(_target,prop){const resolveInstance=typeof prop==="string"&&READ_ENTRY_POINTS.has(prop)?getReadDb:getDb,value=resolveInstance()[prop];if(typeof value==="function")return forwardDatabaseMethod(prop,resolveInstance);return value}});function forwardDatabaseMethod(prop,resolveInstance){const scope=transactionConnection.getStore();return(...args)=>{if(scope){assertTransactionConnection(scope);if(transactionConnection.getStore()!==scope)throw Error("A transaction query must execute in the callback that created it.")}const instance=resolveInstance();if(typeof prop==="string"&&WRITE_ENTRY_POINTS.has(prop))markContextWrote();return guardTransactionQuery(Reflect.apply(Reflect.get(instance,prop),instance,args))}}export const db=Object.create(dbFallback);Object.defineProperties(db,{deleteFrom:{value:deleteFromDatabase},fn:{value:aggregateFunctions},insertInto:{value:insertIntoDatabase},primary:{get:()=>primaryDb},read:{get:()=>readDb},select:{value:selectDatabase},selectFrom:{value:selectFromDatabase},selectFromSub:{value:selectFromSubDatabase},table:{value:tableDatabase},transactional:{value:transactionalDatabase},unsafe:{value:unsafeDatabase},updateTable:{value:updateTableDatabase}});const readDbFallback=new Proxy({},{get(_target,prop){const value=getExplicitReadDb()[prop];if(typeof value==="function")return forwardDatabaseMethod(prop,getExplicitReadDb);return value}});export const readDb=Object.create(readDbFallback);Object.defineProperties(readDb,{fn:{value:aggregateFunctions},select:{value:selectExplicitReadDatabase},selectFrom:{value:selectFromExplicitReadDatabase},selectFromSub:{value:selectFromSubExplicitReadDatabase},table:{value:tableExplicitReadDatabase},transactional:{value:transactionalDatabase},unsafe:{value:unsafeExplicitReadDatabase}});const primaryDb=new Proxy({},{get(_target,prop){const instance=getDb(),value=Reflect.get(instance,prop);return typeof value==="function"?forwardDatabaseMethod(prop,getDb):value}});export{setConfig};
1
+ import{AsyncLocalStorage}from"node:async_hooks";import process from"node:process";import{isPromise,isProxy}from"node:util/types";import{config as queryBuilderConfig,createQueryBuilder,registerPersistentQueryHooks,resetConnection as resetQueryBuilderConnection,setConfig}from"@stacksjs/query-builder";import{SQL}from"bun";import{recordDatabaseQueryError}from"./broken-pool";import{closeDatabaseConnectionsAndPending,retireDatabaseConnections}from"./connection-lifecycle";import{runInTransactionScope}from"./transaction-context";import{env as envVars}from"@stacksjs/env/runtime";import{getConnectionDefaults}from"./defaults";import{isLibsqlDriver,isMysqlWire,isPostgresWire,isVitessSharded,normalizeDatabaseDriver,toQueryBuilderDialect}from"./dialect";import{relativeMigrationDirectory,resolveMigrationDirectory,snapshotDirForQueryBuilder}from"./migration-path";import{configureDatabaseRoutingContext,contextInTransaction,markContextWrote,replicaConnectionKey,resolveReplicaConnection,runInDatabaseRoutingContext,selectReplica,shouldRouteToReplica,withDatabaseRoutingContext,withTransactionContext}from"./replicas";import{aggregateFunctions}from"./types";const sqliteDefaults=getConnectionDefaults("sqlite",envVars),mysqlDefaults=getConnectionDefaults("mysql",envVars),postgresDefaults=getConnectionDefaults("postgres",envVars);let appEnv=envVars.APP_ENV||"local",dbDriver=normalizeDatabaseDriver(envVars.DB_CONNECTION||"sqlite"),queryBuilderDialect=toQueryBuilderDialect(dbDriver),queryLoggingEnabled=envVars.DB_QUERY_LOGGING_ENABLED??!isProductionEnvironment(appEnv),dbConfig={connections:{sqlite:{database:sqliteDefaults.database,prefix:""},mysql:{name:mysqlDefaults.database,host:mysqlDefaults.host,username:mysqlDefaults.username,password:mysqlDefaults.password,port:mysqlDefaults.port,prefix:""},singlestore:{name:mysqlDefaults.database,host:mysqlDefaults.host,username:mysqlDefaults.username,password:mysqlDefaults.password,port:mysqlDefaults.port,prefix:""},vitess:{name:mysqlDefaults.database,host:mysqlDefaults.host,username:mysqlDefaults.username,password:mysqlDefaults.password,port:15306,prefix:"",sharded:isVitessSharded()},postgres:{name:postgresDefaults.database,host:postgresDefaults.host,username:postgresDefaults.username,password:postgresDefaults.password,port:postgresDefaults.port,prefix:""},turso:{url:envVars.TURSO_DATABASE_URL||"",authToken:envVars.TURSO_AUTH_TOKEN||"",prefix:""}}},dbConfigLockTail=Promise.resolve();const DB_CONFIG_LOCK_MAX_HOLD_MS=60000;export function acquireDbConfigLock(){let release=()=>{};const held=new Promise((resolve)=>{release=resolve}),acquired=dbConfigLockTail.then(()=>{const watchdog=setTimeout(()=>{console.warn(`[database] the db config lock was held for over ${DB_CONFIG_LOCK_MAX_HOLD_MS/1000}s. Releasing it so the queue can proceed - a test file most likely failed before its afterAll ran.`);release()},DB_CONFIG_LOCK_MAX_HOLD_MS);watchdog.unref?.();held.then(()=>clearTimeout(watchdog));return release});dbConfigLockTail=dbConfigLockTail.then(()=>held).catch(()=>{});return acquired}export function initializeDbConfig(config){if(config?.app?.env)appEnv=config.app.env;if(config?.database?.default)dbDriver=normalizeDatabaseDriver(config.database.default);queryBuilderDialect=toQueryBuilderDialect(dbDriver);if(config?.database?.connections)dbConfig=config.database;configureDatabaseRoutingContext(getReplicas().length>0);queryLoggingEnabled=config?.database?.queryLogging?.enabled??envVars.DB_QUERY_LOGGING_ENABLED??!isProductionEnvironment(appEnv);syncDatabaseQueryHooks();syncBrokenPoolDetection();updateQueryBuilderConfig();_dbInstance=null;_replicaInstances=new Map}function getEnv(){return appEnv}function isProductionEnvironment(value){return value==="production"||value==="prod"}function getDriver(){return dbDriver}function getDatabaseConfig(){return dbConfig}function getDialect(){return queryBuilderDialect}export{getDialect as getDatabaseDialect};function getDbConfig(){return{url:void 0,authToken:void 0,...getDriverDbConfig()}}function getDriverDbConfig(){const driver=getDriver(),database=getDatabaseConfig(),env=getEnv();if(isLibsqlDriver(driver)){const turso=database.connections?.turso,url=turso?.url||envVars.TURSO_DATABASE_URL||"";if(!url)throw Error("[database] DB_CONNECTION=turso needs the database URL: set TURSO_DATABASE_URL (libsql://<db>-<org>.turso.io, or http://127.0.0.1:8080 for `turso dev`) or connections.turso.url in config/database.ts.");return{database:url,url,authToken:turso?.authToken||envVars.TURSO_AUTH_TOKEN||void 0}}if(driver==="sqlite"){const defaultName=env!=="testing"?"database/stacks.sqlite":"database/stacks_testing.sqlite";return{database:database.connections?.sqlite?.database??defaultName}}if(driver==="mysql")return{database:database.connections?.mysql?.name||"stacks",host:database.connections?.mysql?.host??"127.0.0.1",username:database.connections?.mysql?.username??"root",password:database.connections?.mysql?.password??"",port:database.connections?.mysql?.port??3306};if(driver==="singlestore")return{database:database.connections?.singlestore?.name||"stacks",host:database.connections?.singlestore?.host??"127.0.0.1",username:database.connections?.singlestore?.username??"root",password:database.connections?.singlestore?.password??"",port:database.connections?.singlestore?.port??3306};if(driver==="vitess")return{database:database.connections?.vitess?.name||"stacks",host:database.connections?.vitess?.host??"127.0.0.1",username:database.connections?.vitess?.username??"root",password:database.connections?.vitess?.password??"",port:database.connections?.vitess?.port??15306};if(driver==="postgres"){const dbName=database.connections?.postgres?.name??"stacks";return{database:env==="testing"?`${dbName}_testing`:dbName,host:database.connections?.postgres?.host??"127.0.0.1",username:database.connections?.postgres?.username??"",password:database.connections?.postgres?.password??"",port:database.connections?.postgres?.port??5432}}return{database:":memory:"}}export const QB_SNAPSHOT_DIR="storage/framework/database";export function qbSnapshotDir(){return snapshotDirForQueryBuilder()}export const RAW_QUERY_SOFT_DELETE_CONFIG={enabled:!1,column:"deleted_at",defaultFilter:!0};export function createDatabaseQueryHooks(dispatch){function forward(event){try{Promise.resolve(dispatch(event)).catch(()=>{})}catch{}}return{onQueryEnd:(event)=>forward({query:{sql:event.sql,parameters:event.params},queryDurationMillis:event.durationMs}),onQueryError:(event)=>forward({query:{sql:event.sql,parameters:event.params},queryDurationMillis:event.durationMs,error:event.error})}}let queryLoggerModule;function forwardDatabaseQuery(event){transactionConnection.exit(()=>{queryLoggerModule??=import("./query-logger").catch((error)=>{queryLoggerModule=void 0;throw error});queryLoggerModule.then(({logQuery})=>logQuery(event)).catch(()=>{})})}let unregisterDatabaseQueryHooks;function syncDatabaseQueryHooks(){const shouldInstall=!isProductionEnvironment(appEnv)||queryLoggingEnabled;if(shouldInstall&&!unregisterDatabaseQueryHooks)unregisterDatabaseQueryHooks=registerPersistentQueryHooks(createDatabaseQueryHooks(forwardDatabaseQuery));else if(!shouldInstall&&unregisterDatabaseQueryHooks){unregisterDatabaseQueryHooks();unregisterDatabaseQueryHooks=void 0}}syncDatabaseQueryHooks();function primaryPoolIdentity(){const dialect=String(queryBuilderConfig.dialect),configured=queryBuilderConfig.database??{},env=process.env.DB_CONNECTION===dialect?process.env:{};return{driver:dialect,host:env.DB_HOST||configured.host||"localhost",port:Number(env.DB_PORT||configured.port)||void 0,database:String(env.DB_DATABASE||configured.database||"").replace(/^['"]|['"]$/g,"")}}export function recordPrimaryDatabaseQueryError(error){recordDatabaseQueryError(error,primaryPoolIdentity())}let unregisterBrokenPoolDetection;function detectsBrokenPools(){return isPostgresWire(getDriver())||isMysqlWire(getDriver())}function syncBrokenPoolDetection(){const shouldInstall=detectsBrokenPools();if(shouldInstall&&!unregisterBrokenPoolDetection)unregisterBrokenPoolDetection=registerPersistentQueryHooks({onQueryError:(event)=>recordPrimaryDatabaseQueryError(event.error)});else if(!shouldInstall&&unregisterBrokenPoolDetection){unregisterBrokenPoolDetection();unregisterBrokenPoolDetection=void 0}}syncBrokenPoolDetection();function replicaQueryHooks(pool){return{onQueryError:(event)=>{try{recordDatabaseQueryError(event.error,pool);queryBuilderConfig.hooks?.onQueryError?.(event)}catch{}}}}function getPoolConfig(){const driver=getDriver();if(driver==="sqlite"||isLibsqlDriver(driver))return;return getDatabaseConfig().connections?.[driver]?.pool}const EMPTY_REPLICAS=[];function getReplicas(){const driver=getDriver();if(driver==="sqlite"||isLibsqlDriver(driver))return EMPTY_REPLICAS;return getDatabaseConfig().connections?.[driver]?.replicas??EMPTY_REPLICAS}function getReadPolicy(){return getDatabaseConfig().reads??{}}export{runInDatabaseRoutingContext,withDatabaseRoutingContext};function toBunPoolOptions(pool){if(!pool)return{};const options={};if(pool.max!==void 0)options.max=pool.max;if(pool.idleTimeoutMs!==void 0)options.idleTimeout=Math.round(pool.idleTimeoutMs/1000);if(pool.acquireTimeoutMs!==void 0)options.connectionTimeout=Math.round(pool.acquireTimeoutMs/1000);if(pool.maxLifetimeMs!==void 0)options.maxLifetime=Math.round(pool.maxLifetimeMs/1000);return options}function updateQueryBuilderConfig(){const dialect=getDialect(),dbConfigForQb=getDbConfig(),pool=getPoolConfig();setConfig({dialect,vitess:{sharded:isVitessSharded(dbConfig.connections.vitess?.sharded)},database:pool?{...dbConfigForQb,pool}:dbConfigForQb,verbose:getEnv()!=="production",snapshotDir:qbSnapshotDir(),migrationDir:relativeMigrationDirectory(resolveMigrationDirectory(toQueryBuilderDialect(dialect),{snapshotDir:qbSnapshotDir()})),timestamps:{createdAt:"created_at",updatedAt:"updated_at",defaultOrderColumn:"created_at"},softDeletes:RAW_QUERY_SOFT_DELETE_CONFIG})}updateQueryBuilderConfig();let _dbInstance=null,_configInitPromise=null;function ensureConfigLoaded(){if(!_configInitPromise)_configInitPromise=(async()=>{try{const{config,overridesReady}=await import("@stacksjs/config/runtime");await overridesReady;if(config){initializeDbConfig(config);_dbInstance=null}}catch{}})();return _configInitPromise}export async function ensureDatabaseConfigLoaded(){await ensureConfigLoaded()}export{applySqlitePragmas,SQLITE_BOOTSTRAP_PRAGMAS}from"@stacksjs/query-builder";const sqliteTxOwner=new AsyncLocalStorage;let sqliteTxTail=Promise.resolve();function runOwnedSqliteTransaction(run,parent){const owner={parent,accepting:!0,transactions:0,completionTail:Promise.resolve()};return sqliteTxOwner.run(owner,async()=>{try{return await run()}finally{owner.accepting=!1;await owner.completionTail}})}function serializeSqliteTransaction(run){let owner=sqliteTxOwner.getStore();while(owner&&!owner.accepting)owner=owner.parent;if(owner?.accepting&&owner.transactions>0)return sqliteTxOwner.run(owner,run);const completionOwner=owner?.accepting?owner:void 0,result=(completionOwner?.completionTail??sqliteTxTail).then(()=>runOwnedSqliteTransaction(run,completionOwner)),tail=result.then(()=>{return},()=>{return});if(completionOwner)completionOwner.completionTail=tail;else sqliteTxTail=tail;return result}function applySqliteTransactionSerialization(instance){const original=instance.transaction.bind(instance);instance.transaction=(...args)=>serializeSqliteTransaction(()=>original(...args))}const dispatchScopedBuilders=new WeakSet,transactionConnection=new AsyncLocalStorage;function activeTransactionConnection(){const scope=transactionConnection.getStore();if(!scope)return;assertTransactionConnection(scope);return scope.connection}function assertTransactionConnection(scope){for(let owner=scope;owner;owner=owner.parent)if(!owner.active)throw Error("Database work cannot continue from a closed transaction callback.");if(scope.nested)throw Error("Await the active nested transaction before issuing more database work.")}function guardTransactionQuery(value){const scope=transactionConnection.getStore();if(!scope||!value||typeof value!=="object"||typeof value.execute!=="function")return value;const wrappers=new WeakMap,wrap=(target)=>{const cached=wrappers.get(target);if(cached)return cached;const proxy=new Proxy(target,{get(object,property){const member=Reflect.get(object,property,object);if(typeof member!=="function")return member;return(...args)=>{assertTransactionConnection(scope);if(transactionConnection.getStore()!==scope)throw Error("A transaction query must execute in the callback that created it.");const guardedArgs=args.map((arg)=>typeof arg==="function"?function(...values){return Reflect.apply(arg,this,values.map((value)=>value&&typeof value==="object"&&typeof Reflect.get(value,"execute")==="function"?wrap(value):value))}:arg),result=Reflect.apply(member,object,guardedArgs);if(result===object)return proxy;if(result&&typeof result==="object"&&!isPromise(result)&&(typeof Reflect.get(result,"execute")==="function"||typeof Reflect.get(result,"next")==="function"))return wrap(result);return result}}});wrappers.set(target,proxy);return proxy};return wrap(value)}function applyTransactionDispatchScope(instance){if(dispatchScopedBuilders.has(instance))return;dispatchScopedBuilders.add(instance);let defaultOptions={};const setDefaults=instance.setTransactionDefaults.bind(instance);instance.setTransactionDefaults=(defaults)=>{setDefaults(defaults);defaultOptions={...defaultOptions,...defaults}};for(const method of["transaction","savepoint"]){const original=instance[method].bind(instance);instance[method]=async(callback,options)=>{const active=activeTransactionConnection();if(active&&active!==instance)return active[method](callback,options);const parent=transactionConnection.getStore(),effectiveOptions={...defaultOptions,...options},afterCommit=method==="transaction"?effectiveOptions.afterCommit:void 0,parentObserver=(listener)=>listener?(...args)=>{const nested=parent?.nested;if(parent)parent.nested=!1;try{listener(...args)}finally{if(parent)parent.nested=nested}}:void 0,result=await runInTransactionScope(async(runAttempt)=>{const owner=sqliteTxOwner.getStore();if(owner)owner.transactions++;if(parent)parent.nested=!0;try{const runCallback=AsyncLocalStorage.bind((tx)=>runAttempt(async()=>{applyTransactionDispatchScope(tx);const scope={connection:tx,parent,active:!0,nested:!1};return transactionConnection.run(scope,async()=>{try{return await callback(tx)}finally{scope.active=!1}})}));return await original(runCallback,method==="transaction"?{...options,afterCommit:void 0,onRollback:parentObserver(effectiveOptions.onRollback),afterRollback:parentObserver(effectiveOptions.afterRollback),onRetry:parentObserver(effectiveOptions.onRetry)}:options)}finally{if(parent)parent.nested=!1;if(owner)owner.transactions--}});await afterCommit?.();return result}}}function applyTransactionRoutingContext(instance){const original=instance.transaction.bind(instance);instance.transaction=(...args)=>withTransactionContext(()=>original(...args))}function getDb(){if(_databaseClosePromise)throw Error("Database connections are shutting down");const active=activeTransactionConnection();if(active)return active;if(!_dbInstance){updateQueryBuilderConfig();_dbInstance=createQueryBuilder();applyTransactionDispatchScope(_dbInstance);if(getDialect()==="sqlite")applySqliteTransactionSerialization(_dbInstance);applyTransactionRoutingContext(_dbInstance)}return _dbInstance}let _replicaInstances=new Map,_databaseClosePromise=null;const _retiredConnectionClosures=new Set;export function resetDatabaseConnection(){const connections=[..._dbInstance?[_dbInstance]:[],..._replicaInstances.values()];if(connections.length>0)retireDatabaseConnections(_retiredConnectionClosures,connections,(error)=>console.error(`[database] Failed to close retired connection: ${error.message}`));resetQueryBuilderConnection();lastGeneralSqliteStatement=void 0;_dbInstance=null;_replicaInstances=new Map}export function closeDatabaseConnection(){if(_databaseClosePromise)return _databaseClosePromise;const connections=[..._dbInstance?[_dbInstance]:[],..._replicaInstances.values()];_dbInstance=null;_replicaInstances=new Map;lastGeneralSqliteStatement=void 0;const closing=(async()=>{try{await closeDatabaseConnectionsAndPending(connections,_retiredConnectionClosures)}finally{resetQueryBuilderConnection()}})();_databaseClosePromise=closing;closing.then(()=>{if(_databaseClosePromise===closing)_databaseClosePromise=null},()=>{if(_databaseClosePromise===closing)_databaseClosePromise=null});return closing}function getReplicaDb(replica){const primary=getDbConfig(),resolved=resolveReplicaConnection(replica,primary),key=replicaConnectionKey(resolved),cached=_replicaInstances.get(key);if(cached)return cached;const scheme=isMysqlWire(getDriver())?"mysql":"postgres",auth=resolved.username?`${encodeURIComponent(resolved.username)}:${encodeURIComponent(resolved.password??"")}@`:"",url=`${scheme}://${auth}${resolved.host}:${resolved.port}/${resolved.database}`,sql=new SQL({url,...toBunPoolOptions(getPoolConfig())}),instance=detectsBrokenPools()?createQueryBuilder({sql,hooks:replicaQueryHooks({driver:getDriver(),host:resolved.host,port:resolved.port,database:resolved.database})}):createQueryBuilder({sql});_replicaInstances.set(key,instance);return instance}function getReadDb(){const active=activeTransactionConnection();if(active)return active;const replicas=getReplicas();if(replicas.length===0)return getDb();const policy=getReadPolicy();if(!shouldRouteToReplica({policy,replicas}))return getDb();const replica=selectReplica(replicas,policy.strategy);return replica?getReplicaDb(replica):getDb()}function getExplicitReadDb(){const active=activeTransactionConnection();if(active)return active;const replicas=getReplicas();if(!replicas.length||contextInTransaction())return getDb();const replica=selectReplica(replicas,getReadPolicy().strategy);return replica?getReplicaDb(replica):getDb()}const WRITE_ENTRY_POINTS=new Set(["insertInto","updateTable","deleteFrom","create","createMany","insertOrIgnore","insertGetId","updateOrInsert","upsert"]),READ_ENTRY_POINTS=new Set(["selectFrom","selectFromSub","select"]);ensureConfigLoaded();export function asRows(rows){return rows}export function asRow(row){return row}const SIMPLE_SQLITE_TABLE=/^[A-Z_][A-Z0-9_]*$/i,SIMPLE_SQLITE_COLUMN=/^[A-Z_][A-Z0-9_]*(?:\.[A-Z_][A-Z0-9_]*)?$/i,SIMPLE_SQLITE_SELECTION=/^[A-Z_][A-Z0-9_]*(?:\.[A-Z_][A-Z0-9_]*)?(?:\s+AS\s+[A-Z_][A-Z0-9_]*)?$/i,SIMPLE_SQLITE_OPERATORS=new Set(["=","!=","<>","<","<=",">",">=","like","not like"]),SQLITE_IDENTIFIER_CACHE_LIMIT=512;function memoizeSqliteIdentifier(pattern){const valid=new Set;return(value)=>{if(valid.has(value))return!0;if(!pattern.test(value))return!1;if(valid.size<SQLITE_IDENTIFIER_CACHE_LIMIT)valid.add(value);return!0}}const isSimpleSqliteTable=memoizeSqliteIdentifier(SIMPLE_SQLITE_TABLE),isSimpleSqliteColumn=memoizeSqliteIdentifier(SIMPLE_SQLITE_COLUMN),isSimpleSqliteSelection=memoizeSqliteIdentifier(SIMPLE_SQLITE_SELECTION);let lastParameterizedSqliteSelect,lastUnparameterizedSqliteSelect,lastSqliteSelection;const GUARDED_SQLITE_SELECT_METHODS=new Set(["select","where","limit","execute","executeSync","executeTakeFirstSync"]);function hasActiveQueryBuilderHooks(){return Boolean(queryBuilderConfig.hooks&&Object.values(queryBuilderConfig.hooks).some((value)=>value!==void 0))}function resolveDeferredSqliteTerminal(target,property,executeStatement,executeFirstStatement,materialize,proxy){if(property==="get"){target.get=target.execute;return target.execute}if(property==="selectAll"){const selectAll=()=>proxy;target.selectAll=selectAll;return selectAll}if(property==="first"){const first=async()=>executeFirstStatement();target.first=first;return first}if(property==="firstOrFail"||property==="executeTakeFirstOrThrow"){const firstOrFail=async()=>{const row=executeFirstStatement();if(row===void 0)throw Error("Record not found");return row};target.firstOrFail=firstOrFail;target.executeTakeFirstOrThrow=firstOrFail;return firstOrFail}if(property==="exists"||property==="doesntExist"){const findsRow=property==="exists",check=async()=>executeFirstStatement()!==void 0===findsRow;target[property]=check;return check}if(property==="value"){const value=async(column)=>executeFirstStatement()?.[column];target.value=value;return value}if(property==="count"||property==="sum"||property==="avg"||property==="min"||property==="max"){const emptyValue=property==="min"||property==="max"?null:0,aggregate=(...args)=>runDeferredSqliteAggregate(property,args,emptyValue,materialize,executeStatement);target[property]=aggregate;return aggregate}if(property==="pluck"){const pluck=(...args)=>{if(args.length!==1){const builder=materialize();return builder.pluck.call(builder,...args)}try{const column=args[0],rows=executeStatement(),values=Array(rows.length);for(let index=0;index<rows.length;index++)values[index]=rows[index]?.[column];return Promise.resolve(values)}catch(error){return Promise.reject(error)}};target.pluck=pluck;return pluck}}function runDeferredSqliteAggregate(name,args,emptyValue,materialize,executeStatement){const column=args[0],acceptsNoColumn=name==="count"&&args.length===0;if(!acceptsNoColumn&&(args.length!==1||typeof column!=="string"||!isSimpleSqliteColumn(column))){const builder=materialize();return builder[name].call(builder,...args)}try{const expression=`${name.toUpperCase()}(${acceptsNoColumn?"*":column}) AS aggregate`,value=executeStatement(!1,expression)[0]?.aggregate??emptyValue;return Promise.resolve(name==="count"||name==="sum"||name==="avg"?Number(value):value)}catch(error){return Promise.reject(error)}}const fastSqliteDatabaseCache=new WeakMap;let lastGeneralSqliteStatement;function fastSqliteDatabase(instance){if(fastSqliteDatabaseCache.has(instance))return fastSqliteDatabaseCache.get(instance)??void 0;const database=instance.sql?._wrapper?.database,resolved=database&&typeof database.query==="function"?database:null;fastSqliteDatabaseCache.set(instance,resolved);return resolved??void 0}function runFastSqliteSql(instance,sqliteDatabase,sql,params){if(sqliteDatabase){let cached=lastGeneralSqliteStatement;if(!cached||cached.database!==sqliteDatabase||cached.sql!==sql){cached={database:sqliteDatabase,sql,statement:sqliteDatabase.query(sql)};lastGeneralSqliteStatement=cached}return params?cached.statement.all(...params):cached.statement.all()}return instance.unsafe(sql,params).executeSync()}let lastSqlitePlaceholders;function renderSqlitePlaceholders(length){const cached=lastSqlitePlaceholders;if(cached&&cached.length===length)return cached.sql;const sql=Array(length).fill("?").join(", ");if(typeof length==="number")lastSqlitePlaceholders={length,sql};return sql}function snapshotSimpleSqliteMembershipValues(values){if(isProxy(values)||Object.getPrototypeOf(values)!==Array.prototype||Object.hasOwn(values,Symbol.iterator))return;const snapshot=Array(values.length);for(let index=0;index<values.length;index++){const descriptor=Object.getOwnPropertyDescriptor(values,index);if(!descriptor||!("value"in descriptor))return;snapshot[index]=descriptor.value}return snapshot}function createDeferredSqliteSelect(instance,table){const sqliteDatabase=fastSqliteDatabase(instance);let selectKeyword="SELECT",columns,selectedColumnsSql,predicateColumn,predicateOperator,predicateValue,predicateParameterized=!0,predicateValues,additionalPredicates,orderings,rowLimit,rowOffset,materialized;const materialize=()=>{if(materialized)return materialized;let builder=instance.selectFrom(table);if(columns)builder=builder.select.call(builder,columns);if(selectKeyword==="SELECT DISTINCT")builder=builder.distinct.call(builder);if(predicateColumn!==void 0){if(predicateValues)if(predicateOperator==="LIKE LOWER"||predicateOperator==="NOT LIKE LOWER")builder=builder[predicateOperator==="LIKE LOWER"?"whereILike":"whereNotILike"].call(builder,predicateColumn.slice(6,-1),predicateValues[0]);else builder=builder[predicateOperator==="IN"?"whereIn":"whereNotIn"].call(builder,predicateColumn,predicateValues);else if(predicateParameterized)builder=builder.where.call(builder,predicateColumn,predicateOperator,predicateValue);else builder=builder[predicateOperator==="IS NULL"?"whereNull":"whereNotNull"].call(builder,predicateColumn);if(additionalPredicates)for(const predicate of additionalPredicates)if(predicate.values)if(predicate.operator==="LIKE LOWER"||predicate.operator==="NOT LIKE LOWER"){const method=predicate.operator==="LIKE LOWER"?"whereILike":"whereNotILike";builder=builder[method].call(builder,predicate.column.slice(6,-1),predicate.values[0])}else{const method=predicate.operator==="IN"?"whereIn":"whereNotIn";builder=builder[method].call(builder,predicate.column,predicate.values)}else if(predicate.parameterized)builder=builder.where.call(builder,predicate.column,predicate.operator,predicate.value);else{const method=predicate.operator==="IS NULL"?"whereNull":"whereNotNull";builder=builder[method].call(builder,predicate.column)}}if(orderings){const apply=builder.orderBy;for(const ordering of orderings)builder=apply.call(builder,ordering.column,ordering.direction)}if(rowLimit!==void 0)builder=builder.limit.call(builder,rowLimit);if(rowOffset!==void 0)builder=builder.offset.call(builder,rowOffset);materialized=builder;return builder};let proxy;const executeStatement=(firstOnly=!1,selection)=>{const selected=selection??selectedColumnsSql??"*",effectiveLimit=rowLimit??(firstOnly&&rowOffset===void 0?1:void 0);if(predicateColumn===void 0&&orderings===void 0&&rowOffset===void 0){const cached=lastUnparameterizedSqliteSelect;if(cached&&cached.instance===instance&&cached.selectKeyword===selectKeyword&&cached.selected===selected&&cached.table===table&&cached.limit===effectiveLimit){if(sqliteDatabase){const statement=cached.statement??=sqliteDatabase.query(cached.sql);if(effectiveLimit===1){const row=statement.get();return row===null?[]:[row]}return statement.all()}return runFastSqliteSql(instance,sqliteDatabase,cached.sql)}const limit=effectiveLimit===void 0?"":` LIMIT ${effectiveLimit}`,sql=`${selectKeyword} ${selected} FROM ${table}${limit}`;lastUnparameterizedSqliteSelect={instance,selectKeyword,selected,table,limit:effectiveLimit,sql,statement:sqliteDatabase?.query(sql)};if(lastUnparameterizedSqliteSelect.statement){if(effectiveLimit===1){const row=lastUnparameterizedSqliteSelect.statement.get();return row===null?[]:[row]}return lastUnparameterizedSqliteSelect.statement.all()}return runFastSqliteSql(instance,sqliteDatabase,sql)}if(predicateColumn!==void 0&&predicateValues===void 0&&predicateParameterized&&additionalPredicates===void 0&&orderings===void 0&&rowOffset===void 0){const cached=lastParameterizedSqliteSelect;if(cached&&cached.instance===instance&&cached.selectKeyword===selectKeyword&&cached.selected===selected&&cached.table===table&&cached.predicateColumn===predicateColumn&&cached.predicateOperator===predicateOperator&&cached.limit===effectiveLimit){if(sqliteDatabase){const statement=cached.statement??=sqliteDatabase.query(cached.sql);if(effectiveLimit===1){const row=statement.get(predicateValue);return row===null?[]:[row]}return statement.all(predicateValue)}return runFastSqliteSql(instance,sqliteDatabase,cached.sql,[predicateValue])}const sql=`${selectKeyword} ${selected} FROM ${table} WHERE ${predicateColumn} ${predicateOperator} ?${effectiveLimit===void 0?"":` LIMIT ${effectiveLimit}`}`;lastParameterizedSqliteSelect={instance,selectKeyword,selected,table,predicateColumn,predicateOperator,limit:effectiveLimit,sql,statement:sqliteDatabase?.query(sql)};if(lastParameterizedSqliteSelect.statement){if(effectiveLimit===1){const row=lastParameterizedSqliteSelect.statement.get(predicateValue);return row===null?[]:[row]}return lastParameterizedSqliteSelect.statement.all(predicateValue)}return runFastSqliteSql(instance,sqliteDatabase,sql,[predicateValue])}let query=`${selectKeyword} ${selected} FROM ${table}`;const params=[];if(predicateColumn!==void 0){query+=` WHERE ${predicateColumn} ${predicateOperator}`;if(predicateValues){query+=` (${renderSqlitePlaceholders(predicateValues.length)})`;params.push(...predicateValues)}else if(predicateParameterized){query+=" ?";params.push(predicateValue)}if(additionalPredicates){const additionalCount=additionalPredicates.length;query+=" AND ";for(let index=0;index<additionalCount;index++){if(index>0)query+=" AND ";const predicate=additionalPredicates[index];if(predicate.values){params.push(...predicate.values);query+=`${predicate.column} ${predicate.operator} (${renderSqlitePlaceholders(predicate.values.length)})`;continue}if(predicate.parameterized){params.push(predicate.value);query+=`${predicate.column} ${predicate.operator} ?`;continue}query+=`${predicate.column} ${predicate.operator}`}}}if(orderings)query+=` ORDER BY ${orderings.map((ordering)=>`${ordering.column} ${ordering.direction.toUpperCase()}`).join(", ")}`;if(effectiveLimit!==void 0)query+=` LIMIT ${effectiveLimit}`;if(rowOffset!==void 0)query+=` OFFSET ${rowOffset}`;return runFastSqliteSql(instance,sqliteDatabase,query,params)},executeFirstStatement=()=>{if(rowLimit===0)return;const selected=selectedColumnsSql??"*";if(predicateColumn===void 0&&orderings===void 0&&rowOffset===void 0){const cached=lastUnparameterizedSqliteSelect;if(cached&&cached.instance===instance&&cached.selectKeyword===selectKeyword&&cached.selected===selected&&cached.table===table&&cached.limit===1){if(sqliteDatabase)return(cached.statement??=sqliteDatabase.query(cached.sql)).get()??void 0;return runFastSqliteSql(instance,sqliteDatabase,cached.sql)[0]}const sql=`${selectKeyword} ${selected} FROM ${table} LIMIT 1`,statement=sqliteDatabase?.query(sql);lastUnparameterizedSqliteSelect={instance,selectKeyword,selected,table,limit:1,sql,statement};return statement?statement.get()??void 0:runFastSqliteSql(instance,sqliteDatabase,sql)[0]}if(predicateColumn!==void 0&&predicateValues===void 0&&predicateParameterized&&additionalPredicates===void 0&&orderings===void 0&&rowOffset===void 0){const cached=lastParameterizedSqliteSelect;if(cached&&cached.instance===instance&&cached.selectKeyword===selectKeyword&&cached.selected===selected&&cached.table===table&&cached.predicateColumn===predicateColumn&&cached.predicateOperator===predicateOperator&&cached.limit===1){if(sqliteDatabase)return(cached.statement??=sqliteDatabase.query(cached.sql)).get(predicateValue)??void 0;return runFastSqliteSql(instance,sqliteDatabase,cached.sql,[predicateValue])[0]}const sql=`${selectKeyword} ${selected} FROM ${table} WHERE ${predicateColumn} ${predicateOperator} ? LIMIT 1`,statement=sqliteDatabase?.query(sql);lastParameterizedSqliteSelect={instance,selectKeyword,selected,table,predicateColumn,predicateOperator,limit:1,sql,statement};return statement?statement.get(predicateValue)??void 0:runFastSqliteSql(instance,sqliteDatabase,sql,[predicateValue])[0]}return executeStatement(!0)[0]},createExtensions=()=>({distinct(){if(selectKeyword==="SELECT DISTINCT")return materialize().distinct();selectKeyword="SELECT DISTINCT";return proxy},where(column,operator,value){if(column!==null&&typeof column==="object"&&!Array.isArray(column)&&operator===void 0&&value===void 0){const prototype=Object.getPrototypeOf(column),entries=prototype===Object.prototype||prototype===null?Object.entries(column):[];if(entries.length>0&&entries.every(([key])=>isSimpleSqliteColumn(key))){for(const[key,entryValue]of entries)if(predicateColumn===void 0){predicateColumn=key;predicateOperator="=";predicateValue=entryValue;predicateParameterized=!0;predicateValues=void 0}else(additionalPredicates??=[]).push({column:key,operator:"=",value:entryValue,parameterized:!0});return proxy}}const builder=materialize();return builder.where.call(builder,column,operator,value)},whereNull(column){if(typeof column!=="string"||!isSimpleSqliteColumn(column)){const builder=materialize();return builder.whereNull.call(builder,column)}if(predicateColumn===void 0){predicateColumn=column;predicateOperator="IS NULL";predicateValue=void 0;predicateParameterized=!1;predicateValues=void 0}else(additionalPredicates??=[]).push({column,operator:"IS NULL",value:void 0,parameterized:!1});return proxy},whereNotNull(column){if(typeof column!=="string"||!isSimpleSqliteColumn(column)){const builder=materialize();return builder.whereNotNull.call(builder,column)}if(predicateColumn===void 0){predicateColumn=column;predicateOperator="IS NOT NULL";predicateValue=void 0;predicateParameterized=!1;predicateValues=void 0}else(additionalPredicates??=[]).push({column,operator:"IS NOT NULL",value:void 0,parameterized:!1});return proxy},whereLike(column,value,caseSensitive=!1){if(typeof column!=="string"||!isSimpleSqliteColumn(column)){const builder=materialize();return builder.whereLike.call(builder,column,value,caseSensitive)}if(!caseSensitive)return proxy.whereILike(column,value);if(predicateColumn===void 0){predicateColumn=column;predicateOperator="LIKE";predicateValue=value;predicateParameterized=!0;predicateValues=void 0}else(additionalPredicates??=[]).push({column,operator:"LIKE",value,parameterized:!0});return proxy},whereNotLike(column,value,caseSensitive=!1){if(typeof column!=="string"||!isSimpleSqliteColumn(column)){const builder=materialize();return builder.whereNotLike.call(builder,column,value,caseSensitive)}if(!caseSensitive)return proxy.whereNotILike(column,value);if(predicateColumn===void 0){predicateColumn=column;predicateOperator="NOT LIKE";predicateValue=value;predicateParameterized=!0;predicateValues=void 0}else(additionalPredicates??=[]).push({column,operator:"NOT LIKE",value,parameterized:!0});return proxy},whereILike(column,value){if(typeof column!=="string"||!isSimpleSqliteColumn(column)){const builder=materialize();return builder.whereILike.call(builder,column,value)}const sqlColumn=`LOWER(${column})`;if(predicateColumn===void 0){predicateColumn=sqlColumn;predicateOperator="LIKE LOWER";predicateValue=void 0;predicateParameterized=!1;predicateValues=[value]}else(additionalPredicates??=[]).push({column:sqlColumn,operator:"LIKE LOWER",value:void 0,parameterized:!1,values:[value]});return proxy},whereNotILike(column,value){if(typeof column!=="string"||!isSimpleSqliteColumn(column)){const builder=materialize();return builder.whereNotILike.call(builder,column,value)}const sqlColumn=`LOWER(${column})`;if(predicateColumn===void 0){predicateColumn=sqlColumn;predicateOperator="NOT LIKE LOWER";predicateValue=void 0;predicateParameterized=!1;predicateValues=[value]}else(additionalPredicates??=[]).push({column:sqlColumn,operator:"NOT LIKE LOWER",value:void 0,parameterized:!1,values:[value]});return proxy},whereIn(column,values){if(typeof column!=="string"||!isSimpleSqliteColumn(column)||!Array.isArray(values)){const builder=materialize();return builder.whereIn.call(builder,column,values)}const snapshot=values.slice();if(predicateColumn===void 0){predicateColumn=column;predicateOperator="IN";predicateValue=void 0;predicateParameterized=!1;predicateValues=snapshot}else(additionalPredicates??=[]).push({column,operator:"IN",value:void 0,parameterized:!1,values:snapshot});return proxy},whereNotIn(column,values){if(typeof column!=="string"||!isSimpleSqliteColumn(column)||!Array.isArray(values)){const builder=materialize();return builder.whereNotIn.call(builder,column,values)}const snapshot=values.slice();if(predicateColumn===void 0){predicateColumn=column;predicateOperator="NOT IN";predicateValue=void 0;predicateParameterized=!1;predicateValues=snapshot}else(additionalPredicates??=[]).push({column,operator:"NOT IN",value:void 0,parameterized:!1,values:snapshot});return proxy},whereBetween(...args){const[column,startOrValues,end]=args,values=Array.isArray(startOrValues)?startOrValues:args.length>=3?[startOrValues,end]:void 0;if(typeof column!=="string"||!isSimpleSqliteColumn(column)||!values||values.length<2){const builder=materialize();return builder.whereBetween.call(builder,...args)}const lower=values[0],upper=values[1];if(predicateColumn===void 0){predicateColumn=column;predicateOperator=">=";predicateValue=lower;predicateParameterized=!0;predicateValues=void 0}else(additionalPredicates??=[]).push({column,operator:">=",value:lower,parameterized:!0});(additionalPredicates??=[]).push({column,operator:"<=",value:upper,parameterized:!0});return proxy},whereDate(...args){const[column,operator,date]=args;if(typeof column!=="string"||!isSimpleSqliteColumn(column)||typeof operator!=="string"||!SIMPLE_SQLITE_OPERATORS.has(operator.toLowerCase())||typeof date!=="string"&&!(date instanceof Date)){const builder=materialize();return builder.whereDate.call(builder,...args)}const normalizedDate=date instanceof Date?date.toISOString():date;if(predicateColumn===void 0){predicateColumn=column;predicateOperator=operator;predicateValue=normalizedDate;predicateParameterized=!0;predicateValues=void 0}else(additionalPredicates??=[]).push({column,operator,value:normalizedDate,parameterized:!0});return proxy},orderBy(column,direction="asc"){if(typeof column!=="string"||!isSimpleSqliteColumn(column)){const builder=materialize();return builder.orderBy.call(builder,column,direction)}(orderings??=[]).push({column,direction:direction==="asc"?"asc":"desc"});return proxy},orderByDesc(column){if(typeof column!=="string"||!isSimpleSqliteColumn(column)){const builder=materialize();return builder.orderByDesc.call(builder,column)}(orderings??=[]).push({column,direction:"desc"});return proxy},latest(column){const resolvedColumn=column??queryBuilderConfig.timestamps.defaultOrderColumn;if(typeof resolvedColumn!=="string"||!isSimpleSqliteColumn(resolvedColumn)){const builder=materialize();return builder.latest.call(builder,column)}(orderings??=[]).push({column:resolvedColumn,direction:"desc"});return proxy},oldest(column){const resolvedColumn=column??queryBuilderConfig.timestamps.defaultOrderColumn;if(typeof resolvedColumn!=="string"||!isSimpleSqliteColumn(resolvedColumn)){const builder=materialize();return builder.oldest.call(builder,column)}(orderings??=[]).push({column:resolvedColumn,direction:"asc"});return proxy},offset(value){if(typeof value!=="number"||value<0||!Number.isInteger(value)){const builder=materialize();return builder.offset.call(builder,value)}rowOffset=value;return proxy}});let extensions;const base={select(value){if(materialized)return Reflect.apply(materialized.select,materialized,arguments);if(typeof value==="string"){if(value!=="*"&&!isSimpleSqliteSelection(value)){const builder=materialize();return builder.select.call(builder,value)}columns=value;selectedColumnsSql=value;return proxy}const selected=Array.isArray(value)?value:[value];let simple=!1,selection;const cached=lastSqliteSelection;if(cached&&cached.columns.length===selected.length){simple=!0;for(let index=0;simple&&index<selected.length;index++)simple=cached.columns[index]===selected[index];if(simple)selection=cached.sql}if(!simple){simple=selected.length>0;selection="";for(let index=0;simple&&index<selected.length;index++){const column=selected[index];simple=typeof column==="string"&&(column==="*"||isSimpleSqliteSelection(column));if(simple)selection+=index===0?column:`, ${column}`}if(simple)lastSqliteSelection={columns:selected.slice(),sql:selection}}if(!simple){const builder=materialize();return builder.select.call(builder,value)}columns=selected;selectedColumnsSql=selection;return proxy},where(column,operator,value){if(materialized)return Reflect.apply(materialized.where,materialized,arguments);if(typeof column!=="string"||!isSimpleSqliteColumn(column)||typeof operator!=="string"||operator!=="="&&!SIMPLE_SQLITE_OPERATORS.has(operator)&&!SIMPLE_SQLITE_OPERATORS.has(operator.toLowerCase())){if(typeof column==="string"&&isSimpleSqliteColumn(column)&&typeof operator==="string"){const normalized=operator.toLowerCase();if(normalized==="in"||normalized==="not in"){const values=Array.isArray(value)?snapshotSimpleSqliteMembershipValues(value):[value];if(values!==void 0){const membershipOperator=normalized==="in"?"IN":"NOT IN";if(predicateColumn===void 0){predicateColumn=column;predicateOperator=membershipOperator;predicateValue=void 0;predicateParameterized=!1;predicateValues=values}else(additionalPredicates??=[]).push({column,operator:membershipOperator,value:void 0,parameterized:!1,values});return proxy}}}return(extensions??=createExtensions()).where(column,operator,value)}if(predicateColumn===void 0){predicateColumn=column;predicateOperator=operator;predicateValue=value;predicateParameterized=!0;predicateValues=void 0}else(additionalPredicates??=[]).push({column,operator,value,parameterized:!0});return proxy},limit(value){if(materialized)return Reflect.apply(materialized.limit,materialized,arguments);if(typeof value!=="number"||value<0||!Number.isInteger(value)){const builder=materialize();return builder.limit.call(builder,value)}rowLimit=value;return proxy},execute(){if(materialized)return Reflect.apply(materialized.execute,materialized,arguments);try{return Promise.resolve(executeStatement())}catch(error){return Promise.reject(error)}},executeSync(){if(materialized)return Reflect.apply(materialized.executeSync,materialized,arguments);return executeStatement()},executeTakeFirstSync(){if(materialized)return Reflect.apply(materialized.executeTakeFirstSync,materialized,arguments);return executeFirstStatement()}};let forwardedMethods;proxy=new Proxy(base,{get(target,property){if(materialized){const builder=materialized,value=builder[property];return typeof value==="function"?value.bind(builder):value}if(property==="executeTakeFirst")return target.executeTakeFirst??=function(){if(materialized)return Reflect.apply(materialized.executeTakeFirst,materialized,arguments);try{return Promise.resolve(executeFirstStatement())}catch(error){return Promise.reject(error)}};if(GUARDED_SQLITE_SELECT_METHODS.has(property))return target[property];const cached=forwardedMethods?.[property];if(cached!==void 0)return cached;const value=target[property]??resolveDeferredSqliteTerminal(target,property,executeStatement,executeFirstStatement,materialize,proxy)??(extensions??=createExtensions())[property];if(value===void 0){const builder=materialize(),fallback=builder[property];return typeof fallback==="function"?fallback.bind(builder):fallback}if(typeof value!=="function")return value;const forward=(...args)=>{const builder=materialized;return builder?Reflect.apply(builder[property],builder,args):Reflect.apply(value,proxy,args)};(forwardedMethods??=Object.create(null))[property]=forward;return forward}});return proxy}function usesDeferredSqliteSelect(table){return getDialect()==="sqlite"&&!isLibsqlDriver(getDriver())&&!queryBuilderConfig.softDeletes?.enabled&&!hasActiveQueryBuilderHooks()&&isSimpleSqliteTable(table)}function selectFromDatabase(table){const instance=getDialect()==="sqlite"?getDb():getReadDb();if(usesDeferredSqliteSelect(table))return guardTransactionQuery(createDeferredSqliteSelect(instance,table));return guardTransactionQuery(instance.selectFrom(table))}function selectFromExplicitReadDatabase(table){const instance=getExplicitReadDb();if(usesDeferredSqliteSelect(table))return guardTransactionQuery(createDeferredSqliteSelect(instance,table));return guardTransactionQuery(instance.selectFrom(table))}function unsafeDatabase(query,params){return guardTransactionQuery(getDb().unsafe(query,params))}function unsafeExplicitReadDatabase(query,params){return guardTransactionQuery(getExplicitReadDb().unsafe(query,params))}function insertIntoDatabase(table){markContextWrote();return guardTransactionQuery(getDb().insertInto(table))}function updateTableDatabase(table){markContextWrote();return guardTransactionQuery(getDb().updateTable(table))}function deleteFromDatabase(table){markContextWrote();return guardTransactionQuery(getDb().deleteFrom(table))}function tableDatabase(table){return guardTransactionQuery(getDb().table(table))}function selectDatabase(table,...columns){return guardTransactionQuery(getReadDb().select(table,...columns))}function selectFromSubDatabase(subquery,alias){return guardTransactionQuery(getReadDb().selectFromSub(subquery,alias))}function tableExplicitReadDatabase(table){return guardTransactionQuery(getExplicitReadDb().table(table))}function selectExplicitReadDatabase(table,...columns){return guardTransactionQuery(getExplicitReadDb().select(table,...columns))}function selectFromSubExplicitReadDatabase(subquery,alias){return guardTransactionQuery(getExplicitReadDb().selectFromSub(subquery,alias))}function transactionalDatabase(callback,options){return(...args)=>getDb().transaction((tx)=>callback(tx,...args),options)}const dbFallback=new Proxy({},{get(_target,prop){const resolveInstance=typeof prop==="string"&&READ_ENTRY_POINTS.has(prop)?getReadDb:getDb,value=resolveInstance()[prop];if(typeof value==="function")return forwardDatabaseMethod(prop,resolveInstance);return value}});function forwardDatabaseMethod(prop,resolveInstance){const scope=transactionConnection.getStore();return(...args)=>{if(scope){assertTransactionConnection(scope);if(transactionConnection.getStore()!==scope)throw Error("A transaction query must execute in the callback that created it.")}const instance=resolveInstance();if(typeof prop==="string"&&WRITE_ENTRY_POINTS.has(prop))markContextWrote();return guardTransactionQuery(Reflect.apply(Reflect.get(instance,prop),instance,args))}}export const db=Object.create(dbFallback);Object.defineProperties(db,{deleteFrom:{value:deleteFromDatabase},fn:{value:aggregateFunctions},insertInto:{value:insertIntoDatabase},primary:{get:()=>primaryDb},read:{get:()=>readDb},select:{value:selectDatabase},selectFrom:{value:selectFromDatabase},selectFromSub:{value:selectFromSubDatabase},table:{value:tableDatabase},transactional:{value:transactionalDatabase},unsafe:{value:unsafeDatabase},updateTable:{value:updateTableDatabase}});const readDbFallback=new Proxy({},{get(_target,prop){const value=getExplicitReadDb()[prop];if(typeof value==="function")return forwardDatabaseMethod(prop,getExplicitReadDb);return value}});export const readDb=Object.create(readDbFallback);Object.defineProperties(readDb,{fn:{value:aggregateFunctions},select:{value:selectExplicitReadDatabase},selectFrom:{value:selectFromExplicitReadDatabase},selectFromSub:{value:selectFromSubExplicitReadDatabase},table:{value:tableExplicitReadDatabase},transactional:{value:transactionalDatabase},unsafe:{value:unsafeExplicitReadDatabase}});const primaryDb=new Proxy({},{get(_target,prop){const instance=getDb(),value=Reflect.get(instance,prop);return typeof value==="function"?forwardDatabaseMethod(prop,getDb):value}});export{setConfig};
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.41",
5
+ "version": "0.75.43",
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.41",
70
- "@stacksjs/env": "0.75.41",
71
- "@stacksjs/error-handling": "0.75.41",
72
- "@stacksjs/faker": "^0.75.41",
73
- "@stacksjs/features": "0.75.41",
74
- "@stacksjs/logging": "0.75.41",
75
- "@stacksjs/model-meta": "0.75.41",
76
- "@stacksjs/path": "0.75.41",
77
- "@stacksjs/query-builder": "^0.75.41",
78
- "@stacksjs/security": "0.75.41",
79
- "@stacksjs/storage": "0.75.41",
80
- "@stacksjs/strings": "0.75.41",
69
+ "@stacksjs/config": "0.75.43",
70
+ "@stacksjs/env": "0.75.43",
71
+ "@stacksjs/error-handling": "0.75.43",
72
+ "@stacksjs/faker": "^0.75.43",
73
+ "@stacksjs/features": "0.75.43",
74
+ "@stacksjs/logging": "0.75.43",
75
+ "@stacksjs/model-meta": "0.75.43",
76
+ "@stacksjs/path": "0.75.43",
77
+ "@stacksjs/query-builder": "^0.75.43",
78
+ "@stacksjs/security": "0.75.43",
79
+ "@stacksjs/storage": "0.75.43",
80
+ "@stacksjs/strings": "0.75.43",
81
81
  "@stacksjs/ts-validation": "^0.5.6",
82
- "bun-query-builder": "^0.3.4",
82
+ "bun-query-builder": "^0.3.5",
83
83
  "dynamodb-tooling": "^0.3.6"
84
84
  },
85
85
  "devDependencies": {
86
- "@stacksjs/cli": "0.75.41",
87
- "@stacksjs/router": "0.75.41",
88
- "@stacksjs/utils": "0.75.41",
86
+ "@stacksjs/cli": "0.75.43",
87
+ "@stacksjs/router": "0.75.43",
88
+ "@stacksjs/utils": "0.75.43",
89
89
  "better-dx": "^0.2.24"
90
90
  }
91
91
  }