uql-orm 0.33.0 → 0.34.0
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 +35 -4
- package/dist/dialect/abstractSqlDialect.d.ts +21 -1
- package/dist/dialect/abstractSqlDialect.js +67 -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/generator/mongoSchemaGenerator.js +4 -4
- package/dist/migrate/migrator.js +2 -2
- package/dist/migrate/schemaGenerator.d.ts +7 -1
- package/dist/migrate/schemaGenerator.js +20 -6
- 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/schemaASTBuilder.d.ts +1 -1
- package/dist/schema/schemaASTBuilder.js +10 -9
- 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 +1 -1
- package/dist/type/querier.d.ts +5 -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,12 @@
|
|
|
1
1
|
import { applyFilters, buildQueryWhereAsMap } from '../util/dialect.util.js';
|
|
2
|
+
/**
|
|
3
|
+
* The dialect's share of a pool's {@link ExtraOptions}: what changes the SQL rather than the
|
|
4
|
+
* connection. Every pool builds its dialect through this, so a new option lands here instead of in
|
|
5
|
+
* each of the thirteen constructors.
|
|
6
|
+
*/
|
|
7
|
+
export function dialectOptionsFrom(extra) {
|
|
8
|
+
return { namingStrategy: extra?.namingStrategy, schema: extra?.schema };
|
|
9
|
+
}
|
|
2
10
|
/**
|
|
3
11
|
* Base abstract class for all database dialects (SQL and NoSQL).
|
|
4
12
|
*/
|
|
@@ -25,15 +33,38 @@ export class AbstractDialect {
|
|
|
25
33
|
return this.#features;
|
|
26
34
|
}
|
|
27
35
|
/**
|
|
28
|
-
*
|
|
36
|
+
* The table's own name, unqualified, applying naming strategy if necessary. Also the alias the
|
|
37
|
+
* root table gets once {@link resolveTableName} qualifies it, so it has to stay a single
|
|
38
|
+
* identifier: columns are prefixed with it, and a dotted prefix escapes as one identifier that
|
|
39
|
+
* nothing declared.
|
|
29
40
|
*/
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
41
|
+
resolveTableAlias(meta) {
|
|
42
|
+
const className = meta.entity.name;
|
|
43
|
+
const name = meta.name ?? className;
|
|
44
|
+
if (name !== className || !this.namingStrategy) {
|
|
33
45
|
return name;
|
|
34
46
|
}
|
|
35
47
|
return this.namingStrategy.tableName(name);
|
|
36
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Where the table lives: {@link resolveTableAlias} behind its schema, when one applies. The schema
|
|
51
|
+
* skips the naming strategy - it names an object the author wrote out, and snake_casing `myCrm`
|
|
52
|
+
* would point at one that does not exist.
|
|
53
|
+
*/
|
|
54
|
+
resolveTableName(meta) {
|
|
55
|
+
const name = this.resolveTableAlias(meta);
|
|
56
|
+
const schema = this.resolveSchema(meta);
|
|
57
|
+
return schema ? `${schema}.${name}` : name;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Which schema an entity is read from: its own wins over the pool's default, the way its `name`
|
|
61
|
+
* wins over the naming strategy. `undefined` is an answer, not a gap - the table stays unqualified
|
|
62
|
+
* and resolves through the connection's `search_path`, which is what every deployment predating
|
|
63
|
+
* this option relies on.
|
|
64
|
+
*/
|
|
65
|
+
resolveSchema(meta) {
|
|
66
|
+
return this.features.schemas ? (meta.schema ?? this.options.schema) : undefined;
|
|
67
|
+
}
|
|
37
68
|
/**
|
|
38
69
|
* Resolve the column/field name for a property, applying naming strategy if necessary.
|
|
39
70
|
*/
|
|
@@ -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;
|
|
@@ -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,29 @@ 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
|
+
const alias = this.escapeId(this.resolveTableAlias(meta), true);
|
|
199
|
+
const schema = this.resolveSchema(meta);
|
|
200
|
+
return schema ? `${this.escapeId(schema, true)}.${alias}` : alias;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* A FROM or JOIN operand plus the alias to prefix its columns by, aliased only once a schema puts
|
|
204
|
+
* something in front of the name. See {@link resolveTableAlias} for why the prefix cannot be the
|
|
205
|
+
* qualified path.
|
|
206
|
+
*/
|
|
207
|
+
tableRef(meta) {
|
|
208
|
+
const alias = this.resolveTableAlias(meta);
|
|
209
|
+
const name = this.escapedTableName(meta);
|
|
210
|
+
const schema = this.resolveSchema(meta);
|
|
211
|
+
return { alias, ref: schema ? `${name} ${this.escapeId(alias, true)}` : name };
|
|
185
212
|
}
|
|
186
213
|
/** Columns are alias-qualified once anything else is in play: a join, or a to-many being filled. */
|
|
187
214
|
resolveRelationAwarePrefix(tableName, meta, opts, populate, joins) {
|
|
@@ -198,11 +225,11 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
198
225
|
this.selectFields(ctx, join.entity, join.query.$select, { prefix: join.path, autoPrefixAlias: true }, join.query.$exclude);
|
|
199
226
|
}
|
|
200
227
|
}
|
|
201
|
-
selectRelationJoins(ctx, meta,
|
|
228
|
+
selectRelationJoins(ctx, meta, rootAlias, joins) {
|
|
202
229
|
for (const join of joins.values()) {
|
|
203
230
|
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.
|
|
231
|
+
const parentAlias = join.parent ? this.escapeId(join.parent.path, true) : this.escapeId(rootAlias, true);
|
|
232
|
+
ctx.append(` ${join.required ? 'INNER' : 'LEFT'} JOIN ${this.escapedTableName(join.meta)} ${joinAlias} ON `);
|
|
206
233
|
join.relation.references.forEach((reference, index) => {
|
|
207
234
|
if (index > 0)
|
|
208
235
|
ctx.append(' AND ');
|
|
@@ -264,11 +291,13 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
264
291
|
if (val instanceof QueryRaw) {
|
|
265
292
|
if (key === '$exists' || key === '$nexists') {
|
|
266
293
|
ctx.append(key === '$exists' ? 'EXISTS (' : 'NOT EXISTS (');
|
|
267
|
-
|
|
294
|
+
// The alias: the enclosing statement declares one, and Postgres forbids reaching past it
|
|
295
|
+
// to the qualified name it aliased.
|
|
296
|
+
const alias = this.resolveTableAlias(meta);
|
|
268
297
|
this.getRawValue(ctx, {
|
|
269
298
|
value: val,
|
|
270
|
-
prefix:
|
|
271
|
-
escapedPrefix: this.escapeId(
|
|
299
|
+
prefix: alias,
|
|
300
|
+
escapedPrefix: this.escapeId(alias, true, true),
|
|
272
301
|
});
|
|
273
302
|
ctx.append(')');
|
|
274
303
|
return;
|
|
@@ -735,7 +764,8 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
735
764
|
}
|
|
736
765
|
this.assertLockSupported(entity, q, joins);
|
|
737
766
|
const meta = getMeta(entity);
|
|
738
|
-
|
|
767
|
+
// `OF` names the alias in the FROM, never the schema-qualified path it was aliased from.
|
|
768
|
+
const target = joins.size > 0 ? ` OF ${this.escapeId(this.resolveTableAlias(meta), true)}` : '';
|
|
739
769
|
const suffix = wait === 'skip' ? ' SKIP LOCKED' : wait === 'nowait' ? ' NOWAIT' : '';
|
|
740
770
|
ctx.append(` FOR UPDATE${target}${suffix}`);
|
|
741
771
|
}
|
|
@@ -757,7 +787,7 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
757
787
|
]);
|
|
758
788
|
aggregate(ctx, entity, q, opts = {}) {
|
|
759
789
|
const meta = getMeta(entity);
|
|
760
|
-
const tableName = this.
|
|
790
|
+
const tableName = this.escapedTableName(meta);
|
|
761
791
|
const groupKeys = [];
|
|
762
792
|
const selectParts = [];
|
|
763
793
|
// Every column the statement emits, mapped to the SQL that references it. `$having` and `$sort`
|
|
@@ -787,7 +817,7 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
787
817
|
if (!selectParts.length) {
|
|
788
818
|
throw new TypeError('aggregate requires at least one $group column or $agg function');
|
|
789
819
|
}
|
|
790
|
-
ctx.append(`SELECT ${selectParts.join(', ')} FROM ${
|
|
820
|
+
ctx.append(`SELECT ${selectParts.join(', ')} FROM ${tableName}`);
|
|
791
821
|
this.where(ctx, entity, q.$where, opts);
|
|
792
822
|
if (groupKeys.length) {
|
|
793
823
|
ctx.append(` GROUP BY ${groupKeys.join(', ')}`);
|
|
@@ -893,8 +923,8 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
893
923
|
columns[i] = this.escapedColumnName(meta, key);
|
|
894
924
|
kinds[i] = this.persistKind(field);
|
|
895
925
|
}
|
|
896
|
-
const tableName = this.
|
|
897
|
-
ctx.append(`INSERT INTO ${
|
|
926
|
+
const tableName = this.escapedTableName(meta);
|
|
927
|
+
ctx.append(`INSERT INTO ${tableName} (${columns.join(', ')}) VALUES (`);
|
|
898
928
|
for (let r = 0; r < payloads.length; r++) {
|
|
899
929
|
if (r > 0) {
|
|
900
930
|
ctx.append('), (');
|
|
@@ -931,8 +961,8 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
931
961
|
const meta = getMeta(entity);
|
|
932
962
|
const [filledPayload] = fillOnFields(meta, payload, 'onUpdate');
|
|
933
963
|
const keys = filterFieldKeys(meta, filledPayload, 'onUpdate');
|
|
934
|
-
const tableName = this.
|
|
935
|
-
ctx.append(`UPDATE ${
|
|
964
|
+
const tableName = this.escapedTableName(meta);
|
|
965
|
+
ctx.append(`UPDATE ${tableName} SET `);
|
|
936
966
|
for (let i = 0; i < keys.length; i++) {
|
|
937
967
|
if (i > 0) {
|
|
938
968
|
ctx.append(', ');
|
|
@@ -1014,14 +1044,14 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
1014
1044
|
}
|
|
1015
1045
|
delete(ctx, entity, q, opts = {}) {
|
|
1016
1046
|
const meta = getMeta(entity);
|
|
1017
|
-
const tableName = this.
|
|
1047
|
+
const tableName = this.escapedTableName(meta);
|
|
1018
1048
|
// Soft-delete (stamp only live rows) unless `hardDelete` is requested or the entity has no
|
|
1019
1049
|
// soft-delete field (e.g. a cascade onto a non-soft-deletable child).
|
|
1020
1050
|
if (!opts.hardDelete && meta.softDelete) {
|
|
1021
1051
|
const field = meta.fields[meta.softDelete];
|
|
1022
1052
|
if (field) {
|
|
1023
1053
|
const columnName = this.resolveColumnName(meta.softDelete, field);
|
|
1024
|
-
ctx.append(`UPDATE ${
|
|
1054
|
+
ctx.append(`UPDATE ${tableName} SET ${this.escapeId(columnName)} = `);
|
|
1025
1055
|
this.formatPersistableValue(ctx, field, getSoftDeleteValue(field));
|
|
1026
1056
|
this.search(ctx, entity, q, opts);
|
|
1027
1057
|
return;
|
|
@@ -1029,7 +1059,7 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
1029
1059
|
}
|
|
1030
1060
|
// Hard delete removes matching rows regardless of soft-delete state (keeps other filters, e.g. tenant).
|
|
1031
1061
|
// Only rewrite the filters when there is a soft-delete filter to disable.
|
|
1032
|
-
ctx.append(`DELETE FROM ${
|
|
1062
|
+
ctx.append(`DELETE FROM ${tableName}`);
|
|
1033
1063
|
this.search(ctx, entity, q, meta.softDelete ? { ...opts, filters: withoutSoftDeleteFilter(opts.filters) } : opts);
|
|
1034
1064
|
}
|
|
1035
1065
|
escapeId(val, forbidQualified, addDot) {
|
|
@@ -1353,38 +1383,39 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
1353
1383
|
*/
|
|
1354
1384
|
appendRelationSubquery(ctx, entity, rel, opts, projection, val) {
|
|
1355
1385
|
const meta = getMeta(entity);
|
|
1356
|
-
|
|
1386
|
+
// Aliases, not paths, everywhere a column is prefixed; `tableRef` declares them in the FROM.
|
|
1387
|
+
const parentAlias = this.resolveTableAlias(meta);
|
|
1357
1388
|
const references = rel.references;
|
|
1358
|
-
const escapedParentId = this.escapedParentColumn(
|
|
1389
|
+
const escapedParentId = this.escapedParentColumn(parentAlias, meta, opts, meta.id);
|
|
1359
1390
|
const relatedEntity = rel.entity();
|
|
1360
1391
|
const relatedMeta = getMeta(relatedEntity);
|
|
1361
|
-
const
|
|
1392
|
+
const { alias: relatedAlias, ref: relatedRef } = this.tableRef(relatedMeta);
|
|
1362
1393
|
// Resolved before any SQL is emitted: it also decides whether the mm form reaches the target.
|
|
1363
1394
|
const targetWhere = this.scopedWhereMap(relatedMeta, val);
|
|
1364
1395
|
ctx.append(`(SELECT ${projection} FROM `);
|
|
1365
1396
|
if (rel.cardinality === 'mm' && rel.through) {
|
|
1366
1397
|
const throughEntity = rel.through();
|
|
1367
1398
|
const throughMeta = getMeta(throughEntity);
|
|
1368
|
-
const
|
|
1369
|
-
ctx.append(
|
|
1370
|
-
ctx.append(` WHERE ${this.escapedColumn(
|
|
1399
|
+
const { alias: throughAlias, ref: throughRef } = this.tableRef(throughMeta);
|
|
1400
|
+
ctx.append(throughRef);
|
|
1401
|
+
ctx.append(` WHERE ${this.escapedColumn(throughAlias, throughMeta, references[0].local)} = ${escapedParentId}`);
|
|
1371
1402
|
// The junction is a row being read too: a soft-deleted link is not a link.
|
|
1372
|
-
this.where(ctx, throughEntity, {}, { prefix:
|
|
1403
|
+
this.where(ctx, throughEntity, {}, { prefix: throughAlias, clause: 'AND' });
|
|
1373
1404
|
if (hasKeys(targetWhere)) {
|
|
1374
|
-
ctx.append(` AND ${this.escapedColumn(
|
|
1375
|
-
ctx.append(`SELECT ${this.escapedColumn(
|
|
1376
|
-
this.renderWhere(ctx, relatedEntity, targetWhere, { prefix:
|
|
1405
|
+
ctx.append(` AND ${this.escapedColumn(throughAlias, throughMeta, references[1].local)} IN (`);
|
|
1406
|
+
ctx.append(`SELECT ${this.escapedColumn(relatedAlias, relatedMeta, relatedMeta.id)} FROM ${relatedRef}`);
|
|
1407
|
+
this.renderWhere(ctx, relatedEntity, targetWhere, { prefix: relatedAlias, clause: 'WHERE' });
|
|
1377
1408
|
ctx.append(')');
|
|
1378
1409
|
}
|
|
1379
1410
|
}
|
|
1380
1411
|
else {
|
|
1381
|
-
const joinLeft = this.escapedColumn(
|
|
1412
|
+
const joinLeft = this.escapedColumn(relatedAlias, relatedMeta, references[0].foreign);
|
|
1382
1413
|
const joinRight = rel.cardinality === '1m'
|
|
1383
1414
|
? escapedParentId
|
|
1384
|
-
: this.escapedParentColumn(
|
|
1385
|
-
ctx.append(
|
|
1415
|
+
: this.escapedParentColumn(parentAlias, meta, opts, references[0].local);
|
|
1416
|
+
ctx.append(relatedRef);
|
|
1386
1417
|
ctx.append(` WHERE ${joinLeft} = ${joinRight}`);
|
|
1387
|
-
this.renderWhere(ctx, relatedEntity, targetWhere, { prefix:
|
|
1418
|
+
this.renderWhere(ctx, relatedEntity, targetWhere, { prefix: relatedAlias, clause: 'AND' });
|
|
1388
1419
|
}
|
|
1389
1420
|
ctx.append(')');
|
|
1390
1421
|
}
|
|
@@ -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
|
|
@@ -22,18 +22,18 @@ export class MongoSchemaGenerator extends AbstractDialect {
|
|
|
22
22
|
return this.selected(entities, options?.only).flatMap((entity) => this.generateCreateTable(entity, options));
|
|
23
23
|
}
|
|
24
24
|
generateDropSchema(entities) {
|
|
25
|
-
return this.selected(entities).map((entity) => this.generateDropTable(this.resolveTableName(
|
|
25
|
+
return this.selected(entities).map((entity) => this.generateDropTable(this.resolveTableName(getMeta(entity))));
|
|
26
26
|
}
|
|
27
27
|
selected(entities, only) {
|
|
28
28
|
if (!only) {
|
|
29
29
|
return entities;
|
|
30
30
|
}
|
|
31
31
|
const wanted = new Set(only);
|
|
32
|
-
return entities.filter((entity) => wanted.has(this.resolveTableName(
|
|
32
|
+
return entities.filter((entity) => wanted.has(this.resolveTableName(getMeta(entity))));
|
|
33
33
|
}
|
|
34
34
|
generateCreateTable(entity, _options) {
|
|
35
35
|
const meta = getMeta(entity);
|
|
36
|
-
const collectionName = this.resolveTableName(
|
|
36
|
+
const collectionName = this.resolveTableName(meta);
|
|
37
37
|
const indexes = [];
|
|
38
38
|
for (const key of getKeys(meta.fields)) {
|
|
39
39
|
const field = meta.fields[key];
|
|
@@ -132,7 +132,7 @@ export class MongoSchemaGenerator extends AbstractDialect {
|
|
|
132
132
|
}
|
|
133
133
|
diffSchema(entity, currentTable) {
|
|
134
134
|
const meta = getMeta(entity);
|
|
135
|
-
const collectionName = this.resolveTableName(
|
|
135
|
+
const collectionName = this.resolveTableName(meta);
|
|
136
136
|
if (!currentTable) {
|
|
137
137
|
return { tableName: collectionName, type: 'create' };
|
|
138
138
|
}
|
package/dist/migrate/migrator.js
CHANGED
|
@@ -283,7 +283,7 @@ export class Migrator {
|
|
|
283
283
|
const diffs = [];
|
|
284
284
|
for (const entity of this.entities) {
|
|
285
285
|
const meta = getMeta(entity);
|
|
286
|
-
const tableName = this.schemaGenerator.resolveTableName(
|
|
286
|
+
const tableName = this.schemaGenerator.resolveTableName(meta);
|
|
287
287
|
const currentTable = ast.getTable(tableName);
|
|
288
288
|
const diff = this.schemaGenerator.diffSchema(entity, currentTable);
|
|
289
289
|
if (diff) {
|
|
@@ -299,7 +299,7 @@ export class Migrator {
|
|
|
299
299
|
}
|
|
300
300
|
for (const entity of this.entities) {
|
|
301
301
|
const meta = getMeta(entity);
|
|
302
|
-
const name = this.schemaGenerator.resolveTableName(
|
|
302
|
+
const name = this.schemaGenerator.resolveTableName(meta);
|
|
303
303
|
if (name === tableName) {
|
|
304
304
|
return entity;
|
|
305
305
|
}
|
|
@@ -13,7 +13,7 @@ export declare class SqlSchemaGenerator implements SqlDdlGenerator {
|
|
|
13
13
|
constructor(dialect: AbstractSqlDialect, defaultForeignKeyAction?: ForeignKeyAction);
|
|
14
14
|
get namingStrategy(): NamingStrategy | undefined;
|
|
15
15
|
get features(): DialectFeatures;
|
|
16
|
-
resolveTableName<E>(
|
|
16
|
+
resolveTableName<E>(meta: EntityMeta<E>): string;
|
|
17
17
|
resolveColumnName(key: string, field: FieldOptions): string;
|
|
18
18
|
/**
|
|
19
19
|
* Escape an identifier (table name, column name, etc.)
|
|
@@ -38,6 +38,12 @@ export declare class SqlSchemaGenerator implements SqlDdlGenerator {
|
|
|
38
38
|
* foreign key in, but it resolves targets lazily, so a forward reference is fine there.
|
|
39
39
|
*/
|
|
40
40
|
generateCreateSchema(entities: readonly Type<unknown>[], options?: CreateSchemaOptions): string[];
|
|
41
|
+
/**
|
|
42
|
+
* One statement per distinct schema the entities name, in first-seen order. `resolveSchema` is
|
|
43
|
+
* already `undefined` on an engine without schemas, so this is empty there, and empty in the
|
|
44
|
+
* ordinary case where nothing named one.
|
|
45
|
+
*/
|
|
46
|
+
private generateCreateSchemas;
|
|
41
47
|
generateDropSchema(entities: readonly Type<unknown>[], options?: DropSchemaOptions): string[];
|
|
42
48
|
/**
|
|
43
49
|
* The tables of `entities` in dependency order, optionally narrowed to `only`. The AST always spans
|
|
@@ -23,8 +23,8 @@ export class SqlSchemaGenerator {
|
|
|
23
23
|
get features() {
|
|
24
24
|
return this.dialect.features;
|
|
25
25
|
}
|
|
26
|
-
resolveTableName(
|
|
27
|
-
return this.dialect.resolveTableName(
|
|
26
|
+
resolveTableName(meta) {
|
|
27
|
+
return this.dialect.resolveTableName(meta);
|
|
28
28
|
}
|
|
29
29
|
resolveColumnName(key, field) {
|
|
30
30
|
return this.dialect.resolveColumnName(key, field);
|
|
@@ -65,7 +65,10 @@ export class SqlSchemaGenerator {
|
|
|
65
65
|
// Inline only where a constraint cannot be added afterwards, which is what makes the cyclic case
|
|
66
66
|
// work everywhere else.
|
|
67
67
|
const inline = withForeignKeys && !this.features.foreignKeyAlter;
|
|
68
|
-
|
|
68
|
+
// Namespaces first: a qualified `CREATE TABLE` fails against a schema nobody created, and the
|
|
69
|
+
// schema is the one part of the layout a migration cannot infer from the table it is making.
|
|
70
|
+
const statements = this.generateCreateSchemas(entities);
|
|
71
|
+
statements.push(...tables.flatMap((table) => this.generateCreateTableFromNode(inline ? table : { ...table, outgoingRelations: [] }, options)));
|
|
69
72
|
if (withForeignKeys && !inline) {
|
|
70
73
|
for (const table of tables) {
|
|
71
74
|
for (const rel of table.outgoingRelations) {
|
|
@@ -82,6 +85,17 @@ export class SqlSchemaGenerator {
|
|
|
82
85
|
}
|
|
83
86
|
return statements;
|
|
84
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* One statement per distinct schema the entities name, in first-seen order. `resolveSchema` is
|
|
90
|
+
* already `undefined` on an engine without schemas, so this is empty there, and empty in the
|
|
91
|
+
* ordinary case where nothing named one.
|
|
92
|
+
*/
|
|
93
|
+
generateCreateSchemas(entities) {
|
|
94
|
+
const named = entities
|
|
95
|
+
.map((entity) => this.dialect.resolveSchema(getMeta(entity)))
|
|
96
|
+
.filter((it) => it !== undefined);
|
|
97
|
+
return [...new Set(named)].map((schema) => this.dialect.createSchemaSql(schema));
|
|
98
|
+
}
|
|
85
99
|
generateDropSchema(entities, options = {}) {
|
|
86
100
|
return this.orderedTables(entities, 'drop').map((table) => this.generateDropTable(table.name, options));
|
|
87
101
|
}
|
|
@@ -302,7 +316,7 @@ export class SqlSchemaGenerator {
|
|
|
302
316
|
const meta = getMeta(entity);
|
|
303
317
|
if (!currentTable) {
|
|
304
318
|
return {
|
|
305
|
-
tableName: this.dialect.resolveTableName(
|
|
319
|
+
tableName: this.dialect.resolveTableName(meta),
|
|
306
320
|
type: 'create',
|
|
307
321
|
};
|
|
308
322
|
}
|
|
@@ -340,7 +354,7 @@ export class SqlSchemaGenerator {
|
|
|
340
354
|
return undefined;
|
|
341
355
|
}
|
|
342
356
|
return {
|
|
343
|
-
tableName: this.dialect.resolveTableName(
|
|
357
|
+
tableName: this.dialect.resolveTableName(meta),
|
|
344
358
|
type: 'alter',
|
|
345
359
|
columnsToAdd: columnsToAdd.length > 0 ? columnsToAdd : undefined,
|
|
346
360
|
columnsToAlter: columnsToAlter.length > 0 ? columnsToAlter : undefined,
|
|
@@ -581,7 +595,7 @@ export class SqlSchemaGenerator {
|
|
|
581
595
|
*/
|
|
582
596
|
export function buildEntityAST(generator, entities, defaultForeignKeyAction) {
|
|
583
597
|
return buildSchemaAST(entities, {
|
|
584
|
-
resolveTableName: (
|
|
598
|
+
resolveTableName: (meta) => generator.resolveTableName(meta),
|
|
585
599
|
resolveColumnName: (key, field) => generator.resolveColumnName(key, field),
|
|
586
600
|
defaultForeignKeyAction,
|
|
587
601
|
});
|
|
@@ -11,6 +11,7 @@ export const mongoDialectFeatures = {
|
|
|
11
11
|
supportsJsonb: false,
|
|
12
12
|
ifNotExists: false,
|
|
13
13
|
indexIfNotExists: false,
|
|
14
|
+
schemas: false, // the connection picks the database, and a collection name takes no dot
|
|
14
15
|
dropTableCascade: false,
|
|
15
16
|
renameColumn: false,
|
|
16
17
|
foreignKeyAlter: false,
|
|
@@ -156,7 +157,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
156
157
|
? this.junctionLookup(relOpts, relMeta, relEntity, targetScope, temp, tail, opts)
|
|
157
158
|
: {
|
|
158
159
|
$lookup: {
|
|
159
|
-
from: this.resolveTableName(
|
|
160
|
+
from: this.resolveTableName(relMeta),
|
|
160
161
|
...this.joinKeys(meta, relMeta, relOpts),
|
|
161
162
|
pipeline: [...(hasKeys(targetScope) ? [{ $match: targetScope }] : []), ...tail],
|
|
162
163
|
as: temp,
|
|
@@ -177,14 +178,14 @@ export class MongoDialect extends AbstractDialect {
|
|
|
177
178
|
const nested = MongoDialect.REL_NESTED_KEY;
|
|
178
179
|
return {
|
|
179
180
|
$lookup: {
|
|
180
|
-
from: this.resolveTableName(
|
|
181
|
+
from: this.resolveTableName(throughMeta),
|
|
181
182
|
localField: MongoDialect.ID_KEY,
|
|
182
183
|
foreignField: this.columnOf(throughMeta, relOpts.references[0].local),
|
|
183
184
|
pipeline: [
|
|
184
185
|
...(hasKeys(junctionScope) ? [{ $match: junctionScope }] : []),
|
|
185
186
|
{
|
|
186
187
|
$lookup: {
|
|
187
|
-
from: this.resolveTableName(
|
|
188
|
+
from: this.resolveTableName(relMeta),
|
|
188
189
|
localField: this.columnOf(throughMeta, relOpts.references[1].local),
|
|
189
190
|
foreignField: MongoDialect.ID_KEY,
|
|
190
191
|
pipeline: [...(hasKeys(targetScope) ? [{ $match: targetScope }] : []), { $limit: 1 }],
|
|
@@ -575,7 +576,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
575
576
|
];
|
|
576
577
|
pipeline.push({
|
|
577
578
|
$lookup: {
|
|
578
|
-
from: this.resolveTableName(join.
|
|
579
|
+
from: this.resolveTableName(join.meta),
|
|
579
580
|
...this.joinKeys(parentMeta, join.meta, join.relation),
|
|
580
581
|
...(lookupPipeline.length ? { pipeline: lookupPipeline } : {}),
|
|
581
582
|
as: join.key,
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { MongoClient } from 'mongodb';
|
|
2
|
+
import { dialectOptionsFrom } from '../dialect/abstractDialect.js';
|
|
2
3
|
import { AbstractQuerierPool } from '../querier/index.js';
|
|
3
4
|
import { MongodbNativeDialect } from './mongodbNativeDialect.js';
|
|
4
5
|
import { MongodbQuerier } from './mongodbQuerier.js';
|
|
5
6
|
export class MongodbQuerierPool extends AbstractQuerierPool {
|
|
6
7
|
client;
|
|
7
8
|
constructor(uri, opts, extra) {
|
|
8
|
-
super(new MongodbNativeDialect(
|
|
9
|
+
super(new MongodbNativeDialect(dialectOptionsFrom(extra)), extra);
|
|
9
10
|
this.client = new MongoClient(uri, opts);
|
|
10
11
|
}
|
|
11
12
|
async getQuerier() {
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { createPool } from 'mysql2/promise';
|
|
2
|
+
import { dialectOptionsFrom } from '../dialect/abstractDialect.js';
|
|
2
3
|
import { AbstractSqlQuerierPool } from '../querier/index.js';
|
|
3
4
|
import { MySql2Dialect } from './mysql2Dialect.js';
|
|
4
5
|
import { MySql2Querier } from './mysql2Querier.js';
|
|
5
6
|
export class MySql2QuerierPool extends AbstractSqlQuerierPool {
|
|
6
7
|
pool;
|
|
7
8
|
constructor(opts, extra) {
|
|
8
|
-
super(new MySql2Dialect(
|
|
9
|
+
super(new MySql2Dialect(dialectOptionsFrom(extra)), extra);
|
|
9
10
|
this.pool = createPool(opts);
|
|
10
11
|
}
|
|
11
12
|
async getQuerier() {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Pool, types } from '@neondatabase/serverless';
|
|
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 { NeonDialect } from './neonDialect.js';
|
|
@@ -6,7 +7,7 @@ import { NeonQuerier } from './neonQuerier.js';
|
|
|
6
7
|
export class NeonQuerierPool extends AbstractPgQuerierPool {
|
|
7
8
|
constructor(opts, extra) {
|
|
8
9
|
// Neon's own `types`, not `pg`'s: this entry has to load on an edge runtime where `pg` is absent.
|
|
9
|
-
super(new NeonDialect(
|
|
10
|
+
super(new NeonDialect(dialectOptionsFrom(extra)), new Pool({ types: numericTypes(types), ...opts }), extra);
|
|
10
11
|
}
|
|
11
12
|
buildQuerier(connect) {
|
|
12
13
|
return new NeonQuerier(connect, this.dialect, this.extra);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { dialectOptionsFrom } from '../dialect/abstractDialect.js';
|
|
1
2
|
import { AbstractSharedHandleQuerierPool } from '../querier/abstractSharedHandleQuerierPool.js';
|
|
2
3
|
import { PgliteDialect } from './pgliteDialect.js';
|
|
3
4
|
import { PgliteQuerier } from './pgliteQuerier.js';
|
|
@@ -20,7 +21,7 @@ export class PgliteQuerierPool extends AbstractSharedHandleQuerierPool {
|
|
|
20
21
|
dataDir;
|
|
21
22
|
opts;
|
|
22
23
|
constructor(dataDir = 'memory://', opts, extra) {
|
|
23
|
-
super(new PgliteDialect(
|
|
24
|
+
super(new PgliteDialect(dialectOptionsFrom(extra)), extra);
|
|
24
25
|
this.dataDir = dataDir;
|
|
25
26
|
this.opts = opts;
|
|
26
27
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Pool, types } from 'pg';
|
|
2
|
+
import { dialectOptionsFrom } from '../dialect/abstractDialect.js';
|
|
2
3
|
import { AbstractPgQuerierPool } from './abstractPgQuerierPool.js';
|
|
3
4
|
import { PgDialect } from './pgDialect.js';
|
|
4
5
|
import { numericTypes } from './pgNumericTypes.js';
|
|
@@ -7,7 +8,7 @@ export class PgQuerierPool extends AbstractPgQuerierPool {
|
|
|
7
8
|
constructor(opts, extra) {
|
|
8
9
|
// keepAlive reduces (but can't eliminate) idle connections being silently
|
|
9
10
|
// dropped by NATs/firewalls on long-lived remote connections.
|
|
10
|
-
super(new PgDialect(
|
|
11
|
+
super(new PgDialect(dialectOptionsFrom(extra)), new Pool({ keepAlive: true, types: numericTypes(types), ...opts }), extra);
|
|
11
12
|
}
|
|
12
13
|
buildQuerier(connect) {
|
|
13
14
|
return new PgQuerier(connect, this.dialect, this.extra);
|
|
@@ -14,7 +14,7 @@ import type { ForeignKeyAction } from './types.js';
|
|
|
14
14
|
*/
|
|
15
15
|
export interface BuildSchemaASTOptions {
|
|
16
16
|
/** Custom table name resolver */
|
|
17
|
-
resolveTableName?: (
|
|
17
|
+
resolveTableName?: (meta: EntityMeta<unknown>) => string;
|
|
18
18
|
/** Custom column name resolver */
|
|
19
19
|
resolveColumnName?: (key: string, field: FieldOptions) => string;
|
|
20
20
|
/** Naming strategy to use */
|
|
@@ -19,13 +19,14 @@ export function buildSchemaAST(entities, options = {}) {
|
|
|
19
19
|
const { namingStrategy } = options;
|
|
20
20
|
const ctx = {
|
|
21
21
|
ast: new SchemaAST(),
|
|
22
|
-
resolveTableName: options.resolveTableName ??
|
|
22
|
+
resolveTableName: options.resolveTableName ??
|
|
23
|
+
((m) => namingStrategy?.tableName(m.name ?? m.entity.name) ?? m.name ?? m.entity.name),
|
|
23
24
|
resolveColumnName: options.resolveColumnName ?? ((k, f) => namingStrategy?.columnName(f.name ?? k) ?? f.name ?? k),
|
|
24
25
|
defaultForeignKeyAction: options.defaultForeignKeyAction ?? DEFAULT_FOREIGN_KEY_ACTION,
|
|
25
26
|
};
|
|
26
27
|
for (const pass of [addTableFromEntity, addRelationshipsFromEntity, addIndexesFromEntity]) {
|
|
27
28
|
for (const entity of entities) {
|
|
28
|
-
pass(ctx,
|
|
29
|
+
pass(ctx, getMeta(entity));
|
|
29
30
|
}
|
|
30
31
|
}
|
|
31
32
|
return ctx.ast;
|
|
@@ -64,8 +65,8 @@ function resolveColumnCanonicalType(field, seen = new Set()) {
|
|
|
64
65
|
/**
|
|
65
66
|
* Add a table from entity metadata.
|
|
66
67
|
*/
|
|
67
|
-
function addTableFromEntity(ctx,
|
|
68
|
-
const tableName = ctx.resolveTableName(
|
|
68
|
+
function addTableFromEntity(ctx, meta) {
|
|
69
|
+
const tableName = ctx.resolveTableName(meta);
|
|
69
70
|
const columns = new Map();
|
|
70
71
|
const primaryKey = [];
|
|
71
72
|
// Create placeholder table (will be fully initialized below)
|
|
@@ -115,8 +116,8 @@ function addTableFromEntity(ctx, entity, meta) {
|
|
|
115
116
|
/**
|
|
116
117
|
* Add relationships from entity relation decorators.
|
|
117
118
|
*/
|
|
118
|
-
function addRelationshipsFromEntity(ctx,
|
|
119
|
-
const tableName = ctx.resolveTableName(
|
|
119
|
+
function addRelationshipsFromEntity(ctx, meta) {
|
|
120
|
+
const tableName = ctx.resolveTableName(meta);
|
|
120
121
|
const table = ctx.ast.getTable(tableName);
|
|
121
122
|
if (!table)
|
|
122
123
|
return;
|
|
@@ -127,7 +128,7 @@ function addRelationshipsFromEntity(ctx, entity, meta) {
|
|
|
127
128
|
continue;
|
|
128
129
|
const relatedEntity = relation.entity();
|
|
129
130
|
const relatedMeta = getMeta(relatedEntity);
|
|
130
|
-
const relatedTableName = ctx.resolveTableName(
|
|
131
|
+
const relatedTableName = ctx.resolveTableName(relatedMeta);
|
|
131
132
|
const relatedTable = ctx.ast.getTable(relatedTableName);
|
|
132
133
|
if (!relatedTable)
|
|
133
134
|
continue;
|
|
@@ -171,8 +172,8 @@ function addRelationshipsFromEntity(ctx, entity, meta) {
|
|
|
171
172
|
* Add indexes from field options (`@Field({ index })`) and from `@Index([...])`, which have nothing
|
|
172
173
|
* in common beyond their target table.
|
|
173
174
|
*/
|
|
174
|
-
function addIndexesFromEntity(ctx,
|
|
175
|
-
const tableName = ctx.resolveTableName(
|
|
175
|
+
function addIndexesFromEntity(ctx, meta) {
|
|
176
|
+
const tableName = ctx.resolveTableName(meta);
|
|
176
177
|
const table = ctx.ast.getTable(tableName);
|
|
177
178
|
if (!table)
|
|
178
179
|
return;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { dialectOptionsFrom } from '../dialect/abstractDialect.js';
|
|
1
2
|
import { AbstractSharedHandleQuerierPool } from '../querier/abstractSharedHandleQuerierPool.js';
|
|
2
3
|
import { SqliteDialect } from './sqliteDialect.js';
|
|
3
4
|
import { SqliteQuerier } from './sqliteQuerier.js';
|
|
@@ -11,7 +12,7 @@ import { SqliteQuerier } from './sqliteQuerier.js';
|
|
|
11
12
|
export class AbstractLocalSqliteQuerierPool extends AbstractSharedHandleQuerierPool {
|
|
12
13
|
opts;
|
|
13
14
|
constructor(opts, extra) {
|
|
14
|
-
super(new SqliteDialect(
|
|
15
|
+
super(new SqliteDialect(dialectOptionsFrom(extra)), extra);
|
|
15
16
|
this.opts = opts;
|
|
16
17
|
}
|
|
17
18
|
async openDb() {
|
|
@@ -8,6 +8,7 @@ export class SqliteDialect extends AbstractSqlDialect {
|
|
|
8
8
|
supportsJsonb: false,
|
|
9
9
|
ifNotExists: true,
|
|
10
10
|
indexIfNotExists: true,
|
|
11
|
+
schemas: false, // SQLite's namespaces are attached database files, not declared objects
|
|
11
12
|
dropTableCascade: false,
|
|
12
13
|
renameColumn: true,
|
|
13
14
|
foreignKeyAlter: false, // SQLite does not support adding FKs to existing tables
|
|
@@ -70,7 +71,7 @@ export class SqliteDialect extends AbstractSqlDialect {
|
|
|
70
71
|
*/
|
|
71
72
|
appendTextSearch(ctx, entity, meta, search) {
|
|
72
73
|
const columns = search.$fields.map((key) => this.escapeId(this.resolveColumnName(key, meta.fields[key])));
|
|
73
|
-
ctx.append(`${this.
|
|
74
|
+
ctx.append(`${this.escapedTableName(meta)} MATCH {${columns.join(' ')}} : `);
|
|
74
75
|
ctx.addValue(search.$value);
|
|
75
76
|
}
|
|
76
77
|
/**
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { dialectOptionsFrom } from '../dialect/abstractDialect.js';
|
|
1
2
|
import { AbstractSharedHandleQuerierPool } from '../querier/abstractSharedHandleQuerierPool.js';
|
|
2
3
|
import { TursoDialect } from './tursoDialect.js';
|
|
3
4
|
import { TursoLocalQuerier } from './tursoLocalQuerier.js';
|
|
@@ -12,7 +13,7 @@ export class TursoLocalQuerierPool extends AbstractSharedHandleQuerierPool {
|
|
|
12
13
|
filename;
|
|
13
14
|
opts;
|
|
14
15
|
constructor(filename = ':memory:', opts, extra) {
|
|
15
|
-
super(new TursoDialect(
|
|
16
|
+
super(new TursoDialect(dialectOptionsFrom(extra)), extra);
|
|
16
17
|
this.filename = filename;
|
|
17
18
|
this.opts = opts;
|
|
18
19
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { dialectOptionsFrom } from '../dialect/abstractDialect.js';
|
|
1
2
|
import { AbstractHranaQuerierPool } from '../sqlite/hranaQuerierPool.js';
|
|
2
3
|
import { TursoDialect } from './tursoDialect.js';
|
|
3
4
|
import { TursoQuerier } from './tursoQuerier.js';
|
|
@@ -21,7 +22,7 @@ export class TursoQuerierPool extends AbstractHranaQuerierPool {
|
|
|
21
22
|
* with the same shape: `@libsql/client/web`, `@libsql/client-wasm`, or a test double.
|
|
22
23
|
*/
|
|
23
24
|
constructor(conf, extra) {
|
|
24
|
-
super(new TursoDialect(
|
|
25
|
+
super(new TursoDialect(dialectOptionsFrom(extra)), extra);
|
|
25
26
|
this.conf = conf;
|
|
26
27
|
this.ownsClient = !isClient(conf);
|
|
27
28
|
}
|
package/dist/type/dialect.d.ts
CHANGED
|
@@ -76,6 +76,12 @@ export type InsertIdSource = 'returning' | 'firstId' | 'lastId';
|
|
|
76
76
|
export interface EngineFeatures {
|
|
77
77
|
readonly ifNotExists: boolean;
|
|
78
78
|
readonly indexIfNotExists: boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Whether the engine has namespaces a table can sit behind. `false` leaves every table
|
|
81
|
+
* unqualified, whatever `schema` an entity or pool named: SQLite attaches database files and
|
|
82
|
+
* MongoDB takes its database from the connection, so neither has one to name.
|
|
83
|
+
*/
|
|
84
|
+
readonly schemas: boolean;
|
|
79
85
|
readonly dropTableCascade: boolean;
|
|
80
86
|
readonly renameColumn: boolean;
|
|
81
87
|
readonly foreignKeyAlter: boolean;
|
package/dist/type/entity.d.ts
CHANGED
|
@@ -577,6 +577,8 @@ export type EntityIndexMeta = {
|
|
|
577
577
|
export type EntityMeta<E> = {
|
|
578
578
|
readonly entity: Type<E>;
|
|
579
579
|
name?: string;
|
|
580
|
+
/** Set only when the entity named one; unset defers to the pool where it is used. See `AbstractDialect.resolveSchema`. */
|
|
581
|
+
schema?: string;
|
|
580
582
|
id: IdKey<E>;
|
|
581
583
|
softDelete?: FieldKey<E>;
|
|
582
584
|
/** Named, default-on `$where` filters applied to every query unless bypassed. */
|
|
@@ -605,6 +607,11 @@ export type EntityMeta<E> = {
|
|
|
605
607
|
*/
|
|
606
608
|
export type EntityOptions<E = unknown> = {
|
|
607
609
|
readonly name?: string;
|
|
610
|
+
/**
|
|
611
|
+
* The schema (in MySQL terms, database) this table lives in, pinning it whichever pool reads it;
|
|
612
|
+
* unset follows the pool's own. Not in `name`: a dotted `name` is rejected.
|
|
613
|
+
*/
|
|
614
|
+
readonly schema?: string;
|
|
608
615
|
/** Named, default-on `$where` filters (soft-delete is auto-registered from `@Field({ softDelete })`). */
|
|
609
616
|
readonly filters?: Record<string, FilterOptions<E>>;
|
|
610
617
|
/** Scalar fields; use `isId: true` on exactly one field for the primary key. */
|
package/dist/type/migration.d.ts
CHANGED
|
@@ -245,7 +245,7 @@ export interface SchemaGenerator {
|
|
|
245
245
|
/**
|
|
246
246
|
* Resolve table name using entity and naming strategy
|
|
247
247
|
*/
|
|
248
|
-
resolveTableName<E>(
|
|
248
|
+
resolveTableName<E>(meta: EntityMeta<E>): string;
|
|
249
249
|
/**
|
|
250
250
|
* Resolve column name using field options and naming strategy
|
|
251
251
|
*/
|
package/dist/type/querier.d.ts
CHANGED
|
@@ -164,5 +164,10 @@ export type ExtraOptions = {
|
|
|
164
164
|
/** Threshold in milliseconds - queries exceeding this are logged as slow. */
|
|
165
165
|
readonly slowQuery?: number;
|
|
166
166
|
readonly namingStrategy?: NamingStrategy;
|
|
167
|
+
/**
|
|
168
|
+
* Default schema (in MySQL terms, database) for entities naming none; unset leaves them
|
|
169
|
+
* unqualified. `@Entity({ schema })` overrides it.
|
|
170
|
+
*/
|
|
171
|
+
readonly schema?: string;
|
|
167
172
|
readonly listeners?: readonly QuerierListener[];
|
|
168
173
|
};
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"homepage": "https://uql-orm.dev",
|
|
4
4
|
"description": "JSON-native TypeScript ORM for Node.js, Bun and Deno. Supports PostgreSQL, PGlite, MySQL, MariaDB, SQLite, CockroachDB, Turso, Neon, Cloudflare D1 and MongoDB. Queries are plain JSON, typed to the leaf.",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.34.0",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"engines": {
|
|
9
9
|
"node": ">=24"
|