@stacksjs/database 0.70.256 → 0.70.258
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 +12 -12
- package/dist/database.js +2 -1
- package/dist/datetime-columns.d.ts +36 -0
- package/dist/datetime-columns.js +85 -0
- package/dist/ddl-constraints.d.ts +31 -0
- package/dist/ddl-constraints.js +111 -0
- package/dist/dialect.d.ts +82 -0
- package/dist/dialect.js +79 -0
- package/dist/driver-config.d.ts +55 -5
- package/dist/driver-config.js +28 -0
- package/dist/drivers/defaults/index.d.ts +0 -1
- package/dist/drivers/defaults/index.js +0 -1
- package/dist/drivers/defaults/traits.d.ts +17 -31
- package/dist/drivers/defaults/traits.js +18 -1142
- package/dist/drivers/mysql.d.ts +0 -1
- package/dist/drivers/mysql.js +1 -14
- package/dist/drivers/postgres.d.ts +0 -1
- package/dist/drivers/postgres.js +2 -35
- package/dist/index.d.ts +20 -0
- package/dist/index.js +6 -0
- package/dist/notification-tables.js +6 -6
- package/dist/query-logger.js +2 -1
- package/dist/rbac-tables.d.ts +3 -3
- package/dist/rbac-tables.js +16 -13
- package/dist/replicas.d.ts +52 -0
- package/dist/replicas.js +74 -0
- package/dist/schema.d.ts +1 -0
- package/dist/sql-helpers.d.ts +60 -7
- package/dist/sql-helpers.js +31 -5
- package/dist/trait-tables.d.ts +126 -0
- package/dist/trait-tables.js +206 -0
- package/dist/utils.d.ts +33 -0
- package/dist/utils.js +102 -13
- package/dist/vschema.d.ts +84 -0
- package/dist/vschema.js +121 -0
- package/package.json +11 -11
- package/dist/drivers/defaults/passwords.d.ts +0 -4
- package/dist/drivers/defaults/passwords.js +0 -106
package/dist/auth-tables.js
CHANGED
|
@@ -45,7 +45,7 @@ export async function ensureUsersAuthColumns(sql, options = {}) {
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
export async function migrateAuthTables(options = {}) {
|
|
48
|
-
const dbDriver = getDbDriver(), sql = sqlHelpers(dbDriver), { isPostgres, boolTrue, now, pkColumn, nullableTimestamp } = sql;
|
|
48
|
+
const dbDriver = getDbDriver(), sql = sqlHelpers(dbDriver), { isPostgres, boolTrue, now, pkColumn, nullableTimestamp, datetime } = sql;
|
|
49
49
|
if (options.verbose)
|
|
50
50
|
log.info(`Creating auth tables for ${dbDriver}...`);
|
|
51
51
|
try {
|
|
@@ -61,7 +61,7 @@ export async function migrateAuthTables(options = {}) {
|
|
|
61
61
|
personal_access_client BOOLEAN NOT NULL DEFAULT ${sql.boolFalse},
|
|
62
62
|
password_client BOOLEAN NOT NULL DEFAULT ${sql.boolFalse},
|
|
63
63
|
revoked BOOLEAN NOT NULL DEFAULT ${sql.boolFalse},
|
|
64
|
-
created_at
|
|
64
|
+
created_at ${datetime} DEFAULT CURRENT_TIMESTAMP,
|
|
65
65
|
updated_at ${nullableTimestamp}
|
|
66
66
|
)
|
|
67
67
|
`).execute();
|
|
@@ -77,7 +77,7 @@ export async function migrateAuthTables(options = {}) {
|
|
|
77
77
|
scopes TEXT,
|
|
78
78
|
revoked BOOLEAN NOT NULL DEFAULT ${sql.boolFalse},
|
|
79
79
|
expires_at ${nullableTimestamp},
|
|
80
|
-
created_at
|
|
80
|
+
created_at ${datetime} DEFAULT CURRENT_TIMESTAMP,
|
|
81
81
|
updated_at ${nullableTimestamp}
|
|
82
82
|
)
|
|
83
83
|
`).execute();
|
|
@@ -91,7 +91,7 @@ export async function migrateAuthTables(options = {}) {
|
|
|
91
91
|
token TEXT NOT NULL,
|
|
92
92
|
revoked BOOLEAN NOT NULL DEFAULT ${sql.boolFalse},
|
|
93
93
|
expires_at ${nullableTimestamp},
|
|
94
|
-
created_at
|
|
94
|
+
created_at ${datetime} DEFAULT CURRENT_TIMESTAMP
|
|
95
95
|
)
|
|
96
96
|
`).execute();
|
|
97
97
|
await createTokenIndex("idx_oauth_refresh_tokens_token", "oauth_refresh_tokens", "token");
|
|
@@ -102,7 +102,7 @@ export async function migrateAuthTables(options = {}) {
|
|
|
102
102
|
${pkColumn},
|
|
103
103
|
email VARCHAR(255) NOT NULL,
|
|
104
104
|
token VARCHAR(255) NOT NULL,
|
|
105
|
-
created_at
|
|
105
|
+
created_at ${datetime} DEFAULT CURRENT_TIMESTAMP
|
|
106
106
|
)
|
|
107
107
|
`).execute();
|
|
108
108
|
try {
|
|
@@ -124,7 +124,7 @@ export async function migrateAuthTables(options = {}) {
|
|
|
124
124
|
backup_eligible BOOLEAN NOT NULL DEFAULT ${sql.boolFalse},
|
|
125
125
|
backup_status BOOLEAN NOT NULL DEFAULT ${sql.boolFalse},
|
|
126
126
|
transports TEXT,
|
|
127
|
-
created_at
|
|
127
|
+
created_at ${datetime} DEFAULT CURRENT_TIMESTAMP,
|
|
128
128
|
last_used_at ${nullableTimestamp}
|
|
129
129
|
)
|
|
130
130
|
`).execute();
|
|
@@ -141,8 +141,8 @@ export async function migrateAuthTables(options = {}) {
|
|
|
141
141
|
user_id INTEGER NOT NULL,
|
|
142
142
|
challenge TEXT NOT NULL,
|
|
143
143
|
purpose VARCHAR(20) NOT NULL,
|
|
144
|
-
expires_at
|
|
145
|
-
created_at
|
|
144
|
+
expires_at ${datetime} NOT NULL,
|
|
145
|
+
created_at ${datetime} DEFAULT CURRENT_TIMESTAMP
|
|
146
146
|
)
|
|
147
147
|
`).execute();
|
|
148
148
|
try {
|
|
@@ -156,8 +156,8 @@ export async function migrateAuthTables(options = {}) {
|
|
|
156
156
|
CREATE TABLE IF NOT EXISTS two_factor_challenges (
|
|
157
157
|
id VARCHAR(255) PRIMARY KEY,
|
|
158
158
|
user_id INTEGER NOT NULL,
|
|
159
|
-
expires_at
|
|
160
|
-
created_at
|
|
159
|
+
expires_at ${datetime} NOT NULL,
|
|
160
|
+
created_at ${datetime} DEFAULT CURRENT_TIMESTAMP
|
|
161
161
|
)
|
|
162
162
|
`).execute();
|
|
163
163
|
try {
|
|
@@ -171,8 +171,8 @@ export async function migrateAuthTables(options = {}) {
|
|
|
171
171
|
CREATE TABLE IF NOT EXISTS two_factor_pending_secrets (
|
|
172
172
|
user_id INTEGER PRIMARY KEY,
|
|
173
173
|
secret VARCHAR(255) NOT NULL,
|
|
174
|
-
expires_at
|
|
175
|
-
created_at
|
|
174
|
+
expires_at ${datetime} NOT NULL,
|
|
175
|
+
created_at ${datetime} DEFAULT CURRENT_TIMESTAMP
|
|
176
176
|
)
|
|
177
177
|
`).execute();
|
|
178
178
|
if (options.verbose)
|
package/dist/database.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { toQueryBuilderDialect } from "./dialect";
|
|
1
2
|
import { QB_SNAPSHOT_DIR } from "./utils";
|
|
2
3
|
import { createQueryBuilder, setConfig } from "@stacksjs/query-builder";
|
|
3
4
|
import { env as stacksEnv } from "@stacksjs/env";
|
|
@@ -41,7 +42,7 @@ export class Database {
|
|
|
41
42
|
return;
|
|
42
43
|
setConfig({
|
|
43
44
|
snapshotDir: QB_SNAPSHOT_DIR,
|
|
44
|
-
dialect: this._options.driver,
|
|
45
|
+
dialect: toQueryBuilderDialect(this._options.driver),
|
|
45
46
|
database: this._options.connection,
|
|
46
47
|
verbose: this._options.verbose,
|
|
47
48
|
timestamps: this._options.timestamps,
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The framework-owned tables whose datetime columns this guarantee covers.
|
|
3
|
+
*
|
|
4
|
+
* Model-backed tables are deliberately absent: their columns come from the
|
|
5
|
+
* migration generator, which is a separate path with its own column types.
|
|
6
|
+
*/
|
|
7
|
+
export declare function frameworkDatetimeTables(): string[];
|
|
8
|
+
/**
|
|
9
|
+
* Every `TIMESTAMP` column still present on a framework table.
|
|
10
|
+
*
|
|
11
|
+
* Reads `information_schema` rather than `SHOW COLUMNS` so the query is a
|
|
12
|
+
* single round-trip and can be filtered to the current schema.
|
|
13
|
+
*/
|
|
14
|
+
export declare function findTimestampColumns(): Promise<TimestampColumn[]>;
|
|
15
|
+
/**
|
|
16
|
+
* The `MODIFY` that converts one column, preserving its nullability, default
|
|
17
|
+
* and any `ON UPDATE CURRENT_TIMESTAMP` clause.
|
|
18
|
+
*
|
|
19
|
+
* Identifiers come from `information_schema` and are re-validated here before
|
|
20
|
+
* being spliced into DDL, which cannot take a placeholder.
|
|
21
|
+
*/
|
|
22
|
+
export declare function modifyToDatetimeSql(column: TimestampColumn): string;
|
|
23
|
+
/**
|
|
24
|
+
* Convert every remaining `TIMESTAMP` column on a framework table to
|
|
25
|
+
* `DATETIME`. MySQL-only and idempotent — once converted the
|
|
26
|
+
* `information_schema` query returns nothing and this is a single cheap
|
|
27
|
+
* SELECT.
|
|
28
|
+
*/
|
|
29
|
+
export declare function ensureUtcDatetimeColumns(options?: { verbose?: boolean }): Promise<{ success: boolean, converted: number, error?: string }>;
|
|
30
|
+
declare interface TimestampColumn {
|
|
31
|
+
table: string
|
|
32
|
+
column: string
|
|
33
|
+
nullable: boolean
|
|
34
|
+
columnDefault: string | null
|
|
35
|
+
extra: string
|
|
36
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
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
|
|
34
|
+
FROM information_schema.COLUMNS
|
|
35
|
+
WHERE TABLE_SCHEMA = DATABASE()
|
|
36
|
+
AND TABLE_NAME IN (${placeholders})
|
|
37
|
+
AND (
|
|
38
|
+
DATA_TYPE = 'timestamp'
|
|
39
|
+
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
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** Find capability violations in one file's SQL. */
|
|
2
|
+
export declare function auditDdlSql(sql: string, file: string, dialect: string): DdlViolation[];
|
|
3
|
+
/** Audit a whole migration directory against the target dialect's capabilities. */
|
|
4
|
+
export declare function auditDdlConstraints(options: {
|
|
5
|
+
dir: string
|
|
6
|
+
dialect: string
|
|
7
|
+
}): DdlConstraintAudit;
|
|
8
|
+
/**
|
|
9
|
+
* The user-facing explanation.
|
|
10
|
+
*
|
|
11
|
+
* Grouped by capability rather than by file: twenty foreign keys across
|
|
12
|
+
* eighteen files are one decision to make, not twenty, and listing them
|
|
13
|
+
* per-file buries that.
|
|
14
|
+
*/
|
|
15
|
+
export declare function formatDdlConstraintError(audit: DdlConstraintAudit, dialect: string, dir: string): string;
|
|
16
|
+
/** Environment escape hatch, matching the dialect auditor's. */
|
|
17
|
+
export declare const DDL_CONSTRAINT_OVERRIDE_ENV: 'STACKS_ALLOW_DDL_CONSTRAINT_VIOLATIONS';
|
|
18
|
+
export declare interface DdlViolation {
|
|
19
|
+
capability: DdlCapability
|
|
20
|
+
construct: string
|
|
21
|
+
file: string
|
|
22
|
+
line: number
|
|
23
|
+
snippet: string
|
|
24
|
+
}
|
|
25
|
+
export declare interface DdlConstraintAudit {
|
|
26
|
+
total: number
|
|
27
|
+
violations: DdlViolation[]
|
|
28
|
+
empty: boolean
|
|
29
|
+
}
|
|
30
|
+
/** The capability a construct requires. */
|
|
31
|
+
export type DdlCapability = 'foreignKeys' | 'autoIncrement' | 'createIndexIfNotExists';
|
|
@@ -0,0 +1,111 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Look up a dialect's capabilities.
|
|
3
|
+
*
|
|
4
|
+
* Unknown dialects resolve to SQLite. That mirrors the fallback every call
|
|
5
|
+
* site already had (`isSqlite = !isPostgres && !isMysql`) and keeps an
|
|
6
|
+
* unrecognized `DB_CONNECTION` value behaving as it did before this module
|
|
7
|
+
* existed, rather than throwing during module load in an app that is merely
|
|
8
|
+
* misconfigured.
|
|
9
|
+
*/
|
|
10
|
+
export declare function dialectCapabilities(dialect: string): DialectCapabilities;
|
|
11
|
+
/** Whether the framework has an explicit capability row for this dialect. */
|
|
12
|
+
export declare function isKnownDialect(dialect: string): boolean;
|
|
13
|
+
/** Every dialect with a capability row, for CLI help and validation messages. */
|
|
14
|
+
export declare function knownDialects(): string[];
|
|
15
|
+
/**
|
|
16
|
+
* Whether a dialect speaks the MySQL wire protocol.
|
|
17
|
+
*
|
|
18
|
+
* This is the check that governs SQL *rendering* — placeholders, quoting,
|
|
19
|
+
* `NOW()` vs `datetime('now')`. It deliberately says nothing about DDL
|
|
20
|
+
* feature support; ask `dialectCapabilities()` for that. Conflating the two
|
|
21
|
+
* is what let SingleStore's lack of foreign keys hide behind a `true`.
|
|
22
|
+
*/
|
|
23
|
+
export declare function isMysqlWire(dialect: string): boolean;
|
|
24
|
+
/** Whether a dialect speaks the PostgreSQL wire protocol. */
|
|
25
|
+
export declare function isPostgresWire(dialect: string): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Collapse a Stacks dialect to the one bun-query-builder renders for.
|
|
28
|
+
*
|
|
29
|
+
* Stacks tracks dialects that the query builder has no separate renderer
|
|
30
|
+
* for, because they differ only in DDL. Passing such a name straight
|
|
31
|
+
* through would make the query builder fall back to its own default and
|
|
32
|
+
* render the wrong SQL, so every path into `setConfig` goes through here.
|
|
33
|
+
*/
|
|
34
|
+
export declare function toQueryBuilderDialect(dialect: string): QueryBuilderDialect;
|
|
35
|
+
export declare interface DialectCapabilities {
|
|
36
|
+
dialect: string
|
|
37
|
+
wire: SqlWireProtocol
|
|
38
|
+
queryBuilderDialect: QueryBuilderDialect
|
|
39
|
+
defaultPort?: number
|
|
40
|
+
identifierQuote: '`' | '"'
|
|
41
|
+
supportsForeignKeys: boolean
|
|
42
|
+
supportsAutoIncrement: boolean
|
|
43
|
+
supportsAtomicMultiTableTransactions: boolean
|
|
44
|
+
requiresOnlineDdl: boolean
|
|
45
|
+
supportsCreateIndexIfNotExists: boolean
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Dialect capability table.
|
|
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
|
+
*/
|
|
71
|
+
/**
|
|
72
|
+
* The connection protocol a dialect speaks. Determines placeholder style,
|
|
73
|
+
* identifier quoting, and which connection URL scheme dials it.
|
|
74
|
+
*/
|
|
75
|
+
export type SqlWireProtocol = 'mysql' | 'postgres' | 'sqlite';
|
|
76
|
+
/**
|
|
77
|
+
* The dialect string handed to bun-query-builder. Several Stacks dialects
|
|
78
|
+
* collapse onto one of these — the query builder only needs to know how to
|
|
79
|
+
* render SQL, and a dialect that is wire- and DML-identical to MySQL wants
|
|
80
|
+
* MySQL's renderer even when Stacks tracks it separately for DDL purposes.
|
|
81
|
+
*/
|
|
82
|
+
export type QueryBuilderDialect = 'sqlite' | 'mysql' | 'singlestore' | 'vitess' | 'postgres';
|
package/dist/dialect.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
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
|
+
}
|
package/dist/driver-config.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { NetworkedConnectionOptions, PoolOptions, ReadPolicyOptions, ReplicaOptions } from '@stacksjs/types';
|
|
1
2
|
import type { StacksDialect } from '@stacksjs/query-builder';
|
|
2
3
|
/**
|
|
3
4
|
* Get the connection string for a given driver and configuration
|
|
@@ -45,6 +46,15 @@ export declare function detectDriver(): StacksDialect;
|
|
|
45
46
|
* charset: 'utf8mb4',
|
|
46
47
|
* ssl: false
|
|
47
48
|
* },
|
|
49
|
+
* vitess: {
|
|
50
|
+
* name: 'stacks',
|
|
51
|
+
* host: '127.0.0.1',
|
|
52
|
+
* port: 15306,
|
|
53
|
+
* username: 'root',
|
|
54
|
+
* password: '',
|
|
55
|
+
* prefix: '',
|
|
56
|
+
* ssl: false
|
|
57
|
+
* },
|
|
48
58
|
* postgres: {
|
|
49
59
|
* name: 'stacks',
|
|
50
60
|
* host: '127.0.0.1',
|
|
@@ -57,7 +67,7 @@ export declare function detectDriver(): StacksDialect;
|
|
|
57
67
|
* }
|
|
58
68
|
* ```
|
|
59
69
|
*/
|
|
60
|
-
export declare const driverDefaults: Record<StacksDialect, Partial<SqliteConfig | MysqlConfig | SinglestoreConfig | PostgresConfig | DynamoDbConfig>>;
|
|
70
|
+
export declare const driverDefaults: Record<StacksDialect, Partial<SqliteConfig | MysqlConfig | SinglestoreConfig | VitessConfig | PostgresConfig | DynamoDbConfig>>;
|
|
61
71
|
/**
|
|
62
72
|
* SQLite specific configuration
|
|
63
73
|
*/
|
|
@@ -66,9 +76,13 @@ export declare interface SqliteConfig {
|
|
|
66
76
|
prefix?: string
|
|
67
77
|
}
|
|
68
78
|
/**
|
|
69
|
-
* MySQL specific configuration
|
|
79
|
+
* MySQL specific configuration.
|
|
80
|
+
*
|
|
81
|
+
* Extends `NetworkedConnectionConfig` for `pool` and `replicas` — both are
|
|
82
|
+
* meaningless for the embedded SQLite dialect, which is why they live on a
|
|
83
|
+
* base the client-server shapes extend rather than on every connection.
|
|
70
84
|
*/
|
|
71
|
-
export declare interface MysqlConfig {
|
|
85
|
+
export declare interface MysqlConfig extends NetworkedConnectionConfig {
|
|
72
86
|
name: string
|
|
73
87
|
host?: string
|
|
74
88
|
port?: number
|
|
@@ -86,7 +100,7 @@ export declare interface MysqlConfig {
|
|
|
86
100
|
* SHARD KEY / SORT KEY, no foreign keys) — handled by the migration generator,
|
|
87
101
|
* not by the connection layer.
|
|
88
102
|
*/
|
|
89
|
-
export declare interface SinglestoreConfig {
|
|
103
|
+
export declare interface SinglestoreConfig extends NetworkedConnectionConfig {
|
|
90
104
|
name: string
|
|
91
105
|
host?: string
|
|
92
106
|
port?: number
|
|
@@ -96,10 +110,33 @@ export declare interface SinglestoreConfig {
|
|
|
96
110
|
charset?: string
|
|
97
111
|
ssl?: boolean
|
|
98
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* Vitess specific configuration.
|
|
115
|
+
*
|
|
116
|
+
* Vitess is a sharding layer in front of MySQL. The application connects to
|
|
117
|
+
* vtgate, which speaks the MySQL wire protocol, so this shares MySQL's
|
|
118
|
+
* connection shape. Two things differ and both matter:
|
|
119
|
+
*
|
|
120
|
+
* - the default port is vtgate's 15306, not mysqld's 3306. Connecting to
|
|
121
|
+
* 3306 on a Vitess cluster reaches an individual tablet's underlying
|
|
122
|
+
* MySQL and silently bypasses sharding altogether.
|
|
123
|
+
* - `name` is a KEYSPACE, not a database. It is the unit Vitess shards,
|
|
124
|
+
* and it is what the VSchema is written against.
|
|
125
|
+
*/
|
|
126
|
+
export declare interface VitessConfig extends NetworkedConnectionConfig {
|
|
127
|
+
name: string
|
|
128
|
+
host?: string
|
|
129
|
+
port?: number
|
|
130
|
+
username?: string
|
|
131
|
+
password?: string
|
|
132
|
+
prefix?: string
|
|
133
|
+
ssl?: boolean
|
|
134
|
+
tabletType?: 'primary' | 'replica' | 'rdonly'
|
|
135
|
+
}
|
|
99
136
|
/**
|
|
100
137
|
* PostgreSQL specific configuration
|
|
101
138
|
*/
|
|
102
|
-
export declare interface PostgresConfig {
|
|
139
|
+
export declare interface PostgresConfig extends NetworkedConnectionConfig {
|
|
103
140
|
name: string
|
|
104
141
|
host?: string
|
|
105
142
|
port?: number
|
|
@@ -135,6 +172,7 @@ export declare interface DatabaseConnections {
|
|
|
135
172
|
sqlite?: SqliteConfig
|
|
136
173
|
mysql?: MysqlConfig
|
|
137
174
|
singlestore?: SinglestoreConfig
|
|
175
|
+
vitess?: VitessConfig
|
|
138
176
|
postgres?: PostgresConfig
|
|
139
177
|
dynamodb?: DynamoDbConfig
|
|
140
178
|
}
|
|
@@ -146,4 +184,16 @@ export declare interface FullDatabaseConfig {
|
|
|
146
184
|
connections: DatabaseConnections
|
|
147
185
|
migrations?: string
|
|
148
186
|
migrationLocks?: string
|
|
187
|
+
reads?: ReadPolicyConfig
|
|
149
188
|
}
|
|
189
|
+
/**
|
|
190
|
+
* Connection pool, replica, and read-policy shapes.
|
|
191
|
+
*
|
|
192
|
+
* Defined in `@stacksjs/types` (the package `config/database.ts` is typed
|
|
193
|
+
* against) and aliased here so the database package has one name for them
|
|
194
|
+
* and users never see two subtly different definitions of the same block.
|
|
195
|
+
*/
|
|
196
|
+
export type PoolConfig = PoolOptions;
|
|
197
|
+
export type ReplicaConfig = ReplicaOptions;
|
|
198
|
+
export type ReadPolicyConfig = ReadPolicyOptions;
|
|
199
|
+
export type NetworkedConnectionConfig = NetworkedConnectionOptions;
|