@ultimabase/postgres 0.1.2 → 0.3.0-canary.20261031104207

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # @ultimabase/postgres
2
2
 
3
+ ## 0.3.0-canary.20261031104207
4
+
5
+ ### Minor Changes
6
+
7
+ - 353c2bf: A string field takes `unique: 'insensitive'`, and a unique index `mode: 'insensitive'`: `Alice` then clashes with `alice`, while each row keeps its case, and the `UNIQUE_VIOLATION` names the field (D-170). `postgres()` makes a unique index over `lower()`, which also serves an insensitive `equals`: run a migration after the upgrade to add it. `@ultimabase/testing` checks an adapter's case-insensitive keys.
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [41cfff6]
12
+ - Updated dependencies [6e8ebf6]
13
+ - Updated dependencies [9cd2dd4]
14
+ - Updated dependencies [353c2bf]
15
+ - ultimabase@0.3.0-canary.20261031104207
16
+
17
+ ## 0.2.0
18
+
19
+ ### Patch Changes
20
+
21
+ - Updated dependencies [2c3bf4e]
22
+ - Updated dependencies [fd1263e]
23
+ - ultimabase@0.2.0
24
+
3
25
  ## 0.1.2
4
26
 
5
27
  ### Patch Changes
@@ -1 +1 @@
1
- {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACX,QAAQ,EAGR,MAAM,QAAQ,CAAA;AAKf,OAAO,KAAK,EACX,OAAO,EASP,MAAM,YAAY,CAAA;AAuBnB,MAAM,MAAM,OAAO,GAAG;IACrB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAA;IAChC,0DAA0D;IAC1D,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,uEAAuE;IACvE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,KAAK,EAAE,QAAQ,KAAK,IAAI,CAAA;IACxC,uFAAuF;IACvF,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAA;CAChC,CAAA;AAED,4EAA4E;AAC5E,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG;IAChC;;;OAGG;IACH,QAAQ,CAAC,GAAG,EAAE,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACxD,OAAO,EAAE,oBAAoB,EAC7B,GAAG,MAAM,EAAE,SAAS,OAAO,EAAE,KACzB,OAAO,CAAC,CAAC,EAAE,CAAC,CAAA;CACjB,CAAA;AAmED,mDAAmD;AACnD,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAIhE;AAED,8EAA8E;AAC9E,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,OAAO,GAAG,QAAQ,CA2MxD"}
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACX,QAAQ,EAGR,MAAM,QAAQ,CAAA;AAKf,OAAO,KAAK,EACX,OAAO,EASP,MAAM,YAAY,CAAA;AAwBnB,MAAM,MAAM,OAAO,GAAG;IACrB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAA;IAChC,0DAA0D;IAC1D,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,uEAAuE;IACvE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,KAAK,EAAE,QAAQ,KAAK,IAAI,CAAA;IACxC,uFAAuF;IACvF,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAA;CAChC,CAAA;AAED,4EAA4E;AAC5E,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG;IAChC;;;OAGG;IACH,QAAQ,CAAC,GAAG,EAAE,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACxD,OAAO,EAAE,oBAAoB,EAC7B,GAAG,MAAM,EAAE,SAAS,OAAO,EAAE,KACzB,OAAO,CAAC,CAAC,EAAE,CAAC,CAAA;CACjB,CAAA;AAmED,mDAAmD;AACnD,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAIhE;AAED,8EAA8E;AAC9E,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,OAAO,GAAG,QAAQ,CA2MxD"}
package/dist/adapter.js CHANGED
@@ -13,7 +13,8 @@ import { plan } from './plan.js';
13
13
  import { pooled } from './pool.js';
14
14
  import { compileCount, compileFind } from './read.js';
15
15
  import { connecting, dropped, emptied, scratchName, trigrams, } from './scratch.js';
16
- import { parseSnapshot, snapshotOf } from './snapshot.js';
16
+ import { parseSnapshot } from './snapshot-parse.js';
17
+ import { snapshotOf } from './snapshot.js';
17
18
  import { compileWrite, translate } from './write.js';
18
19
  /** What the callback a rollback ends throws, which `rollback()` swallows. */
19
20
  const rolledBack = new Error('The transaction was rolled back.');
package/dist/filter.d.ts CHANGED
@@ -38,8 +38,12 @@ export declare class Filters {
38
38
  private exists;
39
39
  private relation;
40
40
  private scalar;
41
- /** A value of `kind`, a column or an aggregate (D-149), meets `condition`. */
42
- compare(column: Sql, kind: Kind, condition: Condition): Sql;
41
+ /**
42
+ * A value of `kind`, a column or an aggregate (D-149), meets `condition`;
43
+ * a `lowered` column's insensitive equality reads its case-insensitive
44
+ * key's index (D-170).
45
+ */
46
+ compare(column: Sql, kind: Kind, condition: Condition, lowered?: boolean): Sql;
43
47
  /** A string that is not an id equals no id, as in `memory()` (D-98). */
44
48
  private equal;
45
49
  /** `in`: no item is false; an item no known value equals is dropped. */
@@ -1 +1 @@
1
- {"version":3,"file":"filter.d.ts","sourceRoot":"","sources":["../src/filter.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAEhE,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,YAAY,CAAA;AAErC,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AAEtD,YAAY,EAAE,GAAG,EAAE,MAAM,YAAY,CAAA;AACrC,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,YAAY,CAAA;AAErC,KAAK,QAAQ,GAAG,OAAO,CAAC,WAAW,EAAE;IAAE,QAAQ,CAAC,IAAI,EAAE,KAAK,GAAG,MAAM,CAAA;CAAE,CAAC,CAAA;AAEvE,2EAA2E;AAC3E,MAAM,MAAM,KAAK,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,GAAG,CAAC;IAAC,QAAQ,CAAC,EAAE,EAAE,GAAG,CAAA;CAAE,CAAA;AAE5D,eAAO,MAAM,IAAI,EAAE,MAC8C,CAAA;AAEjE;;;;GAIG;AACH,wBAAgB,KAAK,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,GAAG,GAAG,CAQpD;AAED,wBAAgB,SAAS,CACxB,IAAI,EAAE,SAAS,GAAG,SAAS,MAAM,EAAE,GACjC,IAAI,IAAI,SAAS,MAAM,EAAE,CAE3B;AAED,qBAAa,OAAO;IACnB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,mEAAmE;IACnE,QAAQ,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAY;IACxC,uEAAuE;IACvE,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC7C,OAAO,CAAC,IAAI,CAAI;IAEhB,YACC,OAAO,EAAE,OAAO,EAChB,OAAO,GAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAa,EAIhD;IAED,yDAAyD;IACzD,KAAK,IAAI,MAAM,CAGd;IAED,MAAM,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,GAAG,CAErD;IAED,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,GAAG,CA6BnE;IAED,oFAAoF;IACpF,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,GAAG,CAEvB;IAED,8EAA8E;IAC9E,KAAK,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,GAAG,KAAK,CAyBrE;IAED,OAAO,CAAC,MAAM;IAad,OAAO,CAAC,QAAQ;IA4ChB,OAAO,CAAC,MAAM;IAUd,8EAA8E;IAC9E,OAAO,CAAC,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS,GAAG,GAAG,CAuE1D;IAED,wEAAwE;IACxE,OAAO,CAAC,KAAK;IAOb,wEAAwE;IACxE,OAAO,CAAC,MAAM;IAed,wDAAwD;IACxD,OAAO,CAAC,IAAI;CAQZ"}
1
+ {"version":3,"file":"filter.d.ts","sourceRoot":"","sources":["../src/filter.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAEhE,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,YAAY,CAAA;AAErC,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AAEtD,YAAY,EAAE,GAAG,EAAE,MAAM,YAAY,CAAA;AACrC,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,YAAY,CAAA;AAErC,KAAK,QAAQ,GAAG,OAAO,CAAC,WAAW,EAAE;IAAE,QAAQ,CAAC,IAAI,EAAE,KAAK,GAAG,MAAM,CAAA;CAAE,CAAC,CAAA;AAEvE,2EAA2E;AAC3E,MAAM,MAAM,KAAK,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,GAAG,CAAC;IAAC,QAAQ,CAAC,EAAE,EAAE,GAAG,CAAA;CAAE,CAAA;AAE5D,eAAO,MAAM,IAAI,EAAE,MAC8C,CAAA;AAEjE;;;;GAIG;AACH,wBAAgB,KAAK,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,GAAG,GAAG,CAQpD;AAED,wBAAgB,SAAS,CACxB,IAAI,EAAE,SAAS,GAAG,SAAS,MAAM,EAAE,GACjC,IAAI,IAAI,SAAS,MAAM,EAAE,CAE3B;AAED,qBAAa,OAAO;IACnB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,mEAAmE;IACnE,QAAQ,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAY;IACxC,uEAAuE;IACvE,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC7C,OAAO,CAAC,IAAI,CAAI;IAEhB,YACC,OAAO,EAAE,OAAO,EAChB,OAAO,GAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAa,EAIhD;IAED,yDAAyD;IACzD,KAAK,IAAI,MAAM,CAGd;IAED,MAAM,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,GAAG,CAErD;IAED,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,GAAG,CA6BnE;IAED,oFAAoF;IACpF,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,GAAG,CAEvB;IAED,8EAA8E;IAC9E,KAAK,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,GAAG,KAAK,CAyBrE;IAED,OAAO,CAAC,MAAM;IAad,OAAO,CAAC,QAAQ;IA4ChB,OAAO,CAAC,MAAM;IAgBd;;;;OAIG;IACH,OAAO,CAAC,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS,EAAE,OAAO,UAAQ,GAAG,GAAG,CAyE3E;IAED,wEAAwE;IACxE,OAAO,CAAC,KAAK;IAOb,wEAAwE;IACxE,OAAO,CAAC,MAAM;IAed,wDAAwD;IACxD,OAAO,CAAC,IAAI;CAQZ"}
package/dist/filter.js CHANGED
@@ -142,10 +142,17 @@ export class Filters {
142
142
  }
143
143
  scalar(table, alias, name, condition) {
144
144
  const column = this.column(table, alias, name);
145
- return this.compare(column, this.catalog.kind(table, name), condition);
145
+ const kind = this.catalog.kind(table, name);
146
+ const lowered = (kind === 'string' || kind === 'enum') &&
147
+ table.uniques.some((unique) => unique.lower === true && unique.fields.includes(name));
148
+ return this.compare(column, kind, condition, lowered);
146
149
  }
147
- /** A value of `kind`, a column or an aggregate (D-149), meets `condition`. */
148
- compare(column, kind, condition) {
150
+ /**
151
+ * A value of `kind`, a column or an aggregate (D-149), meets `condition`;
152
+ * a `lowered` column's insensitive equality reads its case-insensitive
153
+ * key's index (D-170).
154
+ */
155
+ compare(column, kind, condition, lowered = false) {
149
156
  if (condition.path !== undefined)
150
157
  return jsonPath(column, condition);
151
158
  // An id matches and compares as its string, as in `memory()`.
@@ -167,9 +174,11 @@ export class Filters {
167
174
  case 'not': {
168
175
  const equal = operand === null
169
176
  ? sql `${column} is null`
170
- : insensitive
171
- ? sql `${text} ilike ${pattern(operand, '', '')}`
172
- : this.equal(column, kind, operand);
177
+ : lowered && insensitive
178
+ ? sql `lower(${collated}) = lower(cast(${operand} as text))`
179
+ : insensitive
180
+ ? sql `${text} ilike ${pattern(operand, '', '')}`
181
+ : this.equal(column, kind, operand);
173
182
  parts.push(operator === 'equals' ? equal : sql `(not ${equal})`);
174
183
  break;
175
184
  }
@@ -2,7 +2,7 @@ import { sql } from 'kysely';
2
2
  import { parse } from 'pg-connection-string';
3
3
  import { isLocalHost, MigrationFailed, Refused } from 'ultimabase';
4
4
  import { id } from './plan-parts.js';
5
- import { parseSnapshot } from './snapshot.js';
5
+ import { parseSnapshot } from './snapshot-parse.js';
6
6
  /** The tables UltimaBase keeps its own records in. */
7
7
  export const tables = [
8
8
  'ultimabase_schema',
package/dist/names.d.ts CHANGED
@@ -7,12 +7,17 @@ export type Kind = ScalarType | 'id' | 'integer' | 'text[]' | 'double precision[
7
7
  export type Unique = {
8
8
  readonly name: string;
9
9
  readonly fields: readonly string[];
10
+ /** A case-insensitive key compares its text columns lowered (D-170). */
11
+ readonly lower?: true;
10
12
  };
11
13
  export type Table = {
12
14
  readonly name: string;
13
15
  readonly model: SchemaModel;
14
16
  readonly fields: ReadonlyMap<string, SchemaField>;
15
- /** The primary key, unique fields, unique indexes, one-to-one keys. */
17
+ /**
18
+ * The primary key, unique fields, unique indexes, one-to-one keys, and
19
+ * the case-insensitive keys (D-170).
20
+ */
16
21
  readonly uniques: readonly Unique[];
17
22
  };
18
23
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"names.d.ts","sourceRoot":"","sources":["../src/names.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAE9E;;;GAGG;AACH,MAAM,MAAM,IAAI,GACf,UAAU,GAAG,IAAI,GAAG,SAAS,GAAG,QAAQ,GAAG,oBAAoB,GAAG,WAAW,CAAA;AAE9E,MAAM,MAAM,MAAM,GAAG;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAA;CAClC,CAAA;AAED,MAAM,MAAM,KAAK,GAAG;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAA;IAC3B,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,WAAW,CAAC,CAAA;IACjD,uEAAuE;IACvE,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAA;CACnC,CAAA;AAED;;;;GAIG;AACH,MAAM,MAAM,IAAI,GAAG;IAClB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CACvB,CAAA;AAQD,wBAAgB,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAK1C;AAED,oEAAoE;AACpE,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAM/C;AAED,2EAA2E;AAC3E,MAAM,MAAM,QAAQ,GAAG;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CACvB,CAAA;AAED,qBAAa,OAAO;IACnB,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,KAAK,CAAC,CAAA;IAC3C,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;IACzC,gEAAgE;IAChE,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAA;IAC1E,gEAAgE;IAChE,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAA;IACnD,0DAA0D;IAC1D,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IAEzB,YAAY,MAAM,EAAE,MAAM,EAAE,QAAQ,SAAW,EAoC9C;IAED,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,CAI1B;IAED,gEAAgE;IAChE,MAAM,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CASzC;IAED,IAAI,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAQrC;IAED,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAIvC;IAED,sEAAsE;IACtE,OAAO,CAAC,aAAa;IAuBrB;;;OAGG;IACH,OAAO,CAAC,OAAO;CAqCf"}
1
+ {"version":3,"file":"names.d.ts","sourceRoot":"","sources":["../src/names.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAE9E;;;GAGG;AACH,MAAM,MAAM,IAAI,GACf,UAAU,GAAG,IAAI,GAAG,SAAS,GAAG,QAAQ,GAAG,oBAAoB,GAAG,WAAW,CAAA;AAE9E,MAAM,MAAM,MAAM,GAAG;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAA;IAClC,wEAAwE;IACxE,QAAQ,CAAC,KAAK,CAAC,EAAE,IAAI,CAAA;CACrB,CAAA;AAED,MAAM,MAAM,KAAK,GAAG;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAA;IAC3B,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,WAAW,CAAC,CAAA;IACjD;;;OAGG;IACH,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAA;CACnC,CAAA;AAED;;;;GAIG;AACH,MAAM,MAAM,IAAI,GAAG;IAClB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CACvB,CAAA;AAQD,wBAAgB,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAK1C;AAED,oEAAoE;AACpE,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAM/C;AAED,2EAA2E;AAC3E,MAAM,MAAM,QAAQ,GAAG;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CACvB,CAAA;AAED,qBAAa,OAAO;IACnB,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,KAAK,CAAC,CAAA;IAC3C,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;IACzC,gEAAgE;IAChE,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAA;IAC1E,gEAAgE;IAChE,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAA;IACnD,0DAA0D;IAC1D,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IAEzB,YAAY,MAAM,EAAE,MAAM,EAAE,QAAQ,SAAW,EAqC9C;IAED,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,CAI1B;IAED,gEAAgE;IAChE,MAAM,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CASzC;IAED,IAAI,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAQrC;IAED,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAIvC;IAED,sEAAsE;IACtE,OAAO,CAAC,aAAa;IAuBrB;;;OAGG;IACH,OAAO,CAAC,OAAO;CAqCf"}
package/dist/names.js CHANGED
@@ -43,22 +43,24 @@ export class Catalog {
43
43
  this.tables = tables;
44
44
  const uniques = new Map();
45
45
  for (const table of tables.values()) {
46
- const sets = [['id']];
46
+ const sets = [
47
+ { fields: ['id'], lower: false },
48
+ ];
47
49
  for (const field of table.model.fields) {
48
50
  const stored = field.kind === 'scalar' || (field.kind === 'one' && field.stored);
49
51
  if (stored && field.unique)
50
- sets.push([field.name]);
52
+ sets.push({ fields: [field.name], lower: false });
51
53
  }
52
- for (const index of table.model.indexes) {
53
- if (index.unique)
54
- sets.push(index.fields);
54
+ for (const { unique, fields, mode } of table.model.indexes) {
55
+ if (unique)
56
+ sets.push({ fields, lower: mode === 'insensitive' });
55
57
  }
56
- const named = sets.map((fields, at) => ({
57
- name: constraint(at === 0
58
- ? `${table.name}_pkey`
59
- : `${table.name}_${fields.map((field) => this.column(table, field)).join('_')}_key`),
60
- fields,
61
- }));
58
+ const named = sets.map(({ fields, lower }, at) => {
59
+ const columns = fields.map((field) => this.column(table, field));
60
+ const key = `${table.name}_${columns.join('_')}${lower ? '_lower' : ''}_key`;
61
+ const name = constraint(at === 0 ? `${table.name}_pkey` : key);
62
+ return lower ? { name, fields, lower } : { name, fields };
63
+ });
62
64
  tables.set(table.model.name, { ...table, uniques: named });
63
65
  for (const { name, fields } of named) {
64
66
  uniques.set(name, { model: table.model.name, fields: [...fields] });
@@ -1 +1 @@
1
- {"version":3,"file":"plan-indexes.d.ts","sourceRoot":"","sources":["../src/plan-indexes.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,IAAI,EAAS,MAAM,iBAAiB,CAAA;AAElD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AAkBlD,qDAAqD;AACrD,wBAAgB,WAAW,CAC1B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,aAAa,EACnB,EAAE,EAAE,aAAa,EACjB,IAAI,EAAE,IAAI,GACR,IAAI,CAoBN;AAED,sDAAsD;AACtD,eAAO,MAAM,YAAY,SAClB,aAAa,GAAG,SAAS,MAC3B,aAAa,KACf,OAA8D,CAAA;AAEjE;;;GAGG;AACH,wBAAgB,UAAU,CACzB,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,aAAa,GAAG,SAAS,EAC9B,KAAK,EAAE,aAAa,EACpB,IAAI,EAAE,IAAI,GACR,IAAI,CAgCN"}
1
+ {"version":3,"file":"plan-indexes.d.ts","sourceRoot":"","sources":["../src/plan-indexes.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,IAAI,EAAS,MAAM,iBAAiB,CAAA;AAElD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AAkClD,qDAAqD;AACrD,wBAAgB,WAAW,CAC1B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,aAAa,EACnB,EAAE,EAAE,aAAa,EACjB,IAAI,EAAE,IAAI,GACR,IAAI,CA2BN;AAED,sDAAsD;AACtD,eAAO,MAAM,YAAY,SAClB,aAAa,GAAG,SAAS,MAC3B,aAAa,KACf,OAA8D,CAAA;AAEjE;;;GAGG;AACH,wBAAgB,UAAU,CACzB,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,aAAa,GAAG,SAAS,EAC9B,KAAK,EAAE,aAAa,EACpB,IAAI,EAAE,IAAI,GACR,IAAI,CA4CN"}
@@ -1,8 +1,8 @@
1
1
  // The indexes a plan drops and creates: B-tree, GIN on a list (D-120),
2
- // trigram for `contains` (D-141), and the `_search` column with its GIN
3
- // index (D-148).
2
+ // trigram for `contains` (D-141), the `_search` column with its GIN index
3
+ // (D-148), and the unique indexes of case-insensitive keys (D-170).
4
4
  import { literal } from './fills.js';
5
- import { added, changed, fields, id, list, same } from './plan-parts.js';
5
+ import { added, changed, fields, id, keyOf, list, same } from './plan-parts.js';
6
6
  const indexesOf = (table) => Object.fromEntries(Object.entries(table?.indexes ?? {}).map(([key, columns]) => [
7
7
  key,
8
8
  {
@@ -10,6 +10,16 @@ const indexesOf = (table) => Object.fromEntries(Object.entries(table?.indexes ??
10
10
  gin: columns.some((name) => table?.columns[name]?.type.endsWith('[]') === true),
11
11
  },
12
12
  ]));
13
+ /**
14
+ * A case-insensitive key's columns, a text one lowered under the database's
15
+ * collation, as an insensitive equality reads it (D-170).
16
+ */
17
+ const lowersOf = (table) => Object.fromEntries(Object.entries(table?.lowerUniques ?? {}).map(([key, columns]) => [
18
+ key,
19
+ columns.map((column) => table?.columns[column]?.type === 'text collate "C"'
20
+ ? `lower(${id(column)} collate "default")`
21
+ : id(column)),
22
+ ]));
13
23
  /** Drops the indexes of `from` that `to` changes. */
14
24
  export function dropIndexes(name, from, to, emit) {
15
25
  for (const [key, { columns }] of changed(indexesOf(from), indexesOf(to))) {
@@ -18,6 +28,10 @@ export function dropIndexes(name, from, to, emit) {
18
28
  for (const [key, column] of changed(from.trigrams, to.trigrams)) {
19
29
  emit(`drop index ${id(key)}`, `dropped the contains index on ${fields(from, [column])}`);
20
30
  }
31
+ for (const [key] of changed(lowersOf(from), lowersOf(to))) {
32
+ const columns = from.lowerUniques?.[key] ?? [];
33
+ emit(`drop index ${id(key)}`, `dropped the case-insensitive unique key on ${fields(from, columns)}`);
34
+ }
21
35
  // A generated column holds nothing of its own: a change makes it again.
22
36
  if (from.search !== null && !same(from.search, to.search)) {
23
37
  emit(`alter table ${id(name)} drop column "_search"`, `dropped the search on ${fields(from, from.search.columns)}`);
@@ -36,6 +50,14 @@ export function addIndexes(name, was, table, emit) {
36
50
  for (const [key, column] of added(was?.trigrams ?? {}, table.trigrams)) {
37
51
  emit(`create index ${id(key)} on ${id(name)} using gin ((${id(column)} collate "default") gin_trgm_ops)`, `indexed ${fields(table, [column])} for contains`);
38
52
  }
53
+ for (const [key, lowered] of added(lowersOf(was), lowersOf(table))) {
54
+ const columns = table.lowerUniques?.[key] ?? [];
55
+ emit(`create unique index ${id(key)} on ${id(name)} (${lowered.join(', ')})`, `added a case-insensitive unique key on ${fields(table, columns)}`, was === undefined
56
+ ? {}
57
+ : {
58
+ tries: `a case-insensitive unique key on ${keyOf(table, columns)} fails when two rows share a value regardless of case`,
59
+ });
60
+ }
39
61
  const { search } = table;
40
62
  if (search === null || (was !== undefined && same(was.search, search))) {
41
63
  return;
@@ -13,4 +13,6 @@ export declare const changed: <T>(from: Named<T>, to: Named<T>) => [string, T][]
13
13
  export declare const added: <T>(from: Named<T>, to: Named<T>) => [string, T][];
14
14
  /** The model's names for columns, which messages use. */
15
15
  export declare const fields: (table: TableSnapshot, columns: readonly string[]) => string;
16
+ /** A key's model and fields, as `User (username)`. */
17
+ export declare const keyOf: (table: TableSnapshot, columns: readonly string[]) => string;
16
18
  //# sourceMappingURL=plan-parts.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"plan-parts.d.ts","sourceRoot":"","sources":["../src/plan-parts.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAC3C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AAElD,eAAO,MAAM,EAAE,SAAU,MAAM,KAAG,MAA2C,CAAA;AAE7E,eAAO,MAAM,IAAI,UAAW,SAAS,MAAM,EAAE,KAAG,MACvB,CAAA;AAEzB,eAAO,MAAM,IAAI,MAAO,OAAO,KAAK,OAAO,KAAG,OACN,CAAA;AAExC,MAAM,MAAM,KAAK,CAAC,CAAC,IAAI;IAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,CAAC,CAAA;CAAE,CAAA;AAErD,MAAM,MAAM,IAAI,GAAG,CAClB,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,EACZ,IAAI,CAAC,EAAE,IAAI,CAAC,SAAS,EAAE,OAAO,GAAG,OAAO,GAAG,QAAQ,CAAC,KAChD,IAAI,CAAA;AAET,kEAAkE;AAClE,eAAO,MAAM,OAAO,GAAI,CAAC,QAAQ,KAAK,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,KAAG,CAAC,MAAM,EAAE,CAAC,CAAC,EACE,CAAA;AAEvE,kEAAkE;AAClE,eAAO,MAAM,KAAK,GAAI,CAAC,QAAQ,KAAK,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,KAAG,CAAC,MAAM,EAAE,CAAC,CAAC,EACjD,CAAA;AAElB,yDAAyD;AACzD,eAAO,MAAM,MAAM,UACX,aAAa,WACX,SAAS,MAAM,EAAE,KACxB,MACwE,CAAA"}
1
+ {"version":3,"file":"plan-parts.d.ts","sourceRoot":"","sources":["../src/plan-parts.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAC3C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AAElD,eAAO,MAAM,EAAE,SAAU,MAAM,KAAG,MAA2C,CAAA;AAE7E,eAAO,MAAM,IAAI,UAAW,SAAS,MAAM,EAAE,KAAG,MACvB,CAAA;AAEzB,eAAO,MAAM,IAAI,MAAO,OAAO,KAAK,OAAO,KAAG,OACN,CAAA;AAExC,MAAM,MAAM,KAAK,CAAC,CAAC,IAAI;IAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,CAAC,CAAA;CAAE,CAAA;AAErD,MAAM,MAAM,IAAI,GAAG,CAClB,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,EACZ,IAAI,CAAC,EAAE,IAAI,CAAC,SAAS,EAAE,OAAO,GAAG,OAAO,GAAG,QAAQ,CAAC,KAChD,IAAI,CAAA;AAET,kEAAkE;AAClE,eAAO,MAAM,OAAO,GAAI,CAAC,QAAQ,KAAK,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,KAAG,CAAC,MAAM,EAAE,CAAC,CAAC,EACE,CAAA;AAEvE,kEAAkE;AAClE,eAAO,MAAM,KAAK,GAAI,CAAC,QAAQ,KAAK,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,KAAG,CAAC,MAAM,EAAE,CAAC,CAAC,EACjD,CAAA;AAElB,yDAAyD;AACzD,eAAO,MAAM,MAAM,UACX,aAAa,WACX,SAAS,MAAM,EAAE,KACxB,MACwE,CAAA;AAE3E,sDAAsD;AACtD,eAAO,MAAM,KAAK,UACV,aAAa,WACX,SAAS,MAAM,EAAE,KACxB,MAAsD,CAAA"}
@@ -7,3 +7,5 @@ export const changed = (from, to) => Object.entries(from).filter(([name, value])
7
7
  export const added = (from, to) => changed(to, from);
8
8
  /** The model's names for columns, which messages use. */
9
9
  export const fields = (table, columns) => columns.map((column) => table.columns[column]?.field ?? column).join(', ');
10
+ /** A key's model and fields, as `User (username)`. */
11
+ export const keyOf = (table, columns) => `${table.model} (${fields(table, columns)})`;
@@ -1 +1 @@
1
- {"version":3,"file":"plan.d.ts","sourceRoot":"","sources":["../src/plan.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAC3C,OAAO,KAAK,EAAQ,KAAK,EAAE,MAAM,YAAY,CAAA;AAK7C,OAAO,KAAK,EAAqB,QAAQ,EAAiB,MAAM,eAAe,CAAA;AA4S/E,kEAAkE;AAClE,wBAAgB,IAAI,CACnB,IAAI,EAAE,QAAQ,GAAG,SAAS,EAC1B,EAAE,EAAE,QAAQ,EACZ,KAAK,EAAE,KAAK,GACV,SAAS,EAAE,CAyEb"}
1
+ {"version":3,"file":"plan.d.ts","sourceRoot":"","sources":["../src/plan.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAC3C,OAAO,KAAK,EAAQ,KAAK,EAAE,MAAM,YAAY,CAAA;AAK7C,OAAO,KAAK,EAAqB,QAAQ,EAAiB,MAAM,eAAe,CAAA;AAyS/E,kEAAkE;AAClE,wBAAgB,IAAI,CACnB,IAAI,EAAE,QAAQ,GAAG,SAAS,EAC1B,EAAE,EAAE,QAAQ,EACZ,KAAK,EAAE,KAAK,GACV,SAAS,EAAE,CAyEb"}
package/dist/plan.js CHANGED
@@ -1,8 +1,7 @@
1
1
  import { literal } from './fills.js';
2
2
  import { addIndexes, addsTrigrams, dropIndexes } from './plan-indexes.js';
3
- import { added, changed, fields, id, list, same } from './plan-parts.js';
3
+ import { added, changed, fields, id, keyOf, list, same } from './plan-parts.js';
4
4
  /** A key's columns as a tried change names them: `Locker (label, size)`. */
5
- const keyOf = (table, columns) => `${table.model} (${fields(table, columns)})`;
6
5
  /** An enum's values; a list's elements are among them, with `<@` (D-127). */
7
6
  function checked(name, { column, values, list: all }) {
8
7
  const items = values.map(literal).join(', ');
@@ -0,0 +1,4 @@
1
+ import type { Snapshot } from './snapshot.ts';
2
+ /** A snapshot read back from a database or a file, checked. */
3
+ export declare function parseSnapshot(value: unknown): Snapshot;
4
+ //# sourceMappingURL=snapshot-parse.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshot-parse.d.ts","sourceRoot":"","sources":["../src/snapshot-parse.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAKX,QAAQ,EAER,MAAM,eAAe,CAAA;AA4HtB,+DAA+D;AAC/D,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,QAAQ,CAMtD"}
@@ -0,0 +1,109 @@
1
+ const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
2
+ const isStrings = (value) => Array.isArray(value) && value.every((item) => typeof item === 'string');
3
+ /** Each entry of a record, checked by `read`, or `undefined`. */
4
+ function entries(value, read) {
5
+ if (!isRecord(value))
6
+ return undefined;
7
+ const result = {};
8
+ for (const [key, item] of Object.entries(value)) {
9
+ const checked = read(item);
10
+ if (checked === undefined)
11
+ return undefined;
12
+ result[key] = checked;
13
+ }
14
+ return result;
15
+ }
16
+ const columnOf = (value) => isRecord(value) &&
17
+ typeof value['field'] === 'string' &&
18
+ typeof value['type'] === 'string' &&
19
+ typeof value['nullable'] === 'boolean'
20
+ ? {
21
+ field: value['field'],
22
+ type: value['type'],
23
+ nullable: value['nullable'],
24
+ }
25
+ : undefined;
26
+ const checkOf = (value) => isRecord(value) &&
27
+ typeof value['column'] === 'string' &&
28
+ isStrings(value['values'])
29
+ ? {
30
+ column: value['column'],
31
+ values: value['values'],
32
+ ...(value['list'] === true ? { list: true } : {}),
33
+ }
34
+ : undefined;
35
+ const foreignKeyOf = (value) => isRecord(value) &&
36
+ typeof value['column'] === 'string' &&
37
+ typeof value['table'] === 'string' &&
38
+ typeof value['cascade'] === 'boolean'
39
+ ? {
40
+ column: value['column'],
41
+ table: value['table'],
42
+ cascade: value['cascade'],
43
+ }
44
+ : undefined;
45
+ const stringsOf = (value) => isStrings(value) ? value : undefined;
46
+ /** A search, `null` for none, or `undefined` when it is neither. */
47
+ const searchOf = (value) => value === null
48
+ ? null
49
+ : isRecord(value) &&
50
+ typeof value['index'] === 'string' &&
51
+ typeof value['language'] === 'string' &&
52
+ isStrings(value['columns'])
53
+ ? {
54
+ index: value['index'],
55
+ language: value['language'],
56
+ columns: value['columns'],
57
+ }
58
+ : undefined;
59
+ function tableOf(value) {
60
+ if (!isRecord(value))
61
+ return undefined;
62
+ const key = value['primaryKey'];
63
+ const columns = entries(value['columns'], columnOf);
64
+ const uniques = entries(value['uniques'], stringsOf);
65
+ const checks = entries(value['checks'], checkOf);
66
+ const indexes = entries(value['indexes'], stringsOf);
67
+ const trigrams = entries(value['trigrams'], (item) => typeof item === 'string' ? item : undefined);
68
+ const foreignKeys = entries(value['foreignKeys'], foreignKeyOf);
69
+ const search = searchOf(value['search']);
70
+ const lower = value['lowerUniques'] === undefined
71
+ ? {}
72
+ : entries(value['lowerUniques'], stringsOf);
73
+ const model = value['model'];
74
+ if (typeof model !== 'string' ||
75
+ !isRecord(key) ||
76
+ typeof key['name'] !== 'string' ||
77
+ !isStrings(key['columns']) ||
78
+ columns === undefined ||
79
+ uniques === undefined ||
80
+ checks === undefined ||
81
+ indexes === undefined ||
82
+ trigrams === undefined ||
83
+ foreignKeys === undefined ||
84
+ search === undefined ||
85
+ lower === undefined) {
86
+ return undefined;
87
+ }
88
+ const primaryKey = { name: key['name'], columns: key['columns'] };
89
+ return {
90
+ model,
91
+ columns,
92
+ primaryKey,
93
+ uniques,
94
+ checks,
95
+ indexes,
96
+ trigrams,
97
+ foreignKeys,
98
+ search,
99
+ ...(Object.keys(lower).length > 0 ? { lowerUniques: lower } : {}),
100
+ };
101
+ }
102
+ /** A snapshot read back from a database or a file, checked. */
103
+ export function parseSnapshot(value) {
104
+ const tables = isRecord(value) ? entries(value['tables'], tableOf) : undefined;
105
+ if (!isRecord(value) || value['postgres'] !== 1 || tables === undefined) {
106
+ throw new Error('The schema snapshot is not one postgres() wrote.');
107
+ }
108
+ return { postgres: 1, tables };
109
+ }
@@ -47,6 +47,10 @@ export type TableSnapshot = {
47
47
  };
48
48
  /** The `_search` column, from these columns, and its GIN index (D-148). */
49
49
  readonly search: Search | null;
50
+ /** The case-insensitive keys' columns, absent when none (D-170). */
51
+ readonly lowerUniques?: {
52
+ readonly [name: string]: string[];
53
+ };
50
54
  };
51
55
  export type Search = {
52
56
  readonly index: string;
@@ -61,6 +65,4 @@ export type Snapshot = {
61
65
  };
62
66
  /** The snapshot of the catalog's tables, join tables last. */
63
67
  export declare function snapshotOf(catalog: Catalog): Snapshot;
64
- /** A snapshot read back from a database or a file, checked. */
65
- export declare function parseSnapshot(value: unknown): Snapshot;
66
68
  //# sourceMappingURL=snapshot.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"snapshot.d.ts","sourceRoot":"","sources":["../src/snapshot.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAS,MAAM,YAAY,CAAA;AAIhD,8DAA8D;AAC9D,MAAM,MAAM,MAAM,GAAG;IACpB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAA;CAC1B,CAAA;AAED,MAAM,MAAM,UAAU,GAAG;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;CACzB,CAAA;AAED,iEAAiE;AACjE,MAAM,MAAM,KAAK,GAAG;IACnB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,CAAA;IACzB,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,CAAA;CACpB,CAAA;AAED;;;GAGG;AACH,MAAM,MAAM,aAAa,GAAG;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,OAAO,EAAE;QAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAA;IACrD,QAAQ,CAAC,UAAU,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,CAAA;KAAE,CAAA;IAC1E,QAAQ,CAAC,OAAO,EAAE;QAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;KAAE,CAAA;IACvD,QAAQ,CAAC,MAAM,EAAE;QAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,KAAK,CAAA;KAAE,CAAA;IACnD,QAAQ,CAAC,OAAO,EAAE;QAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;KAAE,CAAA;IACvD,sDAAsD;IACtD,QAAQ,CAAC,QAAQ,EAAE;QAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAA;IACtD,QAAQ,CAAC,WAAW,EAAE;QAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,UAAU,CAAA;KAAE,CAAA;IAC7D,2EAA2E;IAC3E,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;CAC9B,CAAA;AAED,MAAM,MAAM,MAAM,GAAG;IACpB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAC1B,CAAA;AAED,MAAM,MAAM,QAAQ,GAAG;IACtB,QAAQ,CAAC,QAAQ,EAAE,CAAC,CAAA;IACpB,QAAQ,CAAC,MAAM,EAAE;QAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,aAAa,CAAA;KAAE,CAAA;CAC3D,CAAA;AA8HD,8DAA8D;AAC9D,wBAAgB,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,QAAQ,CAkFrD;AAsHD,+DAA+D;AAC/D,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,QAAQ,CAMtD"}
1
+ {"version":3,"file":"snapshot.d.ts","sourceRoot":"","sources":["../src/snapshot.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAS,MAAM,YAAY,CAAA;AAIhD,8DAA8D;AAC9D,MAAM,MAAM,MAAM,GAAG;IACpB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAA;CAC1B,CAAA;AAED,MAAM,MAAM,UAAU,GAAG;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;CACzB,CAAA;AAED,iEAAiE;AACjE,MAAM,MAAM,KAAK,GAAG;IACnB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,CAAA;IACzB,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,CAAA;CACpB,CAAA;AAED;;;GAGG;AACH,MAAM,MAAM,aAAa,GAAG;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,OAAO,EAAE;QAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAA;IACrD,QAAQ,CAAC,UAAU,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,CAAA;KAAE,CAAA;IAC1E,QAAQ,CAAC,OAAO,EAAE;QAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;KAAE,CAAA;IACvD,QAAQ,CAAC,MAAM,EAAE;QAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,KAAK,CAAA;KAAE,CAAA;IACnD,QAAQ,CAAC,OAAO,EAAE;QAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;KAAE,CAAA;IACvD,sDAAsD;IACtD,QAAQ,CAAC,QAAQ,EAAE;QAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAA;IACtD,QAAQ,CAAC,WAAW,EAAE;QAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,UAAU,CAAA;KAAE,CAAA;IAC7D,2EAA2E;IAC3E,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,oEAAoE;IACpE,QAAQ,CAAC,YAAY,CAAC,EAAE;QAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;KAAE,CAAA;CAC7D,CAAA;AAED,MAAM,MAAM,MAAM,GAAG;IACpB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAC1B,CAAA;AAED,MAAM,MAAM,QAAQ,GAAG;IACtB,QAAQ,CAAC,QAAQ,EAAE,CAAC,CAAA;IACpB,QAAQ,CAAC,MAAM,EAAE;QAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,aAAa,CAAA;KAAE,CAAA;CAC3D,CAAA;AA+HD,8DAA8D;AAC9D,wBAAgB,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,QAAQ,CAsFrD"}
package/dist/snapshot.js CHANGED
@@ -105,9 +105,16 @@ export function snapshotOf(catalog) {
105
105
  field(catalog, claims, table, building, given);
106
106
  }
107
107
  const [, ...uniques] = table.uniques;
108
- for (const { name, fields } of uniques) {
109
- const what = `the unique key on ${on(model, fields)}`;
110
- building.uniques[schema(name, what)] = fields.map((given) => catalog.column(table, given));
108
+ for (const { name, fields, lower } of uniques) {
109
+ const key = `unique key on ${on(model, fields)}`;
110
+ const columns = fields.map((given) => catalog.column(table, given));
111
+ if (lower === undefined) {
112
+ building.uniques[schema(name, `the ${key}`)] = columns;
113
+ }
114
+ else {
115
+ const claimed = schema(name, `the case-insensitive ${key}`);
116
+ (building.lowerUniques ??= {})[claimed] = columns;
117
+ }
111
118
  }
112
119
  for (const { fields, unique, contains } of table.model.indexes) {
113
120
  if (unique)
@@ -166,107 +173,3 @@ export function snapshotOf(catalog) {
166
173
  }
167
174
  return { postgres: 1, tables };
168
175
  }
169
- const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
170
- const isStrings = (value) => Array.isArray(value) && value.every((item) => typeof item === 'string');
171
- /** Each entry of a record, checked by `read`, or `undefined`. */
172
- function entries(value, read) {
173
- if (!isRecord(value))
174
- return undefined;
175
- const result = {};
176
- for (const [key, item] of Object.entries(value)) {
177
- const checked = read(item);
178
- if (checked === undefined)
179
- return undefined;
180
- result[key] = checked;
181
- }
182
- return result;
183
- }
184
- const columnOf = (value) => isRecord(value) &&
185
- typeof value['field'] === 'string' &&
186
- typeof value['type'] === 'string' &&
187
- typeof value['nullable'] === 'boolean'
188
- ? {
189
- field: value['field'],
190
- type: value['type'],
191
- nullable: value['nullable'],
192
- }
193
- : undefined;
194
- const checkOf = (value) => isRecord(value) &&
195
- typeof value['column'] === 'string' &&
196
- isStrings(value['values'])
197
- ? {
198
- column: value['column'],
199
- values: value['values'],
200
- ...(value['list'] === true ? { list: true } : {}),
201
- }
202
- : undefined;
203
- const foreignKeyOf = (value) => isRecord(value) &&
204
- typeof value['column'] === 'string' &&
205
- typeof value['table'] === 'string' &&
206
- typeof value['cascade'] === 'boolean'
207
- ? {
208
- column: value['column'],
209
- table: value['table'],
210
- cascade: value['cascade'],
211
- }
212
- : undefined;
213
- const stringsOf = (value) => isStrings(value) ? value : undefined;
214
- /** A search, `null` for none, or `undefined` when it is neither. */
215
- const searchOf = (value) => value === null
216
- ? null
217
- : isRecord(value) &&
218
- typeof value['index'] === 'string' &&
219
- typeof value['language'] === 'string' &&
220
- isStrings(value['columns'])
221
- ? {
222
- index: value['index'],
223
- language: value['language'],
224
- columns: value['columns'],
225
- }
226
- : undefined;
227
- function tableOf(value) {
228
- if (!isRecord(value))
229
- return undefined;
230
- const key = value['primaryKey'];
231
- const columns = entries(value['columns'], columnOf);
232
- const uniques = entries(value['uniques'], stringsOf);
233
- const checks = entries(value['checks'], checkOf);
234
- const indexes = entries(value['indexes'], stringsOf);
235
- const trigrams = entries(value['trigrams'], (item) => typeof item === 'string' ? item : undefined);
236
- const foreignKeys = entries(value['foreignKeys'], foreignKeyOf);
237
- const search = searchOf(value['search']);
238
- const model = value['model'];
239
- if (typeof model !== 'string' ||
240
- !isRecord(key) ||
241
- typeof key['name'] !== 'string' ||
242
- !isStrings(key['columns']) ||
243
- columns === undefined ||
244
- uniques === undefined ||
245
- checks === undefined ||
246
- indexes === undefined ||
247
- trigrams === undefined ||
248
- foreignKeys === undefined ||
249
- search === undefined) {
250
- return undefined;
251
- }
252
- const primaryKey = { name: key['name'], columns: key['columns'] };
253
- return {
254
- model,
255
- columns,
256
- primaryKey,
257
- uniques,
258
- checks,
259
- indexes,
260
- trigrams,
261
- foreignKeys,
262
- search,
263
- };
264
- }
265
- /** A snapshot read back from a database or a file, checked. */
266
- export function parseSnapshot(value) {
267
- const tables = isRecord(value) ? entries(value['tables'], tableOf) : undefined;
268
- if (!isRecord(value) || value['postgres'] !== 1 || tables === undefined) {
269
- throw new Error('The schema snapshot is not one postgres() wrote.');
270
- }
271
- return { postgres: 1, tables };
272
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimabase/postgres",
3
- "version": "0.1.2",
3
+ "version": "0.3.0-canary.20261031104207",
4
4
  "description": "The PostgreSQL adapter for UltimaBase.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -36,12 +36,12 @@
36
36
  "pg-connection-string": "2.14.1"
37
37
  },
38
38
  "peerDependencies": {
39
- "ultimabase": "^0.1.2"
39
+ "ultimabase": "0.3.0-canary.20261031104207"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/pg": "8.23.1",
43
- "@ultimabase/testing": "0.1.2",
44
- "ultimabase": "0.1.2",
43
+ "@ultimabase/testing": "0.3.0-canary.20261031104207",
44
+ "ultimabase": "0.3.0-canary.20261031104207",
45
45
  "vite": "8.3.4",
46
46
  "vitest": "5.0.5"
47
47
  }
package/src/adapter.ts CHANGED
@@ -39,7 +39,8 @@ import {
39
39
  scratchName,
40
40
  trigrams,
41
41
  } from './scratch.ts'
42
- import { parseSnapshot, snapshotOf } from './snapshot.ts'
42
+ import { parseSnapshot } from './snapshot-parse.ts'
43
+ import { snapshotOf } from './snapshot.ts'
43
44
  import type { Statement } from './write.ts'
44
45
  import { compileWrite, translate } from './write.ts'
45
46
 
package/src/filter.ts CHANGED
@@ -193,11 +193,21 @@ export class Filters {
193
193
  condition: Condition,
194
194
  ): Sql {
195
195
  const column = this.column(table, alias, name)
196
- return this.compare(column, this.catalog.kind(table, name), condition)
196
+ const kind = this.catalog.kind(table, name)
197
+ const lowered =
198
+ (kind === 'string' || kind === 'enum') &&
199
+ table.uniques.some(
200
+ (unique) => unique.lower === true && unique.fields.includes(name),
201
+ )
202
+ return this.compare(column, kind, condition, lowered)
197
203
  }
198
204
 
199
- /** A value of `kind`, a column or an aggregate (D-149), meets `condition`. */
200
- compare(column: Sql, kind: Kind, condition: Condition): Sql {
205
+ /**
206
+ * A value of `kind`, a column or an aggregate (D-149), meets `condition`;
207
+ * a `lowered` column's insensitive equality reads its case-insensitive
208
+ * key's index (D-170).
209
+ */
210
+ compare(column: Sql, kind: Kind, condition: Condition, lowered = false): Sql {
201
211
  if (condition.path !== undefined) return jsonPath(column, condition)
202
212
  // An id matches and compares as its string, as in `memory()`.
203
213
  const cast = kind === 'id' ? sql`cast(${column} as text)` : column
@@ -218,9 +228,11 @@ export class Filters {
218
228
  const equal =
219
229
  operand === null
220
230
  ? sql`${column} is null`
221
- : insensitive
222
- ? sql`${text} ilike ${pattern(operand, '', '')}`
223
- : this.equal(column, kind, operand)
231
+ : lowered && insensitive
232
+ ? sql`lower(${collated}) = lower(cast(${operand} as text))`
233
+ : insensitive
234
+ ? sql`${text} ilike ${pattern(operand, '', '')}`
235
+ : this.equal(column, kind, operand)
224
236
  parts.push(operator === 'equals' ? equal : sql`(not ${equal})`)
225
237
  break
226
238
  }
package/src/migrations.ts CHANGED
@@ -7,7 +7,7 @@ import { parse } from 'pg-connection-string'
7
7
  import type { Migration, Run, Stored } from 'ultimabase'
8
8
  import { isLocalHost, MigrationFailed, Refused } from 'ultimabase'
9
9
  import { id } from './plan-parts.ts'
10
- import { parseSnapshot } from './snapshot.ts'
10
+ import { parseSnapshot } from './snapshot-parse.ts'
11
11
 
12
12
  /** The tables UltimaBase keeps its own records in. */
13
13
  export const tables: readonly string[] = [
package/src/names.ts CHANGED
@@ -14,13 +14,18 @@ export type Kind =
14
14
  export type Unique = {
15
15
  readonly name: string
16
16
  readonly fields: readonly string[]
17
+ /** A case-insensitive key compares its text columns lowered (D-170). */
18
+ readonly lower?: true
17
19
  }
18
20
 
19
21
  export type Table = {
20
22
  readonly name: string
21
23
  readonly model: SchemaModel
22
24
  readonly fields: ReadonlyMap<string, SchemaField>
23
- /** The primary key, unique fields, unique indexes, one-to-one keys. */
25
+ /**
26
+ * The primary key, unique fields, unique indexes, one-to-one keys, and
27
+ * the case-insensitive keys (D-170).
28
+ */
24
29
  readonly uniques: readonly Unique[]
25
30
  }
26
31
 
@@ -88,23 +93,24 @@ export class Catalog {
88
93
  this.tables = tables
89
94
  const uniques = new Map<string, { model: string; fields: string[] }>()
90
95
  for (const table of tables.values()) {
91
- const sets: (readonly string[])[] = [['id']]
96
+ const sets: { fields: readonly string[]; lower: boolean }[] = [
97
+ { fields: ['id'], lower: false },
98
+ ]
92
99
  for (const field of table.model.fields) {
93
100
  const stored =
94
101
  field.kind === 'scalar' || (field.kind === 'one' && field.stored)
95
- if (stored && field.unique) sets.push([field.name])
102
+ if (stored && field.unique)
103
+ sets.push({ fields: [field.name], lower: false })
96
104
  }
97
- for (const index of table.model.indexes) {
98
- if (index.unique) sets.push(index.fields)
105
+ for (const { unique, fields, mode } of table.model.indexes) {
106
+ if (unique) sets.push({ fields, lower: mode === 'insensitive' })
99
107
  }
100
- const named = sets.map((fields, at) => ({
101
- name: constraint(
102
- at === 0
103
- ? `${table.name}_pkey`
104
- : `${table.name}_${fields.map((field) => this.column(table, field)).join('_')}_key`,
105
- ),
106
- fields,
107
- }))
108
+ const named = sets.map(({ fields, lower }, at) => {
109
+ const columns = fields.map((field) => this.column(table, field))
110
+ const key = `${table.name}_${columns.join('_')}${lower ? '_lower' : ''}_key`
111
+ const name = constraint(at === 0 ? `${table.name}_pkey` : key)
112
+ return lower ? { name, fields, lower } : { name, fields }
113
+ })
108
114
  tables.set(table.model.name, { ...table, uniques: named })
109
115
  for (const { name, fields } of named) {
110
116
  uniques.set(name, { model: table.model.name, fields: [...fields] })
@@ -1,9 +1,9 @@
1
1
  // The indexes a plan drops and creates: B-tree, GIN on a list (D-120),
2
- // trigram for `contains` (D-141), and the `_search` column with its GIN
3
- // index (D-148).
2
+ // trigram for `contains` (D-141), the `_search` column with its GIN index
3
+ // (D-148), and the unique indexes of case-insensitive keys (D-170).
4
4
  import { literal } from './fills.ts'
5
5
  import type { Emit, Named } from './plan-parts.ts'
6
- import { added, changed, fields, id, list, same } from './plan-parts.ts'
6
+ import { added, changed, fields, id, keyOf, list, same } from './plan-parts.ts'
7
7
  import type { TableSnapshot } from './snapshot.ts'
8
8
 
9
9
  /** An index, by GIN on a list (D-120, D-127): its column's type decides. */
@@ -22,6 +22,22 @@ const indexesOf = (table: TableSnapshot | undefined): Named<Index> =>
22
22
  ]),
23
23
  )
24
24
 
25
+ /**
26
+ * A case-insensitive key's columns, a text one lowered under the database's
27
+ * collation, as an insensitive equality reads it (D-170).
28
+ */
29
+ const lowersOf = (table: TableSnapshot | undefined): Named<string[]> =>
30
+ Object.fromEntries(
31
+ Object.entries(table?.lowerUniques ?? {}).map(([key, columns]) => [
32
+ key,
33
+ columns.map((column) =>
34
+ table?.columns[column]?.type === 'text collate "C"'
35
+ ? `lower(${id(column)} collate "default")`
36
+ : id(column),
37
+ ),
38
+ ]),
39
+ )
40
+
25
41
  /** Drops the indexes of `from` that `to` changes. */
26
42
  export function dropIndexes(
27
43
  name: string,
@@ -41,6 +57,13 @@ export function dropIndexes(
41
57
  `dropped the contains index on ${fields(from, [column])}`,
42
58
  )
43
59
  }
60
+ for (const [key] of changed(lowersOf(from), lowersOf(to))) {
61
+ const columns = from.lowerUniques?.[key] ?? []
62
+ emit(
63
+ `drop index ${id(key)}`,
64
+ `dropped the case-insensitive unique key on ${fields(from, columns)}`,
65
+ )
66
+ }
44
67
  // A generated column holds nothing of its own: a change makes it again.
45
68
  if (from.search !== null && !same(from.search, to.search)) {
46
69
  emit(
@@ -81,6 +104,18 @@ export function addIndexes(
81
104
  `indexed ${fields(table, [column])} for contains`,
82
105
  )
83
106
  }
107
+ for (const [key, lowered] of added(lowersOf(was), lowersOf(table))) {
108
+ const columns = table.lowerUniques?.[key] ?? []
109
+ emit(
110
+ `create unique index ${id(key)} on ${id(name)} (${lowered.join(', ')})`,
111
+ `added a case-insensitive unique key on ${fields(table, columns)}`,
112
+ was === undefined
113
+ ? {}
114
+ : {
115
+ tries: `a case-insensitive unique key on ${keyOf(table, columns)} fails when two rows share a value regardless of case`,
116
+ },
117
+ )
118
+ }
84
119
  const { search } = table
85
120
  if (search === null || (was !== undefined && same(was.search, search))) {
86
121
  return
package/src/plan-parts.ts CHANGED
@@ -33,3 +33,9 @@ export const fields = (
33
33
  columns: readonly string[],
34
34
  ): string =>
35
35
  columns.map((column) => table.columns[column]?.field ?? column).join(', ')
36
+
37
+ /** A key's model and fields, as `User (username)`. */
38
+ export const keyOf = (
39
+ table: TableSnapshot,
40
+ columns: readonly string[],
41
+ ): string => `${table.model} (${fields(table, columns)})`
package/src/plan.ts CHANGED
@@ -7,13 +7,10 @@ import type { Fill, Fills } from './fills.ts'
7
7
  import { literal } from './fills.ts'
8
8
  import { addIndexes, addsTrigrams, dropIndexes } from './plan-indexes.ts'
9
9
  import type { Emit, Named } from './plan-parts.ts'
10
- import { added, changed, fields, id, list, same } from './plan-parts.ts'
10
+ import { added, changed, fields, id, keyOf, list, same } from './plan-parts.ts'
11
11
  import type { Check, ForeignKey, Snapshot, TableSnapshot } from './snapshot.ts'
12
12
 
13
13
  /** A key's columns as a tried change names them: `Locker (label, size)`. */
14
- const keyOf = (table: TableSnapshot, columns: readonly string[]): string =>
15
- `${table.model} (${fields(table, columns)})`
16
-
17
14
  /** An enum's values; a list's elements are among them, with `<@` (D-127). */
18
15
  function checked(name: string, { column, values, list: all }: Check): string {
19
16
  const items = values.map(literal).join(', ')
@@ -0,0 +1,141 @@
1
+ // A snapshot read back from a database or `migrations/schema.json` (D-105),
2
+ // checked; a key 0.2 did not write is optional (D-170).
3
+ import type {
4
+ Check,
5
+ Column,
6
+ ForeignKey,
7
+ Search,
8
+ Snapshot,
9
+ TableSnapshot,
10
+ } from './snapshot.ts'
11
+
12
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
13
+ typeof value === 'object' && value !== null && !Array.isArray(value)
14
+
15
+ const isStrings = (value: unknown): value is string[] =>
16
+ Array.isArray(value) && value.every((item) => typeof item === 'string')
17
+
18
+ /** Each entry of a record, checked by `read`, or `undefined`. */
19
+ function entries<T>(
20
+ value: unknown,
21
+ read: (item: unknown) => T | undefined,
22
+ ): Record<string, T> | undefined {
23
+ if (!isRecord(value)) return undefined
24
+ const result: Record<string, T> = {}
25
+ for (const [key, item] of Object.entries(value)) {
26
+ const checked = read(item)
27
+ if (checked === undefined) return undefined
28
+ result[key] = checked
29
+ }
30
+ return result
31
+ }
32
+
33
+ const columnOf = (value: unknown): Column | undefined =>
34
+ isRecord(value) &&
35
+ typeof value['field'] === 'string' &&
36
+ typeof value['type'] === 'string' &&
37
+ typeof value['nullable'] === 'boolean'
38
+ ? {
39
+ field: value['field'],
40
+ type: value['type'],
41
+ nullable: value['nullable'],
42
+ }
43
+ : undefined
44
+
45
+ const checkOf = (value: unknown): Check | undefined =>
46
+ isRecord(value) &&
47
+ typeof value['column'] === 'string' &&
48
+ isStrings(value['values'])
49
+ ? {
50
+ column: value['column'],
51
+ values: value['values'],
52
+ ...(value['list'] === true ? { list: true as const } : {}),
53
+ }
54
+ : undefined
55
+
56
+ const foreignKeyOf = (value: unknown): ForeignKey | undefined =>
57
+ isRecord(value) &&
58
+ typeof value['column'] === 'string' &&
59
+ typeof value['table'] === 'string' &&
60
+ typeof value['cascade'] === 'boolean'
61
+ ? {
62
+ column: value['column'],
63
+ table: value['table'],
64
+ cascade: value['cascade'],
65
+ }
66
+ : undefined
67
+
68
+ const stringsOf = (value: unknown): string[] | undefined =>
69
+ isStrings(value) ? value : undefined
70
+
71
+ /** A search, `null` for none, or `undefined` when it is neither. */
72
+ const searchOf = (value: unknown): Search | null | undefined =>
73
+ value === null
74
+ ? null
75
+ : isRecord(value) &&
76
+ typeof value['index'] === 'string' &&
77
+ typeof value['language'] === 'string' &&
78
+ isStrings(value['columns'])
79
+ ? {
80
+ index: value['index'],
81
+ language: value['language'],
82
+ columns: value['columns'],
83
+ }
84
+ : undefined
85
+
86
+ function tableOf(value: unknown): TableSnapshot | undefined {
87
+ if (!isRecord(value)) return undefined
88
+ const key = value['primaryKey']
89
+ const columns = entries(value['columns'], columnOf)
90
+ const uniques = entries(value['uniques'], stringsOf)
91
+ const checks = entries(value['checks'], checkOf)
92
+ const indexes = entries(value['indexes'], stringsOf)
93
+ const trigrams = entries(value['trigrams'], (item) =>
94
+ typeof item === 'string' ? item : undefined,
95
+ )
96
+ const foreignKeys = entries(value['foreignKeys'], foreignKeyOf)
97
+ const search = searchOf(value['search'])
98
+ const lower =
99
+ value['lowerUniques'] === undefined
100
+ ? {}
101
+ : entries(value['lowerUniques'], stringsOf)
102
+ const model = value['model']
103
+ if (
104
+ typeof model !== 'string' ||
105
+ !isRecord(key) ||
106
+ typeof key['name'] !== 'string' ||
107
+ !isStrings(key['columns']) ||
108
+ columns === undefined ||
109
+ uniques === undefined ||
110
+ checks === undefined ||
111
+ indexes === undefined ||
112
+ trigrams === undefined ||
113
+ foreignKeys === undefined ||
114
+ search === undefined ||
115
+ lower === undefined
116
+ ) {
117
+ return undefined
118
+ }
119
+ const primaryKey = { name: key['name'], columns: key['columns'] }
120
+ return {
121
+ model,
122
+ columns,
123
+ primaryKey,
124
+ uniques,
125
+ checks,
126
+ indexes,
127
+ trigrams,
128
+ foreignKeys,
129
+ search,
130
+ ...(Object.keys(lower).length > 0 ? { lowerUniques: lower } : {}),
131
+ }
132
+ }
133
+
134
+ /** A snapshot read back from a database or a file, checked. */
135
+ export function parseSnapshot(value: unknown): Snapshot {
136
+ const tables = isRecord(value) ? entries(value['tables'], tableOf) : undefined
137
+ if (!isRecord(value) || value['postgres'] !== 1 || tables === undefined) {
138
+ throw new Error('The schema snapshot is not one postgres() wrote.')
139
+ }
140
+ return { postgres: 1, tables }
141
+ }
package/src/snapshot.ts CHANGED
@@ -42,6 +42,8 @@ export type TableSnapshot = {
42
42
  readonly foreignKeys: { readonly [name: string]: ForeignKey }
43
43
  /** The `_search` column, from these columns, and its GIN index (D-148). */
44
44
  readonly search: Search | null
45
+ /** The case-insensitive keys' columns, absent when none (D-170). */
46
+ readonly lowerUniques?: { readonly [name: string]: string[] }
45
47
  }
46
48
 
47
49
  export type Search = {
@@ -83,6 +85,7 @@ type Building = {
83
85
  trigrams: Record<string, string>
84
86
  foreignKeys: Record<string, ForeignKey>
85
87
  search: Search | null
88
+ lowerUniques?: Record<string, string[]>
86
89
  }
87
90
 
88
91
  type Claim = (name: string, what: string) => string
@@ -200,11 +203,15 @@ export function snapshotOf(catalog: Catalog): Snapshot {
200
203
  field(catalog, claims, table, building, given)
201
204
  }
202
205
  const [, ...uniques] = table.uniques
203
- for (const { name, fields } of uniques) {
204
- const what = `the unique key on ${on(model, fields)}`
205
- building.uniques[schema(name, what)] = fields.map((given) =>
206
- catalog.column(table, given),
207
- )
206
+ for (const { name, fields, lower } of uniques) {
207
+ const key = `unique key on ${on(model, fields)}`
208
+ const columns = fields.map((given) => catalog.column(table, given))
209
+ if (lower === undefined) {
210
+ building.uniques[schema(name, `the ${key}`)] = columns
211
+ } else {
212
+ const claimed = schema(name, `the case-insensitive ${key}`)
213
+ ;(building.lowerUniques ??= {})[claimed] = columns
214
+ }
208
215
  }
209
216
  for (const { fields, unique, contains } of table.model.indexes) {
210
217
  if (unique) continue
@@ -263,128 +270,3 @@ export function snapshotOf(catalog: Catalog): Snapshot {
263
270
  }
264
271
  return { postgres: 1, tables }
265
272
  }
266
-
267
- const isRecord = (value: unknown): value is Record<string, unknown> =>
268
- typeof value === 'object' && value !== null && !Array.isArray(value)
269
-
270
- const isStrings = (value: unknown): value is string[] =>
271
- Array.isArray(value) && value.every((item) => typeof item === 'string')
272
-
273
- /** Each entry of a record, checked by `read`, or `undefined`. */
274
- function entries<T>(
275
- value: unknown,
276
- read: (item: unknown) => T | undefined,
277
- ): Record<string, T> | undefined {
278
- if (!isRecord(value)) return undefined
279
- const result: Record<string, T> = {}
280
- for (const [key, item] of Object.entries(value)) {
281
- const checked = read(item)
282
- if (checked === undefined) return undefined
283
- result[key] = checked
284
- }
285
- return result
286
- }
287
-
288
- const columnOf = (value: unknown): Column | undefined =>
289
- isRecord(value) &&
290
- typeof value['field'] === 'string' &&
291
- typeof value['type'] === 'string' &&
292
- typeof value['nullable'] === 'boolean'
293
- ? {
294
- field: value['field'],
295
- type: value['type'],
296
- nullable: value['nullable'],
297
- }
298
- : undefined
299
-
300
- const checkOf = (value: unknown): Check | undefined =>
301
- isRecord(value) &&
302
- typeof value['column'] === 'string' &&
303
- isStrings(value['values'])
304
- ? {
305
- column: value['column'],
306
- values: value['values'],
307
- ...(value['list'] === true ? { list: true as const } : {}),
308
- }
309
- : undefined
310
-
311
- const foreignKeyOf = (value: unknown): ForeignKey | undefined =>
312
- isRecord(value) &&
313
- typeof value['column'] === 'string' &&
314
- typeof value['table'] === 'string' &&
315
- typeof value['cascade'] === 'boolean'
316
- ? {
317
- column: value['column'],
318
- table: value['table'],
319
- cascade: value['cascade'],
320
- }
321
- : undefined
322
-
323
- const stringsOf = (value: unknown): string[] | undefined =>
324
- isStrings(value) ? value : undefined
325
-
326
- /** A search, `null` for none, or `undefined` when it is neither. */
327
- const searchOf = (value: unknown): Search | null | undefined =>
328
- value === null
329
- ? null
330
- : isRecord(value) &&
331
- typeof value['index'] === 'string' &&
332
- typeof value['language'] === 'string' &&
333
- isStrings(value['columns'])
334
- ? {
335
- index: value['index'],
336
- language: value['language'],
337
- columns: value['columns'],
338
- }
339
- : undefined
340
-
341
- function tableOf(value: unknown): TableSnapshot | undefined {
342
- if (!isRecord(value)) return undefined
343
- const key = value['primaryKey']
344
- const columns = entries(value['columns'], columnOf)
345
- const uniques = entries(value['uniques'], stringsOf)
346
- const checks = entries(value['checks'], checkOf)
347
- const indexes = entries(value['indexes'], stringsOf)
348
- const trigrams = entries(value['trigrams'], (item) =>
349
- typeof item === 'string' ? item : undefined,
350
- )
351
- const foreignKeys = entries(value['foreignKeys'], foreignKeyOf)
352
- const search = searchOf(value['search'])
353
- const model = value['model']
354
- if (
355
- typeof model !== 'string' ||
356
- !isRecord(key) ||
357
- typeof key['name'] !== 'string' ||
358
- !isStrings(key['columns']) ||
359
- columns === undefined ||
360
- uniques === undefined ||
361
- checks === undefined ||
362
- indexes === undefined ||
363
- trigrams === undefined ||
364
- foreignKeys === undefined ||
365
- search === undefined
366
- ) {
367
- return undefined
368
- }
369
- const primaryKey = { name: key['name'], columns: key['columns'] }
370
- return {
371
- model,
372
- columns,
373
- primaryKey,
374
- uniques,
375
- checks,
376
- indexes,
377
- trigrams,
378
- foreignKeys,
379
- search,
380
- }
381
- }
382
-
383
- /** A snapshot read back from a database or a file, checked. */
384
- export function parseSnapshot(value: unknown): Snapshot {
385
- const tables = isRecord(value) ? entries(value['tables'], tableOf) : undefined
386
- if (!isRecord(value) || value['postgres'] !== 1 || tables === undefined) {
387
- throw new Error('The schema snapshot is not one postgres() wrote.')
388
- }
389
- return { postgres: 1, tables }
390
- }