@stacksjs/database 0.74.46 → 0.74.47
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/affected-rows.d.ts +57 -0
- package/dist/affected-rows.js +1 -0
- package/dist/auth-tables.js +16 -10
- package/dist/connection-lifecycle.d.ts +18 -0
- package/dist/connection-lifecycle.js +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1 -1
- package/dist/migrations.d.ts +13 -13
- package/dist/migrations.js +1 -1
- package/dist/query-log-bindings.d.ts +52 -0
- package/dist/query-log-bindings.js +2 -0
- package/dist/query-logger.js +2 -2
- package/dist/replicas.js +1 -1
- package/dist/safe-migrations.d.ts +3 -2
- package/dist/safe-migrations.js +8 -4
- package/dist/transaction-context.d.ts +9 -7
- package/dist/transaction-context.js +1 -1
- package/dist/utils.d.ts +28 -4
- package/dist/utils.js +1 -1
- package/package.json +17 -17
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How many rows a write statement affected, whichever driver ran it.
|
|
3
|
+
*
|
|
4
|
+
* Every entry point spells this differently, and the spellings are not
|
|
5
|
+
* interchangeable. Measured through Stacks' own `db` against SQLite,
|
|
6
|
+
* PostgreSQL 16 and MySQL 8.4 on Bun 1.4:
|
|
7
|
+
*
|
|
8
|
+
* | entry point | SQLite | PostgreSQL | MySQL |
|
|
9
|
+
* | ---------------------------------- | ---------------- | ----------------------------- | ------------------------------ |
|
|
10
|
+
* | `db.unsafe` / `trx.unsafe` write | `{ changes: N }` | `{ count: N, affectedRows: null }` | `{ count: 0, affectedRows: N }` |
|
|
11
|
+
* | fluent `.execute()` | `N` | `N` | `N` |
|
|
12
|
+
* | fluent `.executeTakeFirst()` | `{ numUpdatedRows: N }` or `{ numDeletedRows: N }`, on all three |
|
|
13
|
+
*
|
|
14
|
+
* Two traps follow from that table, and each has shipped:
|
|
15
|
+
*
|
|
16
|
+
* - `rowCount` is node-postgres's name. Bun's PostgreSQL client never sets it,
|
|
17
|
+
* so `result.changes ?? result.rowCount` reads 0 on PostgreSQL AND MySQL.
|
|
18
|
+
* - On MySQL `count` is always 0 for a write. Reading `count` before
|
|
19
|
+
* `affectedRows` returns 0 there, so the order below is load-bearing, and
|
|
20
|
+
* `affectedRows` is skipped only when it is `null` (PostgreSQL), never when it
|
|
21
|
+
* is `0`.
|
|
22
|
+
*
|
|
23
|
+
* One difference no field can hide: MySQL counts rows a statement CHANGED,
|
|
24
|
+
* PostgreSQL and SQLite count rows it MATCHED. An UPDATE that writes a value a
|
|
25
|
+
* row already holds reports 0 on MySQL. Do not use this count to decide whether
|
|
26
|
+
* a row exists after an UPDATE; check existence directly.
|
|
27
|
+
*
|
|
28
|
+
* The count is also lost if a raw `unsafe` result is returned OUT of
|
|
29
|
+
* `db.transaction(...)` on PostgreSQL or MySQL: the transaction resolves to a
|
|
30
|
+
* plain array with none of these fields. Read it inside the callback.
|
|
31
|
+
*
|
|
32
|
+
* Formerly `commerce/src/utils/mutation-count.ts`, where it only served commerce.
|
|
33
|
+
*/
|
|
34
|
+
export declare function mutationCount(result: unknown): number;
|
|
35
|
+
/**
|
|
36
|
+
* The rows an UPDATE matched, which is not what its affected-row count says.
|
|
37
|
+
*
|
|
38
|
+
* MySQL counts rows a statement CHANGED; PostgreSQL and SQLite count rows it
|
|
39
|
+
* MATCHED. An UPDATE writing values a row already holds therefore reports 0
|
|
40
|
+
* on MySQL while the row is sitting right there, and Bun's MySQL client
|
|
41
|
+
* exposes no `CLIENT_FOUND_ROWS` for a connection setting to fix it. So any
|
|
42
|
+
* call site reading a count of 0 as "no such row" is wrong on MySQL:
|
|
43
|
+
* soft-deleting an order that is already CANCELED answered false, and bulk
|
|
44
|
+
* soft delete undercounted by however many rows were already in the target
|
|
45
|
+
* state (stacksjs/stacks#2639).
|
|
46
|
+
*
|
|
47
|
+
* `returning(key)` answers MATCHED on all three. Measured through Stacks `db`
|
|
48
|
+
* against SQLite, PostgreSQL 16 and MySQL 8.4.5, updating a row to the value
|
|
49
|
+
* it already holds: `.executeTakeFirst()` gave 1, 1 and 0, while this gave the
|
|
50
|
+
* id on all three. An id that does not exist gave none on all three.
|
|
51
|
+
*
|
|
52
|
+
* NOT for compare-and-set. On MySQL bun-query-builder emulates `returning` as
|
|
53
|
+
* SELECT-then-UPDATE-then-SELECT, so the ids are read before the write and a
|
|
54
|
+
* row another session changed in between still reports as matched. Guard those
|
|
55
|
+
* with a locking read inside a transaction instead.
|
|
56
|
+
*/
|
|
57
|
+
export declare function matchedRows<T = { id: number }>(update: { returning: (column: string) => { execute: () => Promise<unknown> } }, key?: string): Promise<T[]>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function mutationCount(result){if(typeof result==="number")return Number.isFinite(result)?result:0;if(typeof result==="bigint")return Number(result);if(!result||typeof result!=="object")return 0;const record=result;for(const key of["changes","affectedRows","count","numAffectedRows","numDeletedRows","numInsertedOrUpdatedRows","numUpdatedRows"])if(record[key]!==void 0&&record[key]!==null)return mutationCount(record[key]);if(Array.isArray(result))return result.reduce((total,item)=>total+mutationCount(item),0);return 0}export async function matchedRows(update,key="id"){const rows=await update.returning(key).execute();return Array.isArray(rows)?rows:[]}
|
package/dist/auth-tables.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import{randomBytes}from"node:crypto";import{log}from"@stacksjs/logging";import{env as envVars}from"@stacksjs/env";import{db}from"./utils";import{sqlHelpers}from"./sql-helpers";import{indexSqlForDialect}from"./dialect";function getDbDriver(){return envVars.DB_CONNECTION||"sqlite"}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 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 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)",getDbDriver())).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=getDbDriver(),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(`
|
|
1
|
+
import{randomBytes}from"node:crypto";import{log}from"@stacksjs/logging";import{env as envVars}from"@stacksjs/env";import{makeHash}from"@stacksjs/security";import{db}from"./utils";import{sqlHelpers}from"./sql-helpers";import{indexSqlForDialect,isDuplicateColumnError,isDuplicateIndexError}from"./dialect";function getDbDriver(){return envVars.DB_CONNECTION||"sqlite"}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 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 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)",getDbDriver())).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=getDbDriver(),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
|
+
user_id BIGINT,
|
|
4
5
|
name VARCHAR(255) NOT NULL,
|
|
5
6
|
secret VARCHAR(100),
|
|
6
7
|
provider VARCHAR(255),
|
|
@@ -11,7 +12,7 @@ import{randomBytes}from"node:crypto";import{log}from"@stacksjs/logging";import{e
|
|
|
11
12
|
created_at ${datetime} DEFAULT ${utcNow},
|
|
12
13
|
updated_at ${nullableTimestamp}
|
|
13
14
|
)
|
|
14
|
-
`).execute();if(options.verbose)log.info("Creating oauth_access_tokens table...");await db.unsafe(`
|
|
15
|
+
`).execute();try{await db.unsafe("ALTER TABLE oauth_clients ADD COLUMN user_id BIGINT").execute()}catch(error){if(!isDuplicateColumnError(error))throw error}if(options.verbose)log.info("Creating oauth_access_tokens table...");await db.unsafe(`
|
|
15
16
|
CREATE TABLE IF NOT EXISTS oauth_access_tokens (
|
|
16
17
|
${pkColumn},
|
|
17
18
|
-- The owner, polymorphically: users, authors, any table whose model
|
|
@@ -39,7 +40,7 @@ import{randomBytes}from"node:crypto";import{log}from"@stacksjs/logging";import{e
|
|
|
39
40
|
created_at ${datetime} DEFAULT ${utcNow},
|
|
40
41
|
updated_at ${nullableTimestamp}
|
|
41
42
|
)
|
|
42
|
-
`).execute();await createTokenIndex("idx_oauth_access_tokens_token","oauth_access_tokens","token");for(const alterSql of oauthAccessTokenDeviceColumnsSql())try{await db.unsafe(alterSql).execute()}catch{if(options.verbose)log.debug(`[auth-tables] Skipped (already applied): ${alterSql}`)}for(const alterSql of oauthAccessTokenTokenableColumnsSql())try{await db.unsafe(alterSql).execute()}catch{if(options.verbose)log.debug(`[auth-tables] Skipped (already applied): ${alterSql}`)}for(const backfillSql of oauthAccessTokenTokenableBackfillSql())
|
|
43
|
+
`).execute();await createTokenIndex("idx_oauth_access_tokens_token","oauth_access_tokens","token");for(const alterSql of oauthAccessTokenDeviceColumnsSql())try{await db.unsafe(alterSql).execute()}catch{if(options.verbose)log.debug(`[auth-tables] Skipped (already applied): ${alterSql}`)}for(const alterSql of oauthAccessTokenTokenableColumnsSql())try{await db.unsafe(alterSql).execute()}catch{if(options.verbose)log.debug(`[auth-tables] Skipped (already applied): ${alterSql}`)}for(const backfillSql of oauthAccessTokenTokenableBackfillSql())await db.unsafe(backfillSql).execute();if(options.verbose)log.info("Creating oauth_refresh_tokens table...");await db.unsafe(`
|
|
43
44
|
CREATE TABLE IF NOT EXISTS oauth_refresh_tokens (
|
|
44
45
|
${pkColumn},
|
|
45
46
|
access_token_id INTEGER NOT NULL,
|
|
@@ -60,7 +61,7 @@ import{randomBytes}from"node:crypto";import{log}from"@stacksjs/logging";import{e
|
|
|
60
61
|
expires_at ${nullableTimestamp},
|
|
61
62
|
created_at ${datetime} DEFAULT ${utcNow}
|
|
62
63
|
)
|
|
63
|
-
`).execute();for(const alterSql of passwordResetsExpiresAtSql())try{await db.unsafe(alterSql).execute()}catch{if(options.verbose)log.debug(`[auth-tables] Skipped (already applied): ${alterSql}`)}try{await db.unsafe(indexSqlForDialect("CREATE INDEX IF NOT EXISTS
|
|
64
|
+
`).execute();for(const alterSql of passwordResetsExpiresAtSql())try{await db.unsafe(alterSql).execute()}catch{if(options.verbose)log.debug(`[auth-tables] Skipped (already applied): ${alterSql}`)}try{await db.unsafe(indexSqlForDialect("CREATE UNIQUE INDEX IF NOT EXISTS idx_password_resets_email_unique ON password_resets(email)",dbDriver)).execute()}catch(error){if(!isDuplicateIndexError(error))throw error}if(options.verbose)log.info("Creating email_verifications table...");await db.unsafe(`
|
|
64
65
|
CREATE TABLE IF NOT EXISTS email_verifications (
|
|
65
66
|
${pkColumn},
|
|
66
67
|
user_id INTEGER NOT NULL,
|
|
@@ -68,7 +69,7 @@ import{randomBytes}from"node:crypto";import{log}from"@stacksjs/logging";import{e
|
|
|
68
69
|
expires_at ${datetime} NOT NULL,
|
|
69
70
|
created_at ${datetime} DEFAULT ${utcNow}
|
|
70
71
|
)
|
|
71
|
-
`).execute();try{await db.unsafe(indexSqlForDialect("CREATE INDEX IF NOT EXISTS
|
|
72
|
+
`).execute();try{await db.unsafe(indexSqlForDialect("CREATE UNIQUE INDEX IF NOT EXISTS idx_email_verifications_user_id_unique ON email_verifications(user_id)",dbDriver)).execute()}catch(error){if(!isDuplicateIndexError(error))throw error}if(options.verbose)log.info("Creating passkeys table...");await db.unsafe(`
|
|
72
73
|
CREATE TABLE IF NOT EXISTS passkeys (
|
|
73
74
|
id VARCHAR(255) PRIMARY KEY,
|
|
74
75
|
cred_public_key TEXT NOT NULL,
|
|
@@ -106,12 +107,17 @@ import{randomBytes}from"node:crypto";import{log}from"@stacksjs/logging";import{e
|
|
|
106
107
|
expires_at ${datetime} NOT NULL,
|
|
107
108
|
created_at ${datetime} DEFAULT ${utcNow}
|
|
108
109
|
)
|
|
109
|
-
`).execute();if(options.verbose)log.info("Ensuring users auth columns exist (users may not exist yet on a fresh install - see ensureUsersAuthColumns)...");await ensureUsersAuthColumns(sql,options);
|
|
110
|
-
|
|
111
|
-
|
|
110
|
+
`).execute();if(options.verbose)log.info("Ensuring users auth columns exist (users may not exist yet on a fresh install - see ensureUsersAuthColumns)...");await ensureUsersAuthColumns(sql,options);await db.unsafe(`SELECT id, user_id, name, secret, provider, redirect, personal_access_client,
|
|
111
|
+
password_client, revoked, created_at, updated_at FROM oauth_clients LIMIT 0`).execute();await db.unsafe(`SELECT id, tokenable_type, tokenable_id, user_id, oauth_client_id,
|
|
112
|
+
token, name, scopes, revoked, expires_at, user_agent, ip_address, created_at, updated_at
|
|
113
|
+
FROM oauth_access_tokens LIMIT 0`).execute();await db.unsafe(`SELECT id, access_token_id, token, revoked, expires_at, created_at
|
|
114
|
+
FROM oauth_refresh_tokens LIMIT 0`).execute();if(options.verbose)log.info("Ensuring personal access client exists...");if((await db.unsafe(`
|
|
115
|
+
SELECT id FROM oauth_clients
|
|
116
|
+
WHERE personal_access_client = ${boolTrue} AND revoked = ${sql.boolFalse} LIMIT 1
|
|
117
|
+
`).execute())?.length===0){const secret=randomBytes(40).toString("hex"),hashedSecret=await makeHash(secret,{algorithm:"bcrypt"});if(isPostgres)await db.unsafe(`
|
|
112
118
|
INSERT INTO oauth_clients (name, secret, provider, redirect, personal_access_client, password_client, revoked, created_at)
|
|
113
119
|
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW())
|
|
114
|
-
`,["Personal Access Client",
|
|
120
|
+
`,["Personal Access Client",hashedSecret,"local","http://localhost",!0,!1,!1]).execute();else await db.unsafe(`
|
|
115
121
|
INSERT INTO oauth_clients (name, secret, provider, redirect, personal_access_client, password_client, revoked, created_at)
|
|
116
122
|
VALUES (?, ?, ?, ?, ?, ?, ?, ${now})
|
|
117
|
-
`,["Personal Access Client",
|
|
123
|
+
`,["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=getDbDriver();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{}}}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drain every distinct database client before reporting shutdown complete.
|
|
3
|
+
*
|
|
4
|
+
* Promise.allSettled is intentional: one failed pool must not prevent the
|
|
5
|
+
* remaining primary or replica pools from releasing their sockets.
|
|
6
|
+
*/
|
|
7
|
+
export declare function closeDatabaseConnections(connections: Iterable<CloseableDatabaseConnection>): Promise<void>;
|
|
8
|
+
/**
|
|
9
|
+
* Start draining connections that reset has detached, while retaining their
|
|
10
|
+
* closure promise for a later process shutdown.
|
|
11
|
+
*/
|
|
12
|
+
export declare function retireDatabaseConnections(pending: PendingDatabaseConnectionClosures, connections: Iterable<CloseableDatabaseConnection>, onError: (error: unknown) => void): void;
|
|
13
|
+
/** Drain active connections together with every reset closure still pending. */
|
|
14
|
+
export declare function closeDatabaseConnectionsAndPending(connections: Iterable<CloseableDatabaseConnection>, pending: PendingDatabaseConnectionClosures): Promise<void>;
|
|
15
|
+
export declare interface CloseableDatabaseConnection {
|
|
16
|
+
close: () => Promise<void> | void
|
|
17
|
+
}
|
|
18
|
+
export type PendingDatabaseConnectionClosures = Set<Promise<void>>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export async function closeDatabaseConnections(connections){const errors=(await Promise.allSettled([...new Set(connections)].map((connection)=>connection.close()))).filter((result)=>result.status==="rejected").map((result)=>result.reason);if(errors.length===1)throw errors[0];if(errors.length>1)throw AggregateError(errors,"Failed to close database connections")}export function retireDatabaseConnections(pending,connections,onError){const closing=closeDatabaseConnections(connections);pending.add(closing);closing.then(()=>pending.delete(closing),(error)=>{pending.delete(closing);onError(error)})}export function closeDatabaseConnectionsAndPending(connections,pending){return closeDatabaseConnections([...connections,...[...pending].map((closing)=>({close:()=>closing}))])}
|
package/dist/index.d.ts
CHANGED
|
@@ -85,6 +85,9 @@ export * from './migrations';
|
|
|
85
85
|
export { setQueryTracker, logQuery } from './query-logger';
|
|
86
86
|
// Zero-downtime migration helpers
|
|
87
87
|
export { addColumnSafely, backfillInBatches, renameColumnSafely } from './safe-migrations';
|
|
88
|
+
// Affected-row counts. Read every write result through this: each driver spells
|
|
89
|
+
// the count differently and the hand-rolled readers were wrong on two of three.
|
|
90
|
+
export { matchedRows, mutationCount } from './affected-rows';
|
|
88
91
|
// Seeding
|
|
89
92
|
export * from './seeder';
|
|
90
93
|
// Driver utilities
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export{Database,createDatabase,createMysqlDatabase,createPostgresDatabase,createSqliteDatabase}from"./database";export{detectDriver,driverDefaults,getConfigFromEnv,getConnectionString,mergeWithDefaults,validateDriverConfig}from"./driver-config";export*from"./utils";export*from"./types";export*from"./migrations";export{setQueryTracker,logQuery}from"./query-logger";export{addColumnSafely,backfillInBatches,renameColumnSafely}from"./safe-migrations";export*from"./seeder";export*from"./drivers";export*from"./custom";export*from"./auth-tables";export*from"./utc-defaults";export*from"./uuid-columns";export*from"./managed-columns";export*from"./relation-columns";export{ensureNotificationForeignKeys,migrateNotificationTables}from"./notification-tables";export{migrateRbacTables}from"./rbac-tables";export*from"./trait-tables";export*from"./datetime-columns";export*from"./dialect";export*from"./replicas";export*from"./ddl-constraints";export*from"./erd";export*from"./vschema";export*from"./sql-helpers";export*from"./defaults";export*from"./migration-dialect";export*from"./migration-path";export*from"./migration-ledger";export*from"./model-sources";export*from"./package-migrations";export*from"./package-models";export*from"./shadowed-models";export*from"./ensure-database";export{auditForeignKeys,classifyDeclaredFKs,findFkOrphans,fkKey,getDeclaredFKs,getLiveFKs,getLiveTables}from"./fk-audit";export{auditSchemaDrift,formatSchemaDrift}from"./schema-drift";export{auditUniqueIndexes,getDeclaredUniques,getLiveUniqueIndexes}from"./unique-audit";export{__flushAfterCommitNow,__pendingAfterCommitCount,enqueueAfterCommit,isInTransaction,runInTransactionScope}from"./transaction-context";export{createQueryBuilder,setConfig}from"@stacksjs/query-builder";export{createDynamo,dynamo,EntityQueryBuilder,generateKeyPattern,parseKeyPattern,buildKey,marshall,unmarshall}from"./drivers/dynamodb";
|
|
1
|
+
export{Database,createDatabase,createMysqlDatabase,createPostgresDatabase,createSqliteDatabase}from"./database";export{detectDriver,driverDefaults,getConfigFromEnv,getConnectionString,mergeWithDefaults,validateDriverConfig}from"./driver-config";export*from"./utils";export*from"./types";export*from"./migrations";export{setQueryTracker,logQuery}from"./query-logger";export{addColumnSafely,backfillInBatches,renameColumnSafely}from"./safe-migrations";export{matchedRows,mutationCount}from"./affected-rows";export*from"./seeder";export*from"./drivers";export*from"./custom";export*from"./auth-tables";export*from"./utc-defaults";export*from"./uuid-columns";export*from"./managed-columns";export*from"./relation-columns";export{ensureNotificationForeignKeys,migrateNotificationTables}from"./notification-tables";export{migrateRbacTables}from"./rbac-tables";export*from"./trait-tables";export*from"./datetime-columns";export*from"./dialect";export*from"./replicas";export*from"./ddl-constraints";export*from"./erd";export*from"./vschema";export*from"./sql-helpers";export*from"./defaults";export*from"./migration-dialect";export*from"./migration-path";export*from"./migration-ledger";export*from"./model-sources";export*from"./package-migrations";export*from"./package-models";export*from"./shadowed-models";export*from"./ensure-database";export{auditForeignKeys,classifyDeclaredFKs,findFkOrphans,fkKey,getDeclaredFKs,getLiveFKs,getLiveTables}from"./fk-audit";export{auditSchemaDrift,formatSchemaDrift}from"./schema-drift";export{auditUniqueIndexes,getDeclaredUniques,getLiveUniqueIndexes}from"./unique-audit";export{__flushAfterCommitNow,__pendingAfterCommitCount,enqueueAfterCommit,isInTransaction,runInTransactionScope}from"./transaction-context";export{createQueryBuilder,setConfig}from"@stacksjs/query-builder";export{createDynamo,dynamo,EntityQueryBuilder,generateKeyPattern,parseKeyPattern,buildKey,marshall,unmarshall}from"./drivers/dynamodb";
|
package/dist/migrations.d.ts
CHANGED
|
@@ -61,19 +61,19 @@ export declare function withoutProtectedTableDropSql(statements: string[], prote
|
|
|
61
61
|
* standalone index file is the only uniqueness enforcement on SQLite
|
|
62
62
|
* (stacksjs/stacks#1952).
|
|
63
63
|
*
|
|
64
|
-
* Two flavours of "no-op on SQLite"
|
|
65
|
-
*
|
|
66
|
-
* - **Skip
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
64
|
+
* Two flavours of "no-op on SQLite", both of which keep the file:
|
|
65
|
+
*
|
|
66
|
+
* - **Skip** (`skipMigration`): the file is portable — it would run
|
|
67
|
+
* cleanly on MySQL/Postgres — but doesn't apply to SQLite. Record it as
|
|
68
|
+
* executed in the migrations tracking table so it doesn't replay, and
|
|
69
|
+
* leave the file on disk so a future `DB_CONNECTION` flip can pick it up.
|
|
70
|
+
* This is the right path for FK constraint files. (stacksjs/stacks#1916)
|
|
71
|
+
*
|
|
72
|
+
* - **Retire** (`retireMigration`): the file cannot run against this
|
|
73
|
+
* schema — an unrecorded duplicate CREATE TABLE, or a DROP COLUMN whose
|
|
74
|
+
* columns are already gone. Also recorded as executed and also left on
|
|
75
|
+
* disk: the corpus is tracked source, and this sweep cannot tell a
|
|
76
|
+
* generated file from a hand-written one (stacksjs/stacks#2234).
|
|
77
77
|
*/
|
|
78
78
|
/**
|
|
79
79
|
* Split a migration file into its statements.
|
package/dist/migrations.js
CHANGED
|
@@ -3,7 +3,7 @@ var {require}=import.meta;import{existsSync,mkdirSync,readdirSync,readFileSync,r
|
|
|
3
3
|
`),output=[];for(const line of lines){const match=/^(\s*)ALTER\s+TABLE\s+("?[\w.]+"?)\s+ALTER\s+COLUMN\s+("?[\w]+"?)\s+TYPE\s/i.exec(line);if(match){const[,indent,table,column]=match,drop=`${indent}ALTER TABLE ${table} ALTER COLUMN ${column} DROP DEFAULT;`;if((output.length>0?output[output.length-1].trim():"")!==drop.trim())output.push(drop)}output.push(line)}return output.join(`
|
|
4
4
|
`)}export function guardPostgresEnumTypes(sql){return assertPostgresEnumMembers(wrapPostgresEnumTypes(sql))}function wrapPostgresEnumTypes(sql){return sql.replace(/CREATE\s+TYPE\s+("?[\w.]+"?)\s+AS\s+ENUM\s*\(([^)]*)\)/gi,(match,name,members,offset,whole)=>{if(/\bBEGIN\s*$/i.test(whole.slice(Math.max(0,offset-40),offset)))return match;return`DO $stacks$ BEGIN CREATE TYPE ${name} AS ENUM (${members}); EXCEPTION WHEN duplicate_object THEN null; END $stacks$`})}function assertPostgresEnumMembers(sql){return sql.replace(/DO \$stacks\$[\s\S]*?END \$stacks\$;?/g,(block)=>{const created=/CREATE\s+TYPE\s+("?[\w.]+"?)\s+AS\s+ENUM\s*\(([^)]*)\)/i.exec(block);if(!created)return block;const[,name,members]=created,missing=enumMembers(members).map((member)=>`ALTER TYPE ${name} ADD VALUE IF NOT EXISTS ${member};`).filter((statement)=>!sql.includes(statement));if(missing.length===0)return block;return`${block.endsWith(";")?block:`${block};`}
|
|
5
5
|
${missing.join(`
|
|
6
|
-
`)}`})}function enumMembers(list){return[...list.matchAll(/'(?:[^']|'')*'/g)].map((match)=>match[0])}export function preprocessSqliteMigrations(){const migrationsDir=migrationDirectory("sqlite"),quarantined=[];try{for(const stale of readdirSync(migrationsDir).filter((f)=>f.endsWith(".sql.unsupported"))){const original=join(migrationsDir,stale.slice(0,-12));try{if(!existsSync(original))renameSync(join(migrationsDir,stale),original);else unlinkSync(join(migrationsDir,stale))}catch{}}}catch{}let files;try{files=readdirSync(migrationsDir).filter((f)=>f.endsWith(".sql"))}catch{return quarantined}const droppedMigrations=[],skipMigration=(file,reason)=>{log.info(`Skipping migration on SQLite (${reason}): ${file}`);droppedMigrations.push(file)},partitionUnsupported=(file,filePath,statements,pattern,feature)=>{const unsupported=statements.filter((s)=>pattern.test(s));if(unsupported.length===statements.length){skipMigration(file,`SQLite does not support ${feature}`);return"skipped"}const hiddenPath=`${filePath}.unsupported`;try{renameSync(filePath,hiddenPath)}catch(error){log.error(`[migration] Could not quarantine ${file}: ${error instanceof Error?error.message:String(error)}`);return"runnable"}quarantined.push({original:filePath,hidden:hiddenPath,feature});log.warn(`[migration] ${file} mixes ${unsupported.length} ${feature} statement(s) SQLite cannot run with ${statements.length-unsupported.length} it can, so none of it was applied. Split them into separate migrations, or run this app on MySQL/Postgres. The rest of the corpus was applied.`);return"quarantined"},
|
|
6
|
+
`)}`})}function enumMembers(list){return[...list.matchAll(/'(?:[^']|'')*'/g)].map((match)=>match[0])}export function preprocessSqliteMigrations(){const migrationsDir=migrationDirectory("sqlite"),quarantined=[];try{for(const stale of readdirSync(migrationsDir).filter((f)=>f.endsWith(".sql.unsupported"))){const original=join(migrationsDir,stale.slice(0,-12));try{if(!existsSync(original))renameSync(join(migrationsDir,stale),original);else unlinkSync(join(migrationsDir,stale))}catch{}}}catch{}let files;try{files=readdirSync(migrationsDir).filter((f)=>f.endsWith(".sql"))}catch{return quarantined}const droppedMigrations=[],skipMigration=(file,reason)=>{log.info(`Skipping migration on SQLite (${reason}): ${file}`);droppedMigrations.push(file)},partitionUnsupported=(file,filePath,statements,pattern,feature)=>{const unsupported=statements.filter((s)=>pattern.test(s));if(unsupported.length===statements.length){skipMigration(file,`SQLite does not support ${feature}`);return"skipped"}const hiddenPath=`${filePath}.unsupported`;try{renameSync(filePath,hiddenPath)}catch(error){log.error(`[migration] Could not quarantine ${file}: ${error instanceof Error?error.message:String(error)}`);return"runnable"}quarantined.push({original:filePath,hidden:hiddenPath,feature});log.warn(`[migration] ${file} mixes ${unsupported.length} ${feature} statement(s) SQLite cannot run with ${statements.length-unsupported.length} it can, so none of it was applied. Split them into separate migrations, or run this app on MySQL/Postgres. The rest of the corpus was applied.`);return"quarantined"},retireMigration=(file,reason)=>{log.info(`Not running no-op migration (${reason}): ${file}`);droppedMigrations.push(file)},replayMigrations=[],addConstraintPattern=/^\s*ALTER\s+TABLE\s+.+\s+ADD\s+CONSTRAINT\s+/i,createTypePattern=/^\s*CREATE\s+TYPE\s+/i,createUniqueIndexPattern=/^\s*CREATE\s+UNIQUE\s+INDEX\s+(?:IF\s+NOT\s+EXISTS\s+)?["'`]?(\w+)["'`]?/i,dropColumnPattern=/^\s*ALTER\s+TABLE\s+["']?(\w+)["']?\s+DROP\s+COLUMN\s+["']?(\w+)["']?\s*$/i,addColumnPattern=/^\s*ALTER\s+TABLE\s+["']?(\w+)["']?\s+ADD\s+COLUMN\s+["'`]?(\w+)["'`]?/i,createTablePattern=/^\s*CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["']?(\w+)["']?/i,createTableEarliest=new Map;for(const file of files){const m=file.match(/^\d+-create-(\w+)-table\.sql$/);if(!m||!m[1])continue;const tableName=m[1],existing=createTableEarliest.get(tableName);if(!existing||file<existing)createTableEarliest.set(tableName,file)}const earlierCreateDefinesColumn=(migrationFile,table,column)=>{const createFile=createTableEarliest.get(table);if(!createFile||createFile>=migrationFile)return!1;try{const createContent=readFileSync(join(migrationsDir,createFile),"utf8"),createStatement=sqlStatementsOf(createContent).find((statement)=>statement.match(createTablePattern)?.[1]===table);if(!createStatement)return!1;const escapedColumn=column.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");return new RegExp(`(?:^|[,(])\\s*["'\`]?${escapedColumn}["'\`]?\\s+`,"i").test(createStatement)}catch{return!1}},sqliteDbPath=sqliteDatabasePath();let sqliteDb=null;if(existsSync(sqliteDbPath))try{const{Database}=require("bun:sqlite");sqliteDb=new Database(sqliteDbPath,{readonly:!0})}catch{}const migrationWasRecorded=(file)=>{if(!sqliteDb)return!1;try{return Boolean(sqliteDb.prepare("SELECT 1 FROM migrations WHERE migration = ? LIMIT 1").get(file))}catch{return!1}};for(const file of files){log.debug(`[migration] Running: ${file}`);const filePath=join(migrationsDir,file),content=readFileSync(filePath,"utf-8"),statements=sqlStatementsOf(content);if(statements.length===0)continue;const uniqueIndexNames=statements.map((s)=>s.match(createUniqueIndexPattern)?.[1]).filter((name)=>Boolean(name));if(sqliteDb&&uniqueIndexNames.length===statements.length){const indexExists=sqliteDb.prepare("SELECT name FROM sqlite_master WHERE type = 'index' AND name = ?");if(uniqueIndexNames.filter((name)=>!indexExists.get(name)).length>0){log.info(`Re-queueing unique-index migration (index missing from database): ${file}`);replayMigrations.push(file)}continue}if(migrationWasRecorded(file))continue;const firstStatement=statements[0],createTableMatch=firstStatement?firstStatement.match(createTablePattern):null;if(createTableMatch&&createTableMatch[1]){const tableName=createTableMatch[1],earliest=createTableEarliest.get(tableName);if(earliest&&earliest!==file&&!isPackageMigration(file)){retireMigration(file,`duplicate create-table for "${tableName}" (kept ${earliest})`);continue}}if(statements.some((s)=>addConstraintPattern.test(s))){if(partitionUnsupported(file,filePath,statements,addConstraintPattern,"ALTER TABLE ADD CONSTRAINT")!=="runnable")continue}if(statements.some((s)=>createTypePattern.test(s))){if(partitionUnsupported(file,filePath,statements,createTypePattern,"CREATE TYPE (enum types)")!=="runnable")continue}const addColumnTargets=statements.map((s)=>s.match(addColumnPattern)).filter((m)=>Boolean(m?.[1]&&m[2])).map((m)=>({table:m[1],column:m[2]}));if(addColumnTargets.length>0&&addColumnTargets.length===statements.length){const satisfied=addColumnTargets.filter(({table,column})=>{try{if(sqliteDb){const safeTableName=table.replace(/[^a-zA-Z0-9_]/g,""),columns=sqliteDb.prepare(`PRAGMA table_info("${safeTableName}")`).all();if(columns.some((col)=>col.name===column))return!0;if(columns.length>0)return!1}return earlierCreateDefinesColumn(file,table,column)}catch{return!1}});if(satisfied.length===addColumnTargets.length){skipMigration(file,"every column it adds already exists or is defined by an earlier create-table migration");continue}if(satisfied.length>0)log.warn(`[migration] ${file} is partially applied: ${satisfied.map((p)=>`${p.table}.${p.column}`).join(", ")} already exist${satisfied.length===1?"s":""} or will be created earlier, the rest do not. SQLite cannot skip a single ADD COLUMN, so this file will fail. Add the remaining columns by hand, or split the file so the applied statements sit in their own migration.`)}if(statements.some((s)=>dropColumnPattern.test(s))){let modified=!1;const filteredStatements=[];for(const stmt of statements){const dropColMatch=stmt.match(dropColumnPattern);if(dropColMatch&&dropColMatch[1]&&dropColMatch[2]){const tableName=dropColMatch[1],columnName=dropColMatch[2];if(!sqliteDb){if(earlierCreateDefinesColumn(file,tableName,columnName)){filteredStatements.push(stmt);continue}log.info(`Skipping DROP COLUMN "${columnName}" - no database exists yet: ${file}`);modified=!0;continue}try{const safeTableName=tableName.replace(/[^a-zA-Z0-9_]/g,""),columns=sqliteDb.prepare(`PRAGMA table_info("${safeTableName}")`).all();if(columns.length===0){if(earlierCreateDefinesColumn(file,tableName,columnName)){filteredStatements.push(stmt);continue}log.info(`Skipping DROP COLUMN "${columnName}" - table "${tableName}" does not exist yet: ${file}`);modified=!0;continue}if(!columns.some((col)=>col.name===columnName)){log.info(`Skipping DROP COLUMN "${columnName}" from "${tableName}" - column does not exist: ${file}`);modified=!0;continue}}catch{if(earlierCreateDefinesColumn(file,tableName,columnName)){filteredStatements.push(stmt);continue}log.info(`Skipping DROP COLUMN "${columnName}" - table "${tableName}" not found: ${file}`);modified=!0;continue}}filteredStatements.push(stmt)}if(modified){if(filteredStatements.length===0)retireMigration(file,"columns already absent from table");else writeFileSync(filePath,`${filteredStatements.join(`;
|
|
7
7
|
`)};
|
|
8
8
|
`);continue}}}if(sqliteDb)try{sqliteDb.close()}catch{}if(droppedMigrations.length>0||replayMigrations.length>0)try{const dbPath=sqliteDatabasePath();mkdirSync(dirname(dbPath),{recursive:!0});const{Database}=require("bun:sqlite"),writeDb=new Database(dbPath);try{writeDb.exec(`CREATE TABLE IF NOT EXISTS migrations (
|
|
9
9
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a table or column name marks its values as secret. Names are split
|
|
3
|
+
* into words (`remember_token`, `rememberToken` and `REMEMBER-TOKEN` all read
|
|
4
|
+
* as `remember token`) so `pin` matches a `pin` column and not `shipping`.
|
|
5
|
+
*/
|
|
6
|
+
export declare function isSensitiveName(name: string): boolean;
|
|
7
|
+
export declare function looksLikeCredential(text: string): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* `captureBindings` as config or `DB_QUERY_LOGGING_CAPTURE_BINDINGS` gave it:
|
|
10
|
+
* `undefined` when it is not set, `null` when it is set to something that is
|
|
11
|
+
* not a switch, else the boolean it spells. The env proxy turns only `true`
|
|
12
|
+
* and `false` into booleans, so `0`, `off` or ` Yes ` arrive here as text.
|
|
13
|
+
*/
|
|
14
|
+
export declare function parseCaptureBindings(setting: unknown): boolean | null | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* The bindings of `sql` as they may be persisted: one entry per parameter,
|
|
17
|
+
* each the value itself, `<redacted>`, or a type tag such as `<string>`.
|
|
18
|
+
*/
|
|
19
|
+
export declare function queryLogBindings(sql: string, parameters: readonly unknown[], options: QueryLogBindingOptions): unknown[];
|
|
20
|
+
/** {@link queryLogBindings} as the JSON text the `bindings` column holds. */
|
|
21
|
+
export declare function serializeQueryLogBindings(sql: string, parameters: unknown, options: QueryLogBindingOptions): string;
|
|
22
|
+
/**
|
|
23
|
+
* What `query_logs.bindings` keeps of the values a query bound.
|
|
24
|
+
*
|
|
25
|
+
* `GET /api/queries/:id` returns the whole row and the table outlives the
|
|
26
|
+
* request by the retention window, so it must not become a second copy of every
|
|
27
|
+
* session id, reset token and password hash the application touches. Until
|
|
28
|
+
* bun-query-builder 0.2.70 only SQLite delivered bindings at all. Every
|
|
29
|
+
* dialect does now, which made this the common case rather than an edge.
|
|
30
|
+
*
|
|
31
|
+
* A value is stored as `<redacted>` when any of these holds:
|
|
32
|
+
* - the SQL binds it to a column with a sensitive name (`password`,
|
|
33
|
+
* `remember_token`, `api_key`, `two_factor_secret`, ...);
|
|
34
|
+
* - it is text and the statement targets a table with a sensitive name
|
|
35
|
+
* (`sessions`, `password_resets`, `oauth_access_tokens`, ...), where even
|
|
36
|
+
* the `id` is a credential;
|
|
37
|
+
* - it is text, its column cannot be worked out, and the statement names
|
|
38
|
+
* something sensitive anywhere;
|
|
39
|
+
* - it is an object, or text holding JSON, with a key that has a sensitive
|
|
40
|
+
* name at any depth (`{"api_key": ...}` bound to a `settings` column);
|
|
41
|
+
* - it looks like a credential on its own: a password hash, a JWT, a run of
|
|
42
|
+
* 32 or more hex digits, a long mixed-case random token, or a key with a
|
|
43
|
+
* well-known prefix.
|
|
44
|
+
*
|
|
45
|
+
* Without `captureValues` nothing but each value's type is kept, so the count
|
|
46
|
+
* and shape of the bindings survive without the data. Binary values are only
|
|
47
|
+
* ever recorded by type.
|
|
48
|
+
*/
|
|
49
|
+
export declare const REDACTED_BINDING: '<redacted>';
|
|
50
|
+
export declare interface QueryLogBindingOptions {
|
|
51
|
+
captureValues: boolean
|
|
52
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export const REDACTED_BINDING="<redacted>";const SENSITIVE_WORDS=new Set(["apikey","authorization","bearer","challenge","challenges","cookie","cookies","credential","credentials","cvc","cvv","hash","hotp","jwt","nonce","otp","passphrase","passwd","password","passwords","pin","pwd","salt","secret","secrets","session","sessions","ssn","token","tokens","totp","verification","verifications"]),SENSITIVE_PAIRS=new Set(["access key","api key","app key","auth code","auth codes","backup code","backup codes","card number","credit card","encryption key","license key","license keys","private key","recovery code","recovery codes","secret key","security code","signing key","two factor"]);export function isSensitiveName(name){const words=name.replace(/([a-z\d])([A-Z])/g,"$1_$2").toLowerCase().split(/[^a-z\d]+/).filter(Boolean);return words.some((word,index)=>SENSITIVE_WORDS.has(word)||SENSITIVE_PAIRS.has(`${word} ${words[index+1]}`))}const CREDENTIAL_PATTERNS=[/\$(?:2[abxy]?|argon2(?:id|i|d)|scrypt|pbkdf2[\w-]*|[156y])\$/,/\beyJ[\w-]{8,}\.[\w-]{8,}\.[\w-]*/,/[\da-f]{32,}/i,/\b(?:sk|pk|rk)_(?:live|test)_\w{16,}/,/\bAKIA[\dA-Z]{16}\b/],OPAQUE_RUN=/[\w-]{32,}/g;export function looksLikeCredential(text){if(CREDENTIAL_PATTERNS.some((pattern)=>pattern.test(text)))return!0;for(const[run]of text.matchAll(OPAQUE_RUN))if(/[A-Z]/.test(run)&&/[a-z]/.test(run)&&/\d/.test(run))return!0;return!1}const KEY_END=/"\s*:/g,MAX_KEY_LENGTH=128,MAX_ESCAPES=16;function escapesBefore(text,at){let count=0;while(count<MAX_ESCAPES&&text[at-count-1]==="\\")count++;return count}function hasSensitiveKey(text){for(const{index:end}of text.matchAll(KEY_END)){const escapes=escapesBefore(text,end),floor=Math.max(0,end-escapes-MAX_KEY_LENGTH);for(let start=end-escapes-1;start>=floor;start--){if(text[start]!=='"'||escapesBefore(text,start)!==escapes)continue;if(isSensitiveName(text.slice(start+1,end-escapes)))return!0;break}}return!1}const KEYWORDS=new Set(["all","and","any","as","between","by","case","collate","conflict","delete","distinct","do","duplicate","else","end","escape","exists","false","from","glob","having","ignore","ilike","in","insert","into","is","join","like","limit","match","not","null","offset","on","only","or","regexp","replace","returning","rlike","select","set","some","table","then","true","update","using","values","when","where","with"]),COMPARISONS=new Set(["=","==","!=","<>","<",">","<=",">=","glob","ilike","is","like","match","regexp","rlike"]),TABLE_KEYWORDS=new Set(["from","into","join","table","update"]);function keyword(token){return token?.kind==="word"?token.text.toLowerCase():void 0}function isName(token){return token?.kind==="name"||token?.kind==="word"&&!KEYWORDS.has(keyword(token))}function isWord(token,...words){const word=keyword(token);return word!==void 0&&words.includes(word)}function isComparison(token){return token?.kind==="op"&&COMPARISONS.has(token.text)||COMPARISONS.has(keyword(token)??"")}function scanSql(sql){const tokens=[];let complete=!0,positional=0,at=0;const push=(token)=>{const dot=tokens.at(-1);if((token.kind==="name"||token.kind==="word")&&dot?.kind==="op"&&dot.text==="."&&isName(tokens.at(-2))){tokens.splice(-2,2);token={kind:"name",text:token.text}}tokens.push(token)},quoted=(quote,backslashEscapes)=>{let text="";for(at++;at<sql.length;at++){const char=sql[at];if(backslashEscapes&&char==="\\"){text+=sql[++at]??"";continue}if(char===quote){if(sql[at+1]!==quote){at++;return text}at++}text+=char}complete=!1;return text},sticky=(pattern)=>{pattern.lastIndex=at;const match=pattern.exec(sql);if(match)at+=match[0].length;return match};while(at<sql.length){const char=sql[at];if(/\s/.test(char))at++;else if(sql.startsWith("--",at)){const end=sql.indexOf(`
|
|
2
|
+
`,at);at=end<0?sql.length:end}else if(sql.startsWith("/*",at)){const end=sql.indexOf("*/",at+2);if(end<0)complete=!1;at=end<0?sql.length:end+2}else if(char==="'")push({kind:"value",text:quoted("'",!0)});else if(char==='"'||char==="`")push({kind:"name",text:quoted(char,!1)});else if(char==="$"){const numbered=sticky(/\$(\d+)/y);if(numbered){push({kind:"param",text:numbered[0],param:Number(numbered[1])-1});continue}const dollar=sticky(/\$([A-Z_a-z]\w*)?\$/y);if(!dollar){push({kind:"op",text:char});at++;continue}const end=sql.indexOf(dollar[0],at);if(end<0)complete=!1;at=end<0?sql.length:end+dollar[0].length;push({kind:"value",text:""})}else if(char==="?"){const numbered=sticky(/\?(\d+)/y);if(numbered)push({kind:"param",text:numbered[0],param:Number(numbered[1])-1});else{at++;push({kind:"param",text:"?",param:positional++})}}else if(/\d/.test(char)){sticky(/\d+(?:\.\d+)?(?:e[+-]?\d+)?/iy);push({kind:"value",text:""})}else if(/[A-Z_a-z]/.test(char))push({kind:"word",text:sticky(/[A-Z_a-z][\w$]*/y)[0]});else if(char==="("||char===")"||char===","){at++;push({kind:char,text:char})}else push({kind:"op",text:sticky(/<=|>=|<>|!=|==|\|\||::|->>|->|[^\s\w]/y)[0]})}if(tokens.some((token)=>token.kind==="param"&&token.text.startsWith("$"))){for(const token of tokens)if(token.kind==="param"&&token.text.startsWith("?")){token.kind="op";delete token.param}}return{tokens,complete}}function pairParentheses(tokens){const openers=[],open=[];for(const[index,token]of tokens.entries()){openers.push((token.kind===")"?open.pop():open.at(-1))??-1);if(token.kind==="(")open.push(index)}return openers}function skipNot(tokens,at){return isWord(tokens[at],"not")?at-1:at}function operandColumn({tokens,openers},at){const token=tokens[at];if(token?.kind!==")")return isName(token)?token.text:void 0;return tokens.slice(openers[at]+1,at).find((inner)=>isName(inner))?.text}function valueColumn(statement,start,end){const{tokens,openers}=statement,operator=isWord(tokens[start-1],"not")&&isWord(tokens[start-2],"is")?start-2:start-1,before=tokens[operator];if(isComparison(before))return operandColumn(statement,skipNot(tokens,operator-1));if(isWord(before,"between"))return operandColumn(statement,skipNot(tokens,start-2));if(isWord(before,"and")&&isWord(tokens[start-3],"between"))return operandColumn(statement,skipNot(tokens,start-4));if(before?.kind==="("||before?.kind===","){const open=openers[start];if(open>0&&isWord(tokens[open-1],"in"))return operandColumn(statement,skipNot(tokens,open-2));if(open>0&&(isName(tokens[open-1])||isWord(tokens[open-1],"any","all","some")))return valueColumn(statement,open-1,open-1)}const after=tokens[end+1],operand=tokens[end+2];if(after?.kind==="op"&&isComparison(after)&&isName(operand)&&tokens[end+3]?.kind!=="(")return operand.text;return}function insertColumns(tokens){const columns=new Map,insert=tokens.findIndex((token)=>isWord(token,"insert","replace")),into=tokens.findIndex((token,index)=>index>insert&&isWord(token,"into"));if(insert<0||into<0||tokens[into+2]?.kind!=="(")return columns;const names=[];let index=into+3,expectName=!0;for(;index<tokens.length&&tokens[index].kind!==")";index++){const token=tokens[index];if(token.kind===",")expectName=!0;else if(expectName){names.push(isName(token)?token.text:void 0);expectName=!1}}if(!isWord(tokens[index+1],"values","value"))return columns;let depth=0,item=0;for(index+=2;index<tokens.length;index++){const token=tokens[index];if(token.kind==="("){if(depth++===0)item=0}else if(token.kind===")")depth--;else if(token.kind===","&&depth===1)item++;else if(token.kind==="param"&&depth>0)columns.set(index,names[item]);else if(depth===0&&token.kind==="word")break}return columns}function bindingContext(sql){const{tokens,complete}=scanSql(sql),columns=new Map;if(!complete){const sensitive=(sql.match(/[A-Z_a-z][\w$]*/g)??[]).some((word)=>isSensitiveName(word));return{columns,sensitiveTable:sensitive,sensitiveMention:sensitive}}const statement={tokens,openers:pairParentheses(tokens)},inserted=insertColumns(tokens);for(const[index,token]of tokens.entries()){if(token.kind!=="param"||token.param===void 0)continue;const column=inserted.get(index)??valueColumn(statement,index,index),seen=columns.get(token.param)??[];seen.push(column);columns.set(token.param,seen)}const sensitiveTable=tokens.some((token,index)=>isName(token)&&TABLE_KEYWORDS.has(keyword(tokens[index-1])??"")&&!isWord(tokens[index-2],"key")&&isSensitiveName(token.text)),sensitiveMention=tokens.some((token)=>isName(token)&&isSensitiveName(token.text));return{columns,sensitiveTable,sensitiveMention}}function isBytes(value){return value instanceof ArrayBuffer||ArrayBuffer.isView(value)}function typeTag(value){if(value instanceof Date)return"<date>";if(isBytes(value))return"<bytes>";return`<${typeof value}>`}function jsonText(value){try{return JSON.stringify(value)}catch{return}}const SWITCH_ON=new Set(["1","true","yes","on"]),SWITCH_OFF=new Set(["0","false","no","off"]);export function parseCaptureBindings(setting){if(typeof setting==="boolean")return setting;if(setting===void 0||setting===null)return;const text=String(setting).trim().toLowerCase();if(text==="")return;if(SWITCH_ON.has(text))return!0;if(SWITCH_OFF.has(text))return!1;return null}export function queryLogBindings(sql,parameters,options){if(parameters.length===0)return[];const context=options.captureValues?bindingContext(sql):void 0;return parameters.map((value,index)=>{if(value===null||value===void 0)return null;if(!context)return typeTag(value);const columns=context.columns.get(index)??[];if(columns.some((column)=>column!==void 0&&isSensitiveName(column)))return REDACTED_BINDING;if(isBytes(value))return typeTag(value);if(typeof value==="string"||typeof value==="object"&&!(value instanceof Date)){const unresolved=columns.every((column)=>column===void 0);if(context.sensitiveTable||unresolved&&context.sensitiveMention)return REDACTED_BINDING;const text=typeof value==="string"?value:jsonText(value);if(text===void 0)return typeTag(value);if(looksLikeCredential(text)||hasSensitiveKey(text))return REDACTED_BINDING}return typeof value==="bigint"?String(value):value})}export function serializeQueryLogBindings(sql,parameters,options){const values=Array.isArray(parameters)?parameters:[parameters];try{return JSON.stringify(queryLogBindings(sql,values,options))}catch{return"[]"}}
|
package/dist/query-logger.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{sqlDateTime}from"./sql-helpers";import{memoryUsage}from"node:process";import{AsyncLocalStorage}from"node:async_hooks";import{config}from"@stacksjs/config";import{log}from"@stacksjs/logging";import{normalizeQuery,parseQuery}from"./query-parser";import{db}from"./utils";const QUERY_TRACKER_KEY=Symbol.for("stacks.database.queryTracker");let configuredQueryTracker=()=>{};export function setQueryTracker(fn){configuredQueryTracker=fn}function trackQuery(query,durationMs,connection){const shared=globalThis[QUERY_TRACKER_KEY];(typeof shared==="function"?shared:configuredQueryTracker)(query,durationMs,connection)}const queryLogContext=new AsyncLocalStorage,QUERY_LOG_BATCH_SIZE=100,QUERY_LOG_BATCH_DELAY_MS=5,pendingQueryLogs=[];let queryLogFlushScheduled=!1,queryLogFlushInFlight=!1;function enqueueQueryLog(record){const settled=new Promise((resolve)=>{pendingQueryLogs.push({record,resolve})});if(pendingQueryLogs.length>=QUERY_LOG_BATCH_SIZE)flushQueuedQueryLogs();else if(!queryLogFlushScheduled){queryLogFlushScheduled=!0;setTimeout(()=>{queryLogFlushScheduled=!1;flushQueuedQueryLogs()},QUERY_LOG_BATCH_DELAY_MS)}return settled}async function flushQueuedQueryLogs(){if(queryLogFlushInFlight)return;queryLogFlushInFlight=!0;try{while(pendingQueryLogs.length>0){const batch=pendingQueryLogs.splice(0,QUERY_LOG_BATCH_SIZE),recordsByShape=Map.groupBy(batch.map((item)=>item.record),(record)=>Object.keys(record).join("\x00"));for(const records of recordsByShape.values())if(!await queryLogContext.run(!0,()=>storeQueryLogs(records,records.length===1)))for(const record of records)await queryLogContext.run(!0,()=>storeQueryLogs([record]));for(const item of batch)item.resolve()}}finally{queryLogFlushInFlight=!1;if(pendingQueryLogs.length>0&&!queryLogFlushScheduled){queryLogFlushScheduled=!0;setTimeout(()=>{queryLogFlushScheduled=!1;flushQueuedQueryLogs()},QUERY_LOG_BATCH_DELAY_MS)}}}export async function logQuery(event){if(queryLogContext.getStore())return;try{const{query,durationMs,error,
|
|
2
|
-
`).slice(1).find((line)=>!line.includes("query-logger.ts"));let caller={};if(callerLine){const methodMatch=callerLine.match(/at (.+?) \(/),fileMatch=callerLine.match(/\((.+?):(\d+):(\d+)\)/);if(methodMatch&&methodMatch[1]){const methodParts=methodMatch[1].split(".");caller={model:methodParts.length>1?methodParts[0]:void 0,method:methodParts.length>1?methodParts[1]:methodParts[0]}}if(fileMatch&&fileMatch[1]&&fileMatch[2])caller={...caller,file:fileMatch[1],line:Number.parseInt(fileMatch[2],10)}}const result={trace:sanitizeStackTrace(stack),caller};if(result.trace===stack&&stack.length<=8192)lastTraceInfo=result;return result}catch{return{trace:"",caller:{}}}}async function enhanceWithQueryAnalysis(logRecord){try{const{tables,type}=parseQuery(logRecord.query);logRecord.affected_tables=JSON.stringify(tables||[]);if(type==="SELECT"&&config.database?.queryLogging?.analysis?.explainPlan){const explainResult=await getExplainPlan(logRecord.query);if(explainResult){logRecord.explain_plan=explainResult.plan;logRecord.indexes_used=JSON.stringify(explainResult.indexesUsed||[]);logRecord.missing_indexes=JSON.stringify(explainResult.missingIndexes||[]);if(config.database?.queryLogging?.analysis?.suggestions)logRecord.optimization_suggestions=JSON.stringify(generateOptimizationSuggestions(explainResult,logRecord))}}const tags=[type];if(tables&&tables.length>0)tags.push(...tables.map((table)=>`table:${table}`));logRecord.tags=JSON.stringify(tags)}catch(error){log.debug("Error during query analysis:",error)}}const EXPLAIN_DIALECTS=new Set(["sqlite","mysql","postgres"]);async function getExplainPlan(query){try{const rawDriver=(await import("@stacksjs/config")).config?.database?.default,driver=typeof rawDriver==="string"?rawDriver:"sqlite";if(!EXPLAIN_DIALECTS.has(driver))return null;let sqlText;if(driver==="mysql")sqlText=`EXPLAIN FORMAT=JSON ${query}`;else if(driver==="postgres")sqlText=`EXPLAIN (FORMAT JSON) ${query}`;else sqlText=`EXPLAIN QUERY PLAN ${query}`;const result=await db.unsafe?.(sqlText);if(!result)return null;const raw=result,rows=Array.isArray(raw)?raw:raw?.rows??[],planText=JSON.stringify(rows),indexesUsed=[],missingIndexes=[];if(driver==="sqlite")for(const row of rows){const detail=row?.detail||"",idxMatch=detail.match(/USING (?:COVERING )?INDEX (\w+)/i);if(idxMatch&&idxMatch[1])indexesUsed.push(idxMatch[1]);else{const scanMatch=detail.match(/^SCAN\s+(\w+)/i);if(scanMatch&&scanMatch[1])missingIndexes.push(scanMatch[1])}}else if(driver==="mysql"){const text=planText;for(const m of text.matchAll(/"key"\s*:\s*"([^"]+)"/g))if(m[1])indexesUsed.push(m[1]);for(const m of text.matchAll(/"table_name"\s*:\s*"([^"]+)"[^}]*"access_type"\s*:\s*"ALL"/g))if(m[1])missingIndexes.push(m[1])}else if(driver==="postgres"){const text=planText;for(const m of text.matchAll(/"Index Name"\s*:\s*"([^"]+)"/g))if(m[1])indexesUsed.push(m[1]);for(const m of text.matchAll(/"Node Type"\s*:\s*"Seq Scan"[^}]*"Relation Name"\s*:\s*"([^"]+)"/g))if(m[1])missingIndexes.push(m[1])}return{plan:planText.length>4000?`${planText.slice(0,4000)}\u2026`:planText,indexesUsed:Array.from(new Set(indexesUsed)),missingIndexes:Array.from(new Set(missingIndexes))}}catch(err){log.debug("[query-logger] EXPLAIN failed (non-fatal):",err);return null}}function generateOptimizationSuggestions(explainResult,logRecord){const suggestions=[];if(explainResult.missingIndexes&&explainResult.missingIndexes.length>0)suggestions.push(`Consider adding index on ${explainResult.missingIndexes.join(", ")}`);if(logRecord.status==="slow"){suggestions.push("Consider optimizing this query to reduce execution time");if(logRecord.query.toLowerCase().includes("select *"))suggestions.push("Specify only needed columns instead of using SELECT *");if(!logRecord.query.toLowerCase().includes("limit"))suggestions.push("Consider adding LIMIT clause to reduce result set size")}return suggestions}async function storeQueryLogs(logRecords,reportFailure=!0){try{const values=logRecords;if(values.length===1)await db.insertInto("query_logs").values(values).execute();else await db.transaction(async(rawTrx)=>{await rawTrx.insertInto("query_logs").values(values).execute()});return!0}catch(error){if(reportFailure){const message=error instanceof Error?error.message:String(error);if(/no such table|does not exist|doesn't exist/i.test(message)&&/query_logs/i.test(message))log.debug("Query logging will start after the query_logs table is migrated.");else log.error("Failed to store query log:",error)}return!1}}
|
|
1
|
+
import{sqlDateTime}from"./sql-helpers";import{memoryUsage}from"node:process";import{AsyncLocalStorage}from"node:async_hooks";import{config}from"@stacksjs/config";import{env as envVars}from"@stacksjs/env";import{log}from"@stacksjs/logging";import{parseCaptureBindings,serializeQueryLogBindings}from"./query-log-bindings";import{normalizeQuery,parseQuery}from"./query-parser";import{db,getDatabaseDialect}from"./utils";const QUERY_TRACKER_KEY=Symbol.for("stacks.database.queryTracker");let configuredQueryTracker=()=>{};export function setQueryTracker(fn){configuredQueryTracker=fn}function trackQuery(query,durationMs,connection){const shared=globalThis[QUERY_TRACKER_KEY];(typeof shared==="function"?shared:configuredQueryTracker)(query,durationMs,connection)}const queryLogContext=new AsyncLocalStorage,QUERY_LOG_BATCH_SIZE=100,QUERY_LOG_BATCH_DELAY_MS=5,pendingQueryLogs=[];let queryLogFlushScheduled=!1,queryLogFlushInFlight=!1;function enqueueQueryLog(record){const settled=new Promise((resolve)=>{pendingQueryLogs.push({record,resolve})});if(pendingQueryLogs.length>=QUERY_LOG_BATCH_SIZE)flushQueuedQueryLogs();else if(!queryLogFlushScheduled){queryLogFlushScheduled=!0;setTimeout(()=>{queryLogFlushScheduled=!1;flushQueuedQueryLogs()},QUERY_LOG_BATCH_DELAY_MS)}return settled}async function flushQueuedQueryLogs(){if(queryLogFlushInFlight)return;queryLogFlushInFlight=!0;try{while(pendingQueryLogs.length>0){const batch=pendingQueryLogs.splice(0,QUERY_LOG_BATCH_SIZE),recordsByShape=Map.groupBy(batch.map((item)=>item.record),(record)=>Object.keys(record).join("\x00"));for(const records of recordsByShape.values())if(!await queryLogContext.run(!0,()=>storeQueryLogs(records,records.length===1)))for(const record of records)await queryLogContext.run(!0,()=>storeQueryLogs([record]));for(const item of batch)item.resolve()}}finally{queryLogFlushInFlight=!1;if(pendingQueryLogs.length>0&&!queryLogFlushScheduled){queryLogFlushScheduled=!0;setTimeout(()=>{queryLogFlushScheduled=!1;flushQueuedQueryLogs()},QUERY_LOG_BATCH_DELAY_MS)}}}export async function logQuery(event){if(queryLogContext.getStore())return;try{const{query,durationMs,error,parameters}=extractQueryInfo(event);if(query)try{trackQuery(query,durationMs,config.database?.default||"unknown")}catch{}if(!config.database?.queryLogging?.enabled)return;const excludedQueries=config.database?.queryLogging?.excludedQueries;if(!query.trim()||isExcludedQuery(query,Array.isArray(excludedQueries)?excludedQueries:[]))return;const status=determineQueryStatus(durationMs,error),logRecord=await createQueryLogRecord(query,durationMs,status,error,parameters);if(config.database?.queryLogging?.analysis?.enabled&&(status==="slow"||config.database.queryLogging.analysis.analyzeAll))await enhanceWithQueryAnalysis(logRecord);await enqueueQueryLog(logRecord);if(status!=="completed")log[status==="failed"?"error":"warn"](`Query ${status}:`,{query:logRecord.query,duration:logRecord.duration,connection:logRecord.connection,...error&&{error}})}catch(err){log.error("Failed to log query:",err)}}export function isExcludedQuery(query,excludedQueries){const normalizedQuery=query.toLowerCase();return excludedQueries.some((pattern)=>{const normalizedPattern=pattern.trim().toLowerCase();return normalizedPattern.length>0&&normalizedQuery.includes(normalizedPattern)})}function queryText(sql){if(typeof sql==="string")return usableQueryText(sql);if(sql&&typeof sql==="object"&&typeof sql.then==="function")return"";if(sql&&typeof sql.toString==="function")return usableQueryText(String(sql));return""}function usableQueryText(text){return text.startsWith("[object ")?"":text}function extractQueryInfo(event){const query=queryText(event.query?.sql),durationMs=event.queryDurationMillis||0,error=event.error,parameters=event.query?.parameters;return{query,durationMs,error,parameters}}function determineQueryStatus(durationMs,error){const slowThreshold=config.database?.queryLogging?.slowThreshold||100;if(error)return"failed";if(durationMs>slowThreshold)return"slow";return"completed"}const warnedCaptureBindings=new Set;function capturesBindingValues(){const setting=config.database?.queryLogging?.captureBindings??envVars.DB_QUERY_LOGGING_CAPTURE_BINDINGS,capture=parseCaptureBindings(setting);if(capture===null){const shown=String(setting);if(!warnedCaptureBindings.has(shown)){warnedCaptureBindings.add(shown);console.warn(`[database] queryLogging.captureBindings (DB_QUERY_LOGGING_CAPTURE_BINDINGS) is "${shown}", which is not true, false, 1, 0, yes, no, on or off; query logs keep only the type of each binding.`)}return!1}if(capture!==void 0)return capture;const appEnv=config.app?.env;return appEnv!=="production"&&appEnv!=="prod"}async function createQueryLogRecord(query,durationMs,status,error,parameters){const connection=config.database.default||"unknown",normalizedQuery=normalizeQuery(query)||query,traceInfo=status==="completed"&&!config.database?.queryLogging?.captureAllTraces?void 0:extractTraceInfo(),bindings=parameters?serializeQueryLogBindings(query,parameters,{captureValues:capturesBindingValues()}):void 0;return{query,normalized_query:normalizedQuery,duration:durationMs,connection,status,error:error?String(error):void 0,executed_at:sqlDateTime(),bindings,trace:traceInfo?.trace,...traceInfo?.caller??{},memory_usage:memoryUsage().heapUsed/1024/1024}}const SECRET_PATTERNS=[/\b(?:sk|pk|rk|api|secret|token|bearer|password|pwd|key)[_-]?[A-Za-z0-9]{12,}/gi,/\bsk_(?:live|test)_[A-Za-z0-9]{20,}/g,/\bAKIA[0-9A-Z]{16}/g,/\b[A-Za-z0-9_-]{40,}\b(?=\s|$|['"])/g];function sanitizeStackTrace(stack){let out=stack;for(const pattern of SECRET_PATTERNS)out=out.replace(pattern,"<redacted>");return out}let lastTraceInfo;function extractTraceInfo(){try{const stack=Error("Stack trace capture").stack||"";if(lastTraceInfo?.trace===stack)return lastTraceInfo;const callerLine=stack.split(`
|
|
2
|
+
`).slice(1).find((line)=>!line.includes("query-logger.ts"));let caller={};if(callerLine){const methodMatch=callerLine.match(/at (.+?) \(/),fileMatch=callerLine.match(/\((.+?):(\d+):(\d+)\)/);if(methodMatch&&methodMatch[1]){const methodParts=methodMatch[1].split(".");caller={model:methodParts.length>1?methodParts[0]:void 0,method:methodParts.length>1?methodParts[1]:methodParts[0]}}if(fileMatch&&fileMatch[1]&&fileMatch[2])caller={...caller,file:fileMatch[1],line:Number.parseInt(fileMatch[2],10)}}const result={trace:sanitizeStackTrace(stack),caller};if(result.trace===stack&&stack.length<=8192)lastTraceInfo=result;return result}catch{return{trace:"",caller:{}}}}async function enhanceWithQueryAnalysis(logRecord){try{const{tables,type}=parseQuery(logRecord.query);logRecord.affected_tables=JSON.stringify(tables||[]);if(type==="SELECT"&&config.database?.queryLogging?.analysis?.explainPlan){const explainResult=await getExplainPlan(logRecord.query);if(explainResult){logRecord.explain_plan=explainResult.plan;logRecord.indexes_used=JSON.stringify(explainResult.indexesUsed||[]);logRecord.missing_indexes=JSON.stringify(explainResult.missingIndexes||[]);if(config.database?.queryLogging?.analysis?.suggestions)logRecord.optimization_suggestions=JSON.stringify(generateOptimizationSuggestions(explainResult,logRecord))}}const tags=[type];if(tables&&tables.length>0)tags.push(...tables.map((table)=>`table:${table}`));logRecord.tags=JSON.stringify(tags)}catch(error){log.debug("Error during query analysis:",error)}}const EXPLAIN_DIALECTS=new Set(["sqlite","mysql","postgres"]);async function getExplainPlan(query){try{const rawDriver=(await import("@stacksjs/config")).config?.database?.default,driver=typeof rawDriver==="string"?rawDriver:"sqlite";if(!EXPLAIN_DIALECTS.has(driver))return null;let sqlText;if(driver==="mysql")sqlText=`EXPLAIN FORMAT=JSON ${query}`;else if(driver==="postgres")sqlText=`EXPLAIN (FORMAT JSON) ${query}`;else sqlText=`EXPLAIN QUERY PLAN ${query}`;const result=await db.unsafe?.(sqlText);if(!result)return null;const raw=result,rows=Array.isArray(raw)?raw:raw?.rows??[],planText=JSON.stringify(rows),indexesUsed=[],missingIndexes=[];if(driver==="sqlite")for(const row of rows){const detail=row?.detail||"",idxMatch=detail.match(/USING (?:COVERING )?INDEX (\w+)/i);if(idxMatch&&idxMatch[1])indexesUsed.push(idxMatch[1]);else{const scanMatch=detail.match(/^SCAN\s+(\w+)/i);if(scanMatch&&scanMatch[1])missingIndexes.push(scanMatch[1])}}else if(driver==="mysql"){const text=planText;for(const m of text.matchAll(/"key"\s*:\s*"([^"]+)"/g))if(m[1])indexesUsed.push(m[1]);for(const m of text.matchAll(/"table_name"\s*:\s*"([^"]+)"[^}]*"access_type"\s*:\s*"ALL"/g))if(m[1])missingIndexes.push(m[1])}else if(driver==="postgres"){const text=planText;for(const m of text.matchAll(/"Index Name"\s*:\s*"([^"]+)"/g))if(m[1])indexesUsed.push(m[1]);for(const m of text.matchAll(/"Node Type"\s*:\s*"Seq Scan"[^}]*"Relation Name"\s*:\s*"([^"]+)"/g))if(m[1])missingIndexes.push(m[1])}return{plan:planText.length>4000?`${planText.slice(0,4000)}\u2026`:planText,indexesUsed:Array.from(new Set(indexesUsed)),missingIndexes:Array.from(new Set(missingIndexes))}}catch(err){log.debug("[query-logger] EXPLAIN failed (non-fatal):",err);return null}}function generateOptimizationSuggestions(explainResult,logRecord){const suggestions=[];if(explainResult.missingIndexes&&explainResult.missingIndexes.length>0)suggestions.push(`Consider adding index on ${explainResult.missingIndexes.join(", ")}`);if(logRecord.status==="slow"){suggestions.push("Consider optimizing this query to reduce execution time");if(logRecord.query.toLowerCase().includes("select *"))suggestions.push("Specify only needed columns instead of using SELECT *");if(!logRecord.query.toLowerCase().includes("limit"))suggestions.push("Consider adding LIMIT clause to reduce result set size")}return suggestions}async function storeQueryLogs(logRecords,reportFailure=!0){try{const values=logRecords;if(values.length===1)await db.insertInto("query_logs").values(values).execute();else if(getDatabaseDialect()==="sqlite"){const columns=Object.keys(values[0]),identifiers=columns.map((column)=>`"${column.replaceAll('"','""')}"`).join(", "),row=`(${columns.map(()=>"?").join(", ")})`,statement=`INSERT INTO query_logs (${identifiers}) VALUES ${values.map(()=>row).join(", ")}`,bindings=values.flatMap((value)=>columns.map((column)=>value[column]??null));db.unsafe("SAVEPOINT stacks_query_log_batch").executeSync();try{db.unsafe(statement,bindings).executeSync();db.unsafe("RELEASE SAVEPOINT stacks_query_log_batch").executeSync()}catch(error){db.unsafe("ROLLBACK TO SAVEPOINT stacks_query_log_batch").executeSync();db.unsafe("RELEASE SAVEPOINT stacks_query_log_batch").executeSync();throw error}}else await db.transaction(async(rawTrx)=>{await rawTrx.insertInto("query_logs").values(values).execute()});return!0}catch(error){if(reportFailure){const message=error instanceof Error?error.message:String(error);if(/no such table|does not exist|doesn't exist/i.test(message)&&/query_logs/i.test(message))log.debug("Query logging will start after the query_logs table is migrated.");else log.error("Failed to store query log:",error)}return!1}}
|
package/dist/replicas.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{AsyncLocalStorage}from"node:async_hooks";const routingContext=new AsyncLocalStorage;let databaseRoutingContextEnabled=!1;export function configureDatabaseRoutingContext(enabled){databaseRoutingContextEnabled=enabled}export function withDatabaseRoutingContext(fn){return databaseRoutingContextEnabled?withRoutingContext(fn):fn()}export function runInDatabaseRoutingContext(fn,arg){return databaseRoutingContextEnabled?routingContext.run({wroteInContext:!1,inTransaction:!1},fn,arg):fn(arg)}export function withRoutingContext(fn){return routingContext.run({wroteInContext:!1,inTransaction:!1},fn)}export function markContextWrote(){const store=routingContext.getStore();if(store)store.wroteInContext=!0}export async function withTransactionContext(fn){const store=routingContext.getStore();if(!store)return fn
|
|
1
|
+
import{AsyncLocalStorage}from"node:async_hooks";const routingContext=new AsyncLocalStorage;let databaseRoutingContextEnabled=!1;export function configureDatabaseRoutingContext(enabled){databaseRoutingContextEnabled=enabled}export function withDatabaseRoutingContext(fn){return databaseRoutingContextEnabled?withRoutingContext(fn):fn()}export function runInDatabaseRoutingContext(fn,arg){return databaseRoutingContextEnabled?routingContext.run({wroteInContext:!1,inTransaction:!1},fn,arg):fn(arg)}export function withRoutingContext(fn){return routingContext.run({wroteInContext:!1,inTransaction:!1},fn)}export function markContextWrote(){const store=routingContext.getStore();if(store)store.wroteInContext=!0}export async function withTransactionContext(fn){const store=routingContext.getStore();if(!store)return routingContext.run({wroteInContext:!1,inTransaction:!0},fn);const previous=store.inTransaction;store.inTransaction=!0;try{return await fn()}finally{store.inTransaction=previous}}export function contextHasWritten(){return routingContext.getStore()?.wroteInContext??!1}export function contextInTransaction(){return routingContext.getStore()?.inTransaction??!1}export function shouldRouteToReplica(options){const{policy,replicas}=options;if(!replicas?.length)return!1;if(!policy?.autoRoute)return!1;if(contextInTransaction())return!1;if(contextHasWritten())return!1;return!0}let roundRobinCursor=0;export function resetReplicaCursor(){roundRobinCursor=0}export function selectReplica(replicas,strategy="round-robin",random=Math.random){if(!replicas.length)return;if(replicas.length===1)return replicas[0];if(strategy==="random")return replicas[Math.floor(random()*replicas.length)];if(strategy==="weighted"){const weights=replicas.map((r)=>Math.max(0,r.weight??1)),total=weights.reduce((sum,w)=>sum+w,0);if(total<=0)return replicas[roundRobinCursor++%replicas.length];let ticket=random()*total;for(let i=0;i<replicas.length;i++){ticket-=weights[i];if(ticket<0)return replicas[i]}return replicas[replicas.length-1]}return replicas[roundRobinCursor++%replicas.length]}export function resolveReplicaConnection(replica,primary){return{database:primary.name??primary.database??"",host:replica.host,port:replica.port??primary.port,username:replica.username??primary.username,password:replica.password??primary.password}}
|
|
@@ -11,8 +11,9 @@ import { db } from './utils';
|
|
|
11
11
|
* handle the rerun case — wrap in `if (!columnExists)` if you need that)
|
|
12
12
|
*
|
|
13
13
|
* Caveats:
|
|
14
|
-
* - SQLite
|
|
15
|
-
*
|
|
14
|
+
* - SQLite has no `ALTER COLUMN`, so a NOT NULL column is added in one
|
|
15
|
+
* statement and needs a `defaultValue` to fill the existing rows; without
|
|
16
|
+
* one this throws before touching the table
|
|
16
17
|
* - Postgres < 11 rewrites the entire table when a default is added;
|
|
17
18
|
* this helper assumes ≥ 11
|
|
18
19
|
*
|
package/dist/safe-migrations.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import{db}from"./utils";export async function addColumnSafely(db,tableName,columnName,options){const{type,defaultValue,notNull=!1,batchSize=1000}=options,dbAny=db,defaultSql=defaultValue===void 0?"":` DEFAULT ${formatDefault(defaultValue)}`;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
|
|
1
|
+
import process from"node:process";import{env as envVars}from"@stacksjs/env";import{mutationCount}from"./affected-rows";import{dialectCapabilities,isKnownDialect}from"./dialect";import{db}from"./utils";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
|
-
|
|
5
|
-
|
|
4
|
+
LIMIT ${batchSize}
|
|
5
|
+
`:`
|
|
6
|
+
UPDATE ${quote(tableName)} SET ${quote(columnName)} = ${formatDefault(value)}
|
|
7
|
+
WHERE ${quote(columnName)} IS NULL
|
|
8
|
+
AND ${rowIdColumn(wire)} IN (
|
|
9
|
+
SELECT ${rowIdColumn(wire)} FROM ${quote(tableName)}
|
|
6
10
|
WHERE ${quote(columnName)} IS NULL
|
|
7
11
|
LIMIT ${batchSize}
|
|
8
12
|
)
|
|
9
|
-
|
|
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=String(process.env.DB_CONNECTION||envVars.DB_CONNECTION||"sqlite").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"}
|
|
@@ -7,8 +7,8 @@ export declare function isInTransaction(): boolean;
|
|
|
7
7
|
/**
|
|
8
8
|
* Enqueue a callback to fire after the surrounding transaction
|
|
9
9
|
* commits. Returns:
|
|
10
|
-
* - `true
|
|
11
|
-
* - `false
|
|
10
|
+
* - `true`: handled, either buffered or discarded from a closed scope
|
|
11
|
+
* - `false`: no transaction context; caller should execute immediately
|
|
12
12
|
*
|
|
13
13
|
* This is the low-level primitive. Higher-level facades (queue
|
|
14
14
|
* dispatch, mailer send, event emit) wrap it with their own
|
|
@@ -24,12 +24,14 @@ export declare function enqueueAfterCommit(callback: AfterCommitCallback): boole
|
|
|
24
24
|
* Used by `@stacksjs/orm`'s `transaction()` wrapper to thread the
|
|
25
25
|
* scope through user code. Apps don't call this directly.
|
|
26
26
|
*
|
|
27
|
-
* Nested calls
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
27
|
+
* Nested calls share an ordered buffer but keep distinct owners. A failed
|
|
28
|
+
* savepoint discards only its own callbacks and its descendants' callbacks;
|
|
29
|
+
* a successful savepoint still waits for the outermost commit to flush.
|
|
30
|
+
* The callback receives an attempt runner for driver retries. Starting a new
|
|
31
|
+
* attempt discards the previous one's callbacks even if COMMIT, rather than
|
|
32
|
+
* the callback, failed. Each attempt gets its own async-context owner.
|
|
31
33
|
*/
|
|
32
|
-
export declare function runInTransactionScope<T>(fn: () => Promise<T>, options?: { onError?: (err: unknown, index: number) => void }): Promise<T>;
|
|
34
|
+
export declare function runInTransactionScope<T>(fn: (runAttempt: <R>(callback: () => Promise<R>) => Promise<R>) => Promise<T>, options?: { onError?: (err: unknown, index: number) => void }): Promise<T>;
|
|
33
35
|
/**
|
|
34
36
|
* Test-only escape hatch — manually flush the current scope's
|
|
35
37
|
* buffered callbacks without ending the transaction. Production
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{AsyncLocalStorage}from"node:async_hooks";const transactionStorage=new AsyncLocalStorage;export function isInTransaction(){return transactionStorage.getStore()!==void 0}export function enqueueAfterCommit(callback){const scope=transactionStorage.getStore();if(!scope)return!1;scope.pending.push(callback);return!0}export async function runInTransactionScope(fn,options={}){const
|
|
1
|
+
import{AsyncLocalStorage}from"node:async_hooks";const transactionStorage=new AsyncLocalStorage;export function isInTransaction(){return transactionStorage.getStore()!==void 0}export function enqueueAfterCommit(callback){const scope=transactionStorage.getStore();if(!scope)return!1;for(let owner=scope;owner;owner=owner.parent)if(!owner.accepting)return!0;scope.pending.push({callback,owner:scope});return!0}export async function runInTransactionScope(fn,options={}){const parent=transactionStorage.getStore(),scope={pending:parent?.pending??[],parent,accepting:!0,onError:parent?.onError??options.onError};let previousAttempt;const runAttempt=async(callback)=>{if(previousAttempt)discardScope(previousAttempt);const attempt={pending:scope.pending,parent:scope,accepting:!0,onError:scope.onError};previousAttempt=attempt;return runOwnedScope(attempt,callback)},result=await runOwnedScope(scope,()=>fn(runAttempt));if(!parent)await flushScope(scope);return result}async function runOwnedScope(scope,callback){try{return await transactionStorage.run(scope,callback)}catch(error){discardScope(scope);throw error}finally{scope.accepting=!1}}function discardScope(scope){scope.accepting=!1;for(let i=scope.pending.length-1;i>=0;i--){let owner=scope.pending[i].owner;while(owner&&owner!==scope)owner=owner.parent;if(owner===scope)scope.pending.splice(i,1)}}async function flushScope(scope){for(let i=0;i<scope.pending.length;i++)try{await scope.pending[i].callback()}catch(err){if(scope.onError)try{scope.onError(err,i)}catch{}else console.error("[transaction-context] after-commit callback threw:",err)}scope.pending.length=0}export async function __flushAfterCommitNow(){const scope=transactionStorage.getStore();if(!scope)return 0;const count=scope.pending.length;await flushScope(scope);return count}export function __pendingAfterCommitCount(){return transactionStorage.getStore()?.pending.length??0}
|
package/dist/utils.d.ts
CHANGED
|
@@ -4,6 +4,16 @@ import type { FrameworkSchema } from './framework-schema';
|
|
|
4
4
|
import type { QueryHooks } from '@stacksjs/query-builder';
|
|
5
5
|
export declare function acquireDbConfigLock(): Promise<() => void>;
|
|
6
6
|
export declare function initializeDbConfig(config: DbConfigSource | null | undefined): void;
|
|
7
|
+
/**
|
|
8
|
+
* Get the dialect type for bun-query-builder.
|
|
9
|
+
*
|
|
10
|
+
* Collapses through the capability table rather than an if-chain: Stacks
|
|
11
|
+
* tracks dialects that bun-query-builder has no separate renderer for
|
|
12
|
+
* (they diverge only in DDL), and those must be handed down as the dialect
|
|
13
|
+
* whose SQL they actually speak. Unknown values still fall back to sqlite,
|
|
14
|
+
* matching the previous behavior.
|
|
15
|
+
*/
|
|
16
|
+
declare function getDialect(): QueryBuilderDialect;
|
|
7
17
|
/**
|
|
8
18
|
* The snapshot directory to hand to `setConfig`, resolved at call time.
|
|
9
19
|
*
|
|
@@ -26,6 +36,14 @@ export declare function ensureDatabaseConfigLoaded(): Promise<void>;
|
|
|
26
36
|
* reset and replay, so both caches must be invalidated as one operation.
|
|
27
37
|
*/
|
|
28
38
|
export declare function resetDatabaseConnection(): void;
|
|
39
|
+
/**
|
|
40
|
+
* Close every framework-owned database pool and wait until it has drained.
|
|
41
|
+
*
|
|
42
|
+
* Unlike resetDatabaseConnection(), this is a shutdown boundary. New queries
|
|
43
|
+
* are rejected while the cached primary and replica builders close so they
|
|
44
|
+
* cannot attach themselves to a pool that is already draining.
|
|
45
|
+
*/
|
|
46
|
+
export declare function closeDatabaseConnection(): Promise<void>;
|
|
29
47
|
/**
|
|
30
48
|
* Fluent chain returned by entry-point methods like `selectFrom`/`updateTable`.
|
|
31
49
|
*
|
|
@@ -151,16 +169,20 @@ export declare interface DatabaseQueryLogEvent {
|
|
|
151
169
|
*
|
|
152
170
|
* `UnsafeReturn` above describes a SELECT - the rows. An UPDATE, INSERT or
|
|
153
171
|
* DELETE resolves to the driver's own result object instead, and every driver
|
|
154
|
-
* spells the affected-row count differently
|
|
155
|
-
*
|
|
156
|
-
*
|
|
172
|
+
* spells the affected-row count differently.
|
|
173
|
+
*
|
|
174
|
+
* **Do not read the count off these fields yourself.** Use `mutationCount`
|
|
175
|
+
* from `./affected-rows`. Hand-rolled readers here were wrong on two of three
|
|
176
|
+
* dialects: `changes ?? rowCount` reads 0 on both PostgreSQL and MySQL, and
|
|
177
|
+
* reading `count` before `affectedRows` reads 0 on MySQL.
|
|
157
178
|
*/
|
|
158
179
|
export declare interface DbWriteResult {
|
|
159
180
|
changes?: number
|
|
160
181
|
numUpdatedRows?: number | bigint
|
|
161
182
|
numAffectedRows?: number | bigint
|
|
162
183
|
numDeletedRows?: number | bigint
|
|
163
|
-
affectedRows?: number
|
|
184
|
+
affectedRows?: number | null
|
|
185
|
+
count?: number | null
|
|
164
186
|
rowsAffected?: number
|
|
165
187
|
rowCount?: number
|
|
166
188
|
lastInsertRowid?: number | bigint
|
|
@@ -542,6 +564,8 @@ export type RowOf<T extends TableName> = T extends keyof DatabaseSchema
|
|
|
542
564
|
* accepted so an app does not have to regenerate to keep compiling.
|
|
543
565
|
*/
|
|
544
566
|
declare type Shape<T> = T extends { columns: infer C } ? C : T;
|
|
567
|
+
/** The initialized database's SQL dialect, including application config overrides. */
|
|
568
|
+
export { getDialect as getDatabaseDialect };
|
|
545
569
|
export { runInDatabaseRoutingContext, withDatabaseRoutingContext };
|
|
546
570
|
// SQLite bootstrap pragmas (stacksjs/stacks#1951) now live in
|
|
547
571
|
// @stacksjs/query-builder — the one chokepoint every framework
|
package/dist/utils.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{AsyncLocalStorage}from"node:async_hooks";import{types as nodeUtilTypes}from"node:util";import{config as queryBuilderConfig,createQueryBuilder,registerPersistentQueryHooks,resetConnection as resetQueryBuilderConnection,setConfig}from"@stacksjs/query-builder";import{SQL}from"bun";import{runInTransactionScope}from"./transaction-context";import{env as envVars}from"@stacksjs/env";import{getConnectionDefaults}from"./defaults";import{isMysqlWire,isVitessSharded,toQueryBuilderDialect}from"./dialect";import{relativeMigrationDirectory,resolveMigrationDirectory,snapshotDirForQueryBuilder}from"./migration-path";import{configureDatabaseRoutingContext,contextInTransaction,markContextWrote,resolveReplicaConnection,runInDatabaseRoutingContext,selectReplica,shouldRouteToReplica,withDatabaseRoutingContext,withTransactionContext}from"./replicas";import{aggregateFunctions}from"./types";const sqliteDefaults=getConnectionDefaults("sqlite",envVars),mysqlDefaults=getConnectionDefaults("mysql",envVars),postgresDefaults=getConnectionDefaults("postgres",envVars);let appEnv=envVars.APP_ENV||"local",dbDriver=envVars.DB_CONNECTION||"sqlite",queryBuilderDialect=toQueryBuilderDialect(dbDriver),queryLoggingEnabled=envVars.DB_QUERY_LOGGING_ENABLED??!isProductionEnvironment(appEnv),dbConfig={connections:{sqlite:{database:sqliteDefaults.database,prefix:""},mysql:{name:mysqlDefaults.database,host:mysqlDefaults.host,username:mysqlDefaults.username,password:mysqlDefaults.password,port:mysqlDefaults.port,prefix:""},singlestore:{name:mysqlDefaults.database,host:mysqlDefaults.host,username:mysqlDefaults.username,password:mysqlDefaults.password,port:mysqlDefaults.port,prefix:""},vitess:{name:mysqlDefaults.database,host:mysqlDefaults.host,username:mysqlDefaults.username,password:mysqlDefaults.password,port:15306,prefix:"",sharded:isVitessSharded()},postgres:{name:postgresDefaults.database,host:postgresDefaults.host,username:postgresDefaults.username,password:postgresDefaults.password,port:postgresDefaults.port,prefix:""}}},dbConfigLockTail=Promise.resolve();const DB_CONFIG_LOCK_MAX_HOLD_MS=60000;export function acquireDbConfigLock(){let release=()=>{};const held=new Promise((resolve)=>{release=resolve}),acquired=dbConfigLockTail.then(()=>{const watchdog=setTimeout(()=>{console.warn(`[database] the db config lock was held for over ${DB_CONFIG_LOCK_MAX_HOLD_MS/1000}s. Releasing it so the queue can proceed - a test file most likely failed before its afterAll ran.`);release()},DB_CONFIG_LOCK_MAX_HOLD_MS);watchdog.unref?.();held.then(()=>clearTimeout(watchdog));return release});dbConfigLockTail=dbConfigLockTail.then(()=>held).catch(()=>{});return acquired}export function initializeDbConfig(config){if(config?.app?.env)appEnv=config.app.env;if(config?.database?.default)dbDriver=config.database.default;queryBuilderDialect=toQueryBuilderDialect(dbDriver);if(config?.database?.connections)dbConfig=config.database;configureDatabaseRoutingContext(getReplicas().length>0);queryLoggingEnabled=config?.database?.queryLogging?.enabled??envVars.DB_QUERY_LOGGING_ENABLED??!isProductionEnvironment(appEnv);syncDatabaseQueryHooks();updateQueryBuilderConfig();_dbInstance=null;_replicaInstances=new Map}function getEnv(){return appEnv}function isProductionEnvironment(value){return value==="production"||value==="prod"}function getDriver(){return dbDriver}function getDatabaseConfig(){return dbConfig}function getDialect(){return queryBuilderDialect}function getDbConfig(){const driver=getDriver(),database=getDatabaseConfig(),env=getEnv();if(driver==="sqlite"){const defaultName=env!=="testing"?"database/stacks.sqlite":"database/stacks_testing.sqlite";return{database:database.connections?.sqlite?.database??defaultName}}if(driver==="mysql")return{database:database.connections?.mysql?.name||"stacks",host:database.connections?.mysql?.host??"127.0.0.1",username:database.connections?.mysql?.username??"root",password:database.connections?.mysql?.password??"",port:database.connections?.mysql?.port??3306};if(driver==="singlestore")return{database:database.connections?.singlestore?.name||"stacks",host:database.connections?.singlestore?.host??"127.0.0.1",username:database.connections?.singlestore?.username??"root",password:database.connections?.singlestore?.password??"",port:database.connections?.singlestore?.port??3306};if(driver==="vitess")return{database:database.connections?.vitess?.name||"stacks",host:database.connections?.vitess?.host??"127.0.0.1",username:database.connections?.vitess?.username??"root",password:database.connections?.vitess?.password??"",port:database.connections?.vitess?.port??15306};if(driver==="postgres"){const dbName=database.connections?.postgres?.name??"stacks";return{database:env==="testing"?`${dbName}_testing`:dbName,host:database.connections?.postgres?.host??"127.0.0.1",username:database.connections?.postgres?.username??"",password:database.connections?.postgres?.password??"",port:database.connections?.postgres?.port??5432}}return{database:":memory:"}}export const QB_SNAPSHOT_DIR="storage/framework/database";export function qbSnapshotDir(){return snapshotDirForQueryBuilder()}export const RAW_QUERY_SOFT_DELETE_CONFIG={enabled:!1,column:"deleted_at",defaultFilter:!0};export function createDatabaseQueryHooks(dispatch){function forward(event){try{Promise.resolve(dispatch(event)).catch(()=>{})}catch{}}return{onQueryEnd:(event)=>forward({query:{sql:event.sql,parameters:event.params},queryDurationMillis:event.durationMs}),onQueryError:(event)=>forward({query:{sql:event.sql,parameters:event.params},queryDurationMillis:event.durationMs,error:event.error})}}let queryLoggerModule;function forwardDatabaseQuery(event){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 getPoolConfig(){const driver=getDriver();if(driver==="sqlite")return;return getDatabaseConfig().connections?.[driver]?.pool}const EMPTY_REPLICAS=[];function getReplicas(){const driver=getDriver();if(driver==="sqlite")return EMPTY_REPLICAS;return getDatabaseConfig().connections?.[driver]?.replicas??EMPTY_REPLICAS}function getReadPolicy(){return getDatabaseConfig().reads??{}}export{runInDatabaseRoutingContext,withDatabaseRoutingContext};function toBunPoolOptions(pool){if(!pool)return{};const options={};if(pool.max!==void 0)options.max=pool.max;if(pool.idleTimeoutMs!==void 0)options.idleTimeout=Math.round(pool.idleTimeoutMs/1000);if(pool.acquireTimeoutMs!==void 0)options.connectionTimeout=Math.round(pool.acquireTimeoutMs/1000);if(pool.maxLifetimeMs!==void 0)options.maxLifetime=Math.round(pool.maxLifetimeMs/1000);return options}function updateQueryBuilderConfig(){const dialect=getDialect(),dbConfigForQb=getDbConfig(),pool=getPoolConfig();setConfig({dialect,vitess:{sharded:isVitessSharded(dbConfig.connections.vitess?.sharded)},database:pool?{...dbConfigForQb,pool}:dbConfigForQb,verbose:getEnv()!=="production",snapshotDir:qbSnapshotDir(),migrationDir:relativeMigrationDirectory(resolveMigrationDirectory(toQueryBuilderDialect(dialect),{snapshotDir:qbSnapshotDir()})),timestamps:{createdAt:"created_at",updatedAt:"updated_at",defaultOrderColumn:"created_at"},softDeletes:RAW_QUERY_SOFT_DELETE_CONFIG})}updateQueryBuilderConfig();let _dbInstance=null,_configInitPromise=null;function ensureConfigLoaded(){if(!_configInitPromise)_configInitPromise=(async()=>{try{const{config,overridesReady}=await import("@stacksjs/config");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 serializeSqliteTransaction(run){if(sqliteTxOwner.getStore())return run();const result=sqliteTxTail.then(()=>sqliteTxOwner.run(!0,run));sqliteTxTail=result.then(()=>{return},()=>{return});return result}function applySqliteTransactionSerialization(instance){const original=instance.transaction.bind(instance);instance.transaction=(...args)=>serializeSqliteTransaction(()=>original(...args))}function applyTransactionDispatchScope(instance){const original=instance.transaction.bind(instance);instance.transaction=(...args)=>runInTransactionScope(()=>original(...args))}function applyTransactionRoutingContext(instance){const original=instance.transaction.bind(instance);instance.transaction=(...args)=>withTransactionContext(()=>original(...args))}function getDb(){if(!_dbInstance){updateQueryBuilderConfig();_dbInstance=createQueryBuilder();if(getDialect()==="sqlite")applySqliteTransactionSerialization(_dbInstance);applyTransactionDispatchScope(_dbInstance);applyTransactionRoutingContext(_dbInstance)}return _dbInstance}let _replicaInstances=new Map;export function resetDatabaseConnection(){resetQueryBuilderConnection();lastGeneralSqliteStatement=void 0;_dbInstance=null;_replicaInstances=new Map}function getReplicaDb(replica){const primary=getDbConfig(),resolved=resolveReplicaConnection(replica,primary),key=`${resolved.host}:${resolved.port??""}`,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=createQueryBuilder({sql});_replicaInstances.set(key,instance);return instance}function getReadDb(){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 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(nodeUtilTypes.isProxy(values)||Object.getPrototypeOf(values)!==Array.prototype||Object.hasOwn(values,Symbol.iterator))return;const snapshot=Array(values.length);for(let index=0;index<values.length;index++){const descriptor=Object.getOwnPropertyDescriptor(values,index);if(!descriptor||!("value"in descriptor))return;snapshot[index]=descriptor.value}return snapshot}function createDeferredSqliteSelect(instance,table){const sqliteDatabase=fastSqliteDatabase(instance);let selectKeyword="SELECT",columns,selectedColumnsSql,predicateColumn,predicateOperator,predicateValue,predicateParameterized=!0,predicateValues,additionalPredicates,orderings,rowLimit,rowOffset,materialized;const materialize=()=>{if(materialized)return materialized;let builder=instance.selectFrom(table);if(columns)builder=builder.select.call(builder,columns);if(selectKeyword==="SELECT DISTINCT")builder=builder.distinct.call(builder);if(predicateColumn!==void 0){if(predicateValues)if(predicateOperator==="LIKE LOWER"||predicateOperator==="NOT LIKE LOWER")builder=builder[predicateOperator==="LIKE LOWER"?"whereILike":"whereNotILike"].call(builder,predicateColumn.slice(6,-1),predicateValues[0]);else builder=builder[predicateOperator==="IN"?"whereIn":"whereNotIn"].call(builder,predicateColumn,predicateValues);else if(predicateParameterized)builder=builder.where.call(builder,predicateColumn,predicateOperator,predicateValue);else builder=builder[predicateOperator==="IS NULL"?"whereNull":"whereNotNull"].call(builder,predicateColumn);if(additionalPredicates)for(const predicate of additionalPredicates)if(predicate.values)if(predicate.operator==="LIKE LOWER"||predicate.operator==="NOT LIKE LOWER"){const method=predicate.operator==="LIKE LOWER"?"whereILike":"whereNotILike";builder=builder[method].call(builder,predicate.column.slice(6,-1),predicate.values[0])}else{const method=predicate.operator==="IN"?"whereIn":"whereNotIn";builder=builder[method].call(builder,predicate.column,predicate.values)}else if(predicate.parameterized)builder=builder.where.call(builder,predicate.column,predicate.operator,predicate.value);else{const method=predicate.operator==="IS NULL"?"whereNull":"whereNotNull";builder=builder[method].call(builder,predicate.column)}}if(orderings){const apply=builder.orderBy;for(const ordering of orderings)builder=apply.call(builder,ordering.column,ordering.direction)}if(rowLimit!==void 0)builder=builder.limit.call(builder,rowLimit);if(rowOffset!==void 0)builder=builder.offset.call(builder,rowOffset);materialized=builder;return builder};let proxy;const executeStatement=(firstOnly=!1,selection)=>{const selected=selection??selectedColumnsSql??"*",effectiveLimit=rowLimit??(firstOnly&&rowOffset===void 0?1:void 0);if(predicateColumn===void 0&&orderings===void 0&&rowOffset===void 0){const cached=lastUnparameterizedSqliteSelect;if(cached&&cached.instance===instance&&cached.selectKeyword===selectKeyword&&cached.selected===selected&&cached.table===table&&cached.limit===effectiveLimit){if(sqliteDatabase){const statement=cached.statement??=sqliteDatabase.query(cached.sql);if(effectiveLimit===1){const row=statement.get();return row===null?[]:[row]}return statement.all()}return runFastSqliteSql(instance,sqliteDatabase,cached.sql)}const limit=effectiveLimit===void 0?"":` LIMIT ${effectiveLimit}`,sql=`${selectKeyword} ${selected} FROM ${table}${limit}`;lastUnparameterizedSqliteSelect={instance,selectKeyword,selected,table,limit:effectiveLimit,sql,statement:sqliteDatabase?.query(sql)};if(lastUnparameterizedSqliteSelect.statement){if(effectiveLimit===1){const row=lastUnparameterizedSqliteSelect.statement.get();return row===null?[]:[row]}return lastUnparameterizedSqliteSelect.statement.all()}return runFastSqliteSql(instance,sqliteDatabase,sql)}if(predicateColumn!==void 0&&predicateValues===void 0&&predicateParameterized&&additionalPredicates===void 0&&orderings===void 0&&rowOffset===void 0){const cached=lastParameterizedSqliteSelect;if(cached&&cached.instance===instance&&cached.selectKeyword===selectKeyword&&cached.selected===selected&&cached.table===table&&cached.predicateColumn===predicateColumn&&cached.predicateOperator===predicateOperator&&cached.limit===effectiveLimit){if(sqliteDatabase){const statement=cached.statement??=sqliteDatabase.query(cached.sql);if(effectiveLimit===1){const row=statement.get(predicateValue);return row===null?[]:[row]}return statement.all(predicateValue)}return runFastSqliteSql(instance,sqliteDatabase,cached.sql,[predicateValue])}const sql=`${selectKeyword} ${selected} FROM ${table} WHERE ${predicateColumn} ${predicateOperator} ?${effectiveLimit===void 0?"":` LIMIT ${effectiveLimit}`}`;lastParameterizedSqliteSelect={instance,selectKeyword,selected,table,predicateColumn,predicateOperator,limit:effectiveLimit,sql,statement:sqliteDatabase?.query(sql)};if(lastParameterizedSqliteSelect.statement){if(effectiveLimit===1){const row=lastParameterizedSqliteSelect.statement.get(predicateValue);return row===null?[]:[row]}return lastParameterizedSqliteSelect.statement.all(predicateValue)}return runFastSqliteSql(instance,sqliteDatabase,sql,[predicateValue])}let query=`${selectKeyword} ${selected} FROM ${table}`;const params=[];if(predicateColumn!==void 0){query+=` WHERE ${predicateColumn} ${predicateOperator}`;if(predicateValues){query+=` (${renderSqlitePlaceholders(predicateValues.length)})`;params.push(...predicateValues)}else if(predicateParameterized){query+=" ?";params.push(predicateValue)}if(additionalPredicates){const additionalCount=additionalPredicates.length;query+=" AND ";for(let index=0;index<additionalCount;index++){if(index>0)query+=" AND ";const predicate=additionalPredicates[index];if(predicate.values){params.push(...predicate.values);query+=`${predicate.column} ${predicate.operator} (${renderSqlitePlaceholders(predicate.values.length)})`;continue}if(predicate.parameterized){params.push(predicate.value);query+=`${predicate.column} ${predicate.operator} ?`;continue}query+=`${predicate.column} ${predicate.operator}`}}}if(orderings)query+=` ORDER BY ${orderings.map((ordering)=>`${ordering.column} ${ordering.direction.toUpperCase()}`).join(", ")}`;if(effectiveLimit!==void 0)query+=` LIMIT ${effectiveLimit}`;if(rowOffset!==void 0)query+=` OFFSET ${rowOffset}`;return runFastSqliteSql(instance,sqliteDatabase,query,params)},executeFirstStatement=()=>{if(rowLimit===0)return;const selected=selectedColumnsSql??"*";if(predicateColumn===void 0&&orderings===void 0&&rowOffset===void 0){const cached=lastUnparameterizedSqliteSelect;if(cached&&cached.instance===instance&&cached.selectKeyword===selectKeyword&&cached.selected===selected&&cached.table===table&&cached.limit===1){if(sqliteDatabase)return(cached.statement??=sqliteDatabase.query(cached.sql)).get()??void 0;return runFastSqliteSql(instance,sqliteDatabase,cached.sql)[0]}const sql=`${selectKeyword} ${selected} FROM ${table} LIMIT 1`,statement=sqliteDatabase?.query(sql);lastUnparameterizedSqliteSelect={instance,selectKeyword,selected,table,limit:1,sql,statement};return statement?statement.get()??void 0:runFastSqliteSql(instance,sqliteDatabase,sql)[0]}if(predicateColumn!==void 0&&predicateValues===void 0&&predicateParameterized&&additionalPredicates===void 0&&orderings===void 0&&rowOffset===void 0){const cached=lastParameterizedSqliteSelect;if(cached&&cached.instance===instance&&cached.selectKeyword===selectKeyword&&cached.selected===selected&&cached.table===table&&cached.predicateColumn===predicateColumn&&cached.predicateOperator===predicateOperator&&cached.limit===1){if(sqliteDatabase)return(cached.statement??=sqliteDatabase.query(cached.sql)).get(predicateValue)??void 0;return runFastSqliteSql(instance,sqliteDatabase,cached.sql,[predicateValue])[0]}const sql=`${selectKeyword} ${selected} FROM ${table} WHERE ${predicateColumn} ${predicateOperator} ? LIMIT 1`,statement=sqliteDatabase?.query(sql);lastParameterizedSqliteSelect={instance,selectKeyword,selected,table,predicateColumn,predicateOperator,limit:1,sql,statement};return statement?statement.get(predicateValue)??void 0:runFastSqliteSql(instance,sqliteDatabase,sql,[predicateValue])[0]}return executeStatement(!0)[0]},createExtensions=()=>({distinct(){if(selectKeyword==="SELECT DISTINCT")return materialize().distinct();selectKeyword="SELECT DISTINCT";return proxy},where(column,operator,value){if(column!==null&&typeof column==="object"&&!Array.isArray(column)&&operator===void 0&&value===void 0){const prototype=Object.getPrototypeOf(column),entries=prototype===Object.prototype||prototype===null?Object.entries(column):[];if(entries.length>0&&entries.every(([key])=>isSimpleSqliteColumn(key))){for(const[key,entryValue]of entries)if(predicateColumn===void 0){predicateColumn=key;predicateOperator="=";predicateValue=entryValue;predicateParameterized=!0;predicateValues=void 0}else(additionalPredicates??=[]).push({column:key,operator:"=",value:entryValue,parameterized:!0});return proxy}}const builder=materialize();return builder.where.call(builder,column,operator,value)},whereNull(column){if(typeof column!=="string"||!isSimpleSqliteColumn(column)){const builder=materialize();return builder.whereNull.call(builder,column)}if(predicateColumn===void 0){predicateColumn=column;predicateOperator="IS NULL";predicateValue=void 0;predicateParameterized=!1;predicateValues=void 0}else(additionalPredicates??=[]).push({column,operator:"IS NULL",value:void 0,parameterized:!1});return proxy},whereNotNull(column){if(typeof column!=="string"||!isSimpleSqliteColumn(column)){const builder=materialize();return builder.whereNotNull.call(builder,column)}if(predicateColumn===void 0){predicateColumn=column;predicateOperator="IS NOT NULL";predicateValue=void 0;predicateParameterized=!1;predicateValues=void 0}else(additionalPredicates??=[]).push({column,operator:"IS NOT NULL",value:void 0,parameterized:!1});return proxy},whereLike(column,value,caseSensitive=!1){if(typeof column!=="string"||!isSimpleSqliteColumn(column)){const builder=materialize();return builder.whereLike.call(builder,column,value,caseSensitive)}if(!caseSensitive)return proxy.whereILike(column,value);if(predicateColumn===void 0){predicateColumn=column;predicateOperator="LIKE";predicateValue=value;predicateParameterized=!0;predicateValues=void 0}else(additionalPredicates??=[]).push({column,operator:"LIKE",value,parameterized:!0});return proxy},whereNotLike(column,value,caseSensitive=!1){if(typeof column!=="string"||!isSimpleSqliteColumn(column)){const builder=materialize();return builder.whereNotLike.call(builder,column,value,caseSensitive)}if(!caseSensitive)return proxy.whereNotILike(column,value);if(predicateColumn===void 0){predicateColumn=column;predicateOperator="NOT LIKE";predicateValue=value;predicateParameterized=!0;predicateValues=void 0}else(additionalPredicates??=[]).push({column,operator:"NOT LIKE",value,parameterized:!0});return proxy},whereILike(column,value){if(typeof column!=="string"||!isSimpleSqliteColumn(column)){const builder=materialize();return builder.whereILike.call(builder,column,value)}const sqlColumn=`LOWER(${column})`;if(predicateColumn===void 0){predicateColumn=sqlColumn;predicateOperator="LIKE LOWER";predicateValue=void 0;predicateParameterized=!1;predicateValues=[value]}else(additionalPredicates??=[]).push({column:sqlColumn,operator:"LIKE LOWER",value:void 0,parameterized:!1,values:[value]});return proxy},whereNotILike(column,value){if(typeof column!=="string"||!isSimpleSqliteColumn(column)){const builder=materialize();return builder.whereNotILike.call(builder,column,value)}const sqlColumn=`LOWER(${column})`;if(predicateColumn===void 0){predicateColumn=sqlColumn;predicateOperator="NOT LIKE LOWER";predicateValue=void 0;predicateParameterized=!1;predicateValues=[value]}else(additionalPredicates??=[]).push({column:sqlColumn,operator:"NOT LIKE LOWER",value:void 0,parameterized:!1,values:[value]});return proxy},whereIn(column,values){if(typeof column!=="string"||!isSimpleSqliteColumn(column)||!Array.isArray(values)){const builder=materialize();return builder.whereIn.call(builder,column,values)}const snapshot=values.slice();if(predicateColumn===void 0){predicateColumn=column;predicateOperator="IN";predicateValue=void 0;predicateParameterized=!1;predicateValues=snapshot}else(additionalPredicates??=[]).push({column,operator:"IN",value:void 0,parameterized:!1,values:snapshot});return proxy},whereNotIn(column,values){if(typeof column!=="string"||!isSimpleSqliteColumn(column)||!Array.isArray(values)){const builder=materialize();return builder.whereNotIn.call(builder,column,values)}const snapshot=values.slice();if(predicateColumn===void 0){predicateColumn=column;predicateOperator="NOT IN";predicateValue=void 0;predicateParameterized=!1;predicateValues=snapshot}else(additionalPredicates??=[]).push({column,operator:"NOT IN",value:void 0,parameterized:!1,values:snapshot});return proxy},whereBetween(...args){const[column,startOrValues,end]=args,values=Array.isArray(startOrValues)?startOrValues:args.length>=3?[startOrValues,end]:void 0;if(typeof column!=="string"||!isSimpleSqliteColumn(column)||!values||values.length<2){const builder=materialize();return builder.whereBetween.call(builder,...args)}const lower=values[0],upper=values[1];if(predicateColumn===void 0){predicateColumn=column;predicateOperator=">=";predicateValue=lower;predicateParameterized=!0;predicateValues=void 0}else(additionalPredicates??=[]).push({column,operator:">=",value:lower,parameterized:!0});(additionalPredicates??=[]).push({column,operator:"<=",value:upper,parameterized:!0});return proxy},whereDate(...args){const[column,operator,date]=args;if(typeof column!=="string"||!isSimpleSqliteColumn(column)||typeof operator!=="string"||!SIMPLE_SQLITE_OPERATORS.has(operator.toLowerCase())||typeof date!=="string"&&!(date instanceof Date)){const builder=materialize();return builder.whereDate.call(builder,...args)}const normalizedDate=date instanceof Date?date.toISOString():date;if(predicateColumn===void 0){predicateColumn=column;predicateOperator=operator;predicateValue=normalizedDate;predicateParameterized=!0;predicateValues=void 0}else(additionalPredicates??=[]).push({column,operator,value:normalizedDate,parameterized:!0});return proxy},orderBy(column,direction="asc"){if(typeof column!=="string"||!isSimpleSqliteColumn(column)){const builder=materialize();return builder.orderBy.call(builder,column,direction)}(orderings??=[]).push({column,direction:direction==="asc"?"asc":"desc"});return proxy},orderByDesc(column){if(typeof column!=="string"||!isSimpleSqliteColumn(column)){const builder=materialize();return builder.orderByDesc.call(builder,column)}(orderings??=[]).push({column,direction:"desc"});return proxy},latest(column){const resolvedColumn=column??queryBuilderConfig.timestamps.defaultOrderColumn;if(typeof resolvedColumn!=="string"||!isSimpleSqliteColumn(resolvedColumn)){const builder=materialize();return builder.latest.call(builder,column)}(orderings??=[]).push({column:resolvedColumn,direction:"desc"});return proxy},oldest(column){const resolvedColumn=column??queryBuilderConfig.timestamps.defaultOrderColumn;if(typeof resolvedColumn!=="string"||!isSimpleSqliteColumn(resolvedColumn)){const builder=materialize();return builder.oldest.call(builder,column)}(orderings??=[]).push({column:resolvedColumn,direction:"asc"});return proxy},offset(value){if(typeof value!=="number"||value<0||!Number.isInteger(value)){const builder=materialize();return builder.offset.call(builder,value)}rowOffset=value;return proxy}});let extensions;const base={select(value){if(materialized)return Reflect.apply(materialized.select,materialized,arguments);if(typeof value==="string"){if(value!=="*"&&!isSimpleSqliteSelection(value)){const builder=materialize();return builder.select.call(builder,value)}columns=value;selectedColumnsSql=value;return proxy}const selected=Array.isArray(value)?value:[value];let simple=!1,selection;const cached=lastSqliteSelection;if(cached&&cached.columns.length===selected.length){simple=!0;for(let index=0;simple&&index<selected.length;index++)simple=cached.columns[index]===selected[index];if(simple)selection=cached.sql}if(!simple){simple=selected.length>0;selection="";for(let index=0;simple&&index<selected.length;index++){const column=selected[index];simple=typeof column==="string"&&(column==="*"||isSimpleSqliteSelection(column));if(simple)selection+=index===0?column:`, ${column}`}if(simple)lastSqliteSelection={columns:selected.slice(),sql:selection}}if(!simple){const builder=materialize();return builder.select.call(builder,value)}columns=selected;selectedColumnsSql=selection;return proxy},where(column,operator,value){if(materialized)return Reflect.apply(materialized.where,materialized,arguments);if(typeof column!=="string"||!isSimpleSqliteColumn(column)||typeof operator!=="string"||operator!=="="&&!SIMPLE_SQLITE_OPERATORS.has(operator)&&!SIMPLE_SQLITE_OPERATORS.has(operator.toLowerCase())){if(typeof column==="string"&&isSimpleSqliteColumn(column)&&typeof operator==="string"){const normalized=operator.toLowerCase();if(normalized==="in"||normalized==="not in"){const values=Array.isArray(value)?snapshotSimpleSqliteMembershipValues(value):[value];if(values!==void 0){const membershipOperator=normalized==="in"?"IN":"NOT IN";if(predicateColumn===void 0){predicateColumn=column;predicateOperator=membershipOperator;predicateValue=void 0;predicateParameterized=!1;predicateValues=values}else(additionalPredicates??=[]).push({column,operator:membershipOperator,value:void 0,parameterized:!1,values});return proxy}}}return(extensions??=createExtensions()).where(column,operator,value)}if(predicateColumn===void 0){predicateColumn=column;predicateOperator=operator;predicateValue=value;predicateParameterized=!0;predicateValues=void 0}else(additionalPredicates??=[]).push({column,operator,value,parameterized:!0});return proxy},limit(value){if(materialized)return Reflect.apply(materialized.limit,materialized,arguments);if(typeof value!=="number"||value<0||!Number.isInteger(value)){const builder=materialize();return builder.limit.call(builder,value)}rowLimit=value;return proxy},execute(){if(materialized)return Reflect.apply(materialized.execute,materialized,arguments);try{return Promise.resolve(executeStatement())}catch(error){return Promise.reject(error)}},executeSync(){if(materialized)return Reflect.apply(materialized.executeSync,materialized,arguments);return executeStatement()},executeTakeFirstSync(){if(materialized)return Reflect.apply(materialized.executeTakeFirstSync,materialized,arguments);return executeFirstStatement()}};let forwardedMethods;proxy=new Proxy(base,{get(target,property){if(materialized){const builder=materialized,value=builder[property];return typeof value==="function"?value.bind(builder):value}if(property==="executeTakeFirst")return target.executeTakeFirst??=function(){if(materialized)return Reflect.apply(materialized.executeTakeFirst,materialized,arguments);try{return Promise.resolve(executeFirstStatement())}catch(error){return Promise.reject(error)}};if(GUARDED_SQLITE_SELECT_METHODS.has(property))return target[property];const cached=forwardedMethods?.[property];if(cached!==void 0)return cached;const value=target[property]??resolveDeferredSqliteTerminal(target,property,executeStatement,executeFirstStatement,materialize,proxy)??(extensions??=createExtensions())[property];if(value===void 0){const builder=materialize(),fallback=builder[property];return typeof fallback==="function"?fallback.bind(builder):fallback}if(typeof value!=="function")return value;const forward=(...args)=>{const builder=materialized;return builder?Reflect.apply(builder[property],builder,args):Reflect.apply(value,proxy,args)};(forwardedMethods??=Object.create(null))[property]=forward;return forward}});return proxy}function selectFromDatabase(table){const dialect=getDialect(),instance=dialect==="sqlite"?getDb():getReadDb();if(dialect==="sqlite"&&!queryBuilderConfig.softDeletes?.enabled&&!hasActiveQueryBuilderHooks()&&isSimpleSqliteTable(table))return createDeferredSqliteSelect(instance,table);return instance.selectFrom(table)}function selectFromExplicitReadDatabase(table){const dialect=getDialect(),instance=getExplicitReadDb();if(dialect==="sqlite"&&!queryBuilderConfig.softDeletes?.enabled&&!hasActiveQueryBuilderHooks()&&isSimpleSqliteTable(table))return createDeferredSqliteSelect(instance,table);return instance.selectFrom(table)}function unsafeDatabase(query,params){return getDb().unsafe(query,params)}function unsafeExplicitReadDatabase(query,params){return getExplicitReadDb().unsafe(query,params)}function insertIntoDatabase(table){markContextWrote();return getDb().insertInto(table)}function updateTableDatabase(table){markContextWrote();return getDb().updateTable(table)}function deleteFromDatabase(table){markContextWrote();return getDb().deleteFrom(table)}function tableDatabase(table){return getDb().table(table)}function selectDatabase(table,...columns){return getReadDb().select(table,...columns)}function selectFromSubDatabase(subquery,alias){return getReadDb().selectFromSub(subquery,alias)}function tableExplicitReadDatabase(table){return getExplicitReadDb().table(table)}function selectExplicitReadDatabase(table,...columns){return getExplicitReadDb().select(table,...columns)}function selectFromSubExplicitReadDatabase(subquery,alias){return getExplicitReadDb().selectFromSub(subquery,alias)}const dbFallback=new Proxy({},{get(_target,prop){if(typeof prop==="string"&&WRITE_ENTRY_POINTS.has(prop))markContextWrote();const instance=typeof prop==="string"&&READ_ENTRY_POINTS.has(prop)?getReadDb():getDb(),value=instance[prop];if(typeof value==="function")return value.bind(instance);return value}});export const db=Object.create(dbFallback);Object.defineProperties(db,{deleteFrom:{value:deleteFromDatabase},fn:{value:aggregateFunctions},insertInto:{value:insertIntoDatabase},read:{get:()=>readDb},select:{value:selectDatabase},selectFrom:{value:selectFromDatabase},selectFromSub:{value:selectFromSubDatabase},table:{value:tableDatabase},unsafe:{value:unsafeDatabase},updateTable:{value:updateTableDatabase}});const readDbFallback=new Proxy({},{get(_target,prop){const instance=getExplicitReadDb(),value=instance[prop];if(typeof value==="function")return value.bind(instance);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},unsafe:{value:unsafeExplicitReadDatabase}});export{setConfig};
|
|
1
|
+
import{AsyncLocalStorage}from"node:async_hooks";import{types as nodeUtilTypes}from"node:util";import{config as queryBuilderConfig,createQueryBuilder,registerPersistentQueryHooks,resetConnection as resetQueryBuilderConnection,setConfig}from"@stacksjs/query-builder";import{SQL}from"bun";import{closeDatabaseConnectionsAndPending,retireDatabaseConnections}from"./connection-lifecycle";import{runInTransactionScope}from"./transaction-context";import{env as envVars}from"@stacksjs/env";import{getConnectionDefaults}from"./defaults";import{isMysqlWire,isVitessSharded,toQueryBuilderDialect}from"./dialect";import{relativeMigrationDirectory,resolveMigrationDirectory,snapshotDirForQueryBuilder}from"./migration-path";import{configureDatabaseRoutingContext,contextInTransaction,markContextWrote,resolveReplicaConnection,runInDatabaseRoutingContext,selectReplica,shouldRouteToReplica,withDatabaseRoutingContext,withTransactionContext}from"./replicas";import{aggregateFunctions}from"./types";const sqliteDefaults=getConnectionDefaults("sqlite",envVars),mysqlDefaults=getConnectionDefaults("mysql",envVars),postgresDefaults=getConnectionDefaults("postgres",envVars);let appEnv=envVars.APP_ENV||"local",dbDriver=envVars.DB_CONNECTION||"sqlite",queryBuilderDialect=toQueryBuilderDialect(dbDriver),queryLoggingEnabled=envVars.DB_QUERY_LOGGING_ENABLED??!isProductionEnvironment(appEnv),dbConfig={connections:{sqlite:{database:sqliteDefaults.database,prefix:""},mysql:{name:mysqlDefaults.database,host:mysqlDefaults.host,username:mysqlDefaults.username,password:mysqlDefaults.password,port:mysqlDefaults.port,prefix:""},singlestore:{name:mysqlDefaults.database,host:mysqlDefaults.host,username:mysqlDefaults.username,password:mysqlDefaults.password,port:mysqlDefaults.port,prefix:""},vitess:{name:mysqlDefaults.database,host:mysqlDefaults.host,username:mysqlDefaults.username,password:mysqlDefaults.password,port:15306,prefix:"",sharded:isVitessSharded()},postgres:{name:postgresDefaults.database,host:postgresDefaults.host,username:postgresDefaults.username,password:postgresDefaults.password,port:postgresDefaults.port,prefix:""}}},dbConfigLockTail=Promise.resolve();const DB_CONFIG_LOCK_MAX_HOLD_MS=60000;export function acquireDbConfigLock(){let release=()=>{};const held=new Promise((resolve)=>{release=resolve}),acquired=dbConfigLockTail.then(()=>{const watchdog=setTimeout(()=>{console.warn(`[database] the db config lock was held for over ${DB_CONFIG_LOCK_MAX_HOLD_MS/1000}s. Releasing it so the queue can proceed - a test file most likely failed before its afterAll ran.`);release()},DB_CONFIG_LOCK_MAX_HOLD_MS);watchdog.unref?.();held.then(()=>clearTimeout(watchdog));return release});dbConfigLockTail=dbConfigLockTail.then(()=>held).catch(()=>{});return acquired}export function initializeDbConfig(config){if(config?.app?.env)appEnv=config.app.env;if(config?.database?.default)dbDriver=config.database.default;queryBuilderDialect=toQueryBuilderDialect(dbDriver);if(config?.database?.connections)dbConfig=config.database;configureDatabaseRoutingContext(getReplicas().length>0);queryLoggingEnabled=config?.database?.queryLogging?.enabled??envVars.DB_QUERY_LOGGING_ENABLED??!isProductionEnvironment(appEnv);syncDatabaseQueryHooks();updateQueryBuilderConfig();_dbInstance=null;_replicaInstances=new Map}function getEnv(){return appEnv}function isProductionEnvironment(value){return value==="production"||value==="prod"}function getDriver(){return dbDriver}function getDatabaseConfig(){return dbConfig}function getDialect(){return queryBuilderDialect}export{getDialect as getDatabaseDialect};function getDbConfig(){const driver=getDriver(),database=getDatabaseConfig(),env=getEnv();if(driver==="sqlite"){const defaultName=env!=="testing"?"database/stacks.sqlite":"database/stacks_testing.sqlite";return{database:database.connections?.sqlite?.database??defaultName}}if(driver==="mysql")return{database:database.connections?.mysql?.name||"stacks",host:database.connections?.mysql?.host??"127.0.0.1",username:database.connections?.mysql?.username??"root",password:database.connections?.mysql?.password??"",port:database.connections?.mysql?.port??3306};if(driver==="singlestore")return{database:database.connections?.singlestore?.name||"stacks",host:database.connections?.singlestore?.host??"127.0.0.1",username:database.connections?.singlestore?.username??"root",password:database.connections?.singlestore?.password??"",port:database.connections?.singlestore?.port??3306};if(driver==="vitess")return{database:database.connections?.vitess?.name||"stacks",host:database.connections?.vitess?.host??"127.0.0.1",username:database.connections?.vitess?.username??"root",password:database.connections?.vitess?.password??"",port:database.connections?.vitess?.port??15306};if(driver==="postgres"){const dbName=database.connections?.postgres?.name??"stacks";return{database:env==="testing"?`${dbName}_testing`:dbName,host:database.connections?.postgres?.host??"127.0.0.1",username:database.connections?.postgres?.username??"",password:database.connections?.postgres?.password??"",port:database.connections?.postgres?.port??5432}}return{database:":memory:"}}export const QB_SNAPSHOT_DIR="storage/framework/database";export function qbSnapshotDir(){return snapshotDirForQueryBuilder()}export const RAW_QUERY_SOFT_DELETE_CONFIG={enabled:!1,column:"deleted_at",defaultFilter:!0};export function createDatabaseQueryHooks(dispatch){function forward(event){try{Promise.resolve(dispatch(event)).catch(()=>{})}catch{}}return{onQueryEnd:(event)=>forward({query:{sql:event.sql,parameters:event.params},queryDurationMillis:event.durationMs}),onQueryError:(event)=>forward({query:{sql:event.sql,parameters:event.params},queryDurationMillis:event.durationMs,error:event.error})}}let queryLoggerModule;function forwardDatabaseQuery(event){transactionConnection.exit(()=>{queryLoggerModule??=import("./query-logger").catch((error)=>{queryLoggerModule=void 0;throw error});queryLoggerModule.then(({logQuery})=>logQuery(event)).catch(()=>{})})}let unregisterDatabaseQueryHooks;function syncDatabaseQueryHooks(){const shouldInstall=!isProductionEnvironment(appEnv)||queryLoggingEnabled;if(shouldInstall&&!unregisterDatabaseQueryHooks)unregisterDatabaseQueryHooks=registerPersistentQueryHooks(createDatabaseQueryHooks(forwardDatabaseQuery));else if(!shouldInstall&&unregisterDatabaseQueryHooks){unregisterDatabaseQueryHooks();unregisterDatabaseQueryHooks=void 0}}syncDatabaseQueryHooks();function getPoolConfig(){const driver=getDriver();if(driver==="sqlite")return;return getDatabaseConfig().connections?.[driver]?.pool}const EMPTY_REPLICAS=[];function getReplicas(){const driver=getDriver();if(driver==="sqlite")return EMPTY_REPLICAS;return getDatabaseConfig().connections?.[driver]?.replicas??EMPTY_REPLICAS}function getReadPolicy(){return getDatabaseConfig().reads??{}}export{runInDatabaseRoutingContext,withDatabaseRoutingContext};function toBunPoolOptions(pool){if(!pool)return{};const options={};if(pool.max!==void 0)options.max=pool.max;if(pool.idleTimeoutMs!==void 0)options.idleTimeout=Math.round(pool.idleTimeoutMs/1000);if(pool.acquireTimeoutMs!==void 0)options.connectionTimeout=Math.round(pool.acquireTimeoutMs/1000);if(pool.maxLifetimeMs!==void 0)options.maxLifetime=Math.round(pool.maxLifetimeMs/1000);return options}function updateQueryBuilderConfig(){const dialect=getDialect(),dbConfigForQb=getDbConfig(),pool=getPoolConfig();setConfig({dialect,vitess:{sharded:isVitessSharded(dbConfig.connections.vitess?.sharded)},database:pool?{...dbConfigForQb,pool}:dbConfigForQb,verbose:getEnv()!=="production",snapshotDir:qbSnapshotDir(),migrationDir:relativeMigrationDirectory(resolveMigrationDirectory(toQueryBuilderDialect(dialect),{snapshotDir:qbSnapshotDir()})),timestamps:{createdAt:"created_at",updatedAt:"updated_at",defaultOrderColumn:"created_at"},softDeletes:RAW_QUERY_SOFT_DELETE_CONFIG})}updateQueryBuilderConfig();let _dbInstance=null,_configInitPromise=null;function ensureConfigLoaded(){if(!_configInitPromise)_configInitPromise=(async()=>{try{const{config,overridesReady}=await import("@stacksjs/config");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"&&!nodeUtilTypes.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{return await original((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}})}),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=`${resolved.host}:${resolved.port??""}`,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=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(nodeUtilTypes.isProxy(values)||Object.getPrototypeOf(values)!==Array.prototype||Object.hasOwn(values,Symbol.iterator))return;const snapshot=Array(values.length);for(let index=0;index<values.length;index++){const descriptor=Object.getOwnPropertyDescriptor(values,index);if(!descriptor||!("value"in descriptor))return;snapshot[index]=descriptor.value}return snapshot}function createDeferredSqliteSelect(instance,table){const sqliteDatabase=fastSqliteDatabase(instance);let selectKeyword="SELECT",columns,selectedColumnsSql,predicateColumn,predicateOperator,predicateValue,predicateParameterized=!0,predicateValues,additionalPredicates,orderings,rowLimit,rowOffset,materialized;const materialize=()=>{if(materialized)return materialized;let builder=instance.selectFrom(table);if(columns)builder=builder.select.call(builder,columns);if(selectKeyword==="SELECT DISTINCT")builder=builder.distinct.call(builder);if(predicateColumn!==void 0){if(predicateValues)if(predicateOperator==="LIKE LOWER"||predicateOperator==="NOT LIKE LOWER")builder=builder[predicateOperator==="LIKE LOWER"?"whereILike":"whereNotILike"].call(builder,predicateColumn.slice(6,-1),predicateValues[0]);else builder=builder[predicateOperator==="IN"?"whereIn":"whereNotIn"].call(builder,predicateColumn,predicateValues);else if(predicateParameterized)builder=builder.where.call(builder,predicateColumn,predicateOperator,predicateValue);else builder=builder[predicateOperator==="IS NULL"?"whereNull":"whereNotNull"].call(builder,predicateColumn);if(additionalPredicates)for(const predicate of additionalPredicates)if(predicate.values)if(predicate.operator==="LIKE LOWER"||predicate.operator==="NOT LIKE LOWER"){const method=predicate.operator==="LIKE LOWER"?"whereILike":"whereNotILike";builder=builder[method].call(builder,predicate.column.slice(6,-1),predicate.values[0])}else{const method=predicate.operator==="IN"?"whereIn":"whereNotIn";builder=builder[method].call(builder,predicate.column,predicate.values)}else if(predicate.parameterized)builder=builder.where.call(builder,predicate.column,predicate.operator,predicate.value);else{const method=predicate.operator==="IS NULL"?"whereNull":"whereNotNull";builder=builder[method].call(builder,predicate.column)}}if(orderings){const apply=builder.orderBy;for(const ordering of orderings)builder=apply.call(builder,ordering.column,ordering.direction)}if(rowLimit!==void 0)builder=builder.limit.call(builder,rowLimit);if(rowOffset!==void 0)builder=builder.offset.call(builder,rowOffset);materialized=builder;return builder};let proxy;const executeStatement=(firstOnly=!1,selection)=>{const selected=selection??selectedColumnsSql??"*",effectiveLimit=rowLimit??(firstOnly&&rowOffset===void 0?1:void 0);if(predicateColumn===void 0&&orderings===void 0&&rowOffset===void 0){const cached=lastUnparameterizedSqliteSelect;if(cached&&cached.instance===instance&&cached.selectKeyword===selectKeyword&&cached.selected===selected&&cached.table===table&&cached.limit===effectiveLimit){if(sqliteDatabase){const statement=cached.statement??=sqliteDatabase.query(cached.sql);if(effectiveLimit===1){const row=statement.get();return row===null?[]:[row]}return statement.all()}return runFastSqliteSql(instance,sqliteDatabase,cached.sql)}const limit=effectiveLimit===void 0?"":` LIMIT ${effectiveLimit}`,sql=`${selectKeyword} ${selected} FROM ${table}${limit}`;lastUnparameterizedSqliteSelect={instance,selectKeyword,selected,table,limit:effectiveLimit,sql,statement:sqliteDatabase?.query(sql)};if(lastUnparameterizedSqliteSelect.statement){if(effectiveLimit===1){const row=lastUnparameterizedSqliteSelect.statement.get();return row===null?[]:[row]}return lastUnparameterizedSqliteSelect.statement.all()}return runFastSqliteSql(instance,sqliteDatabase,sql)}if(predicateColumn!==void 0&&predicateValues===void 0&&predicateParameterized&&additionalPredicates===void 0&&orderings===void 0&&rowOffset===void 0){const cached=lastParameterizedSqliteSelect;if(cached&&cached.instance===instance&&cached.selectKeyword===selectKeyword&&cached.selected===selected&&cached.table===table&&cached.predicateColumn===predicateColumn&&cached.predicateOperator===predicateOperator&&cached.limit===effectiveLimit){if(sqliteDatabase){const statement=cached.statement??=sqliteDatabase.query(cached.sql);if(effectiveLimit===1){const row=statement.get(predicateValue);return row===null?[]:[row]}return statement.all(predicateValue)}return runFastSqliteSql(instance,sqliteDatabase,cached.sql,[predicateValue])}const sql=`${selectKeyword} ${selected} FROM ${table} WHERE ${predicateColumn} ${predicateOperator} ?${effectiveLimit===void 0?"":` LIMIT ${effectiveLimit}`}`;lastParameterizedSqliteSelect={instance,selectKeyword,selected,table,predicateColumn,predicateOperator,limit:effectiveLimit,sql,statement:sqliteDatabase?.query(sql)};if(lastParameterizedSqliteSelect.statement){if(effectiveLimit===1){const row=lastParameterizedSqliteSelect.statement.get(predicateValue);return row===null?[]:[row]}return lastParameterizedSqliteSelect.statement.all(predicateValue)}return runFastSqliteSql(instance,sqliteDatabase,sql,[predicateValue])}let query=`${selectKeyword} ${selected} FROM ${table}`;const params=[];if(predicateColumn!==void 0){query+=` WHERE ${predicateColumn} ${predicateOperator}`;if(predicateValues){query+=` (${renderSqlitePlaceholders(predicateValues.length)})`;params.push(...predicateValues)}else if(predicateParameterized){query+=" ?";params.push(predicateValue)}if(additionalPredicates){const additionalCount=additionalPredicates.length;query+=" AND ";for(let index=0;index<additionalCount;index++){if(index>0)query+=" AND ";const predicate=additionalPredicates[index];if(predicate.values){params.push(...predicate.values);query+=`${predicate.column} ${predicate.operator} (${renderSqlitePlaceholders(predicate.values.length)})`;continue}if(predicate.parameterized){params.push(predicate.value);query+=`${predicate.column} ${predicate.operator} ?`;continue}query+=`${predicate.column} ${predicate.operator}`}}}if(orderings)query+=` ORDER BY ${orderings.map((ordering)=>`${ordering.column} ${ordering.direction.toUpperCase()}`).join(", ")}`;if(effectiveLimit!==void 0)query+=` LIMIT ${effectiveLimit}`;if(rowOffset!==void 0)query+=` OFFSET ${rowOffset}`;return runFastSqliteSql(instance,sqliteDatabase,query,params)},executeFirstStatement=()=>{if(rowLimit===0)return;const selected=selectedColumnsSql??"*";if(predicateColumn===void 0&&orderings===void 0&&rowOffset===void 0){const cached=lastUnparameterizedSqliteSelect;if(cached&&cached.instance===instance&&cached.selectKeyword===selectKeyword&&cached.selected===selected&&cached.table===table&&cached.limit===1){if(sqliteDatabase)return(cached.statement??=sqliteDatabase.query(cached.sql)).get()??void 0;return runFastSqliteSql(instance,sqliteDatabase,cached.sql)[0]}const sql=`${selectKeyword} ${selected} FROM ${table} LIMIT 1`,statement=sqliteDatabase?.query(sql);lastUnparameterizedSqliteSelect={instance,selectKeyword,selected,table,limit:1,sql,statement};return statement?statement.get()??void 0:runFastSqliteSql(instance,sqliteDatabase,sql)[0]}if(predicateColumn!==void 0&&predicateValues===void 0&&predicateParameterized&&additionalPredicates===void 0&&orderings===void 0&&rowOffset===void 0){const cached=lastParameterizedSqliteSelect;if(cached&&cached.instance===instance&&cached.selectKeyword===selectKeyword&&cached.selected===selected&&cached.table===table&&cached.predicateColumn===predicateColumn&&cached.predicateOperator===predicateOperator&&cached.limit===1){if(sqliteDatabase)return(cached.statement??=sqliteDatabase.query(cached.sql)).get(predicateValue)??void 0;return runFastSqliteSql(instance,sqliteDatabase,cached.sql,[predicateValue])[0]}const sql=`${selectKeyword} ${selected} FROM ${table} WHERE ${predicateColumn} ${predicateOperator} ? LIMIT 1`,statement=sqliteDatabase?.query(sql);lastParameterizedSqliteSelect={instance,selectKeyword,selected,table,predicateColumn,predicateOperator,limit:1,sql,statement};return statement?statement.get(predicateValue)??void 0:runFastSqliteSql(instance,sqliteDatabase,sql,[predicateValue])[0]}return executeStatement(!0)[0]},createExtensions=()=>({distinct(){if(selectKeyword==="SELECT DISTINCT")return materialize().distinct();selectKeyword="SELECT DISTINCT";return proxy},where(column,operator,value){if(column!==null&&typeof column==="object"&&!Array.isArray(column)&&operator===void 0&&value===void 0){const prototype=Object.getPrototypeOf(column),entries=prototype===Object.prototype||prototype===null?Object.entries(column):[];if(entries.length>0&&entries.every(([key])=>isSimpleSqliteColumn(key))){for(const[key,entryValue]of entries)if(predicateColumn===void 0){predicateColumn=key;predicateOperator="=";predicateValue=entryValue;predicateParameterized=!0;predicateValues=void 0}else(additionalPredicates??=[]).push({column:key,operator:"=",value:entryValue,parameterized:!0});return proxy}}const builder=materialize();return builder.where.call(builder,column,operator,value)},whereNull(column){if(typeof column!=="string"||!isSimpleSqliteColumn(column)){const builder=materialize();return builder.whereNull.call(builder,column)}if(predicateColumn===void 0){predicateColumn=column;predicateOperator="IS NULL";predicateValue=void 0;predicateParameterized=!1;predicateValues=void 0}else(additionalPredicates??=[]).push({column,operator:"IS NULL",value:void 0,parameterized:!1});return proxy},whereNotNull(column){if(typeof column!=="string"||!isSimpleSqliteColumn(column)){const builder=materialize();return builder.whereNotNull.call(builder,column)}if(predicateColumn===void 0){predicateColumn=column;predicateOperator="IS NOT NULL";predicateValue=void 0;predicateParameterized=!1;predicateValues=void 0}else(additionalPredicates??=[]).push({column,operator:"IS NOT NULL",value:void 0,parameterized:!1});return proxy},whereLike(column,value,caseSensitive=!1){if(typeof column!=="string"||!isSimpleSqliteColumn(column)){const builder=materialize();return builder.whereLike.call(builder,column,value,caseSensitive)}if(!caseSensitive)return proxy.whereILike(column,value);if(predicateColumn===void 0){predicateColumn=column;predicateOperator="LIKE";predicateValue=value;predicateParameterized=!0;predicateValues=void 0}else(additionalPredicates??=[]).push({column,operator:"LIKE",value,parameterized:!0});return proxy},whereNotLike(column,value,caseSensitive=!1){if(typeof column!=="string"||!isSimpleSqliteColumn(column)){const builder=materialize();return builder.whereNotLike.call(builder,column,value,caseSensitive)}if(!caseSensitive)return proxy.whereNotILike(column,value);if(predicateColumn===void 0){predicateColumn=column;predicateOperator="NOT LIKE";predicateValue=value;predicateParameterized=!0;predicateValues=void 0}else(additionalPredicates??=[]).push({column,operator:"NOT LIKE",value,parameterized:!0});return proxy},whereILike(column,value){if(typeof column!=="string"||!isSimpleSqliteColumn(column)){const builder=materialize();return builder.whereILike.call(builder,column,value)}const sqlColumn=`LOWER(${column})`;if(predicateColumn===void 0){predicateColumn=sqlColumn;predicateOperator="LIKE LOWER";predicateValue=void 0;predicateParameterized=!1;predicateValues=[value]}else(additionalPredicates??=[]).push({column:sqlColumn,operator:"LIKE LOWER",value:void 0,parameterized:!1,values:[value]});return proxy},whereNotILike(column,value){if(typeof column!=="string"||!isSimpleSqliteColumn(column)){const builder=materialize();return builder.whereNotILike.call(builder,column,value)}const sqlColumn=`LOWER(${column})`;if(predicateColumn===void 0){predicateColumn=sqlColumn;predicateOperator="NOT LIKE LOWER";predicateValue=void 0;predicateParameterized=!1;predicateValues=[value]}else(additionalPredicates??=[]).push({column:sqlColumn,operator:"NOT LIKE LOWER",value:void 0,parameterized:!1,values:[value]});return proxy},whereIn(column,values){if(typeof column!=="string"||!isSimpleSqliteColumn(column)||!Array.isArray(values)){const builder=materialize();return builder.whereIn.call(builder,column,values)}const snapshot=values.slice();if(predicateColumn===void 0){predicateColumn=column;predicateOperator="IN";predicateValue=void 0;predicateParameterized=!1;predicateValues=snapshot}else(additionalPredicates??=[]).push({column,operator:"IN",value:void 0,parameterized:!1,values:snapshot});return proxy},whereNotIn(column,values){if(typeof column!=="string"||!isSimpleSqliteColumn(column)||!Array.isArray(values)){const builder=materialize();return builder.whereNotIn.call(builder,column,values)}const snapshot=values.slice();if(predicateColumn===void 0){predicateColumn=column;predicateOperator="NOT IN";predicateValue=void 0;predicateParameterized=!1;predicateValues=snapshot}else(additionalPredicates??=[]).push({column,operator:"NOT IN",value:void 0,parameterized:!1,values:snapshot});return proxy},whereBetween(...args){const[column,startOrValues,end]=args,values=Array.isArray(startOrValues)?startOrValues:args.length>=3?[startOrValues,end]:void 0;if(typeof column!=="string"||!isSimpleSqliteColumn(column)||!values||values.length<2){const builder=materialize();return builder.whereBetween.call(builder,...args)}const lower=values[0],upper=values[1];if(predicateColumn===void 0){predicateColumn=column;predicateOperator=">=";predicateValue=lower;predicateParameterized=!0;predicateValues=void 0}else(additionalPredicates??=[]).push({column,operator:">=",value:lower,parameterized:!0});(additionalPredicates??=[]).push({column,operator:"<=",value:upper,parameterized:!0});return proxy},whereDate(...args){const[column,operator,date]=args;if(typeof column!=="string"||!isSimpleSqliteColumn(column)||typeof operator!=="string"||!SIMPLE_SQLITE_OPERATORS.has(operator.toLowerCase())||typeof date!=="string"&&!(date instanceof Date)){const builder=materialize();return builder.whereDate.call(builder,...args)}const normalizedDate=date instanceof Date?date.toISOString():date;if(predicateColumn===void 0){predicateColumn=column;predicateOperator=operator;predicateValue=normalizedDate;predicateParameterized=!0;predicateValues=void 0}else(additionalPredicates??=[]).push({column,operator,value:normalizedDate,parameterized:!0});return proxy},orderBy(column,direction="asc"){if(typeof column!=="string"||!isSimpleSqliteColumn(column)){const builder=materialize();return builder.orderBy.call(builder,column,direction)}(orderings??=[]).push({column,direction:direction==="asc"?"asc":"desc"});return proxy},orderByDesc(column){if(typeof column!=="string"||!isSimpleSqliteColumn(column)){const builder=materialize();return builder.orderByDesc.call(builder,column)}(orderings??=[]).push({column,direction:"desc"});return proxy},latest(column){const resolvedColumn=column??queryBuilderConfig.timestamps.defaultOrderColumn;if(typeof resolvedColumn!=="string"||!isSimpleSqliteColumn(resolvedColumn)){const builder=materialize();return builder.latest.call(builder,column)}(orderings??=[]).push({column:resolvedColumn,direction:"desc"});return proxy},oldest(column){const resolvedColumn=column??queryBuilderConfig.timestamps.defaultOrderColumn;if(typeof resolvedColumn!=="string"||!isSimpleSqliteColumn(resolvedColumn)){const builder=materialize();return builder.oldest.call(builder,column)}(orderings??=[]).push({column:resolvedColumn,direction:"asc"});return proxy},offset(value){if(typeof value!=="number"||value<0||!Number.isInteger(value)){const builder=materialize();return builder.offset.call(builder,value)}rowOffset=value;return proxy}});let extensions;const base={select(value){if(materialized)return Reflect.apply(materialized.select,materialized,arguments);if(typeof value==="string"){if(value!=="*"&&!isSimpleSqliteSelection(value)){const builder=materialize();return builder.select.call(builder,value)}columns=value;selectedColumnsSql=value;return proxy}const selected=Array.isArray(value)?value:[value];let simple=!1,selection;const cached=lastSqliteSelection;if(cached&&cached.columns.length===selected.length){simple=!0;for(let index=0;simple&&index<selected.length;index++)simple=cached.columns[index]===selected[index];if(simple)selection=cached.sql}if(!simple){simple=selected.length>0;selection="";for(let index=0;simple&&index<selected.length;index++){const column=selected[index];simple=typeof column==="string"&&(column==="*"||isSimpleSqliteSelection(column));if(simple)selection+=index===0?column:`, ${column}`}if(simple)lastSqliteSelection={columns:selected.slice(),sql:selection}}if(!simple){const builder=materialize();return builder.select.call(builder,value)}columns=selected;selectedColumnsSql=selection;return proxy},where(column,operator,value){if(materialized)return Reflect.apply(materialized.where,materialized,arguments);if(typeof column!=="string"||!isSimpleSqliteColumn(column)||typeof operator!=="string"||operator!=="="&&!SIMPLE_SQLITE_OPERATORS.has(operator)&&!SIMPLE_SQLITE_OPERATORS.has(operator.toLowerCase())){if(typeof column==="string"&&isSimpleSqliteColumn(column)&&typeof operator==="string"){const normalized=operator.toLowerCase();if(normalized==="in"||normalized==="not in"){const values=Array.isArray(value)?snapshotSimpleSqliteMembershipValues(value):[value];if(values!==void 0){const membershipOperator=normalized==="in"?"IN":"NOT IN";if(predicateColumn===void 0){predicateColumn=column;predicateOperator=membershipOperator;predicateValue=void 0;predicateParameterized=!1;predicateValues=values}else(additionalPredicates??=[]).push({column,operator:membershipOperator,value:void 0,parameterized:!1,values});return proxy}}}return(extensions??=createExtensions()).where(column,operator,value)}if(predicateColumn===void 0){predicateColumn=column;predicateOperator=operator;predicateValue=value;predicateParameterized=!0;predicateValues=void 0}else(additionalPredicates??=[]).push({column,operator,value,parameterized:!0});return proxy},limit(value){if(materialized)return Reflect.apply(materialized.limit,materialized,arguments);if(typeof value!=="number"||value<0||!Number.isInteger(value)){const builder=materialize();return builder.limit.call(builder,value)}rowLimit=value;return proxy},execute(){if(materialized)return Reflect.apply(materialized.execute,materialized,arguments);try{return Promise.resolve(executeStatement())}catch(error){return Promise.reject(error)}},executeSync(){if(materialized)return Reflect.apply(materialized.executeSync,materialized,arguments);return executeStatement()},executeTakeFirstSync(){if(materialized)return Reflect.apply(materialized.executeTakeFirstSync,materialized,arguments);return executeFirstStatement()}};let forwardedMethods;proxy=new Proxy(base,{get(target,property){if(materialized){const builder=materialized,value=builder[property];return typeof value==="function"?value.bind(builder):value}if(property==="executeTakeFirst")return target.executeTakeFirst??=function(){if(materialized)return Reflect.apply(materialized.executeTakeFirst,materialized,arguments);try{return Promise.resolve(executeFirstStatement())}catch(error){return Promise.reject(error)}};if(GUARDED_SQLITE_SELECT_METHODS.has(property))return target[property];const cached=forwardedMethods?.[property];if(cached!==void 0)return cached;const value=target[property]??resolveDeferredSqliteTerminal(target,property,executeStatement,executeFirstStatement,materialize,proxy)??(extensions??=createExtensions())[property];if(value===void 0){const builder=materialize(),fallback=builder[property];return typeof fallback==="function"?fallback.bind(builder):fallback}if(typeof value!=="function")return value;const forward=(...args)=>{const builder=materialized;return builder?Reflect.apply(builder[property],builder,args):Reflect.apply(value,proxy,args)};(forwardedMethods??=Object.create(null))[property]=forward;return forward}});return proxy}function selectFromDatabase(table){const dialect=getDialect(),instance=dialect==="sqlite"?getDb():getReadDb();if(dialect==="sqlite"&&!queryBuilderConfig.softDeletes?.enabled&&!hasActiveQueryBuilderHooks()&&isSimpleSqliteTable(table))return guardTransactionQuery(createDeferredSqliteSelect(instance,table));return guardTransactionQuery(instance.selectFrom(table))}function selectFromExplicitReadDatabase(table){const dialect=getDialect(),instance=getExplicitReadDb();if(dialect==="sqlite"&&!queryBuilderConfig.softDeletes?.enabled&&!hasActiveQueryBuilderHooks()&&isSimpleSqliteTable(table))return guardTransactionQuery(createDeferredSqliteSelect(instance,table));return guardTransactionQuery(instance.selectFrom(table))}function unsafeDatabase(query,params){return guardTransactionQuery(getDb().unsafe(query,params))}function unsafeExplicitReadDatabase(query,params){return guardTransactionQuery(getExplicitReadDb().unsafe(query,params))}function insertIntoDatabase(table){markContextWrote();return guardTransactionQuery(getDb().insertInto(table))}function updateTableDatabase(table){markContextWrote();return guardTransactionQuery(getDb().updateTable(table))}function deleteFromDatabase(table){markContextWrote();return guardTransactionQuery(getDb().deleteFrom(table))}function tableDatabase(table){return guardTransactionQuery(getDb().table(table))}function selectDatabase(table,...columns){return guardTransactionQuery(getReadDb().select(table,...columns))}function selectFromSubDatabase(subquery,alias){return guardTransactionQuery(getReadDb().selectFromSub(subquery,alias))}function tableExplicitReadDatabase(table){return guardTransactionQuery(getExplicitReadDb().table(table))}function selectExplicitReadDatabase(table,...columns){return guardTransactionQuery(getExplicitReadDb().select(table,...columns))}function selectFromSubExplicitReadDatabase(subquery,alias){return guardTransactionQuery(getExplicitReadDb().selectFromSub(subquery,alias))}function transactionalDatabase(callback,options){return(...args)=>getDb().transaction((tx)=>callback(tx,...args),options)}const dbFallback=new Proxy({},{get(_target,prop){const resolveInstance=typeof prop==="string"&&READ_ENTRY_POINTS.has(prop)?getReadDb:getDb,value=resolveInstance()[prop];if(typeof value==="function")return forwardDatabaseMethod(prop,resolveInstance);return value}});function forwardDatabaseMethod(prop,resolveInstance){const scope=transactionConnection.getStore();return(...args)=>{if(scope){assertTransactionConnection(scope);if(transactionConnection.getStore()!==scope)throw Error("A transaction query must execute in the callback that created it.")}const instance=resolveInstance();if(typeof prop==="string"&&WRITE_ENTRY_POINTS.has(prop))markContextWrote();return guardTransactionQuery(Reflect.apply(Reflect.get(instance,prop),instance,args))}}export const db=Object.create(dbFallback);Object.defineProperties(db,{deleteFrom:{value:deleteFromDatabase},fn:{value:aggregateFunctions},insertInto:{value:insertIntoDatabase},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}});export{setConfig};
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@stacksjs/database",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"sideEffects": false,
|
|
5
|
-
"version": "0.74.
|
|
5
|
+
"version": "0.74.47",
|
|
6
6
|
"description": "The Stacks database integration.",
|
|
7
7
|
"author": "Chris Breuer",
|
|
8
8
|
"contributors": [
|
|
@@ -60,26 +60,26 @@
|
|
|
60
60
|
"prepublishOnly": "bun run build"
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
|
-
"@stacksjs/config": "0.74.
|
|
64
|
-
"@stacksjs/env": "0.74.
|
|
65
|
-
"@stacksjs/error-handling": "0.74.
|
|
66
|
-
"@stacksjs/faker": "^0.74.
|
|
67
|
-
"@stacksjs/features": "0.74.
|
|
68
|
-
"@stacksjs/logging": "0.74.
|
|
69
|
-
"@stacksjs/model-meta": "0.74.
|
|
70
|
-
"@stacksjs/path": "0.74.
|
|
71
|
-
"@stacksjs/query-builder": "^0.74.
|
|
72
|
-
"@stacksjs/security": "0.74.
|
|
73
|
-
"@stacksjs/storage": "0.74.
|
|
74
|
-
"@stacksjs/strings": "0.74.
|
|
63
|
+
"@stacksjs/config": "0.74.47",
|
|
64
|
+
"@stacksjs/env": "0.74.47",
|
|
65
|
+
"@stacksjs/error-handling": "0.74.47",
|
|
66
|
+
"@stacksjs/faker": "^0.74.47",
|
|
67
|
+
"@stacksjs/features": "0.74.47",
|
|
68
|
+
"@stacksjs/logging": "0.74.47",
|
|
69
|
+
"@stacksjs/model-meta": "0.74.47",
|
|
70
|
+
"@stacksjs/path": "0.74.47",
|
|
71
|
+
"@stacksjs/query-builder": "^0.74.47",
|
|
72
|
+
"@stacksjs/security": "0.74.47",
|
|
73
|
+
"@stacksjs/storage": "0.74.47",
|
|
74
|
+
"@stacksjs/strings": "0.74.47",
|
|
75
75
|
"@stacksjs/ts-validation": "^0.5.6",
|
|
76
|
-
"bun-query-builder": "^0.2.
|
|
76
|
+
"bun-query-builder": "^0.2.70",
|
|
77
77
|
"dynamodb-tooling": "^0.3.2"
|
|
78
78
|
},
|
|
79
79
|
"devDependencies": {
|
|
80
|
-
"@stacksjs/cli": "0.74.
|
|
81
|
-
"@stacksjs/router": "0.74.
|
|
82
|
-
"@stacksjs/utils": "0.74.
|
|
80
|
+
"@stacksjs/cli": "0.74.47",
|
|
81
|
+
"@stacksjs/router": "0.74.47",
|
|
82
|
+
"@stacksjs/utils": "0.74.47",
|
|
83
83
|
"better-dx": "^0.2.24"
|
|
84
84
|
}
|
|
85
85
|
}
|