@shivaedev/sql 0.36.1 → 0.38.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 (125) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +91 -1
  3. package/dist/Model.d.ts +9 -0
  4. package/dist/Model.d.ts.map +1 -0
  5. package/dist/Model.js +6 -0
  6. package/dist/Model.js.map +1 -0
  7. package/dist/VersionedRepositoryShape.d.ts +6 -0
  8. package/dist/VersionedRepositoryShape.d.ts.map +1 -0
  9. package/dist/VersionedRepositoryShape.js +2 -0
  10. package/dist/VersionedRepositoryShape.js.map +1 -0
  11. package/dist/WorkerRelease.d.ts +1 -8
  12. package/dist/WorkerRelease.d.ts.map +1 -1
  13. package/dist/WorkerRelease.js +4 -29
  14. package/dist/WorkerRelease.js.map +1 -1
  15. package/dist/adapters/groupColumns.d.ts +14 -0
  16. package/dist/adapters/groupColumns.d.ts.map +1 -0
  17. package/dist/adapters/groupColumns.js +54 -0
  18. package/dist/adapters/groupColumns.js.map +1 -0
  19. package/dist/adapters/storageSchema.d.ts +4 -0
  20. package/dist/adapters/storageSchema.d.ts.map +1 -0
  21. package/dist/adapters/storageSchema.js +7 -0
  22. package/dist/adapters/storageSchema.js.map +1 -0
  23. package/dist/backfillPostgres.d.ts +18 -0
  24. package/dist/backfillPostgres.d.ts.map +1 -0
  25. package/dist/backfillPostgres.js +35 -0
  26. package/dist/backfillPostgres.js.map +1 -0
  27. package/dist/batchWrites.d.ts.map +1 -1
  28. package/dist/batchWrites.js +6 -3
  29. package/dist/batchWrites.js.map +1 -1
  30. package/dist/compositeRepository.d.ts.map +1 -1
  31. package/dist/compositeRepository.js +4 -1
  32. package/dist/compositeRepository.js.map +1 -1
  33. package/dist/conditionalUpsert.d.ts.map +1 -1
  34. package/dist/conditionalUpsert.js +5 -2
  35. package/dist/conditionalUpsert.js.map +1 -1
  36. package/dist/conflictWrites.d.ts.map +1 -1
  37. package/dist/conflictWrites.js +6 -3
  38. package/dist/conflictWrites.js.map +1 -1
  39. package/dist/coordinateWorkerRelease.js +1 -1
  40. package/dist/coordinateWorkerRelease.js.map +1 -1
  41. package/dist/initializeWorkerRelease.d.ts +7 -0
  42. package/dist/initializeWorkerRelease.d.ts.map +1 -0
  43. package/dist/initializeWorkerRelease.js +7 -0
  44. package/dist/initializeWorkerRelease.js.map +1 -0
  45. package/dist/insertFromSelect.d.ts.map +1 -1
  46. package/dist/insertFromSelect.js +4 -1
  47. package/dist/insertFromSelect.js.map +1 -1
  48. package/dist/latestPerGroup.d.ts.map +1 -1
  49. package/dist/latestPerGroup.js +7 -3
  50. package/dist/latestPerGroup.js.map +1 -1
  51. package/dist/makeOrderedChildren.d.ts +26 -0
  52. package/dist/makeOrderedChildren.d.ts.map +1 -0
  53. package/dist/makeOrderedChildren.js +38 -0
  54. package/dist/makeOrderedChildren.js.map +1 -0
  55. package/dist/makeWorkerRelease.d.ts +8 -0
  56. package/dist/makeWorkerRelease.d.ts.map +1 -0
  57. package/dist/makeWorkerRelease.js +12 -0
  58. package/dist/makeWorkerRelease.js.map +1 -0
  59. package/dist/orderedChildStatements.d.ts +30 -0
  60. package/dist/orderedChildStatements.d.ts.map +1 -0
  61. package/dist/orderedChildStatements.js +61 -0
  62. package/dist/orderedChildStatements.js.map +1 -0
  63. package/dist/registerWorkerRelease.d.ts +4 -0
  64. package/dist/registerWorkerRelease.d.ts.map +1 -0
  65. package/dist/registerWorkerRelease.js +14 -0
  66. package/dist/registerWorkerRelease.js.map +1 -0
  67. package/dist/repository.d.ts +8 -8
  68. package/dist/repository.d.ts.map +1 -1
  69. package/dist/repository.js.map +1 -1
  70. package/dist/repositoryQueries.d.ts.map +1 -1
  71. package/dist/repositoryQueries.js +6 -2
  72. package/dist/repositoryQueries.js.map +1 -1
  73. package/dist/repositoryUpdate.d.ts +3 -3
  74. package/dist/repositoryUpdate.d.ts.map +1 -1
  75. package/dist/repositoryWrites.d.ts.map +1 -1
  76. package/dist/repositoryWrites.js +21 -6
  77. package/dist/repositoryWrites.js.map +1 -1
  78. package/dist/setWorkerReleaseActive.d.ts +4 -0
  79. package/dist/setWorkerReleaseActive.d.ts.map +1 -0
  80. package/dist/setWorkerReleaseActive.js +13 -0
  81. package/dist/setWorkerReleaseActive.js.map +1 -0
  82. package/dist/sqlNameTransforms.d.ts +5 -0
  83. package/dist/sqlNameTransforms.d.ts.map +1 -0
  84. package/dist/sqlNameTransforms.js +8 -0
  85. package/dist/sqlNameTransforms.js.map +1 -0
  86. package/dist/upsertResult.d.ts +2 -1
  87. package/dist/upsertResult.d.ts.map +1 -1
  88. package/dist/upsertResult.js +3 -2
  89. package/dist/upsertResult.js.map +1 -1
  90. package/dist/versionedRepository.d.ts +15 -6
  91. package/dist/versionedRepository.d.ts.map +1 -1
  92. package/dist/versionedRepository.js +8 -5
  93. package/dist/versionedRepository.js.map +1 -1
  94. package/dist/versionedStatements.d.ts +1 -1
  95. package/dist/versionedStatements.d.ts.map +1 -1
  96. package/dist/versionedStatements.js +10 -4
  97. package/dist/versionedStatements.js.map +1 -1
  98. package/package.json +11 -11
  99. package/src/Model.ts +7 -0
  100. package/src/VersionedRepositoryShape.ts +11 -0
  101. package/src/WorkerRelease.ts +4 -33
  102. package/src/adapters/groupColumns.ts +63 -0
  103. package/src/adapters/storageSchema.ts +10 -0
  104. package/src/backfillPostgres.ts +47 -0
  105. package/src/batchWrites.ts +6 -3
  106. package/src/compositeRepository.ts +4 -1
  107. package/src/conditionalUpsert.ts +5 -2
  108. package/src/conflictWrites.ts +6 -3
  109. package/src/coordinateWorkerRelease.ts +1 -1
  110. package/src/initializeWorkerRelease.ts +7 -0
  111. package/src/insertFromSelect.ts +4 -1
  112. package/src/latestPerGroup.ts +7 -3
  113. package/src/makeOrderedChildren.ts +78 -0
  114. package/src/makeWorkerRelease.ts +12 -0
  115. package/src/orderedChildStatements.ts +76 -0
  116. package/src/registerWorkerRelease.ts +15 -0
  117. package/src/repository.ts +12 -13
  118. package/src/repositoryQueries.ts +6 -2
  119. package/src/repositoryUpdate.ts +3 -3
  120. package/src/repositoryWrites.ts +21 -6
  121. package/src/setWorkerReleaseActive.ts +17 -0
  122. package/src/sqlNameTransforms.ts +9 -0
  123. package/src/upsertResult.ts +4 -2
  124. package/src/versionedRepository.ts +31 -11
  125. package/src/versionedStatements.ts +14 -4
@@ -1,40 +1,11 @@
1
- import { Effect, Semaphore } from "effect";
2
1
  import { defineService } from "@shivaedev/service/defineService.ts";
3
2
  import { genericMethod } from "@shivaedev/service/genericMethod.ts";
4
-
5
- export const makeWorkerRelease = Effect.fn("EffectSql.makeWorkerRelease")(function* () {
6
- const mutex = Semaphore.makeUnsafe(1);
7
- const controls = new Set<(active: boolean) => Effect.Effect<void, unknown>>();
8
- let active = false;
9
- const locked = mutex.withPermits(1);
10
- function register<E>(control: (active: boolean) => Effect.Effect<void, E>) {
11
- const attach = Effect.gen(function* () {
12
- controls.add(control);
13
- yield* control(active);
14
- });
15
- const detach = Effect.sync(() => {
16
- controls.delete(control);
17
- });
18
- return Effect.acquireRelease(locked(attach), () => locked(detach));
19
- }
20
- return {
21
- service: { register },
22
- setActive: (next: boolean) =>
23
- locked(
24
- Effect.gen(function* () {
25
- if (next === active) {
26
- return;
27
- }
28
- active = next;
29
- yield* Effect.forEach(controls, (control) => control(active), { concurrency: "unbounded", discard: true });
30
- }),
31
- ).pipe(Effect.uninterruptible),
32
- };
33
- });
3
+ import { initializeWorkerRelease } from "./initializeWorkerRelease.ts";
4
+ import { registerWorkerRelease } from "./registerWorkerRelease.ts";
34
5
 
35
6
  export const WorkerRelease = defineService({
36
7
  id: "@shivaedev/sql/WorkerRelease",
37
- initialize: makeWorkerRelease(),
38
- methods: ({ service }) => ({ register: genericMethod(service.register) }),
8
+ initialize: initializeWorkerRelease,
9
+ methods: (state) => ({ register: genericMethod(registerWorkerRelease(state)) }),
39
10
  requires: [],
40
11
  });
@@ -0,0 +1,63 @@
1
+ import type { Schema } from "effect";
2
+ import { Predicate, SchemaAST } from "effect";
3
+ import type { SqlNameTransforms } from "#clientOptions.ts";
4
+
5
+ interface Column {
6
+ readonly column: string;
7
+ readonly key: string;
8
+ readonly member: PropertyKey | undefined;
9
+ }
10
+
11
+ export function groupColumns(fields: Schema.Struct.Fields, keys: readonly string[] = Object.keys(fields)): Column[] {
12
+ return keys.flatMap<Column>((key) => {
13
+ const ast = fields[key]?.ast;
14
+ const prefix = ast?.annotations?.["@shivaedev/sql/groupPrefix"];
15
+ return typeof prefix === "string" && ast !== undefined && SchemaAST.isObjects(ast)
16
+ ? ast.propertySignatures.map((field) => ({ column: `${prefix}${String(field.name)}`, key, member: field.name }))
17
+ : [{ column: key, key, member: undefined }];
18
+ });
19
+ }
20
+
21
+ export function rowMapping(fields: Schema.Struct.Fields, names?: SqlNameTransforms) {
22
+ const columns = groupColumns(fields);
23
+ function returnedName(column: string) {
24
+ return names?.transformResultNames?.(names.transformQueryNames?.(column) ?? column) ?? column;
25
+ }
26
+ const seen = new Set<string>();
27
+ for (const { column } of columns) {
28
+ if (seen.has(column)) {
29
+ throw new Error(`Duplicate grouped column: ${column}`);
30
+ }
31
+ seen.add(column);
32
+ }
33
+ return {
34
+ expand: (row: Readonly<Record<string, unknown>>) => {
35
+ const expanded: Record<string, unknown> = { ...row };
36
+ for (const { key, member, column } of columns) {
37
+ if (member !== undefined && Object.hasOwn(row, returnedName(column))) {
38
+ const held = expanded[key];
39
+ expanded[key] = { ...(typeof held === "object" && held !== null ? held : {}), [member]: row[returnedName(column)] };
40
+ delete expanded[returnedName(column)];
41
+ }
42
+ }
43
+ return expanded;
44
+ },
45
+ flatten: (row: unknown) => {
46
+ if (!Predicate.isObject(row)) {
47
+ return {};
48
+ }
49
+ const flat: Record<string, unknown> = { ...row };
50
+ for (const { key, member, column } of columns) {
51
+ if (member === undefined) {
52
+ continue;
53
+ }
54
+ delete flat[key];
55
+ const value = row[key];
56
+ if (Predicate.isObject(value) && Object.hasOwn(value, member)) {
57
+ flat[column] = Reflect.get(value, member);
58
+ }
59
+ }
60
+ return flat;
61
+ },
62
+ };
63
+ }
@@ -0,0 +1,10 @@
1
+ import { Schema, SchemaGetter } from "effect";
2
+ import { rowMapping } from "#adapters/groupColumns.ts";
3
+ import type { SqlNameTransforms } from "#clientOptions.ts";
4
+
5
+ export function storageSchema<TSchema extends Schema.Constraint>(schema: TSchema, fields: Schema.Struct.Fields, names?: SqlNameTransforms) {
6
+ const mapping = rowMapping(fields, names);
7
+ return Schema.Record(Schema.String, Schema.Unknown).pipe(
8
+ Schema.decodeTo(schema, { decode: SchemaGetter.transform(mapping.expand), encode: SchemaGetter.transform(mapping.flatten) }),
9
+ );
10
+ }
@@ -0,0 +1,47 @@
1
+ import { Data, Effect, Option, Schema } from "effect";
2
+ import { SqlClient } from "effect/sql";
3
+ import { advisoryXactLock } from "#advisoryXactLock.ts";
4
+ import { transact } from "#transact.ts";
5
+
6
+ export class BackfillInsideTransaction extends Data.TaggedError("BackfillInsideTransaction") {}
7
+
8
+ export interface BackfillOptions<TRow extends object, TProgress, E, R> {
9
+ readonly batchSize: number;
10
+ readonly loadProgress: Effect.Effect<TProgress, E, R>;
11
+ readonly lock: bigint | string;
12
+ readonly process: (row: TRow) => Effect.Effect<unknown, E, R>;
13
+ readonly saveProgress: (last: TRow, previous: TProgress) => Effect.Effect<unknown, E, R>;
14
+ readonly select: (progress: TProgress, limit: number) => Effect.Effect<readonly TRow[], E, R>;
15
+ }
16
+
17
+ const BatchSize = Schema.Number.check(Schema.isInt(), Schema.isGreaterThan(0), Schema.isLessThanOrEqualTo(Number.MAX_SAFE_INTEGER));
18
+
19
+ export const backfillPostgres = Effect.fn("Sql.backfillPostgres")(function* <TRow extends object, TProgress, E, R>(
20
+ options: BackfillOptions<TRow, TProgress, E, R>,
21
+ ) {
22
+ const sql = yield* SqlClient.SqlClient;
23
+ if (Option.isSome(yield* Effect.serviceOption(sql.transactionService))) {
24
+ return yield* new BackfillInsideTransaction();
25
+ }
26
+ const batchSize = yield* Schema.decodeUnknownEffect(BatchSize)(options.batchSize);
27
+ let processed = 0;
28
+ while (true) {
29
+ const count = yield* Effect.gen(function* () {
30
+ yield* advisoryXactLock(options.lock).pipe(Effect.catchTag("AdvisoryLockOutsideTransaction", Effect.die));
31
+ const progress = yield* options.loadProgress;
32
+ const rows = (yield* options.select(progress, batchSize)).slice(0, batchSize);
33
+ for (const row of rows) {
34
+ yield* options.process(row);
35
+ }
36
+ const last = rows.at(-1);
37
+ if (last !== undefined) {
38
+ yield* options.saveProgress(last, progress);
39
+ }
40
+ return rows.length;
41
+ }).pipe(transact({ onSqlError: (error) => error }));
42
+ processed += count;
43
+ if (count === 0) {
44
+ return processed;
45
+ }
46
+ }
47
+ });
@@ -3,7 +3,9 @@ import type { Model } from "effect/schema";
3
3
  import { SqlClient } from "effect/sql";
4
4
  import type { SqlError } from "effect/sql/SqlError";
5
5
  import type { Fragment } from "effect/sql/Statement";
6
+ import { storageSchema } from "#adapters/storageSchema.ts";
6
7
  import type { RepositoryChanges } from "#repositoryChanges.ts";
8
+ import { sqlNameTransforms } from "#sqlNameTransforms.ts";
7
9
  import { makeUpsertDecoder, upsertReturning } from "#upsertResult.ts";
8
10
 
9
11
  export type InsertMany<TModel extends Model.Any, TServices = never> = (
@@ -22,11 +24,12 @@ export const makeInsertMany = Effect.fnUntraced(function* <TServices = never>(
22
24
  upsert = false,
23
25
  ) {
24
26
  const sql = yield* SqlClient.SqlClient;
27
+ const nameTransforms = yield* sqlNameTransforms;
25
28
  const prepareRow = prepare ?? (() => Effect.succeed(sql``));
26
- const encode = Schema.encodeEffect(model.insert);
29
+ const encode = Schema.encodeEffect(storageSchema(model.insert, model.fields, nameTransforms));
27
30
  const record = Schema.decodeUnknownEffect(Schema.Record(Schema.String, Schema.Unknown));
28
- const decode = Schema.decodeUnknownEffect(Schema.Array(model));
29
- const decodeUpsert = makeUpsertDecoder(model);
31
+ const decode = Schema.decodeUnknownEffect(Schema.Array(storageSchema(model, model.fields, nameTransforms)));
32
+ const decodeUpsert = makeUpsertDecoder(model, nameTransforms);
30
33
  const returning = options.changes.returning(upsert ? yield* upsertReturning() : sql`*`);
31
34
  return (inputs: readonly unknown[]) =>
32
35
  Effect.gen(function* () {
@@ -3,11 +3,13 @@ import { Effect, Schema } from "effect";
3
3
  import type { Model } from "effect/schema";
4
4
  import { SqlClient, SqlSchema } from "effect/sql";
5
5
  import type { SqlError } from "effect/sql/SqlError";
6
+ import { storageSchema } from "#adapters/storageSchema.ts";
6
7
  import type { BatchedDelete } from "#batchedDelete.ts";
7
8
  import type { RepositoryChanges } from "#repositoryChanges.ts";
8
9
  import type { RepositoryListFilter } from "#repositoryFilter.ts";
9
10
  import type { Update, UpdateVoid } from "#repositoryUpdate.ts";
10
11
  import { repositoryWrites } from "#repositoryWrites.ts";
12
+ import { sqlNameTransforms } from "#sqlNameTransforms.ts";
11
13
  import type { IdColumn, NativeRepository } from "#versionedRepository.ts";
12
14
 
13
15
  export type CompositeKey<TModel extends Model.Any> = readonly [IdColumn<TModel>, IdColumn<TModel>, ...IdColumn<TModel>[]];
@@ -51,6 +53,7 @@ export const makeCompositeCrud = Effect.fnUntraced(function* (
51
53
  changes: RepositoryChanges,
52
54
  ) {
53
55
  const sql = yield* SqlClient.SqlClient;
56
+ const nameTransforms = yield* sqlNameTransforms;
54
57
  const { columns, tableName, spanPrefix } = options;
55
58
  const entries: [string, Schema.Constraint][] = [];
56
59
  for (const column of columns) {
@@ -76,7 +79,7 @@ export const makeCompositeCrud = Effect.fnUntraced(function* (
76
79
  (key) => sql`select * from ${sql(tableName)} where ${predicate(key)}`,
77
80
  ),
78
81
  Request: Key,
79
- Result: model,
82
+ Result: storageSchema(model, model.fields, nameTransforms),
80
83
  });
81
84
 
82
85
  return {
@@ -2,9 +2,11 @@ import { Effect, Schema } from "effect";
2
2
  import type { Model } from "effect/schema";
3
3
  import { SqlClient } from "effect/sql";
4
4
  import type { SqlError } from "effect/sql/SqlError";
5
+ import { storageSchema } from "#adapters/storageSchema.ts";
5
6
  import { makeConflictClause } from "#makeConflictClause.ts";
6
7
  import type { RepositoryChanges } from "#repositoryChanges.ts";
7
8
  import type { Comparisons, FindManyFilter } from "#repositoryFilter.ts";
9
+ import { sqlNameTransforms } from "#sqlNameTransforms.ts";
8
10
  import type { UpsertAssignment } from "#upsertColumns.ts";
9
11
  import { makeUpsertDecoder, type UpsertRow, upsertReturning } from "#upsertResult.ts";
10
12
 
@@ -65,10 +67,11 @@ export const makeConditionalUpsert = Effect.fnUntraced(function* (
65
67
  },
66
68
  ) {
67
69
  const sql = yield* SqlClient.SqlClient;
70
+ const nameTransforms = yield* sqlNameTransforms;
68
71
  const returning = yield* upsertReturning();
69
- const encode = Schema.encodeEffect(model.insert);
72
+ const encode = Schema.encodeEffect(storageSchema(model.insert, model.fields, nameTransforms));
70
73
  const record = Schema.decodeUnknownEffect(Schema.Record(Schema.String, Schema.Unknown));
71
- const decode = makeUpsertDecoder(model);
74
+ const decode = makeUpsertDecoder(model, nameTransforms);
72
75
  const clause = yield* makeConflictClause(model, options);
73
76
  return (
74
77
  inputs: readonly unknown[],
@@ -2,11 +2,13 @@ import { Effect, Option, Schema } from "effect";
2
2
  import type { Model } from "effect/schema";
3
3
  import { SqlClient } from "effect/sql";
4
4
  import type { SqlError } from "effect/sql/SqlError";
5
+ import { storageSchema } from "#adapters/storageSchema.ts";
5
6
  import { type InsertMany, makeInsertMany } from "#batchWrites.ts";
6
7
  import { type ConditionalUpsert, type ConditionalUpsertMany, makeConditionalUpsert } from "#conditionalUpsert.ts";
7
8
  import { type InsertFromSelect, makeInsertFromSelect } from "#insertFromSelect.ts";
8
9
  import { makeConflictClause } from "#makeConflictClause.ts";
9
10
  import type { RepositoryChanges } from "#repositoryChanges.ts";
11
+ import { sqlNameTransforms } from "#sqlNameTransforms.ts";
10
12
  import type { UpsertAssignment, UpsertColumn } from "#upsertColumns.ts";
11
13
  import { makeUpsertDecoder, upsertReturning } from "#upsertResult.ts";
12
14
 
@@ -52,11 +54,12 @@ export const makeConflictWrites = Effect.fnUntraced(function* (
52
54
  },
53
55
  ) {
54
56
  const sql = yield* SqlClient.SqlClient;
57
+ const nameTransforms = yield* sqlNameTransforms;
55
58
  const { tableName, spanPrefix } = options;
56
59
  const returning = yield* upsertReturning();
57
- const encode = Schema.encodeEffect(model.insert);
58
- const decode = Schema.decodeUnknownEffect(model);
59
- const decodeUpsert = makeUpsertDecoder(model);
60
+ const encode = Schema.encodeEffect(storageSchema(model.insert, model.fields, nameTransforms));
61
+ const decode = Schema.decodeUnknownEffect(storageSchema(model, model.fields, nameTransforms));
62
+ const decodeUpsert = makeUpsertDecoder(model, nameTransforms);
60
63
  const record = Schema.decodeUnknownEffect(Schema.Record(Schema.String, Schema.Unknown));
61
64
  const conditional = yield* makeConditionalUpsert(model, options);
62
65
  const clause = yield* makeConflictClause(model, options);
@@ -1,9 +1,9 @@
1
1
  import { Duration, Effect, Exit, Option, Predicate, Schedule, Scope } from "effect";
2
2
  import { isSqlError } from "effect/sql/SqlError";
3
3
  import { WorkerReleaseTesting } from "#testing/WorkerRelease.ts";
4
+ import { makeWorkerRelease } from "./makeWorkerRelease.ts";
4
5
  import { readPostgresRelease } from "./postgresRelease.ts";
5
6
  import { releaseWorkers } from "./releaseWorkers.ts";
6
- import { makeWorkerRelease } from "./WorkerRelease.ts";
7
7
 
8
8
  function isCloseable(scope: Scope.Scope): scope is Scope.Closeable {
9
9
  return Predicate.hasProperty(scope, "~effect/Scope/Closeable") && scope["~effect/Scope/Closeable"] === "~effect/Scope/Closeable";
@@ -0,0 +1,7 @@
1
+ import { Effect, Semaphore } from "effect";
2
+
3
+ export const initializeWorkerRelease = Effect.sync(() => ({
4
+ active: false,
5
+ controls: new Set<(active: boolean) => Effect.Effect<void, unknown>>(),
6
+ locked: Semaphore.makeUnsafe(1).withPermits(1),
7
+ }));
@@ -3,8 +3,10 @@ import type { Model } from "effect/schema";
3
3
  import { SqlClient } from "effect/sql";
4
4
  import type { SqlError } from "effect/sql/SqlError";
5
5
  import type { Fragment } from "effect/sql/Statement";
6
+ import { storageSchema } from "#adapters/storageSchema.ts";
6
7
  import type { RepositoryChanges } from "#repositoryChanges.ts";
7
8
  import type { RepositorySelect } from "#repositoryQueries.ts";
9
+ import { sqlNameTransforms } from "#sqlNameTransforms.ts";
8
10
 
9
11
  type CompatibleColumn<TType, TEncoded, TFields extends Schema.Struct.Fields> = {
10
12
  [TKey in keyof TFields]: TFields[TKey]["Type"] extends TType ? (TFields[TKey]["Encoded"] extends TEncoded ? TKey : never) : never;
@@ -31,7 +33,8 @@ export const makeInsertFromSelect = Effect.fnUntraced(function* <TServices = nev
31
33
  prepare?: (columns: readonly string[]) => Effect.Effect<Fragment, SqlError | Schema.SchemaError, TServices>,
32
34
  ) {
33
35
  const sql = yield* SqlClient.SqlClient;
34
- const decode = Schema.decodeUnknownEffect(Schema.Array(model));
36
+ const nameTransforms = yield* sqlNameTransforms;
37
+ const decode = Schema.decodeUnknownEffect(Schema.Array(storageSchema(model, model.fields, nameTransforms)));
35
38
  return (source: RepositorySelect<Schema.Struct.Fields, unknown>, columns: Readonly<Record<string, string>>) =>
36
39
  Effect.gen(function* () {
37
40
  const names = Object.keys(columns);
@@ -2,9 +2,12 @@ import { Effect, Schema } from "effect";
2
2
  import type { Model } from "effect/schema";
3
3
  import { SqlClient } from "effect/sql";
4
4
  import type { Fragment } from "effect/sql/Statement";
5
+ import { groupColumns } from "#adapters/groupColumns.ts";
6
+ import { storageSchema } from "#adapters/storageSchema.ts";
5
7
  import { pinnedGroups } from "#pinnedGroups.ts";
6
8
  import type { LatestPerGroup } from "#repository.ts";
7
9
  import { makeFilter } from "#repositoryFilter.ts";
10
+ import { sqlNameTransforms } from "#sqlNameTransforms.ts";
8
11
  import { tupleUnnest } from "#tupleUnnest.ts";
9
12
 
10
13
  function groupMasks(input: readonly (readonly unknown[])[]) {
@@ -20,6 +23,7 @@ function groupMasks(input: readonly (readonly unknown[])[]) {
20
23
 
21
24
  export const latestPerGroup = Effect.fnUntraced(function* (model: Model.Any, tableName: string) {
22
25
  const sql = yield* SqlClient.SqlClient;
26
+ const nameTransforms = yield* sqlNameTransforms;
23
27
  const { fieldAt, where } = yield* makeFilter(model);
24
28
  function groupSource(groups: readonly (readonly unknown[])[], partitionBy: readonly string[], fromTuple: boolean) {
25
29
  const partitions = sql.csv(partitionBy.map((key) => sql`${sql(key)}`));
@@ -37,7 +41,7 @@ export const latestPerGroup = Effect.fnUntraced(function* (model: Model.Any, tab
37
41
  const predicate = yield* where(query.where, true);
38
42
  const partitions = sql.csv(query.partitionBy.map((key) => sql`${sql(key)}`));
39
43
  const keys = Object.keys(model.fields);
40
- const columns = sql.csv(keys.map((key) => sql`${sql(key)}`));
44
+ const columns = sql.csv(groupColumns(model.fields, keys).map(({ column }) => sql`${sql(column)}`));
41
45
  const pinned = yield* pinnedGroups(model, query.partitionBy, query.where).pipe(Effect.provideService(SqlClient.SqlClient, sql));
42
46
  if (pinned !== undefined && pinned.groups.length === 0) {
43
47
  return [];
@@ -70,7 +74,7 @@ export const latestPerGroup = Effect.fnUntraced(function* (model: Model.Any, tab
70
74
  const ordering = sql.csv(query.orderBy.map(({ field, direction }) => sql`${sql(field)} ${sql.literal(direction === "desc" ? "desc" : "asc")}`));
71
75
  const rows =
72
76
  yield* sql`${tupleSource} select ${columns} from (${sql.join(" union all ")(branches)}) as ${sql("__effect_sql_pinned")} order by ${partitions}, ${ordering}`;
73
- return yield* Schema.decodeUnknownEffect(Schema.Array(Schema.Struct(model.fields)))(rows);
77
+ return yield* Schema.decodeUnknownEffect(Schema.Array(storageSchema(Schema.Struct(model.fields), model.fields, nameTransforms)))(rows);
74
78
  }
75
79
  let rankColumn = "__effect_sql_rank";
76
80
  while (Object.hasOwn(model.fields, rankColumn)) {
@@ -81,6 +85,6 @@ export const latestPerGroup = Effect.fnUntraced(function* (model: Model.Any, tab
81
85
  from ${sql(tableName)} where ${predicate}
82
86
  ) as ${sql("__effect_sql_ranked")} where ${sql(rankColumn)} <= ${rowLimit}
83
87
  order by ${partitions}, ${sql(rankColumn)}`;
84
- return yield* Schema.decodeUnknownEffect(Schema.Array(Schema.Struct(model.fields)))(rows);
88
+ return yield* Schema.decodeUnknownEffect(Schema.Array(storageSchema(Schema.Struct(model.fields), model.fields, nameTransforms)))(rows);
85
89
  });
86
90
  });
@@ -0,0 +1,78 @@
1
+ import { type Cause, Effect, Schema } from "effect";
2
+ import type { Model } from "effect/schema";
3
+ import { SqlClient, SqlSchema } from "effect/sql";
4
+ import type { SqlError } from "effect/sql/SqlError";
5
+ import { storageSchema } from "#adapters/storageSchema.ts";
6
+ import { type ChildOrderError, orderedChildStatements } from "#orderedChildStatements.ts";
7
+ import { repositoryChanges } from "#repositoryChanges.ts";
8
+ import { sqlNameTransforms } from "#sqlNameTransforms.ts";
9
+ import type { IdColumn, VersionColumn } from "#versionedRepository.ts";
10
+
11
+ type Column<TModel extends Model.Any> = Extract<keyof TModel["fields"] & keyof TModel["insert"]["Type"], string>;
12
+ type Position<TModel extends Model.Any> = {
13
+ [TKey in Column<TModel>]: TModel["fields"][TKey] extends Schema.ConstraintCodec<number, number, never, never> ? TKey : never;
14
+ }[Column<TModel>];
15
+
16
+ export const makeOrderedChildren = Effect.fn("Sql.makeOrderedChildren")(function* <
17
+ TModel extends Model.Any,
18
+ const TId extends Extract<IdColumn<TModel>, string>,
19
+ const TParent extends Column<TModel>,
20
+ const TPosition extends Position<TModel>,
21
+ >(
22
+ model: TModel,
23
+ options: {
24
+ readonly idColumn: TId;
25
+ readonly parentColumn: TParent;
26
+ readonly parentTable: string;
27
+ readonly parentIdColumn: string;
28
+ readonly positionColumn: TPosition;
29
+ readonly spanPrefix: string;
30
+ readonly tableName: string;
31
+ readonly versionColumn?: VersionColumn<TModel, TId>;
32
+ },
33
+ ) {
34
+ const sql = yield* SqlClient.SqlClient;
35
+ const nameTransforms = yield* sqlNameTransforms;
36
+ const changes = yield* repositoryChanges(options.tableName, [options.idColumn], model.fields);
37
+ const statements = yield* orderedChildStatements(model, options, changes);
38
+ const encode = Schema.encodeEffect(storageSchema(model.insert, model.fields, nameTransforms));
39
+ const decode = Schema.decodeUnknownEffect(storageSchema(model, model.fields, nameTransforms));
40
+ const { parentColumn, positionColumn, versionColumn, tableName, spanPrefix } = options;
41
+ function append(
42
+ parent: TModel["fields"][TParent]["Type"],
43
+ input: Omit<TModel["insert"]["Type"], TParent | TPosition>,
44
+ ): Effect.Effect<
45
+ TModel["Type"],
46
+ SqlError | Schema.SchemaError | Cause.NoSuchElementError,
47
+ TModel["insert"]["EncodingServices"] | TModel["DecodingServices"]
48
+ > {
49
+ return Effect.gen(function* () {
50
+ const encodedParent = yield* statements.lock(parent);
51
+ const last = yield* SqlSchema.findOne({
52
+ execute: (value) =>
53
+ sql`select coalesce(max(${sql(positionColumn)}) + 1, 0)::integer as next from ${sql(tableName)} where ${sql(parentColumn)} = ${value}`,
54
+ Request: Schema.Unknown,
55
+ Result: Schema.Struct({ next: Schema.Int }),
56
+ })(encodedParent);
57
+ const encoded = yield* encode({ ...input, [parentColumn]: parent, [positionColumn]: last.next });
58
+ const row = versionColumn === undefined ? encoded : { ...encoded, [versionColumn]: 0 };
59
+ const rows = yield* sql`insert into ${sql(tableName)} ${sql.insert(row)} returning ${changes.returning()}`;
60
+ yield* changes.record(rows);
61
+ return yield* decode(rows[0]);
62
+ }).pipe(changes.write, Effect.withSpan(`${spanPrefix}.append`));
63
+ }
64
+ type OrderResult = Effect.Effect<
65
+ void,
66
+ SqlError | Schema.SchemaError | Cause.NoSuchElementError | ChildOrderError,
67
+ TModel["fields"][TParent | TId]["EncodingServices"] | TModel["fields"][TId]["DecodingServices"]
68
+ >;
69
+ function move(parent: TModel["fields"][TParent]["Type"], id: TModel["fields"][TId]["Type"], position: number): OrderResult;
70
+ function move(parent: unknown, id: unknown, position: number) {
71
+ return statements.move(parent, id, position).pipe(changes.write, Effect.withSpan(`${spanPrefix}.move`));
72
+ }
73
+ function reorder(parent: TModel["fields"][TParent]["Type"], ids: readonly TModel["fields"][TId]["Type"][]): OrderResult;
74
+ function reorder(parent: unknown, ids: readonly unknown[]) {
75
+ return statements.reorder(parent, ids).pipe(changes.write, Effect.withSpan(`${spanPrefix}.reorder`));
76
+ }
77
+ return { append, move, reorder };
78
+ });
@@ -0,0 +1,12 @@
1
+ import { Effect } from "effect";
2
+ import { initializeWorkerRelease } from "./initializeWorkerRelease.ts";
3
+ import { registerWorkerRelease } from "./registerWorkerRelease.ts";
4
+ import { setWorkerReleaseActive } from "./setWorkerReleaseActive.ts";
5
+
6
+ export const makeWorkerRelease = Effect.fn("EffectSql.makeWorkerRelease")(function* () {
7
+ const state = yield* initializeWorkerRelease;
8
+ return {
9
+ service: { register: registerWorkerRelease(state) },
10
+ setActive: setWorkerReleaseActive(state),
11
+ };
12
+ });
@@ -0,0 +1,76 @@
1
+ import { Cause, Data, Effect, Equal, Schema } from "effect";
2
+ import type { Model } from "effect/schema";
3
+ import { SqlClient, SqlSchema } from "effect/sql";
4
+ import type { RepositoryChanges } from "#repositoryChanges.ts";
5
+
6
+ export class ChildOrderError extends Data.TaggedError("ChildOrderError")<{
7
+ readonly reason: "membership" | "position";
8
+ }> {}
9
+
10
+ export interface ChildOrderOptions {
11
+ readonly idColumn: string;
12
+ readonly parentColumn: string;
13
+ readonly parentIdColumn: string;
14
+ readonly parentTable: string;
15
+ readonly positionColumn: string;
16
+ readonly tableName: string;
17
+ }
18
+
19
+ export const orderedChildStatements = Effect.fnUntraced(function* (model: Model.Any, options: ChildOrderOptions, changes: RepositoryChanges) {
20
+ const sql = yield* SqlClient.SqlClient;
21
+ const { idColumn, parentColumn, parentTable, parentIdColumn, positionColumn, tableName } = options;
22
+ const parent = model.fields[parentColumn];
23
+ const idField = model.fields[idColumn];
24
+ if (parent === undefined || idField === undefined) {
25
+ return yield* Effect.die(new Error("Unknown ordered child key column"));
26
+ }
27
+ const encodeParent = Schema.encodeEffect(parent);
28
+ const encodeId = Schema.encodeEffect(idField);
29
+ const lock = Effect.fnUntraced(function* (value: unknown) {
30
+ const encoded = yield* encodeParent(value);
31
+ const rows = yield* sql`select ${sql(parentIdColumn)} from ${sql(parentTable)} where ${sql(parentIdColumn)} = ${encoded} for update`;
32
+ if (rows.length === 0) {
33
+ return yield* new Cause.NoSuchElementError();
34
+ }
35
+ return encoded;
36
+ });
37
+ const children = SqlSchema.findAll({
38
+ execute: (encodedParent) =>
39
+ sql`select ${sql(idColumn)} as ${sql("childId")} from ${sql(tableName)} where ${sql(parentColumn)} = ${encodedParent} order by ${sql(positionColumn)}, ${sql(idColumn)}`,
40
+ Request: Schema.Unknown,
41
+ Result: Schema.Struct({ childId: idField }),
42
+ });
43
+
44
+ const write = Effect.fnUntraced(function* (encodedParent: unknown, ids: readonly unknown[]) {
45
+ if (ids.length === 0) {
46
+ return;
47
+ }
48
+ const encoded = yield* Effect.forEach(ids, (value) => encodeId(value));
49
+ const positions = sql.join(" ", false)(encoded.map((value, index) => sql`when ${value} then ${index}`));
50
+ const rows = yield* sql`update ${sql(tableName)} set ${sql(positionColumn)} = case ${sql(idColumn)} ${positions} end
51
+ where ${sql(parentColumn)} = ${encodedParent} returning ${changes.returning()}`;
52
+ yield* changes.record(rows);
53
+ });
54
+ const reorder = Effect.fnUntraced(function* (value: unknown, ids: readonly unknown[]) {
55
+ const encodedParent = yield* lock(value);
56
+ const current = yield* children(encodedParent);
57
+ if (ids.length !== current.length || current.some(({ childId }) => ids.filter((key) => Equal.equals(childId, key)).length !== 1)) {
58
+ return yield* new ChildOrderError({ reason: "membership" });
59
+ }
60
+ yield* write(encodedParent, ids);
61
+ });
62
+ const move = Effect.fnUntraced(function* (value: unknown, child: unknown, position: number) {
63
+ const encodedParent = yield* lock(value);
64
+ const current = yield* children(encodedParent);
65
+ if (!current.some(({ childId }) => Equal.equals(childId, child))) {
66
+ return yield* new ChildOrderError({ reason: "membership" });
67
+ }
68
+ if (!Number.isInteger(position) || position < 0 || position >= current.length) {
69
+ return yield* new ChildOrderError({ reason: "position" });
70
+ }
71
+ const ids = current.map(({ childId }) => childId).filter((key) => !Equal.equals(key, child));
72
+ ids.splice(position, 0, child);
73
+ yield* write(encodedParent, ids);
74
+ });
75
+ return { lock, move, reorder };
76
+ });
@@ -0,0 +1,15 @@
1
+ import { Effect } from "effect";
2
+ import type { initializeWorkerRelease } from "./initializeWorkerRelease.ts";
3
+
4
+ export function registerWorkerRelease(state: Effect.Success<typeof initializeWorkerRelease>) {
5
+ return function register<E>(control: (active: boolean) => Effect.Effect<void, E>) {
6
+ const attach = Effect.gen(function* () {
7
+ state.controls.add(control);
8
+ yield* control(state.active);
9
+ });
10
+ const detach = Effect.sync(() => {
11
+ state.controls.delete(control);
12
+ });
13
+ return Effect.acquireRelease(state.locked(attach), () => state.locked(detach));
14
+ };
15
+ }
package/src/repository.ts CHANGED
@@ -10,7 +10,8 @@ import { repositoryChanges } from "#repositoryChanges.ts";
10
10
  import { type BucketRegistrationError, type RepositoryOptions, validateRepositoryCollection } from "#repositoryCollection.ts";
11
11
  import type { FindManyFilter, RepositoryFilter, RepositoryListFilter } from "#repositoryFilter.ts";
12
12
  import { makeQueries, type Queries } from "#repositoryQueries.ts";
13
- import { type IdColumn, makeVersionedWrites, type VersionColumn, type VersionedWrites } from "#versionedRepository.ts";
13
+ import type { VersionedRepositoryShape } from "#VersionedRepositoryShape.ts";
14
+ import { type IdColumn, makeVersionedWrites, type VersionColumn } from "#versionedRepository.ts";
14
15
 
15
16
  type Key<TModel extends Model.Any> = Extract<keyof TModel["fields"] & keyof Row<TModel>, string>;
16
17
  type Row<TModel extends Model.Any> = Schema.Struct.Type<TModel["fields"]>;
@@ -41,39 +42,37 @@ export class OptimisticConcurrencyError extends Data.TaggedError("OptimisticConc
41
42
  readonly reason: "missing" | "stale";
42
43
  }> {}
43
44
 
44
- type VersionedRepository<TModel extends Model.Any, TId extends PrimaryKey<TModel>> = Omit<
45
- Repository<TModel, TId>,
46
- keyof VersionedWrites<TModel, TId>
47
- > &
48
- VersionedWrites<TModel, TId> &
49
- Queries<TModel>;
50
-
51
45
  export function makeRepository<
52
46
  TModel extends Model.Any,
53
47
  const TId extends PrimaryKey<TModel>,
54
48
  const TConflict extends ConflictColumns<TModel>,
55
49
  const TWhere extends keyof RepositoryFilter<TModel> = never,
50
+ const TVersion extends VersionColumn<TModel, TId> = VersionColumn<TModel, TId>,
56
51
  >(
57
52
  model: TModel,
58
53
  options: RepositoryOptions<TId> & {
59
- readonly versionColumn: VersionColumn<TModel, TId>;
54
+ readonly versionColumn: TVersion;
60
55
  readonly conflictColumns: TConflict;
61
56
  readonly conflictWhere?: Pick<RepositoryFilter<TModel>, TWhere>;
62
57
  },
63
58
  ): Effect.Effect<
64
- VersionedRepository<TModel, TId> &
59
+ VersionedRepositoryShape<TModel, TId, TVersion> &
65
60
  Pick<ConflictWrites<TModel, never, TModel["fields"][Extract<TWhere, keyof TModel["fields"]>]["EncodingServices"]>, "insertOrIgnore">,
66
61
  BucketRegistrationError,
67
62
  SqlClient.SqlClient
68
63
  >;
69
- export function makeRepository<TModel extends Model.Any, const TId extends PrimaryKey<TModel>>(
64
+ export function makeRepository<
65
+ TModel extends Model.Any,
66
+ const TId extends PrimaryKey<TModel>,
67
+ const TVersion extends VersionColumn<TModel, TId> = VersionColumn<TModel, TId>,
68
+ >(
70
69
  model: TModel,
71
70
  options: RepositoryOptions<TId> & {
72
- readonly versionColumn: VersionColumn<TModel, TId>;
71
+ readonly versionColumn: TVersion;
73
72
  readonly conflictColumns?: never;
74
73
  readonly conflictWhere?: never;
75
74
  },
76
- ): Effect.Effect<VersionedRepository<TModel, TId>, BucketRegistrationError, SqlClient.SqlClient>;
75
+ ): Effect.Effect<VersionedRepositoryShape<TModel, TId, TVersion>, BucketRegistrationError, SqlClient.SqlClient>;
77
76
  export function makeRepository<
78
77
  TModel extends Model.Any,
79
78
  const TId extends PrimaryKey<TModel>,