shelving 1.278.0 → 1.280.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.
@@ -0,0 +1,33 @@
1
+ import { SQL } from "bun";
2
+ import type { Collection } from "../db/collection/Collection.js";
3
+ import { PostgresProvider, SQLFragment } from "../db/index.js";
4
+ import type { DBProvider } from "../db/provider/DBProvider.js";
5
+ import type { ImmutableArray } from "../util/array.js";
6
+ import type { Data } from "../util/data.js";
7
+ import type { Identifier, Item } from "../util/item.js";
8
+ import type { Query } from "../util/query.js";
9
+ /**
10
+ * PostgreSQL database provider backed by Bun's built-in `Bun.SQL` driver.
11
+ *
12
+ * Implements the `PostgresProvider` SQL abstraction by executing tagged-template queries against a `Bun.SQL` connection.
13
+ * - Identifiers are escaped through `Bun.SQL`'s own `sql()` helper rather than naive string quoting, which is more secure.
14
+ * - Supports transactions via `transact()` — the callback runs in a `SERIALIZABLE` Postgres transaction, and contention aborts are retried automatically.
15
+ * - Requires the `bun` peer dependency and a running Bun environment.
16
+ *
17
+ * @see https://shelving.cc/bun/BunPostgresProvider
18
+ */
19
+ export declare class BunPostgresProvider<I extends Identifier = Identifier, T extends Data = Data> extends PostgresProvider<I, T> {
20
+ private _sql;
21
+ constructor(sql: SQL);
22
+ /** Composes via `SQLFragment` (which flattens embedded fragments at construction), since `Bun.SQL` would otherwise bind them as `$n` parameters. */
23
+ exec<X extends Data>(strings: TemplateStringsArray, ...values: ImmutableArray<unknown>): Promise<ImmutableArray<X>>;
24
+ /** Escapes the identifier via `Bun.SQL`'s first-class `sql()` wrapping rather than manual quoting, which is more secure. */
25
+ sqlIdentifier(name: string): SQLFragment;
26
+ /** Coerces the count to a number, since `Bun.SQL` returns Postgres's 64-bit `COUNT(*)` as a string. */
27
+ countQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query?: Query<Item<II, TT>>): Promise<number>;
28
+ /**
29
+ * Runs the callback in a `SERIALIZABLE` Postgres transaction via `Bun.SQL`'s `begin()` — resolving commits, throwing rolls back and rethrows.
30
+ * - Retries the whole callback (up to 5 attempts, with jittered exponential backoff) when Postgres aborts it for contention (serialization failure or deadlock), so the callback must have no side effects other than through its provider.
31
+ */
32
+ transact<X>(callback: (provider: DBProvider<I, T>) => Promise<X>): Promise<X>;
33
+ }
@@ -0,0 +1,79 @@
1
+ import { SQL } from "bun";
2
+ import { PostgresProvider, SQLFragment } from "../db/index.js";
3
+ import { UnsupportedError } from "../error/UnsupportedError.js";
4
+ import { getDelay } from "../util/async.js";
5
+ import { getRandom } from "../util/random.js";
6
+ // Constants.
7
+ const TRANSACTION_ATTEMPTS = 5;
8
+ const RETRYABLE_SQLSTATES = ["40001", "40P01"]; // Serialization failure and deadlock.
9
+ /**
10
+ * PostgreSQL database provider backed by Bun's built-in `Bun.SQL` driver.
11
+ *
12
+ * Implements the `PostgresProvider` SQL abstraction by executing tagged-template queries against a `Bun.SQL` connection.
13
+ * - Identifiers are escaped through `Bun.SQL`'s own `sql()` helper rather than naive string quoting, which is more secure.
14
+ * - Supports transactions via `transact()` — the callback runs in a `SERIALIZABLE` Postgres transaction, and contention aborts are retried automatically.
15
+ * - Requires the `bun` peer dependency and a running Bun environment.
16
+ *
17
+ * @see https://shelving.cc/bun/BunPostgresProvider
18
+ */
19
+ export class BunPostgresProvider extends PostgresProvider {
20
+ _sql;
21
+ constructor(sql) {
22
+ super();
23
+ this._sql = sql;
24
+ }
25
+ /** Composes via `SQLFragment` (which flattens embedded fragments at construction), since `Bun.SQL` would otherwise bind them as `$n` parameters. */
26
+ exec(strings, ...values) {
27
+ const flat = new SQLFragment(strings, values);
28
+ return this._sql(_getTemplateStrings(flat.strings), ...flat.values);
29
+ }
30
+ /** Escapes the identifier via `Bun.SQL`'s first-class `sql()` wrapping rather than manual quoting, which is more secure. */
31
+ sqlIdentifier(name) {
32
+ return this.sql `${this._sql(name)}`;
33
+ }
34
+ /** Coerces the count to a number, since `Bun.SQL` returns Postgres's 64-bit `COUNT(*)` as a string. */
35
+ async countQuery(collection, query) {
36
+ return Number.parseInt((await super.countQuery(collection, query)).toString(), 10);
37
+ }
38
+ /**
39
+ * Runs the callback in a `SERIALIZABLE` Postgres transaction via `Bun.SQL`'s `begin()` — resolving commits, throwing rolls back and rethrows.
40
+ * - Retries the whole callback (up to 5 attempts, with jittered exponential backoff) when Postgres aborts it for contention (serialization failure or deadlock), so the callback must have no side effects other than through its provider.
41
+ */
42
+ async transact(callback) {
43
+ let aborted;
44
+ for (let attempt = 0; attempt < TRANSACTION_ATTEMPTS; attempt++) {
45
+ // Back off with jitter before each retry so contending transactions de-synchronise instead of re-aborting each other in lockstep.
46
+ if (attempt)
47
+ await getDelay(getRandom(0, 100 * 2 ** attempt));
48
+ try {
49
+ return await this._sql.begin("isolation level serializable", tx => callback(new _BunPostgresTransaction(tx)));
50
+ }
51
+ catch (thrown) {
52
+ if (!_isRetryableError(thrown))
53
+ throw thrown;
54
+ aborted = thrown; // Retry the transaction after contention.
55
+ }
56
+ }
57
+ throw aborted;
58
+ }
59
+ }
60
+ /** Transaction-scoped provider for `BunPostgresProvider.transact()` — every query runs on the transaction's reserved connection. */
61
+ class _BunPostgresTransaction extends BunPostgresProvider {
62
+ /** Not supported inside a transaction — always throws `UnsupportedError`. */
63
+ transact(callback) {
64
+ throw new UnsupportedError("BunPostgresProvider does not support nested transactions", {
65
+ provider: this,
66
+ received: callback,
67
+ caller: this.transact,
68
+ });
69
+ }
70
+ }
71
+ /** Is a thrown value a Postgres contention abort that a fresh transaction attempt may resolve? */
72
+ function _isRetryableError(thrown) {
73
+ return thrown instanceof SQL.PostgresError && RETRYABLE_SQLSTATES.includes(thrown.errno ?? thrown.code);
74
+ }
75
+ /** Convert a strings array into the `TemplateStringsArray` shape `Bun.SQL` expects. */
76
+ function _getTemplateStrings(strings) {
77
+ const raw = [...strings];
78
+ return Object.assign(raw, { raw });
79
+ }
package/bun/index.d.ts CHANGED
@@ -1 +1 @@
1
- export * from "./BunPostgreSQLProvider.js";
1
+ export * from "./BunPostgresProvider.js";
package/bun/index.js CHANGED
@@ -1 +1 @@
1
- export * from "./BunPostgreSQLProvider.js";
1
+ export * from "./BunPostgresProvider.js";
package/db/index.d.ts CHANGED
@@ -2,7 +2,7 @@ export * from "./cache/CollectionCache.js";
2
2
  export * from "./cache/DBCache.js";
3
3
  export * from "./collection/Collection.js";
4
4
  export * from "./migrate/DBMigrator.js";
5
- export * from "./migrate/PostgreSQLMigrator.js";
5
+ export * from "./migrate/PostgresMigrator.js";
6
6
  export * from "./migrate/SQLiteMigrator.js";
7
7
  export * from "./migrate/SQLMigrator.js";
8
8
  export * from "./provider/CacheDBProvider.js";
@@ -11,7 +11,7 @@ export * from "./provider/DBProvider.js";
11
11
  export * from "./provider/DebugDBProvider.js";
12
12
  export * from "./provider/MemoryDBProvider.js";
13
13
  export * from "./provider/MockDBProvider.js";
14
- export * from "./provider/PostgreSQLProvider.js";
14
+ export * from "./provider/PostgresProvider.js";
15
15
  export * from "./provider/SQLiteProvider.js";
16
16
  export * from "./provider/SQLProvider.js";
17
17
  export * from "./provider/StorageDBProvider.js";
package/db/index.js CHANGED
@@ -2,7 +2,7 @@ export * from "./cache/CollectionCache.js";
2
2
  export * from "./cache/DBCache.js";
3
3
  export * from "./collection/Collection.js";
4
4
  export * from "./migrate/DBMigrator.js";
5
- export * from "./migrate/PostgreSQLMigrator.js";
5
+ export * from "./migrate/PostgresMigrator.js";
6
6
  export * from "./migrate/SQLiteMigrator.js";
7
7
  export * from "./migrate/SQLMigrator.js";
8
8
  export * from "./provider/CacheDBProvider.js";
@@ -11,7 +11,7 @@ export * from "./provider/DBProvider.js";
11
11
  export * from "./provider/DebugDBProvider.js";
12
12
  export * from "./provider/MemoryDBProvider.js";
13
13
  export * from "./provider/MockDBProvider.js";
14
- export * from "./provider/PostgreSQLProvider.js";
14
+ export * from "./provider/PostgresProvider.js";
15
15
  export * from "./provider/SQLiteProvider.js";
16
16
  export * from "./provider/SQLProvider.js";
17
17
  export * from "./provider/StorageDBProvider.js";
@@ -16,9 +16,9 @@ type PostgreSQLColumnRow = {
16
16
  /**
17
17
  * PostgreSQL migrator that inspects the live schema via `pg_catalog` tables to diff and migrate columns.
18
18
  *
19
- * @see https://shelving.cc/db/PostgreSQLMigrator
19
+ * @see https://shelving.cc/db/PostgresMigrator
20
20
  */
21
- export declare class PostgreSQLMigrator<T extends SQLProvider = SQLProvider> extends SQLMigrator<T> {
21
+ export declare class PostgresMigrator<T extends SQLProvider = SQLProvider> extends SQLMigrator<T> {
22
22
  protected getTables(): Promise<readonly string[]>;
23
23
  protected getTable(name: string): Promise<SQLTable | undefined>;
24
24
  protected getCreateTableSuffix<TData extends Data>(_collection: Collection<string, Identifier, TData>): string;
@@ -18,9 +18,9 @@ const COMPATIBLE_STRING_TYPES = ["character varying", "varchar", "text", "char",
18
18
  /**
19
19
  * PostgreSQL migrator that inspects the live schema via `pg_catalog` tables to diff and migrate columns.
20
20
  *
21
- * @see https://shelving.cc/db/PostgreSQLMigrator
21
+ * @see https://shelving.cc/db/PostgresMigrator
22
22
  */
23
- export class PostgreSQLMigrator extends SQLMigrator {
23
+ export class PostgresMigrator extends SQLMigrator {
24
24
  async getTables() {
25
25
  const rows = await this.provider.exec `
26
26
  SELECT c.relname AS ${this.provider.sqlIdentifier("name")}
@@ -119,7 +119,8 @@ export declare abstract class DBProvider<I extends Identifier = Identifier, T ex
119
119
  abstract getQuerySequence<II extends I, TT extends T>(collection: Collection<string, II, TT>, query?: Query<Item<II, TT>>): ItemsSequence<II, TT>;
120
120
  /**
121
121
  * Set (overwrite) the data for every item matching a query.
122
- * - Not guaranteed atomic: an implementation may resolve the matching items first and then write per item (two-step see `ThroughDBProvider`), so wrap the call in `transact()` when atomicity matters.
122
+ * - Two-step by default: resolves the query to its matching items with `getQuery()`, then sets each one concurrently with `setItem()`. Engine providers override this with a native query write where one exists (e.g. SQL `UPDATE WHERE`).
123
+ * - Not guaranteed atomic: the resolve and the writes are separate steps, so wrap the call in `transact()` when atomicity matters.
123
124
  *
124
125
  * @param collection Collection to write to.
125
126
  * @param query Query selecting the items to set.
@@ -127,10 +128,11 @@ export declare abstract class DBProvider<I extends Identifier = Identifier, T ex
127
128
  * @example await provider.setQuery(users, { age: 40 }, { active: true });
128
129
  * @see https://shelving.cc/db/DBProvider/setQuery
129
130
  */
130
- abstract setQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query: Query<Item<II, TT>>, data: TT): Promise<void>;
131
+ setQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query: Query<Item<II, TT>>, data: TT): Promise<void>;
131
132
  /**
132
133
  * Apply partial updates to every item matching a query.
133
- * - Not guaranteed atomic: an implementation may resolve the matching items first and then write per item (two-step see `ThroughDBProvider`), so wrap the call in `transact()` when atomicity matters.
134
+ * - Two-step by default: resolves the query to its matching items with `getQuery()`, then updates each one concurrently with `updateItem()`. Engine providers override this with a native query write where one exists (e.g. SQL `UPDATE WHERE`).
135
+ * - Not guaranteed atomic: the resolve and the writes are separate steps, so wrap the call in `transact()` when atomicity matters.
134
136
  *
135
137
  * @param collection Collection to write to.
136
138
  * @param query Query selecting the items to update.
@@ -138,17 +140,18 @@ export declare abstract class DBProvider<I extends Identifier = Identifier, T ex
138
140
  * @example await provider.updateQuery(users, { age: 40 }, { active: true });
139
141
  * @see https://shelving.cc/db/DBProvider/updateQuery
140
142
  */
141
- abstract updateQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query: Query<Item<II, TT>>, updates: Updates<TT>): Promise<void>;
143
+ updateQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query: Query<Item<II, TT>>, updates: Updates<TT>): Promise<void>;
142
144
  /**
143
145
  * Delete every item matching a query.
144
- * - Not guaranteed atomic: an implementation may resolve the matching items first and then delete per item (two-step see `ThroughDBProvider`), so wrap the call in `transact()` when atomicity matters.
146
+ * - Two-step by default: resolves the query to its matching items with `getQuery()`, then deletes each one concurrently with `deleteItem()`. Engine providers override this with a native query write where one exists (e.g. SQL `DELETE WHERE`).
147
+ * - Not guaranteed atomic: the resolve and the writes are separate steps, so wrap the call in `transact()` when atomicity matters.
145
148
  *
146
149
  * @param collection Collection to delete from.
147
150
  * @param query Query selecting the items to delete.
148
151
  * @example await provider.deleteQuery(users, { active: false });
149
152
  * @see https://shelving.cc/db/DBProvider/deleteQuery
150
153
  */
151
- abstract deleteQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query: Query<Item<II, TT>>): Promise<void>;
154
+ deleteQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query: Query<Item<II, TT>>): Promise<void>;
152
155
  /**
153
156
  * Get the first item matching a query, or `undefined` if there are none.
154
157
  *
@@ -1,6 +1,7 @@
1
1
  import { RequiredError } from "../../error/RequiredError.js";
2
2
  import { UnsupportedError } from "../../error/UnsupportedError.js";
3
3
  import { countArray, getFirst } from "../../util/array.js";
4
+ import { awaitValues } from "../../util/async.js";
4
5
  import { awaitDispose } from "../../util/dispose.js";
5
6
  /**
6
7
  * Provider with a fully asynchronous interface for database access.
@@ -49,6 +50,50 @@ export class DBProvider {
49
50
  async countQuery(collection, query) {
50
51
  return countArray(await this.getQuery(collection, query));
51
52
  }
53
+ /**
54
+ * Set (overwrite) the data for every item matching a query.
55
+ * - Two-step by default: resolves the query to its matching items with `getQuery()`, then sets each one concurrently with `setItem()`. Engine providers override this with a native query write where one exists (e.g. SQL `UPDATE … WHERE`).
56
+ * - Not guaranteed atomic: the resolve and the writes are separate steps, so wrap the call in `transact()` when atomicity matters.
57
+ *
58
+ * @param collection Collection to write to.
59
+ * @param query Query selecting the items to set.
60
+ * @param data Full data to store for each matching item.
61
+ * @example await provider.setQuery(users, { age: 40 }, { active: true });
62
+ * @see https://shelving.cc/db/DBProvider/setQuery
63
+ */
64
+ async setQuery(collection, query, data) {
65
+ const items = await this.getQuery(collection, query);
66
+ await awaitValues(...items.map(({ id }) => this.setItem(collection, id, data)));
67
+ }
68
+ /**
69
+ * Apply partial updates to every item matching a query.
70
+ * - Two-step by default: resolves the query to its matching items with `getQuery()`, then updates each one concurrently with `updateItem()`. Engine providers override this with a native query write where one exists (e.g. SQL `UPDATE … WHERE`).
71
+ * - Not guaranteed atomic: the resolve and the writes are separate steps, so wrap the call in `transact()` when atomicity matters.
72
+ *
73
+ * @param collection Collection to write to.
74
+ * @param query Query selecting the items to update.
75
+ * @param updates Updates to apply to each matching item.
76
+ * @example await provider.updateQuery(users, { age: 40 }, { active: true });
77
+ * @see https://shelving.cc/db/DBProvider/updateQuery
78
+ */
79
+ async updateQuery(collection, query, updates) {
80
+ const items = await this.getQuery(collection, query);
81
+ await awaitValues(...items.map(({ id }) => this.updateItem(collection, id, updates)));
82
+ }
83
+ /**
84
+ * Delete every item matching a query.
85
+ * - Two-step by default: resolves the query to its matching items with `getQuery()`, then deletes each one concurrently with `deleteItem()`. Engine providers override this with a native query write where one exists (e.g. SQL `DELETE … WHERE`).
86
+ * - Not guaranteed atomic: the resolve and the writes are separate steps, so wrap the call in `transact()` when atomicity matters.
87
+ *
88
+ * @param collection Collection to delete from.
89
+ * @param query Query selecting the items to delete.
90
+ * @example await provider.deleteQuery(users, { active: false });
91
+ * @see https://shelving.cc/db/DBProvider/deleteQuery
92
+ */
93
+ async deleteQuery(collection, query) {
94
+ const items = await this.getQuery(collection, query);
95
+ await awaitValues(...items.map(({ id }) => this.deleteItem(collection, id)));
96
+ }
52
97
  /**
53
98
  * Get the first item matching a query, or `undefined` if there are none.
54
99
  *
@@ -7,15 +7,15 @@ import { type SQLFragment, SQLProvider } from "./SQLProvider.js";
7
7
  /**
8
8
  * Abstract PostgreSQL provider with JSONB function support for nested keys, array containment, and array mutations.
9
9
  *
10
- * @see https://shelving.cc/db/PostgreSQLProvider
10
+ * @see https://shelving.cc/db/PostgresProvider
11
11
  */
12
- export declare abstract class PostgreSQLProvider<I extends Identifier = Identifier, T extends Data = Data> extends SQLProvider<I, T> {
12
+ export declare abstract class PostgresProvider<I extends Identifier = Identifier, T extends Data = Data> extends SQLProvider<I, T> {
13
13
  /** Get the Postgres JSONB path for the nested segments of a key, e.g. `{"b","c"}`. */
14
14
  private sqlPath;
15
15
  /** Extract via the Postgres `#>>` JSONB operator for nested keys, e.g. `"a" #>> {"b"}`. */
16
16
  sqlExtract(key: Segments): SQLFragment;
17
17
  /** Add Postgres JSONB support for nested keys and `with` / `omit` array mutations. */
18
18
  sqlUpdate(update: Update): SQLFragment;
19
- /** Add Postgres JSONB support for `contains` filters and deeply-nested queries. */
19
+ /** Add Postgres JSONB support for `contains` filters, and boolean literals for empty `in` / `out` filters. */
20
20
  sqlFilter(filter: QueryFilter): SQLFragment;
21
21
  }
@@ -2,9 +2,9 @@ import { SQLProvider } from "./SQLProvider.js";
2
2
  /**
3
3
  * Abstract PostgreSQL provider with JSONB function support for nested keys, array containment, and array mutations.
4
4
  *
5
- * @see https://shelving.cc/db/PostgreSQLProvider
5
+ * @see https://shelving.cc/db/PostgresProvider
6
6
  */
7
- export class PostgreSQLProvider extends SQLProvider {
7
+ export class PostgresProvider extends SQLProvider {
8
8
  /** Get the Postgres JSONB path for the nested segments of a key, e.g. `{"b","c"}`. */
9
9
  sqlPath(key) {
10
10
  return this.sqlConcat(key.slice(1).map(k => this.sqlIdentifier(k)), ",", "{", "}");
@@ -65,12 +65,17 @@ export class PostgreSQLProvider extends SQLProvider {
65
65
  }
66
66
  return super.sqlUpdate(update);
67
67
  }
68
- /** Add Postgres JSONB support for `contains` filters and deeply-nested queries. */
68
+ /** Add Postgres JSONB support for `contains` filters, and boolean literals for empty `in` / `out` filters. */
69
69
  sqlFilter(filter) {
70
70
  const { key, operator, value } = filter;
71
71
  // Implement `contains` filters.
72
72
  if (operator === "contains")
73
73
  return this.sql `${this.sqlExtract(key)} @> ${[value]}`;
74
+ // Postgres `WHERE` requires a boolean, so empty `in` / `out` filters can't use the base class's `0` / `1` integer literals.
75
+ if (operator === "in" && !value.length)
76
+ return this.sql `FALSE`;
77
+ if (operator === "out" && !value.length)
78
+ return this.sql `TRUE`;
74
79
  return super.sqlFilter(filter);
75
80
  }
76
81
  }
@@ -8,11 +8,16 @@ import type { Collection } from "../collection/Collection.js";
8
8
  import { DBProvider } from "./DBProvider.js";
9
9
  /**
10
10
  * SQL fragment made from template strings plus embedded expressions, ready to be composed into a query.
11
+ *
12
+ * - Flattens eagerly: `SQLFragment` values are spliced inline at construction, so `values` only ever contains bindable parameters (or driver-specific identifier tokens) — never other fragments.
13
+ * - Concrete providers can therefore pass `strings` / `values` straight to their driver, and detect fragments with a plain `instanceof` check.
14
+ *
11
15
  * @see https://shelving.cc/db/SQLFragment
12
16
  */
13
- export interface SQLFragment {
17
+ export declare class SQLFragment {
14
18
  readonly strings: ImmutableArray<string>;
15
19
  readonly values: ImmutableArray<unknown>;
20
+ constructor(strings: ImmutableArray<string>, values: ImmutableArray<unknown>);
16
21
  }
17
22
  /**
18
23
  * Abstract database provider that implements CRUD and query operations by generating and executing SQL.
@@ -41,6 +46,7 @@ export declare abstract class SQLProvider<I extends Identifier = Identifier, T e
41
46
  setItem<II extends I, TT extends T>(collection: Collection<string, II, TT>, id: II, data: TT): Promise<void>;
42
47
  updateItem<II extends I, TT extends T>(collection: Collection<string, II, TT>, id: II, updates: Updates<Item<II, TT>>): Promise<void>;
43
48
  deleteItem<II extends I, TT extends T>(collection: Collection<string, II, TT>, id: II): Promise<void>;
49
+ /** Counts via a subquery so a `$limit` in the query caps the counted rows rather than the (single) result row. */
44
50
  countQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query?: Query<Item<II, TT>>): Promise<number>;
45
51
  getQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query?: Query<Item<II, TT>>): Promise<Items<II, TT>>;
46
52
  /** Unsupported by SQL providers — always throws `UnsupportedError`. */
@@ -3,6 +3,39 @@ import { UnsupportedError } from "../../error/UnsupportedError.js";
3
3
  import { getQueryFilters, getQueryLimit, getQueryOrders } from "../../util/query.js";
4
4
  import { getUpdates } from "../../util/update.js";
5
5
  import { DBProvider } from "./DBProvider.js";
6
+ /**
7
+ * SQL fragment made from template strings plus embedded expressions, ready to be composed into a query.
8
+ *
9
+ * - Flattens eagerly: `SQLFragment` values are spliced inline at construction, so `values` only ever contains bindable parameters (or driver-specific identifier tokens) — never other fragments.
10
+ * - Concrete providers can therefore pass `strings` / `values` straight to their driver, and detect fragments with a plain `instanceof` check.
11
+ *
12
+ * @see https://shelving.cc/db/SQLFragment
13
+ */
14
+ export class SQLFragment {
15
+ strings;
16
+ values;
17
+ constructor(strings, values) {
18
+ const outStrings = [strings[0] ?? ""];
19
+ const outValues = [];
20
+ for (const [i, value] of values.entries()) {
21
+ if (value instanceof SQLFragment) {
22
+ // Splice the (already flat) fragment inline.
23
+ outStrings[outStrings.length - 1] += value.strings[0] ?? "";
24
+ for (let n = 0; n < value.values.length; n++) {
25
+ outValues.push(value.values[n]);
26
+ outStrings.push(value.strings[n + 1] ?? "");
27
+ }
28
+ }
29
+ else {
30
+ outValues.push(value);
31
+ outStrings.push("");
32
+ }
33
+ outStrings[outStrings.length - 1] += strings[i + 1] ?? "";
34
+ }
35
+ this.strings = outStrings;
36
+ this.values = outValues;
37
+ }
38
+ }
6
39
  /**
7
40
  * Abstract database provider that implements CRUD and query operations by generating and executing SQL.
8
41
  *
@@ -52,10 +85,13 @@ export class SQLProvider extends DBProvider {
52
85
  async deleteItem(collection, id) {
53
86
  await this.exec `DELETE FROM ${this.sqlIdentifier(collection.name)} WHERE ${this.sqlIdentifier("id")} = ${id}`;
54
87
  }
88
+ /** Counts via a subquery so a `$limit` in the query caps the counted rows rather than the (single) result row. */
55
89
  async countQuery(collection, query) {
56
90
  const rows = await this.exec `
57
- SELECT COUNT(*) AS "count" FROM ${this.sqlIdentifier(collection.name)}
58
- ${query ? this.sqlClauses(query) : this.sql ``}
91
+ SELECT COUNT(*) AS "count" FROM (
92
+ SELECT 1 FROM ${this.sqlIdentifier(collection.name)}
93
+ ${query ? this.sqlClauses(query) : this.sql ``}
94
+ ) AS ${this.sqlIdentifier("items")}
59
95
  `;
60
96
  return rows[0]?.count ?? 0;
61
97
  }
@@ -87,7 +123,7 @@ export class SQLProvider extends DBProvider {
87
123
  * @see https://shelving.cc/db/SQLProvider/sql
88
124
  */
89
125
  sql(strings, ...values) {
90
- return { strings, values };
126
+ return new SQLFragment(strings, values);
91
127
  }
92
128
  /**
93
129
  * Define an SQL fragment for an escaped identifier, e.g. `"myTable"`.
@@ -97,7 +133,7 @@ export class SQLProvider extends DBProvider {
97
133
  * @see https://shelving.cc/db/SQLProvider/sqlIdentifier
98
134
  */
99
135
  sqlIdentifier(name) {
100
- return { strings: [_escapeIdentifier(name)], values: [] };
136
+ return new SQLFragment([_escapeIdentifier(name)], []);
101
137
  }
102
138
  /**
103
139
  * Define an SQL fragment that extracts a value at a key for comparison, e.g. `"a" #>> {"b","c"}` in Postgres.
@@ -125,7 +161,7 @@ export class SQLProvider extends DBProvider {
125
161
  */
126
162
  sqlConcat(values, separator = ", ", before = "", after = "") {
127
163
  const strings = [before, ...new Array(Math.max(0, values.length - 1)).fill(separator), after];
128
- return { strings, values };
164
+ return new SQLFragment(strings, values);
129
165
  }
130
166
  /**
131
167
  * Define an SQL fragment for setting a list of values, e.g. `"a" = 1, "b" = 2`.
@@ -4,17 +4,17 @@ import type { Query } from "../../util/query.js";
4
4
  import type { Sourceable } from "../../util/source.js";
5
5
  import type { Updates } from "../../util/update.js";
6
6
  import type { Collection } from "../collection/Collection.js";
7
- import type { DBProvider } from "./DBProvider.js";
7
+ import { DBProvider } from "./DBProvider.js";
8
8
  /**
9
- * Database provider that passes every operation straight through to a wrapped `source` provider.
9
+ * Database provider that passes every core operation straight through to a wrapped `source` provider.
10
10
  *
11
11
  * - Base for the layered `Through*Provider` family (validation, caching, logging, change tracking); subclasses override individual methods to add behaviour and call `super` to delegate.
12
- * - Query writes (`setQuery()`, `updateQuery()`, `deleteQuery()`) are two-step by default resolved to their matching items with `getQuery()`, then written per item through this provider's own item methods so wrapper behaviour applies to every implied write. Subclasses that don't need per-item behaviour override them to pass through to `source` directly.
12
+ * - Only the core operations delegate to `source` — derived reads (`DBProvider.requireItem()`, `DBProvider.getFirst()`, `DBProvider.requireFirst()`) and two-step query writes are inherited from `DBProvider`, so they route through this provider's own overridden methods and wrapper behaviour applies to everything they do. Wrappers that don't need per-item behaviour override the query writes to pass through to `source` directly.
13
13
  * - Exposes `source` and implements `Sourceable`, so wrapped providers can be discovered with `getSource()` / `requireSource()`.
14
14
  *
15
15
  * @see https://shelving.cc/db/ThroughDBProvider
16
16
  */
17
- export declare class ThroughDBProvider<I extends Identifier, T extends Data> implements DBProvider<I, T>, Sourceable<DBProvider<I, T>> {
17
+ export declare class ThroughDBProvider<I extends Identifier, T extends Data> extends DBProvider<I, T> implements Sourceable<DBProvider<I, T>> {
18
18
  /**
19
19
  * The wrapped source provider that every operation is delegated to.
20
20
  *
@@ -23,28 +23,15 @@ export declare class ThroughDBProvider<I extends Identifier, T extends Data> imp
23
23
  readonly source: DBProvider<I, T>;
24
24
  constructor(source: DBProvider<I, T>);
25
25
  getItem<II extends I, TT extends T>(collection: Collection<string, II, TT>, id: II): Promise<OptionalItem<II, TT>>;
26
- requireItem<II extends I, TT extends T>(collection: Collection<string, II, TT>, id: II): Promise<Item<II, TT>>;
27
26
  getItemSequence<II extends I, TT extends T>(collection: Collection<string, II, TT>, id: II): OptionalItemSequence<II, TT>;
28
27
  addItem<II extends I, TT extends T>(collection: Collection<string, II, TT>, data: TT): Promise<II>;
29
28
  setItem<II extends I, TT extends T>(collection: Collection<string, II, TT>, id: II, data: TT): Promise<void>;
30
29
  updateItem<II extends I, TT extends T>(collection: Collection<string, II, TT>, id: II, updates: Updates<Item<II, TT>>): Promise<void>;
31
30
  deleteItem<II extends I, TT extends T>(collection: Collection<string, II, TT>, id: II): Promise<void>;
31
+ /** Delegates to `source` so its native counting is kept (the base implementation would fetch the items and count them). */
32
32
  countQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query?: Query<Item<II, TT>>): Promise<number>;
33
33
  getQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query?: Query<Item<II, TT>>): Promise<Items<II, TT>>;
34
34
  getQuerySequence<II extends I, TT extends T>(collection: Collection<string, II, TT>, query?: Query<Item<II, TT>>): ItemsSequence<II, TT>;
35
- /**
36
- * Two-step: resolve the query to its matching items with `getQuery()`, then set each one with `setItem()`.
37
- * - Routes every implied write through this provider's own item methods, so wrapper behaviour applies to each item — the same theory as `transact()` re-wrapping the transaction provider.
38
- * - The per-item writes run concurrently (`awaitValues()`), so a batch over a remote source costs one round-trip of latency, not one per item.
39
- * - The resolve and the writes are separate steps, so this is only atomic inside `transact()`.
40
- */
41
- setQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query: Query<Item<II, TT>>, data: TT): Promise<void>;
42
- /** Two-step: resolve the query to its matching items with `getQuery()`, then update each one concurrently with `updateItem()` — see `ThroughDBProvider.setQuery()`. */
43
- updateQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query: Query<Item<II, TT>>, updates: Updates<TT>): Promise<void>;
44
- /** Two-step: resolve the query to its matching items with `getQuery()`, then delete each one concurrently with `deleteItem()` — see `ThroughDBProvider.setQuery()`. */
45
- deleteQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query: Query<Item<II, TT>>): Promise<void>;
46
- getFirst<II extends I, TT extends T>(collection: Collection<string, II, TT>, query: Query<Item<II, TT>>): Promise<OptionalItem<II, TT>>;
47
- requireFirst<II extends I, TT extends T>(collection: Collection<string, II, TT>, query: Query<Item<II, TT>>): Promise<Item<II, TT>>;
48
35
  transact<X>(callback: (provider: DBProvider<I, T>) => Promise<X>): Promise<X>;
49
36
  /** Clone this provider with different `source`. */
50
37
  cloneWith(source: DBProvider<I, T>): this;
@@ -1,15 +1,15 @@
1
- import { awaitValues } from "../../util/async.js";
2
1
  import { awaitDispose } from "../../util/dispose.js";
2
+ import { DBProvider } from "./DBProvider.js";
3
3
  /**
4
- * Database provider that passes every operation straight through to a wrapped `source` provider.
4
+ * Database provider that passes every core operation straight through to a wrapped `source` provider.
5
5
  *
6
6
  * - Base for the layered `Through*Provider` family (validation, caching, logging, change tracking); subclasses override individual methods to add behaviour and call `super` to delegate.
7
- * - Query writes (`setQuery()`, `updateQuery()`, `deleteQuery()`) are two-step by default resolved to their matching items with `getQuery()`, then written per item through this provider's own item methods so wrapper behaviour applies to every implied write. Subclasses that don't need per-item behaviour override them to pass through to `source` directly.
7
+ * - Only the core operations delegate to `source` — derived reads (`DBProvider.requireItem()`, `DBProvider.getFirst()`, `DBProvider.requireFirst()`) and two-step query writes are inherited from `DBProvider`, so they route through this provider's own overridden methods and wrapper behaviour applies to everything they do. Wrappers that don't need per-item behaviour override the query writes to pass through to `source` directly.
8
8
  * - Exposes `source` and implements `Sourceable`, so wrapped providers can be discovered with `getSource()` / `requireSource()`.
9
9
  *
10
10
  * @see https://shelving.cc/db/ThroughDBProvider
11
11
  */
12
- export class ThroughDBProvider {
12
+ export class ThroughDBProvider extends DBProvider {
13
13
  /**
14
14
  * The wrapped source provider that every operation is delegated to.
15
15
  *
@@ -17,14 +17,12 @@ export class ThroughDBProvider {
17
17
  */
18
18
  source;
19
19
  constructor(source) {
20
+ super();
20
21
  this.source = source;
21
22
  }
22
23
  getItem(collection, id) {
23
24
  return this.source.getItem(collection, id);
24
25
  }
25
- requireItem(collection, id) {
26
- return this.source.requireItem(collection, id);
27
- }
28
26
  getItemSequence(collection, id) {
29
27
  return this.source.getItemSequence(collection, id);
30
28
  }
@@ -40,6 +38,7 @@ export class ThroughDBProvider {
40
38
  deleteItem(collection, id) {
41
39
  return this.source.deleteItem(collection, id);
42
40
  }
41
+ /** Delegates to `source` so its native counting is kept (the base implementation would fetch the items and count them). */
43
42
  countQuery(collection, query) {
44
43
  return this.source.countQuery(collection, query);
45
44
  }
@@ -49,32 +48,6 @@ export class ThroughDBProvider {
49
48
  getQuerySequence(collection, query) {
50
49
  return this.source.getQuerySequence(collection, query);
51
50
  }
52
- /**
53
- * Two-step: resolve the query to its matching items with `getQuery()`, then set each one with `setItem()`.
54
- * - Routes every implied write through this provider's own item methods, so wrapper behaviour applies to each item — the same theory as `transact()` re-wrapping the transaction provider.
55
- * - The per-item writes run concurrently (`awaitValues()`), so a batch over a remote source costs one round-trip of latency, not one per item.
56
- * - The resolve and the writes are separate steps, so this is only atomic inside `transact()`.
57
- */
58
- async setQuery(collection, query, data) {
59
- const items = await this.getQuery(collection, query);
60
- await awaitValues(...items.map(({ id }) => this.setItem(collection, id, data)));
61
- }
62
- /** Two-step: resolve the query to its matching items with `getQuery()`, then update each one concurrently with `updateItem()` — see `ThroughDBProvider.setQuery()`. */
63
- async updateQuery(collection, query, updates) {
64
- const items = await this.getQuery(collection, query);
65
- await awaitValues(...items.map(({ id }) => this.updateItem(collection, id, updates)));
66
- }
67
- /** Two-step: resolve the query to its matching items with `getQuery()`, then delete each one concurrently with `deleteItem()` — see `ThroughDBProvider.setQuery()`. */
68
- async deleteQuery(collection, query) {
69
- const items = await this.getQuery(collection, query);
70
- await awaitValues(...items.map(({ id }) => this.deleteItem(collection, id)));
71
- }
72
- getFirst(collection, query) {
73
- return this.source.getFirst(collection, query);
74
- }
75
- requireFirst(collection, query) {
76
- return this.source.requireFirst(collection, query);
77
- }
78
51
  // Run the transaction against the wrapped `source` provider, keeping this provider's behaviour inside the transaction.
79
52
  transact(callback) {
80
53
  return this.source.transact(transaction => callback(this.cloneWith(transaction)));
@@ -85,6 +58,7 @@ export class ThroughDBProvider {
85
58
  }
86
59
  // Implement `AsyncDisposable`
87
60
  async [Symbol.asyncDispose]() {
88
- await awaitDispose(this.source);
61
+ await awaitDispose(this.source, // Dispose the source API provider.
62
+ super[Symbol.asyncDispose]());
89
63
  }
90
64
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shelving",
3
- "version": "1.278.0",
3
+ "version": "1.280.0",
4
4
  "author": "Dave Houlbrooke <dave@shax.com>",
5
5
  "repository": {
6
6
  "type": "git",
@@ -68,7 +68,8 @@
68
68
  "fix": "bun run --sequential fix:*",
69
69
  "fix:0:lint": "biome check --write .",
70
70
  "fix:1:style": "stylelint \"modules/**/*.css\" --fix",
71
- "test:firebase": "bunx firebase-tools emulators:exec --only firestore --project shelving-test \"bun test ./modules/firebase\"",
71
+ "firebase": "bunx firebase-tools emulators:exec --only firestore --project shelving-test \"bun test ./modules/firebase\"",
72
+ "postgres": "bun ./scripts/postgres.ts",
72
73
  "docs:build": "bun ./docs/build.tsx",
73
74
  "docs:start": "bun ./docs/start.tsx",
74
75
  "build": "bun run --sequential build:*",
@@ -1,21 +0,0 @@
1
- import type { SQL } from "bun";
2
- import { PostgreSQLProvider, type SQLFragment } from "../db/index.js";
3
- import type { ImmutableArray } from "../util/array.js";
4
- import type { Data } from "../util/data.js";
5
- import type { Identifier } from "../util/item.js";
6
- /**
7
- * PostgreSQL database provider backed by Bun's built-in `Bun.SQL` driver.
8
- *
9
- * Implements the `PostgreSQLProvider` SQL abstraction by executing tagged-template queries against a `Bun.SQL` connection.
10
- * - Identifiers are escaped through `Bun.SQL`'s own `sql()` helper rather than naive string quoting, which is more secure.
11
- * - Requires the `bun` peer dependency and a running Bun environment.
12
- *
13
- * @see https://shelving.cc/bun/BunPostgreSQLProvider
14
- */
15
- export declare class BunPostgreSQLProvider<I extends Identifier = Identifier, T extends Data = Data> extends PostgreSQLProvider<I, T> {
16
- private _sql;
17
- constructor(sql: SQL);
18
- exec<X extends Data>(strings: TemplateStringsArray, ...values: ImmutableArray<unknown>): Promise<ImmutableArray<X>>;
19
- /** Escapes the identifier via `Bun.SQL`'s first-class `sql()` wrapping rather than manual quoting, which is more secure. */
20
- sqlIdentifier(name: string): SQLFragment;
21
- }
@@ -1,24 +0,0 @@
1
- import { PostgreSQLProvider } from "../db/index.js";
2
- /**
3
- * PostgreSQL database provider backed by Bun's built-in `Bun.SQL` driver.
4
- *
5
- * Implements the `PostgreSQLProvider` SQL abstraction by executing tagged-template queries against a `Bun.SQL` connection.
6
- * - Identifiers are escaped through `Bun.SQL`'s own `sql()` helper rather than naive string quoting, which is more secure.
7
- * - Requires the `bun` peer dependency and a running Bun environment.
8
- *
9
- * @see https://shelving.cc/bun/BunPostgreSQLProvider
10
- */
11
- export class BunPostgreSQLProvider extends PostgreSQLProvider {
12
- _sql;
13
- constructor(sql) {
14
- super();
15
- this._sql = sql;
16
- }
17
- exec(strings, ...values) {
18
- return this._sql(strings, ...values);
19
- }
20
- /** Escapes the identifier via `Bun.SQL`'s first-class `sql()` wrapping rather than manual quoting, which is more secure. */
21
- sqlIdentifier(name) {
22
- return this.sql `${this._sql(name)}`;
23
- }
24
- }