Problem
feature_tables has a global UNIQUE(hook_name) constraint — one physical features.<hook> table exists per hook name, node-wide. When a convention registers a hook whose name already has a table, CreateFeatureTables catches the ConflictError and logs a warning (domain/feature/handler/create_feature_tables.py), silently adopting the existing table.
That's fine when the two hooks genuinely share a shape, but nothing checks this. If a second convention registers a hook with the same name and a different declared column schema, its writes target a table whose columns don't match its declaration:
- inserts with extra columns fail at the DB layer;
- inserts missing columns silently produce rows that don't conform to the hook's declared
FeatureSchema;
- the
/data/{schema}/{feature} manifest reports the catalog's stored feature_schema, which only reflects whichever hook registered first.
The read side was hardened in #139 (feature streams and counts are now scoped to the requested schema via a records join), but the write-side registration hazard remains.
Options
- Reject loudly on shape mismatch: on conflict, compare the incoming
FeatureSchema against the catalog row; identical → reuse (current behavior, now safe), different → fail convention registration with a clear error. Minimal change, keeps shared tables for genuinely shared hooks.
- Schema-scope physical tables (e.g. key
feature_tables on (schema_id, hook_name)): removes cross-schema sharing entirely, at the cost of a migration and changes to the feature write path and the /data/ read joins.
Option 1 is the cheap, safe fix; option 2 is the cleaner model if shared hook tables turn out to have no intentional use case. Needs a design call on whether cross-schema hook sharing is a feature or an accident.
Context
Surfaced while reviewing PR #139 — the read-side scoping bug fixed there (9c95694) had this registration behavior as its root cause.
Problem
feature_tableshas a globalUNIQUE(hook_name)constraint — one physicalfeatures.<hook>table exists per hook name, node-wide. When a convention registers a hook whose name already has a table,CreateFeatureTablescatches theConflictErrorand logs a warning (domain/feature/handler/create_feature_tables.py), silently adopting the existing table.That's fine when the two hooks genuinely share a shape, but nothing checks this. If a second convention registers a hook with the same name and a different declared column schema, its writes target a table whose columns don't match its declaration:
FeatureSchema;/data/{schema}/{feature}manifest reports the catalog's storedfeature_schema, which only reflects whichever hook registered first.The read side was hardened in #139 (feature streams and counts are now scoped to the requested schema via a
recordsjoin), but the write-side registration hazard remains.Options
FeatureSchemaagainst the catalog row; identical → reuse (current behavior, now safe), different → fail convention registration with a clear error. Minimal change, keeps shared tables for genuinely shared hooks.feature_tableson(schema_id, hook_name)): removes cross-schema sharing entirely, at the cost of a migration and changes to the feature write path and the/data/read joins.Option 1 is the cheap, safe fix; option 2 is the cleaner model if shared hook tables turn out to have no intentional use case. Needs a design call on whether cross-schema hook sharing is a feature or an accident.
Context
Surfaced while reviewing PR #139 — the read-side scoping bug fixed there (
9c95694) had this registration behavior as its root cause.