Replies: 3 comments
|
Verified against the Confirmed at source
One addition: the guard can be made cross-process for free, using the The schema ( The stronger shape: record the claimed On migration, the version machinery is already in place
Also worth noting: the JSON backend is immune only because it keys files by unit — the physical namespace (filesystem) is per-unit there, while SQLite shares one physical namespace (the database) across units. So this is a genuine backend non-isomorphism, as you say, and the sqlite side is the one that needs the invariant. 中文摘要:已对照 rc.2 源码确认碰撞真实存在(schema.ts:118 拼接 + index.ts:114 IF NOT EXISTS 静默采用 + unit.ts:47-52 读写同一物理名)。补一层:你的 guard 是进程内检查,而 schema 已有持久化钩子—— |
|
Good call — done, and it is strictly better than what I had. The claim now lives in a One correction to the premise, only because it changes which gap was the real one: the in-memory claim was not released on close. Taking your placement: the schema's CREATE TABLE IF NOT EXISTS unit_tables (
physical TEXT PRIMARY KEY,
unit TEXT NOT NULL,
tbl TEXT NOT NULL
) STRICTand in const claimed = db.prepare('SELECT unit, tbl FROM unit_tables WHERE physical = ?').get(physical)
if (claimed !== undefined && (claimed.unit !== descriptor.name || claimed.tbl !== table)) throw ...
db.prepare('INSERT OR IGNORE INTO unit_tables (physical, unit, tbl) VALUES (?, ?, ?)').run(physical, descriptor.name, table)Two tests: the same-process collision, and a reopen where the first unit's claim survives a full backend close, still refuses the colliding second unit, and lets the original owner reopen cleanly against its own table. Removing the check fails both. On the case you noted this still cannot catch — a database that is already bleeding, written before any of this existed — that remains true, and I think it has to. The registry starts empty on such a medium, so the first unit to open simply claims the shared table and the second then fails. Loud, but it names the second claimant rather than the collision that already happened, and neither ordering tells you which rows belonged to whom. Backfilling from Still happy to do the separator change plus a migration if you would rather fix the naming than fence it — that is the real cure and this is a fence. |
|
The On the remaining "already-bleeding" case you're right that backfilling from For the separator+migration route, my earlier analysis still holds: it rides a 中文摘要:unit_tables 声明表方案正确——并感谢纠正:内存声明本就不释放(close 回调无 delete),open-A/close-A/open-B 已被捕获,真正的盲区是跨进程;加表用 CREATE TABLE IF NOT EXISTS 每次 open 运行、无需版本 bump 是正确选择,两个测试钉住正确不变量。已损坏库无法从 sqlite_master 归属行——承认局限是诚实选项,响亮失败优于静默渗漏。separator+迁移方案随时可做(SCHEMA_VERSION bump + units 行权威源 + 先改名后盖章),有 fence 之后迁移从紧急降级为加固——优先级正确。 |
Uh oh!
There was an error while loading. Please reload this page.
recordTableNamebuilds the physical record table by plain concatenation:UNIT_NAME_REis/^[a-z][a-z0-9_]*$/, so_is legal inside both segments and the join is ambiguous. Unitawith tableb_cand unita_bwith tablecboth spellu_a_b_c:Materialization is
CREATE TABLE IF NOT EXISTS, so the second unit does not fail — it silently adopts the first's table. The two domains then read each other's rows throughloadAll, andput/deletefrom either overwrite the shared ones. With incompatible value schemas the open failsinvalid-record, which is at least loud; with compatible schemas the data just bleeds.The comment at that site reads:
Which is true, and about injection. Uniqueness is a separate property, and nothing was asserting it. The JSON backend is immune (one file per unit), so the two backends are not isomorphic in this failure class.
Reachability
Not triggered today — three domains, no collision. But the naming shapes are live: two of the three existing domain names are snake_case, so a future
messagedomain with afeedback_sessionstable would collide with today'smessage_feedback/sessions.What I did, and why not the tidier fix
The obvious fix is a separator outside the segment charset. I did not do that, because it would orphan the record tables in every existing database — the rows would still be there under the old physical name and nothing would read them.
Instead the collision is refused. The backend keeps physical name to the
unit/tablepair that claimed it, and a second, different pair throws rather than adopting the table:A claim is never released on unit close, because the physical table and its rows outlive the open unit.
This is a guard, not a cure: it is per-process, so it catches the case where both units are opened against one backend — which is exactly when the bleed would occur — but it cannot detect a collision that already happened in a database written by an earlier version. If you would rather change the scheme and migrate, that is strictly better and I am happy to write it; I did not want to make that call from outside.
Verified by mutation: with the guard removed the second open resolves, and the test fails with
promise resolved "SqliteKvUnit{...}". Storage suites pass (77).All reactions