From da3c35c6a4fe8fb4624ac4e6f71232a027ff7d83 Mon Sep 17 00:00:00 2001 From: anti Date: Mon, 27 Apr 2026 17:55:35 -0400 Subject: [PATCH] fix(realism): synthetic_files path fits MySQL utf8mb4 index cap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The (decky_uuid VARCHAR(64), path VARCHAR(1024)) UNIQUE constraint generated a 4352-byte composite key under utf8mb4 (4 bytes/char), busting MySQL's 3072-byte cap and crashing decnet api on init with: Specified key was too long; max key length is 3072 bytes Tighten path to VARCHAR(512) — (64+512)*4 = 2304 bytes, well under the cap. Real realism + canary placement paths are short (/home//Documents/, ~70 chars); 512 keeps headroom without the index hassle. Pre-v1, no migration helper. Adds a regression test pinning the (decky_uuid + path) byte budget so a future widening fails loudly in CI rather than at MySQL deploy time. --- decnet/web/db/models/realism.py | 6 +++++- tests/realism/test_synthetic_files_repo.py | 15 +++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/decnet/web/db/models/realism.py b/decnet/web/db/models/realism.py index 0eaea29b..668e10e7 100644 --- a/decnet/web/db/models/realism.py +++ b/decnet/web/db/models/realism.py @@ -58,7 +58,11 @@ class SyntheticFile(SQLModel, table=True): ) uuid: str = Field(default_factory=lambda: str(uuid4()), primary_key=True) decky_uuid: str = Field(index=True, max_length=64) - path: str = Field(max_length=1024) + # Capped at 512 so the (decky_uuid, path) unique index fits MySQL's + # 3072-byte utf8mb4 limit: (64+512)*4 = 2304 bytes. Real realism + + # canary paths are well under (longest is + # ``/home//Documents/Q3-Operations-Review.docx``, ~70 chars). + path: str = Field(max_length=512) persona: str = Field(max_length=128) # EmailPersona.name content_class: str = Field(max_length=32, index=True) # ContentClass enum value created_at: datetime = Field( diff --git a/tests/realism/test_synthetic_files_repo.py b/tests/realism/test_synthetic_files_repo.py index 8a5f5a24..55fa6a7c 100644 --- a/tests/realism/test_synthetic_files_repo.py +++ b/tests/realism/test_synthetic_files_repo.py @@ -150,3 +150,18 @@ async def test_get_synthetic_file_returns_row(repo): @pytest.mark.asyncio async def test_get_synthetic_file_returns_none_when_missing(repo): assert await repo.get_synthetic_file("does-not-exist") is None + + +def test_path_max_length_fits_mysql_utf8mb4_index_limit(): + """The unique (decky_uuid, path) index has to fit MySQL's 3072-byte + utf8mb4 cap: (decky_uuid_len + path_len) * 4 <= 3072. A regression + that widens path past this triggers + ``Specified key was too long`` on MySQL DB init.""" + from decnet.web.db.models.realism import SyntheticFile + fields = SyntheticFile.model_fields + decky_len = fields["decky_uuid"].metadata[0].max_length # type: ignore[attr-defined] + path_len = fields["path"].metadata[0].max_length # type: ignore[attr-defined] + assert (decky_len + path_len) * 4 <= 3072, ( + f"(decky_uuid={decky_len} + path={path_len}) * 4 = " + f"{(decky_len + path_len) * 4} exceeds MySQL utf8mb4 index cap" + )