feat(reconstruction): add track model
This commit is contained in:
parent
70a5a18ad6
commit
6925a7bc1d
23 changed files with 1883 additions and 75 deletions
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
description: Analyse en lecture seule architecture, persistance et invariants
|
||||
mode: subagent
|
||||
model: opencode-go/qwen3.8-max
|
||||
model: opencode-go/gpt-5.6-luna
|
||||
temperature: 0.1
|
||||
maxSteps: 40
|
||||
permission:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
description: Fallback MiMo pour reprendre une tranche déjà préparée
|
||||
mode: subagent
|
||||
model: opencode-go/mimo-v2.5-pro
|
||||
model: opencode-go/gpt-5.6-luna
|
||||
temperature: 0.1
|
||||
maxSteps: 100
|
||||
permission:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
description: Implémente les tranches substantielles et corrections complexes de Lardon3D
|
||||
mode: subagent
|
||||
model: opencode-go/deepseek-v4-pro
|
||||
model: opencode-go/gpt-5.6-luna
|
||||
temperature: 0.1
|
||||
maxSteps: 120
|
||||
permission:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
description: Audite concurrence, ownership et durées de vie partagées
|
||||
mode: subagent
|
||||
model: opencode-go/deepseek-v4-pro
|
||||
model: opencode-go/gpt-5.6-luna
|
||||
temperature: 0.1
|
||||
maxSteps: 50
|
||||
permission:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
description: Diagnostic technique ciblé en lecture seule pour Lardon3D
|
||||
mode: subagent
|
||||
model: opencode-go/deepseek-v4-pro
|
||||
model: opencode-go/gpt-5.6-luna
|
||||
temperature: 0.1
|
||||
maxSteps: 12
|
||||
permission:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
description: Maintient la documentation canonique selon le code validé
|
||||
mode: subagent
|
||||
model: opencode-go/mimo-v2.5-pro
|
||||
model: opencode-go/gpt-5.6-luna
|
||||
temperature: 0.0
|
||||
maxSteps: 60
|
||||
permission:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
description: Orchestre les tickets Lardon3D longs par phases durables
|
||||
mode: primary
|
||||
model: opencode-go/mimo-v2.5-pro
|
||||
model: opencode-go/gpt-5.6-luna
|
||||
temperature: 0.1
|
||||
maxSteps: 200
|
||||
permission:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
description: Lit des fichiers et symboles ciblés puis retourne une synthèse
|
||||
mode: subagent
|
||||
model: opencode-go/mimo-v2.5
|
||||
model: opencode-go/gpt-5.6-luna
|
||||
temperature: 0.0
|
||||
maxSteps: 30
|
||||
permission:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
description: Effectue la seconde revue de production après tests verts
|
||||
mode: subagent
|
||||
model: opencode-go/qwen3.8-max
|
||||
model: opencode-go/gpt-5.6-luna
|
||||
temperature: 0.0
|
||||
maxSteps: 20
|
||||
permission:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
description: Exécute les validations Lardon3D strictement séquencées
|
||||
mode: subagent
|
||||
model: opencode-go/deepseek-v4-pro
|
||||
model: opencode-go/gpt-5.6-luna
|
||||
temperature: 0.0
|
||||
maxSteps: 80
|
||||
permission:
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ persistante, enrichissable et versionnable.
|
|||
- **Image View** : vues triées et filtrées pour la TUI
|
||||
- **Task** : moteur de tâches avec pause/reprise, annulation et séquences
|
||||
- **Task Checkpoint v1** : snapshot durable, fichier atomique et reprise sûre
|
||||
- **Project Database v13** : résultats géométriques et tâche Geometric Verifier durables
|
||||
- **Project Database v14** : résultats géométriques, tâche Geometric Verifier durable et Track Model v1
|
||||
- **Geometric Verification Model v1** : identité, masque d'inliers et modèle 3×3 persistants
|
||||
- **Geometric Verifier v1** : Fundamental USAC/MAGSAC, reprise et lots resource-aware
|
||||
- **Task Kind Registry** : identité métier durable et reconstruction runtime explicite
|
||||
|
|
@ -117,6 +117,7 @@ Acquisitions
|
|||
- [Matcher](docs/architecture/matcher.md)
|
||||
- [Geometric Verification](docs/architecture/geometric_verification.md)
|
||||
- [Geometric Verifier](docs/architecture/geometric_verifier.md)
|
||||
- [Track Model](docs/architecture/tracks.md)
|
||||
- [Backend Vulkan ORB](docs/architecture/vulkan_matcher.md)
|
||||
- [Viewer](docs/architecture/viewer.md)
|
||||
- [Revue des fondations](docs/architecture/foundation_review.md)
|
||||
|
|
|
|||
|
|
@ -261,7 +261,7 @@ VERIFIED ou REJECTED est retournée avant toute lecture Feature/Match et sans ap
|
|||
contrainte concurrente déclenche un unique `find` de l'identité, jamais un overwrite ou une
|
||||
récursion. Changer un paramètre scientifique produit un autre fingerprint et un autre résultat.
|
||||
|
||||
Les tests E2E utilisent le vrai Project DB v13, deux Feature Files à 8192 points, des Match Files
|
||||
Les tests E2E utilisent le vrai Project DB (migré v13→v14 à l'ouverture), deux Feature Files à 8192 points, des Match Files
|
||||
hashés, le vrai MAGSAC et le Model v1. VERIFIED est rechargé après close/reopen avec modèle et
|
||||
masque bit-identiques ; REJECTED conserve son support et est également réutilisé.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
# Base de données projet Lardon3D
|
||||
|
||||
> Version courante : **v13**. La migration transactionnelle v12→v13 ajoute
|
||||
> uniquement `geometric_verifier_tasks` pour la tâche durable. La migration
|
||||
> v11→v12 ajoute le modèle immutable `geometric_verification_results`. La migration v10→v11 ajoute
|
||||
> Version courante : **v14**. La migration transactionnelle v13→v14 ajoute
|
||||
> les tables `track_sets`, `tracks` et `track_observations` pour le Track
|
||||
> Model v1. La migration v12→v13 ajoute uniquement `geometric_verifier_tasks`
|
||||
> pour la tâche durable. La migration v11→v12 ajoute le modèle immutable
|
||||
> `geometric_verification_results`. La migration v10→v11 ajoute
|
||||
> `matcher_tasks` pour la tâche Matcher durable. La version v10 publiée ajoute
|
||||
> uniquement `match_results` pour le Match Result Model. La migration v8→v9
|
||||
> ajoute la table `candidate_pair_generate_tasks` pour la tâche durable
|
||||
|
|
@ -68,18 +70,23 @@ entités. Elle est conçue pour être légère, persistante et permettre la repr
|
|||
- Statut (validée, rejetée)
|
||||
- Métriques
|
||||
|
||||
#### Track
|
||||
- Identifiant unique
|
||||
- Observations
|
||||
- Point 3D associé
|
||||
- Qualité
|
||||
#### Track Set (v14)
|
||||
- Identifiant unique (`track_set_id`)
|
||||
- Identité de reuse (builder, verifier, scope)
|
||||
- Nombre de tracks et GVR
|
||||
- Immutable après publication
|
||||
|
||||
#### Observation
|
||||
- Identifiant unique
|
||||
- Image
|
||||
- Position 2D
|
||||
- Descripteur
|
||||
#### Track (v14)
|
||||
- Identifiant unique (`track_id`)
|
||||
- Track Set parent
|
||||
- Nombre d'observations (≥ 2)
|
||||
- Pas de coordonnées 3D en v1
|
||||
|
||||
#### Track Observation (v14)
|
||||
- Identifiant composite `(track_set_id, feature_set_id, feature_index)`
|
||||
- Track parent
|
||||
- Position dans le track (`position_in_track`)
|
||||
- Feature Set et index de feature
|
||||
|
||||
#### Camera
|
||||
- Identifiant unique
|
||||
|
|
@ -151,9 +158,9 @@ entités. Elle est conçue pour être légère, persistante et permettre la repr
|
|||
- Image → Visual Signature (1:N)
|
||||
- Candidate Pair → Image (2)
|
||||
- Verified Pair → Candidate Pair (1)
|
||||
- Track → Observation (N:M)
|
||||
- Observation → Image (1)
|
||||
- Observation → Point3D (N:1)
|
||||
- Track Set → Track (1:N, CASCADE)
|
||||
- Track → Track Observation (1:N, CASCADE)
|
||||
- Track Observation → Feature Set (N:1)
|
||||
- Camera → Pose (1:N)
|
||||
- Pose → Reconstruction Layer (N:M)
|
||||
- Reconstruction Layer → Artifact (1:N)
|
||||
|
|
@ -456,22 +463,108 @@ L'index parent sert la liste paginée ; la contrainte UNIQUE sert le find exact.
|
|||
Le contrat complet, dont l'ordre des bits, est dans
|
||||
`geometric_verification.md`.
|
||||
|
||||
## Schéma v14 implémenté
|
||||
|
||||
La migration v13→v14 ajoute les tables `track_sets`, `tracks` et
|
||||
`track_observations` pour le Track Model v1. Le schéma complet est dans
|
||||
`tracks.md`. Schéma abrégé (la chaîne SQL exécutable canonique reste dans
|
||||
`src/project_db.c`) :
|
||||
|
||||
```sql
|
||||
CREATE TABLE track_sets(
|
||||
track_set_id INTEGER PRIMARY KEY AUTOINCREMENT
|
||||
CHECK(track_set_id > 0),
|
||||
builder_kind TEXT NOT NULL
|
||||
CHECK(length(builder_kind) > 0 AND length(builder_kind) <= 64),
|
||||
builder_version INTEGER NOT NULL CHECK(builder_version > 0),
|
||||
parameter_fingerprint BLOB NOT NULL
|
||||
CHECK(length(parameter_fingerprint) = 32),
|
||||
verifier_kind INTEGER NOT NULL CHECK(verifier_kind > 0),
|
||||
verifier_version INTEGER NOT NULL CHECK(verifier_version > 0),
|
||||
verifier_fingerprint BLOB NOT NULL
|
||||
CHECK(length(verifier_fingerprint) = 32),
|
||||
input_scope_hash BLOB NOT NULL
|
||||
CHECK(length(input_scope_hash) = 32),
|
||||
gvr_count INTEGER NOT NULL CHECK(gvr_count >= 1),
|
||||
track_count INTEGER NOT NULL CHECK(track_count >= 0),
|
||||
created_at INTEGER NOT NULL CHECK(created_at >= 0),
|
||||
UNIQUE(builder_kind, builder_version, parameter_fingerprint,
|
||||
verifier_kind, verifier_version, verifier_fingerprint,
|
||||
input_scope_hash)
|
||||
);
|
||||
|
||||
CREATE TABLE tracks(
|
||||
track_id INTEGER PRIMARY KEY AUTOINCREMENT CHECK(track_id > 0),
|
||||
track_set_id INTEGER NOT NULL
|
||||
REFERENCES track_sets(track_set_id) ON DELETE CASCADE,
|
||||
observation_count INTEGER NOT NULL CHECK(observation_count >= 2)
|
||||
);
|
||||
|
||||
CREATE INDEX tracks_set_idx
|
||||
ON tracks(track_set_id, track_id);
|
||||
|
||||
CREATE TABLE track_observations(
|
||||
track_set_id INTEGER NOT NULL,
|
||||
track_id INTEGER NOT NULL
|
||||
REFERENCES tracks(track_id) ON DELETE CASCADE,
|
||||
feature_set_id INTEGER NOT NULL
|
||||
REFERENCES feature_sets(feature_set_id),
|
||||
feature_index INTEGER NOT NULL CHECK(feature_index >= 0),
|
||||
position_in_track INTEGER NOT NULL CHECK(position_in_track >= 0),
|
||||
PRIMARY KEY(track_set_id, feature_set_id, feature_index),
|
||||
UNIQUE(track_id, position_in_track)
|
||||
);
|
||||
|
||||
CREATE INDEX track_observations_lookup_idx
|
||||
ON track_observations(feature_set_id, feature_index, track_set_id);
|
||||
```
|
||||
|
||||
**Invariants SQL** :
|
||||
- `PRIMARY KEY(track_set_id, feature_set_id, feature_index)` : dans un Track
|
||||
Set donné, une observation n'apparaît qu'une fois
|
||||
- `REFERENCES tracks(track_id) ON DELETE CASCADE` : supprimer un track
|
||||
supprime ses observations
|
||||
- `REFERENCES feature_sets(feature_set_id)` : le Feature Set existe
|
||||
- `CHECK(observation_count >= 2)` : minimum structurel
|
||||
- `UNIQUE(builder_kind, builder_version, parameter_fingerprint,
|
||||
verifier_kind, verifier_version, verifier_fingerprint, input_scope_hash)` :
|
||||
identité de reuse sur `track_sets`
|
||||
- `ON DELETE CASCADE` depuis `track_sets` : supprimer un set supprime tout
|
||||
- `UNIQUE(track_id, position_in_track)` : chaque position dans un track est
|
||||
unique
|
||||
|
||||
**Invariants API** (non protégés par le schéma SQL) :
|
||||
- `track_set_id` dans `track_observations` correspond au `track_set_id` du
|
||||
`track_id` parent
|
||||
- Une seule observation par image par track (validation via
|
||||
`feature_sets.image_id`)
|
||||
- `feature_index < feature_sets.feature_count`
|
||||
- `observation_count` cohérent avec le nombre réel d'observations
|
||||
- `track_count` cohérent avec le nombre réel de tracks
|
||||
|
||||
**Statut** : les tables sont créées par la migration et validées par les
|
||||
tests. L'API C (`create_track_set`, `load_track_set`, `find_track_set`,
|
||||
`list_track_sets`, `load_track`, `list_tracks`, `find_track_by_observation`)
|
||||
est exposée et implémentée. Les limites réelles restent l'absence de Track
|
||||
Builder algorithmique, de tâche dédiée et de triangulation.
|
||||
|
||||
## Ouverture et migrations
|
||||
|
||||
Une DB vide reçoit la chaîne de schémas jusqu'à v13 dans une transaction
|
||||
Une DB vide reçoit la chaîne de schémas jusqu'à v14 dans une transaction
|
||||
`BEGIN IMMEDIATE`. Une DB v1 reçoit transactionnellement les colonnes nullable
|
||||
`task_kind` et `task_kind_version`, puis les migrations v2→v3. Les anciennes lignes restent
|
||||
`NULL/NULL`, sans type inventé et sans perte des projets, tâches, checkpoints ou
|
||||
artefacts. Une interruption ou erreur provoque un rollback complet. Les DB v1
|
||||
à v12 sont migrées séquentiellement vers v13.
|
||||
à v13 sont migrées séquentiellement vers v14.
|
||||
Une v10 publiée est validée comme telle avant que v10→v11 crée
|
||||
`matcher_tasks` ; son absence n'est donc pas une corruption. Une version future est refusée et une DB contenant
|
||||
des tables sans métadonnée de version est considérée corrompue. La fonction
|
||||
interne de migration applique uniquement la chaîne séquentielle connue jusqu'à
|
||||
v13 ; une valeur hors de 1..13 est refusée. La failure injectée v12 rollbacke
|
||||
v14 ; une valeur hors de 1..14 est refusée. La failure injectée v12 rollbacke
|
||||
la table, l'index et le changement de version, laissant une vraie v11 utilisable.
|
||||
La failure injectée v13 conserve une vraie v12 sans `geometric_verifier_tasks` ;
|
||||
un retry applique ensuite v12→v13.
|
||||
un retry applique ensuite v12→v13. La failure injectée v14 conserve une vraie
|
||||
v13 sans `track_sets` ; un retry applique ensuite v13→v14.
|
||||
|
||||
Migration v1→v2 exacte, exécutée entre `BEGIN IMMEDIATE` et `COMMIT` :
|
||||
|
||||
|
|
@ -608,8 +701,8 @@ ouvert.
|
|||
|
||||
## Statut
|
||||
|
||||
**IMPLEMENTED** — SQLite système, schéma v13 et migrations séquentielles
|
||||
v1→v2→v3→v4→v5→v6→v7→v8→v9→v10→v11→v12→v13, identité projet, transactions
|
||||
**IMPLEMENTED** — SQLite système, schéma v14 et migrations séquentielles
|
||||
v1→v2→v3→v4→v5→v6→v7→v8→v9→v10→v11→v12→v13→v14, identité projet, transactions
|
||||
tâche+checkpoint, pagination de reprise et artefacts génériques.
|
||||
|
||||
**IMPLEMENTED** — ouverture/fermeture avec le projet, identité INI/DB cohérente,
|
||||
|
|
@ -634,6 +727,20 @@ positionne `metadata.legacy_image_catalog_pending=1` dès qu'une ancienne tâche
|
|||
d'import existe. Cet indicateur signifie « données legacy potentiellement non
|
||||
cataloguées », pas « images migrées ».
|
||||
|
||||
**IMPLEMENTED** — Track Model v1 : tables `track_sets`, `tracks` et
|
||||
`track_observations` créées par la migration v13→v14, contraintes SQL
|
||||
(unicité d'identité de reuse, CASCADE, observation unique par set) et
|
||||
tests de migration/failure validés. Le schéma complet et les invariants
|
||||
sont documentés dans `tracks.md`.
|
||||
|
||||
**IMPLEMENTED** — API C Track Model v1 : header `project_db.h` et
|
||||
source `project_db.c` exposent `create_track_set`, `load_track_set`,
|
||||
`find_track_set`, `list_track_sets`, `load_track`, `list_tracks`,
|
||||
`find_track_by_observation` et `free_track`. Contraintes et
|
||||
invariants documentés dans `tracks.md`. Limites réelles : pas de
|
||||
Track Builder algorithmique, pas de tâche dédiée, pas de
|
||||
triangulation.
|
||||
|
||||
**NOT_YET_WIRED** — autosave à toutes les transitions, retry UI des sources
|
||||
indisponibles, migration de la TUI legacy et réconciliation des fichiers
|
||||
orphelins et compaction Visual Index. Feature Store et Visual Index v1 sont implémentés.
|
||||
|
|
|
|||
|
|
@ -122,7 +122,9 @@ USAC/MAGSAC avec configuration, seed et fingerprint déterministes.
|
|||
| **Lien avec le catalogue** | Les tracks référenceront les images par `image_id`, jamais par nom ou chemin. |
|
||||
| **Persistance** | Les tracks sont persistés entre les sessions de traitement. Un track ne peut être détruit que par une action explicite de l'utilisateur. |
|
||||
|
||||
**Statut :** PLANNED — aucune implémentation existante.
|
||||
**Statut :** PARTIAL — le Track Model v1 (persistance) est implémenté dans
|
||||
Project DB v14 (`track_sets`, `tracks`, `track_observations`). Le Track
|
||||
Builder, la triangulation et le Sparse SfM restent PLANNED.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
523
docs/architecture/tracks.md
Normal file
523
docs/architecture/tracks.md
Normal 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.
|
||||
|
|
@ -1,5 +1,14 @@
|
|||
# Matching & Tracks
|
||||
|
||||
> **Document historique.** Ce document décrit la vision conceptuelle initiale
|
||||
> du matching et des tracks. Le contrat persistant réel du Track Model v1
|
||||
> (schéma, invariants, API) est documenté dans
|
||||
> [tracks.md](../architecture/tracks.md). Les différences notables :
|
||||
> le Track Model v1 ne contient aucune coordonnée 3D, aucun statut
|
||||
> (ACTIVE/OPTIMIZED/REJECTED), aucune matrice de co-visibilité et aucun
|
||||
> plafond de longueur arbitraire. La triangulation, le Sparse SfM et le
|
||||
> Bundle Adjustment sont des étapes ultérieures séparées.
|
||||
|
||||
> Frontière v1A : les groupes de support ORB/SIFT sont uniquement des preuves
|
||||
> locales intra-image. Ils ne comparent pas les espaces Hamming et L2, ne sont
|
||||
> pas des matches multivues et ne créent aucun track. Le futur matcher choisira
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
#include <lardon3d/task.h>
|
||||
|
||||
enum {
|
||||
LARDON3D_PROJECT_DB_SCHEMA_VERSION = 13,
|
||||
LARDON3D_PROJECT_DB_SCHEMA_VERSION = 14,
|
||||
LARDON3D_PROJECT_DB_ID_CAPACITY = 65,
|
||||
LARDON3D_PROJECT_DB_KIND_CAPACITY = 65,
|
||||
LARDON3D_PROJECT_DB_PATH_CAPACITY = 4096,
|
||||
|
|
@ -19,6 +19,7 @@ enum {
|
|||
LARDON3D_PROJECT_DB_CANDIDATE_PAIR_PAGE_MAX = 256,
|
||||
LARDON3D_PROJECT_DB_MATCH_RESULT_PAGE_MAX = 256,
|
||||
LARDON3D_PROJECT_DB_GEOMETRIC_RESULT_PAGE_MAX = 256,
|
||||
LARDON3D_PROJECT_DB_TRACK_PAGE_MAX = 64,
|
||||
LARDON3D_PROJECT_DB_INLIER_MASK_MAX = 1024,
|
||||
LARDON3D_PROJECT_DB_FUNDAMENTAL_COEFFICIENTS = 9,
|
||||
LARDON3D_PROJECT_DB_SCANSET_NAME_CAPACITY = 256,
|
||||
|
|
@ -347,6 +348,55 @@ typedef struct {
|
|||
bool exclude_same_asset;
|
||||
} Lardon3DProjectDbCandidatePairGenerateTask;
|
||||
|
||||
typedef struct {
|
||||
uint64_t feature_set_id;
|
||||
uint32_t feature_index;
|
||||
uint32_t position_in_track;
|
||||
} Lardon3DProjectDbTrackObservation;
|
||||
|
||||
typedef struct {
|
||||
uint64_t track_id;
|
||||
uint64_t track_set_id;
|
||||
uint32_t observation_count;
|
||||
Lardon3DProjectDbTrackObservation *observations;
|
||||
} Lardon3DProjectDbTrack;
|
||||
|
||||
typedef struct {
|
||||
uint64_t track_set_id;
|
||||
char builder_kind[LARDON3D_PROJECT_DB_KIND_CAPACITY];
|
||||
uint32_t builder_version;
|
||||
unsigned char parameter_fingerprint[LARDON3D_PROJECT_DB_SHA256_SIZE];
|
||||
int verifier_kind;
|
||||
uint32_t verifier_version;
|
||||
unsigned char verifier_fingerprint[LARDON3D_PROJECT_DB_SHA256_SIZE];
|
||||
unsigned char input_scope_hash[LARDON3D_PROJECT_DB_SHA256_SIZE];
|
||||
uint64_t gvr_count;
|
||||
uint64_t track_count;
|
||||
int64_t created_at;
|
||||
} Lardon3DProjectDbTrackSet;
|
||||
|
||||
void lardon3d_project_db_free_track(Lardon3DProjectDbTrack *track);
|
||||
Lardon3DProjectDbResult lardon3d_project_db_create_track_set(
|
||||
Lardon3DProjectDb *database, const Lardon3DProjectDbTrackSet *configuration,
|
||||
const Lardon3DProjectDbTrack *tracks, size_t track_count,
|
||||
Lardon3DProjectDbTrackSet *published);
|
||||
Lardon3DProjectDbResult lardon3d_project_db_load_track_set(
|
||||
Lardon3DProjectDb *database, uint64_t track_set_id, Lardon3DProjectDbTrackSet *track_set);
|
||||
Lardon3DProjectDbResult lardon3d_project_db_find_track_set(
|
||||
Lardon3DProjectDb *database, const Lardon3DProjectDbTrackSet *identity,
|
||||
Lardon3DProjectDbTrackSet *track_set);
|
||||
Lardon3DProjectDbResult lardon3d_project_db_list_track_sets(
|
||||
Lardon3DProjectDb *database, uint64_t after_track_set_id, Lardon3DProjectDbTrackSet *track_sets,
|
||||
size_t capacity, size_t *count);
|
||||
Lardon3DProjectDbResult lardon3d_project_db_load_track(
|
||||
Lardon3DProjectDb *database, uint64_t track_id, Lardon3DProjectDbTrack *track);
|
||||
Lardon3DProjectDbResult lardon3d_project_db_list_tracks(
|
||||
Lardon3DProjectDb *database, uint64_t track_set_id, uint64_t after_track_id,
|
||||
Lardon3DProjectDbTrack *tracks, size_t capacity, size_t *count);
|
||||
Lardon3DProjectDbResult lardon3d_project_db_find_track_by_observation(
|
||||
Lardon3DProjectDb *database, uint64_t track_set_id, uint64_t feature_set_id,
|
||||
uint32_t feature_index, Lardon3DProjectDbTrack *track);
|
||||
|
||||
Lardon3DProjectDbResult lardon3d_project_db_open(const char *path, Lardon3DProjectDb **database,
|
||||
char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]);
|
||||
void lardon3d_project_db_close(Lardon3DProjectDb *database);
|
||||
|
|
|
|||
636
src/project_db.c
636
src/project_db.c
|
|
@ -9,6 +9,7 @@
|
|||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <time.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 "
|
||||
"geometric_verification_results(match_result_id,geometric_verification_result_id);";
|
||||
|
||||
static const char schema_track_v14[] =
|
||||
"CREATE TABLE track_sets(track_set_id INTEGER PRIMARY KEY AUTOINCREMENT "
|
||||
"CHECK(track_set_id>0),builder_kind TEXT NOT NULL CHECK(length(builder_kind)>0 AND "
|
||||
"length(builder_kind)<=64),builder_version INTEGER NOT NULL CHECK(builder_version>0),"
|
||||
"parameter_fingerprint BLOB NOT NULL CHECK(length(parameter_fingerprint)=32),"
|
||||
"verifier_kind INTEGER NOT NULL CHECK(verifier_kind>0),verifier_version INTEGER NOT NULL "
|
||||
"CHECK(verifier_version>0),verifier_fingerprint BLOB NOT NULL "
|
||||
"CHECK(length(verifier_fingerprint)=32),input_scope_hash BLOB NOT NULL "
|
||||
"CHECK(length(input_scope_hash)=32),gvr_count INTEGER NOT NULL CHECK(gvr_count>=1),"
|
||||
"track_count INTEGER NOT NULL CHECK(track_count>=0),created_at INTEGER NOT NULL "
|
||||
"CHECK(created_at>=0),UNIQUE(builder_kind,builder_version,parameter_fingerprint,"
|
||||
"verifier_kind,verifier_version,verifier_fingerprint,input_scope_hash));"
|
||||
"CREATE TABLE tracks(track_id INTEGER PRIMARY KEY AUTOINCREMENT CHECK(track_id>0),"
|
||||
"track_set_id INTEGER NOT NULL REFERENCES track_sets(track_set_id) ON DELETE CASCADE,"
|
||||
"observation_count INTEGER NOT NULL CHECK(observation_count>=2));"
|
||||
"CREATE INDEX tracks_set_idx ON tracks(track_set_id,track_id);"
|
||||
"CREATE TABLE track_observations(track_set_id INTEGER NOT NULL,"
|
||||
"track_id INTEGER NOT NULL REFERENCES tracks(track_id) ON DELETE CASCADE,"
|
||||
"feature_set_id INTEGER NOT NULL REFERENCES feature_sets(feature_set_id),"
|
||||
"feature_index INTEGER NOT NULL CHECK(feature_index>=0),"
|
||||
"position_in_track INTEGER NOT NULL CHECK(position_in_track>=0),"
|
||||
"PRIMARY KEY(track_set_id,feature_set_id,feature_index),"
|
||||
"UNIQUE(track_id,position_in_track));"
|
||||
"CREATE INDEX track_observations_lookup_idx ON "
|
||||
"track_observations(feature_set_id,feature_index,track_set_id);";
|
||||
|
||||
static const char schema_geometric_verifier_task_v13[] =
|
||||
"CREATE TABLE geometric_verifier_tasks("
|
||||
"task_id INTEGER PRIMARY KEY REFERENCES tasks(task_id) ON DELETE CASCADE,"
|
||||
|
|
@ -366,7 +393,7 @@ static Lardon3DProjectDbResult migrate(Lardon3DProjectDb *database, unsigned int
|
|||
if (from_version != 0 && from_version != 1 && from_version != 2 && from_version != 3 &&
|
||||
from_version != 4 && from_version != 5 && from_version != 6 && from_version != 7 &&
|
||||
from_version != 8 && from_version != 9 && from_version != 10 && from_version != 11 &&
|
||||
from_version != 12) {
|
||||
from_version != 12 && from_version != 13) {
|
||||
return LARDON3D_PROJECT_DB_CORRUPT;
|
||||
}
|
||||
Lardon3DProjectDbResult result = execute(database, "BEGIN IMMEDIATE", "begin migration");
|
||||
|
|
@ -639,6 +666,21 @@ static Lardon3DProjectDbResult migrate(Lardon3DProjectDb *database, unsigned int
|
|||
"finish schema v13 migration");
|
||||
}
|
||||
}
|
||||
if (result == LARDON3D_PROJECT_DB_OK && from_version < 14) {
|
||||
result = execute(database, schema_track_v14, "migrate schema v13 to v14");
|
||||
#ifdef LARDON3D_PROJECT_DB_TESTING
|
||||
const char *forced_failure = getenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V14");
|
||||
if (result == LARDON3D_PROJECT_DB_OK && forced_failure && strcmp(forced_failure, "1") == 0) {
|
||||
result = execute(database, "INSERT INTO missing_test_table VALUES(1)",
|
||||
"forced migration v14 failure");
|
||||
}
|
||||
#endif
|
||||
if (result == LARDON3D_PROJECT_DB_OK) {
|
||||
result = execute(database,
|
||||
"UPDATE metadata SET value=14 WHERE key='schema_version' AND value=13",
|
||||
"finish schema v14 migration");
|
||||
}
|
||||
}
|
||||
if (result == LARDON3D_PROJECT_DB_OK) {
|
||||
result = execute(database, "COMMIT", "commit migration");
|
||||
}
|
||||
|
|
@ -755,7 +797,10 @@ Lardon3DProjectDbResult lardon3d_project_db_open(const char *path, Lardon3DProje
|
|||
"matcher_tasks",
|
||||
"match_results",
|
||||
"geometric_verification_results",
|
||||
"geometric_verifier_tasks"};
|
||||
"geometric_verifier_tasks",
|
||||
"track_sets",
|
||||
"tracks",
|
||||
"track_observations"};
|
||||
for (size_t index = 0; index < sizeof(required) / sizeof(required[0]) &&
|
||||
result == LARDON3D_PROJECT_DB_OK;
|
||||
++index) {
|
||||
|
|
@ -4306,6 +4351,593 @@ Lardon3DProjectDbResult lardon3d_project_db_list_geometric_verification_results(
|
|||
return db_result;
|
||||
}
|
||||
|
||||
/* Track persistence is deliberately kept as a small, self-contained API. The
|
||||
* caller owns input arrays; loaded observation arrays are owned by the Track
|
||||
* value until lardon3d_project_db_free_track(). */
|
||||
static bool valid_track_identity(const Lardon3DProjectDbTrackSet *set) {
|
||||
return set && set->builder_kind[0] != '\0' && strlen(set->builder_kind) < 65 &&
|
||||
set->builder_version > 0 && set->verifier_kind > 0 && set->verifier_version > 0 &&
|
||||
set->gvr_count > 0 &&
|
||||
set->created_at >= 0;
|
||||
}
|
||||
|
||||
static Lardon3DProjectDbResult read_track_set(sqlite3_stmt *statement,
|
||||
Lardon3DProjectDbTrackSet *set) {
|
||||
sqlite3_int64 id = sqlite3_column_int64(statement, 0);
|
||||
sqlite3_int64 builder_version = sqlite3_column_int64(statement, 2);
|
||||
sqlite3_int64 verifier_kind = sqlite3_column_int64(statement, 4);
|
||||
sqlite3_int64 verifier_version = sqlite3_column_int64(statement, 5);
|
||||
sqlite3_int64 gvr_count = sqlite3_column_int64(statement, 8);
|
||||
sqlite3_int64 track_count = sqlite3_column_int64(statement, 9);
|
||||
const unsigned char *builder = sqlite3_column_text(statement, 1);
|
||||
const void *parameter = sqlite3_column_blob(statement, 3);
|
||||
const void *verifier = sqlite3_column_blob(statement, 6);
|
||||
const void *scope = sqlite3_column_blob(statement, 7);
|
||||
if (id <= 0 || builder_version <= 0 || verifier_kind <= 0 || verifier_version <= 0 ||
|
||||
gvr_count <= 0 || track_count < 0 || sqlite3_column_bytes(statement, 3) != 32 ||
|
||||
sqlite3_column_bytes(statement, 6) != 32 || sqlite3_column_bytes(statement, 7) != 32 ||
|
||||
!builder || !parameter || !verifier || !scope || strlen((const char *)builder) >= 65) {
|
||||
return LARDON3D_PROJECT_DB_CORRUPT;
|
||||
}
|
||||
memset(set, 0, sizeof(*set));
|
||||
set->track_set_id = (uint64_t)id;
|
||||
(void)snprintf(set->builder_kind, sizeof(set->builder_kind), "%s", (const char *)builder);
|
||||
set->builder_version = (uint32_t)builder_version;
|
||||
memcpy(set->parameter_fingerprint, parameter, 32);
|
||||
set->verifier_kind = (int)verifier_kind;
|
||||
set->verifier_version = (uint32_t)verifier_version;
|
||||
memcpy(set->verifier_fingerprint, verifier, 32);
|
||||
memcpy(set->input_scope_hash, scope, 32);
|
||||
set->gvr_count = (uint64_t)gvr_count;
|
||||
set->track_count = (uint64_t)track_count;
|
||||
set->created_at = sqlite3_column_int64(statement, 10);
|
||||
return set->created_at >= 0 ? LARDON3D_PROJECT_DB_OK : LARDON3D_PROJECT_DB_CORRUPT;
|
||||
}
|
||||
|
||||
void lardon3d_project_db_free_track(Lardon3DProjectDbTrack *track) {
|
||||
if (!track) {
|
||||
return;
|
||||
}
|
||||
free(track->observations);
|
||||
memset(track, 0, sizeof(*track));
|
||||
}
|
||||
|
||||
static void bind_track_identity(sqlite3_stmt *statement,
|
||||
const Lardon3DProjectDbTrackSet *set, int first) {
|
||||
(void)sqlite3_bind_text(statement, first, set->builder_kind, -1, SQLITE_TRANSIENT);
|
||||
(void)sqlite3_bind_int64(statement, first + 1, set->builder_version);
|
||||
(void)sqlite3_bind_blob(statement, first + 2, set->parameter_fingerprint, 32, SQLITE_TRANSIENT);
|
||||
(void)sqlite3_bind_int(statement, first + 3, set->verifier_kind);
|
||||
(void)sqlite3_bind_int64(statement, first + 4, set->verifier_version);
|
||||
(void)sqlite3_bind_blob(statement, first + 5, set->verifier_fingerprint, 32, SQLITE_TRANSIENT);
|
||||
(void)sqlite3_bind_blob(statement, first + 6, set->input_scope_hash, 32, SQLITE_TRANSIENT);
|
||||
}
|
||||
|
||||
static Lardon3DProjectDbResult read_track_set_by_statement(
|
||||
Lardon3DProjectDb *database, sqlite3_stmt *statement, Lardon3DProjectDbTrackSet *set) {
|
||||
int code = sqlite3_step(statement);
|
||||
Lardon3DProjectDbResult result = code == SQLITE_ROW ? read_track_set(statement, set)
|
||||
: LARDON3D_PROJECT_DB_NOT_FOUND;
|
||||
if (code != SQLITE_ROW && code != SQLITE_DONE) {
|
||||
result = sqlite_result(database, code, "read track set");
|
||||
}
|
||||
(void)sqlite3_finalize(statement);
|
||||
return result;
|
||||
}
|
||||
|
||||
Lardon3DProjectDbResult lardon3d_project_db_load_track_set(
|
||||
Lardon3DProjectDb *database, uint64_t track_set_id, Lardon3DProjectDbTrackSet *track_set) {
|
||||
if (!database || !valid_catalog_id(track_set_id) || !track_set) {
|
||||
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
|
||||
}
|
||||
memset(track_set, 0, sizeof(*track_set));
|
||||
(void)pthread_mutex_lock(&database->mutex);
|
||||
sqlite3_stmt *statement = NULL;
|
||||
Lardon3DProjectDbResult result = prepare(
|
||||
database,
|
||||
"SELECT track_set_id,builder_kind,builder_version,parameter_fingerprint,verifier_kind,"
|
||||
"verifier_version,verifier_fingerprint,input_scope_hash,gvr_count,track_count,created_at "
|
||||
"FROM track_sets WHERE track_set_id=?1",
|
||||
&statement);
|
||||
if (result == LARDON3D_PROJECT_DB_OK) {
|
||||
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)track_set_id);
|
||||
result = read_track_set_by_statement(database, statement, track_set);
|
||||
}
|
||||
(void)pthread_mutex_unlock(&database->mutex);
|
||||
return result;
|
||||
}
|
||||
|
||||
Lardon3DProjectDbResult lardon3d_project_db_find_track_set(
|
||||
Lardon3DProjectDb *database, const Lardon3DProjectDbTrackSet *identity,
|
||||
Lardon3DProjectDbTrackSet *track_set) {
|
||||
if (!database || !valid_track_identity(identity) || !track_set) {
|
||||
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
|
||||
}
|
||||
memset(track_set, 0, sizeof(*track_set));
|
||||
(void)pthread_mutex_lock(&database->mutex);
|
||||
sqlite3_stmt *statement = NULL;
|
||||
Lardon3DProjectDbResult result = prepare(
|
||||
database,
|
||||
"SELECT track_set_id,builder_kind,builder_version,parameter_fingerprint,verifier_kind,"
|
||||
"verifier_version,verifier_fingerprint,input_scope_hash,gvr_count,track_count,created_at "
|
||||
"FROM track_sets WHERE builder_kind=?1 AND builder_version=?2 AND parameter_fingerprint=?3 "
|
||||
"AND verifier_kind=?4 AND verifier_version=?5 AND verifier_fingerprint=?6 "
|
||||
"AND input_scope_hash=?7",
|
||||
&statement);
|
||||
if (result == LARDON3D_PROJECT_DB_OK) {
|
||||
bind_track_identity(statement, identity, 1);
|
||||
result = read_track_set_by_statement(database, statement, track_set);
|
||||
}
|
||||
(void)pthread_mutex_unlock(&database->mutex);
|
||||
return result;
|
||||
}
|
||||
|
||||
Lardon3DProjectDbResult lardon3d_project_db_list_track_sets(
|
||||
Lardon3DProjectDb *database, uint64_t after_track_set_id, Lardon3DProjectDbTrackSet *track_sets,
|
||||
size_t capacity, size_t *count) {
|
||||
if (!database || !track_sets || !count || capacity == 0 ||
|
||||
capacity > LARDON3D_PROJECT_DB_TRACK_PAGE_MAX) {
|
||||
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
|
||||
}
|
||||
*count = 0;
|
||||
(void)pthread_mutex_lock(&database->mutex);
|
||||
sqlite3_stmt *statement = NULL;
|
||||
Lardon3DProjectDbResult result = prepare(
|
||||
database,
|
||||
"SELECT track_set_id,builder_kind,builder_version,parameter_fingerprint,verifier_kind,"
|
||||
"verifier_version,verifier_fingerprint,input_scope_hash,gvr_count,track_count,created_at "
|
||||
"FROM track_sets WHERE track_set_id>?1 ORDER BY track_set_id LIMIT ?2",
|
||||
&statement);
|
||||
if (result == LARDON3D_PROJECT_DB_OK) {
|
||||
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)after_track_set_id);
|
||||
(void)sqlite3_bind_int64(statement, 2, (sqlite3_int64)capacity);
|
||||
int code;
|
||||
while ((code = sqlite3_step(statement)) == SQLITE_ROW) {
|
||||
result = read_track_set(statement, &track_sets[*count]);
|
||||
if (result != LARDON3D_PROJECT_DB_OK) {
|
||||
break;
|
||||
}
|
||||
++*count;
|
||||
}
|
||||
if (result == LARDON3D_PROJECT_DB_OK && code == SQLITE_DONE) {
|
||||
result = LARDON3D_PROJECT_DB_OK;
|
||||
} else if (result == LARDON3D_PROJECT_DB_OK) {
|
||||
result = sqlite_result(database, code, "list track sets");
|
||||
}
|
||||
(void)sqlite3_finalize(statement);
|
||||
}
|
||||
(void)pthread_mutex_unlock(&database->mutex);
|
||||
return result;
|
||||
}
|
||||
|
||||
static Lardon3DProjectDbResult validate_track_observations_locked(
|
||||
Lardon3DProjectDb *database, const Lardon3DProjectDbTrack *tracks, size_t track_count) {
|
||||
for (size_t track_index = 0; track_index < track_count; ++track_index) {
|
||||
const Lardon3DProjectDbTrack *track = &tracks[track_index];
|
||||
if (!track->observations || track->observation_count < 2) {
|
||||
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
|
||||
}
|
||||
for (uint32_t observation_index = 0; observation_index < track->observation_count;
|
||||
++observation_index) {
|
||||
const Lardon3DProjectDbTrackObservation *observation = &track->observations[observation_index];
|
||||
if (observation->position_in_track != observation_index || observation->feature_set_id == 0) {
|
||||
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
|
||||
}
|
||||
sqlite3_stmt *statement = NULL;
|
||||
Lardon3DProjectDbResult result = prepare(
|
||||
database, "SELECT image_id,feature_count FROM feature_sets WHERE feature_set_id=?1",
|
||||
&statement);
|
||||
if (result != LARDON3D_PROJECT_DB_OK) {
|
||||
return result;
|
||||
}
|
||||
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)observation->feature_set_id);
|
||||
int code = sqlite3_step(statement);
|
||||
sqlite3_int64 image_id = code == SQLITE_ROW ? sqlite3_column_int64(statement, 0) : 0;
|
||||
sqlite3_int64 feature_count = code == SQLITE_ROW ? sqlite3_column_int64(statement, 1) : 0;
|
||||
(void)sqlite3_finalize(statement);
|
||||
if (code != SQLITE_ROW) {
|
||||
return code == SQLITE_DONE ? LARDON3D_PROJECT_DB_NOT_FOUND
|
||||
: sqlite_result(database, code, "validate track feature set");
|
||||
}
|
||||
if (image_id <= 0 || feature_count < 0 || (uint64_t)observation->feature_index >=
|
||||
(uint64_t)feature_count) {
|
||||
return LARDON3D_PROJECT_DB_CONSTRAINT;
|
||||
}
|
||||
for (uint32_t prior = 0; prior < observation_index; ++prior) {
|
||||
const Lardon3DProjectDbTrackObservation *old = &track->observations[prior];
|
||||
if (old->feature_set_id == observation->feature_set_id &&
|
||||
old->feature_index == observation->feature_index) {
|
||||
return LARDON3D_PROJECT_DB_CONSTRAINT;
|
||||
}
|
||||
sqlite3_stmt *image_statement = NULL;
|
||||
result = prepare(database, "SELECT image_id FROM feature_sets WHERE feature_set_id=?1",
|
||||
&image_statement);
|
||||
if (result != LARDON3D_PROJECT_DB_OK) {
|
||||
return result;
|
||||
}
|
||||
(void)sqlite3_bind_int64(image_statement, 1, (sqlite3_int64)old->feature_set_id);
|
||||
code = sqlite3_step(image_statement);
|
||||
sqlite3_int64 old_image = code == SQLITE_ROW ? sqlite3_column_int64(image_statement, 0) : 0;
|
||||
(void)sqlite3_finalize(image_statement);
|
||||
if (code != SQLITE_ROW) {
|
||||
return LARDON3D_PROJECT_DB_CORRUPT;
|
||||
}
|
||||
if (old_image == image_id) {
|
||||
return LARDON3D_PROJECT_DB_CONSTRAINT;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (size_t first = 0; first < track_count; ++first) {
|
||||
for (size_t second = first + 1; second < track_count; ++second) {
|
||||
for (uint32_t a = 0; a < tracks[first].observation_count; ++a) {
|
||||
for (uint32_t b = 0; b < tracks[second].observation_count; ++b) {
|
||||
if (tracks[first].observations[a].feature_set_id == tracks[second].observations[b].feature_set_id &&
|
||||
tracks[first].observations[a].feature_index == tracks[second].observations[b].feature_index) {
|
||||
return LARDON3D_PROJECT_DB_CONSTRAINT;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return LARDON3D_PROJECT_DB_OK;
|
||||
}
|
||||
|
||||
Lardon3DProjectDbResult lardon3d_project_db_create_track_set(
|
||||
Lardon3DProjectDb *database, const Lardon3DProjectDbTrackSet *configuration,
|
||||
const Lardon3DProjectDbTrack *tracks, size_t track_count,
|
||||
Lardon3DProjectDbTrackSet *published) {
|
||||
if (!database || !valid_track_identity(configuration) || !published ||
|
||||
(track_count > 0 && !tracks)) {
|
||||
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
|
||||
}
|
||||
if (configuration->track_count != 0 && configuration->track_count != track_count) {
|
||||
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
|
||||
}
|
||||
memset(published, 0, sizeof(*published));
|
||||
(void)pthread_mutex_lock(&database->mutex);
|
||||
Lardon3DProjectDbResult result = execute(database, "BEGIN IMMEDIATE", "begin track set publish");
|
||||
int64_t publication_time = (int64_t)time(NULL);
|
||||
if (result == LARDON3D_PROJECT_DB_OK && publication_time < 0) {
|
||||
result = LARDON3D_PROJECT_DB_IO_ERROR;
|
||||
}
|
||||
bool reused = false;
|
||||
sqlite3_stmt *statement = NULL;
|
||||
if (result == LARDON3D_PROJECT_DB_OK) {
|
||||
result = prepare(database,
|
||||
"SELECT track_set_id,builder_kind,builder_version,parameter_fingerprint,"
|
||||
"verifier_kind,verifier_version,verifier_fingerprint,input_scope_hash,gvr_count,"
|
||||
"track_count,created_at FROM track_sets WHERE builder_kind=?1 AND "
|
||||
"builder_version=?2 AND parameter_fingerprint=?3 AND verifier_kind=?4 AND "
|
||||
"verifier_version=?5 AND verifier_fingerprint=?6 AND input_scope_hash=?7",
|
||||
&statement);
|
||||
if (result == LARDON3D_PROJECT_DB_OK) {
|
||||
bind_track_identity(statement, configuration, 1);
|
||||
int code = sqlite3_step(statement);
|
||||
if (code == SQLITE_ROW) {
|
||||
result = read_track_set(statement, published);
|
||||
if (result == LARDON3D_PROJECT_DB_OK &&
|
||||
published->gvr_count != configuration->gvr_count) {
|
||||
result = LARDON3D_PROJECT_DB_CONSTRAINT;
|
||||
}
|
||||
reused = result == LARDON3D_PROJECT_DB_OK;
|
||||
} else if (code != SQLITE_DONE) {
|
||||
result = sqlite_result(database, code, "find reusable track set");
|
||||
} else {
|
||||
result = validate_track_observations_locked(database, tracks, track_count);
|
||||
}
|
||||
(void)sqlite3_finalize(statement);
|
||||
statement = NULL;
|
||||
}
|
||||
}
|
||||
if (result == LARDON3D_PROJECT_DB_OK && published->track_set_id == 0) {
|
||||
result = prepare(database,
|
||||
"INSERT INTO track_sets(builder_kind,builder_version,parameter_fingerprint,"
|
||||
"verifier_kind,verifier_version,verifier_fingerprint,input_scope_hash,gvr_count,"
|
||||
"track_count,created_at) VALUES(?1,?2,?3,?4,?5,?6,?7,?8,?9,?10)",
|
||||
&statement);
|
||||
if (result == LARDON3D_PROJECT_DB_OK) {
|
||||
bind_track_identity(statement, configuration, 1);
|
||||
(void)sqlite3_bind_int64(statement, 8, (sqlite3_int64)configuration->gvr_count);
|
||||
(void)sqlite3_bind_int64(statement, 9, (sqlite3_int64)track_count);
|
||||
(void)sqlite3_bind_int64(statement, 10, publication_time);
|
||||
result = step_done(database, statement, "insert track set");
|
||||
statement = NULL;
|
||||
if (result == LARDON3D_PROJECT_DB_OK) {
|
||||
sqlite3_int64 id = sqlite3_last_insert_rowid(database->connection);
|
||||
if (id <= 0) {
|
||||
result = LARDON3D_PROJECT_DB_CORRUPT;
|
||||
} else {
|
||||
published->track_set_id = (uint64_t)id;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (size_t index = 0; result == LARDON3D_PROJECT_DB_OK && index < track_count; ++index) {
|
||||
const Lardon3DProjectDbTrack *track = &tracks[index];
|
||||
result = prepare(database,
|
||||
"INSERT INTO tracks(track_set_id,observation_count) VALUES(?1,?2)",
|
||||
&statement);
|
||||
if (result != LARDON3D_PROJECT_DB_OK) {
|
||||
break;
|
||||
}
|
||||
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)published->track_set_id);
|
||||
(void)sqlite3_bind_int64(statement, 2, track->observation_count);
|
||||
result = step_done(database, statement, "insert track");
|
||||
statement = NULL;
|
||||
if (result != LARDON3D_PROJECT_DB_OK) {
|
||||
break;
|
||||
}
|
||||
sqlite3_int64 track_id = sqlite3_last_insert_rowid(database->connection);
|
||||
if (track_id <= 0) {
|
||||
result = LARDON3D_PROJECT_DB_CORRUPT;
|
||||
break;
|
||||
}
|
||||
for (uint32_t observation = 0;
|
||||
result == LARDON3D_PROJECT_DB_OK && observation < track->observation_count; ++observation) {
|
||||
const Lardon3DProjectDbTrackObservation *item = &track->observations[observation];
|
||||
result = prepare(database,
|
||||
"INSERT INTO track_observations(track_set_id,track_id,feature_set_id,"
|
||||
"feature_index,position_in_track) VALUES(?1,?2,?3,?4,?5)",
|
||||
&statement);
|
||||
if (result != LARDON3D_PROJECT_DB_OK) {
|
||||
break;
|
||||
}
|
||||
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)published->track_set_id);
|
||||
(void)sqlite3_bind_int64(statement, 2, track_id);
|
||||
(void)sqlite3_bind_int64(statement, 3, (sqlite3_int64)item->feature_set_id);
|
||||
(void)sqlite3_bind_int64(statement, 4, item->feature_index);
|
||||
(void)sqlite3_bind_int64(statement, 5, item->position_in_track);
|
||||
result = step_done(database, statement, "insert track observation");
|
||||
statement = NULL;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (result == LARDON3D_PROJECT_DB_OK) {
|
||||
result = execute(database, "COMMIT", "commit track set publish");
|
||||
} else {
|
||||
if (statement) {
|
||||
(void)sqlite3_finalize(statement);
|
||||
}
|
||||
(void)execute(database, "ROLLBACK", "rollback track set publish");
|
||||
}
|
||||
uint64_t published_id = published->track_set_id;
|
||||
(void)pthread_mutex_unlock(&database->mutex);
|
||||
if (result == LARDON3D_PROJECT_DB_OK && published_id != 0 && !reused) {
|
||||
*published = *configuration;
|
||||
published->track_set_id = published_id;
|
||||
published->track_count = track_count;
|
||||
published->created_at = publication_time;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static Lardon3DProjectDbResult load_track_locked(Lardon3DProjectDb *database, uint64_t track_id,
|
||||
Lardon3DProjectDbTrack *track) {
|
||||
sqlite3_stmt *statement = NULL;
|
||||
Lardon3DProjectDbResult result = prepare(
|
||||
database, "SELECT track_id,track_set_id,observation_count FROM tracks WHERE track_id=?1",
|
||||
&statement);
|
||||
if (result != LARDON3D_PROJECT_DB_OK) {
|
||||
return result;
|
||||
}
|
||||
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)track_id);
|
||||
int code = sqlite3_step(statement);
|
||||
if (code != SQLITE_ROW) {
|
||||
result = code == SQLITE_DONE ? LARDON3D_PROJECT_DB_NOT_FOUND
|
||||
: sqlite_result(database, code, "load track");
|
||||
(void)sqlite3_finalize(statement);
|
||||
return result;
|
||||
}
|
||||
sqlite3_int64 set_id = sqlite3_column_int64(statement, 1);
|
||||
sqlite3_int64 count = sqlite3_column_int64(statement, 2);
|
||||
(void)sqlite3_finalize(statement);
|
||||
if (set_id <= 0 || count < 2 || (uint64_t)count > SIZE_MAX / sizeof(*track->observations)) {
|
||||
return LARDON3D_PROJECT_DB_CORRUPT;
|
||||
}
|
||||
sqlite3_stmt *set_statement = NULL;
|
||||
result = prepare(database, "SELECT 1 FROM track_sets WHERE track_set_id=?1", &set_statement);
|
||||
if (result != LARDON3D_PROJECT_DB_OK) {
|
||||
return result;
|
||||
}
|
||||
(void)sqlite3_bind_int64(set_statement, 1, set_id);
|
||||
int set_code = sqlite3_step(set_statement);
|
||||
(void)sqlite3_finalize(set_statement);
|
||||
if (set_code != SQLITE_ROW) {
|
||||
return set_code == SQLITE_DONE ? LARDON3D_PROJECT_DB_CORRUPT
|
||||
: sqlite_result(database, set_code, "load track set parent");
|
||||
}
|
||||
track->observations = calloc((size_t)count, sizeof(*track->observations));
|
||||
if (!track->observations) {
|
||||
return LARDON3D_PROJECT_DB_IO_ERROR;
|
||||
}
|
||||
result = prepare(database,
|
||||
"SELECT feature_set_id,feature_index,position_in_track FROM track_observations "
|
||||
"WHERE track_id=?1 AND track_set_id=?2 ORDER BY position_in_track",
|
||||
&statement);
|
||||
if (result == LARDON3D_PROJECT_DB_OK) {
|
||||
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)track_id);
|
||||
(void)sqlite3_bind_int64(statement, 2, set_id);
|
||||
size_t index = 0;
|
||||
while ((code = sqlite3_step(statement)) == SQLITE_ROW) {
|
||||
if (index == (size_t)count || sqlite3_column_int64(statement, 2) != (sqlite3_int64)index) {
|
||||
result = LARDON3D_PROJECT_DB_CORRUPT;
|
||||
break;
|
||||
}
|
||||
sqlite3_int64 feature_set = sqlite3_column_int64(statement, 0);
|
||||
sqlite3_int64 feature_index = sqlite3_column_int64(statement, 1);
|
||||
if (feature_set <= 0 || feature_index < 0 || feature_index > UINT32_MAX) {
|
||||
result = LARDON3D_PROJECT_DB_CORRUPT;
|
||||
break;
|
||||
}
|
||||
sqlite3_stmt *feature_statement = NULL;
|
||||
result = prepare(database,
|
||||
"SELECT image_id,feature_count FROM feature_sets WHERE feature_set_id=?1",
|
||||
&feature_statement);
|
||||
if (result != LARDON3D_PROJECT_DB_OK) {
|
||||
break;
|
||||
}
|
||||
(void)sqlite3_bind_int64(feature_statement, 1, feature_set);
|
||||
int feature_code = sqlite3_step(feature_statement);
|
||||
sqlite3_int64 image_id = feature_code == SQLITE_ROW ? sqlite3_column_int64(feature_statement, 0) : 0;
|
||||
sqlite3_int64 feature_count = feature_code == SQLITE_ROW
|
||||
? sqlite3_column_int64(feature_statement, 1)
|
||||
: -1;
|
||||
(void)sqlite3_finalize(feature_statement);
|
||||
if (feature_code != SQLITE_ROW || image_id <= 0 || feature_count < 0 ||
|
||||
(uint64_t)feature_index >= (uint64_t)feature_count) {
|
||||
result = LARDON3D_PROJECT_DB_CORRUPT;
|
||||
break;
|
||||
}
|
||||
for (size_t prior = 0; prior < index; ++prior) {
|
||||
sqlite3_stmt *prior_statement = NULL;
|
||||
result = prepare(database, "SELECT image_id FROM feature_sets WHERE feature_set_id=?1",
|
||||
&prior_statement);
|
||||
if (result != LARDON3D_PROJECT_DB_OK) {
|
||||
break;
|
||||
}
|
||||
(void)sqlite3_bind_int64(prior_statement, 1,
|
||||
(sqlite3_int64)track->observations[prior].feature_set_id);
|
||||
int prior_code = sqlite3_step(prior_statement);
|
||||
sqlite3_int64 prior_image = prior_code == SQLITE_ROW
|
||||
? sqlite3_column_int64(prior_statement, 0)
|
||||
: 0;
|
||||
(void)sqlite3_finalize(prior_statement);
|
||||
if (prior_code != SQLITE_ROW || prior_image <= 0) {
|
||||
result = LARDON3D_PROJECT_DB_CORRUPT;
|
||||
break;
|
||||
}
|
||||
if (prior_image == image_id) {
|
||||
result = LARDON3D_PROJECT_DB_CORRUPT;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (result != LARDON3D_PROJECT_DB_OK) {
|
||||
break;
|
||||
}
|
||||
track->observations[index] = (Lardon3DProjectDbTrackObservation){
|
||||
.feature_set_id = (uint64_t)feature_set,
|
||||
.feature_index = (uint32_t)feature_index,
|
||||
.position_in_track = (uint32_t)index};
|
||||
++index;
|
||||
}
|
||||
if (result == LARDON3D_PROJECT_DB_OK && code != SQLITE_DONE) {
|
||||
result = sqlite_result(database, code, "read track observations");
|
||||
}
|
||||
if (result == LARDON3D_PROJECT_DB_OK && index != (size_t)count) {
|
||||
result = LARDON3D_PROJECT_DB_CORRUPT;
|
||||
}
|
||||
(void)sqlite3_finalize(statement);
|
||||
}
|
||||
if (result != LARDON3D_PROJECT_DB_OK) {
|
||||
lardon3d_project_db_free_track(track);
|
||||
return result;
|
||||
}
|
||||
track->track_id = track_id;
|
||||
track->track_set_id = (uint64_t)set_id;
|
||||
track->observation_count = (uint32_t)count;
|
||||
return LARDON3D_PROJECT_DB_OK;
|
||||
}
|
||||
|
||||
Lardon3DProjectDbResult lardon3d_project_db_load_track(
|
||||
Lardon3DProjectDb *database, uint64_t track_id, Lardon3DProjectDbTrack *track) {
|
||||
if (!database || !valid_catalog_id(track_id) || !track) {
|
||||
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
|
||||
}
|
||||
memset(track, 0, sizeof(*track));
|
||||
(void)pthread_mutex_lock(&database->mutex);
|
||||
Lardon3DProjectDbResult result = load_track_locked(database, track_id, track);
|
||||
(void)pthread_mutex_unlock(&database->mutex);
|
||||
return result;
|
||||
}
|
||||
|
||||
Lardon3DProjectDbResult lardon3d_project_db_list_tracks(
|
||||
Lardon3DProjectDb *database, uint64_t track_set_id, uint64_t after_track_id,
|
||||
Lardon3DProjectDbTrack *tracks, size_t capacity, size_t *count) {
|
||||
if (!database || !valid_catalog_id(track_set_id) || !tracks || !count || capacity == 0 ||
|
||||
capacity > LARDON3D_PROJECT_DB_TRACK_PAGE_MAX) {
|
||||
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
|
||||
}
|
||||
*count = 0;
|
||||
(void)pthread_mutex_lock(&database->mutex);
|
||||
sqlite3_stmt *statement = NULL;
|
||||
Lardon3DProjectDbResult result = prepare(
|
||||
database, "SELECT track_id FROM tracks WHERE track_set_id=?1 AND track_id>?2 "
|
||||
"ORDER BY track_id LIMIT ?3", &statement);
|
||||
if (result == LARDON3D_PROJECT_DB_OK) {
|
||||
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)track_set_id);
|
||||
(void)sqlite3_bind_int64(statement, 2, (sqlite3_int64)after_track_id);
|
||||
(void)sqlite3_bind_int64(statement, 3, (sqlite3_int64)capacity);
|
||||
int code;
|
||||
while ((code = sqlite3_step(statement)) == SQLITE_ROW) {
|
||||
sqlite3_int64 id = sqlite3_column_int64(statement, 0);
|
||||
if (id <= 0) {
|
||||
result = LARDON3D_PROJECT_DB_CORRUPT;
|
||||
break;
|
||||
}
|
||||
result = load_track_locked(database, (uint64_t)id, &tracks[*count]);
|
||||
if (result != LARDON3D_PROJECT_DB_OK || tracks[*count].track_set_id != track_set_id) {
|
||||
if (result == LARDON3D_PROJECT_DB_OK) {
|
||||
lardon3d_project_db_free_track(&tracks[*count]);
|
||||
result = LARDON3D_PROJECT_DB_CORRUPT;
|
||||
}
|
||||
break;
|
||||
}
|
||||
++*count;
|
||||
}
|
||||
if (result == LARDON3D_PROJECT_DB_OK && code != SQLITE_DONE) {
|
||||
result = sqlite_result(database, code, "list tracks");
|
||||
}
|
||||
if (result != LARDON3D_PROJECT_DB_OK) {
|
||||
for (size_t index = 0; index < *count; ++index) {
|
||||
lardon3d_project_db_free_track(&tracks[index]);
|
||||
}
|
||||
*count = 0;
|
||||
}
|
||||
(void)sqlite3_finalize(statement);
|
||||
}
|
||||
(void)pthread_mutex_unlock(&database->mutex);
|
||||
return result;
|
||||
}
|
||||
|
||||
Lardon3DProjectDbResult lardon3d_project_db_find_track_by_observation(
|
||||
Lardon3DProjectDb *database, uint64_t track_set_id, uint64_t feature_set_id,
|
||||
uint32_t feature_index, Lardon3DProjectDbTrack *track) {
|
||||
if (!database || !valid_catalog_id(track_set_id) || !valid_catalog_id(feature_set_id) || !track) {
|
||||
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
|
||||
}
|
||||
memset(track, 0, sizeof(*track));
|
||||
(void)pthread_mutex_lock(&database->mutex);
|
||||
sqlite3_stmt *statement = NULL;
|
||||
Lardon3DProjectDbResult result = prepare(
|
||||
database, "SELECT track_id FROM track_observations WHERE track_set_id=?1 AND "
|
||||
"feature_set_id=?2 AND feature_index=?3", &statement);
|
||||
if (result == LARDON3D_PROJECT_DB_OK) {
|
||||
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)track_set_id);
|
||||
(void)sqlite3_bind_int64(statement, 2, (sqlite3_int64)feature_set_id);
|
||||
(void)sqlite3_bind_int64(statement, 3, feature_index);
|
||||
int code = sqlite3_step(statement);
|
||||
if (code == SQLITE_ROW) {
|
||||
sqlite3_int64 id = sqlite3_column_int64(statement, 0);
|
||||
result = id > 0 ? load_track_locked(database, (uint64_t)id, track)
|
||||
: LARDON3D_PROJECT_DB_CORRUPT;
|
||||
if (result == LARDON3D_PROJECT_DB_NOT_FOUND) {
|
||||
result = LARDON3D_PROJECT_DB_CORRUPT;
|
||||
}
|
||||
if (result == LARDON3D_PROJECT_DB_OK && track->track_set_id != track_set_id) {
|
||||
lardon3d_project_db_free_track(track);
|
||||
result = LARDON3D_PROJECT_DB_CORRUPT;
|
||||
}
|
||||
} else {
|
||||
result = code == SQLITE_DONE ? LARDON3D_PROJECT_DB_NOT_FOUND
|
||||
: sqlite_result(database, code, "find track observation");
|
||||
}
|
||||
(void)sqlite3_finalize(statement);
|
||||
}
|
||||
(void)pthread_mutex_unlock(&database->mutex);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
#ifdef LARDON3D_PROJECT_DB_TESTING
|
||||
Lardon3DProjectDbResult lardon3d_project_db_test_delete_feature_identity(
|
||||
Lardon3DProjectDb *database, uint64_t feature_set_id, uint64_t feature_asset_id) {
|
||||
|
|
|
|||
|
|
@ -159,6 +159,9 @@ static bool create_v11_database(const char *path) {
|
|||
"PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;"
|
||||
"DROP TABLE geometric_verifier_tasks;"
|
||||
"DROP TABLE geometric_verification_results;"
|
||||
"DROP TABLE track_observations;"
|
||||
"DROP TABLE tracks;"
|
||||
"DROP TABLE track_sets;"
|
||||
"UPDATE metadata SET value=11 WHERE key='schema_version';"
|
||||
"COMMIT;PRAGMA foreign_keys=ON;");
|
||||
}
|
||||
|
|
@ -167,7 +170,7 @@ static bool test_model_api(const char *path) {
|
|||
char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY];
|
||||
Lardon3DProjectDb *database = NULL;
|
||||
CHECK(lardon3d_project_db_open(path, &database, error) == LARDON3D_PROJECT_DB_OK);
|
||||
CHECK(lardon3d_project_db_schema_version(database) == 13);
|
||||
CHECK(lardon3d_project_db_schema_version(database) == 14);
|
||||
Parents parents;
|
||||
CHECK(create_parents(database, &parents));
|
||||
|
||||
|
|
@ -402,7 +405,7 @@ static bool test_migration(const char *v11_path, const char *failed_path) {
|
|||
"name='geometric_verification_results'",
|
||||
0));
|
||||
CHECK(lardon3d_project_db_open(v11_path, &database, error) == LARDON3D_PROJECT_DB_OK);
|
||||
CHECK(lardon3d_project_db_schema_version(database) == 13);
|
||||
CHECK(lardon3d_project_db_schema_version(database) == 14);
|
||||
Parents parents;
|
||||
CHECK(create_parents(database, &parents));
|
||||
unsigned char fingerprint[32] = {0x91};
|
||||
|
|
@ -414,7 +417,7 @@ static bool test_migration(const char *v11_path, const char *failed_path) {
|
|||
&migrated_result) == LARDON3D_PROJECT_DB_OK);
|
||||
uint64_t migrated_result_id = migrated_result.geometric_verification_result_id;
|
||||
lardon3d_project_db_close(database);
|
||||
CHECK(query_integer(v11_path, "SELECT value FROM metadata WHERE key='schema_version'", 13));
|
||||
CHECK(query_integer(v11_path, "SELECT value FROM metadata WHERE key='schema_version'", 14));
|
||||
CHECK(query_integer(v11_path,
|
||||
"SELECT count(*) FROM sqlite_master WHERE type='index' AND "
|
||||
"name='geometric_verification_results_parent_idx'",
|
||||
|
|
@ -435,7 +438,7 @@ static bool test_migration(const char *v11_path, const char *failed_path) {
|
|||
"name='geometric_verification_results'",
|
||||
0));
|
||||
CHECK(lardon3d_project_db_open(failed_path, &database, error) == LARDON3D_PROJECT_DB_OK);
|
||||
CHECK(lardon3d_project_db_schema_version(database) == 13);
|
||||
CHECK(lardon3d_project_db_schema_version(database) == 14);
|
||||
lardon3d_project_db_close(database);
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -333,6 +333,9 @@ static bool run_task_test(void) {
|
|||
database_path,
|
||||
"PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;"
|
||||
"DROP TABLE geometric_verifier_tasks;"
|
||||
"DROP TABLE track_observations;"
|
||||
"DROP TABLE tracks;"
|
||||
"DROP TABLE track_sets;"
|
||||
"UPDATE metadata SET value=12 WHERE key='schema_version';COMMIT;"));
|
||||
CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V13", "1", 1) == 0);
|
||||
Lardon3DProjectDb *database = NULL;
|
||||
|
|
@ -349,7 +352,7 @@ static bool run_task_test(void) {
|
|||
0));
|
||||
CHECK(lardon3d_project_db_open(database_path, &database, error) ==
|
||||
LARDON3D_PROJECT_DB_OK);
|
||||
CHECK(lardon3d_project_db_schema_version(database) == 13);
|
||||
CHECK(lardon3d_project_db_schema_version(database) == 14);
|
||||
lardon3d_project_db_close(database);
|
||||
CHECK(remove_tree(root));
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -52,6 +52,9 @@ static bool create_v9_database(const char *path) {
|
|||
"DROP TABLE geometric_verification_results;"
|
||||
"DROP TABLE matcher_tasks;"
|
||||
"DROP TABLE match_results;"
|
||||
"DROP TABLE track_observations;"
|
||||
"DROP TABLE tracks;"
|
||||
"DROP TABLE track_sets;"
|
||||
"UPDATE metadata SET value=9 WHERE key='schema_version';COMMIT;PRAGMA foreign_keys=ON;";
|
||||
bool ok = sqlite3_exec(connection, sql, NULL, NULL, NULL) == SQLITE_OK;
|
||||
return sqlite3_close(connection) == SQLITE_OK && ok;
|
||||
|
|
@ -95,7 +98,7 @@ static bool run_test(void) {
|
|||
char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY];
|
||||
Lardon3DProjectDb *database = NULL;
|
||||
CHECK(lardon3d_project_db_open(database_path, &database, error) == LARDON3D_PROJECT_DB_OK);
|
||||
CHECK(database && lardon3d_project_db_schema_version(database) == 13);
|
||||
CHECK(database && lardon3d_project_db_schema_version(database) == 14);
|
||||
|
||||
Lardon3DProjectDbScanSet scanset;
|
||||
CHECK(lardon3d_project_db_create_scanset(database, "Match-test", &scanset) ==
|
||||
|
|
@ -454,7 +457,7 @@ static bool run_test(void) {
|
|||
database = NULL;
|
||||
|
||||
CHECK(lardon3d_project_db_open(database_path, &database, error) == LARDON3D_PROJECT_DB_OK);
|
||||
CHECK(lardon3d_project_db_schema_version(database) == 13);
|
||||
CHECK(lardon3d_project_db_schema_version(database) == 14);
|
||||
|
||||
/* Verify persistence: load previously created results */
|
||||
CHECK(lardon3d_project_db_load_match_result(database, first_id, &loaded) ==
|
||||
|
|
@ -494,10 +497,10 @@ static bool run_test(void) {
|
|||
CHECK(create_v9_database(v9_path));
|
||||
CHECK(query_integer(v9_path, "SELECT value FROM metadata WHERE key='schema_version'", 9));
|
||||
CHECK(lardon3d_project_db_open(v9_path, &database, error) == LARDON3D_PROJECT_DB_OK);
|
||||
CHECK(lardon3d_project_db_schema_version(database) == 13);
|
||||
CHECK(lardon3d_project_db_schema_version(database) == 14);
|
||||
lardon3d_project_db_close(database);
|
||||
database = NULL;
|
||||
CHECK(query_integer(v9_path, "SELECT value FROM metadata WHERE key='schema_version'", 13));
|
||||
CHECK(query_integer(v9_path, "SELECT value FROM metadata WHERE key='schema_version'", 14));
|
||||
CHECK(query_integer(v9_path,
|
||||
"SELECT count(*) FROM sqlite_master WHERE type='table' AND "
|
||||
"name='match_results'", 1));
|
||||
|
|
@ -516,7 +519,7 @@ static bool run_test(void) {
|
|||
"SELECT count(*) FROM sqlite_master WHERE type='table' AND "
|
||||
"name='matcher_tasks'", 0));
|
||||
CHECK(lardon3d_project_db_open(failed_v10_path, &database, error) == LARDON3D_PROJECT_DB_OK &&
|
||||
lardon3d_project_db_schema_version(database) == 13);
|
||||
lardon3d_project_db_schema_version(database) == 14);
|
||||
lardon3d_project_db_close(database);
|
||||
database = NULL;
|
||||
|
||||
|
|
|
|||
|
|
@ -92,6 +92,9 @@ static bool downgrade_project_to_historical_v10(const char *database_path) {
|
|||
"DROP TABLE geometric_verifier_tasks;"
|
||||
"DROP TABLE geometric_verification_results;"
|
||||
"DROP TABLE matcher_tasks;"
|
||||
"DROP TABLE track_observations;"
|
||||
"DROP TABLE tracks;"
|
||||
"DROP TABLE track_sets;"
|
||||
"UPDATE metadata SET value=10 WHERE key='schema_version';"
|
||||
"COMMIT;PRAGMA foreign_keys=ON;";
|
||||
bool success = sqlite3_exec(connection, sql, NULL, NULL, NULL) == SQLITE_OK;
|
||||
|
|
@ -352,7 +355,7 @@ static bool run_test(void) {
|
|||
"name='matcher_tasks'",
|
||||
0));
|
||||
CHECK(reopen_runtime(&fixture));
|
||||
CHECK(lardon3d_project_db_schema_version(fixture.state.project_db) == 13);
|
||||
CHECK(lardon3d_project_db_schema_version(fixture.state.project_db) == 14);
|
||||
CHECK(query_integer(database_path,
|
||||
"SELECT count(*) FROM sqlite_master WHERE type='table' AND "
|
||||
"name='matcher_tasks'",
|
||||
|
|
|
|||
|
|
@ -37,6 +37,20 @@ static void asset_path_for_hash(const unsigned char hash[LARDON3D_PROJECT_DB_SHA
|
|||
hex);
|
||||
}
|
||||
|
||||
static void feature_asset_path_for_hash(
|
||||
const unsigned char hash[LARDON3D_PROJECT_DB_SHA256_SIZE],
|
||||
char path[LARDON3D_PROJECT_DB_PATH_CAPACITY]) {
|
||||
static const char digits[] = "0123456789abcdef";
|
||||
char hex[65];
|
||||
for (size_t index = 0; index < LARDON3D_PROJECT_DB_SHA256_SIZE; ++index) {
|
||||
hex[index * 2] = digits[hash[index] >> 4];
|
||||
hex[index * 2 + 1] = digits[hash[index] & 15U];
|
||||
}
|
||||
hex[64] = '\0';
|
||||
(void)snprintf(path, LARDON3D_PROJECT_DB_PATH_CAPACITY, "assets/features/%c%c/%s", hex[0],
|
||||
hex[1], hex);
|
||||
}
|
||||
|
||||
static Lardon3DTaskDurableSnapshot task_snapshot(uint64_t id, Lardon3DTaskState saved) {
|
||||
Lardon3DTaskDurableSnapshot snapshot = {
|
||||
.id = id,
|
||||
|
|
@ -89,7 +103,7 @@ static bool create_future_database(const char *path) {
|
|||
}
|
||||
bool ok = sqlite3_exec(connection,
|
||||
"CREATE TABLE metadata(key TEXT PRIMARY KEY,value INTEGER NOT NULL);"
|
||||
"INSERT INTO metadata VALUES('schema_version',14);",
|
||||
"INSERT INTO metadata VALUES('schema_version',15);",
|
||||
NULL, NULL, NULL) == SQLITE_OK;
|
||||
return sqlite3_close(connection) == SQLITE_OK && ok;
|
||||
}
|
||||
|
|
@ -109,6 +123,9 @@ static bool create_v7_database(const char *path) {
|
|||
"DROP TABLE match_results;"
|
||||
"DROP TABLE candidate_pair_generate_tasks;"
|
||||
"DROP TABLE candidate_pairs;"
|
||||
"DROP TABLE track_observations;"
|
||||
"DROP TABLE tracks;"
|
||||
"DROP TABLE track_sets;"
|
||||
"UPDATE metadata SET value=7 WHERE key='schema_version';COMMIT;PRAGMA foreign_keys=ON;";
|
||||
bool ok = sqlite3_exec(connection, sql, NULL, NULL, NULL) == SQLITE_OK;
|
||||
return sqlite3_close(connection) == SQLITE_OK && ok;
|
||||
|
|
@ -135,6 +152,9 @@ static bool create_v6_database(const char *path) {
|
|||
"ALTER TABLE feature_sets DROP COLUMN coverage_ratio;"
|
||||
"ALTER TABLE feature_sets DROP COLUMN total_cells;"
|
||||
"ALTER TABLE feature_sets DROP COLUMN occupied_cells;"
|
||||
"DROP TABLE track_observations;"
|
||||
"DROP TABLE tracks;"
|
||||
"DROP TABLE track_sets;"
|
||||
"UPDATE metadata SET value=6 WHERE key='schema_version';COMMIT;PRAGMA foreign_keys=ON;";
|
||||
bool ok = sqlite3_exec(connection, sql, NULL, NULL, NULL) == SQLITE_OK;
|
||||
return sqlite3_close(connection) == SQLITE_OK && ok;
|
||||
|
|
@ -156,12 +176,32 @@ static bool create_v10_database(const char *path) {
|
|||
"DROP TABLE geometric_verifier_tasks;"
|
||||
"DROP TABLE geometric_verification_results;"
|
||||
"DROP TABLE matcher_tasks;"
|
||||
"DROP TABLE track_observations;"
|
||||
"DROP TABLE tracks;"
|
||||
"DROP TABLE track_sets;"
|
||||
"UPDATE metadata SET value=10 WHERE key='schema_version';"
|
||||
"COMMIT;PRAGMA foreign_keys=ON;";
|
||||
bool ok = sqlite3_exec(connection, sql, NULL, NULL, NULL) == SQLITE_OK;
|
||||
return sqlite3_close(connection) == SQLITE_OK && ok;
|
||||
}
|
||||
|
||||
static bool create_v13_database(const char *path) {
|
||||
Lardon3DProjectDb *database = NULL;
|
||||
char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY];
|
||||
if (lardon3d_project_db_open(path, &database, error) != LARDON3D_PROJECT_DB_OK) return false;
|
||||
lardon3d_project_db_close(database);
|
||||
sqlite3 *connection = NULL;
|
||||
if (sqlite3_open(path, &connection) != SQLITE_OK) return false;
|
||||
static const char sql[] =
|
||||
"PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;"
|
||||
"DROP TABLE track_observations;"
|
||||
"DROP TABLE tracks;"
|
||||
"DROP TABLE track_sets;"
|
||||
"UPDATE metadata SET value=13 WHERE key='schema_version';COMMIT;PRAGMA foreign_keys=ON;";
|
||||
bool ok = sqlite3_exec(connection, sql, NULL, NULL, NULL) == SQLITE_OK;
|
||||
return sqlite3_close(connection) == SQLITE_OK && ok;
|
||||
}
|
||||
|
||||
static bool create_v5_database(const char *path) {
|
||||
if (!create_v6_database(path)) return false;
|
||||
sqlite3 *connection = NULL;
|
||||
|
|
@ -302,6 +342,24 @@ static bool query_integer(const char *path, const char *sql, sqlite3_int64 expec
|
|||
matches;
|
||||
}
|
||||
|
||||
static void init_track_configuration(Lardon3DProjectDbTrackSet *configuration,
|
||||
unsigned char scope_byte, uint64_t gvr_count,
|
||||
size_t track_count) {
|
||||
memset(configuration, 0, sizeof(*configuration));
|
||||
(void)snprintf(configuration->builder_kind, sizeof(configuration->builder_kind),
|
||||
"gate-c-track-builder");
|
||||
configuration->builder_version = 1;
|
||||
configuration->parameter_fingerprint[0] = 0x10;
|
||||
configuration->verifier_kind = 1;
|
||||
configuration->verifier_version = 1;
|
||||
configuration->verifier_fingerprint[0] = 0x20;
|
||||
memset(configuration->input_scope_hash, scope_byte,
|
||||
sizeof(configuration->input_scope_hash));
|
||||
configuration->gvr_count = gvr_count;
|
||||
configuration->track_count = track_count;
|
||||
configuration->created_at = 1000;
|
||||
}
|
||||
|
||||
static bool run_test(void) {
|
||||
char directory[] = "/tmp/lardon3d-project-db-XXXXXX";
|
||||
CHECK(mkdtemp(directory));
|
||||
|
|
@ -311,6 +369,7 @@ static bool run_test(void) {
|
|||
char v4_path[512], failed_v5_path[512], failed_v6_path[512], failed_v7_path[512];
|
||||
char direct_v5_path[512], v8_path[512], failed_v8_path[512];
|
||||
char v10_path[512], failed_v11_path[512];
|
||||
char v13_path[512], failed_v14_path[512];
|
||||
CHECK(snprintf(database_path, sizeof(database_path), "%s/project.db", directory) > 0);
|
||||
CHECK(snprintf(artifact_path, sizeof(artifact_path), "%s/artifact.bin", directory) > 0);
|
||||
CHECK(snprintf(future_path, sizeof(future_path), "%s/future.db", directory) > 0);
|
||||
|
|
@ -337,11 +396,13 @@ static bool run_test(void) {
|
|||
0);
|
||||
CHECK(snprintf(v10_path, sizeof(v10_path), "%s/v10.db", directory) > 0);
|
||||
CHECK(snprintf(failed_v11_path, sizeof(failed_v11_path), "%s/failed-v11.db", directory) > 0);
|
||||
CHECK(snprintf(v13_path, sizeof(v13_path), "%s/v13.db", directory) > 0);
|
||||
CHECK(snprintf(failed_v14_path, sizeof(failed_v14_path), "%s/failed-v14.db", directory) > 0);
|
||||
|
||||
char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY];
|
||||
Lardon3DProjectDb *database = NULL;
|
||||
CHECK(lardon3d_project_db_open(database_path, &database, error) == LARDON3D_PROJECT_DB_OK);
|
||||
CHECK(database && lardon3d_project_db_schema_version(database) == 13);
|
||||
CHECK(database && lardon3d_project_db_schema_version(database) == 14);
|
||||
bool legacy_pending = true;
|
||||
CHECK(lardon3d_project_db_legacy_catalog_pending(database, &legacy_pending) ==
|
||||
LARDON3D_PROJECT_DB_OK &&
|
||||
|
|
@ -393,7 +454,6 @@ static bool run_test(void) {
|
|||
fourth_asset_path, 1, "pair-b.jpg", "/source/pair-b.jpg",
|
||||
0, 4, &identity_status, &pair_image_b) ==
|
||||
LARDON3D_PROJECT_DB_OK);
|
||||
|
||||
Lardon3DProjectDbCandidatePair pair;
|
||||
CHECK(lardon3d_project_db_create_candidate_pair(database, replacement_image.image_id,
|
||||
pair_image.image_id, 10, &pair) ==
|
||||
|
|
@ -608,7 +668,7 @@ static bool run_test(void) {
|
|||
LARDON3D_PROJECT_DB_INVALID_ARGUMENT);
|
||||
lardon3d_project_db_close(contexts[0].database);
|
||||
database = NULL;
|
||||
CHECK(query_integer(database_path, "SELECT value FROM metadata WHERE key='schema_version'", 13));
|
||||
CHECK(query_integer(database_path, "SELECT value FROM metadata WHERE key='schema_version'", 14));
|
||||
CHECK(query_integer(database_path, "SELECT count(*) FROM tasks WHERE task_id=1", 1));
|
||||
CHECK(lardon3d_project_db_open(database_path, &database, error) == LARDON3D_PROJECT_DB_OK);
|
||||
CHECK(lardon3d_project_db_load_task(database, 1, &task) == LARDON3D_PROJECT_DB_OK);
|
||||
|
|
@ -630,7 +690,7 @@ static bool run_test(void) {
|
|||
|
||||
CHECK(create_v1_database(legacy_path));
|
||||
CHECK(lardon3d_project_db_open(legacy_path, &database, error) == LARDON3D_PROJECT_DB_OK);
|
||||
CHECK(lardon3d_project_db_schema_version(database) == 13);
|
||||
CHECK(lardon3d_project_db_schema_version(database) == 14);
|
||||
CHECK(lardon3d_project_db_get_project(database, &loaded_project) == LARDON3D_PROJECT_DB_OK &&
|
||||
strcmp(loaded_project.stable_id, "legacy-project") == 0);
|
||||
CHECK(lardon3d_project_db_load_task(database, 9, &task) == LARDON3D_PROJECT_DB_OK);
|
||||
|
|
@ -640,7 +700,7 @@ static bool run_test(void) {
|
|||
LARDON3D_PROJECT_DB_OK);
|
||||
lardon3d_project_db_close(database);
|
||||
database = NULL;
|
||||
CHECK(query_integer(legacy_path, "SELECT value FROM metadata WHERE key='schema_version'", 13));
|
||||
CHECK(query_integer(legacy_path, "SELECT value FROM metadata WHERE key='schema_version'", 14));
|
||||
|
||||
CHECK(create_v1_database(failed_migration_path));
|
||||
CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V2", "1", 1) == 0);
|
||||
|
|
@ -665,7 +725,7 @@ static bool run_test(void) {
|
|||
LARDON3D_PROJECT_DB_OK);
|
||||
lardon3d_project_db_close(database);
|
||||
database = NULL;
|
||||
CHECK(query_integer(v2_path, "SELECT value FROM metadata WHERE key='schema_version'", 13));
|
||||
CHECK(query_integer(v2_path, "SELECT value FROM metadata WHERE key='schema_version'", 14));
|
||||
|
||||
CHECK(create_v2_database(failed_v3_migration_path));
|
||||
CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V3", "1", 1) == 0);
|
||||
|
|
@ -693,7 +753,7 @@ static bool run_test(void) {
|
|||
LARDON3D_PROJECT_DB_OK);
|
||||
lardon3d_project_db_close(database);
|
||||
database = NULL;
|
||||
CHECK(query_integer(v3_path, "SELECT value FROM metadata WHERE key='schema_version'", 13));
|
||||
CHECK(query_integer(v3_path, "SELECT value FROM metadata WHERE key='schema_version'", 14));
|
||||
|
||||
CHECK(create_v3_database(failed_v4_path));
|
||||
CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V4", "1", 1) == 0);
|
||||
|
|
@ -710,7 +770,7 @@ static bool run_test(void) {
|
|||
fprintf(stderr, "Migration v4 (%d): %s\n", (int)v4_result, error);
|
||||
}
|
||||
CHECK(v4_result == LARDON3D_PROJECT_DB_OK);
|
||||
CHECK(lardon3d_project_db_schema_version(database) == 13);
|
||||
CHECK(lardon3d_project_db_schema_version(database) == 14);
|
||||
CHECK(lardon3d_project_db_load_task(database, 9, &task) == LARDON3D_PROJECT_DB_OK);
|
||||
CHECK(lardon3d_project_db_load_artifact(database, "legacy-artifact", &loaded_artifact) ==
|
||||
LARDON3D_PROJECT_DB_OK);
|
||||
|
|
@ -749,24 +809,24 @@ static bool run_test(void) {
|
|||
fprintf(stderr, "Nouvelle tentative migration v7 (%d): %s\n", (int)retry_v7, error);
|
||||
}
|
||||
CHECK(retry_v7 == LARDON3D_PROJECT_DB_OK &&
|
||||
lardon3d_project_db_schema_version(database) == 13);
|
||||
lardon3d_project_db_schema_version(database) == 14);
|
||||
lardon3d_project_db_close(database);
|
||||
database = NULL;
|
||||
|
||||
CHECK(create_v5_database(direct_v5_path));
|
||||
CHECK(query_integer(direct_v5_path, "SELECT value FROM metadata WHERE key='schema_version'", 5));
|
||||
CHECK(lardon3d_project_db_open(direct_v5_path, &database, error) == LARDON3D_PROJECT_DB_OK &&
|
||||
lardon3d_project_db_schema_version(database) == 13);
|
||||
lardon3d_project_db_schema_version(database) == 14);
|
||||
lardon3d_project_db_close(database);
|
||||
database = NULL;
|
||||
|
||||
CHECK(create_v7_database(v8_path));
|
||||
CHECK(query_integer(v8_path, "SELECT value FROM metadata WHERE key='schema_version'", 7));
|
||||
CHECK(lardon3d_project_db_open(v8_path, &database, error) == LARDON3D_PROJECT_DB_OK);
|
||||
CHECK(lardon3d_project_db_schema_version(database) == 13);
|
||||
CHECK(lardon3d_project_db_schema_version(database) == 14);
|
||||
lardon3d_project_db_close(database);
|
||||
database = NULL;
|
||||
CHECK(query_integer(v8_path, "SELECT value FROM metadata WHERE key='schema_version'", 13));
|
||||
CHECK(query_integer(v8_path, "SELECT value FROM metadata WHERE key='schema_version'", 14));
|
||||
|
||||
CHECK(create_v7_database(failed_v8_path));
|
||||
CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V8", "1", 1) == 0);
|
||||
|
|
@ -777,7 +837,7 @@ static bool run_test(void) {
|
|||
"SELECT count(*) FROM sqlite_master WHERE type='table' AND "
|
||||
"name='candidate_pairs'", 0));
|
||||
CHECK(lardon3d_project_db_open(failed_v8_path, &database, error) == LARDON3D_PROJECT_DB_OK &&
|
||||
lardon3d_project_db_schema_version(database) == 13);
|
||||
lardon3d_project_db_schema_version(database) == 14);
|
||||
lardon3d_project_db_close(database);
|
||||
database = NULL;
|
||||
|
||||
|
|
@ -792,10 +852,10 @@ static bool run_test(void) {
|
|||
"name='matcher_tasks'",
|
||||
0));
|
||||
CHECK(lardon3d_project_db_open(v10_path, &database, error) == LARDON3D_PROJECT_DB_OK &&
|
||||
lardon3d_project_db_schema_version(database) == 13);
|
||||
lardon3d_project_db_schema_version(database) == 14);
|
||||
lardon3d_project_db_close(database);
|
||||
database = NULL;
|
||||
CHECK(query_integer(v10_path, "SELECT value FROM metadata WHERE key='schema_version'", 13));
|
||||
CHECK(query_integer(v10_path, "SELECT value FROM metadata WHERE key='schema_version'", 14));
|
||||
CHECK(query_integer(v10_path,
|
||||
"SELECT count(*) FROM sqlite_master WHERE type='table' AND "
|
||||
"name='matcher_tasks'",
|
||||
|
|
@ -818,10 +878,420 @@ static bool run_test(void) {
|
|||
1));
|
||||
CHECK(lardon3d_project_db_open(failed_v11_path, &database, error) ==
|
||||
LARDON3D_PROJECT_DB_OK);
|
||||
CHECK(lardon3d_project_db_schema_version(database) == 13);
|
||||
CHECK(lardon3d_project_db_schema_version(database) == 14);
|
||||
lardon3d_project_db_close(database);
|
||||
database = NULL;
|
||||
|
||||
CHECK(create_v13_database(v13_path));
|
||||
CHECK(query_integer(v13_path,
|
||||
"SELECT value FROM metadata WHERE key='schema_version'", 13));
|
||||
CHECK(query_integer(v13_path,
|
||||
"SELECT count(*) FROM sqlite_master WHERE type='table' AND "
|
||||
"name='track_sets'", 0));
|
||||
CHECK(lardon3d_project_db_open(v13_path, &database, error) == LARDON3D_PROJECT_DB_OK &&
|
||||
lardon3d_project_db_schema_version(database) == 14);
|
||||
lardon3d_project_db_close(database);
|
||||
database = NULL;
|
||||
CHECK(query_integer(v13_path,
|
||||
"SELECT value FROM metadata WHERE key='schema_version'", 14));
|
||||
CHECK(query_integer(v13_path,
|
||||
"SELECT count(*) FROM sqlite_master WHERE type='table' AND "
|
||||
"name='track_sets'", 1));
|
||||
|
||||
CHECK(create_v13_database(failed_v14_path));
|
||||
CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V14", "1", 1) == 0);
|
||||
CHECK(lardon3d_project_db_open(failed_v14_path, &database, error) ==
|
||||
LARDON3D_PROJECT_DB_IO_ERROR);
|
||||
CHECK(unsetenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V14") == 0);
|
||||
CHECK(query_integer(failed_v14_path,
|
||||
"SELECT value FROM metadata WHERE key='schema_version'", 13));
|
||||
CHECK(query_integer(failed_v14_path,
|
||||
"SELECT count(*) FROM sqlite_master WHERE type='table' AND "
|
||||
"name='track_sets'", 0));
|
||||
CHECK(lardon3d_project_db_open(failed_v14_path, &database, error) ==
|
||||
LARDON3D_PROJECT_DB_OK &&
|
||||
lardon3d_project_db_schema_version(database) == 14);
|
||||
lardon3d_project_db_close(database);
|
||||
database = NULL;
|
||||
|
||||
/* Gate C: exercise every public Track API through the Project DB contract. */
|
||||
char track_api_path[512];
|
||||
CHECK(snprintf(track_api_path, sizeof(track_api_path), "%s/track-api.db", directory) > 0);
|
||||
Lardon3DProjectDb *track_database = NULL;
|
||||
CHECK(lardon3d_project_db_open(track_api_path, &track_database, error) ==
|
||||
LARDON3D_PROJECT_DB_OK);
|
||||
Lardon3DProjectDbScanSet track_scanset;
|
||||
CHECK(lardon3d_project_db_create_scanset(track_database, "Track API", &track_scanset) ==
|
||||
LARDON3D_PROJECT_DB_OK);
|
||||
unsigned char track_image_hash_a[32] = {31};
|
||||
unsigned char track_image_hash_b[32] = {32};
|
||||
char track_image_path_a[LARDON3D_PROJECT_DB_PATH_CAPACITY];
|
||||
char track_image_path_b[LARDON3D_PROJECT_DB_PATH_CAPACITY];
|
||||
asset_path_for_hash(track_image_hash_a, track_image_path_a);
|
||||
asset_path_for_hash(track_image_hash_b, track_image_path_b);
|
||||
Lardon3DProjectDbImageRegisterStatus track_image_status;
|
||||
Lardon3DProjectDbImage track_image_a, track_image_b;
|
||||
CHECK(lardon3d_project_db_register_image(
|
||||
track_database, track_scanset.scanset_id, track_image_hash_a, track_image_path_a, 1,
|
||||
"track-a.jpg", "/source/track-a.jpg", 0, 1, &track_image_status, &track_image_a) ==
|
||||
LARDON3D_PROJECT_DB_OK);
|
||||
CHECK(lardon3d_project_db_register_image(
|
||||
track_database, track_scanset.scanset_id, track_image_hash_b, track_image_path_b, 1,
|
||||
"track-b.jpg", "/source/track-b.jpg", 0, 2, &track_image_status, &track_image_b) ==
|
||||
LARDON3D_PROJECT_DB_OK);
|
||||
|
||||
unsigned char feature_parameter[32] = {41};
|
||||
unsigned char source_hash_a[32];
|
||||
unsigned char source_hash_b[32];
|
||||
unsigned char feature_asset_hash_a[32] = {44};
|
||||
unsigned char feature_asset_hash_b[32] = {45};
|
||||
char feature_asset_path_a[LARDON3D_PROJECT_DB_PATH_CAPACITY];
|
||||
char feature_asset_path_b[LARDON3D_PROJECT_DB_PATH_CAPACITY];
|
||||
memcpy(source_hash_a, track_image_hash_a, sizeof(source_hash_a));
|
||||
memcpy(source_hash_b, track_image_hash_b, sizeof(source_hash_b));
|
||||
feature_asset_path_for_hash(feature_asset_hash_a, feature_asset_path_a);
|
||||
feature_asset_path_for_hash(feature_asset_hash_b, feature_asset_path_b);
|
||||
Lardon3DProjectDbFeatureSet track_features_a, track_features_b;
|
||||
CHECK(lardon3d_project_db_register_feature_set(
|
||||
track_database, track_image_a.image_id, "orb", 1, feature_parameter, source_hash_a,
|
||||
128, 1, 32, feature_asset_hash_a, feature_asset_path_a, 128,
|
||||
LARDON3D_DB_FEATURE_ASSET_DURABLE, 0, 10, &track_features_a) ==
|
||||
LARDON3D_PROJECT_DB_OK);
|
||||
CHECK(lardon3d_project_db_register_feature_set(
|
||||
track_database, track_image_b.image_id, "orb", 1, feature_parameter, source_hash_b,
|
||||
128, 1, 32, feature_asset_hash_b, feature_asset_path_b, 128,
|
||||
LARDON3D_DB_FEATURE_ASSET_DURABLE, 0, 11, &track_features_b) ==
|
||||
LARDON3D_PROJECT_DB_OK);
|
||||
|
||||
enum { gate_track_count = 65 };
|
||||
Lardon3DProjectDbTrack tracks[gate_track_count];
|
||||
Lardon3DProjectDbTrackObservation observations[gate_track_count][2];
|
||||
memset(tracks, 0, sizeof(tracks));
|
||||
for (size_t index = 0; index < gate_track_count; ++index) {
|
||||
observations[index][0] = (Lardon3DProjectDbTrackObservation){
|
||||
.feature_set_id = track_features_a.feature_set_id, .feature_index = (uint32_t)index,
|
||||
.position_in_track = 0};
|
||||
observations[index][1] = (Lardon3DProjectDbTrackObservation){
|
||||
.feature_set_id = track_features_b.feature_set_id, .feature_index = (uint32_t)index,
|
||||
.position_in_track = 1};
|
||||
tracks[index] = (Lardon3DProjectDbTrack){.observation_count = 2,
|
||||
.observations = observations[index]};
|
||||
}
|
||||
Lardon3DProjectDbTrackSet track_configuration;
|
||||
init_track_configuration(&track_configuration, 0x51, 1, gate_track_count);
|
||||
Lardon3DProjectDbTrackSet published_tracks;
|
||||
CHECK(lardon3d_project_db_create_track_set(track_database, &track_configuration, tracks,
|
||||
gate_track_count, &published_tracks) ==
|
||||
LARDON3D_PROJECT_DB_OK);
|
||||
CHECK(published_tracks.track_set_id > 0 && published_tracks.track_count == gate_track_count &&
|
||||
published_tracks.gvr_count == 1 && published_tracks.created_at > 1000000000);
|
||||
Lardon3DProjectDbTrackSet loaded_track_set, found_track_set;
|
||||
CHECK(lardon3d_project_db_load_track_set(track_database, published_tracks.track_set_id,
|
||||
&loaded_track_set) == LARDON3D_PROJECT_DB_OK);
|
||||
CHECK(loaded_track_set.track_set_id == published_tracks.track_set_id);
|
||||
CHECK(lardon3d_project_db_find_track_set(track_database, &track_configuration,
|
||||
&found_track_set) == LARDON3D_PROJECT_DB_OK);
|
||||
CHECK(found_track_set.track_set_id == published_tracks.track_set_id);
|
||||
Lardon3DProjectDbTrackSet track_sets_page[64];
|
||||
size_t track_set_count = 0;
|
||||
CHECK(lardon3d_project_db_list_track_sets(track_database, 0, track_sets_page, 64,
|
||||
&track_set_count) == LARDON3D_PROJECT_DB_OK);
|
||||
CHECK(track_set_count == 1 && track_sets_page[0].track_set_id == published_tracks.track_set_id);
|
||||
|
||||
Lardon3DProjectDbTrack loaded_track, found_track;
|
||||
CHECK(lardon3d_project_db_list_tracks(track_database, published_tracks.track_set_id, 0, tracks,
|
||||
64, &track_set_count) == LARDON3D_PROJECT_DB_OK);
|
||||
CHECK(track_set_count == 64 && tracks[0].track_id > 0);
|
||||
uint64_t first_track_id = tracks[0].track_id;
|
||||
uint64_t last_first_page_track_id = tracks[track_set_count - 1].track_id;
|
||||
for (size_t index = 0; index < track_set_count; ++index) {
|
||||
lardon3d_project_db_free_track(&tracks[index]);
|
||||
}
|
||||
CHECK(lardon3d_project_db_list_tracks(track_database, published_tracks.track_set_id,
|
||||
last_first_page_track_id, tracks, 64, &track_set_count) ==
|
||||
LARDON3D_PROJECT_DB_OK);
|
||||
CHECK(track_set_count == 1);
|
||||
uint64_t second_track_id = tracks[0].track_id;
|
||||
CHECK(lardon3d_project_db_load_track(track_database, second_track_id, &loaded_track) ==
|
||||
LARDON3D_PROJECT_DB_OK);
|
||||
CHECK(loaded_track.observation_count == 2);
|
||||
lardon3d_project_db_free_track(&loaded_track);
|
||||
CHECK(lardon3d_project_db_find_track_by_observation(
|
||||
track_database, published_tracks.track_set_id, track_features_a.feature_set_id, 1,
|
||||
&found_track) == LARDON3D_PROJECT_DB_OK);
|
||||
CHECK(found_track.track_id > 0 && found_track.track_set_id == published_tracks.track_set_id);
|
||||
lardon3d_project_db_free_track(&found_track);
|
||||
lardon3d_project_db_free_track(&tracks[0]);
|
||||
|
||||
Lardon3DProjectDbTrackSet reused_tracks;
|
||||
Lardon3DProjectDbTrackSet reuse_configuration = track_configuration;
|
||||
reuse_configuration.track_count = 0;
|
||||
CHECK(lardon3d_project_db_create_track_set(track_database, &reuse_configuration, NULL, 0,
|
||||
&reused_tracks) == LARDON3D_PROJECT_DB_OK);
|
||||
CHECK(reused_tracks.track_set_id == published_tracks.track_set_id);
|
||||
Lardon3DProjectDbTrackSet mismatched_configuration = reuse_configuration;
|
||||
mismatched_configuration.gvr_count = 2;
|
||||
CHECK(lardon3d_project_db_create_track_set(track_database, &mismatched_configuration, NULL, 0,
|
||||
&reused_tracks) == LARDON3D_PROJECT_DB_CONSTRAINT);
|
||||
CHECK(query_integer(track_api_path, "SELECT count(*) FROM track_sets", 1));
|
||||
|
||||
Lardon3DProjectDbTrack invalid_track = {.observation_count = 1, .observations = observations[0]};
|
||||
Lardon3DProjectDbTrackSet invalid_configuration;
|
||||
init_track_configuration(&invalid_configuration, 0x52, 1, 1);
|
||||
CHECK(lardon3d_project_db_create_track_set(track_database, &invalid_configuration, &invalid_track,
|
||||
1, &reused_tracks) ==
|
||||
LARDON3D_PROJECT_DB_INVALID_ARGUMENT);
|
||||
invalid_track.observation_count = 2;
|
||||
observations[0][1].feature_set_id = 999999;
|
||||
init_track_configuration(&invalid_configuration, 0x53, 1, 1);
|
||||
CHECK(lardon3d_project_db_create_track_set(track_database, &invalid_configuration, &invalid_track,
|
||||
1, &reused_tracks) == LARDON3D_PROJECT_DB_NOT_FOUND);
|
||||
observations[0][1].feature_set_id = track_features_b.feature_set_id;
|
||||
observations[0][1].feature_index = 128;
|
||||
init_track_configuration(&invalid_configuration, 0x54, 1, 1);
|
||||
CHECK(lardon3d_project_db_create_track_set(track_database, &invalid_configuration, &invalid_track,
|
||||
1, &reused_tracks) == LARDON3D_PROJECT_DB_CONSTRAINT);
|
||||
observations[0][1].feature_index = 0;
|
||||
observations[0][1].feature_set_id = track_features_a.feature_set_id;
|
||||
init_track_configuration(&invalid_configuration, 0x55, 1, 1);
|
||||
CHECK(lardon3d_project_db_create_track_set(track_database, &invalid_configuration, &invalid_track,
|
||||
1, &reused_tracks) == LARDON3D_PROJECT_DB_CONSTRAINT);
|
||||
observations[0][1].feature_set_id = track_features_a.feature_set_id;
|
||||
observations[0][1].feature_index = 1;
|
||||
init_track_configuration(&invalid_configuration, 0x56, 1, 1);
|
||||
CHECK(lardon3d_project_db_create_track_set(track_database, &invalid_configuration, &invalid_track,
|
||||
1, &reused_tracks) == LARDON3D_PROJECT_DB_CONSTRAINT);
|
||||
observations[0][1].feature_set_id = track_features_b.feature_set_id;
|
||||
observations[0][1].feature_index = 0;
|
||||
observations[0][0].position_in_track = 1;
|
||||
init_track_configuration(&invalid_configuration, 0x57, 1, 1);
|
||||
CHECK(lardon3d_project_db_create_track_set(track_database, &invalid_configuration, &invalid_track,
|
||||
1, &reused_tracks) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT);
|
||||
observations[0][0].position_in_track = 0;
|
||||
CHECK(query_integer(track_api_path, "SELECT count(*) FROM track_sets", 1));
|
||||
|
||||
Lardon3DProjectDbTrackSet cascade_configuration;
|
||||
init_track_configuration(&cascade_configuration, 0x58, 1, 1);
|
||||
Lardon3DProjectDbTrack cascade_track = {
|
||||
.observation_count = 2,
|
||||
.observations = observations[0],
|
||||
};
|
||||
Lardon3DProjectDbTrackSet cascade_published;
|
||||
CHECK(lardon3d_project_db_create_track_set(track_database, &cascade_configuration, &cascade_track,
|
||||
1, &cascade_published) == LARDON3D_PROJECT_DB_OK);
|
||||
CHECK(query_integer(track_api_path, "SELECT count(*) FROM track_sets", 2));
|
||||
sqlite3 *cascade_raw = NULL;
|
||||
CHECK(sqlite3_open(track_api_path, &cascade_raw) == SQLITE_OK);
|
||||
CHECK(sqlite3_exec(cascade_raw, "PRAGMA foreign_keys=ON;", NULL, NULL, NULL) == SQLITE_OK);
|
||||
char delete_cascade_sql[256];
|
||||
(void)snprintf(delete_cascade_sql, sizeof(delete_cascade_sql),
|
||||
"DELETE FROM track_sets WHERE track_set_id=%llu;",
|
||||
(unsigned long long)cascade_published.track_set_id);
|
||||
CHECK(sqlite3_exec(cascade_raw, delete_cascade_sql, NULL, NULL, NULL) == SQLITE_OK);
|
||||
CHECK(sqlite3_close(cascade_raw) == SQLITE_OK);
|
||||
char cascade_count_sql[256];
|
||||
(void)snprintf(cascade_count_sql, sizeof(cascade_count_sql),
|
||||
"SELECT count(*) FROM track_sets WHERE track_set_id=%llu",
|
||||
(unsigned long long)cascade_published.track_set_id);
|
||||
CHECK(query_integer(track_api_path, cascade_count_sql, 0));
|
||||
(void)snprintf(cascade_count_sql, sizeof(cascade_count_sql),
|
||||
"SELECT count(*) FROM tracks WHERE track_set_id=%llu",
|
||||
(unsigned long long)cascade_published.track_set_id);
|
||||
CHECK(query_integer(track_api_path, cascade_count_sql, 0));
|
||||
(void)snprintf(cascade_count_sql, sizeof(cascade_count_sql),
|
||||
"SELECT count(*) FROM track_observations WHERE track_set_id=%llu",
|
||||
(unsigned long long)cascade_published.track_set_id);
|
||||
CHECK(query_integer(track_api_path, cascade_count_sql, 0));
|
||||
|
||||
sqlite3 *track_raw = NULL;
|
||||
CHECK(sqlite3_open(track_api_path, &track_raw) == SQLITE_OK);
|
||||
CHECK(sqlite3_exec(track_raw, "PRAGMA foreign_keys=OFF;", NULL, NULL, NULL) == SQLITE_OK);
|
||||
char corruption_sql[512];
|
||||
(void)snprintf(corruption_sql, sizeof(corruption_sql),
|
||||
"UPDATE tracks SET track_set_id=999999 WHERE track_id=%llu;",
|
||||
(unsigned long long)first_track_id);
|
||||
CHECK(sqlite3_exec(track_raw, corruption_sql, NULL, NULL, NULL) == SQLITE_OK);
|
||||
CHECK(lardon3d_project_db_load_track(track_database, first_track_id, &loaded_track) ==
|
||||
LARDON3D_PROJECT_DB_CORRUPT);
|
||||
(void)snprintf(corruption_sql, sizeof(corruption_sql),
|
||||
"UPDATE tracks SET track_set_id=%llu WHERE track_id=%llu;",
|
||||
(unsigned long long)published_tracks.track_set_id,
|
||||
(unsigned long long)first_track_id);
|
||||
CHECK(sqlite3_exec(track_raw, corruption_sql, NULL, NULL, NULL) == SQLITE_OK);
|
||||
(void)snprintf(corruption_sql, sizeof(corruption_sql),
|
||||
"UPDATE track_observations SET track_id=999999 WHERE track_id=%llu;",
|
||||
(unsigned long long)first_track_id);
|
||||
CHECK(sqlite3_exec(track_raw, corruption_sql, NULL, NULL, NULL) == SQLITE_OK);
|
||||
CHECK(lardon3d_project_db_find_track_by_observation(
|
||||
track_database, published_tracks.track_set_id, track_features_a.feature_set_id, 0,
|
||||
&found_track) == LARDON3D_PROJECT_DB_CORRUPT);
|
||||
(void)snprintf(corruption_sql, sizeof(corruption_sql),
|
||||
"UPDATE track_observations SET track_id=%llu WHERE track_id=999999;",
|
||||
(unsigned long long)first_track_id);
|
||||
CHECK(sqlite3_exec(track_raw, corruption_sql, NULL, NULL, NULL) == SQLITE_OK);
|
||||
(void)snprintf(corruption_sql, sizeof(corruption_sql),
|
||||
"UPDATE track_observations SET feature_set_id=%llu,feature_index=127 "
|
||||
"WHERE track_id=%llu AND "
|
||||
"position_in_track=1;",
|
||||
(unsigned long long)track_features_a.feature_set_id,
|
||||
(unsigned long long)first_track_id);
|
||||
CHECK(sqlite3_exec(track_raw, corruption_sql, NULL, NULL, NULL) == SQLITE_OK);
|
||||
CHECK(lardon3d_project_db_load_track(track_database, first_track_id, &loaded_track) ==
|
||||
LARDON3D_PROJECT_DB_CORRUPT);
|
||||
CHECK(sqlite3_close(track_raw) == SQLITE_OK);
|
||||
lardon3d_project_db_close(track_database);
|
||||
CHECK(unlink(track_api_path) == 0);
|
||||
|
||||
sqlite3 *raw_db = NULL;
|
||||
CHECK(sqlite3_open(database_path, &raw_db) == SQLITE_OK);
|
||||
CHECK(sqlite3_exec(raw_db, "PRAGMA foreign_keys=ON;", NULL, NULL, NULL) == SQLITE_OK);
|
||||
|
||||
sqlite3_stmt *img_check = NULL;
|
||||
CHECK(sqlite3_prepare_v2(raw_db,
|
||||
"SELECT image_id FROM images ORDER BY image_id LIMIT 1",
|
||||
-1, &img_check, NULL) == SQLITE_OK);
|
||||
CHECK(sqlite3_step(img_check) == SQLITE_ROW);
|
||||
sqlite3_int64 img_id = sqlite3_column_int64(img_check, 0);
|
||||
CHECK(img_id > 0);
|
||||
(void)sqlite3_finalize(img_check);
|
||||
|
||||
CHECK(sqlite3_exec(raw_db,
|
||||
"INSERT INTO feature_assets(sha256,path,size_bytes,durability,created_at) "
|
||||
"VALUES(X'CAFECAFECAFECAFECAFECAFECAFECAFECAFECAFECAFECAFECAFECAFECAFECAFE',"
|
||||
"'assets/features/ca/fecafeca',128,0,500);",
|
||||
NULL, NULL, NULL) == SQLITE_OK);
|
||||
sqlite3_int64 fa_id = sqlite3_last_insert_rowid(raw_db);
|
||||
CHECK(fa_id > 0);
|
||||
|
||||
char insert_fs[1024];
|
||||
(void)snprintf(insert_fs, sizeof(insert_fs),
|
||||
"INSERT INTO feature_sets(image_id,feature_asset_id,extractor_kind,"
|
||||
"extractor_version,parameter_fingerprint,source_image_sha256,feature_count,"
|
||||
"descriptor_type,descriptor_dimension,created_at) "
|
||||
"VALUES(%lld,%lld,'orb',1,"
|
||||
"X'CAFECAFECAFECAFECAFECAFECAFECAFECAFECAFECAFECAFECAFECAFECAFECAFE',"
|
||||
"X'CAFECAFECAFECAFECAFECAFECAFECAFECAFECAFECAFECAFECAFECAFECAFECAFE',"
|
||||
"64,1,32,500);",
|
||||
(long long)img_id, (long long)fa_id);
|
||||
CHECK(sqlite3_exec(raw_db, insert_fs, NULL, NULL, NULL) == SQLITE_OK);
|
||||
sqlite3_int64 fs_id = sqlite3_last_insert_rowid(raw_db);
|
||||
CHECK(fs_id > 0);
|
||||
|
||||
CHECK(sqlite3_exec(raw_db,
|
||||
"INSERT INTO track_sets(builder_kind,builder_version,parameter_fingerprint,"
|
||||
"verifier_kind,verifier_version,verifier_fingerprint,input_scope_hash,"
|
||||
"gvr_count,track_count,created_at) "
|
||||
"VALUES('test-builder',1,"
|
||||
"X'000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F',"
|
||||
"1,1,"
|
||||
"X'101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F',"
|
||||
"X'202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F',"
|
||||
"1,0,200);",
|
||||
NULL, NULL, NULL) == SQLITE_OK);
|
||||
|
||||
CHECK(sqlite3_exec(raw_db,
|
||||
"INSERT INTO track_sets(builder_kind,builder_version,parameter_fingerprint,"
|
||||
"verifier_kind,verifier_version,verifier_fingerprint,input_scope_hash,"
|
||||
"gvr_count,track_count,created_at) "
|
||||
"VALUES('test-builder',1,"
|
||||
"X'000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F',"
|
||||
"1,1,"
|
||||
"X'101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F',"
|
||||
"X'202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F',"
|
||||
"1,0,200);",
|
||||
NULL, NULL, NULL) == SQLITE_CONSTRAINT);
|
||||
|
||||
CHECK(sqlite3_exec(raw_db,
|
||||
"INSERT INTO track_sets(builder_kind,builder_version,parameter_fingerprint,"
|
||||
"verifier_kind,verifier_version,verifier_fingerprint,input_scope_hash,"
|
||||
"gvr_count,track_count,created_at) "
|
||||
"VALUES('inv-builder',0,"
|
||||
"X'000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F',"
|
||||
"1,1,"
|
||||
"X'101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F',"
|
||||
"X'202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F',"
|
||||
"1,0,200);",
|
||||
NULL, NULL, NULL) == SQLITE_CONSTRAINT);
|
||||
|
||||
CHECK(sqlite3_exec(raw_db,
|
||||
"INSERT INTO track_sets(builder_kind,builder_version,parameter_fingerprint,"
|
||||
"verifier_kind,verifier_version,verifier_fingerprint,input_scope_hash,"
|
||||
"gvr_count,track_count,created_at) "
|
||||
"VALUES('other-builder',1,"
|
||||
"X'000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F',"
|
||||
"1,1,"
|
||||
"X'101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F',"
|
||||
"X'202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F',"
|
||||
"1,0,200);",
|
||||
NULL, NULL, NULL) == SQLITE_OK);
|
||||
|
||||
CHECK(sqlite3_exec(raw_db,
|
||||
"INSERT INTO track_sets(builder_kind,builder_version,parameter_fingerprint,"
|
||||
"verifier_kind,verifier_version,verifier_fingerprint,input_scope_hash,"
|
||||
"gvr_count,track_count,created_at) "
|
||||
"VALUES('gvr-fail',1,"
|
||||
"X'303132333435363738393A3B3C3D3E3F404142434445464748494A4B4C4D4E4F',"
|
||||
"1,1,"
|
||||
"X'404142434445464748494A4B4C4D4E4F505152535455565758595A5B5C5D5E5F',"
|
||||
"X'505152535455565758595A5B5C5D5E5F606162636465666768696A6B6C6D6E6F',"
|
||||
"0,0,200);",
|
||||
NULL, NULL, NULL) == SQLITE_CONSTRAINT);
|
||||
|
||||
CHECK(sqlite3_exec(raw_db,
|
||||
"INSERT INTO tracks(track_set_id,observation_count) "
|
||||
"VALUES(99999,2);",
|
||||
NULL, NULL, NULL) == SQLITE_CONSTRAINT);
|
||||
|
||||
CHECK(sqlite3_exec(raw_db,
|
||||
"INSERT INTO tracks(track_set_id,observation_count) "
|
||||
"VALUES(1,1);",
|
||||
NULL, NULL, NULL) == SQLITE_CONSTRAINT);
|
||||
|
||||
CHECK(sqlite3_exec(raw_db,
|
||||
"INSERT INTO tracks(track_set_id,observation_count) "
|
||||
"VALUES(1,2);",
|
||||
NULL, NULL, NULL) == SQLITE_OK);
|
||||
|
||||
char insert_obs[512];
|
||||
(void)snprintf(insert_obs, sizeof(insert_obs),
|
||||
"INSERT INTO track_observations(track_set_id,track_id,feature_set_id,"
|
||||
"feature_index,position_in_track) "
|
||||
"VALUES(1,1,%lld,0,0);",
|
||||
(long long)(fs_id + 999));
|
||||
CHECK(sqlite3_exec(raw_db, insert_obs, NULL, NULL, NULL) == SQLITE_CONSTRAINT);
|
||||
|
||||
(void)snprintf(insert_obs, sizeof(insert_obs),
|
||||
"INSERT INTO track_observations(track_set_id,track_id,feature_set_id,"
|
||||
"feature_index,position_in_track) "
|
||||
"VALUES(1,1,%lld,0,0);",
|
||||
(long long)fs_id);
|
||||
CHECK(sqlite3_exec(raw_db, insert_obs, NULL, NULL, NULL) == SQLITE_OK);
|
||||
|
||||
CHECK(sqlite3_exec(raw_db, insert_obs, NULL, NULL,
|
||||
NULL) == SQLITE_CONSTRAINT);
|
||||
|
||||
CHECK(sqlite3_exec(raw_db,
|
||||
"DELETE FROM track_sets WHERE track_set_id=2;",
|
||||
NULL, NULL, NULL) == SQLITE_OK);
|
||||
|
||||
sqlite3_stmt *cascade_check = NULL;
|
||||
CHECK(sqlite3_prepare_v2(raw_db,
|
||||
"SELECT count(*) FROM tracks WHERE track_set_id=2",
|
||||
-1, &cascade_check, NULL) == SQLITE_OK);
|
||||
CHECK(sqlite3_step(cascade_check) == SQLITE_ROW &&
|
||||
sqlite3_column_int(cascade_check, 0) == 0);
|
||||
(void)sqlite3_finalize(cascade_check);
|
||||
cascade_check = NULL;
|
||||
CHECK(sqlite3_prepare_v2(raw_db,
|
||||
"SELECT count(*) FROM track_observations WHERE track_set_id=2",
|
||||
-1, &cascade_check, NULL) == SQLITE_OK);
|
||||
CHECK(sqlite3_step(cascade_check) == SQLITE_ROW &&
|
||||
sqlite3_column_int(cascade_check, 0) == 0);
|
||||
(void)sqlite3_finalize(cascade_check);
|
||||
cascade_check = NULL;
|
||||
|
||||
CHECK(sqlite3_close(raw_db) == SQLITE_OK);
|
||||
|
||||
CHECK(unlink(artifact_path) == 0);
|
||||
CHECK(unlink(database_path) == 0);
|
||||
CHECK(unlink(future_path) == 0);
|
||||
|
|
@ -841,6 +1311,8 @@ static bool run_test(void) {
|
|||
CHECK(unlink(failed_v8_path) == 0);
|
||||
CHECK(unlink(v10_path) == 0);
|
||||
CHECK(unlink(failed_v11_path) == 0);
|
||||
CHECK(unlink(v13_path) == 0);
|
||||
CHECK(unlink(failed_v14_path) == 0);
|
||||
CHECK(rmdir(directory) == 0);
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue