@shivaedev/sql 0.34.0 → 0.35.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 (111) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +2 -2
  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/insertFromSelect.d.ts +3 -3
  26. package/dist/insertFromSelect.d.ts.map +1 -1
  27. package/dist/insertFromSelect.js +5 -3
  28. package/dist/insertFromSelect.js.map +1 -1
  29. package/dist/latestPerGroup.d.ts +2 -2
  30. package/dist/latestPerGroup.d.ts.map +1 -1
  31. package/dist/latestPerGroup.js +5 -4
  32. package/dist/latestPerGroup.js.map +1 -1
  33. package/dist/makeAggregate.d.ts +4 -3
  34. package/dist/makeAggregate.d.ts.map +1 -1
  35. package/dist/makeAggregate.js +35 -31
  36. package/dist/makeAggregate.js.map +1 -1
  37. package/dist/makeConflictClause.d.ts +3 -3
  38. package/dist/makeConflictClause.d.ts.map +1 -1
  39. package/dist/makeConflictClause.js +6 -4
  40. package/dist/makeConflictClause.js.map +1 -1
  41. package/dist/makeQueryClauses.d.ts +3 -3
  42. package/dist/makeQueryClauses.d.ts.map +1 -1
  43. package/dist/makeQueryClauses.js +5 -3
  44. package/dist/makeQueryClauses.js.map +1 -1
  45. package/dist/pinnedGroups.d.ts +3 -4
  46. package/dist/pinnedGroups.d.ts.map +1 -1
  47. package/dist/pinnedGroups.js +8 -10
  48. package/dist/pinnedGroups.js.map +1 -1
  49. package/dist/repository.d.ts +1 -1
  50. package/dist/repository.d.ts.map +1 -1
  51. package/dist/repository.js +7 -9
  52. package/dist/repository.js.map +1 -1
  53. package/dist/repositoryChanges.d.ts +2 -2
  54. package/dist/repositoryChanges.d.ts.map +1 -1
  55. package/dist/repositoryChanges.js +2 -1
  56. package/dist/repositoryChanges.js.map +1 -1
  57. package/dist/repositoryFilter.d.ts +4 -4
  58. package/dist/repositoryFilter.d.ts.map +1 -1
  59. package/dist/repositoryFilter.js +5 -3
  60. package/dist/repositoryFilter.js.map +1 -1
  61. package/dist/repositoryQueries.d.ts +3 -3
  62. package/dist/repositoryQueries.d.ts.map +1 -1
  63. package/dist/repositoryQueries.js +45 -41
  64. package/dist/repositoryQueries.js.map +1 -1
  65. package/dist/repositoryWrites.d.ts +9 -5
  66. package/dist/repositoryWrites.d.ts.map +1 -1
  67. package/dist/repositoryWrites.js +8 -7
  68. package/dist/repositoryWrites.js.map +1 -1
  69. package/dist/tupleIn.d.ts +1 -2
  70. package/dist/tupleIn.d.ts.map +1 -1
  71. package/dist/tupleIn.js +16 -17
  72. package/dist/tupleIn.js.map +1 -1
  73. package/dist/upsertResult.d.ts +2 -2
  74. package/dist/upsertResult.d.ts.map +1 -1
  75. package/dist/upsertResult.js +4 -2
  76. package/dist/upsertResult.js.map +1 -1
  77. package/dist/versionedRepository.d.ts +5 -5
  78. package/dist/versionedRepository.d.ts.map +1 -1
  79. package/dist/versionedRepository.js +5 -3
  80. package/dist/versionedRepository.js.map +1 -1
  81. package/dist/versionedStatements.d.ts +3 -3
  82. package/dist/versionedStatements.d.ts.map +1 -1
  83. package/dist/versionedStatements.js +12 -9
  84. package/dist/versionedStatements.js.map +1 -1
  85. package/package.json +7 -6
  86. package/src/aggregateColumn.ts +4 -3
  87. package/src/batchWrites.ts +10 -10
  88. package/src/batchedDelete.ts +1 -1
  89. package/src/compositeRepository.ts +15 -15
  90. package/src/conditionalUpsert.ts +7 -6
  91. package/src/conflictWrites.ts +14 -10
  92. package/src/insertFromSelect.ts +6 -6
  93. package/src/latestPerGroup.ts +5 -4
  94. package/src/makeAggregate.ts +48 -47
  95. package/src/makeConflictClause.ts +6 -6
  96. package/src/makeQueryClauses.ts +6 -6
  97. package/src/pinnedGroups.ts +10 -14
  98. package/src/repository.ts +8 -9
  99. package/src/repositoryChanges.ts +2 -6
  100. package/src/repositoryFilter.ts +5 -4
  101. package/src/repositoryQueries.ts +57 -55
  102. package/src/repositoryWrites.ts +9 -9
  103. package/src/tupleIn.ts +16 -18
  104. package/src/upsertResult.ts +4 -3
  105. package/src/versionedRepository.ts +6 -6
  106. package/src/versionedStatements.ts +15 -15
  107. package/dist/testing.d.ts +0 -27
  108. package/dist/testing.d.ts.map +0 -1
  109. package/dist/testing.js +0 -75
  110. package/dist/testing.js.map +0 -1
  111. package/src/testing.ts +0 -103
@@ -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
+ });
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
+ });
package/src/tupleIn.ts CHANGED
@@ -51,27 +51,25 @@ export function tupleIn<TModel extends Model.Any, const TFields extends readonly
51
51
  return { [tupleFilter]: { fields, model, types: fields.map((field: TFields[number]) => types[field]), values } };
52
52
  }
53
53
 
54
- export function tuplePredicate(
54
+ export const tuplePredicate = Effect.fnUntraced(function* (
55
55
  input: Partial<TupleFilter<Model.Any>>,
56
- sql: SqlClient.SqlClient,
57
56
  fieldAt: (key: string) => Effect.Effect<Schema.Constraint, Schema.SchemaError>,
58
57
  ) {
59
58
  const tuple = input[tupleFilter];
60
59
  if (tuple === undefined) {
61
- return Effect.succeed(undefined);
60
+ return undefined;
62
61
  }
63
- return Effect.gen(function* () {
64
- const fields = yield* Effect.forEach(tuple.fields, fieldAt);
65
- const encode = Schema.encodeUnknownEffect(Schema.Tuple(fields));
66
- const columns = sql.csv(tuple.fields.map((field) => sql`${sql(field)}`));
67
- const rows: readonly (readonly unknown[])[] = yield* Effect.forEach(tuple.values, (value) => encode(value));
68
- if (rows.length === 0) {
69
- return sql`1 = 0`;
70
- }
71
- if (sql.onDialectOrElse({ orElse: () => false, pg: () => true })) {
72
- const relation = yield* tupleUnnest(tuple.types, rows).pipe(Effect.provideService(SqlClient.SqlClient, sql));
73
- return sql`(${columns}) in (select * from ${relation})`;
74
- }
75
- return sql.or(rows.map((row) => sql`(${columns}) = (${sql.csv(row.map((entry) => sql`${entry}`))})`));
76
- });
77
- }
62
+ const sql = yield* SqlClient.SqlClient;
63
+ const fields = yield* Effect.forEach(tuple.fields, fieldAt);
64
+ const encode = Schema.encodeUnknownEffect(Schema.Tuple(fields));
65
+ const columns = sql.csv(tuple.fields.map((field) => sql`${sql(field)}`));
66
+ const rows: readonly (readonly unknown[])[] = yield* Effect.forEach(tuple.values, (value) => encode(value));
67
+ if (rows.length === 0) {
68
+ return sql`1 = 0`;
69
+ }
70
+ if (sql.onDialectOrElse({ orElse: () => false, pg: () => true })) {
71
+ const relation = yield* tupleUnnest(tuple.types, rows).pipe(Effect.provideService(SqlClient.SqlClient, sql));
72
+ return sql`(${columns}) in (select * from ${relation})`;
73
+ }
74
+ return sql.or(rows.map((row) => sql`(${columns}) = (${sql.csv(row.map((entry) => sql`${entry}`))})`));
75
+ });
@@ -1,15 +1,16 @@
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
 
5
5
  export interface UpsertRow<TRow> {
6
6
  readonly inserted: boolean;
7
7
  readonly row: TRow;
8
8
  }
9
9
 
10
- export function upsertReturning(sql: SqlClient.SqlClient) {
10
+ export const upsertReturning = Effect.fnUntraced(function* () {
11
+ const sql = yield* SqlClient.SqlClient;
11
12
  return sql`*, (xmax = 0) as ${sql("effectSqlInserted")}`;
12
- }
13
+ });
13
14
 
14
15
  export function makeUpsertDecoder(model: Model.Any) {
15
16
  const decode = Schema.decodeUnknownEffect(model);
@@ -1,12 +1,12 @@
1
1
  import { Effect, Option, Schema } from "effect";
2
2
  import type { Model } from "effect/schema";
3
- import type { SqlClient, SqlModel } from "effect/sql";
3
+ import { SqlClient, type SqlModel } from "effect/sql";
4
4
  import type { SqlError } from "effect/sql/SqlError";
5
5
  import type { PrimaryKey, Repository } from "#compositeRepository.ts";
6
6
  import type { OptimisticConcurrencyError } from "#repository.ts";
7
+ import type { RepositoryChanges } from "#repositoryChanges.ts";
7
8
  import type { UpdateInput } from "#repositoryUpdate.ts";
8
9
  import { type VersionedOptions, versionedStatements } from "#versionedStatements.ts";
9
- import type { RepositoryChanges } from "./repositoryChanges.ts";
10
10
 
11
11
  export type IdColumn<TModel extends Model.Any> = keyof TModel["Type"] & keyof TModel["update"]["Type"] & keyof TModel["fields"];
12
12
  export type VersionColumn<TModel extends Model.Any, TId extends PrimaryKey<TModel>> = {
@@ -43,13 +43,13 @@ export interface VersionedWrites<TModel extends Model.Any, TId extends PrimaryKe
43
43
  readonly updateVoid: (input: UpdateInput<TModel>) => Effect.Effect<void, WriteError, TModel["update"]["EncodingServices"]>;
44
44
  }
45
45
 
46
- export function makeVersionedWrites(
46
+ export const makeVersionedWrites = Effect.fnUntraced(function* (
47
47
  model: Model.Any,
48
- sql: SqlClient.SqlClient,
49
48
  options: VersionedOptions & { readonly changes: RepositoryChanges; readonly spanPrefix: string },
50
49
  ) {
50
+ const sql = yield* SqlClient.SqlClient;
51
51
  const { spanPrefix, changes } = options;
52
- const statements = versionedStatements(model, sql, options);
52
+ const statements = yield* versionedStatements(model, options);
53
53
  const decodeRow = Schema.decodeUnknownEffect(model);
54
54
  function insert(input: unknown) {
55
55
  return Effect.gen(function* () {
@@ -106,4 +106,4 @@ export function makeVersionedWrites(
106
106
  }).pipe(changes.write, Effect.withSpan(`${spanPrefix}.delete`));
107
107
  }
108
108
  return { delete: deleteRow, insert, insertVoid, update, updateVoid, ...(options.conflictColumns === undefined ? {} : { insertOrIgnore }) };
109
- }
109
+ });
@@ -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 { makeConflictClause } from "#makeConflictClause.ts";
5
5
  import type { OptimisticConcurrencyError } from "#repository.ts";
6
6
 
@@ -14,21 +14,21 @@ export interface VersionedOptions {
14
14
  readonly versionColumn: string;
15
15
  }
16
16
 
17
- export function versionedStatements(model: Model.Any, sql: SqlClient.SqlClient, options: VersionedOptions) {
17
+ export const versionedStatements = Effect.fnUntraced(function* (model: Model.Any, options: VersionedOptions) {
18
+ const sql = yield* SqlClient.SqlClient;
18
19
  const { tableName, idColumns, versionColumn, conflict } = options;
19
- const fields = Object.fromEntries(
20
- idColumns.map((column) => {
21
- const field = model.fields[column];
22
- if (field === undefined) {
23
- throw new Error(`Unknown ID column: ${column}`);
24
- }
25
- return [column, field];
26
- }),
27
- );
28
- const encodeKey = Schema.encodeEffect(Schema.Struct(fields));
20
+ const entries: [string, Schema.Constraint][] = [];
21
+ for (const column of idColumns) {
22
+ const field = model.fields[column];
23
+ if (field === undefined) {
24
+ return yield* Effect.die(new Error(`Unknown ID column: ${column}`));
25
+ }
26
+ entries.push([column, field]);
27
+ }
28
+ const encodeKey = Schema.encodeEffect(Schema.Struct(Object.fromEntries(entries)));
29
29
  const versionSchema = model.fields[versionColumn];
30
30
  if (versionSchema === undefined) {
31
- throw new Error(`Unknown version column: ${versionColumn}`);
31
+ return yield* Effect.die(new Error(`Unknown version column: ${versionColumn}`));
32
32
  }
33
33
  const encodeVersion = Schema.encodeEffect(versionSchema);
34
34
  const encodeInsert = Schema.encodeEffect(model.insert);
@@ -36,7 +36,7 @@ export function versionedStatements(model: Model.Any, sql: SqlClient.SqlClient,
36
36
  const record = Schema.decodeUnknownEffect(Schema.Record(Schema.String, Schema.Unknown));
37
37
  const validateVersion = Schema.decodeUnknownEffect(Version);
38
38
  const clause =
39
- options.conflictColumns === undefined ? undefined : makeConflictClause(model, sql, { ...options, conflictColumns: options.conflictColumns });
39
+ options.conflictColumns === undefined ? undefined : yield* makeConflictClause(model, { ...options, conflictColumns: options.conflictColumns });
40
40
  function key(input: unknown) {
41
41
  return idColumns.length === 1 ? { [idColumns[0] ?? ""]: input } : input;
42
42
  }
@@ -86,4 +86,4 @@ export function versionedStatements(model: Model.Any, sql: SqlClient.SqlClient,
86
86
  });
87
87
  }
88
88
  return { insert, remove, update };
89
- }
89
+ });
package/dist/testing.d.ts DELETED
@@ -1,27 +0,0 @@
1
- import { Effect, Schema } from "effect";
2
- import { SqlClient } from "effect/sql";
3
- import { type BucketKey, type ItemKey } from "@shivaedev/changes/keys.ts";
4
- declare const UnannouncedWrites_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
5
- readonly _tag: "UnannouncedWrites";
6
- } & Readonly<A>;
7
- export declare class UnannouncedWrites extends UnannouncedWrites_base<{
8
- readonly tables: readonly string[];
9
- }> {
10
- }
11
- export interface ChangeCoverage {
12
- readonly covers: Readonly<Record<string, readonly (ItemKey | BucketKey)[]>>;
13
- }
14
- export declare const withChangeCoverage: <A, E, R>(body: Effect.Effect<A, E, R>, options?: ChangeCoverage | undefined) => Effect.Effect<A, Schema.SchemaError | import("effect/sql/SqlError").SqlError | UnannouncedWrites | Exclude<E, import("effect/sql/SqlError").SqlError>, import("effect/reactivity/Reactivity").Reactivity | SqlClient.SqlClient | Exclude<Exclude<R, never>, never>>;
15
- declare const PublishedKeysMismatch_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
16
- readonly _tag: "PublishedKeysMismatch";
17
- } & Readonly<A>;
18
- export declare class PublishedKeysMismatch extends PublishedKeysMismatch_base<{
19
- readonly expected: readonly unknown[];
20
- readonly actual: readonly unknown[];
21
- }> {
22
- get message(): string;
23
- }
24
- export declare function expectPublished(keys: readonly unknown[]): <A, E, R>(body: Effect.Effect<A, E, R>) => Effect.Effect<A, E | PublishedKeysMismatch, R>;
25
- export declare function expectPublished<A>(keys: (result: A) => readonly unknown[]): <E, R>(body: Effect.Effect<A, E, R>) => Effect.Effect<A, E | PublishedKeysMismatch, R>;
26
- export {};
27
- //# sourceMappingURL=testing.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,EAAE,KAAK,SAAS,EAAE,KAAK,OAAO,EAAoB,MAAM,4BAA4B,CAAC;;;;AAI5F,qBAAa,iBAAkB,SAAQ,uBAAsC;IAAE,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,CAAC;CAAG;AAEvH,MAAM,WAAW,cAAc;IAC9B,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,OAAO,GAAG,SAAS,CAAC,EAAE,CAAC,CAAC,CAAC;CAC5E;AAED,eAAO,MAAM,kBAAkB,GAAsD,CAAC,EAAE,CAAC,EAAE,CAAC,4UAuD1F,CAAC;;;;AAEH,qBAAa,qBAAsB,SAAQ,2BAA0C;IACpF,QAAQ,CAAC,QAAQ,EAAE,SAAS,OAAO,EAAE,CAAC;IACtC,QAAQ,CAAC,MAAM,EAAE,SAAS,OAAO,EAAE,CAAC;CACpC,CAAC;IACD,IAAa,OAAO,WAEnB;CACD;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,SAAS,OAAO,EAAE,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,KAAK,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,GAAG,qBAAqB,EAAE,CAAC,CAAC,CAAC;AACrJ,wBAAgB,eAAe,CAAC,CAAC,EAChC,IAAI,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,SAAS,OAAO,EAAE,GACrC,CAAC,CAAC,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,KAAK,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,GAAG,qBAAqB,EAAE,CAAC,CAAC,CAAC"}
package/dist/testing.js DELETED
@@ -1,75 +0,0 @@
1
- import { Data, Effect, Schema } from "effect";
2
- import { SqlClient } from "effect/sql";
3
- import { invalidationKeys } from "@shivaedev/changes/keys.ts";
4
- import { ChangeDeclarations } from "#ChangeDeclarations.ts";
5
- import { ChangeObserver, transact } from "#transact.ts";
6
- export class UnannouncedWrites extends Data.TaggedError("UnannouncedWrites") {
7
- }
8
- export const withChangeCoverage = Effect.fn("Changes.withChangeCoverage")(function* (body, options) {
9
- const observer = yield* ChangeObserver;
10
- const sql = yield* SqlClient.SqlClient;
11
- const named = new Set();
12
- const declarations = new Map();
13
- const outer = yield* ChangeDeclarations;
14
- const written = sql `select relid, relname, (n_tup_ins + n_tup_upd + n_tup_del)::int as writes from pg_stat_xact_user_tables`.pipe(Effect.flatMap(Schema.decodeUnknownEffect(Schema.Array(Schema.Struct({ relid: Schema.Int, relname: Schema.String, writes: Schema.Int })))));
15
- const checked = Effect.gen(function* () {
16
- const before = new Map((yield* written).map((row) => [row.relid, row.writes]));
17
- const result = yield* body;
18
- const missing = (yield* written).filter((row) => {
19
- const count = row.writes - (before.get(row.relid) ?? 0);
20
- if (count <= 0) {
21
- return false;
22
- }
23
- const automatic = declarations.get(row.relname);
24
- const explicit = options?.covers[row.relname] ?? [];
25
- const expected = [...(automatic?.keys ?? []), ...invalidationKeys(explicit)];
26
- return (count > (automatic?.rows ?? 0) && explicit.length === 0) || expected.some((key) => !named.has(key));
27
- });
28
- if (missing.length > 0) {
29
- return yield* Effect.fail(new UnannouncedWrites({ tables: missing.map((row) => row.relname).sort() }));
30
- }
31
- return result;
32
- });
33
- return yield* checked.pipe(transact({ onSqlError: (error) => error }), Effect.provideService(ChangeDeclarations, {
34
- declare: (table, keys, rows) => Effect.andThen(Effect.sync(() => {
35
- const entry = declarations.get(table) ?? { keys: new Set(), rows: 0 };
36
- for (const key of keys) {
37
- entry.keys.add(key);
38
- }
39
- entry.rows += rows;
40
- declarations.set(table, entry);
41
- }), outer.declare(table, keys, rows)),
42
- }), Effect.provideService(ChangeObserver, (event) => {
43
- if (event._tag === "Recorded") {
44
- for (const key of event.changes) {
45
- named.add(key);
46
- }
47
- }
48
- return observer(event);
49
- }));
50
- });
51
- export class PublishedKeysMismatch extends Data.TaggedError("PublishedKeysMismatch") {
52
- get message() {
53
- return `Expected published keys ${JSON.stringify(this.expected)}; actual published keys ${JSON.stringify(this.actual)}`;
54
- }
55
- }
56
- export function expectPublished(keys) {
57
- return Effect.fn("Changes.expectPublished")(function* (body) {
58
- const observer = yield* ChangeObserver;
59
- const actual = new Set();
60
- const result = yield* body.pipe(Effect.provideService(ChangeObserver, (event) => {
61
- if (event._tag === "Published") {
62
- for (const key of event.changes) {
63
- actual.add(key);
64
- }
65
- }
66
- return observer(event);
67
- }));
68
- const expected = new Set(typeof keys === "function" ? keys(result) : keys);
69
- if (actual.size !== expected.size || [...expected].some((key) => !actual.has(key))) {
70
- return yield* Effect.fail(new PublishedKeysMismatch({ actual: [...actual], expected: [...expected] }));
71
- }
72
- return result;
73
- });
74
- }
75
- //# sourceMappingURL=testing.js.map