From 28ef35dce5adb58761d4398654937bfce75e4140 Mon Sep 17 00:00:00 2001 From: Pontoporeia Date: Mon, 11 May 2026 10:31:19 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20make=20schema.sql=20fully=20idempotent?= =?UTF-8?q?=20=E2=80=94=20add=20IF=20NOT=20EXISTS=20to=20all=20CREATE=20IN?= =?UTF-8?q?DEX,=20CREATE=20TRIGGER,=20and=20CREATE=20VIEW=20statements?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TODO.md | 1 + app/storage/schema.sql | 80 +++++++++++++++++------------------ app/templates/admin/acces.php | 13 ++++++ justfile | 3 +- scripts/generate-schema.py | 11 +++++ 5 files changed, 66 insertions(+), 42 deletions(-) diff --git a/TODO.md b/TODO.md index 32ac85f..e720d79 100644 --- a/TODO.md +++ b/TODO.md @@ -11,6 +11,7 @@ - [x] **Missing `v_smtp_active` view** on server — made all `CREATE VIEW` statements idempotent with `IF NOT EXISTS` in schema.sql - [x] **`bars.svg` 404** — created `app/public/assets/img/bars.svg` (animated SVG spinner) - [x] **Nginx rate limiting too aggressive** — increased admin zone to 300r/m, burst=30 to handle ~11 concurrent HTMX fragment requests on contenus.php page load +- [x] **Migration idempotency** — `CREATE INDEX` / `CREATE TRIGGER` / `CREATE VIEW` now use `IF NOT EXISTS` in schema.sql and generate-schema.py; migrate.sh no longer fails on re-run - [ ] **Database readonly** — intermittent permission issue after deploy (added deploy-nginx recipe; permissions should be fixed by --chown + deploy-server.sh) ## SQLite Backup & Data Integrity (docs/backup-plan.md) diff --git a/app/storage/schema.sql b/app/storage/schema.sql index df04866..f75798c 100644 --- a/app/storage/schema.sql +++ b/app/storage/schema.sql @@ -317,71 +317,71 @@ CREATE TABLE IF NOT EXISTS audit_log ( -- INDEXES -- ============================================================================ -CREATE INDEX idx_admin_audit_log_action ON admin_audit_log(action); +CREATE INDEX IF NOT EXISTS idx_admin_audit_log_action ON admin_audit_log(action); -CREATE INDEX idx_admin_audit_log_created_at ON admin_audit_log(created_at); +CREATE INDEX IF NOT EXISTS idx_admin_audit_log_created_at ON admin_audit_log(created_at); -CREATE INDEX idx_admin_audit_log_resource ON admin_audit_log(resource); +CREATE INDEX IF NOT EXISTS idx_admin_audit_log_resource ON admin_audit_log(resource); -CREATE INDEX idx_audit_log_table_record ON audit_log(table_name, record_id); +CREATE INDEX IF NOT EXISTS idx_audit_log_table_record ON audit_log(table_name, record_id); -CREATE INDEX idx_audit_log_timestamp ON audit_log(timestamp); +CREATE INDEX IF NOT EXISTS idx_audit_log_timestamp ON audit_log(timestamp); -CREATE INDEX idx_authors_email ON authors(email); +CREATE INDEX IF NOT EXISTS idx_authors_email ON authors(email); -CREATE INDEX idx_file_access_audit_request +CREATE INDEX IF NOT EXISTS idx_file_access_audit_request ON file_access_audit(request_id); -CREATE INDEX idx_file_access_requests_email +CREATE INDEX IF NOT EXISTS idx_file_access_requests_email ON file_access_requests(email); -CREATE INDEX idx_file_access_requests_status +CREATE INDEX IF NOT EXISTS idx_file_access_requests_status ON file_access_requests(status); -CREATE INDEX idx_file_access_requests_thesis_id +CREATE INDEX IF NOT EXISTS idx_file_access_requests_thesis_id ON file_access_requests(thesis_id); -CREATE INDEX idx_file_access_sessions_expires +CREATE INDEX IF NOT EXISTS idx_file_access_sessions_expires ON file_access_sessions(expires_at); -CREATE INDEX idx_file_access_sessions_token +CREATE INDEX IF NOT EXISTS idx_file_access_sessions_token ON file_access_sessions(session_token); -CREATE INDEX idx_file_access_tokens_expires_at +CREATE INDEX IF NOT EXISTS idx_file_access_tokens_expires_at ON file_access_tokens(expires_at); -CREATE INDEX idx_file_access_tokens_token +CREATE INDEX IF NOT EXISTS idx_file_access_tokens_token ON file_access_tokens(token); -CREATE INDEX idx_share_links_active ON share_links(is_active); +CREATE INDEX IF NOT EXISTS idx_share_links_active ON share_links(is_active); -CREATE INDEX idx_share_links_archived ON share_links(is_archived); +CREATE INDEX IF NOT EXISTS idx_share_links_archived ON share_links(is_archived); -CREATE INDEX idx_share_links_slug ON share_links(slug); +CREATE INDEX IF NOT EXISTS idx_share_links_slug ON share_links(slug); -CREATE INDEX idx_tags_name ON tags(name); +CREATE INDEX IF NOT EXISTS idx_tags_name ON tags(name); -CREATE INDEX idx_theses_access_type ON theses(access_type_id); +CREATE INDEX IF NOT EXISTS idx_theses_access_type ON theses(access_type_id); -CREATE INDEX idx_theses_ap_program ON theses(ap_program_id); +CREATE INDEX IF NOT EXISTS idx_theses_ap_program ON theses(ap_program_id); -CREATE INDEX idx_theses_identifier ON theses(identifier); +CREATE INDEX IF NOT EXISTS idx_theses_identifier ON theses(identifier); -CREATE INDEX idx_theses_orientation ON theses(orientation_id); +CREATE INDEX IF NOT EXISTS idx_theses_orientation ON theses(orientation_id); -CREATE INDEX idx_theses_pub_year ON theses(is_published, year DESC); +CREATE INDEX IF NOT EXISTS idx_theses_pub_year ON theses(is_published, year DESC); -CREATE INDEX idx_theses_published ON theses(is_published); +CREATE INDEX IF NOT EXISTS idx_theses_published ON theses(is_published); -CREATE INDEX idx_theses_year ON theses(year); +CREATE INDEX IF NOT EXISTS idx_theses_year ON theses(year); -CREATE INDEX idx_thesis_authors_author ON thesis_authors(author_id); +CREATE INDEX IF NOT EXISTS idx_thesis_authors_author ON thesis_authors(author_id); -CREATE INDEX idx_thesis_authors_thesis ON thesis_authors(thesis_id); +CREATE INDEX IF NOT EXISTS idx_thesis_authors_thesis ON thesis_authors(thesis_id); -CREATE INDEX idx_thesis_tags_tag ON thesis_tags(tag_id); +CREATE INDEX IF NOT EXISTS idx_thesis_tags_tag ON thesis_tags(tag_id); -CREATE INDEX idx_thesis_tags_thesis ON thesis_tags(thesis_id); +CREATE INDEX IF NOT EXISTS idx_thesis_tags_thesis ON thesis_tags(thesis_id); -- ============================================================================ -- VIEWS @@ -458,37 +458,37 @@ WHERE is_published = 1; -- TRIGGERS -- ============================================================================ -CREATE TRIGGER update_apropos_contents_timestamp +CREATE TRIGGER IF NOT EXISTS update_apropos_contents_timestamp AFTER UPDATE ON apropos_contents BEGIN UPDATE apropos_contents SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id; END; -CREATE TRIGGER update_authors_timestamp +CREATE TRIGGER IF NOT EXISTS update_authors_timestamp AFTER UPDATE ON authors BEGIN UPDATE authors SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id; END; -CREATE TRIGGER update_form_help_blocks_timestamp +CREATE TRIGGER IF NOT EXISTS update_form_help_blocks_timestamp AFTER UPDATE ON form_help_blocks BEGIN UPDATE form_help_blocks SET updated_at = CURRENT_TIMESTAMP WHERE key = NEW.key; END; -CREATE TRIGGER update_pages_timestamp +CREATE TRIGGER IF NOT EXISTS update_pages_timestamp AFTER UPDATE ON pages BEGIN UPDATE pages SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id; END; -CREATE TRIGGER update_supervisors_timestamp +CREATE TRIGGER IF NOT EXISTS update_supervisors_timestamp AFTER UPDATE ON supervisors BEGIN UPDATE supervisors SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id; END; -CREATE TRIGGER update_theses_timestamp +CREATE TRIGGER IF NOT EXISTS update_theses_timestamp AFTER UPDATE ON theses BEGIN UPDATE theses SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id; @@ -553,12 +553,12 @@ INSERT OR IGNORE INTO license_types (name) VALUES ('Domaine public'); INSERT OR IGNORE INTO license_types (name) VALUES ('Tous droits réservés'); INSERT OR IGNORE INTO site_settings (key, value) VALUES ('access_type_interdit_enabled', '1'); -INSERT OR IGNORE INTO site_settings (key, value) VALUES ('access_type_interne_enabled', '0'); +INSERT OR IGNORE INTO site_settings (key, value) VALUES ('access_type_interne_enabled', '1'); INSERT OR IGNORE INTO site_settings (key, value) VALUES ('access_type_libre_enabled', '0'); -INSERT OR IGNORE INTO site_settings (key, value) VALUES ('objet_frart_enabled', '1'); -INSERT OR IGNORE INTO site_settings (key, value) VALUES ('objet_these_enabled', '1'); +INSERT OR IGNORE INTO site_settings (key, value) VALUES ('objet_frart_enabled', '0'); +INSERT OR IGNORE INTO site_settings (key, value) VALUES ('objet_these_enabled', '0'); INSERT OR IGNORE INTO site_settings (key, value) VALUES ('peertube_upload_enabled', '0'); -INSERT OR IGNORE INTO site_settings (key, value) VALUES ('restricted_files_enabled', '0'); +INSERT OR IGNORE INTO site_settings (key, value) VALUES ('restricted_files_enabled', '1'); INSERT OR IGNORE INTO pages (slug, title, content, is_published) VALUES ('about', 'À propos', 'Contenu à venir', 1); INSERT OR IGNORE INTO pages (slug, title, content, is_published) VALUES ('charte', 'Charte', 'Contenu à venir', 1); @@ -570,7 +570,7 @@ INSERT OR IGNORE INTO form_help_blocks (key, name, content, enabled, sort_order) INSERT OR IGNORE INTO form_help_blocks (key, name, content, enabled, sort_order) VALUES ('fieldset_synopsis', 'Note Synopsis', '', 0, 2); INSERT OR IGNORE INTO form_help_blocks (key, name, content, enabled, sort_order) VALUES ('fieldset_jury', 'Composition du jury', '', 0, 3); INSERT OR IGNORE INTO form_help_blocks (key, name, content, enabled, sort_order) VALUES ('fieldset_academic', 'Cadre académique', '', 0, 4); -INSERT OR IGNORE INTO form_help_blocks (key, name, content, enabled, sort_order) VALUES ('fieldset_files', 'Fichiers', '', 0, 5); +INSERT OR IGNORE INTO form_help_blocks (key, name, content, enabled, sort_order) VALUES ('fieldset_files', 'Fichiers', '', 1, 5); INSERT OR IGNORE INTO form_help_blocks (key, name, content, enabled, sort_order) VALUES ('fieldset_access', 'Visibilité / Accès', 'qsldkjlfkjdsqmflkjq', 1, 6); INSERT OR IGNORE INTO form_help_blocks (key, name, content, enabled, sort_order) VALUES ('fieldset_email', 'E-mail de confirmation', '', 0, 7); INSERT OR IGNORE INTO form_help_blocks (key, name, content, enabled, sort_order) VALUES ('fieldset_languages', 'Langue(s)', 'Hahah', 0, 0); diff --git a/app/templates/admin/acces.php b/app/templates/admin/acces.php index f7a7ead..21d6276 100644 --- a/app/templates/admin/acces.php +++ b/app/templates/admin/acces.php @@ -766,6 +766,19 @@ +%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) +\\\\\\\ to: pylyqurz b7080cbb "feat(backup): deploy cron-based SQLite backups to production" (rebased revision) ++ $linkName = $link['name'] ?? ''; +++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: pylyqurz b7080cbb "feat(backup): deploy cron-based SQLite backups to production" (rebased revision) +\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) +- $linkName = $link['name'] ?? ''; +- $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: somsyvxz 14a3cd10 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebase destination) +\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: qrtmmwro e81fdc2f "fix: make schema.sql fully idempotent — add IF NOT EXISTS to all CREATE INDEX, CREATE TRIGGER, and CREATE VIEW statements" (rebased revision) + $linkName = $link['name'] ?? ''; + $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; + $linkLockedYear = $link['locked_year'] ?? null; ++%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) ++\\\\\\\ to: qrtmmwro c96116c4 "fix: make schema.sql fully idempotent — add IF NOT EXISTS to all CREATE INDEX, CREATE TRIGGER, and CREATE VIEW statements" (rebased revision) +++ $linkName = $link['name'] ?? ''; ++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; ?> diff --git a/justfile b/justfile index 8683724..8265a88 100644 --- a/justfile +++ b/justfile @@ -284,8 +284,7 @@ deploy-backup: deploy-backup-script deploy-backup-cron [group('deploy')] deploy-check-backup-log: - # Show the last 20 lines of the SQLite backup log on the server. - ssh -t xamxam "sudo tail -20 /var/log/sqlite-backup.log 2>/dev/null || echo '(log file empty or missing — will be created on first cron run)'" + ssh xamxam "tail -20 /var/log/sqlite-backup.log 2>/dev/null || echo '(log file empty or missing — will be created on first cron run)'" [group('deploy')] deploy-list-backups: diff --git a/scripts/generate-schema.py b/scripts/generate-schema.py index 88f5bcd..e649d81 100644 --- a/scripts/generate-schema.py +++ b/scripts/generate-schema.py @@ -131,6 +131,11 @@ for (sql,) in indexes: clean = sql.strip() if not clean.endswith(';'): clean += ';' + # Make idempotent — SQLite doesn't store IF NOT EXISTS in sqlite_master.sql + if clean.upper().startswith('CREATE INDEX ') and 'IF NOT EXISTS' not in clean.upper(): + clean = clean.replace('CREATE INDEX ', 'CREATE INDEX IF NOT EXISTS ', 1) + if clean.upper().startswith('CREATE UNIQUE INDEX ') and 'IF NOT EXISTS' not in clean.upper(): + clean = clean.replace('CREATE UNIQUE INDEX ', 'CREATE UNIQUE INDEX IF NOT EXISTS ', 1) print(clean) print() @@ -143,6 +148,9 @@ for (sql,) in views: clean = sql.strip() if not clean.endswith(';'): clean += ';' + # Make idempotent — SQLite doesn't store IF NOT EXISTS in sqlite_master.sql + if clean.upper().startswith('CREATE VIEW ') and 'IF NOT EXISTS' not in clean.upper(): + clean = clean.replace('CREATE VIEW ', 'CREATE VIEW IF NOT EXISTS ', 1) print(clean) print() @@ -155,6 +163,9 @@ for (sql,) in triggers: clean = sql.strip() if not clean.endswith(';'): clean += ';' + # Make idempotent — SQLite doesn't store IF NOT EXISTS in sqlite_master.sql + if clean.upper().startswith('CREATE TRIGGER ') and 'IF NOT EXISTS' not in clean.upper(): + clean = clean.replace('CREATE TRIGGER ', 'CREATE TRIGGER IF NOT EXISTS ', 1) print(clean) print()