cctally 1.117.2 → 1.118.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.
- package/CHANGELOG.md +30 -0
- package/README.md +7 -3
- package/bin/_cctally_cache.py +2783 -175
- package/bin/_cctally_completion.py +106 -33
- package/bin/_cctally_core.py +69 -4
- package/bin/_cctally_dashboard.py +188 -13
- package/bin/_cctally_dashboard_conversation.py +5 -1
- package/bin/_cctally_dashboard_envelope.py +50 -19
- package/bin/_cctally_dashboard_sources.py +23 -1
- package/bin/_cctally_db.py +193 -153
- package/bin/_cctally_diagnosis_sources.py +63 -7
- package/bin/_cctally_doctor.py +865 -93
- package/bin/_cctally_five_hour.py +67 -50
- package/bin/_cctally_forecast.py +35 -13
- package/bin/_cctally_journal.py +186 -18
- package/bin/_cctally_milestone_history.py +859 -62
- package/bin/_cctally_oauth_samples.py +286 -15
- package/bin/_cctally_parser.py +2 -2
- package/bin/_cctally_quota.py +104 -12
- package/bin/_cctally_record.py +153 -29
- package/bin/_cctally_rederive.py +608 -68
- package/bin/_cctally_statusline.py +108 -81
- package/bin/_cctally_store.py +179 -38
- package/bin/_cctally_tui.py +892 -181
- package/bin/_cctally_update.py +32 -13
- package/bin/_lib_codex_conversation_query.py +52 -14
- package/bin/_lib_codex_drain_admission.py +224 -6
- package/bin/_lib_codex_metadata.py +39 -0
- package/bin/_lib_conversation_anon.py +15 -8
- package/bin/_lib_conversation_dispatch.py +7 -0
- package/bin/_lib_conversation_query.py +289 -70
- package/bin/_lib_dashboard_sources.py +37 -1
- package/bin/_lib_detector_access_registry.py +497 -26
- package/bin/_lib_doctor.py +85 -20
- package/bin/_lib_doctor_scan_index.py +321 -0
- package/bin/_lib_ingest_frontier.py +20 -1
- package/bin/_lib_oauth_samples.py +23 -7
- package/bin/_lib_pricing.py +146 -16
- package/bin/_lib_quota.py +19 -0
- package/bin/_lib_rederive.py +11 -6
- package/bin/_lib_retained_records.py +516 -0
- package/bin/_lib_snapshot_cache.py +10 -0
- package/bin/_lib_stale_proof.py +272 -43
- package/bin/_lib_stats_content_revision.py +831 -0
- package/bin/_lib_write_io.py +11 -4
- package/bin/cctally +4 -1
- package/dashboard/static/assets/ConversationsView-D4Iv4ku0.js +72 -0
- package/dashboard/static/assets/{DoctorModal-B0DnU9d9.js → DoctorModal-D4QSq6uz.js} +1 -1
- package/dashboard/static/assets/ModalRoot-C9F5dhZi.js +1 -0
- package/dashboard/static/assets/{ProjectsDrillPanel-DSxXQSa-.js → ProjectsDrillPanel-WxAdRDMt.js} +1 -1
- package/dashboard/static/assets/{SourceDetailModal-BleDpQqG.js → SourceDetailModal-NyyEfXmc.js} +1 -1
- package/dashboard/static/assets/{UpdateModal-CMqaAkSK.js → UpdateModal-CFLq9pT1.js} +1 -1
- package/dashboard/static/assets/{index-BjQV-0-D.js → index-BKTWH9JE.js} +2 -2
- package/dashboard/static/assets/{outlineNavigation-CO-VWBm7.js → outlineNavigation-BnGUwCS3.js} +3 -3
- package/dashboard/static/dashboard.html +1 -1
- package/package.json +4 -1
- package/dashboard/static/assets/ConversationsView-oqNsWzWV.js +0 -72
- package/dashboard/static/assets/ModalRoot-CYua2wP_.js +0 -1
|
@@ -0,0 +1,831 @@
|
|
|
1
|
+
"""The trigger-maintained content revision of stats.db's Claude domain (#1050).
|
|
2
|
+
|
|
3
|
+
An in-place rewrite of a Claude-published stats row (a Model-A convergence, a
|
|
4
|
+
reviewed rederive, the open-block projection, a probe ``UPDATE``) moves none of
|
|
5
|
+
the append-shaped legs the dashboard compares (`MAX(id)`, `_reset_sig`, the
|
|
6
|
+
armed-row digests). This kernel gives that domain one identity
|
|
7
|
+
(spec `docs/superpowers/specs/2026-11-04-1050-correction-signal-design.md`
|
|
8
|
+
§4.1–§4.4):
|
|
9
|
+
|
|
10
|
+
* ``stats_content_revision`` — a singleton row carrying the kernel
|
|
11
|
+
``format_version``, the ``domain_generation`` an installation repair bumps,
|
|
12
|
+
and ``seq``, the global sequence every tracked mutation advances;
|
|
13
|
+
* ``stats_content_revision_scopes`` — per ``(kind, account_key, scope_key)``
|
|
14
|
+
the ``seq`` of the latest tracked mutation that dirtied that scope, with the
|
|
15
|
+
epoch range a ``block`` scope spans.
|
|
16
|
+
|
|
17
|
+
Triggers on the §4.2 tables do the bookkeeping inside the writer's own
|
|
18
|
+
statement: the singleton is advanced FIRST, then each dirtied scope is
|
|
19
|
+
upserted with ``revision`` = the new ``seq``. A rolled-back write rolls its
|
|
20
|
+
trigger effects back with it, and an assignment of identical values fires
|
|
21
|
+
nothing (every ``WHEN`` compares null-safely with ``IS NOT``). The revision
|
|
22
|
+
tables carry no triggers and ``recursive_triggers`` stays off.
|
|
23
|
+
|
|
24
|
+
Installation never bumps the stats epoch. ``classify`` reads only (one bounded
|
|
25
|
+
singleton read plus one ``sqlite_schema`` read), so a ``current`` open writes
|
|
26
|
+
and locks nothing; ``ensure_stats_content_revision`` installs an ``absent``
|
|
27
|
+
store and repairs a ``partial`` one under the open-time maintenance guard with
|
|
28
|
+
a zero wait, and leaves the store untouched on contention.
|
|
29
|
+
|
|
30
|
+
``read_claude_content_revision`` returns the opaque equality token of the
|
|
31
|
+
Claude content domain within stats.db, including its publication generation
|
|
32
|
+
(the #790 contract): the token changes on every tracked mutation, on every
|
|
33
|
+
repair and on every publication, and is ``""`` only for a never-mutated,
|
|
34
|
+
never-repaired, never-published store, so a fresh store stays byte-neutral.
|
|
35
|
+
|
|
36
|
+
Stdlib only, and no sibling import statement (core imports this module, so it
|
|
37
|
+
must stay a proven leaf). The guard and its contention error are reached at
|
|
38
|
+
call time, and only when an install is needed.
|
|
39
|
+
"""
|
|
40
|
+
from __future__ import annotations
|
|
41
|
+
|
|
42
|
+
import hashlib
|
|
43
|
+
import importlib
|
|
44
|
+
import json
|
|
45
|
+
import re
|
|
46
|
+
import secrets
|
|
47
|
+
import sqlite3
|
|
48
|
+
import types
|
|
49
|
+
from typing import Mapping, NamedTuple, Sequence
|
|
50
|
+
|
|
51
|
+
#: The kernel's object format. A store whose singleton names a higher format is
|
|
52
|
+
#: ``unsupported`` (left alone); a lower one is ``partial`` (repaired in place).
|
|
53
|
+
KERNEL_FORMAT_VERSION: int = 1
|
|
54
|
+
|
|
55
|
+
STATE_CURRENT = "current"
|
|
56
|
+
STATE_ABSENT = "absent"
|
|
57
|
+
STATE_PARTIAL = "partial"
|
|
58
|
+
STATE_UNSUPPORTED = "unsupported"
|
|
59
|
+
STATE_MALFORMED = "malformed"
|
|
60
|
+
STATES = frozenset({
|
|
61
|
+
STATE_CURRENT, STATE_ABSENT, STATE_PARTIAL, STATE_UNSUPPORTED,
|
|
62
|
+
STATE_MALFORMED,
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
REVISION_TABLE = "stats_content_revision"
|
|
66
|
+
SCOPE_TABLE = "stats_content_revision_scopes"
|
|
67
|
+
USAGE_SNAPSHOT_INDEX = "idx_five_hour_milestones_usage_snapshot_id"
|
|
68
|
+
TRIGGER_PREFIX = "trg_content_revision_"
|
|
69
|
+
|
|
70
|
+
SCOPE_KINDS = ("cohort", "bucket", "block", "reset_point")
|
|
71
|
+
|
|
72
|
+
REVISION_TABLE_SQL = (
|
|
73
|
+
"CREATE TABLE stats_content_revision ("
|
|
74
|
+
"singleton INTEGER PRIMARY KEY CHECK (singleton = 1), "
|
|
75
|
+
"format_version INTEGER NOT NULL CHECK (format_version >= 1), "
|
|
76
|
+
"domain_generation INTEGER NOT NULL DEFAULT 0 "
|
|
77
|
+
"CHECK (domain_generation >= 0), "
|
|
78
|
+
"seq INTEGER NOT NULL DEFAULT 0 CHECK (seq >= 0))"
|
|
79
|
+
)
|
|
80
|
+
SCOPE_TABLE_SQL = (
|
|
81
|
+
"CREATE TABLE stats_content_revision_scopes ("
|
|
82
|
+
"kind TEXT NOT NULL "
|
|
83
|
+
"CHECK (kind IN ('cohort', 'bucket', 'block', 'reset_point')), "
|
|
84
|
+
"account_key TEXT NOT NULL, "
|
|
85
|
+
"scope_key TEXT NOT NULL, "
|
|
86
|
+
"start_epoch INTEGER, "
|
|
87
|
+
"end_epoch INTEGER, "
|
|
88
|
+
"revision INTEGER NOT NULL, "
|
|
89
|
+
"PRIMARY KEY (kind, account_key, scope_key))"
|
|
90
|
+
)
|
|
91
|
+
#: §4.2: every trigger lookup is a primary-key or indexed equality, and the
|
|
92
|
+
#: snapshot → five-hour milestone reference had no index of its own.
|
|
93
|
+
USAGE_SNAPSHOT_INDEX_SQL = (
|
|
94
|
+
"CREATE INDEX idx_five_hour_milestones_usage_snapshot_id "
|
|
95
|
+
"ON five_hour_milestones(usage_snapshot_id)"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class ContentRevision(NamedTuple):
|
|
100
|
+
"""One read of the identity. ``token``/``cycle_basis`` are ``None`` in
|
|
101
|
+
every untrusted state (``absent``, ``partial``, ``unsupported``)."""
|
|
102
|
+
|
|
103
|
+
state: str
|
|
104
|
+
token: "str | None"
|
|
105
|
+
cycle_basis: "str | None"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class StatsContentRevisionUnreadable(Exception):
|
|
109
|
+
"""The identity cannot be read: a ``malformed`` store, or more than one
|
|
110
|
+
``stats_publication_stamp`` row. Callers treat it like any other failed
|
|
111
|
+
dispatch read (a nonce leg), never as an empty identity."""
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class TableClassification(NamedTuple):
|
|
115
|
+
"""One §4.2 row. ``compared`` columns fire the UPDATE trigger when they
|
|
116
|
+
differ null-safely; ``excluded`` columns are bookkeeping and never do.
|
|
117
|
+
``insert_rule``: ``"tracked"`` (every INSERT), ``"below_max"`` (only an
|
|
118
|
+
INSERT below the table's ``MAX(id)``), ``"exempt"`` (another leg already
|
|
119
|
+
covers INSERT) or ``"untracked"``. ``scopes``: the scope kinds dirtied
|
|
120
|
+
from OLD and NEW (``()`` = the global ``seq`` only). ``owner_predicate``:
|
|
121
|
+
a SQL predicate over ``{R}`` (the OLD/NEW row) restricting tracking to the
|
|
122
|
+
Claude-owned rows, or ``None``."""
|
|
123
|
+
|
|
124
|
+
compared: "tuple[str, ...]"
|
|
125
|
+
excluded: "tuple[str, ...]"
|
|
126
|
+
insert_rule: str
|
|
127
|
+
scopes: "tuple[str, ...]"
|
|
128
|
+
owner_predicate: "str | None" = None
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class ScopeRow(NamedTuple):
|
|
132
|
+
kind: str
|
|
133
|
+
account_key: str
|
|
134
|
+
scope_key: str
|
|
135
|
+
start_epoch: "int | None"
|
|
136
|
+
end_epoch: "int | None"
|
|
137
|
+
revision: int
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# ── §4.2 the classification table ─────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
TRACKED_TABLES: "Mapping[str, TableClassification]" = types.MappingProxyType({
|
|
143
|
+
"weekly_usage_snapshots": TableClassification(
|
|
144
|
+
compared=(
|
|
145
|
+
"id", "captured_at_utc", "week_start_date", "week_end_date",
|
|
146
|
+
"week_start_at", "week_end_at", "weekly_percent", "page_url",
|
|
147
|
+
"source", "payload_json", "account_key",
|
|
148
|
+
"weekly_observation_held", "five_hour_percent",
|
|
149
|
+
"five_hour_resets_at", "five_hour_window_key", "journal_id",
|
|
150
|
+
),
|
|
151
|
+
excluded=(), insert_rule="below_max", scopes=("bucket", "block"),
|
|
152
|
+
),
|
|
153
|
+
"weekly_cost_snapshots": TableClassification(
|
|
154
|
+
compared=(
|
|
155
|
+
"id", "captured_at_utc", "week_start_date", "week_end_date",
|
|
156
|
+
"week_start_at", "week_end_at", "range_start_iso",
|
|
157
|
+
"range_end_iso", "cost_usd", "source", "mode", "project",
|
|
158
|
+
"account_key", "journal_id",
|
|
159
|
+
),
|
|
160
|
+
excluded=(), insert_rule="below_max", scopes=("bucket",),
|
|
161
|
+
),
|
|
162
|
+
"percent_milestones": TableClassification(
|
|
163
|
+
compared=(
|
|
164
|
+
"id", "captured_at_utc", "week_start_date", "week_end_date",
|
|
165
|
+
"week_start_at", "week_end_at", "percent_threshold",
|
|
166
|
+
"cumulative_cost_usd", "marginal_cost_usd", "usage_snapshot_id",
|
|
167
|
+
"cost_snapshot_id", "reset_event_id", "account_key",
|
|
168
|
+
"five_hour_percent_at_crossing", "alerted_at", "journal_id",
|
|
169
|
+
),
|
|
170
|
+
excluded=(), insert_rule="tracked", scopes=("cohort", "bucket"),
|
|
171
|
+
),
|
|
172
|
+
"five_hour_milestones": TableClassification(
|
|
173
|
+
compared=(
|
|
174
|
+
"id", "block_id", "five_hour_window_key", "percent_threshold",
|
|
175
|
+
"captured_at_utc", "usage_snapshot_id", "block_input_tokens",
|
|
176
|
+
"block_output_tokens", "block_cache_create_tokens",
|
|
177
|
+
"block_cache_read_tokens", "block_cost_usd", "marginal_cost_usd",
|
|
178
|
+
"seven_day_pct_at_crossing", "reset_event_id", "account_key",
|
|
179
|
+
"alerted_at", "journal_id",
|
|
180
|
+
),
|
|
181
|
+
excluded=(), insert_rule="tracked", scopes=("block",),
|
|
182
|
+
),
|
|
183
|
+
"five_hour_blocks": TableClassification(
|
|
184
|
+
compared=(
|
|
185
|
+
"id", "five_hour_window_key", "five_hour_resets_at",
|
|
186
|
+
"block_start_at", "first_observed_at_utc", "last_observed_at_utc",
|
|
187
|
+
"final_five_hour_percent", "seven_day_pct_at_block_start",
|
|
188
|
+
"seven_day_pct_at_block_end", "crossed_seven_day_reset",
|
|
189
|
+
"total_input_tokens", "total_output_tokens",
|
|
190
|
+
"total_cache_create_tokens", "total_cache_read_tokens",
|
|
191
|
+
"total_cost_usd", "pricing_provenance_json", "is_closed",
|
|
192
|
+
"account_key", "journal_id",
|
|
193
|
+
),
|
|
194
|
+
excluded=("created_at_utc", "last_updated_at_utc"),
|
|
195
|
+
insert_rule="tracked", scopes=("block",),
|
|
196
|
+
),
|
|
197
|
+
"five_hour_block_models": TableClassification(
|
|
198
|
+
compared=(
|
|
199
|
+
"id", "block_id", "five_hour_window_key", "model",
|
|
200
|
+
"input_tokens", "output_tokens", "cache_create_tokens",
|
|
201
|
+
"cache_read_tokens", "cost_usd", "entry_count", "account_key",
|
|
202
|
+
),
|
|
203
|
+
excluded=(), insert_rule="tracked", scopes=(),
|
|
204
|
+
),
|
|
205
|
+
"five_hour_block_projects": TableClassification(
|
|
206
|
+
compared=(
|
|
207
|
+
"id", "block_id", "five_hour_window_key", "project_path",
|
|
208
|
+
"input_tokens", "output_tokens", "cache_create_tokens",
|
|
209
|
+
"cache_read_tokens", "cost_usd", "entry_count", "account_key",
|
|
210
|
+
),
|
|
211
|
+
excluded=(), insert_rule="tracked", scopes=(),
|
|
212
|
+
),
|
|
213
|
+
"five_hour_reset_events": TableClassification(
|
|
214
|
+
compared=(
|
|
215
|
+
"id", "detected_at_utc", "five_hour_window_key", "prior_percent",
|
|
216
|
+
"post_percent", "effective_reset_at_utc", "account_key",
|
|
217
|
+
"journal_id",
|
|
218
|
+
),
|
|
219
|
+
excluded=(), insert_rule="tracked", scopes=("block",),
|
|
220
|
+
),
|
|
221
|
+
"week_reset_events": TableClassification(
|
|
222
|
+
compared=(
|
|
223
|
+
"id", "detected_at_utc", "old_week_end_at", "new_week_end_at",
|
|
224
|
+
"effective_reset_at_utc", "observed_pre_credit_pct",
|
|
225
|
+
"account_key", "origin_observation_id", "journal_id",
|
|
226
|
+
),
|
|
227
|
+
excluded=(), insert_rule="exempt", scopes=("reset_point",),
|
|
228
|
+
),
|
|
229
|
+
"weekly_credit_floors": TableClassification(
|
|
230
|
+
compared=(
|
|
231
|
+
"id", "week_start_date", "effective_at_utc",
|
|
232
|
+
"observed_pre_credit_pct", "account_key", "journal_id",
|
|
233
|
+
),
|
|
234
|
+
excluded=("applied_at_utc",), insert_rule="exempt", scopes=(),
|
|
235
|
+
),
|
|
236
|
+
"claude_weekly_observation_index": TableClassification(
|
|
237
|
+
compared=(
|
|
238
|
+
"observation_id", "account_key", "captured_at_utc",
|
|
239
|
+
"weekly_percent", "weekly_resets_at_utc",
|
|
240
|
+
"weekly_reading_genuine", "weekly_freshness_eligible",
|
|
241
|
+
),
|
|
242
|
+
excluded=(), insert_rule="exempt", scopes=(),
|
|
243
|
+
),
|
|
244
|
+
# The Claude ownership predicates of `_CLAUDE_STATS_DIGEST_RELATIONS`
|
|
245
|
+
# (`bin/_lib_dashboard_sources.py`): Codex budget rows are not Claude
|
|
246
|
+
# content, and every project budget row is (that relation has no
|
|
247
|
+
# predicate, so neither does its row here).
|
|
248
|
+
"budget_milestones": TableClassification(
|
|
249
|
+
compared=(
|
|
250
|
+
"id", "vendor", "period_start_at", "period", "threshold",
|
|
251
|
+
"budget_usd", "spent_usd", "consumption_pct", "crossed_at_utc",
|
|
252
|
+
"alerted_at", "account_key", "journal_id",
|
|
253
|
+
),
|
|
254
|
+
excluded=(), insert_rule="untracked", scopes=(),
|
|
255
|
+
owner_predicate="{R}.vendor <> 'codex'",
|
|
256
|
+
),
|
|
257
|
+
"projected_milestones": TableClassification(
|
|
258
|
+
compared=(
|
|
259
|
+
"id", "week_start_at", "period", "metric", "threshold",
|
|
260
|
+
"projected_value", "denominator", "crossed_at_utc", "alerted_at",
|
|
261
|
+
"account_key", "journal_id",
|
|
262
|
+
),
|
|
263
|
+
excluded=(), insert_rule="untracked", scopes=(),
|
|
264
|
+
owner_predicate="{R}.metric <> 'codex_budget_usd'",
|
|
265
|
+
),
|
|
266
|
+
"project_budget_milestones": TableClassification(
|
|
267
|
+
compared=(
|
|
268
|
+
"id", "week_start_at", "project_key", "threshold", "budget_usd",
|
|
269
|
+
"spent_usd", "consumption_pct", "crossed_at_utc", "alerted_at",
|
|
270
|
+
"account_key", "journal_id",
|
|
271
|
+
),
|
|
272
|
+
excluded=(), insert_rule="untracked", scopes=(),
|
|
273
|
+
),
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
# ── trigger SQL ───────────────────────────────────────────────────────────
|
|
278
|
+
|
|
279
|
+
_SEQ_ADVANCE = (
|
|
280
|
+
"UPDATE stats_content_revision SET seq = seq + 1 WHERE singleton = 1;"
|
|
281
|
+
)
|
|
282
|
+
_SEQ_VALUE = "(SELECT seq FROM stats_content_revision WHERE singleton = 1)"
|
|
283
|
+
# `MIN(COALESCE(start_epoch, x), x)` widening, with the incoming side also
|
|
284
|
+
# coalesced so a NULL epoch (an unparseable timestamp, or a scope kind that
|
|
285
|
+
# has no range) never erases a recorded one: SQLite's multi-argument MIN/MAX
|
|
286
|
+
# return NULL when any argument is NULL.
|
|
287
|
+
_UPSERT_TAIL = (
|
|
288
|
+
" ON CONFLICT (kind, account_key, scope_key) DO UPDATE SET "
|
|
289
|
+
"start_epoch = MIN(COALESCE(start_epoch, excluded.start_epoch), "
|
|
290
|
+
"COALESCE(excluded.start_epoch, start_epoch)), "
|
|
291
|
+
"end_epoch = MAX(COALESCE(end_epoch, excluded.end_epoch), "
|
|
292
|
+
"COALESCE(excluded.end_epoch, end_epoch)), "
|
|
293
|
+
"revision = excluded.revision;"
|
|
294
|
+
)
|
|
295
|
+
_UPSERT_HEAD = (
|
|
296
|
+
"INSERT INTO stats_content_revision_scopes "
|
|
297
|
+
"(kind, account_key, scope_key, start_epoch, end_epoch, revision) "
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
class _ScopeSelect(NamedTuple):
|
|
302
|
+
"""One scope lookup over the row alias ``{R}``: the select list (kind,
|
|
303
|
+
account, key, start, end), an optional FROM and the WHERE. The WHERE is
|
|
304
|
+
never empty: a FROM-carrying SELECT followed by ``ON CONFLICT`` is the
|
|
305
|
+
documented UPSERT parsing ambiguity, so even a FROM-less one ends with
|
|
306
|
+
``WHERE true``."""
|
|
307
|
+
|
|
308
|
+
columns: str
|
|
309
|
+
source: str
|
|
310
|
+
where: str
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
_BLOCK_COLUMNS = (
|
|
314
|
+
"'block', b.account_key, CAST(b.five_hour_window_key AS TEXT), "
|
|
315
|
+
"unixepoch(b.block_start_at), unixepoch(b.five_hour_resets_at)"
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _scope_selects(table: str) -> "tuple[_ScopeSelect, ...]":
|
|
320
|
+
"""The scope lookups one row of ``table`` dirties, over alias ``{R}``."""
|
|
321
|
+
if table in ("weekly_usage_snapshots",):
|
|
322
|
+
return (
|
|
323
|
+
_ScopeSelect(
|
|
324
|
+
"'bucket', {R}.account_key, {R}.week_start_date, NULL, NULL",
|
|
325
|
+
"", "true"),
|
|
326
|
+
# Every block whose five-hour milestone references this snapshot
|
|
327
|
+
# (the milestone publishes the snapshot's weekly value).
|
|
328
|
+
_ScopeSelect(
|
|
329
|
+
_BLOCK_COLUMNS,
|
|
330
|
+
"FROM five_hour_milestones AS m, five_hour_blocks AS b",
|
|
331
|
+
"m.usage_snapshot_id = {R}.id AND b.id = m.block_id"),
|
|
332
|
+
)
|
|
333
|
+
if table == "weekly_cost_snapshots":
|
|
334
|
+
return (
|
|
335
|
+
_ScopeSelect(
|
|
336
|
+
"'bucket', {R}.account_key, {R}.week_start_date, NULL, NULL",
|
|
337
|
+
"", "true"),
|
|
338
|
+
)
|
|
339
|
+
if table == "percent_milestones":
|
|
340
|
+
return (
|
|
341
|
+
_ScopeSelect(
|
|
342
|
+
"'cohort', {R}.account_key, "
|
|
343
|
+
"{R}.week_start_date || '|' || {R}.reset_event_id, NULL, NULL",
|
|
344
|
+
"", "true"),
|
|
345
|
+
_ScopeSelect(
|
|
346
|
+
"'bucket', {R}.account_key, {R}.week_start_date, NULL, NULL",
|
|
347
|
+
"", "true"),
|
|
348
|
+
)
|
|
349
|
+
if table == "five_hour_milestones":
|
|
350
|
+
# The parent block by `block_id`, else by its natural key.
|
|
351
|
+
return (
|
|
352
|
+
_ScopeSelect(
|
|
353
|
+
_BLOCK_COLUMNS, "FROM five_hour_blocks AS b",
|
|
354
|
+
"b.id = {R}.block_id"),
|
|
355
|
+
_ScopeSelect(
|
|
356
|
+
_BLOCK_COLUMNS, "FROM five_hour_blocks AS b",
|
|
357
|
+
"b.account_key = {R}.account_key "
|
|
358
|
+
"AND b.five_hour_window_key = {R}.five_hour_window_key "
|
|
359
|
+
"AND NOT EXISTS (SELECT 1 FROM five_hour_blocks AS p "
|
|
360
|
+
"WHERE p.id = {R}.block_id)"),
|
|
361
|
+
)
|
|
362
|
+
if table == "five_hour_blocks":
|
|
363
|
+
return (
|
|
364
|
+
_ScopeSelect(
|
|
365
|
+
"'block', {R}.account_key, "
|
|
366
|
+
"CAST({R}.five_hour_window_key AS TEXT), "
|
|
367
|
+
"unixepoch({R}.block_start_at), "
|
|
368
|
+
"unixepoch({R}.five_hour_resets_at)",
|
|
369
|
+
"", "true"),
|
|
370
|
+
)
|
|
371
|
+
if table == "five_hour_reset_events":
|
|
372
|
+
return (
|
|
373
|
+
_ScopeSelect(
|
|
374
|
+
_BLOCK_COLUMNS, "FROM five_hour_blocks AS b",
|
|
375
|
+
"b.account_key = {R}.account_key "
|
|
376
|
+
"AND b.five_hour_window_key = {R}.five_hour_window_key"),
|
|
377
|
+
)
|
|
378
|
+
if table == "week_reset_events":
|
|
379
|
+
return tuple(
|
|
380
|
+
_ScopeSelect(
|
|
381
|
+
f"'reset_point', {{R}}.account_key, "
|
|
382
|
+
f"CAST(unixepoch({{R}}.{column}) AS TEXT), "
|
|
383
|
+
f"unixepoch({{R}}.{column}), unixepoch({{R}}.{column})",
|
|
384
|
+
"", f"unixepoch({{R}}.{column}) IS NOT NULL")
|
|
385
|
+
for column in (
|
|
386
|
+
"effective_reset_at_utc", "old_week_end_at",
|
|
387
|
+
"new_week_end_at")
|
|
388
|
+
)
|
|
389
|
+
return ()
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def _render(template: str, row: str) -> str:
|
|
393
|
+
return template.replace("{R}", row)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _upsert(select: _ScopeSelect, row: str) -> str:
|
|
397
|
+
source = f" {_render(select.source, row)}" if select.source else ""
|
|
398
|
+
return (
|
|
399
|
+
f"{_UPSERT_HEAD}SELECT {_render(select.columns, row)}, {_SEQ_VALUE}"
|
|
400
|
+
f"{source} WHERE {_render(select.where, row)}{_UPSERT_TAIL}"
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def _body(table: str, rows: "Sequence[str]") -> str:
|
|
405
|
+
statements = [_SEQ_ADVANCE]
|
|
406
|
+
for row in rows:
|
|
407
|
+
statements.extend(_upsert(s, row) for s in _scope_selects(table))
|
|
408
|
+
return " BEGIN " + " ".join(statements) + " END"
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def _owner(cls: TableClassification, row: str) -> "str | None":
|
|
412
|
+
if cls.owner_predicate is None:
|
|
413
|
+
return None
|
|
414
|
+
return f"({_render(cls.owner_predicate, row)})"
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def _trigger_sql() -> "dict[str, str]":
|
|
418
|
+
"""Every kernel trigger, name → its exact `sqlite_schema.sql`."""
|
|
419
|
+
out: "dict[str, str]" = {}
|
|
420
|
+
for table, cls in TRACKED_TABLES.items():
|
|
421
|
+
diff = " OR ".join(f"OLD.{c} IS NOT NEW.{c}" for c in cls.compared)
|
|
422
|
+
update_when = f"({diff})"
|
|
423
|
+
if cls.owner_predicate is not None:
|
|
424
|
+
update_when += f" AND ({_owner(cls, 'OLD')} OR {_owner(cls, 'NEW')})"
|
|
425
|
+
out[f"{TRIGGER_PREFIX}{table}_update"] = (
|
|
426
|
+
f"CREATE TRIGGER {TRIGGER_PREFIX}{table}_update "
|
|
427
|
+
f"AFTER UPDATE ON {table} WHEN {update_when}"
|
|
428
|
+
f"{_body(table, ('OLD', 'NEW'))}"
|
|
429
|
+
)
|
|
430
|
+
delete_when = (
|
|
431
|
+
f" WHEN {_owner(cls, 'OLD')}"
|
|
432
|
+
if cls.owner_predicate is not None else ""
|
|
433
|
+
)
|
|
434
|
+
out[f"{TRIGGER_PREFIX}{table}_delete"] = (
|
|
435
|
+
f"CREATE TRIGGER {TRIGGER_PREFIX}{table}_delete "
|
|
436
|
+
f"AFTER DELETE ON {table}{delete_when}{_body(table, ('OLD',))}"
|
|
437
|
+
)
|
|
438
|
+
if cls.insert_rule in ("tracked", "below_max"):
|
|
439
|
+
insert_when = (
|
|
440
|
+
f" WHEN EXISTS (SELECT 1 FROM {table} WHERE id > NEW.id)"
|
|
441
|
+
if cls.insert_rule == "below_max" else ""
|
|
442
|
+
)
|
|
443
|
+
out[f"{TRIGGER_PREFIX}{table}_insert"] = (
|
|
444
|
+
f"CREATE TRIGGER {TRIGGER_PREFIX}{table}_insert "
|
|
445
|
+
f"AFTER INSERT ON {table}{insert_when}"
|
|
446
|
+
f"{_body(table, ('NEW',))}"
|
|
447
|
+
)
|
|
448
|
+
return out
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
TRIGGER_SQL: "Mapping[str, str]" = types.MappingProxyType(_trigger_sql())
|
|
452
|
+
#: Every kernel-owned object, name → SQL: what the rebuild contract requires.
|
|
453
|
+
KERNEL_TABLES = (REVISION_TABLE, SCOPE_TABLE)
|
|
454
|
+
KERNEL_INDEXES = (USAGE_SNAPSHOT_INDEX,)
|
|
455
|
+
KERNEL_TRIGGERS = tuple(sorted(TRIGGER_SQL))
|
|
456
|
+
_TABLE_SQL = {REVISION_TABLE: REVISION_TABLE_SQL, SCOPE_TABLE: SCOPE_TABLE_SQL}
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def trigger_lookup_queries() -> "tuple[tuple[str, str], ...]":
|
|
460
|
+
"""Every trigger lookup as a standalone query over named parameters, for
|
|
461
|
+
`EXPLAIN QUERY PLAN` (§8.4): each scope SELECT with ``{R}.col`` bound as
|
|
462
|
+
``:col``, and the below-max INSERT predicate."""
|
|
463
|
+
out = []
|
|
464
|
+
for table, cls in TRACKED_TABLES.items():
|
|
465
|
+
for index, select in enumerate(_scope_selects(table)):
|
|
466
|
+
source = f" {select.source}" if select.source else ""
|
|
467
|
+
sql = re.sub(
|
|
468
|
+
r"\{R\}\.(\w+)", r":\1",
|
|
469
|
+
f"SELECT {select.columns}{source} WHERE {select.where}")
|
|
470
|
+
out.append((f"{table}#{index}", sql))
|
|
471
|
+
if cls.insert_rule == "below_max":
|
|
472
|
+
out.append((f"{table}#below_max",
|
|
473
|
+
f"SELECT EXISTS (SELECT 1 FROM {table} WHERE id > :id)"))
|
|
474
|
+
out.append(("seq#advance",
|
|
475
|
+
"UPDATE stats_content_revision SET seq = seq + 1 "
|
|
476
|
+
"WHERE singleton = 1"))
|
|
477
|
+
out.append(("scope#upsert",
|
|
478
|
+
"SELECT revision FROM stats_content_revision_scopes "
|
|
479
|
+
"WHERE kind = :kind AND account_key = :account_key "
|
|
480
|
+
"AND scope_key = :scope_key"))
|
|
481
|
+
return tuple(out)
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
# ── classification (reads only) ───────────────────────────────────────────
|
|
485
|
+
|
|
486
|
+
_SCHEMA_READ = (
|
|
487
|
+
"SELECT type, name, sql FROM sqlite_schema "
|
|
488
|
+
"WHERE name IN (?, ?, ?) "
|
|
489
|
+
"OR (type = 'trigger' AND substr(name, 1, ?) = ?)"
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def _inspect(conn) -> "tuple[str, tuple | None]":
|
|
494
|
+
"""The §4.3 state and, when the singleton is valid, its
|
|
495
|
+
``(format_version, domain_generation, seq)``."""
|
|
496
|
+
objects = conn.execute(
|
|
497
|
+
_SCHEMA_READ,
|
|
498
|
+
(REVISION_TABLE, SCOPE_TABLE, USAGE_SNAPSHOT_INDEX,
|
|
499
|
+
len(TRIGGER_PREFIX), TRIGGER_PREFIX),
|
|
500
|
+
).fetchall()
|
|
501
|
+
tables = {str(r[1]): r[2] for r in objects if r[0] == "table"}
|
|
502
|
+
if not tables:
|
|
503
|
+
return STATE_ABSENT, None
|
|
504
|
+
if len(tables) != 2 or any(
|
|
505
|
+
tables[name] != sql for name, sql in _TABLE_SQL.items()):
|
|
506
|
+
return STATE_MALFORMED, None
|
|
507
|
+
try:
|
|
508
|
+
rows = conn.execute(
|
|
509
|
+
"SELECT singleton, format_version, domain_generation, seq "
|
|
510
|
+
"FROM stats_content_revision LIMIT 2"
|
|
511
|
+
).fetchall()
|
|
512
|
+
except sqlite3.Error:
|
|
513
|
+
return STATE_MALFORMED, None
|
|
514
|
+
if len(rows) != 1:
|
|
515
|
+
return STATE_MALFORMED, None
|
|
516
|
+
values = tuple(rows[0])
|
|
517
|
+
if any(type(v) is not int for v in values):
|
|
518
|
+
return STATE_MALFORMED, None
|
|
519
|
+
singleton, format_version, generation, seq = values
|
|
520
|
+
if singleton != 1 or format_version < 1 or generation < 0 or seq < 0:
|
|
521
|
+
return STATE_MALFORMED, None
|
|
522
|
+
singleton_values = (format_version, generation, seq)
|
|
523
|
+
if format_version > KERNEL_FORMAT_VERSION:
|
|
524
|
+
return STATE_UNSUPPORTED, singleton_values
|
|
525
|
+
if format_version < KERNEL_FORMAT_VERSION:
|
|
526
|
+
return STATE_PARTIAL, singleton_values
|
|
527
|
+
present = {
|
|
528
|
+
str(r[1]): r[2] for r in objects if r[0] in ("trigger", "index")
|
|
529
|
+
}
|
|
530
|
+
expected = dict(TRIGGER_SQL)
|
|
531
|
+
expected[USAGE_SNAPSHOT_INDEX] = USAGE_SNAPSHOT_INDEX_SQL
|
|
532
|
+
if present != expected:
|
|
533
|
+
return STATE_PARTIAL, singleton_values
|
|
534
|
+
return STATE_CURRENT, singleton_values
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
def classify(conn) -> str:
|
|
538
|
+
"""The store's §4.3 installation state. Reads only."""
|
|
539
|
+
return _inspect(conn)[0]
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
# ── installation ──────────────────────────────────────────────────────────
|
|
543
|
+
|
|
544
|
+
def _default_guard(live: bool):
|
|
545
|
+
store = importlib.import_module("_cctally_store")
|
|
546
|
+
return store.stats_open_time_guard(live=live, wait_seconds=0.0)
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def _contention_errors() -> "tuple[type, ...]":
|
|
550
|
+
db = importlib.import_module("_cctally_db")
|
|
551
|
+
return (db.StatsDbMaintenanceError,)
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def _is_busy(exc: BaseException) -> bool:
|
|
555
|
+
return isinstance(exc, sqlite3.OperationalError) and (
|
|
556
|
+
"locked" in str(exc) or "busy" in str(exc))
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def _install(conn, state: str, singleton) -> None:
|
|
560
|
+
"""Create or repair every kernel object; the caller owns the transaction.
|
|
561
|
+
Triggers are created last, so seeding the singleton fires nothing."""
|
|
562
|
+
for row in conn.execute(
|
|
563
|
+
"SELECT name FROM sqlite_schema WHERE type = 'trigger' "
|
|
564
|
+
"AND substr(name, 1, ?) = ?",
|
|
565
|
+
(len(TRIGGER_PREFIX), TRIGGER_PREFIX)).fetchall():
|
|
566
|
+
conn.execute(f'DROP TRIGGER IF EXISTS "{row[0]}"')
|
|
567
|
+
conn.execute(f"DROP INDEX IF EXISTS {USAGE_SNAPSHOT_INDEX}")
|
|
568
|
+
if state == STATE_ABSENT:
|
|
569
|
+
conn.execute(REVISION_TABLE_SQL)
|
|
570
|
+
conn.execute(SCOPE_TABLE_SQL)
|
|
571
|
+
conn.execute(
|
|
572
|
+
"INSERT INTO stats_content_revision "
|
|
573
|
+
"(singleton, format_version, domain_generation, seq) "
|
|
574
|
+
"VALUES (1, ?, 0, 0)", (KERNEL_FORMAT_VERSION,))
|
|
575
|
+
else:
|
|
576
|
+
# A repair is a new tracking domain over existing content: rows may
|
|
577
|
+
# have moved while tracking was incomplete, so the identity moves
|
|
578
|
+
# once. `seq` only ever grows.
|
|
579
|
+
_format, generation, seq = singleton
|
|
580
|
+
conn.execute(
|
|
581
|
+
"UPDATE stats_content_revision SET format_version = ?, "
|
|
582
|
+
"domain_generation = ?, seq = ? WHERE singleton = 1",
|
|
583
|
+
(KERNEL_FORMAT_VERSION, generation + 1, seq + 1))
|
|
584
|
+
conn.execute(USAGE_SNAPSHOT_INDEX_SQL)
|
|
585
|
+
for name in KERNEL_TRIGGERS:
|
|
586
|
+
conn.execute(TRIGGER_SQL[name])
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def ensure_stats_content_revision(conn, *, live: bool, guard=None) -> str:
|
|
590
|
+
"""Install or repair the kernel's objects when needed; return the state
|
|
591
|
+
after the call (§4.3).
|
|
592
|
+
|
|
593
|
+
``current``, ``unsupported`` and ``malformed`` stores are returned as they
|
|
594
|
+
are, with nothing written and no lock taken. An ``absent`` or ``partial``
|
|
595
|
+
store is reclassified and acted on under the open-time maintenance guard
|
|
596
|
+
(zero wait) in one transaction. Contention — on the guard or on SQLite's
|
|
597
|
+
own write lock — leaves the store unchanged and returns its state; any
|
|
598
|
+
other failure propagates. ``guard`` overrides the default
|
|
599
|
+
``stats_open_time_guard(live=live, wait_seconds=0.0)`` (a test seam).
|
|
600
|
+
"""
|
|
601
|
+
state = classify(conn)
|
|
602
|
+
if state not in (STATE_ABSENT, STATE_PARTIAL):
|
|
603
|
+
return state
|
|
604
|
+
contention = _contention_errors()
|
|
605
|
+
previous_timeout = None
|
|
606
|
+
try:
|
|
607
|
+
with (guard(live) if guard is not None else _default_guard(live)):
|
|
608
|
+
state, singleton = _inspect(conn)
|
|
609
|
+
if state not in (STATE_ABSENT, STATE_PARTIAL):
|
|
610
|
+
return state
|
|
611
|
+
# SQLite's own write lock is contention too: an opener must not
|
|
612
|
+
# wait out another writer's transaction (busy_timeout 15 s) for
|
|
613
|
+
# optional work the next open retries.
|
|
614
|
+
previous_timeout = int(
|
|
615
|
+
conn.execute("PRAGMA busy_timeout").fetchone()[0])
|
|
616
|
+
conn.execute("PRAGMA busy_timeout = 0")
|
|
617
|
+
own = not conn.in_transaction
|
|
618
|
+
conn.execute("BEGIN IMMEDIATE" if own else "SAVEPOINT scr_install")
|
|
619
|
+
try:
|
|
620
|
+
state, singleton = _inspect(conn)
|
|
621
|
+
if state in (STATE_ABSENT, STATE_PARTIAL):
|
|
622
|
+
_install(conn, state, singleton)
|
|
623
|
+
if own:
|
|
624
|
+
conn.commit()
|
|
625
|
+
else:
|
|
626
|
+
conn.execute("RELEASE scr_install")
|
|
627
|
+
except BaseException:
|
|
628
|
+
if own:
|
|
629
|
+
conn.rollback()
|
|
630
|
+
else:
|
|
631
|
+
conn.execute("ROLLBACK TO scr_install")
|
|
632
|
+
conn.execute("RELEASE scr_install")
|
|
633
|
+
raise
|
|
634
|
+
except contention:
|
|
635
|
+
pass
|
|
636
|
+
except sqlite3.OperationalError as exc:
|
|
637
|
+
if not _is_busy(exc):
|
|
638
|
+
raise
|
|
639
|
+
finally:
|
|
640
|
+
if previous_timeout is not None:
|
|
641
|
+
conn.execute(f"PRAGMA busy_timeout = {previous_timeout:d}")
|
|
642
|
+
return classify(conn)
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
# ── the identity ──────────────────────────────────────────────────────────
|
|
646
|
+
|
|
647
|
+
def _canonical(value) -> str:
|
|
648
|
+
return json.dumps(value, sort_keys=True, separators=(",", ":"),
|
|
649
|
+
ensure_ascii=True)
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
def _stamp_rows(conn) -> list:
|
|
653
|
+
try:
|
|
654
|
+
rows = conn.execute(
|
|
655
|
+
"SELECT record_path, started_at_utc, stamped_at_utc "
|
|
656
|
+
"FROM stats_publication_stamp").fetchall()
|
|
657
|
+
except sqlite3.OperationalError as exc:
|
|
658
|
+
if "no such table" in str(exc):
|
|
659
|
+
return []
|
|
660
|
+
raise
|
|
661
|
+
# `record_path` names a private file; only its digest takes part.
|
|
662
|
+
return sorted(
|
|
663
|
+
[hashlib.sha256(str(r[0]).encode("utf-8")).hexdigest(),
|
|
664
|
+
None if r[1] is None else str(r[1]),
|
|
665
|
+
None if r[2] is None else str(r[2])]
|
|
666
|
+
for r in rows)
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
def read_claude_content_revision(stats_conn) -> ContentRevision:
|
|
670
|
+
"""Read the identity (§4.4). Joins an open transaction without ending it;
|
|
671
|
+
otherwise owns one short ``BEGIN`` … ``ROLLBACK``. Raises
|
|
672
|
+
`StatsContentRevisionUnreadable` on a ``malformed`` store or more than one
|
|
673
|
+
publication stamp row."""
|
|
674
|
+
own = not stats_conn.in_transaction
|
|
675
|
+
if own:
|
|
676
|
+
stats_conn.execute("BEGIN")
|
|
677
|
+
try:
|
|
678
|
+
state, singleton = _inspect(stats_conn)
|
|
679
|
+
if state == STATE_MALFORMED:
|
|
680
|
+
raise StatsContentRevisionUnreadable(
|
|
681
|
+
"stats content revision objects are malformed")
|
|
682
|
+
stamps = _stamp_rows(stats_conn)
|
|
683
|
+
if len(stamps) > 1:
|
|
684
|
+
raise StatsContentRevisionUnreadable(
|
|
685
|
+
f"stats_publication_stamp holds {len(stamps)} rows")
|
|
686
|
+
if state != STATE_CURRENT:
|
|
687
|
+
return ContentRevision(state, None, None)
|
|
688
|
+
format_version, generation, seq = singleton
|
|
689
|
+
stamp_value = stamps or None
|
|
690
|
+
if seq == 0 and generation == 0 and stamp_value is None:
|
|
691
|
+
token = ""
|
|
692
|
+
else:
|
|
693
|
+
user_version = int(
|
|
694
|
+
stats_conn.execute("PRAGMA user_version").fetchone()[0])
|
|
695
|
+
token = hashlib.sha256(_canonical(
|
|
696
|
+
[format_version, generation, user_version, stamp_value, seq]
|
|
697
|
+
).encode("utf-8")).hexdigest()[:16]
|
|
698
|
+
cycle_basis = (
|
|
699
|
+
None if generation == 0 and stamp_value is None
|
|
700
|
+
else _canonical([format_version, generation, stamp_value])
|
|
701
|
+
)
|
|
702
|
+
return ContentRevision(state, token, cycle_basis)
|
|
703
|
+
finally:
|
|
704
|
+
if own:
|
|
705
|
+
stats_conn.rollback()
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
def revision_leg(rev: ContentRevision) -> str:
|
|
709
|
+
"""The published leg: the token (``""`` included), else a fresh nonce,
|
|
710
|
+
so an untrusted store never compares equal across reads."""
|
|
711
|
+
if isinstance(rev.token, str):
|
|
712
|
+
return rev.token
|
|
713
|
+
return "?" + secrets.token_hex(8)
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
def comparable_identity(rev: ContentRevision) -> tuple:
|
|
717
|
+
"""What a build bracket compares: stable across reads of an unchanged
|
|
718
|
+
store in every state, different on any state change."""
|
|
719
|
+
if rev.state == STATE_CURRENT:
|
|
720
|
+
return (rev.state, rev.token)
|
|
721
|
+
return (rev.state,)
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
def read_scope_rows(stats_conn, *, account_key: "str | None") -> "list[ScopeRow]":
|
|
725
|
+
"""Every scope row, in primary-key order. Strict to ``account_key`` when
|
|
726
|
+
it is given; merged across every account (``unattributed`` included) when
|
|
727
|
+
it is ``None``. An absent store has none."""
|
|
728
|
+
sql = (
|
|
729
|
+
"SELECT kind, account_key, scope_key, start_epoch, end_epoch, revision "
|
|
730
|
+
"FROM stats_content_revision_scopes"
|
|
731
|
+
)
|
|
732
|
+
params: tuple = ()
|
|
733
|
+
if account_key is not None:
|
|
734
|
+
sql += " WHERE account_key = ?"
|
|
735
|
+
params = (account_key,)
|
|
736
|
+
sql += " ORDER BY kind, account_key, scope_key"
|
|
737
|
+
try:
|
|
738
|
+
rows = stats_conn.execute(sql, params).fetchall()
|
|
739
|
+
except sqlite3.OperationalError as exc:
|
|
740
|
+
if "no such table" in str(exc):
|
|
741
|
+
return []
|
|
742
|
+
raise
|
|
743
|
+
return [ScopeRow(*tuple(r)) for r in rows]
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
# ── identical child replacement (§4.2) ────────────────────────────────────
|
|
747
|
+
#
|
|
748
|
+
# The open-block projection and a block-close fold or convergence replace a
|
|
749
|
+
# block's model and project rows wholesale. Deleting and re-inserting an
|
|
750
|
+
# identical set would fire the tracked DELETE and INSERT triggers and move
|
|
751
|
+
# `seq` with nothing changed, so the writers first ask whether the stored set
|
|
752
|
+
# already equals the replacement, compared as multisets over every persisted
|
|
753
|
+
# column except `id`. A set that is missing, or differs anywhere, is replaced
|
|
754
|
+
# as before (and so still advances `seq`); inequality is the safe answer to
|
|
755
|
+
# anything this cannot decide.
|
|
756
|
+
|
|
757
|
+
CHILD_TABLES = ("five_hour_block_models", "five_hour_block_projects")
|
|
758
|
+
_CHILD_SELECTS = types.MappingProxyType({
|
|
759
|
+
(table, scoped): (
|
|
760
|
+
f"SELECT * FROM {table} WHERE "
|
|
761
|
+
+ ("account_key = ? AND " if scoped else "")
|
|
762
|
+
+ "five_hour_window_key = ?"
|
|
763
|
+
)
|
|
764
|
+
for table in CHILD_TABLES
|
|
765
|
+
for scoped in (True, False)
|
|
766
|
+
})
|
|
767
|
+
#: No ORDER BY: the column order only has to agree between the stored and the
|
|
768
|
+
#: wanted tuples, which both take it from this one read, and sorting the
|
|
769
|
+
#: pragma's rows costs a temp B-tree on the live record path.
|
|
770
|
+
_CHILD_COLUMNS = "SELECT name, dflt_value FROM pragma_table_info(?)"
|
|
771
|
+
_UNDECIDABLE = object()
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
def _default_value(text):
|
|
775
|
+
"""The value a plain-literal column DEFAULT stores, ``None`` for no
|
|
776
|
+
default or ``NULL``, and ``_UNDECIDABLE`` for any expression."""
|
|
777
|
+
if text is None:
|
|
778
|
+
return None
|
|
779
|
+
value = text.strip()
|
|
780
|
+
while value.startswith("(") and value.endswith(")"):
|
|
781
|
+
value = value[1:-1].strip()
|
|
782
|
+
if value.upper() == "NULL":
|
|
783
|
+
return None
|
|
784
|
+
if len(value) >= 2 and value[0] == value[-1] == "'":
|
|
785
|
+
return value[1:-1].replace("''", "'")
|
|
786
|
+
for parse in (int, float):
|
|
787
|
+
try:
|
|
788
|
+
return parse(value)
|
|
789
|
+
except ValueError:
|
|
790
|
+
pass
|
|
791
|
+
return _UNDECIDABLE
|
|
792
|
+
|
|
793
|
+
|
|
794
|
+
def child_set_unchanged(conn, table: str, *, five_hour_window_key: int,
|
|
795
|
+
account_key: "str | None",
|
|
796
|
+
replacement: "Sequence[Mapping]") -> bool:
|
|
797
|
+
"""True iff ``table``'s rows for the window (and ``account_key`` when it
|
|
798
|
+
is not ``None``, the writer's own DELETE predicate) equal ``replacement``
|
|
799
|
+
as a multiset over every column except ``id``.
|
|
800
|
+
|
|
801
|
+
``replacement`` holds the column → value mappings the writer would
|
|
802
|
+
INSERT; a column a mapping omits takes the table's literal DEFAULT. A
|
|
803
|
+
mapping naming ``id`` or an unknown column, or an omitted column whose
|
|
804
|
+
default is an expression, is never equal. Values compare as Python does,
|
|
805
|
+
so an INSERT that SQLite's affinity would coerce (``'3'`` into an
|
|
806
|
+
INTEGER column) compares unequal and the set is replaced."""
|
|
807
|
+
if table not in CHILD_TABLES:
|
|
808
|
+
raise ValueError(f"not a five-hour child table: {table}")
|
|
809
|
+
from collections import Counter
|
|
810
|
+
declared = conn.execute(_CHILD_COLUMNS, (table,)).fetchall()
|
|
811
|
+
defaults = {str(name): _default_value(dflt) for name, dflt in declared}
|
|
812
|
+
columns = [name for name in defaults if name != "id"]
|
|
813
|
+
wanted: Counter = Counter()
|
|
814
|
+
for row in replacement:
|
|
815
|
+
if "id" in row or any(name not in defaults for name in row):
|
|
816
|
+
return False
|
|
817
|
+
values = tuple(
|
|
818
|
+
row[name] if name in row else defaults[name] for name in columns)
|
|
819
|
+
if any(value is _UNDECIDABLE for value in values):
|
|
820
|
+
return False
|
|
821
|
+
wanted[values] += 1
|
|
822
|
+
scoped = account_key is not None
|
|
823
|
+
params = ((account_key, int(five_hour_window_key)) if scoped
|
|
824
|
+
else (int(five_hour_window_key),))
|
|
825
|
+
cursor = conn.execute(_CHILD_SELECTS[(table, scoped)], params)
|
|
826
|
+
names = [d[0] for d in cursor.description]
|
|
827
|
+
stored: Counter = Counter(
|
|
828
|
+
tuple(dict(zip(names, tuple(r)))[name] for name in columns)
|
|
829
|
+
for r in cursor.fetchall()
|
|
830
|
+
)
|
|
831
|
+
return stored == wanted
|