From 26d04d5eb8eb9e2619212b8009ebe75032017442 Mon Sep 17 00:00:00 2001 From: anti Date: Thu, 23 Apr 2026 22:06:38 -0400 Subject: [PATCH] fix(db): SessionProfile.kd_digraph_simhash must be BINARY(8), not BLOB MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MySQL can't index a BLOB/TEXT column without a prefix length, so create_all() on a fresh MySQL schema blew up with "BLOB/TEXT column 'kd_digraph_simhash' used in key specification without a key length". SimHashes are a fixed 8 bytes — the variable-length type was a SQLAlchemy-side auto-mapping from 'Optional[bytes]', not an actual schema requirement. Switch to BINARY(8), which is portable: MySQL gets a fixed-width indexable BINARY, SQLite treats it as BLOB and doesn't care about key length. --- decnet/web/db/models/attackers.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/decnet/web/db/models/attackers.py b/decnet/web/db/models/attackers.py index 9f2a4274..9c20c19d 100644 --- a/decnet/web/db/models/attackers.py +++ b/decnet/web/db/models/attackers.py @@ -3,7 +3,7 @@ from datetime import datetime, timezone from typing import Any, List, Optional from pydantic import BaseModel -from sqlalchemy import Column, Text, UniqueConstraint +from sqlalchemy import BINARY, Column, Text, UniqueConstraint from sqlmodel import Field, SQLModel from ._base import _BIG_TEXT @@ -138,7 +138,13 @@ class SessionProfile(SQLModel, table=True): kd_arrow_rate: Optional[float] = None kd_tab_rate: Optional[float] = None # 8-byte SimHash over keystroke digraphs — Hamming-comparable across sessions. - kd_digraph_simhash: Optional[bytes] = Field(default=None, index=True) + # Fixed-width BINARY(8) rather than BLOB: MySQL can't index BLOB/TEXT + # columns without a prefix length, and SimHashes are always exactly 8 + # bytes so a variable-length type gains nothing here. + kd_digraph_simhash: Optional[bytes] = Field( + default=None, + sa_column=Column("kd_digraph_simhash", BINARY(8), nullable=True, index=True), + ) # Derived totals. total_keystrokes: Optional[int] = None session_duration_s: Optional[float] = None