# Database Reference XAMXAM SQLite database — schema, configuration, and operations. The database lives at **`app/storage/xamxam.db`**. The canonical schema is **`app/storage/schema.sql`** (base tables + seed data); incremental changes are applied through versioned **migrations** in `app/migrations/` (see the *Schema changes* section). > Legacy docs referenced `posterg.db`, `database/schema.sql`, and > `config/bootstrap.php`. Those are obsolete — the schema now lives under > `app/storage/` and migrations are handled by `app/migrations/run.php`. --- ## Engine & mode - **Engine:** SQLite 3 - **Mode:** WAL (`journal_mode=wal`) for safe concurrent reads + hot backups - **Driver:** PDO (`ext-pdo` + `ext-sqlite3`), wrapped by `app/src/Database.php` --- ## Development quick start ```bash just migrate # run pending migrations (creates DB from schema if missing) just init-db # create DB from app/storage/schema.sql just reset-db # rm xamxam.db + init-db just query # open an interactive sqlite3 shell on app/storage/xamxam.db sqlite3 app/storage/xamxam.db ".tables" ``` --- ## Migrations Migrations live in `app/migrations/`. They are run via `app/migrations/run.php` (wrapped by `just migrate`) and tracked in the `_migrations` table. Apply the same way in production with `just deploy-migrate`. Each migration is a numbered file: ``` app/migrations/applied/ ├── 001_add_objet_column.sql ├── … ├── 041_combined_duration.sql ├── 042_fix_contact_columns.sql └── 043_relabel_promoteur_ulb.php # PHP migrations can run logic too ``` `schema.sql` is the fully-migrated baseline; it is regenerated from the local DB so it always reflects the applied set of migrations. --- ## Tables 32 tables. Grouped by purpose: ### Core | Table | Purpose | |-------|---------| | `theses` | The main entity (a TFE or doctoral work) | | `authors` | People who authored a thesis | | `supervisors` | Jury members (promoteur / lecteur / président) | | `thesis_files` | Uploaded files attached to a thesis | | `theses` subtypes | via `objet` column (`tfe`, `these`, …) and `is_doctoral` | ### Lookup / reference | Table | Notes | |-------|-------| | `orientations` (15) | Arts Numériques, Dessin, …, Gravure | | `ap_programs` (5) | Narration Spéculative, DPM, APS, LIENS, PACS | | `finality_types` (3) | Approfondie, Enseignement, Spécialisée | | `languages` | français, anglais, néerlandais, italian (lowercase; expandable) | | `format_types` | Audio, Vidéo, Site web, Performance, Objet éditorial, Installation, Autre | | `access_types` (3) | Libre / Interne / Interdit | | `license_types` | CC BY 4.0 …, Domaine public, Tous droits réservés | | `tags` | Keywords (dynamic, lowercase-normalised, max 10/thesis) | ### Junction (many-to-many) `thesis_authors`, `thesis_supervisors`, `thesis_languages`, `thesis_formats`, `thesis_tags`. ### Sharing / access | Table | Purpose | |-------|---------| | `share_links` | Student/partage submission links (slug, password, expiry, archived) | | `file_access_requests` | Requests to access restricted files | | `file_access_tokens` | One-time access tokens | | `file_access_sessions` | Session tokens for granted access | | `file_access_audit` | Audit trail of access events | | `one_time_tokens` | Generic single-use, expiring tokens (password reset, etc.); only SHA-256 hashes stored | ### Integrations / settings / content | Table | Purpose | |-------|---------| | `peertube_settings` | PeerTube video-hosting config (instance, channel, labels) | | `smtp_settings` | SMTP relay config + notify email (singleton `id=1`) | | `site_settings` | Feature flags (access types, objets, upload, admin password hash) | | `pages` | Editable content pages (`about`, `charte`, `licenses`) | | `apropos_contents` | Structured contact / about content | | `form_help_blocks` | Help texts on admin/partage forms | | `system_cache` | Key/value cache | ### Auditing | Table | Purpose | |-------|---------| | `admin_audit_log` | Audit log of admin operations (resource, action, status, IP/UA) | | `audit_log` | Low-level row audit (actor, action, before/after JSON) | --- ## Key columns — `theses` | Column | Type | Notes | |--------|------|-------| | `id` | INTEGER PK | Auto | | `identifier` | TEXT | Human-readable id (e.g. `2025-003`) | | `title` | TEXT NOT NULL | | | `subtitle` | TEXT | | | `year` | INTEGER NOT NULL | | | `is_doctoral` | BOOLEAN | `0`=TFE, `1`=doctoral | | `objet` | TEXT | `'tfe'` default; other objets (`these`, `frart`) gated by site_settings | | `orientation_id` / `ap_program_id` / `finality_id` | INT FK | Lookup refs | | `synopsis` / `context_note` / `remarks` | TEXT | | | `duration_value` / `duration_unit` | REAL / TEXT | Normalised duration (`pages` or `minutes`) | | `duration_pages`, `duration_minutes` | INT | Legacy per-unit duration fields (combined handling at app level) | | `has_annexes` | BOOLEAN | | | `access_type_id` | INT FK | Libre / Interne / Interdit | | `license_id` | INT FK | → `license_types` | | `cc2r` | BOOLEAN | CC2r licence checkbox | | `license_custom` | TEXT | Free-form licence text | | `jury_points` | DECIMAL(4,2) | Grade / 20 | | `jury_note_added` | BOOLEAN | | | `contact_visible` | TEXT | `'public'`/`'internal'` control for author contact | | `submitted_at`, `date_depot`, `defense_date`, `published_at` | DATETIME | Lifecycle timestamps (`date_depot` = real deposit date from CSV bulk import; Brussels-local parse → UTC storage) | | `is_published` | BOOLEAN | Public visibility flag | | `status` | TEXT | Default `'active'` | | `baiu_link` | TEXT | Institutional repo link | | `exemplaire_baiu`, `exemplaire_erg` | BOOLEAN | Physical copy flags | | `deleted_at` | DATETIME | Soft delete | | `created_at` / `updated_at` | DATETIME | Auto (`updated_at` trigger) | ### `thesis_supervisors` Tracked via an explicit `role`/flag system rather than separate entity tables: | Column | Notes | |--------|-------| | `role` | `promoteur`, `lecteur`, `president` | | `is_external` | Lecturer is external | | `is_ulb` | Promoteur is from the university (UCLouvain) side | --- ## Views - **`v_theses_full`** — all theses with joined, human-readable fields (orientations, AP, finality, access, license, authors, supervisors with role splits, languages, formats, tags/keywords, contact fields). - **`v_theses_public`** — `v_theses_full` filtered to `is_published = 1`. - **`v_smtp_active`** — active SMTP settings row. --- ## Scaffolded / helper tables - `_migrations` — applied migration bookkeeping (used by the runner). --- ## Business rules - **Access:** can only be *restricted* (Libre → Interne → Interdit), never opened back up — enforced in the UI/flow. - **Keywords (`tags`):** lowercase-normalised, de-duplicated, min 3 (form) / max 10 per thesis. - **Soft deletes:** reference tables such as `languages` carry `deleted_at`; hard `DELETE` is avoided where recoverability matters. - **Files:** stored outside the webroot under `app/storage/`; DB rows use storage-relative paths (`theses//_/…`). --- ## Common operations ```sql -- Published theses SELECT * FROM v_theses_public ORDER BY year DESC; -- Single thesis (admin view) SELECT * FROM v_theses_full WHERE id = ?; -- By year + orientation SELECT * FROM v_theses_public WHERE year = 2025 AND orientation = 'Arts Numériques'; -- Search by keyword (tag) SELECT DISTINCT t.* FROM theses t JOIN thesis_tags tt ON t.id = tt.thesis_id JOIN tags g ON tt.tag_id = g.id WHERE g.name = 'écologie' AND t.is_published = 1; -- Theses per year SELECT year, COUNT(*) FROM theses WHERE is_published = 1 GROUP BY year ORDER BY year DESC; -- Publish a thesis UPDATE theses SET is_published = 1, published_at = CURRENT_TIMESTAMP WHERE id = 5; ``` > Note: keyword/contact fields in `v_theses_*` are `GROUP_CONCAT` aggregates — > prefer joining the junction tables (`thesis_tags`/`tags`) when you need > per-row access. --- ## Backup & maintenance ```bash just backup # SQL dump → app/storage/backup_.sql just backup-snapshot # WAL-safe hot backup + gzip → app/storage/backups/ ``` On the server, backups are scheduled to `/var/backups/xamxam/` via `/etc/cron.d/xamxam-backup` (see [deployment.md](deployment.md)). Manual maintenance: ```bash sqlite3 app/storage/xamxam.db "VACUUM;" # after large deletes sqlite3 app/storage/xamxam.db "ANALYZE;" sqlite3 app/storage/xamxam.db "PRAGMA integrity_check;" # expect "ok" ``` --- ## Schema changes 1. Back up first: `cp app/storage/xamxam.db app/storage/xamxam.db.bak` 2. Add a numbered migration file under `app/migrations/applied/` (`.sql` for DDL/DML, `.php` when data logic is needed). 3. Run `just migrate` (or `just deploy-migrate` on the server) — idempotent, tracked via `_migrations`. 4. When appropriate, regenerate `app/storage/schema.sql` from the applied set so it stays a faithful baseline. Change request format: ``` Table: [table_name] Change: [add/modify/remove] Column: [column_name] Type: [data_type] Reason: [why needed] Example: [sample data] ```