@coffre/db 0.14.4 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. package/README.md +2 -1
  2. package/dist/change-records-_fKvahvs.js +55 -0
  3. package/dist/connect.d.ts +22 -10
  4. package/dist/connect.js +74 -12
  5. package/dist/database-CVteLhp1.d.ts +410 -0
  6. package/dist/{database-BadNeeN-.js → database-D55ENPX-.js} +46 -25
  7. package/dist/dialect.d.ts +4 -3
  8. package/dist/dialect.js +14 -5
  9. package/dist/grants.d.ts +4 -4
  10. package/dist/grants.js +4 -4
  11. package/dist/hyperdrive.d.ts +48 -23
  12. package/dist/hyperdrive.js +168 -91
  13. package/dist/index.d.ts +3 -3
  14. package/dist/index.js +1 -1
  15. package/dist/log-DaD7fyUq.d.ts +722 -0
  16. package/dist/log.d.ts +2 -596
  17. package/dist/log.js +224 -10
  18. package/dist/migrate.d.ts +43 -3
  19. package/dist/migrate.js +149 -29
  20. package/dist/migrations/postgres/0021_prune_vault_releases.sql +19 -0
  21. package/dist/migrations/postgres/0022_log_015.sql +672 -0
  22. package/dist/migrations/postgres/0023_change_path.sql +153 -0
  23. package/dist/migrations/postgres/0024_use_015.sql +556 -0
  24. package/dist/migrations/postgres/0025_app_change.sql +221 -0
  25. package/dist/migrations/postgres/0026_issue_lock.sql +106 -0
  26. package/dist/migrations/postgres/0027_retention.sql +118 -0
  27. package/dist/migrations/postgres/0028_use_stalls.sql +594 -0
  28. package/dist/migrations/postgres/0029_use_objects.sql +887 -0
  29. package/dist/migrations/postgres/0030_unarchive_restores.sql +259 -0
  30. package/dist/migrations/postgres/0031_app_read.sql +65 -0
  31. package/dist/migrations/postgres/0032_secrets_live_index.sql +4 -0
  32. package/dist/migrations/postgres/0033_use_generations.sql +582 -0
  33. package/dist/migrations/postgres/0034_vault_credentials.sql +153 -0
  34. package/dist/migrations/postgres/0035_first_days.sql +3 -0
  35. package/dist/migrations/postgres/0036_vault_issues.sql +91 -0
  36. package/dist/migrations/postgres/0037_vault_bindings.sql +271 -0
  37. package/dist/migrations/postgres/0040_binding_origin.sql +159 -0
  38. package/dist/migrations/postgres/0041_cutover.sql +132 -0
  39. package/dist/migrations/postgres/0042_sign_in_index.sql +7 -0
  40. package/dist/migrations/postgres/0043_issue_revoke_index.sql +4 -0
  41. package/dist/migrations/postgres/0044_use_by_name.sql +705 -0
  42. package/dist/migrations/postgres/0045_decide_first.sql +878 -0
  43. package/dist/migrations/postgres/0046_way_in_uses.sql +1149 -0
  44. package/dist/migrations/postgres/0047_cutover_finish.sql +268 -0
  45. package/dist/migrations/postgres/0048_use_batch.sql +59 -0
  46. package/dist/migrations/postgres/0049_reads.sql +171 -0
  47. package/dist/migrations/postgres/meta/0021_snapshot.json +3334 -0
  48. package/dist/migrations/postgres/meta/0022_snapshot.json +3831 -0
  49. package/dist/migrations/postgres/meta/0023_snapshot.json +3831 -0
  50. package/dist/migrations/postgres/meta/0024_snapshot.json +3831 -0
  51. package/dist/migrations/postgres/meta/0025_snapshot.json +3831 -0
  52. package/dist/migrations/postgres/meta/0026_snapshot.json +3831 -0
  53. package/dist/migrations/postgres/meta/0027_snapshot.json +3853 -0
  54. package/dist/migrations/postgres/meta/0028_snapshot.json +3853 -0
  55. package/dist/migrations/postgres/meta/0029_snapshot.json +3859 -0
  56. package/dist/migrations/postgres/meta/0030_snapshot.json +3859 -0
  57. package/dist/migrations/postgres/meta/0031_snapshot.json +3859 -0
  58. package/dist/migrations/postgres/meta/0032_snapshot.json +3875 -0
  59. package/dist/migrations/postgres/meta/0033_snapshot.json +3875 -0
  60. package/dist/migrations/postgres/meta/0034_snapshot.json +3875 -0
  61. package/dist/migrations/postgres/meta/0035_snapshot.json +3875 -0
  62. package/dist/migrations/postgres/meta/0036_snapshot.json +3875 -0
  63. package/dist/migrations/postgres/meta/0037_snapshot.json +3875 -0
  64. package/dist/migrations/postgres/meta/0038_snapshot.json +3881 -0
  65. package/dist/migrations/postgres/meta/0039_snapshot.json +3881 -0
  66. package/dist/migrations/postgres/meta/0040_snapshot.json +3931 -0
  67. package/dist/migrations/postgres/meta/0041_snapshot.json +3981 -0
  68. package/dist/migrations/postgres/meta/0042_snapshot.json +3981 -0
  69. package/dist/migrations/postgres/meta/0043_snapshot.json +3981 -0
  70. package/dist/migrations/postgres/meta/0044_snapshot.json +3991 -0
  71. package/dist/migrations/postgres/meta/0045_snapshot.json +3997 -0
  72. package/dist/migrations/postgres/meta/0046_snapshot.json +3997 -0
  73. package/dist/migrations/postgres/meta/0047_snapshot.json +4234 -0
  74. package/dist/migrations/postgres/meta/_journal.json +189 -0
  75. package/dist/migrations/sqlite/0018_log_015.sql +84 -0
  76. package/dist/migrations/sqlite/0019_retention.sql +2 -0
  77. package/dist/migrations/sqlite/0020_ended_references.sql +1 -0
  78. package/dist/migrations/sqlite/0021_secrets_live_index.sql +1 -0
  79. package/dist/migrations/sqlite/0022_binding_origin.sql +1 -0
  80. package/dist/migrations/sqlite/0023_sign_in_index.sql +2 -0
  81. package/dist/migrations/sqlite/0024_issue_revoke_index.sql +2 -0
  82. package/dist/migrations/sqlite/0025_way_in_uses.sql +42 -0
  83. package/dist/migrations/sqlite/0026_cutover_finish.sql +11 -0
  84. package/dist/migrations/sqlite/0027_reads.sql +38 -0
  85. package/dist/migrations/sqlite/meta/0018_snapshot.json +3529 -0
  86. package/dist/migrations/sqlite/meta/0019_snapshot.json +3538 -0
  87. package/dist/migrations/sqlite/meta/0020_snapshot.json +3545 -0
  88. package/dist/migrations/sqlite/meta/0021_snapshot.json +3553 -0
  89. package/dist/migrations/sqlite/meta/0022_snapshot.json +3560 -0
  90. package/dist/migrations/sqlite/meta/0023_snapshot.json +3579 -0
  91. package/dist/migrations/sqlite/meta/0024_snapshot.json +3598 -0
  92. package/dist/migrations/sqlite/meta/0025_snapshot.json +3609 -0
  93. package/dist/migrations/sqlite/meta/0026_snapshot.json +3616 -0
  94. package/dist/migrations/sqlite/meta/0027_snapshot.json +3834 -0
  95. package/dist/migrations/sqlite/meta/_journal.json +70 -0
  96. package/dist/platform-Y3gICK6n.d.ts +58 -0
  97. package/dist/portable.d.ts +2 -2
  98. package/dist/postgres-BI3xTnyw.js +1718 -0
  99. package/dist/postgres-platform-node.d.ts +14 -0
  100. package/dist/postgres-platform-node.js +21 -0
  101. package/dist/postgres-platform-workerd.d.ts +61 -0
  102. package/dist/postgres-platform-workerd.js +163 -0
  103. package/dist/{schema-z8GcN_Xu.d.ts → schema-DgoPqqsI.d.ts} +1329 -9
  104. package/dist/schema-sqlite.d.ts +1308 -8
  105. package/dist/schema-sqlite.js +156 -20
  106. package/dist/schema-version.d.ts +1 -1
  107. package/dist/schema-version.js +261 -2
  108. package/dist/schema.d.ts +2 -2
  109. package/dist/schema.js +229 -22
  110. package/package.json +13 -5
  111. package/src/active-subject.ts +9 -0
  112. package/src/change-records.ts +38 -0
  113. package/src/connect.ts +67 -13
  114. package/src/database.ts +67 -51
  115. package/src/dialect.ts +17 -8
  116. package/src/grants.ts +3 -3
  117. package/src/hyperdrive.ts +181 -104
  118. package/src/log.ts +244 -11
  119. package/src/migrate.ts +157 -30
  120. package/src/migrations/postgres/0021_prune_vault_releases.sql +19 -0
  121. package/src/migrations/postgres/0022_log_015.sql +672 -0
  122. package/src/migrations/postgres/0023_change_path.sql +153 -0
  123. package/src/migrations/postgres/0024_use_015.sql +556 -0
  124. package/src/migrations/postgres/0025_app_change.sql +221 -0
  125. package/src/migrations/postgres/0026_issue_lock.sql +106 -0
  126. package/src/migrations/postgres/0027_retention.sql +118 -0
  127. package/src/migrations/postgres/0028_use_stalls.sql +594 -0
  128. package/src/migrations/postgres/0029_use_objects.sql +887 -0
  129. package/src/migrations/postgres/0030_unarchive_restores.sql +259 -0
  130. package/src/migrations/postgres/0031_app_read.sql +65 -0
  131. package/src/migrations/postgres/0032_secrets_live_index.sql +4 -0
  132. package/src/migrations/postgres/0033_use_generations.sql +582 -0
  133. package/src/migrations/postgres/0034_vault_credentials.sql +153 -0
  134. package/src/migrations/postgres/0035_first_days.sql +3 -0
  135. package/src/migrations/postgres/0036_vault_issues.sql +91 -0
  136. package/src/migrations/postgres/0037_vault_bindings.sql +271 -0
  137. package/src/migrations/postgres/0040_binding_origin.sql +159 -0
  138. package/src/migrations/postgres/0041_cutover.sql +132 -0
  139. package/src/migrations/postgres/0042_sign_in_index.sql +7 -0
  140. package/src/migrations/postgres/0043_issue_revoke_index.sql +4 -0
  141. package/src/migrations/postgres/0044_use_by_name.sql +705 -0
  142. package/src/migrations/postgres/0045_decide_first.sql +878 -0
  143. package/src/migrations/postgres/0046_way_in_uses.sql +1149 -0
  144. package/src/migrations/postgres/0047_cutover_finish.sql +268 -0
  145. package/src/migrations/postgres/0048_use_batch.sql +59 -0
  146. package/src/migrations/postgres/0049_reads.sql +171 -0
  147. package/src/migrations/postgres/meta/0021_snapshot.json +3334 -0
  148. package/src/migrations/postgres/meta/0022_snapshot.json +3831 -0
  149. package/src/migrations/postgres/meta/0023_snapshot.json +3831 -0
  150. package/src/migrations/postgres/meta/0024_snapshot.json +3831 -0
  151. package/src/migrations/postgres/meta/0025_snapshot.json +3831 -0
  152. package/src/migrations/postgres/meta/0026_snapshot.json +3831 -0
  153. package/src/migrations/postgres/meta/0027_snapshot.json +3853 -0
  154. package/src/migrations/postgres/meta/0028_snapshot.json +3853 -0
  155. package/src/migrations/postgres/meta/0029_snapshot.json +3859 -0
  156. package/src/migrations/postgres/meta/0030_snapshot.json +3859 -0
  157. package/src/migrations/postgres/meta/0031_snapshot.json +3859 -0
  158. package/src/migrations/postgres/meta/0032_snapshot.json +3875 -0
  159. package/src/migrations/postgres/meta/0033_snapshot.json +3875 -0
  160. package/src/migrations/postgres/meta/0034_snapshot.json +3875 -0
  161. package/src/migrations/postgres/meta/0035_snapshot.json +3875 -0
  162. package/src/migrations/postgres/meta/0036_snapshot.json +3875 -0
  163. package/src/migrations/postgres/meta/0037_snapshot.json +3875 -0
  164. package/src/migrations/postgres/meta/0038_snapshot.json +3881 -0
  165. package/src/migrations/postgres/meta/0039_snapshot.json +3881 -0
  166. package/src/migrations/postgres/meta/0040_snapshot.json +3931 -0
  167. package/src/migrations/postgres/meta/0041_snapshot.json +3981 -0
  168. package/src/migrations/postgres/meta/0042_snapshot.json +3981 -0
  169. package/src/migrations/postgres/meta/0043_snapshot.json +3981 -0
  170. package/src/migrations/postgres/meta/0044_snapshot.json +3991 -0
  171. package/src/migrations/postgres/meta/0045_snapshot.json +3997 -0
  172. package/src/migrations/postgres/meta/0046_snapshot.json +3997 -0
  173. package/src/migrations/postgres/meta/0047_snapshot.json +4234 -0
  174. package/src/migrations/postgres/meta/_journal.json +189 -0
  175. package/src/migrations/sqlite/0018_log_015.sql +84 -0
  176. package/src/migrations/sqlite/0019_retention.sql +2 -0
  177. package/src/migrations/sqlite/0020_ended_references.sql +1 -0
  178. package/src/migrations/sqlite/0021_secrets_live_index.sql +1 -0
  179. package/src/migrations/sqlite/0022_binding_origin.sql +1 -0
  180. package/src/migrations/sqlite/0023_sign_in_index.sql +2 -0
  181. package/src/migrations/sqlite/0024_issue_revoke_index.sql +2 -0
  182. package/src/migrations/sqlite/0025_way_in_uses.sql +42 -0
  183. package/src/migrations/sqlite/0026_cutover_finish.sql +11 -0
  184. package/src/migrations/sqlite/0027_reads.sql +38 -0
  185. package/src/migrations/sqlite/meta/0018_snapshot.json +3529 -0
  186. package/src/migrations/sqlite/meta/0019_snapshot.json +3538 -0
  187. package/src/migrations/sqlite/meta/0020_snapshot.json +3545 -0
  188. package/src/migrations/sqlite/meta/0021_snapshot.json +3553 -0
  189. package/src/migrations/sqlite/meta/0022_snapshot.json +3560 -0
  190. package/src/migrations/sqlite/meta/0023_snapshot.json +3579 -0
  191. package/src/migrations/sqlite/meta/0024_snapshot.json +3598 -0
  192. package/src/migrations/sqlite/meta/0025_snapshot.json +3609 -0
  193. package/src/migrations/sqlite/meta/0026_snapshot.json +3616 -0
  194. package/src/migrations/sqlite/meta/0027_snapshot.json +3834 -0
  195. package/src/migrations/sqlite/meta/_journal.json +70 -0
  196. package/src/postgres-js/README.md +183 -0
  197. package/src/postgres-js/UNLICENSE +24 -0
  198. package/src/postgres-js/bytes.ts +89 -0
  199. package/src/postgres-js/connection.ts +882 -0
  200. package/src/postgres-js/errors.ts +84 -0
  201. package/src/postgres-js/index.ts +516 -0
  202. package/src/postgres-js/platform.node.ts +29 -0
  203. package/src/postgres-js/platform.ts +60 -0
  204. package/src/postgres-js/platform.workerd.ts +225 -0
  205. package/src/postgres-js/query.ts +123 -0
  206. package/src/postgres-js/queue.ts +42 -0
  207. package/src/postgres-js/result.ts +46 -0
  208. package/src/postgres-js/types.ts +52 -0
  209. package/src/postgres.ts +375 -26
  210. package/src/schema-version.ts +2 -2
  211. package/src/schema.sqlite.ts +200 -9
  212. package/src/schema.ts +294 -8
  213. package/src/verify-runtime.ts +18 -29
  214. package/LICENSE +0 -21
  215. package/dist/database-CFhTnnzM.d.ts +0 -73
  216. package/dist/postgres-Bmar8y0q.js +0 -29
  217. package/dist/relations-BfR3JjT1.js +0 -38
  218. package/src/relations.ts +0 -40
package/src/database.ts CHANGED
@@ -1,43 +1,31 @@
1
1
  import { AsyncLocalStorage } from 'node:async_hooks';
2
2
 
3
- import {
4
- createTableRelationsHelpers,
5
- extractTablesRelationalConfig,
6
- getTableName,
7
- is,
8
- Table,
9
- type ExtractTablesWithRelations,
10
- type RelationalSchemaConfig,
11
- } from 'drizzle-orm';
12
- import { NodePgDatabase, NodePgDriver } from 'drizzle-orm/node-postgres';
13
- import { PgDialect } from 'drizzle-orm/pg-core';
14
- import type pg from 'pg';
3
+ import { getTableName, is, Table, type Assume, type RelationalSchemaConfig } from 'drizzle-orm';
4
+ import { PgDatabase, PgDialect, type PgQueryResultHKT, type PgTransactionConfig } from 'drizzle-orm/pg-core';
5
+ import { PostgresJsSession, PostgresJsTransaction } from 'drizzle-orm/postgres-js/session';
15
6
 
16
7
  import { trackCommits } from './commits.ts';
8
+ import type { Row, RowList } from './postgres-js/index.ts';
9
+ import { textTypes, type PostgresClient } from './postgres.ts';
17
10
  import * as schema from './schema.ts';
18
11
 
19
12
  export { schema };
13
+ export type { PostgresClient } from './postgres.ts';
14
+
15
+ /** What a hand-written statement answers on Postgres: its rows, with `count` and `command`. */
16
+ export interface QueryResult extends PgQueryResultHKT {
17
+ type: RowList<Assume<this['row'], Row>[]>;
18
+ }
20
19
 
21
20
  /**
22
21
  * A database, typed as Postgres whichever it is; see portable.ts for why
23
22
  * that holds. connect.ts opens one of each from a URL.
24
23
  */
25
- export type Database = NodePgDatabase<typeof schema>;
24
+ export type Database = PgDatabase<QueryResult, typeof schema, NoRelations>;
26
25
  export type Transaction = Parameters<Parameters<Database['transaction']>[0]>[0];
27
26
  /** Anything a read can run on: the database, or an open transaction. */
28
27
  export type Queryable = Database | Transaction;
29
28
 
30
- /**
31
- * What Drizzle needs from a connection pool. A `pg.Pool` fits, and so does the
32
- * per-invocation Hyperdrive pool, whose class name ends in `Pool` for the
33
- * same reason: that is how Drizzle knows to check out a client for a
34
- * transaction rather than run BEGIN on a shared connection.
35
- */
36
- export type PoolLike = pg.Pool | {
37
- query: pg.Pool['query'];
38
- connect: () => Promise<Pick<pg.PoolClient, 'query'> & { release: () => void }>;
39
- };
40
-
41
29
  /**
42
30
  * A query made on the database from inside one of its own transactions,
43
31
  * rather than through the transaction. With one connection per call, as on
@@ -52,23 +40,28 @@ export class QueryOutsideTransaction extends Error {
52
40
  }
53
41
  }
54
42
 
55
- /** The transaction a flow of work runs inside, with the pool it holds, while it is open. */
56
- const inside = new AsyncLocalStorage<{ pool: PoolLike; open: boolean }>();
43
+ /** The transaction a flow of work runs inside, with the client it holds, while it is open. */
44
+ const inside = new AsyncLocalStorage<{ client: Client; open: boolean }>();
45
+
46
+ /** What Drizzle asks of the client: a statement, or a transaction, which postgres.js runs on a connection it holds. */
47
+ const ASKS = new Set<PropertyKey>(['unsafe', 'begin', 'reserve']);
57
48
 
58
49
  /**
59
- * `pool`, refusing a query or a transaction asked from inside a transaction
50
+ * `client`, refusing a query or a transaction asked from inside a transaction
60
51
  * it is already running. Work from any other flow, a concurrent request's,
61
52
  * or what the transaction left running once its work was done, goes on as before.
62
53
  */
63
- function guarded(pool: PoolLike): PoolLike {
64
- return new Proxy(pool, {
54
+ function guarded(client: Client): Client {
55
+ const refuse = (target: Client) => {
56
+ const open = inside.getStore();
57
+ if (open?.open === true && open.client === target) throw new QueryOutsideTransaction();
58
+ };
59
+ return new Proxy(client, {
65
60
  get(target, key) {
66
61
  const value: unknown = Reflect.get(target, key, target);
67
- if (typeof value !== 'function') return value;
68
- if (key !== 'query' && key !== 'connect') return value.bind(target);
62
+ if (typeof value !== 'function' || !ASKS.has(key)) return value;
69
63
  return (...args: unknown[]) => {
70
- const open = inside.getStore();
71
- if (open?.open === true && open.pool === target) throw new QueryOutsideTransaction();
64
+ refuse(target);
72
65
  return (value as (...args: unknown[]) => unknown).apply(target, args);
73
66
  };
74
67
  },
@@ -84,34 +77,57 @@ function brokenRule(error: unknown): QueryOutsideTransaction | undefined {
84
77
  }
85
78
 
86
79
  /**
87
- * What Drizzle makes of the schema, which `drizzle(pool, { schema })` works
88
- * out again from every table at each call: on Workers, a database is made
89
- * per call, and this was a fifth of a vault call's CPU. The schema never
90
- * changes, so it is worked out once per process or isolate, a settled value.
80
+ * Coffre runs no relational query (`db.query.*`), and Drizzle's work out of
81
+ * them, `extractTablesRelationalConfig` and a query builder per table for
82
+ * every database, was 4 to 11 ms of a cold isolate's CPU and 0.1 ms of each
83
+ * later call on Workers, where a database is made per call. So the database
84
+ * has none: `db.query` is empty, in type and at run time, and a `db.query.x`
85
+ * fails the typecheck. Only the tables stay, for `tablesOf`.
86
+ */
87
+ type NoRelations = Record<never, never>;
88
+ type Relational = RelationalSchemaConfig<NoRelations>;
89
+ const relational: Relational = { fullSchema: schema, schema: {}, tableNamesMap: {} };
90
+
91
+ /**
92
+ * Drizzle's session for postgres.js, but for a transaction's options, which
93
+ * go in its BEGIN, as node-postgres's session sends them, rather than in a
94
+ * SET TRANSACTION of their own: a round trip less for a snapshot read.
91
95
  */
92
- type Relational = RelationalSchemaConfig<ExtractTablesWithRelations<typeof schema>>;
93
- let relational: Relational | undefined;
94
-
95
- function relationalSchema(): Relational {
96
- if (relational === undefined) {
97
- const { tables, tableNamesMap } = extractTablesRelationalConfig(schema, createTableRelationsHelpers);
98
- // Drizzle's own construct passes the same untyped tables on as these.
99
- relational = { fullSchema: schema, schema: tables as Relational['schema'], tableNamesMap };
96
+ class Session extends PostgresJsSession<PostgresClient, typeof schema, NoRelations> {
97
+ override transaction<T>(work: (tx: PostgresJsTransaction<typeof schema, NoRelations>) => Promise<T>, config?: PgTransactionConfig): Promise<T> {
98
+ const { dialect, schema: tables, options } = this as unknown as { dialect: PgDialect; schema: Relational; options: object };
99
+ return this.client.begin(config === undefined ? '' : transactionOptions(config), (client) =>
100
+ work(new PostgresJsTransaction(dialect, new PostgresJsSession(client, dialect, tables, options), tables))) as Promise<T>;
100
101
  }
101
- return relational;
102
102
  }
103
103
 
104
- export function createDatabase(pool: PoolLike): Database {
105
- // What `drizzle(pool, { schema })` builds, but for the schema's config, made once.
104
+ /** What follows BEGIN for `config`, as Drizzle writes it for SET TRANSACTION. */
105
+ function transactionOptions(config: PgTransactionConfig): string {
106
+ return [
107
+ config.isolationLevel === undefined ? null : `isolation level ${config.isolationLevel}`,
108
+ config.accessMode ?? null,
109
+ config.deferrable === undefined ? null : config.deferrable ? 'deferrable' : 'not deferrable',
110
+ ].filter((option) => option !== null).join(' ');
111
+ }
112
+
113
+ /**
114
+ * What a database runs on: a postgres.js client (`postgresClient`), or
115
+ * Workers' `HyperdrivePool`, which makes them. Drizzle asks it for
116
+ * statements (`unsafe`) and transactions (`begin`).
117
+ */
118
+ export type Client = Pick<PostgresClient, 'unsafe' | 'begin'>;
119
+
120
+ export function createDatabase(client: Client): Database {
121
+ // What `drizzle(client, { schema })` sets up, but with no relational queries.
122
+ if ('options' in client) textTypes(client as PostgresClient);
106
123
  const dialect = new PgDialect();
107
- const config = relationalSchema();
108
- const db: Database = new NodePgDatabase(dialect, new NodePgDriver(guarded(pool) as pg.Pool, dialect).createSession(config), config);
124
+ const db = new PgDatabase(dialect, new Session(guarded(client) as PostgresClient, dialect, relational), relational) as Database;
109
125
  // Each transaction's work runs knowing which one it is in, until that work is done. Not until
110
126
  // the COMMIT comes back: the transaction waits on nothing then, so what the work left running
111
127
  // may ask for a query, which waits for the transaction to end rather than for itself.
112
128
  const run = db.transaction.bind(db);
113
129
  db.transaction = (async (work, config) => {
114
- const open = { pool, open: true };
130
+ const open = { client, open: true };
115
131
  try {
116
132
  return await run(
117
133
  (tx) =>
package/src/dialect.ts CHANGED
@@ -11,7 +11,7 @@ import type { Queryable } from './database.ts';
11
11
  * Postgres SQLite
12
12
  * isolation READ COMMITTED one writer at a time
13
13
  * lock a row SELECT … FOR UPDATE one writer at a time
14
- * rows changed rowCount rowsAffected
14
+ * rows changed count rowsAffected
15
15
  * duplicate key SQLSTATE 23505 SQLITE_CONSTRAINT_UNIQUE
16
16
  * clock CURRENT_TIMESTAMP strftime(…, 'now')
17
17
  * clock, in ms clock_timestamp() unixepoch('subsec')
@@ -56,7 +56,7 @@ export function forUpdate<Query extends { for: (strength: 'update' | 'no key upd
56
56
  * are the query's own, unmapped.
57
57
  */
58
58
  export async function rowsOf<Row>(db: Queryable, query: SQL): Promise<Row[]> {
59
- if (engineOf(db) === 'postgres') return (await (db as unknown as { execute(query: SQL): Promise<{ rows: Row[] }> }).execute(query)).rows;
59
+ if (engineOf(db) === 'postgres') return [...(await (db as unknown as { execute(query: SQL): Promise<Row[]> }).execute(query))];
60
60
  return (db as unknown as { all<R>(query: SQL): Promise<R[]> }).all<Row>(query);
61
61
  }
62
62
 
@@ -98,8 +98,8 @@ export function truth(condition: SQL): SQL<boolean> {
98
98
 
99
99
  /** How many rows an insert, update or delete touched. */
100
100
  export function changedRows(result: unknown): number {
101
- const header = result as { rowCount?: number | null; rowsAffected?: number };
102
- return header.rowCount ?? header.rowsAffected ?? 0;
101
+ const header = result as { count?: number | null; rowsAffected?: number };
102
+ return header.count ?? header.rowsAffected ?? 0;
103
103
  }
104
104
 
105
105
  const DUPLICATE_KEY = new Set(['23505', 'SQLITE_CONSTRAINT_UNIQUE', 'SQLITE_CONSTRAINT_PRIMARYKEY']);
@@ -114,16 +114,25 @@ export function isUniqueViolation(error: unknown): boolean {
114
114
  return false;
115
115
  }
116
116
 
117
- /** The network's codes for a server that is not there, and Postgres's for one that will not take the connection, or has none left (53300). */
118
- const UNREACHABLE = new Set(['ECONNREFUSED', 'ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND', 'EAI_AGAIN', 'EHOSTUNREACH', 'ENETUNREACH', 'EPIPE', '53300', '57P01', '57P02', '57P03']);
117
+ /**
118
+ * The network's codes for a server that is not there, postgres.js's for a
119
+ * connection lost, cut short or never made, and Postgres's for one that will
120
+ * not take the connection, or has none left (53300).
121
+ */
122
+ const UNREACHABLE = new Set([
123
+ 'ECONNREFUSED', 'ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND', 'EAI_AGAIN', 'EHOSTUNREACH', 'ENETUNREACH', 'EPIPE',
124
+ 'CONNECTION_CLOSED', 'CONNECTION_DESTROYED', 'CONNECTION_ENDED', 'CONNECT_TIMEOUT',
125
+ '53300', '57P01', '57P02', '57P03',
126
+ ]);
119
127
 
120
- /** Hyperdrive's words for having no connection to give, beside pg's and Workers' for one lost. */
128
+ /** Hyperdrive's words for having no connection to give, beside Workers' for one lost. */
121
129
  const UNREACHABLE_WORDS = /connection terminated|network connection lost|connect(ion)? timeout|open slot in the pool|acquire a connection from the pool|server connection attempt failed/i;
122
130
 
123
131
  /**
124
132
  * Whether a query failed because the database could not be reached or
125
133
  * dropped the connection: an outage, not an answer. Postgres's connection
126
- * exceptions are class 08; pg and Workers' sockets say so in words, and so
134
+ * exceptions are class 08; postgres.js says so in codes of its own, Workers'
135
+ * sockets in words, and so
127
136
  * does Hyperdrive when it has no connection to the database to give: every
128
137
  * one of its pool busy for 15 s ("Timed out while waiting for an open slot
129
138
  * in the pool", FATAL 58000; "Failed to acquire a connection from the
package/src/grants.ts CHANGED
@@ -307,9 +307,9 @@ export async function reportStands(db: Queryable, principal: string, check: Auth
307
307
  return report === 'unreached' || typeof report !== 'string';
308
308
  }
309
309
 
310
- /** The newest access entry about `principal` that `check` finds the vault's own. */
311
- export function newestAccess(db: Queryable, principal: string, check: Authenticity): Promise<Newest> {
312
- return newestAuthentic((limit, before) => accessEntriesAbout(db, principal, limit, before), check);
310
+ /** The newest access entry about `principal` that `check` finds the vault's own; at or below `through`, if given. */
311
+ export function newestAccess(db: Queryable, principal: string, check: Authenticity, through?: bigint): Promise<Newest> {
312
+ return newestAuthentic((limit, before) => accessEntriesAbout(db, principal, limit, before ?? (through === undefined ? undefined : through + 1n)), check);
313
313
  }
314
314
 
315
315
  /** Up to `limit` of the vault's allowed access entries about `principal` before `before` (the newest when undefined), newest first. */
package/src/hyperdrive.ts CHANGED
@@ -1,7 +1,6 @@
1
- import pg from 'pg';
2
-
3
- import type { PoolLike } from './database.ts';
4
- import { postgresConnection } from './postgres.ts';
1
+ import type { Client } from './database.ts';
2
+ import { plainRead } from './postgres-js/index.ts';
3
+ import { postgresClient, textTypes, type PostgresClient } from './postgres.ts';
5
4
 
6
5
  /** Postgres through a Hyperdrive binding: `postgres(env.HYPERDRIVE)`. */
7
6
  export type PostgresDatabase = { readonly engine: 'postgres'; readonly hyperdrive: { readonly connectionString: string } };
@@ -9,14 +8,24 @@ export type PostgresDatabase = { readonly engine: 'postgres'; readonly hyperdriv
9
8
  /** Postgres through Hyperdrive, the one database a Worker runs on, the app's and the vault's. */
10
9
  export function postgres(hyperdrive: { readonly connectionString: string }): PostgresDatabase {
11
10
  if (typeof hyperdrive?.connectionString !== 'string') {
12
- throw new Error('postgres() takes the Hyperdrive binding, e.g. postgres(env.HYPERDRIVE)');
11
+ throw new Error('postgres() was given no Hyperdrive binding: one the code names (HYPERDRIVE, VAULT_HYPERDRIVE, HOT_HYPERDRIVE) is missing from its wrangler.jsonc, or bound under another name');
13
12
  }
14
13
  return { engine: 'postgres', hyperdrive };
15
14
  }
16
15
 
16
+ /** A statement asked of the pool, sent at its turn once awaited or `execute`d, as postgres.js sends one. */
17
+ type Statement = Promise<unknown> & { values(): Statement; execute(): Statement };
18
+
19
+ /** A read asked while a group gathers (`groupReads`): sent with the others at its turn, and answered with its own rows. */
20
+ type Gathered = { text: string; values: unknown[]; arrays: () => boolean; resolve: (rows: unknown) => void; reject: (error: unknown) => void };
21
+
22
+
23
+ /** What the client refuses to write of a group, before anything of it is sent (`build`, postgres-js/connection.ts). */
24
+ const UNSENT = new Set(['GROUP_UNTYPED', 'MAX_PARAMETERS_EXCEEDED', 'UNDEFINED_VALUE']);
25
+
17
26
  /**
18
- * node-postgres for one call on Workers, through Hyperdrive: one client,
19
- * connected on the first query, which every query and transaction of the
27
+ * postgres.js for one call on Workers, through Hyperdrive: one connection,
28
+ * opened by the first query, which every query and transaction of the
20
29
  * call takes its turn on.
21
30
  *
22
31
  * Hyperdrive keeps the pool to the database; a Worker's connections belong
@@ -25,134 +34,202 @@ export function postgres(hyperdrive: { readonly connectionString: string }): Pos
25
34
  * once was now and then cancelled as hung, with nothing left pending, and
26
35
  * every connection costs a round trip before the query can be sent.
27
36
  *
28
- * The turns keep a transaction whole: queries go to the client in the order
29
- * they are asked, a transaction holds it from BEGIN until it is released,
30
- * and a query asked meanwhile waits until then, so nothing runs inside a
31
- * transaction it is not part of. So code inside a transaction must query
32
- * through the transaction: a query on the database there would wait for the
33
- * transaction to end, which waits for it. Neither the vault nor the server
34
- * does.
37
+ * The turns keep a transaction whole: statements go to the connection in
38
+ * the order they are asked, each once the one before it has answered; a
39
+ * transaction holds the connection from BEGIN until it ends, and a query
40
+ * asked meanwhile waits until then, so nothing runs inside a transaction it
41
+ * is not part of. So code inside a transaction must query through the
42
+ * transaction: a query on the database there would wait for the transaction
43
+ * to end, which waits for it. Neither the vault nor the server does.
44
+ *
45
+ * With `groupReads`, the plain reads (`select …`, outside a transaction)
46
+ * asked while the pool waits for their turn, or in the same task, take it
47
+ * together, sent under one Sync (`group` in postgres-js/index.ts): on
48
+ * Hyperdrive, which sends a client's Syncs on one at a time, a round trip
49
+ * for them all, where each alone took one. They run as one implicit
50
+ * transaction, so if one fails they all do, with its error; each is a
51
+ * read, so none had anything to keep. Anything else asked meanwhile, a
52
+ * write or a transaction, closes the group, and goes after it: statements
53
+ * still go in the order they are asked.
35
54
  *
36
- * The class name must end in `Pool`: that is how Drizzle knows to check out
37
- * a client for each transaction.
55
+ * Closed by `end`, once what was asked has answered; asked again after, as
56
+ * a call's background work may, it opens a connection again. Unless `end`
57
+ * was given a time to close by and reached it: then the connection closes
58
+ * at once, failing what is still asked, and nothing opens it again. And
59
+ * `destroy` cuts it the same way, at any time.
38
60
  */
39
- export class HyperdrivePool implements Exclude<PoolLike, pg.Pool> {
61
+ export class HyperdrivePool implements Client {
40
62
  readonly #connectionString: string;
41
- /** The client, connecting or connected; null before the first query, and once it has failed. */
42
- #client: Promise<pg.Client> | null = null;
43
- /** Whether a transaction holds the client. */
44
- #held = false;
45
- /** What waits for its turn, in the order it asked. */
46
- readonly #waiting: (() => void)[] = [];
47
- /** Queries and transactions asked and not done: waiting for the client, their turn, or an answer. */
63
+ /** How long the connection may sit idle before it closes, or undefined: closed by `end` alone. */
64
+ readonly #closeIdleMs: number | undefined;
65
+ /** The client, made by the first statement; null before it, and once `end` closed it. */
66
+ #client: PostgresClient | null = null;
67
+ /** The end of the last turn asked: the next one starts once it has. */
68
+ #tail: Promise<unknown> = Promise.resolve();
69
+ /** Turns asked and not done: waiting, or under way. */
48
70
  #busy = 0;
49
- /** What waits for the client to be idle: `end`. */
71
+ /** What waits for every turn to be done: `end`. */
50
72
  readonly #idle: (() => void)[] = [];
51
- /** How long the client may sit idle before it closes, or null: closed by `end` alone. */
52
- readonly #closeIdleMs: number | null;
53
- #idleTimer: ReturnType<typeof setTimeout> | undefined;
73
+ /** Whether plain reads asked together go together (`groupReads`). */
74
+ readonly #groupReads: boolean;
75
+ /** The reads that will take the next group's turn, while it has not started: what a read asked now joins. */
76
+ #gathering: Gathered[] | null = null;
77
+ /** Closed at the time `end` was given, with turns still asked: none opens a connection again. */
78
+ #cut = false;
79
+ /** Set by `destroy`: every turn after it fails. */
80
+ #destroyed = false;
54
81
 
55
82
  /**
56
83
  * `closeIdleMs`: for what outlives a call, a Durable Object, which queries
57
- * now and then: the client closes once it has been idle that long, and
58
- * the next query connects again.
84
+ * now and then: the connection closes once it has been idle that long,
85
+ * and the next query connects again. `groupReads`: plain reads asked
86
+ * together go together, under one Sync.
59
87
  */
60
- constructor(connectionString: string, options: { closeIdleMs?: number } = {}) {
88
+ constructor(connectionString: string, options: { closeIdleMs?: number; groupReads?: boolean } = {}) {
61
89
  if (connectionString.trim().length === 0) {
62
90
  throw new Error('Hyperdrive returned an empty connection string');
63
91
  }
64
92
  this.#connectionString = connectionString;
65
- this.#closeIdleMs = options.closeIdleMs ?? null;
93
+ this.#closeIdleMs = options.closeIdleMs;
94
+ this.#groupReads = options.groupReads === true;
66
95
  }
67
96
 
68
- readonly query: pg.Pool['query'] = (async (...args: unknown[]) => {
69
- this.#busy += 1;
70
- clearTimeout(this.#idleTimer);
71
- try {
72
- const client = await this.#connected();
73
- return await new Promise((resolve, reject) => {
74
- // Sent at its turn, at once: pg answers its queries in the order they are sent.
75
- this.#turn(() => (client.query as (...queryArgs: unknown[]) => Promise<unknown>)(...args).then(resolve, reject));
76
- });
77
- } finally {
78
- this.#done();
79
- }
80
- }) as pg.Pool['query'];
97
+ readonly unsafe = ((...args: Parameters<PostgresClient['unsafe']>) => {
98
+ let values = false;
99
+ let answer: Promise<unknown> | undefined;
100
+ const [text, parameters = [], options] = args;
101
+ /** Its turn, asked once: the first `then`, `catch`, `finally` or `execute` asks it. */
102
+ const asked = () =>
103
+ (answer ??= this.#groupReads && options === undefined && plainRead(text)
104
+ ? this.#gather({ text, values: parameters, arrays: () => values })
105
+ : this.#turn(() => {
106
+ const query = this.#connected().unsafe(...args);
107
+ return values ? query.values() : query;
108
+ }));
109
+ const statement = {
110
+ values: () => {
111
+ values = true;
112
+ return statement;
113
+ },
114
+ execute: () => {
115
+ void asked().catch(() => {});
116
+ return statement;
117
+ },
118
+ then: (resolve?: (value: unknown) => unknown, reject?: (error: unknown) => unknown) => asked().then(resolve, reject),
119
+ catch: (reject: (error: unknown) => unknown) => asked().catch(reject),
120
+ finally: (done: () => void) => asked().finally(done),
121
+ [Symbol.toStringTag]: 'Statement',
122
+ } as Statement;
123
+ return statement;
124
+ }) as unknown as PostgresClient['unsafe'];
81
125
 
82
- /** The client, for a transaction: held from now until `release`. */
83
- async connect(): Promise<Pick<pg.PoolClient, 'query'> & { release: () => void }> {
84
- this.#busy += 1;
85
- clearTimeout(this.#idleTimer);
86
- let client: pg.Client;
87
- try {
88
- client = await this.#connected();
89
- } catch (error) {
90
- this.#done();
91
- throw error;
126
+ /** A transaction: its turn lasts until it has committed or rolled back. */
127
+ readonly begin = ((...args: unknown[]) =>
128
+ this.#turn(() => (this.#connected().begin as (...beginArgs: unknown[]) => Promise<unknown>)(...args))) as PostgresClient['begin'];
129
+
130
+ /**
131
+ * A read, in the group gathering for the next turn, or in a new one: it
132
+ * starts a task later, so that the reads asked with it join, and once
133
+ * the turns before it are done, which those asked while they ran join.
134
+ */
135
+ #gather(read: Omit<Gathered, 'resolve' | 'reject'>): Promise<unknown> {
136
+ let members = this.#gathering;
137
+ if (members === null) {
138
+ const gathered: Gathered[] = (members = []);
139
+ const asked = new Promise((resolve) => setTimeout(resolve, 0));
140
+ void this.#turn(async () => {
141
+ await asked;
142
+ if (this.#gathering === gathered) this.#gathering = null;
143
+ try {
144
+ const client = this.#connected();
145
+ const alone = async (member: Gathered) => {
146
+ const query = client.unsafe(member.text, member.values);
147
+ member.resolve(await (member.arrays() ? query.values() : query));
148
+ };
149
+ if (gathered.length === 1) return await alone(gathered[0]!);
150
+ try {
151
+ const answers = await client.group(gathered.map((member) => ({ query: member.text, parameters: member.values, arrays: member.arrays() })));
152
+ gathered.forEach((member, i) => member.resolve(answers[i]));
153
+ } catch (error) {
154
+ // A statement the client could not write, a value whose type only the server can say among them: nothing of
155
+ // the group went, and each goes alone, so that only that one fails, as it would have.
156
+ if (!UNSENT.has((error as { code?: unknown }).code as string)) throw error;
157
+ for (const member of gathered) await alone(member).catch(member.reject);
158
+ }
159
+ } catch (error) {
160
+ for (const member of gathered) member.reject(error);
161
+ }
162
+ });
163
+ // After its turn is asked, which closes any group gathering before it.
164
+ this.#gathering = gathered;
92
165
  }
93
- await new Promise<void>((resolve) =>
94
- this.#turn(() => {
95
- this.#held = true;
96
- resolve();
97
- }),
98
- );
99
- let released = false;
100
- return {
101
- query: client.query.bind(client) as pg.PoolClient['query'],
102
- release: () => {
103
- if (released) return;
104
- released = true;
105
- this.#held = false;
106
- this.#next();
107
- this.#done();
108
- },
109
- };
166
+ return new Promise((resolve, reject) => members.push({ ...read, resolve, reject }));
110
167
  }
111
168
 
112
- /** Close the client once nothing waits on it: what a Worker hands `waitUntil` when the call is done. */
113
- async end(): Promise<void> {
114
- clearTimeout(this.#idleTimer);
115
- while (this.#busy > 0) {
116
- await new Promise<void>((resolve) => this.#idle.push(resolve));
169
+ /**
170
+ * Close the connection once every turn asked is done: what a Worker hands `waitUntil` when the call is done. By
171
+ * `by` (ms since the epoch) at the latest, if given: then at once, what is still asked failing, the statement under
172
+ * way included, and none of it sent again. A use's call past its deadline, whose answer has gone, so lets go of
173
+ * Hyperdrive: what it asked and Hyperdrive had not sent goes with the connection (use.ts, `HELD_PAST_DEADLINE_MS`).
174
+ */
175
+ async end(by?: number): Promise<void> {
176
+ // A statement awaited just before reaches the pool a few promise steps later, through Drizzle's session: a task
177
+ // later, every one has taken its turn, and none opens a connection that this would leave open.
178
+ await new Promise((resolve) => setTimeout(resolve, 0));
179
+ let timer: ReturnType<typeof setTimeout> | undefined;
180
+ const reached = by === undefined ? null : new Promise<'cut'>((resolve) => (timer = setTimeout(() => resolve('cut'), Math.max(0, by - Date.now()))));
181
+ while (this.#busy > 0 && !this.#cut) {
182
+ const idle = new Promise<void>((resolve) => this.#idle.push(resolve));
183
+ if ((await (reached === null ? idle : Promise.race([idle, reached]))) === 'cut') this.#cut = true;
117
184
  }
185
+ clearTimeout(timer);
118
186
  const client = this.#client;
119
187
  this.#client = null;
120
- if (client === null) return;
121
- await client.then((connected) => connected.end()).catch(() => {});
188
+ await client?.end(this.#cut ? { timeout: 0 } : undefined).catch(() => {});
122
189
  }
123
190
 
124
- /** Run `go` now if nothing holds the client or waits before it, otherwise at its turn. */
125
- #turn(go: () => void): void {
126
- if (!this.#held && this.#waiting.length === 0) go();
127
- else this.#waiting.push(go);
191
+ /**
192
+ * The connection cut now, what it was asked failing at once, and every
193
+ * turn after refused: for what a host stopped waiting on, a connection
194
+ * gone dead or a connect that never answers (T169), so that whatever
195
+ * still awaits it fails rather than waits for good. Its statement may
196
+ * still run in the database.
197
+ */
198
+ destroy(): void {
199
+ this.#destroyed = true;
200
+ const client = this.#client;
201
+ this.#client = null;
202
+ void client?.end({ timeout: 0 }).catch(() => {});
128
203
  }
129
204
 
130
- /** Give the next turns: queries until one is a transaction, which holds the client. */
131
- #next(): void {
132
- while (!this.#held && this.#waiting.length > 0) this.#waiting.shift()!();
205
+ /** `go` once every turn asked before it is done. Anything but a group's closes the group gathering: it goes after. */
206
+ #turn<T>(go: () => PromiseLike<T>): Promise<T> {
207
+ this.#gathering = null;
208
+ this.#busy += 1;
209
+ const answer = this.#tail.then(go);
210
+ this.#tail = answer.then(
211
+ () => this.#done(),
212
+ () => this.#done(),
213
+ );
214
+ return answer;
133
215
  }
134
216
 
135
- /** One query or transaction done: once none is left, what waits for the client to be idle goes on. */
217
+ /** One turn done: once none is left, what waits for the pool to be idle goes on. */
136
218
  #done(): void {
137
219
  this.#busy -= 1;
138
- if (this.#busy > 0) return;
139
- for (const resolve of this.#idle.splice(0)) resolve();
140
- if (this.#closeIdleMs !== null && this.#client !== null) this.#idleTimer = setTimeout(() => void this.end(), this.#closeIdleMs);
220
+ if (this.#busy === 0) for (const resolve of this.#idle.splice(0)) resolve();
141
221
  }
142
222
 
143
- /** The call's client, connecting it the first time. One that fails is dropped: the next query connects again. */
144
- #connected(): Promise<pg.Client> {
145
- if (this.#client !== null) return this.#client;
146
- const client = new pg.Client(postgresConnection(this.#connectionString));
147
- const connecting: Promise<pg.Client> = client.connect().then(() => client);
148
- const drop = () => {
149
- if (this.#client === connecting) this.#client = null;
150
- };
151
- // A connection lost between queries: pg fails what was waiting on it, and the next query connects again.
152
- client.on('error', drop);
153
- client.on('end', drop);
154
- connecting.catch(drop);
155
- this.#client = connecting;
156
- return connecting;
223
+ /** The call's client, made the first time. postgres.js opens its connection again when one is lost. */
224
+ #connected(): PostgresClient {
225
+ if (this.#destroyed) throw new Error('this connection was cut: its call stopped waiting on it');
226
+ if (this.#cut) throw new Error("the call's connection was closed at the time it was given (`end`): nothing more is sent");
227
+ this.#client ??= textTypes(postgresClient(this.#connectionString, {
228
+ max: 1,
229
+ idle_timeout: this.#closeIdleMs === undefined ? undefined : this.#closeIdleMs / 1000,
230
+ // No timer left to outlive the call: `end` closes the connection, or idling does.
231
+ max_lifetime: null,
232
+ }));
233
+ return this.#client;
157
234
  }
158
235
  }