@stacksjs/database 0.70.258 → 0.70.260
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth-tables.js +18 -137
- package/dist/column.js +1 -26
- package/dist/custom/audits.js +20 -54
- package/dist/custom/errors.js +16 -46
- package/dist/custom/index.js +1 -3
- package/dist/custom/jobs.js +13 -137
- package/dist/database.js +1 -181
- package/dist/datetime-columns.js +2 -79
- package/dist/ddl-constraints.js +7 -111
- package/dist/defaults.js +1 -48
- package/dist/dialect.js +1 -79
- package/dist/driver-config.js +1 -172
- package/dist/drivers/defaults/index.js +1 -1
- package/dist/drivers/defaults/traits.js +1 -29
- package/dist/drivers/dynamodb.js +1 -607
- package/dist/drivers/helpers.js +1 -206
- package/dist/drivers/index.js +1 -9
- package/dist/drivers/mysql.js +58 -299
- package/dist/drivers/postgres.js +78 -368
- package/dist/drivers/sqlite.js +61 -379
- package/dist/ensure-database.js +1 -145
- package/dist/fk-audit.js +3 -187
- package/dist/index.js +1 -64
- package/dist/managed-columns.js +1 -59
- package/dist/migration-dialect.js +4 -107
- package/dist/migration-ledger.js +1 -382
- package/dist/migration-lock.js +1 -143
- package/dist/migrations.js +15 -1118
- package/dist/model-sources.js +1 -76
- package/dist/notification-tables.js +4 -49
- package/dist/query-logger.js +2 -241
- package/dist/query-parser.js +1 -93
- package/dist/rbac-tables.js +6 -61
- package/dist/relation-columns.js +1 -66
- package/dist/replicas.js +1 -74
- package/dist/safe-migrations.js +2 -52
- package/dist/schema.js +1 -10
- package/dist/seeder.js +1 -457
- package/dist/sql-helpers.js +1 -50
- package/dist/table.js +1 -26
- package/dist/tools/setup.js +1 -6
- package/dist/trait-tables.js +8 -153
- package/dist/transaction-context.js +1 -62
- package/dist/types.js +1 -98
- package/dist/unique-audit.js +3 -155
- package/dist/utils.js +1 -285
- package/dist/uuid-columns.js +1 -68
- package/dist/validators.js +1 -122
- package/dist/vschema.js +2 -121
- package/package.json +20 -13
package/dist/database.js
CHANGED
|
@@ -1,181 +1 @@
|
|
|
1
|
-
import { toQueryBuilderDialect }
|
|
2
|
-
import { QB_SNAPSHOT_DIR } from "./utils";
|
|
3
|
-
import { createQueryBuilder, setConfig } from "@stacksjs/query-builder";
|
|
4
|
-
import { env as stacksEnv } from "@stacksjs/env";
|
|
5
|
-
|
|
6
|
-
export class Database {
|
|
7
|
-
_queryBuilder = null;
|
|
8
|
-
_options;
|
|
9
|
-
_initialized = !1;
|
|
10
|
-
constructor(options) {
|
|
11
|
-
this._options = {
|
|
12
|
-
verbose: !1,
|
|
13
|
-
timestamps: {
|
|
14
|
-
createdAt: "created_at",
|
|
15
|
-
updatedAt: "updated_at",
|
|
16
|
-
defaultOrderColumn: "created_at"
|
|
17
|
-
},
|
|
18
|
-
softDeletes: {
|
|
19
|
-
enabled: !1,
|
|
20
|
-
column: "deleted_at",
|
|
21
|
-
defaultFilter: !0
|
|
22
|
-
},
|
|
23
|
-
...options
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
get driver() {
|
|
27
|
-
return this._options.driver;
|
|
28
|
-
}
|
|
29
|
-
get connection() {
|
|
30
|
-
return this._options.connection;
|
|
31
|
-
}
|
|
32
|
-
get isInitialized() {
|
|
33
|
-
return this._initialized;
|
|
34
|
-
}
|
|
35
|
-
get query() {
|
|
36
|
-
if (!this._queryBuilder)
|
|
37
|
-
this.initialize();
|
|
38
|
-
return this._queryBuilder;
|
|
39
|
-
}
|
|
40
|
-
initialize() {
|
|
41
|
-
if (this._initialized)
|
|
42
|
-
return;
|
|
43
|
-
setConfig({
|
|
44
|
-
snapshotDir: QB_SNAPSHOT_DIR,
|
|
45
|
-
dialect: toQueryBuilderDialect(this._options.driver),
|
|
46
|
-
database: this._options.connection,
|
|
47
|
-
verbose: this._options.verbose,
|
|
48
|
-
timestamps: this._options.timestamps,
|
|
49
|
-
softDeletes: this._options.softDeletes,
|
|
50
|
-
hooks: this._options.hooks
|
|
51
|
-
});
|
|
52
|
-
this._queryBuilder = createQueryBuilder();
|
|
53
|
-
this._initialized = !0;
|
|
54
|
-
}
|
|
55
|
-
switchDriver(driver, connection) {
|
|
56
|
-
this.close();
|
|
57
|
-
this._options.driver = driver;
|
|
58
|
-
this._options.connection = connection;
|
|
59
|
-
this._initialized = !1;
|
|
60
|
-
this.initialize();
|
|
61
|
-
}
|
|
62
|
-
async close() {
|
|
63
|
-
if (this._queryBuilder && typeof this._queryBuilder.close === "function")
|
|
64
|
-
await this._queryBuilder.close();
|
|
65
|
-
this._queryBuilder = null;
|
|
66
|
-
this._initialized = !1;
|
|
67
|
-
}
|
|
68
|
-
static fromConfig(config, env) {
|
|
69
|
-
const driver = config.default;
|
|
70
|
-
let connection;
|
|
71
|
-
switch (driver) {
|
|
72
|
-
case "sqlite": {
|
|
73
|
-
connection = { database: env === "testing" ? config.connections.sqlite?.database?.replace(".sqlite", "_testing.sqlite") || "database/stacks_testing.sqlite" : config.connections.sqlite?.database || "database/stacks.sqlite" };
|
|
74
|
-
break;
|
|
75
|
-
}
|
|
76
|
-
case "mysql": {
|
|
77
|
-
const mysql = config.connections.mysql;
|
|
78
|
-
connection = {
|
|
79
|
-
database: mysql?.name || "stacks",
|
|
80
|
-
host: mysql?.host || "127.0.0.1",
|
|
81
|
-
port: mysql?.port || 3306,
|
|
82
|
-
username: mysql?.username || "root",
|
|
83
|
-
password: mysql?.password || ""
|
|
84
|
-
};
|
|
85
|
-
break;
|
|
86
|
-
}
|
|
87
|
-
case "singlestore": {
|
|
88
|
-
const singlestore = config.connections.singlestore;
|
|
89
|
-
connection = {
|
|
90
|
-
database: singlestore?.name || "stacks",
|
|
91
|
-
host: singlestore?.host || "127.0.0.1",
|
|
92
|
-
port: singlestore?.port || 3306,
|
|
93
|
-
username: singlestore?.username || "root",
|
|
94
|
-
password: singlestore?.password || ""
|
|
95
|
-
};
|
|
96
|
-
break;
|
|
97
|
-
}
|
|
98
|
-
case "postgres": {
|
|
99
|
-
const postgres = config.connections.postgres, dbName = postgres?.name || "stacks";
|
|
100
|
-
connection = {
|
|
101
|
-
database: env === "testing" ? `${dbName}_testing` : dbName,
|
|
102
|
-
host: postgres?.host || "127.0.0.1",
|
|
103
|
-
port: postgres?.port || 5432,
|
|
104
|
-
username: postgres?.username || "",
|
|
105
|
-
password: postgres?.password || ""
|
|
106
|
-
};
|
|
107
|
-
break;
|
|
108
|
-
}
|
|
109
|
-
case "dynamodb":
|
|
110
|
-
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.");
|
|
111
|
-
default:
|
|
112
|
-
throw Error(`[database] Unknown DB_CONNECTION "${String(driver)}". Allowed values: sqlite, mysql, postgres.`);
|
|
113
|
-
}
|
|
114
|
-
return new Database({
|
|
115
|
-
driver,
|
|
116
|
-
connection,
|
|
117
|
-
verbose: env !== "production"
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
static fromEnv() {
|
|
121
|
-
const driver = stacksEnv.DB_CONNECTION || "sqlite";
|
|
122
|
-
let connection;
|
|
123
|
-
switch (driver) {
|
|
124
|
-
case "sqlite":
|
|
125
|
-
connection = {
|
|
126
|
-
database: stacksEnv.DB_DATABASE || "database/stacks.sqlite"
|
|
127
|
-
};
|
|
128
|
-
break;
|
|
129
|
-
case "mysql":
|
|
130
|
-
case "singlestore":
|
|
131
|
-
connection = {
|
|
132
|
-
database: stacksEnv.DB_DATABASE || "stacks",
|
|
133
|
-
host: stacksEnv.DB_HOST || "127.0.0.1",
|
|
134
|
-
port: stacksEnv.DB_PORT || 3306,
|
|
135
|
-
username: stacksEnv.DB_USERNAME || "root",
|
|
136
|
-
password: stacksEnv.DB_PASSWORD || ""
|
|
137
|
-
};
|
|
138
|
-
break;
|
|
139
|
-
case "postgres":
|
|
140
|
-
connection = {
|
|
141
|
-
database: stacksEnv.DB_DATABASE || "stacks",
|
|
142
|
-
host: stacksEnv.DB_HOST || "127.0.0.1",
|
|
143
|
-
port: stacksEnv.DB_PORT || 5432,
|
|
144
|
-
username: stacksEnv.DB_USERNAME || "",
|
|
145
|
-
password: stacksEnv.DB_PASSWORD || ""
|
|
146
|
-
};
|
|
147
|
-
break;
|
|
148
|
-
default:
|
|
149
|
-
connection = { database: ":memory:" };
|
|
150
|
-
}
|
|
151
|
-
return new Database({
|
|
152
|
-
driver,
|
|
153
|
-
connection,
|
|
154
|
-
verbose: stacksEnv.APP_ENV !== "prod"
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
export function createDatabase(options) {
|
|
159
|
-
return new Database(options);
|
|
160
|
-
}
|
|
161
|
-
export function createSqliteDatabase(database, options) {
|
|
162
|
-
return new Database({
|
|
163
|
-
driver: "sqlite",
|
|
164
|
-
connection: { database },
|
|
165
|
-
...options
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
export function createPostgresDatabase(connection, options) {
|
|
169
|
-
return new Database({
|
|
170
|
-
driver: "postgres",
|
|
171
|
-
connection,
|
|
172
|
-
...options
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
export function createMysqlDatabase(connection, options) {
|
|
176
|
-
return new Database({
|
|
177
|
-
driver: "mysql",
|
|
178
|
-
connection,
|
|
179
|
-
...options
|
|
180
|
-
});
|
|
181
|
-
}
|
|
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})}
|
package/dist/datetime-columns.js
CHANGED
|
@@ -1,85 +1,8 @@
|
|
|
1
|
-
import process from
|
|
2
|
-
import { log } from "@stacksjs/logging";
|
|
3
|
-
import { env as envVars } from "@stacksjs/env";
|
|
4
|
-
import { db } from "./utils";
|
|
5
|
-
import { dialectCapabilities } from "./dialect";
|
|
6
|
-
import { traitTableNames } from "./trait-tables";
|
|
7
|
-
function getDbDriver() {
|
|
8
|
-
return process.env.DB_CONNECTION || envVars.DB_CONNECTION || "sqlite";
|
|
9
|
-
}
|
|
10
|
-
export function frameworkDatetimeTables() {
|
|
11
|
-
return [
|
|
12
|
-
...traitTableNames(),
|
|
13
|
-
"passkeys",
|
|
14
|
-
"password_resets",
|
|
15
|
-
"oauth_clients",
|
|
16
|
-
"oauth_access_tokens",
|
|
17
|
-
"oauth_refresh_tokens",
|
|
18
|
-
"two_factor_challenges",
|
|
19
|
-
"two_factor_pending_secrets",
|
|
20
|
-
"webauthn_challenges",
|
|
21
|
-
"roles",
|
|
22
|
-
"permissions",
|
|
23
|
-
"user_roles",
|
|
24
|
-
"user_permissions",
|
|
25
|
-
"role_permissions",
|
|
26
|
-
"notifications",
|
|
27
|
-
"notification_preferences",
|
|
28
|
-
"notification_deliveries"
|
|
29
|
-
];
|
|
30
|
-
}
|
|
31
|
-
export async function findTimestampColumns() {
|
|
32
|
-
const tables = frameworkDatetimeTables(), placeholders = tables.map(() => "?").join(", ");
|
|
33
|
-
return (await db.unsafe(`SELECT TABLE_NAME, COLUMN_NAME, IS_NULLABLE, COLUMN_DEFAULT, EXTRA
|
|
1
|
+
import process from"node:process";import{log}from"@stacksjs/logging";import{env as envVars}from"@stacksjs/env";import{db}from"./utils";import{dialectCapabilities}from"./dialect";import{traitTableNames}from"./trait-tables";function getDbDriver(){return process.env.DB_CONNECTION||envVars.DB_CONNECTION||"sqlite"}export function frameworkDatetimeTables(){return[...traitTableNames(),"passkeys","password_resets","oauth_clients","oauth_access_tokens","oauth_refresh_tokens","two_factor_challenges","two_factor_pending_secrets","webauthn_challenges","roles","permissions","user_roles","user_permissions","role_permissions","notifications","notification_preferences","notification_deliveries"]}export async function findTimestampColumns(){const tables=frameworkDatetimeTables(),placeholders=tables.map(()=>"?").join(", ");return(await db.unsafe(`SELECT TABLE_NAME, COLUMN_NAME, IS_NULLABLE, COLUMN_DEFAULT, EXTRA
|
|
34
2
|
FROM information_schema.COLUMNS
|
|
35
3
|
WHERE TABLE_SCHEMA = DATABASE()
|
|
36
4
|
AND TABLE_NAME IN (${placeholders})
|
|
37
5
|
AND (
|
|
38
6
|
DATA_TYPE = 'timestamp'
|
|
39
7
|
OR (DATA_TYPE = 'varchar' AND COLUMN_NAME IN ('created_at', 'updated_at'))
|
|
40
|
-
)`,
|
|
41
|
-
table: String(row.TABLE_NAME ?? row.table_name),
|
|
42
|
-
column: String(row.COLUMN_NAME ?? row.column_name),
|
|
43
|
-
nullable: String(row.IS_NULLABLE ?? row.is_nullable).toUpperCase() === "YES",
|
|
44
|
-
columnDefault: row.COLUMN_DEFAULT ?? row.column_default ?? null,
|
|
45
|
-
extra: String(row.EXTRA ?? row.extra ?? "")
|
|
46
|
-
}));
|
|
47
|
-
}
|
|
48
|
-
export function modifyToDatetimeSql(column) {
|
|
49
|
-
const safe = /^[a-z_]\w*$/i;
|
|
50
|
-
if (!safe.test(column.table) || !safe.test(column.column))
|
|
51
|
-
throw Error(`[datetime-columns] Refusing to alter unsafe identifier: ${column.table}.${column.column}`);
|
|
52
|
-
let ddl = `ALTER TABLE \`${column.table}\` MODIFY \`${column.column}\` DATETIME`;
|
|
53
|
-
ddl += column.nullable ? " NULL" : " NOT NULL";
|
|
54
|
-
if (column.columnDefault !== null) {
|
|
55
|
-
const isFunction = /^CURRENT_TIMESTAMP(\(\d*\))?$/i.test(String(column.columnDefault));
|
|
56
|
-
ddl += isFunction ? ` DEFAULT ${column.columnDefault}` : ` DEFAULT '${String(column.columnDefault).replace(/'/g, "''")}'`;
|
|
57
|
-
}
|
|
58
|
-
if (/on update CURRENT_TIMESTAMP/i.test(column.extra))
|
|
59
|
-
ddl += " ON UPDATE CURRENT_TIMESTAMP";
|
|
60
|
-
return ddl;
|
|
61
|
-
}
|
|
62
|
-
export async function ensureUtcDatetimeColumns(options = {}) {
|
|
63
|
-
const driver = getDbDriver();
|
|
64
|
-
if (dialectCapabilities(driver).wire !== "mysql")
|
|
65
|
-
return { success: !0, converted: 0 };
|
|
66
|
-
try {
|
|
67
|
-
const columns = await findTimestampColumns();
|
|
68
|
-
if (columns.length === 0) {
|
|
69
|
-
if (options.verbose)
|
|
70
|
-
log.info("No TIMESTAMP columns left to convert");
|
|
71
|
-
return { success: !0, converted: 0 };
|
|
72
|
-
}
|
|
73
|
-
for (const column of columns) {
|
|
74
|
-
if (options.verbose)
|
|
75
|
-
log.info(`Converting ${column.table}.${column.column} to DATETIME...`);
|
|
76
|
-
await db.unsafe(modifyToDatetimeSql(column)).execute();
|
|
77
|
-
}
|
|
78
|
-
log.debug(`[datetime-columns] Converted ${columns.length} TIMESTAMP column(s) to DATETIME`);
|
|
79
|
-
return { success: !0, converted: columns.length };
|
|
80
|
-
} catch (error) {
|
|
81
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
82
|
-
log.error(`Failed to convert TIMESTAMP columns to DATETIME: ${message}`);
|
|
83
|
-
return { success: !1, converted: 0, error: message };
|
|
84
|
-
}
|
|
85
|
-
}
|
|
8
|
+
)`,tables).execute()).map((row)=>({table:String(row.TABLE_NAME??row.table_name),column:String(row.COLUMN_NAME??row.column_name),nullable:String(row.IS_NULLABLE??row.is_nullable).toUpperCase()==="YES",columnDefault:row.COLUMN_DEFAULT??row.column_default??null,extra:String(row.EXTRA??row.extra??"")}))}export function modifyToDatetimeSql(column){const safe=/^[a-z_]\w*$/i;if(!safe.test(column.table)||!safe.test(column.column))throw Error(`[datetime-columns] Refusing to alter unsafe identifier: ${column.table}.${column.column}`);let ddl=`ALTER TABLE \`${column.table}\` MODIFY \`${column.column}\` DATETIME`;ddl+=column.nullable?" NULL":" NOT NULL";if(column.columnDefault!==null){const isFunction=/^CURRENT_TIMESTAMP(\(\d*\))?$/i.test(String(column.columnDefault));ddl+=isFunction?` DEFAULT ${column.columnDefault}`:` DEFAULT '${String(column.columnDefault).replace(/'/g,"''")}'`}if(/on update CURRENT_TIMESTAMP/i.test(column.extra))ddl+=" ON UPDATE CURRENT_TIMESTAMP";return ddl}export async function ensureUtcDatetimeColumns(options={}){const driver=getDbDriver();if(dialectCapabilities(driver).wire!=="mysql")return{success:!0,converted:0};try{const columns=await findTimestampColumns();if(columns.length===0){if(options.verbose)log.info("No TIMESTAMP columns left to convert");return{success:!0,converted:0}}for(const column of columns){if(options.verbose)log.info(`Converting ${column.table}.${column.column} to DATETIME...`);await db.unsafe(modifyToDatetimeSql(column)).execute()}log.debug(`[datetime-columns] Converted ${columns.length} TIMESTAMP column(s) to DATETIME`);return{success:!0,converted:columns.length}}catch(error){const message=error instanceof Error?error.message:String(error);log.error(`Failed to convert TIMESTAMP columns to DATETIME: ${message}`);return{success:!1,converted:0,error:message}}}
|
package/dist/ddl-constraints.js
CHANGED
|
@@ -1,111 +1,7 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
{ capability: "autoIncrement", pattern: /\bAUTO_INCREMENT\b(?!\s*=)/i, label: "AUTO_INCREMENT" },
|
|
9
|
-
{ capability: "createIndexIfNotExists", pattern: /\bCREATE\s+(?:UNIQUE\s+)?INDEX\s+IF\s+NOT\s+EXISTS\b/i, label: "CREATE INDEX IF NOT EXISTS" }
|
|
10
|
-
];
|
|
11
|
-
function supports(caps, capability) {
|
|
12
|
-
switch (capability) {
|
|
13
|
-
case "foreignKeys":
|
|
14
|
-
return caps.supportsForeignKeys;
|
|
15
|
-
case "autoIncrement":
|
|
16
|
-
return caps.supportsAutoIncrement;
|
|
17
|
-
case "createIndexIfNotExists":
|
|
18
|
-
return caps.supportsCreateIndexIfNotExists;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
export function auditDdlSql(sql, file, dialect) {
|
|
22
|
-
const caps = dialectCapabilities(dialect), lines = stripSqlNoise(sql).split(`
|
|
23
|
-
`), rawLines = sql.split(`
|
|
24
|
-
`), found = [];
|
|
25
|
-
for (let index = 0;index < lines.length; index++)
|
|
26
|
-
for (const { capability, pattern, label } of CONSTRUCTS) {
|
|
27
|
-
if (supports(caps, capability))
|
|
28
|
-
continue;
|
|
29
|
-
if (pattern.test(lines[index] ?? ""))
|
|
30
|
-
found.push({
|
|
31
|
-
capability,
|
|
32
|
-
construct: label,
|
|
33
|
-
file,
|
|
34
|
-
line: index + 1,
|
|
35
|
-
snippet: (rawLines[index] ?? "").trim().slice(0, 120)
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
return found;
|
|
39
|
-
}
|
|
40
|
-
export function auditDdlConstraints(options) {
|
|
41
|
-
const { dir, dialect } = options;
|
|
42
|
-
if (!existsSync(dir))
|
|
43
|
-
return { total: 0, violations: [], empty: !0 };
|
|
44
|
-
let files;
|
|
45
|
-
try {
|
|
46
|
-
files = readdirSync(dir).filter((f) => f.endsWith(".sql")).sort();
|
|
47
|
-
} catch {
|
|
48
|
-
return { total: 0, violations: [], empty: !0 };
|
|
49
|
-
}
|
|
50
|
-
const violations = [];
|
|
51
|
-
for (const file of files) {
|
|
52
|
-
let sql;
|
|
53
|
-
try {
|
|
54
|
-
sql = readFileSync(join(dir, file), "utf8");
|
|
55
|
-
} catch {
|
|
56
|
-
continue;
|
|
57
|
-
}
|
|
58
|
-
violations.push(...auditDdlSql(sql, file, dialect));
|
|
59
|
-
}
|
|
60
|
-
return { total: files.length, violations, empty: files.length === 0 };
|
|
61
|
-
}
|
|
62
|
-
export const DDL_CONSTRAINT_OVERRIDE_ENV = "STACKS_ALLOW_DDL_CONSTRAINT_VIOLATIONS";
|
|
63
|
-
const REMEDIES = {
|
|
64
|
-
foreignKeys: [
|
|
65
|
-
"Distributed engines cannot enforce a foreign key across shards, so referential",
|
|
66
|
-
"integrity has to move into the application. Regenerate the corpus for this",
|
|
67
|
-
"dialect \u2014 the generator emits the backing index without the constraint \u2014 and",
|
|
68
|
-
"rely on the model relationships plus `buddy doctor` (which reports orphan rows)",
|
|
69
|
-
"instead of database-level cascades."
|
|
70
|
-
].join(`
|
|
71
|
-
`),
|
|
72
|
-
autoIncrement: [
|
|
73
|
-
"Every shard would hand out the same AUTO_INCREMENT values and collide, so the",
|
|
74
|
-
"primary key has to come from somewhere else. Add `useUuid: true` to the model",
|
|
75
|
-
"traits for an application-generated key, or back the table with a sequence in",
|
|
76
|
-
"an unsharded keyspace and reference it from the VSchema."
|
|
77
|
-
].join(`
|
|
78
|
-
`),
|
|
79
|
-
createIndexIfNotExists: [
|
|
80
|
-
"MySQL has no `CREATE INDEX IF NOT EXISTS` form and rejects it as a syntax",
|
|
81
|
-
"error. Regenerate the corpus for this dialect: the generator emits a bare",
|
|
82
|
-
"`CREATE INDEX` and treats the duplicate-key error on replay as success."
|
|
83
|
-
].join(`
|
|
84
|
-
`)
|
|
85
|
-
};
|
|
86
|
-
export function formatDdlConstraintError(audit, dialect, dir) {
|
|
87
|
-
const byCapability = new Map;
|
|
88
|
-
for (const violation of audit.violations) {
|
|
89
|
-
const bucket = byCapability.get(violation.capability) ?? [];
|
|
90
|
-
bucket.push(violation);
|
|
91
|
-
byCapability.set(violation.capability, bucket);
|
|
92
|
-
}
|
|
93
|
-
const lines = [
|
|
94
|
-
`The migration files in ${dir} use SQL features that ${dialect} does not implement.`,
|
|
95
|
-
"",
|
|
96
|
-
"Nothing was migrated, so the database is unchanged.",
|
|
97
|
-
""
|
|
98
|
-
];
|
|
99
|
-
for (const [capability, found] of byCapability) {
|
|
100
|
-
const files = new Set(found.map((v) => v.file));
|
|
101
|
-
lines.push(`${found.length} use(s) of ${found[0]?.construct} across ${files.size} file(s), for example:`);
|
|
102
|
-
for (const violation of found.slice(0, 3))
|
|
103
|
-
lines.push(` ${violation.file}:${violation.line} ${violation.snippet}`);
|
|
104
|
-
lines.push("");
|
|
105
|
-
lines.push(REMEDIES[capability]);
|
|
106
|
-
lines.push("");
|
|
107
|
-
}
|
|
108
|
-
lines.push(`If you know this corpus is correct, re-run with ${DDL_CONSTRAINT_OVERRIDE_ENV}=1 to proceed anyway.`);
|
|
109
|
-
return lines.join(`
|
|
110
|
-
`);
|
|
111
|
-
}
|
|
1
|
+
import{existsSync,readdirSync,readFileSync}from"node:fs";import{join}from"node:path";import{dialectCapabilities}from"./dialect";import{stripSqlNoise}from"./migration-dialect";const CONSTRUCTS=[{capability:"foreignKeys",pattern:/\bFOREIGN\s+KEY\b/i,label:"FOREIGN KEY"},{capability:"foreignKeys",pattern:/\bREFERENCES\b/i,label:"REFERENCES"},{capability:"autoIncrement",pattern:/\bAUTO_INCREMENT\b(?!\s*=)/i,label:"AUTO_INCREMENT"},{capability:"createIndexIfNotExists",pattern:/\bCREATE\s+(?:UNIQUE\s+)?INDEX\s+IF\s+NOT\s+EXISTS\b/i,label:"CREATE INDEX IF NOT EXISTS"}];function supports(caps,capability){switch(capability){case"foreignKeys":return caps.supportsForeignKeys;case"autoIncrement":return caps.supportsAutoIncrement;case"createIndexIfNotExists":return caps.supportsCreateIndexIfNotExists}}export function auditDdlSql(sql,file,dialect){const caps=dialectCapabilities(dialect),lines=stripSqlNoise(sql).split(`
|
|
2
|
+
`),rawLines=sql.split(`
|
|
3
|
+
`),found=[];for(let index=0;index<lines.length;index++)for(const{capability,pattern,label}of CONSTRUCTS){if(supports(caps,capability))continue;if(pattern.test(lines[index]??""))found.push({capability,construct:label,file,line:index+1,snippet:(rawLines[index]??"").trim().slice(0,120)})}return found}export function auditDdlConstraints(options){const{dir,dialect}=options;if(!existsSync(dir))return{total:0,violations:[],empty:!0};let files;try{files=readdirSync(dir).filter((f)=>f.endsWith(".sql")).sort()}catch{return{total:0,violations:[],empty:!0}}const violations=[];for(const file of files){let sql;try{sql=readFileSync(join(dir,file),"utf8")}catch{continue}violations.push(...auditDdlSql(sql,file,dialect))}return{total:files.length,violations,empty:files.length===0}}export const DDL_CONSTRAINT_OVERRIDE_ENV="STACKS_ALLOW_DDL_CONSTRAINT_VIOLATIONS";const REMEDIES={foreignKeys:["Distributed engines cannot enforce a foreign key across shards, so referential","integrity has to move into the application. Regenerate the corpus for this","dialect \u2014 the generator emits the backing index without the constraint \u2014 and","rely on the model relationships plus `buddy doctor` (which reports orphan rows)","instead of database-level cascades."].join(`
|
|
4
|
+
`),autoIncrement:["Every shard would hand out the same AUTO_INCREMENT values and collide, so the","primary key has to come from somewhere else. Add `useUuid: true` to the model","traits for an application-generated key, or back the table with a sequence in","an unsharded keyspace and reference it from the VSchema."].join(`
|
|
5
|
+
`),createIndexIfNotExists:["MySQL has no `CREATE INDEX IF NOT EXISTS` form and rejects it as a syntax","error. Regenerate the corpus for this dialect: the generator emits a bare","`CREATE INDEX` and treats the duplicate-key error on replay as success."].join(`
|
|
6
|
+
`)};export function formatDdlConstraintError(audit,dialect,dir){const byCapability=new Map;for(const violation of audit.violations){const bucket=byCapability.get(violation.capability)??[];bucket.push(violation);byCapability.set(violation.capability,bucket)}const lines=[`The migration files in ${dir} use SQL features that ${dialect} does not implement.`,"","Nothing was migrated, so the database is unchanged.",""];for(const[capability,found]of byCapability){const files=new Set(found.map((v)=>v.file));lines.push(`${found.length} use(s) of ${found[0]?.construct} across ${files.size} file(s), for example:`);for(const violation of found.slice(0,3))lines.push(` ${violation.file}:${violation.line} ${violation.snippet}`);lines.push("");lines.push(REMEDIES[capability]);lines.push("")}lines.push(`If you know this corpus is correct, re-run with ${DDL_CONSTRAINT_OVERRIDE_ENV}=1 to proceed anyway.`);return lines.join(`
|
|
7
|
+
`)}
|
package/dist/defaults.js
CHANGED
|
@@ -1,48 +1 @@
|
|
|
1
|
-
export const DB_HOST_DEFAULT
|
|
2
|
-
mysql: 3306,
|
|
3
|
-
postgres: 5432,
|
|
4
|
-
sqlite: 0
|
|
5
|
-
}, DB_NAMES = {
|
|
6
|
-
default: "stacks",
|
|
7
|
-
sqlitePath: "database/stacks.sqlite",
|
|
8
|
-
sqliteTestingPath: "database/stacks_testing.sqlite"
|
|
9
|
-
}, DB_USERS = {
|
|
10
|
-
mysql: "root",
|
|
11
|
-
postgres: "postgres",
|
|
12
|
-
sqlite: ""
|
|
13
|
-
}, REDIS_DEFAULTS = {
|
|
14
|
-
host: "localhost",
|
|
15
|
-
port: 6379
|
|
16
|
-
}, AWS_DEFAULTS = {
|
|
17
|
-
region: "us-east-1"
|
|
18
|
-
};
|
|
19
|
-
export function getConnectionDefaults(driver, envProxy) {
|
|
20
|
-
const e = envProxy ?? {};
|
|
21
|
-
switch (driver) {
|
|
22
|
-
case "sqlite":
|
|
23
|
-
return {
|
|
24
|
-
database: e.DB_DATABASE_PATH || DB_NAMES.sqlitePath,
|
|
25
|
-
prefix: ""
|
|
26
|
-
};
|
|
27
|
-
case "mysql":
|
|
28
|
-
return {
|
|
29
|
-
database: e.DB_DATABASE || DB_NAMES.default,
|
|
30
|
-
host: e.DB_HOST || DB_HOST_DEFAULT,
|
|
31
|
-
port: e.DB_PORT || DB_PORTS.mysql,
|
|
32
|
-
username: e.DB_USERNAME || DB_USERS.mysql,
|
|
33
|
-
password: e.DB_PASSWORD || "",
|
|
34
|
-
prefix: ""
|
|
35
|
-
};
|
|
36
|
-
case "postgres":
|
|
37
|
-
return {
|
|
38
|
-
database: e.DB_DATABASE || DB_NAMES.default,
|
|
39
|
-
host: e.DB_HOST || DB_HOST_DEFAULT,
|
|
40
|
-
port: e.DB_PORT || DB_PORTS.postgres,
|
|
41
|
-
username: e.DB_USERNAME || DB_USERS.postgres,
|
|
42
|
-
password: e.DB_PASSWORD || "",
|
|
43
|
-
prefix: ""
|
|
44
|
-
};
|
|
45
|
-
default:
|
|
46
|
-
return { database: ":memory:" };
|
|
47
|
-
}
|
|
48
|
-
}
|
|
1
|
+
export const DB_HOST_DEFAULT="127.0.0.1",DB_PORTS={mysql:3306,postgres:5432,sqlite:0},DB_NAMES={default:"stacks",sqlitePath:"database/stacks.sqlite",sqliteTestingPath:"database/stacks_testing.sqlite"},DB_USERS={mysql:"root",postgres:"postgres",sqlite:""},REDIS_DEFAULTS={host:"localhost",port:6379},AWS_DEFAULTS={region:"us-east-1"};export function getConnectionDefaults(driver,envProxy){const e=envProxy??{};switch(driver){case"sqlite":return{database:e.DB_DATABASE_PATH||DB_NAMES.sqlitePath,prefix:""};case"mysql":return{database:e.DB_DATABASE||DB_NAMES.default,host:e.DB_HOST||DB_HOST_DEFAULT,port:e.DB_PORT||DB_PORTS.mysql,username:e.DB_USERNAME||DB_USERS.mysql,password:e.DB_PASSWORD||"",prefix:""};case"postgres":return{database:e.DB_DATABASE||DB_NAMES.default,host:e.DB_HOST||DB_HOST_DEFAULT,port:e.DB_PORT||DB_PORTS.postgres,username:e.DB_USERNAME||DB_USERS.postgres,password:e.DB_PASSWORD||"",prefix:""};default:return{database:":memory:"}}}
|
package/dist/dialect.js
CHANGED
|
@@ -1,79 +1 @@
|
|
|
1
|
-
const CAPABILITIES
|
|
2
|
-
sqlite: {
|
|
3
|
-
dialect: "sqlite",
|
|
4
|
-
wire: "sqlite",
|
|
5
|
-
queryBuilderDialect: "sqlite",
|
|
6
|
-
identifierQuote: '"',
|
|
7
|
-
supportsForeignKeys: !0,
|
|
8
|
-
supportsAutoIncrement: !0,
|
|
9
|
-
supportsAtomicMultiTableTransactions: !0,
|
|
10
|
-
requiresOnlineDdl: !1,
|
|
11
|
-
supportsCreateIndexIfNotExists: !0
|
|
12
|
-
},
|
|
13
|
-
mysql: {
|
|
14
|
-
dialect: "mysql",
|
|
15
|
-
wire: "mysql",
|
|
16
|
-
queryBuilderDialect: "mysql",
|
|
17
|
-
defaultPort: 3306,
|
|
18
|
-
identifierQuote: "`",
|
|
19
|
-
supportsForeignKeys: !0,
|
|
20
|
-
supportsAutoIncrement: !0,
|
|
21
|
-
supportsAtomicMultiTableTransactions: !0,
|
|
22
|
-
requiresOnlineDdl: !1,
|
|
23
|
-
supportsCreateIndexIfNotExists: !1
|
|
24
|
-
},
|
|
25
|
-
singlestore: {
|
|
26
|
-
dialect: "singlestore",
|
|
27
|
-
wire: "mysql",
|
|
28
|
-
queryBuilderDialect: "singlestore",
|
|
29
|
-
defaultPort: 3306,
|
|
30
|
-
identifierQuote: "`",
|
|
31
|
-
supportsForeignKeys: !1,
|
|
32
|
-
supportsAutoIncrement: !0,
|
|
33
|
-
supportsAtomicMultiTableTransactions: !1,
|
|
34
|
-
requiresOnlineDdl: !1,
|
|
35
|
-
supportsCreateIndexIfNotExists: !1
|
|
36
|
-
},
|
|
37
|
-
vitess: {
|
|
38
|
-
dialect: "vitess",
|
|
39
|
-
wire: "mysql",
|
|
40
|
-
queryBuilderDialect: "vitess",
|
|
41
|
-
defaultPort: 15306,
|
|
42
|
-
identifierQuote: "`",
|
|
43
|
-
supportsForeignKeys: !1,
|
|
44
|
-
supportsAutoIncrement: !1,
|
|
45
|
-
supportsAtomicMultiTableTransactions: !1,
|
|
46
|
-
requiresOnlineDdl: !0,
|
|
47
|
-
supportsCreateIndexIfNotExists: !1
|
|
48
|
-
},
|
|
49
|
-
postgres: {
|
|
50
|
-
dialect: "postgres",
|
|
51
|
-
wire: "postgres",
|
|
52
|
-
queryBuilderDialect: "postgres",
|
|
53
|
-
defaultPort: 5432,
|
|
54
|
-
identifierQuote: '"',
|
|
55
|
-
supportsForeignKeys: !0,
|
|
56
|
-
supportsAutoIncrement: !0,
|
|
57
|
-
supportsAtomicMultiTableTransactions: !0,
|
|
58
|
-
requiresOnlineDdl: !1,
|
|
59
|
-
supportsCreateIndexIfNotExists: !0
|
|
60
|
-
}
|
|
61
|
-
};
|
|
62
|
-
export function dialectCapabilities(dialect) {
|
|
63
|
-
return CAPABILITIES[dialect] ?? CAPABILITIES.sqlite;
|
|
64
|
-
}
|
|
65
|
-
export function isKnownDialect(dialect) {
|
|
66
|
-
return dialect in CAPABILITIES;
|
|
67
|
-
}
|
|
68
|
-
export function knownDialects() {
|
|
69
|
-
return Object.keys(CAPABILITIES);
|
|
70
|
-
}
|
|
71
|
-
export function isMysqlWire(dialect) {
|
|
72
|
-
return dialectCapabilities(dialect).wire === "mysql";
|
|
73
|
-
}
|
|
74
|
-
export function isPostgresWire(dialect) {
|
|
75
|
-
return dialectCapabilities(dialect).wire === "postgres";
|
|
76
|
-
}
|
|
77
|
-
export function toQueryBuilderDialect(dialect) {
|
|
78
|
-
return dialectCapabilities(dialect).queryBuilderDialect;
|
|
79
|
-
}
|
|
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){return CAPABILITIES[dialect]??CAPABILITIES.sqlite}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}
|