@shivaedev/sql 0.46.3 → 0.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +61 -27
  3. package/dist/backfillPostgres.d.ts +1 -1
  4. package/dist/backfillPostgres.d.ts.map +1 -1
  5. package/dist/backfillPostgres.js +1 -1
  6. package/dist/backfillPostgres.js.map +1 -1
  7. package/dist/batchedDelete.d.ts +1 -1
  8. package/dist/compositeRepository.d.ts +3 -1
  9. package/dist/compositeRepository.d.ts.map +1 -1
  10. package/dist/compositeRepository.js +2 -2
  11. package/dist/compositeRepository.js.map +1 -1
  12. package/dist/databaseAddress.d.ts.map +1 -1
  13. package/dist/databaseAddress.js +34 -18
  14. package/dist/databaseAddress.js.map +1 -1
  15. package/dist/deleteGroupsBatched.d.ts +1 -1
  16. package/dist/deleteGroupsBatched.d.ts.map +1 -1
  17. package/dist/deleteGroupsBatched.js +1 -1
  18. package/dist/deleteGroupsBatched.js.map +1 -1
  19. package/dist/makeAggregate.d.ts +2 -0
  20. package/dist/makeAggregate.d.ts.map +1 -1
  21. package/dist/makeAggregate.js +1 -0
  22. package/dist/makeAggregate.js.map +1 -1
  23. package/dist/migrations.js +1 -1
  24. package/dist/migrations.js.map +1 -1
  25. package/dist/orderedChildStatements.d.ts +1 -1
  26. package/dist/postgresChanges.js +1 -1
  27. package/dist/postgresChanges.js.map +1 -1
  28. package/dist/readSql.d.ts +19 -0
  29. package/dist/readSql.d.ts.map +1 -0
  30. package/dist/readSql.js +37 -0
  31. package/dist/readSql.js.map +1 -0
  32. package/dist/recordRepositoryRead.d.ts +12 -0
  33. package/dist/recordRepositoryRead.d.ts.map +1 -0
  34. package/dist/recordRepositoryRead.js +102 -0
  35. package/dist/recordRepositoryRead.js.map +1 -0
  36. package/dist/repository.d.ts.map +1 -1
  37. package/dist/repository.js +6 -5
  38. package/dist/repository.js.map +1 -1
  39. package/dist/repositoryChanges.d.ts +2 -2
  40. package/dist/repositoryChanges.d.ts.map +1 -1
  41. package/dist/repositoryChanges.js +30 -14
  42. package/dist/repositoryChanges.js.map +1 -1
  43. package/dist/repositoryCollection.d.ts +2 -0
  44. package/dist/repositoryCollection.d.ts.map +1 -1
  45. package/dist/repositoryCollection.js +14 -1
  46. package/dist/repositoryCollection.js.map +1 -1
  47. package/dist/repositoryQueries.d.ts +2 -0
  48. package/dist/repositoryQueries.d.ts.map +1 -1
  49. package/dist/repositoryQueries.js +3 -0
  50. package/dist/repositoryQueries.js.map +1 -1
  51. package/dist/repositoryWrites.d.ts +1 -1
  52. package/dist/transact.d.ts +5 -2
  53. package/dist/transact.d.ts.map +1 -1
  54. package/dist/transact.js +7 -3
  55. package/dist/transact.js.map +1 -1
  56. package/package.json +16 -16
  57. package/src/backfillPostgres.ts +1 -1
  58. package/src/compositeRepository.ts +9 -2
  59. package/src/databaseAddress.ts +36 -18
  60. package/src/deleteGroupsBatched.ts +0 -1
  61. package/src/makeAggregate.ts +4 -2
  62. package/src/migrations.ts +1 -1
  63. package/src/postgresChanges.ts +1 -1
  64. package/src/readSql.ts +51 -0
  65. package/src/recordRepositoryRead.ts +126 -0
  66. package/src/repository.ts +6 -5
  67. package/src/repositoryChanges.ts +42 -15
  68. package/src/repositoryCollection.ts +18 -1
  69. package/src/repositoryQueries.ts +5 -0
  70. package/src/transact.ts +14 -15
@@ -1 +1 @@
1
- {"version":3,"file":"transact.d.ts","sourceRoot":"","sources":["../src/transact.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEzC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,KAAK,UAAU,MAAM,8BAA8B,CAAC;AAC3D,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,EAAc,KAAK,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAChE,OAAO,EAAE,KAAK,YAAY,IAAI,UAAU,EAAe,MAAM,+BAA+B,CAAC;AAC7F,OAAO,EAAoB,KAAK,QAAQ,EAAc,MAAM,+BAA+B,CAAC;AAC5F,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,+BAA+B,CAAC;AAG7D,MAAM,MAAM,gBAAgB,GAAG,SAAS,OAAO,EAAE,GAAG,cAAc,CAAC,MAAM,EAAE,SAAS,OAAO,EAAE,CAAC,CAAC;AAE/F,MAAM,WAAW,mBAAmB;IACnC,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC;CAC1C;AAED,KAAK,QAAQ,GAAG,SAAS,CAAC,SAAS,GAAG,UAAU,CAAC,UAAU,CAAC;AAE5D,eAAO,MAAM,cAAc,sCAA4G,CAAC;AACxI,eAAO,MAAM,YAAY,4DAEvB,CAAC;AACH,eAAO,MAAM,aAAa,2DAExB,CAAC;AA8CH,eAAO,MAAM,kBAAkB,6GAI7B,CAAC;AAEH,MAAM,WAAW,eAAe,CAAC,MAAM;IACtC,QAAQ,CAAC,UAAU,EAAE,CAAC,KAAK,EAAE,QAAQ,KAAK,MAAM,CAAC;CACjD;AAED,eAAO,MAAM,QAAQ,EAAE;IACtB,CAAC,MAAM,EACN,OAAO,EAAE,eAAe,CAAC,MAAM,CAAC,GAC9B,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,KAAK,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,EAAE,QAAQ,CAAC,GAAG,MAAM,EAAE,CAAC,GAAG,SAAS,CAAC,SAAS,GAAG,UAAU,CAAC,UAAU,CAAC,CAAC;IACjJ,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAC9B,OAAO,EAAE,eAAe,CAAC,MAAM,CAAC,GAC9B,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,EAAE,QAAQ,CAAC,GAAG,MAAM,EAAE,CAAC,GAAG,SAAS,CAAC,SAAS,GAAG,UAAU,CAAC,UAAU,CAAC,CAAC;CAkBpG,CAAC"}
1
+ {"version":3,"file":"transact.d.ts","sourceRoot":"","sources":["../src/transact.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEzC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,KAAK,UAAU,MAAM,8BAA8B,CAAC;AAC3D,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,EAAc,KAAK,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAChE,OAAO,EAAE,KAAK,YAAY,IAAI,UAAU,EAAe,MAAM,+BAA+B,CAAC;AAC7F,OAAO,EAAoB,KAAK,QAAQ,EAAc,MAAM,+BAA+B,CAAC;AAC5F,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,+BAA+B,CAAC;AAG7D,MAAM,MAAM,gBAAgB,GAAG,SAAS,OAAO,EAAE,GAAG,cAAc,CAAC,MAAM,EAAE,SAAS,OAAO,EAAE,CAAC,CAAC;AAE/F,MAAM,WAAW,mBAAmB;IACnC,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC;CAC1C;AAED,KAAK,QAAQ,GAAG,SAAS,CAAC,SAAS,GAAG,UAAU,CAAC,UAAU,CAAC;AAE5D,eAAO,MAAM,cAAc,sCAA4G,CAAC;AACxI,eAAO,MAAM,YAAY,4DAEvB,CAAC;AACH,eAAO,MAAM,aAAa,2DAExB,CAAC;AA8CH,eAAO,MAAM,kBAAkB,6GAI7B,CAAC;AAEH,MAAM,WAAW,eAAe,CAAC,MAAM;IACtC,QAAQ,CAAC,UAAU,EAAE,CAAC,KAAK,EAAE,QAAQ,KAAK,MAAM,CAAC;CACjD;AAED,KAAK,UAAU,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,IAAI,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG,SAAS,CAAC,SAAS,GAAG,UAAU,CAAC,UAAU,CAAC,CAAC;AAEhG,eAAO,MAAM,QAAQ,EAAE;IACtB,IAAI,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,KAAK,UAAU,CAAC,CAAC,EAAE,CAAC,GAAG,QAAQ,EAAE,CAAC,CAAC,CAAC;IAChF,CAAC,MAAM,EAAE,OAAO,EAAE,eAAe,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,KAAK,UAAU,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,EAAE,QAAQ,CAAC,GAAG,MAAM,EAAE,CAAC,CAAC,CAAC;IACzI,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GAAG,UAAU,CAAC,CAAC,EAAE,CAAC,GAAG,QAAQ,EAAE,CAAC,CAAC,CAAC;IAC1E,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,eAAe,CAAC,MAAM,CAAC,GAAG,UAAU,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,EAAE,QAAQ,CAAC,GAAG,MAAM,EAAE,CAAC,CAAC,CAAC;CAkBrI,CAAC"}
package/dist/transact.js CHANGED
@@ -56,13 +56,17 @@ export const invalidateOnCommit = Effect.fn("Transact.invalidateOnCommit")(funct
56
56
  .record(flatten(keys).map((key) => ({ key, publication: options?.publication ?? "shared" })))
57
57
  .pipe(Effect.provideService(invalidations.Observer, observe));
58
58
  });
59
- export const transact = dual(2, Effect.fn("Transact.transact")(function* (effect, options) {
59
+ export const transact = dual((args) => Effect.isEffect(args[0]), Effect.fn("Transact.transact")(function* (effect, options) {
60
60
  const sql = yield* SqlClient.SqlClient;
61
61
  const native = yield* Effect.serviceOption(sql.transactionService);
62
- return yield* invalidations
62
+ const transacted = invalidations
63
63
  .within((body, boundary) => native._tag === "Some"
64
64
  ? sql.withTransaction(body)
65
65
  : boundary.restore(sql.withTransaction(Effect.tap(body, () => sql.afterCommit(boundary.committed)))))(effect)
66
- .pipe(Effect.provideService(invalidations.Observer, observe), Effect.provideService(invalidations.CommittedSink, committed), Effect.catchIf(isSqlError, (error) => Effect.fail(options.onSqlError(error))));
66
+ .pipe(Effect.provideService(invalidations.Observer, observe), Effect.provideService(invalidations.CommittedSink, committed));
67
+ if (options === undefined) {
68
+ return yield* transacted;
69
+ }
70
+ return yield* Effect.catchIf(transacted, isSqlError, (error) => Effect.fail(options.onSqlError(error)));
67
71
  }));
68
72
  //# sourceMappingURL=transact.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"transact.js","sourceRoot":"","sources":["../src/transact.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"transact.js","sourceRoot":"","sources":["../src/transact.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shivaedev/sql",
3
- "version": "0.46.3",
3
+ "version": "0.47.0",
4
4
  "description": "Schema-derived repositories over native Effect SQL",
5
5
  "keywords": [
6
6
  "effect"
@@ -43,27 +43,27 @@
43
43
  "README.md"
44
44
  ],
45
45
  "dependencies": {
46
- "@shivaedev/changes": "0.11.1",
47
- "@shivaedev/service": "0.8.2"
46
+ "@shivaedev/changes": "0.12.0",
47
+ "@shivaedev/service": "0.9.1"
48
48
  },
49
49
  "devDependencies": {
50
- "@effect/platform-node": "4.1.1",
51
- "@effect/sql-pg": "4.1.1",
52
- "@effect/sql-sqlite-node": "4.1.1",
53
- "@shivaedev/contract": "0.43.2",
54
- "@shivaedev/jobs": "0.41.4",
55
- "@shivaedev/quality": "0.73.1",
56
- "@shivaedev/test-kits": "0.10.5",
57
- "@shivaedev/test-sql": "0.24.7",
58
- "@shivaedev/test-story": "0.45.4",
50
+ "@effect/platform-node": "4.1.2",
51
+ "@effect/sql-pg": "4.1.2",
52
+ "@effect/sql-sqlite-node": "4.1.2",
53
+ "@shivaedev/contract": "0.44.0",
54
+ "@shivaedev/jobs": "0.42.2",
55
+ "@shivaedev/quality": "0.77.0",
56
+ "@shivaedev/test-kits": "0.12.0",
57
+ "@shivaedev/test-sql": "0.24.9",
58
+ "@shivaedev/test-story": "0.47.0",
59
59
  "@types/node": "24.19.2",
60
- "effect": "4.1.1",
60
+ "effect": "4.1.2",
61
61
  "typescript": "7.0.2",
62
- "vitest": "5.0.5"
62
+ "vitest": "5.1.0"
63
63
  },
64
64
  "peerDependencies": {
65
- "@effect/sql-pg": "4.1.1",
66
- "effect": "4.1.1"
65
+ "@effect/sql-pg": "4.1.2",
66
+ "effect": "4.1.2"
67
67
  },
68
68
  "peerDependenciesMeta": {
69
69
  "@effect/sql-pg": {
@@ -38,7 +38,7 @@ export const backfillPostgres = Effect.fn("Sql.backfillPostgres")(function* <TRo
38
38
  yield* options.saveProgress(last, progress);
39
39
  }
40
40
  return rows.length;
41
- }).pipe(transact({ onSqlError: (error) => error }));
41
+ }).pipe(transact());
42
42
  processed += count;
43
43
  if (count === 0) {
44
44
  return processed;
@@ -5,6 +5,7 @@ import { SqlClient, SqlSchema } from "effect/sql";
5
5
  import type { SqlError } from "effect/sql/SqlError";
6
6
  import { storageSchema } from "#adapters/storageSchema.ts";
7
7
  import type { BatchedDelete } from "#batchedDelete.ts";
8
+ import type { RecordRead } from "#recordRepositoryRead.ts";
8
9
  import type { RepositoryChanges } from "#repositoryChanges.ts";
9
10
  import type { RepositoryListFilter } from "#repositoryFilter.ts";
10
11
  import type { Update, UpdateVoid } from "#repositoryUpdate.ts";
@@ -47,6 +48,7 @@ export const makeCompositeCrud = Effect.fnUntraced(function* (
47
48
  model: Model.Any,
48
49
  options: {
49
50
  readonly columns: readonly string[];
51
+ readonly recordRead: RecordRead;
50
52
  readonly tableName: string;
51
53
  readonly spanPrefix: string;
52
54
  },
@@ -54,7 +56,7 @@ export const makeCompositeCrud = Effect.fnUntraced(function* (
54
56
  ) {
55
57
  const sql = yield* SqlClient.SqlClient;
56
58
  const nameTransforms = yield* sqlNameTransforms;
57
- const { columns, tableName, spanPrefix } = options;
59
+ const { columns, recordRead, tableName, spanPrefix } = options;
58
60
  const entries: [string, Schema.Constraint][] = [];
59
61
  for (const column of columns) {
60
62
  const field = model.fields[column];
@@ -84,6 +86,11 @@ export const makeCompositeCrud = Effect.fnUntraced(function* (
84
86
 
85
87
  return {
86
88
  ...(yield* repositoryWrites(model, { ...options, idColumns: columns }, changes)),
87
- findById: (id: unknown) => find(id).pipe(Effect.withSpan(`${spanPrefix}.findById`)),
89
+ findById: (id: unknown) =>
90
+ record(columns.length === 1 ? { [columns[0] ?? ""]: id } : id).pipe(
91
+ Effect.flatMap(recordRead),
92
+ Effect.andThen(find(id)),
93
+ Effect.withSpan(`${spanPrefix}.findById`),
94
+ ),
88
95
  };
89
96
  });
@@ -1,25 +1,43 @@
1
1
  import type { PgPoolConfig } from "@effect/sql-pg/PgClient";
2
2
  import { Redacted } from "effect";
3
3
 
4
+ function isDecodableHost(hostname: string): boolean {
5
+ const runs = hostname.match(/(?:%[\da-f]{2})+/giu) ?? [];
6
+ if (hostname.replace(/(?:%[\da-f]{2})+/giu, "").includes("%")) {
7
+ return false;
8
+ }
9
+ const encoder = new TextEncoder();
10
+ const decoder = new TextDecoder();
11
+ return runs.every((run) => {
12
+ const bytes = Uint8Array.from(run.match(/[\da-f]{2}/giu) ?? [], (byte) => Number.parseInt(byte, 16));
13
+ const decoded = encoder.encode(decoder.decode(bytes));
14
+ return decoded.length === bytes.length && decoded.every((byte, index) => byte === bytes[index]);
15
+ });
16
+ }
17
+
4
18
  export function databaseAddress(value: Redacted.Redacted<string>): Pick<PgPoolConfig, "host" | "port"> {
5
- try {
6
- const url = new URL(Redacted.value(value));
7
- if (url.protocol !== "postgres:" && url.protocol !== "postgresql:") {
8
- return {};
9
- }
10
- const hostname = url.hostname.startsWith("[") ? url.hostname.slice(1, -1) : decodeURIComponent(url.hostname);
11
- // PgConnection applies query parameters in order, after the authority.
12
- const host = url.searchParams.getAll("host").at(-1) ?? hostname;
13
- const rawPort = url.searchParams.getAll("port").at(-1) ?? url.port;
14
- const port = rawPort === "" ? undefined : Number(rawPort);
15
- if (host.includes(",") || (port !== undefined && (!Number.isInteger(port) || port < 1 || port > 65_535))) {
16
- return {};
17
- }
18
- return {
19
- ...(host === "" && !url.searchParams.has("host") ? {} : { host }),
20
- ...(port === undefined ? {} : { port }),
21
- };
22
- } catch {
19
+ const address = Redacted.value(value);
20
+ if (!URL.canParse(address)) {
21
+ return {};
22
+ }
23
+ const url = new URL(address);
24
+ if (url.protocol !== "postgres:" && url.protocol !== "postgresql:") {
25
+ return {};
26
+ }
27
+ const rawHostname = url.hostname.startsWith("[") ? url.hostname.slice(1, -1) : url.hostname;
28
+ if (!isDecodableHost(rawHostname)) {
29
+ return {};
30
+ }
31
+ const hostname = decodeURIComponent(rawHostname);
32
+ // PgConnection applies query parameters in order, after the authority.
33
+ const host = url.searchParams.getAll("host").at(-1) ?? hostname;
34
+ const rawPort = url.searchParams.getAll("port").at(-1) ?? url.port;
35
+ const port = rawPort === "" ? undefined : Number(rawPort);
36
+ if (host.includes(",") || (port !== undefined && (!Number.isInteger(port) || port < 1 || port > 65_535))) {
23
37
  return {};
24
38
  }
39
+ return {
40
+ ...(host === "" && !url.searchParams.has("host") ? {} : { host }),
41
+ ...(port === undefined ? {} : { port }),
42
+ };
25
43
  }
@@ -35,7 +35,6 @@ export const deleteGroupsBatched = Effect.fn("Sql.deleteGroupsBatched")(function
35
35
  }
36
36
  return { count, done: candidates.length === 0 };
37
37
  }),
38
- { onSqlError: (error) => error },
39
38
  ),
40
39
  ).pipe(Effect.scoped);
41
40
  });
@@ -6,15 +6,16 @@ import type { Fragment } from "effect/sql/Statement";
6
6
  import { aggregateColumn } from "#aggregateColumn.ts";
7
7
  import type { Aggregate } from "#aggregateQuery.ts";
8
8
  import { makeQueryClauses } from "#makeQueryClauses.ts";
9
+ import type { RecordRead } from "#recordRepositoryRead.ts";
9
10
  import { type FindManyFilter, makeFilter } from "#repositoryFilter.ts";
10
11
 
11
12
  export function makeAggregate<TModel extends Model.Any>(
12
13
  model: TModel,
13
- options: { readonly tableName: string; readonly spanPrefix: string },
14
+ options: { readonly recordRead: RecordRead; readonly tableName: string; readonly spanPrefix: string },
14
15
  ): Effect.Effect<Aggregate<TModel>, never, SqlClient.SqlClient>;
15
16
  export function makeAggregate(
16
17
  model: Model.Any,
17
- options: { readonly tableName: string; readonly spanPrefix: string },
18
+ options: { readonly recordRead: RecordRead; readonly tableName: string; readonly spanPrefix: string },
18
19
  ): Effect.Effect<unknown, never, SqlClient.SqlClient> {
19
20
  return Effect.fnUntraced(function* () {
20
21
  const sql = yield* SqlClient.SqlClient;
@@ -56,6 +57,7 @@ export function makeAggregate(
56
57
  const predicate = yield* where(query.where, true, true);
57
58
  const order = yield* clauses.orderClause(query.orderBy);
58
59
  const limit = yield* clauses.limitClause(query.limit);
60
+ yield* options.recordRead(query.where);
59
61
  const partitions = sql.csv(groups.map((key) => sql`${sql(key)}`));
60
62
  const rows =
61
63
  yield* sql`select ${sql.csv(columns)} from ${sql(options.tableName)} where ${predicate} group by ${partitions} ${order} ${limit}`;
package/src/migrations.ts CHANGED
@@ -78,7 +78,7 @@ export const migratePostgres = Effect.fn("EffectSql.migratePostgres")(function*
78
78
  }
79
79
  }
80
80
  return yield* migrate({ loader: Effect.succeed(current), table });
81
- }).pipe(transact({ onSqlError: (error) => error }), Effect.provideService(Reactivity.Reactivity, reactivity));
81
+ }).pipe(transact(), Effect.provideService(Reactivity.Reactivity, reactivity));
82
82
  const schemaComponents: readonly SchemaComponent<R, E | SqlError | Schema.SchemaError>[] = [
83
83
  ...components,
84
84
  ...(release === undefined ? [] : [postgresRelease(release, table)]),
@@ -59,7 +59,7 @@ export function postgresChanges(options: PostgresChangesOptions) {
59
59
  const publication: CommandPublisher = {
60
60
  publish: (keys) =>
61
61
  invalidateOnCommit(keys).pipe(
62
- transact({ onSqlError: (error) => error }),
62
+ transact(),
63
63
  Effect.provideService(SqlClient.SqlClient, options.client),
64
64
  Effect.catchCause((cause) => Effect.fail(new CommandPublicationError({ cause }))),
65
65
  ),
package/src/readSql.ts ADDED
@@ -0,0 +1,51 @@
1
+ import { Data, Effect, Schema } from "effect";
2
+ import { SqlClient } from "effect/sql";
3
+ import { type Composite, type CompositeBucketKey, type CompositeItemKey, isComposite } from "@shivaedev/changes/compositeKeys.ts";
4
+ import { type BucketKey, type ItemKey, Key, recordKey } from "@shivaedev/changes/keys.ts";
5
+ import { recordRead } from "@shivaedev/changes/readRecorder.ts";
6
+
7
+ export class MultiColumnKeyParameter extends Data.TaggedError("MultiColumnKeyParameter")<{
8
+ readonly key: CompositeBucketKey | CompositeItemKey;
9
+ }> {
10
+ override get message() {
11
+ const named =
12
+ this.key._tag === "Bucket"
13
+ ? `the tuple bucket ${this.key.collection}.${this.key.field} ${this.key.value}`
14
+ : `the composite row ${recordKey(this.key.collection, this.key.id)}`;
15
+ return `readSql cannot bind ${named} as one parameter, because it spans several columns. Bind each column's value and declare the key in the query's reads.`;
16
+ }
17
+ }
18
+
19
+ type Refused<TArgs extends readonly unknown[]> = {
20
+ readonly [TIndex in keyof TArgs]: TArgs[TIndex] extends { readonly [Composite]: true }
21
+ ? "A tuple bucket or composite row key spans several columns, so it cannot be one parameter"
22
+ : TArgs[TIndex];
23
+ };
24
+
25
+ const isKey = Schema.is(Key);
26
+
27
+ function isReadKey(value: unknown): value is BucketKey | ItemKey {
28
+ return isKey(value) && value._tag !== "List";
29
+ }
30
+
31
+ function bound(value: unknown) {
32
+ if (!isReadKey(value)) {
33
+ return value;
34
+ }
35
+ return value._tag === "Bucket" ? value.value : value.id;
36
+ }
37
+
38
+ const readMarked = Effect.fn("Sql.readMarked")(function* (strings: TemplateStringsArray, args: readonly unknown[]) {
39
+ const keys = args.filter(isReadKey);
40
+ const multiColumn = keys.find(isComposite);
41
+ if (multiColumn !== undefined) {
42
+ return yield* new MultiColumnKeyParameter({ key: multiColumn });
43
+ }
44
+ const sql = yield* SqlClient.SqlClient;
45
+ yield* recordRead(keys, { inferred: true });
46
+ return yield* sql(strings, ...args.map(bound));
47
+ });
48
+
49
+ export function readSql<const TArgs extends readonly unknown[]>(strings: TemplateStringsArray, ...args: TArgs & Refused<TArgs>) {
50
+ return readMarked(strings, args);
51
+ }
@@ -0,0 +1,126 @@
1
+ import { Effect, Schema } from "effect";
2
+ import type { Model } from "effect/schema";
3
+ import { declaredBuckets } from "@shivaedev/changes/buckets.ts";
4
+ import { collection, compositeId, type Identity, type Key } from "@shivaedev/changes/keys.ts";
5
+ import { recordRead } from "@shivaedev/changes/readRecorder.ts";
6
+ import type { RepositoryCollection } from "#repositoryCollection.ts";
7
+
8
+ type Where = Readonly<Record<string, unknown>>;
9
+
10
+ export type RecordRead = (where?: Where) => Effect.Effect<void, Schema.SchemaError, unknown>;
11
+
12
+ const AnyIdentity = Schema.Union([Schema.String, Schema.Number]);
13
+
14
+ // Each key is its own registration, renewed on every rerun, so a read pinned to more keys listens on a broader bucket instead.
15
+ const MAX_BUCKET_KEYS = 1000;
16
+
17
+ function isIdentity(value: unknown): value is Identity {
18
+ return typeof value === "string" || typeof value === "number";
19
+ }
20
+
21
+ function isPrimitive(value: unknown): value is Identity | bigint | boolean {
22
+ return isIdentity(value) || typeof value === "bigint" || typeof value === "boolean";
23
+ }
24
+
25
+ function combinations<T>(columns: readonly (readonly T[])[]): readonly (readonly T[])[] {
26
+ return columns.reduce<readonly (readonly T[])[]>((rows, values) => rows.flatMap((row) => values.map((value) => [...row, value])), [[]]);
27
+ }
28
+
29
+ type Pin = readonly [column: string, value: Identity];
30
+
31
+ interface Bucket {
32
+ readonly columns: readonly string[];
33
+ readonly make: (pins: readonly Pin[]) => readonly Key[];
34
+ }
35
+
36
+ export function makeRecordRead(
37
+ model: Model.Any,
38
+ options: { readonly tableName: string; readonly idColumns: readonly string[]; readonly tuples?: RepositoryCollection["tuples"] },
39
+ ): RecordRead {
40
+ const fields = declaredBuckets(model.fields).map(({ field }) => field);
41
+ const keys = collection(options.tableName, AnyIdentity, Object.fromEntries(fields.map((field) => [field, AnyIdentity])));
42
+ const tupleKeys = collection(options.tableName, AnyIdentity, {}, options.tuples);
43
+ const buckets: readonly Bucket[] = [
44
+ ...Object.entries(tupleKeys.tuples).flatMap(([tuple, columns]) => {
45
+ const make = tupleKeys.bucket[tuple];
46
+ return make === undefined ? [] : [{ columns, make: (pins: readonly Pin[]) => [make(Object.fromEntries(pins))] }];
47
+ }),
48
+ ...fields.flatMap((field) => {
49
+ const make = keys.bucket[field];
50
+ return make === undefined ? [] : [{ columns: [field], make: (pins: readonly Pin[]) => pins.map(([, value]) => make(value)) }];
51
+ }),
52
+ ];
53
+ function pinned(where: Where, column: string): readonly unknown[] | undefined {
54
+ const field = model.fields[column];
55
+ const value = where[column];
56
+ if (field === undefined || value === undefined || value === null) {
57
+ return undefined;
58
+ }
59
+ const acceptsValue: (input: unknown) => boolean = Schema.is(field);
60
+ if (acceptsValue(value)) {
61
+ return [value];
62
+ }
63
+ return typeof value === "object" && "in" in value && Array.isArray(value.in) && value.in.every((entry) => entry !== null && entry !== undefined)
64
+ ? value.in
65
+ : undefined;
66
+ }
67
+ // Writes key a row by the values the driver returns, which the encoded value matches unless the codec wraps it in a driver parameter.
68
+ const storedPart = Effect.fnUntraced(function* (column: string, value: unknown) {
69
+ const field = model.fields[column];
70
+ const encoded = field === undefined ? undefined : yield* Schema.encodeUnknownEffect(field)(value);
71
+ return [encoded, value].find(isPrimitive);
72
+ });
73
+ const rowId = Effect.fnUntraced(function* (row: readonly unknown[]) {
74
+ const [single] = row;
75
+ if (row.length === 1 && isIdentity(single)) {
76
+ return single;
77
+ }
78
+ const parts = yield* Effect.forEach(row, (value, index) => storedPart(options.idColumns[index] ?? "", value));
79
+ if (!parts.every(isPrimitive)) {
80
+ return undefined;
81
+ }
82
+ return parts.length === 1 ? String(parts[0]) : compositeId(parts);
83
+ });
84
+ const rowKeys = Effect.fnUntraced(function* (where: Where) {
85
+ const columns: (readonly unknown[])[] = [];
86
+ for (const column of options.idColumns) {
87
+ const values = pinned(where, column);
88
+ if (values === undefined) {
89
+ return undefined;
90
+ }
91
+ columns.push(values);
92
+ }
93
+ const ids = yield* Effect.forEach(combinations(columns), rowId);
94
+ return ids.every((id) => id !== undefined) ? ids.map((id): Key => keys.item(id)) : undefined;
95
+ });
96
+ function pinnedKeys(where: Where, { columns, make }: Bucket) {
97
+ const pins: (readonly Pin[])[] = [];
98
+ let count = 1;
99
+ for (const column of columns) {
100
+ const values = pinned(where, column);
101
+ if (values === undefined || !values.every(isIdentity)) {
102
+ return undefined;
103
+ }
104
+ count *= values.length;
105
+ pins.push(values.map((value): Pin => [column, value]));
106
+ }
107
+ return count > MAX_BUCKET_KEYS ? undefined : { columns, keys: combinations(pins).flatMap(make) };
108
+ }
109
+ function bucketKeys(where: Where): readonly Key[] | undefined {
110
+ const pinnedBuckets = buckets.flatMap((bucket) => pinnedKeys(where, bucket) ?? []);
111
+ let narrowest: readonly Key[] | undefined;
112
+ for (const { columns, keys: candidate } of pinnedBuckets) {
113
+ const covered = pinnedBuckets.some(
114
+ (other) => other.columns.length > columns.length && columns.every((column) => other.columns.includes(column)),
115
+ );
116
+ if (!covered && (narrowest === undefined || candidate.length < narrowest.length)) {
117
+ narrowest = candidate;
118
+ }
119
+ }
120
+ return narrowest;
121
+ }
122
+ return (where = {}) =>
123
+ Effect.gen(function* () {
124
+ yield* recordRead((yield* rowKeys(where)) ?? bucketKeys(where) ?? [keys.list], { inferred: true });
125
+ });
126
+ }
package/src/repository.ts CHANGED
@@ -6,6 +6,7 @@ import { type BatchWrites, makeInsertMany } from "#batchWrites.ts";
6
6
  import { makeCompositeCrud, type PrimaryKey, type Repository } from "#compositeRepository.ts";
7
7
  import { type ConflictColumns, type ConflictWrites, makeConflictWrites } from "#conflictWrites.ts";
8
8
  import { makeInsertFromSelect, type SelectWrites } from "#insertFromSelect.ts";
9
+ import { makeRecordRead } from "#recordRepositoryRead.ts";
9
10
  import { repositoryChanges } from "#repositoryChanges.ts";
10
11
  import { type BucketRegistrationError, type RepositoryOptions, validateRepositoryCollection } from "#repositoryCollection.ts";
11
12
  import type { FindManyFilter, RepositoryFilter, RepositoryListFilter } from "#repositoryFilter.ts";
@@ -118,14 +119,14 @@ export function makeRepository<TModel extends Model.Any, TId extends IdColumn<TM
118
119
  ): Effect.Effect<unknown, BucketRegistrationError, SqlClient.SqlClient> {
119
120
  return Effect.fnUntraced(function* () {
120
121
  yield* validateRepositoryCollection(model.fields, options);
121
- const composite = typeof options.idColumn === "object";
122
- const idColumns = (composite ? options.idColumn : [options.idColumn]).map(String);
123
- const changes = yield* repositoryChanges(options.tableName, idColumns, model.fields);
122
+ const idColumns = (typeof options.idColumn === "object" ? options.idColumn : [options.idColumn]).map(String);
123
+ const changes = yield* repositoryChanges(options.tableName, idColumns, model.fields, options.collection?.tuples);
124
+ const recordRead = makeRecordRead(model, { idColumns, tableName: options.tableName, tuples: options.collection?.tuples });
124
125
  const crud = {
125
- ...(yield* makeCompositeCrud(model, { ...options, columns: idColumns }, changes)),
126
+ ...(yield* makeCompositeCrud(model, { ...options, columns: idColumns, recordRead }, changes)),
126
127
  deleteBatched: yield* makeBatchedDelete(model, options, changes),
127
128
  };
128
- const queries = yield* makeQueries(model, options);
129
+ const queries = yield* makeQueries(model, { ...options, recordRead });
129
130
  return {
130
131
  ...crud,
131
132
  ...queries,
@@ -1,34 +1,61 @@
1
1
  import { Effect, Option, type Schema } from "effect";
2
2
  import * as Reactivity from "effect/reactivity/Reactivity";
3
3
  import { SqlClient } from "effect/sql";
4
- import { declaredBuckets } from "@shivaedev/changes/buckets.ts";
5
- import { collectionInvalidationKeys, compositeId, invalidationKeys, type Key, recordKey } from "@shivaedev/changes/keys.ts";
4
+ import { declaredBuckets, decodeBucketValue } from "@shivaedev/changes/buckets.ts";
5
+ import { collectionInvalidationKeys, compositeId, type Identity, invalidationKeys, type Key, recordKey } from "@shivaedev/changes/keys.ts";
6
6
  import { ChangeDeclarations } from "#ChangeDeclarations.ts";
7
7
  import { invalidateOnCommit, transact } from "#transact.ts";
8
8
 
9
9
  export type RepositoryChanges = Effect.Success<ReturnType<typeof repositoryChanges>>;
10
10
 
11
- export const repositoryChanges = Effect.fnUntraced(function* (table: string, columns: readonly string[], fields: Schema.Struct.Fields = {}) {
11
+ export const repositoryChanges = Effect.fnUntraced(function* (
12
+ table: string,
13
+ columns: readonly string[],
14
+ fields: Schema.Struct.Fields = {},
15
+ tuples: Readonly<Record<string, readonly string[]>> = {},
16
+ ) {
12
17
  const sql = yield* SqlClient.SqlClient;
13
- const buckets = declaredBuckets(fields);
18
+ const declared = [
19
+ ...declaredBuckets(fields).map(({ field }) => ({ columns: [field], field, tuple: false })),
20
+ ...Object.entries(tuples).map(([field, tupleColumns]) => ({ columns: tupleColumns, field, tuple: true })),
21
+ ];
22
+ const columnParts = new Map(
23
+ [...new Set(declared.flatMap((bucket) => bucket.columns))].flatMap((column, index) => {
24
+ const schema = fields[column];
25
+ return schema === undefined ? [] : [[column, { column, decode: decodeBucketValue(schema), old: `shivaeOldBucket${index}` }] as const];
26
+ }),
27
+ );
28
+ const buckets = declared.map((bucket) => ({ ...bucket, parts: bucket.columns.flatMap((column) => columnParts.get(column) ?? []) }));
29
+ const bucketValue = Effect.fnUntraced(function* (
30
+ row: Readonly<Record<string, unknown>>,
31
+ { parts, tuple }: (typeof buckets)[number],
32
+ side: "column" | "old",
33
+ ) {
34
+ const values: Identity[] = [];
35
+ for (const part of parts) {
36
+ const stored = row[part[side]];
37
+ const decoded = stored === null || stored === undefined ? undefined : yield* part.decode(stored);
38
+ if (decoded === null || decoded === undefined) {
39
+ return undefined;
40
+ }
41
+ values.push(decoded);
42
+ }
43
+ return tuple ? compositeId(values.map(String)) : values[0];
44
+ });
14
45
  const bucketKeys = Effect.fnUntraced(function* (row: Readonly<Record<string, unknown>>) {
15
46
  const keys: Key[] = [];
16
- for (const [index, { field, decode }] of buckets.entries()) {
17
- for (const value of [row[field], row[`shivaeOldBucket${index}`]]) {
18
- if (value === null || value === undefined) {
19
- continue;
20
- }
21
- const decoded = yield* decode(value);
22
- if (decoded === null || decoded === undefined) {
23
- continue;
47
+ for (const bucket of buckets) {
48
+ for (const side of ["column", "old"] as const) {
49
+ const value = yield* bucketValue(row, bucket, side);
50
+ if (value !== undefined) {
51
+ keys.push({ _tag: "Bucket", collection: table, field: bucket.field, value });
24
52
  }
25
- keys.push({ _tag: "Bucket", collection: table, field, value: decoded });
26
53
  }
27
54
  }
28
55
  return invalidationKeys(keys);
29
56
  });
30
57
  function returning(base = sql`*`) {
31
- return sql.csv([base, ...buckets.map(({ field }, index) => sql`old.${sql(field)} as ${sql(`shivaeOldBucket${index}`)}`)]);
58
+ return sql.csv([base, ...[...columnParts.values()].map(({ column, old }) => sql`old.${sql(column)} as ${sql(old)}`)]);
32
59
  }
33
60
  const initial = yield* Effect.serviceOption(Reactivity.Reactivity);
34
61
  const fallback = Option.isSome(initial) ? initial.value : yield* Reactivity.make;
@@ -60,7 +87,7 @@ export const repositoryChanges = Effect.fnUntraced(function* (table: string, col
60
87
  yield* provide(invalidateOnCommit([...keys]));
61
88
  });
62
89
  function write<A, E, R>(effect: Effect.Effect<A, E, R>) {
63
- return provide(transact(effect, { onSqlError: (error) => error }));
90
+ return provide(transact(effect));
64
91
  }
65
92
  const recordTable = Effect.fnUntraced(function* (rows: number) {
66
93
  if (rows === 0) {
@@ -1,9 +1,11 @@
1
1
  import { Data, Effect, Order, type Schema } from "effect";
2
2
  import { declaredBuckets } from "@shivaedev/changes/buckets.ts";
3
+ import { isBucketSchema } from "@shivaedev/changes/keys.ts";
3
4
 
4
5
  export interface RepositoryCollection {
5
6
  readonly bucket: Readonly<Record<string, unknown>>;
6
7
  readonly name: string;
8
+ readonly tuples?: Readonly<Record<string, readonly [string, string, ...string[]]>>;
7
9
  }
8
10
 
9
11
  export interface RepositoryOptions<TId> {
@@ -19,11 +21,15 @@ export class BucketRegistrationError extends Data.TaggedError("BucketRegistratio
19
21
  readonly modelFields: readonly string[];
20
22
  readonly collectionFields: readonly string[];
21
23
  readonly invalidFields: readonly string[];
24
+ readonly invalidTupleColumns: readonly string[];
22
25
  }> {
23
26
  override get message() {
24
27
  if (this.invalidFields.length > 0) {
25
28
  return `Bucket registration for ${this.tableName}: fields ${JSON.stringify(this.invalidFields)} must decode to strings, numbers or absent values`;
26
29
  }
30
+ if (this.invalidTupleColumns.length > 0) {
31
+ return `Bucket registration for ${this.tableName}: tuple columns ${JSON.stringify(this.invalidTupleColumns)} must be model fields that decode to strings, numbers or absent values`;
32
+ }
27
33
  return `Bucket registration for ${this.tableName}: model fields ${JSON.stringify(this.modelFields)} disagree with collection ${this.collectionName ?? "(missing)"} fields ${JSON.stringify(this.collectionFields)}`;
28
34
  }
29
35
  }
@@ -37,10 +43,20 @@ export const validateRepositoryCollection = Effect.fnUntraced(function* (
37
43
  .filter(({ supported }) => !supported)
38
44
  .map(({ field }) => field)
39
45
  .sort(Order.String);
46
+ const tuples = options.collection?.tuples ?? {};
47
+ const invalidTupleColumns = Object.entries(tuples).flatMap(([tuple, columns]) =>
48
+ columns.flatMap((column) => {
49
+ const schema = fields[column];
50
+ return schema !== undefined && isBucketSchema(schema) ? [] : [`${tuple}.${column}`];
51
+ }),
52
+ );
40
53
  const modelFields = buckets.map(({ field }) => field).sort(Order.String);
41
- const collectionFields = Object.keys(options.collection?.bucket ?? {}).sort(Order.String);
54
+ const collectionFields = Object.keys(options.collection?.bucket ?? {})
55
+ .filter((field) => !Object.hasOwn(tuples, field))
56
+ .sort(Order.String);
42
57
  if (
43
58
  invalidFields.length === 0
59
+ && invalidTupleColumns.length === 0
44
60
  && ((options.collection === undefined && modelFields.length === 0)
45
61
  || (options.collection?.name === options.tableName
46
62
  && modelFields.length === collectionFields.length
@@ -53,6 +69,7 @@ export const validateRepositoryCollection = Effect.fnUntraced(function* (
53
69
  collectionFields,
54
70
  collectionName: options.collection?.name,
55
71
  invalidFields,
72
+ invalidTupleColumns,
56
73
  modelFields,
57
74
  tableName: options.tableName,
58
75
  }),