@fougere/adapter-sql 0.6.0-alpha.0 → 0.8.0-alpha.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/README.md +2 -2
- package/dist/adapter.schema.json +16 -0
- package/dist/check.d.ts +2 -27
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +2 -27
- package/dist/check.js.map +1 -1
- package/dist/crud.d.ts +21 -81
- package/dist/crud.d.ts.map +1 -1
- package/dist/crud.js +58 -102
- package/dist/crud.js.map +1 -1
- package/dist/ddl.d.ts +8 -53
- package/dist/ddl.d.ts.map +1 -1
- package/dist/ddl.js +16 -55
- package/dist/ddl.js.map +1 -1
- package/dist/dialect.d.ts +7 -50
- package/dist/dialect.d.ts.map +1 -1
- package/dist/dialect.js +2 -5
- package/dist/dialect.js.map +1 -1
- package/dist/diff.d.ts +6 -44
- package/dist/diff.d.ts.map +1 -1
- package/dist/diff.js +20 -50
- package/dist/diff.js.map +1 -1
- package/dist/drift.d.ts +29 -0
- package/dist/drift.d.ts.map +1 -0
- package/dist/drift.js +53 -0
- package/dist/drift.js.map +1 -0
- package/dist/fields.d.ts +10 -9
- package/dist/fields.d.ts.map +1 -1
- package/dist/fields.js +8 -1
- package/dist/fields.js.map +1 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/order.d.ts +23 -0
- package/dist/order.d.ts.map +1 -0
- package/dist/order.js +40 -0
- package/dist/order.js.map +1 -0
- package/dist/query.d.ts +2 -17
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +1 -11
- package/dist/query.js.map +1 -1
- package/dist/setup.d.ts +8 -36
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +11 -21
- package/dist/setup.js.map +1 -1
- package/dist/sqlite.d.ts.map +1 -1
- package/dist/sqlite.js +12 -20
- package/dist/sqlite.js.map +1 -1
- package/dist/step.d.ts +4 -33
- package/dist/step.d.ts.map +1 -1
- package/dist/step.js +11 -46
- package/dist/step.js.map +1 -1
- package/dist/table.d.ts +15 -77
- package/dist/table.d.ts.map +1 -1
- package/dist/table.js +34 -146
- package/dist/table.js.map +1 -1
- package/dist/values.d.ts +1 -17
- package/dist/values.d.ts.map +1 -1
- package/dist/values.js +2 -7
- package/dist/values.js.map +1 -1
- package/package.json +4 -4
- package/src/adapter.schema.json +16 -0
- package/src/check.ts +2 -27
- package/src/crud.ts +66 -105
- package/src/ddl.ts +14 -55
- package/src/dialect.ts +7 -50
- package/src/diff.ts +18 -50
- package/src/drift.ts +75 -0
- package/src/fields.ts +17 -8
- package/src/index.ts +5 -3
- package/src/order.ts +63 -0
- package/src/query.ts +2 -17
- package/src/setup.ts +19 -49
- package/src/sqlite.ts +13 -20
- package/src/step.ts +12 -54
- package/src/table.ts +42 -185
- package/src/values.ts +3 -24
package/src/table.ts
CHANGED
|
@@ -1,18 +1,8 @@
|
|
|
1
1
|
import { Lifecycle, Role } from '@fougere/schema';
|
|
2
|
-
/**
|
|
3
|
-
|
|
4
|
-
*
|
|
5
|
-
* This is the neutral middle term: one projection reads the entity's axes and
|
|
6
|
-
* produces a `TableDef`; a `Dialect` turns that into SQL. Neither half knows the
|
|
7
|
-
* other — the dialect never mentions a field. Adding a dialect touches only the
|
|
8
|
-
* second half.
|
|
9
|
-
*
|
|
10
|
-
* `ColumnDef.stated` is the one member the axes did not produce. It names one engine's
|
|
11
|
-
* column type, so dropping it leaves every column describable.
|
|
12
|
-
*/
|
|
13
|
-
import { Anatomy, FieldGroup, Unique, fieldsOf, lowerFirst, schemaOf, type Field, type SchemaView, type SchemaOrCard } from '@fougere/schema';
|
|
2
|
+
/** Entity → table description, with no SQL in sight. */
|
|
3
|
+
import { Shapes, lowerFirst, type Field, type SchemaView } from '@fougere/schema';
|
|
14
4
|
import { boundsOf, type ShapeBounds } from './check.js';
|
|
15
|
-
import type
|
|
5
|
+
import { sqlEntries, type SqlField } from './fields.js';
|
|
16
6
|
|
|
17
7
|
/** The shape keywords a dialect needs to choose a column type. */
|
|
18
8
|
export interface ColumnShape {
|
|
@@ -62,11 +52,7 @@ export interface TableDef {
|
|
|
62
52
|
columns: ColumnDef[];
|
|
63
53
|
/** PK column names when the key is composite — empty for a simple key. */
|
|
64
54
|
compositePrimary: string[];
|
|
65
|
-
/**
|
|
66
|
-
* Column groups unique together, from `entity(fields, { unique: [...] })`.
|
|
67
|
-
* A single-field group is left to the column's own `unique` — this is the
|
|
68
|
-
* table-level form, for facts no column can hold alone.
|
|
69
|
-
*/
|
|
55
|
+
/** Column groups unique together, from `entity(fields, { unique: [...] })`. */
|
|
70
56
|
uniqueGroups: string[][];
|
|
71
57
|
}
|
|
72
58
|
|
|
@@ -83,11 +69,7 @@ function isStored(field: Field): boolean {
|
|
|
83
69
|
return !Role.of(field).isCollection;
|
|
84
70
|
}
|
|
85
71
|
|
|
86
|
-
/**
|
|
87
|
-
* The target's primary key column. A live thunk (an in-process entity) answers
|
|
88
|
-
* for real; a relation reconstructed from a lone `Card` (without a `Bundle`) has lost it to a
|
|
89
|
-
* name stand-in with no `getFields` — the convention there is to assume `id`.
|
|
90
|
-
*/
|
|
72
|
+
/** The target's primary key column. */
|
|
91
73
|
function primaryColumnOf(target: Partial<SchemaView>): string {
|
|
92
74
|
if (typeof target.getFields !== 'function') return 'id';
|
|
93
75
|
for (const [name, field] of Object.entries(target.getFields())) {
|
|
@@ -96,28 +78,11 @@ function primaryColumnOf(target: Partial<SchemaView>): string {
|
|
|
96
78
|
return 'id'; // declared no primary() field — defensive, shouldn't happen
|
|
97
79
|
}
|
|
98
80
|
|
|
99
|
-
/**
|
|
100
|
-
* The FK target for a `ref()` field: the table it points at plus its PK column.
|
|
101
|
-
*
|
|
102
|
-
* `tableNameOf` is an identity map — built once per app generation pass, see
|
|
103
|
-
* {@link toTables} — from a LIVE entity class to the table name already resolved
|
|
104
|
-
* for it. Reusing that name (instead of re-deriving one from the class name) is
|
|
105
|
-
* what keeps a custom `tableName` resolver honest: `demos/schema-ecommerce`
|
|
106
|
-
* names `Category`'s table `"categories"` (an irregular plural its resolver
|
|
107
|
-
* special-cases) — re-deriving from `Category.name` through the DEFAULT
|
|
108
|
-
* convention would silently produce `"categorys"` instead.
|
|
109
|
-
*
|
|
110
|
-
* A miss (the target isn't part of this batch — a cross-frond target, or a live
|
|
111
|
-
* class the app substituted for one a package hardcoded — e.g.
|
|
112
|
-
* `@fougere/auth-better`'s `AuthSession` always points at its own default
|
|
113
|
-
* `AuthUser`, never at whatever `opts.user` the app actually registered) falls
|
|
114
|
-
* back to deriving the name from the class — correct when that class follows
|
|
115
|
-
* the default convention, wrong if the app ALSO overrides `tableName` for it.
|
|
116
|
-
*/
|
|
81
|
+
/** The FK target for a `ref()` field. */
|
|
117
82
|
function referenceFor(
|
|
118
83
|
field: Field,
|
|
119
84
|
resolve: (name: string) => string,
|
|
120
|
-
tableNameOf?: Map<
|
|
85
|
+
tableNameOf?: Map<SchemaView, string>,
|
|
121
86
|
hosted?: HostedNames,
|
|
122
87
|
): ColumnReference | undefined {
|
|
123
88
|
const relation = Role.of(field).relation;
|
|
@@ -156,14 +121,14 @@ function toColumn(
|
|
|
156
121
|
fieldName: string,
|
|
157
122
|
field: Field,
|
|
158
123
|
resolve: (name: string) => string,
|
|
159
|
-
tableNameOf?: Map<
|
|
124
|
+
tableNameOf?: Map<SchemaView, string>,
|
|
160
125
|
hosted?: HostedNames,
|
|
161
126
|
stated?: SqlField,
|
|
162
127
|
): ColumnDef {
|
|
163
128
|
// The column type comes from the `shape` axis alone. `anatomy` strips the
|
|
164
129
|
// nullable union so a nullable integer stays an integer instead of falling
|
|
165
130
|
// through to text.
|
|
166
|
-
const { base, nullable } =
|
|
131
|
+
const { base, nullable } = Shapes.of(field.shape);
|
|
167
132
|
const lifecycle = Lifecycle.of(field);
|
|
168
133
|
const column: ColumnDef = {
|
|
169
134
|
field: fieldName,
|
|
@@ -180,8 +145,7 @@ function toColumn(
|
|
|
180
145
|
// redundant constraint on every engine. So would indexing what `unique` constrains.
|
|
181
146
|
// Only a constraint of ONE becomes a column constraint; a group of several is a table
|
|
182
147
|
// constraint, emitted once from `uniqueGroups` rather than once per member column.
|
|
183
|
-
|
|
184
|
-
if (soleUnique && !column.primary) column.unique = true;
|
|
148
|
+
if (Role.of(field).isUnique && !column.primary) column.unique = true;
|
|
185
149
|
if (Role.of(field).isIndexed && !column.primary && !column.unique) column.index = true;
|
|
186
150
|
const references = referenceFor(field, resolve, tableNameOf, hosted);
|
|
187
151
|
if (references) column.references = references;
|
|
@@ -194,7 +158,7 @@ export interface RelationResolve {
|
|
|
194
158
|
/** Same resolver used for every entity's own table (default or a custom `tableName`). */
|
|
195
159
|
resolve: (name: string) => string;
|
|
196
160
|
/** Live entity class → its already-resolved table name, reused instead of re-derived. */
|
|
197
|
-
tableNameOf?: Map<
|
|
161
|
+
tableNameOf?: Map<SchemaView, string>;
|
|
198
162
|
/** Which entities this batch holds and which live in another source — decided by NAME. */
|
|
199
163
|
hosted?: HostedNames;
|
|
200
164
|
}
|
|
@@ -207,46 +171,29 @@ export interface HostedNames {
|
|
|
207
171
|
elsewhere: ReadonlySet<string>;
|
|
208
172
|
}
|
|
209
173
|
|
|
210
|
-
/**
|
|
211
|
-
|
|
212
|
-
*
|
|
213
|
-
* Takes the entity as a live class or as a card. Read ONCE into `fields`: `fieldsOf`
|
|
214
|
-
* reconstructs a descriptor on each call, so re-reading per loop would rebuild the schema
|
|
215
|
-
* as many times as this function iterates.
|
|
216
|
-
*
|
|
217
|
-
* A lone card has no live relation targets, so a `ref()` falls back to the conventions
|
|
218
|
-
* `referenceFor`/`primaryColumnOf` already document (name-derived table, `id` as the key).
|
|
219
|
-
* Pass a descriptor through `Bundle.toSchemas` first when the FKs matter — it resolves the
|
|
220
|
-
* targets, and its output is the live-class case again.
|
|
221
|
-
*/
|
|
222
|
-
export function toTable(tableName: string, entity: SchemaOrCard, relations?: RelationResolve): TableDef {
|
|
174
|
+
/** Describe one entity as a table — the single reader of the axes. */
|
|
175
|
+
export function toTable(tableName: string, schema: SchemaView, relations?: RelationResolve): TableDef {
|
|
223
176
|
const resolve = relations?.resolve ?? toTableName;
|
|
224
|
-
const fields =
|
|
177
|
+
const fields = schema.getFields();
|
|
225
178
|
// Read off the entity, since that is where it is declared and addressed by field key.
|
|
226
|
-
const
|
|
179
|
+
const configuration = schema.getAdapters().sql;
|
|
180
|
+
// Judged HERE and not at `entity()`: this runs at boot, after every import, so the
|
|
181
|
+
// format is always loaded. A validator registered with `schema` would depend on which
|
|
182
|
+
// module was imported first.
|
|
183
|
+
sqlEntries.assert(configuration, `${schema.name}.adapters.sql`);
|
|
227
184
|
const columns: ColumnDef[] = [];
|
|
228
185
|
for (const [fieldName, field] of Object.entries(fields)) {
|
|
229
186
|
if (!isStored(field)) continue;
|
|
230
|
-
columns.push(toColumn(fieldName, field, resolve, relations?.tableNameOf, relations?.hosted,
|
|
187
|
+
columns.push(toColumn(fieldName, field, resolve, relations?.tableNameOf, relations?.hosted, configuration?.[fieldName]));
|
|
231
188
|
}
|
|
232
189
|
const primaries = columns.filter((column) => column.primary).map((column) => column.name);
|
|
233
190
|
const stored = new Set(columns.map((column) => column.name));
|
|
234
|
-
//
|
|
235
|
-
//
|
|
236
|
-
//
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const groups = new Map<string, string[]>();
|
|
241
|
-
for (const [fieldName, field] of Object.entries(fields)) {
|
|
242
|
-
for (const group of FieldGroup.on(field, Unique)) {
|
|
243
|
-
const members = group.resolvedOn(fieldName).members.map(toSnakeCase);
|
|
244
|
-
if (members.length > 1 && members.every((column) => stored.has(column))) {
|
|
245
|
-
groups.set(members.join(' '), members);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
const uniqueGroups = [...groups.values()];
|
|
191
|
+
// Declared in field names, realized in column names — and a group naming a field the
|
|
192
|
+
// storage does not keep is not enforceable, so it is dropped rather than emitted against
|
|
193
|
+
// a column that will not exist.
|
|
194
|
+
const uniqueGroups = (schema.getUnique() ?? [])
|
|
195
|
+
.map((group) => group.map(toSnakeCase))
|
|
196
|
+
.filter((members) => members.every((column) => stored.has(column)));
|
|
250
197
|
|
|
251
198
|
return {
|
|
252
199
|
name: tableName,
|
|
@@ -257,12 +204,8 @@ export function toTable(tableName: string, entity: SchemaOrCard, relations?: Rel
|
|
|
257
204
|
}
|
|
258
205
|
|
|
259
206
|
/**
|
|
260
|
-
* Is this column part of a key? MySQL and SQL Server refuse an unbounded text
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
* `unique` and `index` count, and until they could be declared nothing did but the
|
|
264
|
-
* primary key — the comment above already said "or an index" while the code answered
|
|
265
|
-
* for the key alone, because no vocabulary word produced one to answer for.
|
|
207
|
+
* Is this column part of a key? MySQL and SQL Server refuse an unbounded text column in a primary
|
|
208
|
+
* key or an index, so the dialect needs to know.
|
|
266
209
|
*/
|
|
267
210
|
export function isKeyed(table: TableDef, column: ColumnDef): boolean {
|
|
268
211
|
return column.primary
|
|
@@ -280,8 +223,8 @@ export function toTableName(name: string): string {
|
|
|
280
223
|
|
|
281
224
|
export interface EntityEntry {
|
|
282
225
|
name: string;
|
|
283
|
-
/** A live class in-process,
|
|
284
|
-
entityClass:
|
|
226
|
+
/** A live class in-process, or one rebuilt from the card of a frond that never crossed. */
|
|
227
|
+
entityClass: SchemaView;
|
|
285
228
|
}
|
|
286
229
|
|
|
287
230
|
export interface FrondLike {
|
|
@@ -292,43 +235,25 @@ export interface FrondLike {
|
|
|
292
235
|
export interface AppLike {
|
|
293
236
|
fronds: FrondLike[];
|
|
294
237
|
/** Auth runtime entities are migrated alongside scanned fronds when present. */
|
|
295
|
-
auth?: { entities: Record<string,
|
|
296
|
-
/**
|
|
297
|
-
* Entities this app hosts in ANOTHER source — named so a miss can be read.
|
|
298
|
-
*
|
|
299
|
-
* Without it every miss looked alike, so a `ref()` fell back to a derived table name
|
|
300
|
-
* and the constraint was emitted against a table that might not exist. Absent means
|
|
301
|
-
* one source, where a miss can only be a mistake.
|
|
302
|
-
*/
|
|
238
|
+
auth?: { entities: Record<string, SchemaView> };
|
|
239
|
+
/** Entities this app hosts in ANOTHER source — named so a miss can be read. */
|
|
303
240
|
elsewhere?: string[];
|
|
304
241
|
}
|
|
305
242
|
|
|
306
|
-
/**
|
|
307
|
-
* A schema says whether it holds rows; this adapter decides what to emit for it.
|
|
308
|
-
*
|
|
309
|
-
* A root always did. A derivation describes ANOTHER's rows — `Post.pick('id','title')` is
|
|
310
|
-
* a shape, and one dropped under `entities/` used to get a table of its own: measured on a
|
|
311
|
-
* real app, where a projection of an archived entity created a duplicate in the OTHER
|
|
312
|
-
* database. `.anchor()` is what says otherwise, and it is the entity's own word, never a
|
|
313
|
-
* key of the app's config: a frond stays mountable without its host knowing.
|
|
314
|
-
*/
|
|
243
|
+
/** A schema says whether it holds rows; this adapter decides what to emit for it. */
|
|
315
244
|
function verdictOn(entry: EntityEntry): 'table' | 'answer' {
|
|
316
|
-
const
|
|
245
|
+
const { entityClass } = entry;
|
|
317
246
|
|
|
318
|
-
return !
|
|
247
|
+
return !entityClass.derivation || entityClass.anchored ? 'table' : 'answer';
|
|
319
248
|
}
|
|
320
249
|
|
|
321
|
-
/**
|
|
322
|
-
* Every entity this app hosts, once each. The same class reaches here under one
|
|
323
|
-
* registration name from two lists — a frond's `entities/` and the auth runtime's map —
|
|
324
|
-
* and two entries for one name are two CREATE TABLE for one table.
|
|
325
|
-
*/
|
|
250
|
+
/** Every entity this app hosts, once each. */
|
|
326
251
|
function collectEntities(app: AppLike): EntityEntry[] {
|
|
327
252
|
const entries: EntityEntry[] = [];
|
|
328
|
-
const
|
|
253
|
+
const seen = new Set<string>();
|
|
329
254
|
const hold = (entry: EntityEntry) => {
|
|
330
|
-
if (
|
|
331
|
-
|
|
255
|
+
if (seen.has(lowerFirst(entry.name))) return;
|
|
256
|
+
seen.add(lowerFirst(entry.name));
|
|
332
257
|
entries.push(entry);
|
|
333
258
|
};
|
|
334
259
|
|
|
@@ -345,14 +270,12 @@ function collectEntities(app: AppLike): EntityEntry[] {
|
|
|
345
270
|
}
|
|
346
271
|
|
|
347
272
|
/**
|
|
348
|
-
* Every entity an app hosts, as FK-aware tables — the shared middle step behind
|
|
349
|
-
*
|
|
350
|
-
* target state). Builds the identity map once (see `referenceFor`'s doc) so a
|
|
351
|
-
* `ref()` target reuses the SAME resolved name as the entity's own table.
|
|
273
|
+
* Every entity an app hosts, as FK-aware tables — the shared middle step behind `generateSQL` (a
|
|
274
|
+
* from-scratch create pass) and `desiredTables` (the diff's target state).
|
|
352
275
|
*/
|
|
353
276
|
export function toTables(app: AppLike, resolve: (name: string) => string): TableDef[] {
|
|
354
277
|
const entries = collectEntities(app);
|
|
355
|
-
const tableNameOf = new Map<
|
|
278
|
+
const tableNameOf = new Map<SchemaView, string>(entries.map((entry) => [entry.entityClass, resolve(entry.name)]));
|
|
356
279
|
const hosted = app.elsewhere
|
|
357
280
|
? { here: new Set(entries.map((entry) => lowerFirst(entry.name))), elsewhere: new Set(app.elsewhere.map(lowerFirst)) }
|
|
358
281
|
: undefined;
|
|
@@ -360,69 +283,3 @@ export function toTables(app: AppLike, resolve: (name: string) => string): Table
|
|
|
360
283
|
}
|
|
361
284
|
|
|
362
285
|
// ─── Ordering — a referenced table before its referrer ─────────────────────
|
|
363
|
-
|
|
364
|
-
export interface FkEdge {
|
|
365
|
-
table: TableDef;
|
|
366
|
-
column: ColumnDef;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
export interface TableOrder {
|
|
370
|
-
/** Tables in dependency order — a `ref()` target always precedes its referrer. */
|
|
371
|
-
ordered: TableDef[];
|
|
372
|
-
/** FK columns whose target could not be ordered first — a cycle. Constrain after creation. */
|
|
373
|
-
deferred: FkEdge[];
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
/**
|
|
377
|
-
* Order a table set so a `ref()`'s target always exists before the table that
|
|
378
|
-
* points at it — required by every engine except SQLite, which resolves FK
|
|
379
|
-
* targets lazily and accepts any order (and has no `ALTER TABLE ADD CONSTRAINT`
|
|
380
|
-
* to close a cycle with — a caller on that dialect skips this function entirely).
|
|
381
|
-
*
|
|
382
|
-
* A cycle (`Post → Author → Post`, legal in the model — role.ts's relation
|
|
383
|
-
* thunk exists precisely so two entities can reference each other) has no such
|
|
384
|
-
* order: the loop is broken by deferring ONE of its edges per remaining cycle —
|
|
385
|
-
* that FK is added after every table exists, instead of inline. A self-reference
|
|
386
|
-
* (`parentId: ref(() => Category)`) is not a cycle here: a table may always
|
|
387
|
-
* reference its own not-yet-populated rows inline, standard support across
|
|
388
|
-
* every engine — so it's excluded from the dependency graph entirely.
|
|
389
|
-
*/
|
|
390
|
-
export function orderTables(tables: TableDef[]): TableOrder {
|
|
391
|
-
const byName = new Map(tables.map((table) => [table.name, table]));
|
|
392
|
-
const needs = new Map(
|
|
393
|
-
tables.map((table) => [
|
|
394
|
-
table.name,
|
|
395
|
-
new Set(
|
|
396
|
-
table.columns
|
|
397
|
-
.filter((c) => c.references && c.references.table !== table.name && byName.has(c.references.table))
|
|
398
|
-
.map((c) => c.references!.table),
|
|
399
|
-
),
|
|
400
|
-
]),
|
|
401
|
-
);
|
|
402
|
-
|
|
403
|
-
const ordered: TableDef[] = [];
|
|
404
|
-
const deferred: FkEdge[] = [];
|
|
405
|
-
const done = new Set<string>();
|
|
406
|
-
|
|
407
|
-
while (needs.size > 0) {
|
|
408
|
-
const ready = [...needs.keys()].find((name) => [...needs.get(name)!].every((dep) => done.has(dep)));
|
|
409
|
-
if (ready) {
|
|
410
|
-
ordered.push(byName.get(ready)!);
|
|
411
|
-
done.add(ready);
|
|
412
|
-
needs.delete(ready);
|
|
413
|
-
continue;
|
|
414
|
-
}
|
|
415
|
-
// Every table left waits on another table left — a cycle. Break it by
|
|
416
|
-
// deferring one edge: every column of the first remaining table that points
|
|
417
|
-
// at its first unmet dependency.
|
|
418
|
-
const [name, deps] = [...needs.entries()][0];
|
|
419
|
-
const dep = [...deps].find((d) => !done.has(d))!;
|
|
420
|
-
const table = byName.get(name)!;
|
|
421
|
-
for (const column of table.columns) {
|
|
422
|
-
if (column.references?.table === dep) deferred.push({ table, column });
|
|
423
|
-
}
|
|
424
|
-
deps.delete(dep);
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
return { ordered, deferred };
|
|
428
|
-
}
|
package/src/values.ts
CHANGED
|
@@ -1,20 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The values a driver accepts, and the values an entity declares.
|
|
3
|
-
*
|
|
4
|
-
* A driver binds numbers, strings, buffers and null — nothing else. An entity declares
|
|
5
|
-
* booleans, dates, lists and objects. Something has to sit between the two, and until
|
|
6
|
-
* now nothing did: `done: bool()` threw at insert ("SQLite3 can only bind numbers,
|
|
7
|
-
* strings, bigints, buffers, and null"), a `date()` could only be written as the ISO
|
|
8
|
-
* string its own type forbids, and both came back as whatever the column held.
|
|
9
|
-
*
|
|
10
|
-
* The pair below is derived from the column's shape alone — no new declaration, no axis
|
|
11
|
-
* to read. It lives in the storage adapter because that is where the driver's limits are
|
|
12
|
-
* known; the entity keeps saying `boolean` and `Date`.
|
|
13
|
-
*
|
|
14
|
-
* Drivers differ in how much they already do (Postgres hands back real booleans and
|
|
15
|
-
* Dates, SQLite hands back 0/1 and text), so every read guards on what it actually got
|
|
16
|
-
* instead of assuming.
|
|
17
|
-
*/
|
|
1
|
+
/** The values a driver accepts, and the values an entity declares. */
|
|
18
2
|
import type { ColumnShape } from './table.js';
|
|
19
3
|
|
|
20
4
|
export interface ValueCodec {
|
|
@@ -46,13 +30,8 @@ const json: ValueCodec = {
|
|
|
46
30
|
};
|
|
47
31
|
|
|
48
32
|
/**
|
|
49
|
-
* A driver may answer a number as a BigInt — Postgres does it for `count(*)` and for
|
|
50
|
-
*
|
|
51
|
-
* what comes back.
|
|
52
|
-
*
|
|
53
|
-
* Out of range it REFUSES rather than rounding. `Number(9007199254740993n)` is
|
|
54
|
-
* `9007199254740992` — a wrong answer, silently, and the row would look fine. A value
|
|
55
|
-
* that large is a real identifier somewhere, and its field should say `text()`.
|
|
33
|
+
* A driver may answer a number as a BigInt — Postgres does it for `count(*)` and for `bigint`
|
|
34
|
+
* columns, DuckDB for every count.
|
|
56
35
|
*/
|
|
57
36
|
const numeric: ValueCodec = {
|
|
58
37
|
write: (v) => (typeof v === 'bigint' ? fits(v) : v),
|