uql-orm 0.33.0 → 0.34.1
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/bunSql/bunSqlQuerierPool.js +2 -1
- package/dist/cockroachdb/crdbQuerierPool.js +2 -1
- package/dist/d1/d1QuerierPool.js +2 -1
- package/dist/dialect/abstractDialect.d.ts +27 -3
- package/dist/dialect/abstractDialect.js +34 -4
- package/dist/dialect/abstractSqlDialect.d.ts +27 -1
- package/dist/dialect/abstractSqlDialect.js +74 -36
- package/dist/dialect/mysqlLikeSqlDialect.js +1 -0
- package/dist/dialect/pgLikeSqlDialect.js +1 -0
- package/dist/entity/metadata/definition.js +9 -0
- package/dist/http/handler.js +16 -1
- package/dist/libsql/libsqlQuerierPool.js +2 -1
- package/dist/maria/mariadbQuerierPool.js +2 -1
- package/dist/migrate/builder/migrationBuilder.js +3 -1
- package/dist/migrate/builder/tableBuilder.js +2 -1
- package/dist/migrate/generator/definitionToNode.js +3 -3
- package/dist/migrate/generator/mongoSchemaGenerator.js +7 -6
- package/dist/migrate/introspection/abstractSqlSchemaIntrospector.d.ts +12 -1
- package/dist/migrate/introspection/abstractSqlSchemaIntrospector.js +16 -2
- package/dist/migrate/introspection/baseSqlIntrospector.d.ts +7 -1
- package/dist/migrate/introspection/baseSqlIntrospector.js +11 -12
- package/dist/migrate/introspection/mongoIntrospector.js +0 -1
- package/dist/migrate/introspection/mysqlIntrospector.d.ts +1 -0
- package/dist/migrate/introspection/mysqlIntrospector.js +8 -6
- package/dist/migrate/introspection/postgresIntrospector.d.ts +1 -0
- package/dist/migrate/introspection/postgresIntrospector.js +6 -5
- package/dist/migrate/migrator.d.ts +10 -1
- package/dist/migrate/migrator.js +30 -8
- package/dist/migrate/schemaGenerator.d.ts +16 -6
- package/dist/migrate/schemaGenerator.js +53 -24
- package/dist/mongo/mongoDialect.js +5 -4
- package/dist/mongo/mongodbQuerierPool.js +2 -1
- package/dist/mysql/mysql2QuerierPool.js +2 -1
- package/dist/neon/neonQuerierPool.js +2 -1
- package/dist/pglite/pgliteQuerierPool.js +2 -1
- package/dist/postgres/pgQuerierPool.js +2 -1
- package/dist/schema/schemaAST.d.ts +8 -1
- package/dist/schema/schemaAST.js +22 -13
- package/dist/schema/schemaASTBuilder.d.ts +5 -3
- package/dist/schema/schemaASTBuilder.js +24 -31
- package/dist/schema/types.d.ts +10 -3
- package/dist/sqlite/localSqliteQuerierPool.js +2 -1
- package/dist/sqlite/sqliteDialect.js +2 -1
- package/dist/turso/tursoLocalQuerierPool.js +2 -1
- package/dist/turso/tursoQuerierPool.js +2 -1
- package/dist/type/dialect.d.ts +6 -0
- package/dist/type/entity.d.ts +7 -0
- package/dist/type/migration.d.ts +16 -2
- package/dist/type/querier.d.ts +5 -0
- package/dist/util/sql.util.d.ts +17 -0
- package/dist/util/sql.util.js +23 -0
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SQL } from 'bun';
|
|
2
|
+
import { dialectOptionsFrom } from '../dialect/abstractDialect.js';
|
|
2
3
|
import { MariaDialect } from '../maria/mariaDialect.js';
|
|
3
4
|
import { MySqlDialect } from '../mysql/mysqlDialect.js';
|
|
4
5
|
import { AbstractSqlQuerierPool } from '../querier/index.js';
|
|
@@ -20,7 +21,7 @@ export class BunSqlQuerierPool extends AbstractSqlQuerierPool {
|
|
|
20
21
|
sqlDialectName;
|
|
21
22
|
constructor(config, extra) {
|
|
22
23
|
const dialectName = inferDialectName(config);
|
|
23
|
-
super(new DialectMap[dialectName](
|
|
24
|
+
super(new DialectMap[dialectName](dialectOptionsFrom(extra)), extra);
|
|
24
25
|
this.config = config;
|
|
25
26
|
this.sqlDialectName = dialectName;
|
|
26
27
|
const opts = normalizeBunOpts(config, dialectName);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Pool, types } from 'pg';
|
|
2
|
+
import { dialectOptionsFrom } from '../dialect/abstractDialect.js';
|
|
2
3
|
import { AbstractPgQuerierPool } from '../postgres/abstractPgQuerierPool.js';
|
|
3
4
|
import { numericTypes } from '../postgres/pgNumericTypes.js';
|
|
4
5
|
import { CockroachDialect } from './cockroachDialect.js';
|
|
@@ -8,7 +9,7 @@ import { CrdbQuerier } from './crdbQuerier.js';
|
|
|
8
9
|
*/
|
|
9
10
|
export class CrdbQuerierPool extends AbstractPgQuerierPool {
|
|
10
11
|
constructor(opts, extra) {
|
|
11
|
-
super(new CockroachDialect(
|
|
12
|
+
super(new CockroachDialect(dialectOptionsFrom(extra)), new Pool({ keepAlive: true, types: numericTypes(types), ...opts }), extra);
|
|
12
13
|
}
|
|
13
14
|
buildQuerier(connect) {
|
|
14
15
|
return new CrdbQuerier(connect, this.dialect, this.extra);
|
package/dist/d1/d1QuerierPool.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { dialectOptionsFrom } from '../dialect/abstractDialect.js';
|
|
1
2
|
import { AbstractSqlQuerierPool } from '../querier/index.js';
|
|
2
3
|
import { D1Querier } from './d1Querier.js';
|
|
3
4
|
import { D1SqliteDialect } from './d1SqliteDialect.js';
|
|
4
5
|
export class D1QuerierPool extends AbstractSqlQuerierPool {
|
|
5
6
|
db;
|
|
6
7
|
constructor(db, extra) {
|
|
7
|
-
super(new D1SqliteDialect(
|
|
8
|
+
super(new D1SqliteDialect(dialectOptionsFrom(extra)), extra);
|
|
8
9
|
this.db = db;
|
|
9
10
|
}
|
|
10
11
|
async getQuerier() {
|
|
@@ -1,11 +1,19 @@
|
|
|
1
|
-
import type { DialectFeatures, DialectName, EntityMeta, FieldOptions, InsertIdSource, NamingStrategy, QueryOptions, QueryWhere, QueryWhereMap
|
|
1
|
+
import type { DialectFeatures, DialectName, EntityMeta, ExtraOptions, FieldOptions, InsertIdSource, NamingStrategy, QueryOptions, QueryWhere, QueryWhereMap } from '../type/index.js';
|
|
2
2
|
/**
|
|
3
3
|
* Options for initializing a dialect.
|
|
4
4
|
*/
|
|
5
5
|
export interface DialectOptions {
|
|
6
6
|
readonly namingStrategy?: NamingStrategy;
|
|
7
|
+
/** Default schema for entities naming none; unset leaves them unqualified. See {@link AbstractDialect.resolveSchema}. */
|
|
8
|
+
readonly schema?: string;
|
|
7
9
|
readonly driverCapabilities?: Partial<DialectFeatures>;
|
|
8
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* The dialect's share of a pool's {@link ExtraOptions}: what changes the SQL rather than the
|
|
13
|
+
* connection. Every pool builds its dialect through this, so a new option lands here instead of in
|
|
14
|
+
* each of the thirteen constructors.
|
|
15
|
+
*/
|
|
16
|
+
export declare function dialectOptionsFrom(extra: ExtraOptions | undefined): DialectOptions;
|
|
9
17
|
/**
|
|
10
18
|
* Base abstract class for all database dialects (SQL and NoSQL).
|
|
11
19
|
*/
|
|
@@ -34,9 +42,25 @@ export declare abstract class AbstractDialect {
|
|
|
34
42
|
*/
|
|
35
43
|
get features(): DialectFeatures;
|
|
36
44
|
/**
|
|
37
|
-
*
|
|
45
|
+
* The table's own name, unqualified, applying naming strategy if necessary. Also the alias the
|
|
46
|
+
* root table gets once {@link resolveTableName} qualifies it, so it has to stay a single
|
|
47
|
+
* identifier: columns are prefixed with it, and a dotted prefix escapes as one identifier that
|
|
48
|
+
* nothing declared.
|
|
49
|
+
*/
|
|
50
|
+
resolveTableAlias<E>(meta: EntityMeta<E>): string;
|
|
51
|
+
/**
|
|
52
|
+
* Where the table lives: {@link resolveTableAlias} behind its schema, when one applies. The schema
|
|
53
|
+
* skips the naming strategy - it names an object the author wrote out, and snake_casing `myCrm`
|
|
54
|
+
* would point at one that does not exist.
|
|
55
|
+
*/
|
|
56
|
+
resolveTableName<E>(meta: EntityMeta<E>): string;
|
|
57
|
+
/**
|
|
58
|
+
* Which schema an entity is read from: its own wins over the pool's default, the way its `name`
|
|
59
|
+
* wins over the naming strategy. `undefined` is an answer, not a gap - the table stays unqualified
|
|
60
|
+
* and resolves through the connection's `search_path`, which is what every deployment predating
|
|
61
|
+
* this option relies on.
|
|
38
62
|
*/
|
|
39
|
-
|
|
63
|
+
resolveSchema<E>(meta: EntityMeta<E>): string | undefined;
|
|
40
64
|
/**
|
|
41
65
|
* Resolve the column/field name for a property, applying naming strategy if necessary.
|
|
42
66
|
*/
|
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
import { applyFilters, buildQueryWhereAsMap } from '../util/dialect.util.js';
|
|
2
|
+
import { qualifyName } from '../util/sql.util.js';
|
|
3
|
+
/**
|
|
4
|
+
* The dialect's share of a pool's {@link ExtraOptions}: what changes the SQL rather than the
|
|
5
|
+
* connection. Every pool builds its dialect through this, so a new option lands here instead of in
|
|
6
|
+
* each of the thirteen constructors.
|
|
7
|
+
*/
|
|
8
|
+
export function dialectOptionsFrom(extra) {
|
|
9
|
+
return { namingStrategy: extra?.namingStrategy, schema: extra?.schema };
|
|
10
|
+
}
|
|
2
11
|
/**
|
|
3
12
|
* Base abstract class for all database dialects (SQL and NoSQL).
|
|
4
13
|
*/
|
|
@@ -25,15 +34,36 @@ export class AbstractDialect {
|
|
|
25
34
|
return this.#features;
|
|
26
35
|
}
|
|
27
36
|
/**
|
|
28
|
-
*
|
|
37
|
+
* The table's own name, unqualified, applying naming strategy if necessary. Also the alias the
|
|
38
|
+
* root table gets once {@link resolveTableName} qualifies it, so it has to stay a single
|
|
39
|
+
* identifier: columns are prefixed with it, and a dotted prefix escapes as one identifier that
|
|
40
|
+
* nothing declared.
|
|
29
41
|
*/
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
42
|
+
resolveTableAlias(meta) {
|
|
43
|
+
const className = meta.entity.name;
|
|
44
|
+
const name = meta.name ?? className;
|
|
45
|
+
if (name !== className || !this.namingStrategy) {
|
|
33
46
|
return name;
|
|
34
47
|
}
|
|
35
48
|
return this.namingStrategy.tableName(name);
|
|
36
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Where the table lives: {@link resolveTableAlias} behind its schema, when one applies. The schema
|
|
52
|
+
* skips the naming strategy - it names an object the author wrote out, and snake_casing `myCrm`
|
|
53
|
+
* would point at one that does not exist.
|
|
54
|
+
*/
|
|
55
|
+
resolveTableName(meta) {
|
|
56
|
+
return qualifyName(this.resolveTableAlias(meta), this.resolveSchema(meta));
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Which schema an entity is read from: its own wins over the pool's default, the way its `name`
|
|
60
|
+
* wins over the naming strategy. `undefined` is an answer, not a gap - the table stays unqualified
|
|
61
|
+
* and resolves through the connection's `search_path`, which is what every deployment predating
|
|
62
|
+
* this option relies on.
|
|
63
|
+
*/
|
|
64
|
+
resolveSchema(meta) {
|
|
65
|
+
return this.features.schemas ? (meta.schema ?? this.options.schema) : undefined;
|
|
66
|
+
}
|
|
37
67
|
/**
|
|
38
68
|
* Resolve the column/field name for a property, applying naming strategy if necessary.
|
|
39
69
|
*/
|
|
@@ -15,6 +15,12 @@ export declare abstract class AbstractSqlDialect extends IndexSqlDialect impleme
|
|
|
15
15
|
abstract readonly beginTransactionCommand: string;
|
|
16
16
|
abstract readonly commitTransactionCommand: string;
|
|
17
17
|
abstract readonly rollbackTransactionCommand: string;
|
|
18
|
+
/**
|
|
19
|
+
* How this engine declares a namespace, so a generated migration creates the schemas its tables
|
|
20
|
+
* need before creating them. Only reached where {@link DialectFeatures.schemas} is on. MySQL and
|
|
21
|
+
* MariaDB accept the same statement, where it means a database.
|
|
22
|
+
*/
|
|
23
|
+
createSchemaSql(schema: string): string;
|
|
18
24
|
readonly isolationLevelStrategy: 'inline' | 'set-before' | 'none';
|
|
19
25
|
readonly alterColumnStrategy: 'separate-clauses' | 'single-statement';
|
|
20
26
|
readonly alterColumnSyntax: 'ALTER COLUMN' | 'MODIFY COLUMN' | 'none';
|
|
@@ -68,10 +74,24 @@ export declare abstract class AbstractSqlDialect extends IndexSqlDialect impleme
|
|
|
68
74
|
*/
|
|
69
75
|
protected appendTextSearch<E>(_ctx: QueryContext, _entity: Type<E>, _meta: EntityMeta<E>, _search: QueryTextSearchOptions<E>): void;
|
|
70
76
|
select<E>(ctx: QueryContext, entity: Type<E>, q: Query<E>, opts?: QueryOptions, joins?: QueryJoins): void;
|
|
77
|
+
/**
|
|
78
|
+
* The table as a statement writes it, each part escaped on its own rather than as one dotted
|
|
79
|
+
* string taken apart again by {@link escapeId}.
|
|
80
|
+
*/
|
|
81
|
+
protected escapedTableName<E>(meta: EntityMeta<E>): string;
|
|
82
|
+
/**
|
|
83
|
+
* A FROM or JOIN operand plus the alias to prefix its columns by, aliased only once a schema puts
|
|
84
|
+
* something in front of the name. See {@link resolveTableAlias} for why the prefix cannot be the
|
|
85
|
+
* qualified path.
|
|
86
|
+
*/
|
|
87
|
+
protected tableRef<E>(meta: EntityMeta<E>): {
|
|
88
|
+
alias: string;
|
|
89
|
+
ref: string;
|
|
90
|
+
};
|
|
71
91
|
/** Columns are alias-qualified once anything else is in play: a join, or a to-many being filled. */
|
|
72
92
|
private resolveRelationAwarePrefix;
|
|
73
93
|
protected selectRelationFields(ctx: QueryContext, joins: QueryJoins): void;
|
|
74
|
-
protected selectRelationJoins<E>(ctx: QueryContext, meta: EntityMeta<E>,
|
|
94
|
+
protected selectRelationJoins<E>(ctx: QueryContext, meta: EntityMeta<E>, rootAlias: string, joins: QueryJoins): void;
|
|
75
95
|
where<E>(ctx: QueryContext, entity: Type<E>, where?: QueryWhere<E>, opts?: QueryWhereOptions): void;
|
|
76
96
|
/** Renders a `$where` tree without applying entity filters (used for same-scope `$and`/`$or` recursion). */
|
|
77
97
|
protected renderWhere<E>(ctx: QueryContext, entity: Type<E>, where?: QueryWhere<E>, opts?: QueryWhereOptions): void;
|
|
@@ -281,6 +301,12 @@ export declare abstract class AbstractSqlDialect extends IndexSqlDialect impleme
|
|
|
281
301
|
protected getUpsertConflictPathsStr<E>(meta: EntityMeta<E>, conflictPaths: QueryConflictPaths<E>): string;
|
|
282
302
|
delete<E>(ctx: QueryContext, entity: Type<E>, q: QuerySearch<E>, opts?: QueryOptions): void;
|
|
283
303
|
escapeId(val: string | undefined, forbidQualified?: boolean, addDot?: boolean): string;
|
|
304
|
+
/**
|
|
305
|
+
* A name behind its schema, each part escaped on its own rather than as one dotted string taken
|
|
306
|
+
* apart again by {@link escapeId}. Tables and their indexes both use it, since an index lives in
|
|
307
|
+
* the schema of the table it is on.
|
|
308
|
+
*/
|
|
309
|
+
escapeQualifiedId(name: string, schema: string | undefined): string;
|
|
284
310
|
/**
|
|
285
311
|
* Bind one persisted value, classifying its column on the spot. Dialects override
|
|
286
312
|
* {@link appendJsonValue} and {@link appendVectorValue} rather than this, so the chain runs once per
|
|
@@ -9,6 +9,14 @@ import { SqlQueryContext } from './queryContext.js';
|
|
|
9
9
|
import { NO_JOINS, resolveQueryJoins, resolveSortableJoin, } from './queryJoins.js';
|
|
10
10
|
import { isVectorFieldType, resolveVectorCast } from './vectorCast.js';
|
|
11
11
|
export class AbstractSqlDialect extends IndexSqlDialect {
|
|
12
|
+
/**
|
|
13
|
+
* How this engine declares a namespace, so a generated migration creates the schemas its tables
|
|
14
|
+
* need before creating them. Only reached where {@link DialectFeatures.schemas} is on. MySQL and
|
|
15
|
+
* MariaDB accept the same statement, where it means a database.
|
|
16
|
+
*/
|
|
17
|
+
createSchemaSql(schema) {
|
|
18
|
+
return `CREATE SCHEMA IF NOT EXISTS ${this.escapeId(schema, true)}`;
|
|
19
|
+
}
|
|
12
20
|
isolationLevelStrategy = 'inline';
|
|
13
21
|
alterColumnStrategy = 'single-statement';
|
|
14
22
|
alterColumnSyntax = 'ALTER COLUMN';
|
|
@@ -89,8 +97,7 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
89
97
|
}
|
|
90
98
|
search(ctx, entity, q = {}, opts = {}, joins = NO_JOINS) {
|
|
91
99
|
const meta = getMeta(entity);
|
|
92
|
-
const
|
|
93
|
-
const prefix = this.resolveRelationAwarePrefix(tableName, meta, opts, q.$populate, joins);
|
|
100
|
+
const prefix = this.resolveRelationAwarePrefix(this.resolveTableAlias(meta), meta, opts, q.$populate, joins);
|
|
94
101
|
if (opts.prefix !== prefix) {
|
|
95
102
|
opts = { ...opts, prefix };
|
|
96
103
|
}
|
|
@@ -166,8 +173,8 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
166
173
|
}
|
|
167
174
|
select(ctx, entity, q, opts = {}, joins = NO_JOINS) {
|
|
168
175
|
const meta = getMeta(entity);
|
|
169
|
-
const
|
|
170
|
-
const prefix = this.resolveRelationAwarePrefix(
|
|
176
|
+
const { alias, ref } = this.tableRef(meta);
|
|
177
|
+
const prefix = this.resolveRelationAwarePrefix(alias, meta, opts, q.$populate, joins);
|
|
171
178
|
ctx.append(q.$distinct ? 'SELECT DISTINCT ' : 'SELECT ');
|
|
172
179
|
this.selectFields(ctx, entity, q.$select, { prefix }, q.$exclude);
|
|
173
180
|
// Add related fields BEFORE FROM clause
|
|
@@ -179,9 +186,27 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
179
186
|
this.appendVectorProjection(ctx, meta, key, val);
|
|
180
187
|
}
|
|
181
188
|
}
|
|
182
|
-
ctx.append(` FROM ${
|
|
189
|
+
ctx.append(` FROM ${ref}`);
|
|
183
190
|
// Add JOINs AFTER FROM clause
|
|
184
|
-
this.selectRelationJoins(ctx, meta,
|
|
191
|
+
this.selectRelationJoins(ctx, meta, alias, joins);
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* The table as a statement writes it, each part escaped on its own rather than as one dotted
|
|
195
|
+
* string taken apart again by {@link escapeId}.
|
|
196
|
+
*/
|
|
197
|
+
escapedTableName(meta) {
|
|
198
|
+
return this.escapeQualifiedId(this.resolveTableAlias(meta), this.resolveSchema(meta));
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* A FROM or JOIN operand plus the alias to prefix its columns by, aliased only once a schema puts
|
|
202
|
+
* something in front of the name. See {@link resolveTableAlias} for why the prefix cannot be the
|
|
203
|
+
* qualified path.
|
|
204
|
+
*/
|
|
205
|
+
tableRef(meta) {
|
|
206
|
+
const alias = this.resolveTableAlias(meta);
|
|
207
|
+
const name = this.escapedTableName(meta);
|
|
208
|
+
const schema = this.resolveSchema(meta);
|
|
209
|
+
return { alias, ref: schema ? `${name} ${this.escapeId(alias, true)}` : name };
|
|
185
210
|
}
|
|
186
211
|
/** Columns are alias-qualified once anything else is in play: a join, or a to-many being filled. */
|
|
187
212
|
resolveRelationAwarePrefix(tableName, meta, opts, populate, joins) {
|
|
@@ -198,11 +223,11 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
198
223
|
this.selectFields(ctx, join.entity, join.query.$select, { prefix: join.path, autoPrefixAlias: true }, join.query.$exclude);
|
|
199
224
|
}
|
|
200
225
|
}
|
|
201
|
-
selectRelationJoins(ctx, meta,
|
|
226
|
+
selectRelationJoins(ctx, meta, rootAlias, joins) {
|
|
202
227
|
for (const join of joins.values()) {
|
|
203
228
|
const joinAlias = this.escapeId(join.path, true);
|
|
204
|
-
const parentAlias = join.parent ? this.escapeId(join.parent.path, true) : this.escapeId(
|
|
205
|
-
ctx.append(` ${join.required ? 'INNER' : 'LEFT'} JOIN ${this.
|
|
229
|
+
const parentAlias = join.parent ? this.escapeId(join.parent.path, true) : this.escapeId(rootAlias, true);
|
|
230
|
+
ctx.append(` ${join.required ? 'INNER' : 'LEFT'} JOIN ${this.escapedTableName(join.meta)} ${joinAlias} ON `);
|
|
206
231
|
join.relation.references.forEach((reference, index) => {
|
|
207
232
|
if (index > 0)
|
|
208
233
|
ctx.append(' AND ');
|
|
@@ -264,11 +289,13 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
264
289
|
if (val instanceof QueryRaw) {
|
|
265
290
|
if (key === '$exists' || key === '$nexists') {
|
|
266
291
|
ctx.append(key === '$exists' ? 'EXISTS (' : 'NOT EXISTS (');
|
|
267
|
-
|
|
292
|
+
// The alias: the enclosing statement declares one, and Postgres forbids reaching past it
|
|
293
|
+
// to the qualified name it aliased.
|
|
294
|
+
const alias = this.resolveTableAlias(meta);
|
|
268
295
|
this.getRawValue(ctx, {
|
|
269
296
|
value: val,
|
|
270
|
-
prefix:
|
|
271
|
-
escapedPrefix: this.escapeId(
|
|
297
|
+
prefix: alias,
|
|
298
|
+
escapedPrefix: this.escapeId(alias, true, true),
|
|
272
299
|
});
|
|
273
300
|
ctx.append(')');
|
|
274
301
|
return;
|
|
@@ -735,7 +762,8 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
735
762
|
}
|
|
736
763
|
this.assertLockSupported(entity, q, joins);
|
|
737
764
|
const meta = getMeta(entity);
|
|
738
|
-
|
|
765
|
+
// `OF` names the alias in the FROM, never the schema-qualified path it was aliased from.
|
|
766
|
+
const target = joins.size > 0 ? ` OF ${this.escapeId(this.resolveTableAlias(meta), true)}` : '';
|
|
739
767
|
const suffix = wait === 'skip' ? ' SKIP LOCKED' : wait === 'nowait' ? ' NOWAIT' : '';
|
|
740
768
|
ctx.append(` FOR UPDATE${target}${suffix}`);
|
|
741
769
|
}
|
|
@@ -757,7 +785,7 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
757
785
|
]);
|
|
758
786
|
aggregate(ctx, entity, q, opts = {}) {
|
|
759
787
|
const meta = getMeta(entity);
|
|
760
|
-
const tableName = this.
|
|
788
|
+
const tableName = this.escapedTableName(meta);
|
|
761
789
|
const groupKeys = [];
|
|
762
790
|
const selectParts = [];
|
|
763
791
|
// Every column the statement emits, mapped to the SQL that references it. `$having` and `$sort`
|
|
@@ -787,7 +815,7 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
787
815
|
if (!selectParts.length) {
|
|
788
816
|
throw new TypeError('aggregate requires at least one $group column or $agg function');
|
|
789
817
|
}
|
|
790
|
-
ctx.append(`SELECT ${selectParts.join(', ')} FROM ${
|
|
818
|
+
ctx.append(`SELECT ${selectParts.join(', ')} FROM ${tableName}`);
|
|
791
819
|
this.where(ctx, entity, q.$where, opts);
|
|
792
820
|
if (groupKeys.length) {
|
|
793
821
|
ctx.append(` GROUP BY ${groupKeys.join(', ')}`);
|
|
@@ -893,8 +921,8 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
893
921
|
columns[i] = this.escapedColumnName(meta, key);
|
|
894
922
|
kinds[i] = this.persistKind(field);
|
|
895
923
|
}
|
|
896
|
-
const tableName = this.
|
|
897
|
-
ctx.append(`INSERT INTO ${
|
|
924
|
+
const tableName = this.escapedTableName(meta);
|
|
925
|
+
ctx.append(`INSERT INTO ${tableName} (${columns.join(', ')}) VALUES (`);
|
|
898
926
|
for (let r = 0; r < payloads.length; r++) {
|
|
899
927
|
if (r > 0) {
|
|
900
928
|
ctx.append('), (');
|
|
@@ -931,8 +959,8 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
931
959
|
const meta = getMeta(entity);
|
|
932
960
|
const [filledPayload] = fillOnFields(meta, payload, 'onUpdate');
|
|
933
961
|
const keys = filterFieldKeys(meta, filledPayload, 'onUpdate');
|
|
934
|
-
const tableName = this.
|
|
935
|
-
ctx.append(`UPDATE ${
|
|
962
|
+
const tableName = this.escapedTableName(meta);
|
|
963
|
+
ctx.append(`UPDATE ${tableName} SET `);
|
|
936
964
|
for (let i = 0; i < keys.length; i++) {
|
|
937
965
|
if (i > 0) {
|
|
938
966
|
ctx.append(', ');
|
|
@@ -1014,14 +1042,14 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
1014
1042
|
}
|
|
1015
1043
|
delete(ctx, entity, q, opts = {}) {
|
|
1016
1044
|
const meta = getMeta(entity);
|
|
1017
|
-
const tableName = this.
|
|
1045
|
+
const tableName = this.escapedTableName(meta);
|
|
1018
1046
|
// Soft-delete (stamp only live rows) unless `hardDelete` is requested or the entity has no
|
|
1019
1047
|
// soft-delete field (e.g. a cascade onto a non-soft-deletable child).
|
|
1020
1048
|
if (!opts.hardDelete && meta.softDelete) {
|
|
1021
1049
|
const field = meta.fields[meta.softDelete];
|
|
1022
1050
|
if (field) {
|
|
1023
1051
|
const columnName = this.resolveColumnName(meta.softDelete, field);
|
|
1024
|
-
ctx.append(`UPDATE ${
|
|
1052
|
+
ctx.append(`UPDATE ${tableName} SET ${this.escapeId(columnName)} = `);
|
|
1025
1053
|
this.formatPersistableValue(ctx, field, getSoftDeleteValue(field));
|
|
1026
1054
|
this.search(ctx, entity, q, opts);
|
|
1027
1055
|
return;
|
|
@@ -1029,12 +1057,21 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
1029
1057
|
}
|
|
1030
1058
|
// Hard delete removes matching rows regardless of soft-delete state (keeps other filters, e.g. tenant).
|
|
1031
1059
|
// Only rewrite the filters when there is a soft-delete filter to disable.
|
|
1032
|
-
ctx.append(`DELETE FROM ${
|
|
1060
|
+
ctx.append(`DELETE FROM ${tableName}`);
|
|
1033
1061
|
this.search(ctx, entity, q, meta.softDelete ? { ...opts, filters: withoutSoftDeleteFilter(opts.filters) } : opts);
|
|
1034
1062
|
}
|
|
1035
1063
|
escapeId(val, forbidQualified, addDot) {
|
|
1036
1064
|
return escapeSqlId(val, this.escapeIdChar, forbidQualified, addDot);
|
|
1037
1065
|
}
|
|
1066
|
+
/**
|
|
1067
|
+
* A name behind its schema, each part escaped on its own rather than as one dotted string taken
|
|
1068
|
+
* apart again by {@link escapeId}. Tables and their indexes both use it, since an index lives in
|
|
1069
|
+
* the schema of the table it is on.
|
|
1070
|
+
*/
|
|
1071
|
+
escapeQualifiedId(name, schema) {
|
|
1072
|
+
const escaped = this.escapeId(name, true);
|
|
1073
|
+
return schema ? `${this.escapeId(schema, true)}.${escaped}` : escaped;
|
|
1074
|
+
}
|
|
1038
1075
|
/**
|
|
1039
1076
|
* Bind one persisted value, classifying its column on the spot. Dialects override
|
|
1040
1077
|
* {@link appendJsonValue} and {@link appendVectorValue} rather than this, so the chain runs once per
|
|
@@ -1353,38 +1390,39 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
1353
1390
|
*/
|
|
1354
1391
|
appendRelationSubquery(ctx, entity, rel, opts, projection, val) {
|
|
1355
1392
|
const meta = getMeta(entity);
|
|
1356
|
-
|
|
1393
|
+
// Aliases, not paths, everywhere a column is prefixed; `tableRef` declares them in the FROM.
|
|
1394
|
+
const parentAlias = this.resolveTableAlias(meta);
|
|
1357
1395
|
const references = rel.references;
|
|
1358
|
-
const escapedParentId = this.escapedParentColumn(
|
|
1396
|
+
const escapedParentId = this.escapedParentColumn(parentAlias, meta, opts, meta.id);
|
|
1359
1397
|
const relatedEntity = rel.entity();
|
|
1360
1398
|
const relatedMeta = getMeta(relatedEntity);
|
|
1361
|
-
const
|
|
1399
|
+
const { alias: relatedAlias, ref: relatedRef } = this.tableRef(relatedMeta);
|
|
1362
1400
|
// Resolved before any SQL is emitted: it also decides whether the mm form reaches the target.
|
|
1363
1401
|
const targetWhere = this.scopedWhereMap(relatedMeta, val);
|
|
1364
1402
|
ctx.append(`(SELECT ${projection} FROM `);
|
|
1365
1403
|
if (rel.cardinality === 'mm' && rel.through) {
|
|
1366
1404
|
const throughEntity = rel.through();
|
|
1367
1405
|
const throughMeta = getMeta(throughEntity);
|
|
1368
|
-
const
|
|
1369
|
-
ctx.append(
|
|
1370
|
-
ctx.append(` WHERE ${this.escapedColumn(
|
|
1406
|
+
const { alias: throughAlias, ref: throughRef } = this.tableRef(throughMeta);
|
|
1407
|
+
ctx.append(throughRef);
|
|
1408
|
+
ctx.append(` WHERE ${this.escapedColumn(throughAlias, throughMeta, references[0].local)} = ${escapedParentId}`);
|
|
1371
1409
|
// The junction is a row being read too: a soft-deleted link is not a link.
|
|
1372
|
-
this.where(ctx, throughEntity, {}, { prefix:
|
|
1410
|
+
this.where(ctx, throughEntity, {}, { prefix: throughAlias, clause: 'AND' });
|
|
1373
1411
|
if (hasKeys(targetWhere)) {
|
|
1374
|
-
ctx.append(` AND ${this.escapedColumn(
|
|
1375
|
-
ctx.append(`SELECT ${this.escapedColumn(
|
|
1376
|
-
this.renderWhere(ctx, relatedEntity, targetWhere, { prefix:
|
|
1412
|
+
ctx.append(` AND ${this.escapedColumn(throughAlias, throughMeta, references[1].local)} IN (`);
|
|
1413
|
+
ctx.append(`SELECT ${this.escapedColumn(relatedAlias, relatedMeta, relatedMeta.id)} FROM ${relatedRef}`);
|
|
1414
|
+
this.renderWhere(ctx, relatedEntity, targetWhere, { prefix: relatedAlias, clause: 'WHERE' });
|
|
1377
1415
|
ctx.append(')');
|
|
1378
1416
|
}
|
|
1379
1417
|
}
|
|
1380
1418
|
else {
|
|
1381
|
-
const joinLeft = this.escapedColumn(
|
|
1419
|
+
const joinLeft = this.escapedColumn(relatedAlias, relatedMeta, references[0].foreign);
|
|
1382
1420
|
const joinRight = rel.cardinality === '1m'
|
|
1383
1421
|
? escapedParentId
|
|
1384
|
-
: this.escapedParentColumn(
|
|
1385
|
-
ctx.append(
|
|
1422
|
+
: this.escapedParentColumn(parentAlias, meta, opts, references[0].local);
|
|
1423
|
+
ctx.append(relatedRef);
|
|
1386
1424
|
ctx.append(` WHERE ${joinLeft} = ${joinRight}`);
|
|
1387
|
-
this.renderWhere(ctx, relatedEntity, targetWhere, { prefix:
|
|
1425
|
+
this.renderWhere(ctx, relatedEntity, targetWhere, { prefix: relatedAlias, clause: 'AND' });
|
|
1388
1426
|
}
|
|
1389
1427
|
ctx.append(')');
|
|
1390
1428
|
}
|
|
@@ -104,6 +104,14 @@ export function applyMembers(entity, specs) {
|
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
106
|
export function defineEntity(entity, opts = {}) {
|
|
107
|
+
// Ahead of any registration, so a rejected definition leaves nothing half-written in the registry.
|
|
108
|
+
// A dotted name reads like a schema and is not one: it escapes as a single identifier, so the
|
|
109
|
+
// statement builds and then fails at the database. `schema` is the way to say it.
|
|
110
|
+
if (opts.name?.includes('.')) {
|
|
111
|
+
const [schema, ...rest] = opts.name.split('.');
|
|
112
|
+
throw new TypeError(`'${entity.name}' has a dotted name '${opts.name}'. Name the schema separately as ` +
|
|
113
|
+
`{ schema: '${schema}', name: '${rest.join('.')}' }.`);
|
|
114
|
+
}
|
|
107
115
|
const meta = ensureMeta(entity);
|
|
108
116
|
// Covers `defineEntity(Decorated)` called on a class whose members carry decorators. `@Entity()`
|
|
109
117
|
// drains `context.metadata` itself, because TypeScript only attaches `Symbol.metadata` to the class
|
|
@@ -121,6 +129,7 @@ export function defineEntity(entity, opts = {}) {
|
|
|
121
129
|
throw TypeError(`'${entity.name}' must have fields`);
|
|
122
130
|
}
|
|
123
131
|
meta.name = opts.name ?? entity.name;
|
|
132
|
+
meta.schema = opts.schema;
|
|
124
133
|
let proto = Object.getPrototypeOf(entity.prototype);
|
|
125
134
|
while (proto.constructor !== Object) {
|
|
126
135
|
const parent = proto.constructor;
|
package/dist/http/handler.js
CHANGED
|
@@ -2,6 +2,11 @@ import { withContext } from '../context/context.js';
|
|
|
2
2
|
import { getEntities, getMeta } from '../entity/index.js';
|
|
3
3
|
import { entityPath, matchRoute, } from './contract.js';
|
|
4
4
|
import { parseQueryParams } from './query.js';
|
|
5
|
+
/** `Company (crm.Company)`: the class, and the table it maps, which is what tells two apart. */
|
|
6
|
+
function tableOf(entity) {
|
|
7
|
+
const meta = getMeta(entity);
|
|
8
|
+
return `${entity.name} (${meta.schema ? `${meta.schema}.${meta.name}` : meta.name})`;
|
|
9
|
+
}
|
|
5
10
|
export function createRequestHandler(opts) {
|
|
6
11
|
const { include, exclude, pre, preSave, preFilter, post, getContext, pool } = opts;
|
|
7
12
|
let entities = include ?? getEntities();
|
|
@@ -11,8 +16,18 @@ export function createRequestHandler(opts) {
|
|
|
11
16
|
if (!entities.length) {
|
|
12
17
|
throw new TypeError('no entities for the uql middleware');
|
|
13
18
|
}
|
|
19
|
+
// The route is the class name, so two entities mapping one table in different schemas collide here
|
|
20
|
+
// even though nothing else about them does. All of them at once, so fixing the first collision
|
|
21
|
+
// does not just reveal the next.
|
|
22
|
+
const byPath = Map.groupBy(entities, entityPath);
|
|
23
|
+
const collisions = [...byPath].filter(([, clashing]) => clashing.length > 1);
|
|
24
|
+
if (collisions.length) {
|
|
25
|
+
const lines = collisions.map(([path, clashing]) => ` /${path} <- ${clashing.map(tableOf).join(', ')}`);
|
|
26
|
+
throw new TypeError(`every entity below shares a route with another, so all but the first are unreachable:\n${lines.join('\n')}\n` +
|
|
27
|
+
"A route is the kebab-cased class name. Rename a class, or pass only one of them in 'include'.");
|
|
28
|
+
}
|
|
14
29
|
// biome-ignore lint/suspicious/noExplicitAny: heterogeneous entity map
|
|
15
|
-
const entityByPath = new Map(
|
|
30
|
+
const entityByPath = new Map([...byPath].map(([path, [entity]]) => [path, entity]));
|
|
16
31
|
return (req) => {
|
|
17
32
|
const entity = entityByPath.get(req.entityPath);
|
|
18
33
|
if (!entity) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { dialectOptionsFrom } from '../dialect/abstractDialect.js';
|
|
1
2
|
import { AbstractHranaQuerierPool } from '../sqlite/hranaQuerierPool.js';
|
|
2
3
|
import { LibsqlDialect } from './libsqlDialect.js';
|
|
3
4
|
import { LibsqlQuerier } from './libsqlQuerier.js';
|
|
@@ -13,7 +14,7 @@ function remoteMigrationClientConfig(config) {
|
|
|
13
14
|
export class LibsqlQuerierPool extends AbstractHranaQuerierPool {
|
|
14
15
|
conf;
|
|
15
16
|
constructor(conf, extra) {
|
|
16
|
-
super(new LibsqlDialect(
|
|
17
|
+
super(new LibsqlDialect(dialectOptionsFrom(extra)), extra);
|
|
17
18
|
this.conf = conf;
|
|
18
19
|
}
|
|
19
20
|
openClient() {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createPool } from 'mariadb';
|
|
2
|
+
import { dialectOptionsFrom } from '../dialect/abstractDialect.js';
|
|
2
3
|
import { AbstractSqlQuerierPool } from '../querier/index.js';
|
|
3
4
|
import { attachPoolErrorHandler } from '../util/index.js';
|
|
4
5
|
import { MariaDialect } from './mariaDialect.js';
|
|
@@ -6,7 +7,7 @@ import { MariadbQuerier } from './mariadbQuerier.js';
|
|
|
6
7
|
export class MariadbQuerierPool extends AbstractSqlQuerierPool {
|
|
7
8
|
pool;
|
|
8
9
|
constructor(opts, extra) {
|
|
9
|
-
super(new MariaDialect(
|
|
10
|
+
super(new MariaDialect(dialectOptionsFrom(extra)), extra);
|
|
10
11
|
// `mariadb` defaults to handing BIGINT back as a BigInt, and uql maps `type: Number` to BIGINT
|
|
11
12
|
// (see `schema/canonicalType.ts`), so every auto-increment id reached a field declared `number`
|
|
12
13
|
// as `9n` without this. Same trade as the pg pools: exact to 2^53, and `...opts` wins for a
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* - MigrationBuilder: Execute DDL operations (for integration tests/runtime)
|
|
7
7
|
*/
|
|
8
8
|
import { normalizeIndexColumn } from '../../util/index.js';
|
|
9
|
+
import { derivedIndexName } from '../../util/sql.util.js';
|
|
9
10
|
import { createSchemaGenerator } from '../schemaGenerator.js';
|
|
10
11
|
import { splitSqlStatements } from './splitSqlStatements.js';
|
|
11
12
|
import { TableBuilder } from './tableBuilder.js';
|
|
@@ -26,7 +27,8 @@ function createIndexOperation(tableName, columns, options = {}) {
|
|
|
26
27
|
tableName,
|
|
27
28
|
index: {
|
|
28
29
|
...index,
|
|
29
|
-
name: name ??
|
|
30
|
+
name: name ??
|
|
31
|
+
derivedIndexName(tableName, entries.map((entry) => entry.column)),
|
|
30
32
|
entries,
|
|
31
33
|
unique: unique ?? false,
|
|
32
34
|
},
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Fluent API for defining tables in migrations.
|
|
5
5
|
*/
|
|
6
6
|
import { normalizeIndexColumn } from '../../util/index.js';
|
|
7
|
+
import { derivedIndexName } from '../../util/sql.util.js';
|
|
7
8
|
import { ColumnBuilder } from './columnBuilder.js';
|
|
8
9
|
import { t } from './expressions.js';
|
|
9
10
|
/**
|
|
@@ -190,7 +191,7 @@ export class TableBuilder {
|
|
|
190
191
|
// Collect column-level indexes
|
|
191
192
|
for (const col of columns) {
|
|
192
193
|
if (col.index) {
|
|
193
|
-
const indexName = typeof col.index === 'string' ? col.index :
|
|
194
|
+
const indexName = typeof col.index === 'string' ? col.index : derivedIndexName(this._name, [col.name]);
|
|
194
195
|
// Only add if not already in table-level indexes
|
|
195
196
|
if (!this._indexes.some((idx) => idx.name === indexName)) {
|
|
196
197
|
this._indexes.push({
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { derivedForeignKeyName } from '../../util/sql.util.js';
|
|
1
2
|
/**
|
|
2
3
|
* A migration builder's table definition as the AST nodes the generators render from, so a hand-written
|
|
3
4
|
* `createTable` and an entity reach `generateCreateTableFromNode` in the same shape. Free functions and
|
|
@@ -11,7 +12,6 @@ export function tableDefinitionToNode(def) {
|
|
|
11
12
|
columns,
|
|
12
13
|
primaryKey: [], // placeholder
|
|
13
14
|
indexes: [],
|
|
14
|
-
schema: { tables: new Map(), relationships: [], indexes: [] },
|
|
15
15
|
incomingRelations: [],
|
|
16
16
|
outgoingRelations: [],
|
|
17
17
|
comment: def.comment,
|
|
@@ -33,7 +33,7 @@ export function tableDefinitionToNode(def) {
|
|
|
33
33
|
}
|
|
34
34
|
for (const fkDef of def.foreignKeys) {
|
|
35
35
|
const relNode = {
|
|
36
|
-
name: fkDef.name ??
|
|
36
|
+
name: fkDef.name ?? derivedForeignKeyName(def.name, fkDef.columns),
|
|
37
37
|
type: 'ManyToOne', // Builder default
|
|
38
38
|
from: {
|
|
39
39
|
table,
|
|
@@ -64,7 +64,7 @@ export function fullColumnDefinitionToNode(col, tableName) {
|
|
|
64
64
|
referencedBy: [],
|
|
65
65
|
references: col.foreignKey
|
|
66
66
|
? {
|
|
67
|
-
name:
|
|
67
|
+
name: derivedForeignKeyName(tableName, [col.name]),
|
|
68
68
|
type: 'ManyToOne',
|
|
69
69
|
from: { table: { name: tableName }, columns: [] },
|
|
70
70
|
to: {
|