@shivaedev/sql 0.0.0 → 0.22.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.
- package/CHANGELOG.md +275 -0
- package/LICENSE +21 -0
- package/README.md +613 -1
- package/dist/SchemaComponent.d.ts +9 -0
- package/dist/SchemaComponent.d.ts.map +1 -0
- package/dist/SchemaComponent.js +6 -0
- package/dist/SchemaComponent.js.map +1 -0
- package/dist/WorkerRelease.d.ts +14 -0
- package/dist/WorkerRelease.d.ts.map +1 -0
- package/dist/WorkerRelease.js +30 -0
- package/dist/WorkerRelease.js.map +1 -0
- package/dist/advisoryXactLock.d.ts +10 -0
- package/dist/advisoryXactLock.d.ts.map +1 -0
- package/dist/advisoryXactLock.js +18 -0
- package/dist/advisoryXactLock.js.map +1 -0
- package/dist/aggregateColumn.d.ts +8 -0
- package/dist/aggregateColumn.d.ts.map +1 -0
- package/dist/aggregateColumn.js +44 -0
- package/dist/aggregateColumn.js.map +1 -0
- package/dist/aggregateQuery.d.ts +51 -0
- package/dist/aggregateQuery.d.ts.map +1 -0
- package/dist/aggregateQuery.js +2 -0
- package/dist/aggregateQuery.js.map +1 -0
- package/dist/assertPostgresMigrations.d.ts +7 -0
- package/dist/assertPostgresMigrations.d.ts.map +1 -0
- package/dist/assertPostgresMigrations.js +20 -0
- package/dist/assertPostgresMigrations.js.map +1 -0
- package/dist/batchWrites.d.ts +16 -0
- package/dist/batchWrites.d.ts.map +1 -0
- package/dist/batchWrites.js +49 -0
- package/dist/batchWrites.js.map +1 -0
- package/dist/clientOptions.d.ts +8 -0
- package/dist/clientOptions.d.ts.map +1 -0
- package/dist/clientOptions.js +8 -0
- package/dist/clientOptions.js.map +1 -0
- package/dist/compositeRepository.d.ts +41 -0
- package/dist/compositeRepository.d.ts.map +1 -0
- package/dist/compositeRepository.js +38 -0
- package/dist/compositeRepository.js.map +1 -0
- package/dist/conditionalUpsert.d.ts +59 -0
- package/dist/conditionalUpsert.d.ts.map +1 -0
- package/dist/conditionalUpsert.js +28 -0
- package/dist/conditionalUpsert.js.map +1 -0
- package/dist/conflictWrites.d.ts +45 -0
- package/dist/conflictWrites.d.ts.map +1 -0
- package/dist/conflictWrites.js +40 -0
- package/dist/conflictWrites.js.map +1 -0
- package/dist/coordinateWorkerRelease.d.ts +5 -0
- package/dist/coordinateWorkerRelease.d.ts.map +1 -0
- package/dist/coordinateWorkerRelease.js +30 -0
- package/dist/coordinateWorkerRelease.js.map +1 -0
- package/dist/insertFromSelect.d.ts +24 -0
- package/dist/insertFromSelect.d.ts.map +1 -0
- package/dist/insertFromSelect.js +15 -0
- package/dist/insertFromSelect.js.map +1 -0
- package/dist/latestPerGroup.d.ts +11 -0
- package/dist/latestPerGroup.d.ts.map +1 -0
- package/dist/latestPerGroup.js +51 -0
- package/dist/latestPerGroup.js.map +1 -0
- package/dist/listenForChanges.d.ts +5 -0
- package/dist/listenForChanges.d.ts.map +1 -0
- package/dist/listenForChanges.js +75 -0
- package/dist/listenForChanges.js.map +1 -0
- package/dist/makeAggregate.d.ts +8 -0
- package/dist/makeAggregate.d.ts.map +1 -0
- package/dist/makeAggregate.js +38 -0
- package/dist/makeAggregate.js.map +1 -0
- package/dist/makeConflictClause.d.ts +10 -0
- package/dist/makeConflictClause.d.ts.map +1 -0
- package/dist/makeConflictClause.js +41 -0
- package/dist/makeConflictClause.js.map +1 -0
- package/dist/makeQueryClauses.d.ts +14 -0
- package/dist/makeQueryClauses.d.ts.map +1 -0
- package/dist/makeQueryClauses.js +20 -0
- package/dist/makeQueryClauses.js.map +1 -0
- package/dist/migrationConvention.d.ts +3 -0
- package/dist/migrationConvention.d.ts.map +1 -0
- package/dist/migrationConvention.js +3 -0
- package/dist/migrationConvention.js.map +1 -0
- package/dist/migrations.d.ts +12 -0
- package/dist/migrations.d.ts.map +1 -0
- package/dist/migrations.js +41 -0
- package/dist/migrations.js.map +1 -0
- package/dist/migrationsFromDirectory.d.ts +4 -0
- package/dist/migrationsFromDirectory.d.ts.map +1 -0
- package/dist/migrationsFromDirectory.js +7 -0
- package/dist/migrationsFromDirectory.js.map +1 -0
- package/dist/notifyPayload.d.ts +7 -0
- package/dist/notifyPayload.d.ts.map +1 -0
- package/dist/notifyPayload.js +31 -0
- package/dist/notifyPayload.js.map +1 -0
- package/dist/pendingPostgresMigrations.d.ts +24 -0
- package/dist/pendingPostgresMigrations.d.ts.map +1 -0
- package/dist/pendingPostgresMigrations.js +30 -0
- package/dist/pendingPostgresMigrations.js.map +1 -0
- package/dist/pinnedGroups.d.ts +22 -0
- package/dist/pinnedGroups.d.ts.map +1 -0
- package/dist/pinnedGroups.js +35 -0
- package/dist/pinnedGroups.js.map +1 -0
- package/dist/postgresChanges.d.ts +20 -0
- package/dist/postgresChanges.d.ts.map +1 -0
- package/dist/postgresChanges.js +26 -0
- package/dist/postgresChanges.js.map +1 -0
- package/dist/postgresRelease.d.ts +9 -0
- package/dist/postgresRelease.d.ts.map +1 -0
- package/dist/postgresRelease.js +39 -0
- package/dist/postgresRelease.js.map +1 -0
- package/dist/releaseWorkers.d.ts +8 -0
- package/dist/releaseWorkers.d.ts.map +1 -0
- package/dist/releaseWorkers.js +26 -0
- package/dist/releaseWorkers.js.map +1 -0
- package/dist/repository.d.ts +66 -0
- package/dist/repository.d.ts.map +1 -0
- package/dist/repository.js +43 -0
- package/dist/repository.js.map +1 -0
- package/dist/repositoryChanges.d.ts +9 -0
- package/dist/repositoryChanges.d.ts.map +1 -0
- package/dist/repositoryChanges.js +32 -0
- package/dist/repositoryChanges.js.map +1 -0
- package/dist/repositoryFilter.d.ts +43 -0
- package/dist/repositoryFilter.d.ts.map +1 -0
- package/dist/repositoryFilter.js +69 -0
- package/dist/repositoryFilter.js.map +1 -0
- package/dist/repositoryQueries.d.ts +26 -0
- package/dist/repositoryQueries.d.ts.map +1 -0
- package/dist/repositoryQueries.js +49 -0
- package/dist/repositoryQueries.js.map +1 -0
- package/dist/repositoryWrites.d.ts +18 -0
- package/dist/repositoryWrites.d.ts.map +1 -0
- package/dist/repositoryWrites.js +49 -0
- package/dist/repositoryWrites.js.map +1 -0
- package/dist/testing.d.ts +12 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +29 -0
- package/dist/testing.js.map +1 -0
- package/dist/transact.d.ts +18 -0
- package/dist/transact.d.ts.map +1 -0
- package/dist/transact.js +43 -0
- package/dist/transact.js.map +1 -0
- package/dist/tupleIn.d.ts +19 -0
- package/dist/tupleIn.d.ts.map +1 -0
- package/dist/tupleIn.js +20 -0
- package/dist/tupleIn.js.map +1 -0
- package/dist/versionedRepository.d.ts +37 -0
- package/dist/versionedRepository.d.ts.map +1 -0
- package/dist/versionedRepository.js +63 -0
- package/dist/versionedRepository.js.map +1 -0
- package/dist/versionedStatements.d.ts +25 -0
- package/dist/versionedStatements.d.ts.map +1 -0
- package/dist/versionedStatements.js +75 -0
- package/dist/versionedStatements.js.map +1 -0
- package/package.json +71 -4
- package/src/SchemaComponent.ts +12 -0
- package/src/WorkerRelease.ts +38 -0
- package/src/advisoryXactLock.ts +17 -0
- package/src/aggregateColumn.ts +49 -0
- package/src/aggregateQuery.ts +57 -0
- package/src/assertPostgresMigrations.ts +24 -0
- package/src/batchWrites.ts +69 -0
- package/src/clientOptions.ts +16 -0
- package/src/compositeRepository.ts +101 -0
- package/src/conditionalUpsert.ts +97 -0
- package/src/conflictWrites.ts +93 -0
- package/src/coordinateWorkerRelease.ts +33 -0
- package/src/insertFromSelect.ts +47 -0
- package/src/latestPerGroup.ts +58 -0
- package/src/listenForChanges.ts +97 -0
- package/src/makeAggregate.ts +64 -0
- package/src/makeConflictClause.ts +53 -0
- package/src/makeQueryClauses.ts +36 -0
- package/src/migrationConvention.ts +2 -0
- package/src/migrations.ts +58 -0
- package/src/migrationsFromDirectory.ts +7 -0
- package/src/notifyPayload.ts +34 -0
- package/src/pendingPostgresMigrations.ts +42 -0
- package/src/pinnedGroups.ts +46 -0
- package/src/postgresChanges.ts +48 -0
- package/src/postgresRelease.ts +41 -0
- package/src/releaseWorkers.ts +32 -0
- package/src/repository.ts +147 -0
- package/src/repositoryChanges.ts +49 -0
- package/src/repositoryFilter.ts +108 -0
- package/src/repositoryQueries.ts +107 -0
- package/src/repositoryWrites.ts +70 -0
- package/src/testing.ts +38 -0
- package/src/transact.ts +70 -0
- package/src/tupleIn.ts +44 -0
- package/src/versionedRepository.ts +108 -0
- package/src/versionedStatements.ts +90 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Effect, Option, Schema } from "effect";
|
|
2
|
+
import { versionedStatements } from "#versionedStatements.ts";
|
|
3
|
+
export function makeVersionedWrites(model, sql, options) {
|
|
4
|
+
const { spanPrefix, changes } = options;
|
|
5
|
+
const statements = versionedStatements(model, sql, options);
|
|
6
|
+
const decodeRow = Schema.decodeUnknownEffect(model);
|
|
7
|
+
function insert(input) {
|
|
8
|
+
return Effect.gen(function* () {
|
|
9
|
+
const statement = yield* statements.insert(input);
|
|
10
|
+
const rows = yield* sql `${statement} returning *`;
|
|
11
|
+
yield* changes.record(rows);
|
|
12
|
+
return yield* decodeRow(rows[0]);
|
|
13
|
+
}).pipe(changes.write, Effect.withSpan(`${spanPrefix}.insert`));
|
|
14
|
+
}
|
|
15
|
+
function insertVoid(input) {
|
|
16
|
+
return Effect.gen(function* () {
|
|
17
|
+
const statement = yield* statements.insert(input);
|
|
18
|
+
const rows = yield* sql `${statement} returning ${statements.returningKeys}`;
|
|
19
|
+
yield* changes.record(rows);
|
|
20
|
+
}).pipe(changes.write, Effect.withSpan(`${spanPrefix}.insertVoid`));
|
|
21
|
+
}
|
|
22
|
+
function insertOrIgnore(input) {
|
|
23
|
+
return Effect.gen(function* () {
|
|
24
|
+
const statement = yield* statements.insert(input, true);
|
|
25
|
+
const rows = yield* sql `${statement} returning *`;
|
|
26
|
+
yield* changes.record(rows);
|
|
27
|
+
return rows.length === 0 ? Option.none() : Option.some(yield* decodeRow(rows[0]));
|
|
28
|
+
}).pipe(changes.write, Effect.withSpan(`${spanPrefix}.insertOrIgnore`));
|
|
29
|
+
}
|
|
30
|
+
function update(input) {
|
|
31
|
+
return Effect.gen(function* () {
|
|
32
|
+
const { statement, classify } = yield* statements.update(input);
|
|
33
|
+
const rows = yield* sql `${statement} returning *`;
|
|
34
|
+
if (rows.length === 0) {
|
|
35
|
+
return yield* classify();
|
|
36
|
+
}
|
|
37
|
+
yield* changes.record(rows);
|
|
38
|
+
return yield* decodeRow(rows[0]);
|
|
39
|
+
}).pipe(changes.write, Effect.withSpan(`${spanPrefix}.update`));
|
|
40
|
+
}
|
|
41
|
+
function updateVoid(input) {
|
|
42
|
+
return Effect.gen(function* () {
|
|
43
|
+
const { statement, classify } = yield* statements.update(input);
|
|
44
|
+
const rows = yield* sql `${statement} returning ${statements.returningKeys}`;
|
|
45
|
+
if (rows.length === 0) {
|
|
46
|
+
return yield* classify();
|
|
47
|
+
}
|
|
48
|
+
yield* changes.record(rows);
|
|
49
|
+
}).pipe(changes.write, Effect.withSpan(`${spanPrefix}.updateVoid`));
|
|
50
|
+
}
|
|
51
|
+
function deleteRow(id, expected) {
|
|
52
|
+
return Effect.gen(function* () {
|
|
53
|
+
const { statement, classify } = yield* statements.remove(id, expected);
|
|
54
|
+
const rows = yield* sql `${statement} returning ${statements.returningKeys}`;
|
|
55
|
+
if (rows.length === 0) {
|
|
56
|
+
return yield* classify();
|
|
57
|
+
}
|
|
58
|
+
yield* changes.record(rows);
|
|
59
|
+
}).pipe(changes.write, Effect.withSpan(`${spanPrefix}.delete`));
|
|
60
|
+
}
|
|
61
|
+
return { delete: deleteRow, insert, insertVoid, update, updateVoid, ...(options.conflictColumns === undefined ? {} : { insertOrIgnore }) };
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=versionedRepository.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"versionedRepository.js","sourceRoot":"","sources":["../src/versionedRepository.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
import type { Model } from "effect/schema";
|
|
3
|
+
import type { SqlClient } from "effect/sql";
|
|
4
|
+
import type { OptimisticConcurrencyError } from "#repository.ts";
|
|
5
|
+
export interface VersionedOptions {
|
|
6
|
+
readonly conflict: (id: unknown, expectedVersion: number, reason: "missing" | "stale") => OptimisticConcurrencyError;
|
|
7
|
+
readonly conflictColumns?: readonly string[];
|
|
8
|
+
readonly conflictWhere?: Readonly<Record<string, unknown>>;
|
|
9
|
+
readonly idColumns: readonly string[];
|
|
10
|
+
readonly tableName: string;
|
|
11
|
+
readonly versionColumn: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function versionedStatements(model: Model.Any, sql: SqlClient.SqlClient, options: VersionedOptions): {
|
|
14
|
+
insert: (input: unknown, ignore?: boolean) => Effect.Effect<import("effect/sql/Statement").Statement<import("effect/sql/SqlConnection").Row>, Schema.SchemaError, unknown>;
|
|
15
|
+
remove: (id: unknown, expected: number) => Effect.Effect<{
|
|
16
|
+
classify: () => Effect.Effect<never, OptimisticConcurrencyError | import("effect/sql/SqlError").SqlError, never>;
|
|
17
|
+
statement: import("effect/sql/Statement").Statement<import("effect/sql/SqlConnection").Row>;
|
|
18
|
+
}, Schema.SchemaError, unknown>;
|
|
19
|
+
returningKeys: import("effect/sql/Statement").Fragment;
|
|
20
|
+
update: (input: Readonly<Record<string, unknown>>) => Effect.Effect<{
|
|
21
|
+
classify: () => Effect.Effect<never, OptimisticConcurrencyError | import("effect/sql/SqlError").SqlError, never>;
|
|
22
|
+
statement: import("effect/sql/Statement").Statement<import("effect/sql/SqlConnection").Row>;
|
|
23
|
+
}, Schema.SchemaError, unknown>;
|
|
24
|
+
};
|
|
25
|
+
//# sourceMappingURL=versionedStatements.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"versionedStatements.d.ts","sourceRoot":"","sources":["../src/versionedStatements.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACxC,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,gBAAgB,CAAC;AAGjE,MAAM,WAAW,gBAAgB;IAChC,QAAQ,CAAC,QAAQ,EAAE,CAAC,EAAE,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,GAAG,OAAO,KAAK,0BAA0B,CAAC;IACrH,QAAQ,CAAC,eAAe,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC7C,QAAQ,CAAC,aAAa,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAC3D,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAC/B;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,EAAE,GAAG,EAAE,SAAS,CAAC,SAAS,EAAE,OAAO,EAAE,gBAAgB;oBAuCjF,OAAO;iBAsBV,OAAO,YAAY,MAAM;;;;;oBAftB,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;;;;EA2BxD"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
import { makeConflictClause } from "#makeConflictClause.ts";
|
|
3
|
+
const Version = Schema.Number.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0));
|
|
4
|
+
export function versionedStatements(model, sql, options) {
|
|
5
|
+
const { tableName, idColumns, versionColumn, conflict } = options;
|
|
6
|
+
const fields = Object.fromEntries(idColumns.map((column) => {
|
|
7
|
+
const field = model.fields[column];
|
|
8
|
+
if (field === undefined) {
|
|
9
|
+
throw new Error(`Unknown ID column: ${column}`);
|
|
10
|
+
}
|
|
11
|
+
return [column, field];
|
|
12
|
+
}));
|
|
13
|
+
const encodeKey = Schema.encodeEffect(Schema.Struct(fields));
|
|
14
|
+
const versionSchema = model.fields[versionColumn];
|
|
15
|
+
if (versionSchema === undefined) {
|
|
16
|
+
throw new Error(`Unknown version column: ${versionColumn}`);
|
|
17
|
+
}
|
|
18
|
+
const encodeVersion = Schema.encodeEffect(versionSchema);
|
|
19
|
+
const encodeInsert = Schema.encodeEffect(model.insert);
|
|
20
|
+
const encodeUpdate = Schema.encodeEffect(model.update);
|
|
21
|
+
const record = Schema.decodeUnknownEffect(Schema.Record(Schema.String, Schema.Unknown));
|
|
22
|
+
const validateVersion = Schema.decodeUnknownEffect(Version);
|
|
23
|
+
const clause = options.conflictColumns === undefined ? undefined : makeConflictClause(model, sql, { ...options, conflictColumns: options.conflictColumns });
|
|
24
|
+
const returningKeys = sql.csv(idColumns.map((column) => sql `${sql(column)}`));
|
|
25
|
+
function key(input) {
|
|
26
|
+
return idColumns.length === 1 ? { [idColumns[0] ?? ""]: input } : input;
|
|
27
|
+
}
|
|
28
|
+
function inputKey(input) {
|
|
29
|
+
return idColumns.length === 1 ? input[idColumns[0] ?? ""] : Object.fromEntries(idColumns.map((column) => [column, input[column]]));
|
|
30
|
+
}
|
|
31
|
+
function predicate(encoded) {
|
|
32
|
+
return sql.and(idColumns.map((column) => sql `${sql(column)} = ${encoded[column]}`));
|
|
33
|
+
}
|
|
34
|
+
function classify(id, encoded, expected) {
|
|
35
|
+
return Effect.gen(function* () {
|
|
36
|
+
const rows = yield* sql `select 1 from ${sql(tableName)} where ${predicate(encoded)}`;
|
|
37
|
+
return yield* Effect.fail(conflict(id, expected, rows.length === 0 ? "missing" : "stale"));
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
function insert(input, ignore = false) {
|
|
41
|
+
return Effect.gen(function* () {
|
|
42
|
+
const encoded = { ...(yield* record(yield* encodeInsert(input))), [versionColumn]: 0 };
|
|
43
|
+
const suffix = ignore && clause !== undefined ? yield* clause(encoded) : sql ``;
|
|
44
|
+
return sql `insert into ${sql(tableName)} ${sql.insert(encoded)} ${suffix}`;
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
function update(input) {
|
|
48
|
+
return Effect.gen(function* () {
|
|
49
|
+
const encoded = yield* record(yield* encodeUpdate(input));
|
|
50
|
+
const expectedVersion = yield* validateVersion(encoded[versionColumn]);
|
|
51
|
+
const assignments = Object.entries(encoded).filter(([column]) => !idColumns.includes(column) && column !== versionColumn);
|
|
52
|
+
const set = sql.csv([
|
|
53
|
+
...assignments.map(([column, value]) => sql `${sql(column)} = ${value}`),
|
|
54
|
+
sql `${sql(versionColumn)} = ${sql(versionColumn)} + 1`,
|
|
55
|
+
]);
|
|
56
|
+
return {
|
|
57
|
+
classify: () => classify(inputKey(input), encoded, expectedVersion),
|
|
58
|
+
statement: sql `update ${sql(tableName)} set ${set} where ${predicate(encoded)} and ${sql(versionColumn)} = ${expectedVersion}`,
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function remove(id, expected) {
|
|
63
|
+
return Effect.gen(function* () {
|
|
64
|
+
const expectedVersion = yield* validateVersion(yield* encodeVersion(expected));
|
|
65
|
+
const yieldedKey = yield* record(key(id));
|
|
66
|
+
const encoded = yield* record(yield* encodeKey(yieldedKey));
|
|
67
|
+
return {
|
|
68
|
+
classify: () => classify(inputKey(yieldedKey), encoded, expectedVersion),
|
|
69
|
+
statement: sql `delete from ${sql(tableName)} where ${predicate(encoded)} and ${sql(versionColumn)} = ${expectedVersion}`,
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return { insert, remove, returningKeys, update };
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=versionedStatements.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"versionedStatements.js","sourceRoot":"","sources":["../src/versionedStatements.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAGxC,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAG5D,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,EAAE,EAAE,MAAM,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC,CAAC;AAUtF,MAAM,UAAU,mBAAmB,CAAC,KAAgB,EAAE,GAAwB,EAAE,OAAyB;IACxG,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,aAAa,EAAE,QAAQ,EAAE,GAAG,OAAO,CAAC;IAClE,MAAM,MAAM,GAAG,MAAM,CAAC,WAAW,CAChC,SAAS,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE;QACxB,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QACnC,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACzB,MAAM,IAAI,KAAK,CAAC,sBAAsB,MAAM,EAAE,CAAC,CAAC;QACjD,CAAC;QACD,OAAO,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IACxB,CAAC,CAAC,CACF,CAAC;IACF,MAAM,SAAS,GAAG,MAAM,CAAC,YAAY,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;IAC7D,MAAM,aAAa,GAAG,KAAK,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;IAClD,IAAI,aAAa,KAAK,SAAS,EAAE,CAAC;QACjC,MAAM,IAAI,KAAK,CAAC,2BAA2B,aAAa,EAAE,CAAC,CAAC;IAC7D,CAAC;IACD,MAAM,aAAa,GAAG,MAAM,CAAC,YAAY,CAAC,aAAa,CAAC,CAAC;IACzD,MAAM,YAAY,GAAG,MAAM,CAAC,YAAY,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IACvD,MAAM,YAAY,GAAG,MAAM,CAAC,YAAY,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IACvD,MAAM,MAAM,GAAG,MAAM,CAAC,mBAAmB,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;IACxF,MAAM,eAAe,GAAG,MAAM,CAAC,mBAAmB,CAAC,OAAO,CAAC,CAAC;IAC5D,MAAM,MAAM,GACX,OAAO,CAAC,eAAe,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,kBAAkB,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE,GAAG,OAAO,EAAE,eAAe,EAAE,OAAO,CAAC,eAAe,EAAE,CAAC,CAAC;IAC9I,MAAM,aAAa,GAAG,GAAG,CAAC,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,GAAG,CAAA,GAAG,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC;IAC9E,SAAS,GAAG,CAAC,KAAc;QAC1B,OAAO,SAAS,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC;IACzE,CAAC;IACD,SAAS,QAAQ,CAAC,KAAwC;QACzD,OAAO,SAAS,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;IACpI,CAAC;IACD,SAAS,SAAS,CAAC,OAA0C;QAC5D,OAAO,GAAG,CAAC,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,GAAG,CAAA,GAAG,GAAG,CAAC,MAAM,CAAC,MAAM,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC;IACrF,CAAC;IACD,SAAS,QAAQ,CAAC,EAAW,EAAE,OAA0C,EAAE,QAAgB;QAC1F,OAAO,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;YAC1B,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,GAAG,CAAA,iBAAiB,GAAG,CAAC,SAAS,CAAC,UAAU,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC;YACrF,OAAO,KAAK,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;QAC5F,CAAC,CAAC,CAAC;IACJ,CAAC;IACD,SAAS,MAAM,CAAC,KAAc,EAAE,MAAM,GAAG,KAAK;QAC7C,OAAO,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;YAC1B,MAAM,OAAO,GAAG,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,aAAa,CAAC,EAAE,CAAC,EAAE,CAAC;YACvF,MAAM,MAAM,GAAG,MAAM,IAAI,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,CAAA,EAAE,CAAC;YAC/E,OAAO,GAAG,CAAA,eAAe,GAAG,CAAC,SAAS,CAAC,IAAI,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,MAAM,EAAE,CAAC;QAC5E,CAAC,CAAC,CAAC;IACJ,CAAC;IACD,SAAS,MAAM,CAAC,KAAwC;QACvD,OAAO,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;YAC1B,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC;YAC1D,MAAM,eAAe,GAAG,KAAK,CAAC,CAAC,eAAe,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC;YACvE,MAAM,WAAW,GAAG,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC,SAAS,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,MAAM,KAAK,aAAa,CAAC,CAAC;YAC1H,MAAM,GAAG,GAAG,GAAG,CAAC,GAAG,CAAC;gBACnB,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,GAAG,CAAA,GAAG,GAAG,CAAC,MAAM,CAAC,MAAM,KAAK,EAAE,CAAC;gBACvE,GAAG,CAAA,GAAG,GAAG,CAAC,aAAa,CAAC,MAAM,GAAG,CAAC,aAAa,CAAC,MAAM;aACtD,CAAC,CAAC;YACH,OAAO;gBACN,QAAQ,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,EAAE,eAAe,CAAC;gBACnE,SAAS,EAAE,GAAG,CAAA,UAAU,GAAG,CAAC,SAAS,CAAC,QAAQ,GAAG,UAAU,SAAS,CAAC,OAAO,CAAC,QAAQ,GAAG,CAAC,aAAa,CAAC,MAAM,eAAe,EAAE;aAC9H,CAAC;QACH,CAAC,CAAC,CAAC;IACJ,CAAC;IACD,SAAS,MAAM,CAAC,EAAW,EAAE,QAAgB;QAC5C,OAAO,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;YAC1B,MAAM,eAAe,GAAG,KAAK,CAAC,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC,CAAC;YAC/E,MAAM,UAAU,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;YAC1C,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC,CAAC;YAC5D,OAAO;gBACN,QAAQ,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,EAAE,eAAe,CAAC;gBACxE,SAAS,EAAE,GAAG,CAAA,eAAe,GAAG,CAAC,SAAS,CAAC,UAAU,SAAS,CAAC,OAAO,CAAC,QAAQ,GAAG,CAAC,aAAa,CAAC,MAAM,eAAe,EAAE;aACxH,CAAC;QACH,CAAC,CAAC,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,CAAC;AAClD,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,10 +1,77 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shivaedev/sql",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
5
|
-
"
|
|
3
|
+
"version": "0.22.0",
|
|
4
|
+
"description": "Schema-derived repositories over native Effect SQL",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"effect"
|
|
7
|
+
],
|
|
8
|
+
"homepage": "https://github.com/ShivaeDev/platform/tree/main/packages/sql#readme",
|
|
9
|
+
"bugs": {
|
|
10
|
+
"url": "https://github.com/ShivaeDev/platform/issues"
|
|
11
|
+
},
|
|
6
12
|
"repository": {
|
|
7
13
|
"type": "git",
|
|
8
|
-
"url": "git+https://github.com/ShivaeDev/platform.git"
|
|
14
|
+
"url": "git+https://github.com/ShivaeDev/platform.git",
|
|
15
|
+
"directory": "packages/sql"
|
|
16
|
+
},
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"sideEffects": false,
|
|
19
|
+
"type": "module",
|
|
20
|
+
"imports": {
|
|
21
|
+
"#test/*": "./src/test-support/*",
|
|
22
|
+
"#*.ts": {
|
|
23
|
+
"source": "./src/*.ts",
|
|
24
|
+
"types": "./dist/*.d.ts",
|
|
25
|
+
"default": "./dist/*.js"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"exports": {
|
|
29
|
+
"./*.ts": {
|
|
30
|
+
"source": "./src/*.ts",
|
|
31
|
+
"types": "./dist/*.d.ts",
|
|
32
|
+
"default": "./dist/*.js"
|
|
33
|
+
},
|
|
34
|
+
"./package.json": "./package.json"
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist",
|
|
38
|
+
"src",
|
|
39
|
+
"!src/**/*.test.ts",
|
|
40
|
+
"!src/**/*.spec.ts",
|
|
41
|
+
"!src/**/test-support",
|
|
42
|
+
"CHANGELOG.md",
|
|
43
|
+
"README.md"
|
|
44
|
+
],
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@shivaedev/changes": "0.4.0"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@effect/platform-node": "4.0.5",
|
|
50
|
+
"@effect/sql-pg": "4.0.5",
|
|
51
|
+
"@effect/sql-sqlite-node": "4.0.5",
|
|
52
|
+
"@shivaedev/quality": "0.42.0",
|
|
53
|
+
"@shivaedev/service": "0.5.0",
|
|
54
|
+
"@shivaedev/test": "0.10.0",
|
|
55
|
+
"@shivaedev/test-story": "0.21.0",
|
|
56
|
+
"@types/node": "24.19.2",
|
|
57
|
+
"effect": "4.0.5",
|
|
58
|
+
"typescript": "7.0.2",
|
|
59
|
+
"vitest": "5.0.5"
|
|
60
|
+
},
|
|
61
|
+
"peerDependencies": {
|
|
62
|
+
"effect": "4.0.5"
|
|
63
|
+
},
|
|
64
|
+
"engines": {
|
|
65
|
+
"node": ">=24"
|
|
66
|
+
},
|
|
67
|
+
"publishConfig": {
|
|
68
|
+
"access": "public",
|
|
69
|
+
"provenance": true
|
|
70
|
+
},
|
|
71
|
+
"scripts": {
|
|
72
|
+
"build": "node --eval \"import('node:fs').then(({ rmSync }) => rmSync('dist', { force: true, recursive: true }))\" && tsc --project tsconfig.build.json",
|
|
73
|
+
"ready": "pnpm typecheck && pnpm test && pnpm build",
|
|
74
|
+
"test": "vitest run",
|
|
75
|
+
"typecheck": "tsc --noEmit"
|
|
9
76
|
}
|
|
10
77
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { Migrator, type SqlClient } from "effect/sql";
|
|
3
|
+
|
|
4
|
+
export interface SchemaComponent<R = never> {
|
|
5
|
+
readonly apply: Effect.Effect<unknown, unknown, R | SqlClient.SqlClient>;
|
|
6
|
+
readonly name: string;
|
|
7
|
+
readonly pending: Effect.Effect<string | undefined, unknown, R | SqlClient.SqlClient>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function runSchemaComponent<A, R>(name: string, effect: Effect.Effect<A, unknown, R>) {
|
|
11
|
+
return effect.pipe(Effect.mapError((cause) => new Migrator.MigrationError({ cause, kind: "Failed", message: `Schema component ${name} failed.` })));
|
|
12
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Context, Effect, type Scope, Semaphore } from "effect";
|
|
2
|
+
|
|
3
|
+
export class WorkerRelease extends Context.Service<
|
|
4
|
+
WorkerRelease,
|
|
5
|
+
{
|
|
6
|
+
readonly register: <E>(control: (active: boolean) => Effect.Effect<void, E>) => Effect.Effect<void, E, Scope.Scope>;
|
|
7
|
+
}
|
|
8
|
+
>()("@shivaedev/sql/WorkerRelease") {}
|
|
9
|
+
|
|
10
|
+
export const makeWorkerRelease = Effect.fn("EffectSql.makeWorkerRelease")(function* () {
|
|
11
|
+
const mutex = Semaphore.makeUnsafe(1);
|
|
12
|
+
const controls = new Set<(active: boolean) => Effect.Effect<void, unknown>>();
|
|
13
|
+
let active = false;
|
|
14
|
+
const locked = mutex.withPermits(1);
|
|
15
|
+
function register<E>(control: (active: boolean) => Effect.Effect<void, E>) {
|
|
16
|
+
const attach = Effect.gen(function* () {
|
|
17
|
+
controls.add(control);
|
|
18
|
+
yield* control(active);
|
|
19
|
+
});
|
|
20
|
+
const detach = Effect.sync(() => {
|
|
21
|
+
controls.delete(control);
|
|
22
|
+
});
|
|
23
|
+
return Effect.acquireRelease(locked(attach), () => locked(detach));
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
service: WorkerRelease.of({ register }),
|
|
27
|
+
setActive: (next: boolean) =>
|
|
28
|
+
locked(
|
|
29
|
+
Effect.gen(function* () {
|
|
30
|
+
if (next === active) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
active = next;
|
|
34
|
+
yield* Effect.forEach(controls, (control) => control(active), { concurrency: "unbounded", discard: true });
|
|
35
|
+
}),
|
|
36
|
+
).pipe(Effect.uninterruptible),
|
|
37
|
+
};
|
|
38
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Data, Effect, Option } from "effect";
|
|
2
|
+
import { SqlClient } from "effect/sql";
|
|
3
|
+
|
|
4
|
+
export class AdvisoryLockOutsideTransaction extends Data.TaggedError("AdvisoryLockOutsideTransaction") {}
|
|
5
|
+
|
|
6
|
+
export const advisoryXactLock = Effect.fn("EffectSql.advisoryXactLock")(function* (key: bigint | string) {
|
|
7
|
+
const sql = yield* SqlClient.SqlClient;
|
|
8
|
+
const transaction = yield* Effect.serviceOption(sql.transactionService);
|
|
9
|
+
if (Option.isNone(transaction)) {
|
|
10
|
+
return yield* new AdvisoryLockOutsideTransaction();
|
|
11
|
+
}
|
|
12
|
+
if (typeof key === "string") {
|
|
13
|
+
yield* sql`select pg_advisory_xact_lock(hashtextextended(${key}, 0))`;
|
|
14
|
+
} else {
|
|
15
|
+
yield* sql`select pg_advisory_xact_lock(${String(key)}::bigint)`;
|
|
16
|
+
}
|
|
17
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Effect, Schema, SchemaAST } from "effect";
|
|
2
|
+
import type { SqlClient } from "effect/sql";
|
|
3
|
+
|
|
4
|
+
const Expression = Schema.Tuple([Schema.Tuple([Schema.Literals(["count", "sum", "avg", "min", "max", "boolOr", "boolAnd"]), Schema.String])]);
|
|
5
|
+
const functions = { avg: "avg", boolAnd: "bool_and", boolOr: "bool_or", count: "count", max: "max", min: "min", sum: "sum" };
|
|
6
|
+
type Operation = keyof typeof functions;
|
|
7
|
+
|
|
8
|
+
function isNumber(ast: SchemaAST.AST): boolean {
|
|
9
|
+
if (ast._tag === "Union") {
|
|
10
|
+
return ast.types.some(isNumber);
|
|
11
|
+
}
|
|
12
|
+
if (ast._tag === "Suspend") {
|
|
13
|
+
return isNumber(ast.thunk());
|
|
14
|
+
}
|
|
15
|
+
return ast._tag === "Number" || (ast._tag === "Literal" && typeof ast.literal === "number");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function resultSchema(operation: Operation, field: Schema.Constraint): Schema.Constraint {
|
|
19
|
+
switch (operation) {
|
|
20
|
+
case "count":
|
|
21
|
+
return Schema.NumberFromString;
|
|
22
|
+
case "sum":
|
|
23
|
+
case "avg":
|
|
24
|
+
return isNumber(SchemaAST.toType(field.ast)) ? Schema.NumberFromString : Schema.BigDecimalFromString;
|
|
25
|
+
case "boolOr":
|
|
26
|
+
case "boolAnd":
|
|
27
|
+
return Schema.Boolean;
|
|
28
|
+
case "min":
|
|
29
|
+
case "max":
|
|
30
|
+
return field;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function aggregateColumn(sql: SqlClient.SqlClient, fieldAt: (key: string) => Effect.Effect<Schema.Constraint, Schema.SchemaError>) {
|
|
35
|
+
return Effect.fnUntraced(function* (name: string, expression: object) {
|
|
36
|
+
const [[operation, key]] = yield* Schema.decodeUnknownEffect(Expression)(Object.entries(expression));
|
|
37
|
+
const countRows = operation === "count" && key === "*";
|
|
38
|
+
const field = countRows ? Schema.Number : yield* fieldAt(key);
|
|
39
|
+
const operand = countRows ? sql`*` : sql`${sql(key)}`;
|
|
40
|
+
const aggregate = sql`${sql.literal(functions[operation])}(${operand})`;
|
|
41
|
+
const text = operation === "count" || operation === "sum" || operation === "avg";
|
|
42
|
+
const Result = resultSchema(operation, field);
|
|
43
|
+
return {
|
|
44
|
+
column: sql`${text ? sql`cast(${aggregate} as text)` : aggregate} as ${sql(name)}`,
|
|
45
|
+
order: aggregate,
|
|
46
|
+
schema: operation === "count" ? Result : Schema.NullOr(Result),
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { BigDecimal, Effect, Schema } from "effect";
|
|
2
|
+
import type { Model } from "effect/schema";
|
|
3
|
+
import type { SqlError } from "effect/sql/SqlError";
|
|
4
|
+
import type { FindManyFilter, OrderedValue } from "#repositoryFilter.ts";
|
|
5
|
+
|
|
6
|
+
type Row<T extends Model.Any> = Schema.Struct.Type<T["fields"]>;
|
|
7
|
+
type Key<T extends Model.Any> = Extract<keyof T["fields"] & keyof Row<T>, string>;
|
|
8
|
+
type FieldsOfType<T extends Model.Any, TValue> = {
|
|
9
|
+
[TKey in Key<T>]: [NonNullable<Row<T>[TKey]>] extends [TValue] ? TKey : never;
|
|
10
|
+
}[Key<T>];
|
|
11
|
+
interface Operands<T extends Model.Any> {
|
|
12
|
+
avg: FieldsOfType<T, number | BigDecimal.BigDecimal>;
|
|
13
|
+
boolAnd: FieldsOfType<T, boolean>;
|
|
14
|
+
boolOr: FieldsOfType<T, boolean>;
|
|
15
|
+
count: "*" | Key<T>;
|
|
16
|
+
max: FieldsOfType<T, OrderedValue>;
|
|
17
|
+
min: FieldsOfType<T, OrderedValue>;
|
|
18
|
+
sum: FieldsOfType<T, number | BigDecimal.BigDecimal>;
|
|
19
|
+
}
|
|
20
|
+
export type AggregateExpression<T extends Model.Any> = {
|
|
21
|
+
[TOperation in keyof Operands<T>]: { readonly [TKey in TOperation]: Operands<T>[TKey] } & {
|
|
22
|
+
readonly [TKey in Exclude<keyof Operands<T>, TOperation>]?: never;
|
|
23
|
+
};
|
|
24
|
+
}[keyof Operands<T>];
|
|
25
|
+
|
|
26
|
+
type AggregateValue<T extends Model.Any, TExpression> = TExpression extends { readonly count: unknown }
|
|
27
|
+
? number
|
|
28
|
+
: TExpression extends { readonly boolOr: unknown } | { readonly boolAnd: unknown }
|
|
29
|
+
? boolean | null
|
|
30
|
+
: TExpression extends { readonly [TKey in infer TOperation]: infer TField }
|
|
31
|
+
? TOperation extends "sum" | "avg" | "min" | "max"
|
|
32
|
+
? TField extends Key<T>
|
|
33
|
+
? Row<T>[TField] | null
|
|
34
|
+
: never
|
|
35
|
+
: never
|
|
36
|
+
: never;
|
|
37
|
+
|
|
38
|
+
export interface AggregateQuery<T extends Model.Any, TGroup extends readonly Key<T>[], TAggregates> {
|
|
39
|
+
readonly aggregates: TAggregates & { readonly [TKey in TGroup[number]]?: never };
|
|
40
|
+
readonly groupBy: TGroup;
|
|
41
|
+
readonly limit?: number;
|
|
42
|
+
readonly orderBy?: readonly { readonly field: TGroup[number] | Extract<keyof TAggregates, string>; readonly direction: "asc" | "desc" }[];
|
|
43
|
+
readonly where?: FindManyFilter<T>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type Aggregate<T extends Model.Any> = <
|
|
47
|
+
const TGroup extends readonly [Key<T>, ...Key<T>[]],
|
|
48
|
+
const TAggregates extends Readonly<Record<string, AggregateExpression<T>>>,
|
|
49
|
+
>(
|
|
50
|
+
query: AggregateQuery<T, TGroup, TAggregates>,
|
|
51
|
+
) => Effect.Effect<
|
|
52
|
+
Array<{
|
|
53
|
+
readonly [TName in TGroup[number] | keyof TAggregates]: TName extends TGroup[number] ? Row<T>[TName] : AggregateValue<T, TAggregates[TName]>;
|
|
54
|
+
}>,
|
|
55
|
+
SqlError | Schema.SchemaError,
|
|
56
|
+
T["fields"][Key<T>]["EncodingServices"] | T["fields"][Key<T>]["DecodingServices"]
|
|
57
|
+
>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { Migrator } from "effect/sql";
|
|
3
|
+
import type { PostgresMigrationOptions } from "./migrations.ts";
|
|
4
|
+
import { type PendingPostgresMigrations, pendingPostgresMigrations } from "./pendingPostgresMigrations.ts";
|
|
5
|
+
|
|
6
|
+
export const assertPostgresMigrations = Effect.fn("EffectSql.assertPostgresMigrations")(function* <R>(
|
|
7
|
+
options: Pick<PostgresMigrationOptions<R>, "loader" | "table" | "components" | "release">,
|
|
8
|
+
) {
|
|
9
|
+
const pending = yield* pendingPostgresMigrations(options);
|
|
10
|
+
if (pending.migrations.length > 0 || pending.components.length > 0) {
|
|
11
|
+
return yield* Effect.fail(postgresMigrationError(pending));
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export function postgresMigrationError(pending: PendingPostgresMigrations) {
|
|
16
|
+
const details = [
|
|
17
|
+
...pending.migrations.map(({ id, name }) => `migration ${id} (${name})`),
|
|
18
|
+
...pending.components.map(({ name, detail }) => `component ${name}: ${detail}`),
|
|
19
|
+
];
|
|
20
|
+
return new Migrator.MigrationError({
|
|
21
|
+
kind: "BadState",
|
|
22
|
+
message: `Database migrations are not current: ${details.join("; ")}. Run the deployment migration command.`,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
import type { Model } from "effect/schema";
|
|
3
|
+
import type { SqlClient } from "effect/sql";
|
|
4
|
+
import type { SqlError } from "effect/sql/SqlError";
|
|
5
|
+
import type { Fragment } from "effect/sql/Statement";
|
|
6
|
+
import type { RepositoryChanges } from "./repositoryChanges.ts";
|
|
7
|
+
|
|
8
|
+
export type InsertMany<TModel extends Model.Any, TServices = never> = (
|
|
9
|
+
inputs: readonly TModel["insert"]["Type"][],
|
|
10
|
+
) => Effect.Effect<TModel["Type"][], SqlError | Schema.SchemaError, TModel["insert"]["EncodingServices"] | TModel["DecodingServices"] | TServices>;
|
|
11
|
+
|
|
12
|
+
export interface BatchWrites<TModel extends Model.Any> {
|
|
13
|
+
readonly insertMany: InsertMany<TModel>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function makeInsertMany<TServices = never>(
|
|
17
|
+
model: Model.Any,
|
|
18
|
+
sql: SqlClient.SqlClient,
|
|
19
|
+
options: { readonly tableName: string; readonly spanPrefix: string; readonly changes: RepositoryChanges },
|
|
20
|
+
operation: string,
|
|
21
|
+
prepare: (row: Readonly<Record<string, unknown>>) => Effect.Effect<Fragment, Schema.SchemaError, TServices> = () => Effect.succeed(sql``),
|
|
22
|
+
) {
|
|
23
|
+
const encode = Schema.encodeEffect(model.insert);
|
|
24
|
+
const record = Schema.decodeUnknownEffect(Schema.Record(Schema.String, Schema.Unknown));
|
|
25
|
+
const decode = Schema.decodeUnknownEffect(Schema.Array(model));
|
|
26
|
+
return (inputs: readonly unknown[]) =>
|
|
27
|
+
Effect.gen(function* () {
|
|
28
|
+
if (inputs.length === 0) {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
const rows = yield* Effect.forEach(inputs, (input) => Effect.flatMap(encode(input), record));
|
|
32
|
+
const columns = [...new Set(rows.flatMap(Object.keys))];
|
|
33
|
+
if (columns.length === 0) {
|
|
34
|
+
columns.push(...Object.keys(model.fields).slice(0, 1));
|
|
35
|
+
}
|
|
36
|
+
const names = sql.csv(columns.map((column) => sql`${sql(column)}`));
|
|
37
|
+
const chunkSize = Math.max(1, Math.min(1000, Math.floor(10_000 / columns.length)));
|
|
38
|
+
const results: unknown[] = [];
|
|
39
|
+
const changedRows: Readonly<Record<string, unknown>>[] = [];
|
|
40
|
+
let chunk: Fragment[] = [];
|
|
41
|
+
let parameters = 0;
|
|
42
|
+
let suffix: Fragment = sql``;
|
|
43
|
+
const flush = Effect.gen(function* () {
|
|
44
|
+
const returned = yield* sql`insert into ${sql(options.tableName)} (${names}) values ${sql.csv(chunk)} ${suffix} returning *`;
|
|
45
|
+
changedRows.push(...returned);
|
|
46
|
+
for (const row of yield* decode(returned)) {
|
|
47
|
+
results.push(row);
|
|
48
|
+
}
|
|
49
|
+
chunk = [];
|
|
50
|
+
parameters = 0;
|
|
51
|
+
});
|
|
52
|
+
for (const row of rows) {
|
|
53
|
+
const value = sql`(${sql.csv(columns.map((column) => (Object.hasOwn(row, column) ? sql`${row[column]}` : sql`default`)))})`;
|
|
54
|
+
const size = value.compile()[1].length;
|
|
55
|
+
const prepared = yield* prepare(row);
|
|
56
|
+
const suffixSize = sql`${prepared}`.compile()[1].length;
|
|
57
|
+
// Bound native fragment size as well as PostgreSQL's 65,535 bind parameters.
|
|
58
|
+
if (chunk.length > 0 && (chunk.length === chunkSize || parameters + size + suffixSize > 10_000)) {
|
|
59
|
+
yield* flush;
|
|
60
|
+
}
|
|
61
|
+
suffix = prepared;
|
|
62
|
+
chunk.push(value);
|
|
63
|
+
parameters += size;
|
|
64
|
+
}
|
|
65
|
+
yield* flush;
|
|
66
|
+
yield* options.changes.record(changedRows);
|
|
67
|
+
return results;
|
|
68
|
+
}).pipe(options.changes.write, Effect.withSpan(`${options.spanPrefix}.${operation}`));
|
|
69
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { camelToSnake, snakeToCamel } from "effect/String";
|
|
2
|
+
|
|
3
|
+
export interface SqlNameTransforms {
|
|
4
|
+
readonly transformQueryNames?: ((name: string) => string) | undefined;
|
|
5
|
+
readonly transformResultNames?: ((name: string) => string) | undefined;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function clientOptions<TOptions extends object>(
|
|
9
|
+
options: TOptions,
|
|
10
|
+
{ snakeCase = true }: { readonly snakeCase?: boolean } = {},
|
|
11
|
+
): TOptions & SqlNameTransforms {
|
|
12
|
+
return {
|
|
13
|
+
...(snakeCase ? { transformQueryNames: camelToSnake, transformResultNames: snakeToCamel } : {}),
|
|
14
|
+
...options,
|
|
15
|
+
};
|
|
16
|
+
}
|