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.
- fleetmesh_node-0.2.0.dist-info/METADATA +57 -0
- fleetmesh_node-0.2.0.dist-info/RECORD +338 -0
- fleetmesh_node-0.2.0.dist-info/WHEEL +5 -0
- fleetmesh_node-0.2.0.dist-info/entry_points.txt +2 -0
- fleetmesh_node-0.2.0.dist-info/licenses/LICENSE +121 -0
- fleetmesh_node-0.2.0.dist-info/top_level.txt +2 -0
- node/RELEASE +1 -0
- node/__init__.py +35 -0
- node/app/__init__.py +49 -0
- node/app/commands.py +265 -0
- node/app/config.py +131 -0
- node/app/control.py +670 -0
- node/app/core.py +867 -0
- node/app/plugins.py +76 -0
- node/app/router.py +83 -0
- node/app/socket.py +431 -0
- node/app/systemd.py +36 -0
- node/app/views.py +204 -0
- node/channels/__init__.py +378 -0
- node/channels/bus.py +331 -0
- node/channels/journal.py +46 -0
- node/commands/__init__.py +140 -0
- node/commands/adapter.py +210 -0
- node/commands/base.py +278 -0
- node/commands/cluster_cmds.py +183 -0
- node/commands/compute_cmds.py +401 -0
- node/commands/core.py +474 -0
- node/commands/gossip_cmds.py +106 -0
- node/commands/nsite_cmds.py +141 -0
- node/commands/peering.py +402 -0
- node/commands/plugin_cmds.py +135 -0
- node/commands/sidecar_cmds.py +120 -0
- node/commands/sync.py +388 -0
- node/commands/transport_cmds.py +451 -0
- node/commands/video_cmds.py +99 -0
- node/compartment/__init__.py +124 -0
- node/compartment/base.py +868 -0
- node/compartment/command.py +458 -0
- node/compartment/context.py +163 -0
- node/compartment/every.py +108 -0
- node/compartment/handler.py +131 -0
- node/compartment/lifecycle.py +460 -0
- node/compartment/lint.py +388 -0
- node/compartment/need.py +158 -0
- node/compartment/problem.py +521 -0
- node/compartment/refusals.py +120 -0
- node/compartment/registry.py +790 -0
- node/compartment/schema.py +290 -0
- node/compartment/sidecar.py +499 -0
- node/compartment/stores.py +662 -0
- node/compartment/wire.py +225 -0
- node/compartment/worker.py +61 -0
- node/config/__init__.py +43 -0
- node/config/base.py +116 -0
- node/config/change.py +441 -0
- node/config/changestore.py +201 -0
- node/config/compartments.py +1144 -0
- node/config/node.py +318 -0
- node/config/parameter.py +563 -0
- node/config/protocols.py +274 -0
- node/config/records.py +171 -0
- node/examples/__init__.py +1 -0
- node/examples/demo.py +226 -0
- node/examples/follower.py +46 -0
- node/interfaces.py +235 -0
- node/keystore/__init__.py +27 -0
- node/keystore/core.py +103 -0
- node/keystore/enclave.py +688 -0
- node/meshnode.py +111 -0
- node/metrics/__init__.py +396 -0
- node/metrics/adapters.py +102 -0
- node/metrics/face.py +110 -0
- node/planes/__init__.py +21 -0
- node/planes/identity/__init__.py +20 -0
- node/planes/identity/announce/__init__.py +9 -0
- node/planes/identity/announce/compartment.py +447 -0
- node/planes/identity/announce/types.py +26 -0
- node/planes/identity/discovery/__init__.py +9 -0
- node/planes/identity/discovery/compartment.py +522 -0
- node/planes/identity/discovery/types.py +13 -0
- node/planes/identity/linked/__init__.py +1 -0
- node/planes/identity/linked/compartment.py +270 -0
- node/planes/identity/linked/types.py +25 -0
- node/planes/identity/presence/__init__.py +7 -0
- node/planes/identity/presence/compartment.py +409 -0
- node/planes/identity/presence/types.py +14 -0
- node/planes/operations/__init__.py +13 -0
- node/planes/operations/scheduler/__init__.py +9 -0
- node/planes/operations/scheduler/compartment.py +353 -0
- node/planes/operations/scheduler/types.py +21 -0
- node/planes/operations/updates/__init__.py +17 -0
- node/planes/operations/updates/core.py +310 -0
- node/planes/operations/updates/store.py +79 -0
- node/planes/operations/updates/types.py +20 -0
- node/planes/peering/__init__.py +28 -0
- node/planes/peering/budget/__init__.py +43 -0
- node/planes/peering/budget/chain.py +43 -0
- node/planes/peering/budget/core.py +537 -0
- node/planes/peering/budget/evaluation.py +80 -0
- node/planes/peering/budget/meter.py +100 -0
- node/planes/peering/budget/receipts.py +439 -0
- node/planes/peering/budget/shape.py +101 -0
- node/planes/peering/budget/tick.py +110 -0
- node/planes/peering/budget/types.py +67 -0
- node/planes/peering/budget/windows.py +346 -0
- node/planes/peering/capacity/__init__.py +7 -0
- node/planes/peering/capacity/compartment.py +342 -0
- node/planes/peering/capacity/types.py +24 -0
- node/planes/peering/peering/__init__.py +25 -0
- node/planes/peering/peering/core.py +464 -0
- node/planes/peering/peering/envelope.py +142 -0
- node/planes/peering/peering/handshake.py +198 -0
- node/planes/peering/peering/shape.py +80 -0
- node/planes/peering/peering/types.py +39 -0
- node/planes/peering/reporter/__init__.py +42 -0
- node/planes/peering/reporter/constants.py +29 -0
- node/planes/peering/reporter/core.py +299 -0
- node/planes/peering/reporter/finding.py +74 -0
- node/planes/peering/reporter/findings.py +171 -0
- node/planes/peering/reporter/publish.py +285 -0
- node/planes/peering/reporter/reading.py +193 -0
- node/planes/peering/reporter/shape.py +86 -0
- node/planes/peering/reporter/types.py +50 -0
- node/planes/peering/strain/__init__.py +9 -0
- node/planes/peering/strain/compartment.py +204 -0
- node/planes/peering/strain/types.py +21 -0
- node/planes/peering/trust/__init__.py +9 -0
- node/planes/peering/trust/compartment.py +239 -0
- node/planes/peering/trust/types.py +30 -0
- node/planes/services/__init__.py +33 -0
- node/planes/services/compute/__init__.py +160 -0
- node/planes/services/compute/common.py +134 -0
- node/planes/services/compute/compartment.py +681 -0
- node/planes/services/compute/executors/__init__.py +121 -0
- node/planes/services/compute/executors/agent.py +840 -0
- node/planes/services/compute/executors/bacalhau.py +605 -0
- node/planes/services/compute/executors/base.py +127 -0
- node/planes/services/compute/executors/harness.py +439 -0
- node/planes/services/compute/executors/mcp.py +454 -0
- node/planes/services/compute/executors/media.py +898 -0
- node/planes/services/compute/executors/nondet.py +283 -0
- node/planes/services/compute/executors/providers.py +499 -0
- node/planes/services/compute/executors/registry.py +140 -0
- node/planes/services/compute/executors/wasm.py +244 -0
- node/planes/services/compute/executors/zk.py +604 -0
- node/planes/services/compute/jobs.py +227 -0
- node/planes/services/compute/load.py +180 -0
- node/planes/services/compute/market.py +433 -0
- node/planes/services/compute/sandbox.py +282 -0
- node/planes/services/compute/types.py +84 -0
- node/planes/services/compute_wire/__init__.py +58 -0
- node/planes/services/compute_wire/compartment.py +256 -0
- node/planes/services/compute_wire/messages.py +320 -0
- node/planes/services/compute_wire/sealing.py +147 -0
- node/planes/services/compute_wire/session.py +572 -0
- node/planes/services/compute_wire/types.py +49 -0
- node/planes/services/compute_wire/verify.py +331 -0
- node/planes/services/dvm/__init__.py +24 -0
- node/planes/services/dvm/gateway.py +526 -0
- node/planes/services/dvm/protocol.py +35 -0
- node/planes/services/dvm/translate.py +15 -0
- node/planes/services/dvm/types.py +25 -0
- node/planes/services/gateway/__init__.py +40 -0
- node/planes/services/gateway/settle.py +279 -0
- node/planes/services/gateway/translate.py +644 -0
- node/planes/services/gossip/__init__.py +22 -0
- node/planes/services/gossip/core.py +929 -0
- node/planes/services/gossip/envelope.py +154 -0
- node/planes/services/gossip/mesh.py +127 -0
- node/planes/services/gossip/types.py +50 -0
- node/planes/services/nsite/__init__.py +7 -0
- node/planes/services/nsite/client.py +62 -0
- node/planes/services/nsite/compartment.py +752 -0
- node/planes/services/nsite/types.py +44 -0
- node/planes/services/paidfetch/__init__.py +7 -0
- node/planes/services/paidfetch/compartment.py +862 -0
- node/planes/services/paidfetch/types.py +99 -0
- node/planes/services/settle/__init__.py +27 -0
- node/planes/services/settle/compartment.py +384 -0
- node/planes/services/settle/engine.py +456 -0
- node/planes/services/settle/gateways.py +541 -0
- node/planes/services/settle/payment.py +512 -0
- node/planes/services/settle/store.py +74 -0
- node/planes/services/settle/types.py +58 -0
- node/planes/services/sidecars/__init__.py +8 -0
- node/planes/services/sidecars/compartment.py +461 -0
- node/planes/services/sidecars/manifest.py +304 -0
- node/planes/services/sidecars/manifests/10-grasp.json +37 -0
- node/planes/services/sidecars/manifests/20-ncontainer.json +31 -0
- node/planes/services/sidecars/manifests/30-magpie.json +28 -0
- node/planes/services/sidecars/types.py +43 -0
- node/planes/services/video/__init__.py +106 -0
- node/planes/services/video/barter.py +407 -0
- node/planes/services/video/cli.py +158 -0
- node/planes/services/video/compartment.py +932 -0
- node/planes/services/video/ingest.py +524 -0
- node/planes/services/video/ledger.py +705 -0
- node/planes/services/video/packager.py +847 -0
- node/planes/services/video/player.py +456 -0
- node/planes/services/video/timed_segments.py +124 -0
- node/planes/services/video/transit.py +478 -0
- node/planes/store/__init__.py +24 -0
- node/planes/store/blob_replication/__init__.py +7 -0
- node/planes/store/blob_replication/compartment.py +496 -0
- node/planes/store/blob_replication/types.py +35 -0
- node/planes/store/blob_wire/__init__.py +48 -0
- node/planes/store/blob_wire/client.py +57 -0
- node/planes/store/blob_wire/core.py +359 -0
- node/planes/store/blob_wire/intake.py +74 -0
- node/planes/store/blob_wire/messages.py +85 -0
- node/planes/store/projected/__init__.py +7 -0
- node/planes/store/projected/compartment.py +258 -0
- node/planes/store/projected/types.py +29 -0
- node/planes/store/retention/__init__.py +7 -0
- node/planes/store/retention/compartment.py +214 -0
- node/planes/store/retention/types.py +12 -0
- node/planes/store/sync/__init__.py +35 -0
- node/planes/store/sync/core.py +311 -0
- node/planes/store/sync/envelope.py +106 -0
- node/planes/store/sync/handover.py +206 -0
- node/planes/store/sync/initiator.py +128 -0
- node/planes/store/sync/responder.py +114 -0
- node/planes/store/sync/rules.py +179 -0
- node/planes/store/sync/shape.py +80 -0
- node/planes/store/sync/types.py +51 -0
- node/planes/transport/__init__.py +23 -0
- node/planes/transport/bridge/__init__.py +13 -0
- node/planes/transport/bridge/compartment.py +785 -0
- node/planes/transport/bridge/ratelimit.py +44 -0
- node/planes/transport/bridge/types.py +43 -0
- node/planes/transport/lan/__init__.py +9 -0
- node/planes/transport/lan/compartment.py +446 -0
- node/planes/transport/lan/types.py +25 -0
- node/planes/transport/link/__init__.py +7 -0
- node/planes/transport/link/compartment.py +257 -0
- node/planes/transport/link/types.py +27 -0
- node/planes/transport/mediation/__init__.py +45 -0
- node/planes/transport/mediation/compartment.py +238 -0
- node/planes/transport/mediation/leaf.py +107 -0
- node/planes/transport/mediation/messages.py +302 -0
- node/planes/transport/mediation/service.py +233 -0
- node/planes/transport/mediation/store.py +37 -0
- node/planes/transport/mediation/types.py +20 -0
- node/planes/transport/p2p/__init__.py +7 -0
- node/planes/transport/p2p/compartment.py +349 -0
- node/planes/transport/p2p/types.py +22 -0
- node/protocols/__init__.py +277 -0
- node/protocols/activitypub/__init__.py +56 -0
- node/protocols/activitypub/actor.py +230 -0
- node/protocols/activitypub/federation.py +490 -0
- node/protocols/activitypub/media.py +145 -0
- node/protocols/activitypub/models.py +286 -0
- node/protocols/activitypub/package.py +271 -0
- node/protocols/activitypub/store.py +564 -0
- node/protocols/arcade/__init__.py +26 -0
- node/protocols/atproto/__init__.py +13 -0
- node/protocols/atproto/firehose.py +126 -0
- node/protocols/atproto/metering.py +165 -0
- node/protocols/atproto/package.py +155 -0
- node/protocols/atproto/repo.py +250 -0
- node/protocols/atproto/xrpc.py +285 -0
- node/protocols/base.py +109 -0
- node/protocols/circles/__init__.py +29 -0
- node/protocols/dm/__init__.py +50 -0
- node/protocols/grasp/__init__.py +22 -0
- node/protocols/isotime.py +15 -0
- node/protocols/nostr/__init__.py +51 -0
- node/protocols/nostr/blobstore.py +383 -0
- node/protocols/nostr/blossom.py +627 -0
- node/protocols/nostr/client.py +377 -0
- node/protocols/nostr/descriptor.py +211 -0
- node/protocols/nostr/eventstore.py +304 -0
- node/protocols/nostr/package.py +182 -0
- node/protocols/nostr/peers.py +109 -0
- node/protocols/nostr/relayserver.py +656 -0
- node/protocols/p2panda/__init__.py +22 -0
- node/protocols/p2panda/identity.py +19 -0
- node/protocols/p2panda/package.py +236 -0
- node/protocols/p2panda/rung.py +152 -0
- node/protocols/p2panda/store.py +273 -0
- node/protocols/rhio/__init__.py +33 -0
- node/protocols/rhio/package.py +275 -0
- node/protocols/rhio/rung.py +136 -0
- node/protocols/rhio/store.py +1423 -0
- node/protocols/ssb/muxrpc.py +256 -0
- node/protocols/ssb/package.py +267 -0
- node/protocols/textile/__init__.py +111 -0
- node/protocols/textile/transducers.py +259 -0
- node/release.py +119 -0
- node/storage/__init__.py +88 -0
- node/storage/accounts.py +305 -0
- node/storage/adapter.py +161 -0
- node/storage/base.py +385 -0
- node/storage/dialect.py +209 -0
- node/storage/grants.py +317 -0
- node/storage/locator.py +150 -0
- node/storage/postgres.py +294 -0
- node/storage/projected.py +953 -0
- node/storage/sharding.py +448 -0
- node/storage/standing.py +568 -0
- node/testnet/__init__.py +9 -0
- node/testnet/cluster.py +731 -0
- node/testnet/fips_daemon.py +230 -0
- node/transport/__init__.py +94 -0
- node/transport/const.py +110 -0
- node/transport/didcomm_inbound.py +251 -0
- node/transport/dnstxt.py +169 -0
- node/transport/fips_rung/__init__.py +67 -0
- node/transport/fips_rung/core.py +666 -0
- node/transport/fips_rung/listener.py +96 -0
- node/transport/fips_rung/types.py +12 -0
- node/transport/host.py +334 -0
- node/transport/iroh_rung/__init__.py +29 -0
- node/transport/iroh_rung/core.py +477 -0
- node/transport/iroh_rung/quic.py +942 -0
- node/transport/iroh_rung/types.py +22 -0
- node/transport/ladder.py +145 -0
- node/transport/lineproto.py +49 -0
- node/transport/mdns.py +303 -0
- node/transport/netfetch.py +283 -0
- node/transport/relay_rung/__init__.py +23 -0
- node/transport/relay_rung/cooldown.py +87 -0
- node/transport/relay_rung/core.py +138 -0
- node/transport/relay_rung/inbound.py +161 -0
- node/transport/relay_rung/ledger.py +104 -0
- node/transport/relay_rung/listen.py +121 -0
- node/transport/relay_rung/outbound.py +181 -0
- node/transport/relay_rung/shape.py +71 -0
- node/transport/relay_rung/types.py +58 -0
- node/transport/relay_rung/wire.py +63 -0
- node/transport/router.py +761 -0
- node/transport/rung.py +276 -0
- node/transport/ssb_rung.py +166 -0
- node/transport/ws.py +502 -0
- sidecar/fleetmesh_announce.py +338 -0
- sidecar/fleetmesh_p2p.py +373 -0
- sidecar/fleetmesh_rhio.py +352 -0
- sidecar/fleetmesh_ssb.py +603 -0
node/storage/postgres.py
ADDED
|
@@ -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 ()
|