@shivaedev/sql 0.39.0 → 0.40.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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.40.0 - 2026-11-04
4
+
5
+ ### Added
6
+
7
+ - `keysetPage` builds native statement fragments without requiring a `SqlClient` service; its read callback retains its own service requirements.
8
+
3
9
  ## 0.39.0 - 2026-11-04
4
10
 
5
11
  ### Added
package/README.md CHANGED
@@ -69,7 +69,7 @@ const backend = Effect.gen(function* () {
69
69
  }).pipe(Effect.scoped, Effect.provide(database));
70
70
  ```
71
71
 
72
- The Layer owns the pool and waits for every socket to close during shutdown. A reserved connection sees committed writes from client transactions in the same database; it does not implicitly join a client's open transaction. `appDatabase` from `@shivaedev/server/appDatabase.ts` and database-backed `appHost` from `@shivaedev/server/appHost.ts` compose this Layer and expose the pool to application services.
72
+ The Layer owns the pool and waits for every socket to close during shutdown. A reserved connection sees committed writes from client transactions in the same database; it does not implicitly join a client's open transaction. `appDatabase` from `@shivaedev/server/appDatabase.ts` and database-backed `appHost` from `@shivaedev/server/appDatabase.ts` compose this Layer and expose the pool to application services.
73
73
 
74
74
  ### SQL names: configure the client once
75
75
 
@@ -87,7 +87,7 @@ const database = PgClient.layer(
87
87
 
88
88
  `clientOptions` enables Effect SQL's `transformQueryNames` with Effect's `String.camelToSnake` and `transformResultNames` with `String.snakeToCamel` by default. A model field `createdAt` writes the migration-created `created_at` column, and returned rows contain `createdAt`. Inserts, selects, filters, ordering, updates and raw SQL share the client's transforms; column aliases are unnecessary. Write literal migration SQL with snake_case columns. In tagged SQL, interpolated identifiers such as `sql("createdAt")` are transformed; literal SQL text stays as written.
89
89
 
90
- To opt out for an existing database with quoted camelCase columns or models with snake_case fields, use `PgClient.layer(clientOptions(options, { snakeCase: false }))`. Existing native clients are unchanged until configured with this helper. Native `transformQueryNames` and `transformResultNames` supplied in `options` take precedence. For `appHost` from `@shivaedev/server/appHost.ts` or `appDatabase` from `@shivaedev/server/appDatabase.ts`, use `clientOptions` on their `config` object; the host forwards those native transforms to its client.
90
+ To opt out for an existing database with quoted camelCase columns or models with snake_case fields, use `PgClient.layer(clientOptions(options, { snakeCase: false }))`. Existing native clients are unchanged until configured with this helper. Native `transformQueryNames` and `transformResultNames` supplied in `options` take precedence. For `appHost` from `@shivaedev/server/appDatabase.ts` or `appDatabase` from `@shivaedev/server/appDatabase.ts`, use `clientOptions` on their `config` object; the host forwards those native transforms to its client.
91
91
 
92
92
  ### Grouped values in flat columns
93
93
 
@@ -398,7 +398,7 @@ const rows = yield* paymentsBySequence([]);
398
398
 
399
399
  ### Keyset pages over native SQL
400
400
 
401
- `keysetPage` builds seek and ordering fragments and asks your `read` callback for one extra row. It returns `{ rows, next }`, where `next` is an `Option` containing the last returned row's cursor only when another row exists. Your query keeps its predicates, joins and result schema:
401
+ `keysetPage` builds seek and ordering fragments and asks your `read` callback for one extra row. It returns `{ rows, next }`, where `next` is an `Option` containing the last returned row's cursor only when another row exists. Your query keeps its predicates, joins and result schema. The helper builds native statement fragments without reading `SqlClient`; its service requirements come from your `read` callback:
402
402
 
403
403
  ```ts
404
404
  import { Effect, Schema } from "effect";
@@ -1,15 +1,14 @@
1
1
  import { Effect, Option, Schema } from "effect";
2
- import { SqlClient } from "effect/sql";
3
- import type { Fragment } from "effect/sql/Statement";
2
+ import * as Statement from "effect/sql/Statement";
4
3
  export interface KeysetOrder<TCursor> {
5
- readonly column: string | Fragment;
4
+ readonly column: string | Statement.Fragment;
6
5
  readonly direction: "asc" | "desc";
7
6
  readonly value: (cursor: TCursor) => string | number | bigint | boolean;
8
7
  }
9
8
  export interface KeysetPageQuery {
10
9
  readonly limit: number;
11
- readonly orderBy: Fragment;
12
- readonly seek: Fragment;
10
+ readonly orderBy: Statement.Fragment;
11
+ readonly seek: Statement.Fragment;
13
12
  }
14
13
  export interface KeysetPage<TRow, TCursor> {
15
14
  readonly next: Option.Option<TCursor>;
@@ -22,5 +21,5 @@ export declare const keysetPage: <TRow, TCursor, E, R>(options: {
22
21
  readonly tieBreaker: KeysetOrder<TCursor>;
23
22
  readonly cursorFor: (row: TRow) => TCursor;
24
23
  readonly read: (query: KeysetPageQuery) => Effect.Effect<readonly TRow[], E, R>;
25
- }) => Effect.Effect<KeysetPage<TRow, TCursor>, E | Schema.SchemaError, R | SqlClient.SqlClient>;
24
+ }) => Effect.Effect<KeysetPage<TRow, TCursor>, E | Schema.SchemaError, R>;
26
25
  //# sourceMappingURL=keysetPage.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"keysetPage.d.ts","sourceRoot":"","sources":["../src/keysetPage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChD,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAErD,MAAM,WAAW,WAAW,CAAC,OAAO;IACnC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ,CAAC;IACnC,QAAQ,CAAC,SAAS,EAAE,KAAK,GAAG,MAAM,CAAC;IACnC,QAAQ,CAAC,KAAK,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;CACxE;AAED,MAAM,WAAW,eAAe;IAC/B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;CACxB;AAED,MAAM,WAAW,UAAU,CAAC,IAAI,EAAE,OAAO;IACxC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACtC,QAAQ,CAAC,IAAI,EAAE,SAAS,IAAI,EAAE,CAAC;CAC/B;AAID,eAAO,MAAM,UAAU,GAAgC,IAAI,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC;oBACzD,MAAM;qBACL,OAAO,GAAG,SAAS;sBAClB,SAAS,WAAW,CAAC,OAAO,CAAC,EAAE;yBAC5B,WAAW,CAAC,OAAO,CAAC;wBACrB,CAAC,GAAG,EAAE,IAAI,KAAK,OAAO;mBAC3B,CAAC,KAAK,EAAE,eAAe,KAAK,MAAM,CAAC,MAAM,CAAC,SAAS,IAAI,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC;+FAwB9E,CAAC"}
1
+ {"version":3,"file":"keysetPage.d.ts","sourceRoot":"","sources":["../src/keysetPage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChD,OAAO,KAAK,SAAS,MAAM,sBAAsB,CAAC;AAElD,MAAM,WAAW,WAAW,CAAC,OAAO;IACnC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,QAAQ,CAAC;IAC7C,QAAQ,CAAC,SAAS,EAAE,KAAK,GAAG,MAAM,CAAC;IACnC,QAAQ,CAAC,KAAK,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;CACxE;AAED,MAAM,WAAW,eAAe;IAC/B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,SAAS,CAAC,QAAQ,CAAC;IACrC,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC,QAAQ,CAAC;CAClC;AAED,MAAM,WAAW,UAAU,CAAC,IAAI,EAAE,OAAO;IACxC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACtC,QAAQ,CAAC,IAAI,EAAE,SAAS,IAAI,EAAE,CAAC;CAC/B;AAID,eAAO,MAAM,UAAU,GAAgC,IAAI,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC;oBACzD,MAAM;qBACL,OAAO,GAAG,SAAS;sBAClB,SAAS,WAAW,CAAC,OAAO,CAAC,EAAE;yBAC5B,WAAW,CAAC,OAAO,CAAC;wBACrB,CAAC,GAAG,EAAE,IAAI,KAAK,OAAO;mBAC3B,CAAC,KAAK,EAAE,eAAe,KAAK,MAAM,CAAC,MAAM,CAAC,SAAS,IAAI,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC;yEAiC9E,CAAC"}
@@ -1,20 +1,29 @@
1
1
  import { Effect, Option, Schema } from "effect";
2
- import { SqlClient } from "effect/sql";
2
+ import * as Statement from "effect/sql/Statement";
3
3
  const PageSize = Schema.Number.check(Schema.isInt(), Schema.isGreaterThan(0), Schema.isLessThan(Number.MAX_SAFE_INTEGER));
4
4
  export const keysetPage = Effect.fnUntraced(function* (options) {
5
5
  const limit = yield* Schema.decodeUnknownEffect(PageSize)(options.limit);
6
- const sql = yield* SqlClient.SqlClient;
7
- const order = [...options.orderBy, options.tieBreaker];
8
- const columns = order.map(({ column }) => (typeof column === "string" ? sql `${sql(column)}` : column));
6
+ const order = [...options.orderBy, options.tieBreaker].map((part) => ({
7
+ ...part,
8
+ column: typeof part.column === "string" ? Statement.fragment([Statement.identifier(part.column)]) : part.column,
9
+ }));
9
10
  const after = options.after;
10
11
  const values = after === undefined ? undefined : order.map(({ value }) => value(after));
11
- const seek = values === undefined
12
- ? sql `true`
13
- : sql.or(order.map(({ direction }, index) => sql.and([
14
- ...columns.slice(0, index).map((column, previous) => sql `${column} = ${values[previous]}`),
15
- direction === "asc" ? sql `${columns[index]} > ${values[index]}` : sql `${columns[index]} < ${values[index]}`,
16
- ])));
17
- const orderBy = sql.csv(order.map(({ direction }, index) => sql `${columns[index]} ${sql.literal(direction === "asc" ? "asc" : "desc")}`));
12
+ const conditions = values === undefined
13
+ ? []
14
+ : order.map(({ column, direction }, index) => {
15
+ const prefix = order
16
+ .slice(0, index)
17
+ .map(({ column: previousColumn }, previous) => Statement.fragment([...previousColumn.segments, Statement.literal(" = "), Statement.parameter(values[previous])]));
18
+ const comparison = Statement.fragment([
19
+ ...column.segments,
20
+ Statement.literal(direction === "asc" ? " > " : " < "),
21
+ Statement.parameter(values[index]),
22
+ ]);
23
+ return Statement.and([...prefix, comparison]);
24
+ });
25
+ const seek = values === undefined ? Statement.fragment([Statement.literal("true")]) : Statement.or(conditions);
26
+ const orderBy = Statement.csv(order.map(({ column, direction }) => Statement.fragment([...column.segments, Statement.literal(direction === "asc" ? " asc" : " desc")])));
18
27
  const found = yield* options.read({ limit: limit + 1, orderBy, seek });
19
28
  const rows = found.slice(0, limit);
20
29
  const last = rows.at(-1);
@@ -1 +1 @@
1
- {"version":3,"file":"keysetPage.js","sourceRoot":"","sources":["../src/keysetPage.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"keysetPage.js","sourceRoot":"","sources":["../src/keysetPage.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shivaedev/sql",
3
- "version": "0.39.0",
3
+ "version": "0.40.0",
4
4
  "description": "Schema-derived repositories over native Effect SQL",
5
5
  "keywords": [
6
6
  "effect"
@@ -51,11 +51,11 @@
51
51
  "@effect/sql-pg": "4.0.7",
52
52
  "@effect/sql-sqlite-node": "4.0.7",
53
53
  "@shivaedev/contract": "0.36.1",
54
- "@shivaedev/jobs": "0.36.0",
55
- "@shivaedev/quality": "0.61.1",
56
- "@shivaedev/test-kits": "0.4.0",
57
- "@shivaedev/test-sql": "0.21.1",
58
- "@shivaedev/test-story": "0.39.1",
54
+ "@shivaedev/jobs": "0.37.0",
55
+ "@shivaedev/quality": "0.62.0",
56
+ "@shivaedev/test-kits": "0.5.0",
57
+ "@shivaedev/test-sql": "0.21.2",
58
+ "@shivaedev/test-story": "0.39.2",
59
59
  "@types/node": "24.19.2",
60
60
  "effect": "4.0.7",
61
61
  "typescript": "7.0.2",
package/src/keysetPage.ts CHANGED
@@ -1,17 +1,16 @@
1
1
  import { Effect, Option, Schema } from "effect";
2
- import { SqlClient } from "effect/sql";
3
- import type { Fragment } from "effect/sql/Statement";
2
+ import * as Statement from "effect/sql/Statement";
4
3
 
5
4
  export interface KeysetOrder<TCursor> {
6
- readonly column: string | Fragment;
5
+ readonly column: string | Statement.Fragment;
7
6
  readonly direction: "asc" | "desc";
8
7
  readonly value: (cursor: TCursor) => string | number | bigint | boolean;
9
8
  }
10
9
 
11
10
  export interface KeysetPageQuery {
12
11
  readonly limit: number;
13
- readonly orderBy: Fragment;
14
- readonly seek: Fragment;
12
+ readonly orderBy: Statement.Fragment;
13
+ readonly seek: Statement.Fragment;
15
14
  }
16
15
 
17
16
  export interface KeysetPage<TRow, TCursor> {
@@ -28,25 +27,34 @@ export const keysetPage = Effect.fnUntraced(function* <TRow, TCursor, E, R>(opti
28
27
  readonly tieBreaker: KeysetOrder<TCursor>;
29
28
  readonly cursorFor: (row: TRow) => TCursor;
30
29
  readonly read: (query: KeysetPageQuery) => Effect.Effect<readonly TRow[], E, R>;
31
- }): Effect.fn.Return<KeysetPage<TRow, TCursor>, E | Schema.SchemaError, R | SqlClient.SqlClient> {
30
+ }): Effect.fn.Return<KeysetPage<TRow, TCursor>, E | Schema.SchemaError, R> {
32
31
  const limit = yield* Schema.decodeUnknownEffect(PageSize)(options.limit);
33
- const sql = yield* SqlClient.SqlClient;
34
- const order = [...options.orderBy, options.tieBreaker];
35
- const columns = order.map(({ column }) => (typeof column === "string" ? sql`${sql(column)}` : column));
32
+ const order = [...options.orderBy, options.tieBreaker].map((part) => ({
33
+ ...part,
34
+ column: typeof part.column === "string" ? Statement.fragment([Statement.identifier(part.column)]) : part.column,
35
+ }));
36
36
  const after = options.after;
37
37
  const values = after === undefined ? undefined : order.map(({ value }) => value(after));
38
- const seek =
38
+ const conditions =
39
39
  values === undefined
40
- ? sql`true`
41
- : sql.or(
42
- order.map(({ direction }, index) =>
43
- sql.and([
44
- ...columns.slice(0, index).map((column, previous) => sql`${column} = ${values[previous]}`),
45
- direction === "asc" ? sql`${columns[index]} > ${values[index]}` : sql`${columns[index]} < ${values[index]}`,
46
- ]),
47
- ),
48
- );
49
- const orderBy = sql.csv(order.map(({ direction }, index) => sql`${columns[index]} ${sql.literal(direction === "asc" ? "asc" : "desc")}`));
40
+ ? []
41
+ : order.map(({ column, direction }, index) => {
42
+ const prefix = order
43
+ .slice(0, index)
44
+ .map(({ column: previousColumn }, previous) =>
45
+ Statement.fragment([...previousColumn.segments, Statement.literal(" = "), Statement.parameter(values[previous])]),
46
+ );
47
+ const comparison = Statement.fragment([
48
+ ...column.segments,
49
+ Statement.literal(direction === "asc" ? " > " : " < "),
50
+ Statement.parameter(values[index]),
51
+ ]);
52
+ return Statement.and([...prefix, comparison]);
53
+ });
54
+ const seek = values === undefined ? Statement.fragment([Statement.literal("true")]) : Statement.or(conditions);
55
+ const orderBy = Statement.csv(
56
+ order.map(({ column, direction }) => Statement.fragment([...column.segments, Statement.literal(direction === "asc" ? " asc" : " desc")])),
57
+ );
50
58
  const found = yield* options.read({ limit: limit + 1, orderBy, seek });
51
59
  const rows = found.slice(0, limit);
52
60
  const last = rows.at(-1);