@shivaedev/sql 0.34.0 → 0.36.1

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 (124) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +3 -3
  3. package/dist/aggregateColumn.d.ts +3 -3
  4. package/dist/aggregateColumn.d.ts.map +1 -1
  5. package/dist/aggregateColumn.js +4 -2
  6. package/dist/aggregateColumn.js.map +1 -1
  7. package/dist/batchWrites.d.ts +4 -4
  8. package/dist/batchWrites.d.ts.map +1 -1
  9. package/dist/batchWrites.js +8 -5
  10. package/dist/batchWrites.js.map +1 -1
  11. package/dist/batchedDelete.js +1 -1
  12. package/dist/batchedDelete.js.map +1 -1
  13. package/dist/compositeRepository.d.ts +9 -5
  14. package/dist/compositeRepository.d.ts.map +1 -1
  15. package/dist/compositeRepository.js +12 -9
  16. package/dist/compositeRepository.js.map +1 -1
  17. package/dist/conditionalUpsert.d.ts +4 -4
  18. package/dist/conditionalUpsert.d.ts.map +1 -1
  19. package/dist/conditionalUpsert.js +7 -4
  20. package/dist/conditionalUpsert.js.map +1 -1
  21. package/dist/conflictWrites.d.ts +4 -4
  22. package/dist/conflictWrites.d.ts.map +1 -1
  23. package/dist/conflictWrites.js +11 -8
  24. package/dist/conflictWrites.js.map +1 -1
  25. package/dist/coordinateWorkerRelease.d.ts.map +1 -1
  26. package/dist/coordinateWorkerRelease.js +7 -1
  27. package/dist/coordinateWorkerRelease.js.map +1 -1
  28. package/dist/insertFromSelect.d.ts +3 -3
  29. package/dist/insertFromSelect.d.ts.map +1 -1
  30. package/dist/insertFromSelect.js +5 -3
  31. package/dist/insertFromSelect.js.map +1 -1
  32. package/dist/latestPerGroup.d.ts +2 -2
  33. package/dist/latestPerGroup.d.ts.map +1 -1
  34. package/dist/latestPerGroup.js +5 -4
  35. package/dist/latestPerGroup.js.map +1 -1
  36. package/dist/makeAggregate.d.ts +4 -3
  37. package/dist/makeAggregate.d.ts.map +1 -1
  38. package/dist/makeAggregate.js +35 -31
  39. package/dist/makeAggregate.js.map +1 -1
  40. package/dist/makeConflictClause.d.ts +3 -3
  41. package/dist/makeConflictClause.d.ts.map +1 -1
  42. package/dist/makeConflictClause.js +6 -4
  43. package/dist/makeConflictClause.js.map +1 -1
  44. package/dist/makeQueryClauses.d.ts +3 -3
  45. package/dist/makeQueryClauses.d.ts.map +1 -1
  46. package/dist/makeQueryClauses.js +5 -3
  47. package/dist/makeQueryClauses.js.map +1 -1
  48. package/dist/pinnedGroups.d.ts +3 -4
  49. package/dist/pinnedGroups.d.ts.map +1 -1
  50. package/dist/pinnedGroups.js +8 -10
  51. package/dist/pinnedGroups.js.map +1 -1
  52. package/dist/releaseWorkers.d.ts.map +1 -1
  53. package/dist/releaseWorkers.js +10 -4
  54. package/dist/releaseWorkers.js.map +1 -1
  55. package/dist/repository.d.ts +1 -1
  56. package/dist/repository.d.ts.map +1 -1
  57. package/dist/repository.js +7 -9
  58. package/dist/repository.js.map +1 -1
  59. package/dist/repositoryChanges.d.ts +2 -2
  60. package/dist/repositoryChanges.d.ts.map +1 -1
  61. package/dist/repositoryChanges.js +2 -1
  62. package/dist/repositoryChanges.js.map +1 -1
  63. package/dist/repositoryFilter.d.ts +4 -4
  64. package/dist/repositoryFilter.d.ts.map +1 -1
  65. package/dist/repositoryFilter.js +5 -3
  66. package/dist/repositoryFilter.js.map +1 -1
  67. package/dist/repositoryQueries.d.ts +3 -3
  68. package/dist/repositoryQueries.d.ts.map +1 -1
  69. package/dist/repositoryQueries.js +45 -41
  70. package/dist/repositoryQueries.js.map +1 -1
  71. package/dist/repositoryWrites.d.ts +9 -5
  72. package/dist/repositoryWrites.d.ts.map +1 -1
  73. package/dist/repositoryWrites.js +8 -7
  74. package/dist/repositoryWrites.js.map +1 -1
  75. package/dist/testing/WorkerRelease.d.ts +13 -0
  76. package/dist/testing/WorkerRelease.d.ts.map +1 -0
  77. package/dist/testing/WorkerRelease.js +4 -0
  78. package/dist/testing/WorkerRelease.js.map +1 -0
  79. package/dist/tupleIn.d.ts +1 -2
  80. package/dist/tupleIn.d.ts.map +1 -1
  81. package/dist/tupleIn.js +16 -17
  82. package/dist/tupleIn.js.map +1 -1
  83. package/dist/upsertResult.d.ts +2 -2
  84. package/dist/upsertResult.d.ts.map +1 -1
  85. package/dist/upsertResult.js +4 -2
  86. package/dist/upsertResult.js.map +1 -1
  87. package/dist/versionedRepository.d.ts +5 -5
  88. package/dist/versionedRepository.d.ts.map +1 -1
  89. package/dist/versionedRepository.js +5 -3
  90. package/dist/versionedRepository.js.map +1 -1
  91. package/dist/versionedStatements.d.ts +3 -3
  92. package/dist/versionedStatements.d.ts.map +1 -1
  93. package/dist/versionedStatements.js +12 -9
  94. package/dist/versionedStatements.js.map +1 -1
  95. package/package.json +8 -7
  96. package/src/aggregateColumn.ts +4 -3
  97. package/src/batchWrites.ts +10 -10
  98. package/src/batchedDelete.ts +1 -1
  99. package/src/compositeRepository.ts +15 -15
  100. package/src/conditionalUpsert.ts +7 -6
  101. package/src/conflictWrites.ts +14 -10
  102. package/src/coordinateWorkerRelease.ts +7 -1
  103. package/src/insertFromSelect.ts +6 -6
  104. package/src/latestPerGroup.ts +5 -4
  105. package/src/makeAggregate.ts +48 -47
  106. package/src/makeConflictClause.ts +6 -6
  107. package/src/makeQueryClauses.ts +6 -6
  108. package/src/pinnedGroups.ts +10 -14
  109. package/src/releaseWorkers.ts +16 -4
  110. package/src/repository.ts +8 -9
  111. package/src/repositoryChanges.ts +2 -6
  112. package/src/repositoryFilter.ts +5 -4
  113. package/src/repositoryQueries.ts +57 -55
  114. package/src/repositoryWrites.ts +9 -9
  115. package/src/testing/WorkerRelease.ts +13 -0
  116. package/src/tupleIn.ts +16 -18
  117. package/src/upsertResult.ts +4 -3
  118. package/src/versionedRepository.ts +6 -6
  119. package/src/versionedStatements.ts +15 -15
  120. package/dist/testing.d.ts +0 -27
  121. package/dist/testing.d.ts.map +0 -1
  122. package/dist/testing.js +0 -75
  123. package/dist/testing.js.map +0 -1
  124. package/src/testing.ts +0 -103
@@ -18,8 +18,9 @@ function groupMasks(input: readonly (readonly unknown[])[]) {
18
18
  return [...masks.values()];
19
19
  }
20
20
 
21
- export function latestPerGroup(model: Model.Any, sql: SqlClient.SqlClient, tableName: string) {
22
- const { fieldAt, where } = makeFilter(model, sql);
21
+ export const latestPerGroup = Effect.fnUntraced(function* (model: Model.Any, tableName: string) {
22
+ const sql = yield* SqlClient.SqlClient;
23
+ const { fieldAt, where } = yield* makeFilter(model);
23
24
  function groupSource(groups: readonly (readonly unknown[])[], partitionBy: readonly string[], fromTuple: boolean) {
24
25
  const partitions = sql.csv(partitionBy.map((key) => sql`${sql(key)}`));
25
26
  if (fromTuple) {
@@ -37,7 +38,7 @@ export function latestPerGroup(model: Model.Any, sql: SqlClient.SqlClient, table
37
38
  const partitions = sql.csv(query.partitionBy.map((key) => sql`${sql(key)}`));
38
39
  const keys = Object.keys(model.fields);
39
40
  const columns = sql.csv(keys.map((key) => sql`${sql(key)}`));
40
- const pinned = yield* pinnedGroups(model, sql, query.partitionBy, query.where);
41
+ const pinned = yield* pinnedGroups(model, query.partitionBy, query.where).pipe(Effect.provideService(SqlClient.SqlClient, sql));
41
42
  if (pinned !== undefined && pinned.groups.length === 0) {
42
43
  return [];
43
44
  }
@@ -82,4 +83,4 @@ export function latestPerGroup(model: Model.Any, sql: SqlClient.SqlClient, table
82
83
  order by ${partitions}, ${sql(rankColumn)}`;
83
84
  return yield* Schema.decodeUnknownEffect(Schema.Array(Schema.Struct(model.fields)))(rows);
84
85
  });
85
- }
86
+ });
@@ -1,6 +1,6 @@
1
1
  import { Effect, Schema, SchemaIssue } from "effect";
2
2
  import type { Model } from "effect/schema";
3
- import type { SqlClient } from "effect/sql";
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
6
  import { aggregateColumn } from "#aggregateColumn.ts";
@@ -10,55 +10,56 @@ import { type FindManyFilter, makeFilter } from "#repositoryFilter.ts";
10
10
 
11
11
  export function makeAggregate<TModel extends Model.Any>(
12
12
  model: TModel,
13
- sql: SqlClient.SqlClient,
14
13
  options: { readonly tableName: string; readonly spanPrefix: string },
15
- ): Aggregate<TModel>;
14
+ ): Effect.Effect<Aggregate<TModel>, never, SqlClient.SqlClient>;
16
15
  export function makeAggregate(
17
16
  model: Model.Any,
18
- sql: SqlClient.SqlClient,
19
17
  options: { readonly tableName: string; readonly spanPrefix: string },
20
- ): unknown {
21
- const { fieldAt, where } = makeFilter(model, sql);
22
- const selectAggregate = aggregateColumn(sql, fieldAt);
23
- return (query: {
24
- readonly groupBy: readonly string[];
25
- readonly aggregates: Readonly<Record<string, object>>;
26
- readonly where?: FindManyFilter<Model.Any>;
27
- readonly orderBy?: readonly { readonly field: string; readonly direction: "asc" | "desc" }[];
28
- readonly limit?: number;
29
- }): Effect.Effect<unknown, SqlError | Schema.SchemaError, unknown> =>
30
- Effect.gen(function* () {
31
- const groups = yield* Schema.decodeUnknownEffect(Schema.NonEmptyArray(Schema.String))(query.groupBy);
32
- const fields = new Map<string, Schema.Constraint>();
33
- const columns: Fragment[] = [];
34
- const ordering = new Map<string, Fragment>();
35
- for (const key of groups) {
36
- fields.set(key, yield* fieldAt(key));
37
- columns.push(sql`${sql(key)}`);
38
- }
39
- for (const [name, expression] of Object.entries(query.aggregates)) {
40
- if (groups.includes(name)) {
41
- return yield* Effect.fail(new Schema.SchemaError(new SchemaIssue.Pointer([name], new SchemaIssue.UnexpectedKey(model.ast, name))));
18
+ ): Effect.Effect<unknown, never, SqlClient.SqlClient> {
19
+ return Effect.fnUntraced(function* () {
20
+ const sql = yield* SqlClient.SqlClient;
21
+ const { fieldAt, where } = yield* makeFilter(model);
22
+ const selectAggregate = yield* aggregateColumn(fieldAt);
23
+ return (query: {
24
+ readonly groupBy: readonly string[];
25
+ readonly aggregates: Readonly<Record<string, object>>;
26
+ readonly where?: FindManyFilter<Model.Any>;
27
+ readonly orderBy?: readonly { readonly field: string; readonly direction: "asc" | "desc" }[];
28
+ readonly limit?: number;
29
+ }): Effect.Effect<unknown, SqlError | Schema.SchemaError, unknown> =>
30
+ Effect.gen(function* () {
31
+ const groups = yield* Schema.decodeUnknownEffect(Schema.NonEmptyArray(Schema.String))(query.groupBy);
32
+ const fields = new Map<string, Schema.Constraint>();
33
+ const columns: Fragment[] = [];
34
+ const ordering = new Map<string, Fragment>();
35
+ for (const key of groups) {
36
+ fields.set(key, yield* fieldAt(key));
37
+ columns.push(sql`${sql(key)}`);
42
38
  }
43
- const selected = yield* selectAggregate(name, expression);
44
- ordering.set(name, selected.order);
45
- columns.push(selected.column);
46
- fields.set(name, selected.schema);
47
- }
48
- const Result = Schema.Struct(Object.fromEntries(fields));
49
- const clauses = makeQueryClauses(
50
- sql,
51
- (key) =>
52
- fields.has(key)
53
- ? Effect.succeed(fields.get(key))
54
- : Effect.fail(new Schema.SchemaError(new SchemaIssue.Pointer([key], new SchemaIssue.UnexpectedKey(Result.ast, key)))),
55
- (key) => ordering.get(key) ?? sql`${sql(key)}`,
56
- );
57
- const predicate = yield* where(query.where, true, true);
58
- const order = yield* clauses.orderClause(query.orderBy);
59
- const limit = yield* clauses.limitClause(query.limit);
60
- const partitions = sql.csv(groups.map((key) => sql`${sql(key)}`));
61
- const rows = yield* sql`select ${sql.csv(columns)} from ${sql(options.tableName)} where ${predicate} group by ${partitions} ${order} ${limit}`;
62
- return yield* Schema.decodeUnknownEffect(Schema.Array(Result))(rows);
63
- }).pipe(Effect.withSpan(`${options.spanPrefix}.aggregate`));
39
+ for (const [name, expression] of Object.entries(query.aggregates)) {
40
+ if (groups.includes(name)) {
41
+ return yield* Effect.fail(new Schema.SchemaError(new SchemaIssue.Pointer([name], new SchemaIssue.UnexpectedKey(model.ast, name))));
42
+ }
43
+ const selected = yield* selectAggregate(name, expression);
44
+ ordering.set(name, selected.order);
45
+ columns.push(selected.column);
46
+ fields.set(name, selected.schema);
47
+ }
48
+ const Result = Schema.Struct(Object.fromEntries(fields));
49
+ const clauses = yield* makeQueryClauses(
50
+ (key) =>
51
+ fields.has(key)
52
+ ? Effect.succeed(fields.get(key))
53
+ : Effect.fail(new Schema.SchemaError(new SchemaIssue.Pointer([key], new SchemaIssue.UnexpectedKey(Result.ast, key)))),
54
+ (key) => ordering.get(key) ?? sql`${sql(key)}`,
55
+ ).pipe(Effect.provideService(SqlClient.SqlClient, sql));
56
+ const predicate = yield* where(query.where, true, true);
57
+ const order = yield* clauses.orderClause(query.orderBy);
58
+ const limit = yield* clauses.limitClause(query.limit);
59
+ const partitions = sql.csv(groups.map((key) => sql`${sql(key)}`));
60
+ const rows =
61
+ yield* sql`select ${sql.csv(columns)} from ${sql(options.tableName)} where ${predicate} group by ${partitions} ${order} ${limit}`;
62
+ return yield* Schema.decodeUnknownEffect(Schema.Array(Result))(rows);
63
+ }).pipe(Effect.withSpan(`${options.spanPrefix}.aggregate`));
64
+ })();
64
65
  }
@@ -1,12 +1,11 @@
1
1
  import { Array as Arr, Effect, Schema, SchemaAST, SchemaIssue } from "effect";
2
2
  import type { Model } from "effect/schema";
3
- import type { SqlClient } from "effect/sql";
3
+ import { SqlClient } from "effect/sql";
4
4
  import { makeFilter } from "#repositoryFilter.ts";
5
5
  import type { UpsertAssignment } from "#upsertColumns.ts";
6
6
 
7
- export function makeConflictClause(
7
+ export const makeConflictClause = Effect.fnUntraced(function* (
8
8
  model: Model.Any,
9
- sql: SqlClient.SqlClient,
10
9
  options: {
11
10
  readonly tableName: string;
12
11
  readonly conflictColumns: readonly string[];
@@ -14,9 +13,10 @@ export function makeConflictClause(
14
13
  readonly idColumns: readonly string[];
15
14
  },
16
15
  ) {
16
+ const sql = yield* SqlClient.SqlClient;
17
17
  const { conflictColumns, idColumns } = options;
18
- const updateFilter = makeFilter(model, sql, options);
19
- const filter = makeFilter(model, sql);
18
+ const updateFilter = yield* makeFilter(model, options);
19
+ const filter = yield* makeFilter(model);
20
20
  const updateFields = SchemaAST.isObjects(model.update.ast) ? model.update.ast.propertySignatures.map((field) => field.name) : [];
21
21
  const immutable = Object.keys(model.fields).filter((column) => !updateFields.includes(column));
22
22
  function invalid(column: string) {
@@ -95,4 +95,4 @@ export function makeConflictClause(
95
95
  });
96
96
  }
97
97
  return clause;
98
- }
98
+ });
@@ -1,5 +1,5 @@
1
1
  import { Effect, Schema } from "effect";
2
- import type { SqlClient } from "effect/sql";
2
+ import { SqlClient } from "effect/sql";
3
3
  import type { Fragment } from "effect/sql/Statement";
4
4
 
5
5
  const Limit = Schema.Number.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0));
@@ -9,11 +9,11 @@ interface Order {
9
9
  readonly field: string;
10
10
  }
11
11
 
12
- export function makeQueryClauses(
13
- sql: SqlClient.SqlClient,
12
+ export const makeQueryClauses = Effect.fnUntraced(function* (
14
13
  fieldAt: (key: string) => Effect.Effect<unknown, Schema.SchemaError>,
15
- column: (key: string) => Fragment = (key) => sql`${sql(key)}`,
14
+ column?: (key: string) => Fragment,
16
15
  ) {
16
+ const sql = yield* SqlClient.SqlClient;
17
17
  function orderClause(orderBy: Order | readonly Order[] | undefined) {
18
18
  if (orderBy === undefined) {
19
19
  return Effect.succeed(sql``);
@@ -24,7 +24,7 @@ export function makeQueryClauses(
24
24
  }
25
25
  return Effect.map(
26
26
  Effect.forEach(entries, ({ field, direction }) =>
27
- Effect.as(fieldAt(field), sql`${column(field)} ${sql.literal(direction === "desc" ? "desc" : "asc")}`),
27
+ Effect.as(fieldAt(field), sql`${column?.(field) ?? sql`${sql(field)}`} ${sql.literal(direction === "desc" ? "desc" : "asc")}`),
28
28
  ),
29
29
  (columns) => sql`order by ${sql.csv(columns)}`,
30
30
  );
@@ -33,4 +33,4 @@ export function makeQueryClauses(
33
33
  return limit === undefined ? Effect.succeed(sql``) : Effect.map(decodeLimit(limit), (rowLimit) => sql`limit ${rowLimit}`);
34
34
  }
35
35
  return { decodeLimit, limitClause, orderClause };
36
- }
36
+ });
@@ -1,13 +1,11 @@
1
1
  import { Effect, Schema } from "effect";
2
2
  import type { Model } from "effect/schema";
3
- import type { SqlClient } from "effect/sql";
4
3
  import type { RepositoryListFilter } from "#repositoryFilter.ts";
5
4
  import { makeFilter } from "#repositoryFilter.ts";
6
5
  import { tupleFilter } from "#tupleIn.ts";
7
6
 
8
- export function pinnedGroups(
7
+ export const pinnedGroups = Effect.fnUntraced(function* (
9
8
  model: Model.Any,
10
- sql: SqlClient.SqlClient,
11
9
  partitionBy: readonly string[],
12
10
  input: RepositoryListFilter<Model.Any> = {},
13
11
  ) {
@@ -18,7 +16,7 @@ export function pinnedGroups(
18
16
  (value) => typeof value === "object" && value !== null && "in" in value && Array.isArray(value.in) && value.in.length === 0,
19
17
  )
20
18
  ) {
21
- return Effect.succeed({ groups: [], remaining: input, tuple: undefined });
19
+ return { groups: [], remaining: input, tuple: undefined };
22
20
  }
23
21
  const exactTuple = tuple !== undefined && tuple.fields.length === partitionBy.length && partitionBy.every((key) => tuple.fields.includes(key));
24
22
  let groups: readonly (readonly unknown[])[] = [[]];
@@ -30,19 +28,17 @@ export function pinnedGroups(
30
28
  for (const key of partitionBy) {
31
29
  const value = input[key];
32
30
  if (value === undefined) {
33
- return Effect.succeed(undefined);
31
+ return undefined;
34
32
  }
35
33
  const values = typeof value === "object" && value !== null && "in" in value && Array.isArray(value.in) ? value.in : [value];
36
34
  groups = groups.flatMap((group) => values.map((entry) => [...group, entry]));
37
35
  delete remaining[key];
38
36
  }
39
37
  }
40
- const { fieldAt } = makeFilter(model, sql);
41
- return Effect.gen(function* () {
42
- const fields = yield* Effect.forEach(partitionBy, fieldAt);
43
- const groupsEncoded: readonly (readonly unknown[])[] = yield* Effect.forEach(groups, (group) =>
44
- Schema.encodeUnknownEffect(Schema.Tuple(fields))(group),
45
- );
46
- return { groups: groupsEncoded, remaining, tuple: exactTuple ? tuple : undefined };
47
- });
48
- }
38
+ const { fieldAt } = yield* makeFilter(model);
39
+ const fields = yield* Effect.forEach(partitionBy, fieldAt);
40
+ const groupsEncoded: readonly (readonly unknown[])[] = yield* Effect.forEach(groups, (group) =>
41
+ Schema.encodeUnknownEffect(Schema.Tuple(fields))(group),
42
+ );
43
+ return { groups: groupsEncoded, remaining, tuple: exactTuple ? tuple : undefined };
44
+ });
@@ -1,5 +1,6 @@
1
- import { Effect, Random, Schema } from "effect";
1
+ import { Clock, Effect, Option, Random, Schema } from "effect";
2
2
  import { SqlClient } from "effect/sql";
3
+ import { WorkerReleaseTesting } from "#testing/WorkerRelease.ts";
3
4
 
4
5
  export const releaseWorkers = Effect.fn("EffectSql.releaseWorkers")(function* (
5
6
  release: string,
@@ -9,9 +10,17 @@ export const releaseWorkers = Effect.fn("EffectSql.releaseWorkers")(function* (
9
10
  const sql = yield* SqlClient.SqlClient;
10
11
  const processId = `${yield* Random.nextInt}:${yield* Random.nextInt}`;
11
12
  const workers = `${table}_workers`;
13
+ const testing = yield* Effect.serviceOption(WorkerReleaseTesting);
14
+ const now = Option.isSome(testing)
15
+ ? Clock.currentTimeMillis.pipe(
16
+ Effect.provideService(Clock.Clock, testing.value.clock),
17
+ Effect.map((millis) => sql`${new Date(millis).toISOString()}::timestamptz`),
18
+ )
19
+ : Effect.succeed(sql`clock_timestamp()`);
12
20
  return {
13
21
  claim: sql.withTransaction(
14
22
  Effect.gen(function* () {
23
+ const timestamp = yield* now;
15
24
  const [owner] = yield* sql`select release from ${sql(`${table}_release`)} where id = true for update`.withoutTransform.pipe(
16
25
  Effect.flatMap(Schema.decodeUnknownEffect(Schema.Array(Schema.Struct({ release: Schema.String })))),
17
26
  );
@@ -19,16 +28,19 @@ export const releaseWorkers = Effect.fn("EffectSql.releaseWorkers")(function* (
19
28
  return false;
20
29
  }
21
30
  const others = yield* sql`select process_id from ${sql(workers)} where release <> ${release}
22
- and heartbeat > clock_timestamp() - ${staleMillis} * interval '1 millisecond'`;
31
+ and heartbeat > ${timestamp} - ${staleMillis} * interval '1 millisecond'`;
23
32
  if (others.length > 0) {
24
33
  return false;
25
34
  }
26
- yield* sql`insert into ${sql(workers)} (process_id, release, heartbeat) values (${processId}, ${release}, clock_timestamp())
35
+ yield* sql`insert into ${sql(workers)} (process_id, release, heartbeat) values (${processId}, ${release}, ${timestamp})
27
36
  on conflict (process_id) do update set heartbeat = excluded.heartbeat`;
28
37
  return true;
29
38
  }),
30
39
  ),
31
- heartbeat: sql`update ${sql(workers)} set heartbeat = clock_timestamp() where process_id = ${processId}`.pipe(Effect.asVoid),
40
+ heartbeat: now.pipe(
41
+ Effect.flatMap((timestamp) => sql`update ${sql(workers)} set heartbeat = ${timestamp} where process_id = ${processId}`),
42
+ Effect.asVoid,
43
+ ),
32
44
  remove: sql`delete from ${sql(workers)} where process_id = ${processId}`.pipe(Effect.asVoid),
33
45
  };
34
46
  });
package/src/repository.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Data, Effect, type Schema } from "effect";
2
2
  import type { Model } from "effect/schema";
3
- import { SqlClient } from "effect/sql";
3
+ import type { SqlClient } from "effect/sql";
4
4
  import { makeBatchedDelete } from "#batchedDelete.ts";
5
5
  import { type BatchWrites, makeInsertMany } from "#batchWrites.ts";
6
6
  import { makeCompositeCrud, type PrimaryKey, type Repository } from "#compositeRepository.ts";
@@ -117,27 +117,26 @@ export function makeRepository<TModel extends Model.Any, TId extends IdColumn<TM
117
117
  ): Effect.Effect<unknown, BucketRegistrationError, SqlClient.SqlClient> {
118
118
  return Effect.fnUntraced(function* () {
119
119
  yield* validateRepositoryCollection(model.fields, options);
120
- const sql = yield* SqlClient.SqlClient;
121
120
  const composite = typeof options.idColumn === "object";
122
121
  const idColumns = (composite ? options.idColumn : [options.idColumn]).map(String);
123
- const changes = yield* repositoryChanges(sql, options.tableName, idColumns, model.fields);
122
+ const changes = yield* repositoryChanges(options.tableName, idColumns, model.fields);
124
123
  const crud = {
125
- ...makeCompositeCrud(model, sql, { ...options, columns: idColumns }, changes),
124
+ ...(yield* makeCompositeCrud(model, { ...options, columns: idColumns }, changes)),
126
125
  deleteBatched: yield* makeBatchedDelete(model, options, changes),
127
126
  };
128
- const queries = makeQueries(model, sql, options);
127
+ const queries = yield* makeQueries(model, options);
129
128
  return {
130
129
  ...crud,
131
130
  ...queries,
132
131
  ...(options.versionColumn === undefined
133
132
  ? {
134
- insertFromSelect: makeInsertFromSelect(model, sql, { ...options, changes }, "insertFromSelect"),
135
- insertMany: makeInsertMany(model, sql, { ...options, changes }, "insertMany"),
133
+ insertFromSelect: yield* makeInsertFromSelect(model, { ...options, changes }, "insertFromSelect"),
134
+ insertMany: yield* makeInsertMany(model, { ...options, changes }, "insertMany"),
136
135
  ...(options.conflictColumns === undefined
137
136
  ? {}
138
- : makeConflictWrites(model, sql, { ...options, changes, conflictColumns: options.conflictColumns, idColumns })),
137
+ : yield* makeConflictWrites(model, { ...options, changes, conflictColumns: options.conflictColumns, idColumns })),
139
138
  }
140
- : makeVersionedWrites(model, sql, {
139
+ : yield* makeVersionedWrites(model, {
141
140
  ...options,
142
141
  changes,
143
142
  conflict: (id, expectedVersion, reason) => new OptimisticConcurrencyError({ expectedVersion, id, reason, tableName: options.tableName }),
@@ -8,12 +8,8 @@ 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* (
12
- sql: SqlClient.SqlClient,
13
- table: string,
14
- columns: readonly string[],
15
- fields: Schema.Struct.Fields = {},
16
- ) {
11
+ export const repositoryChanges = Effect.fnUntraced(function* (table: string, columns: readonly string[], fields: Schema.Struct.Fields = {}) {
12
+ const sql = yield* SqlClient.SqlClient;
17
13
  const buckets = declaredBuckets(fields);
18
14
  const bucketKeys = Effect.fnUntraced(function* (row: Readonly<Record<string, unknown>>) {
19
15
  const keys: Key[] = [];
@@ -1,6 +1,6 @@
1
1
  import { Effect, Schema, SchemaIssue } from "effect";
2
2
  import type { Model } from "effect/schema";
3
- import type { SqlClient } from "effect/sql";
3
+ import { SqlClient } from "effect/sql";
4
4
  import { type TupleFilter, tuplePredicate } from "#tupleIn.ts";
5
5
 
6
6
  export type RepositoryFilter<TModel extends Model.Any> = {
@@ -34,7 +34,8 @@ function isOperator(key: string): key is keyof typeof operators {
34
34
  return Object.hasOwn(operators, key);
35
35
  }
36
36
 
37
- export function makeFilter<TModel extends Model.Any>(model: TModel, sql: SqlClient.SqlClient, references?: { readonly tableName: string }) {
37
+ export const makeFilter = Effect.fnUntraced(function* <TModel extends Model.Any>(model: TModel, references?: { readonly tableName: string }) {
38
+ const sql = yield* SqlClient.SqlClient;
38
39
  function fieldAt(key: string) {
39
40
  const field = Object.hasOwn(model.fields, key) ? model.fields[key] : undefined;
40
41
  return field === undefined
@@ -105,7 +106,7 @@ export function makeFilter<TModel extends Model.Any>(model: TModel, sql: SqlClie
105
106
  ([key, value]) => predicate(key, value, allowIn, allowComparisons),
106
107
  );
107
108
  if (allowIn) {
108
- const tuple = yield* tuplePredicate(input, sql, fieldAt);
109
+ const tuple = yield* tuplePredicate(input, fieldAt).pipe(Effect.provideService(SqlClient.SqlClient, sql));
109
110
  if (tuple !== undefined) {
110
111
  predicates.push(tuple);
111
112
  }
@@ -114,4 +115,4 @@ export function makeFilter<TModel extends Model.Any>(model: TModel, sql: SqlClie
114
115
  });
115
116
  }
116
117
  return { fieldAt, where };
117
- }
118
+ });
@@ -1,6 +1,6 @@
1
1
  import { Effect, Schema } from "effect";
2
2
  import type { Model } from "effect/schema";
3
- import type { SqlClient } from "effect/sql";
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
6
  import type { Aggregate } from "#aggregateQuery.ts";
@@ -46,62 +46,64 @@ export interface Queries<TModel extends Model.Any> {
46
46
 
47
47
  export function makeQueries<TModel extends Model.Any>(
48
48
  model: TModel,
49
- sql: SqlClient.SqlClient,
50
49
  options: {
51
50
  readonly tableName: string;
52
51
  readonly spanPrefix: string;
53
52
  },
54
- ): Queries<TModel> {
55
- const { fieldAt, where } = makeFilter(model, sql);
56
- const readLatest = makeLatestPerGroup(model, sql, options.tableName);
57
- const { decodeLimit, orderClause, limitClause } = makeQueryClauses(sql, fieldAt);
58
- const aggregate = makeAggregate(model, sql, options);
59
- function count(query?: Pick<FindMany<TModel, Key<TModel>>, "where">): ReturnType<Queries<TModel>["count"]>;
60
- function count(query: Pick<FindMany<TModel, Key<TModel>>, "where"> = {}): Effect.Effect<number, SqlError | Schema.SchemaError, unknown> {
61
- return Effect.gen(function* () {
62
- const predicate = yield* where(query.where, true, true);
63
- const rows = yield* sql`select cast(count(*) as text) as count from ${sql(options.tableName)} where ${predicate}`;
64
- const [result] = yield* Schema.decodeUnknownEffect(CountResult)(rows);
65
- return result.count;
66
- }).pipe(Effect.withSpan(`${options.spanPrefix}.count`));
67
- }
68
- function select<TKey extends Key<TModel> = Key<TModel>>(
69
- query?: FindMany<TModel, TKey>,
70
- ): RepositorySelect<Pick<TModel["fields"], TKey>, TModel["fields"][Key<TModel>]["EncodingServices"]>;
71
- function select(query: FindMany<TModel, Key<TModel>> = {}): RepositorySelect<Schema.Struct.Fields, unknown> {
72
- const keys = query.select ?? Object.keys(model.fields);
73
- const fields = Object.fromEntries(keys.map((key) => [key, model.fields[key]]));
74
- const statement = Effect.gen(function* () {
75
- yield* Effect.forEach(keys, fieldAt);
76
- const predicate = yield* where(query.where, true, true);
77
- const order = yield* orderClause(query.orderBy);
78
- const limit = yield* limitClause(query.limit);
79
- return sql`select ${sql.csv(keys.map((key) => sql`${sql(key)}`))} from ${sql(options.tableName)} where ${predicate} ${order} ${limit}`;
80
- });
81
- return { fields, statement };
82
- }
83
- function findMany<TKey extends Key<TModel> = Key<TModel>>(
84
- query?: FindMany<TModel, TKey>,
85
- ): Effect.Effect<
86
- Pick<Row<TModel>, TKey>[],
87
- SqlError | Schema.SchemaError,
88
- TModel["fields"][Key<TModel>]["EncodingServices"] | TModel["fields"][TKey]["DecodingServices"]
89
- >;
90
- // Object.fromEntries erases the selected key-to-schema correspondence, so the typed overload restates it.
91
- function findMany(query: FindMany<TModel, Key<TModel>> = {}): Effect.Effect<unknown, SqlError | Schema.SchemaError, unknown> {
92
- return Effect.gen(function* () {
93
- const selected = select(query);
94
- const rows = yield* sql`${yield* selected.statement}`;
95
- return yield* Schema.decodeUnknownEffect(Schema.Array(Schema.Struct(selected.fields)))(rows);
96
- }).pipe(Effect.withSpan(`${options.spanPrefix}.findMany`));
97
- }
98
- function latestPerGroup(query: LatestPerGroup<TModel>): ReturnType<Queries<TModel>["latestPerGroup"]>;
99
- function latestPerGroup(query: LatestPerGroup<TModel>): Effect.Effect<unknown, SqlError | Schema.SchemaError, unknown> {
100
- return Effect.gen(function* () {
101
- const order = yield* orderClause(query.orderBy);
102
- const rowLimit = yield* decodeLimit(query.limit);
103
- return yield* readLatest(query, order, rowLimit);
104
- }).pipe(Effect.withSpan(`${options.spanPrefix}.latestPerGroup`));
105
- }
106
- return { aggregate, count, findMany, latestPerGroup, select };
53
+ ): Effect.Effect<Queries<TModel>, never, SqlClient.SqlClient> {
54
+ return Effect.fnUntraced(function* () {
55
+ const sql = yield* SqlClient.SqlClient;
56
+ const { fieldAt, where } = yield* makeFilter(model);
57
+ const readLatest = yield* makeLatestPerGroup(model, options.tableName);
58
+ const { decodeLimit, orderClause, limitClause } = yield* makeQueryClauses(fieldAt);
59
+ const aggregate = yield* makeAggregate(model, options);
60
+ function count(query?: Pick<FindMany<TModel, Key<TModel>>, "where">): ReturnType<Queries<TModel>["count"]>;
61
+ function count(query: Pick<FindMany<TModel, Key<TModel>>, "where"> = {}): Effect.Effect<number, SqlError | Schema.SchemaError, unknown> {
62
+ return Effect.gen(function* () {
63
+ const predicate = yield* where(query.where, true, true);
64
+ const rows = yield* sql`select cast(count(*) as text) as count from ${sql(options.tableName)} where ${predicate}`;
65
+ const [result] = yield* Schema.decodeUnknownEffect(CountResult)(rows);
66
+ return result.count;
67
+ }).pipe(Effect.withSpan(`${options.spanPrefix}.count`));
68
+ }
69
+ function select<TKey extends Key<TModel> = Key<TModel>>(
70
+ query?: FindMany<TModel, TKey>,
71
+ ): RepositorySelect<Pick<TModel["fields"], TKey>, TModel["fields"][Key<TModel>]["EncodingServices"]>;
72
+ function select(query: FindMany<TModel, Key<TModel>> = {}): RepositorySelect<Schema.Struct.Fields, unknown> {
73
+ const keys = query.select ?? Object.keys(model.fields);
74
+ const fields = Object.fromEntries(keys.map((key) => [key, model.fields[key]]));
75
+ const statement = Effect.gen(function* () {
76
+ yield* Effect.forEach(keys, fieldAt);
77
+ const predicate = yield* where(query.where, true, true);
78
+ const order = yield* orderClause(query.orderBy);
79
+ const limit = yield* limitClause(query.limit);
80
+ return sql`select ${sql.csv(keys.map((key) => sql`${sql(key)}`))} from ${sql(options.tableName)} where ${predicate} ${order} ${limit}`;
81
+ });
82
+ return { fields, statement };
83
+ }
84
+ function findMany<TKey extends Key<TModel> = Key<TModel>>(
85
+ query?: FindMany<TModel, TKey>,
86
+ ): Effect.Effect<
87
+ Pick<Row<TModel>, TKey>[],
88
+ SqlError | Schema.SchemaError,
89
+ TModel["fields"][Key<TModel>]["EncodingServices"] | TModel["fields"][TKey]["DecodingServices"]
90
+ >;
91
+ // Object.fromEntries erases the selected key-to-schema correspondence, so the typed overload restates it.
92
+ function findMany(query: FindMany<TModel, Key<TModel>> = {}): Effect.Effect<unknown, SqlError | Schema.SchemaError, unknown> {
93
+ return Effect.gen(function* () {
94
+ const selected = select(query);
95
+ const rows = yield* sql`${yield* selected.statement}`;
96
+ return yield* Schema.decodeUnknownEffect(Schema.Array(Schema.Struct(selected.fields)))(rows);
97
+ }).pipe(Effect.withSpan(`${options.spanPrefix}.findMany`));
98
+ }
99
+ function latestPerGroup(query: LatestPerGroup<TModel>): ReturnType<Queries<TModel>["latestPerGroup"]>;
100
+ function latestPerGroup(query: LatestPerGroup<TModel>): Effect.Effect<unknown, SqlError | Schema.SchemaError, unknown> {
101
+ return Effect.gen(function* () {
102
+ const order = yield* orderClause(query.orderBy);
103
+ const rowLimit = yield* decodeLimit(query.limit);
104
+ return yield* readLatest(query, order, rowLimit);
105
+ }).pipe(Effect.withSpan(`${options.spanPrefix}.latestPerGroup`));
106
+ }
107
+ return { aggregate, count, findMany, latestPerGroup, select };
108
+ })();
107
109
  }
@@ -1,18 +1,18 @@
1
1
  import { Effect, Schema } from "effect";
2
2
  import type { Model } from "effect/schema";
3
- import { type SqlClient, SqlSchema } from "effect/sql";
4
- import type { RepositoryChanges } from "./repositoryChanges.ts";
5
- import { makeFilter } from "./repositoryFilter.ts";
6
- import { selectedUpdate } from "./repositoryUpdate.ts";
3
+ import { SqlClient, SqlSchema } from "effect/sql";
4
+ import type { RepositoryChanges } from "#repositoryChanges.ts";
5
+ import { makeFilter } from "#repositoryFilter.ts";
6
+ import { selectedUpdate } from "#repositoryUpdate.ts";
7
7
 
8
- export function repositoryWrites(
8
+ export const repositoryWrites = Effect.fnUntraced(function* (
9
9
  model: Model.Any,
10
- sql: SqlClient.SqlClient,
11
10
  options: { readonly idColumns: readonly string[]; readonly tableName: string; readonly spanPrefix: string },
12
11
  changes: RepositoryChanges,
13
12
  ) {
13
+ const sql = yield* SqlClient.SqlClient;
14
14
  const { idColumns, tableName, spanPrefix } = options;
15
- const { where } = makeFilter(model, sql);
15
+ const { where } = yield* makeFilter(model);
16
16
  const record = Schema.decodeUnknownEffect(Schema.Record(Schema.String, Schema.Unknown));
17
17
  const filterRecord = Schema.decodeUnknownEffect(Schema.Record(Schema.PropertyKey, Schema.Unknown));
18
18
  function predicate(input: Readonly<Record<string, unknown>>) {
@@ -49,7 +49,7 @@ export function repositoryWrites(
49
49
  const idColumn = idColumns[0] ?? "";
50
50
  const key = model.fields[idColumn];
51
51
  if (key === undefined) {
52
- throw new Error("Unknown repository ID column");
52
+ return yield* Effect.die(new Error("Unknown repository ID column"));
53
53
  }
54
54
  const isKey = Schema.is(key);
55
55
  const removeByKey = SqlSchema.void({
@@ -84,4 +84,4 @@ export function repositoryWrites(
84
84
  .write(settings === undefined ? updateVoid(input) : selected(input, settings.columns, false))
85
85
  .pipe(Effect.withSpan(`${spanPrefix}.updateVoid`)),
86
86
  };
87
- }
87
+ });
@@ -0,0 +1,13 @@
1
+ import { type Clock, Context, type Effect, type Schema, type Scope } from "effect";
2
+ import type { SqlError } from "effect/sql/SqlError";
3
+
4
+ export class WorkerReleaseTesting extends Context.Service<
5
+ WorkerReleaseTesting,
6
+ {
7
+ readonly clock: Clock.Clock;
8
+ readonly register: (tick: {
9
+ readonly heartbeat: Effect.Effect<void, SqlError | Schema.SchemaError>;
10
+ readonly poll: Effect.Effect<void, SqlError | Schema.SchemaError>;
11
+ }) => Effect.Effect<void, never, Scope.Scope>;
12
+ }
13
+ >()("@shivaedev/sql/testing/WorkerReleaseTesting") {}