fix(server): plan 4 — trek_photo_cache_meta.cache_key NOT NULL

The column was a bare TEXT PRIMARY KEY, which SQLite does not implicitly
make NOT NULL (unlike an INTEGER PRIMARY KEY rowid alias). Every writer
(TrekPhotoCacheMetaRepository.upsertMeta, the table's only INSERT path)
has always bound a real string — 3e's R5 confirmed this. Appends a new
migration rebuilding the table with cache_key NOT NULL (SQLite has no
ALTER COLUMN, so this follows the same rebuild recipe
Migration20200101020000 uses in the opposite direction) and updates the
entity/generated metadata to match. Schema tightening, not a behaviour
change.
This commit is contained in:
jubnl
2026-09-24 06:07:31 +02:00
parent e62f463296
commit 5e4c5640a1
3 changed files with 48 additions and 5 deletions
@@ -4,7 +4,7 @@ import { TrekPhotoCacheMetaRepository } from '../repositories/TrekPhotoCacheMeta
export class TrekPhotoCacheMeta {
[EntityRepositoryType]?: TrekPhotoCacheMetaRepository;
[PrimaryKeyProp]?: 'cache_key';
cache_key?: string | null;
cache_key!: string;
content_type: string & Opt = 'image/jpeg';
fetched_at!: number;
}
@@ -13,7 +13,7 @@ export const TrekPhotoCacheMetaSchema = defineEntity({
class: TrekPhotoCacheMeta,
repository: () => TrekPhotoCacheMetaRepository,
properties: {
cache_key: p.text().primary().nullable(),
cache_key: p.text().primary(),
content_type: p.text().default('image/jpeg'),
fetched_at: p.integer().index('idx_trek_photo_cache_meta_fetched_at'),
},
@@ -0,0 +1,41 @@
import { Migration } from '@mikro-orm/migrations';
/**
* Plan 4 Task 8a program carry — `trek_photo_cache_meta.cache_key` was
* declared a bare `TEXT PRIMARY KEY` (Migration20200101014900), which SQLite
* does NOT implicitly make `NOT NULL` the way it does for an `INTEGER
* PRIMARY KEY` rowid alias — a genuine, if surprising, SQLite quirk (3e's R5
* Task 0 finding). Every statement that writes this column
* (`TrekPhotoCacheMetaRepository.upsertMeta`'s `INSERT OR REPLACE`, its only
* writer) has always bound a real `string`, never `null` — this migration
* tightens the schema to match the code's actual invariant, not a behaviour
* change. SQLite has no `ALTER COLUMN ... SET NOT NULL`, so the column is
* added via the standard rebuild-table recipe (the same shape
* Migration20200101020000 uses in the opposite direction).
*/
export class Migration20200101040200_trek_photo_cache_meta_cache_key_not_null extends Migration {
override name = 'Migration20200101040200_trek_photo_cache_meta_cache_key_not_null';
override up(): void {
this.addSql(`
CREATE TABLE trek_photo_cache_meta_new (
cache_key TEXT PRIMARY KEY NOT NULL,
content_type TEXT NOT NULL DEFAULT 'image/jpeg',
fetched_at INTEGER NOT NULL
)
`);
this.addSql(`
INSERT INTO trek_photo_cache_meta_new
SELECT cache_key, content_type, fetched_at
FROM trek_photo_cache_meta
WHERE cache_key IS NOT NULL
`);
this.addSql(`DROP TABLE trek_photo_cache_meta`);
this.addSql(`ALTER TABLE trek_photo_cache_meta_new RENAME TO trek_photo_cache_meta`);
this.addSql(`CREATE INDEX IF NOT EXISTS idx_trek_photo_cache_meta_fetched_at ON trek_photo_cache_meta (fetched_at)`);
}
}
@@ -26,9 +26,11 @@ export class TrekPhotoCacheMetaRepository extends TrekRepository<TrekPhotoCacheM
/**
* TC4 (`put`) — `INSERT OR REPLACE INTO trek_photo_cache_meta (cache_key,
* content_type, fetched_at) VALUES (?, ?, ?)`. `cache_key` is the table's
* only unique/primary column (a genuine nullable TEXT PK per R5's Task 0
* finding — not a bug, and not touched here), so it is the sole conflict
* target, same upsert shape as `AppSettingsRepository.setValue`.
* only unique/primary column (`NOT NULL` since Plan 4 Task 8a's
* cache_key-tightening migration — R5's Task 0 finding was that every
* writer already bound a non-null value, so this was a schema tightening,
* not a behaviour change), so it is the sole conflict target, same upsert
* shape as `AppSettingsRepository.setValue`.
*/
async upsertMeta(cache_key: string, content_type: string, fetched_at: number): Promise<void> {
await this.upsert({ cache_key, content_type, fetched_at }, { onConflictFields: ['cache_key'], onConflictAction: 'merge' });