@zerotal/orm 1.7.3 → 1.7.4
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 +31 -2
- package/api-surface.md +9 -5
- package/package.json +3 -3
- package/src/db/dialects/MysqlDialect.ts +6 -0
- package/src/db/dialects/PostgresDialect.ts +5 -0
- package/src/db/dialects/SqliteDialect.ts +5 -0
- package/src/db/dialects/types.ts +16 -0
- package/src/schema/Blueprint.ts +7 -5
- package/src/schema/ColumnDefinition.ts +14 -3
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,29 @@ follows the Zerotal monorepo's unified versioning.
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [1.7.4] — 2026-08-21
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- **A string column could not carry an index on MySQL.** `table.string()` compiled to `TEXT`
|
|
16
|
+
on every engine and discarded its `length` argument — the parameter existed and was
|
|
17
|
+
documented as "accepted for multi-DB compatibility", wired to nothing. MySQL refuses to key
|
|
18
|
+
a TEXT column without a prefix length, so `table.string("email").unique()` failed at
|
|
19
|
+
`CREATE TABLE`:
|
|
20
|
+
|
|
21
|
+
```text
|
|
22
|
+
BLOB/TEXT column 'email' used in key specification without a key length
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Any natural key — an email, a slug — was unusable on MySQL, and `index()` the same. The
|
|
26
|
+
storage type now comes from the dialect, beside `booleanType` and `autoIncrementColumn`:
|
|
27
|
+
MySQL gets `VARCHAR(length)`, while SQLite and PostgreSQL keep `TEXT`, which PostgreSQL
|
|
28
|
+
indexes happily. `char()` had the identical bug and the identical fix.
|
|
29
|
+
|
|
30
|
+
Found by the new MySQL smoke suite on its first run against a real server.
|
|
31
|
+
|
|
32
|
+
## [1.7.3] — 2026-08-20
|
|
33
|
+
|
|
11
34
|
### Fixed
|
|
12
35
|
|
|
13
36
|
- **A boolean column could not hold a boolean on PostgreSQL.** `table.boolean()` compiled to
|
|
@@ -19,6 +42,10 @@ follows the Zerotal monorepo's unified versioning.
|
|
|
19
42
|
tables keep their integer columns until a migration alters them. Found by the new smoke suite
|
|
20
43
|
that runs the ORM against a real PostgreSQL in CI.
|
|
21
44
|
|
|
45
|
+
## [1.7.2] — 2026-08-18
|
|
46
|
+
|
|
47
|
+
### Fixed
|
|
48
|
+
|
|
22
49
|
- **A seeder that failed partway left its rows behind.** `Seeder.call()` has always wrapped
|
|
23
50
|
_composed_ seeders in a transaction, so a `DatabaseSeeder` that delegates was atomic and one that
|
|
24
51
|
does its work inline — which is most of them — was not. A failure on the fourth table committed
|
|
@@ -31,8 +58,6 @@ follows the Zerotal monorepo's unified versioning.
|
|
|
31
58
|
no connection is bound, because a seeder is not obliged to touch the database and an app that has
|
|
32
59
|
not configured one should not fail to seed over a transaction it never needed.
|
|
33
60
|
|
|
34
|
-
### Fixed
|
|
35
|
-
|
|
36
61
|
- **`DatabaseProvider` now runs in `worker`, so `zt queue:work` can boot.** It did not, and the
|
|
37
62
|
consequence was total rather than partial: `QueueProvider` _does_ run in `worker`, the
|
|
38
63
|
queue's own default driver is `sqlite`, and so the worker asked for a connection this
|
|
@@ -51,6 +76,10 @@ follows the Zerotal monorepo's unified versioning.
|
|
|
51
76
|
|
|
52
77
|
Found building the first cookbook app, whose first queued job could not run.
|
|
53
78
|
|
|
79
|
+
## [1.7.1] — 2026-08-16
|
|
80
|
+
|
|
81
|
+
### Fixed
|
|
82
|
+
|
|
54
83
|
- **Relation keys now accept the JS spelling, like every other identifier.** The convention is
|
|
55
84
|
camelCase in the application and snake_case in the database, converted on the way through —
|
|
56
85
|
and relation keys were the one place it did not happen. `@hasMany(() => Issue, { foreignKey:
|
package/api-surface.md
CHANGED
|
@@ -116,7 +116,7 @@ class Blueprint = {
|
|
|
116
116
|
bigInteger: (name: string) => ColumnBuilder
|
|
117
117
|
binary: (name: string) => ColumnBuilder
|
|
118
118
|
boolean: (name: string) => ColumnBuilder
|
|
119
|
-
char: (name: string,
|
|
119
|
+
char: (name: string, length?: number) => ColumnBuilder
|
|
120
120
|
date: (name: string) => ColumnBuilder
|
|
121
121
|
dateTime: (name: string) => ColumnBuilder
|
|
122
122
|
datetime: (name: string) => ColumnBuilder
|
|
@@ -153,7 +153,7 @@ class Blueprint = {
|
|
|
153
153
|
smallInteger: (name: string) => ColumnBuilder
|
|
154
154
|
softDeletes: (column?: string) => void
|
|
155
155
|
spatialIndex: (columns: string | string[], name?: string) => Blueprint
|
|
156
|
-
string: (name: string,
|
|
156
|
+
string: (name: string, length?: number) => ColumnBuilder
|
|
157
157
|
text: (name: string) => ColumnBuilder
|
|
158
158
|
time: (name: string) => ColumnBuilder
|
|
159
159
|
timestamp: (name: string) => ColumnBuilder
|
|
@@ -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, _isBoolean?: boolean): ColumnBuilder<Locked>
|
|
185
|
+
new <Locked extends string = never>(name: string, _sqlType: string, isPrimary?: boolean, isAutoIncrement?: boolean, _isBoolean?: boolean, _stringLength?: number | undefined): ColumnBuilder<Locked>
|
|
186
186
|
after: (_column: string) => ColumnBuilder<Locked>
|
|
187
187
|
alter: () => ColumnBuilder<Locked>
|
|
188
188
|
before: (_column: string) => ColumnBuilder<Locked>
|
|
@@ -574,6 +574,7 @@ class MysqlDialect = {
|
|
|
574
574
|
readonly name: 'mysql'
|
|
575
575
|
readonly supportsAdvisoryLocks: true
|
|
576
576
|
readonly supportsTransactionalDdl: false
|
|
577
|
+
stringType: (length: number) => string
|
|
577
578
|
}
|
|
578
579
|
|
|
579
580
|
class NPlusOneDetected = {
|
|
@@ -615,6 +616,7 @@ class PostgresDialect = {
|
|
|
615
616
|
readonly name: 'postgres'
|
|
616
617
|
readonly supportsAdvisoryLocks: true
|
|
617
618
|
readonly supportsTransactionalDdl: true
|
|
619
|
+
stringType: () => string
|
|
618
620
|
}
|
|
619
621
|
|
|
620
622
|
class QueryBuilder = {
|
|
@@ -754,6 +756,7 @@ class SqliteDialect = {
|
|
|
754
756
|
readonly name: 'sqlite'
|
|
755
757
|
readonly supportsAdvisoryLocks: false
|
|
756
758
|
readonly supportsTransactionalDdl: true
|
|
759
|
+
stringType: () => string
|
|
757
760
|
}
|
|
758
761
|
|
|
759
762
|
class StateError = {
|
|
@@ -1274,6 +1277,7 @@ interface SqlDialect = {
|
|
|
1274
1277
|
readonly name: DialectName
|
|
1275
1278
|
readonly supportsAdvisoryLocks: boolean
|
|
1276
1279
|
readonly supportsTransactionalDdl: boolean
|
|
1280
|
+
stringType: (length: number) => string
|
|
1277
1281
|
}
|
|
1278
1282
|
|
|
1279
1283
|
interface SQLInstance = {
|
|
@@ -1315,13 +1319,13 @@ type ClassRef = abstract new (...args: never[]) => unknown
|
|
|
1315
1319
|
|
|
1316
1320
|
type Columns = { [K in keyof T & string]: K extends `_${string}` ? never : T[K] extends (...args: any[]) => any ? never : K; }[keyof T & string]
|
|
1317
1321
|
|
|
1318
|
-
type ColumnShorthand = 'string' | '
|
|
1322
|
+
type ColumnShorthand = 'string' | 'text' | 'integer' | 'number' | 'float' | 'boolean' | 'datetime' | 'date' | 'json' | 'array' | 'encrypted' | 'encrypted:json'
|
|
1319
1323
|
|
|
1320
1324
|
type Constructor = new (...args: any[]) => T
|
|
1321
1325
|
|
|
1322
1326
|
type ContextConnectionResolver = (ModelClass?: typeof BaseModel) => SQLInstance | null
|
|
1323
1327
|
|
|
1324
|
-
type DatePart = 'date' | 'time' | '
|
|
1328
|
+
type DatePart = 'date' | 'time' | 'day' | 'month' | 'year'
|
|
1325
1329
|
|
|
1326
1330
|
type DialectName = 'sqlite' | 'postgres' | 'mysql'
|
|
1327
1331
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zerotal/orm",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.4",
|
|
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.4",
|
|
35
|
+
"@zerotal/validator": "1.7.4"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"typescript": "^5.8.0"
|
|
@@ -58,6 +58,12 @@ export class MysqlDialect implements SqlDialect {
|
|
|
58
58
|
return value ? "1" : "0";
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
// VARCHAR so the column can carry an index or a unique constraint; MySQL
|
|
62
|
+
// refuses to key a TEXT column without a prefix length.
|
|
63
|
+
stringType(length: number): string {
|
|
64
|
+
return `VARCHAR(${length})`;
|
|
65
|
+
}
|
|
66
|
+
|
|
61
67
|
advisoryLockSql(key: number): DialectQuery {
|
|
62
68
|
return { sql: `SELECT GET_LOCK(?, -1)`, params: [`zerotal_lock_${key}`] };
|
|
63
69
|
}
|
|
@@ -54,6 +54,11 @@ export class PostgresDialect implements SqlDialect {
|
|
|
54
54
|
return value ? "TRUE" : "FALSE";
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
// PostgreSQL indexes TEXT without a key length, so the portable type stands.
|
|
58
|
+
stringType(): string {
|
|
59
|
+
return "TEXT";
|
|
60
|
+
}
|
|
61
|
+
|
|
57
62
|
advisoryLockSql(key: number): DialectQuery {
|
|
58
63
|
return { sql: `SELECT pg_advisory_lock(?)`, params: [key] };
|
|
59
64
|
}
|
|
@@ -55,6 +55,11 @@ export class SqliteDialect implements SqlDialect {
|
|
|
55
55
|
return value ? "1" : "0";
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
// SQLite has one string type and no length to honour.
|
|
59
|
+
stringType(): string {
|
|
60
|
+
return "TEXT";
|
|
61
|
+
}
|
|
62
|
+
|
|
58
63
|
advisoryLockSql(): DialectQuery | null {
|
|
59
64
|
return null;
|
|
60
65
|
}
|
package/src/db/dialects/types.ts
CHANGED
|
@@ -76,6 +76,22 @@ export interface SqlDialect {
|
|
|
76
76
|
/** A boolean as this engine spells it in a `DEFAULT` clause. */
|
|
77
77
|
booleanLiteral(value: boolean): string;
|
|
78
78
|
|
|
79
|
+
/**
|
|
80
|
+
* The column type a portable `table.string(name, length)` compiles to.
|
|
81
|
+
*
|
|
82
|
+
* SQLite has one string type and ignores the length, so the Blueprint emitted
|
|
83
|
+
* `TEXT` for every engine and threw the length away. MySQL cannot index a TEXT
|
|
84
|
+
* column without a key length, so `table.string("name").unique()` — an email, a
|
|
85
|
+
* slug, any natural key — failed at `CREATE TABLE`:
|
|
86
|
+
*
|
|
87
|
+
* BLOB/TEXT column 'name' used in key specification without a key length
|
|
88
|
+
*
|
|
89
|
+
* The length was already in the signature and already documented as accepted;
|
|
90
|
+
* it just had nowhere to go. PostgreSQL keeps `TEXT`, which it indexes happily
|
|
91
|
+
* and which is the idiomatic choice there.
|
|
92
|
+
*/
|
|
93
|
+
stringType(length: number): string;
|
|
94
|
+
|
|
79
95
|
/** Whether the engine supports application-level advisory locks. */
|
|
80
96
|
readonly supportsAdvisoryLocks: boolean;
|
|
81
97
|
|
package/src/schema/Blueprint.ts
CHANGED
|
@@ -202,19 +202,21 @@ export class Blueprint {
|
|
|
202
202
|
/**
|
|
203
203
|
* Variable-length string column (`VARCHAR`-style), stored as `TEXT`.
|
|
204
204
|
* @param name - Column name.
|
|
205
|
-
* @param
|
|
205
|
+
* @param length - Max length. Ignored on SQLite and PostgreSQL, which have one
|
|
206
|
+
* string type; on MySQL it becomes `VARCHAR(length)`, which is what lets the
|
|
207
|
+
* column carry an index or a unique constraint.
|
|
206
208
|
* @category Column types
|
|
207
209
|
*/
|
|
208
|
-
string(name: string,
|
|
209
|
-
return this._add(new ColumnBuilder(name, "TEXT"));
|
|
210
|
+
string(name: string, length = 255): ColumnBuilder {
|
|
211
|
+
return this._add(new ColumnBuilder(name, "TEXT", false, false, false, length));
|
|
210
212
|
}
|
|
211
213
|
|
|
212
214
|
/**
|
|
213
215
|
* Fixed-length `CHAR` column. Stored as `TEXT` on SQLite; `_length` is ignored.
|
|
214
216
|
* @category Column types
|
|
215
217
|
*/
|
|
216
|
-
char(name: string,
|
|
217
|
-
return this._add(new ColumnBuilder(name, "TEXT"));
|
|
218
|
+
char(name: string, length = 255): ColumnBuilder {
|
|
219
|
+
return this._add(new ColumnBuilder(name, "TEXT", false, false, false, length));
|
|
218
220
|
}
|
|
219
221
|
|
|
220
222
|
/**
|
|
@@ -69,6 +69,11 @@ export class ColumnBuilder<Locked extends string = never> {
|
|
|
69
69
|
* storage type at compile time — see {@link SqlDialect.booleanType}.
|
|
70
70
|
*/
|
|
71
71
|
private _isBoolean = false,
|
|
72
|
+
/**
|
|
73
|
+
* Declared max length for a portable string column. The engine decides
|
|
74
|
+
* whether it matters — see {@link SqlDialect.stringType}.
|
|
75
|
+
*/
|
|
76
|
+
private _stringLength?: number,
|
|
72
77
|
) {
|
|
73
78
|
this._isPrimary = isPrimary;
|
|
74
79
|
this._isAutoIncr = isAutoIncrement;
|
|
@@ -329,9 +334,15 @@ export class ColumnBuilder<Locked extends string = never> {
|
|
|
329
334
|
// PostgreSQL a syntax error and against MySQL a 1064.
|
|
330
335
|
if (this._isAutoIncr) return getDialect(dialect).autoIncrementColumn(this.name);
|
|
331
336
|
|
|
332
|
-
//
|
|
333
|
-
// INTEGER
|
|
334
|
-
|
|
337
|
+
// Storage type is the engine's to choose wherever the engines disagree:
|
|
338
|
+
// SQLite keeps 0/1 in an INTEGER where PostgreSQL has a real boolean, and
|
|
339
|
+
// MySQL needs a VARCHAR length before it will index a string at all.
|
|
340
|
+
const d = getDialect(dialect);
|
|
341
|
+
const sqlType = this._isBoolean
|
|
342
|
+
? d.booleanType
|
|
343
|
+
: this._stringLength !== undefined
|
|
344
|
+
? d.stringType(this._stringLength)
|
|
345
|
+
: this._sqlType;
|
|
335
346
|
const parts: string[] = [`${this.name} ${sqlType}`];
|
|
336
347
|
|
|
337
348
|
if (this._isPrimary) parts.push("PRIMARY KEY");
|