@zerotal/orm 1.7.2 → 1.7.3
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/CHANGELOG.md +10 -2
- package/api-surface.md +9 -1
- package/package.json +3 -3
- package/src/db/dialects/MysqlDialect.ts +7 -0
- package/src/db/dialects/PostgresDialect.ts +6 -0
- package/src/db/dialects/SqliteDialect.ts +7 -0
- package/src/db/dialects/types.ts +20 -0
- package/src/schema/Blueprint.ts +6 -3
- package/src/schema/ColumnDefinition.ts +13 -4
package/CHANGELOG.md
CHANGED
|
@@ -10,8 +10,17 @@ follows the Zerotal monorepo's unified versioning.
|
|
|
10
10
|
|
|
11
11
|
### Fixed
|
|
12
12
|
|
|
13
|
+
- **A boolean column could not hold a boolean on PostgreSQL.** `table.boolean()` compiled to
|
|
14
|
+
`INTEGER` on every engine — correct on SQLite, which has no boolean type, and rejected outright
|
|
15
|
+
by PostgreSQL: `column "…" is of type integer but expression is of type boolean` (42804) on the
|
|
16
|
+
first insert, and again on any `where(column, true)`. `DEFAULT` clauses failed the same way, a
|
|
17
|
+
boolean default having been serialised to `1`. The storage type now comes from the dialect, as
|
|
18
|
+
the auto-increment column already did. SQLite and MySQL are unchanged; existing PostgreSQL
|
|
19
|
+
tables keep their integer columns until a migration alters them. Found by the new smoke suite
|
|
20
|
+
that runs the ORM against a real PostgreSQL in CI.
|
|
21
|
+
|
|
13
22
|
- **A seeder that failed partway left its rows behind.** `Seeder.call()` has always wrapped
|
|
14
|
-
|
|
23
|
+
_composed_ seeders in a transaction, so a `DatabaseSeeder` that delegates was atomic and one that
|
|
15
24
|
does its work inline — which is most of them — was not. A failure on the fourth table committed
|
|
16
25
|
the first three, so the obvious next move, running it again, died on a unique constraint, and the
|
|
17
26
|
only way out was `migrate:fresh`. Migrations became transactional in 1.7.0; this closes the
|
|
@@ -22,7 +31,6 @@ follows the Zerotal monorepo's unified versioning.
|
|
|
22
31
|
no connection is bound, because a seeder is not obliged to touch the database and an app that has
|
|
23
32
|
not configured one should not fail to seed over a transaction it never needed.
|
|
24
33
|
|
|
25
|
-
|
|
26
34
|
### Fixed
|
|
27
35
|
|
|
28
36
|
- **`DatabaseProvider` now runs in `worker`, so `zt queue:work` can boot.** It did not, and the
|
package/api-surface.md
CHANGED
|
@@ -182,7 +182,7 @@ class Cast = {
|
|
|
182
182
|
}
|
|
183
183
|
|
|
184
184
|
class ColumnBuilder = {
|
|
185
|
-
new <Locked extends string = never>(name: string, _sqlType: string, isPrimary?: boolean, isAutoIncrement?: boolean): ColumnBuilder<Locked>
|
|
185
|
+
new <Locked extends string = never>(name: string, _sqlType: string, isPrimary?: boolean, isAutoIncrement?: boolean, _isBoolean?: boolean): ColumnBuilder<Locked>
|
|
186
186
|
after: (_column: string) => ColumnBuilder<Locked>
|
|
187
187
|
alter: () => ColumnBuilder<Locked>
|
|
188
188
|
before: (_column: string) => ColumnBuilder<Locked>
|
|
@@ -566,9 +566,11 @@ class MysqlDialect = {
|
|
|
566
566
|
advisoryLockSql: (key: number) => DialectQuery
|
|
567
567
|
advisoryUnlockSql: (key: number) => DialectQuery
|
|
568
568
|
autoIncrementColumn: (column: string) => string
|
|
569
|
+
booleanLiteral: (value: boolean) => string
|
|
569
570
|
dateExpr: (part: DatePart, column: string) => string
|
|
570
571
|
hasColumnSql: (table: string, column: string) => DialectQuery
|
|
571
572
|
hasTableSql: (table: string) => DialectQuery
|
|
573
|
+
readonly booleanType: 'INTEGER'
|
|
572
574
|
readonly name: 'mysql'
|
|
573
575
|
readonly supportsAdvisoryLocks: true
|
|
574
576
|
readonly supportsTransactionalDdl: false
|
|
@@ -605,9 +607,11 @@ class PostgresDialect = {
|
|
|
605
607
|
advisoryLockSql: (key: number) => DialectQuery
|
|
606
608
|
advisoryUnlockSql: (key: number) => DialectQuery
|
|
607
609
|
autoIncrementColumn: (column: string) => string
|
|
610
|
+
booleanLiteral: (value: boolean) => string
|
|
608
611
|
dateExpr: (part: DatePart, column: string) => string
|
|
609
612
|
hasColumnSql: (table: string, column: string) => DialectQuery
|
|
610
613
|
hasTableSql: (table: string) => DialectQuery
|
|
614
|
+
readonly booleanType: 'BOOLEAN'
|
|
611
615
|
readonly name: 'postgres'
|
|
612
616
|
readonly supportsAdvisoryLocks: true
|
|
613
617
|
readonly supportsTransactionalDdl: true
|
|
@@ -742,9 +746,11 @@ class SqliteDialect = {
|
|
|
742
746
|
advisoryLockSql: () => DialectQuery | null
|
|
743
747
|
advisoryUnlockSql: () => DialectQuery | null
|
|
744
748
|
autoIncrementColumn: (column: string) => string
|
|
749
|
+
booleanLiteral: (value: boolean) => string
|
|
745
750
|
dateExpr: (part: DatePart, column: string) => string
|
|
746
751
|
hasColumnSql: (table: string, column: string) => DialectQuery
|
|
747
752
|
hasTableSql: (table: string) => DialectQuery
|
|
753
|
+
readonly booleanType: 'INTEGER'
|
|
748
754
|
readonly name: 'sqlite'
|
|
749
755
|
readonly supportsAdvisoryLocks: false
|
|
750
756
|
readonly supportsTransactionalDdl: true
|
|
@@ -1260,9 +1266,11 @@ interface SqlDialect = {
|
|
|
1260
1266
|
advisoryLockSql: (key: number) => DialectQuery | null
|
|
1261
1267
|
advisoryUnlockSql: (key: number) => DialectQuery | null
|
|
1262
1268
|
autoIncrementColumn: (column: string) => string
|
|
1269
|
+
booleanLiteral: (value: boolean) => string
|
|
1263
1270
|
dateExpr: (part: DatePart, column: string) => string
|
|
1264
1271
|
hasColumnSql: (table: string, column: string) => DialectQuery
|
|
1265
1272
|
hasTableSql: (table: string) => DialectQuery
|
|
1273
|
+
readonly booleanType: string
|
|
1266
1274
|
readonly name: DialectName
|
|
1267
1275
|
readonly supportsAdvisoryLocks: boolean
|
|
1268
1276
|
readonly supportsTransactionalDdl: boolean
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zerotal/orm",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.3",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"maturity": "stable",
|
|
6
6
|
"private": false,
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
"typecheck": "tsc --noEmit"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@zerotal/core": "1.7.
|
|
35
|
-
"@zerotal/validator": "1.7.
|
|
34
|
+
"@zerotal/core": "1.7.3",
|
|
35
|
+
"@zerotal/validator": "1.7.3"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"typescript": "^5.8.0"
|
|
@@ -51,6 +51,13 @@ export class MysqlDialect implements SqlDialect {
|
|
|
51
51
|
return `${column} INT AUTO_INCREMENT PRIMARY KEY`;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
// MySQL BOOLEAN is a synonym for TINYINT(1) and INTEGER accepts 0/1 all the same.
|
|
55
|
+
readonly booleanType = "INTEGER";
|
|
56
|
+
|
|
57
|
+
booleanLiteral(value: boolean): string {
|
|
58
|
+
return value ? "1" : "0";
|
|
59
|
+
}
|
|
60
|
+
|
|
54
61
|
advisoryLockSql(key: number): DialectQuery {
|
|
55
62
|
return { sql: `SELECT GET_LOCK(?, -1)`, params: [`zerotal_lock_${key}`] };
|
|
56
63
|
}
|
|
@@ -48,6 +48,12 @@ export class PostgresDialect implements SqlDialect {
|
|
|
48
48
|
return `${column} INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY`;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
readonly booleanType = "BOOLEAN";
|
|
52
|
+
|
|
53
|
+
booleanLiteral(value: boolean): string {
|
|
54
|
+
return value ? "TRUE" : "FALSE";
|
|
55
|
+
}
|
|
56
|
+
|
|
51
57
|
advisoryLockSql(key: number): DialectQuery {
|
|
52
58
|
return { sql: `SELECT pg_advisory_lock(?)`, params: [key] };
|
|
53
59
|
}
|
|
@@ -48,6 +48,13 @@ export class SqliteDialect implements SqlDialect {
|
|
|
48
48
|
return `${column} INTEGER PRIMARY KEY AUTOINCREMENT`;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
// SQLite has no boolean type — 0/1 in an INTEGER is the storage class it uses.
|
|
52
|
+
readonly booleanType = "INTEGER";
|
|
53
|
+
|
|
54
|
+
booleanLiteral(value: boolean): string {
|
|
55
|
+
return value ? "1" : "0";
|
|
56
|
+
}
|
|
57
|
+
|
|
51
58
|
advisoryLockSql(): DialectQuery | null {
|
|
52
59
|
return null;
|
|
53
60
|
}
|
package/src/db/dialects/types.ts
CHANGED
|
@@ -56,6 +56,26 @@ export interface SqlDialect {
|
|
|
56
56
|
*/
|
|
57
57
|
autoIncrementColumn(column: string): string;
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* The column type a portable `table.boolean()` compiles to.
|
|
61
|
+
*
|
|
62
|
+
* SQLite has no boolean type and stores 0/1 in an `INTEGER`, which is why the
|
|
63
|
+
* Blueprint emitted `INTEGER` for every engine. PostgreSQL has a real `boolean`
|
|
64
|
+
* and refuses to compare or assign one against an integer column, so a table
|
|
65
|
+
* built that way rejected its own booleans — `column "active" is of type integer
|
|
66
|
+
* but expression is of type boolean` (SQLSTATE 42804) on the first insert, and
|
|
67
|
+
* again on any `where("active", true)`. Nothing caught it because no test
|
|
68
|
+
* executed the DDL against a server; `postgres.smoke.test.ts` now does.
|
|
69
|
+
*
|
|
70
|
+
* MySQL keeps `INTEGER`: its `BOOLEAN` is a synonym for `TINYINT(1)` and it
|
|
71
|
+
* accepts 0/1 either way, so there is no defect there to fix and no reason to
|
|
72
|
+
* churn the DDL of an engine no CI job covers.
|
|
73
|
+
*/
|
|
74
|
+
readonly booleanType: string;
|
|
75
|
+
|
|
76
|
+
/** A boolean as this engine spells it in a `DEFAULT` clause. */
|
|
77
|
+
booleanLiteral(value: boolean): string;
|
|
78
|
+
|
|
59
79
|
/** Whether the engine supports application-level advisory locks. */
|
|
60
80
|
readonly supportsAdvisoryLocks: boolean;
|
|
61
81
|
|
package/src/schema/Blueprint.ts
CHANGED
|
@@ -314,12 +314,15 @@ export class Blueprint {
|
|
|
314
314
|
// ── Boolean ───────────────────────────────────────────────────────────────
|
|
315
315
|
|
|
316
316
|
/**
|
|
317
|
-
* Boolean column.
|
|
318
|
-
*
|
|
317
|
+
* Boolean column. The storage type is the engine's: `INTEGER` holding 0 / 1 on
|
|
318
|
+
* SQLite and MySQL, a real `BOOLEAN` on PostgreSQL — which rejects the integer
|
|
319
|
+
* form for both assignment and comparison, so emitting `INTEGER` everywhere
|
|
320
|
+
* built a column that would not take its own booleans. JS booleans passed to
|
|
321
|
+
* {@link ColumnBuilder.default} follow the same engine's spelling.
|
|
319
322
|
* @category Column types
|
|
320
323
|
*/
|
|
321
324
|
boolean(name: string): ColumnBuilder {
|
|
322
|
-
return this._add(new ColumnBuilder(name, "INTEGER"));
|
|
325
|
+
return this._add(new ColumnBuilder(name, "INTEGER", false, false, true));
|
|
323
326
|
}
|
|
324
327
|
|
|
325
328
|
// ── Date / time columns ───────────────────────────────────────────────────
|
|
@@ -64,6 +64,11 @@ export class ColumnBuilder<Locked extends string = never> {
|
|
|
64
64
|
private _sqlType: string,
|
|
65
65
|
isPrimary = false,
|
|
66
66
|
isAutoIncrement = false,
|
|
67
|
+
/**
|
|
68
|
+
* Logically a boolean, whatever `_sqlType` says. The engine decides the
|
|
69
|
+
* storage type at compile time — see {@link SqlDialect.booleanType}.
|
|
70
|
+
*/
|
|
71
|
+
private _isBoolean = false,
|
|
67
72
|
) {
|
|
68
73
|
this._isPrimary = isPrimary;
|
|
69
74
|
this._isAutoIncr = isAutoIncrement;
|
|
@@ -324,13 +329,16 @@ export class ColumnBuilder<Locked extends string = never> {
|
|
|
324
329
|
// PostgreSQL a syntax error and against MySQL a 1064.
|
|
325
330
|
if (this._isAutoIncr) return getDialect(dialect).autoIncrementColumn(this.name);
|
|
326
331
|
|
|
327
|
-
|
|
332
|
+
// A boolean's storage type is the engine's to choose: SQLite stores 0/1 in an
|
|
333
|
+
// INTEGER, PostgreSQL has a real one and rejects the integer form outright.
|
|
334
|
+
const sqlType = this._isBoolean ? getDialect(dialect).booleanType : this._sqlType;
|
|
335
|
+
const parts: string[] = [`${this.name} ${sqlType}`];
|
|
328
336
|
|
|
329
337
|
if (this._isPrimary) parts.push("PRIMARY KEY");
|
|
330
338
|
if (!this._isNullable && !this._isPrimary) parts.push("NOT NULL");
|
|
331
339
|
|
|
332
340
|
if (this._useCurrent) parts.push("DEFAULT CURRENT_TIMESTAMP");
|
|
333
|
-
else if (this._hasDefault) parts.push(`DEFAULT ${this._serializeDefault()}`);
|
|
341
|
+
else if (this._hasDefault) parts.push(`DEFAULT ${this._serializeDefault(dialect)}`);
|
|
334
342
|
|
|
335
343
|
if (this._isUnique) parts.push("UNIQUE");
|
|
336
344
|
if (this._check) parts.push(`CHECK (${this._check})`);
|
|
@@ -340,10 +348,11 @@ export class ColumnBuilder<Locked extends string = never> {
|
|
|
340
348
|
return parts.join(" ");
|
|
341
349
|
}
|
|
342
350
|
|
|
343
|
-
private _serializeDefault(): string {
|
|
351
|
+
private _serializeDefault(dialect: DialectName = "sqlite"): string {
|
|
344
352
|
const v = this._default;
|
|
345
353
|
if (v === null) return "NULL";
|
|
346
|
-
|
|
354
|
+
// `DEFAULT 1` on a PostgreSQL boolean column is the same 42804 the value itself hit.
|
|
355
|
+
if (typeof v === "boolean") return getDialect(dialect).booleanLiteral(v);
|
|
347
356
|
if (typeof v === "string") return `'${v.replace(/'/g, "''")}'`;
|
|
348
357
|
return String(v);
|
|
349
358
|
}
|