feat(reconstruction): add track model

This commit is contained in:
fy59 2026-08-09 19:06:52 +02:00
parent 70a5a18ad6
commit 6925a7bc1d
23 changed files with 1883 additions and 75 deletions

View file

@ -1,7 +1,7 @@
--- ---
description: Analyse en lecture seule architecture, persistance et invariants description: Analyse en lecture seule architecture, persistance et invariants
mode: subagent mode: subagent
model: opencode-go/qwen3.8-max model: opencode-go/gpt-5.6-luna
temperature: 0.1 temperature: 0.1
maxSteps: 40 maxSteps: 40
permission: permission:

View file

@ -1,7 +1,7 @@
--- ---
description: Fallback MiMo pour reprendre une tranche déjà préparée description: Fallback MiMo pour reprendre une tranche déjà préparée
mode: subagent mode: subagent
model: opencode-go/mimo-v2.5-pro model: opencode-go/gpt-5.6-luna
temperature: 0.1 temperature: 0.1
maxSteps: 100 maxSteps: 100
permission: permission:

View file

@ -1,7 +1,7 @@
--- ---
description: Implémente les tranches substantielles et corrections complexes de Lardon3D description: Implémente les tranches substantielles et corrections complexes de Lardon3D
mode: subagent mode: subagent
model: opencode-go/deepseek-v4-pro model: opencode-go/gpt-5.6-luna
temperature: 0.1 temperature: 0.1
maxSteps: 120 maxSteps: 120
permission: permission:

View file

@ -1,7 +1,7 @@
--- ---
description: Audite concurrence, ownership et durées de vie partagées description: Audite concurrence, ownership et durées de vie partagées
mode: subagent mode: subagent
model: opencode-go/deepseek-v4-pro model: opencode-go/gpt-5.6-luna
temperature: 0.1 temperature: 0.1
maxSteps: 50 maxSteps: 50
permission: permission:

View file

@ -1,7 +1,7 @@
--- ---
description: Diagnostic technique ciblé en lecture seule pour Lardon3D description: Diagnostic technique ciblé en lecture seule pour Lardon3D
mode: subagent mode: subagent
model: opencode-go/deepseek-v4-pro model: opencode-go/gpt-5.6-luna
temperature: 0.1 temperature: 0.1
maxSteps: 12 maxSteps: 12
permission: permission:

View file

@ -1,7 +1,7 @@
--- ---
description: Maintient la documentation canonique selon le code validé description: Maintient la documentation canonique selon le code validé
mode: subagent mode: subagent
model: opencode-go/mimo-v2.5-pro model: opencode-go/gpt-5.6-luna
temperature: 0.0 temperature: 0.0
maxSteps: 60 maxSteps: 60
permission: permission:

View file

@ -1,7 +1,7 @@
--- ---
description: Orchestre les tickets Lardon3D longs par phases durables description: Orchestre les tickets Lardon3D longs par phases durables
mode: primary mode: primary
model: opencode-go/mimo-v2.5-pro model: opencode-go/gpt-5.6-luna
temperature: 0.1 temperature: 0.1
maxSteps: 200 maxSteps: 200
permission: permission:

View file

@ -1,7 +1,7 @@
--- ---
description: Lit des fichiers et symboles ciblés puis retourne une synthèse description: Lit des fichiers et symboles ciblés puis retourne une synthèse
mode: subagent mode: subagent
model: opencode-go/mimo-v2.5 model: opencode-go/gpt-5.6-luna
temperature: 0.0 temperature: 0.0
maxSteps: 30 maxSteps: 30
permission: permission:

View file

@ -1,7 +1,7 @@
--- ---
description: Effectue la seconde revue de production après tests verts description: Effectue la seconde revue de production après tests verts
mode: subagent mode: subagent
model: opencode-go/qwen3.8-max model: opencode-go/gpt-5.6-luna
temperature: 0.0 temperature: 0.0
maxSteps: 20 maxSteps: 20
permission: permission:

View file

@ -1,7 +1,7 @@
--- ---
description: Exécute les validations Lardon3D strictement séquencées description: Exécute les validations Lardon3D strictement séquencées
mode: subagent mode: subagent
model: opencode-go/deepseek-v4-pro model: opencode-go/gpt-5.6-luna
temperature: 0.0 temperature: 0.0
maxSteps: 80 maxSteps: 80
permission: permission:

View file

@ -29,7 +29,7 @@ persistante, enrichissable et versionnable.
- **Image View** : vues triées et filtrées pour la TUI - **Image View** : vues triées et filtrées pour la TUI
- **Task** : moteur de tâches avec pause/reprise, annulation et séquences - **Task** : moteur de tâches avec pause/reprise, annulation et séquences
- **Task Checkpoint v1** : snapshot durable, fichier atomique et reprise sûre - **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 Verification Model v1** : identité, masque d'inliers et modèle 3×3 persistants
- **Geometric Verifier v1** : Fundamental USAC/MAGSAC, reprise et lots resource-aware - **Geometric Verifier v1** : Fundamental USAC/MAGSAC, reprise et lots resource-aware
- **Task Kind Registry** : identité métier durable et reconstruction runtime explicite - **Task Kind Registry** : identité métier durable et reconstruction runtime explicite
@ -117,6 +117,7 @@ Acquisitions
- [Matcher](docs/architecture/matcher.md) - [Matcher](docs/architecture/matcher.md)
- [Geometric Verification](docs/architecture/geometric_verification.md) - [Geometric Verification](docs/architecture/geometric_verification.md)
- [Geometric Verifier](docs/architecture/geometric_verifier.md) - [Geometric Verifier](docs/architecture/geometric_verifier.md)
- [Track Model](docs/architecture/tracks.md)
- [Backend Vulkan ORB](docs/architecture/vulkan_matcher.md) - [Backend Vulkan ORB](docs/architecture/vulkan_matcher.md)
- [Viewer](docs/architecture/viewer.md) - [Viewer](docs/architecture/viewer.md)
- [Revue des fondations](docs/architecture/foundation_review.md) - [Revue des fondations](docs/architecture/foundation_review.md)

View file

@ -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 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. 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 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é. masque bit-identiques ; REJECTED conserve son support et est également réutilisé.

View file

@ -1,8 +1,10 @@
# Base de données projet Lardon3D # Base de données projet Lardon3D
> Version courante : **v13**. La migration transactionnelle v12→v13 ajoute > Version courante : **v14**. La migration transactionnelle v13→v14 ajoute
> uniquement `geometric_verifier_tasks` pour la tâche durable. La migration > les tables `track_sets`, `tracks` et `track_observations` pour le Track
> v11→v12 ajoute le modèle immutable `geometric_verification_results`. La migration v10→v11 ajoute > 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 > `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 > uniquement `match_results` pour le Match Result Model. La migration v8→v9
> ajoute la table `candidate_pair_generate_tasks` pour la tâche durable > 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) - Statut (validée, rejetée)
- Métriques - Métriques
#### Track #### Track Set (v14)
- Identifiant unique - Identifiant unique (`track_set_id`)
- Observations - Identité de reuse (builder, verifier, scope)
- Point 3D associé - Nombre de tracks et GVR
- Qualité - Immutable après publication
#### Observation #### Track (v14)
- Identifiant unique - Identifiant unique (`track_id`)
- Image - Track Set parent
- Position 2D - Nombre d'observations (≥ 2)
- Descripteur - Pas de coordonnées 3D en v1
#### Track Observation (v14)
- Identifiant composite `(track_set_id, feature_set_id, feature_index)`
- Track parent - Track parent
- Position dans le track (`position_in_track`)
- Feature Set et index de feature
#### Camera #### Camera
- Identifiant unique - 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) - Image → Visual Signature (1:N)
- Candidate Pair → Image (2) - Candidate Pair → Image (2)
- Verified Pair → Candidate Pair (1) - Verified Pair → Candidate Pair (1)
- Track → Observation (N:M) - Track Set → Track (1:N, CASCADE)
- Observation → Image (1) - Track → Track Observation (1:N, CASCADE)
- Observation → Point3D (N:1) - Track Observation → Feature Set (N:1)
- Camera → Pose (1:N) - Camera → Pose (1:N)
- Pose → Reconstruction Layer (N:M) - Pose → Reconstruction Layer (N:M)
- Reconstruction Layer → Artifact (1:N) - 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 Le contrat complet, dont l'ordre des bits, est dans
`geometric_verification.md`. `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 ## 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 `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 `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 `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 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 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 `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 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'à 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 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` ; 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` : Migration v1→v2 exacte, exécutée entre `BEGIN IMMEDIATE` et `COMMIT` :
@ -608,8 +701,8 @@ ouvert.
## Statut ## Statut
**IMPLEMENTED** — SQLite système, schéma v13 et migrations séquentielles **IMPLEMENTED** — SQLite système, schéma v14 et migrations séquentielles
v1→v2→v3→v4→v5→v6→v7→v8→v9→v10→v11→v12→v13, identité projet, transactions 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. tâche+checkpoint, pagination de reprise et artefacts génériques.
**IMPLEMENTED** — ouverture/fermeture avec le projet, identité INI/DB cohérente, **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 d'import existe. Cet indicateur signifie « données legacy potentiellement non
cataloguées », pas « images migrées ». 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 **NOT_YET_WIRED** — autosave à toutes les transitions, retry UI des sources
indisponibles, migration de la TUI legacy et réconciliation des fichiers 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. orphelins et compaction Visual Index. Feature Store et Visual Index v1 sont implémentés.

View file

@ -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. | | **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. | | **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.
--- ---

523
docs/architecture/tracks.md Normal file
View file

@ -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.

View file

@ -1,5 +1,14 @@
# Matching & Tracks # 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 > 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 > 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 > pas des matches multivues et ne créent aucun track. Le futur matcher choisira

View file

@ -9,7 +9,7 @@
#include <lardon3d/task.h> #include <lardon3d/task.h>
enum { enum {
LARDON3D_PROJECT_DB_SCHEMA_VERSION = 13, LARDON3D_PROJECT_DB_SCHEMA_VERSION = 14,
LARDON3D_PROJECT_DB_ID_CAPACITY = 65, LARDON3D_PROJECT_DB_ID_CAPACITY = 65,
LARDON3D_PROJECT_DB_KIND_CAPACITY = 65, LARDON3D_PROJECT_DB_KIND_CAPACITY = 65,
LARDON3D_PROJECT_DB_PATH_CAPACITY = 4096, LARDON3D_PROJECT_DB_PATH_CAPACITY = 4096,
@ -19,6 +19,7 @@ enum {
LARDON3D_PROJECT_DB_CANDIDATE_PAIR_PAGE_MAX = 256, LARDON3D_PROJECT_DB_CANDIDATE_PAIR_PAGE_MAX = 256,
LARDON3D_PROJECT_DB_MATCH_RESULT_PAGE_MAX = 256, LARDON3D_PROJECT_DB_MATCH_RESULT_PAGE_MAX = 256,
LARDON3D_PROJECT_DB_GEOMETRIC_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_INLIER_MASK_MAX = 1024,
LARDON3D_PROJECT_DB_FUNDAMENTAL_COEFFICIENTS = 9, LARDON3D_PROJECT_DB_FUNDAMENTAL_COEFFICIENTS = 9,
LARDON3D_PROJECT_DB_SCANSET_NAME_CAPACITY = 256, LARDON3D_PROJECT_DB_SCANSET_NAME_CAPACITY = 256,
@ -347,6 +348,55 @@ typedef struct {
bool exclude_same_asset; bool exclude_same_asset;
} Lardon3DProjectDbCandidatePairGenerateTask; } 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, Lardon3DProjectDbResult lardon3d_project_db_open(const char *path, Lardon3DProjectDb **database,
char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]); char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]);
void lardon3d_project_db_close(Lardon3DProjectDb *database); void lardon3d_project_db_close(Lardon3DProjectDb *database);

View file

@ -9,6 +9,7 @@
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <sys/stat.h> #include <sys/stat.h>
#include <time.h>
#include <lardon3d/project_db.h> #include <lardon3d/project_db.h>
@ -263,6 +264,32 @@ static const char schema_geometric_verification_v12[] =
"CREATE INDEX geometric_verification_results_parent_idx ON " "CREATE INDEX geometric_verification_results_parent_idx ON "
"geometric_verification_results(match_result_id,geometric_verification_result_id);"; "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[] = static const char schema_geometric_verifier_task_v13[] =
"CREATE TABLE geometric_verifier_tasks(" "CREATE TABLE geometric_verifier_tasks("
"task_id INTEGER PRIMARY KEY REFERENCES tasks(task_id) ON DELETE CASCADE," "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 && 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 != 4 && from_version != 5 && from_version != 6 && from_version != 7 &&
from_version != 8 && from_version != 9 && from_version != 10 && from_version != 11 && 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; return LARDON3D_PROJECT_DB_CORRUPT;
} }
Lardon3DProjectDbResult result = execute(database, "BEGIN IMMEDIATE", "begin migration"); Lardon3DProjectDbResult result = execute(database, "BEGIN IMMEDIATE", "begin migration");
@ -639,6 +666,21 @@ static Lardon3DProjectDbResult migrate(Lardon3DProjectDb *database, unsigned int
"finish schema v13 migration"); "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) { if (result == LARDON3D_PROJECT_DB_OK) {
result = execute(database, "COMMIT", "commit migration"); result = execute(database, "COMMIT", "commit migration");
} }
@ -755,7 +797,10 @@ Lardon3DProjectDbResult lardon3d_project_db_open(const char *path, Lardon3DProje
"matcher_tasks", "matcher_tasks",
"match_results", "match_results",
"geometric_verification_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]) && for (size_t index = 0; index < sizeof(required) / sizeof(required[0]) &&
result == LARDON3D_PROJECT_DB_OK; result == LARDON3D_PROJECT_DB_OK;
++index) { ++index) {
@ -4306,6 +4351,593 @@ Lardon3DProjectDbResult lardon3d_project_db_list_geometric_verification_results(
return db_result; 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 #ifdef LARDON3D_PROJECT_DB_TESTING
Lardon3DProjectDbResult lardon3d_project_db_test_delete_feature_identity( Lardon3DProjectDbResult lardon3d_project_db_test_delete_feature_identity(
Lardon3DProjectDb *database, uint64_t feature_set_id, uint64_t feature_asset_id) { Lardon3DProjectDb *database, uint64_t feature_set_id, uint64_t feature_asset_id) {

View file

@ -157,9 +157,12 @@ static bool create_v11_database(const char *path) {
lardon3d_project_db_close(database); lardon3d_project_db_close(database);
return execute_sql(path, return execute_sql(path,
"PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;" "PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;"
"DROP TABLE geometric_verifier_tasks;" "DROP TABLE geometric_verifier_tasks;"
"DROP TABLE geometric_verification_results;" "DROP TABLE geometric_verification_results;"
"UPDATE metadata SET value=11 WHERE key='schema_version';" "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;"); "COMMIT;PRAGMA foreign_keys=ON;");
} }
@ -167,7 +170,7 @@ static bool test_model_api(const char *path) {
char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]; char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY];
Lardon3DProjectDb *database = NULL; Lardon3DProjectDb *database = NULL;
CHECK(lardon3d_project_db_open(path, &database, error) == LARDON3D_PROJECT_DB_OK); 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; Parents parents;
CHECK(create_parents(database, &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'", "name='geometric_verification_results'",
0)); 0));
CHECK(lardon3d_project_db_open(v11_path, &database, error) == LARDON3D_PROJECT_DB_OK); 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; Parents parents;
CHECK(create_parents(database, &parents)); CHECK(create_parents(database, &parents));
unsigned char fingerprint[32] = {0x91}; 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); &migrated_result) == LARDON3D_PROJECT_DB_OK);
uint64_t migrated_result_id = migrated_result.geometric_verification_result_id; uint64_t migrated_result_id = migrated_result.geometric_verification_result_id;
lardon3d_project_db_close(database); 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, CHECK(query_integer(v11_path,
"SELECT count(*) FROM sqlite_master WHERE type='index' AND " "SELECT count(*) FROM sqlite_master WHERE type='index' AND "
"name='geometric_verification_results_parent_idx'", "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'", "name='geometric_verification_results'",
0)); 0));
CHECK(lardon3d_project_db_open(failed_path, &database, error) == LARDON3D_PROJECT_DB_OK); 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); lardon3d_project_db_close(database);
return true; return true;
} }

View file

@ -331,9 +331,12 @@ static bool run_task_test(void) {
lardon3d_project_db_close(state.project_db); lardon3d_project_db_close(state.project_db);
CHECK(exec_sql( CHECK(exec_sql(
database_path, database_path,
"PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;" "PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;"
"DROP TABLE geometric_verifier_tasks;" "DROP TABLE geometric_verifier_tasks;"
"UPDATE metadata SET value=12 WHERE key='schema_version';COMMIT;")); "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); CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V13", "1", 1) == 0);
Lardon3DProjectDb *database = NULL; Lardon3DProjectDb *database = NULL;
CHECK(lardon3d_project_db_open(database_path, &database, error) == CHECK(lardon3d_project_db_open(database_path, &database, error) ==
@ -349,7 +352,7 @@ static bool run_task_test(void) {
0)); 0));
CHECK(lardon3d_project_db_open(database_path, &database, error) == CHECK(lardon3d_project_db_open(database_path, &database, error) ==
LARDON3D_PROJECT_DB_OK); 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); lardon3d_project_db_close(database);
CHECK(remove_tree(root)); CHECK(remove_tree(root));
return true; return true;

View file

@ -52,6 +52,9 @@ static bool create_v9_database(const char *path) {
"DROP TABLE geometric_verification_results;" "DROP TABLE geometric_verification_results;"
"DROP TABLE matcher_tasks;" "DROP TABLE matcher_tasks;"
"DROP TABLE match_results;" "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;"; "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; bool ok = sqlite3_exec(connection, sql, NULL, NULL, NULL) == SQLITE_OK;
return sqlite3_close(connection) == SQLITE_OK && ok; return sqlite3_close(connection) == SQLITE_OK && ok;
@ -95,7 +98,7 @@ static bool run_test(void) {
char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]; char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY];
Lardon3DProjectDb *database = NULL; Lardon3DProjectDb *database = NULL;
CHECK(lardon3d_project_db_open(database_path, &database, error) == LARDON3D_PROJECT_DB_OK); 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; Lardon3DProjectDbScanSet scanset;
CHECK(lardon3d_project_db_create_scanset(database, "Match-test", &scanset) == CHECK(lardon3d_project_db_create_scanset(database, "Match-test", &scanset) ==
@ -454,7 +457,7 @@ static bool run_test(void) {
database = NULL; database = NULL;
CHECK(lardon3d_project_db_open(database_path, &database, error) == LARDON3D_PROJECT_DB_OK); 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 */ /* Verify persistence: load previously created results */
CHECK(lardon3d_project_db_load_match_result(database, first_id, &loaded) == 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(create_v9_database(v9_path));
CHECK(query_integer(v9_path, "SELECT value FROM metadata WHERE key='schema_version'", 9)); 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_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); lardon3d_project_db_close(database);
database = NULL; 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, CHECK(query_integer(v9_path,
"SELECT count(*) FROM sqlite_master WHERE type='table' AND " "SELECT count(*) FROM sqlite_master WHERE type='table' AND "
"name='match_results'", 1)); "name='match_results'", 1));
@ -516,7 +519,7 @@ static bool run_test(void) {
"SELECT count(*) FROM sqlite_master WHERE type='table' AND " "SELECT count(*) FROM sqlite_master WHERE type='table' AND "
"name='matcher_tasks'", 0)); "name='matcher_tasks'", 0));
CHECK(lardon3d_project_db_open(failed_v10_path, &database, error) == LARDON3D_PROJECT_DB_OK && 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); lardon3d_project_db_close(database);
database = NULL; database = NULL;

View file

@ -92,6 +92,9 @@ static bool downgrade_project_to_historical_v10(const char *database_path) {
"DROP TABLE geometric_verifier_tasks;" "DROP TABLE geometric_verifier_tasks;"
"DROP TABLE geometric_verification_results;" "DROP TABLE geometric_verification_results;"
"DROP TABLE matcher_tasks;" "DROP TABLE matcher_tasks;"
"DROP TABLE track_observations;"
"DROP TABLE tracks;"
"DROP TABLE track_sets;"
"UPDATE metadata SET value=10 WHERE key='schema_version';" "UPDATE metadata SET value=10 WHERE key='schema_version';"
"COMMIT;PRAGMA foreign_keys=ON;"; "COMMIT;PRAGMA foreign_keys=ON;";
bool success = sqlite3_exec(connection, sql, NULL, NULL, NULL) == SQLITE_OK; bool success = sqlite3_exec(connection, sql, NULL, NULL, NULL) == SQLITE_OK;
@ -352,7 +355,7 @@ static bool run_test(void) {
"name='matcher_tasks'", "name='matcher_tasks'",
0)); 0));
CHECK(reopen_runtime(&fixture)); 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, CHECK(query_integer(database_path,
"SELECT count(*) FROM sqlite_master WHERE type='table' AND " "SELECT count(*) FROM sqlite_master WHERE type='table' AND "
"name='matcher_tasks'", "name='matcher_tasks'",

View file

@ -37,6 +37,20 @@ static void asset_path_for_hash(const unsigned char hash[LARDON3D_PROJECT_DB_SHA
hex); 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) { static Lardon3DTaskDurableSnapshot task_snapshot(uint64_t id, Lardon3DTaskState saved) {
Lardon3DTaskDurableSnapshot snapshot = { Lardon3DTaskDurableSnapshot snapshot = {
.id = id, .id = id,
@ -89,7 +103,7 @@ static bool create_future_database(const char *path) {
} }
bool ok = sqlite3_exec(connection, bool ok = sqlite3_exec(connection,
"CREATE TABLE metadata(key TEXT PRIMARY KEY,value INTEGER NOT NULL);" "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; NULL, NULL, NULL) == SQLITE_OK;
return sqlite3_close(connection) == SQLITE_OK && 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 match_results;"
"DROP TABLE candidate_pair_generate_tasks;" "DROP TABLE candidate_pair_generate_tasks;"
"DROP TABLE candidate_pairs;" "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;"; "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; bool ok = sqlite3_exec(connection, sql, NULL, NULL, NULL) == SQLITE_OK;
return sqlite3_close(connection) == SQLITE_OK && 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 coverage_ratio;"
"ALTER TABLE feature_sets DROP COLUMN total_cells;" "ALTER TABLE feature_sets DROP COLUMN total_cells;"
"ALTER TABLE feature_sets DROP COLUMN occupied_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;"; "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; bool ok = sqlite3_exec(connection, sql, NULL, NULL, NULL) == SQLITE_OK;
return sqlite3_close(connection) == SQLITE_OK && 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_verifier_tasks;"
"DROP TABLE geometric_verification_results;" "DROP TABLE geometric_verification_results;"
"DROP TABLE matcher_tasks;" "DROP TABLE matcher_tasks;"
"DROP TABLE track_observations;"
"DROP TABLE tracks;"
"DROP TABLE track_sets;"
"UPDATE metadata SET value=10 WHERE key='schema_version';" "UPDATE metadata SET value=10 WHERE key='schema_version';"
"COMMIT;PRAGMA foreign_keys=ON;"; "COMMIT;PRAGMA foreign_keys=ON;";
bool ok = sqlite3_exec(connection, sql, NULL, NULL, NULL) == SQLITE_OK; bool ok = sqlite3_exec(connection, sql, NULL, NULL, NULL) == SQLITE_OK;
return sqlite3_close(connection) == SQLITE_OK && 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) { static bool create_v5_database(const char *path) {
if (!create_v6_database(path)) return false; if (!create_v6_database(path)) return false;
sqlite3 *connection = NULL; sqlite3 *connection = NULL;
@ -302,6 +342,24 @@ static bool query_integer(const char *path, const char *sql, sqlite3_int64 expec
matches; 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) { static bool run_test(void) {
char directory[] = "/tmp/lardon3d-project-db-XXXXXX"; char directory[] = "/tmp/lardon3d-project-db-XXXXXX";
CHECK(mkdtemp(directory)); 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 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 direct_v5_path[512], v8_path[512], failed_v8_path[512];
char v10_path[512], failed_v11_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(database_path, sizeof(database_path), "%s/project.db", directory) > 0);
CHECK(snprintf(artifact_path, sizeof(artifact_path), "%s/artifact.bin", 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); CHECK(snprintf(future_path, sizeof(future_path), "%s/future.db", directory) > 0);
@ -337,11 +396,13 @@ static bool run_test(void) {
0); 0);
CHECK(snprintf(v10_path, sizeof(v10_path), "%s/v10.db", directory) > 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(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]; char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY];
Lardon3DProjectDb *database = NULL; Lardon3DProjectDb *database = NULL;
CHECK(lardon3d_project_db_open(database_path, &database, error) == LARDON3D_PROJECT_DB_OK); 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; bool legacy_pending = true;
CHECK(lardon3d_project_db_legacy_catalog_pending(database, &legacy_pending) == CHECK(lardon3d_project_db_legacy_catalog_pending(database, &legacy_pending) ==
LARDON3D_PROJECT_DB_OK && LARDON3D_PROJECT_DB_OK &&
@ -390,10 +451,9 @@ static bool run_test(void) {
asset_path_for_hash(fourth_hash, fourth_asset_path); asset_path_for_hash(fourth_hash, fourth_asset_path);
Lardon3DProjectDbImage pair_image_b; Lardon3DProjectDbImage pair_image_b;
CHECK(lardon3d_project_db_register_image(database, replacement_scanset.scanset_id, fourth_hash, CHECK(lardon3d_project_db_register_image(database, replacement_scanset.scanset_id, fourth_hash,
fourth_asset_path, 1, "pair-b.jpg", "/source/pair-b.jpg", fourth_asset_path, 1, "pair-b.jpg", "/source/pair-b.jpg",
0, 4, &identity_status, &pair_image_b) == 0, 4, &identity_status, &pair_image_b) ==
LARDON3D_PROJECT_DB_OK); LARDON3D_PROJECT_DB_OK);
Lardon3DProjectDbCandidatePair pair; Lardon3DProjectDbCandidatePair pair;
CHECK(lardon3d_project_db_create_candidate_pair(database, replacement_image.image_id, CHECK(lardon3d_project_db_create_candidate_pair(database, replacement_image.image_id,
pair_image.image_id, 10, &pair) == pair_image.image_id, 10, &pair) ==
@ -608,7 +668,7 @@ static bool run_test(void) {
LARDON3D_PROJECT_DB_INVALID_ARGUMENT); LARDON3D_PROJECT_DB_INVALID_ARGUMENT);
lardon3d_project_db_close(contexts[0].database); lardon3d_project_db_close(contexts[0].database);
database = NULL; 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(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_open(database_path, &database, error) == LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_load_task(database, 1, &task) == 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(create_v1_database(legacy_path));
CHECK(lardon3d_project_db_open(legacy_path, &database, error) == LARDON3D_PROJECT_DB_OK); 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 && CHECK(lardon3d_project_db_get_project(database, &loaded_project) == LARDON3D_PROJECT_DB_OK &&
strcmp(loaded_project.stable_id, "legacy-project") == 0); strcmp(loaded_project.stable_id, "legacy-project") == 0);
CHECK(lardon3d_project_db_load_task(database, 9, &task) == LARDON3D_PROJECT_DB_OK); 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_OK);
lardon3d_project_db_close(database); lardon3d_project_db_close(database);
database = NULL; 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(create_v1_database(failed_migration_path));
CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V2", "1", 1) == 0); 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_OK);
lardon3d_project_db_close(database); lardon3d_project_db_close(database);
database = NULL; 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(create_v2_database(failed_v3_migration_path));
CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V3", "1", 1) == 0); 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_OK);
lardon3d_project_db_close(database); lardon3d_project_db_close(database);
database = NULL; 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(create_v3_database(failed_v4_path));
CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V4", "1", 1) == 0); 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); fprintf(stderr, "Migration v4 (%d): %s\n", (int)v4_result, error);
} }
CHECK(v4_result == LARDON3D_PROJECT_DB_OK); 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_task(database, 9, &task) == LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_load_artifact(database, "legacy-artifact", &loaded_artifact) == CHECK(lardon3d_project_db_load_artifact(database, "legacy-artifact", &loaded_artifact) ==
LARDON3D_PROJECT_DB_OK); 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); fprintf(stderr, "Nouvelle tentative migration v7 (%d): %s\n", (int)retry_v7, error);
} }
CHECK(retry_v7 == LARDON3D_PROJECT_DB_OK && 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); lardon3d_project_db_close(database);
database = NULL; database = NULL;
CHECK(create_v5_database(direct_v5_path)); CHECK(create_v5_database(direct_v5_path));
CHECK(query_integer(direct_v5_path, "SELECT value FROM metadata WHERE key='schema_version'", 5)); 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 && 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); lardon3d_project_db_close(database);
database = NULL; database = NULL;
CHECK(create_v7_database(v8_path)); CHECK(create_v7_database(v8_path));
CHECK(query_integer(v8_path, "SELECT value FROM metadata WHERE key='schema_version'", 7)); 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_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); lardon3d_project_db_close(database);
database = NULL; 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(create_v7_database(failed_v8_path));
CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V8", "1", 1) == 0); 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 " "SELECT count(*) FROM sqlite_master WHERE type='table' AND "
"name='candidate_pairs'", 0)); "name='candidate_pairs'", 0));
CHECK(lardon3d_project_db_open(failed_v8_path, &database, error) == LARDON3D_PROJECT_DB_OK && 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); lardon3d_project_db_close(database);
database = NULL; database = NULL;
@ -792,10 +852,10 @@ static bool run_test(void) {
"name='matcher_tasks'", "name='matcher_tasks'",
0)); 0));
CHECK(lardon3d_project_db_open(v10_path, &database, error) == LARDON3D_PROJECT_DB_OK && 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); lardon3d_project_db_close(database);
database = NULL; 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, CHECK(query_integer(v10_path,
"SELECT count(*) FROM sqlite_master WHERE type='table' AND " "SELECT count(*) FROM sqlite_master WHERE type='table' AND "
"name='matcher_tasks'", "name='matcher_tasks'",
@ -818,10 +878,420 @@ static bool run_test(void) {
1)); 1));
CHECK(lardon3d_project_db_open(failed_v11_path, &database, error) == CHECK(lardon3d_project_db_open(failed_v11_path, &database, error) ==
LARDON3D_PROJECT_DB_OK); 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); lardon3d_project_db_close(database);
database = NULL; 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(artifact_path) == 0);
CHECK(unlink(database_path) == 0); CHECK(unlink(database_path) == 0);
CHECK(unlink(future_path) == 0); CHECK(unlink(future_path) == 0);
@ -841,6 +1311,8 @@ static bool run_test(void) {
CHECK(unlink(failed_v8_path) == 0); CHECK(unlink(failed_v8_path) == 0);
CHECK(unlink(v10_path) == 0); CHECK(unlink(v10_path) == 0);
CHECK(unlink(failed_v11_path) == 0); CHECK(unlink(failed_v11_path) == 0);
CHECK(unlink(v13_path) == 0);
CHECK(unlink(failed_v14_path) == 0);
CHECK(rmdir(directory) == 0); CHECK(rmdir(directory) == 0);
return true; return true;
} }