mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-07-29 07:21:33 -07:00
77b7ed1fa1
The launcher sync fixture duplicates the production immersion schema and a guard test asserts the two stay identical. Adding seen_ms to the word and kanji occurrence tables left the fixture behind, so every sync merge test failed on the missing column.
326 lines
12 KiB
TypeScript
326 lines
12 KiB
TypeScript
// Schema-version-18 shape of the tables the sync merge touches (plus the
|
|
// app's indexes), mirroring ensureSchema / ensureLifetimeSummaryTables /
|
|
// ensureStatsExcludedWordsTable in src/core/services/immersion-tracker/storage.ts.
|
|
export const IMMERSION_DB_FIXTURE_DDL = `
|
|
CREATE TABLE imm_schema_version (
|
|
schema_version INTEGER PRIMARY KEY,
|
|
applied_at_ms TEXT NOT NULL
|
|
);
|
|
CREATE TABLE imm_rollup_state(
|
|
state_key TEXT PRIMARY KEY,
|
|
state_value TEXT NOT NULL
|
|
);
|
|
CREATE TABLE imm_anime(
|
|
anime_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
normalized_title_key TEXT NOT NULL UNIQUE,
|
|
canonical_title TEXT NOT NULL,
|
|
anilist_id INTEGER UNIQUE,
|
|
title_romaji TEXT,
|
|
title_english TEXT,
|
|
title_native TEXT,
|
|
episodes_total INTEGER,
|
|
description TEXT,
|
|
metadata_json TEXT,
|
|
CREATED_DATE TEXT,
|
|
LAST_UPDATE_DATE TEXT
|
|
);
|
|
CREATE TABLE imm_videos(
|
|
video_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
video_key TEXT NOT NULL UNIQUE,
|
|
anime_id INTEGER,
|
|
canonical_title TEXT NOT NULL,
|
|
source_type INTEGER NOT NULL,
|
|
source_path TEXT,
|
|
source_url TEXT,
|
|
parsed_basename TEXT,
|
|
parsed_title TEXT,
|
|
parsed_season INTEGER,
|
|
parsed_episode INTEGER,
|
|
parser_source TEXT,
|
|
parser_confidence REAL,
|
|
parse_metadata_json TEXT,
|
|
watched INTEGER NOT NULL DEFAULT 0,
|
|
duration_ms INTEGER NOT NULL CHECK(duration_ms>=0),
|
|
file_size_bytes INTEGER CHECK(file_size_bytes>=0),
|
|
codec_id INTEGER, container_id INTEGER,
|
|
width_px INTEGER, height_px INTEGER, fps_x100 INTEGER,
|
|
bitrate_kbps INTEGER, audio_codec_id INTEGER,
|
|
hash_sha256 TEXT, screenshot_path TEXT,
|
|
metadata_json TEXT,
|
|
CREATED_DATE TEXT,
|
|
LAST_UPDATE_DATE TEXT,
|
|
FOREIGN KEY(anime_id) REFERENCES imm_anime(anime_id) ON DELETE SET NULL
|
|
);
|
|
CREATE TABLE imm_sessions(
|
|
session_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
session_uuid TEXT NOT NULL UNIQUE,
|
|
video_id INTEGER NOT NULL,
|
|
started_at_ms TEXT NOT NULL, ended_at_ms TEXT,
|
|
status INTEGER NOT NULL,
|
|
locale_id INTEGER, target_lang_id INTEGER,
|
|
difficulty_tier INTEGER, subtitle_mode INTEGER,
|
|
ended_media_ms INTEGER,
|
|
total_watched_ms INTEGER NOT NULL DEFAULT 0,
|
|
active_watched_ms INTEGER NOT NULL DEFAULT 0,
|
|
lines_seen INTEGER NOT NULL DEFAULT 0,
|
|
tokens_seen INTEGER NOT NULL DEFAULT 0,
|
|
cards_mined INTEGER NOT NULL DEFAULT 0,
|
|
lookup_count INTEGER NOT NULL DEFAULT 0,
|
|
lookup_hits INTEGER NOT NULL DEFAULT 0,
|
|
yomitan_lookup_count INTEGER NOT NULL DEFAULT 0,
|
|
pause_count INTEGER NOT NULL DEFAULT 0,
|
|
pause_ms INTEGER NOT NULL DEFAULT 0,
|
|
seek_forward_count INTEGER NOT NULL DEFAULT 0,
|
|
seek_backward_count INTEGER NOT NULL DEFAULT 0,
|
|
media_buffer_events INTEGER NOT NULL DEFAULT 0,
|
|
CREATED_DATE TEXT,
|
|
LAST_UPDATE_DATE TEXT,
|
|
FOREIGN KEY(video_id) REFERENCES imm_videos(video_id)
|
|
);
|
|
CREATE TABLE imm_session_telemetry(
|
|
telemetry_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
session_id INTEGER NOT NULL,
|
|
sample_ms TEXT NOT NULL,
|
|
total_watched_ms INTEGER NOT NULL DEFAULT 0,
|
|
active_watched_ms INTEGER NOT NULL DEFAULT 0,
|
|
lines_seen INTEGER NOT NULL DEFAULT 0,
|
|
tokens_seen INTEGER NOT NULL DEFAULT 0,
|
|
cards_mined INTEGER NOT NULL DEFAULT 0,
|
|
lookup_count INTEGER NOT NULL DEFAULT 0,
|
|
lookup_hits INTEGER NOT NULL DEFAULT 0,
|
|
yomitan_lookup_count INTEGER NOT NULL DEFAULT 0,
|
|
pause_count INTEGER NOT NULL DEFAULT 0,
|
|
pause_ms INTEGER NOT NULL DEFAULT 0,
|
|
seek_forward_count INTEGER NOT NULL DEFAULT 0,
|
|
seek_backward_count INTEGER NOT NULL DEFAULT 0,
|
|
media_buffer_events INTEGER NOT NULL DEFAULT 0,
|
|
CREATED_DATE TEXT,
|
|
LAST_UPDATE_DATE TEXT,
|
|
FOREIGN KEY(session_id) REFERENCES imm_sessions(session_id) ON DELETE CASCADE
|
|
);
|
|
CREATE TABLE imm_session_events(
|
|
event_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
session_id INTEGER NOT NULL,
|
|
ts_ms TEXT NOT NULL,
|
|
event_type INTEGER NOT NULL,
|
|
line_index INTEGER,
|
|
segment_start_ms INTEGER,
|
|
segment_end_ms INTEGER,
|
|
tokens_delta INTEGER NOT NULL DEFAULT 0,
|
|
cards_delta INTEGER NOT NULL DEFAULT 0,
|
|
payload_json TEXT,
|
|
CREATED_DATE TEXT,
|
|
LAST_UPDATE_DATE TEXT,
|
|
FOREIGN KEY(session_id) REFERENCES imm_sessions(session_id) ON DELETE CASCADE
|
|
);
|
|
CREATE TABLE imm_daily_rollups(
|
|
rollup_day INTEGER NOT NULL,
|
|
video_id INTEGER,
|
|
total_sessions INTEGER NOT NULL DEFAULT 0,
|
|
total_active_min REAL NOT NULL DEFAULT 0,
|
|
total_lines_seen INTEGER NOT NULL DEFAULT 0,
|
|
total_tokens_seen INTEGER NOT NULL DEFAULT 0,
|
|
total_cards INTEGER NOT NULL DEFAULT 0,
|
|
cards_per_hour REAL,
|
|
tokens_per_min REAL,
|
|
lookup_hit_rate REAL,
|
|
CREATED_DATE TEXT,
|
|
LAST_UPDATE_DATE TEXT,
|
|
PRIMARY KEY (rollup_day, video_id)
|
|
);
|
|
CREATE TABLE imm_monthly_rollups(
|
|
rollup_month INTEGER NOT NULL,
|
|
video_id INTEGER,
|
|
total_sessions INTEGER NOT NULL DEFAULT 0,
|
|
total_active_min REAL NOT NULL DEFAULT 0,
|
|
total_lines_seen INTEGER NOT NULL DEFAULT 0,
|
|
total_tokens_seen INTEGER NOT NULL DEFAULT 0,
|
|
total_cards INTEGER NOT NULL DEFAULT 0,
|
|
CREATED_DATE TEXT,
|
|
LAST_UPDATE_DATE TEXT,
|
|
PRIMARY KEY (rollup_month, video_id)
|
|
);
|
|
CREATE TABLE imm_words(
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
headword TEXT,
|
|
word TEXT,
|
|
reading TEXT,
|
|
part_of_speech TEXT,
|
|
pos1 TEXT,
|
|
pos2 TEXT,
|
|
pos3 TEXT,
|
|
first_seen REAL,
|
|
last_seen REAL,
|
|
frequency INTEGER,
|
|
frequency_rank INTEGER,
|
|
UNIQUE(headword, word, reading)
|
|
);
|
|
CREATE TABLE imm_kanji(
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
kanji TEXT,
|
|
first_seen REAL,
|
|
last_seen REAL,
|
|
frequency INTEGER,
|
|
UNIQUE(kanji)
|
|
);
|
|
CREATE TABLE imm_subtitle_lines(
|
|
line_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
session_id INTEGER NOT NULL,
|
|
event_id INTEGER,
|
|
video_id INTEGER NOT NULL,
|
|
anime_id INTEGER,
|
|
line_index INTEGER NOT NULL,
|
|
segment_start_ms INTEGER,
|
|
segment_end_ms INTEGER,
|
|
text TEXT NOT NULL,
|
|
secondary_text TEXT,
|
|
CREATED_DATE INTEGER,
|
|
LAST_UPDATE_DATE INTEGER,
|
|
FOREIGN KEY(session_id) REFERENCES imm_sessions(session_id) ON DELETE CASCADE,
|
|
FOREIGN KEY(event_id) REFERENCES imm_session_events(event_id) ON DELETE SET NULL,
|
|
FOREIGN KEY(video_id) REFERENCES imm_videos(video_id) ON DELETE CASCADE,
|
|
FOREIGN KEY(anime_id) REFERENCES imm_anime(anime_id) ON DELETE SET NULL
|
|
);
|
|
CREATE TABLE imm_word_line_occurrences(
|
|
line_id INTEGER NOT NULL,
|
|
word_id INTEGER NOT NULL,
|
|
occurrence_count INTEGER NOT NULL,
|
|
seen_ms INTEGER,
|
|
PRIMARY KEY(line_id, word_id),
|
|
FOREIGN KEY(line_id) REFERENCES imm_subtitle_lines(line_id) ON DELETE CASCADE,
|
|
FOREIGN KEY(word_id) REFERENCES imm_words(id) ON DELETE CASCADE
|
|
);
|
|
CREATE TABLE imm_kanji_line_occurrences(
|
|
line_id INTEGER NOT NULL,
|
|
kanji_id INTEGER NOT NULL,
|
|
occurrence_count INTEGER NOT NULL,
|
|
seen_ms INTEGER,
|
|
PRIMARY KEY(line_id, kanji_id),
|
|
FOREIGN KEY(line_id) REFERENCES imm_subtitle_lines(line_id) ON DELETE CASCADE,
|
|
FOREIGN KEY(kanji_id) REFERENCES imm_kanji(id) ON DELETE CASCADE
|
|
);
|
|
CREATE TABLE imm_media_art(
|
|
video_id INTEGER PRIMARY KEY,
|
|
anilist_id INTEGER,
|
|
cover_url TEXT,
|
|
cover_blob BLOB,
|
|
cover_blob_hash TEXT,
|
|
title_romaji TEXT,
|
|
title_english TEXT,
|
|
episodes_total INTEGER,
|
|
fetched_at_ms TEXT NOT NULL,
|
|
CREATED_DATE TEXT,
|
|
LAST_UPDATE_DATE TEXT,
|
|
FOREIGN KEY(video_id) REFERENCES imm_videos(video_id) ON DELETE CASCADE
|
|
);
|
|
CREATE TABLE imm_youtube_videos(
|
|
video_id INTEGER PRIMARY KEY,
|
|
youtube_video_id TEXT NOT NULL,
|
|
video_url TEXT NOT NULL,
|
|
video_title TEXT,
|
|
video_thumbnail_url TEXT,
|
|
channel_id TEXT,
|
|
channel_name TEXT,
|
|
channel_url TEXT,
|
|
channel_thumbnail_url TEXT,
|
|
uploader_id TEXT,
|
|
uploader_url TEXT,
|
|
description TEXT,
|
|
metadata_json TEXT,
|
|
fetched_at_ms TEXT NOT NULL,
|
|
CREATED_DATE TEXT,
|
|
LAST_UPDATE_DATE TEXT,
|
|
FOREIGN KEY(video_id) REFERENCES imm_videos(video_id) ON DELETE CASCADE
|
|
);
|
|
CREATE TABLE imm_cover_art_blobs(
|
|
blob_hash TEXT PRIMARY KEY,
|
|
cover_blob BLOB NOT NULL,
|
|
CREATED_DATE TEXT,
|
|
LAST_UPDATE_DATE TEXT
|
|
);
|
|
CREATE TABLE imm_lifetime_global(
|
|
global_id INTEGER PRIMARY KEY CHECK(global_id = 1),
|
|
total_sessions INTEGER NOT NULL DEFAULT 0,
|
|
total_active_ms INTEGER NOT NULL DEFAULT 0,
|
|
total_cards INTEGER NOT NULL DEFAULT 0,
|
|
active_days INTEGER NOT NULL DEFAULT 0,
|
|
episodes_started INTEGER NOT NULL DEFAULT 0,
|
|
episodes_completed INTEGER NOT NULL DEFAULT 0,
|
|
anime_completed INTEGER NOT NULL DEFAULT 0,
|
|
last_rebuilt_ms TEXT,
|
|
CREATED_DATE TEXT,
|
|
LAST_UPDATE_DATE TEXT
|
|
);
|
|
CREATE TABLE imm_lifetime_anime(
|
|
anime_id INTEGER PRIMARY KEY,
|
|
total_sessions INTEGER NOT NULL DEFAULT 0,
|
|
total_active_ms INTEGER NOT NULL DEFAULT 0,
|
|
total_cards INTEGER NOT NULL DEFAULT 0,
|
|
total_lines_seen INTEGER NOT NULL DEFAULT 0,
|
|
total_tokens_seen INTEGER NOT NULL DEFAULT 0,
|
|
episodes_started INTEGER NOT NULL DEFAULT 0,
|
|
episodes_completed INTEGER NOT NULL DEFAULT 0,
|
|
first_watched_ms TEXT,
|
|
last_watched_ms TEXT,
|
|
CREATED_DATE TEXT,
|
|
LAST_UPDATE_DATE TEXT,
|
|
FOREIGN KEY(anime_id) REFERENCES imm_anime(anime_id) ON DELETE CASCADE
|
|
);
|
|
CREATE TABLE imm_lifetime_media(
|
|
video_id INTEGER PRIMARY KEY,
|
|
total_sessions INTEGER NOT NULL DEFAULT 0,
|
|
total_active_ms INTEGER NOT NULL DEFAULT 0,
|
|
total_cards INTEGER NOT NULL DEFAULT 0,
|
|
total_lines_seen INTEGER NOT NULL DEFAULT 0,
|
|
total_tokens_seen INTEGER NOT NULL DEFAULT 0,
|
|
completed INTEGER NOT NULL DEFAULT 0,
|
|
first_watched_ms TEXT,
|
|
last_watched_ms TEXT,
|
|
CREATED_DATE TEXT,
|
|
LAST_UPDATE_DATE TEXT,
|
|
FOREIGN KEY(video_id) REFERENCES imm_videos(video_id) ON DELETE CASCADE
|
|
);
|
|
CREATE TABLE imm_lifetime_applied_sessions(
|
|
session_id INTEGER PRIMARY KEY,
|
|
applied_at_ms TEXT NOT NULL,
|
|
CREATED_DATE TEXT,
|
|
LAST_UPDATE_DATE TEXT,
|
|
FOREIGN KEY(session_id) REFERENCES imm_sessions(session_id) ON DELETE CASCADE
|
|
);
|
|
CREATE TABLE imm_stats_excluded_words(
|
|
headword TEXT NOT NULL,
|
|
word TEXT NOT NULL,
|
|
reading TEXT NOT NULL,
|
|
CREATED_DATE TEXT,
|
|
LAST_UPDATE_DATE TEXT,
|
|
PRIMARY KEY(headword, word, reading)
|
|
);
|
|
CREATE INDEX idx_anime_normalized_title ON imm_anime(normalized_title_key);
|
|
CREATE INDEX idx_anime_anilist_id ON imm_anime(anilist_id);
|
|
CREATE INDEX idx_videos_anime_id ON imm_videos(anime_id);
|
|
CREATE INDEX idx_sessions_video_started ON imm_sessions(video_id, started_at_ms DESC);
|
|
CREATE INDEX idx_sessions_status_started ON imm_sessions(status, started_at_ms DESC);
|
|
CREATE INDEX idx_sessions_started_at ON imm_sessions(started_at_ms DESC);
|
|
CREATE INDEX idx_sessions_ended_at ON imm_sessions(ended_at_ms DESC);
|
|
CREATE INDEX idx_telemetry_session_sample ON imm_session_telemetry(session_id, sample_ms DESC);
|
|
CREATE INDEX idx_telemetry_sample_ms ON imm_session_telemetry(sample_ms DESC);
|
|
CREATE INDEX idx_events_session_ts ON imm_session_events(session_id, ts_ms DESC);
|
|
CREATE INDEX idx_events_type_ts ON imm_session_events(event_type, ts_ms DESC);
|
|
CREATE INDEX idx_rollups_day_video ON imm_daily_rollups(rollup_day, video_id);
|
|
CREATE INDEX idx_rollups_month_video ON imm_monthly_rollups(rollup_month, video_id);
|
|
CREATE INDEX idx_words_headword_word_reading ON imm_words(headword, word, reading);
|
|
CREATE INDEX idx_words_frequency ON imm_words(frequency DESC);
|
|
CREATE INDEX idx_kanji_kanji ON imm_kanji(kanji);
|
|
CREATE INDEX idx_kanji_frequency ON imm_kanji(frequency DESC);
|
|
CREATE INDEX idx_subtitle_lines_session_line ON imm_subtitle_lines(session_id, line_index);
|
|
CREATE INDEX idx_subtitle_lines_video_line ON imm_subtitle_lines(video_id, line_index);
|
|
CREATE INDEX idx_subtitle_lines_anime_line ON imm_subtitle_lines(anime_id, line_index);
|
|
CREATE INDEX idx_word_line_occurrences_word_seen ON imm_word_line_occurrences(word_id, seen_ms, occurrence_count, line_id);
|
|
CREATE INDEX idx_kanji_line_occurrences_kanji_seen ON imm_kanji_line_occurrences(kanji_id, seen_ms, occurrence_count, line_id);
|
|
CREATE INDEX idx_media_art_cover_blob_hash ON imm_media_art(cover_blob_hash);
|
|
CREATE INDEX idx_media_art_anilist_id ON imm_media_art(anilist_id);
|
|
CREATE INDEX idx_media_art_cover_url ON imm_media_art(cover_url);
|
|
CREATE INDEX idx_youtube_videos_channel_id ON imm_youtube_videos(channel_id);
|
|
CREATE INDEX idx_youtube_videos_youtube_video_id ON imm_youtube_videos(youtube_video_id);
|
|
`;
|