@stacksjs/database 0.70.260 → 0.70.262
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/database.d.ts +1 -0
- package/dist/database.js +1 -1
- package/dist/dialect.d.ts +10 -25
- package/dist/dialect.js +1 -1
- package/dist/driver-config.d.ts +3 -1
- package/dist/driver-config.js +1 -1
- package/dist/migration-lock.d.ts +1 -1
- package/dist/migration-lock.js +1 -1
- package/dist/migrations.d.ts +16 -1
- package/dist/migrations.js +5 -3
- package/dist/utils.js +1 -1
- package/package.json +11 -11
package/dist/database.d.ts
CHANGED
package/dist/database.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{toQueryBuilderDialect}from"./dialect";import{QB_SNAPSHOT_DIR}from"./utils";import{createQueryBuilder,setConfig}from"@stacksjs/query-builder";import{env as stacksEnv}from"@stacksjs/env";export class Database{_queryBuilder=null;_options;_initialized=!1;constructor(options){this._options={verbose:!1,timestamps:{createdAt:"created_at",updatedAt:"updated_at",defaultOrderColumn:"created_at"},softDeletes:{enabled:!1,column:"deleted_at",defaultFilter:!0},...options}}get driver(){return this._options.driver}get connection(){return this._options.connection}get isInitialized(){return this._initialized}get query(){if(!this._queryBuilder)this.initialize();return this._queryBuilder}initialize(){if(this._initialized)return;setConfig({snapshotDir:QB_SNAPSHOT_DIR,dialect:toQueryBuilderDialect(this._options.driver),database:this._options.connection,verbose:this._options.verbose,timestamps:this._options.timestamps,softDeletes:this._options.softDeletes,hooks:this._options.hooks});this._queryBuilder=createQueryBuilder();this._initialized=!0}switchDriver(driver,connection){this.close();this._options.driver=driver;this._options.connection=connection;this._initialized=!1;this.initialize()}async close(){if(this._queryBuilder&&typeof this._queryBuilder.close==="function")await this._queryBuilder.close();this._queryBuilder=null;this._initialized=!1}static fromConfig(config,env){const driver=config.default;let connection;switch(driver){case"sqlite":{connection={database:env==="testing"?config.connections.sqlite?.database?.replace(".sqlite","_testing.sqlite")||"database/stacks_testing.sqlite":config.connections.sqlite?.database||"database/stacks.sqlite"};break}case"mysql":{const mysql=config.connections.mysql;connection={database:mysql?.name||"stacks",host:mysql?.host||"127.0.0.1",port:mysql?.port||3306,username:mysql?.username||"root",password:mysql?.password||""};break}case"singlestore":{const singlestore=config.connections.singlestore;connection={database:singlestore?.name||"stacks",host:singlestore?.host||"127.0.0.1",port:singlestore?.port||3306,username:singlestore?.username||"root",password:singlestore?.password||""};break}case"postgres":{const postgres=config.connections.postgres,dbName=postgres?.name||"stacks";connection={database:env==="testing"?`${dbName}_testing`:dbName,host:postgres?.host||"127.0.0.1",port:postgres?.port||5432,username:postgres?.username||"",password:postgres?.password||""};break}case"dynamodb":throw Error("[database] DB_CONNECTION=dynamodb is not a SQL driver. Use the entity-style `dynamo.entity(...)` API for DynamoDB access, or set DB_CONNECTION to sqlite/mysql/postgres for SQL workloads.");default:throw Error(`[database] Unknown DB_CONNECTION "${String(driver)}". Allowed values: sqlite, mysql, postgres.`)}return new Database({driver,connection,verbose:env!=="production"})}static fromEnv(){const driver=stacksEnv.DB_CONNECTION||"sqlite";let connection;switch(driver){case"sqlite":connection={database:stacksEnv.DB_DATABASE||"database/stacks.sqlite"};break;case"mysql":case"singlestore":connection={database:stacksEnv.DB_DATABASE||"stacks",host:stacksEnv.DB_HOST||"127.0.0.1",port:stacksEnv.DB_PORT||3306,username:stacksEnv.DB_USERNAME||"root",password:stacksEnv.DB_PASSWORD||""};break;case"postgres":connection={database:stacksEnv.DB_DATABASE||"stacks",host:stacksEnv.DB_HOST||"127.0.0.1",port:stacksEnv.DB_PORT||5432,username:stacksEnv.DB_USERNAME||"",password:stacksEnv.DB_PASSWORD||""};break;default:connection={database:":memory:"}}return new Database({driver,connection,verbose:stacksEnv.APP_ENV!=="prod"})}}export function createDatabase(options){return new Database(options)}export function createSqliteDatabase(database,options){return new Database({driver:"sqlite",connection:{database},...options})}export function createPostgresDatabase(connection,options){return new Database({driver:"postgres",connection,...options})}export function createMysqlDatabase(connection,options){return new Database({driver:"mysql",connection,...options})}
|
|
1
|
+
import{isVitessSharded,toQueryBuilderDialect}from"./dialect";import{QB_SNAPSHOT_DIR}from"./utils";import{createQueryBuilder,setConfig}from"@stacksjs/query-builder";import{env as stacksEnv}from"@stacksjs/env";export class Database{_queryBuilder=null;_options;_initialized=!1;constructor(options){this._options={verbose:!1,timestamps:{createdAt:"created_at",updatedAt:"updated_at",defaultOrderColumn:"created_at"},softDeletes:{enabled:!1,column:"deleted_at",defaultFilter:!0},...options}}get driver(){return this._options.driver}get connection(){return this._options.connection}get isInitialized(){return this._initialized}get query(){if(!this._queryBuilder)this.initialize();return this._queryBuilder}initialize(){if(this._initialized)return;setConfig({snapshotDir:QB_SNAPSHOT_DIR,dialect:toQueryBuilderDialect(this._options.driver),vitess:{sharded:isVitessSharded(this._options.connection.sharded)},database:this._options.connection,verbose:this._options.verbose,timestamps:this._options.timestamps,softDeletes:this._options.softDeletes,hooks:this._options.hooks});this._queryBuilder=createQueryBuilder();this._initialized=!0}switchDriver(driver,connection){this.close();this._options.driver=driver;this._options.connection=connection;this._initialized=!1;this.initialize()}async close(){if(this._queryBuilder&&typeof this._queryBuilder.close==="function")await this._queryBuilder.close();this._queryBuilder=null;this._initialized=!1}static fromConfig(config,env){const driver=config.default;let connection;switch(driver){case"sqlite":{connection={database:env==="testing"?config.connections.sqlite?.database?.replace(".sqlite","_testing.sqlite")||"database/stacks_testing.sqlite":config.connections.sqlite?.database||"database/stacks.sqlite"};break}case"mysql":{const mysql=config.connections.mysql;connection={database:mysql?.name||"stacks",host:mysql?.host||"127.0.0.1",port:mysql?.port||3306,username:mysql?.username||"root",password:mysql?.password||""};break}case"singlestore":{const singlestore=config.connections.singlestore;connection={database:singlestore?.name||"stacks",host:singlestore?.host||"127.0.0.1",port:singlestore?.port||3306,username:singlestore?.username||"root",password:singlestore?.password||""};break}case"postgres":{const postgres=config.connections.postgres,dbName=postgres?.name||"stacks";connection={database:env==="testing"?`${dbName}_testing`:dbName,host:postgres?.host||"127.0.0.1",port:postgres?.port||5432,username:postgres?.username||"",password:postgres?.password||""};break}case"dynamodb":throw Error("[database] DB_CONNECTION=dynamodb is not a SQL driver. Use the entity-style `dynamo.entity(...)` API for DynamoDB access, or set DB_CONNECTION to sqlite/mysql/postgres for SQL workloads.");default:throw Error(`[database] Unknown DB_CONNECTION "${String(driver)}". Allowed values: sqlite, mysql, postgres.`)}return new Database({driver,connection,verbose:env!=="production"})}static fromEnv(){const driver=stacksEnv.DB_CONNECTION||"sqlite";let connection;switch(driver){case"sqlite":connection={database:stacksEnv.DB_DATABASE||"database/stacks.sqlite"};break;case"mysql":case"singlestore":connection={database:stacksEnv.DB_DATABASE||"stacks",host:stacksEnv.DB_HOST||"127.0.0.1",port:stacksEnv.DB_PORT||3306,username:stacksEnv.DB_USERNAME||"root",password:stacksEnv.DB_PASSWORD||""};break;case"postgres":connection={database:stacksEnv.DB_DATABASE||"stacks",host:stacksEnv.DB_HOST||"127.0.0.1",port:stacksEnv.DB_PORT||5432,username:stacksEnv.DB_USERNAME||"",password:stacksEnv.DB_PASSWORD||""};break;default:connection={database:":memory:"}}return new Database({driver,connection,verbose:stacksEnv.APP_ENV!=="prod"})}}export function createDatabase(options){return new Database(options)}export function createSqliteDatabase(database,options){return new Database({driver:"sqlite",connection:{database},...options})}export function createPostgresDatabase(connection,options){return new Database({driver:"postgres",connection,...options})}export function createMysqlDatabase(connection,options){return new Database({driver:"mysql",connection,...options})}
|
package/dist/dialect.d.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the Vitess topology from an explicit option or the conventional
|
|
3
|
+
* environment variable. The historical behavior was sharded, so an omitted
|
|
4
|
+
* setting remains conservative and never enables unsupported DDL by accident.
|
|
5
|
+
*/
|
|
6
|
+
export declare function isVitessSharded(explicit?: boolean): boolean;
|
|
1
7
|
/**
|
|
2
8
|
* Look up a dialect's capabilities.
|
|
3
9
|
*
|
|
@@ -7,7 +13,7 @@
|
|
|
7
13
|
* existed, rather than throwing during module load in an app that is merely
|
|
8
14
|
* misconfigured.
|
|
9
15
|
*/
|
|
10
|
-
export declare function dialectCapabilities(dialect: string): DialectCapabilities;
|
|
16
|
+
export declare function dialectCapabilities(dialect: string, options?: DialectCapabilityOptions): DialectCapabilities;
|
|
11
17
|
/** Whether the framework has an explicit capability row for this dialect. */
|
|
12
18
|
export declare function isKnownDialect(dialect: string): boolean;
|
|
13
19
|
/** Every dialect with a capability row, for CLI help and validation messages. */
|
|
@@ -44,30 +50,9 @@ export declare interface DialectCapabilities {
|
|
|
44
50
|
requiresOnlineDdl: boolean
|
|
45
51
|
supportsCreateIndexIfNotExists: boolean
|
|
46
52
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
* Every dialect-conditional branch in the framework used to be an inline
|
|
51
|
-
* string comparison — `driver === 'mysql' || driver === 'singlestore'` in
|
|
52
|
-
* sql-helpers, a second copy in utils' `getDialect()`, a third in
|
|
53
|
-
* driver-config's connection-string switch, a fourth in the migration
|
|
54
|
-
* corpus auditor. Adding a dialect meant finding all of them, and missing
|
|
55
|
-
* one degraded silently: a MySQL-wire dialect that fell through to the
|
|
56
|
-
* SQLite branch emitted `datetime('now')` and `AUTOINCREMENT` at a MySQL
|
|
57
|
-
* server and only failed at execution time.
|
|
58
|
-
*
|
|
59
|
-
* This module is the single place that knows what a dialect can do. The
|
|
60
|
-
* branches above now ask it instead of re-deriving the answer, so a new
|
|
61
|
-
* entry in `CAPABILITIES` is the whole change.
|
|
62
|
-
*
|
|
63
|
-
* The distinction that matters most here is **wire protocol vs. feature
|
|
64
|
-
* set**. Several dialects speak the MySQL wire protocol and share its DML
|
|
65
|
-
* rendering (backtick quoting, `?` placeholders, `LAST_INSERT_ID()`,
|
|
66
|
-
* `ON DUPLICATE KEY UPDATE`) while diverging sharply in what DDL they
|
|
67
|
-
* accept. SingleStore takes MySQL's wire and rejects its foreign keys.
|
|
68
|
-
* Treating "is MySQL" as one boolean is what made that divergence hard to
|
|
69
|
-
* express, so `wire` and the `supports*` flags are tracked separately.
|
|
70
|
-
*/
|
|
53
|
+
export declare interface DialectCapabilityOptions {
|
|
54
|
+
vitessSharded?: boolean
|
|
55
|
+
}
|
|
71
56
|
/**
|
|
72
57
|
* The connection protocol a dialect speaks. Determines placeholder style,
|
|
73
58
|
* identifier quoting, and which connection URL scheme dials it.
|
package/dist/dialect.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const CAPABILITIES={sqlite:{dialect:"sqlite",wire:"sqlite",queryBuilderDialect:"sqlite",identifierQuote:'"',supportsForeignKeys:!0,supportsAutoIncrement:!0,supportsAtomicMultiTableTransactions:!0,requiresOnlineDdl:!1,supportsCreateIndexIfNotExists:!0},mysql:{dialect:"mysql",wire:"mysql",queryBuilderDialect:"mysql",defaultPort:3306,identifierQuote:"`",supportsForeignKeys:!0,supportsAutoIncrement:!0,supportsAtomicMultiTableTransactions:!0,requiresOnlineDdl:!1,supportsCreateIndexIfNotExists:!1},singlestore:{dialect:"singlestore",wire:"mysql",queryBuilderDialect:"singlestore",defaultPort:3306,identifierQuote:"`",supportsForeignKeys:!1,supportsAutoIncrement:!0,supportsAtomicMultiTableTransactions:!1,requiresOnlineDdl:!1,supportsCreateIndexIfNotExists:!1},vitess:{dialect:"vitess",wire:"mysql",queryBuilderDialect:"vitess",defaultPort:15306,identifierQuote:"`",supportsForeignKeys:!1,supportsAutoIncrement:!1,supportsAtomicMultiTableTransactions:!1,requiresOnlineDdl:!0,supportsCreateIndexIfNotExists:!1},postgres:{dialect:"postgres",wire:"postgres",queryBuilderDialect:"postgres",defaultPort:5432,identifierQuote:'"',supportsForeignKeys:!0,supportsAutoIncrement:!0,supportsAtomicMultiTableTransactions:!0,requiresOnlineDdl:!1,supportsCreateIndexIfNotExists:!0}};export function dialectCapabilities(dialect){
|
|
1
|
+
import process from"node:process";const CAPABILITIES={sqlite:{dialect:"sqlite",wire:"sqlite",queryBuilderDialect:"sqlite",identifierQuote:'"',supportsForeignKeys:!0,supportsAutoIncrement:!0,supportsAtomicMultiTableTransactions:!0,requiresOnlineDdl:!1,supportsCreateIndexIfNotExists:!0},mysql:{dialect:"mysql",wire:"mysql",queryBuilderDialect:"mysql",defaultPort:3306,identifierQuote:"`",supportsForeignKeys:!0,supportsAutoIncrement:!0,supportsAtomicMultiTableTransactions:!0,requiresOnlineDdl:!1,supportsCreateIndexIfNotExists:!1},singlestore:{dialect:"singlestore",wire:"mysql",queryBuilderDialect:"singlestore",defaultPort:3306,identifierQuote:"`",supportsForeignKeys:!1,supportsAutoIncrement:!0,supportsAtomicMultiTableTransactions:!1,requiresOnlineDdl:!1,supportsCreateIndexIfNotExists:!1},vitess:{dialect:"vitess",wire:"mysql",queryBuilderDialect:"vitess",defaultPort:15306,identifierQuote:"`",supportsForeignKeys:!1,supportsAutoIncrement:!1,supportsAtomicMultiTableTransactions:!1,requiresOnlineDdl:!0,supportsCreateIndexIfNotExists:!1},postgres:{dialect:"postgres",wire:"postgres",queryBuilderDialect:"postgres",defaultPort:5432,identifierQuote:'"',supportsForeignKeys:!0,supportsAutoIncrement:!0,supportsAtomicMultiTableTransactions:!0,requiresOnlineDdl:!1,supportsCreateIndexIfNotExists:!0}};export function isVitessSharded(explicit){if(explicit!==void 0)return explicit;const raw=process.env.DB_VITESS_SHARDED?.trim().toLowerCase();if(raw===void 0||raw==="")return!0;return!["0","false","no","off"].includes(raw)}export function dialectCapabilities(dialect,options={}){const caps=CAPABILITIES[dialect]??CAPABILITIES.sqlite;if(dialect!=="vitess"||isVitessSharded(options.vitessSharded))return caps;return{...CAPABILITIES.mysql,dialect:"vitess",queryBuilderDialect:"vitess",defaultPort:15306}}export function isKnownDialect(dialect){return dialect in CAPABILITIES}export function knownDialects(){return Object.keys(CAPABILITIES)}export function isMysqlWire(dialect){return dialectCapabilities(dialect).wire==="mysql"}export function isPostgresWire(dialect){return dialectCapabilities(dialect).wire==="postgres"}export function toQueryBuilderDialect(dialect){return dialectCapabilities(dialect).queryBuilderDialect}
|
package/dist/driver-config.d.ts
CHANGED
|
@@ -53,7 +53,8 @@ export declare function detectDriver(): StacksDialect;
|
|
|
53
53
|
* username: 'root',
|
|
54
54
|
* password: '',
|
|
55
55
|
* prefix: '',
|
|
56
|
-
* ssl: false
|
|
56
|
+
* ssl: false,
|
|
57
|
+
* sharded: true
|
|
57
58
|
* },
|
|
58
59
|
* postgres: {
|
|
59
60
|
* name: 'stacks',
|
|
@@ -132,6 +133,7 @@ export declare interface VitessConfig extends NetworkedConnectionConfig {
|
|
|
132
133
|
prefix?: string
|
|
133
134
|
ssl?: boolean
|
|
134
135
|
tabletType?: 'primary' | 'replica' | 'rdonly'
|
|
136
|
+
sharded?: boolean
|
|
135
137
|
}
|
|
136
138
|
/**
|
|
137
139
|
* PostgreSQL specific configuration
|
package/dist/driver-config.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{env}from"@stacksjs/env";export const driverDefaults={sqlite:{database:"database/stacks.sqlite",prefix:""},mysql:{name:"stacks",host:"127.0.0.1",port:3306,username:"root",password:"",prefix:"",charset:"utf8mb4",collation:"utf8mb4_unicode_ci"},singlestore:{name:"stacks",host:"127.0.0.1",port:3306,username:"root",password:"",prefix:"",charset:"utf8mb4",ssl:!1},vitess:{name:"stacks",host:"127.0.0.1",port:15306,username:"root",password:"",prefix:"",ssl:!1},postgres:{name:"stacks",host:"127.0.0.1",port:5432,username:"postgres",password:"",prefix:"",schema:"public"},browser:{}};export function getConnectionString(driver,config){switch(driver){case"sqlite":{const sqliteConfig=config;if(sqliteConfig.database===":memory:")return":memory:";return`sqlite://${sqliteConfig.database}`}case"mysql":{const mysqlConfig=config,{name,host="127.0.0.1",port=3306,username="root",password=""}=mysqlConfig;return`mysql://${username}:${password}@${host}:${port}/${name}`}case"singlestore":{const ssConfig=config,{name,host="127.0.0.1",port=3306,username="root",password=""}=ssConfig;return`mysql://${username}:${password}@${host}:${port}/${name}`}case"vitess":{const vtConfig=config,{name,host="127.0.0.1",port=15306,username="root",password="",tabletType}=vtConfig,keyspace=tabletType?`${name}@${tabletType}`:name;return`mysql://${username}:${password}@${host}:${port}/${keyspace}`}case"postgres":{const pgConfig=config,{name,host="127.0.0.1",port=5432,username="postgres",password=""}=pgConfig;return`postgres://${username}:${password}@${host}:${port}/${name}`}default:throw Error(`Unsupported driver: ${driver}`)}}export function validateDriverConfig(driver,config){const errors=[];switch(driver){case"sqlite":{if(!config.database)errors.push("SQLite requires a database path");break}case"mysql":{if(!config.name)errors.push("MySQL requires a database name");break}case"singlestore":{if(!config.name)errors.push("SingleStore requires a database name");break}case"vitess":{if(!config.name)errors.push("Vitess requires a keyspace name");break}case"postgres":{if(!config.name)errors.push("PostgreSQL requires a database name");break}default:errors.push(`Unsupported driver: ${driver}`)}return{valid:errors.length===0,errors}}export function mergeWithDefaults(driver,config){return{...driverDefaults[driver],...config}}export function getConfigFromEnv(driver){switch(driver){case"sqlite":return{database:env.DB_DATABASE||"database/stacks.sqlite",prefix:env.DB_PREFIX||""};case"mysql":return{name:env.DB_DATABASE||"stacks",host:env.DB_HOST||"127.0.0.1",port:env.DB_PORT??3306,username:env.DB_USERNAME||"root",password:env.DB_PASSWORD||"",prefix:env.DB_PREFIX||""};case"singlestore":return{name:env.DB_DATABASE||"stacks",host:env.DB_HOST||"127.0.0.1",port:env.DB_PORT??3306,username:env.DB_USERNAME||"root",password:env.DB_PASSWORD||"",prefix:env.DB_PREFIX||"",ssl:env.DB_SSL==="true"||env.DB_SSL==="1"};case"vitess":return{name:env.DB_DATABASE||"stacks",host:env.DB_HOST||"127.0.0.1",port:env.DB_PORT??15306,username:env.DB_USERNAME||"root",password:env.DB_PASSWORD||"",prefix:env.DB_PREFIX||"",ssl:env.DB_SSL==="true"||env.DB_SSL==="1"};case"postgres":return{name:env.DB_DATABASE||"stacks",host:env.DB_HOST||"127.0.0.1",port:env.DB_PORT??5432,username:env.DB_USERNAME||"postgres",password:env.DB_PASSWORD||"",prefix:env.DB_PREFIX||"",schema:env.DB_SCHEMA||"public"};default:throw Error(`Unsupported driver: ${driver}`)}}export function detectDriver(){if(env.DB_CONNECTION)return env.DB_CONNECTION;if(env.DATABASE_URL?.startsWith("postgres"))return"postgres";if(env.DATABASE_URL?.startsWith("mysql"))return"mysql";return"sqlite"}
|
|
1
|
+
import{env}from"@stacksjs/env";export const driverDefaults={sqlite:{database:"database/stacks.sqlite",prefix:""},mysql:{name:"stacks",host:"127.0.0.1",port:3306,username:"root",password:"",prefix:"",charset:"utf8mb4",collation:"utf8mb4_unicode_ci"},singlestore:{name:"stacks",host:"127.0.0.1",port:3306,username:"root",password:"",prefix:"",charset:"utf8mb4",ssl:!1},vitess:{name:"stacks",host:"127.0.0.1",port:15306,username:"root",password:"",prefix:"",ssl:!1,sharded:!0},postgres:{name:"stacks",host:"127.0.0.1",port:5432,username:"postgres",password:"",prefix:"",schema:"public"},browser:{}};export function getConnectionString(driver,config){switch(driver){case"sqlite":{const sqliteConfig=config;if(sqliteConfig.database===":memory:")return":memory:";return`sqlite://${sqliteConfig.database}`}case"mysql":{const mysqlConfig=config,{name,host="127.0.0.1",port=3306,username="root",password=""}=mysqlConfig;return`mysql://${username}:${password}@${host}:${port}/${name}`}case"singlestore":{const ssConfig=config,{name,host="127.0.0.1",port=3306,username="root",password=""}=ssConfig;return`mysql://${username}:${password}@${host}:${port}/${name}`}case"vitess":{const vtConfig=config,{name,host="127.0.0.1",port=15306,username="root",password="",tabletType}=vtConfig,keyspace=tabletType?`${name}@${tabletType}`:name;return`mysql://${username}:${password}@${host}:${port}/${keyspace}`}case"postgres":{const pgConfig=config,{name,host="127.0.0.1",port=5432,username="postgres",password=""}=pgConfig;return`postgres://${username}:${password}@${host}:${port}/${name}`}default:throw Error(`Unsupported driver: ${driver}`)}}export function validateDriverConfig(driver,config){const errors=[];switch(driver){case"sqlite":{if(!config.database)errors.push("SQLite requires a database path");break}case"mysql":{if(!config.name)errors.push("MySQL requires a database name");break}case"singlestore":{if(!config.name)errors.push("SingleStore requires a database name");break}case"vitess":{if(!config.name)errors.push("Vitess requires a keyspace name");break}case"postgres":{if(!config.name)errors.push("PostgreSQL requires a database name");break}default:errors.push(`Unsupported driver: ${driver}`)}return{valid:errors.length===0,errors}}export function mergeWithDefaults(driver,config){return{...driverDefaults[driver],...config}}export function getConfigFromEnv(driver){switch(driver){case"sqlite":return{database:env.DB_DATABASE||"database/stacks.sqlite",prefix:env.DB_PREFIX||""};case"mysql":return{name:env.DB_DATABASE||"stacks",host:env.DB_HOST||"127.0.0.1",port:env.DB_PORT??3306,username:env.DB_USERNAME||"root",password:env.DB_PASSWORD||"",prefix:env.DB_PREFIX||""};case"singlestore":return{name:env.DB_DATABASE||"stacks",host:env.DB_HOST||"127.0.0.1",port:env.DB_PORT??3306,username:env.DB_USERNAME||"root",password:env.DB_PASSWORD||"",prefix:env.DB_PREFIX||"",ssl:env.DB_SSL==="true"||env.DB_SSL==="1"};case"vitess":return{name:env.DB_DATABASE||"stacks",host:env.DB_HOST||"127.0.0.1",port:env.DB_PORT??15306,username:env.DB_USERNAME||"root",password:env.DB_PASSWORD||"",prefix:env.DB_PREFIX||"",ssl:env.DB_SSL==="true"||env.DB_SSL==="1",sharded:!["0","false","no","off"].includes(String(env.DB_VITESS_SHARDED??"true").toLowerCase())};case"postgres":return{name:env.DB_DATABASE||"stacks",host:env.DB_HOST||"127.0.0.1",port:env.DB_PORT??5432,username:env.DB_USERNAME||"postgres",password:env.DB_PASSWORD||"",prefix:env.DB_PREFIX||"",schema:env.DB_SCHEMA||"public"};default:throw Error(`Unsupported driver: ${driver}`)}}export function detectDriver(){if(env.DB_CONNECTION)return env.DB_CONNECTION;if(env.DATABASE_URL?.startsWith("postgres"))return"postgres";if(env.DATABASE_URL?.startsWith("mysql"))return"mysql";return"sqlite"}
|
package/dist/migration-lock.d.ts
CHANGED
|
@@ -20,4 +20,4 @@ export declare function acquireMigrationLock(dialect: Dialect, adminDb: { unsafe
|
|
|
20
20
|
export declare interface MigrationLockHandle {
|
|
21
21
|
release: () => Promise<void>
|
|
22
22
|
}
|
|
23
|
-
export type Dialect = 'sqlite' | 'mysql' | 'postgres';
|
|
23
|
+
export type Dialect = 'sqlite' | 'mysql' | 'vitess' | 'postgres';
|
package/dist/migration-lock.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{Buffer}from"node:buffer";import{createHash}from"node:crypto";import{closeSync,openSync,readFileSync,statSync,unlinkSync,writeFileSync}from"node:fs";import process from"node:process";import{userDatabasePath}from"@stacksjs/path";const DEFAULT_TIMEOUT_MS=30000,INITIAL_BACKOFF_MS=100,MAX_BACKOFF_MS=2000,STALE_LOCK_MS=60000,LOCK_NAME="stacks_migrations";export async function acquireMigrationLock(dialect,adminDb,opts={}){const timeoutMs=opts.timeoutMs??DEFAULT_TIMEOUT_MS;if(dialect!=="sqlite"&&dialect!=="postgres"&&dialect!=="mysql")throw Error(`[migration-lock] unknown dialect: ${String(dialect)}`);if(dialect==="sqlite")return acquireSqliteLock(opts.sqliteLockPath,timeoutMs);if(!adminDb)throw Error(`[migration-lock] ${dialect} requires a database connection to acquire the lock`);if(dialect==="postgres")return acquirePostgresLock(adminDb,timeoutMs);return acquireMySqlLock(adminDb,timeoutMs)}function lockKeysForPostgres(){const hash=createHash("sha256").update(LOCK_NAME).digest(),key1=hash.readInt32BE(0),key2=hash.readInt32BE(4);return{key1,key2}}async function acquirePostgresLock(adminDb,timeoutMs){const{key1,key2}=lockKeysForPostgres(),start=Date.now();let backoff=INITIAL_BACKOFF_MS;while(!0){const result=await adminDb.unsafe(`SELECT pg_try_advisory_lock(${key1}, ${key2}) AS acquired`);if(extractFirstBool(result,"acquired"))return{release:async()=>{try{await adminDb.unsafe(`SELECT pg_advisory_unlock(${key1}, ${key2})`)}catch{}}};if(Date.now()-start>=timeoutMs)throw Error("[migration-lock] another migration is in progress \u2014 could not acquire postgres advisory lock within timeout");await sleepWithJitter(backoff);backoff=Math.min(backoff*2,MAX_BACKOFF_MS)}}async function acquireMySqlLock(adminDb,timeoutMs){const start=Date.now();let backoff=INITIAL_BACKOFF_MS;while(!0){const result=await adminDb.unsafe(`SELECT GET_LOCK('${LOCK_NAME}', 0) AS acquired`);if(extractFirstInt(result,"acquired")===1)return{release:async()=>{try{await adminDb.unsafe(`SELECT RELEASE_LOCK('${LOCK_NAME}')`)}catch{}}};if(Date.now()-start>=timeoutMs)throw Error("[migration-lock] another migration is in progress \u2014 could not acquire MySQL named lock within timeout");await sleepWithJitter(backoff);backoff=Math.min(backoff*2,MAX_BACKOFF_MS)}}function defaultSqliteLockPath(){return userDatabasePath(".migration.lock")}async function acquireSqliteLock(lockPath,timeoutMs){const path=lockPath??defaultSqliteLockPath(),start=Date.now();let backoff=INITIAL_BACKOFF_MS;while(!0){if(tryCreateLockFile(path)){let released=!1;return{release:async()=>{if(released)return;released=!0;try{unlinkSync(path)}catch{}}}}reclaimIfStale(path);if(Date.now()-start>=timeoutMs)throw Error(`[migration-lock] another migration is in progress \u2014 lock file ${path} held within timeout`);await sleepWithJitter(backoff);backoff=Math.min(backoff*2,MAX_BACKOFF_MS)}}function tryCreateLockFile(path){try{const fd=openSync(path,"wx");try{const payload=JSON.stringify({pid:process.pid,startedAt:new Date().toISOString()});writeFileSync(fd,Buffer.from(payload,"utf8"))}finally{closeSync(fd)}return!0}catch(e){if(e.code==="EEXIST")return!1;throw e}}function reclaimIfStale(path){try{const st=statSync(path);if(Date.now()-st.mtimeMs>STALE_LOCK_MS)try{unlinkSync(path)}catch{}}catch{}}function sleepWithJitter(ms){const jittered=ms*(1+Math.random()*0.25);return new Promise((resolve)=>setTimeout(resolve,jittered))}function extractFirstBool(result,column){const row=pluckFirstRow(result);if(!row)return!1;const value=row[column];return value===!0||value===1||value==="1"||value==="t"}function extractFirstInt(result,column){const row=pluckFirstRow(result);if(!row)return null;const value=row[column];if(typeof value==="number")return value;if(typeof value==="string"&&/^-?\d+$/.test(value))return Number.parseInt(value,10);return null}function pluckFirstRow(result){if(!result)return null;if(Array.isArray(result))return result[0];if(typeof result==="object"&&"rows"in result&&Array.isArray(result.rows))return result.rows[0];return null}
|
|
1
|
+
import{Buffer}from"node:buffer";import{createHash}from"node:crypto";import{closeSync,openSync,readFileSync,statSync,unlinkSync,writeFileSync}from"node:fs";import process from"node:process";import{userDatabasePath}from"@stacksjs/path";const DEFAULT_TIMEOUT_MS=30000,INITIAL_BACKOFF_MS=100,MAX_BACKOFF_MS=2000,STALE_LOCK_MS=60000,LOCK_NAME="stacks_migrations";export async function acquireMigrationLock(dialect,adminDb,opts={}){const timeoutMs=opts.timeoutMs??DEFAULT_TIMEOUT_MS;if(dialect!=="sqlite"&&dialect!=="postgres"&&dialect!=="mysql"&&dialect!=="vitess")throw Error(`[migration-lock] unknown dialect: ${String(dialect)}`);if(dialect==="sqlite")return acquireSqliteLock(opts.sqliteLockPath,timeoutMs);if(!adminDb)throw Error(`[migration-lock] ${dialect} requires a database connection to acquire the lock`);if(dialect==="postgres")return acquirePostgresLock(adminDb,timeoutMs);return acquireMySqlLock(adminDb,timeoutMs)}function lockKeysForPostgres(){const hash=createHash("sha256").update(LOCK_NAME).digest(),key1=hash.readInt32BE(0),key2=hash.readInt32BE(4);return{key1,key2}}async function acquirePostgresLock(adminDb,timeoutMs){const{key1,key2}=lockKeysForPostgres(),start=Date.now();let backoff=INITIAL_BACKOFF_MS;while(!0){const result=await adminDb.unsafe(`SELECT pg_try_advisory_lock(${key1}, ${key2}) AS acquired`);if(extractFirstBool(result,"acquired"))return{release:async()=>{try{await adminDb.unsafe(`SELECT pg_advisory_unlock(${key1}, ${key2})`)}catch{}}};if(Date.now()-start>=timeoutMs)throw Error("[migration-lock] another migration is in progress \u2014 could not acquire postgres advisory lock within timeout");await sleepWithJitter(backoff);backoff=Math.min(backoff*2,MAX_BACKOFF_MS)}}async function acquireMySqlLock(adminDb,timeoutMs){const start=Date.now();let backoff=INITIAL_BACKOFF_MS;while(!0){const result=await adminDb.unsafe(`SELECT GET_LOCK('${LOCK_NAME}', 0) AS acquired`);if(extractFirstInt(result,"acquired")===1)return{release:async()=>{try{await adminDb.unsafe(`SELECT RELEASE_LOCK('${LOCK_NAME}')`)}catch{}}};if(Date.now()-start>=timeoutMs)throw Error("[migration-lock] another migration is in progress \u2014 could not acquire MySQL named lock within timeout");await sleepWithJitter(backoff);backoff=Math.min(backoff*2,MAX_BACKOFF_MS)}}function defaultSqliteLockPath(){return userDatabasePath(".migration.lock")}async function acquireSqliteLock(lockPath,timeoutMs){const path=lockPath??defaultSqliteLockPath(),start=Date.now();let backoff=INITIAL_BACKOFF_MS;while(!0){if(tryCreateLockFile(path)){let released=!1;return{release:async()=>{if(released)return;released=!0;try{unlinkSync(path)}catch{}}}}reclaimIfStale(path);if(Date.now()-start>=timeoutMs)throw Error(`[migration-lock] another migration is in progress \u2014 lock file ${path} held within timeout`);await sleepWithJitter(backoff);backoff=Math.min(backoff*2,MAX_BACKOFF_MS)}}function tryCreateLockFile(path){try{const fd=openSync(path,"wx");try{const payload=JSON.stringify({pid:process.pid,startedAt:new Date().toISOString()});writeFileSync(fd,Buffer.from(payload,"utf8"))}finally{closeSync(fd)}return!0}catch(e){if(e.code==="EEXIST")return!1;throw e}}function reclaimIfStale(path){try{const st=statSync(path);if(Date.now()-st.mtimeMs>STALE_LOCK_MS)try{unlinkSync(path)}catch{}}catch{}}function sleepWithJitter(ms){const jittered=ms*(1+Math.random()*0.25);return new Promise((resolve)=>setTimeout(resolve,jittered))}function extractFirstBool(result,column){const row=pluckFirstRow(result);if(!row)return!1;const value=row[column];return value===!0||value===1||value==="1"||value==="t"}function extractFirstInt(result,column){const row=pluckFirstRow(result);if(!row)return null;const value=row[column];if(typeof value==="number")return value;if(typeof value==="string"&&/^-?\d+$/.test(value))return Number.parseInt(value,10);return null}function pluckFirstRow(result){if(!result)return null;if(Array.isArray(result))return result[0];if(typeof result==="object"&&"rows"in result&&Array.isArray(result.rows))return result.rows[0];return null}
|
package/dist/migrations.d.ts
CHANGED
|
@@ -43,7 +43,8 @@ export declare function prepareMigrationModelsDir(): { modelsDir: string, skip:
|
|
|
43
43
|
*/
|
|
44
44
|
export declare function sqlStatementsOf(content: string): string[];
|
|
45
45
|
/**
|
|
46
|
-
* Make a Postgres `CREATE TYPE … AS ENUM` statement safe to run twice
|
|
46
|
+
* Make a Postgres `CREATE TYPE … AS ENUM` statement safe to run twice, and make
|
|
47
|
+
* it mean something when the type is already there.
|
|
47
48
|
*
|
|
48
49
|
* `CREATE TYPE` has no `IF NOT EXISTS`, so a corpus containing one can only be
|
|
49
50
|
* applied to a database that has never seen it. Every other way a migration run
|
|
@@ -54,6 +55,20 @@ export declare function sqlStatementsOf(content: string): string[];
|
|
|
54
55
|
*
|
|
55
56
|
* The guard is a `DO` block catching `duplicate_object`, which needs a
|
|
56
57
|
* dollar-quoted body - hence the splitter above having to understand one.
|
|
58
|
+
*
|
|
59
|
+
* The guard on its own is not enough, and quietly broke the case it sat next
|
|
60
|
+
* to. Adding a value to an enum in a model generates a `CREATE TYPE` naming the
|
|
61
|
+
* full new set; on a database that already has the type, the guard swallows it
|
|
62
|
+
* and the new value never arrives. The column keeps the old set, every insert
|
|
63
|
+
* using the new value fails, and the next diff proposes the same statement
|
|
64
|
+
* again - which is exactly the silent-no-op loop the guard was introduced to
|
|
65
|
+
* end, moved one step along.
|
|
66
|
+
*
|
|
67
|
+
* So each member is also asserted with `ALTER TYPE … ADD VALUE IF NOT EXISTS`,
|
|
68
|
+
* which is idempotent on its own and is a no-op on the fresh-database path
|
|
69
|
+
* where the `CREATE TYPE` just defined all of them. Values are only ever added:
|
|
70
|
+
* removing one would need a rewrite of every row that carries it, which is a
|
|
71
|
+
* data migration and not something a schema diff may decide to do by itself.
|
|
57
72
|
*/
|
|
58
73
|
/**
|
|
59
74
|
* Put a `DROP DEFAULT` in front of every `ALTER COLUMN … TYPE …`.
|
package/dist/migrations.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
var {require}=import.meta;import{existsSync,mkdirSync,readdirSync,readFileSync,renameSync,unlinkSync,writeFileSync}from"node:fs";import{dirname,isAbsolute,join}from"node:path";import{log as _log}from"@stacksjs/logging";const log={info:(...args)=>typeof _log?.info==="function"?_log.info(...args):console.log(...args),success:(msg)=>typeof _log?.success==="function"?_log.success(msg):console.log(msg),warn:(msg)=>typeof _log?.warn==="function"?_log.warn(msg):console.warn(msg),error:(...args)=>typeof _log?.error==="function"?_log.error(...args):console.error(...args),debug:(...args)=>typeof _log?.debug==="function"?_log.debug(...args):console.debug(...args)};import{err,handleError,ok}from"@stacksjs/error-handling";import{path}from"@stacksjs/path";import{defaultModelsPath}from"./seeder";import{createQueryBuilder,executeMigration as qbExecuteMigration,generateMigration as qbGenerateMigration,resetConnection,resetDatabase as qbResetDatabase,config as qbConfig,saveMigrationSnapshot,setConfig}from"@stacksjs/query-builder";import{db,QB_SNAPSHOT_DIR}from"./utils";import{classifyConnectionError,createDatabase,describeTarget,manualCreateHint,probeTargetDatabase,resolveConnectionTarget}from"./ensure-database";import{resolveModelSources}from"./model-sources";import{frameworkManagedColumns,withoutManagedColumnDrops,withoutManagedColumnDropSql}from"./managed-columns";import{acquireMigrationLock}from"./migration-lock";import{migrateNotificationTables}from"./notification-tables";import{env as envVars}from"@stacksjs/env";import{getConnectionDefaults}from"./defaults";const databaseEnv={DB_CONNECTION:process.env.DB_CONNECTION||envVars.DB_CONNECTION,DB_DATABASE_PATH:process.env.DB_DATABASE_PATH||envVars.DB_DATABASE_PATH,DB_DATABASE:process.env.DB_DATABASE||envVars.DB_DATABASE,DB_HOST:process.env.DB_HOST||envVars.DB_HOST,DB_PORT:process.env.DB_PORT?Number(process.env.DB_PORT):envVars.DB_PORT,DB_USERNAME:process.env.DB_USERNAME||envVars.DB_USERNAME,DB_PASSWORD:process.env.DB_PASSWORD||envVars.DB_PASSWORD},dbDriver=databaseEnv.DB_CONNECTION||"sqlite",sqliteDefaults=getConnectionDefaults("sqlite",databaseEnv),mysqlDefaults=getConnectionDefaults("mysql",databaseEnv),postgresDefaults=getConnectionDefaults("postgres",databaseEnv),dbConfig={default:dbDriver,connections:{sqlite:{database:sqliteDefaults.database,prefix:""},mysql:{name:mysqlDefaults.database,host:mysqlDefaults.host,username:mysqlDefaults.username,password:mysqlDefaults.password,port:mysqlDefaults.port,prefix:""},postgres:{name:postgresDefaults.database,host:postgresDefaults.host,username:postgresDefaults.username,password:postgresDefaults.password,port:postgresDefaults.port,prefix:""}}};function sqliteDatabasePath(){const configured=dbConfig.connections.sqlite.database||"stacks.db";return isAbsolute(configured)?configured:join(process.cwd(),configured)}function getDriver(){return dbConfig.default||"sqlite"}function getDialect(){const driver=getDriver();if(driver==="sqlite"||driver==="mysql"||driver==="postgres")return driver;if(driver==="singlestore")return"mysql";if(driver==="dynamodb")throw Error("[database] DB_CONNECTION=dynamodb is not compatible with the SQL migration runner. "+"DynamoDB has no schema-migration concept \u2014 use the entity-style `dynamo.entity(...)` "+"API from @stacksjs/database directly. To run SQL migrations, set DB_CONNECTION to one of: sqlite, mysql, postgres.");throw Error(`[database] Unknown DB_CONNECTION "${driver}". Allowed values: sqlite, mysql, postgres, dynamodb.`)}function getQbDialect(){return getDriver()==="singlestore"?"singlestore":getDialect()}function configureQueryBuilder(){const dialect=getDialect(),connectionConfig=dbConfig.connections[dialect];setConfig({dialect,verbose:!1,snapshotDir:QB_SNAPSHOT_DIR,database:{database:connectionConfig?.name||connectionConfig?.database||"stacks",host:connectionConfig?.host||"localhost",port:connectionConfig?.port||(dialect==="postgres"?5432:dialect==="mysql"?3306:0),username:connectionConfig?.username||"",password:connectionConfig?.password||""}});resetConnection()}export function prepareMigrationModelsDir(){const sources=resolveModelSources();return{modelsDir:sources?.dir??path.userModelsPath(),skip:!sources}}export function sqlStatementsOf(content){const statements=[];let current="",quote=null,dollarTag=null;for(let i=0;i<content.length;i++){const char=content[i];if(dollarTag){current+=char;if(char==="$"&&content.startsWith(dollarTag,i)){current+=content.slice(i+1,i+dollarTag.length);i+=dollarTag.length-1;dollarTag=null}continue}if(quote){current+=char;if(quote==="single"&&char==="'"||quote==="double"&&char==='"')quote=null;continue}if(char==="-"&&content[i+1]==="-"){const newline=content.indexOf(`
|
|
1
|
+
var {require}=import.meta;import{existsSync,mkdirSync,readdirSync,readFileSync,renameSync,unlinkSync,writeFileSync}from"node:fs";import{dirname,isAbsolute,join}from"node:path";import{log as _log}from"@stacksjs/logging";const log={info:(...args)=>typeof _log?.info==="function"?_log.info(...args):console.log(...args),success:(msg)=>typeof _log?.success==="function"?_log.success(msg):console.log(msg),warn:(msg)=>typeof _log?.warn==="function"?_log.warn(msg):console.warn(msg),error:(...args)=>typeof _log?.error==="function"?_log.error(...args):console.error(...args),debug:(...args)=>typeof _log?.debug==="function"?_log.debug(...args):console.debug(...args)};import{err,handleError,ok}from"@stacksjs/error-handling";import{path}from"@stacksjs/path";import{defaultModelsPath}from"./seeder";import{createQueryBuilder,executeMigration as qbExecuteMigration,generateMigration as qbGenerateMigration,resetConnection,resetDatabase as qbResetDatabase,config as qbConfig,saveMigrationSnapshot,setConfig}from"@stacksjs/query-builder";import{db,QB_SNAPSHOT_DIR}from"./utils";import{classifyConnectionError,createDatabase,describeTarget,manualCreateHint,probeTargetDatabase,resolveConnectionTarget}from"./ensure-database";import{resolveModelSources}from"./model-sources";import{frameworkManagedColumns,withoutManagedColumnDrops,withoutManagedColumnDropSql}from"./managed-columns";import{acquireMigrationLock}from"./migration-lock";import{migrateNotificationTables}from"./notification-tables";import{env as envVars}from"@stacksjs/env";import{getConnectionDefaults}from"./defaults";import{isVitessSharded}from"./dialect";const databaseEnv={DB_CONNECTION:process.env.DB_CONNECTION||envVars.DB_CONNECTION,DB_DATABASE_PATH:process.env.DB_DATABASE_PATH||envVars.DB_DATABASE_PATH,DB_DATABASE:process.env.DB_DATABASE||envVars.DB_DATABASE,DB_HOST:process.env.DB_HOST||envVars.DB_HOST,DB_PORT:process.env.DB_PORT?Number(process.env.DB_PORT):envVars.DB_PORT,DB_USERNAME:process.env.DB_USERNAME||envVars.DB_USERNAME,DB_PASSWORD:process.env.DB_PASSWORD||envVars.DB_PASSWORD,DB_VITESS_SHARDED:process.env.DB_VITESS_SHARDED||envVars.DB_VITESS_SHARDED},dbDriver=databaseEnv.DB_CONNECTION||"sqlite",sqliteDefaults=getConnectionDefaults("sqlite",databaseEnv),mysqlDefaults=getConnectionDefaults("mysql",databaseEnv),singlestoreDefaults=getConnectionDefaults("singlestore",databaseEnv),vitessDefaults=getConnectionDefaults("vitess",databaseEnv),postgresDefaults=getConnectionDefaults("postgres",databaseEnv),dbConfig={default:dbDriver,connections:{sqlite:{database:sqliteDefaults.database,prefix:""},mysql:{name:mysqlDefaults.database,host:mysqlDefaults.host,username:mysqlDefaults.username,password:mysqlDefaults.password,port:mysqlDefaults.port,prefix:""},singlestore:{name:singlestoreDefaults.database,host:singlestoreDefaults.host,username:singlestoreDefaults.username,password:singlestoreDefaults.password,port:singlestoreDefaults.port,prefix:""},vitess:{name:vitessDefaults.database,host:vitessDefaults.host,username:vitessDefaults.username,password:vitessDefaults.password,port:vitessDefaults.port,prefix:"",sharded:isVitessSharded()},postgres:{name:postgresDefaults.database,host:postgresDefaults.host,username:postgresDefaults.username,password:postgresDefaults.password,port:postgresDefaults.port,prefix:""}}};function sqliteDatabasePath(){const configured=dbConfig.connections.sqlite.database||"stacks.db";return isAbsolute(configured)?configured:join(process.cwd(),configured)}function getDriver(){return dbConfig.default||"sqlite"}function getDialect(){const driver=getDriver();if(driver==="sqlite"||driver==="mysql"||driver==="vitess"||driver==="postgres")return driver;if(driver==="singlestore")return"mysql";if(driver==="dynamodb")throw Error("[database] DB_CONNECTION=dynamodb is not compatible with the SQL migration runner. "+"DynamoDB has no schema-migration concept \u2014 use the entity-style `dynamo.entity(...)` "+"API from @stacksjs/database directly. To run SQL migrations, set DB_CONNECTION to one of: sqlite, mysql, singlestore, vitess, postgres.");throw Error(`[database] Unknown DB_CONNECTION "${driver}". Allowed values: sqlite, mysql, singlestore, vitess, postgres, dynamodb.`)}function getQbDialect(){return getDriver()==="singlestore"?"singlestore":getDialect()}function configureQueryBuilder(){const dialect=getDialect(),connectionConfig=dbConfig.connections[dialect];setConfig({dialect:getQbDialect(),vitess:{sharded:isVitessSharded(connectionConfig?.sharded)},verbose:!1,snapshotDir:QB_SNAPSHOT_DIR,database:{database:connectionConfig?.name||connectionConfig?.database||"stacks",host:connectionConfig?.host||"localhost",port:connectionConfig?.port||(dialect==="postgres"?5432:dialect==="mysql"?3306:0),username:connectionConfig?.username||"",password:connectionConfig?.password||""}});resetConnection()}export function prepareMigrationModelsDir(){const sources=resolveModelSources();return{modelsDir:sources?.dir??path.userModelsPath(),skip:!sources}}export function sqlStatementsOf(content){const statements=[];let current="",quote=null,dollarTag=null;for(let i=0;i<content.length;i++){const char=content[i];if(dollarTag){current+=char;if(char==="$"&&content.startsWith(dollarTag,i)){current+=content.slice(i+1,i+dollarTag.length);i+=dollarTag.length-1;dollarTag=null}continue}if(quote){current+=char;if(quote==="single"&&char==="'"||quote==="double"&&char==='"')quote=null;continue}if(char==="-"&&content[i+1]==="-"){const newline=content.indexOf(`
|
|
2
2
|
`,i);if(newline===-1)break;i=newline-1;continue}const dollar=char==="$"?/^\$[A-Za-z_]*\$/.exec(content.slice(i)):null;if(dollar){dollarTag=dollar[0];current+=dollarTag;i+=dollarTag.length-1;continue}if(char==="'"){quote="single";current+=char;continue}if(char==='"'){quote="double";current+=char;continue}if(char===";"){const trimmed=current.trim();if(trimmed.length>0)statements.push(trimmed);current="";continue}current+=char}const trailing=current.trim();if(trailing.length>0)statements.push(trailing);return statements}export function orderPostgresColumnTypeChanges(sql){const lines=sql.split(`
|
|
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
|
-
`)}export function guardPostgresEnumTypes(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$`})}
|
|
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
|
+
${missing.join(`
|
|
6
|
+
`)}`})}function enumMembers(list){return[...list.matchAll(/'(?:[^']|'')*'/g)].map((match)=>match[0])}export function preprocessSqliteMigrations(){const migrationsDir=join(process.cwd(),"database","migrations");let files;try{files=readdirSync(migrationsDir).filter((f)=>f.endsWith(".sql"))}catch{return}const droppedMigrations=[],skipMigration=(file,reason)=>{log.info(`Skipping migration on SQLite (${reason}): ${file}`);droppedMigrations.push(file)},deleteMigration=(file,filePath,reason)=>{log.info(`Dropping no-op migration (${reason}): ${file}`);try{unlinkSync(filePath)}catch{}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){deleteMigration(file,filePath,`duplicate create-table for "${tableName}" (kept ${earliest})`);continue}}if(statements.every((s)=>addConstraintPattern.test(s))){skipMigration(file,"SQLite does not support ALTER TABLE ADD CONSTRAINT");continue}if(statements.every((s)=>createTypePattern.test(s))){skipMigration(file,"SQLite does not support CREATE TYPE (enum types)");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}" \u2014 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}" \u2014 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}" \u2014 column does not exist: ${file}`);modified=!0;continue}}catch{if(earlierCreateDefinesColumn(file,tableName,columnName)){filteredStatements.push(stmt);continue}log.info(`Skipping DROP COLUMN "${columnName}" \u2014 table "${tableName}" not found: ${file}`);modified=!0;continue}}filteredStatements.push(stmt)}if(modified){if(filteredStatements.length===0)deleteMigration(file,filePath,"columns already absent from table");else writeFileSync(filePath,`${filteredStatements.join(`;
|
|
5
7
|
`)};
|
|
6
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 (
|
|
7
9
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -23,7 +25,7 @@ Create it yourself with: ${manualCreateHint(target)}`);if(result.created)log.su
|
|
|
23
25
|
JOIN pg_class c ON c.oid = a.attrelid
|
|
24
26
|
WHERE a.atttypid = t.oid AND c.relkind = 'r' AND NOT a.attisdropped
|
|
25
27
|
)
|
|
26
|
-
`).execute(),names=(Array.isArray(rows)?rows:rows?.rows??[]).map((row)=>String(row.name??row.typname??"")).filter(Boolean);for(const name of names)try{await db.unsafe(`DROP TYPE IF EXISTS "${name}" CASCADE`).execute();log.debug(`Dropped orphaned enum type: ${name}`)}catch(error){log.warn(`Could not drop enum type ${name}: ${error instanceof Error?error.message:String(error)}`)}}catch(error){log.warn(`Could not list enum types to drop: ${error instanceof Error?error.message:String(error)}`)}}async function dropFrameworkTables(dialect){if(dialect==="mysql")try{await db.unsafe("SET FOREIGN_KEY_CHECKS = 0").execute()}catch(error){log.warn(`Could not disable foreign key checks: ${error instanceof Error?error.message:String(error)}`)}if(dialect==="sqlite")try{await db.unsafe("PRAGMA foreign_keys = OFF").execute()}catch(error){log.warn(`Could not disable foreign key checks: ${error instanceof Error?error.message:String(error)}`)}for(const tableName of FRAMEWORK_TABLES)try{let dropSql;if(dialect==="postgres")dropSql=`DROP TABLE IF EXISTS "${tableName}" CASCADE`;else if(dialect==="mysql")dropSql=`DROP TABLE IF EXISTS \`${tableName}\``;else dropSql=`DROP TABLE IF EXISTS "${tableName}"`;log.info(`Dropping framework table: ${tableName}`);await db.unsafe(dropSql).execute();log.info(`Dropped framework table: ${tableName}`)}catch(error){const kind=classifyConnectionError(error);if(kind==="missing-database"||kind==="missing-role"||kind==="auth-failed"||kind==="server-unreachable"||kind==="timeout")throw error;log.warn(`Could not drop table ${tableName}: ${error instanceof Error?error.message:String(error)}`)}if(dialect==="mysql")try{await db.unsafe("SET FOREIGN_KEY_CHECKS = 1").execute()}catch(error){log.warn(`Could not re-enable foreign key checks: ${error instanceof Error?error.message:String(error)}`)}if(dialect==="sqlite")try{await db.unsafe("PRAGMA foreign_keys = ON").execute()}catch(error){log.warn(`Could not re-enable foreign key checks: ${error instanceof Error?error.message:String(error)}`)}}function resolveGenerateOptions(options){const applyRenames=options.applyRenames??(process.env.STACKS_MIGRATE_NO_RENAME==="1"?!1:void 0),fromDb=options.fromDb??(process.env.STACKS_MIGRATE_FROM_DB==="1"?!0:void 0);return{applyRenames,fromDb}}export async function previewPendingMigrations(options={}){try{configureQueryBuilder();const dialect=getDialect(),{modelsDir,skip}=prepareMigrationModelsDir();if(skip)return[];const{applyRenames,fromDb}=resolveGenerateOptions(options),operations=(await qbGenerateMigration(modelsDir,{dialect:getQbDialect(),dryRun:!0,applyRenames,fromDb})).operations??[];if(!operations.some((op)=>op.kind==="drop_column"))return operations;return withoutManagedColumnDrops(operations,await frameworkManagedColumns())}catch(error){log.debug(`[migration] preview failed: ${error instanceof Error?error.message:String(error)}`);return[]}}function snapshotDirLabel(){return qbConfig?.snapshotDir||QB_SNAPSHOT_DIR}function resolveSnapshotDir(){return join(process.cwd(),snapshotDirLabel())}function snapshotPathFor(dialect){return join(resolveSnapshotDir(),`model-snapshot.${dialect}.json`)}function readStoredMigrationPlan(dialect){const snapshotPath=snapshotPathFor(dialect);if(!existsSync(snapshotPath))return;let parsed;try{parsed=JSON.parse(readFileSync(snapshotPath,"utf8"))}catch(error){throw Error(`The migration snapshot is not valid JSON: ${snapshotPath}. Repair or regenerate it before creating migrations.`,{cause:error})}const candidate=parsed&&typeof parsed==="object"&&"plan"in parsed?parsed.plan:parsed;if(!candidate||typeof candidate!=="object"||!Array.isArray(candidate.tables))throw TypeError(`The migration snapshot has an invalid structure: ${snapshotPath}. Repair or regenerate it before creating migrations.`);return candidate}export function preserveMigrationPlanTableOrder(next,previous){const orderedByPrevious=(values,previousValues,keyFor)=>{if(!previousValues)return[...values];const previousOrder=new Map(previousValues.map((value,index)=>[keyFor(value),index])),nextOrder=new Map(values.map((value,index)=>[keyFor(value),index])),previousCount=previousValues.length;return[...values].sort((left,right)=>{const leftKey=keyFor(left),rightKey=keyFor(right),leftOrder=previousOrder.get(leftKey)??previousCount+nextOrder.get(leftKey),rightOrder=previousOrder.get(rightKey)??previousCount+nextOrder.get(rightKey);return leftOrder-rightOrder})},previousTables=new Map(previous?.tables.map((table)=>[table.table,table])??[]),tables=next.tables.map((table)=>{const previousTable=previousTables.get(table.table),previousColumns=new Map(previousTable?.columns.map((column)=>[column.name,column])??[]);return{...table,columns:orderedByPrevious(table.columns.map((column)=>{if(next.dialect!=="sqlite"||!column.enumTypeName||previousColumns.get(column.name)?.enumTypeName)return column;const portableColumn={...column};delete portableColumn.enumTypeName;return portableColumn}),previousTable?.columns,(column)=>column.name),indexes:orderedByPrevious(table.indexes,previousTable?.indexes,(index)=>index.name)}});return{...next,tables:orderedByPrevious(tables,previous?.tables,(table)=>table.table)}}function detectSnapshotDialectMismatch(dialect){const qbDir=resolveSnapshotDir();let files;try{files=readdirSync(qbDir)}catch{return null}const snapshotFor=(d)=>`model-snapshot.${d}.json`;if(files.includes(snapshotFor(dialect)))return null;for(const f of files){const m=/^model-snapshot\.(\w+)\.json$/.exec(f);if(m?.[1]&&m[1]!==dialect)return m[1]}return null}export async function generateMigrations(options={}){try{log.debug("Generating migrations...");configureQueryBuilder();const dialect=getDialect(),mismatch=detectSnapshotDialectMismatch(dialect);if(mismatch){const snapshotDir=snapshotDirLabel();return err(Error(`Refusing to generate migrations: resolved dialect "${dialect}" has no snapshot in ${snapshotDir}/, but "${mismatch}" does. DB_CONNECTION is likely unset or wrong `+"(missing .env?) \u2014 generating now would write a full duplicate migration set in the "+`wrong dialect. Set DB_CONNECTION=${mismatch} (or your intended dialect) and retry. To intentionally start a new dialect from scratch, remove ${snapshotDir}/model-snapshot.${mismatch}.json first.`))}const storedPlan=readStoredMigrationPlan(getQbDialect()),{modelsDir,skip}=prepareMigrationModelsDir();if(skip){log.debug("No app/Models directory found; using committed framework migrations");return ok("Migrations generated")}const{applyRenames,fromDb}=resolveGenerateOptions(options);log.debug(`[migration] Generating migrations for dialect: ${dialect}, models: ${modelsDir}`);const result=await qbGenerateMigration(modelsDir,{dialect:getQbDialect(),dryRun:!0,applyRenames,fromDb});let sqlStatements=result.sqlStatements??[];if(dialect==="sqlite")sqlStatements=inlineSqliteAddedColumnReferences(sqlStatements,result.plan);if(result.hasChanges&&sqlStatements.length>0){const filtered=withoutManagedColumnDropSql(sqlStatements,await frameworkManagedColumns(),result.operations??[]);if(filtered.removed.length>0)log.debug(`[migration] Skipped ${filtered.removed.length} generated drop(s) of framework-managed column(s) (stacksjs/stacks#2075)`);sqlStatements=filtered.statements}if(result.hasChanges&&sqlStatements.length>0){const dangling=findDanglingTypeReferences(sqlStatements);if(dangling.length>0){const created=createMissingEnumTypes(dangling,result.plan);if(created.statements.length>0){sqlStatements=[...created.statements,...sqlStatements];log.debug(`[migration] Created ${created.statements.length} enum type(s) an ALTER needed: ${created.defined.join(", ")}`)}const unresolved=dangling.filter((name)=>!created.defined.includes(name));if(unresolved.length>0){const before=sqlStatements.length;sqlStatements=sqlStatements.filter((statement)=>!referencesUndefinedType(statement,unresolved));log.warn(`[migration] Skipped ${before-sqlStatements.length} generated statement(s) referencing enum type(s) nothing creates and no model defines values for (${unresolved.slice(0,3).join(", ")}${unresolved.length>3?", \u2026":""}).`)}}}if(result.hasChanges){const written=persistGeneratedMigrations(sqlStatements);if(written>0)log.success(`Generated ${written} migration file${written===1?"":"s"}`);else log.debug("Migration generation produced no new files (already up to date)")}else log.debug("No changes detected");const stablePlan=preserveMigrationPlanTableOrder(result.plan,storedPlan);if(!storedPlan||JSON.stringify(stablePlan)!==JSON.stringify(storedPlan))saveMigrationSnapshot(stablePlan,{dialect:getQbDialect()});else log.debug("Model snapshot unchanged");return ok("Migrations generated")}catch(error){return err(handleError("Migration generation failed",error))}}export function referencesUndefinedType(statement,dangling){return dangling.some((name)=>statement.includes(`"${name}"`))}export function findDanglingTypeReferences(statements){const defined=new Set,referenced=new Set;for(const statement of statements){for(const match of statement.matchAll(/CREATE\s+TYPE\s+"([^"]+)"/gi))defined.add(match[1]);for(const match of statement.matchAll(/\bTYPE\s+"([^"]+)"/gi))referenced.add(match[1])}return[...referenced].filter((name)=>!defined.has(name)).sort()}export function createMissingEnumTypes(dangling,plan){if(dangling.length===0)return{statements:[],defined:[]};const values=new Map;for(const table of plan?.tables??[]){if(!table.table)continue;for(const column of table.columns??[]){if(!column.name||!column.enumValues?.length)continue;values.set(`${table.table}_${column.name}_type`,column.enumValues)}}const statements=[],defined=[];for(const name of dangling){const members=values.get(name);if(!members?.length)continue;const literals=members.map((member)=>`'${String(member).replaceAll("'","''")}'`).join(", ");statements.push(`${guardPostgresEnumTypes(`CREATE TYPE "${name}" AS ENUM (${literals})`)};`);defined.push(name)}return{statements,defined}}export function inlineSqliteAddedColumnReferences(statements,plan){const references=new Map;for(const table of plan?.tables??[]){if(!table.table)continue;for(const column of table.columns??[]){const reference=column.references;if(!column.name||!reference?.table||!reference.column)continue;references.set(`${table.table}.${column.name}`,{table:reference.table,column:reference.column})}}if(references.size===0)return statements;return statements.map((statement)=>{if(/\bREFERENCES\b/i.test(statement))return statement;const match=statement.match(/^(\s*ALTER\s+TABLE\s+["`]?(\w+)["`]?\s+ADD\s+COLUMN\s+["`]?(\w+)["`]?\s+)([\s\S]*?)(;?\s*)$/i),reference=match?.[2]&&match[3]?references.get(`${match[2]}.${match[3]}`):void 0;if(!match||!reference)return statement;return`${match[1]}${match[4].trimEnd()} REFERENCES "${reference.table}"("${reference.column}")${match[5]}`})}export async function regenerateMigrationCorpus(options={}){try{configureQueryBuilder();const dialect=options.dialect??getQbDialect(),dir=options.dir??join(process.cwd(),"database","migrations"),sources=resolveModelSources();if(!sources)return err(Error("No models found. Define models in app/Models, or ensure the framework defaults at storage/framework/defaults/app/Models are present."));try{writeFileSync(join(sources.dir,`.qb-migrations.${dialect}.json`),JSON.stringify({plan:{dialect,tables:[]}}))}catch{}const snapshotPath=join(resolveSnapshotDir(),`model-snapshot.${dialect}.json`),parkedSnapshot=`${snapshotPath}.regenerating`;let snapshotParked=!1;if(existsSync(snapshotPath))try{renameSync(snapshotPath,parkedSnapshot);snapshotParked=!0}catch{}let result;try{result=await qbGenerateMigration(sources.dir,{dialect,dryRun:!0})}finally{if(snapshotParked)try{renameSync(parkedSnapshot,snapshotPath)}catch{}}const statements=result.sqlStatements??[];if(statements.length===0)return err(Error(`The generator produced no SQL for dialect "${dialect}".`));const dangling=findDanglingTypeReferences(statements);if(dangling.length>0)return err(Error(`The generator emitted ${dangling.length} reference(s) to enum type(s) it never creates: ${dangling.slice(0,5).join(", ")}${dangling.length>5?`, +${dangling.length-5} more`:""}. Writing this corpus would fail partway through a migration. This is a bug in the migration generator, not in your models.`));const groups=groupGeneratedStatements(statements);let existing=[];try{existing=readdirSync(dir).filter((f)=>f.endsWith(".sql")).sort()}catch{}const files=groups.map((group,index)=>({name:`${String(index+1).padStart(10,"0")}-${group.label}.sql`,statements:group.statements.length}));if(options.dryRun)return ok({dialect,models:sources.models.length,files,removed:existing,dir});mkdirSync(dir,{recursive:!0});for(const file of existing)unlinkSync(join(dir,file));groups.forEach((group,index)=>{const body=`${group.statements.map((s)=>s.trim().replace(/;\s*$/,"")).join(`;
|
|
28
|
+
`).execute(),names=(Array.isArray(rows)?rows:rows?.rows??[]).map((row)=>String(row.name??row.typname??"")).filter(Boolean);for(const name of names)try{await db.unsafe(`DROP TYPE IF EXISTS "${name}" CASCADE`).execute();log.debug(`Dropped orphaned enum type: ${name}`)}catch(error){log.warn(`Could not drop enum type ${name}: ${error instanceof Error?error.message:String(error)}`)}}catch(error){log.warn(`Could not list enum types to drop: ${error instanceof Error?error.message:String(error)}`)}}async function dropFrameworkTables(dialect){if(dialect==="mysql"||dialect==="vitess")try{await db.unsafe("SET FOREIGN_KEY_CHECKS = 0").execute()}catch(error){log.warn(`Could not disable foreign key checks: ${error instanceof Error?error.message:String(error)}`)}if(dialect==="sqlite")try{await db.unsafe("PRAGMA foreign_keys = OFF").execute()}catch(error){log.warn(`Could not disable foreign key checks: ${error instanceof Error?error.message:String(error)}`)}for(const tableName of FRAMEWORK_TABLES)try{let dropSql;if(dialect==="postgres")dropSql=`DROP TABLE IF EXISTS "${tableName}" CASCADE`;else if(dialect==="mysql"||dialect==="vitess")dropSql=`DROP TABLE IF EXISTS \`${tableName}\``;else dropSql=`DROP TABLE IF EXISTS "${tableName}"`;log.info(`Dropping framework table: ${tableName}`);await db.unsafe(dropSql).execute();log.info(`Dropped framework table: ${tableName}`)}catch(error){const kind=classifyConnectionError(error);if(kind==="missing-database"||kind==="missing-role"||kind==="auth-failed"||kind==="server-unreachable"||kind==="timeout")throw error;log.warn(`Could not drop table ${tableName}: ${error instanceof Error?error.message:String(error)}`)}if(dialect==="mysql"||dialect==="vitess")try{await db.unsafe("SET FOREIGN_KEY_CHECKS = 1").execute()}catch(error){log.warn(`Could not re-enable foreign key checks: ${error instanceof Error?error.message:String(error)}`)}if(dialect==="sqlite")try{await db.unsafe("PRAGMA foreign_keys = ON").execute()}catch(error){log.warn(`Could not re-enable foreign key checks: ${error instanceof Error?error.message:String(error)}`)}}function resolveGenerateOptions(options){const applyRenames=options.applyRenames??(process.env.STACKS_MIGRATE_NO_RENAME==="1"?!1:void 0),fromDb=options.fromDb??(process.env.STACKS_MIGRATE_FROM_DB==="1"?!0:void 0);return{applyRenames,fromDb}}export async function previewPendingMigrations(options={}){try{configureQueryBuilder();const dialect=getDialect(),{modelsDir,skip}=prepareMigrationModelsDir();if(skip)return[];const{applyRenames,fromDb}=resolveGenerateOptions(options),operations=(await qbGenerateMigration(modelsDir,{dialect:getQbDialect(),dryRun:!0,applyRenames,fromDb})).operations??[];if(!operations.some((op)=>op.kind==="drop_column"))return operations;return withoutManagedColumnDrops(operations,await frameworkManagedColumns())}catch(error){log.debug(`[migration] preview failed: ${error instanceof Error?error.message:String(error)}`);return[]}}function snapshotDirLabel(){return qbConfig?.snapshotDir||QB_SNAPSHOT_DIR}function resolveSnapshotDir(){return join(process.cwd(),snapshotDirLabel())}function snapshotPathFor(dialect){return join(resolveSnapshotDir(),`model-snapshot.${dialect}.json`)}function readStoredMigrationPlan(dialect){const snapshotPath=snapshotPathFor(dialect);if(!existsSync(snapshotPath))return;let parsed;try{parsed=JSON.parse(readFileSync(snapshotPath,"utf8"))}catch(error){throw Error(`The migration snapshot is not valid JSON: ${snapshotPath}. Repair or regenerate it before creating migrations.`,{cause:error})}const candidate=parsed&&typeof parsed==="object"&&"plan"in parsed?parsed.plan:parsed;if(!candidate||typeof candidate!=="object"||!Array.isArray(candidate.tables))throw TypeError(`The migration snapshot has an invalid structure: ${snapshotPath}. Repair or regenerate it before creating migrations.`);return candidate}export function preserveMigrationPlanTableOrder(next,previous){const orderedByPrevious=(values,previousValues,keyFor)=>{if(!previousValues)return[...values];const previousOrder=new Map(previousValues.map((value,index)=>[keyFor(value),index])),nextOrder=new Map(values.map((value,index)=>[keyFor(value),index])),previousCount=previousValues.length;return[...values].sort((left,right)=>{const leftKey=keyFor(left),rightKey=keyFor(right),leftOrder=previousOrder.get(leftKey)??previousCount+nextOrder.get(leftKey),rightOrder=previousOrder.get(rightKey)??previousCount+nextOrder.get(rightKey);return leftOrder-rightOrder})},previousTables=new Map(previous?.tables.map((table)=>[table.table,table])??[]),tables=next.tables.map((table)=>{const previousTable=previousTables.get(table.table),previousColumns=new Map(previousTable?.columns.map((column)=>[column.name,column])??[]);return{...table,columns:orderedByPrevious(table.columns.map((column)=>{if(next.dialect!=="sqlite"||!column.enumTypeName||previousColumns.get(column.name)?.enumTypeName)return column;const portableColumn={...column};delete portableColumn.enumTypeName;return portableColumn}),previousTable?.columns,(column)=>column.name),indexes:orderedByPrevious(table.indexes,previousTable?.indexes,(index)=>index.name)}});return{...next,tables:orderedByPrevious(tables,previous?.tables,(table)=>table.table)}}function detectSnapshotDialectMismatch(dialect){const qbDir=resolveSnapshotDir();let files;try{files=readdirSync(qbDir)}catch{return null}const snapshotFor=(d)=>`model-snapshot.${d}.json`;if(files.includes(snapshotFor(dialect)))return null;for(const f of files){const m=/^model-snapshot\.(\w+)\.json$/.exec(f);if(m?.[1]&&m[1]!==dialect)return m[1]}return null}export async function generateMigrations(options={}){try{log.debug("Generating migrations...");configureQueryBuilder();const dialect=getDialect(),mismatch=detectSnapshotDialectMismatch(dialect);if(mismatch){const snapshotDir=snapshotDirLabel();return err(Error(`Refusing to generate migrations: resolved dialect "${dialect}" has no snapshot in ${snapshotDir}/, but "${mismatch}" does. DB_CONNECTION is likely unset or wrong `+"(missing .env?) \u2014 generating now would write a full duplicate migration set in the "+`wrong dialect. Set DB_CONNECTION=${mismatch} (or your intended dialect) and retry. To intentionally start a new dialect from scratch, remove ${snapshotDir}/model-snapshot.${mismatch}.json first.`))}const storedPlan=readStoredMigrationPlan(getQbDialect()),{modelsDir,skip}=prepareMigrationModelsDir();if(skip){log.debug("No app/Models directory found; using committed framework migrations");return ok("Migrations generated")}const{applyRenames,fromDb}=resolveGenerateOptions(options);log.debug(`[migration] Generating migrations for dialect: ${dialect}, models: ${modelsDir}`);const result=await qbGenerateMigration(modelsDir,{dialect:getQbDialect(),dryRun:!0,applyRenames,fromDb});let sqlStatements=result.sqlStatements??[];if(dialect==="sqlite")sqlStatements=inlineSqliteAddedColumnReferences(sqlStatements,result.plan);if(result.hasChanges&&sqlStatements.length>0){const filtered=withoutManagedColumnDropSql(sqlStatements,await frameworkManagedColumns(),result.operations??[]);if(filtered.removed.length>0)log.debug(`[migration] Skipped ${filtered.removed.length} generated drop(s) of framework-managed column(s) (stacksjs/stacks#2075)`);sqlStatements=filtered.statements}if(result.hasChanges&&sqlStatements.length>0){const dangling=findDanglingTypeReferences(sqlStatements);if(dangling.length>0){const created=createMissingEnumTypes(dangling,result.plan);if(created.statements.length>0){sqlStatements=[...created.statements,...sqlStatements];log.debug(`[migration] Created ${created.statements.length} enum type(s) an ALTER needed: ${created.defined.join(", ")}`)}const unresolved=dangling.filter((name)=>!created.defined.includes(name));if(unresolved.length>0){const before=sqlStatements.length;sqlStatements=sqlStatements.filter((statement)=>!referencesUndefinedType(statement,unresolved));log.warn(`[migration] Skipped ${before-sqlStatements.length} generated statement(s) referencing enum type(s) nothing creates and no model defines values for (${unresolved.slice(0,3).join(", ")}${unresolved.length>3?", \u2026":""}).`)}}}if(result.hasChanges){const written=persistGeneratedMigrations(sqlStatements);if(written>0)log.success(`Generated ${written} migration file${written===1?"":"s"}`);else log.debug("Migration generation produced no new files (already up to date)")}else log.debug("No changes detected");const stablePlan=preserveMigrationPlanTableOrder(result.plan,storedPlan);if(!storedPlan||JSON.stringify(stablePlan)!==JSON.stringify(storedPlan))saveMigrationSnapshot(stablePlan,{dialect:getQbDialect()});else log.debug("Model snapshot unchanged");return ok("Migrations generated")}catch(error){return err(handleError("Migration generation failed",error))}}export function referencesUndefinedType(statement,dangling){return dangling.some((name)=>statement.includes(`"${name}"`))}export function findDanglingTypeReferences(statements){const defined=new Set,referenced=new Set;for(const statement of statements){for(const match of statement.matchAll(/CREATE\s+TYPE\s+"([^"]+)"/gi))defined.add(match[1]);for(const match of statement.matchAll(/\bTYPE\s+"([^"]+)"/gi))referenced.add(match[1])}return[...referenced].filter((name)=>!defined.has(name)).sort()}export function createMissingEnumTypes(dangling,plan){if(dangling.length===0)return{statements:[],defined:[]};const values=new Map;for(const table of plan?.tables??[]){if(!table.table)continue;for(const column of table.columns??[]){if(!column.name||!column.enumValues?.length)continue;values.set(`${table.table}_${column.name}_type`,column.enumValues)}}const statements=[],defined=[];for(const name of dangling){const members=values.get(name);if(!members?.length)continue;const literals=members.map((member)=>`'${String(member).replaceAll("'","''")}'`).join(", "),guarded=guardPostgresEnumTypes(`CREATE TYPE "${name}" AS ENUM (${literals})`);statements.push(guarded.endsWith(";")?guarded:`${guarded};`);defined.push(name)}return{statements,defined}}export function inlineSqliteAddedColumnReferences(statements,plan){const references=new Map;for(const table of plan?.tables??[]){if(!table.table)continue;for(const column of table.columns??[]){const reference=column.references;if(!column.name||!reference?.table||!reference.column)continue;references.set(`${table.table}.${column.name}`,{table:reference.table,column:reference.column})}}if(references.size===0)return statements;return statements.map((statement)=>{if(/\bREFERENCES\b/i.test(statement))return statement;const match=statement.match(/^(\s*ALTER\s+TABLE\s+["`]?(\w+)["`]?\s+ADD\s+COLUMN\s+["`]?(\w+)["`]?\s+)([\s\S]*?)(;?\s*)$/i),reference=match?.[2]&&match[3]?references.get(`${match[2]}.${match[3]}`):void 0;if(!match||!reference)return statement;return`${match[1]}${match[4].trimEnd()} REFERENCES "${reference.table}"("${reference.column}")${match[5]}`})}export async function regenerateMigrationCorpus(options={}){try{configureQueryBuilder();const dialect=options.dialect??getQbDialect(),dir=options.dir??join(process.cwd(),"database","migrations"),sources=resolveModelSources();if(!sources)return err(Error("No models found. Define models in app/Models, or ensure the framework defaults at storage/framework/defaults/app/Models are present."));try{writeFileSync(join(sources.dir,`.qb-migrations.${dialect}.json`),JSON.stringify({plan:{dialect,tables:[]}}))}catch{}const snapshotPath=join(resolveSnapshotDir(),`model-snapshot.${dialect}.json`),parkedSnapshot=`${snapshotPath}.regenerating`;let snapshotParked=!1;if(existsSync(snapshotPath))try{renameSync(snapshotPath,parkedSnapshot);snapshotParked=!0}catch{}let result;try{result=await qbGenerateMigration(sources.dir,{dialect,dryRun:!0})}finally{if(snapshotParked)try{renameSync(parkedSnapshot,snapshotPath)}catch{}}const statements=result.sqlStatements??[];if(statements.length===0)return err(Error(`The generator produced no SQL for dialect "${dialect}".`));const dangling=findDanglingTypeReferences(statements);if(dangling.length>0)return err(Error(`The generator emitted ${dangling.length} reference(s) to enum type(s) it never creates: ${dangling.slice(0,5).join(", ")}${dangling.length>5?`, +${dangling.length-5} more`:""}. Writing this corpus would fail partway through a migration. This is a bug in the migration generator, not in your models.`));const groups=groupGeneratedStatements(statements);let existing=[];try{existing=readdirSync(dir).filter((f)=>f.endsWith(".sql")).sort()}catch{}const files=groups.map((group,index)=>({name:`${String(index+1).padStart(10,"0")}-${group.label}.sql`,statements:group.statements.length}));if(options.dryRun)return ok({dialect,models:sources.models.length,files,removed:existing,dir});mkdirSync(dir,{recursive:!0});for(const file of existing)unlinkSync(join(dir,file));groups.forEach((group,index)=>{const body=`${group.statements.map((s)=>s.trim().replace(/;\s*$/,"")).join(`;
|
|
27
29
|
`)};
|
|
28
30
|
`;writeFileSync(join(dir,files[index].name),body)});return ok({dialect,models:sources.models.length,files,removed:existing,dir})}catch(error){return err(handleError("Migration regeneration failed",error))}}function persistGeneratedMigrations(sqlStatements){if(!sqlStatements?.length)return 0;const migrationsDir=join(process.cwd(),"database","migrations");try{require("node:fs").mkdirSync(migrationsDir,{recursive:!0})}catch{}let existingSql="";try{for(const f of readdirSync(migrationsDir).filter((f)=>f.endsWith(".sql")))existingSql+=`
|
|
29
31
|
${readFileSync(join(migrationsDir,f),"utf8")}`}catch{}const normalize=(s)=>s.replace(/\s+/g," ").trim(),haystack=normalize(existingSql),groups=groupGeneratedStatements(sqlStatements);let written=0,cursor=nextMigrationNumber(migrationsDir);for(const group of groups){const fresh=group.statements.filter((stmt)=>!haystack.includes(normalize(stmt)));if(fresh.length===0)continue;const filename=`${String(cursor).padStart(10,"0")}-${group.label}.sql`,filePath=join(migrationsDir,filename),body=`${fresh.map((s)=>s.trim().replace(/;\s*$/,"")).join(`;
|
package/dist/utils.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{AsyncLocalStorage}from"node:async_hooks";import{createQueryBuilder,registerPersistentQueryHooks,setConfig}from"@stacksjs/query-builder";import{SQL}from"bun";import{env as envVars}from"@stacksjs/env";import{getConnectionDefaults}from"./defaults";import{isMysqlWire,toQueryBuilderDialect}from"./dialect";import{contextInTransaction,markContextWrote,resolveReplicaConnection,selectReplica,shouldRouteToReplica,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",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:""},postgres:{name:postgresDefaults.database,host:postgresDefaults.host,username:postgresDefaults.username,password:postgresDefaults.password,port:postgresDefaults.port,prefix:""}}},dbConfigLockTail=Promise.resolve();export function acquireDbConfigLock(){let release=()=>{};const held=new Promise((resolve)=>{release=resolve}),acquired=dbConfigLockTail.then(()=>release);dbConfigLockTail=dbConfigLockTail.then(()=>held);return acquired}export function initializeDbConfig(config){if(config?.app?.env)appEnv=config.app.env;if(config?.database?.default)dbDriver=config.database.default;if(config?.database)dbConfig=config.database;updateQueryBuilderConfig();_dbInstance=null;_replicaInstances=new Map}function getEnv(){return appEnv}function getDriver(){return dbDriver}function getDatabaseConfig(){return dbConfig}function getDialect(){return toQueryBuilderDialect(getDriver())}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",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})}}function forwardDatabaseQuery(event){import("./query-logger").then(({logQuery})=>logQuery(event)).catch(()=>{})}registerPersistentQueryHooks(createDatabaseQueryHooks(forwardDatabaseQuery));function getPoolConfig(){const driver=getDriver();if(driver==="sqlite")return;return getDatabaseConfig().connections?.[driver]?.pool}function getReplicas(){const driver=getDriver();if(driver==="sqlite")return[];return getDatabaseConfig().connections?.[driver]?.replicas??[]}function getReadPolicy(){return getDatabaseConfig().reads??{}}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,database:pool?{...dbConfigForQb,pool}:dbConfigForQb,verbose:getEnv()!=="production",snapshotDir:QB_SNAPSHOT_DIR,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 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);applyTransactionRoutingContext(_dbInstance)}return _dbInstance}let _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(),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 const db=new Proxy({},{get(_target,prop){if(prop==="fn")return aggregateFunctions;if(prop==="read")return readDb;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}}),readDb=new Proxy({},{get(_target,prop){if(prop==="fn")return aggregateFunctions;const instance=getExplicitReadDb(),value=instance[prop];if(typeof value==="function")return value.bind(instance);return value}});export{setConfig};
|
|
1
|
+
import{AsyncLocalStorage}from"node:async_hooks";import{createQueryBuilder,registerPersistentQueryHooks,setConfig}from"@stacksjs/query-builder";import{SQL}from"bun";import{env as envVars}from"@stacksjs/env";import{getConnectionDefaults}from"./defaults";import{isMysqlWire,isVitessSharded,toQueryBuilderDialect}from"./dialect";import{contextInTransaction,markContextWrote,resolveReplicaConnection,selectReplica,shouldRouteToReplica,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",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();export function acquireDbConfigLock(){let release=()=>{};const held=new Promise((resolve)=>{release=resolve}),acquired=dbConfigLockTail.then(()=>release);dbConfigLockTail=dbConfigLockTail.then(()=>held);return acquired}export function initializeDbConfig(config){if(config?.app?.env)appEnv=config.app.env;if(config?.database?.default)dbDriver=config.database.default;if(config?.database)dbConfig=config.database;updateQueryBuilderConfig();_dbInstance=null;_replicaInstances=new Map}function getEnv(){return appEnv}function getDriver(){return dbDriver}function getDatabaseConfig(){return dbConfig}function getDialect(){return toQueryBuilderDialect(getDriver())}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",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})}}function forwardDatabaseQuery(event){import("./query-logger").then(({logQuery})=>logQuery(event)).catch(()=>{})}registerPersistentQueryHooks(createDatabaseQueryHooks(forwardDatabaseQuery));function getPoolConfig(){const driver=getDriver();if(driver==="sqlite")return;return getDatabaseConfig().connections?.[driver]?.pool}function getReplicas(){const driver=getDriver();if(driver==="sqlite")return[];return getDatabaseConfig().connections?.[driver]?.replicas??[]}function getReadPolicy(){return getDatabaseConfig().reads??{}}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:QB_SNAPSHOT_DIR,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 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);applyTransactionRoutingContext(_dbInstance)}return _dbInstance}let _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(),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 const db=new Proxy({},{get(_target,prop){if(prop==="fn")return aggregateFunctions;if(prop==="read")return readDb;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}}),readDb=new Proxy({},{get(_target,prop){if(prop==="fn")return aggregateFunctions;const instance=getExplicitReadDb(),value=instance[prop];if(typeof value==="function")return value.bind(instance);return value}});export{setConfig};
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@stacksjs/database",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"sideEffects": false,
|
|
5
|
-
"version": "0.70.
|
|
5
|
+
"version": "0.70.262",
|
|
6
6
|
"description": "The Stacks database integration.",
|
|
7
7
|
"author": "Chris Breuer",
|
|
8
8
|
"contributors": [
|
|
@@ -61,19 +61,19 @@
|
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
63
|
"@stacksjs/ts-validation": "^0.5.0",
|
|
64
|
-
"bun-query-builder": "^0.2.
|
|
64
|
+
"bun-query-builder": "^0.2.7",
|
|
65
65
|
"dynamodb-tooling": "^0.3.2"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
|
-
"@stacksjs/cli": "0.70.
|
|
69
|
-
"@stacksjs/config": "0.70.
|
|
70
|
-
"@stacksjs/logging": "0.70.
|
|
71
|
-
"@stacksjs/router": "0.70.
|
|
68
|
+
"@stacksjs/cli": "0.70.262",
|
|
69
|
+
"@stacksjs/config": "0.70.262",
|
|
70
|
+
"@stacksjs/logging": "0.70.262",
|
|
71
|
+
"@stacksjs/router": "0.70.262",
|
|
72
72
|
"better-dx": "^0.2.17",
|
|
73
|
-
"@stacksjs/path": "0.70.
|
|
74
|
-
"@stacksjs/query-builder": "0.70.
|
|
75
|
-
"@stacksjs/storage": "0.70.
|
|
76
|
-
"@stacksjs/strings": "0.70.
|
|
77
|
-
"@stacksjs/utils": "0.70.
|
|
73
|
+
"@stacksjs/path": "0.70.262",
|
|
74
|
+
"@stacksjs/query-builder": "0.70.262",
|
|
75
|
+
"@stacksjs/storage": "0.70.262",
|
|
76
|
+
"@stacksjs/strings": "0.70.262",
|
|
77
|
+
"@stacksjs/utils": "0.70.262"
|
|
78
78
|
}
|
|
79
79
|
}
|