Files
ERitors-Scribe-Desktop/electron/database/schema.ts
natreex cec5830360 Introduce series management functionality and repository updates
- Added `series-world.repo.ts` to handle database operations related to series worlds and their elements.
- Implemented `series-sync.repo.ts` for managing synchronization between books and series.
- Expanded `spell.ipc.ts` data models to include `seriesSpellId` for spell synchronization.
- Refactored `insertSpellTag` method in `spelltag.repo.ts` for improved error handling and logic clarity.
2026-01-26 19:57:56 -05:00

1071 lines
48 KiB
TypeScript

import sqlite3 from 'node-sqlite3-wasm';
import { app } from 'electron';
type Database = sqlite3.Database;
/**
* SQLite schema based on the MySQL erit_main_db schema
* All tables use snake_case naming to match the server database
* Data is encrypted before storage and decrypted on retrieval
*/
// =============================================================================
// MIGRATIONS
// =============================================================================
const schemaVersion = 3;
/**
* DEV ONLY - S'exécute à chaque refresh, pas besoin de version
* Mets ta query, test, efface après
*/
const devQueries: string[] = [];
const isDev:boolean = !app.isPackaged;
function columnExists(db: Database, table: string, column: string): boolean {
const result = db.all(`PRAGMA table_info(${table})`) as { name: string }[];
return result?.some((row) => row.name === column) ?? false;
}
function addColumn(db: Database, table: string, column: string, type: string): void {
if (!columnExists(db, table, column)) {
db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${type}`);
}
}
function getDbVersion(db: Database): number {
const result = db.get('PRAGMA user_version') as { user_version: number } | undefined;
return result?.user_version ?? 0;
}
function setDbVersion(db: Database, version: number): void {
db.exec(`PRAGMA user_version = ${version}`);
}
/**
* Check if old _schema_version table exists
*/
function hasOldSchemaTable(db: Database): boolean {
const result = db.get(`
SELECT name FROM sqlite_master
WHERE type='table' AND name='_schema_version'
`) as { name: string } | undefined;
return !!result;
}
/**
* Get version from old _schema_version table
*/
function getOldSchemaVersion(db: Database): number {
try {
const result = db.get('SELECT version FROM _schema_version LIMIT 1') as { version: number } | undefined;
return result?.version ?? 0;
} catch {
return 0;
}
}
/**
* Migrate from old _schema_version table to PRAGMA user_version
* Old system: v3 = all migrations done (book_tools created, NOT NULL fixes applied)
* New system: v1 = equivalent starting point
*/
function migrateFromOldSystem(db: Database): void {
const oldVersion = getOldSchemaVersion(db);
// Old v3 means all previous migrations were done
// Map to new system: old v3 = new v1
if (oldVersion >= 3) {
setDbVersion(db, schemaVersion);
}
// Add last_update column to book_tools if missing (was added after v3)
addColumn(db, 'book_tools', 'last_update', 'INTEGER DEFAULT 0');
// Add spells_enabled column to book_tools if missing
addColumn(db, 'book_tools', 'spells_enabled', 'INTEGER NOT NULL DEFAULT 0');
// Add new character fields if missing
addColumn(db, 'book_characters', 'nickname', 'TEXT DEFAULT NULL');
addColumn(db, 'book_characters', 'age', 'TEXT DEFAULT NULL');
addColumn(db, 'book_characters', 'gender', 'TEXT DEFAULT NULL');
addColumn(db, 'book_characters', 'species', 'TEXT DEFAULT NULL');
addColumn(db, 'book_characters', 'nationality', 'TEXT DEFAULT NULL');
addColumn(db, 'book_characters', 'status', 'TEXT DEFAULT NULL');
addColumn(db, 'book_characters', 'speech_pattern', 'TEXT DEFAULT NULL');
addColumn(db, 'book_characters', 'catchphrase', 'TEXT DEFAULT NULL');
addColumn(db, 'book_characters', 'residence', 'TEXT DEFAULT NULL');
addColumn(db, 'book_characters', 'notes', 'TEXT DEFAULT NULL');
addColumn(db, 'book_characters', 'color', 'TEXT DEFAULT NULL');
// Create book_spell_tags table if missing
db.exec(`
CREATE TABLE IF NOT EXISTS book_spell_tags (
tag_id TEXT PRIMARY KEY,
book_id TEXT NOT NULL,
user_id TEXT NOT NULL,
name TEXT NOT NULL,
name_hash TEXT NOT NULL,
color TEXT,
last_update INTEGER DEFAULT 0,
FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE
);
`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_spell_tags_book ON book_spell_tags(book_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_spell_tags_user ON book_spell_tags(user_id)`);
// Create book_spells table if missing
db.exec(`
CREATE TABLE IF NOT EXISTS book_spells (
spell_id TEXT PRIMARY KEY,
book_id TEXT NOT NULL,
user_id TEXT NOT NULL,
name TEXT NOT NULL,
name_hash TEXT NOT NULL,
description TEXT NOT NULL,
appearance TEXT NOT NULL,
tags TEXT NOT NULL,
power_level TEXT,
components TEXT,
limitations TEXT,
notes TEXT,
last_update INTEGER DEFAULT 0,
FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE
);
`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_spells_book ON book_spells(book_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_spells_user ON book_spells(user_id)`);
// Create series tables (v3)
db.exec(`CREATE TABLE IF NOT EXISTS book_series (series_id TEXT PRIMARY KEY, user_id TEXT NOT NULL, name TEXT NOT NULL, hashed_name TEXT NOT NULL, description TEXT, cover_image TEXT, last_update INTEGER DEFAULT 0, FOREIGN KEY (user_id) REFERENCES erit_users(user_id) ON DELETE CASCADE);`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_book_series_user ON book_series(user_id)`);
db.exec(`CREATE TABLE IF NOT EXISTS series_books (series_id TEXT NOT NULL, book_id TEXT NOT NULL, book_order INTEGER NOT NULL DEFAULT 1, last_update INTEGER DEFAULT 0, PRIMARY KEY (series_id, book_id), FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE, FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE);`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_books_book ON series_books(book_id)`);
db.exec(`CREATE TABLE IF NOT EXISTS series_characters (character_id TEXT PRIMARY KEY, series_id TEXT NOT NULL, user_id TEXT NOT NULL, first_name TEXT NOT NULL, last_name TEXT, nickname TEXT, age TEXT, gender TEXT, species TEXT, nationality TEXT, status TEXT, category TEXT NOT NULL, title TEXT, image TEXT, role TEXT, biography TEXT, history TEXT, speech_pattern TEXT, catchphrase TEXT, residence TEXT, notes TEXT, color TEXT, last_update INTEGER DEFAULT 0, FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE);`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_characters_series ON series_characters(series_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_characters_user ON series_characters(user_id)`);
db.exec(`CREATE TABLE IF NOT EXISTS series_characters_attributes (attr_id TEXT PRIMARY KEY, character_id TEXT NOT NULL, user_id TEXT NOT NULL, attribute_name TEXT NOT NULL, attribute_value TEXT NOT NULL, last_update INTEGER DEFAULT 0, FOREIGN KEY (character_id) REFERENCES series_characters(character_id) ON DELETE CASCADE);`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_char_attrs_character ON series_characters_attributes(character_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_char_attrs_user ON series_characters_attributes(user_id)`);
db.exec(`CREATE TABLE IF NOT EXISTS series_worlds (world_id TEXT PRIMARY KEY, series_id TEXT NOT NULL, user_id TEXT NOT NULL, name TEXT NOT NULL, hashed_name TEXT NOT NULL, history TEXT, politics TEXT, economy TEXT, religion TEXT, languages TEXT, last_update INTEGER DEFAULT 0, FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE);`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_worlds_series ON series_worlds(series_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_worlds_user ON series_worlds(user_id)`);
db.exec(`CREATE TABLE IF NOT EXISTS series_world_elements (element_id TEXT PRIMARY KEY, world_id TEXT NOT NULL, user_id TEXT NOT NULL, element_type INTEGER NOT NULL, name TEXT NOT NULL, original_name TEXT NOT NULL, description TEXT, last_update INTEGER DEFAULT 0, FOREIGN KEY (world_id) REFERENCES series_worlds(world_id) ON DELETE CASCADE);`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_world_elements_world ON series_world_elements(world_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_world_elements_user ON series_world_elements(user_id)`);
db.exec(`CREATE TABLE IF NOT EXISTS series_locations (loc_id TEXT PRIMARY KEY, series_id TEXT NOT NULL, user_id TEXT NOT NULL, loc_name TEXT NOT NULL, loc_original_name TEXT NOT NULL, last_update INTEGER DEFAULT 0, FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE);`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_locations_series ON series_locations(series_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_locations_user ON series_locations(user_id)`);
db.exec(`CREATE TABLE IF NOT EXISTS series_location_elements (element_id TEXT PRIMARY KEY, location_id TEXT NOT NULL, user_id TEXT NOT NULL, element_name TEXT NOT NULL, original_name TEXT NOT NULL, element_description TEXT, last_update INTEGER DEFAULT 0, FOREIGN KEY (location_id) REFERENCES series_locations(loc_id) ON DELETE CASCADE);`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_loc_elements_location ON series_location_elements(location_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_loc_elements_user ON series_location_elements(user_id)`);
db.exec(`CREATE TABLE IF NOT EXISTS series_location_sub_elements (sub_element_id TEXT PRIMARY KEY, element_id TEXT NOT NULL, user_id TEXT NOT NULL, sub_elem_name TEXT NOT NULL, original_name TEXT NOT NULL, sub_elem_description TEXT, last_update INTEGER DEFAULT 0, FOREIGN KEY (element_id) REFERENCES series_location_elements(element_id) ON DELETE CASCADE);`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_loc_sub_elements_element ON series_location_sub_elements(element_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_loc_sub_elements_user ON series_location_sub_elements(user_id)`);
db.exec(`CREATE TABLE IF NOT EXISTS series_spells (spell_id TEXT PRIMARY KEY, series_id TEXT NOT NULL, user_id TEXT NOT NULL, name TEXT NOT NULL, name_hash TEXT NOT NULL, description TEXT NOT NULL, appearance TEXT NOT NULL, tags TEXT, power_level TEXT, components TEXT, limitations TEXT, notes TEXT, last_update INTEGER DEFAULT 0, FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE);`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_spells_series ON series_spells(series_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_spells_user ON series_spells(user_id)`);
db.exec(`CREATE TABLE IF NOT EXISTS series_spell_tags (tag_id TEXT PRIMARY KEY, series_id TEXT NOT NULL, user_id TEXT NOT NULL, name TEXT NOT NULL, hashed_name TEXT NOT NULL, color TEXT, last_update INTEGER DEFAULT 0, FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE);`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_spell_tags_series ON series_spell_tags(series_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_spell_tags_user ON series_spell_tags(user_id)`);
// Add series_*_id columns to existing book tables (v3)
addColumn(db, 'book_characters', 'series_character_id', 'TEXT DEFAULT NULL');
addColumn(db, 'book_world', 'series_world_id', 'TEXT DEFAULT NULL');
addColumn(db, 'book_location', 'series_location_id', 'TEXT DEFAULT NULL');
addColumn(db, 'book_spells', 'series_spell_id', 'TEXT DEFAULT NULL');
// Drop old schema version table
db.exec('DROP TABLE IF EXISTS _schema_version');
}
export function runMigrations(db: Database): void {
// DEV: run test queries (skip errors silently)
if (isDev && devQueries.length > 0) {
for (const query of devQueries) {
try {
db.exec(query);
} catch (_) { /* ignore */ }
}
}
// PROD only: versioned migrations
if (isDev) return;
// Migrate from old _schema_version system to PRAGMA user_version
if (hasOldSchemaTable(db)) {
migrateFromOldSystem(db);
return; // Migration done, no need to continue
}
const currentVersion = getDbVersion(db);
if (currentVersion >= schemaVersion) return;
// v1 - book_tools table + spell book tables (for fresh DBs or DBs without old system)
if (currentVersion < 1) {
// Book Tools
db.exec(`
CREATE TABLE IF NOT EXISTS book_tools (
book_id TEXT NOT NULL,
user_id TEXT NOT NULL,
characters_enabled INTEGER NOT NULL DEFAULT 0,
worlds_enabled INTEGER NOT NULL DEFAULT 0,
locations_enabled INTEGER NOT NULL DEFAULT 0,
spells_enabled INTEGER NOT NULL DEFAULT 0,
last_update INTEGER DEFAULT 0,
UNIQUE (book_id, user_id),
FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE
);
`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_book_tools_book ON book_tools(book_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_book_tools_user ON book_tools(user_id)`);
// Book Spell Tags
db.exec(`
CREATE TABLE IF NOT EXISTS book_spell_tags (
tag_id TEXT PRIMARY KEY,
book_id TEXT NOT NULL,
user_id TEXT NOT NULL,
name TEXT NOT NULL,
name_hash TEXT NOT NULL,
color TEXT,
last_update INTEGER DEFAULT 0,
FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE
);
`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_spell_tags_book ON book_spell_tags(book_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_spell_tags_user ON book_spell_tags(user_id)`);
// Book Spells
db.exec(`
CREATE TABLE IF NOT EXISTS book_spells (
spell_id TEXT PRIMARY KEY,
book_id TEXT NOT NULL,
user_id TEXT NOT NULL,
name TEXT NOT NULL,
name_hash TEXT NOT NULL,
description TEXT NOT NULL,
appearance TEXT NOT NULL,
tags TEXT NOT NULL,
power_level TEXT,
components TEXT,
limitations TEXT,
notes TEXT,
last_update INTEGER DEFAULT 0,
FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE
);
`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_spells_book ON book_spells(book_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_spells_user ON book_spells(user_id)`);
}
// v2 - Add new character fields (nickname, age, gender, species, nationality, status, etc.)
if (currentVersion < 2) {
addColumn(db, 'book_characters', 'nickname', 'TEXT DEFAULT NULL');
addColumn(db, 'book_characters', 'age', 'TEXT DEFAULT NULL');
addColumn(db, 'book_characters', 'gender', 'TEXT DEFAULT NULL');
addColumn(db, 'book_characters', 'species', 'TEXT DEFAULT NULL');
addColumn(db, 'book_characters', 'nationality', 'TEXT DEFAULT NULL');
addColumn(db, 'book_characters', 'status', 'TEXT DEFAULT NULL');
addColumn(db, 'book_characters', 'speech_pattern', 'TEXT DEFAULT NULL');
addColumn(db, 'book_characters', 'catchphrase', 'TEXT DEFAULT NULL');
addColumn(db, 'book_characters', 'residence', 'TEXT DEFAULT NULL');
addColumn(db, 'book_characters', 'notes', 'TEXT DEFAULT NULL');
addColumn(db, 'book_characters', 'color', 'TEXT DEFAULT NULL');
}
// v3 - Add series tables and series_*_id columns to existing book tables
if (currentVersion < 3) {
// Create series tables first (order matters for foreign keys)
// Book Series (main series table)
db.exec(`CREATE TABLE IF NOT EXISTS book_series (series_id TEXT PRIMARY KEY, user_id TEXT NOT NULL, name TEXT NOT NULL, hashed_name TEXT NOT NULL, description TEXT, cover_image TEXT, last_update INTEGER DEFAULT 0, FOREIGN KEY (user_id) REFERENCES erit_users(user_id) ON DELETE CASCADE);`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_book_series_user ON book_series(user_id)`);
// Series Books (link series to books with order)
db.exec(`CREATE TABLE IF NOT EXISTS series_books (series_id TEXT NOT NULL, book_id TEXT NOT NULL, book_order INTEGER NOT NULL DEFAULT 1, last_update INTEGER DEFAULT 0, PRIMARY KEY (series_id, book_id), FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE, FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE);`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_books_book ON series_books(book_id)`);
// Series Characters
db.exec(`CREATE TABLE IF NOT EXISTS series_characters (character_id TEXT PRIMARY KEY, series_id TEXT NOT NULL, user_id TEXT NOT NULL, first_name TEXT NOT NULL, last_name TEXT, nickname TEXT, age TEXT, gender TEXT, species TEXT, nationality TEXT, status TEXT, category TEXT NOT NULL, title TEXT, image TEXT, role TEXT, biography TEXT, history TEXT, speech_pattern TEXT, catchphrase TEXT, residence TEXT, notes TEXT, color TEXT, last_update INTEGER DEFAULT 0, FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE);`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_characters_series ON series_characters(series_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_characters_user ON series_characters(user_id)`);
// Series Characters Attributes
db.exec(`CREATE TABLE IF NOT EXISTS series_characters_attributes (attr_id TEXT PRIMARY KEY, character_id TEXT NOT NULL, user_id TEXT NOT NULL, attribute_name TEXT NOT NULL, attribute_value TEXT NOT NULL, last_update INTEGER DEFAULT 0, FOREIGN KEY (character_id) REFERENCES series_characters(character_id) ON DELETE CASCADE);`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_char_attrs_character ON series_characters_attributes(character_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_char_attrs_user ON series_characters_attributes(user_id)`);
// Series Worlds
db.exec(`CREATE TABLE IF NOT EXISTS series_worlds (world_id TEXT PRIMARY KEY, series_id TEXT NOT NULL, user_id TEXT NOT NULL, name TEXT NOT NULL, hashed_name TEXT NOT NULL, history TEXT, politics TEXT, economy TEXT, religion TEXT, languages TEXT, last_update INTEGER DEFAULT 0, FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE);`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_worlds_series ON series_worlds(series_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_worlds_user ON series_worlds(user_id)`);
// Series World Elements
db.exec(`CREATE TABLE IF NOT EXISTS series_world_elements (element_id TEXT PRIMARY KEY, world_id TEXT NOT NULL, user_id TEXT NOT NULL, element_type INTEGER NOT NULL, name TEXT NOT NULL, original_name TEXT NOT NULL, description TEXT, last_update INTEGER DEFAULT 0, FOREIGN KEY (world_id) REFERENCES series_worlds(world_id) ON DELETE CASCADE);`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_world_elements_world ON series_world_elements(world_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_world_elements_user ON series_world_elements(user_id)`);
// Series Locations
db.exec(`CREATE TABLE IF NOT EXISTS series_locations (loc_id TEXT PRIMARY KEY, series_id TEXT NOT NULL, user_id TEXT NOT NULL, loc_name TEXT NOT NULL, loc_original_name TEXT NOT NULL, last_update INTEGER DEFAULT 0, FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE);`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_locations_series ON series_locations(series_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_locations_user ON series_locations(user_id)`);
// Series Location Elements
db.exec(`CREATE TABLE IF NOT EXISTS series_location_elements (element_id TEXT PRIMARY KEY, location_id TEXT NOT NULL, user_id TEXT NOT NULL, element_name TEXT NOT NULL, original_name TEXT NOT NULL, element_description TEXT, last_update INTEGER DEFAULT 0, FOREIGN KEY (location_id) REFERENCES series_locations(loc_id) ON DELETE CASCADE);`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_loc_elements_location ON series_location_elements(location_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_loc_elements_user ON series_location_elements(user_id)`);
// Series Location Sub Elements
db.exec(`CREATE TABLE IF NOT EXISTS series_location_sub_elements (sub_element_id TEXT PRIMARY KEY, element_id TEXT NOT NULL, user_id TEXT NOT NULL, sub_elem_name TEXT NOT NULL, original_name TEXT NOT NULL, sub_elem_description TEXT, last_update INTEGER DEFAULT 0, FOREIGN KEY (element_id) REFERENCES series_location_elements(element_id) ON DELETE CASCADE);`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_loc_sub_elements_element ON series_location_sub_elements(element_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_loc_sub_elements_user ON series_location_sub_elements(user_id)`);
// Series Spells
db.exec(`CREATE TABLE IF NOT EXISTS series_spells (spell_id TEXT PRIMARY KEY, series_id TEXT NOT NULL, user_id TEXT NOT NULL, name TEXT NOT NULL, name_hash TEXT NOT NULL, description TEXT NOT NULL, appearance TEXT NOT NULL, tags TEXT, power_level TEXT, components TEXT, limitations TEXT, notes TEXT, last_update INTEGER DEFAULT 0, FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE);`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_spells_series ON series_spells(series_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_spells_user ON series_spells(user_id)`);
// Series Spell Tags
db.exec(`CREATE TABLE IF NOT EXISTS series_spell_tags (tag_id TEXT PRIMARY KEY, series_id TEXT NOT NULL, user_id TEXT NOT NULL, name TEXT NOT NULL, hashed_name TEXT NOT NULL, color TEXT, last_update INTEGER DEFAULT 0, FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE);`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_spell_tags_series ON series_spell_tags(series_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_spell_tags_user ON series_spell_tags(user_id)`);
// Add series_*_id columns to existing book tables
addColumn(db, 'book_characters', 'series_character_id', 'TEXT DEFAULT NULL');
addColumn(db, 'book_world', 'series_world_id', 'TEXT DEFAULT NULL');
addColumn(db, 'book_location', 'series_location_id', 'TEXT DEFAULT NULL');
addColumn(db, 'book_spells', 'series_spell_id', 'TEXT DEFAULT NULL');
}
setDbVersion(db, schemaVersion);
}
/**
* Initialize the local SQLite database with all required tables
* @param db - SQLite database instance
*/
export function initializeSchema(db: Database): void {
// Enable foreign keys
db.exec('PRAGMA foreign_keys = ON');
// AI Conversations
db.exec(`
CREATE TABLE IF NOT EXISTS ai_conversations (
conversation_id TEXT PRIMARY KEY,
book_id TEXT NOT NULL,
mode TEXT NOT NULL,
title TEXT NOT NULL,
start_date INTEGER NOT NULL,
status INTEGER NOT NULL,
user_id TEXT NOT NULL,
summary TEXT,
convo_meta TEXT NOT NULL,
FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE
);
`);
// AI Messages History
db.exec(`
CREATE TABLE IF NOT EXISTS ai_messages_history (
message_id TEXT PRIMARY KEY,
conversation_id TEXT NOT NULL,
role TEXT NOT NULL,
message TEXT NOT NULL,
message_date INTEGER NOT NULL,
FOREIGN KEY (conversation_id) REFERENCES ai_conversations(conversation_id) ON DELETE CASCADE
);
`);
// Book Acts
db.exec(`
CREATE TABLE IF NOT EXISTS book_acts (
act_id INTEGER PRIMARY KEY,
title TEXT NOT NULL,
last_update INTEGER DEFAULT 0
);
`);
// Book Act Summaries
db.exec(`
CREATE TABLE IF NOT EXISTS book_act_summaries (
act_sum_id TEXT PRIMARY KEY,
book_id TEXT NOT NULL,
user_id TEXT NOT NULL,
act_index INTEGER NOT NULL,
summary TEXT,
last_update INTEGER DEFAULT 0,
FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE
);
`);
// Book AI Guide Line
db.exec(`
CREATE TABLE IF NOT EXISTS book_ai_guide_line (
user_id TEXT NOT NULL,
book_id TEXT NOT NULL,
global_resume TEXT,
themes TEXT,
verbe_tense INTEGER,
narrative_type INTEGER,
langue INTEGER,
dialogue_type INTEGER,
tone TEXT,
atmosphere TEXT,
current_resume TEXT,
last_update INTEGER DEFAULT 0,
PRIMARY KEY (user_id, book_id),
FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE
);
`);
// Book Chapters
db.exec(`
CREATE TABLE IF NOT EXISTS book_chapters (
chapter_id TEXT PRIMARY KEY,
book_id TEXT NOT NULL,
author_id TEXT NOT NULL,
title TEXT NOT NULL,
hashed_title TEXT,
words_count INTEGER,
chapter_order INTEGER,
last_update INTEGER DEFAULT 0,
FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE
);
`);
// Book Chapter Content
db.exec(`
CREATE TABLE IF NOT EXISTS book_chapter_content (
content_id TEXT PRIMARY KEY,
chapter_id TEXT NOT NULL,
author_id TEXT NOT NULL,
version INTEGER NOT NULL DEFAULT 2,
content TEXT,
words_count INTEGER NOT NULL,
time_on_it INTEGER NOT NULL DEFAULT 0,
last_update INTEGER DEFAULT 0,
FOREIGN KEY (chapter_id) REFERENCES book_chapters(chapter_id) ON DELETE CASCADE
);
`);
// Book Chapter Infos
db.exec(`
CREATE TABLE IF NOT EXISTS book_chapter_infos (
chapter_info_id TEXT PRIMARY KEY,
chapter_id TEXT,
act_id INTEGER,
incident_id TEXT,
plot_point_id TEXT,
book_id TEXT,
author_id TEXT,
summary TEXT,
goal TEXT,
last_update INTEGER DEFAULT 0,
FOREIGN KEY (chapter_id) REFERENCES book_chapters(chapter_id) ON DELETE CASCADE,
FOREIGN KEY (incident_id) REFERENCES book_incidents(incident_id) ON DELETE CASCADE,
FOREIGN KEY (plot_point_id) REFERENCES book_plot_points(plot_point_id) ON DELETE CASCADE
);
`);
// Book Characters
db.exec(`
CREATE TABLE IF NOT EXISTS book_characters (
character_id TEXT PRIMARY KEY,
book_id TEXT NOT NULL,
user_id TEXT NOT NULL,
first_name TEXT NOT NULL,
last_name TEXT,
nickname TEXT,
age TEXT,
gender TEXT,
species TEXT,
nationality TEXT,
status TEXT,
category TEXT NOT NULL,
title TEXT,
image TEXT,
role TEXT,
biography TEXT,
history TEXT,
speech_pattern TEXT,
catchphrase TEXT,
residence TEXT,
notes TEXT,
color TEXT,
series_character_id TEXT,
last_update INTEGER DEFAULT 0,
FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE,
FOREIGN KEY (series_character_id) REFERENCES series_characters(character_id) ON DELETE SET NULL
);
`);
// Book Character Attributes
db.exec(`
CREATE TABLE IF NOT EXISTS book_characters_attributes (
attr_id TEXT PRIMARY KEY,
character_id TEXT NOT NULL,
user_id TEXT NOT NULL,
attribute_name TEXT NOT NULL,
attribute_value TEXT NOT NULL,
last_update INTEGER DEFAULT 0,
FOREIGN KEY (character_id) REFERENCES book_characters(character_id) ON DELETE CASCADE
);
`);
// Book Character Relations
db.exec(`
CREATE TABLE IF NOT EXISTS book_characters_relations (
rel_id INTEGER PRIMARY KEY,
character_id INTEGER NOT NULL,
char_name TEXT NOT NULL,
type TEXT NOT NULL,
description TEXT NOT NULL,
history TEXT NOT NULL,
last_update INTEGER DEFAULT 0
);
`);
// Book Guide Line
db.exec(`
CREATE TABLE IF NOT EXISTS book_guide_line (
user_id TEXT NOT NULL,
book_id TEXT NOT NULL,
tone TEXT,
atmosphere TEXT,
writing_style TEXT,
themes TEXT,
symbolism TEXT,
motifs TEXT,
narrative_voice TEXT,
pacing TEXT,
intended_audience TEXT,
key_messages TEXT,
last_update INTEGER DEFAULT 0,
PRIMARY KEY (user_id, book_id),
FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE
);
`);
// Book Incidents
db.exec(`
CREATE TABLE IF NOT EXISTS book_incidents (
incident_id TEXT PRIMARY KEY,
author_id TEXT NOT NULL,
book_id TEXT NOT NULL,
title TEXT NOT NULL,
hashed_title TEXT NOT NULL,
summary TEXT,
last_update INTEGER DEFAULT 0,
FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE
);
`);
// Book Issues
db.exec(`
CREATE TABLE IF NOT EXISTS book_issues (
issue_id TEXT PRIMARY KEY,
author_id TEXT NOT NULL,
book_id TEXT NOT NULL,
name TEXT NOT NULL,
hashed_issue_name TEXT NOT NULL,
last_update INTEGER DEFAULT 0,
FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE
);
`);
// Book Location
db.exec(`
CREATE TABLE IF NOT EXISTS book_location (
loc_id TEXT PRIMARY KEY,
book_id TEXT NOT NULL,
user_id TEXT NOT NULL,
loc_name TEXT NOT NULL,
loc_original_name TEXT NOT NULL,
series_location_id TEXT,
last_update INTEGER DEFAULT 0,
FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE,
FOREIGN KEY (series_location_id) REFERENCES series_locations(loc_id) ON DELETE SET NULL
);
`);
// Book Plot Points
db.exec(`
CREATE TABLE IF NOT EXISTS book_plot_points (
plot_point_id TEXT PRIMARY KEY,
title TEXT NOT NULL,
hashed_title TEXT NOT NULL,
summary TEXT,
linked_incident_id TEXT,
author_id TEXT NOT NULL,
book_id TEXT NOT NULL,
last_update INTEGER DEFAULT 0,
FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE
);
`);
// Book World
db.exec(`
CREATE TABLE IF NOT EXISTS book_world (
world_id TEXT PRIMARY KEY,
name TEXT NOT NULL,
hashed_name TEXT NOT NULL,
author_id TEXT NOT NULL,
book_id TEXT NOT NULL,
history TEXT,
politics TEXT,
economy TEXT,
religion TEXT,
languages TEXT,
series_world_id TEXT,
last_update INTEGER DEFAULT 0,
FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE,
FOREIGN KEY (series_world_id) REFERENCES series_worlds(world_id) ON DELETE SET NULL
);
`);
// Book World Elements
db.exec(`
CREATE TABLE IF NOT EXISTS book_world_elements (
element_id TEXT PRIMARY KEY,
world_id TEXT NOT NULL,
user_id TEXT NOT NULL,
element_type INTEGER NOT NULL,
name TEXT NOT NULL,
original_name TEXT NOT NULL,
description TEXT,
last_update INTEGER DEFAULT 0,
FOREIGN KEY (world_id) REFERENCES book_world(world_id) ON DELETE CASCADE
);
`);
// Erit Books
db.exec(`
CREATE TABLE IF NOT EXISTS erit_books (
book_id TEXT PRIMARY KEY,
type TEXT NOT NULL,
author_id TEXT NOT NULL,
title TEXT NOT NULL,
hashed_title TEXT NOT NULL,
sub_title TEXT,
hashed_sub_title TEXT,
summary TEXT,
serie_id INTEGER,
desired_release_date TEXT,
desired_word_count INTEGER,
words_count INTEGER,
cover_image TEXT,
last_update INTEGER DEFAULT 0
);
`);
// Erit Book Series
db.exec(`
CREATE TABLE IF NOT EXISTS erit_book_series (
serie_id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
author_id INTEGER NOT NULL
);
`);
// Erit Editor Settings
db.exec(`
CREATE TABLE IF NOT EXISTS erit_editor (
user_id TEXT,
type TEXT NOT NULL,
text_size INTEGER NOT NULL,
text_intent INTEGER NOT NULL,
interline TEXT NOT NULL,
paper_width INTEGER NOT NULL,
theme TEXT NOT NULL,
focus INTEGER NOT NULL
);
`);
// Erit Users
db.exec(`
CREATE TABLE IF NOT EXISTS erit_users (
user_id TEXT PRIMARY KEY,
first_name TEXT NOT NULL,
last_name TEXT NOT NULL,
username TEXT NOT NULL,
email TEXT NOT NULL,
origin_email TEXT NOT NULL,
origin_username TEXT NOT NULL,
author_name TEXT,
origin_author_name TEXT,
plateform TEXT NOT NULL,
social_id TEXT,
user_group INTEGER NOT NULL DEFAULT 4,
password TEXT,
term_accepted INTEGER NOT NULL DEFAULT 0,
verify_code TEXT,
reg_date INTEGER NOT NULL,
account_verified INTEGER NOT NULL DEFAULT 0,
erite_points INTEGER NOT NULL DEFAULT 100,
stripe_customer_id TEXT,
credits_balance REAL DEFAULT 0
);
`);
// Location Element
db.exec(`
CREATE TABLE IF NOT EXISTS location_element (
element_id TEXT PRIMARY KEY,
location TEXT NOT NULL,
user_id TEXT NOT NULL,
element_name TEXT NOT NULL,
original_name TEXT NOT NULL,
element_description TEXT,
last_update INTEGER DEFAULT 0,
FOREIGN KEY (location) REFERENCES book_location(loc_id) ON DELETE CASCADE
);
`);
// Location Sub Element
db.exec(`
CREATE TABLE IF NOT EXISTS location_sub_element (
sub_element_id TEXT PRIMARY KEY,
element_id TEXT NOT NULL,
user_id TEXT NOT NULL,
sub_elem_name TEXT NOT NULL,
original_name TEXT NOT NULL,
sub_elem_description TEXT,
last_update INTEGER DEFAULT 0,
FOREIGN KEY (element_id) REFERENCES location_element(element_id) ON DELETE CASCADE
);
`);
// User Keys
db.exec(`
CREATE TABLE IF NOT EXISTS user_keys (
user_id TEXT NOT NULL,
brand TEXT NOT NULL,
key TEXT NOT NULL,
actif INTEGER NOT NULL DEFAULT 1,
FOREIGN KEY (user_id) REFERENCES erit_users(user_id) ON DELETE CASCADE
);
`);
// User Last Chapter
db.exec(`
CREATE TABLE IF NOT EXISTS user_last_chapter (
user_id TEXT NOT NULL,
book_id TEXT NOT NULL,
chapter_id TEXT NOT NULL,
version INTEGER NOT NULL,
PRIMARY KEY (user_id, book_id),
FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE,
FOREIGN KEY (chapter_id) REFERENCES book_chapters(chapter_id) ON DELETE CASCADE
);
`);
// Book Tools
db.exec(`
CREATE TABLE IF NOT EXISTS book_tools (
book_id TEXT NOT NULL,
user_id TEXT NOT NULL,
characters_enabled INTEGER NOT NULL DEFAULT 0,
worlds_enabled INTEGER NOT NULL DEFAULT 0,
locations_enabled INTEGER NOT NULL DEFAULT 0,
spells_enabled INTEGER NOT NULL DEFAULT 0,
last_update INTEGER DEFAULT 0,
UNIQUE (book_id, user_id),
FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE
);
`);
// Book Spell Tags
db.exec(`
CREATE TABLE IF NOT EXISTS book_spell_tags (
tag_id TEXT PRIMARY KEY,
book_id TEXT NOT NULL,
user_id TEXT NOT NULL,
name TEXT NOT NULL,
name_hash TEXT NOT NULL,
color TEXT,
last_update INTEGER DEFAULT 0,
FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE
);
`);
// Book Spells
db.exec(`
CREATE TABLE IF NOT EXISTS book_spells (
spell_id TEXT PRIMARY KEY,
book_id TEXT NOT NULL,
user_id TEXT NOT NULL,
name TEXT NOT NULL,
name_hash TEXT NOT NULL,
description TEXT NOT NULL,
appearance TEXT NOT NULL,
tags TEXT NOT NULL,
power_level TEXT,
components TEXT,
limitations TEXT,
notes TEXT,
series_spell_id TEXT,
last_update INTEGER DEFAULT 0,
FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE,
FOREIGN KEY (series_spell_id) REFERENCES series_spells(spell_id) ON DELETE SET NULL
);
`);
// =============================================================================
// SERIES TABLES
// =============================================================================
// Book Series (main series table)
db.exec(`
CREATE TABLE IF NOT EXISTS book_series (
series_id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
name TEXT NOT NULL,
hashed_name TEXT NOT NULL,
description TEXT,
cover_image TEXT,
last_update INTEGER DEFAULT 0,
FOREIGN KEY (user_id) REFERENCES erit_users(user_id) ON DELETE CASCADE
);
`);
// Series Books (link series to books with order)
db.exec(`
CREATE TABLE IF NOT EXISTS series_books (
series_id TEXT NOT NULL,
book_id TEXT NOT NULL,
book_order INTEGER NOT NULL DEFAULT 1,
last_update INTEGER DEFAULT 0,
PRIMARY KEY (series_id, book_id),
FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE,
FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE
);
`);
// Series Characters
db.exec(`
CREATE TABLE IF NOT EXISTS series_characters (
character_id TEXT PRIMARY KEY,
series_id TEXT NOT NULL,
user_id TEXT NOT NULL,
first_name TEXT NOT NULL,
last_name TEXT,
nickname TEXT,
age TEXT,
gender TEXT,
species TEXT,
nationality TEXT,
status TEXT,
category TEXT NOT NULL,
title TEXT,
image TEXT,
role TEXT,
biography TEXT,
history TEXT,
speech_pattern TEXT,
catchphrase TEXT,
residence TEXT,
notes TEXT,
color TEXT,
last_update INTEGER DEFAULT 0,
FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE
);
`);
// Series Characters Attributes
db.exec(`
CREATE TABLE IF NOT EXISTS series_characters_attributes (
attr_id TEXT PRIMARY KEY,
character_id TEXT NOT NULL,
user_id TEXT NOT NULL,
attribute_name TEXT NOT NULL,
attribute_value TEXT NOT NULL,
last_update INTEGER DEFAULT 0,
FOREIGN KEY (character_id) REFERENCES series_characters(character_id) ON DELETE CASCADE
);
`);
// Series Worlds
db.exec(`
CREATE TABLE IF NOT EXISTS series_worlds (
world_id TEXT PRIMARY KEY,
series_id TEXT NOT NULL,
user_id TEXT NOT NULL,
name TEXT NOT NULL,
hashed_name TEXT NOT NULL,
history TEXT,
politics TEXT,
economy TEXT,
religion TEXT,
languages TEXT,
last_update INTEGER DEFAULT 0,
FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE
);
`);
// Series World Elements
db.exec(`
CREATE TABLE IF NOT EXISTS series_world_elements (
element_id TEXT PRIMARY KEY,
world_id TEXT NOT NULL,
user_id TEXT NOT NULL,
element_type INTEGER NOT NULL,
name TEXT NOT NULL,
original_name TEXT NOT NULL,
description TEXT,
last_update INTEGER DEFAULT 0,
FOREIGN KEY (world_id) REFERENCES series_worlds(world_id) ON DELETE CASCADE
);
`);
// Series Locations
db.exec(`
CREATE TABLE IF NOT EXISTS series_locations (
loc_id TEXT PRIMARY KEY,
series_id TEXT NOT NULL,
user_id TEXT NOT NULL,
loc_name TEXT NOT NULL,
loc_original_name TEXT NOT NULL,
last_update INTEGER DEFAULT 0,
FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE
);
`);
// Series Location Elements
db.exec(`
CREATE TABLE IF NOT EXISTS series_location_elements (
element_id TEXT PRIMARY KEY,
location_id TEXT NOT NULL,
user_id TEXT NOT NULL,
element_name TEXT NOT NULL,
original_name TEXT NOT NULL,
element_description TEXT,
last_update INTEGER DEFAULT 0,
FOREIGN KEY (location_id) REFERENCES series_locations(loc_id) ON DELETE CASCADE
);
`);
// Series Location Sub Elements
db.exec(`
CREATE TABLE IF NOT EXISTS series_location_sub_elements (
sub_element_id TEXT PRIMARY KEY,
element_id TEXT NOT NULL,
user_id TEXT NOT NULL,
sub_elem_name TEXT NOT NULL,
original_name TEXT NOT NULL,
sub_elem_description TEXT,
last_update INTEGER DEFAULT 0,
FOREIGN KEY (element_id) REFERENCES series_location_elements(element_id) ON DELETE CASCADE
);
`);
// Series Spells
db.exec(`
CREATE TABLE IF NOT EXISTS series_spells (
spell_id TEXT PRIMARY KEY,
series_id TEXT NOT NULL,
user_id TEXT NOT NULL,
name TEXT NOT NULL,
name_hash TEXT NOT NULL,
description TEXT NOT NULL,
appearance TEXT NOT NULL,
tags TEXT,
power_level TEXT,
components TEXT,
limitations TEXT,
notes TEXT,
last_update INTEGER DEFAULT 0,
FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE
);
`);
// Series Spell Tags
db.exec(`
CREATE TABLE IF NOT EXISTS series_spell_tags (
tag_id TEXT PRIMARY KEY,
series_id TEXT NOT NULL,
user_id TEXT NOT NULL,
name TEXT NOT NULL,
hashed_name TEXT NOT NULL,
color TEXT,
last_update INTEGER DEFAULT 0,
FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE
);
`);
// Create indexes for better performance
createIndexes(db);
}
/**
* Create indexes for frequently queried columns
*/
function createIndexes(db: Database): void {
db.exec(`CREATE INDEX IF NOT EXISTS idx_ai_conversations_book ON ai_conversations(book_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_ai_conversations_user ON ai_conversations(user_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_ai_messages_conversation ON ai_messages_history(conversation_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_chapters_book ON book_chapters(book_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_chapter_content_chapter ON book_chapter_content(chapter_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_characters_book ON book_characters(book_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_character_attrs_character ON book_characters_attributes(character_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_world_book ON book_world(book_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_world_elements_world ON book_world_elements(world_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_book_tools_book ON book_tools(book_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_book_tools_user ON book_tools(user_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_spell_tags_book ON book_spell_tags(book_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_spell_tags_user ON book_spell_tags(user_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_spells_book ON book_spells(book_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_spells_user ON book_spells(user_id)`);
// Series tables indexes
db.exec(`CREATE INDEX IF NOT EXISTS idx_book_series_user ON book_series(user_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_books_book ON series_books(book_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_characters_series ON series_characters(series_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_characters_user ON series_characters(user_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_char_attrs_character ON series_characters_attributes(character_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_char_attrs_user ON series_characters_attributes(user_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_worlds_series ON series_worlds(series_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_worlds_user ON series_worlds(user_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_world_elements_world ON series_world_elements(world_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_world_elements_user ON series_world_elements(user_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_locations_series ON series_locations(series_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_locations_user ON series_locations(user_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_loc_elements_location ON series_location_elements(location_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_loc_elements_user ON series_location_elements(user_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_loc_sub_elements_element ON series_location_sub_elements(element_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_loc_sub_elements_user ON series_location_sub_elements(user_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_spells_series ON series_spells(series_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_spells_user ON series_spells(user_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_spell_tags_series ON series_spell_tags(series_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_series_spell_tags_user ON series_spell_tags(user_id)`);
}
/**
* Drop all tables (for testing/reset)
*/
export function dropAllTables(db: Database): void {
const tables = db.all(`
SELECT name FROM sqlite_master
WHERE type='table' AND name NOT LIKE 'sqlite_%'
`) as { name: string }[];
db.exec('PRAGMA foreign_keys = OFF');
for (const row of tables) {
db.exec(`DROP TABLE IF EXISTS ${row.name}`);
}
db.exec('PRAGMA foreign_keys = ON');
}