@stacksjs/database 0.75.152 → 0.75.153
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth-tables.js +2 -2
- package/dist/custom/jobs.js +2 -2
- package/dist/datetime-columns.js +2 -2
- package/dist/dialect.d.ts +36 -0
- package/dist/dialect.js +1 -1
- package/dist/drivers/defaults/traits.js +1 -1
- package/dist/fk-audit.js +1 -1
- package/dist/migration-ledger.js +1 -1
- package/dist/notification-tables.js +2 -2
- package/dist/rbac-tables.js +2 -2
- package/dist/safe-migrations.js +2 -2
- package/dist/trait-tables.js +2 -2
- package/dist/unique-audit.js +1 -1
- package/dist/utc-defaults.js +2 -2
- package/dist/utils.js +1 -1
- package/dist/uuid-columns.js +1 -1
- package/package.json +16 -16
package/dist/auth-tables.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{randomBytes}from"node:crypto";import{log}from"@stacksjs/logging";import{
|
|
1
|
+
import{randomBytes}from"node:crypto";import{log}from"@stacksjs/logging";import{makeHash}from"@stacksjs/security";import{db}from"./utils";import{sqlHelpers}from"./sql-helpers";import{indexSqlForDialect,isDuplicateColumnError,isDuplicateIndexError,resolvedDatabaseDriver}from"./dialect";export const AUTH_TABLES=["oauth_clients","oauth_grants","oauth_auth_codes","oauth_authorization_requests","oauth_access_tokens","oauth_refresh_tokens","password_resets","email_verifications","passkeys","webauthn_challenges","two_factor_challenges","two_factor_pending_secrets"];export function usersEmailVerifiedAtSql(sql){return`ALTER TABLE users ADD COLUMN email_verified_at ${sql.nullableTimestamp}`}export function usersPasswordChangedAtSql(sql){return`ALTER TABLE users ADD COLUMN password_changed_at ${sql.nullableTimestamp}`}export function usersTwoFactorColumnsSql(sql){return["ALTER TABLE users ADD COLUMN two_factor_secret VARCHAR(255)",`ALTER TABLE users ADD COLUMN two_factor_enabled BOOLEAN NOT NULL DEFAULT ${sql.boolFalse}`,"ALTER TABLE users ADD COLUMN two_factor_last_used_step BIGINT"]}export function usersStripeIdSql(){return"ALTER TABLE users ADD COLUMN stripe_id VARCHAR(255)"}export function oauthClientProviderColumnsSql(){return["ALTER TABLE oauth_clients ADD COLUMN client_type VARCHAR(20)","ALTER TABLE oauth_clients ADD COLUMN redirect_uris TEXT","ALTER TABLE oauth_clients ADD COLUMN grant_types TEXT","ALTER TABLE oauth_clients ADD COLUMN token_endpoint_auth_method VARCHAR(32)","ALTER TABLE oauth_clients ADD COLUMN allowed_scopes TEXT","ALTER TABLE oauth_clients ADD COLUMN allowed_resources TEXT"]}export function oauthAccessTokenTokenableColumnsSql(){return["ALTER TABLE oauth_access_tokens ADD COLUMN tokenable_type VARCHAR(255)","ALTER TABLE oauth_access_tokens ADD COLUMN tokenable_id INTEGER"]}export function oauthAccessTokenTokenableBackfillSql(usersTable="users"){return[`UPDATE oauth_access_tokens SET tokenable_type = '${usersTable}', tokenable_id = user_id WHERE tokenable_id IS NULL`]}export function oauthAccessTokenDeviceColumnsSql(){return["ALTER TABLE oauth_access_tokens ADD COLUMN user_agent VARCHAR(255)","ALTER TABLE oauth_access_tokens ADD COLUMN ip_address VARCHAR(45)"]}export function oauthAccessTokenDelegationColumnsSql(){return["ALTER TABLE oauth_access_tokens ADD COLUMN oauth_grant_id VARCHAR(32)","ALTER TABLE oauth_access_tokens ADD COLUMN resources TEXT","ALTER TABLE oauth_access_tokens ADD COLUMN audiences TEXT","ALTER TABLE oauth_access_tokens ADD COLUMN workspace_id VARCHAR(255)"]}export function oauthRefreshTokenDelegationColumnsSql(){return["ALTER TABLE oauth_refresh_tokens ADD COLUMN family_id VARCHAR(64)","ALTER TABLE oauth_refresh_tokens ADD COLUMN parent_id BIGINT"]}export function passwordResetsExpiresAtSql(){return["ALTER TABLE password_resets ADD COLUMN expires_at TIMESTAMP"]}export async function ensureUsersAuthColumns(sql,options={}){const alters=[usersEmailVerifiedAtSql(sql),usersPasswordChangedAtSql(sql),...usersTwoFactorColumnsSql(sql),usersStripeIdSql()];for(const alterSql of alters)try{await db.unsafe(alterSql).execute()}catch{if(options.verbose)log.debug(`[auth-tables] Skipped (already applied or users missing): ${alterSql}`)}try{await db.unsafe(indexSqlForDialect("CREATE UNIQUE INDEX IF NOT EXISTS idx_users_stripe_id ON users(stripe_id)",resolvedDatabaseDriver())).execute()}catch{if(options.verbose)log.debug("[auth-tables] Skipped users.stripe_id unique index (already applied or users missing)")}}export async function migrateAuthTables(options={}){const dbDriver=resolvedDatabaseDriver(),sql=sqlHelpers(dbDriver),{isPostgres,boolTrue,now,pkColumn,nullableTimestamp,datetime,utcNow}=sql;if(options.verbose)log.info(`Creating auth tables for ${dbDriver}...`);try{if(options.verbose)log.info("Creating oauth_clients table...");await db.unsafe(`
|
|
2
2
|
CREATE TABLE IF NOT EXISTS oauth_clients (
|
|
3
3
|
${pkColumn},
|
|
4
4
|
user_id BIGINT,
|
|
@@ -191,4 +191,4 @@ import{randomBytes}from"node:crypto";import{log}from"@stacksjs/logging";import{e
|
|
|
191
191
|
`,["Personal Access Client",hashedSecret,"local","http://localhost",!0,!1,!1]).execute();else await db.unsafe(`
|
|
192
192
|
INSERT INTO oauth_clients (name, secret, provider, redirect, personal_access_client, password_client, revoked, created_at)
|
|
193
193
|
VALUES (?, ?, ?, ?, ?, ?, ?, ${now})
|
|
194
|
-
`,["Personal Access Client",hashedSecret,"local","http://localhost",1,0,0]).execute();if(options.verbose)log.success("Personal access client created")}log.debug("Auth tables migrated successfully");return{success:!0}}catch(error){const errorMessage=error instanceof Error?error.message:String(error);log.error("Failed to migrate auth tables:",errorMessage);return{success:!1,error:errorMessage}}}async function createTokenIndex(indexName,tableName,column){const dbDriver=
|
|
194
|
+
`,["Personal Access Client",hashedSecret,"local","http://localhost",1,0,0]).execute();if(options.verbose)log.success("Personal access client created")}log.debug("Auth tables migrated successfully");return{success:!0}}catch(error){const errorMessage=error instanceof Error?error.message:String(error);log.error("Failed to migrate auth tables:",errorMessage);return{success:!1,error:errorMessage}}}async function createTokenIndex(indexName,tableName,column){const dbDriver=resolvedDatabaseDriver();try{await db.unsafe(indexSqlForDialect(`CREATE INDEX IF NOT EXISTS ${indexName} ON ${tableName}(${column}(255))`,dbDriver)).execute()}catch{try{await db.unsafe(indexSqlForDialect(`CREATE INDEX IF NOT EXISTS ${indexName} ON ${tableName}(${column})`,dbDriver)).execute()}catch{}}}
|
package/dist/custom/jobs.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{err,ok}from"@stacksjs/error-handling";import{existsSync,readdirSync,writeFileSync,mkdirSync}from"node:fs";import{join}from"node:path";import process from"node:process";import Database from"bun:sqlite";import{env as envVars}from"@stacksjs/env";import{resolveMigrationDirectory}from"../migration-path";function
|
|
1
|
+
import{err,ok}from"@stacksjs/error-handling";import{existsSync,readdirSync,writeFileSync,mkdirSync}from"node:fs";import{join}from"node:path";import process from"node:process";import Database from"bun:sqlite";import{env as envVars}from"@stacksjs/env";import{resolvedDatabaseDriver}from"../dialect";import{resolveMigrationDirectory}from"../migration-path";function getMigrationsPath(){return resolveMigrationDirectory(resolvedDatabaseDriver())}function hasJobsMigrationBeenCreated(){try{const migrationsPath=getMigrationsPath();if(!existsSync(migrationsPath))return!1;return readdirSync(migrationsPath).some((file)=>file.includes("create-jobs-table"))}catch{return!1}}async function jobBatchesTableExists(){const sqlitePath=getDatabasePath();if(existsSync(sqlitePath))try{const sqlite=new Database(sqlitePath),result=sqlite.query("SELECT name FROM sqlite_master WHERE type='table' AND name='job_batches'").get();sqlite.close();return result!==null}catch{}if(resolvedDatabaseDriver()!=="sqlite")try{const{db}=await import("../utils");await db.selectFrom("job_batches").select("id").limit(1).execute();return!0}catch{return!1}return!1}function getDatabasePath(){const appRoot=envVars.APP_ROOT||process.cwd();return join(appRoot,"database","stacks.sqlite")}async function jobsTableExists(){const driver=resolvedDatabaseDriver(),sqlitePath=getDatabasePath();if(existsSync(sqlitePath))try{const sqlite=new Database(sqlitePath),result=sqlite.query("SELECT name FROM sqlite_master WHERE type='table' AND name='jobs'").get();sqlite.close();return result!==null}catch{}if(driver!=="sqlite")try{const{db}=await import("../utils");await db.selectFrom("jobs").select("id").limit(1).execute();return!0}catch{return!1}return!1}async function ensureQueueSupportTables(driver){const sqliteStatements=[`CREATE TABLE IF NOT EXISTS dead_letter_jobs (
|
|
2
2
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
3
3
|
uuid TEXT NOT NULL,
|
|
4
4
|
connection TEXT NOT NULL,
|
|
@@ -98,7 +98,7 @@ import{err,ok}from"@stacksjs/error-handling";import{existsSync,readdirSync,write
|
|
|
98
98
|
job_name VARCHAR(255) NOT NULL,
|
|
99
99
|
queue VARCHAR(255) NOT NULL DEFAULT 'default',
|
|
100
100
|
dispatched_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
101
|
-
)`],{db}=await import("../utils");for(const statement of statements)await db.unsafe(statement).execute()}export async function createJobsMigration(){try{const driver=
|
|
101
|
+
)`],{db}=await import("../utils");for(const statement of statements)await db.unsafe(statement).execute()}export async function createJobsMigration(){try{const driver=resolvedDatabaseDriver();if(["sqlite","mysql","postgres"].includes(driver)){if(!hasJobsMigrationBeenCreated()){let migrationContent="";if(driver==="sqlite")migrationContent=`-- Create jobs table
|
|
102
102
|
CREATE TABLE IF NOT EXISTS jobs (
|
|
103
103
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
104
104
|
queue TEXT NOT NULL DEFAULT 'default',
|
package/dist/datetime-columns.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import
|
|
1
|
+
import{log}from"@stacksjs/logging";import{db}from"./utils";import{dialectCapabilities,resolvedDatabaseDriver}from"./dialect";import{traitTableNames}from"./trait-tables";export function frameworkDatetimeTables(){return[...traitTableNames(),"passkeys","password_resets","email_verifications","oauth_clients","oauth_access_tokens","oauth_refresh_tokens","two_factor_challenges","two_factor_pending_secrets","webauthn_challenges","roles","permissions","user_roles","user_permissions","role_permissions","notifications","notification_preferences","notification_deliveries"]}export async function findTimestampColumns(){const tables=frameworkDatetimeTables(),placeholders=tables.map(()=>"?").join(", ");return(await db.unsafe(`SELECT TABLE_NAME, COLUMN_NAME, IS_NULLABLE, COLUMN_DEFAULT, EXTRA
|
|
2
2
|
FROM information_schema.COLUMNS
|
|
3
3
|
WHERE TABLE_SCHEMA = DATABASE()
|
|
4
4
|
AND TABLE_NAME IN (${placeholders})
|
|
5
5
|
AND (
|
|
6
6
|
DATA_TYPE = 'timestamp'
|
|
7
7
|
OR (DATA_TYPE = 'varchar' AND COLUMN_NAME IN ('created_at', 'updated_at'))
|
|
8
|
-
)`,tables).execute()).map((row)=>({table:String(row.TABLE_NAME??row.table_name),column:String(row.COLUMN_NAME??row.column_name),nullable:String(row.IS_NULLABLE??row.is_nullable).toUpperCase()==="YES",columnDefault:row.COLUMN_DEFAULT??row.column_default??null,extra:String(row.EXTRA??row.extra??"")}))}export function modifyToDatetimeSql(column){const safe=/^[a-z_]\w*$/i;if(!safe.test(column.table)||!safe.test(column.column))throw Error(`[datetime-columns] Refusing to alter unsafe identifier: ${column.table}.${column.column}`);let ddl=`ALTER TABLE \`${column.table}\` MODIFY \`${column.column}\` DATETIME`;ddl+=column.nullable?" NULL":" NOT NULL";if(column.columnDefault!==null){const isFunction=/^CURRENT_TIMESTAMP(\(\d*\))?$/i.test(String(column.columnDefault));ddl+=isFunction?` DEFAULT ${column.columnDefault}`:` DEFAULT '${String(column.columnDefault).replace(/'/g,"''")}'`}if(/on update CURRENT_TIMESTAMP/i.test(column.extra))ddl+=" ON UPDATE CURRENT_TIMESTAMP";return ddl}export async function ensureUtcDatetimeColumns(options={}){const driver=
|
|
8
|
+
)`,tables).execute()).map((row)=>({table:String(row.TABLE_NAME??row.table_name),column:String(row.COLUMN_NAME??row.column_name),nullable:String(row.IS_NULLABLE??row.is_nullable).toUpperCase()==="YES",columnDefault:row.COLUMN_DEFAULT??row.column_default??null,extra:String(row.EXTRA??row.extra??"")}))}export function modifyToDatetimeSql(column){const safe=/^[a-z_]\w*$/i;if(!safe.test(column.table)||!safe.test(column.column))throw Error(`[datetime-columns] Refusing to alter unsafe identifier: ${column.table}.${column.column}`);let ddl=`ALTER TABLE \`${column.table}\` MODIFY \`${column.column}\` DATETIME`;ddl+=column.nullable?" NULL":" NOT NULL";if(column.columnDefault!==null){const isFunction=/^CURRENT_TIMESTAMP(\(\d*\))?$/i.test(String(column.columnDefault));ddl+=isFunction?` DEFAULT ${column.columnDefault}`:` DEFAULT '${String(column.columnDefault).replace(/'/g,"''")}'`}if(/on update CURRENT_TIMESTAMP/i.test(column.extra))ddl+=" ON UPDATE CURRENT_TIMESTAMP";return ddl}export async function ensureUtcDatetimeColumns(options={}){const driver=resolvedDatabaseDriver();if(dialectCapabilities(driver).wire!=="mysql")return{success:!0,converted:0};try{const columns=await findTimestampColumns();if(columns.length===0){if(options.verbose)log.info("No TIMESTAMP columns left to convert");return{success:!0,converted:0}}for(const column of columns){if(options.verbose)log.info(`Converting ${column.table}.${column.column} to DATETIME...`);await db.unsafe(modifyToDatetimeSql(column)).execute()}log.debug(`[datetime-columns] Converted ${columns.length} TIMESTAMP column(s) to DATETIME`);return{success:!0,converted:columns.length}}catch(error){const message=error instanceof Error?error.message:String(error);log.error(`Failed to convert TIMESTAMP columns to DATETIME: ${message}`);return{success:!1,converted:0,error:message}}}
|
package/dist/dialect.d.ts
CHANGED
|
@@ -24,6 +24,42 @@ export declare function isKnownDialect(dialect: string): boolean;
|
|
|
24
24
|
* as written.
|
|
25
25
|
*/
|
|
26
26
|
export declare function normalizeDatabaseDriver(driver: string): string;
|
|
27
|
+
/**
|
|
28
|
+
* Record the driver an application configured, or clear it with null.
|
|
29
|
+
*
|
|
30
|
+
* `initializeDbConfig` owns the config lifecycle and is the only caller.
|
|
31
|
+
*/
|
|
32
|
+
export declare function setConfiguredDatabaseDriver(driver: string | null | undefined): void;
|
|
33
|
+
/**
|
|
34
|
+
* The driver the SQL being generated is for: `database.default` when an
|
|
35
|
+
* application configured one, and otherwise `DB_CONNECTION`.
|
|
36
|
+
*
|
|
37
|
+
* Every pass that renders DDL by hand asks this, because each one picks
|
|
38
|
+
* placeholders, quoting, boolean literals and timestamp defaults per dialect:
|
|
39
|
+
* the notification, auth, RBAC and trait table guarantees, the datetime and
|
|
40
|
+
* uuid column repairs, the UTC defaults, the safe-migration helpers, and the
|
|
41
|
+
* schema audits choosing a catalogue to introspect. So does everything that
|
|
42
|
+
* picks a migration corpus directory by dialect.
|
|
43
|
+
*
|
|
44
|
+
* Thirteen modules used to answer it privately, each reading `DB_CONNECTION`
|
|
45
|
+
* and nothing else, so an application that configured `database.default` got
|
|
46
|
+
* the connection it configured and DDL for whatever its environment said. On
|
|
47
|
+
* a sqlite file with `DB_CONNECTION=postgres` still in `.env`, the first
|
|
48
|
+
* guarantee failed on `near "AT": syntax error` from `AT TIME ZONE`
|
|
49
|
+
* (stacksjs/stacks#2897).
|
|
50
|
+
*
|
|
51
|
+
* It lives here, in the module that already decides what a dialect is, rather
|
|
52
|
+
* than beside the connection, because the modules that ask are deliberately
|
|
53
|
+
* light. The schema audits take a connection as an argument and the migration
|
|
54
|
+
* ledger only reads files, so neither can import the connection layer merely
|
|
55
|
+
* to learn which dialect it speaks.
|
|
56
|
+
*
|
|
57
|
+
* The environment is read on every call rather than captured at import,
|
|
58
|
+
* because setting `DB_CONNECTION` around a call is how this repository's own
|
|
59
|
+
* tooling and tests select a dialect, and a value frozen at import would
|
|
60
|
+
* quietly ignore them.
|
|
61
|
+
*/
|
|
62
|
+
export declare function resolvedDatabaseDriver(): string;
|
|
27
63
|
/**
|
|
28
64
|
* Whether a driver is SQLite reached over the network (Turso / libSQL).
|
|
29
65
|
*
|
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},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}
|
|
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}let configuredDriver=null;export function setConfiguredDatabaseDriver(driver){const named=driver?.trim();configuredDriver=named?normalizeDatabaseDriver(named):null}export function resolvedDatabaseDriver(){return configuredDriver??normalizeDatabaseDriver(process.env.DB_CONNECTION||"sqlite")}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}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{db}from"../../utils";import{
|
|
1
|
+
import{db}from"../../utils";import{resolvedDatabaseDriver}from"../../dialect";import{dialectCapabilities}from"../../dialect";import{traitTableNames}from"../../trait-tables";export function commonTableNames(){return[...traitTableNames(),"passkeys","password_resets","query_logs","tags","comments","categories_models","activities"]}export async function dropCommonTables(){const{identifierQuote:q,wire}=dialectCapabilities(resolvedDatabaseDriver()),cascade=wire==="postgres"?" CASCADE":"";for(const table of commonTableNames())await db.unsafe(`DROP TABLE IF EXISTS ${q}${table}${q}${cascade}`).execute()}export async function dropMigrationTables(){const{identifierQuote:q,wire}=dialectCapabilities(resolvedDatabaseDriver()),cascade=wire==="postgres"?" CASCADE":"";for(const table of["migrations","migration_locks"])await db.unsafe(`DROP TABLE IF EXISTS ${q}${table}${q}${cascade}`).execute()}
|
package/dist/fk-audit.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{existsSync}from"node:fs";import{dirname}from"node:path";import{plural,snakeCase}from"@stacksjs/strings";import{path}from"@stacksjs/path";import{globSync}from"@stacksjs/storage";import{dialectCapabilities,isKnownDialect,toSqlIntrospectionDialect}from"./dialect";export function safeGlob(pattern){const metaIdx=pattern.search(/[*?[]/),root=metaIdx===-1?dirname(pattern):dirname(pattern.slice(0,metaIdx));if(!existsSync(root))return[];try{return globSync(pattern,{absolute:!0})}catch{return[]}}function modelTable(model){return model.table||plural(snakeCase(model.name||""))}function modelNameForColumn(column){return column.replace(/_id$/,"").split("_").map((part)=>part.charAt(0).toUpperCase()+part.slice(1)).join("")}export function getDeclaredFKsFromModels(models){const meta=new Map(models.map((model)=>[String(model.name??""),{primaryKey:String(model.primaryKey??"id"),table:modelTable(model)}])),declared=[],seen=new Set,push=(fk)=>{const key=`${fk.fromTable}.${fk.fromColumn}\u2192${fk.toTable}.${fk.toColumn}`.toLowerCase();if(seen.has(key))return;seen.add(key);declared.push(fk)};for(const model of models){const fromTable=modelTable(model),modelName=String(model.name??""),attributes=model.attributes??{},belongsTo=model.belongsTo,relations=Array.isArray(belongsTo)?belongsTo:belongsTo&&typeof belongsTo==="object"?Object.keys(belongsTo).map((name)=>({model:name})):[],declaresBelongsTo=relations.length>0,columns=new Set(Object.keys(attributes).map((name)=>snakeCase(name))),polymorphic=(column)=>columns.has(`${column.slice(0,-3)}_type`);for(const[attributeName,attribute]of Object.entries(attributes)){const fromColumn=snakeCase(attributeName);if(!fromColumn.endsWith("_id")||attribute.foreignKey===!1||polymorphic(fromColumn))continue;if(attribute.foreignKey&&typeof attribute.foreignKey==="object"){push({fromTable,fromColumn,toTable:attribute.foreignKey.table,toColumn:attribute.foreignKey.column??"id",model:modelName});continue}const declaredTarget=relations.some((entry)=>{const relatedName=typeof entry==="string"?entry:String(entry.model??"");return(typeof entry==="object"&&entry.foreignKey?String(entry.foreignKey):`${snakeCase(relatedName)}_id`)===fromColumn});if(declaresBelongsTo&&!declaredTarget)continue;const related=meta.get(modelNameForColumn(fromColumn));if(related)push({fromTable,fromColumn,toTable:related.table,toColumn:related.primaryKey,model:modelName})}for(const entry of relations){const relatedName=typeof entry==="string"?entry:String(entry.model??"");if(!relatedName)continue;const fromColumn=typeof entry==="object"&&entry.foreignKey?String(entry.foreignKey):`${snakeCase(relatedName)}_id`;if(Object.keys(attributes).some((attribute)=>snakeCase(attribute)===fromColumn)||polymorphic(fromColumn))continue;const related=meta.get(relatedName);if(related)push({fromTable,fromColumn,toTable:related.table,toColumn:related.primaryKey,model:modelName})}}return declared}export async function getDeclaredFKs(){const userModelFiles=safeGlob(path.userModelsPath("*.ts")),modelFiles=userModelFiles.length>0?userModelFiles:safeGlob(path.storagePath("framework/defaults/app/Models/**/*.ts")),models=[];for(const modelFile of modelFiles){let model;try{model=(await import(modelFile)).default}catch{continue}if(model&&typeof model==="object")models.push(model)}return getDeclaredFKsFromModels(models)}export async function getLiveFKs(){const{db}=await import("./utils"),dialect=
|
|
1
|
+
import{existsSync}from"node:fs";import{dirname}from"node:path";import{plural,snakeCase}from"@stacksjs/strings";import{path}from"@stacksjs/path";import{globSync}from"@stacksjs/storage";import{dialectCapabilities,isKnownDialect,resolvedDatabaseDriver,toSqlIntrospectionDialect}from"./dialect";export function safeGlob(pattern){const metaIdx=pattern.search(/[*?[]/),root=metaIdx===-1?dirname(pattern):dirname(pattern.slice(0,metaIdx));if(!existsSync(root))return[];try{return globSync(pattern,{absolute:!0})}catch{return[]}}function modelTable(model){return model.table||plural(snakeCase(model.name||""))}function modelNameForColumn(column){return column.replace(/_id$/,"").split("_").map((part)=>part.charAt(0).toUpperCase()+part.slice(1)).join("")}export function getDeclaredFKsFromModels(models){const meta=new Map(models.map((model)=>[String(model.name??""),{primaryKey:String(model.primaryKey??"id"),table:modelTable(model)}])),declared=[],seen=new Set,push=(fk)=>{const key=`${fk.fromTable}.${fk.fromColumn}\u2192${fk.toTable}.${fk.toColumn}`.toLowerCase();if(seen.has(key))return;seen.add(key);declared.push(fk)};for(const model of models){const fromTable=modelTable(model),modelName=String(model.name??""),attributes=model.attributes??{},belongsTo=model.belongsTo,relations=Array.isArray(belongsTo)?belongsTo:belongsTo&&typeof belongsTo==="object"?Object.keys(belongsTo).map((name)=>({model:name})):[],declaresBelongsTo=relations.length>0,columns=new Set(Object.keys(attributes).map((name)=>snakeCase(name))),polymorphic=(column)=>columns.has(`${column.slice(0,-3)}_type`);for(const[attributeName,attribute]of Object.entries(attributes)){const fromColumn=snakeCase(attributeName);if(!fromColumn.endsWith("_id")||attribute.foreignKey===!1||polymorphic(fromColumn))continue;if(attribute.foreignKey&&typeof attribute.foreignKey==="object"){push({fromTable,fromColumn,toTable:attribute.foreignKey.table,toColumn:attribute.foreignKey.column??"id",model:modelName});continue}const declaredTarget=relations.some((entry)=>{const relatedName=typeof entry==="string"?entry:String(entry.model??"");return(typeof entry==="object"&&entry.foreignKey?String(entry.foreignKey):`${snakeCase(relatedName)}_id`)===fromColumn});if(declaresBelongsTo&&!declaredTarget)continue;const related=meta.get(modelNameForColumn(fromColumn));if(related)push({fromTable,fromColumn,toTable:related.table,toColumn:related.primaryKey,model:modelName})}for(const entry of relations){const relatedName=typeof entry==="string"?entry:String(entry.model??"");if(!relatedName)continue;const fromColumn=typeof entry==="object"&&entry.foreignKey?String(entry.foreignKey):`${snakeCase(relatedName)}_id`;if(Object.keys(attributes).some((attribute)=>snakeCase(attribute)===fromColumn)||polymorphic(fromColumn))continue;const related=meta.get(relatedName);if(related)push({fromTable,fromColumn,toTable:related.table,toColumn:related.primaryKey,model:modelName})}}return declared}export async function getDeclaredFKs(){const userModelFiles=safeGlob(path.userModelsPath("*.ts")),modelFiles=userModelFiles.length>0?userModelFiles:safeGlob(path.storagePath("framework/defaults/app/Models/**/*.ts")),models=[];for(const modelFile of modelFiles){let model;try{model=(await import(modelFile)).default}catch{continue}if(model&&typeof model==="object")models.push(model)}return getDeclaredFKsFromModels(models)}export async function getLiveFKs(){const{db}=await import("./utils"),dialect=currentDialect();if(dialect==="sqlite")return getSqliteLiveFKs(db);if(dialect==="mysql")return getMysqlLiveFKs(db);if(dialect==="postgres")return getPostgresLiveFKs(db);return[]}export async function getLiveTables(){const{db}=await import("./utils"),dialect=currentDialect(),query=dialect==="sqlite"?"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'":dialect==="mysql"?"SELECT TABLE_NAME AS name FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE()":dialect==="postgres"?"SELECT table_name AS name FROM information_schema.tables WHERE table_schema = current_schema()":"";if(!query)return new Set;try{const rows=await db.unsafe(query);return new Set((Array.isArray(rows)?rows:[]).map((row)=>row.name).filter((name)=>Boolean(name)).map((name)=>name.toLowerCase()))}catch{return new Set}}export async function auditForeignKeys(){const declared=await getDeclaredFKs(),live=await getLiveFKs(),liveKeys=new Set(live.map((fk)=>`${fk.fromTable.toLowerCase()}.${fk.fromColumn.toLowerCase()}\u2192${fk.toTable.toLowerCase()}.${fk.toColumn.toLowerCase()}`)),liveTables=await getLiveTables();return{declared,live,...classifyDeclaredFKs(declared,liveKeys,liveTables)}}export function classifyDeclaredFKs(declared,liveKeys,liveTables){const canClassify=liveTables.size>0,missing=[],absentTable=[];for(const d of declared){const key=`${d.fromTable.toLowerCase()}.${d.fromColumn.toLowerCase()}\u2192${d.toTable.toLowerCase()}.${d.toColumn.toLowerCase()}`;if(liveKeys.has(key))continue;if(canClassify&&!liveTables.has(d.fromTable.toLowerCase()))absentTable.push(d);else missing.push(d)}return{missing,absentTable}}export function fkKey(fk){return`${fk.fromTable.toLowerCase()}.${fk.fromColumn.toLowerCase()}\u2192${fk.toTable.toLowerCase()}.${fk.toColumn.toLowerCase()}`}export async function findFkOrphans(dialect){if((dialect??currentDialect())!=="sqlite")return{supported:!1,total:0,orphans:[]};const{db}=await import("./utils"),rows=await db.unsafe("PRAGMA foreign_key_check").execute(),checkRows=Array.isArray(rows)?rows:[],fkListCache=new Map;async function fkListFor(table){if(fkListCache.has(table))return fkListCache.get(table);if(!/^[a-z_]\w*$/i.test(table)){fkListCache.set(table,[]);return[]}const list=await db.unsafe(`PRAGMA foreign_key_list("${table}")`).execute(),arr=Array.isArray(list)?list:[];fkListCache.set(table,arr);return arr}const grouped=new Map;for(const raw of checkRows){const r=raw,table=String(r.table??""),parent=String(r.parent??"");if(!table||!parent)continue;const fkid=Number(r.fkid??0);let column="";const fkList=await fkListFor(table);for(const fk of fkList){const f=fk;if(Number(f.id)===fkid&&f.from){column=String(f.from);break}}const key=`${table}\x00${parent}\x00${fkid}`;let entry=grouped.get(key);if(!entry){entry={table,column,parent,count:0,sampleRowids:[]};grouped.set(key,entry)}entry.count++;if(entry.sampleRowids.length<5&&typeof r.rowid==="number")entry.sampleRowids.push(r.rowid)}const orphans=[...grouped.values()];return{supported:!0,total:orphans.reduce((sum,o)=>sum+o.count,0),orphans}}function currentDialect(){const driver=resolvedDatabaseDriver().toLowerCase();if(!isKnownDialect(driver)||!dialectCapabilities(driver).supportsForeignKeys)return"other";return toSqlIntrospectionDialect(driver)}async function getSqliteLiveFKs(db){const tables=await db.unsafe("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"),rows=Array.isArray(tables)?tables:[],fks=[];for(const row of rows){const fromTable=row.name;if(!fromTable)continue;if(!/^[a-z_][\w]*$/i.test(fromTable))continue;const fkRows=await db.unsafe(`PRAGMA foreign_key_list("${fromTable}")`);for(const fk of Array.isArray(fkRows)?fkRows:[]){const r=fk;if((r.seq??0)!==0)continue;if(!r.from||!r.to||!r.table)continue;fks.push({fromTable,fromColumn:r.from,toTable:r.table,toColumn:r.to})}}return fks}async function getMysqlLiveFKs(db){const rows=await db.unsafe(`
|
|
2
2
|
SELECT TABLE_NAME, COLUMN_NAME, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME
|
|
3
3
|
FROM information_schema.KEY_COLUMN_USAGE
|
|
4
4
|
WHERE TABLE_SCHEMA = DATABASE()
|
package/dist/migration-ledger.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{existsSync,readdirSync,readFileSync}from"node:fs";import process from"node:process";import{join}from"node:path";import{comparableIndexKey,postgresIndexStatement}from"./index-shape";import{resolveMigrationDirectory}from"./migration-path";import{sqlStatementsOf}from"./sql-statements";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)},shapes=new Map;for(const statement of sqlStatementsOf(sql)){const match=/^\s*CREATE\s+(?:UNIQUE\s+)?INDEX\s+(?:CONCURRENTLY\s+)?(?:IF\s+NOT\s+EXISTS\s+)?["`[]?([\w$]+)/i.exec(statement),key=match?.[1]?comparableIndexKey(statement):void 0;if(match?.[1]&&key)shapes.set(match[1].toLowerCase(),key)}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]){const shape=shapes.get(index[1].toLowerCase());push({kind:"index",name:index[1],...index[2]?{table:index[2]}:{},...shape?{shape}:{}});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,indexShapes: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, sql FROM sqlite_master WHERE type='index' AND name NOT LIKE 'sqlite_%'")){const name=pick(row,"name");if(name)schema.indexes.add(name.toLowerCase());const key=comparableIndexKey(pick(row,"sql"));if(key)schema.indexShapes.add(key)}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, indexdef AS definition FROM pg_indexes WHERE schemaname = 'public'")){const name=pick(row,"name","indexname");if(name)schema.indexes.add(name.toLowerCase());const key=comparableIndexKey(postgresIndexStatement(pick(row,"definition","indexdef")));if(key)schema.indexShapes.add(key)}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;if(effect.shape&&schema.indexShapes.has(effect.shape))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:[],gated:[],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),gate=await resolveGate(options,dir),sources=new Map,gated=files.filter((file)=>gate?.exclude.has(file));for(const file of files){if(gate?.exclude.has(file))continue;try{const sql=readFileSync(join(dir,file),"utf8");sources.set(file,gate?gate.rewrite(sql):sql)}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),...gated].sort(),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,gated,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{}}const Q=(pattern)=>new RegExp(pattern.replaceAll("{id}",IDENT),"i"),CREATE_TABLE=Q(String.raw`^CREATE\s+(?:TEMP(?:ORARY)?\s+)?TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?{id}`),DROP_TABLE=Q(String.raw`^DROP\s+TABLE\s+(?:IF\s+EXISTS\s+)?{id}`),RENAME_TABLE=Q(String.raw`^ALTER\s+TABLE\s+(?:IF\s+EXISTS\s+)?(?:ONLY\s+)?{id}\s+RENAME\s+TO\s+{id}`),ALTER_TABLE=Q(String.raw`^ALTER\s+TABLE\s+(?:IF\s+EXISTS\s+)?(?:ONLY\s+)?{id}`),CREATE_INDEX=Q(String.raw`^CREATE\s+(?:UNIQUE\s+)?INDEX\s+(?:CONCURRENTLY\s+)?(?:IF\s+NOT\s+EXISTS\s+)?{id}\s+ON\s+(?:ONLY\s+)?{id}`),DROP_INDEX=Q(String.raw`^DROP\s+INDEX\s+(?:CONCURRENTLY\s+)?(?:IF\s+EXISTS\s+)?{id}(?:\s+ON\s+{id})?`),DML=Q(String.raw`^(?:INSERT\s+(?:OR\s+\w+\s+)?INTO|REPLACE\s+INTO|UPDATE|DELETE\s+FROM)\s+{id}`),NEUTRAL=/^(?:BEGIN|COMMIT|END|PRAGMA|SET|SELECT\s+1|COMMENT\s+ON|CREATE\s+TYPE\s+\S+\s+AS\s+ENUM|ALTER\s+TYPE\s+\S+\s+ADD\s+VALUE|STACKS_ENUM_GUARD)\b/i;function statementShape(statement,indexTables){const lower=(name)=>(name??"").toLowerCase();if(NEUTRAL.test(statement))return{modifies:[]};let m=RENAME_TABLE.exec(statement);if(m)return{modifies:[lower(m[1]),lower(m[2])],rename:{from:lower(m[1]),to:lower(m[2])}};m=CREATE_TABLE.exec(statement);if(m)return{modifies:[lower(m[1])],creates:lower(m[1])};m=DROP_TABLE.exec(statement);if(m)return{modifies:[lower(m[1])],drops:lower(m[1])};m=ALTER_TABLE.exec(statement);if(m)return{modifies:[lower(m[1])]};m=CREATE_INDEX.exec(statement);if(m)return{modifies:[lower(m[2])],index:{name:lower(m[1]),table:lower(m[2])}};m=DROP_INDEX.exec(statement);if(m){const table=m[2]?lower(m[2]):indexTables.get(lower(m[1]));return table?{modifies:[table]}:{modifies:[],unknown:!0}}m=DML.exec(statement);if(m)return{modifies:[lower(m[1])]};return{modifies:[],unknown:!0}}function requeueStatements(sql){return stripForEffects(sql).replace(/\bDO\s+\$(\w*)\$([\s\S]*?)\$\1\$/gi,(block,_tag,body)=>/^\s*BEGIN\s+CREATE\s+TYPE\b[\s\S]*\bEXCEPTION\s+WHEN\s+duplicate_object\s+THEN\s+null\s*;\s*END\s*$/i.test(body)?"STACKS_ENUM_GUARD":block).split(";").map((statement)=>statement.trim()).filter(Boolean)}async function resolveGate(options,dir){if(options.gate||options.dir)return options.gate;try{const{featureGateForLedger}=await import("./migrations");return await featureGateForLedger(dir)}catch{return}}export function planRequeue(files,liveTables){const ordered=[...files].sort((a,b)=>a.file.localeCompare(b.file)),indexTables=new Map,expected=new Set,lineageOf=new Map,lineage=(table)=>{let group=lineageOf.get(table);if(!group){group=new Set([table]);lineageOf.set(table,group)}return group},link=(a,b)=>{const merged=new Set([...lineage(a),...lineage(b)]);for(const name of merged)lineageOf.set(name,merged)},shapes=ordered.map(({file,sql,recorded})=>{const statements=requeueStatements(sql).map((statement)=>statementShape(statement,indexTables));for(const shape of statements){if(shape.index)indexTables.set(shape.index.name,shape.index.table);if(shape.creates)expected.add(shape.creates);if(shape.drops)expected.delete(shape.drops);if(shape.rename){expected.delete(shape.rename.from);expected.add(shape.rename.to);link(shape.rename.from,shape.rename.to)}}const created=new Set(statements.flatMap((shape)=>shape.creates?[shape.creates]:[])),removed=new Set(statements.flatMap((shape)=>[shape.drops,shape.rename?.from].filter((t)=>Boolean(t)))),scaffold=new Set([...created].filter((table)=>removed.has(table)||statements.some((shape)=>shape.rename?.from===table)));return{file,recorded,unknown:statements.some((shape)=>shape.unknown),modifies:new Set(statements.flatMap((shape)=>shape.modifies)),scaffold}}),missing=[...expected].filter((table)=>!liveTables.has(table)).sort(),rebuildable=new Set(missing.flatMap((table)=>[...lineage(table)]).filter((table)=>!liveTables.has(table))),chains=new Map(missing.map((table)=>{const names=lineage(table);return[table,shapes.filter((shape)=>[...shape.modifies].some((name)=>names.has(name)))]})),why=new Map;for(const[table,chain]of chains)for(const shape of chain){if(shape.unknown){why.set(table,`${shape.file} contains a statement the planner cannot classify`);break}const elsewhere=[...shape.modifies].filter((t)=>!rebuildable.has(t)&&!shape.scaffold.has(t));if(elsewhere.length>0){why.set(table,`${shape.file} also changes ${elsewhere.join(", ")}, which ${elsewhere.length===1?"is":"are"} not missing`);break}}let changed=!0;while(changed){changed=!1;for(const[table,chain]of chains){if(why.has(table))continue;const blocker=chain.flatMap((shape)=>[...shape.modifies]).find((other)=>other!==table&&missing.some((t)=>why.has(t)&&lineage(t).has(other)));if(blocker){why.set(table,`shares a migration with ${blocker}, which cannot be rebuilt`);changed=!0}}}const requeue=new Set;for(const[table,chain]of chains){if(why.has(table))continue;for(const shape of chain)if(shape.recorded)requeue.add(shape.file)}return{missing,requeue:ordered.map(({file})=>file).filter((file)=>requeue.has(file)),refused:[...why].map(([table,reason])=>({table,reason}))}}export async function auditMissingTables(options={}){const dialect=options.dialect??await currentDialect();if(dialect==="other")return{supported:!1,missing:[],live:new Set};const dir=migrationsDir(options.dir),run=options.run??await defaultRunner(),recorded=new Set(await readLedger(run)),gate=await resolveGate(options,dir),files=listMigrationFiles(dir).filter((file)=>recorded.has(file)&&!gate?.exclude.has(file)).flatMap((file)=>{try{const sql=readFileSync(join(dir,file),"utf8");return[{file,sql:gate?gate.rewrite(sql):sql,recorded:!0}]}catch{return[]}}),live=await readLiveSchema(dialect,run);return{supported:!0,missing:planRequeue(files,live.tables).missing,live:live.tables}}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(),gate=await resolveGate(options,migrationsDir(options.dir)),audit=await auditMigrationLedger({dir:options.dir,dialect:options.dialect,run,gate}),result={remapped:[],recorded:[],pruned:[],requeued:[],retired:[],skipped:[]};if(!audit.supported){result.skipped.push({file:"*",reason:`dialect "${audit.dialect}" is not audited`});return result}const written=new Set(options.regenerated?.written??[]),removedByRegeneration=new Set(options.regenerated?.removed??[]),onDisk=new Set([...audit.entries.map((entry)=>entry.file),...audit.gated]),plan={...audit.remapPlan,remap:audit.remapPlan.remap.filter((r)=>!written.has(r.to))},retirable=[...audit.remapPlan.remap.filter((r)=>written.has(r.to)).map((r)=>r.from),...audit.remapPlan.dropped,...audit.remapPlan.superseded,...audit.remapPlan.ambiguous].filter((row)=>removedByRegeneration.has(row)&&!onDisk.has(row)&&SAFE_MIGRATION_FILE.test(row)),retiring=new Set(retirable);for(const row of plan.ambiguous.filter((row)=>!retiring.has(row)))result.skipped.push({file:row,reason:"ledger row matches more than one file by logical name"});for(const row of plan.dropped.filter((row)=>!retiring.has(row)))result.skipped.push({file:row,reason:"recorded migration no longer exists on disk"});for(const{from}of audit.remapPlan.remap.filter((r)=>written.has(r.to)&&!retiring.has(r.from)))result.skipped.push({file:from,reason:"its file was replaced by a regenerated one; not repointed, because the contents differ"});const prunable=plan.superseded.filter((row)=>SAFE_MIGRATION_FILE.test(row)&&!retiring.has(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=[],revertedEntries=[];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:written.has(entry.file)?`${entry.present.length}/${entry.effects.length} effects present - left pending; run \`buddy migrate:align\` first so the next migrate does not duplicate what exists under another name`:`${entry.present.length}/${entry.effects.length} effects present - resolve by hand, or pass --include-partial`});continue}if(entry.status==="reverted")revertedEntries.push(entry)}const toRequeue=[];if(options.requeueReverted){const live=await readLiveSchema(audit.dialect,run),recordedFiles=new Set(audit.entries.filter((entry)=>entry.recorded).map((entry)=>entry.file)),corpus=audit.entries.flatMap((entry)=>{try{const sql=readFileSync(join(audit.dir,entry.file),"utf8");return[{file:entry.file,sql:gate?gate.rewrite(sql):sql,recorded:recordedFiles.has(entry.file)}]}catch{return[]}}),plan=planRequeue(corpus,live.tables);toRequeue.push(...plan.requeue);for(const{table,reason}of plan.refused)result.skipped.push({file:table,reason:`missing table not rebuilt: ${reason}`})}for(const entry of revertedEntries.filter((entry)=>!toRequeue.includes(entry.file))){if(written.has(entry.file)){result.skipped.push({file:entry.file,reason:`recorded under the same name as the file it replaced, but ${entry.absent.length} effect(s) of the regenerated file are missing - \`buddy migrate:align\` creates them`});continue}result.skipped.push({file:entry.file,reason:options.requeueReverted?`recorded, but ${entry.absent.length} effect(s) are missing from the schema`:`recorded, but ${entry.absent.length} effect(s) are missing from the schema - --requeue-reverted rebuilds a table that is gone entirely`})}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)),requeueable=toRequeue.filter((file)=>!unsafe(file));for(const file of toRequeue.filter(unsafe))result.skipped.push({file,reason:"migration filename is not safe to write to the ledger"});if(options.dryRun){result.remapped=remapped;result.recorded=recordable;result.pruned=prunable;result.requeued=requeueable;result.retired=retirable;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)}for(const row of retirable){await run(`DELETE FROM migrations WHERE migration = '${row}'`);result.retired.push(row)}for(const file of requeueable){await run(`DELETE FROM migrations WHERE migration = '${file}'`);result.requeued.push(file)}return result}
|
|
1
|
+
import{existsSync,readdirSync,readFileSync}from"node:fs";import{join}from"node:path";import{resolvedDatabaseDriver}from"./dialect";import{comparableIndexKey,postgresIndexStatement}from"./index-shape";import{resolveMigrationDirectory}from"./migration-path";import{sqlStatementsOf}from"./sql-statements";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)},shapes=new Map;for(const statement of sqlStatementsOf(sql)){const match=/^\s*CREATE\s+(?:UNIQUE\s+)?INDEX\s+(?:CONCURRENTLY\s+)?(?:IF\s+NOT\s+EXISTS\s+)?["`[]?([\w$]+)/i.exec(statement),key=match?.[1]?comparableIndexKey(statement):void 0;if(match?.[1]&&key)shapes.set(match[1].toLowerCase(),key)}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]){const shape=shapes.get(index[1].toLowerCase());push({kind:"index",name:index[1],...index[2]?{table:index[2]}:{},...shape?{shape}:{}});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,indexShapes: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, sql FROM sqlite_master WHERE type='index' AND name NOT LIKE 'sqlite_%'")){const name=pick(row,"name");if(name)schema.indexes.add(name.toLowerCase());const key=comparableIndexKey(pick(row,"sql"));if(key)schema.indexShapes.add(key)}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, indexdef AS definition FROM pg_indexes WHERE schemaname = 'public'")){const name=pick(row,"name","indexname");if(name)schema.indexes.add(name.toLowerCase());const key=comparableIndexKey(postgresIndexStatement(pick(row,"definition","indexdef")));if(key)schema.indexShapes.add(key)}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;if(effect.shape&&schema.indexShapes.has(effect.shape))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=resolvedDatabaseDriver().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[]}}function currentDialect(){const driver=resolvedDatabaseDriver().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??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:[],gated:[],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),gate=await resolveGate(options,dir),sources=new Map,gated=files.filter((file)=>gate?.exclude.has(file));for(const file of files){if(gate?.exclude.has(file))continue;try{const sql=readFileSync(join(dir,file),"utf8");sources.set(file,gate?gate.rewrite(sql):sql)}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),...gated].sort(),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,gated,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{}}const Q=(pattern)=>new RegExp(pattern.replaceAll("{id}",IDENT),"i"),CREATE_TABLE=Q(String.raw`^CREATE\s+(?:TEMP(?:ORARY)?\s+)?TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?{id}`),DROP_TABLE=Q(String.raw`^DROP\s+TABLE\s+(?:IF\s+EXISTS\s+)?{id}`),RENAME_TABLE=Q(String.raw`^ALTER\s+TABLE\s+(?:IF\s+EXISTS\s+)?(?:ONLY\s+)?{id}\s+RENAME\s+TO\s+{id}`),ALTER_TABLE=Q(String.raw`^ALTER\s+TABLE\s+(?:IF\s+EXISTS\s+)?(?:ONLY\s+)?{id}`),CREATE_INDEX=Q(String.raw`^CREATE\s+(?:UNIQUE\s+)?INDEX\s+(?:CONCURRENTLY\s+)?(?:IF\s+NOT\s+EXISTS\s+)?{id}\s+ON\s+(?:ONLY\s+)?{id}`),DROP_INDEX=Q(String.raw`^DROP\s+INDEX\s+(?:CONCURRENTLY\s+)?(?:IF\s+EXISTS\s+)?{id}(?:\s+ON\s+{id})?`),DML=Q(String.raw`^(?:INSERT\s+(?:OR\s+\w+\s+)?INTO|REPLACE\s+INTO|UPDATE|DELETE\s+FROM)\s+{id}`),NEUTRAL=/^(?:BEGIN|COMMIT|END|PRAGMA|SET|SELECT\s+1|COMMENT\s+ON|CREATE\s+TYPE\s+\S+\s+AS\s+ENUM|ALTER\s+TYPE\s+\S+\s+ADD\s+VALUE|STACKS_ENUM_GUARD)\b/i;function statementShape(statement,indexTables){const lower=(name)=>(name??"").toLowerCase();if(NEUTRAL.test(statement))return{modifies:[]};let m=RENAME_TABLE.exec(statement);if(m)return{modifies:[lower(m[1]),lower(m[2])],rename:{from:lower(m[1]),to:lower(m[2])}};m=CREATE_TABLE.exec(statement);if(m)return{modifies:[lower(m[1])],creates:lower(m[1])};m=DROP_TABLE.exec(statement);if(m)return{modifies:[lower(m[1])],drops:lower(m[1])};m=ALTER_TABLE.exec(statement);if(m)return{modifies:[lower(m[1])]};m=CREATE_INDEX.exec(statement);if(m)return{modifies:[lower(m[2])],index:{name:lower(m[1]),table:lower(m[2])}};m=DROP_INDEX.exec(statement);if(m){const table=m[2]?lower(m[2]):indexTables.get(lower(m[1]));return table?{modifies:[table]}:{modifies:[],unknown:!0}}m=DML.exec(statement);if(m)return{modifies:[lower(m[1])]};return{modifies:[],unknown:!0}}function requeueStatements(sql){return stripForEffects(sql).replace(/\bDO\s+\$(\w*)\$([\s\S]*?)\$\1\$/gi,(block,_tag,body)=>/^\s*BEGIN\s+CREATE\s+TYPE\b[\s\S]*\bEXCEPTION\s+WHEN\s+duplicate_object\s+THEN\s+null\s*;\s*END\s*$/i.test(body)?"STACKS_ENUM_GUARD":block).split(";").map((statement)=>statement.trim()).filter(Boolean)}async function resolveGate(options,dir){if(options.gate||options.dir)return options.gate;try{const{featureGateForLedger}=await import("./migrations");return await featureGateForLedger(dir)}catch{return}}export function planRequeue(files,liveTables){const ordered=[...files].sort((a,b)=>a.file.localeCompare(b.file)),indexTables=new Map,expected=new Set,lineageOf=new Map,lineage=(table)=>{let group=lineageOf.get(table);if(!group){group=new Set([table]);lineageOf.set(table,group)}return group},link=(a,b)=>{const merged=new Set([...lineage(a),...lineage(b)]);for(const name of merged)lineageOf.set(name,merged)},shapes=ordered.map(({file,sql,recorded})=>{const statements=requeueStatements(sql).map((statement)=>statementShape(statement,indexTables));for(const shape of statements){if(shape.index)indexTables.set(shape.index.name,shape.index.table);if(shape.creates)expected.add(shape.creates);if(shape.drops)expected.delete(shape.drops);if(shape.rename){expected.delete(shape.rename.from);expected.add(shape.rename.to);link(shape.rename.from,shape.rename.to)}}const created=new Set(statements.flatMap((shape)=>shape.creates?[shape.creates]:[])),removed=new Set(statements.flatMap((shape)=>[shape.drops,shape.rename?.from].filter((t)=>Boolean(t)))),scaffold=new Set([...created].filter((table)=>removed.has(table)||statements.some((shape)=>shape.rename?.from===table)));return{file,recorded,unknown:statements.some((shape)=>shape.unknown),modifies:new Set(statements.flatMap((shape)=>shape.modifies)),scaffold}}),missing=[...expected].filter((table)=>!liveTables.has(table)).sort(),rebuildable=new Set(missing.flatMap((table)=>[...lineage(table)]).filter((table)=>!liveTables.has(table))),chains=new Map(missing.map((table)=>{const names=lineage(table);return[table,shapes.filter((shape)=>[...shape.modifies].some((name)=>names.has(name)))]})),why=new Map;for(const[table,chain]of chains)for(const shape of chain){if(shape.unknown){why.set(table,`${shape.file} contains a statement the planner cannot classify`);break}const elsewhere=[...shape.modifies].filter((t)=>!rebuildable.has(t)&&!shape.scaffold.has(t));if(elsewhere.length>0){why.set(table,`${shape.file} also changes ${elsewhere.join(", ")}, which ${elsewhere.length===1?"is":"are"} not missing`);break}}let changed=!0;while(changed){changed=!1;for(const[table,chain]of chains){if(why.has(table))continue;const blocker=chain.flatMap((shape)=>[...shape.modifies]).find((other)=>other!==table&&missing.some((t)=>why.has(t)&&lineage(t).has(other)));if(blocker){why.set(table,`shares a migration with ${blocker}, which cannot be rebuilt`);changed=!0}}}const requeue=new Set;for(const[table,chain]of chains){if(why.has(table))continue;for(const shape of chain)if(shape.recorded)requeue.add(shape.file)}return{missing,requeue:ordered.map(({file})=>file).filter((file)=>requeue.has(file)),refused:[...why].map(([table,reason])=>({table,reason}))}}export async function auditMissingTables(options={}){const dialect=options.dialect??currentDialect();if(dialect==="other")return{supported:!1,missing:[],live:new Set};const dir=migrationsDir(options.dir),run=options.run??await defaultRunner(),recorded=new Set(await readLedger(run)),gate=await resolveGate(options,dir),files=listMigrationFiles(dir).filter((file)=>recorded.has(file)&&!gate?.exclude.has(file)).flatMap((file)=>{try{const sql=readFileSync(join(dir,file),"utf8");return[{file,sql:gate?gate.rewrite(sql):sql,recorded:!0}]}catch{return[]}}),live=await readLiveSchema(dialect,run);return{supported:!0,missing:planRequeue(files,live.tables).missing,live:live.tables}}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(),gate=await resolveGate(options,migrationsDir(options.dir)),audit=await auditMigrationLedger({dir:options.dir,dialect:options.dialect,run,gate}),result={remapped:[],recorded:[],pruned:[],requeued:[],retired:[],skipped:[]};if(!audit.supported){result.skipped.push({file:"*",reason:`dialect "${audit.dialect}" is not audited`});return result}const written=new Set(options.regenerated?.written??[]),removedByRegeneration=new Set(options.regenerated?.removed??[]),onDisk=new Set([...audit.entries.map((entry)=>entry.file),...audit.gated]),plan={...audit.remapPlan,remap:audit.remapPlan.remap.filter((r)=>!written.has(r.to))},retirable=[...audit.remapPlan.remap.filter((r)=>written.has(r.to)).map((r)=>r.from),...audit.remapPlan.dropped,...audit.remapPlan.superseded,...audit.remapPlan.ambiguous].filter((row)=>removedByRegeneration.has(row)&&!onDisk.has(row)&&SAFE_MIGRATION_FILE.test(row)),retiring=new Set(retirable);for(const row of plan.ambiguous.filter((row)=>!retiring.has(row)))result.skipped.push({file:row,reason:"ledger row matches more than one file by logical name"});for(const row of plan.dropped.filter((row)=>!retiring.has(row)))result.skipped.push({file:row,reason:"recorded migration no longer exists on disk"});for(const{from}of audit.remapPlan.remap.filter((r)=>written.has(r.to)&&!retiring.has(r.from)))result.skipped.push({file:from,reason:"its file was replaced by a regenerated one; not repointed, because the contents differ"});const prunable=plan.superseded.filter((row)=>SAFE_MIGRATION_FILE.test(row)&&!retiring.has(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=[],revertedEntries=[];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:written.has(entry.file)?`${entry.present.length}/${entry.effects.length} effects present - left pending; run \`buddy migrate:align\` first so the next migrate does not duplicate what exists under another name`:`${entry.present.length}/${entry.effects.length} effects present - resolve by hand, or pass --include-partial`});continue}if(entry.status==="reverted")revertedEntries.push(entry)}const toRequeue=[];if(options.requeueReverted){const live=await readLiveSchema(audit.dialect,run),recordedFiles=new Set(audit.entries.filter((entry)=>entry.recorded).map((entry)=>entry.file)),corpus=audit.entries.flatMap((entry)=>{try{const sql=readFileSync(join(audit.dir,entry.file),"utf8");return[{file:entry.file,sql:gate?gate.rewrite(sql):sql,recorded:recordedFiles.has(entry.file)}]}catch{return[]}}),plan=planRequeue(corpus,live.tables);toRequeue.push(...plan.requeue);for(const{table,reason}of plan.refused)result.skipped.push({file:table,reason:`missing table not rebuilt: ${reason}`})}for(const entry of revertedEntries.filter((entry)=>!toRequeue.includes(entry.file))){if(written.has(entry.file)){result.skipped.push({file:entry.file,reason:`recorded under the same name as the file it replaced, but ${entry.absent.length} effect(s) of the regenerated file are missing - \`buddy migrate:align\` creates them`});continue}result.skipped.push({file:entry.file,reason:options.requeueReverted?`recorded, but ${entry.absent.length} effect(s) are missing from the schema`:`recorded, but ${entry.absent.length} effect(s) are missing from the schema - --requeue-reverted rebuilds a table that is gone entirely`})}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)),requeueable=toRequeue.filter((file)=>!unsafe(file));for(const file of toRequeue.filter(unsafe))result.skipped.push({file,reason:"migration filename is not safe to write to the ledger"});if(options.dryRun){result.remapped=remapped;result.recorded=recordable;result.pruned=prunable;result.requeued=requeueable;result.retired=retirable;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)}for(const row of retirable){await run(`DELETE FROM migrations WHERE migration = '${row}'`);result.retired.push(row)}for(const file of requeueable){await run(`DELETE FROM migrations WHERE migration = '${file}'`);result.requeued.push(file)}return result}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{log}from"@stacksjs/logging";import{
|
|
1
|
+
import{log}from"@stacksjs/logging";import{db}from"./utils";import{sqlHelpers}from"./sql-helpers";import{dialectCapabilities,indexSqlForDialect,isDuplicateIndexError,resolvedDatabaseDriver}from"./dialect";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 notificationTablePreflight(sql){const preflight=[];for(const table of notificationTableNames){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){preflight.push({table,reason:"no-create-statement"});continue}const referencePattern=new RegExp(`\\b${escaped}\\b`,"i"),beforeCreate=sql.slice(0,create.index);if(referencePattern.test(beforeCreate))preflight.push({table,reason:"referenced-before-create"})}return preflight}export function preflightShapeWarning(entry){if(entry.reason!=="referenced-before-create")return null;return`[notifications] "${entry.table}" is created by your own migrations, but an earlier migration reads it first, so the framework has to create it before the batch runs. Your CREATE TABLE IF NOT EXISTS then finds the table already there and does nothing, leaving it with the framework's columns rather than the ones your model declares, so the first index or insert on a column only you declare fails with "column does not exist". Move whatever reads "${entry.table}" after the migration that creates it.`}async function warnOnShapeMismatch(entry){const table=entry.table,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{const warning=preflightShapeWarning(entry);if(warning)log.warn(warning)}}export async function migrateNotificationTables(options={}){const dbDriver=
|
|
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 notificationTablePreflight(sql){const preflight=[];for(const table of notificationTableNames){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){preflight.push({table,reason:"no-create-statement"});continue}const referencePattern=new RegExp(`\\b${escaped}\\b`,"i"),beforeCreate=sql.slice(0,create.index);if(referencePattern.test(beforeCreate))preflight.push({table,reason:"referenced-before-create"})}return preflight}export function preflightShapeWarning(entry){if(entry.reason!=="referenced-before-create")return null;return`[notifications] "${entry.table}" is created by your own migrations, but an earlier migration reads it first, so the framework has to create it before the batch runs. Your CREATE TABLE IF NOT EXISTS then finds the table already there and does nothing, leaving it with the framework's columns rather than the ones your model declares, so the first index or insert on a column only you declare fails with "column does not exist". Move whatever reads "${entry.table}" after the migration that creates it.`}async function warnOnShapeMismatch(entry){const table=entry.table,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{const warning=preflightShapeWarning(entry);if(warning)log.warn(warning)}}export async function migrateNotificationTables(options={}){const dbDriver=resolvedDatabaseDriver(),sql=sqlHelpers(dbDriver),requested=new Map;for(const entry of options.tables??notificationTableNames){const normalized=typeof entry==="string"?{table:entry,reason:"no-create-statement"}:entry;requested.set(normalized.table,normalized)}if(options.verbose)log.info(`Creating notification tables for ${dbDriver}...`);try{const notifications=requested.get("notifications");if(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)}const preferences=requested.get("notification_preferences");if(preferences){if(options.verbose)log.info("Creating notification_preferences table...");await warnOnShapeMismatch(preferences);await db.unsafe(notificationPreferencesTableSql(sql)).execute();await createIndex("CREATE INDEX IF NOT EXISTS idx_notification_preferences_user ON notification_preferences (user_id)",dbDriver)}const deliveries=requested.get("notification_deliveries");if(deliveries){if(options.verbose)log.info("Creating notification deliveries table...");await warnOnShapeMismatch(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=resolvedDatabaseDriver();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)})`)}}}
|
package/dist/rbac-tables.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{log}from"@stacksjs/logging";import{
|
|
1
|
+
import{log}from"@stacksjs/logging";import{db}from"./utils";import{resolvedDatabaseDriver}from"./dialect";import{sqlHelpers}from"./sql-helpers";export function rolesTableSql(sql){const{pkColumn,nullableTimestamp,datetime,utcNow}=sql;return`CREATE TABLE IF NOT EXISTS roles (
|
|
2
2
|
${pkColumn},
|
|
3
3
|
name VARCHAR(255) NOT NULL,
|
|
4
4
|
guard_name VARCHAR(255) NOT NULL DEFAULT 'web',
|
|
@@ -29,4 +29,4 @@ import{log}from"@stacksjs/logging";import{env as envVars}from"@stacksjs/env";imp
|
|
|
29
29
|
permission_id INTEGER NOT NULL,
|
|
30
30
|
created_at ${datetime} DEFAULT ${utcNow},
|
|
31
31
|
PRIMARY KEY (role_id, permission_id)
|
|
32
|
-
)`}export async function migrateRbacTables(options={}){const dbDriver=
|
|
32
|
+
)`}export async function migrateRbacTables(options={}){const dbDriver=resolvedDatabaseDriver(),sql=sqlHelpers(dbDriver);if(options.verbose)log.info(`Creating RBAC tables for ${dbDriver}...`);try{if(options.verbose)log.info("Creating roles table...");await db.unsafe(rolesTableSql(sql)).execute();if(options.verbose)log.info("Creating permissions table...");await db.unsafe(permissionsTableSql(sql)).execute();if(options.verbose)log.info("Creating user_roles pivot...");await db.unsafe(userRolesTableSql(sql)).execute();if(options.verbose)log.info("Creating user_permissions pivot...");await db.unsafe(userPermissionsTableSql(sql)).execute();if(options.verbose)log.info("Creating role_permissions pivot...");await db.unsafe(rolePermissionsTableSql(sql)).execute();if(options.verbose)log.success("RBAC tables created");return{success:!0}}catch(error){const message=error instanceof Error?error.message:String(error);log.error(`Failed to create RBAC tables: ${message}`);return{success:!1,error:message}}}
|
package/dist/safe-migrations.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import{mutationCount}from"./affected-rows";import{dialectCapabilities,isKnownDialect}from"./dialect";import{db}from"./utils";import{resolvedDatabaseDriver}from"./dialect";export async function addColumnSafely(db,tableName,columnName,options){const{type,defaultValue,notNull=!1,batchSize=1000}=options,dbAny=db,wire=wireProtocol(),defaultSql=defaultValue===void 0?"":` DEFAULT ${formatDefault(defaultValue)}`;if(wire==="sqlite"){if(notNull&&defaultValue===void 0)throw Error(`Cannot add NOT NULL column ${JSON.stringify(columnName)} to ${JSON.stringify(tableName)} on SQLite without a defaultValue: SQLite has no ALTER COLUMN, so the constraint has to be part of the ADD COLUMN, which needs a value for the existing rows.`);const notNullSql=notNull?" NOT NULL":"";await execRaw(dbAny,`ALTER TABLE ${quote(tableName)} ADD COLUMN ${quote(columnName)} ${type}${notNullSql}${defaultSql}`);return}await execRaw(dbAny,`ALTER TABLE ${quote(tableName)} ADD COLUMN ${quote(columnName)} ${type}${defaultSql}`);if(defaultValue!==void 0)await backfillInBatches(db,tableName,columnName,defaultValue,batchSize);if(notNull)await execRaw(dbAny,wire==="mysql"?`ALTER TABLE ${quote(tableName)} MODIFY COLUMN ${quote(columnName)} ${type} NOT NULL${defaultSql}`:`ALTER TABLE ${quote(tableName)} ALTER COLUMN ${quote(columnName)} SET NOT NULL`)}async function execRaw(dbAny,statement){if(typeof dbAny.unsafe==="function")return await dbAny.unsafe(statement);throw TypeError(`This database connection exposes no \`unsafe()\`, which is the only way these safe-migration helpers can run raw DDL. Statement: ${statement}`)}export async function backfillInBatches(db,tableName,columnName,value,batchSize=1000){if(value===null)return;const dbAny=db,wire=wireProtocol();let updated=0;do{const batchSql=wire==="mysql"?`
|
|
2
2
|
UPDATE ${quote(tableName)} SET ${quote(columnName)} = ${formatDefault(value)}
|
|
3
3
|
WHERE ${quote(columnName)} IS NULL
|
|
4
4
|
LIMIT ${batchSize}
|
|
@@ -10,4 +10,4 @@ import process from"node:process";import{env as envVars}from"@stacksjs/env";impo
|
|
|
10
10
|
WHERE ${quote(columnName)} IS NULL
|
|
11
11
|
LIMIT ${batchSize}
|
|
12
12
|
)
|
|
13
|
-
`;updated=mutationCount(await execRaw(dbAny,batchSql))}while(updated>0)}export async function renameColumnSafely(db,tableName,oldName,newName,options){const dbAny=db;if(options.atomic){await execRaw(dbAny,`ALTER TABLE ${quote(tableName)} RENAME COLUMN ${quote(oldName)} TO ${quote(newName)}`);return}await execRaw(dbAny,`ALTER TABLE ${quote(tableName)} ADD COLUMN ${quote(newName)} ${options.type}`);await execRaw(dbAny,`UPDATE ${quote(tableName)} SET ${quote(newName)} = ${quote(oldName)}`)}function wireProtocol(){const driver=
|
|
13
|
+
`;updated=mutationCount(await execRaw(dbAny,batchSql))}while(updated>0)}export async function renameColumnSafely(db,tableName,oldName,newName,options){const dbAny=db;if(options.atomic){await execRaw(dbAny,`ALTER TABLE ${quote(tableName)} RENAME COLUMN ${quote(oldName)} TO ${quote(newName)}`);return}await execRaw(dbAny,`ALTER TABLE ${quote(tableName)} ADD COLUMN ${quote(newName)} ${options.type}`);await execRaw(dbAny,`UPDATE ${quote(tableName)} SET ${quote(newName)} = ${quote(oldName)}`)}function wireProtocol(){const driver=resolvedDatabaseDriver().toLowerCase();return isKnownDialect(driver)?dialectCapabilities(driver).wire:"sqlite"}function quote(name){if(!/^[a-z_][a-z0-9_]*$/i.test(name))throw Error(`Refusing to quote unsafe identifier: ${JSON.stringify(name)}`);const quoteChar=wireProtocol()==="mysql"?"`":'"';return`${quoteChar}${name}${quoteChar}`}function formatDefault(value){if(value===null)return"NULL";if(typeof value==="number")return String(value);if(typeof value==="boolean")return value?"TRUE":"FALSE";return`'${String(value).replace(/'/g,"''")}'`}function rowIdColumn(wire){return wire==="postgres"?"ctid":"rowid"}
|
package/dist/trait-tables.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{log}from"@stacksjs/logging";import{
|
|
1
|
+
import{log}from"@stacksjs/logging";import{db}from"./utils";import{resolvedDatabaseDriver}from"./dialect";import{sqlHelpers}from"./sql-helpers";import{indexSqlForDialect,isDuplicateColumnError,isDuplicateIndexError}from"./dialect";export{indexSqlForDialect}from"./dialect";function createdAt(sql){return`created_at ${sql.datetime}`}function updatedAt(sql){return`updated_at ${sql.nullableTimestamp}`}export function traitTableNames(){return["chat_messages","chat_conversations","commentables","taggables","categorizables","commentable_upvotes","taggable_models","categorizable_models"]}export const UNSCOPED_OWNER_ID=0;export function commentablesTableSql(sql){const{pkColumn,boolTrue,utcNow}=sql;return`CREATE TABLE IF NOT EXISTS commentables (
|
|
2
2
|
${pkColumn},
|
|
3
3
|
title VARCHAR(255) NOT NULL,
|
|
4
4
|
body TEXT NOT NULL,
|
|
@@ -76,4 +76,4 @@ import{log}from"@stacksjs/logging";import{env as envVars}from"@stacksjs/env";imp
|
|
|
76
76
|
created_at ${sql.datetime} NOT NULL,
|
|
77
77
|
read_at ${sql.nullableTimestamp},
|
|
78
78
|
UNIQUE (conversation_id, sender_id, client_key)
|
|
79
|
-
)`]}export async function migrateTraitTables(options={}){const dbDriver=
|
|
79
|
+
)`]}export async function migrateTraitTables(options={}){const dbDriver=resolvedDatabaseDriver(),sql=sqlHelpers(dbDriver);if(options.verbose)log.info(`Creating polymorphic trait tables for ${dbDriver}...`);try{for(const statement of messagingTableSql(sql))await db.unsafe(statement).execute();if(options.verbose)log.info("Creating commentables table...");await db.unsafe(commentablesTableSql(sql)).execute();if(options.verbose)log.info("Creating taggables table...");await db.unsafe(taggablesTableSql(sql)).execute();if(options.verbose)log.info("Creating categorizables table...");await db.unsafe(categorizablesTableSql(sql)).execute();if(options.verbose)log.info("Creating commentable_upvotes table...");await db.unsafe(commentableUpvotesTableSql(sql)).execute();if(options.verbose)log.info("Creating taggable_models pivot...");await db.unsafe(taggableModelsTableSql(sql)).execute();if(options.verbose)log.info("Creating categorizable_models pivot...");await db.unsafe(categorizableModelsTableSql(sql)).execute();try{for(const{table,foreignKey}of await likeableTargets()){if(options.verbose)log.info(`Creating ${table} table...`);await db.unsafe(likesTableSql(sql,table,foreignKey)).execute()}}catch(error){log.debug(`[trait-tables] Skipped likeable tables: ${error instanceof Error?error.message:String(error)}`)}for(const{table,column,definition}of traitTableColumnGuarantees(sql))try{await db.unsafe(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`).execute();if(options.verbose)log.info(`Added missing ${table}.${column}`)}catch(error){if(!isDuplicateColumnError(error))throw error}for(const statement of traitTableIndexSql())try{await db.unsafe(indexSqlForDialect(statement,dbDriver)).execute()}catch(error){if(!isDuplicateIndexError(error))throw error}if(options.verbose)log.success("Polymorphic trait tables created");return{success:!0}}catch(error){const message=error instanceof Error?error.message:String(error);log.error(`Failed to create polymorphic trait tables: ${message}`);return{success:!1,error:message}}}
|
package/dist/unique-audit.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{path}from"@stacksjs/path";import{plural,snakeCase}from"@stacksjs/strings";import{toSqlIntrospectionDialect}from"./dialect";import{safeGlob}from"./fk-audit";export async function getDeclaredUniques(){const modelFiles=[...safeGlob(path.userModelsPath("*.ts")),...safeGlob(path.storagePath("framework/defaults/app/Models/**/*.ts"))],declared=[];for(const modelFile of modelFiles){let model;try{model=(await import(modelFile)).default}catch{continue}if(!model||typeof model!=="object")continue;const table=model.table||plural(snakeCase(model.name||"")),modelName=model.name||"",attributes=model.attributes;if(attributes&&typeof attributes==="object"){for(const[field,attr]of Object.entries(attributes))if(attr&&typeof attr==="object"&&attr.unique===!0)declared.push({table,columns:[snakeCase(field)],model:modelName,source:"attribute"})}const indexes=model.indexes;if(Array.isArray(indexes)){for(const index of indexes)if(index&&index.unique===!0&&Array.isArray(index.columns)&&index.columns.length>0)declared.push({table,columns:index.columns.map((c)=>snakeCase(c)),model:modelName,source:"index",indexName:index.name})}}return declared}export async function getLiveUniqueIndexes(dialect){const{db}=await import("./utils"),d=dialect??
|
|
1
|
+
import{path}from"@stacksjs/path";import{plural,snakeCase}from"@stacksjs/strings";import{resolvedDatabaseDriver,toSqlIntrospectionDialect}from"./dialect";import{safeGlob}from"./fk-audit";export async function getDeclaredUniques(){const modelFiles=[...safeGlob(path.userModelsPath("*.ts")),...safeGlob(path.storagePath("framework/defaults/app/Models/**/*.ts"))],declared=[];for(const modelFile of modelFiles){let model;try{model=(await import(modelFile)).default}catch{continue}if(!model||typeof model!=="object")continue;const table=model.table||plural(snakeCase(model.name||"")),modelName=model.name||"",attributes=model.attributes;if(attributes&&typeof attributes==="object"){for(const[field,attr]of Object.entries(attributes))if(attr&&typeof attr==="object"&&attr.unique===!0)declared.push({table,columns:[snakeCase(field)],model:modelName,source:"attribute"})}const indexes=model.indexes;if(Array.isArray(indexes)){for(const index of indexes)if(index&&index.unique===!0&&Array.isArray(index.columns)&&index.columns.length>0)declared.push({table,columns:index.columns.map((c)=>snakeCase(c)),model:modelName,source:"index",indexName:index.name})}}return declared}export async function getLiveUniqueIndexes(dialect){const{db}=await import("./utils"),d=dialect??currentDialect();if(d==="sqlite")return getSqliteLiveUniques(db);if(d==="mysql")return getMysqlLiveUniques(db);if(d==="postgres")return getPostgresLiveUniques(db);return[]}export async function auditUniqueIndexes(dialect){const d=dialect??currentDialect();if(d!=="sqlite"&&d!=="mysql"&&d!=="postgres")return{supported:!1,declared:[],live:[],missing:[],skippedTables:[]};const declared=await getDeclaredUniques(),live=await getLiveUniqueIndexes(d),liveTables=await getLiveTables(d),liveByTable=new Map;for(const idx of live){const t=idx.table.toLowerCase(),key=columnSetKey(idx.columns);if(!liveByTable.has(t))liveByTable.set(t,new Set);liveByTable.get(t).add(key)}const missing=[],skippedTables=new Set;for(const decl of declared){const t=decl.table.toLowerCase();if(!liveTables.has(t)){skippedTables.add(decl.table);continue}const key=columnSetKey(decl.columns);if(!(liveByTable.get(t)?.has(key)??!1))missing.push(decl)}return{supported:!0,declared,live,missing,skippedTables:[...skippedTables]}}async function getLiveTables(dialect){const{db}=await import("./utils");let rows=[];if(dialect==="sqlite"){const r=await db.unsafe("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").execute();rows=(Array.isArray(r)?r:[]).map((x)=>x.name)}else if(dialect==="mysql"){const r=await db.unsafe("SELECT TABLE_NAME AS name FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE()").execute();rows=(Array.isArray(r)?r:[]).map((x)=>x.name??x.TABLE_NAME)}else if(dialect==="postgres"){const r=await db.unsafe("SELECT tablename AS name FROM pg_tables WHERE schemaname = 'public'").execute();rows=(Array.isArray(r)?r:[]).map((x)=>x.name??x.tablename)}return new Set(rows.filter((n)=>typeof n==="string"&&n.length>0).map((n)=>n.toLowerCase()))}function columnSetKey(columns){return[...columns].map((c)=>c.toLowerCase()).sort().join(",")}function currentDialect(){return toSqlIntrospectionDialect(resolvedDatabaseDriver().toLowerCase())}async function getSqliteLiveUniques(db){const tables=await db.unsafe("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").execute(),rows=Array.isArray(tables)?tables:[],out=[];for(const row of rows){const table=row.name;if(!table)continue;if(!/^[a-z_]\w*$/i.test(table))continue;const indexRows=await db.unsafe(`PRAGMA index_list("${table}")`).execute();for(const idx of Array.isArray(indexRows)?indexRows:[]){const r=idx;if(Number(r.unique)!==1||!r.name)continue;if(!/^[a-z_]\w*$/i.test(r.name))continue;const colRows=await db.unsafe(`PRAGMA index_info("${r.name}")`).execute(),columns=(Array.isArray(colRows)?colRows:[]).map((c)=>String(c.name??"")).filter((c)=>c.length>0);if(columns.length>0)out.push({table,name:r.name,columns})}}return out}async function getMysqlLiveUniques(db){const rows=await db.unsafe(`
|
|
2
2
|
SELECT TABLE_NAME, INDEX_NAME, COLUMN_NAME, SEQ_IN_INDEX
|
|
3
3
|
FROM information_schema.STATISTICS
|
|
4
4
|
WHERE TABLE_SCHEMA = DATABASE()
|
package/dist/utc-defaults.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import
|
|
1
|
+
import{log}from"@stacksjs/logging";import{db}from"./utils";import{dialectCapabilities,resolvedDatabaseDriver}from"./dialect";import{frameworkDatetimeTables}from"./datetime-columns";import{sqlHelpers}from"./sql-helpers";export function utcDefaultTables(){return[...frameworkDatetimeTables(),"migrations"]}export async function findSessionClockDefaults(driver=resolvedDatabaseDriver()){const schema=dialectCapabilities(driver).wire==="mysql"?"DATABASE()":"current_schema()",safe=/^[a-z_]\w*$/i,names=utcDefaultTables().filter((table)=>safe.test(table)).map((table)=>`'${table}'`).join(", ");return(await db.unsafe(`SELECT table_name, column_name
|
|
2
2
|
FROM information_schema.columns
|
|
3
3
|
WHERE table_schema = ${schema}
|
|
4
4
|
AND table_name IN (${names})
|
|
5
5
|
AND data_type LIKE 'timestamp%'
|
|
6
6
|
AND column_default IS NOT NULL
|
|
7
7
|
AND LOWER(column_default) NOT LIKE '%utc%'
|
|
8
|
-
AND (LOWER(column_default) LIKE '%current_timestamp%' OR LOWER(column_default) = 'now()')`).execute()).map((row)=>({table:String(row.table_name??row.TABLE_NAME),column:String(row.column_name??row.COLUMN_NAME)}))}export function utcDefaultSql(target,driver=
|
|
8
|
+
AND (LOWER(column_default) LIKE '%current_timestamp%' OR LOWER(column_default) = 'now()')`).execute()).map((row)=>({table:String(row.table_name??row.TABLE_NAME),column:String(row.column_name??row.COLUMN_NAME)}))}export function utcDefaultSql(target,driver=resolvedDatabaseDriver()){const safe=/^[a-z_]\w*$/i;if(!safe.test(target.table)||!safe.test(target.column))throw Error(`[utc-defaults] Refusing to alter unsafe identifier: ${target.table}.${target.column}`);return`ALTER TABLE ${target.table} ALTER COLUMN ${target.column} SET DEFAULT ${sqlHelpers(driver).utcNow}`}export async function ensureUtcTimestampDefaults(options={}){const driver=resolvedDatabaseDriver();if(dialectCapabilities(driver).wire==="sqlite")return{success:!0,pinned:0};try{const targets=await findSessionClockDefaults(driver);if(targets.length===0){if(options.verbose)log.info("No timestamp defaults left to pin to UTC");return{success:!0,pinned:0}}for(const target of targets){if(options.verbose)log.info(`Pinning ${target.table}.${target.column} default to UTC...`);await db.unsafe(utcDefaultSql(target,driver)).execute()}log.debug(`[utc-defaults] Pinned ${targets.length} timestamp default(s) to UTC`);return{success:!0,pinned:targets.length}}catch(error){const message=error instanceof Error?error.message:String(error);log.debug(`[utc-defaults] Could not pin timestamp defaults: ${message}`);return{success:!1,pinned:0,error:message}}}
|
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{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);const envDbConfig=()=>({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:""}}});let dbConfig=envDbConfig(),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){const baseline=envDbConfig(),supplied=config.database.connections,connections={...baseline.connections};for(const[name,connection]of Object.entries(supplied))if(connection&&typeof connection==="object")connections[name]={...connections[name]??{},...connection};dbConfig={...config.database,connections}}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}export function resolvedDatabaseConfig(){return{driver:dbDriver,connections:dbConfig.connections}}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};
|
|
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,setConfiguredDatabaseDriver,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);const envDbConfig=()=>({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:""}}});let dbConfig=envDbConfig(),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);setConfiguredDatabaseDriver(dbDriver)}queryBuilderDialect=toQueryBuilderDialect(dbDriver);if(config?.database?.connections){const baseline=envDbConfig(),supplied=config.database.connections,connections={...baseline.connections};for(const[name,connection]of Object.entries(supplied))if(connection&&typeof connection==="object")connections[name]={...connections[name]??{},...connection};dbConfig={...config.database,connections}}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}export function resolvedDatabaseConfig(){return{driver:dbDriver,connections:dbConfig.connections}}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/dist/uuid-columns.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import
|
|
1
|
+
import{log}from"@stacksjs/logging";import{path}from"@stacksjs/path";import{getTableName}from"@stacksjs/model-meta";import{fs}from"@stacksjs/storage";import{db}from"./utils";import{resolvedDatabaseDriver}from"./dialect";import{sqlHelpers}from"./sql-helpers";function uuidColumnType(sql){if(sql.isPostgres)return"UUID";if(sql.isMysql)return"VARCHAR(255)";return"TEXT"}export function uuidColumnSql(table,sql){return`ALTER TABLE ${table} ADD COLUMN uuid ${uuidColumnType(sql)}`}async function loadModelsFrom(dir){const out=[];if(!fs.existsSync(dir))return out;const entries=fs.readdirSync(dir,{withFileTypes:!0});for(const entry of entries){const fullPath=path.join(dir,entry.name);if(entry.isDirectory()){out.push(...await loadModelsFrom(fullPath));continue}if(!entry.name.endsWith(".ts"))continue;if(entry.name.startsWith("_")||entry.name.startsWith("index"))continue;try{const imported=(await import(fullPath)).default;if(imported?.name||imported?.table)out.push({filePath:fullPath,model:imported})}catch{}}return out}export async function findUuidTables(){const dirs=[path.userModelsPath(),path.frameworkPath("defaults/app/Models")],tables=new Set;for(const dir of dirs)for(const{filePath,model}of await loadModelsFrom(dir)){if(!model.traits?.useUuid)continue;tables.add(getTableName(model,filePath))}return[...tables]}export async function ensureUuidColumns(sql,options={}){const tables=await findUuidTables();for(const table of tables){try{await db.unsafe(uuidColumnSql(table,sql)).execute();if(options.verbose)log.debug(`[uuid-columns] Added uuid column to ${table}`)}catch{if(options.verbose)log.debug(`[uuid-columns] Skipped (already applied or ${table} missing): uuid column`);continue}if(sql.isSqlite)await reindexAfterAddingUuid(table)}}async function reindexAfterAddingUuid(table){try{await db.unsafe(`REINDEX "${table.replace(/"/g,'""')}"`).execute()}catch(error){log.warn(`[uuid-columns] Added uuid to ${table} but could not rebuild its indexes: ${error instanceof Error?error.message:String(error)}. Run REINDEX "${table}" before writing to it.`)}}export async function ensureUuidColumnsForCurrentDriver(options={}){await ensureUuidColumns(sqlHelpers(resolvedDatabaseDriver()),options)}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@stacksjs/database",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"sideEffects": false,
|
|
5
|
-
"version": "0.75.
|
|
5
|
+
"version": "0.75.153",
|
|
6
6
|
"description": "The Stacks database integration.",
|
|
7
7
|
"author": "Chris Breuer",
|
|
8
8
|
"contributors": [
|
|
@@ -66,26 +66,26 @@
|
|
|
66
66
|
"prepublishOnly": "bun run build"
|
|
67
67
|
},
|
|
68
68
|
"dependencies": {
|
|
69
|
-
"@stacksjs/config": "0.75.
|
|
70
|
-
"@stacksjs/env": "0.75.
|
|
71
|
-
"@stacksjs/error-handling": "0.75.
|
|
72
|
-
"@stacksjs/faker": "0.75.
|
|
73
|
-
"@stacksjs/features": "0.75.
|
|
74
|
-
"@stacksjs/logging": "0.75.
|
|
75
|
-
"@stacksjs/model-meta": "0.75.
|
|
76
|
-
"@stacksjs/path": "0.75.
|
|
77
|
-
"@stacksjs/query-builder": "0.75.
|
|
78
|
-
"@stacksjs/security": "0.75.
|
|
79
|
-
"@stacksjs/storage": "0.75.
|
|
80
|
-
"@stacksjs/strings": "0.75.
|
|
69
|
+
"@stacksjs/config": "0.75.153",
|
|
70
|
+
"@stacksjs/env": "0.75.153",
|
|
71
|
+
"@stacksjs/error-handling": "0.75.153",
|
|
72
|
+
"@stacksjs/faker": "0.75.153",
|
|
73
|
+
"@stacksjs/features": "0.75.153",
|
|
74
|
+
"@stacksjs/logging": "0.75.153",
|
|
75
|
+
"@stacksjs/model-meta": "0.75.153",
|
|
76
|
+
"@stacksjs/path": "0.75.153",
|
|
77
|
+
"@stacksjs/query-builder": "0.75.153",
|
|
78
|
+
"@stacksjs/security": "0.75.153",
|
|
79
|
+
"@stacksjs/storage": "0.75.153",
|
|
80
|
+
"@stacksjs/strings": "0.75.153",
|
|
81
81
|
"@stacksjs/ts-validation": "^0.5.6",
|
|
82
82
|
"bun-query-builder": "^0.3.8",
|
|
83
83
|
"dynamodb-tooling": "^0.3.6"
|
|
84
84
|
},
|
|
85
85
|
"devDependencies": {
|
|
86
|
-
"@stacksjs/cli": "0.75.
|
|
87
|
-
"@stacksjs/router": "0.75.
|
|
88
|
-
"@stacksjs/utils": "0.75.
|
|
86
|
+
"@stacksjs/cli": "0.75.153",
|
|
87
|
+
"@stacksjs/router": "0.75.153",
|
|
88
|
+
"@stacksjs/utils": "0.75.153",
|
|
89
89
|
"better-dx": "^0.2.24"
|
|
90
90
|
}
|
|
91
91
|
}
|