fleetmesh-node 0.2.0__py3-none-any.whl

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 (338) hide show
  1. fleetmesh_node-0.2.0.dist-info/METADATA +57 -0
  2. fleetmesh_node-0.2.0.dist-info/RECORD +338 -0
  3. fleetmesh_node-0.2.0.dist-info/WHEEL +5 -0
  4. fleetmesh_node-0.2.0.dist-info/entry_points.txt +2 -0
  5. fleetmesh_node-0.2.0.dist-info/licenses/LICENSE +121 -0
  6. fleetmesh_node-0.2.0.dist-info/top_level.txt +2 -0
  7. node/RELEASE +1 -0
  8. node/__init__.py +35 -0
  9. node/app/__init__.py +49 -0
  10. node/app/commands.py +265 -0
  11. node/app/config.py +131 -0
  12. node/app/control.py +670 -0
  13. node/app/core.py +867 -0
  14. node/app/plugins.py +76 -0
  15. node/app/router.py +83 -0
  16. node/app/socket.py +431 -0
  17. node/app/systemd.py +36 -0
  18. node/app/views.py +204 -0
  19. node/channels/__init__.py +378 -0
  20. node/channels/bus.py +331 -0
  21. node/channels/journal.py +46 -0
  22. node/commands/__init__.py +140 -0
  23. node/commands/adapter.py +210 -0
  24. node/commands/base.py +278 -0
  25. node/commands/cluster_cmds.py +183 -0
  26. node/commands/compute_cmds.py +401 -0
  27. node/commands/core.py +474 -0
  28. node/commands/gossip_cmds.py +106 -0
  29. node/commands/nsite_cmds.py +141 -0
  30. node/commands/peering.py +402 -0
  31. node/commands/plugin_cmds.py +135 -0
  32. node/commands/sidecar_cmds.py +120 -0
  33. node/commands/sync.py +388 -0
  34. node/commands/transport_cmds.py +451 -0
  35. node/commands/video_cmds.py +99 -0
  36. node/compartment/__init__.py +124 -0
  37. node/compartment/base.py +868 -0
  38. node/compartment/command.py +458 -0
  39. node/compartment/context.py +163 -0
  40. node/compartment/every.py +108 -0
  41. node/compartment/handler.py +131 -0
  42. node/compartment/lifecycle.py +460 -0
  43. node/compartment/lint.py +388 -0
  44. node/compartment/need.py +158 -0
  45. node/compartment/problem.py +521 -0
  46. node/compartment/refusals.py +120 -0
  47. node/compartment/registry.py +790 -0
  48. node/compartment/schema.py +290 -0
  49. node/compartment/sidecar.py +499 -0
  50. node/compartment/stores.py +662 -0
  51. node/compartment/wire.py +225 -0
  52. node/compartment/worker.py +61 -0
  53. node/config/__init__.py +43 -0
  54. node/config/base.py +116 -0
  55. node/config/change.py +441 -0
  56. node/config/changestore.py +201 -0
  57. node/config/compartments.py +1144 -0
  58. node/config/node.py +318 -0
  59. node/config/parameter.py +563 -0
  60. node/config/protocols.py +274 -0
  61. node/config/records.py +171 -0
  62. node/examples/__init__.py +1 -0
  63. node/examples/demo.py +226 -0
  64. node/examples/follower.py +46 -0
  65. node/interfaces.py +235 -0
  66. node/keystore/__init__.py +27 -0
  67. node/keystore/core.py +103 -0
  68. node/keystore/enclave.py +688 -0
  69. node/meshnode.py +111 -0
  70. node/metrics/__init__.py +396 -0
  71. node/metrics/adapters.py +102 -0
  72. node/metrics/face.py +110 -0
  73. node/planes/__init__.py +21 -0
  74. node/planes/identity/__init__.py +20 -0
  75. node/planes/identity/announce/__init__.py +9 -0
  76. node/planes/identity/announce/compartment.py +447 -0
  77. node/planes/identity/announce/types.py +26 -0
  78. node/planes/identity/discovery/__init__.py +9 -0
  79. node/planes/identity/discovery/compartment.py +522 -0
  80. node/planes/identity/discovery/types.py +13 -0
  81. node/planes/identity/linked/__init__.py +1 -0
  82. node/planes/identity/linked/compartment.py +270 -0
  83. node/planes/identity/linked/types.py +25 -0
  84. node/planes/identity/presence/__init__.py +7 -0
  85. node/planes/identity/presence/compartment.py +409 -0
  86. node/planes/identity/presence/types.py +14 -0
  87. node/planes/operations/__init__.py +13 -0
  88. node/planes/operations/scheduler/__init__.py +9 -0
  89. node/planes/operations/scheduler/compartment.py +353 -0
  90. node/planes/operations/scheduler/types.py +21 -0
  91. node/planes/operations/updates/__init__.py +17 -0
  92. node/planes/operations/updates/core.py +310 -0
  93. node/planes/operations/updates/store.py +79 -0
  94. node/planes/operations/updates/types.py +20 -0
  95. node/planes/peering/__init__.py +28 -0
  96. node/planes/peering/budget/__init__.py +43 -0
  97. node/planes/peering/budget/chain.py +43 -0
  98. node/planes/peering/budget/core.py +537 -0
  99. node/planes/peering/budget/evaluation.py +80 -0
  100. node/planes/peering/budget/meter.py +100 -0
  101. node/planes/peering/budget/receipts.py +439 -0
  102. node/planes/peering/budget/shape.py +101 -0
  103. node/planes/peering/budget/tick.py +110 -0
  104. node/planes/peering/budget/types.py +67 -0
  105. node/planes/peering/budget/windows.py +346 -0
  106. node/planes/peering/capacity/__init__.py +7 -0
  107. node/planes/peering/capacity/compartment.py +342 -0
  108. node/planes/peering/capacity/types.py +24 -0
  109. node/planes/peering/peering/__init__.py +25 -0
  110. node/planes/peering/peering/core.py +464 -0
  111. node/planes/peering/peering/envelope.py +142 -0
  112. node/planes/peering/peering/handshake.py +198 -0
  113. node/planes/peering/peering/shape.py +80 -0
  114. node/planes/peering/peering/types.py +39 -0
  115. node/planes/peering/reporter/__init__.py +42 -0
  116. node/planes/peering/reporter/constants.py +29 -0
  117. node/planes/peering/reporter/core.py +299 -0
  118. node/planes/peering/reporter/finding.py +74 -0
  119. node/planes/peering/reporter/findings.py +171 -0
  120. node/planes/peering/reporter/publish.py +285 -0
  121. node/planes/peering/reporter/reading.py +193 -0
  122. node/planes/peering/reporter/shape.py +86 -0
  123. node/planes/peering/reporter/types.py +50 -0
  124. node/planes/peering/strain/__init__.py +9 -0
  125. node/planes/peering/strain/compartment.py +204 -0
  126. node/planes/peering/strain/types.py +21 -0
  127. node/planes/peering/trust/__init__.py +9 -0
  128. node/planes/peering/trust/compartment.py +239 -0
  129. node/planes/peering/trust/types.py +30 -0
  130. node/planes/services/__init__.py +33 -0
  131. node/planes/services/compute/__init__.py +160 -0
  132. node/planes/services/compute/common.py +134 -0
  133. node/planes/services/compute/compartment.py +681 -0
  134. node/planes/services/compute/executors/__init__.py +121 -0
  135. node/planes/services/compute/executors/agent.py +840 -0
  136. node/planes/services/compute/executors/bacalhau.py +605 -0
  137. node/planes/services/compute/executors/base.py +127 -0
  138. node/planes/services/compute/executors/harness.py +439 -0
  139. node/planes/services/compute/executors/mcp.py +454 -0
  140. node/planes/services/compute/executors/media.py +898 -0
  141. node/planes/services/compute/executors/nondet.py +283 -0
  142. node/planes/services/compute/executors/providers.py +499 -0
  143. node/planes/services/compute/executors/registry.py +140 -0
  144. node/planes/services/compute/executors/wasm.py +244 -0
  145. node/planes/services/compute/executors/zk.py +604 -0
  146. node/planes/services/compute/jobs.py +227 -0
  147. node/planes/services/compute/load.py +180 -0
  148. node/planes/services/compute/market.py +433 -0
  149. node/planes/services/compute/sandbox.py +282 -0
  150. node/planes/services/compute/types.py +84 -0
  151. node/planes/services/compute_wire/__init__.py +58 -0
  152. node/planes/services/compute_wire/compartment.py +256 -0
  153. node/planes/services/compute_wire/messages.py +320 -0
  154. node/planes/services/compute_wire/sealing.py +147 -0
  155. node/planes/services/compute_wire/session.py +572 -0
  156. node/planes/services/compute_wire/types.py +49 -0
  157. node/planes/services/compute_wire/verify.py +331 -0
  158. node/planes/services/dvm/__init__.py +24 -0
  159. node/planes/services/dvm/gateway.py +526 -0
  160. node/planes/services/dvm/protocol.py +35 -0
  161. node/planes/services/dvm/translate.py +15 -0
  162. node/planes/services/dvm/types.py +25 -0
  163. node/planes/services/gateway/__init__.py +40 -0
  164. node/planes/services/gateway/settle.py +279 -0
  165. node/planes/services/gateway/translate.py +644 -0
  166. node/planes/services/gossip/__init__.py +22 -0
  167. node/planes/services/gossip/core.py +929 -0
  168. node/planes/services/gossip/envelope.py +154 -0
  169. node/planes/services/gossip/mesh.py +127 -0
  170. node/planes/services/gossip/types.py +50 -0
  171. node/planes/services/nsite/__init__.py +7 -0
  172. node/planes/services/nsite/client.py +62 -0
  173. node/planes/services/nsite/compartment.py +752 -0
  174. node/planes/services/nsite/types.py +44 -0
  175. node/planes/services/paidfetch/__init__.py +7 -0
  176. node/planes/services/paidfetch/compartment.py +862 -0
  177. node/planes/services/paidfetch/types.py +99 -0
  178. node/planes/services/settle/__init__.py +27 -0
  179. node/planes/services/settle/compartment.py +384 -0
  180. node/planes/services/settle/engine.py +456 -0
  181. node/planes/services/settle/gateways.py +541 -0
  182. node/planes/services/settle/payment.py +512 -0
  183. node/planes/services/settle/store.py +74 -0
  184. node/planes/services/settle/types.py +58 -0
  185. node/planes/services/sidecars/__init__.py +8 -0
  186. node/planes/services/sidecars/compartment.py +461 -0
  187. node/planes/services/sidecars/manifest.py +304 -0
  188. node/planes/services/sidecars/manifests/10-grasp.json +37 -0
  189. node/planes/services/sidecars/manifests/20-ncontainer.json +31 -0
  190. node/planes/services/sidecars/manifests/30-magpie.json +28 -0
  191. node/planes/services/sidecars/types.py +43 -0
  192. node/planes/services/video/__init__.py +106 -0
  193. node/planes/services/video/barter.py +407 -0
  194. node/planes/services/video/cli.py +158 -0
  195. node/planes/services/video/compartment.py +932 -0
  196. node/planes/services/video/ingest.py +524 -0
  197. node/planes/services/video/ledger.py +705 -0
  198. node/planes/services/video/packager.py +847 -0
  199. node/planes/services/video/player.py +456 -0
  200. node/planes/services/video/timed_segments.py +124 -0
  201. node/planes/services/video/transit.py +478 -0
  202. node/planes/store/__init__.py +24 -0
  203. node/planes/store/blob_replication/__init__.py +7 -0
  204. node/planes/store/blob_replication/compartment.py +496 -0
  205. node/planes/store/blob_replication/types.py +35 -0
  206. node/planes/store/blob_wire/__init__.py +48 -0
  207. node/planes/store/blob_wire/client.py +57 -0
  208. node/planes/store/blob_wire/core.py +359 -0
  209. node/planes/store/blob_wire/intake.py +74 -0
  210. node/planes/store/blob_wire/messages.py +85 -0
  211. node/planes/store/projected/__init__.py +7 -0
  212. node/planes/store/projected/compartment.py +258 -0
  213. node/planes/store/projected/types.py +29 -0
  214. node/planes/store/retention/__init__.py +7 -0
  215. node/planes/store/retention/compartment.py +214 -0
  216. node/planes/store/retention/types.py +12 -0
  217. node/planes/store/sync/__init__.py +35 -0
  218. node/planes/store/sync/core.py +311 -0
  219. node/planes/store/sync/envelope.py +106 -0
  220. node/planes/store/sync/handover.py +206 -0
  221. node/planes/store/sync/initiator.py +128 -0
  222. node/planes/store/sync/responder.py +114 -0
  223. node/planes/store/sync/rules.py +179 -0
  224. node/planes/store/sync/shape.py +80 -0
  225. node/planes/store/sync/types.py +51 -0
  226. node/planes/transport/__init__.py +23 -0
  227. node/planes/transport/bridge/__init__.py +13 -0
  228. node/planes/transport/bridge/compartment.py +785 -0
  229. node/planes/transport/bridge/ratelimit.py +44 -0
  230. node/planes/transport/bridge/types.py +43 -0
  231. node/planes/transport/lan/__init__.py +9 -0
  232. node/planes/transport/lan/compartment.py +446 -0
  233. node/planes/transport/lan/types.py +25 -0
  234. node/planes/transport/link/__init__.py +7 -0
  235. node/planes/transport/link/compartment.py +257 -0
  236. node/planes/transport/link/types.py +27 -0
  237. node/planes/transport/mediation/__init__.py +45 -0
  238. node/planes/transport/mediation/compartment.py +238 -0
  239. node/planes/transport/mediation/leaf.py +107 -0
  240. node/planes/transport/mediation/messages.py +302 -0
  241. node/planes/transport/mediation/service.py +233 -0
  242. node/planes/transport/mediation/store.py +37 -0
  243. node/planes/transport/mediation/types.py +20 -0
  244. node/planes/transport/p2p/__init__.py +7 -0
  245. node/planes/transport/p2p/compartment.py +349 -0
  246. node/planes/transport/p2p/types.py +22 -0
  247. node/protocols/__init__.py +277 -0
  248. node/protocols/activitypub/__init__.py +56 -0
  249. node/protocols/activitypub/actor.py +230 -0
  250. node/protocols/activitypub/federation.py +490 -0
  251. node/protocols/activitypub/media.py +145 -0
  252. node/protocols/activitypub/models.py +286 -0
  253. node/protocols/activitypub/package.py +271 -0
  254. node/protocols/activitypub/store.py +564 -0
  255. node/protocols/arcade/__init__.py +26 -0
  256. node/protocols/atproto/__init__.py +13 -0
  257. node/protocols/atproto/firehose.py +126 -0
  258. node/protocols/atproto/metering.py +165 -0
  259. node/protocols/atproto/package.py +155 -0
  260. node/protocols/atproto/repo.py +250 -0
  261. node/protocols/atproto/xrpc.py +285 -0
  262. node/protocols/base.py +109 -0
  263. node/protocols/circles/__init__.py +29 -0
  264. node/protocols/dm/__init__.py +50 -0
  265. node/protocols/grasp/__init__.py +22 -0
  266. node/protocols/isotime.py +15 -0
  267. node/protocols/nostr/__init__.py +51 -0
  268. node/protocols/nostr/blobstore.py +383 -0
  269. node/protocols/nostr/blossom.py +627 -0
  270. node/protocols/nostr/client.py +377 -0
  271. node/protocols/nostr/descriptor.py +211 -0
  272. node/protocols/nostr/eventstore.py +304 -0
  273. node/protocols/nostr/package.py +182 -0
  274. node/protocols/nostr/peers.py +109 -0
  275. node/protocols/nostr/relayserver.py +656 -0
  276. node/protocols/p2panda/__init__.py +22 -0
  277. node/protocols/p2panda/identity.py +19 -0
  278. node/protocols/p2panda/package.py +236 -0
  279. node/protocols/p2panda/rung.py +152 -0
  280. node/protocols/p2panda/store.py +273 -0
  281. node/protocols/rhio/__init__.py +33 -0
  282. node/protocols/rhio/package.py +275 -0
  283. node/protocols/rhio/rung.py +136 -0
  284. node/protocols/rhio/store.py +1423 -0
  285. node/protocols/ssb/muxrpc.py +256 -0
  286. node/protocols/ssb/package.py +267 -0
  287. node/protocols/textile/__init__.py +111 -0
  288. node/protocols/textile/transducers.py +259 -0
  289. node/release.py +119 -0
  290. node/storage/__init__.py +88 -0
  291. node/storage/accounts.py +305 -0
  292. node/storage/adapter.py +161 -0
  293. node/storage/base.py +385 -0
  294. node/storage/dialect.py +209 -0
  295. node/storage/grants.py +317 -0
  296. node/storage/locator.py +150 -0
  297. node/storage/postgres.py +294 -0
  298. node/storage/projected.py +953 -0
  299. node/storage/sharding.py +448 -0
  300. node/storage/standing.py +568 -0
  301. node/testnet/__init__.py +9 -0
  302. node/testnet/cluster.py +731 -0
  303. node/testnet/fips_daemon.py +230 -0
  304. node/transport/__init__.py +94 -0
  305. node/transport/const.py +110 -0
  306. node/transport/didcomm_inbound.py +251 -0
  307. node/transport/dnstxt.py +169 -0
  308. node/transport/fips_rung/__init__.py +67 -0
  309. node/transport/fips_rung/core.py +666 -0
  310. node/transport/fips_rung/listener.py +96 -0
  311. node/transport/fips_rung/types.py +12 -0
  312. node/transport/host.py +334 -0
  313. node/transport/iroh_rung/__init__.py +29 -0
  314. node/transport/iroh_rung/core.py +477 -0
  315. node/transport/iroh_rung/quic.py +942 -0
  316. node/transport/iroh_rung/types.py +22 -0
  317. node/transport/ladder.py +145 -0
  318. node/transport/lineproto.py +49 -0
  319. node/transport/mdns.py +303 -0
  320. node/transport/netfetch.py +283 -0
  321. node/transport/relay_rung/__init__.py +23 -0
  322. node/transport/relay_rung/cooldown.py +87 -0
  323. node/transport/relay_rung/core.py +138 -0
  324. node/transport/relay_rung/inbound.py +161 -0
  325. node/transport/relay_rung/ledger.py +104 -0
  326. node/transport/relay_rung/listen.py +121 -0
  327. node/transport/relay_rung/outbound.py +181 -0
  328. node/transport/relay_rung/shape.py +71 -0
  329. node/transport/relay_rung/types.py +58 -0
  330. node/transport/relay_rung/wire.py +63 -0
  331. node/transport/router.py +761 -0
  332. node/transport/rung.py +276 -0
  333. node/transport/ssb_rung.py +166 -0
  334. node/transport/ws.py +502 -0
  335. sidecar/fleetmesh_announce.py +338 -0
  336. sidecar/fleetmesh_p2p.py +373 -0
  337. sidecar/fleetmesh_rhio.py +352 -0
  338. sidecar/fleetmesh_ssb.py +603 -0
@@ -0,0 +1,294 @@
1
+ """PostgreSQL, as an option beside the SQLite default.
2
+
3
+ Optional in the convention this repo already uses for wasmtime: the driver is
4
+ imported lazily, a node without it still starts and serves, and a
5
+ `postgres://` URL is refused by name rather than failing at import. CI sets
6
+ FLEETMESH_REQUIRE_POSTGRES=1 so a missing driver cannot turn a suite green by
7
+ skipping.
8
+
9
+ The thirteen stores write SQLite's dialect, which is the house dialect, and
10
+ `sql()` here renders it into PostgreSQL's. The census that says this is a
11
+ short list rather than a rewrite is docs/storage-backends.md section 4; what
12
+ it actually comes to:
13
+
14
+ placeholders `?` -> `%s`, outside string literals
15
+ upserts `INSERT OR REPLACE` -> `ON CONFLICT (pk) DO UPDATE SET ...`
16
+ `INSERT OR IGNORE` -> `ON CONFLICT DO NOTHING`
17
+ types nothing to do. The node's DDL uses TEXT and INTEGER and
18
+ nothing else across all 44 tables, and both are PostgreSQL
19
+ types.
20
+ WITHOUT ROWID dropped; it is a SQLite storage hint.
21
+ PRAGMA never reaches here; `tune` sets what a server needs instead.
22
+
23
+ The conflict target comes from the table's own DDL. Every `CREATE TABLE` in
24
+ the tree passes through `sql()` during `_ensure_schema`, before any INSERT can,
25
+ so the dialect learns each table's columns and primary key on the way past and
26
+ has them when an upsert arrives. All 44 tables declare a primary key, so
27
+ `DO UPDATE` always has a target - checked, not assumed.
28
+
29
+ One connection behind one lock, the shape BaseStore already had. That is what
30
+ makes this backend need no retry policy: with a single writer there is nothing
31
+ to serialize against, so no transaction is ever told to retry.
32
+ """
33
+ from __future__ import annotations
34
+
35
+ import re
36
+ import threading
37
+ from typing import Any, Optional
38
+
39
+ from fleetmesh_core.storage.store_postgres import ERROR_HINT, available, driver
40
+
41
+ __all__ = ['ERROR_HINT', 'PostgresDialect', 'available', 'driver', 'placeholders']
42
+
43
+
44
+ _CREATE = re.compile(r'CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?([A-Za-z_]\w*)\s*\((.*)\)\s*'
45
+ r'(?:WITHOUT\s+ROWID)?\s*;?\s*$', re.I | re.S)
46
+ _INLINE_PK = re.compile(r'^\s*([A-Za-z_]\w*)\s+\w+[^,]*\bPRIMARY\s+KEY\b', re.I)
47
+ _TABLE_PK = re.compile(r'\bPRIMARY\s+KEY\s*\(([^)]*)\)', re.I)
48
+ _UPSERT = re.compile(r'^(\s*)INSERT\s+OR\s+(REPLACE|IGNORE)\s+INTO\s+([A-Za-z_]\w*)\s*'
49
+ r'(\(([^)]*)\))?\s*(VALUES\b.*)$', re.I | re.S)
50
+
51
+
52
+ def _split_columns(body: str) -> list[str]:
53
+ """Top-level comma split of a CREATE TABLE body, ignoring nested parens."""
54
+ out, depth, cur = [], 0, ''
55
+ for ch in body:
56
+ if ch == '(':
57
+ depth += 1
58
+ elif ch == ')':
59
+ depth -= 1
60
+ if ch == ',' and depth == 0:
61
+ out.append(cur)
62
+ cur = ''
63
+ else:
64
+ cur += ch
65
+ if cur.strip():
66
+ out.append(cur)
67
+ return out
68
+
69
+
70
+ def placeholders(statement: str) -> str:
71
+ """`?` -> `%s`, leaving anything inside a single-quoted literal alone.
72
+
73
+ The node's SQL carries literals like `status='verified'`; a blind replace
74
+ would corrupt one that happened to contain a question mark.
75
+ """
76
+ out, in_lit, i = [], False, 0
77
+ while i < len(statement):
78
+ ch = statement[i]
79
+ if ch == "'":
80
+ # '' inside a literal is an escaped quote, not the end of it
81
+ if in_lit and i + 1 < len(statement) and statement[i + 1] == "'":
82
+ out.append("''")
83
+ i += 2
84
+ continue
85
+ in_lit = not in_lit
86
+ out.append(ch)
87
+ elif ch == '?' and not in_lit:
88
+ out.append('%s')
89
+ else:
90
+ out.append(ch)
91
+ i += 1
92
+ return ''.join(out)
93
+
94
+
95
+ class PostgresDialect:
96
+ """PostgreSQL. Learns each table's shape from the DDL it renders."""
97
+
98
+ name: str = 'postgres'
99
+ rowid_expr: Optional[str] = None # no insertion-order expression; see Dialect
100
+
101
+ def __init__(self) -> None:
102
+ self._cols: dict[str, list[str]] = {}
103
+ self._pk: dict[str, list[str]] = {}
104
+
105
+ # ---- connection -------------------------------------------------------
106
+
107
+ def connect(self, target: str, timeout: float, readonly: bool = False) -> Any:
108
+ pg = driver()
109
+ conn = pg.connect(target, connect_timeout=int(max(1, timeout)), autocommit=False)
110
+ if readonly:
111
+ conn.read_only = True
112
+ return conn
113
+
114
+ def tune(self, db: Any, target: str, lock: threading.RLock) -> None:
115
+ """What a server needs in place of SQLite's PRAGMAs: a statement
116
+ timeout so a wedged query cannot hold the one connection for ever."""
117
+ with lock:
118
+ cur = db.cursor()
119
+ try:
120
+ cur.execute("SET statement_timeout = '30s'")
121
+ cur.execute("SET idle_in_transaction_session_timeout = '60s'")
122
+ finally:
123
+ cur.close()
124
+ db.commit()
125
+
126
+ def learn_table(self, table: Any) -> None:
127
+ name = getattr(table, 'name', '')
128
+ if not name:
129
+ return
130
+ cols = [c.name for c in getattr(table, 'columns', ()) if hasattr(c, 'name')]
131
+ pk = list(getattr(table, 'primary_key', ()))
132
+ if not pk:
133
+ pk = [c.name for c in getattr(table, 'columns', ()) if getattr(c, 'primary_key', False)]
134
+ self._cols[name.lower()] = cols
135
+ if pk:
136
+ self._pk[name.lower()] = pk
137
+
138
+ def _learn(self, statement: str) -> None:
139
+ m = _CREATE.match(statement.strip())
140
+ if not m:
141
+ return
142
+ table, body = m.group(1), m.group(2)
143
+ cols, pk = [], []
144
+ for part in _split_columns(body):
145
+ tpk = _TABLE_PK.search(part)
146
+ if tpk and not re.match(r'^\s*[A-Za-z_]\w*\s+\w', part):
147
+ pk = [c.strip() for c in tpk.group(1).split(',') if c.strip()]
148
+ continue
149
+ name = re.match(r'^\s*([A-Za-z_]\w*)', part)
150
+ if not name:
151
+ continue
152
+ if name.group(1).upper() in ('PRIMARY', 'UNIQUE', 'FOREIGN', 'CHECK', 'CONSTRAINT'):
153
+ tpk = _TABLE_PK.search(part)
154
+ if tpk:
155
+ pk = [c.strip() for c in tpk.group(1).split(',') if c.strip()]
156
+ continue
157
+ cols.append(name.group(1))
158
+ if _INLINE_PK.match(part):
159
+ pk = [name.group(1)]
160
+ self._cols[table.lower()] = cols
161
+ if pk:
162
+ self._pk[table.lower()] = pk
163
+
164
+ def sql(self, statement: Any) -> str:
165
+ if hasattr(statement, 'to_sql'):
166
+ self.learn_table(statement)
167
+ statement = statement.to_sql('postgres')
168
+ else:
169
+ self._learn(statement)
170
+ out = re.sub(r'\s+WITHOUT\s+ROWID\b', '', statement, flags=re.I)
171
+ m = _UPSERT.match(out)
172
+ if m:
173
+ indent, mode, table, _, named, tail = m.groups()
174
+ key = table.lower()
175
+ if mode.upper() == 'IGNORE':
176
+ out = '%sINSERT INTO %s %s %s ON CONFLICT DO NOTHING' % (
177
+ indent, table, '(%s)' % named if named else '', tail)
178
+ else:
179
+ pk = self._pk.get(key, [])
180
+ cols = [c.strip() for c in named.split(',')] if named else self._cols.get(key, [])
181
+ sets = [c for c in cols if c.lower() not in {p.lower() for p in pk}]
182
+ if not pk:
183
+ raise RuntimeError(
184
+ 'INSERT OR REPLACE INTO %s needs a primary key to become an upsert, '
185
+ 'and none was declared in its CREATE TABLE' % table)
186
+ assign = ', '.join('%s = EXCLUDED.%s' % (c, c) for c in sets) or None
187
+ action = ('DO UPDATE SET %s' % assign) if assign else 'DO NOTHING'
188
+ out = '%sINSERT INTO %s %s %s ON CONFLICT (%s) %s' % (
189
+ indent, table, '(%s)' % named if named else '', tail,
190
+ ', '.join(pk), action)
191
+ return placeholders(out)
192
+
193
+ # ---- transaction control ----------------------------------------------
194
+ #
195
+ # The driver owns the transaction. psycopg opens one on the first
196
+ # statement and tracks it; issuing raw BEGIN/COMMIT/ROLLBACK alongside
197
+ # that leaves the two disagreeing about whether a transaction is open,
198
+ # and work committed by one path gets rolled back by the other. That is
199
+ # how the first Postgres run recorded schema version 2 while the column
200
+ # version 1 adds was missing: the ALTER applied and was then undone,
201
+ # while the version write survived on the other side of a raw COMMIT.
202
+
203
+ def begin(self, db: Any) -> None:
204
+ return None # psycopg begins on the first statement
205
+
206
+ def commit(self, db: Any) -> None:
207
+ db.commit()
208
+
209
+ def rollback(self, db: Any) -> None:
210
+ db.rollback()
211
+
212
+ # ---- schema version ---------------------------------------------------
213
+
214
+ def get_version(self, db: Any, store: str) -> int:
215
+ """The schema version of ONE store, keyed by its name.
216
+
217
+ SQLite gets this for free: each store is its own file, so
218
+ `PRAGMA user_version` is per store. Here all fourteen share one
219
+ database, and a single version row made them collide - AccountStore
220
+ wrote 2, GrantStore read 2, concluded its own migrations were applied
221
+ and skipped the ALTER that adds `grants.env_created_at`. The table was
222
+ then one column short of what its INSERT supplies, and the first
223
+ write failed with "INSERT has more expressions than target columns".
224
+ Only a multi-store test on a server could find that; one store alone
225
+ passes.
226
+ """
227
+ cur = db.cursor()
228
+ try:
229
+ cur.execute('CREATE TABLE IF NOT EXISTS fleetmesh_schema_version ('
230
+ ' store TEXT PRIMARY KEY, version INTEGER NOT NULL)')
231
+ cur.execute('SELECT version FROM fleetmesh_schema_version WHERE store = %s', (store,))
232
+ row = cur.fetchone()
233
+ finally:
234
+ cur.close()
235
+ return int(row[0]) if row else 0
236
+
237
+ def set_version(self, db: Any, store: str, version: int) -> None:
238
+ cur = db.cursor()
239
+ try:
240
+ cur.execute('INSERT INTO fleetmesh_schema_version VALUES (%s, %s) '
241
+ 'ON CONFLICT (store) DO UPDATE SET version = EXCLUDED.version',
242
+ (store, int(version)))
243
+ finally:
244
+ cur.close()
245
+
246
+ # ---- reflection and maintenance ---------------------------------------
247
+
248
+ def has_table(self, db: Any, table: str) -> bool:
249
+ cur = db.cursor()
250
+ try:
251
+ cur.execute('SELECT 1 FROM information_schema.tables '
252
+ 'WHERE table_schema = current_schema() AND table_name = %s', (table,))
253
+ return cur.fetchone() is not None
254
+ finally:
255
+ cur.close()
256
+
257
+ def has_column(self, db: Any, table: str, column: str) -> bool:
258
+ cur = db.cursor()
259
+ try:
260
+ cur.execute('SELECT 1 FROM information_schema.columns '
261
+ 'WHERE table_schema = current_schema() AND table_name = %s '
262
+ 'AND column_name = %s', (table, column))
263
+ return cur.fetchone() is not None
264
+ finally:
265
+ cur.close()
266
+
267
+ def is_duplicate_column(self, exc: BaseException) -> bool:
268
+ return getattr(getattr(exc, 'sqlstate', None), 'strip', lambda: '')() == '42701' \
269
+ or 'already exists' in str(exc).lower()
270
+
271
+ def vacuum(self, db: Any, pages: int) -> int:
272
+ """ANALYZE, and nothing more. Postgres reclaims by autovacuum; a manual
273
+ VACUUM here would block the one connection the node has, and
274
+ `pages` has no meaning on a server. Returns 0 - nothing was reclaimed
275
+ by this call, and saying otherwise would be a fake count."""
276
+ try:
277
+ db.commit()
278
+ with db.cursor() as cur:
279
+ cur.execute('ANALYZE')
280
+ db.commit()
281
+ except Exception:
282
+ return 0
283
+ return 0
284
+
285
+ def integrity_check(self, db: Any) -> Optional[str]:
286
+ """None. File integrity is a file-backed idea; a server answers for its
287
+ own soundness and `PRAGMA quick_check` has no counterpart here."""
288
+ return None
289
+
290
+ def errors(self) -> tuple[type[BaseException], ...]:
291
+ try:
292
+ return (driver().Error,)
293
+ except RuntimeError:
294
+ return ()