@fougere/adapter-sql 0.12.0-alpha.0 → 0.12.2-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.
Files changed (51) hide show
  1. package/dist/check.js.map +1 -1
  2. package/dist/crud/SqlStorage.d.ts +4 -5
  3. package/dist/crud/SqlStorage.d.ts.map +1 -1
  4. package/dist/crud/SqlStorage.js +33 -35
  5. package/dist/crud/SqlStorage.js.map +1 -1
  6. package/dist/ddl/SqlSink.d.ts +5 -2
  7. package/dist/ddl/SqlSink.d.ts.map +1 -1
  8. package/dist/ddl/SqlSink.js +19 -12
  9. package/dist/ddl/SqlSink.js.map +1 -1
  10. package/dist/dialect/Dialect.d.ts.map +1 -1
  11. package/dist/dialect/Dialect.js.map +1 -1
  12. package/dist/diff/Change.d.ts.map +1 -1
  13. package/dist/diff/Change.js.map +1 -1
  14. package/dist/diff/SchemaState.d.ts.map +1 -1
  15. package/dist/diff/SchemaState.js.map +1 -1
  16. package/dist/fields/Engine.d.ts +3 -3
  17. package/dist/fields/Engine.d.ts.map +1 -1
  18. package/dist/fields/Engine.js +2 -1
  19. package/dist/fields/Engine.js.map +1 -1
  20. package/dist/fields/SqlFields.d.ts +3 -3
  21. package/dist/fields/SqlFields.d.ts.map +1 -1
  22. package/dist/fields/SqlFields.js +10 -4
  23. package/dist/fields/SqlFields.js.map +1 -1
  24. package/dist/source/SqlSource.d.ts.map +1 -1
  25. package/dist/source/SqlSource.js.map +1 -1
  26. package/dist/sqlite/SqliteSource.d.ts.map +1 -1
  27. package/dist/sqlite/SqliteSource.js.map +1 -1
  28. package/dist/step/Plan.d.ts.map +1 -1
  29. package/dist/step/Plan.js.map +1 -1
  30. package/dist/table/TableDef.d.ts +2 -0
  31. package/dist/table/TableDef.d.ts.map +1 -1
  32. package/dist/table/TableDef.js +18 -11
  33. package/dist/table/TableDef.js.map +1 -1
  34. package/dist/values.d.ts.map +1 -1
  35. package/dist/values.js.map +1 -1
  36. package/package.json +4 -4
  37. package/src/check.ts +1 -1
  38. package/src/crud/SqlStorage.ts +59 -40
  39. package/src/ddl/SqlSink.ts +30 -12
  40. package/src/dialect/Dialect.ts +3 -0
  41. package/src/diff/Change.ts +8 -0
  42. package/src/diff/SchemaState.ts +1 -0
  43. package/src/fields/Engine.ts +10 -3
  44. package/src/fields/SqlFields.ts +13 -12
  45. package/src/source/SqlSource.ts +1 -0
  46. package/src/sqlite/SqliteSource.ts +1 -0
  47. package/src/step/Plan.ts +11 -0
  48. package/src/table/TableDef.ts +23 -11
  49. package/src/values.ts +2 -0
  50. package/dist/adapter.schema.json +0 -16
  51. package/src/adapter.schema.json +0 -16
@@ -17,6 +17,7 @@ export async function actualState(db: Kysely<any>): Promise<SchemaState> {
17
17
  if (table.isView) continue;
18
18
  state.set(table.name, new Set(table.columns.map((column) => column.name)));
19
19
  }
20
+
20
21
  return state;
21
22
  }
22
23
 
@@ -57,6 +58,7 @@ export function delta(desired: TableDef[], actual: SchemaState): Change[] {
57
58
  if (column.index || (column.unique && !column.primary)) changes.push({ kind: 'createIndex', table, column });
58
59
  }
59
60
  }
61
+
60
62
  return changes;
61
63
  }
62
64
 
@@ -81,6 +83,7 @@ export function orderChanges(changes: Change[], dialectName: DialectName): Chang
81
83
 
82
84
  const createChanges: Change[] = ordered.map((table) => {
83
85
  const names = deferredColumnsOf.get(table.name);
86
+
84
87
  return names ? { kind: 'createTable', table, deferredColumns: [...names] } : { kind: 'createTable', table };
85
88
  });
86
89
  const constraintChanges: Change[] = deferred.map(({ table, column }) => ({ kind: 'addConstraint', table, column }));
@@ -95,6 +98,7 @@ export function changeSQL(change: Change, dialectName: DialectName): string {
95
98
  if (change.kind === 'createTable') {
96
99
  // Reuse the same renderer as a fresh install — one builder, no drift.
97
100
  const skip = change.deferredColumns ? new Set(change.deferredColumns) : undefined;
101
+
98
102
  return createTableSQL(change.table, dialectName, { skipReferences: skip });
99
103
  }
100
104
  if (change.kind === 'addConstraint') {
@@ -107,6 +111,7 @@ export function changeSQL(change: Change, dialectName: DialectName): string {
107
111
  }
108
112
  const { table, column } = change;
109
113
  const type = columnTypeFor(dialect, column, isKeyed(table, column));
114
+
110
115
  return compiler(dialectName)
111
116
  .schema.alterTable(table.name)
112
117
  .addColumn(column.name, sql.raw(type) as any, (col) => {
@@ -126,6 +131,7 @@ export function changeSQL(change: Change, dialectName: DialectName): string {
126
131
  // arrives later is bounded like a column that was there from the start.
127
132
  const check = checkFor(column);
128
133
  if (check) built = built.check(check);
134
+
129
135
  return built;
130
136
  })
131
137
  .compile().sql;
@@ -139,6 +145,7 @@ export async function planMigration(
139
145
  ): Promise<{ changes: Change[]; statements: string[] }> {
140
146
  const dialect = options?.dialect ?? 'sqlite';
141
147
  const changes = orderChanges(delta(desiredTables(app, options), await actualState(db)), dialect);
148
+
142
149
  return { changes, statements: changes.map((change) => changeSQL(change, dialect)) };
143
150
  }
144
151
 
@@ -153,5 +160,6 @@ export async function migrate(
153
160
  for (const statement of statements) {
154
161
  await sql.raw(statement).execute(db);
155
162
  }
163
+
156
164
  return changes;
157
165
  }
@@ -9,5 +9,6 @@ export async function actualState(db: Kysely<any>): Promise<SchemaState> {
9
9
  if (table.isView) continue;
10
10
  state.set(table.name, new Set(table.columns.map((column) => column.name)));
11
11
  }
12
+
12
13
  return state;
13
14
  }
@@ -1,4 +1,11 @@
1
- import ENTRY_FORMAT from '../adapter.schema.json' with { type: 'json' };
1
+ import type { DialectName } from '../dialect/DialectName.js';
2
2
 
3
- /** The engines the format names — the one list, read off the file that states it. */
4
- export type Engine = keyof typeof ENTRY_FORMAT.properties.columnType.properties;
3
+ /** The engines an entry may address: the dialects this adapter speaks, and nothing else. */
4
+ export const ENGINES = ['sqlite', 'pg', 'mysql', 'mssql'] as const;
5
+
6
+ export type Engine = (typeof ENGINES)[number];
7
+
8
+ type Assert<T extends true> = T;
9
+
10
+ /** A fifth dialect does not compile until `ENGINES` names it. */
11
+ type _EnginesMatchDialects = Assert<[Exclude<DialectName, Engine>] extends [never] ? true : false>;
@@ -2,24 +2,25 @@
2
2
  * What an entity may state for THIS adapter, declared from OUTSIDE `@fougere/schema` — which names
3
3
  * no engine and no column type, and must not learn one to let this exist.
4
4
  */
5
- import { AdapterFieldValidator, type Shape } from '@fougere/schema';
6
- import ENTRY_FORMAT from '../adapter.schema.json' with { type: 'json' };
7
- import type { DialectName } from '../dialect/DialectName.js';
5
+ import { Format, JsonSchemaValidator, type JsonSchema } from '@fougere/schema';
6
+ import { ENGINES } from './Engine.js';
8
7
  import type { SqlField } from './SqlField.js';
9
- import type { Engine } from './Engine.js';
10
8
 
11
9
  /** What sql holds, addressed by field — the shape every augmentation of the registry takes. */
12
10
  export type SqlFields<K extends string> = Readonly<Partial<Record<K, SqlField>>>;
13
11
 
14
- /** Judges what an entity states under `adapters.sql`, against the format this adapter ships. */
15
- export const sqlEntries = AdapterFieldValidator.of(ENTRY_FORMAT as Shape);
12
+ const ENTRY_FORMAT = Format.of('https://fougere.dev/schema/adapter/sql')
13
+ .key(
14
+ 'columnType',
15
+ ENGINES.reduce((held, engine) => held.key(engine, Format.text), Format.of()).closed(),
16
+ )
17
+ .closed();
16
18
 
17
- type Assert<T extends true> = T;
18
-
19
- /** A fifth dialect does not compile until `adapter.schema.json` names it. */
20
- type _EnginesMatchDialects = Assert<
21
- [Exclude<DialectName, Engine>] extends [never] ? true : false
22
- >;
19
+ /** Judges what an entity states under `adapters.sql`: the format this adapter ships, keyed by field. */
20
+ export const sqlEntries = JsonSchemaValidator.of({
21
+ type: 'object',
22
+ additionalProperties: ENTRY_FORMAT.schema as JsonSchema,
23
+ });
23
24
 
24
25
  declare module '@fougere/schema' {
25
26
  interface FougereEntityAdapters<K extends string> {
@@ -54,6 +54,7 @@ export function createKyselySource(
54
54
  opts: SqlSourceOptions = {},
55
55
  ): SqlSource {
56
56
  const db = new Kysely<any>({ dialect: kyselyDialect, log: logQueries(opts.name ?? dialect) });
57
+
57
58
  return {
58
59
  db,
59
60
  dialect,
@@ -24,6 +24,7 @@ export function createSqliteSource(opts: SqliteSourceOptions = {}): SqliteSource
24
24
  sqlite.pragma('journal_mode = WAL');
25
25
  sqlite.pragma('foreign_keys = ON');
26
26
  const db = new Kysely<any>({ dialect: new SqliteDialect({ database: sqlite }), log: logQueries(opts.name ?? path) });
27
+
27
28
  return {
28
29
  db,
29
30
  sqlite,
package/src/step/Plan.ts CHANGED
@@ -45,11 +45,13 @@ function compose(held: ShapeChange[], change: ShapeChange): void {
45
45
  const at = held.findIndex((each) => each.kind === 'renamed' && each.to === change.from);
46
46
  if (at === -1) {
47
47
  held.push(change);
48
+
48
49
  return;
49
50
  }
50
51
  const first = held[at] as Extract<ShapeChange, { kind: 'renamed' }>;
51
52
  if (first.from === change.to) held.splice(at, 1);
52
53
  else held[at] = { ...first, to: change.to };
54
+
53
55
  return;
54
56
  }
55
57
 
@@ -59,6 +61,7 @@ function compose(held: ShapeChange[], change: ShapeChange): void {
59
61
  const at = held.findIndex((each) => each.kind === 'renamed' && each.to === field);
60
62
  if (at === -1) {
61
63
  held.push(change);
64
+
62
65
  return;
63
66
  }
64
67
 
@@ -119,6 +122,7 @@ function realise(
119
122
  if (!change.required) return {};
120
123
  const column = target.columns.find((each) => each.field === change.field);
121
124
  if (column?.default !== undefined) return {};
125
+
122
126
  return {
123
127
  entity,
124
128
  field: change.field,
@@ -128,6 +132,7 @@ function realise(
128
132
 
129
133
  case 'required':
130
134
  if (!change.to) return {}; // Loosening is the engine's business, and no row is at risk.
135
+
131
136
  return {
132
137
  entity,
133
138
  field: change.field,
@@ -165,6 +170,7 @@ function restated(entity: string, change: Extract<ShapeChange, { kind: 'restated
165
170
  const was = literalOf(change.from);
166
171
  const is = literalOf(change.to);
167
172
  if (was === is) return {};
173
+
168
174
  return refuse(`default moved ${show(was)} → ${show(is)} — the table keeps the old one, and nothing here alters a DEFAULT`);
169
175
  }
170
176
 
@@ -180,12 +186,14 @@ function restated(entity: string, change: Extract<ShapeChange, { kind: 'restated
180
186
  // What is left is the index, and only its appearance: the additive pass proposes every
181
187
  // declared index at every boot, and nothing has ever dropped one.
182
188
  if (from.index && !to.index) return refuse(`index gone — nothing drops an index today, so the table keeps it`);
189
+
183
190
  return {};
184
191
  }
185
192
 
186
193
  /** The value a lifecycle declares at create, when it declares one — what reaches DEFAULT. */
187
194
  function literalOf(rules: { create?: unknown } | undefined): unknown {
188
195
  const create = rules?.create;
196
+
189
197
  return create && typeof create === 'object' && 'value' in create ? (create as { value: unknown }).value : undefined;
190
198
  }
191
199
 
@@ -194,6 +202,7 @@ const show = (value: unknown): string => (value === undefined ? 'none' : JSON.st
194
202
  /** Has the table already moved? Read off the columns themselves. */
195
203
  function done(change: StepChange, columns: Set<string> | undefined): boolean {
196
204
  if (!columns) return false;
205
+
197
206
  return change.kind === 'renameColumn'
198
207
  ? !columns.has(change.from) && columns.has(change.to)
199
208
  : !columns.has(change.column);
@@ -202,6 +211,7 @@ function done(change: StepChange, columns: Set<string> | undefined): boolean {
202
211
  /** One statement per change — the same rule `migrate` follows: no driver here batches. */
203
212
  export function stepSQL(change: StepChange, dialectName: DialectName = 'sqlite'): string {
204
213
  const alter = compiler(dialectName).schema.alterTable(change.table);
214
+
205
215
  return change.kind === 'renameColumn'
206
216
  ? alter.renameColumn(change.from, change.to).compile().sql
207
217
  : alter.dropColumn(change.column).compile().sql;
@@ -222,5 +232,6 @@ export async function applyStep(plan: Plan, db: Kysely<any>, dialectName: Dialec
222
232
  await sql.raw(statement).execute(db);
223
233
  run.push(statement);
224
234
  }
235
+
225
236
  return run;
226
237
  }
@@ -1,5 +1,5 @@
1
1
  import { Lifecycle, Role } from '@fougere/schema';
2
- import { Shapes, lowerFirst, type Field, type SchemaView } from '@fougere/schema';
2
+ import { SchemaError, Shapes, dotted, lowerFirst, type Field, type SchemaView } from '@fougere/schema';
3
3
  import { boundsOf } from '../check.js';
4
4
  import { type SqlField } from '../fields/SqlField.js';
5
5
  import { sqlEntries } from '../fields/SqlFields.js';
@@ -17,6 +17,8 @@ export interface TableDef {
17
17
  compositePrimary: string[];
18
18
  /** Column groups unique together, from `entity(fields, { unique: [...] })`. */
19
19
  uniqueGroups: string[][];
20
+ /** Column groups indexed together, from `entity(fields, { index: [...] })`. */
21
+ indexGroups: string[][];
20
22
  }
21
23
 
22
24
  /** camelCase → snake_case */
@@ -29,15 +31,16 @@ export function toSnakeCase(str: string): string {
29
31
  * other field becomes exactly one column.
30
32
  */
31
33
  function isStored(field: Field): boolean {
32
- return !Role.of(field).isCollection;
34
+ return !Role.of(field).isCollection();
33
35
  }
34
36
 
35
37
  /** The target's primary key column. */
36
38
  function primaryColumnOf(target: Partial<SchemaView>): string {
37
39
  if (typeof target.getFields !== 'function') return 'id';
38
40
  for (const [name, field] of Object.entries(target.getFields())) {
39
- if (Role.of(field).isPrimary) return toSnakeCase(name);
41
+ if (Role.of(field).isPrimary()) return toSnakeCase(name);
40
42
  }
43
+
41
44
  return 'id'; // declared no primary() field — defensive, shouldn't happen
42
45
  }
43
46
 
@@ -49,7 +52,7 @@ function referenceFor(
49
52
  hosted?: HostedNames,
50
53
  ): ColumnReference | undefined {
51
54
  const role = Role.of(field);
52
- if (!role.isReference) return undefined;
55
+ if (!role.isReference()) return undefined;
53
56
  const target = role.target as Partial<SchemaView> & { name?: string };
54
57
  const mapped = tableNameOf?.get(target as SchemaView);
55
58
  if (mapped === undefined && hosted !== undefined) {
@@ -77,6 +80,7 @@ function referenceFor(
77
80
  }
78
81
  const table = mapped ?? resolve(lowerFirst(target.name ?? ''));
79
82
  const column = primaryColumnOf(target);
83
+
80
84
  return role.onDelete ? { table, column, onDelete: role.onDelete } : { table, column };
81
85
  }
82
86
 
@@ -98,7 +102,7 @@ function toColumn(
98
102
  name: toSnakeCase(fieldName),
99
103
  type: Shapes.typeOf(field.shape),
100
104
  nullable,
101
- primary: Role.of(field).isPrimary,
105
+ primary: Role.of(field).isPrimary(),
102
106
  };
103
107
  const bounds = boundsOf(base as Record<string, unknown> | undefined);
104
108
  if (bounds) column.bounds = bounds;
@@ -108,11 +112,12 @@ function toColumn(
108
112
  // redundant constraint on every engine. So would indexing what `unique` constrains.
109
113
  // Only a constraint of ONE becomes a column constraint; a group of several is a table
110
114
  // constraint, emitted once from `uniqueGroups` rather than once per member column.
111
- if (Role.of(field).isUnique && !column.primary) column.unique = true;
112
- if (Role.of(field).isIndexed && !column.primary && !column.unique) column.index = true;
115
+ if (Role.of(field).isUnique() && !column.primary) column.unique = true;
116
+ if (Role.of(field).isIndexed() && !column.primary && !column.unique) column.index = true;
113
117
  const references = referenceFor(field, resolve, tableNameOf, hosted);
114
118
  if (references) column.references = references;
115
119
  if (stated) column.stated = stated;
120
+
116
121
  return column;
117
122
  }
118
123
 
@@ -125,7 +130,10 @@ export function toTable(tableName: string, schema: SchemaView, relations?: Relat
125
130
  // Judged HERE and not at `entity()`: this runs at boot, after every import, so the
126
131
  // format is always loaded. A validator registered with `schema` would depend on which
127
132
  // module was imported first.
128
- sqlEntries.assert(configuration, `${schema.name}.adapters.sql`);
133
+ const refusal = configuration === undefined
134
+ ? undefined
135
+ : sqlEntries.refusalOf(configuration, [`${schema.name}.adapters.sql`]);
136
+ if (refusal) throw new SchemaError(`${dotted(refusal.path)}: ${refusal.message}`);
129
137
  const columns: ColumnDef[] = [];
130
138
  for (const [fieldName, field] of Object.entries(fields)) {
131
139
  if (!isStored(field)) continue;
@@ -136,7 +144,7 @@ export function toTable(tableName: string, schema: SchemaView, relations?: Relat
136
144
  // Declared in field names, realized in column names — and a group naming a field the
137
145
  // storage does not keep is not enforceable, so it is dropped rather than emitted against
138
146
  // a column that will not exist.
139
- const uniqueGroups = (schema.getUnique() ?? [])
147
+ const realized = (groups: readonly (readonly string[])[]) => groups
140
148
  .map((group) => group.map(toSnakeCase))
141
149
  .filter((members) => members.every((column) => stored.has(column)));
142
150
 
@@ -144,7 +152,8 @@ export function toTable(tableName: string, schema: SchemaView, relations?: Relat
144
152
  name: tableName,
145
153
  columns,
146
154
  compositePrimary: primaries.length > 1 ? primaries : [],
147
- uniqueGroups,
155
+ uniqueGroups: realized(schema.getUnique() ?? []),
156
+ indexGroups: realized(schema.getIndex() ?? []),
148
157
  };
149
158
  }
150
159
 
@@ -156,7 +165,9 @@ export function isKeyed(table: TableDef, column: ColumnDef): boolean {
156
165
  return column.primary
157
166
  || column.unique === true
158
167
  || column.index === true
159
- || table.compositePrimary.includes(column.name);
168
+ || table.compositePrimary.includes(column.name)
169
+ || table.indexGroups.some((group) => group.includes(column.name))
170
+ || table.uniqueGroups.some((group) => group.includes(column.name));
160
171
  }
161
172
 
162
173
  // ─── App-wide entity collection — shared by generateSQL and desiredTables ──
@@ -205,6 +216,7 @@ export function toTables(app: AppLike, resolve: (name: string) => string): Table
205
216
  const hosted = app.elsewhere
206
217
  ? { here: new Set(entries.map((entry) => lowerFirst(entry.name))), elsewhere: new Set(app.elsewhere.map(lowerFirst)) }
207
218
  : undefined;
219
+
208
220
  return entries.map((entry) => toTable(resolve(entry.name), entry.entityClass, { resolve, tableNameOf, hosted }));
209
221
  }
210
222
 
package/src/values.ts CHANGED
@@ -52,6 +52,7 @@ const MAX_SAFE = BigInt(Number.MAX_SAFE_INTEGER);
52
52
  function nullSafe(codec: ValueCodec): ValueCodec {
53
53
  const pass = (fn: (v: unknown) => unknown) => (v: unknown) =>
54
54
  v === null || v === undefined ? v : fn(v);
55
+
55
56
  return { write: pass(codec.write), read: pass(codec.read) };
56
57
  }
57
58
 
@@ -82,5 +83,6 @@ export function codecsOf(columns: { field: string; type?: ShapeType }[]): Map<st
82
83
  const codec = codecFor(column.type);
83
84
  if (codec !== identity) codecs.set(column.field, codec);
84
85
  }
86
+
85
87
  return codecs;
86
88
  }
@@ -1,16 +0,0 @@
1
- {
2
- "type": "object",
3
- "properties": {
4
- "columnType": {
5
- "type": "object",
6
- "properties": {
7
- "sqlite": { "type": "string" },
8
- "pg": { "type": "string" },
9
- "mysql": { "type": "string" },
10
- "mssql": { "type": "string" }
11
- },
12
- "additionalProperties": false
13
- }
14
- },
15
- "additionalProperties": false
16
- }
@@ -1,16 +0,0 @@
1
- {
2
- "type": "object",
3
- "properties": {
4
- "columnType": {
5
- "type": "object",
6
- "properties": {
7
- "sqlite": { "type": "string" },
8
- "pg": { "type": "string" },
9
- "mysql": { "type": "string" },
10
- "mssql": { "type": "string" }
11
- },
12
- "additionalProperties": false
13
- }
14
- },
15
- "additionalProperties": false
16
- }