@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/dist/schema.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
- import { n as relationsOf, t as ACTIVE_SUBJECT } from "./relations-BfR3JjT1.js";
2
+ import { n as recorded, r as ACTIVE_SUBJECT } from "./change-records-_fKvahvs.js";
3
3
  import { sql } from "drizzle-orm";
4
- import { bigint, boolean, check, customType, foreignKey, index, integer, pgTable, primaryKey, text, timestamp, unique, uniqueIndex, uuid } from "drizzle-orm/pg-core";
4
+ import { bigint, boolean, check, customType, foreignKey, index, integer, pgTable, primaryKey, smallint, text, timestamp, unique, uniqueIndex, uuid } from "drizzle-orm/pg-core";
5
5
  //#region src/schema.ts
6
6
  var schema_exports = /* @__PURE__ */ __exportAll({
7
7
  auditChainHead: () => auditChainHead,
@@ -13,8 +13,17 @@ var schema_exports = /* @__PURE__ */ __exportAll({
13
13
  dismissedKeys: () => dismissedKeys,
14
14
  enrollments: () => enrollments,
15
15
  environments: () => environments,
16
- environmentsRelations: () => environmentsRelations,
17
16
  identities: () => identities,
17
+ logBulk: () => logBulk,
18
+ logChanges: () => logChanges,
19
+ logCheckpoints: () => logCheckpoints,
20
+ logConfig: () => logConfig,
21
+ logFence: () => logFence,
22
+ logReadDays: () => logReadDays,
23
+ logReads: () => logReads,
24
+ logReadsCursor: () => logReadsCursor,
25
+ logTiles: () => logTiles,
26
+ logUses: () => logUses,
18
27
  mcpApprovals: () => mcpApprovals,
19
28
  mcpConnections: () => mcpConnections,
20
29
  oauthClients: () => oauthClients,
@@ -22,7 +31,6 @@ var schema_exports = /* @__PURE__ */ __exportAll({
22
31
  secretReferences: () => secretReferences,
23
32
  secretVersions: () => secretVersions,
24
33
  secrets: () => secrets,
25
- secretsRelations: () => secretsRelations,
26
34
  serviceBindings: () => serviceBindings,
27
35
  vaultGrants: () => vaultGrants,
28
36
  vaultMembers: () => vaultMembers,
@@ -63,8 +71,11 @@ const environments = pgTable("environments", {
63
71
  createdAt: createdAt(),
64
72
  archivedAt: timestamp("archived_at", { withTimezone: true }),
65
73
  parentId: uuid("parent_id"),
66
- owner: text()
74
+ owner: text(),
75
+ protected: boolean(),
76
+ seal: bytea()
67
77
  }, (table) => [
78
+ check("environments_seal_check", sql`${table.seal} IS NULL OR octet_length(${table.seal}) = 16`),
68
79
  unique("environments_project_id_slug_key").on(table.projectId, table.slug),
69
80
  unique("environments_project_scoped").on(table.id, table.projectId),
70
81
  check("environments_slug_check", sql`${table.slug} ~ ${PLACE_SLUG}`),
@@ -93,8 +104,10 @@ const secrets = pgTable("secrets", {
93
104
  currentVersionId: uuid("current_version_id").references(() => secretVersions.id, { onDelete: "no action" }),
94
105
  currentVersion: integer("current_version").notNull().default(0),
95
106
  archivedAt: timestamp("archived_at", { withTimezone: true }),
96
- folder: text()
107
+ folder: text(),
108
+ seal: bytea()
97
109
  }, (table) => [
110
+ check("secrets_seal_check", sql`${table.seal} IS NULL OR octet_length(${table.seal}) = 16`),
98
111
  unique("secrets_project_id_environment_id_key_key").on(table.projectId, table.environmentId, table.key),
99
112
  check("secrets_key_check", sql`${table.key} ~ '^[A-Za-z_][A-Za-z0-9_]{0,127}$'`),
100
113
  folderCheck("secrets_folder_check", table.folder),
@@ -113,7 +126,8 @@ const secrets = pgTable("secrets", {
113
126
  columns: [table.environmentId, table.projectId],
114
127
  foreignColumns: [environments.id, environments.projectId]
115
128
  }),
116
- index("secrets_lookup_idx").on(table.projectId, table.environmentId, table.key)
129
+ index("secrets_lookup_idx").on(table.projectId, table.environmentId, table.key),
130
+ index("secrets_live_idx").on(table.environmentId).where(sql`${table.archivedAt} IS NULL`)
117
131
  ]);
118
132
  const secretVersions = pgTable("secret_versions", {
119
133
  id: uuid().primaryKey().defaultRandom(),
@@ -192,8 +206,10 @@ const vaultMembers = pgTable("vault_members", {
192
206
  statusChangedAt: bigint("status_changed_at", { mode: "number" }).notNull(),
193
207
  statusChangedBy: text("status_changed_by").notNull(),
194
208
  accessSeq: bigint("access_seq", { mode: "bigint" }).notNull(),
195
- mac: bytea().notNull()
209
+ mac: bytea().notNull(),
210
+ seal: bytea()
196
211
  }, (table) => [
212
+ check("vault_members_seal_check", sql`${table.seal} IS NULL OR octet_length(${table.seal}) = 16`),
197
213
  check("vault_members_mac_check", sql`octet_length(${table.mac}) = 32`),
198
214
  check("vault_members_principal_check", sql`${table.principal} ~ '^(user|token):[^[:space:]:][^[:space:]]*$'`),
199
215
  check("vault_members_user_lowercase", sql`${table.principal} NOT LIKE 'user:%' OR ${table.principal} = lower(${table.principal})`),
@@ -250,8 +266,9 @@ const vaultGrants = pgTable("vault_grants", {
250
266
  * read them from the entries before (an entry that names none, which only
251
267
  * a forged one is, counts as a key of its own, `entry:<seq>`). No login
252
268
  * writes it: the vault and the backup read it. A row a window old counts
253
- * for nothing, so nothing prunes them: there are at most as many as
254
- * principals times keys, and a count reads one principal's.
269
+ * for nothing: the vault's checkpoint forgets those a day and an hour old,
270
+ * longer than any window (`prune_vault_releases`), so a count reads about a
271
+ * day of keys.
255
272
  */
256
273
  const vaultReleases = pgTable("vault_releases", {
257
274
  principal: text().notNull(),
@@ -282,7 +299,8 @@ const auditLog = pgTable("audit_log", {
282
299
  metadata: text().notNull().default("{}"),
283
300
  prevHash: bytea("prev_hash").notNull(),
284
301
  mac: bytea().notNull(),
285
- hash: bytea().notNull()
302
+ hash: bytea().notNull(),
303
+ log015: boolean("log015")
286
304
  }, (table) => [
287
305
  check("audit_log_author_check", sql`${table.author} IN ('app', 'vault')`),
288
306
  check("audit_log_actor_check", sql`${table.actor} ~ '^(user|token|system):.+$'`),
@@ -352,7 +370,8 @@ const secretReferences = pgTable("secret_references", {
352
370
  sourceSecretId: uuid("source_secret_id").notNull(),
353
371
  createdSeq: bigint("created_seq", { mode: "bigint" }).notNull(),
354
372
  createdAt: createdAt(),
355
- createdBy: text("created_by").notNull()
373
+ createdBy: text("created_by").notNull(),
374
+ endedAt: timestamp("ended_at", { withTimezone: true })
356
375
  }, (table) => [
357
376
  unique("secret_references_created_seq_key").on(table.createdSeq),
358
377
  foreignKey({
@@ -440,13 +459,16 @@ const serviceBindings = pgTable("service_bindings", {
440
459
  claims: text().notNull(),
441
460
  scope: text(),
442
461
  policy: text(),
462
+ origin: text(),
443
463
  label: text(),
444
464
  createdAt: createdAt(),
445
465
  createdBy: text("created_by").notNull(),
446
466
  lastUsedAt: timestamp("last_used_at", { withTimezone: true }),
447
467
  revokedAt: timestamp("revoked_at", { withTimezone: true }),
448
- revokedBy: text("revoked_by")
468
+ revokedBy: text("revoked_by"),
469
+ seal: bytea()
449
470
  }, (table) => [
471
+ check("service_bindings_seal_check", sql`${table.seal} IS NULL OR octet_length(${table.seal}) = 16`),
450
472
  check("service_bindings_principal_check", sql`${table.principal} LIKE 'token:%'`),
451
473
  check("service_bindings_auth_mac_check", sql`octet_length(${table.authMac}) = 32`),
452
474
  check("service_bindings_claims_check", sql`${table.claims}::jsonb IS NOT NULL`),
@@ -526,8 +548,10 @@ const mcpConnections = pgTable("mcp_connections", {
526
548
  lastUsedAt: timestamp("last_used_at", { withTimezone: true }),
527
549
  lastUsedIp: text("last_used_ip"),
528
550
  revokedAt: timestamp("revoked_at", { withTimezone: true }),
529
- revokedBy: text("revoked_by")
551
+ revokedBy: text("revoked_by"),
552
+ seal: bytea()
530
553
  }, (table) => [
554
+ check("mcp_connections_seal_check", sql`${table.seal} IS NULL OR octet_length(${table.seal}) = 16`),
531
555
  check("mcp_connections_principal_check", sql`${table.principal} LIKE 'user:%'`),
532
556
  check("mcp_connections_registration_check", sql`${table.registration} IN ('cimd', 'dcr')`),
533
557
  check("mcp_connections_auth_mac_check", sql`octet_length(${table.authMac}) = 32`),
@@ -638,8 +662,10 @@ const credentials = pgTable("credentials", {
638
662
  constraints: text(),
639
663
  enrollmentId: uuid("enrollment_id"),
640
664
  approvedAt: timestamp("approved_at", { withTimezone: true }),
641
- approvedBy: text("approved_by")
665
+ approvedBy: text("approved_by"),
666
+ seal: bytea()
642
667
  }, (table) => [
668
+ check("credentials_seal_check", sql`${table.seal} IS NULL OR octet_length(${table.seal}) = 16`),
643
669
  unique("credentials_token_hash_key").on(table.tokenHash),
644
670
  check("credentials_kind_check", sql`${table.kind} IN ('browser', 'cli', 'service', 'personal', 'handle', 'workload')`),
645
671
  check("credentials_principal_check", sql`${table.principal} LIKE 'user:%' OR ${table.principal} LIKE 'token:%'`),
@@ -712,11 +738,192 @@ const deviceAuthorizations = pgTable("device_authorizations", {
712
738
  foreignColumns: [vaultMembers.principal]
713
739
  }).onDelete("restrict")
714
740
  ]);
715
- const { environmentsRelations, secretsRelations } = relationsOf({
716
- projects,
717
- environments,
718
- secrets,
719
- secretVersions
720
- });
741
+ /**
742
+ * 0.15's configuration objects (0022_log_015): decision inputs no other row
743
+ * holds, one row each, sealed as every input is. `policy` holds the bulk
744
+ * cap's count and window, which a use reads in its own statement; `roles`
745
+ * the role tables. JSON, as the vault writes it. Written only through
746
+ * `change_input`.
747
+ */
748
+ const logConfig = pgTable("log_config", {
749
+ name: text().primaryKey(),
750
+ value: text().notNull(),
751
+ seal: bytea()
752
+ }, (table) => [
753
+ check("log_config_name_check", sql`${table.name} IN ('policy', 'roles')`),
754
+ check("log_config_value_check", sql`${table.value}::jsonb IS NOT NULL`),
755
+ check("log_config_seal_check", sql`${table.seal} IS NULL OR octet_length(${table.seal}) = 16`)
756
+ ]);
757
+ /**
758
+ * 0.15's log (0022_log_015, docs/design/hot-path-one.md): two tables numbered
759
+ * from one sequence, `log_number`, each number taken under the fence's lock
760
+ * (a credential's issue: under the issue lock, which the barrier waits for;
761
+ * 0026), so that the number alone orders uses against changes. Nothing reads or
762
+ * writes them before 0.15's vault; the vault login reads them and writes
763
+ * them only through the migration's functions.
764
+ *
765
+ * A change: any write to a decision input, or to configuration, kept for
766
+ * good. `object` is the input it changes (`log_object(kind, id)`), `seal`
767
+ * that object's seal after it, `payload` what it does. The MAC is the
768
+ * vault's, 16 bytes, its key's tag then HMAC-SHA256 cut to 12 (D162), over everything but the number,
769
+ * which the vault does not know when it asks. The vault login reads them,
770
+ * and the app's reads the changes too, for the audit pages: a sign-in
771
+ * through a trust binding, and the vault's token issues, revokes and
772
+ * sign-outs, are recorded there alone (D147, T156, T164).
773
+ */
774
+ const logChanges = pgTable("log_changes", {
775
+ number: bigint({ mode: "bigint" }).primaryKey(),
776
+ at: bigint({ mode: "number" }).notNull(),
777
+ requestId: text("request_id").notNull(),
778
+ object: uuid().notNull(),
779
+ actor: text().notNull(),
780
+ payload: text().notNull(),
781
+ seal: bytea(),
782
+ mac: bytea().notNull()
783
+ }, (table) => [
784
+ unique("log_changes_request_id_key").on(table.requestId),
785
+ check("log_changes_seal_check", sql`${table.seal} IS NULL OR octet_length(${table.seal}) = 16`),
786
+ check("log_changes_mac_check", sql`octet_length(${table.mac}) = 16`),
787
+ index("log_changes_object_idx").on(table.object, table.number),
788
+ index("log_changes_sign_in_idx").on(table.at, table.number).where(recorded("postgres", table.payload, "signIn")),
789
+ index("log_changes_sign_in_actor_idx").on(table.actor, table.at, table.number).where(recorded("postgres", table.payload, "signIn")),
790
+ index("log_changes_issue_revoke_idx").on(table.at, table.number).where(recorded("postgres", table.payload, "issue", "revoke", "signOut")),
791
+ index("log_changes_issue_revoke_actor_idx").on(table.actor, table.at, table.number).where(recorded("postgres", table.payload, "issue", "revoke", "signOut"))
792
+ ]);
793
+ /**
794
+ * A use, the slim row (R53): no principal (the credential's), no
795
+ * environment (the secret's, or `place`), no kind (the table says it).
796
+ * Partitioned by the day of `at` (`make_day`), which retention drops whole;
797
+ * fixed-width columns first, and eight of them, so the row pads to nothing. One index, the
798
+ * replay's: a retried call repeats its `at` and request id, both under its
799
+ * MAC, and lands in the same day.
800
+ */
801
+ const logUses = pgTable("log_uses", {
802
+ number: bigint({ mode: "bigint" }).notNull(),
803
+ at: bigint({ mode: "number" }).notNull(),
804
+ requestId: bigint("request_id", { mode: "bigint" }).notNull(),
805
+ credential: uuid().notNull(),
806
+ secret: uuid().notNull(),
807
+ purpose: smallint().notNull(),
808
+ mac: bytea().notNull(),
809
+ place: uuid()
810
+ }, (table) => [check("log_uses_mac_check", sql`octet_length(${table.mac}) = 16`), uniqueIndex("log_uses_request_idx").on(table.at, table.requestId)]);
811
+ /**
812
+ * 0.15's reads as the audit pages list them (T166; formal/Reads.tla): a
813
+ * projection of each use a signed pair seals, written by the vault's
814
+ * projector apart from the checkpointer, from the sealed objects at the
815
+ * use's number: its account, its key's place, the fork it was read in,
816
+ * through a reference the source and its place, the version read; under the
817
+ * vault's MAC over all of it, the number included. Partitioned by the day of
818
+ * `at`, as the uses are (`make_reads_day`), and dropped with them.
819
+ */
820
+ const logReads = pgTable("log_reads", {
821
+ number: bigint({ mode: "bigint" }).notNull(),
822
+ at: bigint({ mode: "number" }).notNull(),
823
+ credential: uuid().notNull(),
824
+ purpose: smallint().notNull(),
825
+ principal: text().notNull(),
826
+ secret: uuid().notNull(),
827
+ environment: uuid().notNull(),
828
+ project: uuid().notNull(),
829
+ place: uuid(),
830
+ source: uuid(),
831
+ sourceEnvironment: uuid("source_environment"),
832
+ sourceProject: uuid("source_project"),
833
+ version: uuid(),
834
+ mac: bytea().notNull()
835
+ }, (table) => [
836
+ check("log_reads_mac_check", sql`octet_length(${table.mac}) = 16`),
837
+ uniqueIndex("log_reads_number_idx").on(table.at, table.number),
838
+ index("log_reads_principal_idx").on(table.principal, table.at, table.number)
839
+ ]);
840
+ /** The days each member's reads are in (`log_reads`), so that a page of theirs reads those days alone: the projector writes them with the reads. */
841
+ const logReadDays = pgTable("log_read_days", {
842
+ principal: text().notNull(),
843
+ day: integer().notNull()
844
+ }, (table) => [primaryKey({
845
+ name: "log_read_days_pkey",
846
+ columns: [table.principal, table.day]
847
+ })]);
848
+ /** How far the projector has gone: the number every use at or below it is projected, and when. */
849
+ const logReadsCursor = pgTable("log_reads_cursor", {
850
+ onlyRow: boolean("only_row").primaryKey().default(true),
851
+ through: bigint({ mode: "bigint" }).notNull(),
852
+ pair: bigint({ mode: "bigint" }).notNull(),
853
+ at: bigint({ mode: "number" }).notNull(),
854
+ mac: bytea()
855
+ }, (table) => [check("log_reads_cursor_only_row_check", sql`${table.onlyRow}`), check("log_reads_cursor_mac_check", sql`${table.mac} IS NULL OR octet_length(${table.mac}) = 16`)]);
856
+ /**
857
+ * The bulk cap (R28, R52): each principal's last logged use of each key, as
858
+ * the start of that use's refresh period, written only by a use that holds
859
+ * the principal's lock (exclusive to enter a key, shared to move it on:
860
+ * 0028), at most once per refresh per key. A row older than the window, a
861
+ * refresh, the deadline and a minute counts for nothing; `prune_bulk`
862
+ * forgets it an hour after that.
863
+ */
864
+ const logBulk = pgTable("log_bulk", {
865
+ principal: text().notNull(),
866
+ secret: uuid().notNull(),
867
+ last: bigint({ mode: "number" }).notNull()
868
+ }, (table) => [primaryKey({
869
+ name: "log_bulk_pkey",
870
+ columns: [table.principal, table.secret]
871
+ })]);
872
+ /**
873
+ * The fence's one row: the lowest `at` a use may still be written with,
874
+ * raised by each checkpoint's barrier (`close_one`), never lowered. Made
875
+ * by the migration; without it every use is refused.
876
+ */
877
+ const logFence = pgTable("log_fence", {
878
+ onlyRow: boolean("only_row").primaryKey().default(true),
879
+ atFloor: bigint("at_floor", { mode: "number" }).notNull()
880
+ }, (table) => [check("log_fence_only_row_check", sql`${table.onlyRow}`)]);
881
+ /**
882
+ * A signed checkpoint: the pair of tree heads (W212), changes and uses, at
883
+ * one `upto`, every number at or below which is final. One row, so that no
884
+ * state holds one head without the other. `at_floor` is the floor its
885
+ * barrier set: the next window's uses have an `at` at or above it.
886
+ */
887
+ const logCheckpoints = pgTable("log_checkpoints", {
888
+ upto: bigint({ mode: "bigint" }).primaryKey(),
889
+ at: bigint({ mode: "number" }).notNull(),
890
+ atFloor: bigint("at_floor", { mode: "number" }).notNull(),
891
+ changesSize: bigint("changes_size", { mode: "bigint" }).notNull(),
892
+ changesRoot: bytea("changes_root").notNull(),
893
+ changesNote: text("changes_note").notNull(),
894
+ usesSize: bigint("uses_size", { mode: "bigint" }).notNull(),
895
+ usesRoot: bytea("uses_root").notNull(),
896
+ usesNote: text("uses_note").notNull()
897
+ }, (table) => [
898
+ check("log_checkpoints_changes_root_check", sql`octet_length(${table.changesRoot}) = 32`),
899
+ check("log_checkpoints_uses_root_check", sql`octet_length(${table.usesRoot}) = 32`),
900
+ index("log_checkpoints_floor_idx").on(table.atFloor, table.upto)
901
+ ]);
902
+ /**
903
+ * The two trees' tiles (C2SP tlog-tiles: height 8, up to 256 hashes of one
904
+ * level). Partitioned by tree; the uses tree's leaf tiles (level 0) by
905
+ * `leaf`, the first leaf a tile covers (`make_tiles`), so that retention
906
+ * drops a table rather than deleting a day's rows (W212); its levels above,
907
+ * which span far past their first leaf, in one table (W217).
908
+ */
909
+ const logTiles = pgTable("log_tiles", {
910
+ tree: text().notNull(),
911
+ level: smallint().notNull(),
912
+ leaf: bigint({ mode: "bigint" }).notNull(),
913
+ hashes: bytea().notNull()
914
+ }, (table) => [
915
+ primaryKey({
916
+ name: "log_tiles_pkey",
917
+ columns: [
918
+ table.tree,
919
+ table.level,
920
+ table.leaf
921
+ ]
922
+ }),
923
+ check("log_tiles_tree_check", sql`${table.tree} IN ('changes', 'uses')`),
924
+ check("log_tiles_level_check", sql`${table.level} BETWEEN 0 AND 7`),
925
+ check("log_tiles_leaf_check", sql`${table.leaf} >= 0 AND ${table.leaf} % (256::numeric ^ (${table.level} + 1)) = 0`),
926
+ check("log_tiles_hashes_check", sql`octet_length(${table.hashes}) BETWEEN 32 AND 8192 AND octet_length(${table.hashes}) % 32 = 0`)
927
+ ]);
721
928
  //#endregion
722
- export { auditChainHead, auditLog, cappedAlerts, consumedTokens, credentials, deviceAuthorizations, dismissedKeys, enrollments, environments, environmentsRelations, identities, mcpApprovals, mcpConnections, oauthClients, projects, secretReferences, secretVersions, secrets, secretsRelations, serviceBindings, schema_exports as t, vaultGrants, vaultMembers, vaultReleases };
929
+ export { auditChainHead, auditLog, cappedAlerts, consumedTokens, credentials, deviceAuthorizations, dismissedKeys, enrollments, environments, identities, logBulk, logChanges, logCheckpoints, logConfig, logFence, logReadDays, logReads, logReadsCursor, logTiles, logUses, mcpApprovals, mcpConnections, oauthClients, projects, secretReferences, secretVersions, secrets, serviceBindings, schema_exports as t, vaultGrants, vaultMembers, vaultReleases };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coffre/db",
3
- "version": "0.14.4",
3
+ "version": "0.15.0",
4
4
  "description": "coffre's database: the schema, its migrations and the connections, on Postgres or SQLite.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -74,16 +74,24 @@
74
74
  },
75
75
  "./package.json": "./package.json"
76
76
  },
77
+ "imports": {
78
+ "#postgres/platform": {
79
+ "coffre:source": {
80
+ "workerd": "./src/postgres-js/platform.workerd.ts",
81
+ "default": "./src/postgres-js/platform.node.ts"
82
+ },
83
+ "types": "./dist/postgres-platform-node.d.ts",
84
+ "workerd": "./dist/postgres-platform-workerd.js",
85
+ "default": "./dist/postgres-platform-node.js"
86
+ }
87
+ },
77
88
  "dependencies": {
78
89
  "@libsql/client": "0.18.0",
79
90
  "drizzle-orm": "0.45.2",
80
- "pg": "8.16.3",
81
- "pg-connection-string": "2.14.0",
82
- "@coffre/core": "0.14.4"
91
+ "@coffre/core": "0.15.0"
83
92
  },
84
93
  "devDependencies": {
85
94
  "@types/node": "26.1.1",
86
- "@types/pg": "8.20.0",
87
95
  "drizzle-kit": "0.31.10",
88
96
  "tsdown": "0.23.0",
89
97
  "typescript": "5.9.3"
@@ -0,0 +1,9 @@
1
+ import { sql } from 'drizzle-orm';
2
+
3
+ /**
4
+ * `identities.active_subject`: the subject while the account is bound, null
5
+ * once revoked. A unique index on (provider, issuer_hash, active_subject)
6
+ * binds an account to one person at a time. Plain column names, so the
7
+ * expression reads the same on both databases.
8
+ */
9
+ export const ACTIVE_SUBJECT = sql`CASE WHEN revoked_at IS NULL THEN subject END`;
@@ -0,0 +1,38 @@
1
+ import { sql, type SQL, type SQLWrapper } from 'drizzle-orm';
2
+
3
+ /**
4
+ * What a change of 0.15's log records besides its op, which the audit pages
5
+ * list where 0.14's entry stood (D147): a sign-in through a trust binding
6
+ * (`signIn`, T156); a token the vault issued (`issue`); a credential it
7
+ * revoked by request, or that signed itself out (a revoke's `reason`,
8
+ * T164). Each is known by text its payload holds (`holds`), and by how the
9
+ * payload starts (`starts`): the vault writes it canonical, its keys in
10
+ * order, so an issue's is `{"caller":…` and a revoke's
11
+ * `{"op":"credential.revoke",…`. A string the payload holds has its quotes
12
+ * escaped, but an object's keys need not be the vault's own (a role's
13
+ * name, in the roles' change: review r1), so where the text alone could be
14
+ * one, the start says whose payload it is. The partial indexes of
15
+ * `log_changes` hold each set, and a read that is to use one repeats its
16
+ * condition (`recorded`), word for word and with no parameter.
17
+ */
18
+ export const RECORDS = {
19
+ signIn: { holds: '"signIn":{' },
20
+ issue: { starts: '{"caller":"', holds: '"issue":{"by":"' },
21
+ revoke: { starts: '{"op":"credential.revoke",', holds: '"reason":"revoke"' },
22
+ signOut: { starts: '{"op":"credential.revoke",', holds: '"reason":"sign_out"' },
23
+ } as const satisfies Record<string, { starts?: string; holds: string }>;
24
+
25
+ export type ChangeRecord = keyof typeof RECORDS;
26
+
27
+ /** Whether `payload` holds one of `records`, on Postgres or SQLite: an index's condition, and the reads' that use it. */
28
+ export function recorded(engine: 'postgres' | 'sqlite', payload: SQLWrapper, ...records: ChangeRecord[]): SQL {
29
+ const at = (text: string) => {
30
+ const marker = sql.raw(`'${text}'`);
31
+ return engine === 'postgres' ? sql`strpos(${payload}, ${marker})` : sql`instr(${payload}, ${marker})`;
32
+ };
33
+ const each = records.map((record) => {
34
+ const { starts, holds }: { starts?: string; holds: string } = RECORDS[record];
35
+ return starts === undefined ? sql`${at(holds)} > 0` : sql`(${at(starts)} = 1 AND ${at(holds)} > 0)`;
36
+ });
37
+ return each.length === 1 ? each[0]! : sql`(${sql.join(each, sql` OR `)})`;
38
+ }
package/src/connect.ts CHANGED
@@ -1,30 +1,30 @@
1
1
  import { resolve } from 'node:path';
2
2
 
3
- import type { Client, ResultSet, Transaction as LibsqlTransaction, TransactionMode } from '@libsql/client';
4
- import pg from 'pg';
5
-
3
+ import type { Client, InStatement, ResultSet, Transaction as LibsqlTransaction, TransactionMode } from '@libsql/client';
6
4
  import { createDatabase, type Database } from './database.ts';
7
5
  import type { Engine } from './dialect.ts';
8
6
  import { asPostgresDatabase } from './portable.ts';
9
- import { postgresConnection } from './postgres.ts';
7
+ import { postgresClient } from './postgres.ts';
10
8
 
11
- export { postgresConnection } from './postgres.ts';
9
+ export { postgresClient, postgresOptions, postgresSession, quoteIdentifier, quoteLiteral, SessionLost, type ClientOptions, type PostgresClient } from './postgres.ts';
12
10
 
13
11
  /**
14
12
  * Open a database from its URL, on a Node server:
15
13
  *
16
- * postgres://… or postgresql://… node-postgres
14
+ * postgres://… or postgresql://… postgres.js, a pool of up to 10 connections
17
15
  * file:… or libsql:… @libsql/client (SQLite)
18
16
  *
19
17
  * The SQLite driver loads only when asked for. The Worker does not
20
18
  * come through here; it builds its Postgres database from the Hyperdrive
21
- * pool with createDatabase.
19
+ * pool with createDatabase. `max`, for a pool of its own beside another,
20
+ * as the vault's checkpointer has (T169).
22
21
  */
23
- export async function openDatabase(url: string): Promise<OpenDatabase> {
22
+ export async function openDatabase(url: string, options: { max?: number } = {}): Promise<OpenDatabase> {
24
23
  switch (engineOfUrl(url)) {
25
24
  case 'postgres': {
26
- const pool = new pg.Pool(postgresConnection(url));
27
- return { engine: 'postgres', db: createDatabase(pool), close: () => pool.end() };
25
+ // As node-postgres's pool was: 10 connections, each closed after 10 s idle.
26
+ const client = postgresClient(url, { max: options.max ?? 10, idle_timeout: 10 });
27
+ return { engine: 'postgres', db: createDatabase(client), close: () => client.end(), cut: () => client.end({ timeout: 0 }) };
28
28
  }
29
29
  case 'sqlite': {
30
30
  const [{ createClient }, { drizzle }, schema] = await Promise.all([
@@ -36,12 +36,13 @@ export async function openDatabase(url: string): Promise<OpenDatabase> {
36
36
  const client = createClient({ url, timeout: 5_000, intMode: 'bigint' });
37
37
  if (url.startsWith('file:')) await client.execute('PRAGMA journal_mode = WAL');
38
38
  const db = drizzle({ client: oneTransactionAtATime(client, writeQueueFor(url)), schema });
39
- return { engine: 'sqlite', db: asPostgresDatabase(db), close: async () => client.close() };
39
+ return { engine: 'sqlite', db: asPostgresDatabase(db), close: async () => client.close(), cut: async () => client.close() };
40
40
  }
41
41
  }
42
42
  }
43
43
 
44
- export type OpenDatabase = { engine: Engine; db: Database; close: () => Promise<void> };
44
+ /** `close` once what was asked has answered; `cut` at once, what is in flight failing (T169). */
45
+ export type OpenDatabase = { engine: Engine; db: Database; close: () => Promise<void>; cut: () => Promise<void> };
45
46
 
46
47
  export function engineOfUrl(url: string): Engine {
47
48
  const scheme = url.slice(0, url.indexOf(':'));
@@ -61,6 +62,16 @@ export function engineOfUrl(url: string): Engine {
61
62
  * connections reach it. Each begins IMMEDIATE (libsql's default), taking the
62
63
  * write lock with its first statement, which is what makes a read inside it
63
64
  * as good as a locked one (see forUpdate).
65
+ *
66
+ * A lone statement that writes waits its turn too. It cannot wait for the
67
+ * lock itself: libsql's busy wait blocks the thread, so the transaction
68
+ * holding the lock, in this process, cannot go on to commit, and the write
69
+ * fails after `timeout`. A connection whose write failed that way still
70
+ * reads the file as it was then, until its statement is collected:
71
+ *
72
+ * six MCP calls at once the first's approval, in a transaction BEGIN IMMEDIATE, then awaits
73
+ * another's last_used_at a lone UPDATE, outside the queue the thread blocked 5 s, SQLITE_BUSY
74
+ * it reads the approval on that connection no row
64
75
  */
65
76
  class WriteQueue {
66
77
  #tail: Promise<void> = Promise.resolve();
@@ -89,7 +100,17 @@ function oneTransactionAtATime(client: Client, queue: WriteQueue): Client {
89
100
  return new Proxy(client, {
90
101
  get(target, key) {
91
102
  if (key === 'execute') {
92
- return async (...args: Parameters<Client['execute']>) => numbers(await target.execute(...args));
103
+ return async (...args: Parameters<Client['execute']>) => {
104
+ // Parameters<> sees execute's last overload only, (sql, args); Drizzle calls the other, ({ sql, args }).
105
+ const [statement] = args as unknown as [InStatement];
106
+ if (!writes(typeof statement === 'string' ? statement : statement.sql)) return numbers(await target.execute(...args));
107
+ const release = await queue.acquire();
108
+ try {
109
+ return numbers(await target.execute(...args));
110
+ } finally {
111
+ release();
112
+ }
113
+ };
93
114
  }
94
115
  if (key === 'transaction') {
95
116
  return async (mode?: TransactionMode) => {
@@ -154,6 +175,39 @@ function settling(tx: LibsqlTransaction, release: () => void): LibsqlTransaction
154
175
  return tx;
155
176
  }
156
177
 
178
+ /**
179
+ * Whether a lone statement may write: all but a query (SELECT or VALUES, or
180
+ * a WITH whose statement is one) and EXPLAIN, read from its words outside
181
+ * parentheses, strings, quoted names and comments.
182
+ *
183
+ * select … from "mcp_approvals" where … reads
184
+ * WITH policy AS (SELECT …) SELECT json_object(…) reads
185
+ * WITH old AS (SELECT …) DELETE FROM … writes
186
+ * update "mcp_connections" set … writes
187
+ */
188
+ export function writes(statement: string): boolean {
189
+ let depth = 0;
190
+ let first: string | null = null;
191
+ for (const [token] of statement.matchAll(/--[^\n]*|\/\*[\s\S]*?(?:\*\/|$)|'(?:[^']|'')*'?|"(?:[^"]|"")*"?|`[^`]*`?|\[[^\]]*\]?|[()]|[A-Za-z_][\w$]*/g)) {
192
+ if (token === '(') depth += 1;
193
+ else if (token === ')') depth -= 1;
194
+ else if (depth === 0 && /^[A-Za-z_]/.test(token)) {
195
+ const word = token.toLowerCase();
196
+ if (first === null) {
197
+ first = word;
198
+ if (word !== 'with') return !QUERIES.has(word);
199
+ } else if (STATEMENTS.has(word)) {
200
+ return !QUERIES.has(word);
201
+ }
202
+ }
203
+ }
204
+ return true;
205
+ }
206
+
207
+ const QUERIES = new Set(['select', 'values', 'explain']);
208
+ /** The words a WITH's own statement starts with. */
209
+ const STATEMENTS = new Set(['select', 'values', 'insert', 'update', 'delete', 'replace']);
210
+
157
211
  /** Keep large counters exact; Drizzle's booleans and dates still need safe numbers. */
158
212
  function numbers(result: ResultSet): ResultSet {
159
213
  result.rows = result.rows.map((row) => {