diff --git a/.opencode/agents/lardon-architect.md b/.opencode/agents/lardon-architect.md index 8740d0e..f868869 100644 --- a/.opencode/agents/lardon-architect.md +++ b/.opencode/agents/lardon-architect.md @@ -1,7 +1,7 @@ --- description: Analyse en lecture seule architecture, persistance et invariants mode: subagent -model: opencode-go/qwen3.8-max +model: opencode-go/gpt-5.6-luna temperature: 0.1 maxSteps: 40 permission: diff --git a/.opencode/agents/lardon-build-backup.md b/.opencode/agents/lardon-build-backup.md index be29aef..946c7b4 100644 --- a/.opencode/agents/lardon-build-backup.md +++ b/.opencode/agents/lardon-build-backup.md @@ -1,7 +1,7 @@ --- description: Fallback MiMo pour reprendre une tranche déjà préparée mode: subagent -model: opencode-go/mimo-v2.5-pro +model: opencode-go/gpt-5.6-luna temperature: 0.1 maxSteps: 100 permission: diff --git a/.opencode/agents/lardon-build.md b/.opencode/agents/lardon-build.md index 0ff9fa3..0c7ab35 100644 --- a/.opencode/agents/lardon-build.md +++ b/.opencode/agents/lardon-build.md @@ -1,7 +1,7 @@ --- description: Implémente les tranches substantielles et corrections complexes de Lardon3D mode: subagent -model: opencode-go/deepseek-v4-pro +model: opencode-go/gpt-5.6-luna temperature: 0.1 maxSteps: 120 permission: diff --git a/.opencode/agents/lardon-concurrency.md b/.opencode/agents/lardon-concurrency.md index 8464ce2..baeef47 100644 --- a/.opencode/agents/lardon-concurrency.md +++ b/.opencode/agents/lardon-concurrency.md @@ -1,7 +1,7 @@ --- description: Audite concurrence, ownership et durées de vie partagées mode: subagent -model: opencode-go/deepseek-v4-pro +model: opencode-go/gpt-5.6-luna temperature: 0.1 maxSteps: 50 permission: diff --git a/.opencode/agents/lardon-diagnose.md b/.opencode/agents/lardon-diagnose.md index 8cbc36b..57ea65e 100644 --- a/.opencode/agents/lardon-diagnose.md +++ b/.opencode/agents/lardon-diagnose.md @@ -1,7 +1,7 @@ --- description: Diagnostic technique ciblé en lecture seule pour Lardon3D mode: subagent -model: opencode-go/deepseek-v4-pro +model: opencode-go/gpt-5.6-luna temperature: 0.1 maxSteps: 12 permission: diff --git a/.opencode/agents/lardon-docs.md b/.opencode/agents/lardon-docs.md index 3d863a3..5312ab6 100644 --- a/.opencode/agents/lardon-docs.md +++ b/.opencode/agents/lardon-docs.md @@ -1,7 +1,7 @@ --- description: Maintient la documentation canonique selon le code validé mode: subagent -model: opencode-go/mimo-v2.5-pro +model: opencode-go/gpt-5.6-luna temperature: 0.0 maxSteps: 60 permission: diff --git a/.opencode/agents/lardon-orchestrator.md b/.opencode/agents/lardon-orchestrator.md index 425fb46..0217230 100644 --- a/.opencode/agents/lardon-orchestrator.md +++ b/.opencode/agents/lardon-orchestrator.md @@ -1,7 +1,7 @@ --- description: Orchestre les tickets Lardon3D longs par phases durables mode: primary -model: opencode-go/mimo-v2.5-pro +model: opencode-go/gpt-5.6-luna temperature: 0.1 maxSteps: 200 permission: diff --git a/.opencode/agents/lardon-read.md b/.opencode/agents/lardon-read.md index da6e727..4600b5c 100644 --- a/.opencode/agents/lardon-read.md +++ b/.opencode/agents/lardon-read.md @@ -1,7 +1,7 @@ --- description: Lit des fichiers et symboles ciblés puis retourne une synthèse mode: subagent -model: opencode-go/mimo-v2.5 +model: opencode-go/gpt-5.6-luna temperature: 0.0 maxSteps: 30 permission: diff --git a/.opencode/agents/lardon-review.md b/.opencode/agents/lardon-review.md index 9504f7b..df86e50 100644 --- a/.opencode/agents/lardon-review.md +++ b/.opencode/agents/lardon-review.md @@ -1,7 +1,7 @@ --- description: Effectue la seconde revue de production après tests verts mode: subagent -model: opencode-go/qwen3.8-max +model: opencode-go/gpt-5.6-luna temperature: 0.0 maxSteps: 20 permission: diff --git a/.opencode/agents/lardon-tests.md b/.opencode/agents/lardon-tests.md index c21dc8d..9b90c7c 100644 --- a/.opencode/agents/lardon-tests.md +++ b/.opencode/agents/lardon-tests.md @@ -1,7 +1,7 @@ --- description: Exécute les validations Lardon3D strictement séquencées mode: subagent -model: opencode-go/deepseek-v4-pro +model: opencode-go/gpt-5.6-luna temperature: 0.0 maxSteps: 80 permission: diff --git a/README.md b/README.md index 720b349..f7d86e1 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ persistante, enrichissable et versionnable. - **Image View** : vues triées et filtrées pour la TUI - **Task** : moteur de tâches avec pause/reprise, annulation et séquences - **Task Checkpoint v1** : snapshot durable, fichier atomique et reprise sûre -- **Project Database v13** : résultats géométriques et tâche Geometric Verifier durables +- **Project Database v14** : résultats géométriques, tâche Geometric Verifier durable et Track Model v1 - **Geometric Verification Model v1** : identité, masque d'inliers et modèle 3×3 persistants - **Geometric Verifier v1** : Fundamental USAC/MAGSAC, reprise et lots resource-aware - **Task Kind Registry** : identité métier durable et reconstruction runtime explicite @@ -117,6 +117,7 @@ Acquisitions - [Matcher](docs/architecture/matcher.md) - [Geometric Verification](docs/architecture/geometric_verification.md) - [Geometric Verifier](docs/architecture/geometric_verifier.md) +- [Track Model](docs/architecture/tracks.md) - [Backend Vulkan ORB](docs/architecture/vulkan_matcher.md) - [Viewer](docs/architecture/viewer.md) - [Revue des fondations](docs/architecture/foundation_review.md) diff --git a/docs/architecture/geometric_verifier.md b/docs/architecture/geometric_verifier.md index 13f0e29..76fa791 100644 --- a/docs/architecture/geometric_verifier.md +++ b/docs/architecture/geometric_verifier.md @@ -261,7 +261,7 @@ VERIFIED ou REJECTED est retournée avant toute lecture Feature/Match et sans ap contrainte concurrente déclenche un unique `find` de l'identité, jamais un overwrite ou une récursion. Changer un paramètre scientifique produit un autre fingerprint et un autre résultat. -Les tests E2E utilisent le vrai Project DB v13, deux Feature Files à 8192 points, des Match Files +Les tests E2E utilisent le vrai Project DB (migré v13→v14 à l'ouverture), deux Feature Files à 8192 points, des Match Files hashés, le vrai MAGSAC et le Model v1. VERIFIED est rechargé après close/reopen avec modèle et masque bit-identiques ; REJECTED conserve son support et est également réutilisé. diff --git a/docs/architecture/project_database.md b/docs/architecture/project_database.md index f6dcdd7..7ec9cb3 100644 --- a/docs/architecture/project_database.md +++ b/docs/architecture/project_database.md @@ -1,8 +1,10 @@ # Base de données projet Lardon3D -> Version courante : **v13**. La migration transactionnelle v12→v13 ajoute -> uniquement `geometric_verifier_tasks` pour la tâche durable. La migration -> v11→v12 ajoute le modèle immutable `geometric_verification_results`. La migration v10→v11 ajoute +> Version courante : **v14**. La migration transactionnelle v13→v14 ajoute +> les tables `track_sets`, `tracks` et `track_observations` pour le Track +> Model v1. La migration v12→v13 ajoute uniquement `geometric_verifier_tasks` +> pour la tâche durable. La migration v11→v12 ajoute le modèle immutable +> `geometric_verification_results`. La migration v10→v11 ajoute > `matcher_tasks` pour la tâche Matcher durable. La version v10 publiée ajoute > uniquement `match_results` pour le Match Result Model. La migration v8→v9 > ajoute la table `candidate_pair_generate_tasks` pour la tâche durable @@ -68,18 +70,23 @@ entités. Elle est conçue pour être légère, persistante et permettre la repr - Statut (validée, rejetée) - Métriques -#### Track -- Identifiant unique -- Observations -- Point 3D associé -- Qualité +#### Track Set (v14) +- Identifiant unique (`track_set_id`) +- Identité de reuse (builder, verifier, scope) +- Nombre de tracks et GVR +- Immutable après publication -#### Observation -- Identifiant unique -- Image -- Position 2D -- Descripteur +#### Track (v14) +- Identifiant unique (`track_id`) +- Track Set parent +- Nombre d'observations (≥ 2) +- Pas de coordonnées 3D en v1 + +#### Track Observation (v14) +- Identifiant composite `(track_set_id, feature_set_id, feature_index)` - Track parent +- Position dans le track (`position_in_track`) +- Feature Set et index de feature #### Camera - Identifiant unique @@ -151,9 +158,9 @@ entités. Elle est conçue pour être légère, persistante et permettre la repr - Image → Visual Signature (1:N) - Candidate Pair → Image (2) - Verified Pair → Candidate Pair (1) -- Track → Observation (N:M) -- Observation → Image (1) -- Observation → Point3D (N:1) +- Track Set → Track (1:N, CASCADE) +- Track → Track Observation (1:N, CASCADE) +- Track Observation → Feature Set (N:1) - Camera → Pose (1:N) - Pose → Reconstruction Layer (N:M) - Reconstruction Layer → Artifact (1:N) @@ -456,22 +463,108 @@ L'index parent sert la liste paginée ; la contrainte UNIQUE sert le find exact. Le contrat complet, dont l'ordre des bits, est dans `geometric_verification.md`. +## Schéma v14 implémenté + +La migration v13→v14 ajoute les tables `track_sets`, `tracks` et +`track_observations` pour le Track Model v1. Le schéma complet est dans +`tracks.md`. Schéma abrégé (la chaîne SQL exécutable canonique reste dans +`src/project_db.c`) : + +```sql +CREATE TABLE track_sets( + track_set_id INTEGER PRIMARY KEY AUTOINCREMENT + CHECK(track_set_id > 0), + builder_kind TEXT NOT NULL + CHECK(length(builder_kind) > 0 AND length(builder_kind) <= 64), + builder_version INTEGER NOT NULL CHECK(builder_version > 0), + parameter_fingerprint BLOB NOT NULL + CHECK(length(parameter_fingerprint) = 32), + verifier_kind INTEGER NOT NULL CHECK(verifier_kind > 0), + verifier_version INTEGER NOT NULL CHECK(verifier_version > 0), + verifier_fingerprint BLOB NOT NULL + CHECK(length(verifier_fingerprint) = 32), + input_scope_hash BLOB NOT NULL + CHECK(length(input_scope_hash) = 32), + gvr_count INTEGER NOT NULL CHECK(gvr_count >= 1), + track_count INTEGER NOT NULL CHECK(track_count >= 0), + created_at INTEGER NOT NULL CHECK(created_at >= 0), + UNIQUE(builder_kind, builder_version, parameter_fingerprint, + verifier_kind, verifier_version, verifier_fingerprint, + input_scope_hash) +); + +CREATE TABLE tracks( + track_id INTEGER PRIMARY KEY AUTOINCREMENT CHECK(track_id > 0), + track_set_id INTEGER NOT NULL + REFERENCES track_sets(track_set_id) ON DELETE CASCADE, + observation_count INTEGER NOT NULL CHECK(observation_count >= 2) +); + +CREATE INDEX tracks_set_idx + ON tracks(track_set_id, track_id); + +CREATE TABLE track_observations( + track_set_id INTEGER NOT NULL, + track_id INTEGER NOT NULL + REFERENCES tracks(track_id) ON DELETE CASCADE, + feature_set_id INTEGER NOT NULL + REFERENCES feature_sets(feature_set_id), + feature_index INTEGER NOT NULL CHECK(feature_index >= 0), + position_in_track INTEGER NOT NULL CHECK(position_in_track >= 0), + PRIMARY KEY(track_set_id, feature_set_id, feature_index), + UNIQUE(track_id, position_in_track) +); + +CREATE INDEX track_observations_lookup_idx + ON track_observations(feature_set_id, feature_index, track_set_id); +``` + +**Invariants SQL** : +- `PRIMARY KEY(track_set_id, feature_set_id, feature_index)` : dans un Track + Set donné, une observation n'apparaît qu'une fois +- `REFERENCES tracks(track_id) ON DELETE CASCADE` : supprimer un track + supprime ses observations +- `REFERENCES feature_sets(feature_set_id)` : le Feature Set existe +- `CHECK(observation_count >= 2)` : minimum structurel +- `UNIQUE(builder_kind, builder_version, parameter_fingerprint, + verifier_kind, verifier_version, verifier_fingerprint, input_scope_hash)` : + identité de reuse sur `track_sets` +- `ON DELETE CASCADE` depuis `track_sets` : supprimer un set supprime tout +- `UNIQUE(track_id, position_in_track)` : chaque position dans un track est + unique + +**Invariants API** (non protégés par le schéma SQL) : +- `track_set_id` dans `track_observations` correspond au `track_set_id` du + `track_id` parent +- Une seule observation par image par track (validation via + `feature_sets.image_id`) +- `feature_index < feature_sets.feature_count` +- `observation_count` cohérent avec le nombre réel d'observations +- `track_count` cohérent avec le nombre réel de tracks + +**Statut** : les tables sont créées par la migration et validées par les +tests. L'API C (`create_track_set`, `load_track_set`, `find_track_set`, +`list_track_sets`, `load_track`, `list_tracks`, `find_track_by_observation`) +est exposée et implémentée. Les limites réelles restent l'absence de Track +Builder algorithmique, de tâche dédiée et de triangulation. + ## Ouverture et migrations -Une DB vide reçoit la chaîne de schémas jusqu'à v13 dans une transaction +Une DB vide reçoit la chaîne de schémas jusqu'à v14 dans une transaction `BEGIN IMMEDIATE`. Une DB v1 reçoit transactionnellement les colonnes nullable `task_kind` et `task_kind_version`, puis les migrations v2→v3. Les anciennes lignes restent `NULL/NULL`, sans type inventé et sans perte des projets, tâches, checkpoints ou artefacts. Une interruption ou erreur provoque un rollback complet. Les DB v1 -à v12 sont migrées séquentiellement vers v13. +à v13 sont migrées séquentiellement vers v14. Une v10 publiée est validée comme telle avant que v10→v11 crée `matcher_tasks` ; son absence n'est donc pas une corruption. Une version future est refusée et une DB contenant des tables sans métadonnée de version est considérée corrompue. La fonction interne de migration applique uniquement la chaîne séquentielle connue jusqu'à -v13 ; une valeur hors de 1..13 est refusée. La failure injectée v12 rollbacke +v14 ; une valeur hors de 1..14 est refusée. La failure injectée v12 rollbacke la table, l'index et le changement de version, laissant une vraie v11 utilisable. La failure injectée v13 conserve une vraie v12 sans `geometric_verifier_tasks` ; -un retry applique ensuite v12→v13. +un retry applique ensuite v12→v13. La failure injectée v14 conserve une vraie +v13 sans `track_sets` ; un retry applique ensuite v13→v14. Migration v1→v2 exacte, exécutée entre `BEGIN IMMEDIATE` et `COMMIT` : @@ -608,8 +701,8 @@ ouvert. ## Statut -**IMPLEMENTED** — SQLite système, schéma v13 et migrations séquentielles -v1→v2→v3→v4→v5→v6→v7→v8→v9→v10→v11→v12→v13, identité projet, transactions +**IMPLEMENTED** — SQLite système, schéma v14 et migrations séquentielles +v1→v2→v3→v4→v5→v6→v7→v8→v9→v10→v11→v12→v13→v14, identité projet, transactions tâche+checkpoint, pagination de reprise et artefacts génériques. **IMPLEMENTED** — ouverture/fermeture avec le projet, identité INI/DB cohérente, @@ -634,6 +727,20 @@ positionne `metadata.legacy_image_catalog_pending=1` dès qu'une ancienne tâche d'import existe. Cet indicateur signifie « données legacy potentiellement non cataloguées », pas « images migrées ». +**IMPLEMENTED** — Track Model v1 : tables `track_sets`, `tracks` et +`track_observations` créées par la migration v13→v14, contraintes SQL +(unicité d'identité de reuse, CASCADE, observation unique par set) et +tests de migration/failure validés. Le schéma complet et les invariants +sont documentés dans `tracks.md`. + +**IMPLEMENTED** — API C Track Model v1 : header `project_db.h` et +source `project_db.c` exposent `create_track_set`, `load_track_set`, +`find_track_set`, `list_track_sets`, `load_track`, `list_tracks`, +`find_track_by_observation` et `free_track`. Contraintes et +invariants documentés dans `tracks.md`. Limites réelles : pas de +Track Builder algorithmique, pas de tâche dédiée, pas de +triangulation. + **NOT_YET_WIRED** — autosave à toutes les transitions, retry UI des sources indisponibles, migration de la TUI legacy et réconciliation des fichiers orphelins et compaction Visual Index. Feature Store et Visual Index v1 sont implémentés. diff --git a/docs/architecture/reconstruction_pipeline.md b/docs/architecture/reconstruction_pipeline.md index 08e4985..1690ab9 100644 --- a/docs/architecture/reconstruction_pipeline.md +++ b/docs/architecture/reconstruction_pipeline.md @@ -122,7 +122,9 @@ USAC/MAGSAC avec configuration, seed et fingerprint déterministes. | **Lien avec le catalogue** | Les tracks référenceront les images par `image_id`, jamais par nom ou chemin. | | **Persistance** | Les tracks sont persistés entre les sessions de traitement. Un track ne peut être détruit que par une action explicite de l'utilisateur. | -**Statut :** PLANNED — aucune implémentation existante. +**Statut :** PARTIAL — le Track Model v1 (persistance) est implémenté dans +Project DB v14 (`track_sets`, `tracks`, `track_observations`). Le Track +Builder, la triangulation et le Sparse SfM restent PLANNED. --- diff --git a/docs/architecture/tracks.md b/docs/architecture/tracks.md new file mode 100644 index 0000000..178507e --- /dev/null +++ b/docs/architecture/tracks.md @@ -0,0 +1,523 @@ +# Track Model v1 + +## Scope + +Track Model v1 est le contrat persistant qui transforme les correspondances +géométriquement vérifiées en structures multi-view cohérentes. Il stocke des +ensembles d'observations 2D liées à un même point physique supposé. Il ne +calcule rien, ne triangule pas, ne contient aucune coordonnée 3D et ne résout +aucun conflit. Le Track Builder, la triangulation, le Sparse SfM et le Bundle +Adjustment sont des étapes ultérieures séparées. + +## Track definition + +Un **Track** est un ensemble d'observations 2D cohérentes d'un même point +physique supposé, observé à travers plusieurs images. Chaque observation est +identifiée par `(feature_set_id, feature_index)`. + +Un Track n'est **pas** un point 3D. Il ne contient aucune coordonnée 3D, +aucune erreur de reprojection, aucun statut de triangulation. La +triangulation appartient à une étape ultérieure. + +La chaîne scientifique correcte est : + +```text +Matcher → Match Result → Geometric Verification → Track Builder (futur) +→ Track Model → Sparse SfM (futur) +``` + +Le Matcher ne produit pas les Tracks. Le Track Builder futur les +assemblera à partir des Geometric Verification Results. + +## Observation identity + +Une observation est identifiée par : + +``` +(feature_set_id, feature_index) +``` + +- `feature_set_id` : identifiant SQLite AUTOINCREMENT du Feature Set. Le + Feature Set porte directement `image_id` comme colonne NOT NULL FK. L'image + est dérivable par `SELECT image_id FROM feature_sets WHERE feature_set_id=?`. +- `feature_index` : ordinal zero-based dans le tableau de keypoints du Feature + File, stable tant que le Feature Set existe. Un Feature Set publié est + immutable : aucune API de production ne modifie ses colonnes après INSERT. + +L'identité `(feature_set_id, feature_index)` est suffisante. Il est inutile +de porter `image_id` dans la table d'observations car il est dérivable via +`feature_sets.image_id`. + +Note : `feature_sets` ne possède pas de colonne d'état. L'existence d'une +ligne publiée dans la table constitue le contrat réel de disponibilité du +Feature Set. + +## Scientific inputs + +Les Tracks sont construits exclusivement à partir de : + +``` +Geometric Verification Result + status == GEOMETRIC_VERIFIED (2) +``` + +correspondant exactement au VERIFICATION_SELECTOR du Track Set. + +Pour chaque résultat vérifié, les entrées du Match File dont le bit +correspondant dans le masque d'inliers vaut 1 fournissent les correspondances +valides. La chaîne de dérivation est : + +```text +GVR → match_result_id + → candidate_pair + feature_set_id_a + feature_set_id_b + → Match File entry[i] = (feature_index_a, feature_index_b, distance) + → bit i du masque d'inliers = 1 + → observation A: (feature_set_id_a, feature_index_a) + → observation B: (feature_set_id_b, feature_index_b) +``` + +Un `GEOMETRIC_REJECTED` ne produit aucun track. Un Match Result non vérifié +géométriquement ne suffit pas. + +## VERIFICATION_SELECTOR + +Le VERIFICATION_SELECTOR définit la configuration de Geometric Verification +éligible pour un Track Set. Il est stocké sur le Track Set et fait partie de +son identité de reuse. + +``` +( + verifier_kind INTEGER, -- ex: 1 = FUNDAMENTAL + verifier_version INTEGER, + parameter_fingerprint BLOB(32) +) +``` + +Le Track Builder ne consomme que les GVR avec `status == GEOMETRIC_VERIFIED` +correspondant exactement à ce tuple. Aucune sélection par timestamp, "latest" +ou ordre d'insertion n'est permise. + +Valeur production : `(1, 1, SHA-256 de l'encodage canonique 84 octets)`. + +## INPUT_SCOPE + +L'INPUT_SCOPE représente l'ensemble scientifique réel des entrées consommées +par une Track Generation donnée. Il est distinct du VERIFICATION_SELECTOR : +le selector dit quels GVR sont admissibles, le scope dit quels GVR ont +effectivement été consommés. + +``` +input_scope_hash BLOB(32) -- SHA-256 canonique +gvr_count INTEGER -- nombre de GVR consommés +``` + +### INPUT_SCOPE_HASH + +| Propriété | Valeur | +|-----------|--------| +| Domain/version | `L3DTSIS1` (8 octets ASCII) | +| Items | `geometric_verification_result_id` des GVR consommés | +| Canonical ordering | IDs triés par ordre croissant | +| Serialization | Chaque ID : 8 octets little-endian | +| Digest | SHA-256 | +| DB-local IDs | OUI — le reuse est scoped à une DB projet | +| Duplicate handling | Inutile — les IDs sont uniques par construction | +| Empty scope | Interdit — un Track Set sans GVR n'a pas de sens | + +Le digest est calculé sur `L3DTSIS1` (8 octets) suivi des IDs sérialisés : +`SHA-256(L3DTSIS1 || id_0 || id_1 || ... || id_N)` où chaque `id_i` est +8 octets little-endian et les IDs sont triés par ordre croissant. + +Le `gvr_count` est stocké comme métadonnée de validation. Il permet de +détecter un scope incomplet sans re-hasher. Il ne fait pas partie du hash +lui-même. + +Le scope_hash est DB-local : il utilise les `geometric_verification_result_id` +SQLite. Deux DB distinctes avec les mêmes données produiront des IDs +différents. Le reuse est donc scoped à une seule DB projet. + +## Track membership invariants + +1. **Minimum structurel** : un Track contient au moins 2 observations. + Une seule observation ne constitue aucune relation multi-view. Le futur + Track Builder, la triangulation ou le Sparse SfM pourront appliquer des + critères plus stricts. Le Model ne fixe pas de plafond de reconstruction. + +2. **One observation per image** : un Track ne contient pas deux observations + issues de la même image. Cette contrainte est validée par l'API lors de la + création. Le schéma v1 ne dénormalise pas `image_id` dans + `track_observations` ; l'API vérifie déterministement la relation via + `feature_sets.image_id` avant publication sous `BEGIN IMMEDIATE`. + + **SQL** : non protégé (pas de colonne `image_id` dans `track_observations`). + **API** : validation par jointure `feature_sets.image_id` avant INSERT. + +3. **Observation unique across tracks** : dans un même Track Set, une + observation `(feature_set_id, feature_index)` n'appartient qu'à un seul + Track. + + **SQL** : `PRIMARY KEY(track_set_id, feature_set_id, feature_index)` sur + `track_observations`. Le `track_set_id` est dénormalisé depuis `tracks`. + **API** : validation que `track_set_id` correspond au `track_set_id` du + `track_id` parent. + +4. **Feature Set existence** : chaque `feature_set_id` référencé existe dans + la table `feature_sets`. La FK SQLite garantit la référence. + + **SQL** : `REFERENCES feature_sets(feature_set_id)`. + +5. **Feature index bounds** : `feature_index < feature_sets.feature_count` + pour l'observation correspondante. + + **SQL** : `CHECK(feature_index >= 0)`. + **API** : validation de la borne supérieure via `feature_sets.feature_count` + (SQLite CHECK ne peut pas référencer une autre table). + +## Track identity + +Un Track persistant possède un identifiant opaque : + +``` +track_id INTEGER PRIMARY KEY AUTOINCREMENT CHECK(track_id > 0) +``` + +Il n'a pas d'identité scientifique dérivée de son contenu en v1. Les raisons : + +- un hash de membership rendrait les INSERTs dépendants de l'ordre ; +- le contenu d'un track peut être reconstruit depuis les GVR sources ; +- un `track_id` opaque suffit pour la persistance, le référencement et la + pagination ; +- la corruption est détectée par cohérence interne (doublons, images + manquantes, index hors bornes) plutôt que par re-hash. + +La reproductibilité est assurée au niveau du Track Set (parent), pas du Track +individuel. + +## Track Set / Generation + +Un **Track Set** est le parent obligatoire de tout Track persistant. Il +représente une génération complète de Track Building. + +Champs : + +``` +track_set_id INTEGER PK AUTOINCREMENT +builder_kind TEXT(1..64) +builder_version INTEGER > 0 +parameter_fingerprint BLOB(32) +verifier_kind INTEGER -- VERIFICATION_SELECTOR +verifier_version INTEGER +verifier_fingerprint BLOB(32) +input_scope_hash BLOB(32) +gvr_count INTEGER >= 1 +track_count INTEGER >= 0 +created_at INTEGER >= 0 +``` + +### Identité de reuse + +``` +( + builder_kind, + builder_version, + parameter_fingerprint, + verifier_kind, + verifier_version, + verifier_fingerprint, + input_scope_hash +) +``` + +`gvr_count` est stocké comme métadonnée de validation mais ne fait pas +partie de l'identité de reuse. Le même `input_scope_hash` avec un `gvr_count` +différent indiquerait une corruption (hash cohérent mais nombre de sources +incohérent). + +Un set existant avec cette identité exacte est réutilisé. `INSERT OR REPLACE` +est interdit. + +### Immutabilité + +Un Track Set publié est **immutable**. Aucune opération d'append, remove ou +merge n'est permise sur un track ou un set existant. + +L'invalidation scientifique (nouvelle entrée, nouveau scope, nouvelle +configuration) produit un nouveau Track Set. Le set précédent reste intact. + +La suppression référentielle utilise `ON DELETE CASCADE` : supprimer un +Track Set supprime ses tracks et observations. + +### Justification + +- chaque rebuild crée un nouveau set, les anciens restent intacts ; +- plusieurs configurations peuvent coexister (expérimentation) ; +- l'invalidation est simple : supprimer un set supprime ses tracks via + CASCADE ; +- la reproductibilité est portée par le fingerprint et le scope_hash ; +- pas d'UPDATE/INSERT/MERGE sur des tracks existants ; +- cohérent avec tous les résultats publiés existants (Feature Sets, Match + Results, GVRs) qui sont immutables après publication. + +Le Track Builder futur construira en mémoire, puis publiera un set complet +dans une transaction. Aucun track n'est visible avant que le set entier soit +validé. + +## Immutability / incrementality + +Un Track publié dans un set est **immutable**. + +L'incrémentalité est gérée par création de nouveaux sets : + +1. nouvelles images → nouveaux Match Results → nouveaux GVR → nouveau + Track Set ; +2. le set précédent reste valide et consultable ; +3. le futur Sparse SfM choisira quel set consommer. + +Cette approche est cohérente avec la philosophie Lardon3D : + +- résultats atomiques ; +- pas de destruction silencieuse ; +- reprise à frontière connue ; +- conservation de l'historique. + +## Persistence + +### Conceptual schema + +```sql +CREATE TABLE track_sets( + track_set_id INTEGER PRIMARY KEY AUTOINCREMENT + CHECK(track_set_id > 0), + builder_kind TEXT NOT NULL + CHECK(length(builder_kind) > 0 AND length(builder_kind) <= 64), + builder_version INTEGER NOT NULL CHECK(builder_version > 0), + parameter_fingerprint BLOB NOT NULL + CHECK(length(parameter_fingerprint) = 32), + verifier_kind INTEGER NOT NULL CHECK(verifier_kind > 0), + verifier_version INTEGER NOT NULL CHECK(verifier_version > 0), + verifier_fingerprint BLOB NOT NULL + CHECK(length(verifier_fingerprint) = 32), + input_scope_hash BLOB NOT NULL + CHECK(length(input_scope_hash) = 32), + gvr_count INTEGER NOT NULL CHECK(gvr_count >= 1), + track_count INTEGER NOT NULL CHECK(track_count >= 0), + created_at INTEGER NOT NULL CHECK(created_at >= 0), + UNIQUE(builder_kind, builder_version, parameter_fingerprint, + verifier_kind, verifier_version, verifier_fingerprint, + input_scope_hash) +); + +CREATE TABLE tracks( + track_id INTEGER PRIMARY KEY AUTOINCREMENT CHECK(track_id > 0), + track_set_id INTEGER NOT NULL + REFERENCES track_sets(track_set_id) ON DELETE CASCADE, + observation_count INTEGER NOT NULL CHECK(observation_count >= 2) +); + +CREATE INDEX tracks_set_idx + ON tracks(track_set_id, track_id); + +CREATE TABLE track_observations( + track_set_id INTEGER NOT NULL, + track_id INTEGER NOT NULL + REFERENCES tracks(track_id) ON DELETE CASCADE, + feature_set_id INTEGER NOT NULL + REFERENCES feature_sets(feature_set_id), + feature_index INTEGER NOT NULL CHECK(feature_index >= 0), + position_in_track INTEGER NOT NULL CHECK(position_in_track >= 0), + PRIMARY KEY(track_set_id, feature_set_id, feature_index), + UNIQUE(track_id, position_in_track) +); + +CREATE INDEX track_observations_lookup_idx + ON track_observations(feature_set_id, feature_index, track_set_id); +``` + +### Schema invariants + +**SQL-enforced :** + +- `track_observations.PRIMARY KEY(track_set_id, feature_set_id, feature_index)` + : dans un Track Set donné, une observation n'apparaît qu'une fois. Cela + garantit qu'une observation scientifique appartient à au plus un Track dans + ce set. +- `REFERENCES tracks(track_id) ON DELETE CASCADE` : l'observation appartient + à un track existant ; supprimer le track supprime l'observation. +- `REFERENCES feature_sets(feature_set_id)` : le Feature Set existe. +- `CHECK(observation_count >= 2)` : minimum structurel. +- `UNIQUE(builder_kind, builder_version, parameter_fingerprint, + verifier_kind, verifier_version, verifier_fingerprint, + input_scope_hash)` sur `track_sets` : identité de reuse, empêche les + doublons de set pour une même configuration et un même scope. +- `ON DELETE CASCADE` depuis `track_sets` : supprimer un set supprime tout. +- `CHECK(feature_index >= 0)` : borne inférieure de l'index. +- `UNIQUE(track_id, position_in_track)` : chaque position dans un track est + unique. L'ordre est déterminé par le Track Builder lors de la publication. + +**API-enforced :** + +- `track_set_id` dans `track_observations` correspond au `track_set_id` du + `track_id` parent. Le schéma ne comporte pas de FK composite (aucun + précédent dans le codebase). L'API valide cette cohérence avant INSERT sous + `BEGIN IMMEDIATE`. +- Une seule observation par image par track. L'API valide via jointure à + `feature_sets.image_id`. +- `feature_index < feature_sets.feature_count`. L'API valide la borne + supérieure. +- `observation_count` cohérent avec le nombre réel d'observations insérées. +- `track_count` cohérent avec le nombre réel de tracks insérés. +- `position_in_track` contigu à partir de 0 pour chaque track. + +### Note sur la dénormalisation + +`track_set_id` dans `track_observations` dénormalise une clé grandparent, +après le même pattern utilisé par `visual_index_memberships.visual_index_id`. +Le pattern parent-key-in-UNIQUE est déjà répandu dans le codebase. La cohérence +repose sur le chemin d'écriture unique du Track Builder et la validation API +sous transaction. + +`track_observations.track_set_id` n'a pas de FK directe vers `track_sets` +pour éviter un second chemin CASCADE depuis `track_sets` vers +`track_observations` (le premier chemin passe par `tracks`). La cohérence +est garantie par l'API sous `BEGIN IMMEDIATE`. + +## Provenance + +### Track Set provenance + +Chaque Track Set conserve : + +- `builder_kind`, `builder_version`, `parameter_fingerprint` : configuration + du Track Builder ; +- `verifier_kind`, `verifier_version`, `verifier_fingerprint` : configuration + du Geometric Verifier consommé ; +- `input_scope_hash`, `gvr_count` : ensemble réel des GVR consommés. + +Ces champs suffisent pour identifier la configuration scientifique complète +ayant produit le set. + +### Edge provenance (deferred) + +En v1, la provenance détaillée (quels GVR spécifiques ont contribué à quel +track individuel) n'est pas persistée. Les raisons : + +- elle peut être reconstruite en comparant les memberships du set aux GVR + disponibles ; +- une table `track_set_sources` volumineuse complexifie la DB sans bénéfice + immédiat ; +- le Track Builder futur pourra l'ajouter dans une migration ultérieure. + +## Invalidation + +### Invalidation scientifique + +Un nouveau scope, une nouvelle configuration de verifier ou un nouveau +builder produit un **nouveau** Track Set avec une identité différente. Le set + précédent reste intact et consultable. Aucune mutation silencieuse n'est +permise. + +### Suppression référentielle + +`ON DELETE CASCADE` s'applique : + +- `track_sets` → `tracks` → `track_observations` : supprimer un set supprime + tous ses tracks et observations ; +- `feature_sets` → (pas de CASCADE vers `track_observations`) : la FK utilise + le comportement par défaut (NO ACTION). Supprimer un Feature Set référencé + par une observation est interdit tant que l'observation existe. + +## Atomic publication + +L'unité persistante est le Track Set complet. La publication est une seule +transaction `BEGIN IMMEDIATE` contenant l'INSERT du set, de tous ses tracks +et de toutes ses observations. + +- aucun track n'est visible avant le COMMIT du set entier ; +- un rollback ne laisse aucune ligne partielle ; +- le `created_at` du set est le timestamp de la transaction ; +- le `track_count` et `gvr_count` sont validés contre les INSERTs réels. + +Le Track Builder futur utilisera le Task Runtime pour le checkpoint/reprise +et le Resource Governor pour l'admission. Le Model ne contient aucune +logique d'exécution. + +## Resource bounds + +- **Pas de plafond de longueur arbitraire** : le Model ne fixe pas de + maximum sur le nombre d'observations par Track. Un projet avec N images + peut produire des tracks de longueur jusqu'à N. +- **Lecture paginée** : `list_tracks` et `list_track_sets` utilisent une + page de 64 entrées avec curseur. +- **Chargement borné** : load track by id charge les observations du track ; + la taille est bornée naturellement par le nombre d'images dans le scope. +- **Pas de chargement complet du graphe** : aucune API ne charge tous les + tracks et toutes les observations d'un projet en une seule fois. +- **Pas de matrice dense** : aucune matrice de co-visibilité N×N n'est + matérialisée par le Model. + +## Corruption handling + +Le loader doit détecter : + +- track absent (`track_id` référencé mais inexistant) ; +- observation invalide (`feature_set_id` inexistant) ; +- duplicate observation dans un même track set ; +- deux observations de la même image dans un même track ; +- `feature_index` hors bornes du Feature Set ; +- `observation_count` incohérent avec le nombre réel d'observations ; +- `track_set_id` dans `track_observations` ne correspondant pas au + `track_set_id` du `track_id` parent ; +- `track_set` parent absent. + +Toute corruption retourne `CORRUPT` sans résultat partiel. + +## API + +L'API publique implémente : + +- `lardon3d_project_db_create_track_set()` — INSERT set + ses tracks + + observations dans une seule transaction `BEGIN IMMEDIATE`. +- `lardon3d_project_db_load_track_set()` — SELECT par ID. +- `lardon3d_project_db_find_track_set()` — SELECT par identité exacte. +- `lardon3d_project_db_list_track_sets()` — SELECT paginé ORDER BY id, + page 64. +- `lardon3d_project_db_load_track()` — SELECT par ID avec observations. +- `lardon3d_project_db_list_tracks()` — SELECT par set, paginé ORDER BY + id, page 64. +- `lardon3d_project_db_find_track_by_observation()` — recherche par + `(feature_set_id, feature_index)` dans un set donné. + +La création valide en C : existence des Feature Sets, bornes des +`feature_index`, unicité des observations, unicité image par track, +`observation_count` cohérent, `track_set_id` cohérent. L'INSERT est +transactionnel. + +## Explicitly out of scope + +- Track Builder algorithmique (union-find, connected components) ; +- triangulation ; +- coordonnées 3D ; +- Essential matrix ; +- camera pose ; +- bundle adjustment ; +- sparse reconstruction / Sparse SfM ; +- reprojection error ; +- dense reconstruction ; +- Track optimization ou merge ; +- mutation de tracks existants ; +- co-visibilité (matrice ou calcul) ; +- sélection par timestamp ou "latest". + +## Track rejected state + +Le Model v1 ne persiste pas d'état Track rejected. Le Model représente des +Tracks structurellement valides (≥ 2 observations, cohérents). Le futur +Track Builder décidera quels candidats publier. Les candidats non publiés +n'existent pas dans le Model. + +## Versioning + +Project DB schema version future décrira le stockage Track. `builder_version` +et `verifier_version` décrivent indépendamment les contrats scientifiques. +Changer un algorithme n'impose une migration DB que si la représentation +persistante change. diff --git a/docs/concepts/matching_and_tracks.md b/docs/concepts/matching_and_tracks.md index ced08bf..cc816ff 100644 --- a/docs/concepts/matching_and_tracks.md +++ b/docs/concepts/matching_and_tracks.md @@ -1,5 +1,14 @@ # Matching & Tracks +> **Document historique.** Ce document décrit la vision conceptuelle initiale +> du matching et des tracks. Le contrat persistant réel du Track Model v1 +> (schéma, invariants, API) est documenté dans +> [tracks.md](../architecture/tracks.md). Les différences notables : +> le Track Model v1 ne contient aucune coordonnée 3D, aucun statut +> (ACTIVE/OPTIMIZED/REJECTED), aucune matrice de co-visibilité et aucun +> plafond de longueur arbitraire. La triangulation, le Sparse SfM et le +> Bundle Adjustment sont des étapes ultérieures séparées. + > Frontière v1A : les groupes de support ORB/SIFT sont uniquement des preuves > locales intra-image. Ils ne comparent pas les espaces Hamming et L2, ne sont > pas des matches multivues et ne créent aucun track. Le futur matcher choisira diff --git a/include/lardon3d/project_db.h b/include/lardon3d/project_db.h index a41fbd8..d1580f6 100644 --- a/include/lardon3d/project_db.h +++ b/include/lardon3d/project_db.h @@ -9,7 +9,7 @@ #include enum { - LARDON3D_PROJECT_DB_SCHEMA_VERSION = 13, + LARDON3D_PROJECT_DB_SCHEMA_VERSION = 14, LARDON3D_PROJECT_DB_ID_CAPACITY = 65, LARDON3D_PROJECT_DB_KIND_CAPACITY = 65, LARDON3D_PROJECT_DB_PATH_CAPACITY = 4096, @@ -19,6 +19,7 @@ enum { LARDON3D_PROJECT_DB_CANDIDATE_PAIR_PAGE_MAX = 256, LARDON3D_PROJECT_DB_MATCH_RESULT_PAGE_MAX = 256, LARDON3D_PROJECT_DB_GEOMETRIC_RESULT_PAGE_MAX = 256, + LARDON3D_PROJECT_DB_TRACK_PAGE_MAX = 64, LARDON3D_PROJECT_DB_INLIER_MASK_MAX = 1024, LARDON3D_PROJECT_DB_FUNDAMENTAL_COEFFICIENTS = 9, LARDON3D_PROJECT_DB_SCANSET_NAME_CAPACITY = 256, @@ -347,6 +348,55 @@ typedef struct { bool exclude_same_asset; } Lardon3DProjectDbCandidatePairGenerateTask; +typedef struct { + uint64_t feature_set_id; + uint32_t feature_index; + uint32_t position_in_track; +} Lardon3DProjectDbTrackObservation; + +typedef struct { + uint64_t track_id; + uint64_t track_set_id; + uint32_t observation_count; + Lardon3DProjectDbTrackObservation *observations; +} Lardon3DProjectDbTrack; + +typedef struct { + uint64_t track_set_id; + char builder_kind[LARDON3D_PROJECT_DB_KIND_CAPACITY]; + uint32_t builder_version; + unsigned char parameter_fingerprint[LARDON3D_PROJECT_DB_SHA256_SIZE]; + int verifier_kind; + uint32_t verifier_version; + unsigned char verifier_fingerprint[LARDON3D_PROJECT_DB_SHA256_SIZE]; + unsigned char input_scope_hash[LARDON3D_PROJECT_DB_SHA256_SIZE]; + uint64_t gvr_count; + uint64_t track_count; + int64_t created_at; +} Lardon3DProjectDbTrackSet; + +void lardon3d_project_db_free_track(Lardon3DProjectDbTrack *track); +Lardon3DProjectDbResult lardon3d_project_db_create_track_set( + Lardon3DProjectDb *database, const Lardon3DProjectDbTrackSet *configuration, + const Lardon3DProjectDbTrack *tracks, size_t track_count, + Lardon3DProjectDbTrackSet *published); +Lardon3DProjectDbResult lardon3d_project_db_load_track_set( + Lardon3DProjectDb *database, uint64_t track_set_id, Lardon3DProjectDbTrackSet *track_set); +Lardon3DProjectDbResult lardon3d_project_db_find_track_set( + Lardon3DProjectDb *database, const Lardon3DProjectDbTrackSet *identity, + Lardon3DProjectDbTrackSet *track_set); +Lardon3DProjectDbResult lardon3d_project_db_list_track_sets( + Lardon3DProjectDb *database, uint64_t after_track_set_id, Lardon3DProjectDbTrackSet *track_sets, + size_t capacity, size_t *count); +Lardon3DProjectDbResult lardon3d_project_db_load_track( + Lardon3DProjectDb *database, uint64_t track_id, Lardon3DProjectDbTrack *track); +Lardon3DProjectDbResult lardon3d_project_db_list_tracks( + Lardon3DProjectDb *database, uint64_t track_set_id, uint64_t after_track_id, + Lardon3DProjectDbTrack *tracks, size_t capacity, size_t *count); +Lardon3DProjectDbResult lardon3d_project_db_find_track_by_observation( + Lardon3DProjectDb *database, uint64_t track_set_id, uint64_t feature_set_id, + uint32_t feature_index, Lardon3DProjectDbTrack *track); + Lardon3DProjectDbResult lardon3d_project_db_open(const char *path, Lardon3DProjectDb **database, char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]); void lardon3d_project_db_close(Lardon3DProjectDb *database); diff --git a/src/project_db.c b/src/project_db.c index 7c6d1e8..9f3d958 100644 --- a/src/project_db.c +++ b/src/project_db.c @@ -9,6 +9,7 @@ #include #include #include +#include #include @@ -263,6 +264,32 @@ static const char schema_geometric_verification_v12[] = "CREATE INDEX geometric_verification_results_parent_idx ON " "geometric_verification_results(match_result_id,geometric_verification_result_id);"; +static const char schema_track_v14[] = + "CREATE TABLE track_sets(track_set_id INTEGER PRIMARY KEY AUTOINCREMENT " + "CHECK(track_set_id>0),builder_kind TEXT NOT NULL CHECK(length(builder_kind)>0 AND " + "length(builder_kind)<=64),builder_version INTEGER NOT NULL CHECK(builder_version>0)," + "parameter_fingerprint BLOB NOT NULL CHECK(length(parameter_fingerprint)=32)," + "verifier_kind INTEGER NOT NULL CHECK(verifier_kind>0),verifier_version INTEGER NOT NULL " + "CHECK(verifier_version>0),verifier_fingerprint BLOB NOT NULL " + "CHECK(length(verifier_fingerprint)=32),input_scope_hash BLOB NOT NULL " + "CHECK(length(input_scope_hash)=32),gvr_count INTEGER NOT NULL CHECK(gvr_count>=1)," + "track_count INTEGER NOT NULL CHECK(track_count>=0),created_at INTEGER NOT NULL " + "CHECK(created_at>=0),UNIQUE(builder_kind,builder_version,parameter_fingerprint," + "verifier_kind,verifier_version,verifier_fingerprint,input_scope_hash));" + "CREATE TABLE tracks(track_id INTEGER PRIMARY KEY AUTOINCREMENT CHECK(track_id>0)," + "track_set_id INTEGER NOT NULL REFERENCES track_sets(track_set_id) ON DELETE CASCADE," + "observation_count INTEGER NOT NULL CHECK(observation_count>=2));" + "CREATE INDEX tracks_set_idx ON tracks(track_set_id,track_id);" + "CREATE TABLE track_observations(track_set_id INTEGER NOT NULL," + "track_id INTEGER NOT NULL REFERENCES tracks(track_id) ON DELETE CASCADE," + "feature_set_id INTEGER NOT NULL REFERENCES feature_sets(feature_set_id)," + "feature_index INTEGER NOT NULL CHECK(feature_index>=0)," + "position_in_track INTEGER NOT NULL CHECK(position_in_track>=0)," + "PRIMARY KEY(track_set_id,feature_set_id,feature_index)," + "UNIQUE(track_id,position_in_track));" + "CREATE INDEX track_observations_lookup_idx ON " + "track_observations(feature_set_id,feature_index,track_set_id);"; + static const char schema_geometric_verifier_task_v13[] = "CREATE TABLE geometric_verifier_tasks(" "task_id INTEGER PRIMARY KEY REFERENCES tasks(task_id) ON DELETE CASCADE," @@ -366,7 +393,7 @@ static Lardon3DProjectDbResult migrate(Lardon3DProjectDb *database, unsigned int if (from_version != 0 && from_version != 1 && from_version != 2 && from_version != 3 && from_version != 4 && from_version != 5 && from_version != 6 && from_version != 7 && from_version != 8 && from_version != 9 && from_version != 10 && from_version != 11 && - from_version != 12) { + from_version != 12 && from_version != 13) { return LARDON3D_PROJECT_DB_CORRUPT; } Lardon3DProjectDbResult result = execute(database, "BEGIN IMMEDIATE", "begin migration"); @@ -639,6 +666,21 @@ static Lardon3DProjectDbResult migrate(Lardon3DProjectDb *database, unsigned int "finish schema v13 migration"); } } + if (result == LARDON3D_PROJECT_DB_OK && from_version < 14) { + result = execute(database, schema_track_v14, "migrate schema v13 to v14"); +#ifdef LARDON3D_PROJECT_DB_TESTING + const char *forced_failure = getenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V14"); + if (result == LARDON3D_PROJECT_DB_OK && forced_failure && strcmp(forced_failure, "1") == 0) { + result = execute(database, "INSERT INTO missing_test_table VALUES(1)", + "forced migration v14 failure"); + } +#endif + if (result == LARDON3D_PROJECT_DB_OK) { + result = execute(database, + "UPDATE metadata SET value=14 WHERE key='schema_version' AND value=13", + "finish schema v14 migration"); + } + } if (result == LARDON3D_PROJECT_DB_OK) { result = execute(database, "COMMIT", "commit migration"); } @@ -755,7 +797,10 @@ Lardon3DProjectDbResult lardon3d_project_db_open(const char *path, Lardon3DProje "matcher_tasks", "match_results", "geometric_verification_results", - "geometric_verifier_tasks"}; + "geometric_verifier_tasks", + "track_sets", + "tracks", + "track_observations"}; for (size_t index = 0; index < sizeof(required) / sizeof(required[0]) && result == LARDON3D_PROJECT_DB_OK; ++index) { @@ -4306,6 +4351,593 @@ Lardon3DProjectDbResult lardon3d_project_db_list_geometric_verification_results( return db_result; } +/* Track persistence is deliberately kept as a small, self-contained API. The + * caller owns input arrays; loaded observation arrays are owned by the Track + * value until lardon3d_project_db_free_track(). */ +static bool valid_track_identity(const Lardon3DProjectDbTrackSet *set) { + return set && set->builder_kind[0] != '\0' && strlen(set->builder_kind) < 65 && + set->builder_version > 0 && set->verifier_kind > 0 && set->verifier_version > 0 && + set->gvr_count > 0 && + set->created_at >= 0; +} + +static Lardon3DProjectDbResult read_track_set(sqlite3_stmt *statement, + Lardon3DProjectDbTrackSet *set) { + sqlite3_int64 id = sqlite3_column_int64(statement, 0); + sqlite3_int64 builder_version = sqlite3_column_int64(statement, 2); + sqlite3_int64 verifier_kind = sqlite3_column_int64(statement, 4); + sqlite3_int64 verifier_version = sqlite3_column_int64(statement, 5); + sqlite3_int64 gvr_count = sqlite3_column_int64(statement, 8); + sqlite3_int64 track_count = sqlite3_column_int64(statement, 9); + const unsigned char *builder = sqlite3_column_text(statement, 1); + const void *parameter = sqlite3_column_blob(statement, 3); + const void *verifier = sqlite3_column_blob(statement, 6); + const void *scope = sqlite3_column_blob(statement, 7); + if (id <= 0 || builder_version <= 0 || verifier_kind <= 0 || verifier_version <= 0 || + gvr_count <= 0 || track_count < 0 || sqlite3_column_bytes(statement, 3) != 32 || + sqlite3_column_bytes(statement, 6) != 32 || sqlite3_column_bytes(statement, 7) != 32 || + !builder || !parameter || !verifier || !scope || strlen((const char *)builder) >= 65) { + return LARDON3D_PROJECT_DB_CORRUPT; + } + memset(set, 0, sizeof(*set)); + set->track_set_id = (uint64_t)id; + (void)snprintf(set->builder_kind, sizeof(set->builder_kind), "%s", (const char *)builder); + set->builder_version = (uint32_t)builder_version; + memcpy(set->parameter_fingerprint, parameter, 32); + set->verifier_kind = (int)verifier_kind; + set->verifier_version = (uint32_t)verifier_version; + memcpy(set->verifier_fingerprint, verifier, 32); + memcpy(set->input_scope_hash, scope, 32); + set->gvr_count = (uint64_t)gvr_count; + set->track_count = (uint64_t)track_count; + set->created_at = sqlite3_column_int64(statement, 10); + return set->created_at >= 0 ? LARDON3D_PROJECT_DB_OK : LARDON3D_PROJECT_DB_CORRUPT; +} + +void lardon3d_project_db_free_track(Lardon3DProjectDbTrack *track) { + if (!track) { + return; + } + free(track->observations); + memset(track, 0, sizeof(*track)); +} + +static void bind_track_identity(sqlite3_stmt *statement, + const Lardon3DProjectDbTrackSet *set, int first) { + (void)sqlite3_bind_text(statement, first, set->builder_kind, -1, SQLITE_TRANSIENT); + (void)sqlite3_bind_int64(statement, first + 1, set->builder_version); + (void)sqlite3_bind_blob(statement, first + 2, set->parameter_fingerprint, 32, SQLITE_TRANSIENT); + (void)sqlite3_bind_int(statement, first + 3, set->verifier_kind); + (void)sqlite3_bind_int64(statement, first + 4, set->verifier_version); + (void)sqlite3_bind_blob(statement, first + 5, set->verifier_fingerprint, 32, SQLITE_TRANSIENT); + (void)sqlite3_bind_blob(statement, first + 6, set->input_scope_hash, 32, SQLITE_TRANSIENT); +} + +static Lardon3DProjectDbResult read_track_set_by_statement( + Lardon3DProjectDb *database, sqlite3_stmt *statement, Lardon3DProjectDbTrackSet *set) { + int code = sqlite3_step(statement); + Lardon3DProjectDbResult result = code == SQLITE_ROW ? read_track_set(statement, set) + : LARDON3D_PROJECT_DB_NOT_FOUND; + if (code != SQLITE_ROW && code != SQLITE_DONE) { + result = sqlite_result(database, code, "read track set"); + } + (void)sqlite3_finalize(statement); + return result; +} + +Lardon3DProjectDbResult lardon3d_project_db_load_track_set( + Lardon3DProjectDb *database, uint64_t track_set_id, Lardon3DProjectDbTrackSet *track_set) { + if (!database || !valid_catalog_id(track_set_id) || !track_set) { + return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; + } + memset(track_set, 0, sizeof(*track_set)); + (void)pthread_mutex_lock(&database->mutex); + sqlite3_stmt *statement = NULL; + Lardon3DProjectDbResult result = prepare( + database, + "SELECT track_set_id,builder_kind,builder_version,parameter_fingerprint,verifier_kind," + "verifier_version,verifier_fingerprint,input_scope_hash,gvr_count,track_count,created_at " + "FROM track_sets WHERE track_set_id=?1", + &statement); + if (result == LARDON3D_PROJECT_DB_OK) { + (void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)track_set_id); + result = read_track_set_by_statement(database, statement, track_set); + } + (void)pthread_mutex_unlock(&database->mutex); + return result; +} + +Lardon3DProjectDbResult lardon3d_project_db_find_track_set( + Lardon3DProjectDb *database, const Lardon3DProjectDbTrackSet *identity, + Lardon3DProjectDbTrackSet *track_set) { + if (!database || !valid_track_identity(identity) || !track_set) { + return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; + } + memset(track_set, 0, sizeof(*track_set)); + (void)pthread_mutex_lock(&database->mutex); + sqlite3_stmt *statement = NULL; + Lardon3DProjectDbResult result = prepare( + database, + "SELECT track_set_id,builder_kind,builder_version,parameter_fingerprint,verifier_kind," + "verifier_version,verifier_fingerprint,input_scope_hash,gvr_count,track_count,created_at " + "FROM track_sets WHERE builder_kind=?1 AND builder_version=?2 AND parameter_fingerprint=?3 " + "AND verifier_kind=?4 AND verifier_version=?5 AND verifier_fingerprint=?6 " + "AND input_scope_hash=?7", + &statement); + if (result == LARDON3D_PROJECT_DB_OK) { + bind_track_identity(statement, identity, 1); + result = read_track_set_by_statement(database, statement, track_set); + } + (void)pthread_mutex_unlock(&database->mutex); + return result; +} + +Lardon3DProjectDbResult lardon3d_project_db_list_track_sets( + Lardon3DProjectDb *database, uint64_t after_track_set_id, Lardon3DProjectDbTrackSet *track_sets, + size_t capacity, size_t *count) { + if (!database || !track_sets || !count || capacity == 0 || + capacity > LARDON3D_PROJECT_DB_TRACK_PAGE_MAX) { + return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; + } + *count = 0; + (void)pthread_mutex_lock(&database->mutex); + sqlite3_stmt *statement = NULL; + Lardon3DProjectDbResult result = prepare( + database, + "SELECT track_set_id,builder_kind,builder_version,parameter_fingerprint,verifier_kind," + "verifier_version,verifier_fingerprint,input_scope_hash,gvr_count,track_count,created_at " + "FROM track_sets WHERE track_set_id>?1 ORDER BY track_set_id LIMIT ?2", + &statement); + if (result == LARDON3D_PROJECT_DB_OK) { + (void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)after_track_set_id); + (void)sqlite3_bind_int64(statement, 2, (sqlite3_int64)capacity); + int code; + while ((code = sqlite3_step(statement)) == SQLITE_ROW) { + result = read_track_set(statement, &track_sets[*count]); + if (result != LARDON3D_PROJECT_DB_OK) { + break; + } + ++*count; + } + if (result == LARDON3D_PROJECT_DB_OK && code == SQLITE_DONE) { + result = LARDON3D_PROJECT_DB_OK; + } else if (result == LARDON3D_PROJECT_DB_OK) { + result = sqlite_result(database, code, "list track sets"); + } + (void)sqlite3_finalize(statement); + } + (void)pthread_mutex_unlock(&database->mutex); + return result; +} + +static Lardon3DProjectDbResult validate_track_observations_locked( + Lardon3DProjectDb *database, const Lardon3DProjectDbTrack *tracks, size_t track_count) { + for (size_t track_index = 0; track_index < track_count; ++track_index) { + const Lardon3DProjectDbTrack *track = &tracks[track_index]; + if (!track->observations || track->observation_count < 2) { + return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; + } + for (uint32_t observation_index = 0; observation_index < track->observation_count; + ++observation_index) { + const Lardon3DProjectDbTrackObservation *observation = &track->observations[observation_index]; + if (observation->position_in_track != observation_index || observation->feature_set_id == 0) { + return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; + } + sqlite3_stmt *statement = NULL; + Lardon3DProjectDbResult result = prepare( + database, "SELECT image_id,feature_count FROM feature_sets WHERE feature_set_id=?1", + &statement); + if (result != LARDON3D_PROJECT_DB_OK) { + return result; + } + (void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)observation->feature_set_id); + int code = sqlite3_step(statement); + sqlite3_int64 image_id = code == SQLITE_ROW ? sqlite3_column_int64(statement, 0) : 0; + sqlite3_int64 feature_count = code == SQLITE_ROW ? sqlite3_column_int64(statement, 1) : 0; + (void)sqlite3_finalize(statement); + if (code != SQLITE_ROW) { + return code == SQLITE_DONE ? LARDON3D_PROJECT_DB_NOT_FOUND + : sqlite_result(database, code, "validate track feature set"); + } + if (image_id <= 0 || feature_count < 0 || (uint64_t)observation->feature_index >= + (uint64_t)feature_count) { + return LARDON3D_PROJECT_DB_CONSTRAINT; + } + for (uint32_t prior = 0; prior < observation_index; ++prior) { + const Lardon3DProjectDbTrackObservation *old = &track->observations[prior]; + if (old->feature_set_id == observation->feature_set_id && + old->feature_index == observation->feature_index) { + return LARDON3D_PROJECT_DB_CONSTRAINT; + } + sqlite3_stmt *image_statement = NULL; + result = prepare(database, "SELECT image_id FROM feature_sets WHERE feature_set_id=?1", + &image_statement); + if (result != LARDON3D_PROJECT_DB_OK) { + return result; + } + (void)sqlite3_bind_int64(image_statement, 1, (sqlite3_int64)old->feature_set_id); + code = sqlite3_step(image_statement); + sqlite3_int64 old_image = code == SQLITE_ROW ? sqlite3_column_int64(image_statement, 0) : 0; + (void)sqlite3_finalize(image_statement); + if (code != SQLITE_ROW) { + return LARDON3D_PROJECT_DB_CORRUPT; + } + if (old_image == image_id) { + return LARDON3D_PROJECT_DB_CONSTRAINT; + } + } + } + } + for (size_t first = 0; first < track_count; ++first) { + for (size_t second = first + 1; second < track_count; ++second) { + for (uint32_t a = 0; a < tracks[first].observation_count; ++a) { + for (uint32_t b = 0; b < tracks[second].observation_count; ++b) { + if (tracks[first].observations[a].feature_set_id == tracks[second].observations[b].feature_set_id && + tracks[first].observations[a].feature_index == tracks[second].observations[b].feature_index) { + return LARDON3D_PROJECT_DB_CONSTRAINT; + } + } + } + } + } + return LARDON3D_PROJECT_DB_OK; +} + +Lardon3DProjectDbResult lardon3d_project_db_create_track_set( + Lardon3DProjectDb *database, const Lardon3DProjectDbTrackSet *configuration, + const Lardon3DProjectDbTrack *tracks, size_t track_count, + Lardon3DProjectDbTrackSet *published) { + if (!database || !valid_track_identity(configuration) || !published || + (track_count > 0 && !tracks)) { + return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; + } + if (configuration->track_count != 0 && configuration->track_count != track_count) { + return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; + } + memset(published, 0, sizeof(*published)); + (void)pthread_mutex_lock(&database->mutex); + Lardon3DProjectDbResult result = execute(database, "BEGIN IMMEDIATE", "begin track set publish"); + int64_t publication_time = (int64_t)time(NULL); + if (result == LARDON3D_PROJECT_DB_OK && publication_time < 0) { + result = LARDON3D_PROJECT_DB_IO_ERROR; + } + bool reused = false; + sqlite3_stmt *statement = NULL; + if (result == LARDON3D_PROJECT_DB_OK) { + result = prepare(database, + "SELECT track_set_id,builder_kind,builder_version,parameter_fingerprint," + "verifier_kind,verifier_version,verifier_fingerprint,input_scope_hash,gvr_count," + "track_count,created_at FROM track_sets WHERE builder_kind=?1 AND " + "builder_version=?2 AND parameter_fingerprint=?3 AND verifier_kind=?4 AND " + "verifier_version=?5 AND verifier_fingerprint=?6 AND input_scope_hash=?7", + &statement); + if (result == LARDON3D_PROJECT_DB_OK) { + bind_track_identity(statement, configuration, 1); + int code = sqlite3_step(statement); + if (code == SQLITE_ROW) { + result = read_track_set(statement, published); + if (result == LARDON3D_PROJECT_DB_OK && + published->gvr_count != configuration->gvr_count) { + result = LARDON3D_PROJECT_DB_CONSTRAINT; + } + reused = result == LARDON3D_PROJECT_DB_OK; + } else if (code != SQLITE_DONE) { + result = sqlite_result(database, code, "find reusable track set"); + } else { + result = validate_track_observations_locked(database, tracks, track_count); + } + (void)sqlite3_finalize(statement); + statement = NULL; + } + } + if (result == LARDON3D_PROJECT_DB_OK && published->track_set_id == 0) { + result = prepare(database, + "INSERT INTO track_sets(builder_kind,builder_version,parameter_fingerprint," + "verifier_kind,verifier_version,verifier_fingerprint,input_scope_hash,gvr_count," + "track_count,created_at) VALUES(?1,?2,?3,?4,?5,?6,?7,?8,?9,?10)", + &statement); + if (result == LARDON3D_PROJECT_DB_OK) { + bind_track_identity(statement, configuration, 1); + (void)sqlite3_bind_int64(statement, 8, (sqlite3_int64)configuration->gvr_count); + (void)sqlite3_bind_int64(statement, 9, (sqlite3_int64)track_count); + (void)sqlite3_bind_int64(statement, 10, publication_time); + result = step_done(database, statement, "insert track set"); + statement = NULL; + if (result == LARDON3D_PROJECT_DB_OK) { + sqlite3_int64 id = sqlite3_last_insert_rowid(database->connection); + if (id <= 0) { + result = LARDON3D_PROJECT_DB_CORRUPT; + } else { + published->track_set_id = (uint64_t)id; + } + } + } + for (size_t index = 0; result == LARDON3D_PROJECT_DB_OK && index < track_count; ++index) { + const Lardon3DProjectDbTrack *track = &tracks[index]; + result = prepare(database, + "INSERT INTO tracks(track_set_id,observation_count) VALUES(?1,?2)", + &statement); + if (result != LARDON3D_PROJECT_DB_OK) { + break; + } + (void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)published->track_set_id); + (void)sqlite3_bind_int64(statement, 2, track->observation_count); + result = step_done(database, statement, "insert track"); + statement = NULL; + if (result != LARDON3D_PROJECT_DB_OK) { + break; + } + sqlite3_int64 track_id = sqlite3_last_insert_rowid(database->connection); + if (track_id <= 0) { + result = LARDON3D_PROJECT_DB_CORRUPT; + break; + } + for (uint32_t observation = 0; + result == LARDON3D_PROJECT_DB_OK && observation < track->observation_count; ++observation) { + const Lardon3DProjectDbTrackObservation *item = &track->observations[observation]; + result = prepare(database, + "INSERT INTO track_observations(track_set_id,track_id,feature_set_id," + "feature_index,position_in_track) VALUES(?1,?2,?3,?4,?5)", + &statement); + if (result != LARDON3D_PROJECT_DB_OK) { + break; + } + (void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)published->track_set_id); + (void)sqlite3_bind_int64(statement, 2, track_id); + (void)sqlite3_bind_int64(statement, 3, (sqlite3_int64)item->feature_set_id); + (void)sqlite3_bind_int64(statement, 4, item->feature_index); + (void)sqlite3_bind_int64(statement, 5, item->position_in_track); + result = step_done(database, statement, "insert track observation"); + statement = NULL; + } + } + } + if (result == LARDON3D_PROJECT_DB_OK) { + result = execute(database, "COMMIT", "commit track set publish"); + } else { + if (statement) { + (void)sqlite3_finalize(statement); + } + (void)execute(database, "ROLLBACK", "rollback track set publish"); + } + uint64_t published_id = published->track_set_id; + (void)pthread_mutex_unlock(&database->mutex); + if (result == LARDON3D_PROJECT_DB_OK && published_id != 0 && !reused) { + *published = *configuration; + published->track_set_id = published_id; + published->track_count = track_count; + published->created_at = publication_time; + } + return result; +} + +static Lardon3DProjectDbResult load_track_locked(Lardon3DProjectDb *database, uint64_t track_id, + Lardon3DProjectDbTrack *track) { + sqlite3_stmt *statement = NULL; + Lardon3DProjectDbResult result = prepare( + database, "SELECT track_id,track_set_id,observation_count FROM tracks WHERE track_id=?1", + &statement); + if (result != LARDON3D_PROJECT_DB_OK) { + return result; + } + (void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)track_id); + int code = sqlite3_step(statement); + if (code != SQLITE_ROW) { + result = code == SQLITE_DONE ? LARDON3D_PROJECT_DB_NOT_FOUND + : sqlite_result(database, code, "load track"); + (void)sqlite3_finalize(statement); + return result; + } + sqlite3_int64 set_id = sqlite3_column_int64(statement, 1); + sqlite3_int64 count = sqlite3_column_int64(statement, 2); + (void)sqlite3_finalize(statement); + if (set_id <= 0 || count < 2 || (uint64_t)count > SIZE_MAX / sizeof(*track->observations)) { + return LARDON3D_PROJECT_DB_CORRUPT; + } + sqlite3_stmt *set_statement = NULL; + result = prepare(database, "SELECT 1 FROM track_sets WHERE track_set_id=?1", &set_statement); + if (result != LARDON3D_PROJECT_DB_OK) { + return result; + } + (void)sqlite3_bind_int64(set_statement, 1, set_id); + int set_code = sqlite3_step(set_statement); + (void)sqlite3_finalize(set_statement); + if (set_code != SQLITE_ROW) { + return set_code == SQLITE_DONE ? LARDON3D_PROJECT_DB_CORRUPT + : sqlite_result(database, set_code, "load track set parent"); + } + track->observations = calloc((size_t)count, sizeof(*track->observations)); + if (!track->observations) { + return LARDON3D_PROJECT_DB_IO_ERROR; + } + result = prepare(database, + "SELECT feature_set_id,feature_index,position_in_track FROM track_observations " + "WHERE track_id=?1 AND track_set_id=?2 ORDER BY position_in_track", + &statement); + if (result == LARDON3D_PROJECT_DB_OK) { + (void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)track_id); + (void)sqlite3_bind_int64(statement, 2, set_id); + size_t index = 0; + while ((code = sqlite3_step(statement)) == SQLITE_ROW) { + if (index == (size_t)count || sqlite3_column_int64(statement, 2) != (sqlite3_int64)index) { + result = LARDON3D_PROJECT_DB_CORRUPT; + break; + } + sqlite3_int64 feature_set = sqlite3_column_int64(statement, 0); + sqlite3_int64 feature_index = sqlite3_column_int64(statement, 1); + if (feature_set <= 0 || feature_index < 0 || feature_index > UINT32_MAX) { + result = LARDON3D_PROJECT_DB_CORRUPT; + break; + } + sqlite3_stmt *feature_statement = NULL; + result = prepare(database, + "SELECT image_id,feature_count FROM feature_sets WHERE feature_set_id=?1", + &feature_statement); + if (result != LARDON3D_PROJECT_DB_OK) { + break; + } + (void)sqlite3_bind_int64(feature_statement, 1, feature_set); + int feature_code = sqlite3_step(feature_statement); + sqlite3_int64 image_id = feature_code == SQLITE_ROW ? sqlite3_column_int64(feature_statement, 0) : 0; + sqlite3_int64 feature_count = feature_code == SQLITE_ROW + ? sqlite3_column_int64(feature_statement, 1) + : -1; + (void)sqlite3_finalize(feature_statement); + if (feature_code != SQLITE_ROW || image_id <= 0 || feature_count < 0 || + (uint64_t)feature_index >= (uint64_t)feature_count) { + result = LARDON3D_PROJECT_DB_CORRUPT; + break; + } + for (size_t prior = 0; prior < index; ++prior) { + sqlite3_stmt *prior_statement = NULL; + result = prepare(database, "SELECT image_id FROM feature_sets WHERE feature_set_id=?1", + &prior_statement); + if (result != LARDON3D_PROJECT_DB_OK) { + break; + } + (void)sqlite3_bind_int64(prior_statement, 1, + (sqlite3_int64)track->observations[prior].feature_set_id); + int prior_code = sqlite3_step(prior_statement); + sqlite3_int64 prior_image = prior_code == SQLITE_ROW + ? sqlite3_column_int64(prior_statement, 0) + : 0; + (void)sqlite3_finalize(prior_statement); + if (prior_code != SQLITE_ROW || prior_image <= 0) { + result = LARDON3D_PROJECT_DB_CORRUPT; + break; + } + if (prior_image == image_id) { + result = LARDON3D_PROJECT_DB_CORRUPT; + break; + } + } + if (result != LARDON3D_PROJECT_DB_OK) { + break; + } + track->observations[index] = (Lardon3DProjectDbTrackObservation){ + .feature_set_id = (uint64_t)feature_set, + .feature_index = (uint32_t)feature_index, + .position_in_track = (uint32_t)index}; + ++index; + } + if (result == LARDON3D_PROJECT_DB_OK && code != SQLITE_DONE) { + result = sqlite_result(database, code, "read track observations"); + } + if (result == LARDON3D_PROJECT_DB_OK && index != (size_t)count) { + result = LARDON3D_PROJECT_DB_CORRUPT; + } + (void)sqlite3_finalize(statement); + } + if (result != LARDON3D_PROJECT_DB_OK) { + lardon3d_project_db_free_track(track); + return result; + } + track->track_id = track_id; + track->track_set_id = (uint64_t)set_id; + track->observation_count = (uint32_t)count; + return LARDON3D_PROJECT_DB_OK; +} + +Lardon3DProjectDbResult lardon3d_project_db_load_track( + Lardon3DProjectDb *database, uint64_t track_id, Lardon3DProjectDbTrack *track) { + if (!database || !valid_catalog_id(track_id) || !track) { + return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; + } + memset(track, 0, sizeof(*track)); + (void)pthread_mutex_lock(&database->mutex); + Lardon3DProjectDbResult result = load_track_locked(database, track_id, track); + (void)pthread_mutex_unlock(&database->mutex); + return result; +} + +Lardon3DProjectDbResult lardon3d_project_db_list_tracks( + Lardon3DProjectDb *database, uint64_t track_set_id, uint64_t after_track_id, + Lardon3DProjectDbTrack *tracks, size_t capacity, size_t *count) { + if (!database || !valid_catalog_id(track_set_id) || !tracks || !count || capacity == 0 || + capacity > LARDON3D_PROJECT_DB_TRACK_PAGE_MAX) { + return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; + } + *count = 0; + (void)pthread_mutex_lock(&database->mutex); + sqlite3_stmt *statement = NULL; + Lardon3DProjectDbResult result = prepare( + database, "SELECT track_id FROM tracks WHERE track_set_id=?1 AND track_id>?2 " + "ORDER BY track_id LIMIT ?3", &statement); + if (result == LARDON3D_PROJECT_DB_OK) { + (void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)track_set_id); + (void)sqlite3_bind_int64(statement, 2, (sqlite3_int64)after_track_id); + (void)sqlite3_bind_int64(statement, 3, (sqlite3_int64)capacity); + int code; + while ((code = sqlite3_step(statement)) == SQLITE_ROW) { + sqlite3_int64 id = sqlite3_column_int64(statement, 0); + if (id <= 0) { + result = LARDON3D_PROJECT_DB_CORRUPT; + break; + } + result = load_track_locked(database, (uint64_t)id, &tracks[*count]); + if (result != LARDON3D_PROJECT_DB_OK || tracks[*count].track_set_id != track_set_id) { + if (result == LARDON3D_PROJECT_DB_OK) { + lardon3d_project_db_free_track(&tracks[*count]); + result = LARDON3D_PROJECT_DB_CORRUPT; + } + break; + } + ++*count; + } + if (result == LARDON3D_PROJECT_DB_OK && code != SQLITE_DONE) { + result = sqlite_result(database, code, "list tracks"); + } + if (result != LARDON3D_PROJECT_DB_OK) { + for (size_t index = 0; index < *count; ++index) { + lardon3d_project_db_free_track(&tracks[index]); + } + *count = 0; + } + (void)sqlite3_finalize(statement); + } + (void)pthread_mutex_unlock(&database->mutex); + return result; +} + +Lardon3DProjectDbResult lardon3d_project_db_find_track_by_observation( + Lardon3DProjectDb *database, uint64_t track_set_id, uint64_t feature_set_id, + uint32_t feature_index, Lardon3DProjectDbTrack *track) { + if (!database || !valid_catalog_id(track_set_id) || !valid_catalog_id(feature_set_id) || !track) { + return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; + } + memset(track, 0, sizeof(*track)); + (void)pthread_mutex_lock(&database->mutex); + sqlite3_stmt *statement = NULL; + Lardon3DProjectDbResult result = prepare( + database, "SELECT track_id FROM track_observations WHERE track_set_id=?1 AND " + "feature_set_id=?2 AND feature_index=?3", &statement); + if (result == LARDON3D_PROJECT_DB_OK) { + (void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)track_set_id); + (void)sqlite3_bind_int64(statement, 2, (sqlite3_int64)feature_set_id); + (void)sqlite3_bind_int64(statement, 3, feature_index); + int code = sqlite3_step(statement); + if (code == SQLITE_ROW) { + sqlite3_int64 id = sqlite3_column_int64(statement, 0); + result = id > 0 ? load_track_locked(database, (uint64_t)id, track) + : LARDON3D_PROJECT_DB_CORRUPT; + if (result == LARDON3D_PROJECT_DB_NOT_FOUND) { + result = LARDON3D_PROJECT_DB_CORRUPT; + } + if (result == LARDON3D_PROJECT_DB_OK && track->track_set_id != track_set_id) { + lardon3d_project_db_free_track(track); + result = LARDON3D_PROJECT_DB_CORRUPT; + } + } else { + result = code == SQLITE_DONE ? LARDON3D_PROJECT_DB_NOT_FOUND + : sqlite_result(database, code, "find track observation"); + } + (void)sqlite3_finalize(statement); + } + (void)pthread_mutex_unlock(&database->mutex); + return result; +} + + #ifdef LARDON3D_PROJECT_DB_TESTING Lardon3DProjectDbResult lardon3d_project_db_test_delete_feature_identity( Lardon3DProjectDb *database, uint64_t feature_set_id, uint64_t feature_asset_id) { diff --git a/tests/test_geometric_verification.c b/tests/test_geometric_verification.c index 784ae43..08f1b80 100644 --- a/tests/test_geometric_verification.c +++ b/tests/test_geometric_verification.c @@ -157,9 +157,12 @@ static bool create_v11_database(const char *path) { lardon3d_project_db_close(database); return execute_sql(path, "PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;" - "DROP TABLE geometric_verifier_tasks;" - "DROP TABLE geometric_verification_results;" - "UPDATE metadata SET value=11 WHERE key='schema_version';" + "DROP TABLE geometric_verifier_tasks;" + "DROP TABLE geometric_verification_results;" + "DROP TABLE track_observations;" + "DROP TABLE tracks;" + "DROP TABLE track_sets;" + "UPDATE metadata SET value=11 WHERE key='schema_version';" "COMMIT;PRAGMA foreign_keys=ON;"); } @@ -167,7 +170,7 @@ static bool test_model_api(const char *path) { char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]; Lardon3DProjectDb *database = NULL; CHECK(lardon3d_project_db_open(path, &database, error) == LARDON3D_PROJECT_DB_OK); - CHECK(lardon3d_project_db_schema_version(database) == 13); + CHECK(lardon3d_project_db_schema_version(database) == 14); Parents parents; CHECK(create_parents(database, &parents)); @@ -402,7 +405,7 @@ static bool test_migration(const char *v11_path, const char *failed_path) { "name='geometric_verification_results'", 0)); CHECK(lardon3d_project_db_open(v11_path, &database, error) == LARDON3D_PROJECT_DB_OK); - CHECK(lardon3d_project_db_schema_version(database) == 13); + CHECK(lardon3d_project_db_schema_version(database) == 14); Parents parents; CHECK(create_parents(database, &parents)); unsigned char fingerprint[32] = {0x91}; @@ -414,7 +417,7 @@ static bool test_migration(const char *v11_path, const char *failed_path) { &migrated_result) == LARDON3D_PROJECT_DB_OK); uint64_t migrated_result_id = migrated_result.geometric_verification_result_id; lardon3d_project_db_close(database); - CHECK(query_integer(v11_path, "SELECT value FROM metadata WHERE key='schema_version'", 13)); + CHECK(query_integer(v11_path, "SELECT value FROM metadata WHERE key='schema_version'", 14)); CHECK(query_integer(v11_path, "SELECT count(*) FROM sqlite_master WHERE type='index' AND " "name='geometric_verification_results_parent_idx'", @@ -435,7 +438,7 @@ static bool test_migration(const char *v11_path, const char *failed_path) { "name='geometric_verification_results'", 0)); CHECK(lardon3d_project_db_open(failed_path, &database, error) == LARDON3D_PROJECT_DB_OK); - CHECK(lardon3d_project_db_schema_version(database) == 13); + CHECK(lardon3d_project_db_schema_version(database) == 14); lardon3d_project_db_close(database); return true; } diff --git a/tests/test_geometric_verifier_task.c b/tests/test_geometric_verifier_task.c index a09bdf5..54c7e07 100644 --- a/tests/test_geometric_verifier_task.c +++ b/tests/test_geometric_verifier_task.c @@ -331,9 +331,12 @@ static bool run_task_test(void) { lardon3d_project_db_close(state.project_db); CHECK(exec_sql( database_path, - "PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;" - "DROP TABLE geometric_verifier_tasks;" - "UPDATE metadata SET value=12 WHERE key='schema_version';COMMIT;")); + "PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;" + "DROP TABLE geometric_verifier_tasks;" + "DROP TABLE track_observations;" + "DROP TABLE tracks;" + "DROP TABLE track_sets;" + "UPDATE metadata SET value=12 WHERE key='schema_version';COMMIT;")); CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V13", "1", 1) == 0); Lardon3DProjectDb *database = NULL; CHECK(lardon3d_project_db_open(database_path, &database, error) == @@ -349,7 +352,7 @@ static bool run_task_test(void) { 0)); CHECK(lardon3d_project_db_open(database_path, &database, error) == LARDON3D_PROJECT_DB_OK); - CHECK(lardon3d_project_db_schema_version(database) == 13); + CHECK(lardon3d_project_db_schema_version(database) == 14); lardon3d_project_db_close(database); CHECK(remove_tree(root)); return true; diff --git a/tests/test_match_result.c b/tests/test_match_result.c index 3fafeae..bb5a18a 100644 --- a/tests/test_match_result.c +++ b/tests/test_match_result.c @@ -52,6 +52,9 @@ static bool create_v9_database(const char *path) { "DROP TABLE geometric_verification_results;" "DROP TABLE matcher_tasks;" "DROP TABLE match_results;" + "DROP TABLE track_observations;" + "DROP TABLE tracks;" + "DROP TABLE track_sets;" "UPDATE metadata SET value=9 WHERE key='schema_version';COMMIT;PRAGMA foreign_keys=ON;"; bool ok = sqlite3_exec(connection, sql, NULL, NULL, NULL) == SQLITE_OK; return sqlite3_close(connection) == SQLITE_OK && ok; @@ -95,7 +98,7 @@ static bool run_test(void) { char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]; Lardon3DProjectDb *database = NULL; CHECK(lardon3d_project_db_open(database_path, &database, error) == LARDON3D_PROJECT_DB_OK); - CHECK(database && lardon3d_project_db_schema_version(database) == 13); + CHECK(database && lardon3d_project_db_schema_version(database) == 14); Lardon3DProjectDbScanSet scanset; CHECK(lardon3d_project_db_create_scanset(database, "Match-test", &scanset) == @@ -454,7 +457,7 @@ static bool run_test(void) { database = NULL; CHECK(lardon3d_project_db_open(database_path, &database, error) == LARDON3D_PROJECT_DB_OK); - CHECK(lardon3d_project_db_schema_version(database) == 13); + CHECK(lardon3d_project_db_schema_version(database) == 14); /* Verify persistence: load previously created results */ CHECK(lardon3d_project_db_load_match_result(database, first_id, &loaded) == @@ -494,10 +497,10 @@ static bool run_test(void) { CHECK(create_v9_database(v9_path)); CHECK(query_integer(v9_path, "SELECT value FROM metadata WHERE key='schema_version'", 9)); CHECK(lardon3d_project_db_open(v9_path, &database, error) == LARDON3D_PROJECT_DB_OK); - CHECK(lardon3d_project_db_schema_version(database) == 13); + CHECK(lardon3d_project_db_schema_version(database) == 14); lardon3d_project_db_close(database); database = NULL; - CHECK(query_integer(v9_path, "SELECT value FROM metadata WHERE key='schema_version'", 13)); + CHECK(query_integer(v9_path, "SELECT value FROM metadata WHERE key='schema_version'", 14)); CHECK(query_integer(v9_path, "SELECT count(*) FROM sqlite_master WHERE type='table' AND " "name='match_results'", 1)); @@ -516,7 +519,7 @@ static bool run_test(void) { "SELECT count(*) FROM sqlite_master WHERE type='table' AND " "name='matcher_tasks'", 0)); CHECK(lardon3d_project_db_open(failed_v10_path, &database, error) == LARDON3D_PROJECT_DB_OK && - lardon3d_project_db_schema_version(database) == 13); + lardon3d_project_db_schema_version(database) == 14); lardon3d_project_db_close(database); database = NULL; diff --git a/tests/test_matcher_task.c b/tests/test_matcher_task.c index 92e8c35..ea744aa 100644 --- a/tests/test_matcher_task.c +++ b/tests/test_matcher_task.c @@ -92,6 +92,9 @@ static bool downgrade_project_to_historical_v10(const char *database_path) { "DROP TABLE geometric_verifier_tasks;" "DROP TABLE geometric_verification_results;" "DROP TABLE matcher_tasks;" + "DROP TABLE track_observations;" + "DROP TABLE tracks;" + "DROP TABLE track_sets;" "UPDATE metadata SET value=10 WHERE key='schema_version';" "COMMIT;PRAGMA foreign_keys=ON;"; bool success = sqlite3_exec(connection, sql, NULL, NULL, NULL) == SQLITE_OK; @@ -352,7 +355,7 @@ static bool run_test(void) { "name='matcher_tasks'", 0)); CHECK(reopen_runtime(&fixture)); - CHECK(lardon3d_project_db_schema_version(fixture.state.project_db) == 13); + CHECK(lardon3d_project_db_schema_version(fixture.state.project_db) == 14); CHECK(query_integer(database_path, "SELECT count(*) FROM sqlite_master WHERE type='table' AND " "name='matcher_tasks'", diff --git a/tests/test_project_db.c b/tests/test_project_db.c index e4a7bf3..d9a39fa 100644 --- a/tests/test_project_db.c +++ b/tests/test_project_db.c @@ -37,6 +37,20 @@ static void asset_path_for_hash(const unsigned char hash[LARDON3D_PROJECT_DB_SHA hex); } +static void feature_asset_path_for_hash( + const unsigned char hash[LARDON3D_PROJECT_DB_SHA256_SIZE], + char path[LARDON3D_PROJECT_DB_PATH_CAPACITY]) { + static const char digits[] = "0123456789abcdef"; + char hex[65]; + for (size_t index = 0; index < LARDON3D_PROJECT_DB_SHA256_SIZE; ++index) { + hex[index * 2] = digits[hash[index] >> 4]; + hex[index * 2 + 1] = digits[hash[index] & 15U]; + } + hex[64] = '\0'; + (void)snprintf(path, LARDON3D_PROJECT_DB_PATH_CAPACITY, "assets/features/%c%c/%s", hex[0], + hex[1], hex); +} + static Lardon3DTaskDurableSnapshot task_snapshot(uint64_t id, Lardon3DTaskState saved) { Lardon3DTaskDurableSnapshot snapshot = { .id = id, @@ -89,7 +103,7 @@ static bool create_future_database(const char *path) { } bool ok = sqlite3_exec(connection, "CREATE TABLE metadata(key TEXT PRIMARY KEY,value INTEGER NOT NULL);" - "INSERT INTO metadata VALUES('schema_version',14);", + "INSERT INTO metadata VALUES('schema_version',15);", NULL, NULL, NULL) == SQLITE_OK; return sqlite3_close(connection) == SQLITE_OK && ok; } @@ -109,6 +123,9 @@ static bool create_v7_database(const char *path) { "DROP TABLE match_results;" "DROP TABLE candidate_pair_generate_tasks;" "DROP TABLE candidate_pairs;" + "DROP TABLE track_observations;" + "DROP TABLE tracks;" + "DROP TABLE track_sets;" "UPDATE metadata SET value=7 WHERE key='schema_version';COMMIT;PRAGMA foreign_keys=ON;"; bool ok = sqlite3_exec(connection, sql, NULL, NULL, NULL) == SQLITE_OK; return sqlite3_close(connection) == SQLITE_OK && ok; @@ -135,6 +152,9 @@ static bool create_v6_database(const char *path) { "ALTER TABLE feature_sets DROP COLUMN coverage_ratio;" "ALTER TABLE feature_sets DROP COLUMN total_cells;" "ALTER TABLE feature_sets DROP COLUMN occupied_cells;" + "DROP TABLE track_observations;" + "DROP TABLE tracks;" + "DROP TABLE track_sets;" "UPDATE metadata SET value=6 WHERE key='schema_version';COMMIT;PRAGMA foreign_keys=ON;"; bool ok = sqlite3_exec(connection, sql, NULL, NULL, NULL) == SQLITE_OK; return sqlite3_close(connection) == SQLITE_OK && ok; @@ -156,12 +176,32 @@ static bool create_v10_database(const char *path) { "DROP TABLE geometric_verifier_tasks;" "DROP TABLE geometric_verification_results;" "DROP TABLE matcher_tasks;" + "DROP TABLE track_observations;" + "DROP TABLE tracks;" + "DROP TABLE track_sets;" "UPDATE metadata SET value=10 WHERE key='schema_version';" "COMMIT;PRAGMA foreign_keys=ON;"; bool ok = sqlite3_exec(connection, sql, NULL, NULL, NULL) == SQLITE_OK; return sqlite3_close(connection) == SQLITE_OK && ok; } +static bool create_v13_database(const char *path) { + Lardon3DProjectDb *database = NULL; + char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]; + if (lardon3d_project_db_open(path, &database, error) != LARDON3D_PROJECT_DB_OK) return false; + lardon3d_project_db_close(database); + sqlite3 *connection = NULL; + if (sqlite3_open(path, &connection) != SQLITE_OK) return false; + static const char sql[] = + "PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;" + "DROP TABLE track_observations;" + "DROP TABLE tracks;" + "DROP TABLE track_sets;" + "UPDATE metadata SET value=13 WHERE key='schema_version';COMMIT;PRAGMA foreign_keys=ON;"; + bool ok = sqlite3_exec(connection, sql, NULL, NULL, NULL) == SQLITE_OK; + return sqlite3_close(connection) == SQLITE_OK && ok; +} + static bool create_v5_database(const char *path) { if (!create_v6_database(path)) return false; sqlite3 *connection = NULL; @@ -302,6 +342,24 @@ static bool query_integer(const char *path, const char *sql, sqlite3_int64 expec matches; } +static void init_track_configuration(Lardon3DProjectDbTrackSet *configuration, + unsigned char scope_byte, uint64_t gvr_count, + size_t track_count) { + memset(configuration, 0, sizeof(*configuration)); + (void)snprintf(configuration->builder_kind, sizeof(configuration->builder_kind), + "gate-c-track-builder"); + configuration->builder_version = 1; + configuration->parameter_fingerprint[0] = 0x10; + configuration->verifier_kind = 1; + configuration->verifier_version = 1; + configuration->verifier_fingerprint[0] = 0x20; + memset(configuration->input_scope_hash, scope_byte, + sizeof(configuration->input_scope_hash)); + configuration->gvr_count = gvr_count; + configuration->track_count = track_count; + configuration->created_at = 1000; +} + static bool run_test(void) { char directory[] = "/tmp/lardon3d-project-db-XXXXXX"; CHECK(mkdtemp(directory)); @@ -311,6 +369,7 @@ static bool run_test(void) { char v4_path[512], failed_v5_path[512], failed_v6_path[512], failed_v7_path[512]; char direct_v5_path[512], v8_path[512], failed_v8_path[512]; char v10_path[512], failed_v11_path[512]; + char v13_path[512], failed_v14_path[512]; CHECK(snprintf(database_path, sizeof(database_path), "%s/project.db", directory) > 0); CHECK(snprintf(artifact_path, sizeof(artifact_path), "%s/artifact.bin", directory) > 0); CHECK(snprintf(future_path, sizeof(future_path), "%s/future.db", directory) > 0); @@ -337,11 +396,13 @@ static bool run_test(void) { 0); CHECK(snprintf(v10_path, sizeof(v10_path), "%s/v10.db", directory) > 0); CHECK(snprintf(failed_v11_path, sizeof(failed_v11_path), "%s/failed-v11.db", directory) > 0); + CHECK(snprintf(v13_path, sizeof(v13_path), "%s/v13.db", directory) > 0); + CHECK(snprintf(failed_v14_path, sizeof(failed_v14_path), "%s/failed-v14.db", directory) > 0); char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]; Lardon3DProjectDb *database = NULL; CHECK(lardon3d_project_db_open(database_path, &database, error) == LARDON3D_PROJECT_DB_OK); - CHECK(database && lardon3d_project_db_schema_version(database) == 13); + CHECK(database && lardon3d_project_db_schema_version(database) == 14); bool legacy_pending = true; CHECK(lardon3d_project_db_legacy_catalog_pending(database, &legacy_pending) == LARDON3D_PROJECT_DB_OK && @@ -390,10 +451,9 @@ static bool run_test(void) { asset_path_for_hash(fourth_hash, fourth_asset_path); Lardon3DProjectDbImage pair_image_b; CHECK(lardon3d_project_db_register_image(database, replacement_scanset.scanset_id, fourth_hash, - fourth_asset_path, 1, "pair-b.jpg", "/source/pair-b.jpg", - 0, 4, &identity_status, &pair_image_b) == - LARDON3D_PROJECT_DB_OK); - + fourth_asset_path, 1, "pair-b.jpg", "/source/pair-b.jpg", + 0, 4, &identity_status, &pair_image_b) == + LARDON3D_PROJECT_DB_OK); Lardon3DProjectDbCandidatePair pair; CHECK(lardon3d_project_db_create_candidate_pair(database, replacement_image.image_id, pair_image.image_id, 10, &pair) == @@ -608,7 +668,7 @@ static bool run_test(void) { LARDON3D_PROJECT_DB_INVALID_ARGUMENT); lardon3d_project_db_close(contexts[0].database); database = NULL; - CHECK(query_integer(database_path, "SELECT value FROM metadata WHERE key='schema_version'", 13)); + CHECK(query_integer(database_path, "SELECT value FROM metadata WHERE key='schema_version'", 14)); CHECK(query_integer(database_path, "SELECT count(*) FROM tasks WHERE task_id=1", 1)); CHECK(lardon3d_project_db_open(database_path, &database, error) == LARDON3D_PROJECT_DB_OK); CHECK(lardon3d_project_db_load_task(database, 1, &task) == LARDON3D_PROJECT_DB_OK); @@ -630,7 +690,7 @@ static bool run_test(void) { CHECK(create_v1_database(legacy_path)); CHECK(lardon3d_project_db_open(legacy_path, &database, error) == LARDON3D_PROJECT_DB_OK); - CHECK(lardon3d_project_db_schema_version(database) == 13); + CHECK(lardon3d_project_db_schema_version(database) == 14); CHECK(lardon3d_project_db_get_project(database, &loaded_project) == LARDON3D_PROJECT_DB_OK && strcmp(loaded_project.stable_id, "legacy-project") == 0); CHECK(lardon3d_project_db_load_task(database, 9, &task) == LARDON3D_PROJECT_DB_OK); @@ -640,7 +700,7 @@ static bool run_test(void) { LARDON3D_PROJECT_DB_OK); lardon3d_project_db_close(database); database = NULL; - CHECK(query_integer(legacy_path, "SELECT value FROM metadata WHERE key='schema_version'", 13)); + CHECK(query_integer(legacy_path, "SELECT value FROM metadata WHERE key='schema_version'", 14)); CHECK(create_v1_database(failed_migration_path)); CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V2", "1", 1) == 0); @@ -665,7 +725,7 @@ static bool run_test(void) { LARDON3D_PROJECT_DB_OK); lardon3d_project_db_close(database); database = NULL; - CHECK(query_integer(v2_path, "SELECT value FROM metadata WHERE key='schema_version'", 13)); + CHECK(query_integer(v2_path, "SELECT value FROM metadata WHERE key='schema_version'", 14)); CHECK(create_v2_database(failed_v3_migration_path)); CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V3", "1", 1) == 0); @@ -693,7 +753,7 @@ static bool run_test(void) { LARDON3D_PROJECT_DB_OK); lardon3d_project_db_close(database); database = NULL; - CHECK(query_integer(v3_path, "SELECT value FROM metadata WHERE key='schema_version'", 13)); + CHECK(query_integer(v3_path, "SELECT value FROM metadata WHERE key='schema_version'", 14)); CHECK(create_v3_database(failed_v4_path)); CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V4", "1", 1) == 0); @@ -710,7 +770,7 @@ static bool run_test(void) { fprintf(stderr, "Migration v4 (%d): %s\n", (int)v4_result, error); } CHECK(v4_result == LARDON3D_PROJECT_DB_OK); - CHECK(lardon3d_project_db_schema_version(database) == 13); + CHECK(lardon3d_project_db_schema_version(database) == 14); CHECK(lardon3d_project_db_load_task(database, 9, &task) == LARDON3D_PROJECT_DB_OK); CHECK(lardon3d_project_db_load_artifact(database, "legacy-artifact", &loaded_artifact) == LARDON3D_PROJECT_DB_OK); @@ -749,24 +809,24 @@ static bool run_test(void) { fprintf(stderr, "Nouvelle tentative migration v7 (%d): %s\n", (int)retry_v7, error); } CHECK(retry_v7 == LARDON3D_PROJECT_DB_OK && - lardon3d_project_db_schema_version(database) == 13); + lardon3d_project_db_schema_version(database) == 14); lardon3d_project_db_close(database); database = NULL; CHECK(create_v5_database(direct_v5_path)); CHECK(query_integer(direct_v5_path, "SELECT value FROM metadata WHERE key='schema_version'", 5)); CHECK(lardon3d_project_db_open(direct_v5_path, &database, error) == LARDON3D_PROJECT_DB_OK && - lardon3d_project_db_schema_version(database) == 13); + lardon3d_project_db_schema_version(database) == 14); lardon3d_project_db_close(database); database = NULL; CHECK(create_v7_database(v8_path)); CHECK(query_integer(v8_path, "SELECT value FROM metadata WHERE key='schema_version'", 7)); CHECK(lardon3d_project_db_open(v8_path, &database, error) == LARDON3D_PROJECT_DB_OK); - CHECK(lardon3d_project_db_schema_version(database) == 13); + CHECK(lardon3d_project_db_schema_version(database) == 14); lardon3d_project_db_close(database); database = NULL; - CHECK(query_integer(v8_path, "SELECT value FROM metadata WHERE key='schema_version'", 13)); + CHECK(query_integer(v8_path, "SELECT value FROM metadata WHERE key='schema_version'", 14)); CHECK(create_v7_database(failed_v8_path)); CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V8", "1", 1) == 0); @@ -777,7 +837,7 @@ static bool run_test(void) { "SELECT count(*) FROM sqlite_master WHERE type='table' AND " "name='candidate_pairs'", 0)); CHECK(lardon3d_project_db_open(failed_v8_path, &database, error) == LARDON3D_PROJECT_DB_OK && - lardon3d_project_db_schema_version(database) == 13); + lardon3d_project_db_schema_version(database) == 14); lardon3d_project_db_close(database); database = NULL; @@ -792,10 +852,10 @@ static bool run_test(void) { "name='matcher_tasks'", 0)); CHECK(lardon3d_project_db_open(v10_path, &database, error) == LARDON3D_PROJECT_DB_OK && - lardon3d_project_db_schema_version(database) == 13); + lardon3d_project_db_schema_version(database) == 14); lardon3d_project_db_close(database); database = NULL; - CHECK(query_integer(v10_path, "SELECT value FROM metadata WHERE key='schema_version'", 13)); + CHECK(query_integer(v10_path, "SELECT value FROM metadata WHERE key='schema_version'", 14)); CHECK(query_integer(v10_path, "SELECT count(*) FROM sqlite_master WHERE type='table' AND " "name='matcher_tasks'", @@ -818,10 +878,420 @@ static bool run_test(void) { 1)); CHECK(lardon3d_project_db_open(failed_v11_path, &database, error) == LARDON3D_PROJECT_DB_OK); - CHECK(lardon3d_project_db_schema_version(database) == 13); + CHECK(lardon3d_project_db_schema_version(database) == 14); lardon3d_project_db_close(database); database = NULL; + CHECK(create_v13_database(v13_path)); + CHECK(query_integer(v13_path, + "SELECT value FROM metadata WHERE key='schema_version'", 13)); + CHECK(query_integer(v13_path, + "SELECT count(*) FROM sqlite_master WHERE type='table' AND " + "name='track_sets'", 0)); + CHECK(lardon3d_project_db_open(v13_path, &database, error) == LARDON3D_PROJECT_DB_OK && + lardon3d_project_db_schema_version(database) == 14); + lardon3d_project_db_close(database); + database = NULL; + CHECK(query_integer(v13_path, + "SELECT value FROM metadata WHERE key='schema_version'", 14)); + CHECK(query_integer(v13_path, + "SELECT count(*) FROM sqlite_master WHERE type='table' AND " + "name='track_sets'", 1)); + + CHECK(create_v13_database(failed_v14_path)); + CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V14", "1", 1) == 0); + CHECK(lardon3d_project_db_open(failed_v14_path, &database, error) == + LARDON3D_PROJECT_DB_IO_ERROR); + CHECK(unsetenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V14") == 0); + CHECK(query_integer(failed_v14_path, + "SELECT value FROM metadata WHERE key='schema_version'", 13)); + CHECK(query_integer(failed_v14_path, + "SELECT count(*) FROM sqlite_master WHERE type='table' AND " + "name='track_sets'", 0)); + CHECK(lardon3d_project_db_open(failed_v14_path, &database, error) == + LARDON3D_PROJECT_DB_OK && + lardon3d_project_db_schema_version(database) == 14); + lardon3d_project_db_close(database); + database = NULL; + + /* Gate C: exercise every public Track API through the Project DB contract. */ + char track_api_path[512]; + CHECK(snprintf(track_api_path, sizeof(track_api_path), "%s/track-api.db", directory) > 0); + Lardon3DProjectDb *track_database = NULL; + CHECK(lardon3d_project_db_open(track_api_path, &track_database, error) == + LARDON3D_PROJECT_DB_OK); + Lardon3DProjectDbScanSet track_scanset; + CHECK(lardon3d_project_db_create_scanset(track_database, "Track API", &track_scanset) == + LARDON3D_PROJECT_DB_OK); + unsigned char track_image_hash_a[32] = {31}; + unsigned char track_image_hash_b[32] = {32}; + char track_image_path_a[LARDON3D_PROJECT_DB_PATH_CAPACITY]; + char track_image_path_b[LARDON3D_PROJECT_DB_PATH_CAPACITY]; + asset_path_for_hash(track_image_hash_a, track_image_path_a); + asset_path_for_hash(track_image_hash_b, track_image_path_b); + Lardon3DProjectDbImageRegisterStatus track_image_status; + Lardon3DProjectDbImage track_image_a, track_image_b; + CHECK(lardon3d_project_db_register_image( + track_database, track_scanset.scanset_id, track_image_hash_a, track_image_path_a, 1, + "track-a.jpg", "/source/track-a.jpg", 0, 1, &track_image_status, &track_image_a) == + LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_register_image( + track_database, track_scanset.scanset_id, track_image_hash_b, track_image_path_b, 1, + "track-b.jpg", "/source/track-b.jpg", 0, 2, &track_image_status, &track_image_b) == + LARDON3D_PROJECT_DB_OK); + + unsigned char feature_parameter[32] = {41}; + unsigned char source_hash_a[32]; + unsigned char source_hash_b[32]; + unsigned char feature_asset_hash_a[32] = {44}; + unsigned char feature_asset_hash_b[32] = {45}; + char feature_asset_path_a[LARDON3D_PROJECT_DB_PATH_CAPACITY]; + char feature_asset_path_b[LARDON3D_PROJECT_DB_PATH_CAPACITY]; + memcpy(source_hash_a, track_image_hash_a, sizeof(source_hash_a)); + memcpy(source_hash_b, track_image_hash_b, sizeof(source_hash_b)); + feature_asset_path_for_hash(feature_asset_hash_a, feature_asset_path_a); + feature_asset_path_for_hash(feature_asset_hash_b, feature_asset_path_b); + Lardon3DProjectDbFeatureSet track_features_a, track_features_b; + CHECK(lardon3d_project_db_register_feature_set( + track_database, track_image_a.image_id, "orb", 1, feature_parameter, source_hash_a, + 128, 1, 32, feature_asset_hash_a, feature_asset_path_a, 128, + LARDON3D_DB_FEATURE_ASSET_DURABLE, 0, 10, &track_features_a) == + LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_register_feature_set( + track_database, track_image_b.image_id, "orb", 1, feature_parameter, source_hash_b, + 128, 1, 32, feature_asset_hash_b, feature_asset_path_b, 128, + LARDON3D_DB_FEATURE_ASSET_DURABLE, 0, 11, &track_features_b) == + LARDON3D_PROJECT_DB_OK); + + enum { gate_track_count = 65 }; + Lardon3DProjectDbTrack tracks[gate_track_count]; + Lardon3DProjectDbTrackObservation observations[gate_track_count][2]; + memset(tracks, 0, sizeof(tracks)); + for (size_t index = 0; index < gate_track_count; ++index) { + observations[index][0] = (Lardon3DProjectDbTrackObservation){ + .feature_set_id = track_features_a.feature_set_id, .feature_index = (uint32_t)index, + .position_in_track = 0}; + observations[index][1] = (Lardon3DProjectDbTrackObservation){ + .feature_set_id = track_features_b.feature_set_id, .feature_index = (uint32_t)index, + .position_in_track = 1}; + tracks[index] = (Lardon3DProjectDbTrack){.observation_count = 2, + .observations = observations[index]}; + } + Lardon3DProjectDbTrackSet track_configuration; + init_track_configuration(&track_configuration, 0x51, 1, gate_track_count); + Lardon3DProjectDbTrackSet published_tracks; + CHECK(lardon3d_project_db_create_track_set(track_database, &track_configuration, tracks, + gate_track_count, &published_tracks) == + LARDON3D_PROJECT_DB_OK); + CHECK(published_tracks.track_set_id > 0 && published_tracks.track_count == gate_track_count && + published_tracks.gvr_count == 1 && published_tracks.created_at > 1000000000); + Lardon3DProjectDbTrackSet loaded_track_set, found_track_set; + CHECK(lardon3d_project_db_load_track_set(track_database, published_tracks.track_set_id, + &loaded_track_set) == LARDON3D_PROJECT_DB_OK); + CHECK(loaded_track_set.track_set_id == published_tracks.track_set_id); + CHECK(lardon3d_project_db_find_track_set(track_database, &track_configuration, + &found_track_set) == LARDON3D_PROJECT_DB_OK); + CHECK(found_track_set.track_set_id == published_tracks.track_set_id); + Lardon3DProjectDbTrackSet track_sets_page[64]; + size_t track_set_count = 0; + CHECK(lardon3d_project_db_list_track_sets(track_database, 0, track_sets_page, 64, + &track_set_count) == LARDON3D_PROJECT_DB_OK); + CHECK(track_set_count == 1 && track_sets_page[0].track_set_id == published_tracks.track_set_id); + + Lardon3DProjectDbTrack loaded_track, found_track; + CHECK(lardon3d_project_db_list_tracks(track_database, published_tracks.track_set_id, 0, tracks, + 64, &track_set_count) == LARDON3D_PROJECT_DB_OK); + CHECK(track_set_count == 64 && tracks[0].track_id > 0); + uint64_t first_track_id = tracks[0].track_id; + uint64_t last_first_page_track_id = tracks[track_set_count - 1].track_id; + for (size_t index = 0; index < track_set_count; ++index) { + lardon3d_project_db_free_track(&tracks[index]); + } + CHECK(lardon3d_project_db_list_tracks(track_database, published_tracks.track_set_id, + last_first_page_track_id, tracks, 64, &track_set_count) == + LARDON3D_PROJECT_DB_OK); + CHECK(track_set_count == 1); + uint64_t second_track_id = tracks[0].track_id; + CHECK(lardon3d_project_db_load_track(track_database, second_track_id, &loaded_track) == + LARDON3D_PROJECT_DB_OK); + CHECK(loaded_track.observation_count == 2); + lardon3d_project_db_free_track(&loaded_track); + CHECK(lardon3d_project_db_find_track_by_observation( + track_database, published_tracks.track_set_id, track_features_a.feature_set_id, 1, + &found_track) == LARDON3D_PROJECT_DB_OK); + CHECK(found_track.track_id > 0 && found_track.track_set_id == published_tracks.track_set_id); + lardon3d_project_db_free_track(&found_track); + lardon3d_project_db_free_track(&tracks[0]); + + Lardon3DProjectDbTrackSet reused_tracks; + Lardon3DProjectDbTrackSet reuse_configuration = track_configuration; + reuse_configuration.track_count = 0; + CHECK(lardon3d_project_db_create_track_set(track_database, &reuse_configuration, NULL, 0, + &reused_tracks) == LARDON3D_PROJECT_DB_OK); + CHECK(reused_tracks.track_set_id == published_tracks.track_set_id); + Lardon3DProjectDbTrackSet mismatched_configuration = reuse_configuration; + mismatched_configuration.gvr_count = 2; + CHECK(lardon3d_project_db_create_track_set(track_database, &mismatched_configuration, NULL, 0, + &reused_tracks) == LARDON3D_PROJECT_DB_CONSTRAINT); + CHECK(query_integer(track_api_path, "SELECT count(*) FROM track_sets", 1)); + + Lardon3DProjectDbTrack invalid_track = {.observation_count = 1, .observations = observations[0]}; + Lardon3DProjectDbTrackSet invalid_configuration; + init_track_configuration(&invalid_configuration, 0x52, 1, 1); + CHECK(lardon3d_project_db_create_track_set(track_database, &invalid_configuration, &invalid_track, + 1, &reused_tracks) == + LARDON3D_PROJECT_DB_INVALID_ARGUMENT); + invalid_track.observation_count = 2; + observations[0][1].feature_set_id = 999999; + init_track_configuration(&invalid_configuration, 0x53, 1, 1); + CHECK(lardon3d_project_db_create_track_set(track_database, &invalid_configuration, &invalid_track, + 1, &reused_tracks) == LARDON3D_PROJECT_DB_NOT_FOUND); + observations[0][1].feature_set_id = track_features_b.feature_set_id; + observations[0][1].feature_index = 128; + init_track_configuration(&invalid_configuration, 0x54, 1, 1); + CHECK(lardon3d_project_db_create_track_set(track_database, &invalid_configuration, &invalid_track, + 1, &reused_tracks) == LARDON3D_PROJECT_DB_CONSTRAINT); + observations[0][1].feature_index = 0; + observations[0][1].feature_set_id = track_features_a.feature_set_id; + init_track_configuration(&invalid_configuration, 0x55, 1, 1); + CHECK(lardon3d_project_db_create_track_set(track_database, &invalid_configuration, &invalid_track, + 1, &reused_tracks) == LARDON3D_PROJECT_DB_CONSTRAINT); + observations[0][1].feature_set_id = track_features_a.feature_set_id; + observations[0][1].feature_index = 1; + init_track_configuration(&invalid_configuration, 0x56, 1, 1); + CHECK(lardon3d_project_db_create_track_set(track_database, &invalid_configuration, &invalid_track, + 1, &reused_tracks) == LARDON3D_PROJECT_DB_CONSTRAINT); + observations[0][1].feature_set_id = track_features_b.feature_set_id; + observations[0][1].feature_index = 0; + observations[0][0].position_in_track = 1; + init_track_configuration(&invalid_configuration, 0x57, 1, 1); + CHECK(lardon3d_project_db_create_track_set(track_database, &invalid_configuration, &invalid_track, + 1, &reused_tracks) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT); + observations[0][0].position_in_track = 0; + CHECK(query_integer(track_api_path, "SELECT count(*) FROM track_sets", 1)); + + Lardon3DProjectDbTrackSet cascade_configuration; + init_track_configuration(&cascade_configuration, 0x58, 1, 1); + Lardon3DProjectDbTrack cascade_track = { + .observation_count = 2, + .observations = observations[0], + }; + Lardon3DProjectDbTrackSet cascade_published; + CHECK(lardon3d_project_db_create_track_set(track_database, &cascade_configuration, &cascade_track, + 1, &cascade_published) == LARDON3D_PROJECT_DB_OK); + CHECK(query_integer(track_api_path, "SELECT count(*) FROM track_sets", 2)); + sqlite3 *cascade_raw = NULL; + CHECK(sqlite3_open(track_api_path, &cascade_raw) == SQLITE_OK); + CHECK(sqlite3_exec(cascade_raw, "PRAGMA foreign_keys=ON;", NULL, NULL, NULL) == SQLITE_OK); + char delete_cascade_sql[256]; + (void)snprintf(delete_cascade_sql, sizeof(delete_cascade_sql), + "DELETE FROM track_sets WHERE track_set_id=%llu;", + (unsigned long long)cascade_published.track_set_id); + CHECK(sqlite3_exec(cascade_raw, delete_cascade_sql, NULL, NULL, NULL) == SQLITE_OK); + CHECK(sqlite3_close(cascade_raw) == SQLITE_OK); + char cascade_count_sql[256]; + (void)snprintf(cascade_count_sql, sizeof(cascade_count_sql), + "SELECT count(*) FROM track_sets WHERE track_set_id=%llu", + (unsigned long long)cascade_published.track_set_id); + CHECK(query_integer(track_api_path, cascade_count_sql, 0)); + (void)snprintf(cascade_count_sql, sizeof(cascade_count_sql), + "SELECT count(*) FROM tracks WHERE track_set_id=%llu", + (unsigned long long)cascade_published.track_set_id); + CHECK(query_integer(track_api_path, cascade_count_sql, 0)); + (void)snprintf(cascade_count_sql, sizeof(cascade_count_sql), + "SELECT count(*) FROM track_observations WHERE track_set_id=%llu", + (unsigned long long)cascade_published.track_set_id); + CHECK(query_integer(track_api_path, cascade_count_sql, 0)); + + sqlite3 *track_raw = NULL; + CHECK(sqlite3_open(track_api_path, &track_raw) == SQLITE_OK); + CHECK(sqlite3_exec(track_raw, "PRAGMA foreign_keys=OFF;", NULL, NULL, NULL) == SQLITE_OK); + char corruption_sql[512]; + (void)snprintf(corruption_sql, sizeof(corruption_sql), + "UPDATE tracks SET track_set_id=999999 WHERE track_id=%llu;", + (unsigned long long)first_track_id); + CHECK(sqlite3_exec(track_raw, corruption_sql, NULL, NULL, NULL) == SQLITE_OK); + CHECK(lardon3d_project_db_load_track(track_database, first_track_id, &loaded_track) == + LARDON3D_PROJECT_DB_CORRUPT); + (void)snprintf(corruption_sql, sizeof(corruption_sql), + "UPDATE tracks SET track_set_id=%llu WHERE track_id=%llu;", + (unsigned long long)published_tracks.track_set_id, + (unsigned long long)first_track_id); + CHECK(sqlite3_exec(track_raw, corruption_sql, NULL, NULL, NULL) == SQLITE_OK); + (void)snprintf(corruption_sql, sizeof(corruption_sql), + "UPDATE track_observations SET track_id=999999 WHERE track_id=%llu;", + (unsigned long long)first_track_id); + CHECK(sqlite3_exec(track_raw, corruption_sql, NULL, NULL, NULL) == SQLITE_OK); + CHECK(lardon3d_project_db_find_track_by_observation( + track_database, published_tracks.track_set_id, track_features_a.feature_set_id, 0, + &found_track) == LARDON3D_PROJECT_DB_CORRUPT); + (void)snprintf(corruption_sql, sizeof(corruption_sql), + "UPDATE track_observations SET track_id=%llu WHERE track_id=999999;", + (unsigned long long)first_track_id); + CHECK(sqlite3_exec(track_raw, corruption_sql, NULL, NULL, NULL) == SQLITE_OK); + (void)snprintf(corruption_sql, sizeof(corruption_sql), + "UPDATE track_observations SET feature_set_id=%llu,feature_index=127 " + "WHERE track_id=%llu AND " + "position_in_track=1;", + (unsigned long long)track_features_a.feature_set_id, + (unsigned long long)first_track_id); + CHECK(sqlite3_exec(track_raw, corruption_sql, NULL, NULL, NULL) == SQLITE_OK); + CHECK(lardon3d_project_db_load_track(track_database, first_track_id, &loaded_track) == + LARDON3D_PROJECT_DB_CORRUPT); + CHECK(sqlite3_close(track_raw) == SQLITE_OK); + lardon3d_project_db_close(track_database); + CHECK(unlink(track_api_path) == 0); + + sqlite3 *raw_db = NULL; + CHECK(sqlite3_open(database_path, &raw_db) == SQLITE_OK); + CHECK(sqlite3_exec(raw_db, "PRAGMA foreign_keys=ON;", NULL, NULL, NULL) == SQLITE_OK); + + sqlite3_stmt *img_check = NULL; + CHECK(sqlite3_prepare_v2(raw_db, + "SELECT image_id FROM images ORDER BY image_id LIMIT 1", + -1, &img_check, NULL) == SQLITE_OK); + CHECK(sqlite3_step(img_check) == SQLITE_ROW); + sqlite3_int64 img_id = sqlite3_column_int64(img_check, 0); + CHECK(img_id > 0); + (void)sqlite3_finalize(img_check); + + CHECK(sqlite3_exec(raw_db, + "INSERT INTO feature_assets(sha256,path,size_bytes,durability,created_at) " + "VALUES(X'CAFECAFECAFECAFECAFECAFECAFECAFECAFECAFECAFECAFECAFECAFECAFECAFE'," + "'assets/features/ca/fecafeca',128,0,500);", + NULL, NULL, NULL) == SQLITE_OK); + sqlite3_int64 fa_id = sqlite3_last_insert_rowid(raw_db); + CHECK(fa_id > 0); + + char insert_fs[1024]; + (void)snprintf(insert_fs, sizeof(insert_fs), + "INSERT INTO feature_sets(image_id,feature_asset_id,extractor_kind," + "extractor_version,parameter_fingerprint,source_image_sha256,feature_count," + "descriptor_type,descriptor_dimension,created_at) " + "VALUES(%lld,%lld,'orb',1," + "X'CAFECAFECAFECAFECAFECAFECAFECAFECAFECAFECAFECAFECAFECAFECAFECAFE'," + "X'CAFECAFECAFECAFECAFECAFECAFECAFECAFECAFECAFECAFECAFECAFECAFECAFE'," + "64,1,32,500);", + (long long)img_id, (long long)fa_id); + CHECK(sqlite3_exec(raw_db, insert_fs, NULL, NULL, NULL) == SQLITE_OK); + sqlite3_int64 fs_id = sqlite3_last_insert_rowid(raw_db); + CHECK(fs_id > 0); + + CHECK(sqlite3_exec(raw_db, + "INSERT INTO track_sets(builder_kind,builder_version,parameter_fingerprint," + "verifier_kind,verifier_version,verifier_fingerprint,input_scope_hash," + "gvr_count,track_count,created_at) " + "VALUES('test-builder',1," + "X'000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F'," + "1,1," + "X'101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F'," + "X'202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F'," + "1,0,200);", + NULL, NULL, NULL) == SQLITE_OK); + + CHECK(sqlite3_exec(raw_db, + "INSERT INTO track_sets(builder_kind,builder_version,parameter_fingerprint," + "verifier_kind,verifier_version,verifier_fingerprint,input_scope_hash," + "gvr_count,track_count,created_at) " + "VALUES('test-builder',1," + "X'000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F'," + "1,1," + "X'101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F'," + "X'202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F'," + "1,0,200);", + NULL, NULL, NULL) == SQLITE_CONSTRAINT); + + CHECK(sqlite3_exec(raw_db, + "INSERT INTO track_sets(builder_kind,builder_version,parameter_fingerprint," + "verifier_kind,verifier_version,verifier_fingerprint,input_scope_hash," + "gvr_count,track_count,created_at) " + "VALUES('inv-builder',0," + "X'000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F'," + "1,1," + "X'101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F'," + "X'202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F'," + "1,0,200);", + NULL, NULL, NULL) == SQLITE_CONSTRAINT); + + CHECK(sqlite3_exec(raw_db, + "INSERT INTO track_sets(builder_kind,builder_version,parameter_fingerprint," + "verifier_kind,verifier_version,verifier_fingerprint,input_scope_hash," + "gvr_count,track_count,created_at) " + "VALUES('other-builder',1," + "X'000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F'," + "1,1," + "X'101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F'," + "X'202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F'," + "1,0,200);", + NULL, NULL, NULL) == SQLITE_OK); + + CHECK(sqlite3_exec(raw_db, + "INSERT INTO track_sets(builder_kind,builder_version,parameter_fingerprint," + "verifier_kind,verifier_version,verifier_fingerprint,input_scope_hash," + "gvr_count,track_count,created_at) " + "VALUES('gvr-fail',1," + "X'303132333435363738393A3B3C3D3E3F404142434445464748494A4B4C4D4E4F'," + "1,1," + "X'404142434445464748494A4B4C4D4E4F505152535455565758595A5B5C5D5E5F'," + "X'505152535455565758595A5B5C5D5E5F606162636465666768696A6B6C6D6E6F'," + "0,0,200);", + NULL, NULL, NULL) == SQLITE_CONSTRAINT); + + CHECK(sqlite3_exec(raw_db, + "INSERT INTO tracks(track_set_id,observation_count) " + "VALUES(99999,2);", + NULL, NULL, NULL) == SQLITE_CONSTRAINT); + + CHECK(sqlite3_exec(raw_db, + "INSERT INTO tracks(track_set_id,observation_count) " + "VALUES(1,1);", + NULL, NULL, NULL) == SQLITE_CONSTRAINT); + + CHECK(sqlite3_exec(raw_db, + "INSERT INTO tracks(track_set_id,observation_count) " + "VALUES(1,2);", + NULL, NULL, NULL) == SQLITE_OK); + + char insert_obs[512]; + (void)snprintf(insert_obs, sizeof(insert_obs), + "INSERT INTO track_observations(track_set_id,track_id,feature_set_id," + "feature_index,position_in_track) " + "VALUES(1,1,%lld,0,0);", + (long long)(fs_id + 999)); + CHECK(sqlite3_exec(raw_db, insert_obs, NULL, NULL, NULL) == SQLITE_CONSTRAINT); + + (void)snprintf(insert_obs, sizeof(insert_obs), + "INSERT INTO track_observations(track_set_id,track_id,feature_set_id," + "feature_index,position_in_track) " + "VALUES(1,1,%lld,0,0);", + (long long)fs_id); + CHECK(sqlite3_exec(raw_db, insert_obs, NULL, NULL, NULL) == SQLITE_OK); + + CHECK(sqlite3_exec(raw_db, insert_obs, NULL, NULL, + NULL) == SQLITE_CONSTRAINT); + + CHECK(sqlite3_exec(raw_db, + "DELETE FROM track_sets WHERE track_set_id=2;", + NULL, NULL, NULL) == SQLITE_OK); + + sqlite3_stmt *cascade_check = NULL; + CHECK(sqlite3_prepare_v2(raw_db, + "SELECT count(*) FROM tracks WHERE track_set_id=2", + -1, &cascade_check, NULL) == SQLITE_OK); + CHECK(sqlite3_step(cascade_check) == SQLITE_ROW && + sqlite3_column_int(cascade_check, 0) == 0); + (void)sqlite3_finalize(cascade_check); + cascade_check = NULL; + CHECK(sqlite3_prepare_v2(raw_db, + "SELECT count(*) FROM track_observations WHERE track_set_id=2", + -1, &cascade_check, NULL) == SQLITE_OK); + CHECK(sqlite3_step(cascade_check) == SQLITE_ROW && + sqlite3_column_int(cascade_check, 0) == 0); + (void)sqlite3_finalize(cascade_check); + cascade_check = NULL; + + CHECK(sqlite3_close(raw_db) == SQLITE_OK); + CHECK(unlink(artifact_path) == 0); CHECK(unlink(database_path) == 0); CHECK(unlink(future_path) == 0); @@ -841,6 +1311,8 @@ static bool run_test(void) { CHECK(unlink(failed_v8_path) == 0); CHECK(unlink(v10_path) == 0); CHECK(unlink(failed_v11_path) == 0); + CHECK(unlink(v13_path) == 0); + CHECK(unlink(failed_v14_path) == 0); CHECK(rmdir(directory) == 0); return true; }