@ultimabase/postgres 0.2.0 → 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 +14 -0
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +2 -1
- package/dist/filter.d.ts +6 -2
- package/dist/filter.d.ts.map +1 -1
- package/dist/filter.js +15 -6
- package/dist/migrations.js +1 -1
- package/dist/names.d.ts +6 -1
- package/dist/names.d.ts.map +1 -1
- package/dist/names.js +13 -11
- package/dist/plan-indexes.d.ts.map +1 -1
- package/dist/plan-indexes.js +25 -3
- package/dist/plan-parts.d.ts +2 -0
- package/dist/plan-parts.d.ts.map +1 -1
- package/dist/plan-parts.js +2 -0
- package/dist/plan.d.ts.map +1 -1
- package/dist/plan.js +1 -2
- package/dist/snapshot-parse.d.ts +4 -0
- package/dist/snapshot-parse.d.ts.map +1 -0
- package/dist/snapshot-parse.js +109 -0
- package/dist/snapshot.d.ts +4 -2
- package/dist/snapshot.d.ts.map +1 -1
- package/dist/snapshot.js +10 -107
- package/package.json +4 -4
- package/src/adapter.ts +2 -1
- package/src/filter.ts +18 -6
- package/src/migrations.ts +1 -1
- package/src/names.ts +19 -13
- package/src/plan-indexes.ts +38 -3
- package/src/plan-parts.ts +6 -0
- package/src/plan.ts +1 -4
- package/src/snapshot-parse.ts +141 -0
- package/src/snapshot.ts +12 -130
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
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
|
+
|
|
3
17
|
## 0.2.0
|
|
4
18
|
|
|
5
19
|
### Patch Changes
|
package/dist/adapter.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
|
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
|
-
/**
|
|
42
|
-
|
|
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. */
|
package/dist/filter.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
-
|
|
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
|
-
/**
|
|
148
|
-
|
|
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
|
|
172
|
-
:
|
|
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
|
}
|
package/dist/migrations.js
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
/**
|
package/dist/names.d.ts.map
CHANGED
|
@@ -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;
|
|
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 = [
|
|
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
|
|
53
|
-
if (
|
|
54
|
-
sets.push(
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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;
|
|
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"}
|
package/dist/plan-indexes.js
CHANGED
|
@@ -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),
|
|
3
|
-
//
|
|
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;
|
package/dist/plan-parts.d.ts
CHANGED
|
@@ -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
|
package/dist/plan-parts.d.ts.map
CHANGED
|
@@ -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"}
|
package/dist/plan-parts.js
CHANGED
|
@@ -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)})`;
|
package/dist/plan.d.ts.map
CHANGED
|
@@ -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;
|
|
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 @@
|
|
|
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
|
+
}
|
package/dist/snapshot.d.ts
CHANGED
|
@@ -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
|
package/dist/snapshot.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
|
110
|
-
|
|
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.
|
|
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": "
|
|
39
|
+
"ultimabase": "0.3.0-canary.20261031104207"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@types/pg": "8.23.1",
|
|
43
|
-
"@ultimabase/testing": "0.
|
|
44
|
-
"ultimabase": "0.
|
|
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
|
|
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
|
-
|
|
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
|
-
/**
|
|
200
|
-
|
|
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
|
|
223
|
-
:
|
|
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
|
-
/**
|
|
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:
|
|
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)
|
|
102
|
+
if (stored && field.unique)
|
|
103
|
+
sets.push({ fields: [field.name], lower: false })
|
|
96
104
|
}
|
|
97
|
-
for (const
|
|
98
|
-
if (
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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] })
|
package/src/plan-indexes.ts
CHANGED
|
@@ -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),
|
|
3
|
-
//
|
|
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
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
}
|