0077_add_committed_at_raw.py
python
sha256:06cdc3de1e7f8091954906cf91f22c941373474c3322392a79300adc19b33197
fix(#195): narrow fix — committed_at_raw preserves commit i…
Sonnet 5
minor
⚠ breaking
2 days ago
| 1 | """Add committed_at_raw to musehub_commits |
| 2 | |
| 3 | musehub#195 (narrow fix): committed_at is identity-bearing -- a commit's |
| 4 | content-addressed id is a hash whose preimage includes the exact ISO-8601 |
| 5 | committed_at string the client used. musehub_commits.timestamp is a |
| 6 | Postgres timestamptz, which normalizes any non-UTC offset to UTC on |
| 7 | write. When the wire-serve path (_to_wire_commit) reconstructs a commit's |
| 8 | bytes from `timestamp.isoformat()` to send it back to a client, a commit |
| 9 | originally authored with a non-UTC offset gets served with a DIFFERENT |
| 10 | committed_at string than the one baked into its id -- the client correctly |
| 11 | rejects the download as an integrity failure. |
| 12 | |
| 13 | This is an instance of the broader problem tracked in musehub#63 (commits |
| 14 | are DB-only, reconstructed from DB fields rather than served as stored |
| 15 | canonical bytes). This migration is the deliberately narrow, additive fix: |
| 16 | capture the exact original string alongside the normalized timestamptz, so |
| 17 | the serve path can use it verbatim instead of re-deriving a lossy one. |
| 18 | Still correct after #63 eventually lands (canonical-bytes storage would |
| 19 | make this column redundant, not wrong). |
| 20 | |
| 21 | Additive and nullable -- existing rows get NULL. The serve path |
| 22 | (musehub/services/musehub_wire_shared.py::_to_wire_commit) falls back to |
| 23 | `timestamp.isoformat()` for NULL rows and verifies the reconstructed bytes |
| 24 | actually reproduce the stored commit_id, refusing to serve (fail closed) |
| 25 | rather than silently serving a mismatched reconstruction if they don't. |
| 26 | |
| 27 | Revision ID: 0077 |
| 28 | Revises: 0076 |
| 29 | """ |
| 30 | from __future__ import annotations |
| 31 | |
| 32 | from typing import Sequence, Union |
| 33 | |
| 34 | import sqlalchemy as sa |
| 35 | from alembic import op |
| 36 | |
| 37 | |
| 38 | # revision identifiers, used by Alembic. |
| 39 | revision: str = '0077' |
| 40 | down_revision: Union[str, None] = '0076' |
| 41 | branch_labels: Union[str, Sequence[str], None] = None |
| 42 | depends_on: Union[str, Sequence[str], None] = None |
| 43 | |
| 44 | |
| 45 | def upgrade() -> None: |
| 46 | op.add_column( |
| 47 | 'musehub_commits', |
| 48 | sa.Column('committed_at_raw', sa.Text(), nullable=True), |
| 49 | ) |
| 50 | |
| 51 | |
| 52 | def downgrade() -> None: |
| 53 | op.drop_column('musehub_commits', 'committed_at_raw') |
File History
1 commit
sha256:06cdc3de1e7f8091954906cf91f22c941373474c3322392a79300adc19b33197
fix(#195): narrow fix — committed_at_raw preserves commit i…
Sonnet 5
minor
⚠
2 days ago