@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.
Files changed (50) hide show
  1. package/dist/auth-tables.js +18 -137
  2. package/dist/column.js +1 -26
  3. package/dist/custom/audits.js +20 -54
  4. package/dist/custom/errors.js +16 -46
  5. package/dist/custom/index.js +1 -3
  6. package/dist/custom/jobs.js +13 -137
  7. package/dist/database.js +1 -181
  8. package/dist/datetime-columns.js +2 -79
  9. package/dist/ddl-constraints.js +7 -111
  10. package/dist/defaults.js +1 -48
  11. package/dist/dialect.js +1 -79
  12. package/dist/driver-config.js +1 -172
  13. package/dist/drivers/defaults/index.js +1 -1
  14. package/dist/drivers/defaults/traits.js +1 -29
  15. package/dist/drivers/dynamodb.js +1 -607
  16. package/dist/drivers/helpers.js +1 -206
  17. package/dist/drivers/index.js +1 -9
  18. package/dist/drivers/mysql.js +58 -299
  19. package/dist/drivers/postgres.js +78 -368
  20. package/dist/drivers/sqlite.js +61 -379
  21. package/dist/ensure-database.js +1 -145
  22. package/dist/fk-audit.js +3 -187
  23. package/dist/index.js +1 -64
  24. package/dist/managed-columns.js +1 -59
  25. package/dist/migration-dialect.js +4 -107
  26. package/dist/migration-ledger.js +1 -382
  27. package/dist/migration-lock.js +1 -143
  28. package/dist/migrations.js +15 -1118
  29. package/dist/model-sources.js +1 -76
  30. package/dist/notification-tables.js +4 -49
  31. package/dist/query-logger.js +2 -241
  32. package/dist/query-parser.js +1 -93
  33. package/dist/rbac-tables.js +6 -61
  34. package/dist/relation-columns.js +1 -66
  35. package/dist/replicas.js +1 -74
  36. package/dist/safe-migrations.js +2 -52
  37. package/dist/schema.js +1 -10
  38. package/dist/seeder.js +1 -457
  39. package/dist/sql-helpers.js +1 -50
  40. package/dist/table.js +1 -26
  41. package/dist/tools/setup.js +1 -6
  42. package/dist/trait-tables.js +8 -153
  43. package/dist/transaction-context.js +1 -62
  44. package/dist/types.js +1 -98
  45. package/dist/unique-audit.js +3 -155
  46. package/dist/utils.js +1 -285
  47. package/dist/uuid-columns.js +1 -68
  48. package/dist/validators.js +1 -122
  49. package/dist/vschema.js +2 -121
  50. package/package.json +20 -13
package/dist/database.js CHANGED
@@ -1,181 +1 @@
1
- import { toQueryBuilderDialect } from "./dialect";
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})}
@@ -1,85 +1,8 @@
1
- import process from "node:process";
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
- )`, tables).execute()).map((row) => ({
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}}}
@@ -1,111 +1,7 @@
1
- import { existsSync, readdirSync, readFileSync } from "node:fs";
2
- import { join } from "node:path";
3
- import { dialectCapabilities } from "./dialect";
4
- import { stripSqlNoise } from "./migration-dialect";
5
- const CONSTRUCTS = [
6
- { capability: "foreignKeys", pattern: /\bFOREIGN\s+KEY\b/i, label: "FOREIGN KEY" },
7
- { capability: "foreignKeys", pattern: /\bREFERENCES\b/i, label: "REFERENCES" },
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 = "127.0.0.1", DB_PORTS = {
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}