From 95700de339b21560cdaeb7c2fed405b9d6a44499 Mon Sep 17 00:00:00 2001 From: fy59 Date: Mon, 10 Aug 2026 11:47:41 +0200 Subject: [PATCH] feat(reconstruction): freeze sparse SfM persistence model v1 --- docs/architecture/feature_store.md | 3 +- docs/architecture/project_database.md | 25 +- docs/architecture/reconstruction_pipeline.md | 8 +- docs/architecture/resource_aware_pipeline.md | 5 + docs/architecture/sparse_sfm.md | 154 +- include/lardon3d/project_db.h | 2 +- include/lardon3d/sparse_sfm_model.h | 238 +++ meson.build | 74 +- src/project_db.c | 158 +- src/project_db_internal.h | 24 + src/project_db_sparse_sfm.c | 1522 ++++++++++++++ tests/test_geometric_verification.c | 408 ++-- tests/test_geometric_verifier_task.c | 24 +- tests/test_match_result.c | 429 ++-- tests/test_matcher_task.c | 51 +- tests/test_project_db.c | 1940 +++++++++++++----- tests/test_sparse_sfm_model.c | 1075 ++++++++++ tests/test_sparse_sfm_resource.c | 313 +++ 18 files changed, 5479 insertions(+), 974 deletions(-) create mode 100644 include/lardon3d/sparse_sfm_model.h create mode 100644 src/project_db_internal.h create mode 100644 src/project_db_sparse_sfm.c create mode 100644 tests/test_sparse_sfm_model.c create mode 100644 tests/test_sparse_sfm_resource.c diff --git a/docs/architecture/feature_store.md b/docs/architecture/feature_store.md index 34bff5e..5e0e091 100644 --- a/docs/architecture/feature_store.md +++ b/docs/architecture/feature_store.md @@ -119,7 +119,8 @@ l'extraction après import et planification multi-image/DAG. les Feature Sets persistés. **IMPLEMENTED** — vérification géométrique et tracks. **Sparse SfM Gate A -PASS**, production Sparse SfM remains **NOT_IMPLEMENTED** until its later +PASS**, the Sparse SfM v16 persistence model is **FROZEN** after Gate B; +the numerical solver and reconstruction pipeline remain **PLANNED** until later implementation gates. ## Extension v2 multi-descriptor diff --git a/docs/architecture/project_database.md b/docs/architecture/project_database.md index c9b0183..fc69c2c 100644 --- a/docs/architecture/project_database.md +++ b/docs/architecture/project_database.md @@ -1,6 +1,8 @@ # Base de données projet Lardon3D -> Version courante : **v15**. La migration transactionnelle v14→v15 ajoute +> Version courante : **v16**. La migration transactionnelle v15→v16 ajoute +> le modèle persistant Sparse SfM (calibrations, scopes et reconstructions). +> La migration transactionnelle v14→v15 ajoute > `track_builder_tasks` pour le payload durable explicite du Task Builder. La > migration transactionnelle v13→v14 ajoute > les tables `track_sets`, `tracks` et `track_observations` pour le Track @@ -20,6 +22,21 @@ La base de données projet stocke les métadonnées de reconstruction et les relations entre les entités. Elle est conçue pour être légère, persistante et permettre la reprise après interruption. +## Sparse SfM Gate B (v16) + +Le modèle Sparse SfM v1 est publié atomiquement dans les tables +`sparse_calibrations`, `sparse_calibration_scopes`, +`sparse_calibration_scope_images`, `sparse_reconstructions`, +`sparse_reconstruction_components`, `sparse_registered_images`, +`sparse_landmarks` et `sparse_landmark_observations`. Les résultats sont +immuables et les collections volumineuses sont lues par curseurs bornés ; la +base ne charge jamais une reconstruction complète par défaut. Les coordonnées +restent dans le repère arbitraire de chaque composant et les références +d'observation ne dupliquent ni descripteurs ni coordonnées de pixels. +Les relations de suppression disposent d'index enfants dédiés, notamment sur +`calibration_id` dans les membres de scope et `calibration_scope_id` dans les +reconstructions, afin que les vérifications FK restent indexées. + ## Structure conceptuelle ### Entités principales @@ -758,6 +775,12 @@ sont documentés dans `tracks.md`. **IMPLEMENTED** — Track Builder v1 durable : table `track_builder_tasks`, scope asset atomique, payload v1 validé, migration v14→v15 et rollback/retry testés. +**IMPLEMENTED** — Sparse SfM v1 persistant : modèle v16 immutable, publication +atomique, composants déterministes, lecteurs bornés, corruption/lifecycle +validation, migration v15→v16 et comparateur fresh/migrated validés par Gate B. +Le modèle de persistance est gelé pour v1 ; le solveur numérique reste hors de +Project DB v16. + **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`, diff --git a/docs/architecture/reconstruction_pipeline.md b/docs/architecture/reconstruction_pipeline.md index c314025..50756ca 100644 --- a/docs/architecture/reconstruction_pipeline.md +++ b/docs/architecture/reconstruction_pipeline.md @@ -124,13 +124,15 @@ USAC/MAGSAC avec configuration, seed et fingerprint déterministes. **Statut :** COMPLETED/FROZEN — le Track Builder v1 direct et durable est implémenté dans Project DB v15 (`track_sets`, `tracks`, `track_observations` et -le payload de tâche). La triangulation et le Sparse SfM restent PLANNED. +le payload de tâche). La triangulation et le solveur Sparse SfM restent PLANNED; +le modèle de persistance Sparse SfM v16 est gelé après Gate B. **Sparse SfM Gate A : PASS.** Le contrat géométrique, la stratégie incremental, la triangulation candidate, le gauge, les conventions de pose, les limites BA et l'enveloppe matérielle sont documentés dans -`architecture/sparse_sfm.md`. La production Sparse SfM reste -**NOT_IMPLEMENTED** jusqu'aux Gates B–G. +`architecture/sparse_sfm.md`. Le solveur Sparse SfM reste +**NOT_IMPLEMENTED** jusqu'aux Gates B3–G; sa +persistance v16 et ses lecteurs bornés sont implémentés en B2. --- diff --git a/docs/architecture/resource_aware_pipeline.md b/docs/architecture/resource_aware_pipeline.md index deb1713..375b5c0 100644 --- a/docs/architecture/resource_aware_pipeline.md +++ b/docs/architecture/resource_aware_pipeline.md @@ -106,3 +106,8 @@ une distribution de latence estimator-only. Le profil maximal explicite et les pools multi-workers restent hors périmètre. SIFT/RootSIFT et Feature Extraction Vulkan restent hors de ce contrat. + +La validation B3 du modèle Sparse SfM v16 a utilisé des processus frais, un +fixture synthétique de 100 000 landmarks et 500 000 observations, cinq passes +de paging sur 50 000 landmarks, et n'a observé ni OOM, ni swap storm, ni dérive +RSS applicative. Cette preuve concerne la persistance bornée, pas le solveur. diff --git a/docs/architecture/sparse_sfm.md b/docs/architecture/sparse_sfm.md index f9be395..117a44f 100644 --- a/docs/architecture/sparse_sfm.md +++ b/docs/architecture/sparse_sfm.md @@ -3,12 +3,17 @@ ## Status and boundary **GATE A — DECISION after contract and probe study.** This document defines the -scientific and architectural contract for the future Sparse SfM layer. It does -not implement a solver, change Track Model v1, change Track Builder v1, add a -Project DB v16, or add a Task Kind. `FACT`, `CANDIDATE`, `DECISION` and `FROZEN` -remain explicit: the upstream Track Model and Track Builder are FROZEN; the -choices below are Gate A decisions for the next implementation gates, not a -claim that Sparse SfM is implemented. +scientific and architectural contract for the Sparse SfM layer. B2 implements +the immutable v16 persistence model and its bounded readers; it does not +implement a numerical solver, change Track Model v1, change Track Builder v1, +or add a Task Kind. `FACT`, `CANDIDATE`, `DECISION` and `FROZEN` remain explicit: +the upstream Track Model and Track Builder are FROZEN, while numerical Sparse +SfM remains deferred to later gates. + +**GATE B — PASS.** The v16 persistence model, migration, corruption/lifecycle +proofs, structural comparator, representative resource validation, normal +suite and ASan/UBSan closure are complete. Numerical Sparse SfM remains +deferred to Gate C and later gates. ## Scope @@ -383,3 +388,140 @@ the synthetic ground-truth and sparse-solver gates. No production Sparse SfM, triangulator, camera solver, BA, Project DB v16, metric alignment, control-point scale, dense/MVS, mesh, texturing, Vulkan SfM, GPU BA, network/distributed scheduling or UI workflow is implemented here. + +## Gate B — model and persistence contract + +This section is the **DECISION** contract for the v16 SQL/API work. +It preserves every Gate A decision and supplies only durable vocabulary; no +numeric geometry is introduced. + +### Calibration definition and identity + +A calibration is an immutable known pinhole model with `width`, `height`, +`fx`, `fy`, `cx`, `cy`, zero skew, `k1`, `k2`, `p1`, and `p2`, all binary64 and +finite. `width` and `height` belong to scientific identity. A calibration's +provenance is an explicit enum (`USER_EXPLICIT` or `IMPORTED_TRUSTED` in v1) +plus a 32-byte provenance fingerprint supplied by the caller. EXIF is never a +calibration origin. Two equal numeric models with different provenance +fingerprints are distinct scientific calibrations because their trust scope is +different; equal content and equal provenance are reused. + +The scientific calibration hash is SHA-256 over explicit little-endian fields: + +```text +ASCII "L3D3DCP1" (8 bytes) +format_version=1 (uint32) +model_kind (uint32), model_version (uint32) +width (uint32), height (uint32) +fx, fy, cx, cy, k1, k2, p1, p2 (8 canonical binary64 values) +provenance_kind (uint32), provenance_fingerprint (32 bytes) +``` + +NaN and infinities are rejected. Negative zero is canonicalized to positive +zero before hashing and storage. No native struct, padding or locale text is +serialized. SQLite row IDs remain DB-local references; the hash is the +scientific calibration identity. + +### Calibration scope + +A scope is immutable and assigns exactly one calibration to each relevant +image. Groups are allowed only when dimensions, crop/orientation coordinate +frame, model/version, numeric parameters and provenance identity are equal; +device or EXIF model names are insufficient. Scope identity is project-local +and content-addressed by SHA-256 over: + +```text +ASCII "L3D3DSC1" (8 bytes) +format_version=1 (uint32) +member_count (uint64) +for members sorted by image_id: + image_id (uint64), calibration_hash (32 bytes) +``` + +The member count is consistency metadata and is also encoded in the digest. +There is no latest-calibration lookup and no silent K rescaling. Feature File +dimensions must match the calibration dimensions exactly. + +### Reconstruction identity and components + +The immutable reconstruction identity is: + +```text +(track_set_id, + calibration_scope_id, + sfm_kind=INCREMENTAL, + sfm_version=1, + parameter_fingerprint[32]) +``` + +`track_set_id` and `calibration_scope_id` are project-local immutable database +references, consistent with the existing Track Model identity convention. The +parameter fingerprint is required to be a 32-byte SHA-256 value but its final +byte encoding remains a later geometry-gate decision. Runtime IDs, timestamps, +worker count, resource state and metrics are excluded. + +Component identity is the minimum registered `image_id` in that component. It +is deterministic, project-local, unique because an image belongs to at most one +component, independent of DFS/hash order, and compact. A component always has +at least one registered image. Its coordinates use an independent unit-baseline +gauge and are never comparable to another component without later alignment. + +### Persisted model + +The minimum v16 model is: + +- `sparse_calibrations`: immutable calibration content and hash; +- `sparse_calibration_scopes`: immutable scope hash/member count; +- `sparse_calibration_scope_images`: one image-to-calibration assignment; +- `sparse_reconstructions`: immutable identity and pixel reprojection metrics; +- `sparse_reconstruction_components`: component key and counts; +- `sparse_registered_images`: one world-to-camera pose per image; +- `sparse_landmarks`: one component-local binary64 point per Track; +- `sparse_landmark_observations`: minimal references to the upstream Feature Set + and feature index, without duplicated descriptors or x/y coordinates. + +Track ID is globally unique in the existing Track Model table, so one landmark +per reconstruction is uniquely keyed by `(reconstruction_id, track_id)`; +component key remains an attribute/consistency relation rather than redundant +landmark identity. Landmark publication validates that the Track belongs to the +reconstruction's exact Track Set and that every observation belongs to that +Track and its component. Track splitting is impossible in v1. + +Observation references are persisted because future BA needs bounded indexed +access from landmark to registered observations without repeatedly reopening +Feature Files. Only `feature_set_id`, `feature_index` and canonical track +position are stored; image ID and x/y remain derivable from immutable upstream +models. This is a deliberate normalization/resource trade-off, not a second +copy of Feature data. + +Persisted reprojection metrics use explicit pixel units and names: +`reprojection_rmse_px` and `reprojection_median_px`. They are diagnostics, not +identity. No metric scale or `_mm` field exists. + +### Constraints and publication + +Publication requires at least two registered images, one component and one +landmark. A result with no usable geometry is rejected rather than represented +by a meaningless empty scientific row. Two-camera reconstruction is valid. +There are no READY/FAILED/PARTIAL scientific states: row existence means a +complete immutable publication. A failed transaction leaves no visible row. + +SQL enforces one pose per image, one component per registered image, one +landmark per Track, exact reconstruction uniqueness, scope member uniqueness, +and child foreign keys. The API additionally validates Track Set ownership, +component consistency, calibration coverage, finite values and rotation +orthonormality/determinant. Rotation matrices are never repaired. + +The publication transaction inserts the reconstruction and all children using +prepared statements in bounded loops. It contains no Feature File I/O, Track +paging, solver work or Governor wait. Child pages use cursor order and bounded +capacity (64 cameras, 64 landmarks, 64 observations); total scientific counts +have no arbitrary cap beyond checked 64-bit/SQLite limits. + +### v16 migration intent + +Project DB v15 remains immutable. Gate B adds one transactional v15→v16 +migration containing only the eight Sparse SfM model tables and their required +indexes. A true historical v15 fixture, injected rollback, retry, fresh-schema +equivalence and close/reopen are mandatory. No Task, Governor, triangulation, +PnP, BA, GPU or Project DB v17 is introduced. diff --git a/include/lardon3d/project_db.h b/include/lardon3d/project_db.h index 89ebd73..7983efa 100644 --- a/include/lardon3d/project_db.h +++ b/include/lardon3d/project_db.h @@ -9,7 +9,7 @@ #include enum { - LARDON3D_PROJECT_DB_SCHEMA_VERSION = 15, + LARDON3D_PROJECT_DB_SCHEMA_VERSION = 16, LARDON3D_PROJECT_DB_ID_CAPACITY = 65, LARDON3D_PROJECT_DB_KIND_CAPACITY = 65, LARDON3D_PROJECT_DB_PATH_CAPACITY = 4096, diff --git a/include/lardon3d/sparse_sfm_model.h b/include/lardon3d/sparse_sfm_model.h new file mode 100644 index 0000000..0fcb372 --- /dev/null +++ b/include/lardon3d/sparse_sfm_model.h @@ -0,0 +1,238 @@ +#ifndef LARDON3D_SPARSE_SFM_MODEL_H +#define LARDON3D_SPARSE_SFM_MODEL_H + +#include +#include +#include + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +enum { + LARDON3D_SPARSE_SFM_SHA256_SIZE = 32, + LARDON3D_SPARSE_SFM_PAGE_MAX = 64, + LARDON3D_SPARSE_SFM_CALIBRATION_KIND_PINHOLE = 1, + LARDON3D_SPARSE_SFM_CALIBRATION_VERSION = 1, + LARDON3D_SPARSE_SFM_PROVENANCE_USER_EXPLICIT = 1, + LARDON3D_SPARSE_SFM_PROVENANCE_IMPORTED_TRUSTED = 2, + LARDON3D_SPARSE_SFM_KIND_INCREMENTAL = 1, + LARDON3D_SPARSE_SFM_VERSION = 1, +}; + +typedef struct { + uint64_t calibration_id; + unsigned char scientific_hash[32]; + uint32_t model_kind; + uint32_t model_version; + uint32_t width; + uint32_t height; + double fx; + double fy; + double cx; + double cy; + double k1; + double k2; + double p1; + double p2; + uint32_t provenance_kind; + unsigned char provenance_fingerprint[32]; +} Lardon3DSparseCalibration; + +typedef struct { + uint64_t image_id; + uint64_t calibration_id; + unsigned char calibration_hash[32]; +} Lardon3DSparseCalibrationMember; + +typedef struct { + uint64_t scope_id; + unsigned char scientific_hash[32]; + uint64_t member_count; +} Lardon3DSparseCalibrationScope; + +typedef struct { + uint64_t reconstruction_id; + uint64_t track_set_id; + uint64_t calibration_scope_id; + uint32_t sfm_kind; + uint32_t sfm_version; + unsigned char parameter_fingerprint[32]; + uint64_t component_count; + uint64_t registered_image_count; + uint64_t landmark_count; + double reprojection_rmse_px; + double reprojection_median_px; +} Lardon3DSparseReconstruction; + +typedef struct { + uint64_t component_id; + uint64_t component_key; + uint64_t registered_image_count; + uint64_t landmark_count; +} Lardon3DSparseComponent; + +typedef struct { + uint64_t image_id; + uint64_t component_key; + double rotation_cw[9]; + double translation_cw[3]; +} Lardon3DSparseRegisteredImage; + +typedef struct { + uint64_t landmark_id; + uint64_t track_id; + uint64_t component_key; + double x; + double y; + double z; + double reprojection_rmse_px; + double reprojection_median_px; + uint64_t observation_count; +} Lardon3DSparseLandmark; + +typedef struct { + uint64_t landmark_id; + uint64_t track_id; + uint64_t feature_set_id; + uint32_t feature_index; + uint32_t position_in_track; +} Lardon3DSparseLandmarkObservation; + +typedef struct { + uint64_t track_set_id; + uint64_t calibration_scope_id; + uint32_t sfm_kind; + uint32_t sfm_version; + unsigned char parameter_fingerprint[32]; + const Lardon3DSparseComponent *components; + size_t component_count; + const Lardon3DSparseRegisteredImage *registered_images; + size_t registered_image_count; + const Lardon3DSparseLandmark *landmarks; + size_t landmark_count; + const Lardon3DSparseLandmarkObservation *observations; + size_t observation_count; + double reprojection_rmse_px; + double reprojection_median_px; + int64_t created_at; +} Lardon3DSparsePublication; + +typedef struct { + uint64_t after_id; + size_t capacity; + size_t count; + uint64_t next_after_id; + Lardon3DSparseCalibration *items; +} Lardon3DSparseCalibrationPage; + +typedef struct { + uint64_t after_id; + size_t capacity; + size_t count; + uint64_t next_after_id; + Lardon3DSparseReconstruction *items; +} Lardon3DSparseReconstructionPage; + +typedef struct { + uint64_t after_component_key; + size_t capacity; + size_t count; + uint64_t next_component_key; + Lardon3DSparseComponent *items; +} Lardon3DSparseComponentPage; + +typedef struct { + uint64_t after_image_id; + size_t capacity; + size_t count; + uint64_t next_image_id; + Lardon3DSparseRegisteredImage *items; +} Lardon3DSparseRegisteredImagePage; + +typedef struct { + uint64_t after_track_id; + size_t capacity; + size_t count; + uint64_t next_track_id; + Lardon3DSparseLandmark *items; +} Lardon3DSparseLandmarkPage; + +typedef struct { + uint64_t after_landmark_id; + uint32_t after_position_in_track; + size_t capacity; + size_t count; + uint64_t next_landmark_id; + uint32_t next_position_in_track; + Lardon3DSparseLandmarkObservation *items; +} Lardon3DSparseObservationPage; + +Lardon3DProjectDbResult +lardon3d_sparse_calibration_create(Lardon3DProjectDb *database, + const Lardon3DSparseCalibration *input, + Lardon3DSparseCalibration *output); +Lardon3DProjectDbResult +lardon3d_sparse_calibration_load(Lardon3DProjectDb *database, + uint64_t calibration_id, + Lardon3DSparseCalibration *output); +Lardon3DProjectDbResult lardon3d_sparse_calibration_find_by_hash( + Lardon3DProjectDb *database, const unsigned char scientific_hash[32], + Lardon3DSparseCalibration *output); + +Lardon3DProjectDbResult lardon3d_sparse_calibration_scope_create( + Lardon3DProjectDb *database, const Lardon3DSparseCalibrationMember *members, + size_t member_count, Lardon3DSparseCalibrationScope *output); +Lardon3DProjectDbResult +lardon3d_sparse_calibration_scope_load(Lardon3DProjectDb *database, + uint64_t scope_id, + Lardon3DSparseCalibrationScope *output); +Lardon3DProjectDbResult lardon3d_sparse_calibration_scope_find_by_hash( + Lardon3DProjectDb *database, const unsigned char scientific_hash[32], + Lardon3DSparseCalibrationScope *output); +Lardon3DProjectDbResult lardon3d_sparse_calibration_scope_list_members( + Lardon3DProjectDb *database, uint64_t scope_id, uint64_t after_image_id, + Lardon3DSparseCalibrationMember *items, size_t capacity, size_t *count, + uint64_t *next_after_image_id); + +Lardon3DProjectDbResult lardon3d_sparse_reconstruction_publish( + Lardon3DProjectDb *database, const Lardon3DSparsePublication *publication, + Lardon3DSparseReconstruction *output); +Lardon3DProjectDbResult lardon3d_sparse_reconstruction_find_exact( + Lardon3DProjectDb *database, uint64_t track_set_id, + uint64_t calibration_scope_id, uint32_t sfm_kind, uint32_t sfm_version, + const unsigned char parameter_fingerprint[32], + Lardon3DSparseReconstruction *output); +Lardon3DProjectDbResult +lardon3d_sparse_reconstruction_load(Lardon3DProjectDb *database, + uint64_t reconstruction_id, + Lardon3DSparseReconstruction *output); +Lardon3DProjectDbResult +lardon3d_sparse_reconstruction_list(Lardon3DProjectDb *database, + uint64_t after_id, size_t capacity, + Lardon3DSparseReconstructionPage *page); +Lardon3DProjectDbResult +lardon3d_sparse_component_list(Lardon3DProjectDb *database, + uint64_t reconstruction_id, + uint64_t after_component_key, size_t capacity, + Lardon3DSparseComponentPage *page); +Lardon3DProjectDbResult +lardon3d_sparse_registered_image_list(Lardon3DProjectDb *database, + uint64_t reconstruction_id, + uint64_t after_image_id, size_t capacity, + Lardon3DSparseRegisteredImagePage *page); +Lardon3DProjectDbResult lardon3d_sparse_landmark_list( + Lardon3DProjectDb *database, uint64_t reconstruction_id, + uint64_t after_track_id, size_t capacity, Lardon3DSparseLandmarkPage *page); +Lardon3DProjectDbResult lardon3d_sparse_observation_list( + Lardon3DProjectDb *database, uint64_t reconstruction_id, + uint64_t after_landmark_id, uint32_t after_position_in_track, + size_t capacity, Lardon3DSparseObservationPage *page); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/meson.build b/meson.build index ad5c34d..169bc6c 100644 --- a/meson.build +++ b/meson.build @@ -143,7 +143,7 @@ executable( 'src/geometric_verifier_task.c', 'src/image_view.c', 'src/project.c', - 'src/project_db.c', + 'src/project_db.c', 'src/project_db_sparse_sfm.c', 'src/task.c', 'src/task_checkpoint.c', 'src/task_kind_registry.c', @@ -172,7 +172,7 @@ import_test = executable( 'src/app_state.c', 'src/import.c', 'src/image_catalog_persistent.c', - 'src/project_db.c', + 'src/project_db.c', 'src/project_db_sparse_sfm.c', 'src/task.c', 'src/resource_governor.c', 'src/resource_snapshot.c', @@ -191,7 +191,7 @@ import_task_test = executable( 'src/import.c', 'src/import_task.c', 'src/project.c', - 'src/project_db.c', + 'src/project_db.c', 'src/project_db_sparse_sfm.c', 'src/task.c', 'src/task_checkpoint.c', 'src/task_kind_registry.c', @@ -229,7 +229,7 @@ image_catalog_test = executable( 'src/app_state.c', 'src/image_catalog.c', 'src/image_catalog_persistent.c', - 'src/project_db.c', + 'src/project_db.c', 'src/project_db_sparse_sfm.c', 'src/task.c', 'src/resource_governor.c', 'src/resource_snapshot.c', @@ -246,7 +246,7 @@ persistent_image_catalog_test = executable( 'tests/test_persistent_image_catalog.c', 'src/app_state.c', 'src/image_catalog_persistent.c', - 'src/project_db.c', + 'src/project_db.c', 'src/project_db_sparse_sfm.c', 'src/task.c', 'src/resource_governor.c', 'src/resource_snapshot.c', @@ -266,7 +266,7 @@ feature_store_test = executable( 'src/image_catalog_persistent.c', 'src/feature_extractor_opencv.cpp', 'src/feature_store.c', - 'src/project_db.c', + 'src/project_db.c', 'src/project_db_sparse_sfm.c', 'src/task.c', 'src/resource_governor.c', 'src/resource_snapshot.c', @@ -286,7 +286,7 @@ feature_file_robustness_test = executable( 'src/image_catalog_persistent.c', 'src/feature_extractor_opencv.cpp', 'src/feature_store.c', - 'src/project_db.c', + 'src/project_db.c', 'src/project_db_sparse_sfm.c', 'src/task.c', 'src/resource_governor.c', 'src/resource_snapshot.c', @@ -304,7 +304,7 @@ visual_index_test = executable( 'tests/test_visual_index.c', 'src/app_state.c', 'src/image_catalog_persistent.c', 'src/feature_store.c', 'src/feature_extractor_opencv.cpp', - 'src/visual_index.c', 'src/project_db.c', 'src/task.c', + 'src/visual_index.c', 'src/project_db.c', 'src/project_db_sparse_sfm.c', 'src/task.c', 'src/resource_governor.c', 'src/resource_snapshot.c', ], c_args: [ @@ -322,7 +322,7 @@ candidate_pair_gen_test = executable( 'src/image_catalog_persistent.c', 'src/feature_store.c', 'src/feature_extractor_opencv.cpp', 'src/visual_index.c', 'src/candidate_pair_gen.c', - 'src/project_db.c', 'src/task.c', + 'src/project_db.c', 'src/project_db_sparse_sfm.c', 'src/task.c', 'src/resource_governor.c', 'src/resource_snapshot.c', ], include_directories: include_directories('include'), @@ -334,7 +334,7 @@ candidate_pair_task_test = executable( 'test-candidate-pair-task', sources: [ 'tests/test_candidate_pair_task.c', 'src/app_state.c', - 'src/project.c', 'src/project_db.c', 'src/task.c', + 'src/project.c', 'src/project_db.c', 'src/project_db_sparse_sfm.c', 'src/task.c', 'src/task_checkpoint.c', 'src/task_kind_registry.c', 'src/task_kinds.c', 'src/matcher_task.c', 'src/matcher.cpp', 'src/geometric_verifier_task.c', 'src/geometric_verifier.cpp', @@ -364,7 +364,7 @@ matcher_task_test = executable( 'test-matcher-task', sources: [ 'tests/test_matcher_task.c', 'src/app_state.c', 'src/project.c', - 'src/project_db.c', 'src/task.c', 'src/task_checkpoint.c', + 'src/project_db.c', 'src/project_db_sparse_sfm.c', 'src/task.c', 'src/task_checkpoint.c', 'src/task_kind_registry.c', 'src/task_kinds.c', 'src/task_queue.c', 'src/matcher_task.c', 'src/matcher.cpp', 'src/match_file.c', 'src/geometric_verifier_task.c', 'src/geometric_verifier.cpp', @@ -390,7 +390,7 @@ feature_task_test = executable( 'test-feature-task', sources: [ 'tests/test_feature_task.c', 'src/app_state.c', 'src/project.c', - 'src/project_db.c', 'src/task.c', 'src/task_checkpoint.c', + 'src/project_db.c', 'src/project_db_sparse_sfm.c', 'src/task.c', 'src/task_checkpoint.c', 'src/task_kind_registry.c', 'src/task_kinds.c', 'src/task_queue.c', 'src/matcher_task.c', 'src/matcher.cpp', 'src/match_file.c', 'src/geometric_verifier_task.c', 'src/geometric_verifier.cpp', @@ -424,7 +424,7 @@ precision_consolidation_test = executable( 'test-precision-consolidation', sources: [ 'tests/test_precision_consolidation.c', 'src/app_state.c', - 'src/project.c', 'src/project_db.c', 'src/task.c', + 'src/project.c', 'src/project_db.c', 'src/project_db_sparse_sfm.c', 'src/task.c', 'src/task_checkpoint.c', 'src/task_kind_registry.c', 'src/task_kinds.c', 'src/matcher_task.c', 'src/matcher.cpp', 'src/geometric_verifier_task.c', 'src/geometric_verifier.cpp', @@ -496,7 +496,7 @@ project_db_test = executable( 'test-project-db', sources: [ 'tests/test_project_db.c', - 'src/project_db.c', + 'src/project_db.c', 'src/project_db_sparse_sfm.c', 'src/task.c', 'src/resource_governor.c', 'src/resource_snapshot.c', @@ -508,11 +508,39 @@ project_db_test = executable( test('project-db', project_db_test, timeout: 30) +sparse_sfm_model_test = executable( + 'test-sparse-sfm-model', + sources: [ + 'tests/test_sparse_sfm_model.c', + 'src/project_db.c', 'src/project_db_sparse_sfm.c', + 'src/task.c', + 'src/resource_governor.c', + 'src/resource_snapshot.c', + ], + include_directories: include_directories('include'), + dependencies: [threads, sqlite3, openssl], +) + +test('sparse-sfm-model', sparse_sfm_model_test, timeout: 30) + +sparse_sfm_resource_test = executable( + 'test-sparse-sfm-resource', + sources: [ + 'tests/test_sparse_sfm_resource.c', + 'src/project_db.c', 'src/project_db_sparse_sfm.c', + 'src/task.c', + 'src/resource_governor.c', + 'src/resource_snapshot.c', + ], + include_directories: include_directories('include'), + dependencies: [threads, sqlite3, openssl], +) + match_result_test = executable( 'test-match-result', sources: [ 'tests/test_match_result.c', - 'src/project_db.c', + 'src/project_db.c', 'src/project_db_sparse_sfm.c', 'src/task.c', 'src/resource_governor.c', 'src/resource_snapshot.c', @@ -528,7 +556,7 @@ geometric_verification_test = executable( 'test-geometric-verification', sources: [ 'tests/test_geometric_verification.c', - 'src/project_db.c', + 'src/project_db.c', 'src/project_db_sparse_sfm.c', 'src/task.c', 'src/resource_governor.c', 'src/resource_snapshot.c', @@ -546,7 +574,7 @@ project_test = executable( 'tests/test_project.c', 'src/app_state.c', 'src/project.c', - 'src/project_db.c', + 'src/project_db.c', 'src/project_db_sparse_sfm.c', 'src/task.c', 'src/task_checkpoint.c', 'src/task_kind_registry.c', @@ -679,7 +707,7 @@ matcher_test = executable( 'src/matcher.cpp', 'src/feature_store.c', 'src/feature_extractor_opencv.cpp', - 'src/project_db.c', + 'src/project_db.c', 'src/project_db_sparse_sfm.c', 'src/task.c', 'src/resource_governor.c', 'src/resource_snapshot.c', @@ -749,7 +777,7 @@ matcher_e2e_test = executable( 'src/matcher.cpp', 'src/feature_store.c', 'src/feature_extractor_opencv.cpp', - 'src/project_db.c', + 'src/project_db.c', 'src/project_db_sparse_sfm.c', 'src/task.c', 'src/resource_governor.c', 'src/resource_snapshot.c', @@ -771,7 +799,7 @@ executable( 'src/matcher.cpp', 'src/feature_store.c', 'src/feature_extractor_opencv.cpp', - 'src/project_db.c', + 'src/project_db.c', 'src/project_db_sparse_sfm.c', 'src/task.c', 'src/resource_governor.c', 'src/resource_snapshot.c', @@ -797,7 +825,7 @@ geometric_verifier_test = executable( 'src/match_file.c', 'src/feature_store.c', 'src/feature_extractor_opencv.cpp', - 'src/project_db.c', + 'src/project_db.c', 'src/project_db_sparse_sfm.c', 'src/task.c', 'src/resource_governor.c', 'src/resource_snapshot.c', @@ -815,7 +843,7 @@ geometric_verifier_task_test = executable( 'test-geometric-verifier-task', sources: [ 'tests/test_geometric_verifier_task.c', 'src/app_state.c', 'src/project.c', - 'src/project_db.c', 'src/task.c', 'src/task_checkpoint.c', + 'src/project_db.c', 'src/project_db_sparse_sfm.c', 'src/task.c', 'src/task_checkpoint.c', 'src/task_kind_registry.c', 'src/task_kinds.c', 'src/task_queue.c', 'src/geometric_verifier_task.c', 'src/geometric_verifier.cpp', 'src/match_file.c', 'src/feature_store.c', 'src/resource_governor.c', @@ -851,7 +879,7 @@ track_builder_project_test = executable( 'src/track_builder.cpp', 'src/track_builder_project.cpp', 'src/track_builder_task.cpp', - 'src/project_db.c', + 'src/project_db.c', 'src/project_db_sparse_sfm.c', 'src/app_state.c', 'src/task.c', 'src/task_kind_registry.c', diff --git a/src/project_db.c b/src/project_db.c index b600ffc..aaa8099 100644 --- a/src/project_db.c +++ b/src/project_db.c @@ -12,12 +12,7 @@ #include #include - -struct Lardon3DProjectDb { - sqlite3 *connection; - pthread_mutex_t mutex; - char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]; -}; +#include "project_db_internal.h" static const char schema_v5[] = "CREATE TABLE metadata(key TEXT PRIMARY KEY,value INTEGER NOT NULL);" @@ -319,13 +314,122 @@ static const char schema_track_builder_task_v15[] = "scope_sha256 BLOB NOT NULL CHECK(length(scope_sha256)=32)," "scope_format_version INTEGER NOT NULL CHECK(scope_format_version=1));"; +static const char schema_sparse_sfm_v16_a[] = + "CREATE TABLE sparse_calibrations(" + "calibration_id INTEGER PRIMARY KEY AUTOINCREMENT CHECK(calibration_id>0)," + "scientific_hash BLOB NOT NULL CHECK(length(scientific_hash)=32) UNIQUE," + "model_kind INTEGER NOT NULL CHECK(model_kind=1),model_version INTEGER NOT " + "NULL CHECK(model_version=1)," + "width INTEGER NOT NULL CHECK(width>0),height INTEGER NOT NULL " + "CHECK(height>0)," + "fx REAL NOT NULL,fy REAL NOT NULL,cx REAL NOT NULL,cy REAL NOT NULL," + "k1 REAL NOT NULL,k2 REAL NOT NULL,p1 REAL NOT NULL,p2 REAL NOT NULL," + "provenance_kind INTEGER NOT NULL CHECK(provenance_kind IN(1,2))," + "provenance_fingerprint BLOB NOT NULL " + "CHECK(length(provenance_fingerprint)=32));" + "CREATE TABLE sparse_calibration_scopes(" + "scope_id INTEGER PRIMARY KEY AUTOINCREMENT CHECK(scope_id>0)," + "scientific_hash BLOB NOT NULL CHECK(length(scientific_hash)=32) UNIQUE," + "member_count INTEGER NOT NULL CHECK(member_count>0));" + "CREATE TABLE sparse_calibration_scope_images(" + "scope_id INTEGER NOT NULL REFERENCES sparse_calibration_scopes(scope_id) " + "ON DELETE CASCADE," + "image_id INTEGER NOT NULL REFERENCES images(image_id)," + "calibration_id INTEGER NOT NULL REFERENCES " + "sparse_calibrations(calibration_id)," + "PRIMARY KEY(scope_id,image_id));" + "CREATE INDEX sparse_scope_images_calibration_idx ON " + "sparse_calibration_scope_images(scope_id,calibration_id);" + "CREATE INDEX sparse_scope_images_calibration_lookup_idx ON " + "sparse_calibration_scope_images(calibration_id,scope_id);"; + +static const char schema_sparse_sfm_v16_b[] = + "CREATE TABLE sparse_reconstructions(" + "reconstruction_id INTEGER PRIMARY KEY AUTOINCREMENT " + "CHECK(reconstruction_id>0)," + "track_set_id INTEGER NOT NULL REFERENCES track_sets(track_set_id)," + "calibration_scope_id INTEGER NOT NULL REFERENCES " + "sparse_calibration_scopes(scope_id)," + "sfm_kind INTEGER NOT NULL CHECK(sfm_kind=1),sfm_version INTEGER NOT NULL " + "CHECK(sfm_version=1)," + "parameter_fingerprint BLOB NOT NULL " + "CHECK(length(parameter_fingerprint)=32)," + "component_count INTEGER NOT NULL CHECK(component_count>0)," + "registered_image_count INTEGER NOT NULL CHECK(registered_image_count>=2)," + "landmark_count INTEGER NOT NULL CHECK(landmark_count>0)," + "reprojection_rmse_px REAL NOT NULL,reprojection_median_px REAL NOT " + "NULL,created_at INTEGER NOT NULL," + "UNIQUE(track_set_id,calibration_scope_id,sfm_kind,sfm_version,parameter_" + "fingerprint));" + "CREATE INDEX sparse_reconstructions_scope_idx ON " + "sparse_reconstructions(calibration_scope_id);" + "CREATE TABLE sparse_reconstruction_components(" + "component_id INTEGER PRIMARY KEY AUTOINCREMENT CHECK(component_id>0)," + "reconstruction_id INTEGER NOT NULL REFERENCES " + "sparse_reconstructions(reconstruction_id) ON DELETE CASCADE," + "component_key INTEGER NOT NULL " + "CHECK(component_key>0),registered_image_count INTEGER NOT NULL " + "CHECK(registered_image_count>0)," + "landmark_count INTEGER NOT NULL CHECK(landmark_count>0)," + "UNIQUE(reconstruction_id,component_id),UNIQUE(reconstruction_id,component_" + "key));" + "CREATE INDEX sparse_components_page_idx ON " + "sparse_reconstruction_components(reconstruction_id,component_key);" + "CREATE TABLE sparse_registered_images(" + "reconstruction_id INTEGER NOT NULL REFERENCES " + "sparse_reconstructions(reconstruction_id) ON DELETE CASCADE," + "component_id INTEGER NOT NULL, image_id INTEGER NOT NULL REFERENCES " + "images(image_id)," + "rotation_00 REAL NOT NULL,rotation_01 REAL NOT NULL,rotation_02 REAL NOT " + "NULL," + "rotation_10 REAL NOT NULL,rotation_11 REAL NOT NULL,rotation_12 REAL NOT " + "NULL," + "rotation_20 REAL NOT NULL,rotation_21 REAL NOT NULL,rotation_22 REAL NOT " + "NULL," + "translation_0 REAL NOT NULL,translation_1 REAL NOT NULL,translation_2 " + "REAL NOT NULL," + "PRIMARY KEY(reconstruction_id,image_id)," + "FOREIGN KEY(reconstruction_id,component_id) REFERENCES " + "sparse_reconstruction_components(reconstruction_id,component_id));" + "CREATE INDEX sparse_registered_page_idx ON " + "sparse_registered_images(reconstruction_id,component_id,image_id);" + "CREATE INDEX sparse_registered_image_lookup_idx ON " + "sparse_registered_images(reconstruction_id,image_id);" + "CREATE TABLE sparse_landmarks(" + "landmark_id INTEGER PRIMARY KEY AUTOINCREMENT CHECK(landmark_id>0)," + "reconstruction_id INTEGER NOT NULL REFERENCES " + "sparse_reconstructions(reconstruction_id) ON DELETE CASCADE," + "component_id INTEGER NOT NULL,track_id INTEGER NOT NULL REFERENCES " + "tracks(track_id)," + "x REAL NOT NULL,y REAL NOT NULL,z REAL NOT NULL,reprojection_rmse_px REAL " + "NOT NULL," + "reprojection_median_px REAL NOT NULL,observation_count INTEGER NOT NULL " + "CHECK(observation_count>0)," + "UNIQUE(reconstruction_id,track_id),FOREIGN " + "KEY(reconstruction_id,component_id) " + "REFERENCES " + "sparse_reconstruction_components(reconstruction_id,component_id));" + "CREATE INDEX sparse_landmarks_page_idx ON " + "sparse_landmarks(reconstruction_id,component_id,track_id);" + "CREATE INDEX sparse_landmark_track_lookup_idx ON " + "sparse_landmarks(reconstruction_id,track_id);" + "CREATE TABLE sparse_landmark_observations(" + "landmark_id INTEGER NOT NULL REFERENCES sparse_landmarks(landmark_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(landmark_id,feature_set_id,feature_index));" + "CREATE INDEX sparse_landmark_observations_page_idx ON " + "sparse_landmark_observations(landmark_id,position_in_track);"; + static void copy_error(char destination[LARDON3D_PROJECT_DB_ERROR_CAPACITY], const char *text) { if (destination) { (void)snprintf(destination, LARDON3D_PROJECT_DB_ERROR_CAPACITY, "%s", text ? text : ""); } } -static Lardon3DProjectDbResult sqlite_result(Lardon3DProjectDb *database, int code, +Lardon3DProjectDbResult sqlite_result(Lardon3DProjectDb *database, int code, const char *context) { (void)snprintf(database->error, sizeof(database->error), "%s: %s", context, sqlite3_errmsg(database->connection)); @@ -367,7 +471,7 @@ static bool database_time(sqlite3_int64 value, time_t *output) { return true; } -static Lardon3DProjectDbResult execute(Lardon3DProjectDb *database, const char *sql, +Lardon3DProjectDbResult execute(Lardon3DProjectDb *database, const char *sql, const char *context) { char *message = NULL; int code = sqlite3_exec(database->connection, sql, NULL, NULL, &message); @@ -380,7 +484,7 @@ static Lardon3DProjectDbResult execute(Lardon3DProjectDb *database, const char * return sqlite_result(database, code, context); } -static Lardon3DProjectDbResult prepare(Lardon3DProjectDb *database, const char *sql, +Lardon3DProjectDbResult prepare(Lardon3DProjectDb *database, const char *sql, sqlite3_stmt **statement) { int code = sqlite3_prepare_v2(database->connection, sql, -1, statement, NULL); return code == SQLITE_OK ? LARDON3D_PROJECT_DB_OK : sqlite_result(database, code, "prepare"); @@ -409,7 +513,8 @@ 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 != 13 && from_version != 14) { + from_version != 12 && from_version != 13 && from_version != 14 && + from_version != 15) { return LARDON3D_PROJECT_DB_CORRUPT; } Lardon3DProjectDbResult result = execute(database, "BEGIN IMMEDIATE", "begin migration"); @@ -713,6 +818,27 @@ static Lardon3DProjectDbResult migrate(Lardon3DProjectDb *database, unsigned int "finish schema v15 migration"); } } + if (result == LARDON3D_PROJECT_DB_OK && from_version < 16) { + result = execute(database, schema_sparse_sfm_v16_a, + "migrate schema v15 to v16 (part 1)"); +#ifdef LARDON3D_PROJECT_DB_TESTING + if (result == LARDON3D_PROJECT_DB_OK && + getenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V16")) { + result = execute(database, "INSERT INTO missing_v16_test_table VALUES(1)", + "forced migration v16 failure"); + } +#endif + if (result == LARDON3D_PROJECT_DB_OK) { + result = execute(database, schema_sparse_sfm_v16_b, + "migrate schema v15 to v16 (part 2)"); + } + if (result == LARDON3D_PROJECT_DB_OK) { + result = execute( + database, + "UPDATE metadata SET value=16 WHERE key='schema_version' AND value=15", + "finish schema v16 migration"); + } + } if (result == LARDON3D_PROJECT_DB_OK) { result = execute(database, "COMMIT", "commit migration"); } @@ -833,7 +959,15 @@ Lardon3DProjectDbResult lardon3d_project_db_open(const char *path, Lardon3DProje "track_sets", "tracks", "track_observations", - "track_builder_tasks"}; + "track_builder_tasks", + "sparse_calibrations", + "sparse_calibration_scopes", + "sparse_calibration_scope_images", + "sparse_reconstructions", + "sparse_reconstruction_components", + "sparse_registered_images", + "sparse_landmarks", + "sparse_landmark_observations"}; for (size_t index = 0; index < sizeof(required) / sizeof(required[0]) && result == LARDON3D_PROJECT_DB_OK; ++index) { @@ -908,7 +1042,7 @@ Lardon3DProjectDbResult lardon3d_project_db_legacy_catalog_pending(Lardon3DProje return result; } -static Lardon3DProjectDbResult step_done(Lardon3DProjectDb *database, sqlite3_stmt *statement, +Lardon3DProjectDbResult step_done(Lardon3DProjectDb *database, sqlite3_stmt *statement, const char *context) { int code = sqlite3_step(statement); if (code != SQLITE_DONE) { diff --git a/src/project_db_internal.h b/src/project_db_internal.h new file mode 100644 index 0000000..b5345ac --- /dev/null +++ b/src/project_db_internal.h @@ -0,0 +1,24 @@ +#ifndef LARDON3D_PROJECT_DB_INTERNAL_H +#define LARDON3D_PROJECT_DB_INTERNAL_H + +#include +#include + +#include + +struct Lardon3DProjectDb { + sqlite3 *connection; + pthread_mutex_t mutex; + char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]; +}; + +Lardon3DProjectDbResult sqlite_result(Lardon3DProjectDb *database, int code, + const char *context); +Lardon3DProjectDbResult execute(Lardon3DProjectDb *database, const char *sql, + const char *context); +Lardon3DProjectDbResult prepare(Lardon3DProjectDb *database, const char *sql, + sqlite3_stmt **statement); +Lardon3DProjectDbResult step_done(Lardon3DProjectDb *database, + sqlite3_stmt *statement, const char *context); + +#endif diff --git a/src/project_db_sparse_sfm.c b/src/project_db_sparse_sfm.c new file mode 100644 index 0000000..1e2cf85 --- /dev/null +++ b/src/project_db_sparse_sfm.c @@ -0,0 +1,1522 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "project_db_internal.h" +#include + +static bool sparse_finite(double value) { return isfinite(value) != 0; } + +static bool sparse_fits_sqlite(uint64_t value) { + return value <= (uint64_t)INT64_MAX; +} + +static Lardon3DProjectDbResult sparse_step_reuse(Lardon3DProjectDb *db, + sqlite3_stmt *statement, + const char *context) { + int code = sqlite3_step(statement); + if (code != SQLITE_DONE) + return sqlite_result(db, code, context); + sqlite3_reset(statement); + sqlite3_clear_bindings(statement); + return LARDON3D_PROJECT_DB_OK; +} + +static void sparse_put_u32(unsigned char *data, size_t *offset, + uint32_t value) { + for (unsigned byte = 0; byte < 4; ++byte) + data[(*offset)++] = (unsigned char)(value >> (byte * 8)); +} + +static void sparse_put_u64(unsigned char *data, size_t *offset, + uint64_t value) { + for (unsigned byte = 0; byte < 8; ++byte) + data[(*offset)++] = (unsigned char)(value >> (byte * 8)); +} + +static void sparse_put_double(unsigned char *data, size_t *offset, + double value) { + uint64_t bits = 0; + if (value == 0.0) + value = 0.0; + memcpy(&bits, &value, sizeof(bits)); + sparse_put_u64(data, offset, bits); +} + +static bool sparse_hash(const unsigned char *data, size_t size, + unsigned char hash[32]) { + unsigned int length = 0; + return EVP_Digest(data, size, hash, &length, EVP_sha256(), NULL) == 1 && + length == 32; +} + +static bool sparse_calibration_valid(const Lardon3DSparseCalibration *input) { + if (!input || + input->model_kind != LARDON3D_SPARSE_SFM_CALIBRATION_KIND_PINHOLE || + input->model_version != 1 || input->width == 0 || input->height == 0 || + input->provenance_kind < LARDON3D_SPARSE_SFM_PROVENANCE_USER_EXPLICIT || + input->provenance_kind > + LARDON3D_SPARSE_SFM_PROVENANCE_IMPORTED_TRUSTED || + !(input->fx > 0.0) || !(input->fy > 0.0) || input->cx < 0.0 || + input->cy < 0.0 || input->cx >= input->width || + input->cy >= input->height) + return false; + const double values[] = {input->fx, input->fy, input->cx, input->cy, + input->k1, input->k2, input->p1, input->p2}; + for (size_t index = 0; index < sizeof(values) / sizeof(values[0]); ++index) + if (!sparse_finite(values[index])) + return false; + return true; +} + +static bool sparse_calibration_hash(const Lardon3DSparseCalibration *input, + unsigned char hash[32]) { + if (!sparse_calibration_valid(input)) + return false; + unsigned char bytes[8 + 4 * 6 + 8 * 8 + 4 + 32]; + size_t offset = 0; + memcpy(bytes + offset, "L3D3DCP1", 8); + offset += 8; + sparse_put_u32(bytes, &offset, 1); + sparse_put_u32(bytes, &offset, input->model_kind); + sparse_put_u32(bytes, &offset, input->model_version); + sparse_put_u32(bytes, &offset, input->width); + sparse_put_u32(bytes, &offset, input->height); + const double values[] = {input->fx, input->fy, input->cx, input->cy, + input->k1, input->k2, input->p1, input->p2}; + for (size_t index = 0; index < sizeof(values) / sizeof(values[0]); ++index) + sparse_put_double(bytes, &offset, values[index]); + sparse_put_u32(bytes, &offset, input->provenance_kind); + memcpy(bytes + offset, input->provenance_fingerprint, 32); + offset += 32; + return sparse_hash(bytes, offset, hash); +} + +static Lardon3DProjectDbResult +sparse_load_calibration_locked(Lardon3DProjectDb *db, uint64_t id, + Lardon3DSparseCalibration *output) { + sqlite3_stmt *statement = NULL; + Lardon3DProjectDbResult result = + prepare(db, + "SELECT " + "scientific_hash,model_kind,model_version,width,height,fx,fy,cx," + "cy,k1,k2,p1,p2," + "provenance_kind,provenance_fingerprint FROM sparse_calibrations " + "WHERE calibration_id=?1", + &statement); + if (result != LARDON3D_PROJECT_DB_OK) + return result; + sqlite3_bind_int64(statement, 1, (sqlite3_int64)id); + int code = sqlite3_step(statement); + if (code == SQLITE_DONE) + result = LARDON3D_PROJECT_DB_NOT_FOUND; + else if (code != SQLITE_ROW || sqlite3_column_bytes(statement, 0) != 32 || + sqlite3_column_bytes(statement, 14) != 32) + result = LARDON3D_PROJECT_DB_CORRUPT; + else { + memset(output, 0, sizeof(*output)); + output->calibration_id = id; + memcpy(output->scientific_hash, sqlite3_column_blob(statement, 0), 32); + output->model_kind = (uint32_t)sqlite3_column_int64(statement, 1); + output->model_version = (uint32_t)sqlite3_column_int64(statement, 2); + output->width = (uint32_t)sqlite3_column_int64(statement, 3); + output->height = (uint32_t)sqlite3_column_int64(statement, 4); + output->fx = sqlite3_column_double(statement, 5); + output->fy = sqlite3_column_double(statement, 6); + output->cx = sqlite3_column_double(statement, 7); + output->cy = sqlite3_column_double(statement, 8); + output->k1 = sqlite3_column_double(statement, 9); + output->k2 = sqlite3_column_double(statement, 10); + output->p1 = sqlite3_column_double(statement, 11); + output->p2 = sqlite3_column_double(statement, 12); + output->provenance_kind = (uint32_t)sqlite3_column_int64(statement, 13); + memcpy(output->provenance_fingerprint, sqlite3_column_blob(statement, 14), + 32); + unsigned char expected[32]; + if (!sparse_calibration_hash(output, expected) || + memcmp(expected, output->scientific_hash, 32) != 0) + result = LARDON3D_PROJECT_DB_CORRUPT; + } + sqlite3_finalize(statement); + if (result != LARDON3D_PROJECT_DB_OK) + memset(output, 0, sizeof(*output)); + return result; +} + +Lardon3DProjectDbResult +lardon3d_sparse_calibration_create(Lardon3DProjectDb *db, + const Lardon3DSparseCalibration *input, + Lardon3DSparseCalibration *output) { + if (!db || !sparse_calibration_valid(input) || !output) + return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; + unsigned char hash[32]; + if (!sparse_calibration_hash(input, hash)) + return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; + (void)pthread_mutex_lock(&db->mutex); + Lardon3DProjectDbResult result = + execute(db, "BEGIN IMMEDIATE", "begin calibration"); + sqlite3_stmt *statement = NULL; + if (result == LARDON3D_PROJECT_DB_OK) + result = prepare( + db, + "INSERT OR IGNORE INTO " + "sparse_calibrations(scientific_hash,model_kind,model_version,width," + "height," + "fx,fy,cx,cy,k1,k2,p1,p2,provenance_kind,provenance_fingerprint) " + "VALUES(?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14,?15)", + &statement); + if (result == LARDON3D_PROJECT_DB_OK) { + sqlite3_bind_blob(statement, 1, hash, 32, SQLITE_TRANSIENT); + sqlite3_bind_int64(statement, 2, input->model_kind); + sqlite3_bind_int64(statement, 3, input->model_version); + sqlite3_bind_int64(statement, 4, input->width); + sqlite3_bind_int64(statement, 5, input->height); + sqlite3_bind_double(statement, 6, input->fx); + sqlite3_bind_double(statement, 7, input->fy); + sqlite3_bind_double(statement, 8, input->cx); + sqlite3_bind_double(statement, 9, input->cy); + sqlite3_bind_double(statement, 10, input->k1); + sqlite3_bind_double(statement, 11, input->k2); + sqlite3_bind_double(statement, 12, input->p1); + sqlite3_bind_double(statement, 13, input->p2); + sqlite3_bind_int64(statement, 14, input->provenance_kind); + sqlite3_bind_blob(statement, 15, input->provenance_fingerprint, 32, + SQLITE_TRANSIENT); + result = step_done(db, statement, "insert calibration"); + statement = NULL; + } + if (statement) + sqlite3_finalize(statement); + uint64_t id = 0; + if (result == LARDON3D_PROJECT_DB_OK) { + result = prepare(db, + "SELECT calibration_id FROM sparse_calibrations WHERE " + "scientific_hash=?1", + &statement); + if (result == LARDON3D_PROJECT_DB_OK) { + sqlite3_bind_blob(statement, 1, hash, 32, SQLITE_TRANSIENT); + if (sqlite3_step(statement) != SQLITE_ROW) + result = LARDON3D_PROJECT_DB_CORRUPT; + else + id = (uint64_t)sqlite3_column_int64(statement, 0); + sqlite3_finalize(statement); + } + } + if (result == LARDON3D_PROJECT_DB_OK) + result = execute(db, "COMMIT", "commit calibration"); + if (result != LARDON3D_PROJECT_DB_OK) + (void)execute(db, "ROLLBACK", "rollback calibration"); + if (result == LARDON3D_PROJECT_DB_OK) + result = sparse_load_calibration_locked(db, id, output); + (void)pthread_mutex_unlock(&db->mutex); + return result; +} + +Lardon3DProjectDbResult +lardon3d_sparse_calibration_load(Lardon3DProjectDb *db, uint64_t id, + Lardon3DSparseCalibration *output) { + if (!db || !id || !output) + return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; + (void)pthread_mutex_lock(&db->mutex); + Lardon3DProjectDbResult result = + sparse_load_calibration_locked(db, id, output); + (void)pthread_mutex_unlock(&db->mutex); + return result; +} + +Lardon3DProjectDbResult +lardon3d_sparse_calibration_find_by_hash(Lardon3DProjectDb *db, + const unsigned char hash[32], + Lardon3DSparseCalibration *output) { + if (!db || !hash || !output) + return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; + (void)pthread_mutex_lock(&db->mutex); + sqlite3_stmt *statement = NULL; + Lardon3DProjectDbResult result = prepare( + db, + "SELECT calibration_id FROM sparse_calibrations WHERE scientific_hash=?1", + &statement); + if (result == LARDON3D_PROJECT_DB_OK) { + sqlite3_bind_blob(statement, 1, hash, 32, SQLITE_TRANSIENT); + int code = sqlite3_step(statement); + if (code == SQLITE_DONE) + result = LARDON3D_PROJECT_DB_NOT_FOUND; + else if (code != SQLITE_ROW) + result = LARDON3D_PROJECT_DB_CORRUPT; + else + result = sparse_load_calibration_locked( + db, (uint64_t)sqlite3_column_int64(statement, 0), output); + sqlite3_finalize(statement); + } + if (result != LARDON3D_PROJECT_DB_OK) + memset(output, 0, sizeof(*output)); + (void)pthread_mutex_unlock(&db->mutex); + return result; +} + +static int sparse_scope_member_compare(const void *left, const void *right) { + const Lardon3DSparseCalibrationMember *a = left; + const Lardon3DSparseCalibrationMember *b = right; + if (a->image_id < b->image_id) + return -1; + if (a->image_id > b->image_id) + return 1; + return 0; +} + +static bool sparse_scope_hash(const Lardon3DSparseCalibrationMember *members, + size_t count, unsigned char hash[32]) { + if (!members || count == 0 || count > (SIZE_MAX - 20U) / 40U) + return false; + size_t size = 20U + count * 40U; + unsigned char *bytes = malloc(size); + if (!bytes) + return false; + size_t offset = 0; + memcpy(bytes, "L3D3DSC1", 8); + offset = 8; + sparse_put_u32(bytes, &offset, 1); + sparse_put_u64(bytes, &offset, count); + for (size_t index = 0; index < count; ++index) { + sparse_put_u64(bytes, &offset, members[index].image_id); + memcpy(bytes + offset, members[index].calibration_hash, 32); + offset += 32; + } + bool ok = sparse_hash(bytes, offset, hash); + free(bytes); + return ok; +} + +static bool sparse_scope_hash_locked(Lardon3DProjectDb *db, uint64_t scope_id, + uint64_t member_count, + unsigned char hash[32]) { + if (member_count > (uint64_t)INT64_MAX) + return false; + EVP_MD_CTX *context = EVP_MD_CTX_new(); + if (!context || EVP_DigestInit_ex(context, EVP_sha256(), NULL) != 1) { + EVP_MD_CTX_free(context); + return false; + } + unsigned char header[20]; + size_t offset = 0; + memcpy(header, "L3D3DSC1", 8); + offset = 8; + sparse_put_u32(header, &offset, 1); + sparse_put_u64(header, &offset, member_count); + bool ok = EVP_DigestUpdate(context, header, sizeof(header)) == 1; + sqlite3_stmt *statement = NULL; + if (ok) + ok = prepare( + db, + "SELECT m.image_id,c.scientific_hash FROM " + "sparse_calibration_scope_images AS m JOIN sparse_calibrations AS c " + "ON c.calibration_id=m.calibration_id WHERE m.scope_id=?1 " + "ORDER BY m.image_id", + &statement) == LARDON3D_PROJECT_DB_OK; + if (ok) { + sqlite3_bind_int64(statement, 1, (sqlite3_int64)scope_id); + uint64_t count = 0; + int code = SQLITE_ROW; + while ((code = sqlite3_step(statement)) == SQLITE_ROW) { + if (sqlite3_column_bytes(statement, 1) != 32 || count == UINT64_MAX) { + ok = false; + break; + } + unsigned char member[40]; + offset = 0; + sparse_put_u64(member, &offset, + (uint64_t)sqlite3_column_int64(statement, 0)); + memcpy(member + offset, sqlite3_column_blob(statement, 1), 32); + if (EVP_DigestUpdate(context, member, sizeof(member)) != 1) { + ok = false; + break; + } + ++count; + } + if (code != SQLITE_DONE || count != member_count) + ok = false; + sqlite3_finalize(statement); + } + unsigned int length = 0; + if (ok) + ok = EVP_DigestFinal_ex(context, hash, &length) == 1 && length == 32; + EVP_MD_CTX_free(context); + return ok; +} + +Lardon3DProjectDbResult lardon3d_sparse_calibration_scope_create( + Lardon3DProjectDb *db, const Lardon3DSparseCalibrationMember *members, + size_t member_count, Lardon3DSparseCalibrationScope *output) { + if (!db || !members || !output || member_count == 0 || + member_count > INT64_MAX) + return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; + if (member_count > SIZE_MAX / sizeof(*members)) + return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; + + Lardon3DSparseCalibrationMember *canonical = + malloc(member_count * sizeof(*canonical)); + if (!canonical) + return LARDON3D_PROJECT_DB_IO_ERROR; + memcpy(canonical, members, member_count * sizeof(*canonical)); + qsort(canonical, member_count, sizeof(*canonical), + sparse_scope_member_compare); + for (size_t index = 0; index < member_count; ++index) { + if (!sparse_fits_sqlite(canonical[index].image_id) || + canonical[index].image_id == 0 || + (index && canonical[index - 1].image_id == canonical[index].image_id)) { + free(canonical); + return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; + } + } + unsigned char scope_hash[32]; + if (!sparse_scope_hash(canonical, member_count, scope_hash)) { + free(canonical); + return LARDON3D_PROJECT_DB_IO_ERROR; + } + + (void)pthread_mutex_lock(&db->mutex); + Lardon3DProjectDbResult result = + execute(db, "BEGIN IMMEDIATE", "begin calibration scope"); + sqlite3_stmt *statement = NULL; + if (result == LARDON3D_PROJECT_DB_OK) + result = prepare(db, + "SELECT scientific_hash FROM sparse_calibrations " + "WHERE calibration_id=?1", + &statement); + for (size_t index = 0; + result == LARDON3D_PROJECT_DB_OK && index < member_count; ++index) { + sqlite3_bind_int64(statement, 1, + (sqlite3_int64)canonical[index].calibration_id); + if (sqlite3_step(statement) != SQLITE_ROW || + sqlite3_column_bytes(statement, 0) != 32 || + memcmp(sqlite3_column_blob(statement, 0), canonical[index].calibration_hash, + 32) != 0) + result = LARDON3D_PROJECT_DB_CONSTRAINT; + sqlite3_reset(statement); + sqlite3_clear_bindings(statement); + } + if (statement) + sqlite3_finalize(statement); + statement = NULL; + if (result == LARDON3D_PROJECT_DB_OK) + result = prepare( + db, + "INSERT OR IGNORE INTO sparse_calibration_scopes(scientific_hash," + "member_count) VALUES(?1,?2)", + &statement); + if (result == LARDON3D_PROJECT_DB_OK) { + sqlite3_bind_blob(statement, 1, scope_hash, 32, SQLITE_TRANSIENT); + sqlite3_bind_int64(statement, 2, (sqlite3_int64)member_count); + result = step_done(db, statement, "insert calibration scope"); + statement = NULL; + } + if (statement) + sqlite3_finalize(statement); + + uint64_t scope_id = 0; + if (result == LARDON3D_PROJECT_DB_OK) { + result = prepare(db, + "SELECT scope_id FROM sparse_calibration_scopes " + "WHERE scientific_hash=?1", + &statement); + if (result == LARDON3D_PROJECT_DB_OK) { + sqlite3_bind_blob(statement, 1, scope_hash, 32, SQLITE_TRANSIENT); + if (sqlite3_step(statement) != SQLITE_ROW) + result = LARDON3D_PROJECT_DB_CORRUPT; + else + scope_id = (uint64_t)sqlite3_column_int64(statement, 0); + sqlite3_finalize(statement); + statement = NULL; + } + } + if (result == LARDON3D_PROJECT_DB_OK) + result = prepare( + db, + "INSERT OR IGNORE INTO sparse_calibration_scope_images(scope_id," + "image_id,calibration_id) VALUES(?1,?2,?3)", + &statement); + for (size_t index = 0; + result == LARDON3D_PROJECT_DB_OK && index < member_count; ++index) { + sqlite3_bind_int64(statement, 1, (sqlite3_int64)scope_id); + sqlite3_bind_int64(statement, 2, (sqlite3_int64)canonical[index].image_id); + sqlite3_bind_int64(statement, 3, + (sqlite3_int64)canonical[index].calibration_id); + result = + sparse_step_reuse(db, statement, "insert calibration scope member"); + } + if (statement) + sqlite3_finalize(statement); + if (result == LARDON3D_PROJECT_DB_OK) + result = execute(db, "COMMIT", "commit calibration scope"); + if (result != LARDON3D_PROJECT_DB_OK) + (void)execute(db, "ROLLBACK", "rollback calibration scope"); + if (result == LARDON3D_PROJECT_DB_OK) { + memset(output, 0, sizeof(*output)); + output->scope_id = scope_id; + memcpy(output->scientific_hash, scope_hash, 32); + output->member_count = member_count; + } + free(canonical); + (void)pthread_mutex_unlock(&db->mutex); + return result; +} + +Lardon3DProjectDbResult +lardon3d_sparse_calibration_scope_load(Lardon3DProjectDb *db, uint64_t scope_id, + Lardon3DSparseCalibrationScope *output) { + if (!db || !scope_id || !output) + return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; + (void)pthread_mutex_lock(&db->mutex); + sqlite3_stmt *statement = NULL; + Lardon3DProjectDbResult result = + prepare(db, + "SELECT scientific_hash,member_count FROM " + "sparse_calibration_scopes WHERE scope_id=?1", + &statement); + if (result == LARDON3D_PROJECT_DB_OK) { + sqlite3_bind_int64(statement, 1, (sqlite3_int64)(sqlite3_int64)scope_id); + int code = sqlite3_step(statement); + if (code == SQLITE_DONE) + result = LARDON3D_PROJECT_DB_NOT_FOUND; + else if (code != SQLITE_ROW || sqlite3_column_bytes(statement, 0) != 32 || + sqlite3_column_int64(statement, 1) <= 0) + result = LARDON3D_PROJECT_DB_CORRUPT; + else { + memset(output, 0, sizeof(*output)); + output->scope_id = scope_id; + memcpy(output->scientific_hash, sqlite3_column_blob(statement, 0), 32); + output->member_count = (uint64_t)sqlite3_column_int64(statement, 1); + unsigned char expected[32]; + if (!sparse_scope_hash_locked(db, scope_id, output->member_count, + expected) || + memcmp(expected, output->scientific_hash, 32) != 0) + result = LARDON3D_PROJECT_DB_CORRUPT; + } + sqlite3_finalize(statement); + } + (void)pthread_mutex_unlock(&db->mutex); + return result; +} + +Lardon3DProjectDbResult lardon3d_sparse_calibration_scope_find_by_hash( + Lardon3DProjectDb *db, const unsigned char hash[32], + Lardon3DSparseCalibrationScope *output) { + if (!db || !hash || !output) + return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; + (void)pthread_mutex_lock(&db->mutex); + sqlite3_stmt *statement = NULL; + Lardon3DProjectDbResult result = + prepare(db, + "SELECT scope_id FROM sparse_calibration_scopes " + "WHERE scientific_hash=?1", + &statement); + uint64_t scope_id = 0; + if (result == LARDON3D_PROJECT_DB_OK) { + sqlite3_bind_blob(statement, 1, hash, 32, SQLITE_TRANSIENT); + int code = sqlite3_step(statement); + if (code == SQLITE_DONE) + result = LARDON3D_PROJECT_DB_NOT_FOUND; + else if (code != SQLITE_ROW) + result = LARDON3D_PROJECT_DB_CORRUPT; + else + scope_id = (uint64_t)sqlite3_column_int64(statement, 0); + sqlite3_finalize(statement); + } + if (result == LARDON3D_PROJECT_DB_OK) { + result = prepare(db, + "SELECT scientific_hash,member_count FROM " + "sparse_calibration_scopes WHERE scope_id=?1", + &statement); + if (result == LARDON3D_PROJECT_DB_OK) { + sqlite3_bind_int64(statement, 1, (sqlite3_int64)scope_id); + if (sqlite3_step(statement) != SQLITE_ROW || + sqlite3_column_bytes(statement, 0) != 32 || + sqlite3_column_int64(statement, 1) <= 0) { + result = LARDON3D_PROJECT_DB_CORRUPT; + } else { + memset(output, 0, sizeof(*output)); + output->scope_id = scope_id; + memcpy(output->scientific_hash, sqlite3_column_blob(statement, 0), 32); + output->member_count = (uint64_t)sqlite3_column_int64(statement, 1); + unsigned char expected[32]; + if (!sparse_scope_hash_locked(db, scope_id, output->member_count, + expected) || + memcmp(expected, output->scientific_hash, 32) != 0) + result = LARDON3D_PROJECT_DB_CORRUPT; + } + sqlite3_finalize(statement); + } + } + if (result != LARDON3D_PROJECT_DB_OK) + memset(output, 0, sizeof(*output)); + (void)pthread_mutex_unlock(&db->mutex); + return result; +} + +Lardon3DProjectDbResult lardon3d_sparse_calibration_scope_list_members( + Lardon3DProjectDb *db, uint64_t scope_id, uint64_t after_image_id, + Lardon3DSparseCalibrationMember *items, size_t capacity, size_t *count, + uint64_t *next_after_image_id) { + if (!db || !scope_id || !items || !count || !next_after_image_id || + capacity > LARDON3D_SPARSE_SFM_PAGE_MAX) + return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; + *count = 0; + *next_after_image_id = after_image_id; + (void)pthread_mutex_lock(&db->mutex); + sqlite3_stmt *statement = NULL; + Lardon3DProjectDbResult result = prepare( + db, + "SELECT image_id,calibration_id FROM sparse_calibration_scope_images " + "WHERE scope_id=?1 AND image_id>?2 ORDER BY image_id LIMIT ?3", + &statement); + if (result == LARDON3D_PROJECT_DB_OK) { + sqlite3_bind_int64(statement, 1, (sqlite3_int64)(sqlite3_int64)scope_id); + sqlite3_bind_int64(statement, 2, (sqlite3_int64)after_image_id); + sqlite3_bind_int64(statement, 3, (sqlite3_int64)(sqlite3_int64)capacity); + while (*count < capacity && sqlite3_step(statement) == SQLITE_ROW) { + items[*count].image_id = (uint64_t)sqlite3_column_int64(statement, 0); + items[*count].calibration_id = + (uint64_t)sqlite3_column_int64(statement, 1); + Lardon3DSparseCalibration calibration; + result = sparse_load_calibration_locked(db, items[*count].calibration_id, + &calibration); + if (result != LARDON3D_PROJECT_DB_OK) + break; + memcpy(items[*count].calibration_hash, calibration.scientific_hash, 32); + *next_after_image_id = items[*count].image_id; + ++*count; + } + sqlite3_finalize(statement); + } + if (result != LARDON3D_PROJECT_DB_OK) { + *count = 0; + *next_after_image_id = after_image_id; + } + (void)pthread_mutex_unlock(&db->mutex); + return result; +} + +static bool sparse_rotation_valid(const double rotation[9]) { + if (!rotation) + return false; + for (size_t index = 0; index < 9; ++index) + if (!sparse_finite(rotation[index])) + return false; + double determinant = + rotation[0] * (rotation[4] * rotation[8] - rotation[5] * rotation[7]) - + rotation[1] * (rotation[3] * rotation[8] - rotation[5] * rotation[6]) + + rotation[2] * (rotation[3] * rotation[7] - rotation[4] * rotation[6]); + if (fabs(determinant - 1.0) > 1e-6) + return false; + for (size_t row = 0; row < 3; ++row) + for (size_t column = 0; column < 3; ++column) { + double dot = 0.0; + for (size_t axis = 0; axis < 3; ++axis) + dot += rotation[row * 3 + axis] * rotation[column * 3 + axis]; + if (fabs(dot - (row == column ? 1.0 : 0.0)) > 1e-6) + return false; + } + return true; +} + +static bool +sparse_publication_valid(const Lardon3DSparsePublication *publication) { + if (!publication || !sparse_fits_sqlite(publication->track_set_id) || + !sparse_fits_sqlite(publication->calibration_scope_id) || + !publication->track_set_id || !publication->calibration_scope_id || + publication->sfm_kind != LARDON3D_SPARSE_SFM_KIND_INCREMENTAL || + publication->sfm_version != 1 || !publication->components || + publication->component_count == 0 || !publication->registered_images || + publication->registered_image_count < 2 || !publication->landmarks || + publication->landmark_count == 0 || + (!publication->observations && publication->observation_count) || + !sparse_finite(publication->reprojection_rmse_px) || + !sparse_finite(publication->reprojection_median_px)) + return false; + for (size_t index = 0; index < publication->component_count; ++index) { + const Lardon3DSparseComponent *component = &publication->components[index]; + if (!sparse_fits_sqlite(component->component_key) || + !sparse_fits_sqlite(component->registered_image_count) || + !sparse_fits_sqlite(component->landmark_count) || + !component->component_key || !component->registered_image_count || + !component->landmark_count || + (index && publication->components[index - 1].component_key >= + component->component_key)) + return false; + } + for (size_t index = 0; index < publication->registered_image_count; ++index) { + const Lardon3DSparseRegisteredImage *pose = + &publication->registered_images[index]; + if (!sparse_fits_sqlite(pose->image_id) || + !sparse_fits_sqlite(pose->component_key) || !pose->image_id || + !pose->component_key || !sparse_rotation_valid(pose->rotation_cw) || + !sparse_finite(pose->translation_cw[0]) || + !sparse_finite(pose->translation_cw[1]) || + !sparse_finite(pose->translation_cw[2]) || + (index && + publication->registered_images[index - 1].image_id >= pose->image_id)) + return false; + } + if (publication->component_count > SIZE_MAX / sizeof(size_t) / 3U || + publication->component_count > SIZE_MAX / sizeof(uint64_t)) + return false; + size_t *component_image_counts = + calloc(publication->component_count * 3U, sizeof(size_t)); + if (!component_image_counts) + return false; + uint64_t *minimum_image_ids = + malloc(publication->component_count * sizeof(*minimum_image_ids)); + if (!minimum_image_ids) { + free(component_image_counts); + return false; + } + for (size_t index = 0; index < publication->component_count; ++index) + minimum_image_ids[index] = UINT64_MAX; + bool components_valid = true; + for (size_t index = 0; + components_valid && index < publication->registered_image_count; + ++index) { + size_t low = 0; + size_t high = publication->component_count; + uint64_t key = publication->registered_images[index].component_key; + while (low < high) { + size_t middle = low + (high - low) / 2; + if (publication->components[middle].component_key < key) + low = middle + 1; + else + high = middle; + } + if (low == publication->component_count || + publication->components[low].component_key != key) + components_valid = false; + else { + ++component_image_counts[low]; + if (publication->registered_images[index].image_id < + minimum_image_ids[low]) + minimum_image_ids[low] = publication->registered_images[index].image_id; + } + } + for (size_t index = 0; + components_valid && index < publication->landmark_count; ++index) { + size_t low = 0; + size_t high = publication->component_count; + uint64_t key = publication->landmarks[index].component_key; + while (low < high) { + size_t middle = low + (high - low) / 2; + if (publication->components[middle].component_key < key) + low = middle + 1; + else + high = middle; + } + if (low == publication->component_count || + publication->components[low].component_key != key) + components_valid = false; + else + ++component_image_counts[publication->component_count + low]; + } + for (size_t index = 0; + components_valid && index < publication->component_count; ++index) + if (component_image_counts[index] != + publication->components[index].registered_image_count || + component_image_counts[publication->component_count + index] != + publication->components[index].landmark_count || + minimum_image_ids[index] != publication->components[index].component_key) + components_valid = false; + free(minimum_image_ids); + free(component_image_counts); + if (!components_valid) + return false; + for (size_t index = 0; index < publication->landmark_count; ++index) { + const Lardon3DSparseLandmark *landmark = &publication->landmarks[index]; + if (!sparse_fits_sqlite(landmark->track_id) || + !sparse_fits_sqlite(landmark->component_key) || + !sparse_fits_sqlite(landmark->observation_count) || + !landmark->track_id || !landmark->component_key || + !landmark->observation_count || !sparse_finite(landmark->x) || + !sparse_finite(landmark->y) || !sparse_finite(landmark->z) || + !sparse_finite(landmark->reprojection_rmse_px) || + !sparse_finite(landmark->reprojection_median_px) || + (index && + publication->landmarks[index - 1].track_id >= landmark->track_id)) + return false; + } + return true; +} + +static const Lardon3DSparseLandmark * +sparse_find_landmark(const Lardon3DSparsePublication *publication, + uint64_t track_id) { + size_t low = 0; + size_t high = publication->landmark_count; + while (low < high) { + size_t middle = low + (high - low) / 2; + uint64_t candidate = publication->landmarks[middle].track_id; + if (candidate == track_id) + return &publication->landmarks[middle]; + if (candidate < track_id) + low = middle + 1; + else + high = middle; + } + return NULL; +} + +static const Lardon3DSparseComponent *sparse_find_component( + const Lardon3DSparsePublication *publication, uint64_t component_key) { + size_t low = 0; + size_t high = publication->component_count; + while (low < high) { + size_t middle = low + (high - low) / 2; + uint64_t candidate = publication->components[middle].component_key; + if (candidate == component_key) + return &publication->components[middle]; + if (candidate < component_key) + low = middle + 1; + else + high = middle; + } + return NULL; +} + +static const Lardon3DSparseRegisteredImage *sparse_find_registered_image( + const Lardon3DSparsePublication *publication, uint64_t image_id) { + size_t low = 0; + size_t high = publication->registered_image_count; + while (low < high) { + size_t middle = low + (high - low) / 2; + uint64_t candidate = publication->registered_images[middle].image_id; + if (candidate == image_id) + return &publication->registered_images[middle]; + if (candidate < image_id) + low = middle + 1; + else + high = middle; + } + return NULL; +} + +static Lardon3DProjectDbResult +sparse_validate_relations(Lardon3DProjectDb *db, + const Lardon3DSparsePublication *publication) { + sqlite3_stmt *scope_statement = NULL; + sqlite3_stmt *track_statement = NULL; + sqlite3_stmt *observation_statement = NULL; + sqlite3_stmt *track_component_statement = NULL; + Lardon3DProjectDbResult result = + prepare(db, + "SELECT 1 FROM sparse_calibration_scope_images " + "WHERE scope_id=?1 AND image_id=?2", + &scope_statement); + if (result == LARDON3D_PROJECT_DB_OK) + result = prepare(db, + "SELECT 1 FROM tracks WHERE track_set_id=?1 " + "AND track_id=?2", + &track_statement); + if (result == LARDON3D_PROJECT_DB_OK) + result = + prepare(db, + "SELECT 1 FROM track_observations WHERE track_set_id=?1 " + "AND track_id=?2 AND feature_set_id=?3 AND feature_index=?4 " + "AND position_in_track=?5", + &observation_statement); + if (result == LARDON3D_PROJECT_DB_OK) + result = prepare( + db, + "SELECT f.image_id FROM track_observations AS t JOIN feature_sets AS f " + "ON f.feature_set_id=t.feature_set_id WHERE t.track_set_id=?1 AND " + "t.track_id=?2 ORDER BY t.position_in_track", + &track_component_statement); + for (size_t index = 0; + result == LARDON3D_PROJECT_DB_OK && + index < publication->registered_image_count; + ++index) { + const Lardon3DSparseRegisteredImage *pose = + &publication->registered_images[index]; + if (!sparse_find_component(publication, pose->component_key)) { + result = LARDON3D_PROJECT_DB_CONSTRAINT; + break; + } + } + for (size_t index = 0; + result == LARDON3D_PROJECT_DB_OK && index < publication->landmark_count; + ++index) { + const Lardon3DSparseLandmark *landmark = &publication->landmarks[index]; + if (!sparse_find_component(publication, landmark->component_key)) { + result = LARDON3D_PROJECT_DB_CONSTRAINT; + break; + } + sqlite3_bind_int64(track_component_statement, 1, + (sqlite3_int64)publication->track_set_id); + sqlite3_bind_int64(track_component_statement, 2, + (sqlite3_int64)landmark->track_id); + while (sqlite3_step(track_component_statement) == SQLITE_ROW) { + uint64_t image_id = (uint64_t)sqlite3_column_int64( + track_component_statement, 0); + const Lardon3DSparseRegisteredImage *pose = + sparse_find_registered_image(publication, image_id); + if (!pose || pose->component_key != landmark->component_key) { + result = LARDON3D_PROJECT_DB_CONSTRAINT; + break; + } + } + sqlite3_reset(track_component_statement); + sqlite3_clear_bindings(track_component_statement); + } + for (size_t index = 0; result == LARDON3D_PROJECT_DB_OK && + index < publication->registered_image_count; + ++index) { + sqlite3_bind_int64(scope_statement, 1, + (sqlite3_int64)publication->calibration_scope_id); + sqlite3_bind_int64( + scope_statement, 2, + (sqlite3_int64)publication->registered_images[index].image_id); + int scope_code = sqlite3_step(scope_statement); + if (scope_code != SQLITE_ROW) + if (scope_code != SQLITE_ROW) + result = LARDON3D_PROJECT_DB_CONSTRAINT; + sqlite3_reset(scope_statement); + sqlite3_clear_bindings(scope_statement); + } + for (size_t index = 0; + result == LARDON3D_PROJECT_DB_OK && index < publication->landmark_count; + ++index) { + sqlite3_bind_int64(track_statement, 1, + (sqlite3_int64)publication->track_set_id); + sqlite3_bind_int64(track_statement, 2, + (sqlite3_int64)publication->landmarks[index].track_id); + int track_code = sqlite3_step(track_statement); + if (track_code != SQLITE_ROW) + if (track_code != SQLITE_ROW) + result = LARDON3D_PROJECT_DB_CONSTRAINT; + sqlite3_reset(track_statement); + sqlite3_clear_bindings(track_statement); + } + for (size_t index = 0; result == LARDON3D_PROJECT_DB_OK && + index < publication->observation_count; + ++index) { + const Lardon3DSparseLandmarkObservation *ob = + &publication->observations[index]; + if (!sparse_find_landmark(publication, ob->track_id)) { + result = LARDON3D_PROJECT_DB_CONSTRAINT; + break; + } + sqlite3_bind_int64(observation_statement, 1, + (sqlite3_int64)publication->track_set_id); + sqlite3_bind_int64(observation_statement, 2, (sqlite3_int64)ob->track_id); + sqlite3_bind_int64(observation_statement, 3, + (sqlite3_int64)ob->feature_set_id); + sqlite3_bind_int64(observation_statement, 4, ob->feature_index); + sqlite3_bind_int64(observation_statement, 5, ob->position_in_track); + int observation_code = sqlite3_step(observation_statement); + if (observation_code != SQLITE_ROW) + if (observation_code != SQLITE_ROW) + result = LARDON3D_PROJECT_DB_CONSTRAINT; + sqlite3_reset(observation_statement); + sqlite3_clear_bindings(observation_statement); + } + if (scope_statement) + sqlite3_finalize(scope_statement); + if (track_statement) + sqlite3_finalize(track_statement); + if (observation_statement) + sqlite3_finalize(observation_statement); + if (track_component_statement) + sqlite3_finalize(track_component_statement); + return result; +} + +static Lardon3DProjectDbResult +sparse_load_reconstruction_locked(Lardon3DProjectDb *db, uint64_t id, + Lardon3DSparseReconstruction *output) { + sqlite3_stmt *statement = NULL; + Lardon3DProjectDbResult result = + prepare(db, + "SELECT " + "track_set_id,calibration_scope_id,sfm_kind,sfm_version," + "parameter_fingerprint,component_count,registered_image_count," + "landmark_count,reprojection_rmse_px,reprojection_median_px FROM " + "sparse_reconstructions WHERE reconstruction_id=?1", + &statement); + if (result != LARDON3D_PROJECT_DB_OK) + return result; + sqlite3_bind_int64(statement, 1, (sqlite3_int64)id); + int code = sqlite3_step(statement); + if (code == SQLITE_DONE) + result = LARDON3D_PROJECT_DB_NOT_FOUND; + else if (code != SQLITE_ROW || sqlite3_column_bytes(statement, 4) != 32) + result = LARDON3D_PROJECT_DB_CORRUPT; + else { + memset(output, 0, sizeof(*output)); + output->reconstruction_id = id; + output->track_set_id = (uint64_t)sqlite3_column_int64(statement, 0); + output->calibration_scope_id = (uint64_t)sqlite3_column_int64(statement, 1); + output->sfm_kind = (uint32_t)sqlite3_column_int64(statement, 2); + output->sfm_version = (uint32_t)sqlite3_column_int64(statement, 3); + memcpy(output->parameter_fingerprint, sqlite3_column_blob(statement, 4), + 32); + output->component_count = (uint64_t)sqlite3_column_int64(statement, 5); + output->registered_image_count = + (uint64_t)sqlite3_column_int64(statement, 6); + output->landmark_count = (uint64_t)sqlite3_column_int64(statement, 7); + output->reprojection_rmse_px = sqlite3_column_double(statement, 8); + output->reprojection_median_px = sqlite3_column_double(statement, 9); + if (!output->track_set_id || !output->calibration_scope_id || + output->sfm_kind != LARDON3D_SPARSE_SFM_KIND_INCREMENTAL || + output->sfm_version != LARDON3D_SPARSE_SFM_VERSION || + output->component_count == 0 || output->registered_image_count < 2 || + output->landmark_count == 0 || + !sparse_finite(output->reprojection_rmse_px) || + !sparse_finite(output->reprojection_median_px)) + result = LARDON3D_PROJECT_DB_CORRUPT; + } + sqlite3_finalize(statement); + if (result != LARDON3D_PROJECT_DB_OK) + memset(output, 0, sizeof(*output)); + return result; +} + +Lardon3DProjectDbResult lardon3d_sparse_reconstruction_publish( + Lardon3DProjectDb *db, const Lardon3DSparsePublication *publication, + Lardon3DSparseReconstruction *output) { + if (!db || !output || !sparse_publication_valid(publication)) + return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; + (void)pthread_mutex_lock(&db->mutex); + Lardon3DProjectDbResult result = + execute(db, "BEGIN IMMEDIATE", "begin sparse reconstruction"); + sqlite3_stmt *statement = NULL; + if (result == LARDON3D_PROJECT_DB_OK) + result = prepare(db, "SELECT 1 FROM track_sets WHERE track_set_id=?1", + &statement); + if (result == LARDON3D_PROJECT_DB_OK) { + sqlite3_bind_int64(statement, 1, + (sqlite3_int64)(sqlite3_int64)publication->track_set_id); + if (sqlite3_step(statement) != SQLITE_ROW) + result = LARDON3D_PROJECT_DB_NOT_FOUND; + sqlite3_finalize(statement); + statement = NULL; + } + if (result == LARDON3D_PROJECT_DB_OK) + result = + prepare(db, "SELECT 1 FROM sparse_calibration_scopes WHERE scope_id=?1", + &statement); + if (result == LARDON3D_PROJECT_DB_OK) { + sqlite3_bind_int64( + statement, 1, + (sqlite3_int64)(sqlite3_int64)publication->calibration_scope_id); + if (sqlite3_step(statement) != SQLITE_ROW) + result = LARDON3D_PROJECT_DB_NOT_FOUND; + sqlite3_finalize(statement); + statement = NULL; + } + if (result == LARDON3D_PROJECT_DB_OK) + result = sparse_validate_relations(db, publication); + uint64_t reconstruction_id = 0; + if (result == LARDON3D_PROJECT_DB_OK) + result = prepare( + db, + "INSERT INTO " + "sparse_reconstructions(track_set_id,calibration_scope_id,sfm_kind,sfm_" + "version,parameter_fingerprint,component_count,registered_image_count," + "landmark_count,reprojection_rmse_px,reprojection_median_px,created_at)" + " VALUES(?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11)", + &statement); + if (result == LARDON3D_PROJECT_DB_OK) { + sqlite3_bind_int64(statement, 1, + (sqlite3_int64)(sqlite3_int64)publication->track_set_id); + sqlite3_bind_int64( + statement, 2, + (sqlite3_int64)(sqlite3_int64)publication->calibration_scope_id); + sqlite3_bind_int64(statement, 3, publication->sfm_kind); + sqlite3_bind_int64(statement, 4, publication->sfm_version); + sqlite3_bind_blob(statement, 5, publication->parameter_fingerprint, 32, + SQLITE_TRANSIENT); + sqlite3_bind_int64(statement, 6, + (sqlite3_int64)publication->component_count); + sqlite3_bind_int64(statement, 7, + (sqlite3_int64)publication->registered_image_count); + sqlite3_bind_int64(statement, 8, + (sqlite3_int64)publication->landmark_count); + sqlite3_bind_double(statement, 9, publication->reprojection_rmse_px); + sqlite3_bind_double(statement, 10, publication->reprojection_median_px); + sqlite3_bind_int64(statement, 11, publication->created_at); + result = step_done(db, statement, "insert sparse reconstruction"); + statement = NULL; + } + if (statement) + sqlite3_finalize(statement); + if (result == LARDON3D_PROJECT_DB_OK) + reconstruction_id = (uint64_t)sqlite3_last_insert_rowid(db->connection); + if (result == LARDON3D_PROJECT_DB_OK) + result = prepare( + db, + "INSERT INTO " + "sparse_reconstruction_components(reconstruction_id,component_key," + "registered_image_count,landmark_count) VALUES(?1,?2,?3,?4)", + &statement); + for (size_t index = 0; + result == LARDON3D_PROJECT_DB_OK && index < publication->component_count; + ++index) { + sqlite3_bind_int64(statement, 1, (sqlite3_int64)reconstruction_id); + sqlite3_bind_int64( + statement, 2, + (sqlite3_int64)publication->components[index].component_key); + sqlite3_bind_int64( + statement, 3, + (sqlite3_int64)publication->components[index].registered_image_count); + sqlite3_bind_int64( + statement, 4, + (sqlite3_int64)publication->components[index].landmark_count); + result = sparse_step_reuse(db, statement, "insert sparse component"); + } + if (statement) + sqlite3_finalize(statement); + statement = NULL; + if (result == LARDON3D_PROJECT_DB_OK) + result = prepare(db, + "INSERT INTO " + "sparse_registered_images(reconstruction_id,component_id," + "image_id,rotation_00,rotation_01,rotation_02,rotation_10," + "rotation_11,rotation_12,rotation_20,rotation_21,rotation_" + "22,translation_0,translation_1,translation_2) SELECT " + "?1,component_id,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?" + "14 FROM sparse_reconstruction_components WHERE " + "reconstruction_id=?1 AND component_key=?15", + &statement); + for (size_t index = 0; result == LARDON3D_PROJECT_DB_OK && + index < publication->registered_image_count; + ++index) { + const Lardon3DSparseRegisteredImage *pose = + &publication->registered_images[index]; + sqlite3_bind_int64(statement, 1, (sqlite3_int64)reconstruction_id); + sqlite3_bind_int64(statement, 2, (sqlite3_int64)pose->image_id); + for (int bind = 0; bind < 9; ++bind) + sqlite3_bind_double(statement, 3 + bind, pose->rotation_cw[bind]); + for (int bind = 0; bind < 3; ++bind) + sqlite3_bind_double(statement, 12 + bind, pose->translation_cw[bind]); + sqlite3_bind_int64(statement, 15, (sqlite3_int64)pose->component_key); + result = sparse_step_reuse(db, statement, "insert sparse pose"); + } + if (statement) + sqlite3_finalize(statement); + statement = NULL; + if (result == LARDON3D_PROJECT_DB_OK) + result = + prepare(db, + "INSERT INTO " + "sparse_landmarks(reconstruction_id,component_id,track_id,x,y," + "z,reprojection_rmse_px,reprojection_median_px,observation_" + "count) SELECT ?1,component_id,?2,?3,?4,?5,?6,?7,?8 FROM " + "sparse_reconstruction_components WHERE reconstruction_id=?1 " + "AND component_key=?9", + &statement); + for (size_t index = 0; + result == LARDON3D_PROJECT_DB_OK && index < publication->landmark_count; + ++index) { + const Lardon3DSparseLandmark *landmark = &publication->landmarks[index]; + sqlite3_bind_int64(statement, 1, (sqlite3_int64)reconstruction_id); + sqlite3_bind_int64(statement, 2, (sqlite3_int64)landmark->track_id); + sqlite3_bind_double(statement, 3, landmark->x); + sqlite3_bind_double(statement, 4, landmark->y); + sqlite3_bind_double(statement, 5, landmark->z); + sqlite3_bind_double(statement, 6, landmark->reprojection_rmse_px); + sqlite3_bind_double(statement, 7, landmark->reprojection_median_px); + sqlite3_bind_int64(statement, 8, + (sqlite3_int64)landmark->observation_count); + sqlite3_bind_int64(statement, 9, (sqlite3_int64)landmark->component_key); + result = sparse_step_reuse(db, statement, "insert sparse landmark"); + } + if (statement) + sqlite3_finalize(statement); + statement = NULL; + if (result == LARDON3D_PROJECT_DB_OK) + result = prepare( + db, + "INSERT INTO " + "sparse_landmark_observations(landmark_id,feature_set_id,feature_index," + "position_in_track) SELECT landmark_id,?2,?3,?4 FROM sparse_landmarks " + "WHERE reconstruction_id=?1 AND track_id=?5", + &statement); + for (size_t index = 0; result == LARDON3D_PROJECT_DB_OK && + index < publication->observation_count; + ++index) { + const Lardon3DSparseLandmarkObservation *ob = + &publication->observations[index]; + sqlite3_bind_int64(statement, 1, (sqlite3_int64)reconstruction_id); + sqlite3_bind_int64(statement, 2, (sqlite3_int64)ob->feature_set_id); + sqlite3_bind_int64(statement, 3, ob->feature_index); + sqlite3_bind_int64(statement, 4, ob->position_in_track); + sqlite3_bind_int64(statement, 5, (sqlite3_int64)ob->track_id); + result = sparse_step_reuse(db, statement, "insert sparse observation"); + } + if (statement) + sqlite3_finalize(statement); + if (result == LARDON3D_PROJECT_DB_OK) + result = execute(db, "COMMIT", "commit sparse reconstruction"); + if (result != LARDON3D_PROJECT_DB_OK) + (void)execute(db, "ROLLBACK", "rollback sparse reconstruction"); + if (result == LARDON3D_PROJECT_DB_OK) + result = sparse_load_reconstruction_locked(db, reconstruction_id, output); + (void)pthread_mutex_unlock(&db->mutex); + return result; +} + +Lardon3DProjectDbResult lardon3d_sparse_reconstruction_find_exact( + Lardon3DProjectDb *db, uint64_t track_set_id, uint64_t scope_id, + uint32_t kind, uint32_t version, const unsigned char fingerprint[32], + Lardon3DSparseReconstruction *output) { + if (!db || !track_set_id || !scope_id || !fingerprint || !output) + return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; + (void)pthread_mutex_lock(&db->mutex); + sqlite3_stmt *statement = NULL; + Lardon3DProjectDbResult result = + prepare(db, + "SELECT reconstruction_id FROM sparse_reconstructions WHERE " + "track_set_id=?1 AND calibration_scope_id=?2 AND sfm_kind=?3 AND " + "sfm_version=?4 AND parameter_fingerprint=?5", + &statement); + if (result == LARDON3D_PROJECT_DB_OK) { + sqlite3_bind_int64(statement, 1, (sqlite3_int64)track_set_id); + sqlite3_bind_int64(statement, 2, (sqlite3_int64)scope_id); + sqlite3_bind_int64(statement, 3, kind); + sqlite3_bind_int64(statement, 4, version); + sqlite3_bind_blob(statement, 5, fingerprint, 32, SQLITE_TRANSIENT); + int code = sqlite3_step(statement); + if (code == SQLITE_DONE) + result = LARDON3D_PROJECT_DB_NOT_FOUND; + else if (code != SQLITE_ROW) + result = LARDON3D_PROJECT_DB_CORRUPT; + else + result = sparse_load_reconstruction_locked( + db, (uint64_t)sqlite3_column_int64(statement, 0), output); + sqlite3_finalize(statement); + } + (void)pthread_mutex_unlock(&db->mutex); + return result; +} + +Lardon3DProjectDbResult +lardon3d_sparse_reconstruction_load(Lardon3DProjectDb *db, uint64_t id, + Lardon3DSparseReconstruction *output) { + if (!db || !id || !output) + return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; + (void)pthread_mutex_lock(&db->mutex); + Lardon3DProjectDbResult result = + sparse_load_reconstruction_locked(db, id, output); + (void)pthread_mutex_unlock(&db->mutex); + return result; +} + +Lardon3DProjectDbResult +lardon3d_sparse_reconstruction_list(Lardon3DProjectDb *db, uint64_t after_id, + size_t capacity, + Lardon3DSparseReconstructionPage *page) { + if (!db || !page || !page->items || capacity > 64) + return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; + page->after_id = after_id; + page->capacity = capacity; + page->count = 0; + page->next_after_id = after_id; + (void)pthread_mutex_lock(&db->mutex); + sqlite3_stmt *statement = NULL; + Lardon3DProjectDbResult result = + prepare(db, + "SELECT reconstruction_id FROM sparse_reconstructions WHERE " + "reconstruction_id>?1 ORDER BY reconstruction_id LIMIT ?2", + &statement); + if (result == LARDON3D_PROJECT_DB_OK) { + sqlite3_bind_int64(statement, 1, (sqlite3_int64)after_id); + sqlite3_bind_int64(statement, 2, (sqlite3_int64)capacity); + while (page->count < capacity && sqlite3_step(statement) == SQLITE_ROW) { + uint64_t id = (uint64_t)sqlite3_column_int64(statement, 0); + result = + sparse_load_reconstruction_locked(db, id, &page->items[page->count]); + if (result != LARDON3D_PROJECT_DB_OK) + break; + page->next_after_id = id; + ++page->count; + } + sqlite3_finalize(statement); + } + (void)pthread_mutex_unlock(&db->mutex); + return result; +} + +static bool sparse_page_capacity_valid(size_t capacity) { + return capacity <= LARDON3D_SPARSE_SFM_PAGE_MAX; +} + +Lardon3DProjectDbResult +lardon3d_sparse_component_list(Lardon3DProjectDb *db, + uint64_t reconstruction_id, + uint64_t after_component_key, size_t capacity, + Lardon3DSparseComponentPage *page) { + if (!db || !page || !page->items || !reconstruction_id || + !sparse_page_capacity_valid(capacity)) + return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; + page->after_component_key = after_component_key; + page->capacity = capacity; + page->count = 0; + page->next_component_key = after_component_key; + (void)pthread_mutex_lock(&db->mutex); + sqlite3_stmt *statement = NULL; + Lardon3DProjectDbResult result = + prepare(db, + "SELECT 1 FROM sparse_reconstruction_components AS c WHERE " + "c.reconstruction_id=?1 AND (c.component_key!=(SELECT " + "MIN(r.image_id) FROM sparse_registered_images AS r WHERE " + "r.reconstruction_id=c.reconstruction_id AND " + "r.component_id=c.component_id) OR c.registered_image_count!=(" + "SELECT COUNT(*) FROM sparse_registered_images AS r2 WHERE " + "r2.reconstruction_id=c.reconstruction_id AND " + "r2.component_id=c.component_id) OR c.landmark_count!=(SELECT " + "COUNT(*) FROM sparse_landmarks AS l WHERE " + "l.reconstruction_id=c.reconstruction_id AND " + "l.component_id=c.component_id)) LIMIT 1", + &statement); + if (result == LARDON3D_PROJECT_DB_OK) { + sqlite3_bind_int64(statement, 1, (sqlite3_int64)reconstruction_id); + if (sqlite3_step(statement) == SQLITE_ROW) + result = LARDON3D_PROJECT_DB_CORRUPT; + sqlite3_finalize(statement); + statement = NULL; + } + if (result == LARDON3D_PROJECT_DB_OK) + result = prepare(db, + "SELECT component_id,component_key,registered_image_count," + "landmark_count FROM sparse_reconstruction_components " + "WHERE reconstruction_id=?1 AND component_key>?2 " + "ORDER BY component_key LIMIT ?3", + &statement); + if (result == LARDON3D_PROJECT_DB_OK) { + sqlite3_bind_int64(statement, 1, (sqlite3_int64)reconstruction_id); + sqlite3_bind_int64(statement, 2, (sqlite3_int64)after_component_key); + sqlite3_bind_int64(statement, 3, (sqlite3_int64)capacity); + while (page->count < capacity && sqlite3_step(statement) == SQLITE_ROW) { + Lardon3DSparseComponent *item = &page->items[page->count]; + item->component_id = (uint64_t)sqlite3_column_int64(statement, 0); + item->component_key = (uint64_t)sqlite3_column_int64(statement, 1); + item->registered_image_count = + (uint64_t)sqlite3_column_int64(statement, 2); + item->landmark_count = (uint64_t)sqlite3_column_int64(statement, 3); + page->next_component_key = item->component_key; + ++page->count; + } + sqlite3_finalize(statement); + } + if (result != LARDON3D_PROJECT_DB_OK) { + page->count = 0; + page->next_component_key = after_component_key; + } + (void)pthread_mutex_unlock(&db->mutex); + return result; +} + +Lardon3DProjectDbResult lardon3d_sparse_registered_image_list( + Lardon3DProjectDb *db, uint64_t reconstruction_id, uint64_t after_image_id, + size_t capacity, Lardon3DSparseRegisteredImagePage *page) { + if (!db || !page || !page->items || !reconstruction_id || + !sparse_page_capacity_valid(capacity)) + return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; + page->after_image_id = after_image_id; + page->capacity = capacity; + page->count = 0; + page->next_image_id = after_image_id; + (void)pthread_mutex_lock(&db->mutex); + sqlite3_stmt *statement = NULL; + Lardon3DProjectDbResult result = prepare( + db, + "SELECT 1 FROM sparse_reconstruction_components AS c WHERE " + "c.reconstruction_id=?1 AND c.component_key!=(SELECT MIN(r.image_id) " + "FROM sparse_registered_images AS r WHERE r.reconstruction_id=?1 AND " + "r.component_id=c.component_id) LIMIT 1", + &statement); + if (result == LARDON3D_PROJECT_DB_OK) { + sqlite3_bind_int64(statement, 1, (sqlite3_int64)reconstruction_id); + if (sqlite3_step(statement) == SQLITE_ROW) + result = LARDON3D_PROJECT_DB_CORRUPT; + sqlite3_finalize(statement); + statement = NULL; + } + if (result == LARDON3D_PROJECT_DB_OK) + result = prepare( + db, + "SELECT " + "r.image_id,r.component_id,r.rotation_00,r.rotation_01,r.rotation_02," + "r.rotation_10,r.rotation_11,r.rotation_12,r.rotation_20,r.rotation_21," + "r.rotation_22," + "translation_0,translation_1,translation_2,c.component_key " + "FROM sparse_registered_images AS r JOIN " + "sparse_reconstruction_components AS c ON c.reconstruction_id=" + "r.reconstruction_id AND c.component_id=r.component_id " + "WHERE r.reconstruction_id=?1 AND r.image_id>?2 ORDER BY r.image_id " + "LIMIT ?3", + &statement); + if (result == LARDON3D_PROJECT_DB_OK) { + sqlite3_bind_int64(statement, 1, (sqlite3_int64)reconstruction_id); + sqlite3_bind_int64(statement, 2, (sqlite3_int64)after_image_id); + sqlite3_bind_int64(statement, 3, (sqlite3_int64)capacity); + while (page->count < capacity && sqlite3_step(statement) == SQLITE_ROW) { + Lardon3DSparseRegisteredImage *item = &page->items[page->count]; + item->image_id = (uint64_t)sqlite3_column_int64(statement, 0); + item->component_key = 0; + for (int index = 0; index < 9; ++index) + item->rotation_cw[index] = sqlite3_column_double(statement, index + 2); + for (int index = 0; index < 3; ++index) + item->translation_cw[index] = + sqlite3_column_double(statement, index + 11); + item->component_key = (uint64_t)sqlite3_column_int64(statement, 14); + if (!sparse_rotation_valid(item->rotation_cw) || + !sparse_finite(item->translation_cw[0]) || + !sparse_finite(item->translation_cw[1]) || + !sparse_finite(item->translation_cw[2])) { + result = LARDON3D_PROJECT_DB_CORRUPT; + break; + } + page->next_image_id = item->image_id; + ++page->count; + } + sqlite3_finalize(statement); + } + if (result != LARDON3D_PROJECT_DB_OK) { + page->count = 0; + page->next_image_id = after_image_id; + } + (void)pthread_mutex_unlock(&db->mutex); + return result; +} + +Lardon3DProjectDbResult +lardon3d_sparse_landmark_list(Lardon3DProjectDb *db, uint64_t reconstruction_id, + uint64_t after_track_id, size_t capacity, + Lardon3DSparseLandmarkPage *page) { + if (!db || !page || !page->items || !reconstruction_id || + !sparse_page_capacity_valid(capacity)) + return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; + page->after_track_id = after_track_id; + page->capacity = capacity; + page->count = 0; + page->next_track_id = after_track_id; + (void)pthread_mutex_lock(&db->mutex); + sqlite3_stmt *statement = NULL; + Lardon3DProjectDbResult result = prepare( + db, + "SELECT l.landmark_id,l.track_id,l.component_id,l.x,l.y,l.z," + "l.reprojection_rmse_px,l.reprojection_median_px,l.observation_count," + "c.component_key,CASE WHEN c.component_key=(SELECT MIN(ri.image_id) " + "FROM sparse_registered_images AS ri WHERE ri.reconstruction_id=" + "l.reconstruction_id AND ri.component_id=l.component_id) AND " + "EXISTS (SELECT 1 FROM tracks AS tr WHERE tr.track_set_id=r.track_set_id " + "AND tr.track_id=l.track_id) AND NOT EXISTS (SELECT 1 FROM " + "track_observations AS t JOIN feature_sets AS f ON " + "f.feature_set_id=t.feature_set_id WHERE t.track_set_id=r.track_set_id " + "AND t.track_id=l.track_id AND NOT EXISTS (SELECT 1 FROM " + "sparse_registered_images AS ri2 WHERE ri2.reconstruction_id=" + "l.reconstruction_id AND ri2.image_id=f.image_id AND " + "ri2.component_id=l.component_id)) THEN 1 ELSE 0 END " + "FROM sparse_landmarks AS l JOIN sparse_reconstructions AS r ON " + "r.reconstruction_id=l.reconstruction_id JOIN " + "sparse_reconstruction_components AS c ON " + "c.reconstruction_id=l.reconstruction_id AND c.component_id=l.component_id " + "WHERE l.reconstruction_id=?1 AND l.track_id>?2 ORDER BY l.track_id " + "LIMIT ?3", + &statement); + if (result == LARDON3D_PROJECT_DB_OK) { + sqlite3_bind_int64(statement, 1, (sqlite3_int64)reconstruction_id); + sqlite3_bind_int64(statement, 2, (sqlite3_int64)after_track_id); + sqlite3_bind_int64(statement, 3, (sqlite3_int64)capacity); + while (page->count < capacity && sqlite3_step(statement) == SQLITE_ROW) { + Lardon3DSparseLandmark *item = &page->items[page->count]; + item->landmark_id = (uint64_t)sqlite3_column_int64(statement, 0); + item->track_id = (uint64_t)sqlite3_column_int64(statement, 1); + item->component_key = (uint64_t)sqlite3_column_int64(statement, 9); + item->x = sqlite3_column_double(statement, 3); + item->y = sqlite3_column_double(statement, 4); + item->z = sqlite3_column_double(statement, 5); + item->reprojection_rmse_px = sqlite3_column_double(statement, 6); + item->reprojection_median_px = sqlite3_column_double(statement, 7); + item->observation_count = (uint64_t)sqlite3_column_int64(statement, 8); + if (sqlite3_column_int(statement, 10) == 0) { + result = LARDON3D_PROJECT_DB_CORRUPT; + break; + } + if (!sparse_finite(item->x) || !sparse_finite(item->y) || + !sparse_finite(item->z) || + !sparse_finite(item->reprojection_rmse_px) || + !sparse_finite(item->reprojection_median_px)) { + result = LARDON3D_PROJECT_DB_CORRUPT; + break; + } + page->next_track_id = item->track_id; + ++page->count; + } + sqlite3_finalize(statement); + } + if (result != LARDON3D_PROJECT_DB_OK) { + page->count = 0; + page->next_track_id = after_track_id; + } + (void)pthread_mutex_unlock(&db->mutex); + return result; +} + +Lardon3DProjectDbResult lardon3d_sparse_observation_list( + Lardon3DProjectDb *db, uint64_t reconstruction_id, + uint64_t after_landmark_id, uint32_t after_position_in_track, + size_t capacity, Lardon3DSparseObservationPage *page) { + if (!db || !page || !page->items || !reconstruction_id || + !sparse_page_capacity_valid(capacity)) + return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; + page->after_landmark_id = after_landmark_id; + page->after_position_in_track = after_position_in_track; + page->capacity = capacity; + page->count = 0; + page->next_landmark_id = after_landmark_id; + page->next_position_in_track = after_position_in_track; + (void)pthread_mutex_lock(&db->mutex); + sqlite3_stmt *statement = NULL; + Lardon3DProjectDbResult result = prepare( + db, + "SELECT o.landmark_id,l.track_id,o.feature_set_id,o.feature_index," + "o.position_in_track,CASE WHEN EXISTS (SELECT 1 FROM " + "track_observations AS t WHERE t.track_set_id=r.track_set_id AND " + "t.track_id=l.track_id AND t.feature_set_id=o.feature_set_id AND " + "t.feature_index=o.feature_index AND " + "t.position_in_track=o.position_in_track) THEN 1 ELSE 0 END " + "FROM sparse_landmark_observations AS o JOIN sparse_landmarks AS l ON " + "l.landmark_id=o.landmark_id JOIN sparse_reconstructions AS r ON " + "r.reconstruction_id=l.reconstruction_id WHERE l.reconstruction_id=?1 " + "AND (o.landmark_id>?2 OR (o.landmark_id=?2 AND " + "o.position_in_track>?3)) ORDER BY o.landmark_id,o.position_in_track " + "LIMIT ?4", + &statement); + if (result == LARDON3D_PROJECT_DB_OK) { + sqlite3_bind_int64(statement, 1, (sqlite3_int64)reconstruction_id); + sqlite3_bind_int64(statement, 2, (sqlite3_int64)after_landmark_id); + sqlite3_bind_int64(statement, 3, (sqlite3_int64)after_position_in_track); + sqlite3_bind_int64(statement, 4, (sqlite3_int64)capacity); + while (page->count < capacity && sqlite3_step(statement) == SQLITE_ROW) { + Lardon3DSparseLandmarkObservation *item = &page->items[page->count]; + item->landmark_id = (uint64_t)sqlite3_column_int64(statement, 0); + item->track_id = (uint64_t)sqlite3_column_int64(statement, 1); + item->feature_set_id = (uint64_t)sqlite3_column_int64(statement, 2); + item->feature_index = (uint32_t)sqlite3_column_int64(statement, 3); + item->position_in_track = (uint32_t)sqlite3_column_int64(statement, 4); + if (sqlite3_column_int(statement, 5) == 0) { + result = LARDON3D_PROJECT_DB_CORRUPT; + break; + } + page->next_landmark_id = item->landmark_id; + page->next_position_in_track = item->position_in_track; + ++page->count; + } + sqlite3_finalize(statement); + } + (void)pthread_mutex_unlock(&db->mutex); + return result; +} diff --git a/tests/test_geometric_verification.c b/tests/test_geometric_verification.c index 302fc4f..1644220 100644 --- a/tests/test_geometric_verification.c +++ b/tests/test_geometric_verification.c @@ -9,12 +9,12 @@ #include -#define CHECK(condition) \ - do { \ - if (!(condition)) { \ - fprintf(stderr, "Échec ligne %d : %s\n", __LINE__, #condition); \ - return false; \ - } \ +#define CHECK(condition) \ + do { \ + if (!(condition)) { \ + fprintf(stderr, "Échec ligne %d : %s\n", __LINE__, #condition); \ + return false; \ + } \ } while (0) typedef struct { @@ -35,20 +35,24 @@ static bool execute_sql(const char *path, const char *sql) { return sqlite3_close(connection) == SQLITE_OK && ok; } -static bool query_integer(const char *path, const char *sql, sqlite3_int64 expected) { +static bool query_integer(const char *path, const char *sql, + sqlite3_int64 expected) { sqlite3 *connection = NULL; sqlite3_stmt *statement = NULL; - if (sqlite3_open_v2(path, &connection, SQLITE_OPEN_READONLY, NULL) != SQLITE_OK || + if (sqlite3_open_v2(path, &connection, SQLITE_OPEN_READONLY, NULL) != + SQLITE_OK || sqlite3_prepare_v2(connection, sql, -1, &statement, NULL) != SQLITE_OK) { - if (statement) (void)sqlite3_finalize(statement); - if (connection) (void)sqlite3_close(connection); + if (statement) + (void)sqlite3_finalize(statement); + if (connection) + (void)sqlite3_close(connection); return false; } bool matches = sqlite3_step(statement) == SQLITE_ROW && sqlite3_column_int64(statement, 0) == expected && sqlite3_step(statement) == SQLITE_DONE; - return sqlite3_finalize(statement) == SQLITE_OK && sqlite3_close(connection) == SQLITE_OK && - matches; + return sqlite3_finalize(statement) == SQLITE_OK && + sqlite3_close(connection) == SQLITE_OK && matches; } static void asset_path(const unsigned char hash[32], const char *kind, @@ -60,13 +64,14 @@ static void asset_path(const unsigned char hash[32], const char *kind, hex[index * 2 + 1] = digits[hash[index] & 15U]; } hex[64] = '\0'; - (void)snprintf(path, LARDON3D_PROJECT_DB_PATH_CAPACITY, "assets/%s/%c%c/%s", kind, hex[0], - hex[1], hex); + (void)snprintf(path, LARDON3D_PROJECT_DB_PATH_CAPACITY, "assets/%s/%c%c/%s", + kind, hex[0], hex[1], hex); } -static bool create_match_result(Lardon3DProjectDb *database, uint64_t pair_id, uint64_t fs_a, - uint64_t fs_b, uint32_t version, unsigned char fingerprint_byte, - int status, uint32_t match_count, uint64_t *id) { +static bool create_match_result(Lardon3DProjectDb *database, uint64_t pair_id, + uint64_t fs_a, uint64_t fs_b, uint32_t version, + unsigned char fingerprint_byte, int status, + uint32_t match_count, uint64_t *id) { unsigned char fingerprint[32] = {0}; unsigned char hash[32] = {0}; char path[LARDON3D_PROJECT_DB_PATH_CAPACITY]; @@ -75,10 +80,12 @@ static bool create_match_result(Lardon3DProjectDb *database, uint64_t pair_id, u asset_path(hash, "matches", path); Lardon3DProjectDbMatchResult result; Lardon3DProjectDbResult db_result = lardon3d_project_db_create_match_result( - database, pair_id, fs_a, fs_b, "model-test", version, fingerprint, status, match_count, - status == LARDON3D_MATCH_RESULT_STATUS_MATCHED ? hash : NULL, + database, pair_id, fs_a, fs_b, "model-test", version, fingerprint, status, + match_count, status == LARDON3D_MATCH_RESULT_STATUS_MATCHED ? hash : NULL, status == LARDON3D_MATCH_RESULT_STATUS_MATCHED ? path : NULL, - status == LARDON3D_MATCH_RESULT_STATUS_MATCHED ? 32U + (uint64_t)match_count * 12U : 0, + status == LARDON3D_MATCH_RESULT_STATUS_MATCHED + ? 32U + (uint64_t)match_count * 12U + : 0, version, &result); if (db_result != LARDON3D_PROJECT_DB_OK) { return false; @@ -89,8 +96,8 @@ static bool create_match_result(Lardon3DProjectDb *database, uint64_t pair_id, u static bool create_parents(Lardon3DProjectDb *database, Parents *parents) { Lardon3DProjectDbScanSet scanset; - if (lardon3d_project_db_create_scanset(database, "Geometry-model", &scanset) != - LARDON3D_PROJECT_DB_OK) { + if (lardon3d_project_db_create_scanset(database, "Geometry-model", + &scanset) != LARDON3D_PROJECT_DB_OK) { return false; } unsigned char image_hash_a[32] = {0x11}; @@ -102,17 +109,20 @@ static bool create_parents(Lardon3DProjectDb *database, Parents *parents) { Lardon3DProjectDbImageRegisterStatus register_status; Lardon3DProjectDbImage image_a; Lardon3DProjectDbImage image_b; - if (lardon3d_project_db_register_image(database, scanset.scanset_id, image_hash_a, image_path_a, - 10, "a.jpg", "/a.jpg", 0, 1, ®ister_status, - &image_a) != LARDON3D_PROJECT_DB_OK || - lardon3d_project_db_register_image(database, scanset.scanset_id, image_hash_b, image_path_b, - 10, "b.jpg", "/b.jpg", 0, 2, ®ister_status, - &image_b) != LARDON3D_PROJECT_DB_OK) { + if (lardon3d_project_db_register_image( + database, scanset.scanset_id, image_hash_a, image_path_a, 10, "a.jpg", + "/a.jpg", 0, 1, ®ister_status, + &image_a) != LARDON3D_PROJECT_DB_OK || + lardon3d_project_db_register_image( + database, scanset.scanset_id, image_hash_b, image_path_b, 10, "b.jpg", + "/b.jpg", 0, 2, ®ister_status, + &image_b) != LARDON3D_PROJECT_DB_OK) { return false; } Lardon3DProjectDbCandidatePair pair; - if (lardon3d_project_db_create_candidate_pair(database, image_a.image_id, image_b.image_id, 3, - &pair) != LARDON3D_PROJECT_DB_OK) { + if (lardon3d_project_db_create_candidate_pair(database, image_a.image_id, + image_b.image_id, 3, &pair) != + LARDON3D_PROJECT_DB_OK) { return false; } unsigned char feature_hash_a[32] = {0x33}; @@ -124,53 +134,67 @@ static bool create_parents(Lardon3DProjectDb *database, Parents *parents) { Lardon3DProjectDbFeatureSet feature_a; Lardon3DProjectDbFeatureSet feature_b; if (lardon3d_project_db_register_feature_set( - database, image_a.image_id, "orb", 1, feature_hash_a, image_hash_a, 8192, 1, 32, - feature_hash_a, feature_path_a, 100, LARDON3D_DB_FEATURE_ASSET_DURABLE, 0, 4, + database, image_a.image_id, "orb", 1, feature_hash_a, image_hash_a, + 8192, 1, 32, feature_hash_a, feature_path_a, 100, + LARDON3D_DB_FEATURE_ASSET_DURABLE, 0, 4, &feature_a) != LARDON3D_PROJECT_DB_OK || lardon3d_project_db_register_feature_set( - database, image_b.image_id, "orb", 1, feature_hash_b, image_hash_b, 8192, 1, 32, - feature_hash_b, feature_path_b, 100, LARDON3D_DB_FEATURE_ASSET_DURABLE, 0, 5, + database, image_b.image_id, "orb", 1, feature_hash_b, image_hash_b, + 8192, 1, 32, feature_hash_b, feature_path_b, 100, + LARDON3D_DB_FEATURE_ASSET_DURABLE, 0, 5, &feature_b) != LARDON3D_PROJECT_DB_OK) { return false; } parents->candidate_pair_id = pair.candidate_pair_id; parents->feature_set_id_a = feature_a.feature_set_id; parents->feature_set_id_b = feature_b.feature_set_id; - return create_match_result(database, pair.candidate_pair_id, feature_a.feature_set_id, - feature_b.feature_set_id, 1, 0x51, - LARDON3D_MATCH_RESULT_STATUS_MATCHED, 9, &parents->matched_id) && - create_match_result(database, pair.candidate_pair_id, feature_a.feature_set_id, - feature_b.feature_set_id, 2, 0x52, - LARDON3D_MATCH_RESULT_STATUS_NO_MATCH, 0, &parents->no_match_id) && - create_match_result(database, pair.candidate_pair_id, feature_a.feature_set_id, - feature_b.feature_set_id, 3, 0x53, - LARDON3D_MATCH_RESULT_STATUS_MATCHED, 8192, - &parents->second_matched_id); + return create_match_result(database, pair.candidate_pair_id, + feature_a.feature_set_id, feature_b.feature_set_id, + 1, 0x51, LARDON3D_MATCH_RESULT_STATUS_MATCHED, 9, + &parents->matched_id) && + create_match_result(database, pair.candidate_pair_id, + feature_a.feature_set_id, feature_b.feature_set_id, + 2, 0x52, LARDON3D_MATCH_RESULT_STATUS_NO_MATCH, 0, + &parents->no_match_id) && + create_match_result(database, pair.candidate_pair_id, + feature_a.feature_set_id, feature_b.feature_set_id, + 3, 0x53, LARDON3D_MATCH_RESULT_STATUS_MATCHED, + 8192, &parents->second_matched_id); } static bool create_v11_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) { + if (lardon3d_project_db_open(path, &database, error) != + LARDON3D_PROJECT_DB_OK) { return false; } lardon3d_project_db_close(database); return execute_sql(path, "PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;" - "DROP TABLE geometric_verifier_tasks;" - "DROP TABLE geometric_verification_results;" - "DROP TABLE track_observations;" - "DROP TABLE tracks;" - "DROP TABLE track_sets;DROP TABLE track_builder_tasks;" - "UPDATE metadata SET value=11 WHERE key='schema_version';" + "DROP TABLE IF EXISTS sparse_landmark_observations;" + "DROP TABLE IF EXISTS sparse_landmarks;" + "DROP TABLE IF EXISTS sparse_registered_images;" + "DROP TABLE IF EXISTS sparse_reconstruction_components;" + "DROP TABLE IF EXISTS sparse_reconstructions;" + "DROP TABLE IF EXISTS sparse_calibration_scope_images;" + "DROP TABLE IF EXISTS sparse_calibration_scopes;" + "DROP TABLE IF EXISTS sparse_calibrations;" + "DROP TABLE geometric_verifier_tasks;" + "DROP TABLE geometric_verification_results;" + "DROP TABLE track_observations;" + "DROP TABLE tracks;" + "DROP TABLE track_sets;DROP TABLE track_builder_tasks;" + "UPDATE metadata SET value=11 WHERE key='schema_version';" "COMMIT;PRAGMA foreign_keys=ON;"); } 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) == 15); + CHECK(lardon3d_project_db_open(path, &database, error) == + LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_schema_version(database) == 16); Parents parents; CHECK(create_parents(database, &parents)); @@ -182,8 +206,9 @@ static bool test_model_api(const char *path) { double model[9] = {0.0, -1.0, 1e-300, 1e300, 4.0, 5.0, 6.0, 7.0, 8.0}; Lardon3DProjectDbGeometricVerificationResult verified; CHECK(lardon3d_project_db_create_geometric_verification_result( - database, parents.matched_id, LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 1, - fingerprint_a, LARDON3D_GEOMETRIC_VERIFIED, 1, mask_a, sizeof(mask_a), model, 10, + database, parents.matched_id, + LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 1, fingerprint_a, + LARDON3D_GEOMETRIC_VERIFIED, 1, mask_a, sizeof(mask_a), model, 10, &verified) == LARDON3D_PROJECT_DB_OK); CHECK(verified.has_model && verified.inlier_mask_size == 2 && memcmp(verified.model, model, sizeof(model)) == 0); @@ -191,17 +216,20 @@ static bool test_model_api(const char *path) { unsigned char rejected_mask[2] = {0x03, 0x00}; Lardon3DProjectDbGeometricVerificationResult rejected; CHECK(lardon3d_project_db_create_geometric_verification_result( - database, parents.matched_id, LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 2, - fingerprint_b, LARDON3D_GEOMETRIC_REJECTED, 2, rejected_mask, - sizeof(rejected_mask), NULL, 11, &rejected) == LARDON3D_PROJECT_DB_OK); + database, parents.matched_id, + LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 2, fingerprint_b, + LARDON3D_GEOMETRIC_REJECTED, 2, rejected_mask, + sizeof(rejected_mask), NULL, 11, + &rejected) == LARDON3D_PROJECT_DB_OK); CHECK(!rejected.has_model && rejected.inlier_count == 2); unsigned char zero_mask[2] = {0}; Lardon3DProjectDbGeometricVerificationResult zero_inliers; CHECK(lardon3d_project_db_create_geometric_verification_result( - database, parents.matched_id, LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 3, - fingerprint_b, LARDON3D_GEOMETRIC_REJECTED, 0, zero_mask, sizeof(zero_mask), NULL, 11, - &zero_inliers) == LARDON3D_PROJECT_DB_OK); + database, parents.matched_id, + LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 3, fingerprint_b, + LARDON3D_GEOMETRIC_REJECTED, 0, zero_mask, sizeof(zero_mask), NULL, + 11, &zero_inliers) == LARDON3D_PROJECT_DB_OK); Lardon3DProjectDbGeometricVerificationResult loaded; CHECK(lardon3d_project_db_load_geometric_verification_result( @@ -211,17 +239,22 @@ static bool test_model_api(const char *path) { memcmp(loaded.model, model, sizeof(model)) == 0 && memcmp(loaded.inlier_mask, mask_a, sizeof(mask_a)) == 0); CHECK(lardon3d_project_db_find_geometric_verification_result( - database, parents.matched_id, LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 1, - fingerprint_a, &loaded) == LARDON3D_PROJECT_DB_OK && - loaded.geometric_verification_result_id == verified.geometric_verification_result_id); + database, parents.matched_id, + LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 1, fingerprint_a, + &loaded) == LARDON3D_PROJECT_DB_OK && + loaded.geometric_verification_result_id == + verified.geometric_verification_result_id); CHECK(lardon3d_project_db_create_geometric_verification_result( - database, parents.matched_id, LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 1, - fingerprint_a, LARDON3D_GEOMETRIC_REJECTED, 2, rejected_mask, - sizeof(rejected_mask), NULL, 12, &loaded) == LARDON3D_PROJECT_DB_CONSTRAINT); + database, parents.matched_id, + LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 1, fingerprint_a, + LARDON3D_GEOMETRIC_REJECTED, 2, rejected_mask, + sizeof(rejected_mask), NULL, 12, + &loaded) == LARDON3D_PROJECT_DB_CONSTRAINT); CHECK(lardon3d_project_db_create_geometric_verification_result( - database, parents.no_match_id, LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 1, - fingerprint_a, LARDON3D_GEOMETRIC_REJECTED, 0, mask_a, 1, NULL, 12, + database, parents.no_match_id, + LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 1, fingerprint_a, + LARDON3D_GEOMETRIC_REJECTED, 0, mask_a, 1, NULL, 12, &loaded) == LARDON3D_PROJECT_DB_CONSTRAINT); CHECK(lardon3d_project_db_create_geometric_verification_result( database, UINT64_C(9223372036854775807), @@ -231,60 +264,71 @@ static bool test_model_api(const char *path) { unsigned char bad_padding[2] = {0x01, 0x80}; CHECK(lardon3d_project_db_create_geometric_verification_result( - database, parents.matched_id, LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 3, - fingerprint_a, LARDON3D_GEOMETRIC_REJECTED, 1, bad_padding, sizeof(bad_padding), + database, parents.matched_id, + LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 3, fingerprint_a, + LARDON3D_GEOMETRIC_REJECTED, 1, bad_padding, sizeof(bad_padding), NULL, 12, &loaded) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT); CHECK(lardon3d_project_db_create_geometric_verification_result( - database, parents.matched_id, LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 3, - fingerprint_a, LARDON3D_GEOMETRIC_REJECTED, 2, mask_a, sizeof(mask_a), NULL, 12, + database, parents.matched_id, + LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 3, fingerprint_a, + LARDON3D_GEOMETRIC_REJECTED, 2, mask_a, sizeof(mask_a), NULL, 12, &loaded) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT); unsigned char all_inliers[2] = {0xff, 0x01}; CHECK(lardon3d_project_db_create_geometric_verification_result( - database, parents.matched_id, LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 4, - fingerprint_a, LARDON3D_GEOMETRIC_REJECTED, 9, all_inliers, sizeof(all_inliers), NULL, - 12, &loaded) == LARDON3D_PROJECT_DB_OK); + database, parents.matched_id, + LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 4, fingerprint_a, + LARDON3D_GEOMETRIC_REJECTED, 9, all_inliers, sizeof(all_inliers), + NULL, 12, &loaded) == LARDON3D_PROJECT_DB_OK); CHECK(lardon3d_project_db_create_geometric_verification_result( - database, parents.matched_id, LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 5, - fingerprint_a, LARDON3D_GEOMETRIC_REJECTED, 10, all_inliers, sizeof(all_inliers), NULL, - 12, &loaded) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT); + database, parents.matched_id, + LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 5, fingerprint_a, + LARDON3D_GEOMETRIC_REJECTED, 10, all_inliers, sizeof(all_inliers), + NULL, 12, &loaded) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT); CHECK(lardon3d_project_db_create_geometric_verification_result( - database, parents.matched_id, LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 3, - fingerprint_a, LARDON3D_GEOMETRIC_VERIFIED, 1, mask_a, sizeof(mask_a), NULL, 12, + database, parents.matched_id, + LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 3, fingerprint_a, + LARDON3D_GEOMETRIC_VERIFIED, 1, mask_a, sizeof(mask_a), NULL, 12, &loaded) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT); CHECK(lardon3d_project_db_create_geometric_verification_result( - database, parents.matched_id, LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 3, - fingerprint_a, LARDON3D_GEOMETRIC_REJECTED, 1, mask_a, sizeof(mask_a), model, 12, + database, parents.matched_id, + LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 3, fingerprint_a, + LARDON3D_GEOMETRIC_REJECTED, 1, mask_a, sizeof(mask_a), model, 12, &loaded) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT); double bad_model[9] = {0}; bad_model[4] = NAN; CHECK(lardon3d_project_db_create_geometric_verification_result( - database, parents.matched_id, LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 3, - fingerprint_a, LARDON3D_GEOMETRIC_VERIFIED, 1, mask_a, sizeof(mask_a), bad_model, 12, - &loaded) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT); + database, parents.matched_id, + LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 3, fingerprint_a, + LARDON3D_GEOMETRIC_VERIFIED, 1, mask_a, sizeof(mask_a), bad_model, + 12, &loaded) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT); bad_model[4] = INFINITY; CHECK(lardon3d_project_db_create_geometric_verification_result( - database, parents.matched_id, LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 3, - fingerprint_a, LARDON3D_GEOMETRIC_VERIFIED, 1, mask_a, sizeof(mask_a), bad_model, 12, - &loaded) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT); + database, parents.matched_id, + LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 3, fingerprint_a, + LARDON3D_GEOMETRIC_VERIFIED, 1, mask_a, sizeof(mask_a), bad_model, + 12, &loaded) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT); unsigned char max_mask[LARDON3D_PROJECT_DB_INLIER_MASK_MAX] = {0}; max_mask[0] = 0x01; max_mask[1023] = 0x80; CHECK(lardon3d_project_db_create_geometric_verification_result( - database, parents.second_matched_id, LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 1, - fingerprint_a, LARDON3D_GEOMETRIC_REJECTED, 2, max_mask, sizeof(max_mask), NULL, 13, - &loaded) == LARDON3D_PROJECT_DB_OK); + database, parents.second_matched_id, + LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 1, fingerprint_a, + LARDON3D_GEOMETRIC_REJECTED, 2, max_mask, sizeof(max_mask), NULL, + 13, &loaded) == LARDON3D_PROJECT_DB_OK); CHECK(loaded.inlier_mask_size == 1024 && loaded.inlier_count == 2); static const uint32_t boundary_counts[] = {1, 2, 7, 8, 9, 63, 64, 65, 8191}; for (size_t boundary_index = 0; - boundary_index < sizeof(boundary_counts) / sizeof(boundary_counts[0]); ++boundary_index) { + boundary_index < sizeof(boundary_counts) / sizeof(boundary_counts[0]); + ++boundary_index) { uint32_t match_count = boundary_counts[boundary_index]; uint64_t parent_id = 0; - CHECK(create_match_result(database, parents.candidate_pair_id, parents.feature_set_id_a, - parents.feature_set_id_b, (uint32_t)(10U + boundary_index), - (unsigned char)(0x60U + boundary_index), - LARDON3D_MATCH_RESULT_STATUS_MATCHED, match_count, &parent_id)); + CHECK(create_match_result( + database, parents.candidate_pair_id, parents.feature_set_id_a, + parents.feature_set_id_b, (uint32_t)(10U + boundary_index), + (unsigned char)(0x60U + boundary_index), + LARDON3D_MATCH_RESULT_STATUS_MATCHED, match_count, &parent_id)); unsigned char boundary_mask[LARDON3D_PROJECT_DB_INLIER_MASK_MAX] = {0}; size_t boundary_size = ((size_t)match_count + 7U) / 8U; uint32_t last_index = match_count - 1U; @@ -294,33 +338,38 @@ static bool test_model_api(const char *path) { sizeof(boundary_fingerprint)); CHECK(lardon3d_project_db_create_geometric_verification_result( database, parent_id, LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 1, - boundary_fingerprint, LARDON3D_GEOMETRIC_REJECTED, 1, boundary_mask, - boundary_size, NULL, 20 + (int64_t)boundary_index, &loaded) == - LARDON3D_PROJECT_DB_OK); + boundary_fingerprint, LARDON3D_GEOMETRIC_REJECTED, 1, + boundary_mask, boundary_size, NULL, 20 + (int64_t)boundary_index, + &loaded) == LARDON3D_PROJECT_DB_OK); CHECK(loaded.inlier_mask_size == boundary_size && - loaded.inlier_mask[last_index / 8U] == (unsigned char)(1U << (last_index % 8U))); + loaded.inlier_mask[last_index / 8U] == + (unsigned char)(1U << (last_index % 8U))); } Lardon3DProjectDbGeometricVerificationResult page[2]; size_t count = 0; CHECK(lardon3d_project_db_list_geometric_verification_results( - database, parents.matched_id, 0, page, 1, &count) == LARDON3D_PROJECT_DB_OK && + database, parents.matched_id, 0, page, 1, &count) == + LARDON3D_PROJECT_DB_OK && count == 1); uint64_t cursor = page[0].geometric_verification_result_id; CHECK(lardon3d_project_db_list_geometric_verification_results( - database, parents.matched_id, cursor, page, 2, &count) == LARDON3D_PROJECT_DB_OK && + database, parents.matched_id, cursor, page, 2, &count) == + LARDON3D_PROJECT_DB_OK && count == 2 && page[0].geometric_verification_result_id > cursor); CHECK(lardon3d_project_db_list_geometric_verification_results( database, parents.matched_id, 0, page, - LARDON3D_PROJECT_DB_GEOMETRIC_RESULT_PAGE_MAX + 1U, &count) == - LARDON3D_PROJECT_DB_INVALID_ARGUMENT); + LARDON3D_PROJECT_DB_GEOMETRIC_RESULT_PAGE_MAX + 1U, + &count) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT); uint64_t verified_id = verified.geometric_verification_result_id; lardon3d_project_db_close(database); database = NULL; - CHECK(lardon3d_project_db_open(path, &database, error) == LARDON3D_PROJECT_DB_OK); - CHECK(lardon3d_project_db_load_geometric_verification_result(database, verified_id, &loaded) == - LARDON3D_PROJECT_DB_OK && memcmp(loaded.model, model, sizeof(model)) == 0); + CHECK(lardon3d_project_db_open(path, &database, error) == + LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_load_geometric_verification_result( + database, verified_id, &loaded) == LARDON3D_PROJECT_DB_OK && + memcmp(loaded.model, model, sizeof(model)) == 0); lardon3d_project_db_close(database); return true; } @@ -329,12 +378,13 @@ static bool loader_rejects_after_sql(const char *path, const char *sql) { char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]; Lardon3DProjectDb *database = NULL; if (!execute_sql(path, sql) || - lardon3d_project_db_open(path, &database, error) != LARDON3D_PROJECT_DB_OK) { + lardon3d_project_db_open(path, &database, error) != + LARDON3D_PROJECT_DB_OK) { return false; } Lardon3DProjectDbGeometricVerificationResult result; - bool rejected = lardon3d_project_db_load_geometric_verification_result(database, 1, &result) == - LARDON3D_PROJECT_DB_CORRUPT; + bool rejected = lardon3d_project_db_load_geometric_verification_result( + database, 1, &result) == LARDON3D_PROJECT_DB_CORRUPT; lardon3d_project_db_close(database); return rejected; } @@ -342,37 +392,55 @@ static bool loader_rejects_after_sql(const char *path, const char *sql) { static bool test_corruption(const char *path) { static const char reset[] = "PRAGMA ignore_check_constraints=ON;" - "UPDATE geometric_verification_results SET verifier_kind=1,verifier_version=1," - "parameter_fingerprint=zeroblob(32),status=2,inlier_count=1,inlier_mask=x'0100'," + "UPDATE geometric_verification_results SET " + "verifier_kind=1,verifier_version=1," + "parameter_fingerprint=zeroblob(32),status=2,inlier_count=1,inlier_mask=" + "x'0100'," "model_m00=0,model_m01=0,model_m02=0,model_m10=0,model_m11=0,model_m12=0," - "model_m20=0,model_m21=0,model_m22=0 WHERE geometric_verification_result_id=1;"; + "model_m20=0,model_m21=0,model_m22=0 WHERE " + "geometric_verification_result_id=1;"; CHECK(execute_sql(path, reset)); static const char *corruptions[] = { - "PRAGMA ignore_check_constraints=ON;UPDATE geometric_verification_results " + "PRAGMA ignore_check_constraints=ON;UPDATE " + "geometric_verification_results " "SET verifier_kind=9 WHERE geometric_verification_result_id=1;", - "PRAGMA ignore_check_constraints=ON;UPDATE geometric_verification_results " - "SET verifier_kind=1,verifier_version=0 WHERE geometric_verification_result_id=1;", - "PRAGMA ignore_check_constraints=ON;UPDATE geometric_verification_results " + "PRAGMA ignore_check_constraints=ON;UPDATE " + "geometric_verification_results " + "SET verifier_kind=1,verifier_version=0 WHERE " + "geometric_verification_result_id=1;", + "PRAGMA ignore_check_constraints=ON;UPDATE " + "geometric_verification_results " "SET verifier_version=1,parameter_fingerprint=x'01' " "WHERE geometric_verification_result_id=1;", - "PRAGMA ignore_check_constraints=ON;UPDATE geometric_verification_results " + "PRAGMA ignore_check_constraints=ON;UPDATE " + "geometric_verification_results " "SET parameter_fingerprint=zeroblob(32),status=9 " "WHERE geometric_verification_result_id=1;", - "PRAGMA ignore_check_constraints=ON;UPDATE geometric_verification_results " - "SET status=2,inlier_count=9000 WHERE geometric_verification_result_id=1;", - "PRAGMA ignore_check_constraints=ON;UPDATE geometric_verification_results " - "SET inlier_count=1,inlier_mask=x'03' WHERE geometric_verification_result_id=1;", - "PRAGMA ignore_check_constraints=ON;UPDATE geometric_verification_results " + "PRAGMA ignore_check_constraints=ON;UPDATE " + "geometric_verification_results " + "SET status=2,inlier_count=9000 WHERE " + "geometric_verification_result_id=1;", + "PRAGMA ignore_check_constraints=ON;UPDATE " + "geometric_verification_results " + "SET inlier_count=1,inlier_mask=x'03' WHERE " + "geometric_verification_result_id=1;", + "PRAGMA ignore_check_constraints=ON;UPDATE " + "geometric_verification_results " "SET inlier_mask=x'0180' WHERE geometric_verification_result_id=1;", - "PRAGMA ignore_check_constraints=ON;UPDATE geometric_verification_results " + "PRAGMA ignore_check_constraints=ON;UPDATE " + "geometric_verification_results " "SET inlier_mask=x'0300' WHERE geometric_verification_result_id=1;", - "PRAGMA ignore_check_constraints=ON;UPDATE geometric_verification_results " - "SET inlier_mask=x'0100',model_m00=NULL WHERE geometric_verification_result_id=1;", - "PRAGMA ignore_check_constraints=ON;UPDATE geometric_verification_results " + "PRAGMA ignore_check_constraints=ON;UPDATE " + "geometric_verification_results " + "SET inlier_mask=x'0100',model_m00=NULL WHERE " + "geometric_verification_result_id=1;", + "PRAGMA ignore_check_constraints=ON;UPDATE " + "geometric_verification_results " "SET model_m00=1e999 WHERE geometric_verification_result_id=1;", }; - for (size_t index = 0; index < sizeof(corruptions) / sizeof(corruptions[0]); ++index) { + for (size_t index = 0; index < sizeof(corruptions) / sizeof(corruptions[0]); + ++index) { CHECK(execute_sql(path, reset)); CHECK(loader_rejects_after_sql(path, corruptions[index])); } @@ -380,15 +448,17 @@ static bool test_corruption(const char *path) { char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]; Lardon3DProjectDb *database = NULL; CHECK(execute_sql(path, reset)); - CHECK(lardon3d_project_db_open(path, &database, error) == LARDON3D_PROJECT_DB_OK); - Lardon3DProjectDbGeometricVerificationResult result; - CHECK(lardon3d_project_db_load_geometric_verification_result(database, 1, &result) == + CHECK(lardon3d_project_db_open(path, &database, error) == LARDON3D_PROJECT_DB_OK); + Lardon3DProjectDbGeometricVerificationResult result; + CHECK(lardon3d_project_db_load_geometric_verification_result( + database, 1, &result) == LARDON3D_PROJECT_DB_OK); uint64_t parent_id = result.match_result_id; lardon3d_project_db_close(database); char parent_corruption[512]; CHECK(snprintf(parent_corruption, sizeof(parent_corruption), - "PRAGMA ignore_check_constraints=ON;UPDATE match_results SET result_status=0 " + "PRAGMA ignore_check_constraints=ON;UPDATE match_results SET " + "result_status=0 " "WHERE match_result_id=%llu;", (unsigned long long)parent_id) > 0); CHECK(loader_rejects_after_sql(path, parent_corruption)); @@ -399,46 +469,60 @@ static bool test_migration(const char *v11_path, const char *failed_path) { char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]; Lardon3DProjectDb *database = NULL; CHECK(create_v11_database(v11_path)); - CHECK(query_integer(v11_path, "SELECT value FROM metadata WHERE key='schema_version'", 11)); - CHECK(query_integer(v11_path, - "SELECT count(*) FROM sqlite_master WHERE type='table' AND " - "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) == 15); + CHECK(query_integer( + v11_path, "SELECT value FROM metadata WHERE key='schema_version'", 11)); + CHECK( + query_integer(v11_path, + "SELECT count(*) FROM sqlite_master WHERE type='table' AND " + "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) == 16); Parents parents; CHECK(create_parents(database, &parents)); unsigned char fingerprint[32] = {0x91}; unsigned char mask[2] = {0x01, 0x00}; Lardon3DProjectDbGeometricVerificationResult migrated_result; CHECK(lardon3d_project_db_create_geometric_verification_result( - database, parents.matched_id, LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 1, - fingerprint, LARDON3D_GEOMETRIC_REJECTED, 1, mask, sizeof(mask), NULL, 30, + database, parents.matched_id, + LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 1, fingerprint, + LARDON3D_GEOMETRIC_REJECTED, 1, mask, sizeof(mask), NULL, 30, &migrated_result) == LARDON3D_PROJECT_DB_OK); - uint64_t migrated_result_id = migrated_result.geometric_verification_result_id; + uint64_t migrated_result_id = + migrated_result.geometric_verification_result_id; lardon3d_project_db_close(database); - CHECK(query_integer(v11_path, "SELECT value FROM metadata WHERE key='schema_version'", 15)); - CHECK(query_integer(v11_path, - "SELECT count(*) FROM sqlite_master WHERE type='index' AND " - "name='geometric_verification_results_parent_idx'", - 1)); - CHECK(lardon3d_project_db_open(v11_path, &database, error) == LARDON3D_PROJECT_DB_OK); + CHECK(query_integer( + v11_path, "SELECT value FROM metadata WHERE key='schema_version'", 16)); + CHECK( + query_integer(v11_path, + "SELECT count(*) FROM sqlite_master WHERE type='index' AND " + "name='geometric_verification_results_parent_idx'", + 1)); + CHECK(lardon3d_project_db_open(v11_path, &database, error) == + LARDON3D_PROJECT_DB_OK); CHECK(lardon3d_project_db_load_geometric_verification_result( - database, migrated_result_id, &migrated_result) == LARDON3D_PROJECT_DB_OK && + database, migrated_result_id, &migrated_result) == + LARDON3D_PROJECT_DB_OK && migrated_result.match_result_id == parents.matched_id); lardon3d_project_db_close(database); CHECK(create_v11_database(failed_path)); CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V12", "1", 1) == 0); - CHECK(lardon3d_project_db_open(failed_path, &database, error) == LARDON3D_PROJECT_DB_IO_ERROR); + CHECK(lardon3d_project_db_open(failed_path, &database, error) == + LARDON3D_PROJECT_DB_IO_ERROR); CHECK(unsetenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V12") == 0); - CHECK(query_integer(failed_path, "SELECT value FROM metadata WHERE key='schema_version'", 11)); CHECK(query_integer(failed_path, - "SELECT count(*) FROM sqlite_master WHERE type='table' AND " - "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) == 15); + "SELECT value FROM metadata WHERE key='schema_version'", + 11)); + CHECK( + query_integer(failed_path, + "SELECT count(*) FROM sqlite_master WHERE type='table' AND " + "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) == 16); lardon3d_project_db_close(database); return true; } @@ -449,9 +533,11 @@ static bool run_test(void) { char database_path[512]; char v11_path[512]; char failed_path[512]; - CHECK(snprintf(database_path, sizeof(database_path), "%s/project.db", directory) > 0); + CHECK(snprintf(database_path, sizeof(database_path), "%s/project.db", + directory) > 0); CHECK(snprintf(v11_path, sizeof(v11_path), "%s/v11.db", directory) > 0); - CHECK(snprintf(failed_path, sizeof(failed_path), "%s/failed-v12.db", directory) > 0); + CHECK(snprintf(failed_path, sizeof(failed_path), "%s/failed-v12.db", + directory) > 0); CHECK(test_model_api(database_path)); CHECK(test_corruption(database_path)); CHECK(test_migration(v11_path, failed_path)); @@ -462,6 +548,4 @@ static bool run_test(void) { return true; } -int main(void) { - return run_test() ? EXIT_SUCCESS : EXIT_FAILURE; -} +int main(void) { return run_test() ? EXIT_SUCCESS : EXIT_FAILURE; } diff --git a/tests/test_geometric_verifier_task.c b/tests/test_geometric_verifier_task.c index a85cb07..b2b5ec7 100644 --- a/tests/test_geometric_verifier_task.c +++ b/tests/test_geometric_verifier_task.c @@ -331,13 +331,21 @@ static bool run_task_test(void) { lardon3d_project_db_close(state.project_db); CHECK(exec_sql( database_path, - "PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;" - "DROP TABLE geometric_verifier_tasks;" - "DROP TABLE track_observations;" - "DROP TABLE tracks;" - "DROP TABLE track_sets;" - "DROP TABLE track_builder_tasks;" - "UPDATE metadata SET value=12 WHERE key='schema_version';COMMIT;")); + "PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;" + "DROP TABLE IF EXISTS sparse_landmark_observations;" + "DROP TABLE IF EXISTS sparse_landmarks;" + "DROP TABLE IF EXISTS sparse_registered_images;" + "DROP TABLE IF EXISTS sparse_reconstruction_components;" + "DROP TABLE IF EXISTS sparse_reconstructions;" + "DROP TABLE IF EXISTS sparse_calibration_scope_images;" + "DROP TABLE IF EXISTS sparse_calibration_scopes;" + "DROP TABLE IF EXISTS sparse_calibrations;" + "DROP TABLE geometric_verifier_tasks;" + "DROP TABLE track_observations;" + "DROP TABLE tracks;" + "DROP TABLE track_sets;" + "DROP TABLE track_builder_tasks;" + "UPDATE metadata SET value=12 WHERE key='schema_version';COMMIT;")); CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V13", "1", 1) == 0); Lardon3DProjectDb *database = NULL; CHECK(lardon3d_project_db_open(database_path, &database, error) == @@ -353,7 +361,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) == 15); + CHECK(lardon3d_project_db_schema_version(database) == 16); lardon3d_project_db_close(database); CHECK(remove_tree(root)); return true; diff --git a/tests/test_match_result.c b/tests/test_match_result.c index 873a054..29daf59 100644 --- a/tests/test_match_result.c +++ b/tests/test_match_result.c @@ -11,18 +11,20 @@ #include -#define CHECK(condition) \ - do { \ - if (!(condition)) { \ - fprintf(stderr, "Échec ligne %d : %s\n", __LINE__, #condition); \ - return false; \ - } \ +#define CHECK(condition) \ + do { \ + if (!(condition)) { \ + fprintf(stderr, "Échec ligne %d : %s\n", __LINE__, #condition); \ + return false; \ + } \ } while (0) -static bool query_integer(const char *path, const char *sql, sqlite3_int64 expected) { +static bool query_integer(const char *path, const char *sql, + sqlite3_int64 expected) { sqlite3 *connection = NULL; sqlite3_stmt *statement = NULL; - if (sqlite3_open_v2(path, &connection, SQLITE_OPEN_READONLY, NULL) != SQLITE_OK || + if (sqlite3_open_v2(path, &connection, SQLITE_OPEN_READONLY, NULL) != + SQLITE_OK || sqlite3_prepare_v2(connection, sql, -1, &statement, NULL) != SQLITE_OK) { if (statement) { (void)sqlite3_finalize(statement); @@ -35,33 +37,46 @@ static bool query_integer(const char *path, const char *sql, sqlite3_int64 expec bool matches = sqlite3_step(statement) == SQLITE_ROW && sqlite3_column_int64(statement, 0) == expected && sqlite3_step(statement) == SQLITE_DONE; - return sqlite3_finalize(statement) == SQLITE_OK && sqlite3_close(connection) == SQLITE_OK && - matches; + return sqlite3_finalize(statement) == SQLITE_OK && + sqlite3_close(connection) == SQLITE_OK && matches; } static bool create_v9_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; + 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; + if (sqlite3_open(path, &connection) != SQLITE_OK) + return false; static const char sql[] = "PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;" + "DROP TABLE IF EXISTS sparse_landmark_observations;" + "DROP TABLE IF EXISTS sparse_landmarks;" + "DROP TABLE IF EXISTS sparse_registered_images;" + "DROP TABLE IF EXISTS sparse_reconstruction_components;" + "DROP TABLE IF EXISTS sparse_reconstructions;" + "DROP TABLE IF EXISTS sparse_calibration_scope_images;" + "DROP TABLE IF EXISTS sparse_calibration_scopes;" + "DROP TABLE IF EXISTS sparse_calibrations;" "DROP TABLE geometric_verifier_tasks;" "DROP TABLE geometric_verification_results;" "DROP TABLE matcher_tasks;" "DROP TABLE match_results;" "DROP TABLE track_observations;" "DROP TABLE tracks;" - "DROP TABLE track_sets;DROP TABLE track_builder_tasks;" - "UPDATE metadata SET value=9 WHERE key='schema_version';COMMIT;PRAGMA foreign_keys=ON;"; + "DROP TABLE track_sets;DROP TABLE track_builder_tasks;" + "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; } -static void asset_path_for_hash(const unsigned char hash[LARDON3D_PROJECT_DB_SHA256_SIZE], - char path[LARDON3D_PROJECT_DB_PATH_CAPACITY]) { +static void +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 i = 0; i < LARDON3D_PROJECT_DB_SHA256_SIZE; ++i) { @@ -69,8 +84,8 @@ static void asset_path_for_hash(const unsigned char hash[LARDON3D_PROJECT_DB_SHA hex[i * 2 + 1] = digits[hash[i] & 15U]; } hex[64] = '\0'; - (void)snprintf(path, LARDON3D_PROJECT_DB_PATH_CAPACITY, "assets/images/%c%c/%s", hex[0], hex[1], - hex); + (void)snprintf(path, LARDON3D_PROJECT_DB_PATH_CAPACITY, + "assets/images/%c%c/%s", hex[0], hex[1], hex); } static void feature_asset_path_for_hash( @@ -83,22 +98,25 @@ static void feature_asset_path_for_hash( hex[i * 2 + 1] = digits[hash[i] & 15U]; } hex[64] = '\0'; - (void)snprintf(path, LARDON3D_PROJECT_DB_PATH_CAPACITY, "assets/features/%c%c/%s", hex[0], hex[1], - hex); + (void)snprintf(path, LARDON3D_PROJECT_DB_PATH_CAPACITY, + "assets/features/%c%c/%s", hex[0], hex[1], hex); } static bool run_test(void) { char directory[] = "/tmp/lardon3d-match-result-XXXXXX"; CHECK(mkdtemp(directory)); char database_path[512], v9_path[512], failed_v10_path[512]; - CHECK(snprintf(database_path, sizeof(database_path), "%s/project.db", directory) > 0); + CHECK(snprintf(database_path, sizeof(database_path), "%s/project.db", + directory) > 0); CHECK(snprintf(v9_path, sizeof(v9_path), "%s/v9.db", directory) > 0); - CHECK(snprintf(failed_v10_path, sizeof(failed_v10_path), "%s/failed-v10.db", directory) > 0); + CHECK(snprintf(failed_v10_path, sizeof(failed_v10_path), "%s/failed-v10.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) == 15); + CHECK(lardon3d_project_db_open(database_path, &database, error) == + LARDON3D_PROJECT_DB_OK); + CHECK(database && lardon3d_project_db_schema_version(database) == 16); Lardon3DProjectDbScanSet scanset; CHECK(lardon3d_project_db_create_scanset(database, "Match-test", &scanset) == @@ -109,21 +127,24 @@ static bool run_test(void) { asset_path_for_hash(hash_a, path_a); Lardon3DProjectDbImageRegisterStatus identity_status; Lardon3DProjectDbImage image_a; - CHECK(lardon3d_project_db_register_image(database, scanset.scanset_id, hash_a, path_a, 1, - "a.jpg", "/src/a.jpg", 0, 1, &identity_status, + CHECK(lardon3d_project_db_register_image(database, scanset.scanset_id, hash_a, + path_a, 1, "a.jpg", "/src/a.jpg", 0, + 1, &identity_status, &image_a) == LARDON3D_PROJECT_DB_OK); unsigned char hash_b[LARDON3D_PROJECT_DB_SHA256_SIZE] = {0xBB}; char path_b[LARDON3D_PROJECT_DB_PATH_CAPACITY]; asset_path_for_hash(hash_b, path_b); Lardon3DProjectDbImage image_b; - CHECK(lardon3d_project_db_register_image(database, scanset.scanset_id, hash_b, path_b, 1, - "b.jpg", "/src/b.jpg", 0, 2, &identity_status, + CHECK(lardon3d_project_db_register_image(database, scanset.scanset_id, hash_b, + path_b, 1, "b.jpg", "/src/b.jpg", 0, + 2, &identity_status, &image_b) == LARDON3D_PROJECT_DB_OK); Lardon3DProjectDbCandidatePair pair; - CHECK(lardon3d_project_db_create_candidate_pair(database, image_a.image_id, image_b.image_id, 10, - &pair) == LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_create_candidate_pair( + database, image_a.image_id, image_b.image_id, 10, &pair) == + LARDON3D_PROJECT_DB_OK); CHECK(pair.candidate_pair_id > 0); /* Create feature sets for foreign key references */ @@ -132,8 +153,9 @@ static bool run_test(void) { feature_asset_path_for_hash(feature_hash_a, feature_path_a); Lardon3DProjectDbFeatureSet fs_a; CHECK(lardon3d_project_db_register_feature_set( - database, image_a.image_id, "orb", 1, feature_hash_a, hash_a, 100, 1, 32, - feature_hash_a, feature_path_a, 128, LARDON3D_DB_FEATURE_ASSET_DURABLE, 0, 10, + database, image_a.image_id, "orb", 1, feature_hash_a, hash_a, 100, + 1, 32, feature_hash_a, feature_path_a, 128, + LARDON3D_DB_FEATURE_ASSET_DURABLE, 0, 10, &fs_a) == LARDON3D_PROJECT_DB_OK); CHECK(fs_a.feature_set_id > 0 && fs_a.image_id == image_a.image_id); @@ -142,10 +164,10 @@ static bool run_test(void) { feature_asset_path_for_hash(feature_hash_b, feature_path_b); Lardon3DProjectDbFeatureSet fs_b; CHECK(lardon3d_project_db_register_feature_set( - database, image_b.image_id, "sift", 1, feature_hash_b, hash_b, 100, 2, 128, - feature_hash_b, feature_path_b, 256, - LARDON3D_DB_FEATURE_ASSET_PUBLISHED_NOT_DURABLE, 0, 20, &fs_b) == - LARDON3D_PROJECT_DB_OK); + database, image_b.image_id, "sift", 1, feature_hash_b, hash_b, 100, + 2, 128, feature_hash_b, feature_path_b, 256, + LARDON3D_DB_FEATURE_ASSET_PUBLISHED_NOT_DURABLE, 0, 20, + &fs_b) == LARDON3D_PROJECT_DB_OK); CHECK(fs_b.feature_set_id > 0 && fs_b.image_id == image_b.image_id); /* Create MATCHED result with inliers */ @@ -156,14 +178,16 @@ static bool run_test(void) { memset(match_hash, 0x5A, sizeof(match_hash)); const char *match_path = "assets/matches/5a/asset"; CHECK(lardon3d_project_db_create_match_result( - database, pair.candidate_pair_id, fs_a.feature_set_id, fs_b.feature_set_id, - "orb_bf", 1, fp1, LARDON3D_MATCH_RESULT_STATUS_MATCHED, 30, - match_hash, match_path, 392, 100, - &result) == LARDON3D_PROJECT_DB_OK); - CHECK(result.match_result_id > 0 && result.candidate_pair_id == pair.candidate_pair_id && + database, pair.candidate_pair_id, fs_a.feature_set_id, + fs_b.feature_set_id, "orb_bf", 1, fp1, + LARDON3D_MATCH_RESULT_STATUS_MATCHED, 30, match_hash, match_path, + 392, 100, &result) == LARDON3D_PROJECT_DB_OK); + CHECK(result.match_result_id > 0 && + result.candidate_pair_id == pair.candidate_pair_id && result.feature_set_id_a == fs_a.feature_set_id && result.feature_set_id_b == fs_b.feature_set_id && - strcmp(result.matcher_kind, "orb_bf") == 0 && result.matcher_version == 1 && + strcmp(result.matcher_kind, "orb_bf") == 0 && + result.matcher_version == 1 && memcmp(result.parameter_fingerprint, fp1, 32) == 0 && result.result_status == LARDON3D_MATCH_RESULT_STATUS_MATCHED && result.match_count == 30 && result.created_at == 100); @@ -173,9 +197,9 @@ static bool run_test(void) { unsigned char fp2[32]; memset(fp2, 0x02, 32); CHECK(lardon3d_project_db_create_match_result( - database, pair.candidate_pair_id, fs_a.feature_set_id, fs_b.feature_set_id, - "orb_bf", 2, fp2, LARDON3D_MATCH_RESULT_STATUS_NO_MATCH, 0, - NULL, NULL, 0, 200, + database, pair.candidate_pair_id, fs_a.feature_set_id, + fs_b.feature_set_id, "orb_bf", 2, fp2, + LARDON3D_MATCH_RESULT_STATUS_NO_MATCH, 0, NULL, NULL, 0, 200, &result) == LARDON3D_PROJECT_DB_OK); CHECK(result.result_status == LARDON3D_MATCH_RESULT_STATUS_NO_MATCH && result.match_count == 0 && !result.has_match_asset); @@ -183,10 +207,10 @@ static bool run_test(void) { /* Duplicate: same CP + same matcher + same params + same FS → CONSTRAINT */ CHECK(lardon3d_project_db_create_match_result( - database, pair.candidate_pair_id, fs_a.feature_set_id, fs_b.feature_set_id, - "orb_bf", 1, fp1, LARDON3D_MATCH_RESULT_STATUS_MATCHED, 10, - match_hash, match_path, 152, 500, - &result) == LARDON3D_PROJECT_DB_CONSTRAINT); + database, pair.candidate_pair_id, fs_a.feature_set_id, + fs_b.feature_set_id, "orb_bf", 1, fp1, + LARDON3D_MATCH_RESULT_STATUS_MATCHED, 10, match_hash, match_path, + 152, 500, &result) == LARDON3D_PROJECT_DB_CONSTRAINT); /* Create additional Feature Sets for distinct-FS and inverted-ID tests */ unsigned char feature_hash_a2[LARDON3D_PROJECT_DB_SHA256_SIZE] = {0x33}; @@ -197,7 +221,8 @@ static bool run_test(void) { memset(fs_a2_fp, 0xAA, 32); CHECK(lardon3d_project_db_register_feature_set( database, image_a.image_id, "orb", 2, fs_a2_fp, hash_a, 200, 1, 32, - feature_hash_a2, feature_path_a2, 256, LARDON3D_DB_FEATURE_ASSET_DURABLE, 0, 30, + feature_hash_a2, feature_path_a2, 256, + LARDON3D_DB_FEATURE_ASSET_DURABLE, 0, 30, &fs_a2) == LARDON3D_PROJECT_DB_OK); CHECK(fs_a2.feature_set_id > 0 && fs_a2.image_id == image_a.image_id); @@ -208,10 +233,10 @@ static bool run_test(void) { unsigned char fs_b2_fp[32]; memset(fs_b2_fp, 0xBB, 32); CHECK(lardon3d_project_db_register_feature_set( - database, image_b.image_id, "sift", 2, fs_b2_fp, hash_b, 200, 2, 128, - feature_hash_b2, feature_path_b2, 512, - LARDON3D_DB_FEATURE_ASSET_PUBLISHED_NOT_DURABLE, 0, 40, &fs_b2) == - LARDON3D_PROJECT_DB_OK); + database, image_b.image_id, "sift", 2, fs_b2_fp, hash_b, 200, 2, + 128, feature_hash_b2, feature_path_b2, 512, + LARDON3D_DB_FEATURE_ASSET_PUBLISHED_NOT_DURABLE, 0, 40, + &fs_b2) == LARDON3D_PROJECT_DB_OK); CHECK(fs_b2.feature_set_id > 0 && fs_b2.image_id == image_b.image_id); /* Same CP + same matcher + same params + different FS → distinct */ @@ -219,41 +244,46 @@ static bool run_test(void) { memset(fp5, 0x05, 32); Lardon3DProjectDbMatchResult distinct_fs_result; CHECK(lardon3d_project_db_create_match_result( - database, pair.candidate_pair_id, fs_a2.feature_set_id, fs_b2.feature_set_id, - "orb_bf", 1, fp5, LARDON3D_MATCH_RESULT_STATUS_MATCHED, 20, - match_hash, match_path, 272, 400, - &distinct_fs_result) == LARDON3D_PROJECT_DB_OK); + database, pair.candidate_pair_id, fs_a2.feature_set_id, + fs_b2.feature_set_id, "orb_bf", 1, fp5, + LARDON3D_MATCH_RESULT_STATUS_MATCHED, 20, match_hash, match_path, + 272, 400, &distinct_fs_result) == LARDON3D_PROJECT_DB_OK); CHECK(distinct_fs_result.feature_set_id_a == fs_a2.feature_set_id && distinct_fs_result.feature_set_id_b == fs_b2.feature_set_id); uint64_t distinct_id = distinct_fs_result.match_result_id; CHECK(distinct_id != first_id); /* feature_set_id_a > feature_set_id_b but correct ownership → valid */ - /* Register image_c, image_d; create FS for d first, then c → fs_c.id > fs_d.id */ + /* Register image_c, image_d; create FS for d first, then c → fs_c.id > + * fs_d.id */ unsigned char hash_c[LARDON3D_PROJECT_DB_SHA256_SIZE] = {0xCC}; char path_c[LARDON3D_PROJECT_DB_PATH_CAPACITY]; asset_path_for_hash(hash_c, path_c); Lardon3DProjectDbImage image_c; - CHECK(lardon3d_project_db_register_image(database, scanset.scanset_id, hash_c, path_c, 1, - "c.jpg", "/src/c.jpg", 0, 3, &identity_status, - &image_c) == LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_register_image(database, scanset.scanset_id, hash_c, + path_c, 1, "c.jpg", "/src/c.jpg", 0, + 3, &identity_status, + &image_c) == LARDON3D_PROJECT_DB_OK); unsigned char hash_d[LARDON3D_PROJECT_DB_SHA256_SIZE] = {0xDD}; char path_d[LARDON3D_PROJECT_DB_PATH_CAPACITY]; asset_path_for_hash(hash_d, path_d); Lardon3DProjectDbImage image_d; - CHECK(lardon3d_project_db_register_image(database, scanset.scanset_id, hash_d, path_d, 1, - "d.jpg", "/src/d.jpg", 0, 4, &identity_status, - &image_d) == LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_register_image(database, scanset.scanset_id, hash_d, + path_d, 1, "d.jpg", "/src/d.jpg", 0, + 4, &identity_status, + &image_d) == LARDON3D_PROJECT_DB_OK); - /* Create feature set for image_d first (lower ID), then image_c (higher ID) */ + /* Create feature set for image_d first (lower ID), then image_c (higher ID) + */ unsigned char feature_hash_d[LARDON3D_PROJECT_DB_SHA256_SIZE] = {0x55}; char feature_path_d[LARDON3D_PROJECT_DB_PATH_CAPACITY]; feature_asset_path_for_hash(feature_hash_d, feature_path_d); Lardon3DProjectDbFeatureSet fs_d; CHECK(lardon3d_project_db_register_feature_set( - database, image_d.image_id, "orb", 1, feature_hash_d, hash_d, 100, 1, 32, - feature_hash_d, feature_path_d, 128, LARDON3D_DB_FEATURE_ASSET_DURABLE, 0, 50, + database, image_d.image_id, "orb", 1, feature_hash_d, hash_d, 100, + 1, 32, feature_hash_d, feature_path_d, 128, + LARDON3D_DB_FEATURE_ASSET_DURABLE, 0, 50, &fs_d) == LARDON3D_PROJECT_DB_OK); unsigned char feature_hash_c[LARDON3D_PROJECT_DB_SHA256_SIZE] = {0x66}; @@ -261,26 +291,27 @@ static bool run_test(void) { feature_asset_path_for_hash(feature_hash_c, feature_path_c); Lardon3DProjectDbFeatureSet fs_c; CHECK(lardon3d_project_db_register_feature_set( - database, image_c.image_id, "sift", 1, feature_hash_c, hash_c, 100, 2, 128, - feature_hash_c, feature_path_c, 256, - LARDON3D_DB_FEATURE_ASSET_PUBLISHED_NOT_DURABLE, 0, 60, &fs_c) == - LARDON3D_PROJECT_DB_OK); + database, image_c.image_id, "sift", 1, feature_hash_c, hash_c, 100, + 2, 128, feature_hash_c, feature_path_c, 256, + LARDON3D_DB_FEATURE_ASSET_PUBLISHED_NOT_DURABLE, 0, 60, + &fs_c) == LARDON3D_PROJECT_DB_OK); /* fs_c was created after fs_d → fs_c.feature_set_id > fs_d.feature_set_id */ CHECK(fs_c.feature_set_id > fs_d.feature_set_id); /* CP (image_c, image_d): image_c should come first since registered first */ Lardon3DProjectDbCandidatePair pair_cd; - CHECK(lardon3d_project_db_create_candidate_pair(database, image_c.image_id, image_d.image_id, 20, - &pair_cd) == LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_create_candidate_pair( + database, image_c.image_id, image_d.image_id, 20, &pair_cd) == + LARDON3D_PROJECT_DB_OK); unsigned char fp6[32]; memset(fp6, 0x06, 32); Lardon3DProjectDbMatchResult inverted_fs_result; CHECK(lardon3d_project_db_create_match_result( - database, pair_cd.candidate_pair_id, fs_c.feature_set_id, fs_d.feature_set_id, - "orb_bf", 1, fp6, LARDON3D_MATCH_RESULT_STATUS_MATCHED, 15, - match_hash, match_path, 212, 500, - &inverted_fs_result) == LARDON3D_PROJECT_DB_OK); + database, pair_cd.candidate_pair_id, fs_c.feature_set_id, + fs_d.feature_set_id, "orb_bf", 1, fp6, + LARDON3D_MATCH_RESULT_STATUS_MATCHED, 15, match_hash, match_path, + 212, 500, &inverted_fs_result) == LARDON3D_PROJECT_DB_OK); CHECK(inverted_fs_result.feature_set_id_a == fs_c.feature_set_id && inverted_fs_result.feature_set_id_b == fs_d.feature_set_id); CHECK(fs_c.feature_set_id > fs_d.feature_set_id); @@ -289,26 +320,27 @@ static bool run_test(void) { unsigned char fp7[32]; memset(fp7, 0x07, 32); CHECK(lardon3d_project_db_create_match_result( - database, pair.candidate_pair_id, fs_b.feature_set_id, fs_a.feature_set_id, - "orb_bf", 1, fp7, LARDON3D_MATCH_RESULT_STATUS_MATCHED, 5, - match_hash, match_path, 92, 600, - &result) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT); + database, pair.candidate_pair_id, fs_b.feature_set_id, + fs_a.feature_set_id, "orb_bf", 1, fp7, + LARDON3D_MATCH_RESULT_STATUS_MATCHED, 5, match_hash, match_path, 92, + 600, &result) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT); /* FeatureSet B belonging to image A (not image B) → rejected */ unsigned char fp8[32]; memset(fp8, 0x08, 32); CHECK(lardon3d_project_db_create_match_result( - database, pair.candidate_pair_id, fs_a.feature_set_id, fs_a2.feature_set_id, - "orb_bf", 1, fp8, LARDON3D_MATCH_RESULT_STATUS_MATCHED, 5, - match_hash, match_path, 92, 700, - &result) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT); + database, pair.candidate_pair_id, fs_a.feature_set_id, + fs_a2.feature_set_id, "orb_bf", 1, fp8, + LARDON3D_MATCH_RESULT_STATUS_MATCHED, 5, match_hash, match_path, 92, + 700, &result) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT); /* Load by ID */ Lardon3DProjectDbMatchResult loaded; CHECK(lardon3d_project_db_load_match_result(database, first_id, &loaded) == LARDON3D_PROJECT_DB_OK); - CHECK(loaded.match_result_id == first_id && loaded.result_status == - LARDON3D_MATCH_RESULT_STATUS_MATCHED && loaded.match_count == 30); + CHECK(loaded.match_result_id == first_id && + loaded.result_status == LARDON3D_MATCH_RESULT_STATUS_MATCHED && + loaded.match_count == 30); /* Load not found */ CHECK(lardon3d_project_db_load_match_result(database, 999999, &loaded) == @@ -316,50 +348,52 @@ static bool run_test(void) { /* Find by full 6-part identity */ Lardon3DProjectDbMatchResult found; - CHECK(lardon3d_project_db_find_match_result(database, pair.candidate_pair_id, - fs_a.feature_set_id, fs_b.feature_set_id, - "orb_bf", 1, fp1, - &found) == LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_find_match_result( + database, pair.candidate_pair_id, fs_a.feature_set_id, + fs_b.feature_set_id, "orb_bf", 1, fp1, + &found) == LARDON3D_PROJECT_DB_OK); CHECK(found.match_result_id == first_id && found.match_count == 30); /* Find with wrong feature_set_id → NOT_FOUND */ - CHECK(lardon3d_project_db_find_match_result(database, pair.candidate_pair_id, - fs_a2.feature_set_id, fs_b.feature_set_id, - "orb_bf", 1, fp1, - &found) == LARDON3D_PROJECT_DB_NOT_FOUND); + CHECK(lardon3d_project_db_find_match_result( + database, pair.candidate_pair_id, fs_a2.feature_set_id, + fs_b.feature_set_id, "orb_bf", 1, fp1, + &found) == LARDON3D_PROJECT_DB_NOT_FOUND); /* Find not found */ unsigned char unknown_fp[32]; memset(unknown_fp, 0xFF, 32); - CHECK(lardon3d_project_db_find_match_result(database, pair.candidate_pair_id, - fs_a.feature_set_id, fs_b.feature_set_id, - "orb_bf", 1, unknown_fp, - &found) == LARDON3D_PROJECT_DB_NOT_FOUND); + CHECK(lardon3d_project_db_find_match_result( + database, pair.candidate_pair_id, fs_a.feature_set_id, + fs_b.feature_set_id, "orb_bf", 1, unknown_fp, + &found) == LARDON3D_PROJECT_DB_NOT_FOUND); /* Find with wrong candidate_pair */ - CHECK(lardon3d_project_db_find_match_result(database, 999999, - fs_a.feature_set_id, fs_b.feature_set_id, - "orb_bf", 1, fp1, - &found) == LARDON3D_PROJECT_DB_NOT_FOUND); + CHECK(lardon3d_project_db_find_match_result( + database, 999999, fs_a.feature_set_id, fs_b.feature_set_id, + "orb_bf", 1, fp1, &found) == LARDON3D_PROJECT_DB_NOT_FOUND); /* Pagination */ Lardon3DProjectDbMatchResult page[LARDON3D_PROJECT_DB_MATCH_RESULT_PAGE_MAX]; size_t page_count = 0; - CHECK(lardon3d_project_db_list_match_results(database, 0, page, 4, &page_count) == - LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_list_match_results( + database, 0, page, 4, &page_count) == LARDON3D_PROJECT_DB_OK); CHECK(page_count == 4 && page[0].match_result_id == first_id && - page[1].match_result_id == second_id && page[2].match_result_id == distinct_id); + page[1].match_result_id == second_id && + page[2].match_result_id == distinct_id); /* Cursor pagination */ page_count = 0; - CHECK(lardon3d_project_db_list_match_results(database, first_id, page, 2, &page_count) == + CHECK(lardon3d_project_db_list_match_results(database, first_id, page, 2, + &page_count) == LARDON3D_PROJECT_DB_OK); CHECK(page_count == 2 && page[0].match_result_id == second_id && page[1].match_result_id == distinct_id); /* End of list */ page_count = 0; - CHECK(lardon3d_project_db_list_match_results(database, distinct_id, page, 1, &page_count) == + CHECK(lardon3d_project_db_list_match_results(database, distinct_id, page, 1, + &page_count) == LARDON3D_PROJECT_DB_OK); CHECK(page_count == 1 && page[0].match_result_id > distinct_id); @@ -369,114 +403,116 @@ static bool run_test(void) { &page_count) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT); /* Invalid arguments: NULL database */ - CHECK(lardon3d_project_db_create_match_result(NULL, pair.candidate_pair_id, - fs_a.feature_set_id, fs_b.feature_set_id, "orb_bf", 1, fp1, - LARDON3D_MATCH_RESULT_STATUS_MATCHED, 30, - match_hash, match_path, 392, 100, - &result) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT); + CHECK(lardon3d_project_db_create_match_result( + NULL, pair.candidate_pair_id, fs_a.feature_set_id, + fs_b.feature_set_id, "orb_bf", 1, fp1, + LARDON3D_MATCH_RESULT_STATUS_MATCHED, 30, match_hash, match_path, + 392, 100, &result) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT); /* Invalid arguments: 0 ids */ CHECK(lardon3d_project_db_create_match_result( - database, 0, fs_a.feature_set_id, fs_b.feature_set_id, "orb_bf", 1, fp1, - LARDON3D_MATCH_RESULT_STATUS_MATCHED, 30, - match_hash, match_path, 392, 100, &result) == - LARDON3D_PROJECT_DB_INVALID_ARGUMENT); + database, 0, fs_a.feature_set_id, fs_b.feature_set_id, "orb_bf", 1, + fp1, LARDON3D_MATCH_RESULT_STATUS_MATCHED, 30, match_hash, + match_path, 392, 100, + &result) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT); CHECK(lardon3d_project_db_create_match_result( - database, pair.candidate_pair_id, 0, fs_b.feature_set_id, "orb_bf", 1, fp1, - LARDON3D_MATCH_RESULT_STATUS_MATCHED, 30, - match_hash, match_path, 392, 100, &result) == - LARDON3D_PROJECT_DB_INVALID_ARGUMENT); + database, pair.candidate_pair_id, 0, fs_b.feature_set_id, "orb_bf", + 1, fp1, LARDON3D_MATCH_RESULT_STATUS_MATCHED, 30, match_hash, + match_path, 392, 100, + &result) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT); /* Invalid arguments: NULL output */ CHECK(lardon3d_project_db_create_match_result( - database, pair.candidate_pair_id, fs_a.feature_set_id, fs_b.feature_set_id, - "orb_bf", 1, fp1, LARDON3D_MATCH_RESULT_STATUS_MATCHED, 30, - match_hash, match_path, 392, 100, NULL) == - LARDON3D_PROJECT_DB_INVALID_ARGUMENT); + database, pair.candidate_pair_id, fs_a.feature_set_id, + fs_b.feature_set_id, "orb_bf", 1, fp1, + LARDON3D_MATCH_RESULT_STATUS_MATCHED, 30, match_hash, match_path, + 392, 100, NULL) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT); /* Invalid arguments: empty matcher_kind */ CHECK(lardon3d_project_db_create_match_result( - database, pair.candidate_pair_id, fs_a.feature_set_id, fs_b.feature_set_id, - "", 1, fp1, LARDON3D_MATCH_RESULT_STATUS_MATCHED, 30, - match_hash, match_path, 392, 100, &result) == - LARDON3D_PROJECT_DB_INVALID_ARGUMENT); + database, pair.candidate_pair_id, fs_a.feature_set_id, + fs_b.feature_set_id, "", 1, fp1, + LARDON3D_MATCH_RESULT_STATUS_MATCHED, 30, match_hash, match_path, + 392, 100, &result) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT); /* Invalid arguments: 0 matcher_version */ CHECK(lardon3d_project_db_create_match_result( - database, pair.candidate_pair_id, fs_a.feature_set_id, fs_b.feature_set_id, - "orb_bf", 0, fp1, LARDON3D_MATCH_RESULT_STATUS_MATCHED, 30, - match_hash, match_path, 392, 100, &result) == - LARDON3D_PROJECT_DB_INVALID_ARGUMENT); + database, pair.candidate_pair_id, fs_a.feature_set_id, + fs_b.feature_set_id, "orb_bf", 0, fp1, + LARDON3D_MATCH_RESULT_STATUS_MATCHED, 30, match_hash, match_path, + 392, 100, &result) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT); /* Invalid arguments: NULL fingerprint */ CHECK(lardon3d_project_db_create_match_result( - database, pair.candidate_pair_id, fs_a.feature_set_id, fs_b.feature_set_id, - "orb_bf", 1, NULL, LARDON3D_MATCH_RESULT_STATUS_MATCHED, 30, - match_hash, match_path, 392, 100, &result) == - LARDON3D_PROJECT_DB_INVALID_ARGUMENT); + database, pair.candidate_pair_id, fs_a.feature_set_id, + fs_b.feature_set_id, "orb_bf", 1, NULL, + LARDON3D_MATCH_RESULT_STATUS_MATCHED, 30, match_hash, match_path, + 392, 100, &result) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT); /* Invalid arguments: bad status */ CHECK(lardon3d_project_db_create_match_result( - database, pair.candidate_pair_id, fs_a.feature_set_id, fs_b.feature_set_id, - "orb_bf", 1, fp1, 2, 30, - match_hash, match_path, 392, 100, &result) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT); + database, pair.candidate_pair_id, fs_a.feature_set_id, + fs_b.feature_set_id, "orb_bf", 1, fp1, 2, 30, match_hash, + match_path, 392, 100, + &result) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT); /* Invalid result/asset combinations */ CHECK(lardon3d_project_db_create_match_result( - database, pair.candidate_pair_id, fs_a.feature_set_id, fs_b.feature_set_id, - "orb_bf", 1, fp1, LARDON3D_MATCH_RESULT_STATUS_MATCHED, 10, - NULL, NULL, 0, 100, &result) == - LARDON3D_PROJECT_DB_INVALID_ARGUMENT); + database, pair.candidate_pair_id, fs_a.feature_set_id, + fs_b.feature_set_id, "orb_bf", 1, fp1, + LARDON3D_MATCH_RESULT_STATUS_MATCHED, 10, NULL, NULL, 0, 100, + &result) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT); CHECK(lardon3d_project_db_create_match_result( - database, pair.candidate_pair_id, fs_a.feature_set_id, fs_b.feature_set_id, - "bad-no-match", 1, fp1, LARDON3D_MATCH_RESULT_STATUS_NO_MATCH, 0, - match_hash, match_path, 32, 100, &result) == - LARDON3D_PROJECT_DB_INVALID_ARGUMENT); + database, pair.candidate_pair_id, fs_a.feature_set_id, + fs_b.feature_set_id, "bad-no-match", 1, fp1, + LARDON3D_MATCH_RESULT_STATUS_NO_MATCH, 0, match_hash, match_path, + 32, 100, &result) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT); CHECK(lardon3d_project_db_create_match_result( - database, pair.candidate_pair_id, fs_a.feature_set_id, fs_b.feature_set_id, - "too-many", 1, fp1, LARDON3D_MATCH_RESULT_STATUS_MATCHED, 8193, - match_hash, match_path, 98348, 100, &result) == - LARDON3D_PROJECT_DB_INVALID_ARGUMENT); + database, pair.candidate_pair_id, fs_a.feature_set_id, + fs_b.feature_set_id, "too-many", 1, fp1, + LARDON3D_MATCH_RESULT_STATUS_MATCHED, 8193, match_hash, match_path, + 98348, 100, &result) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT); /* Invalid arguments: negative created_at */ CHECK(lardon3d_project_db_create_match_result( - database, pair.candidate_pair_id, fs_a.feature_set_id, fs_b.feature_set_id, - "orb_bf", 1, fp1, LARDON3D_MATCH_RESULT_STATUS_MATCHED, 30, - match_hash, match_path, 392, -1, &result) == - LARDON3D_PROJECT_DB_INVALID_ARGUMENT); + database, pair.candidate_pair_id, fs_a.feature_set_id, + fs_b.feature_set_id, "orb_bf", 1, fp1, + LARDON3D_MATCH_RESULT_STATUS_MATCHED, 30, match_hash, match_path, + 392, -1, &result) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT); /* Status boundary values */ CHECK(lardon3d_project_db_create_match_result( - database, pair.candidate_pair_id, fs_a.feature_set_id, fs_b.feature_set_id, - "boundary", 1, fp1, LARDON3D_MATCH_RESULT_STATUS_NO_MATCH, 0, - NULL, NULL, 0, 500, &result) == - LARDON3D_PROJECT_DB_OK); + database, pair.candidate_pair_id, fs_a.feature_set_id, + fs_b.feature_set_id, "boundary", 1, fp1, + LARDON3D_MATCH_RESULT_STATUS_NO_MATCH, 0, NULL, NULL, 0, 500, + &result) == LARDON3D_PROJECT_DB_OK); /* Close/reopen persistence */ lardon3d_project_db_close(database); database = NULL; - CHECK(lardon3d_project_db_open(database_path, &database, error) == LARDON3D_PROJECT_DB_OK); - CHECK(lardon3d_project_db_schema_version(database) == 15); + CHECK(lardon3d_project_db_open(database_path, &database, error) == + LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_schema_version(database) == 16); /* Verify persistence: load previously created results */ CHECK(lardon3d_project_db_load_match_result(database, first_id, &loaded) == LARDON3D_PROJECT_DB_OK); - CHECK(loaded.match_result_id == first_id && loaded.result_status == - LARDON3D_MATCH_RESULT_STATUS_MATCHED && + CHECK(loaded.match_result_id == first_id && + loaded.result_status == LARDON3D_MATCH_RESULT_STATUS_MATCHED && strcmp(loaded.matcher_kind, "orb_bf") == 0 && loaded.match_count == 30); /* Verify persistence: find by key */ - CHECK(lardon3d_project_db_find_match_result(database, pair.candidate_pair_id, - fs_a.feature_set_id, fs_b.feature_set_id, - "orb_bf", 1, fp1, - &found) == LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_find_match_result( + database, pair.candidate_pair_id, fs_a.feature_set_id, + fs_b.feature_set_id, "orb_bf", 1, fp1, + &found) == LARDON3D_PROJECT_DB_OK); CHECK(found.match_result_id == first_id); /* Verify persistence of distinct result */ Lardon3DProjectDbMatchResult distinct_loaded; - CHECK(lardon3d_project_db_load_match_result(database, distinct_id, &distinct_loaded) == - LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_load_match_result( + database, distinct_id, &distinct_loaded) == LARDON3D_PROJECT_DB_OK); CHECK(distinct_loaded.match_result_id == distinct_id && distinct_loaded.feature_set_id_a == fs_a2.feature_set_id && distinct_loaded.feature_set_id_b == fs_b2.feature_set_id); @@ -484,8 +520,9 @@ static bool run_test(void) { /* Verify inverted result persisted */ Lardon3DProjectDbMatchResult inverted_loaded; - CHECK(lardon3d_project_db_load_match_result(database, inverted_fs_result.match_result_id, - &inverted_loaded) == LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_load_match_result( + database, inverted_fs_result.match_result_id, &inverted_loaded) == + LARDON3D_PROJECT_DB_OK); CHECK(inverted_loaded.feature_set_id_a == fs_c.feature_set_id && inverted_loaded.feature_set_id_b == fs_d.feature_set_id); CHECK(inverted_loaded.feature_set_id_a > inverted_loaded.feature_set_id_b); @@ -495,15 +532,20 @@ static bool run_test(void) { /* Migration from v9 */ 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) == 15); + 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) == 16); lardon3d_project_db_close(database); database = NULL; - CHECK(query_integer(v9_path, "SELECT value FROM metadata WHERE key='schema_version'", 15)); - CHECK(query_integer(v9_path, - "SELECT count(*) FROM sqlite_master WHERE type='table' AND " - "name='match_results'", 1)); + CHECK(query_integer( + v9_path, "SELECT value FROM metadata WHERE key='schema_version'", 16)); + CHECK( + query_integer(v9_path, + "SELECT count(*) FROM sqlite_master WHERE type='table' AND " + "name='match_results'", + 1)); /* Forced migration failure v10 */ CHECK(create_v9_database(failed_v10_path)); @@ -511,15 +553,22 @@ static bool run_test(void) { CHECK(lardon3d_project_db_open(failed_v10_path, &database, error) == LARDON3D_PROJECT_DB_IO_ERROR); CHECK(unsetenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V10") == 0); - CHECK(query_integer(failed_v10_path, "SELECT value FROM metadata WHERE key='schema_version'", 9)); CHECK(query_integer(failed_v10_path, - "SELECT count(*) FROM sqlite_master WHERE type='table' AND " - "name='match_results'", 0)); - CHECK(query_integer(failed_v10_path, - "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) == 15); + "SELECT value FROM metadata WHERE key='schema_version'", + 9)); + CHECK( + query_integer(failed_v10_path, + "SELECT count(*) FROM sqlite_master WHERE type='table' AND " + "name='match_results'", + 0)); + CHECK( + query_integer(failed_v10_path, + "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) == 16); lardon3d_project_db_close(database); database = NULL; @@ -530,6 +579,4 @@ static bool run_test(void) { return true; } -int main(void) { - return run_test() ? EXIT_SUCCESS : EXIT_FAILURE; -} +int main(void) { return run_test() ? EXIT_SUCCESS : EXIT_FAILURE; } diff --git a/tests/test_matcher_task.c b/tests/test_matcher_task.c index 1f6d88a..551ed28 100644 --- a/tests/test_matcher_task.c +++ b/tests/test_matcher_task.c @@ -63,12 +63,12 @@ static bool query_integer(const char *path, const char *sql, sqlite3_int64 expected) { sqlite3 *connection = NULL; sqlite3_stmt *statement = NULL; - bool success = sqlite3_open_v2(path, &connection, SQLITE_OPEN_READONLY, - NULL) == SQLITE_OK && - sqlite3_prepare_v2(connection, sql, -1, &statement, NULL) == - SQLITE_OK && - sqlite3_step(statement) == SQLITE_ROW && - sqlite3_column_int64(statement, 0) == expected; + bool success = + sqlite3_open_v2(path, &connection, SQLITE_OPEN_READONLY, NULL) == + SQLITE_OK && + sqlite3_prepare_v2(connection, sql, -1, &statement, NULL) == SQLITE_OK && + sqlite3_step(statement) == SQLITE_ROW && + sqlite3_column_int64(statement, 0) == expected; if (statement) { (void)sqlite3_finalize(statement); } @@ -89,12 +89,20 @@ static bool downgrade_project_to_historical_v10(const char *database_path) { } static const char sql[] = "PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;" + "DROP TABLE IF EXISTS sparse_landmark_observations;" + "DROP TABLE IF EXISTS sparse_landmarks;" + "DROP TABLE IF EXISTS sparse_registered_images;" + "DROP TABLE IF EXISTS sparse_reconstruction_components;" + "DROP TABLE IF EXISTS sparse_reconstructions;" + "DROP TABLE IF EXISTS sparse_calibration_scope_images;" + "DROP TABLE IF EXISTS sparse_calibration_scopes;" + "DROP TABLE IF EXISTS sparse_calibrations;" "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;DROP TABLE track_builder_tasks;" + "DROP TABLE track_sets;DROP TABLE track_builder_tasks;" "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; @@ -346,20 +354,23 @@ static bool run_test(void) { CHECK(query_integer(database_path, "SELECT value FROM metadata WHERE key='schema_version'", 10)); - CHECK(query_integer(database_path, - "SELECT count(*) FROM sqlite_master WHERE type='table' AND " - "name='match_results'", - 1)); - CHECK(query_integer(database_path, - "SELECT count(*) FROM sqlite_master WHERE type='table' AND " - "name='matcher_tasks'", - 0)); + CHECK( + query_integer(database_path, + "SELECT count(*) FROM sqlite_master WHERE type='table' AND " + "name='match_results'", + 1)); + CHECK( + query_integer(database_path, + "SELECT count(*) FROM sqlite_master WHERE type='table' AND " + "name='matcher_tasks'", + 0)); CHECK(reopen_runtime(&fixture)); - CHECK(lardon3d_project_db_schema_version(fixture.state.project_db) == 15); - CHECK(query_integer(database_path, - "SELECT count(*) FROM sqlite_master WHERE type='table' AND " - "name='matcher_tasks'", - 1)); + CHECK(lardon3d_project_db_schema_version(fixture.state.project_db) == 16); + CHECK( + query_integer(database_path, + "SELECT count(*) FROM sqlite_master WHERE type='table' AND " + "name='matcher_tasks'", + 1)); const Lardon3DTaskKindRegistry *registry = lardon3d_task_kind_registry_production(); const Lardon3DTaskKindDescriptor *descriptor = NULL; diff --git a/tests/test_project_db.c b/tests/test_project_db.c index 7ec04b9..66dcbe8 100644 --- a/tests/test_project_db.c +++ b/tests/test_project_db.c @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -6,26 +7,29 @@ #include #include #include +#include #include #include #include -#define CHECK(condition) \ - do { \ - if (!(condition)) { \ - (void)fprintf(stderr, "Échec ligne %d : %s\n", __LINE__, #condition); \ - return false; \ - } \ +#define CHECK(condition) \ + do { \ + if (!(condition)) { \ + (void)fprintf(stderr, "Échec ligne %d : %s\n", __LINE__, #condition); \ + return false; \ + } \ } while (0) -Lardon3DProjectDbResult lardon3d_project_db_test_orphan_checkpoint(Lardon3DProjectDb *database); Lardon3DProjectDbResult -lardon3d_project_db_test_delete_catalog_identity(Lardon3DProjectDb *database, uint64_t scanset_id, - uint64_t image_id, uint64_t asset_id); +lardon3d_project_db_test_orphan_checkpoint(Lardon3DProjectDb *database); +Lardon3DProjectDbResult lardon3d_project_db_test_delete_catalog_identity( + Lardon3DProjectDb *database, uint64_t scanset_id, uint64_t image_id, + uint64_t asset_id); -static void asset_path_for_hash(const unsigned char hash[LARDON3D_PROJECT_DB_SHA256_SIZE], - char path[LARDON3D_PROJECT_DB_PATH_CAPACITY]) { +static void +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 < sizeof(hex) / 2; ++index) { @@ -33,8 +37,8 @@ static void asset_path_for_hash(const unsigned char hash[LARDON3D_PROJECT_DB_SHA hex[index * 2 + 1] = digits[hash[index] & 15U]; } hex[64] = '\0'; - (void)snprintf(path, LARDON3D_PROJECT_DB_PATH_CAPACITY, "assets/images/%c%c/%s", hex[0], hex[1], - hex); + (void)snprintf(path, LARDON3D_PROJECT_DB_PATH_CAPACITY, + "assets/images/%c%c/%s", hex[0], hex[1], hex); } static void feature_asset_path_for_hash( @@ -47,22 +51,27 @@ static void feature_asset_path_for_hash( 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); + (void)snprintf(path, LARDON3D_PROJECT_DB_PATH_CAPACITY, + "assets/features/%c%c/%s", hex[0], hex[1], hex); } -static Lardon3DTaskDurableSnapshot task_snapshot(uint64_t id, Lardon3DTaskState saved) { +static Lardon3DTaskDurableSnapshot task_snapshot(uint64_t id, + Lardon3DTaskState saved) { Lardon3DTaskDurableSnapshot snapshot = { .id = id, - .estimate = {.minimum_batch_size = 1, .maximum_batch_size = 1, .desired_cpu_threads = 1}, + .estimate = {.minimum_batch_size = 1, + .maximum_batch_size = 1, + .desired_cpu_threads = 1}, .progress = saved == TASK_COMPLETED ? 100 : 25, .saved_state = saved, - .recovery_state = saved == TASK_RUNNING || saved == TASK_PAUSED ? TASK_PENDING : saved, + .recovery_state = + saved == TASK_RUNNING || saved == TASK_PAUSED ? TASK_PENDING : saved, .started_at = {.tv_sec = 10, .tv_nsec = 20}, .finished_at = {.tv_sec = 30, .tv_nsec = 40}, .sequence_count = 2, }; - (void)snprintf(snapshot.name, sizeof(snapshot.name), "Tâche %llu", (unsigned long long)id); + (void)snprintf(snapshot.name, sizeof(snapshot.name), "Tâche %llu", + (unsigned long long)id); return snapshot; } @@ -81,7 +90,8 @@ static void *read_thread(void *userdata) { context->success = true; for (size_t index = 0; index < 100; ++index) { Lardon3DProjectDbTask task; - if (lardon3d_project_db_load_task(context->database, 1, &task) != LARDON3D_PROJECT_DB_OK) { + if (lardon3d_project_db_load_task(context->database, 1, &task) != + LARDON3D_PROJECT_DB_OK) { context->success = false; break; } @@ -91,8 +101,9 @@ static void *read_thread(void *userdata) { static void *allocate_id_thread(void *userdata) { IdThreadContext *context = userdata; - context->success = lardon3d_project_db_allocate_task_id(context->database, &context->id) == - LARDON3D_PROJECT_DB_OK; + context->success = + lardon3d_project_db_allocate_task_id(context->database, &context->id) == + LARDON3D_PROJECT_DB_OK; return NULL; } @@ -101,33 +112,47 @@ static bool create_future_database(const char *path) { if (sqlite3_open(path, &connection) != SQLITE_OK) { return false; } - bool ok = sqlite3_exec(connection, - "CREATE TABLE metadata(key TEXT PRIMARY KEY,value INTEGER NOT NULL);" - "INSERT INTO metadata VALUES('schema_version',16);", - NULL, NULL, NULL) == SQLITE_OK; + bool ok = + sqlite3_exec( + connection, + "CREATE TABLE metadata(key TEXT PRIMARY KEY,value INTEGER NOT NULL);" + "INSERT INTO metadata VALUES('schema_version',17);", + NULL, NULL, NULL) == SQLITE_OK; return sqlite3_close(connection) == SQLITE_OK && ok; } static bool create_v7_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; + 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; + if (sqlite3_open(path, &connection) != SQLITE_OK) + return false; static const char sql[] = "PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;" + "DROP TABLE sparse_landmark_observations;" + "DROP TABLE sparse_landmarks;" + "DROP TABLE sparse_registered_images;" + "DROP TABLE sparse_reconstruction_components;" + "DROP TABLE sparse_reconstructions;" + "DROP TABLE sparse_calibration_scope_images;" + "DROP TABLE sparse_calibration_scopes;" + "DROP TABLE sparse_calibrations;" "DROP TABLE geometric_verifier_tasks;" "DROP TABLE geometric_verification_results;" "DROP TABLE matcher_tasks;" "DROP TABLE match_results;" "DROP TABLE candidate_pair_generate_tasks;" "DROP TABLE candidate_pairs;" - "DROP TABLE track_observations;" - "DROP TABLE track_builder_tasks;" + "DROP TABLE track_observations;" + "DROP TABLE track_builder_tasks;" "DROP TABLE tracks;" "DROP TABLE track_sets;" - "UPDATE metadata SET value=7 WHERE key='schema_version';COMMIT;PRAGMA foreign_keys=ON;"; + "UPDATE metadata SET value=7 WHERE key='schema_version';COMMIT;PRAGMA " + "foreign_keys=ON;"; bool ok = sqlite3_exec(connection, sql, NULL, NULL, NULL) == SQLITE_OK; return sqlite3_close(connection) == SQLITE_OK && ok; } @@ -135,12 +160,23 @@ static bool create_v7_database(const char *path) { static bool create_v6_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; + 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; + if (sqlite3_open(path, &connection) != SQLITE_OK) + return false; static const char sql[] = "PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;" + "DROP TABLE sparse_landmark_observations;" + "DROP TABLE sparse_landmarks;" + "DROP TABLE sparse_registered_images;" + "DROP TABLE sparse_reconstruction_components;" + "DROP TABLE sparse_reconstructions;" + "DROP TABLE sparse_calibration_scope_images;" + "DROP TABLE sparse_calibration_scopes;" + "DROP TABLE sparse_calibrations;" "DROP TABLE geometric_verifier_tasks;" "DROP TABLE geometric_verification_results;" "DROP TABLE matcher_tasks;" @@ -153,11 +189,12 @@ 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 track_builder_tasks;" + "DROP TABLE track_observations;" + "DROP TABLE track_builder_tasks;" "DROP TABLE tracks;" "DROP TABLE track_sets;" - "UPDATE metadata SET value=6 WHERE key='schema_version';COMMIT;PRAGMA foreign_keys=ON;"; + "UPDATE metadata SET value=6 WHERE key='schema_version';COMMIT;PRAGMA " + "foreign_keys=ON;"; bool ok = sqlite3_exec(connection, sql, NULL, NULL, NULL) == SQLITE_OK; return sqlite3_close(connection) == SQLITE_OK && ok; } @@ -165,7 +202,8 @@ static bool create_v6_database(const char *path) { static bool create_v10_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) { + if (lardon3d_project_db_open(path, &database, error) != + LARDON3D_PROJECT_DB_OK) { return false; } lardon3d_project_db_close(database); @@ -175,11 +213,19 @@ static bool create_v10_database(const char *path) { } static const char sql[] = "PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;" + "DROP TABLE sparse_landmark_observations;" + "DROP TABLE sparse_landmarks;" + "DROP TABLE sparse_registered_images;" + "DROP TABLE sparse_reconstruction_components;" + "DROP TABLE sparse_reconstructions;" + "DROP TABLE sparse_calibration_scope_images;" + "DROP TABLE sparse_calibration_scopes;" + "DROP TABLE sparse_calibrations;" "DROP TABLE geometric_verifier_tasks;" "DROP TABLE geometric_verification_results;" "DROP TABLE matcher_tasks;" - "DROP TABLE track_observations;" - "DROP TABLE track_builder_tasks;" + "DROP TABLE track_observations;" + "DROP TABLE track_builder_tasks;" "DROP TABLE tracks;" "DROP TABLE track_sets;" "UPDATE metadata SET value=10 WHERE key='schema_version';" @@ -191,62 +237,450 @@ static bool create_v10_database(const char *path) { 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; + 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; + 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 track_builder_tasks;" + "DROP TABLE sparse_landmark_observations;" + "DROP TABLE sparse_landmarks;" + "DROP TABLE sparse_registered_images;" + "DROP TABLE sparse_reconstruction_components;" + "DROP TABLE sparse_reconstructions;" + "DROP TABLE sparse_calibration_scope_images;" + "DROP TABLE sparse_calibration_scopes;" + "DROP TABLE sparse_calibrations;" + "DROP TABLE track_observations;" + "DROP TABLE track_builder_tasks;" "DROP TABLE tracks;" "DROP TABLE track_sets;" - "UPDATE metadata SET value=13 WHERE key='schema_version';COMMIT;PRAGMA foreign_keys=ON;"; + "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_v14_database(const char *path) { - if (!create_v13_database(path)) return false; + if (!create_v13_database(path)) + return false; sqlite3 *connection = NULL; - if (sqlite3_open(path, &connection) != SQLITE_OK) return false; + if (sqlite3_open(path, &connection) != SQLITE_OK) + return false; static const char sql[] = "PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;" "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," + "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," + "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," + "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);UPDATE metadata SET value=14 WHERE key='schema_version';COMMIT;" + "CREATE INDEX track_observations_lookup_idx ON " + "track_observations(feature_set_id,feature_index," + "track_set_id);UPDATE metadata SET value=14 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; +static bool create_true_v15_database(const char *path) { + if (!create_v14_database(path)) + return false; sqlite3 *connection = NULL; - if (sqlite3_open(path, &connection) != SQLITE_OK) return false; + if (sqlite3_open(path, &connection) != SQLITE_OK) + return false; static const char sql[] = "PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;" - "DROP TABLE visual_index_update_tasks;DROP TABLE visual_index_memberships;" + "CREATE TABLE track_builder_tasks(" + "task_id INTEGER PRIMARY KEY REFERENCES tasks(task_id) ON DELETE CASCADE," + "builder_kind TEXT NOT NULL CHECK(builder_kind='track_builder')," + "builder_version INTEGER NOT NULL CHECK(builder_version>0)," + "builder_fingerprint BLOB NOT NULL CHECK(length(builder_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)," + "scope_path TEXT NOT NULL CHECK(length(scope_path)>0 AND " + "length(scope_path)<=4096)," + "scope_size_bytes INTEGER NOT NULL CHECK(scope_size_bytes>0)," + "scope_sha256 BLOB NOT NULL CHECK(length(scope_sha256)=32)," + "scope_format_version INTEGER NOT NULL CHECK(scope_format_version=1));" + "UPDATE metadata SET value=15 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 execute_test_sql(const char *path, const char *sql) { + sqlite3 *connection = NULL; + bool ok = sqlite3_open(path, &connection) == SQLITE_OK && + sqlite3_exec(connection, sql, NULL, NULL, NULL) == SQLITE_OK; + if (connection) + ok = sqlite3_close(connection) == SQLITE_OK && ok; + return ok; +} + +static bool pragma_is_clean(const char *path, const char *pragma, + const char *expected) { + sqlite3 *connection = NULL; + sqlite3_stmt *statement = NULL; + bool clean = sqlite3_open_v2(path, &connection, SQLITE_OPEN_READONLY, NULL) == + SQLITE_OK && + sqlite3_prepare_v2(connection, pragma, -1, &statement, NULL) == + SQLITE_OK; + if (clean) { + int code = sqlite3_step(statement); + if (strcmp(pragma, "PRAGMA foreign_key_check") == 0) + clean = code == SQLITE_DONE; + else + clean = code == SQLITE_ROW && + strcmp((const char *)sqlite3_column_text(statement, 0), expected) == + 0 && + sqlite3_step(statement) == SQLITE_DONE; + } + if (statement) + sqlite3_finalize(statement); + if (connection) + sqlite3_close(connection); + return clean; +} + +typedef struct { + char *data; + size_t length; + size_t capacity; +} SchemaText; + +static bool schema_append(SchemaText *text, const char *format, ...) { + va_list arguments; + va_start(arguments, format); + va_list copy; + va_copy(copy, arguments); + int required = vsnprintf(NULL, 0, format, copy); + va_end(copy); + if (required < 0) { + va_end(arguments); + return false; + } + size_t needed = text->length + (size_t)required + 1; + if (needed > text->capacity) { + size_t capacity = text->capacity ? text->capacity : 1024; + while (capacity < needed) + capacity *= 2; + char *grown = realloc(text->data, capacity); + if (!grown) { + va_end(arguments); + return false; + } + text->data = grown; + text->capacity = capacity; + } + (void)vsnprintf(text->data + text->length, text->capacity - text->length, + format, arguments); + text->length += (size_t)required; + va_end(arguments); + return true; +} + +static bool schema_append_normalized_sql(SchemaText *text, const char *sql) { + bool whitespace = false; + if (!schema_append(text, "")) + return false; + for (const unsigned char *cursor = (const unsigned char *)sql; + cursor && *cursor; ++cursor) { + if (isspace(*cursor)) { + whitespace = true; + continue; + } + if (whitespace && text->length && text->data[text->length - 1] != ' ') { + if (!schema_append(text, " ")) + return false; + } + whitespace = false; + if (!schema_append(text, "%c", *cursor)) + return false; + } + return true; +} + +static char *schema_format(const char *format, ...) { + va_list arguments; + va_start(arguments, format); + va_list copy; + va_copy(copy, arguments); + int required = vsnprintf(NULL, 0, format, copy); + va_end(copy); + if (required < 0) { + va_end(arguments); + return NULL; + } + char *result = malloc((size_t)required + 1); + if (result) + (void)vsnprintf(result, (size_t)required + 1, format, arguments); + va_end(arguments); + return result; +} + +static int schema_string_compare(const void *left, const void *right) { + const char *const *a = left; + const char *const *b = right; + return strcmp(*a, *b); +} + +static void schema_string_array_free(char **items, size_t count) { + for (size_t index = 0; index < count; ++index) + free(items[index]); + free(items); +} + +static bool schema_append_pragma_lines(sqlite3 *connection, SchemaText *text, + const char *table, const char *pragma) { + char *query = sqlite3_mprintf("PRAGMA %s(%w)", pragma, table); + if (!query) + return false; + sqlite3_stmt *statement = NULL; + bool ok = sqlite3_prepare_v2(connection, query, -1, &statement, NULL) == + SQLITE_OK; + sqlite3_free(query); + char **lines = NULL; + size_t count = 0; + int code = SQLITE_ROW; + while (ok && (code = sqlite3_step(statement)) == SQLITE_ROW) { + char *line = NULL; + if (strcmp(pragma, "table_info") == 0) { + const unsigned char *name = sqlite3_column_text(statement, 1); + const unsigned char *type = sqlite3_column_text(statement, 2); + const unsigned char *default_value = sqlite3_column_text(statement, 4); + line = schema_format("COLUMN|%d|%s|%s|%d|%s|%d", + sqlite3_column_int(statement, 0), + name ? (const char *)name : "", + type ? (const char *)type : "", + sqlite3_column_int(statement, 3), + default_value ? (const char *)default_value : "", + sqlite3_column_int(statement, 5)); + if (!line) + ok = false; + } else if (strcmp(pragma, "foreign_key_list") == 0) { + const unsigned char *target = sqlite3_column_text(statement, 2); + const unsigned char *from = sqlite3_column_text(statement, 3); + const unsigned char *to = sqlite3_column_text(statement, 4); + const unsigned char *update = sqlite3_column_text(statement, 5); + const unsigned char *delete_action = sqlite3_column_text(statement, 6); + const unsigned char *match = sqlite3_column_text(statement, 7); + line = schema_format("FK|%d|%d|%s|%s|%s|%s|%s|%s", + sqlite3_column_int(statement, 0), + sqlite3_column_int(statement, 1), + target ? (const char *)target : "", + from ? (const char *)from : "", + to ? (const char *)to : "", + update ? (const char *)update : "", + delete_action ? (const char *)delete_action : "", + match ? (const char *)match : ""); + if (!line) + ok = false; + } else { + const unsigned char *name = sqlite3_column_text(statement, 1); + const unsigned char *origin = sqlite3_column_text(statement, 3); + const char *display_name = origin && + (strcmp((const char *)origin, "pk") == 0 || + strcmp((const char *)origin, "u") == 0) + ? "" + : (name ? (const char *)name : ""); + line = schema_format( + "INDEX|%s|%s|%d|%d|%s|%d", display_name, + name ? (const char *)name : "", sqlite3_column_int(statement, 0), + sqlite3_column_int(statement, 2), origin ? (const char *)origin : "", + sqlite3_column_int(statement, 4)); + if (!line) + ok = false; + } + if (!ok || !line) + break; + char **grown = realloc(lines, (count + 1) * sizeof(*lines)); + if (!grown) { + free(line); + ok = false; + break; + } + lines = grown; + lines[count++] = line; + } + if (code != SQLITE_DONE) + ok = false; + if (statement) + sqlite3_finalize(statement); + if (ok && count > 1) + qsort(lines, count, sizeof(*lines), schema_string_compare); + for (size_t index = 0; ok && index < count; ++index) { + if (strcmp(pragma, "index_list") == 0) { + char display_name[256]; + char actual_name[256]; + char origin[8]; + int sequence = 0; + int unique = 0; + int partial = 0; + if (sscanf(lines[index], "INDEX|%255[^|]|%255[^|]|%d|%d|%7[^|]|%d", + display_name, actual_name, &sequence, &unique, origin, + &partial) != 7 || + !schema_append(text, "index_list|INDEX|%s|%d|%d|%s|%d\n", + display_name, sequence, unique, origin, partial)) + ok = false; + const char *name_start = strchr(lines[index], '|'); + if (name_start) + ++name_start; + const char *display_end = name_start ? strchr(name_start, '|') : NULL; + const char *actual_start = display_end ? display_end + 1 : NULL; + const char *actual_end = actual_start ? strchr(actual_start, '|') : NULL; + size_t name_length = actual_end && actual_start + ? (size_t)(actual_end - actual_start) + : 0; + char index_name[256]; + if (name_length >= sizeof(index_name)) + ok = false; + else { + memcpy(index_name, actual_start, name_length); + index_name[name_length] = '\0'; + char *index_query = sqlite3_mprintf("PRAGMA index_info(%w)", + index_name); + sqlite3_stmt *index_statement = NULL; + ok = index_query && + sqlite3_prepare_v2(connection, index_query, -1, + &index_statement, NULL) == SQLITE_OK; + sqlite3_free(index_query); + while (ok && sqlite3_step(index_statement) == SQLITE_ROW) { + const char *index_label = strstr(lines[index], "||") + ? "" + : index_name; + if (!schema_append(text, "INDEX_COLUMN|%s|%d|%d|%s\n", index_label, + sqlite3_column_int(index_statement, 0), + sqlite3_column_int(index_statement, 1), + sqlite3_column_text(index_statement, 2) + ? (const char *)sqlite3_column_text( + index_statement, 2) + : "")) + ok = false; + } + if (index_statement) + sqlite3_finalize(index_statement); + } + } else if (!schema_append(text, "%s|%s\n", pragma, lines[index])) + ok = false; + } + schema_string_array_free(lines, count); + return ok; +} + +static bool schema_dump(const char *path, char **output) { + sqlite3 *connection = NULL; + if (sqlite3_open_v2(path, &connection, SQLITE_OPEN_READONLY, NULL) != + SQLITE_OK) + return false; + SchemaText text = {0}; + sqlite3_stmt *statement = NULL; + bool ok = sqlite3_prepare_v2( + connection, + "SELECT type,name,sql FROM sqlite_master WHERE name NOT LIKE " + "'sqlite_%' AND type IN ('table','index','trigger','view') " + "ORDER BY type,name", + -1, &statement, NULL) == SQLITE_OK; + int code = SQLITE_ROW; + while (ok && (code = sqlite3_step(statement)) == SQLITE_ROW) { + const char *type = (const char *)sqlite3_column_text(statement, 0); + const char *name = (const char *)sqlite3_column_text(statement, 1); + const char *sql = (const char *)sqlite3_column_text(statement, 2); + if (!schema_append(&text, "%s|%s|", type, name) || + !schema_append_normalized_sql(&text, sql ? sql : "") || + !schema_append(&text, "\n")) { + ok = false; + break; + } + if (strcmp(type, "table") == 0) { + ok = schema_append_pragma_lines(connection, &text, name, "table_info") && + schema_append_pragma_lines(connection, &text, name, + "foreign_key_list") && + schema_append_pragma_lines(connection, &text, name, "index_list"); + } + } + if (code != SQLITE_DONE) + ok = false; + if (statement) + sqlite3_finalize(statement); + if (sqlite3_close(connection) != SQLITE_OK) + ok = false; + if (!ok) { + free(text.data); + return false; + } + *output = text.data; + return true; +} + +static bool schema_compare(const char *fresh_path, const char *migrated_path, + bool report_difference) { + char *fresh = NULL; + char *migrated = NULL; + bool fresh_ok = schema_dump(fresh_path, &fresh); + bool migrated_ok = schema_dump(migrated_path, &migrated); + bool ok = fresh_ok && migrated_ok; + if (!ok && report_difference) + fprintf(stderr, "schema comparator metadata collection failed (%d,%d)\n", + fresh_ok, migrated_ok); + if (ok && strcmp(fresh, migrated) != 0) { + ok = false; + if (report_difference) + fprintf(stderr, "schema comparator mismatch: structural metadata differs\n"); + } + free(fresh); + free(migrated); + return ok; +} + +static bool create_v5_database(const char *path) { + if (!create_v6_database(path)) + return false; + sqlite3 *connection = NULL; + if (sqlite3_open(path, &connection) != SQLITE_OK) + return false; + static const char sql[] = + "PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;" + "DROP TABLE visual_index_update_tasks;DROP TABLE " + "visual_index_memberships;" "DROP TABLE visual_index_segments;DROP TABLE visual_indexes;" - "UPDATE metadata SET value=5 WHERE key='schema_version';COMMIT;PRAGMA foreign_keys=ON;"; + "UPDATE metadata SET value=5 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; } @@ -256,31 +690,45 @@ static bool create_v1_database(const char *path) { "PRAGMA foreign_keys=ON;" "CREATE TABLE metadata(key TEXT PRIMARY KEY,value INTEGER NOT NULL);" "INSERT INTO metadata VALUES('schema_version',1);" - "CREATE TABLE project(singleton INTEGER PRIMARY KEY CHECK(singleton=1),stable_id TEXT NOT " - "NULL UNIQUE,name TEXT NOT NULL,created_at INTEGER NOT NULL,updated_at INTEGER NOT NULL);" - "CREATE TABLE tasks(task_id INTEGER PRIMARY KEY CHECK(task_id>0),name TEXT NOT " - "NULL,saved_state INTEGER NOT NULL CHECK(saved_state BETWEEN 0 AND 5),recovery_state " - "INTEGER NOT NULL CHECK(recovery_state BETWEEN 0 AND 5),progress INTEGER NOT NULL " + "CREATE TABLE project(singleton INTEGER PRIMARY KEY " + "CHECK(singleton=1),stable_id TEXT NOT " + "NULL UNIQUE,name TEXT NOT NULL,created_at INTEGER NOT NULL,updated_at " + "INTEGER NOT NULL);" + "CREATE TABLE tasks(task_id INTEGER PRIMARY KEY CHECK(task_id>0),name " + "TEXT NOT " + "NULL,saved_state INTEGER NOT NULL CHECK(saved_state BETWEEN 0 AND " + "5),recovery_state " + "INTEGER NOT NULL CHECK(recovery_state BETWEEN 0 AND 5),progress INTEGER " + "NOT NULL " "CHECK(progress BETWEEN 0 AND 100),sequence_count INTEGER NOT NULL " - "CHECK(sequence_count>=0),started_sec INTEGER NOT NULL,started_nsec INTEGER NOT NULL " - "CHECK(started_nsec BETWEEN 0 AND 999999999),finished_sec INTEGER NOT NULL,finished_nsec " - "INTEGER NOT NULL CHECK(finished_nsec BETWEEN 0 AND 999999999),updated_at INTEGER NOT " + "CHECK(sequence_count>=0),started_sec INTEGER NOT NULL,started_nsec " + "INTEGER NOT NULL " + "CHECK(started_nsec BETWEEN 0 AND 999999999),finished_sec INTEGER NOT " + "NULL,finished_nsec " + "INTEGER NOT NULL CHECK(finished_nsec BETWEEN 0 AND " + "999999999),updated_at INTEGER NOT " "NULL);" "CREATE INDEX tasks_recovery_state_idx ON tasks(recovery_state,task_id);" - "CREATE TABLE checkpoints(task_id INTEGER PRIMARY KEY REFERENCES tasks(task_id) ON DELETE " + "CREATE TABLE checkpoints(task_id INTEGER PRIMARY KEY REFERENCES " + "tasks(task_id) ON DELETE " "CASCADE,path TEXT NOT NULL,format_version INTEGER NOT NULL " - "CHECK(format_version>0),durability INTEGER NOT NULL CHECK(durability BETWEEN 0 AND " + "CHECK(format_version>0),durability INTEGER NOT NULL CHECK(durability " + "BETWEEN 0 AND " "1),updated_at INTEGER NOT NULL);" - "CREATE TABLE artifacts(artifact_id TEXT PRIMARY KEY,kind TEXT NOT NULL,path TEXT NOT " - "NULL,state INTEGER NOT NULL CHECK(state BETWEEN 0 AND 1),size_bytes INTEGER NOT NULL " - "CHECK(size_bytes>=0),producer_task_id INTEGER REFERENCES tasks(task_id),created_at " + "CREATE TABLE artifacts(artifact_id TEXT PRIMARY KEY,kind TEXT NOT " + "NULL,path TEXT NOT " + "NULL,state INTEGER NOT NULL CHECK(state BETWEEN 0 AND 1),size_bytes " + "INTEGER NOT NULL " + "CHECK(size_bytes>=0),producer_task_id INTEGER REFERENCES " + "tasks(task_id),created_at " "INTEGER NOT NULL,updated_at INTEGER NOT NULL);" "CREATE INDEX artifacts_state_idx ON artifacts(state,artifact_id);" "CREATE INDEX artifacts_producer_idx ON artifacts(producer_task_id);" "INSERT INTO project VALUES(1,'legacy-project','Legacy',1,1);" "INSERT INTO tasks VALUES(9,'Legacy task',1,0,12,3,1,0,0,0,2);" "INSERT INTO checkpoints VALUES(9,'legacy.chk',1,0,2);" - "INSERT INTO artifacts VALUES('legacy-artifact','legacy','legacy.bin',0,0,9,2,2);"; + "INSERT INTO artifacts " + "VALUES('legacy-artifact','legacy','legacy.bin',0,0,9,2,2);"; sqlite3 *connection = NULL; if (sqlite3_open(path, &connection) != SQLITE_OK) { return false; @@ -302,7 +750,8 @@ static bool create_v2_database(const char *path) { "ALTER TABLE tasks ADD COLUMN task_kind TEXT;" "ALTER TABLE tasks ADD COLUMN task_kind_version INTEGER " "CHECK(task_kind_version IS NULL OR task_kind_version>0);" - "UPDATE tasks SET task_kind='test.work',task_kind_version=1 WHERE task_id=9;" + "UPDATE tasks SET task_kind='test.work',task_kind_version=1 " + "WHERE task_id=9;" "UPDATE metadata SET value=2 WHERE key='schema_version'", NULL, NULL, NULL) == SQLITE_OK; return sqlite3_close(connection) == SQLITE_OK && ok; @@ -316,13 +765,15 @@ static bool create_v3_database(const char *path) { if (sqlite3_open(path, &connection) != SQLITE_OK) { return false; } - bool ok = sqlite3_exec(connection, - "CREATE TABLE image_import_tasks(task_id INTEGER PRIMARY KEY REFERENCES " - "tasks(task_id) ON DELETE CASCADE,source_path TEXT NOT NULL);" - "INSERT INTO image_import_tasks VALUES(9,'/legacy/source');" - "INSERT INTO metadata(key,value) VALUES('next_task_id',10);" - "UPDATE metadata SET value=3 WHERE key='schema_version'", - NULL, NULL, NULL) == SQLITE_OK; + bool ok = sqlite3_exec( + connection, + "CREATE TABLE image_import_tasks(task_id INTEGER PRIMARY KEY " + "REFERENCES " + "tasks(task_id) ON DELETE CASCADE,source_path TEXT NOT NULL);" + "INSERT INTO image_import_tasks VALUES(9,'/legacy/source');" + "INSERT INTO metadata(key,value) VALUES('next_task_id',10);" + "UPDATE metadata SET value=3 WHERE key='schema_version'", + NULL, NULL, NULL) == SQLITE_OK; return sqlite3_close(connection) == SQLITE_OK && ok; } @@ -336,16 +787,21 @@ static bool create_v4_database(const char *path) { } const char *sql = "CREATE TABLE scansets(scanset_id INTEGER PRIMARY KEY AUTOINCREMENT " - "CHECK(scanset_id>0),name TEXT NOT NULL,created_at INTEGER NOT NULL,updated_at INTEGER NOT " + "CHECK(scanset_id>0),name TEXT NOT NULL,created_at INTEGER NOT " + "NULL,updated_at INTEGER NOT " "NULL);" "CREATE TABLE image_assets(asset_id INTEGER PRIMARY KEY AUTOINCREMENT " - "CHECK(asset_id>0),sha256 BLOB NOT NULL UNIQUE,path TEXT NOT NULL UNIQUE,size_bytes " + "CHECK(asset_id>0),sha256 BLOB NOT NULL UNIQUE,path TEXT NOT NULL " + "UNIQUE,size_bytes " "INTEGER NOT NULL,state INTEGER NOT NULL,created_at INTEGER NOT NULL);" "CREATE TABLE images(image_id INTEGER PRIMARY KEY AUTOINCREMENT " - "CHECK(image_id>0),scanset_id INTEGER NOT NULL REFERENCES scansets(scanset_id),asset_id " - "INTEGER NOT NULL REFERENCES image_assets(asset_id),original_name TEXT NOT " + "CHECK(image_id>0),scanset_id INTEGER NOT NULL REFERENCES " + "scansets(scanset_id),asset_id " + "INTEGER NOT NULL REFERENCES image_assets(asset_id),original_name TEXT " + "NOT " "NULL,source_path TEXT NOT NULL,producer_task_id INTEGER REFERENCES " - "tasks(task_id),imported_at INTEGER NOT NULL,UNIQUE(scanset_id,asset_id));" + "tasks(task_id),imported_at INTEGER NOT " + "NULL,UNIQUE(scanset_id,asset_id));" "CREATE INDEX images_scanset_idx ON images(scanset_id,image_id);" "CREATE INDEX images_producer_idx ON images(producer_task_id,image_id);" "ALTER TABLE image_import_tasks ADD COLUMN scanset_id INTEGER REFERENCES " @@ -358,10 +814,12 @@ static bool create_v4_database(const char *path) { return sqlite3_close(connection) == SQLITE_OK && ok; } -static bool query_integer(const char *path, const char *sql, sqlite3_int64 expected) { +static bool query_integer(const char *path, const char *sql, + sqlite3_int64 expected) { sqlite3 *connection = NULL; sqlite3_stmt *statement = NULL; - if (sqlite3_open_v2(path, &connection, SQLITE_OPEN_READONLY, NULL) != SQLITE_OK || + if (sqlite3_open_v2(path, &connection, SQLITE_OPEN_READONLY, NULL) != + SQLITE_OK || sqlite3_prepare_v2(connection, sql, -1, &statement, NULL) != SQLITE_OK) { if (statement) { (void)sqlite3_finalize(statement); @@ -374,16 +832,16 @@ static bool query_integer(const char *path, const char *sql, sqlite3_int64 expec bool matches = sqlite3_step(statement) == SQLITE_ROW && sqlite3_column_int64(statement, 0) == expected && sqlite3_step(statement) == SQLITE_DONE; - return sqlite3_finalize(statement) == SQLITE_OK && sqlite3_close(connection) == SQLITE_OK && - matches; + return sqlite3_finalize(statement) == SQLITE_OK && + sqlite3_close(connection) == SQLITE_OK && matches; } static void init_track_configuration(Lardon3DProjectDbTrackSet *configuration, - unsigned char scope_byte, uint64_t gvr_count, - size_t track_count) { + 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"); + (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; @@ -399,80 +857,99 @@ static void init_track_configuration(Lardon3DProjectDbTrackSet *configuration, static bool run_test(void) { char directory[] = "/tmp/lardon3d-project-db-XXXXXX"; CHECK(mkdtemp(directory)); - char database_path[512], artifact_path[512], future_path[512], corrupt_path[512]; + char database_path[512], artifact_path[512], future_path[512], + corrupt_path[512]; char legacy_path[512], failed_migration_path[512], v2_path[512]; char failed_v3_migration_path[512], v3_path[512], failed_v4_path[512]; - char v4_path[512], failed_v5_path[512], failed_v6_path[512], failed_v7_path[512]; + char v4_path[512], failed_v5_path[512], failed_v6_path[512], + failed_v7_path[512]; char direct_v5_path[512], v8_path[512], failed_v8_path[512]; char v10_path[512], failed_v11_path[512]; - char v13_path[512], true_v14_path[512], failed_v14_path[512], failed_v15_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); - CHECK(snprintf(corrupt_path, sizeof(corrupt_path), "%s/corrupt.db", directory) > 0); - CHECK(snprintf(legacy_path, sizeof(legacy_path), "%s/legacy.db", directory) > 0); - CHECK(snprintf(failed_migration_path, sizeof(failed_migration_path), "%s/failed-migration.db", + char v13_path[512], true_v14_path[512], failed_v14_path[512], + failed_v15_path[512], true_v15_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); + CHECK(snprintf(corrupt_path, sizeof(corrupt_path), "%s/corrupt.db", + directory) > 0); + CHECK(snprintf(legacy_path, sizeof(legacy_path), "%s/legacy.db", directory) > + 0); + CHECK(snprintf(failed_migration_path, sizeof(failed_migration_path), + "%s/failed-migration.db", directory) > 0); CHECK(snprintf(v2_path, sizeof(v2_path), "%s/v2.db", directory) > 0); CHECK(snprintf(failed_v3_migration_path, sizeof(failed_v3_migration_path), "%s/failed-v3-migration.db", directory) > 0); CHECK(snprintf(v3_path, sizeof(v3_path), "%s/v3.db", directory) > 0); - CHECK(snprintf(failed_v4_path, sizeof(failed_v4_path), "%s/failed-v4-migration.db", directory) > - 0); + CHECK(snprintf(failed_v4_path, sizeof(failed_v4_path), + "%s/failed-v4-migration.db", directory) > 0); CHECK(snprintf(v4_path, sizeof(v4_path), "%s/v4.db", directory) > 0); - CHECK(snprintf(failed_v5_path, sizeof(failed_v5_path), "%s/failed-v5-migration.db", directory) > - 0); - CHECK(snprintf(failed_v6_path, sizeof(failed_v6_path), "%s/failed-v6-migration.db", directory) > - 0); - CHECK(snprintf(failed_v7_path, sizeof(failed_v7_path), "%s/failed-v7-migration.db", directory) > - 0); - CHECK(snprintf(direct_v5_path, sizeof(direct_v5_path), "%s/direct-v5.db", directory) > 0); + CHECK(snprintf(failed_v5_path, sizeof(failed_v5_path), + "%s/failed-v5-migration.db", directory) > 0); + CHECK(snprintf(failed_v6_path, sizeof(failed_v6_path), + "%s/failed-v6-migration.db", directory) > 0); + CHECK(snprintf(failed_v7_path, sizeof(failed_v7_path), + "%s/failed-v7-migration.db", directory) > 0); + CHECK(snprintf(direct_v5_path, sizeof(direct_v5_path), "%s/direct-v5.db", + directory) > 0); CHECK(snprintf(v8_path, sizeof(v8_path), "%s/v8.db", directory) > 0); - CHECK(snprintf(failed_v8_path, sizeof(failed_v8_path), "%s/failed-v8-migration.db", directory) > - 0); + CHECK(snprintf(failed_v8_path, sizeof(failed_v8_path), + "%s/failed-v8-migration.db", directory) > 0); CHECK(snprintf(v10_path, sizeof(v10_path), "%s/v10.db", directory) > 0); - CHECK(snprintf(failed_v11_path, sizeof(failed_v11_path), "%s/failed-v11.db", directory) > 0); + CHECK(snprintf(failed_v11_path, sizeof(failed_v11_path), "%s/failed-v11.db", + directory) > 0); CHECK(snprintf(v13_path, sizeof(v13_path), "%s/v13.db", directory) > 0); - CHECK(snprintf(true_v14_path, sizeof(true_v14_path), "%s/true-v14.db", directory) > 0); - CHECK(snprintf(failed_v14_path, sizeof(failed_v14_path), "%s/failed-v14.db", directory) > 0); - CHECK(snprintf(failed_v15_path, sizeof(failed_v15_path), "%s/failed-v15.db", directory) > 0); + CHECK(snprintf(true_v14_path, sizeof(true_v14_path), "%s/true-v14.db", + directory) > 0); + CHECK(snprintf(failed_v14_path, sizeof(failed_v14_path), "%s/failed-v14.db", + directory) > 0); + CHECK(snprintf(failed_v15_path, sizeof(failed_v15_path), "%s/failed-v15.db", + directory) > 0); + CHECK(snprintf(true_v15_path, sizeof(true_v15_path), "%s/true-v15.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) == 15); + CHECK(lardon3d_project_db_open(database_path, &database, error) == + LARDON3D_PROJECT_DB_OK); + CHECK(database && lardon3d_project_db_schema_version(database) == 16); bool legacy_pending = true; CHECK(lardon3d_project_db_legacy_catalog_pending(database, &legacy_pending) == LARDON3D_PROJECT_DB_OK && !legacy_pending); Lardon3DProjectDbScanSet deleted_scanset; - CHECK(lardon3d_project_db_create_scanset(database, "Deleted identity", &deleted_scanset) == + CHECK(lardon3d_project_db_create_scanset(database, "Deleted identity", + &deleted_scanset) == LARDON3D_PROJECT_DB_OK); unsigned char first_hash[LARDON3D_PROJECT_DB_SHA256_SIZE] = {1}; char first_asset_path[LARDON3D_PROJECT_DB_PATH_CAPACITY]; asset_path_for_hash(first_hash, first_asset_path); Lardon3DProjectDbImageRegisterStatus identity_status; Lardon3DProjectDbImage deleted_image; - CHECK(lardon3d_project_db_register_image(database, deleted_scanset.scanset_id, first_hash, - first_asset_path, 1, "deleted.jpg", - "/source/deleted.jpg", 0, 1, &identity_status, - &deleted_image) == LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_register_image( + database, deleted_scanset.scanset_id, first_hash, first_asset_path, + 1, "deleted.jpg", "/source/deleted.jpg", 0, 1, &identity_status, + &deleted_image) == LARDON3D_PROJECT_DB_OK); CHECK(lardon3d_project_db_test_delete_catalog_identity( - database, deleted_scanset.scanset_id, deleted_image.image_id, deleted_image.asset_id) == - LARDON3D_PROJECT_DB_OK); + database, deleted_scanset.scanset_id, deleted_image.image_id, + deleted_image.asset_id) == LARDON3D_PROJECT_DB_OK); Lardon3DProjectDbScanSet replacement_scanset; CHECK(lardon3d_project_db_create_scanset(database, "Replacement identity", - &replacement_scanset) == LARDON3D_PROJECT_DB_OK && + &replacement_scanset) == + LARDON3D_PROJECT_DB_OK && replacement_scanset.scanset_id > deleted_scanset.scanset_id); unsigned char second_hash[LARDON3D_PROJECT_DB_SHA256_SIZE] = {2}; char second_asset_path[LARDON3D_PROJECT_DB_PATH_CAPACITY]; asset_path_for_hash(second_hash, second_asset_path); Lardon3DProjectDbImage replacement_image; - CHECK(lardon3d_project_db_register_image(database, replacement_scanset.scanset_id, second_hash, - second_asset_path, 1, "replacement.jpg", - "/source/replacement.jpg", 0, 2, &identity_status, - &replacement_image) == LARDON3D_PROJECT_DB_OK && + CHECK(lardon3d_project_db_register_image( + database, replacement_scanset.scanset_id, second_hash, + second_asset_path, 1, "replacement.jpg", "/source/replacement.jpg", + 0, 2, &identity_status, + &replacement_image) == LARDON3D_PROJECT_DB_OK && replacement_image.image_id > deleted_image.image_id && replacement_image.asset_id > deleted_image.asset_id); @@ -480,93 +957,104 @@ static bool run_test(void) { char third_asset_path[LARDON3D_PROJECT_DB_PATH_CAPACITY]; asset_path_for_hash(third_hash, third_asset_path); Lardon3DProjectDbImage pair_image; - CHECK(lardon3d_project_db_register_image(database, replacement_scanset.scanset_id, third_hash, - third_asset_path, 1, "pair-a.jpg", "/source/pair-a.jpg", - 0, 3, &identity_status, &pair_image) == - LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_register_image( + database, replacement_scanset.scanset_id, third_hash, + third_asset_path, 1, "pair-a.jpg", "/source/pair-a.jpg", 0, 3, + &identity_status, &pair_image) == LARDON3D_PROJECT_DB_OK); unsigned char fourth_hash[LARDON3D_PROJECT_DB_SHA256_SIZE] = {4}; char fourth_asset_path[LARDON3D_PROJECT_DB_PATH_CAPACITY]; asset_path_for_hash(fourth_hash, fourth_asset_path); Lardon3DProjectDbImage pair_image_b; - CHECK(lardon3d_project_db_register_image(database, replacement_scanset.scanset_id, fourth_hash, - fourth_asset_path, 1, "pair-b.jpg", "/source/pair-b.jpg", - 0, 4, &identity_status, &pair_image_b) == - LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_register_image( + database, replacement_scanset.scanset_id, fourth_hash, + fourth_asset_path, 1, "pair-b.jpg", "/source/pair-b.jpg", 0, 4, + &identity_status, &pair_image_b) == LARDON3D_PROJECT_DB_OK); Lardon3DProjectDbCandidatePair pair; - CHECK(lardon3d_project_db_create_candidate_pair(database, replacement_image.image_id, - pair_image.image_id, 10, &pair) == - LARDON3D_PROJECT_DB_OK); - CHECK(pair.candidate_pair_id > 0 && pair.image_id_a == replacement_image.image_id && + CHECK(lardon3d_project_db_create_candidate_pair( + database, replacement_image.image_id, pair_image.image_id, 10, + &pair) == LARDON3D_PROJECT_DB_OK); + CHECK(pair.candidate_pair_id > 0 && + pair.image_id_a == replacement_image.image_id && pair.image_id_b == pair_image.image_id && pair.created_at == 10); Lardon3DProjectDbCandidatePair first_pair = pair; Lardon3DProjectDbCandidatePair second_pair; - CHECK(lardon3d_project_db_create_candidate_pair(database, replacement_image.image_id, - pair_image_b.image_id, 11, &second_pair) == - LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_create_candidate_pair( + database, replacement_image.image_id, pair_image_b.image_id, 11, + &second_pair) == LARDON3D_PROJECT_DB_OK); CHECK(second_pair.candidate_pair_id > first_pair.candidate_pair_id); - CHECK(lardon3d_project_db_create_candidate_pair(database, pair_image.image_id, - replacement_image.image_id, 12, &pair) == - LARDON3D_PROJECT_DB_CONSTRAINT); - CHECK(lardon3d_project_db_create_candidate_pair(database, replacement_image.image_id, - replacement_image.image_id, 13, &pair) == - LARDON3D_PROJECT_DB_CONSTRAINT); - CHECK(lardon3d_project_db_create_candidate_pair(database, replacement_image.image_id, - pair_image.image_id, -1, &pair) == - LARDON3D_PROJECT_DB_INVALID_ARGUMENT); - CHECK(lardon3d_project_db_create_candidate_pair(database, 0, pair_image.image_id, 14, &pair) == + CHECK(lardon3d_project_db_create_candidate_pair( + database, pair_image.image_id, replacement_image.image_id, 12, + &pair) == LARDON3D_PROJECT_DB_CONSTRAINT); + CHECK(lardon3d_project_db_create_candidate_pair( + database, replacement_image.image_id, replacement_image.image_id, + 13, &pair) == LARDON3D_PROJECT_DB_CONSTRAINT); + CHECK(lardon3d_project_db_create_candidate_pair( + database, replacement_image.image_id, pair_image.image_id, -1, + &pair) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT); + CHECK(lardon3d_project_db_create_candidate_pair( + database, 0, pair_image.image_id, 14, &pair) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT); Lardon3DProjectDbCandidatePair found; - CHECK(lardon3d_project_db_find_candidate_pair(database, pair_image.image_id, - replacement_image.image_id, &found) == - LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_find_candidate_pair( + database, pair_image.image_id, replacement_image.image_id, + &found) == LARDON3D_PROJECT_DB_OK); CHECK(found.candidate_pair_id == first_pair.candidate_pair_id && - found.image_id_a == replacement_image.image_id && found.image_id_b == pair_image.image_id && - found.created_at == 10); - CHECK(lardon3d_project_db_find_candidate_pair(database, replacement_image.image_id, - pair_image.image_id, &found) == - LARDON3D_PROJECT_DB_OK); + found.image_id_a == replacement_image.image_id && + found.image_id_b == pair_image.image_id && found.created_at == 10); + CHECK(lardon3d_project_db_find_candidate_pair( + database, replacement_image.image_id, pair_image.image_id, + &found) == LARDON3D_PROJECT_DB_OK); CHECK(found.candidate_pair_id == first_pair.candidate_pair_id); - CHECK(lardon3d_project_db_find_candidate_pair(database, pair_image.image_id, - pair_image_b.image_id, &found) == + CHECK(lardon3d_project_db_find_candidate_pair( + database, pair_image.image_id, pair_image_b.image_id, &found) == LARDON3D_PROJECT_DB_NOT_FOUND); Lardon3DProjectDbCandidatePair loaded; - CHECK(lardon3d_project_db_load_candidate_pair(database, first_pair.candidate_pair_id, &loaded) == + CHECK(lardon3d_project_db_load_candidate_pair( + database, first_pair.candidate_pair_id, &loaded) == LARDON3D_PROJECT_DB_OK); - CHECK(loaded.image_id_a == first_pair.image_id_a && loaded.image_id_b == first_pair.image_id_b && - loaded.created_at == 10); + CHECK(loaded.image_id_a == first_pair.image_id_a && + loaded.image_id_b == first_pair.image_id_b && loaded.created_at == 10); CHECK(lardon3d_project_db_load_candidate_pair(database, 999999, &loaded) == LARDON3D_PROJECT_DB_NOT_FOUND); - Lardon3DProjectDbCandidatePair pair_page[LARDON3D_PROJECT_DB_CANDIDATE_PAIR_PAGE_MAX]; + Lardon3DProjectDbCandidatePair + pair_page[LARDON3D_PROJECT_DB_CANDIDATE_PAIR_PAGE_MAX]; size_t pair_count = 0; - CHECK(lardon3d_project_db_list_candidate_pairs(database, 0, pair_page, 2, &pair_count) == - LARDON3D_PROJECT_DB_OK); - CHECK(pair_count == 2 && pair_page[0].candidate_pair_id == first_pair.candidate_pair_id && + CHECK(lardon3d_project_db_list_candidate_pairs( + database, 0, pair_page, 2, &pair_count) == LARDON3D_PROJECT_DB_OK); + CHECK(pair_count == 2 && + pair_page[0].candidate_pair_id == first_pair.candidate_pair_id && pair_page[1].candidate_pair_id == second_pair.candidate_pair_id); - CHECK(lardon3d_project_db_list_candidate_pairs(database, first_pair.candidate_pair_id, - pair_page, 2, &pair_count) == - LARDON3D_PROJECT_DB_OK); - CHECK(pair_count == 1 && pair_page[0].candidate_pair_id == second_pair.candidate_pair_id); - CHECK(lardon3d_project_db_list_candidate_pairs(database, second_pair.candidate_pair_id, - pair_page, 1, &pair_count) == - LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_list_candidate_pairs( + database, first_pair.candidate_pair_id, pair_page, 2, + &pair_count) == LARDON3D_PROJECT_DB_OK); + CHECK(pair_count == 1 && + pair_page[0].candidate_pair_id == second_pair.candidate_pair_id); + CHECK(lardon3d_project_db_list_candidate_pairs( + database, second_pair.candidate_pair_id, pair_page, 1, + &pair_count) == LARDON3D_PROJECT_DB_OK); CHECK(pair_count == 0); CHECK(lardon3d_project_db_list_candidate_pairs( - database, 0, pair_page, LARDON3D_PROJECT_DB_CANDIDATE_PAIR_PAGE_MAX + 1, + database, 0, pair_page, + LARDON3D_PROJECT_DB_CANDIDATE_PAIR_PAGE_MAX + 1, &pair_count) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT); Lardon3DProjectDbProject project = {.created_at = 100, .updated_at = 100}; (void)snprintf(project.stable_id, sizeof(project.stable_id), "project-0001"); (void)snprintf(project.name, sizeof(project.name), "Projet test"); - CHECK(lardon3d_project_db_set_project(database, &project) == LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_set_project(database, &project) == + LARDON3D_PROJECT_DB_OK); Lardon3DProjectDbProject loaded_project; - CHECK(lardon3d_project_db_get_project(database, &loaded_project) == LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_get_project(database, &loaded_project) == + LARDON3D_PROJECT_DB_OK); CHECK(strcmp(loaded_project.stable_id, project.stable_id) == 0); project.updated_at = 101; (void)snprintf(project.name, sizeof(project.name), "Projet renommé"); - CHECK(lardon3d_project_db_set_project(database, &project) == LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_set_project(database, &project) == + LARDON3D_PROJECT_DB_OK); Lardon3DProjectDbProject conflict = project; (void)snprintf(conflict.stable_id, sizeof(conflict.stable_id), "other"); - CHECK(lardon3d_project_db_set_project(database, &conflict) == LARDON3D_PROJECT_DB_CONSTRAINT); + CHECK(lardon3d_project_db_set_project(database, &conflict) == + LARDON3D_PROJECT_DB_CONSTRAINT); char copied_error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]; CHECK(lardon3d_project_db_last_error(database, copied_error)); CHECK(copied_error[0] != '\0'); @@ -576,45 +1064,61 @@ static bool run_test(void) { LARDON3D_PROJECT_DB_INVALID_ARGUMENT); Lardon3DTaskDurableSnapshot running = task_snapshot(1, TASK_RUNNING); - Lardon3DProjectDbCheckpoint checkpoint = { - .format_version = 1, .durability = LARDON3D_DB_CHECKPOINT_DURABLE, .updated_at = 200}; - (void)snprintf(checkpoint.path, sizeof(checkpoint.path), "%s/checkpoints/task-1.chk", directory); - CHECK(lardon3d_project_db_record_task(database, &running, "test.work", 1, &checkpoint, 200) == - LARDON3D_PROJECT_DB_OK); + Lardon3DProjectDbCheckpoint checkpoint = {.format_version = 1, + .durability = + LARDON3D_DB_CHECKPOINT_DURABLE, + .updated_at = 200}; + (void)snprintf(checkpoint.path, sizeof(checkpoint.path), + "%s/checkpoints/task-1.chk", directory); + CHECK(lardon3d_project_db_record_task(database, &running, "test.work", 1, + &checkpoint, + 200) == LARDON3D_PROJECT_DB_OK); running.progress = 30; running.sequence_count = 3; - CHECK(lardon3d_project_db_record_task(database, &running, "test.work", 1, &checkpoint, 201) == - LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_record_task(database, &running, "test.work", 1, + &checkpoint, + 201) == LARDON3D_PROJECT_DB_OK); Lardon3DProjectDbScanSet import_scanset; - CHECK(lardon3d_project_db_create_scanset(database, "Legacy import", &import_scanset) == + CHECK(lardon3d_project_db_create_scanset(database, "Legacy import", + &import_scanset) == LARDON3D_PROJECT_DB_OK); Lardon3DProjectDbTask task; - CHECK(lardon3d_project_db_load_task(database, 1, &task) == LARDON3D_PROJECT_DB_OK); - CHECK(task.recovery_state == TASK_PENDING && task.progress == 30 && task.sequence_count == 3); + CHECK(lardon3d_project_db_load_task(database, 1, &task) == + LARDON3D_PROJECT_DB_OK); + CHECK(task.recovery_state == TASK_PENDING && task.progress == 30 && + task.sequence_count == 3); CHECK(task.has_task_kind && strcmp(task.task_kind, "test.work") == 0 && task.task_kind_version == 1); - CHECK(task.has_checkpoint && strcmp(task.checkpoint.path, checkpoint.path) == 0); + CHECK(task.has_checkpoint && + strcmp(task.checkpoint.path, checkpoint.path) == 0); checkpoint.durability = LARDON3D_DB_CHECKPOINT_PUBLISHED_NOT_DURABLE; - CHECK(lardon3d_project_db_record_task(database, &running, "test.work", 1, &checkpoint, 201) == + CHECK(lardon3d_project_db_record_task(database, &running, "test.work", 1, + &checkpoint, + 201) == LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_load_task(database, 1, &task) == LARDON3D_PROJECT_DB_OK); - CHECK(lardon3d_project_db_load_task(database, 1, &task) == LARDON3D_PROJECT_DB_OK); - CHECK(task.checkpoint.durability == LARDON3D_DB_CHECKPOINT_PUBLISHED_NOT_DURABLE); - CHECK(lardon3d_project_db_record_task(database, &running, "test.other", 1, &checkpoint, 202) == - LARDON3D_PROJECT_DB_CONSTRAINT); - CHECK(lardon3d_project_db_record_task(database, &running, "Test.invalid", 1, &checkpoint, 202) == + CHECK(task.checkpoint.durability == + LARDON3D_DB_CHECKPOINT_PUBLISHED_NOT_DURABLE); + CHECK(lardon3d_project_db_record_task(database, &running, "test.other", 1, + &checkpoint, + 202) == LARDON3D_PROJECT_DB_CONSTRAINT); + CHECK(lardon3d_project_db_record_task(database, &running, "Test.invalid", 1, + &checkpoint, 202) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT); - CHECK(lardon3d_project_db_record_task(database, &running, "test.work", 0, &checkpoint, 202) == + CHECK(lardon3d_project_db_record_task(database, &running, "test.work", 0, + &checkpoint, 202) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT); CHECK(lardon3d_project_db_record_image_import_task( - database, &running, "import.images", 1, &checkpoint, "/tmp/source-a", - import_scanset.scanset_id, 202) == LARDON3D_PROJECT_DB_CONSTRAINT); + database, &running, "import.images", 1, &checkpoint, + "/tmp/source-a", import_scanset.scanset_id, + 202) == LARDON3D_PROJECT_DB_CONSTRAINT); Lardon3DTaskDurableSnapshot completed = task_snapshot(2, TASK_COMPLETED); - CHECK(lardon3d_project_db_record_task(database, &completed, "test.work", 1, NULL, 202) == - LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_record_task(database, &completed, "test.work", 1, + NULL, 202) == LARDON3D_PROJECT_DB_OK); Lardon3DTaskDurableSnapshot no_checkpoint = task_snapshot(4, TASK_PENDING); - CHECK(lardon3d_project_db_record_task(database, &no_checkpoint, NULL, 0, NULL, 202) == - LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_record_task(database, &no_checkpoint, NULL, 0, NULL, + 202) == LARDON3D_PROJECT_DB_OK); Lardon3DProjectDbTask page[1]; size_t count = 0; CHECK(lardon3d_project_db_list_recoverable(database, 0, page, 1, &count) == @@ -623,62 +1127,76 @@ static bool run_test(void) { CHECK(lardon3d_project_db_list_recoverable(database, 1, page, 1, &count) == LARDON3D_PROJECT_DB_OK && count == 0); - CHECK(lardon3d_project_db_list_recoverable(database, 0, page, - LARDON3D_PROJECT_DB_RECOVERY_PAGE_MAX + 1, - &count) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT); + CHECK(lardon3d_project_db_list_recoverable( + database, 0, page, LARDON3D_PROJECT_DB_RECOVERY_PAGE_MAX + 1, + &count) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT); Lardon3DTaskDurableSnapshot image_import = task_snapshot(5, TASK_PENDING); CHECK(lardon3d_project_db_record_image_import_task( - database, &image_import, "import.images", 1, &checkpoint, "/tmp/source-a", - import_scanset.scanset_id, 202) == LARDON3D_PROJECT_DB_OK); + database, &image_import, "import.images", 1, &checkpoint, + "/tmp/source-a", import_scanset.scanset_id, + 202) == LARDON3D_PROJECT_DB_OK); Lardon3DProjectDbImageImport import_parameters; - CHECK(lardon3d_project_db_load_image_import(database, 5, &import_parameters) == - LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_load_image_import( + database, 5, &import_parameters) == LARDON3D_PROJECT_DB_OK); CHECK(import_parameters.task_id == 5 && strcmp(import_parameters.source_path, "/tmp/source-a") == 0 && import_parameters.scanset_id == import_scanset.scanset_id); CHECK(lardon3d_project_db_record_image_import_task( - database, &image_import, "import.images", 1, &checkpoint, "/tmp/source-b", - import_scanset.scanset_id, 203) == LARDON3D_PROJECT_DB_CONSTRAINT); + database, &image_import, "import.images", 1, &checkpoint, + "/tmp/source-b", import_scanset.scanset_id, + 203) == LARDON3D_PROJECT_DB_CONSTRAINT); Lardon3DTaskDurableSnapshot rollback_task = task_snapshot(3, TASK_PENDING); CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_CHECKPOINT", "1", 1) == 0); - CHECK(lardon3d_project_db_record_task(database, &rollback_task, "test.work", 1, &checkpoint, + CHECK(lardon3d_project_db_record_task(database, &rollback_task, "test.work", + 1, &checkpoint, 203) == LARDON3D_PROJECT_DB_IO_ERROR); CHECK(unsetenv("LARDON3D_TEST_PROJECT_DB_FAIL_CHECKPOINT") == 0); - CHECK(lardon3d_project_db_load_task(database, 3, &task) == LARDON3D_PROJECT_DB_NOT_FOUND); - CHECK(lardon3d_project_db_test_orphan_checkpoint(database) == LARDON3D_PROJECT_DB_CONSTRAINT); + CHECK(lardon3d_project_db_load_task(database, 3, &task) == + LARDON3D_PROJECT_DB_NOT_FOUND); + CHECK(lardon3d_project_db_test_orphan_checkpoint(database) == + LARDON3D_PROJECT_DB_CONSTRAINT); Lardon3DProjectDbArtifact artifact = {.state = LARDON3D_DB_ARTIFACT_STAGED, .has_producer_task = true, .producer_task_id = 1, .created_at = 300, .updated_at = 300}; - (void)snprintf(artifact.artifact_id, sizeof(artifact.artifact_id), "artifact-1"); + (void)snprintf(artifact.artifact_id, sizeof(artifact.artifact_id), + "artifact-1"); (void)snprintf(artifact.kind, sizeof(artifact.kind), "generic-test"); (void)snprintf(artifact.path, sizeof(artifact.path), "%s", artifact_path); - CHECK(lardon3d_project_db_create_artifact(database, &artifact) == LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_create_artifact(database, &artifact) == + LARDON3D_PROJECT_DB_OK); Lardon3DProjectDbArtifact loaded_artifact; - CHECK(lardon3d_project_db_load_artifact(database, artifact.artifact_id, &loaded_artifact) == + CHECK(lardon3d_project_db_load_artifact(database, artifact.artifact_id, + &loaded_artifact) == LARDON3D_PROJECT_DB_OK); CHECK(loaded_artifact.state == LARDON3D_DB_ARTIFACT_STAGED); - CHECK(lardon3d_project_db_mark_artifact_ready(database, artifact.artifact_id, 301) == + CHECK(lardon3d_project_db_mark_artifact_ready(database, artifact.artifact_id, + 301) == LARDON3D_PROJECT_DB_IO_ERROR); int descriptor = open(artifact_path, O_WRONLY | O_CREAT | O_TRUNC, 0600); CHECK(descriptor >= 0); const char payload[] = "validated"; - CHECK(write(descriptor, payload, sizeof(payload)) == (ssize_t)sizeof(payload)); + CHECK(write(descriptor, payload, sizeof(payload)) == + (ssize_t)sizeof(payload)); CHECK(close(descriptor) == 0); - CHECK(lardon3d_project_db_mark_artifact_ready(database, artifact.artifact_id, 301) == - LARDON3D_PROJECT_DB_OK); - CHECK(lardon3d_project_db_load_artifact(database, artifact.artifact_id, &loaded_artifact) == + CHECK(lardon3d_project_db_mark_artifact_ready(database, artifact.artifact_id, + 301) == LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_load_artifact(database, artifact.artifact_id, + &loaded_artifact) == LARDON3D_PROJECT_DB_OK); CHECK(loaded_artifact.state == LARDON3D_DB_ARTIFACT_READY && loaded_artifact.size_bytes == sizeof(payload)); - CHECK(lardon3d_project_db_create_artifact(database, &artifact) == LARDON3D_PROJECT_DB_CONSTRAINT); + CHECK(lardon3d_project_db_create_artifact(database, &artifact) == + LARDON3D_PROJECT_DB_CONSTRAINT); artifact.producer_task_id = 999; - (void)snprintf(artifact.artifact_id, sizeof(artifact.artifact_id), "orphan-artifact"); - CHECK(lardon3d_project_db_create_artifact(database, &artifact) == LARDON3D_PROJECT_DB_CONSTRAINT); + (void)snprintf(artifact.artifact_id, sizeof(artifact.artifact_id), + "orphan-artifact"); + CHECK(lardon3d_project_db_create_artifact(database, &artifact) == + LARDON3D_PROJECT_DB_CONSTRAINT); artifact = loaded_artifact; artifact.state = LARDON3D_DB_ARTIFACT_STAGED; artifact.size_bytes = 0; @@ -690,14 +1208,20 @@ static bool run_test(void) { pthread_t threads[2]; CHECK(pthread_create(&threads[0], NULL, read_thread, &contexts[0]) == 0); CHECK(pthread_create(&threads[1], NULL, read_thread, &contexts[1]) == 0); - CHECK(pthread_join(threads[0], NULL) == 0 && pthread_join(threads[1], NULL) == 0); + CHECK(pthread_join(threads[0], NULL) == 0 && + pthread_join(threads[1], NULL) == 0); CHECK(contexts[0].success && contexts[1].success); - IdThreadContext id_contexts[2] = {{.database = database}, {.database = database}}; - CHECK(pthread_create(&threads[0], NULL, allocate_id_thread, &id_contexts[0]) == 0); - CHECK(pthread_create(&threads[1], NULL, allocate_id_thread, &id_contexts[1]) == 0); - CHECK(pthread_join(threads[0], NULL) == 0 && pthread_join(threads[1], NULL) == 0); + IdThreadContext id_contexts[2] = {{.database = database}, + {.database = database}}; + CHECK(pthread_create(&threads[0], NULL, allocate_id_thread, + &id_contexts[0]) == 0); + CHECK(pthread_create(&threads[1], NULL, allocate_id_thread, + &id_contexts[1]) == 0); + CHECK(pthread_join(threads[0], NULL) == 0 && + pthread_join(threads[1], NULL) == 0); CHECK(id_contexts[0].success && id_contexts[1].success && - id_contexts[0].id != id_contexts[1].id && id_contexts[0].id > 5 && id_contexts[1].id > 5); + id_contexts[0].id != id_contexts[1].id && id_contexts[0].id > 5 && + id_contexts[1].id > 5); char too_long[LARDON3D_PROJECT_DB_PATH_CAPACITY + 1]; memset(too_long, 'x', sizeof(too_long)); @@ -706,15 +1230,21 @@ 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'", 15)); - 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); - CHECK(lardon3d_project_db_load_artifact(database, "artifact-1", &loaded_artifact) == + CHECK(query_integer(database_path, + "SELECT value FROM metadata WHERE key='schema_version'", + 16)); + 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_find_candidate_pair(database, replacement_image.image_id, - pair_image.image_id, &found) == + CHECK(lardon3d_project_db_load_task(database, 1, &task) == LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_load_artifact(database, "artifact-1", + &loaded_artifact) == + LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_find_candidate_pair( + database, replacement_image.image_id, pair_image.image_id, + &found) == LARDON3D_PROJECT_DB_OK); CHECK(found.candidate_pair_id == first_pair.candidate_pair_id); lardon3d_project_db_close(database); @@ -724,21 +1254,28 @@ static bool run_test(void) { descriptor = open(corrupt_path, O_WRONLY | O_CREAT | O_TRUNC, 0600); CHECK(descriptor >= 0); CHECK(write(descriptor, "not sqlite", 10) == 10 && close(descriptor) == 0); - CHECK(lardon3d_project_db_open(corrupt_path, &database, error) == LARDON3D_PROJECT_DB_CORRUPT); + CHECK(lardon3d_project_db_open(corrupt_path, &database, error) == + LARDON3D_PROJECT_DB_CORRUPT); 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) == 15); - CHECK(lardon3d_project_db_get_project(database, &loaded_project) == LARDON3D_PROJECT_DB_OK && + CHECK(lardon3d_project_db_open(legacy_path, &database, error) == + LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_schema_version(database) == 16); + 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); + CHECK(lardon3d_project_db_load_task(database, 9, &task) == + LARDON3D_PROJECT_DB_OK); CHECK(!task.has_task_kind && task.has_checkpoint && strcmp(task.checkpoint.path, "legacy.chk") == 0); - CHECK(lardon3d_project_db_load_artifact(database, "legacy-artifact", &loaded_artifact) == + CHECK(lardon3d_project_db_load_artifact(database, "legacy-artifact", + &loaded_artifact) == LARDON3D_PROJECT_DB_OK); lardon3d_project_db_close(database); database = NULL; - CHECK(query_integer(legacy_path, "SELECT value FROM metadata WHERE key='schema_version'", 15)); + CHECK(query_integer(legacy_path, + "SELECT value FROM metadata WHERE key='schema_version'", + 16)); CHECK(create_v1_database(failed_migration_path)); CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V2", "1", 1) == 0); @@ -746,24 +1283,30 @@ static bool run_test(void) { LARDON3D_PROJECT_DB_IO_ERROR); CHECK(unsetenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V2") == 0); CHECK(query_integer(failed_migration_path, - "SELECT value FROM metadata WHERE key='schema_version'", 1)); + "SELECT value FROM metadata WHERE key='schema_version'", + 1)); CHECK(lardon3d_project_db_open(failed_migration_path, &database, error) == LARDON3D_PROJECT_DB_OK); - CHECK(lardon3d_project_db_load_task(database, 9, &task) == LARDON3D_PROJECT_DB_OK && + CHECK(lardon3d_project_db_load_task(database, 9, &task) == + LARDON3D_PROJECT_DB_OK && !task.has_task_kind); lardon3d_project_db_close(database); database = NULL; CHECK(create_v2_database(v2_path)); - CHECK(lardon3d_project_db_open(v2_path, &database, error) == LARDON3D_PROJECT_DB_OK); - CHECK(lardon3d_project_db_load_task(database, 9, &task) == LARDON3D_PROJECT_DB_OK && + CHECK(lardon3d_project_db_open(v2_path, &database, error) == + LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_load_task(database, 9, &task) == + LARDON3D_PROJECT_DB_OK && task.has_task_kind && strcmp(task.task_kind, "test.work") == 0); CHECK(task.has_checkpoint && strcmp(task.checkpoint.path, "legacy.chk") == 0); - CHECK(lardon3d_project_db_load_artifact(database, "legacy-artifact", &loaded_artifact) == + CHECK(lardon3d_project_db_load_artifact(database, "legacy-artifact", + &loaded_artifact) == LARDON3D_PROJECT_DB_OK); lardon3d_project_db_close(database); database = NULL; - CHECK(query_integer(v2_path, "SELECT value FROM metadata WHERE key='schema_version'", 15)); + CHECK(query_integer( + v2_path, "SELECT value FROM metadata WHERE key='schema_version'", 16)); CHECK(create_v2_database(failed_v3_migration_path)); CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V3", "1", 1) == 0); @@ -771,133 +1314,178 @@ static bool run_test(void) { LARDON3D_PROJECT_DB_IO_ERROR); CHECK(unsetenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V3") == 0); CHECK(query_integer(failed_v3_migration_path, - "SELECT value FROM metadata WHERE key='schema_version'", 2)); - CHECK(query_integer( - failed_v3_migration_path, - "SELECT count(*) FROM sqlite_master WHERE type='table' AND name='image_import_tasks'", 0)); + "SELECT value FROM metadata WHERE key='schema_version'", + 2)); + CHECK(query_integer(failed_v3_migration_path, + "SELECT count(*) FROM sqlite_master WHERE type='table' " + "AND name='image_import_tasks'", + 0)); CHECK(create_v3_database(v3_path)); - CHECK(lardon3d_project_db_open(v3_path, &database, error) == LARDON3D_PROJECT_DB_OK); - CHECK(lardon3d_project_db_load_image_import(database, 9, &import_parameters) == + CHECK(lardon3d_project_db_open(v3_path, &database, error) == LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_load_image_import( + database, 9, &import_parameters) == LARDON3D_PROJECT_DB_OK); CHECK(strcmp(import_parameters.source_path, "/legacy/source") == 0 && import_parameters.scanset_id > 0); CHECK(lardon3d_project_db_legacy_catalog_pending(database, &legacy_pending) == LARDON3D_PROJECT_DB_OK && legacy_pending); - CHECK(lardon3d_project_db_load_task(database, 9, &task) == LARDON3D_PROJECT_DB_OK && + CHECK(lardon3d_project_db_load_task(database, 9, &task) == + LARDON3D_PROJECT_DB_OK && task.has_task_kind); - CHECK(lardon3d_project_db_load_artifact(database, "legacy-artifact", &loaded_artifact) == + CHECK(lardon3d_project_db_load_artifact(database, "legacy-artifact", + &loaded_artifact) == LARDON3D_PROJECT_DB_OK); lardon3d_project_db_close(database); database = NULL; - CHECK(query_integer(v3_path, "SELECT value FROM metadata WHERE key='schema_version'", 15)); + CHECK(query_integer( + v3_path, "SELECT value FROM metadata WHERE key='schema_version'", 16)); CHECK(create_v3_database(failed_v4_path)); CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V4", "1", 1) == 0); - CHECK(lardon3d_project_db_open(failed_v4_path, &database, error) == LARDON3D_PROJECT_DB_IO_ERROR); + CHECK(lardon3d_project_db_open(failed_v4_path, &database, error) == + LARDON3D_PROJECT_DB_IO_ERROR); CHECK(unsetenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V4") == 0); - CHECK(query_integer(failed_v4_path, "SELECT value FROM metadata WHERE key='schema_version'", 3)); CHECK(query_integer(failed_v4_path, - "SELECT count(*) FROM sqlite_master WHERE type='table' AND name='scansets'", + "SELECT value FROM metadata WHERE key='schema_version'", + 3)); + CHECK(query_integer(failed_v4_path, + "SELECT count(*) FROM sqlite_master WHERE type='table' " + "AND name='scansets'", 0)); CHECK(create_v4_database(v4_path)); - Lardon3DProjectDbResult v4_result = lardon3d_project_db_open(v4_path, &database, error); + Lardon3DProjectDbResult v4_result = + lardon3d_project_db_open(v4_path, &database, error); if (v4_result != LARDON3D_PROJECT_DB_OK) { 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) == 15); - CHECK(lardon3d_project_db_load_task(database, 9, &task) == LARDON3D_PROJECT_DB_OK); - CHECK(lardon3d_project_db_load_artifact(database, "legacy-artifact", &loaded_artifact) == + CHECK(lardon3d_project_db_schema_version(database) == 16); + 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); lardon3d_project_db_close(database); database = NULL; CHECK(create_v4_database(failed_v5_path)); CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V5", "1", 1) == 0); - CHECK(lardon3d_project_db_open(failed_v5_path, &database, error) == LARDON3D_PROJECT_DB_IO_ERROR); + CHECK(lardon3d_project_db_open(failed_v5_path, &database, error) == + LARDON3D_PROJECT_DB_IO_ERROR); CHECK(unsetenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V5") == 0); - CHECK(query_integer(failed_v5_path, "SELECT value FROM metadata WHERE key='schema_version'", 4)); - CHECK(query_integer( - failed_v5_path, - "SELECT count(*) FROM sqlite_master WHERE type='table' AND name='feature_sets'", 0)); + CHECK(query_integer(failed_v5_path, + "SELECT value FROM metadata WHERE key='schema_version'", + 4)); + CHECK(query_integer(failed_v5_path, + "SELECT count(*) FROM sqlite_master WHERE type='table' " + "AND name='feature_sets'", + 0)); CHECK(create_v4_database(failed_v6_path)); CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V6", "1", 1) == 0); - CHECK(lardon3d_project_db_open(failed_v6_path, &database, error) == LARDON3D_PROJECT_DB_IO_ERROR); + CHECK(lardon3d_project_db_open(failed_v6_path, &database, error) == + LARDON3D_PROJECT_DB_IO_ERROR); CHECK(unsetenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V6") == 0); - CHECK(query_integer(failed_v6_path, "SELECT value FROM metadata WHERE key='schema_version'", 4)); - CHECK(query_integer( - failed_v6_path, - "SELECT count(*) FROM sqlite_master WHERE type='table' AND name='visual_indexes'", 0)); + CHECK(query_integer(failed_v6_path, + "SELECT value FROM metadata WHERE key='schema_version'", + 4)); + CHECK(query_integer(failed_v6_path, + "SELECT count(*) FROM sqlite_master WHERE type='table' " + "AND name='visual_indexes'", + 0)); CHECK(create_v6_database(failed_v7_path)); CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V7", "1", 1) == 0); - CHECK(lardon3d_project_db_open(failed_v7_path, &database, error) == LARDON3D_PROJECT_DB_IO_ERROR); + CHECK(lardon3d_project_db_open(failed_v7_path, &database, error) == + LARDON3D_PROJECT_DB_IO_ERROR); CHECK(unsetenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V7") == 0); - CHECK(query_integer(failed_v7_path, "SELECT value FROM metadata WHERE key='schema_version'", 6)); CHECK(query_integer(failed_v7_path, - "SELECT count(*) FROM sqlite_master WHERE type='table' AND " - "name='sift_extract_tasks'", 0)); - Lardon3DProjectDbResult retry_v7 = lardon3d_project_db_open( - failed_v7_path, &database, error); + "SELECT value FROM metadata WHERE key='schema_version'", + 6)); + CHECK( + query_integer(failed_v7_path, + "SELECT count(*) FROM sqlite_master WHERE type='table' AND " + "name='sift_extract_tasks'", + 0)); + Lardon3DProjectDbResult retry_v7 = + lardon3d_project_db_open(failed_v7_path, &database, error); if (retry_v7 != LARDON3D_PROJECT_DB_OK) { - fprintf(stderr, "Nouvelle tentative migration v7 (%d): %s\n", (int)retry_v7, error); + fprintf(stderr, "Nouvelle tentative migration v7 (%d): %s\n", (int)retry_v7, + error); } CHECK(retry_v7 == LARDON3D_PROJECT_DB_OK && - lardon3d_project_db_schema_version(database) == 15); + lardon3d_project_db_schema_version(database) == 16); 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) == 15); + 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) == 16); 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) == 15); + 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) == 16); lardon3d_project_db_close(database); database = NULL; - CHECK(query_integer(v8_path, "SELECT value FROM metadata WHERE key='schema_version'", 15)); + CHECK(query_integer( + v8_path, "SELECT value FROM metadata WHERE key='schema_version'", 16)); CHECK(create_v7_database(failed_v8_path)); CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V8", "1", 1) == 0); - CHECK(lardon3d_project_db_open(failed_v8_path, &database, error) == LARDON3D_PROJECT_DB_IO_ERROR); + CHECK(lardon3d_project_db_open(failed_v8_path, &database, error) == + LARDON3D_PROJECT_DB_IO_ERROR); CHECK(unsetenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V8") == 0); - CHECK(query_integer(failed_v8_path, "SELECT value FROM metadata WHERE key='schema_version'", 7)); CHECK(query_integer(failed_v8_path, - "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) == 15); + "SELECT value FROM metadata WHERE key='schema_version'", + 7)); + CHECK( + query_integer(failed_v8_path, + "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) == 16); lardon3d_project_db_close(database); database = NULL; CHECK(create_v10_database(v10_path)); - CHECK(query_integer(v10_path, "SELECT value FROM metadata WHERE key='schema_version'", 10)); - CHECK(query_integer(v10_path, - "SELECT count(*) FROM sqlite_master WHERE type='table' AND " - "name='match_results'", - 1)); - CHECK(query_integer(v10_path, - "SELECT count(*) FROM sqlite_master WHERE type='table' AND " - "name='matcher_tasks'", - 0)); - CHECK(lardon3d_project_db_open(v10_path, &database, error) == LARDON3D_PROJECT_DB_OK && - lardon3d_project_db_schema_version(database) == 15); + CHECK(query_integer( + v10_path, "SELECT value FROM metadata WHERE key='schema_version'", 10)); + CHECK( + query_integer(v10_path, + "SELECT count(*) FROM sqlite_master WHERE type='table' AND " + "name='match_results'", + 1)); + CHECK( + query_integer(v10_path, + "SELECT count(*) FROM sqlite_master WHERE type='table' AND " + "name='matcher_tasks'", + 0)); + CHECK(lardon3d_project_db_open(v10_path, &database, error) == + LARDON3D_PROJECT_DB_OK && + lardon3d_project_db_schema_version(database) == 16); lardon3d_project_db_close(database); database = NULL; - CHECK(query_integer(v10_path, "SELECT value FROM metadata WHERE key='schema_version'", 15)); - CHECK(query_integer(v10_path, - "SELECT count(*) FROM sqlite_master WHERE type='table' AND " - "name='matcher_tasks'", - 1)); + CHECK(query_integer( + v10_path, "SELECT value FROM metadata WHERE key='schema_version'", 16)); + CHECK( + query_integer(v10_path, + "SELECT count(*) FROM sqlite_master WHERE type='table' AND " + "name='matcher_tasks'", + 1)); CHECK(create_v10_database(failed_v11_path)); CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V11", "1", 1) == 0); @@ -905,55 +1493,246 @@ static bool run_test(void) { LARDON3D_PROJECT_DB_IO_ERROR); CHECK(unsetenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V11") == 0); CHECK(query_integer(failed_v11_path, - "SELECT value FROM metadata WHERE key='schema_version'", 10)); - CHECK(query_integer(failed_v11_path, - "SELECT count(*) FROM sqlite_master WHERE type='table' AND " - "name='matcher_tasks'", - 0)); - CHECK(query_integer(failed_v11_path, - "SELECT count(*) FROM sqlite_master WHERE type='table' AND " - "name='match_results'", - 1)); + "SELECT value FROM metadata WHERE key='schema_version'", + 10)); + CHECK( + query_integer(failed_v11_path, + "SELECT count(*) FROM sqlite_master WHERE type='table' AND " + "name='matcher_tasks'", + 0)); + CHECK( + query_integer(failed_v11_path, + "SELECT count(*) FROM sqlite_master WHERE type='table' AND " + "name='match_results'", + 1)); CHECK(lardon3d_project_db_open(failed_v11_path, &database, error) == LARDON3D_PROJECT_DB_OK); - CHECK(lardon3d_project_db_schema_version(database) == 15); + CHECK(lardon3d_project_db_schema_version(database) == 16); 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) == 15); + 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) == 16); lardon3d_project_db_close(database); database = NULL; - CHECK(query_integer(v13_path, - "SELECT value FROM metadata WHERE key='schema_version'", 15)); - CHECK(query_integer(v13_path, - "SELECT count(*) FROM sqlite_master WHERE type='table' AND " - "name='track_sets'", 1)); + CHECK(query_integer( + v13_path, "SELECT value FROM metadata WHERE key='schema_version'", 16)); + CHECK( + query_integer(v13_path, + "SELECT count(*) FROM sqlite_master WHERE type='table' AND " + "name='track_sets'", + 1)); CHECK(create_v14_database(true_v14_path)); CHECK(query_integer(true_v14_path, - "SELECT value FROM metadata WHERE key='schema_version'", 14)); - CHECK(query_integer(true_v14_path, - "SELECT count(*) FROM sqlite_master WHERE type='table' AND " - "name='track_builder_tasks'", 0)); + "SELECT value FROM metadata WHERE key='schema_version'", + 14)); + CHECK( + query_integer(true_v14_path, + "SELECT count(*) FROM sqlite_master WHERE type='table' AND " + "name='track_builder_tasks'", + 0)); Lardon3DProjectDbResult true_v14_result = lardon3d_project_db_open(true_v14_path, &database, error); if (true_v14_result != LARDON3D_PROJECT_DB_OK) { - fprintf(stderr, "true v14 upgrade: %d %s path=%s\n", true_v14_result, error, true_v14_path); + fprintf(stderr, "true v14 upgrade: %d %s path=%s\n", true_v14_result, error, + true_v14_path); } CHECK(true_v14_result == LARDON3D_PROJECT_DB_OK && - lardon3d_project_db_schema_version(database) == 15); + lardon3d_project_db_schema_version(database) == 16); + lardon3d_project_db_close(database); + database = NULL; + CHECK( + query_integer(true_v14_path, + "SELECT count(*) FROM sqlite_master WHERE type='table' AND " + "name='track_builder_tasks'", + 1)); + + CHECK(create_true_v15_database(true_v15_path)); + CHECK(query_integer(true_v15_path, + "SELECT value FROM metadata WHERE key='schema_version'", + 15)); + const char *v16_tables[] = { + "sparse_calibrations", "sparse_calibration_scopes", + "sparse_calibration_scope_images", "sparse_reconstructions", + "sparse_reconstruction_components", "sparse_registered_images", + "sparse_landmarks", "sparse_landmark_observations"}; + const char *v16_indexes[] = { + "sparse_scope_images_calibration_idx", + "sparse_scope_images_calibration_lookup_idx", + "sparse_reconstructions_scope_idx", "sparse_components_page_idx", + "sparse_registered_page_idx", "sparse_registered_image_lookup_idx", + "sparse_landmarks_page_idx", "sparse_landmark_track_lookup_idx", + "sparse_landmark_observations_page_idx"}; + for (size_t index = 0; index < sizeof(v16_tables) / sizeof(v16_tables[0]); + ++index) { + char schema_query[256]; + (void)snprintf(schema_query, sizeof(schema_query), + "SELECT count(*) FROM sqlite_master WHERE type='table' " + "AND name='%s'", + v16_tables[index]); + CHECK(query_integer(true_v15_path, schema_query, 0)); + } + for (size_t index = 0; + index < sizeof(v16_indexes) / sizeof(v16_indexes[0]); ++index) { + char schema_query[256]; + (void)snprintf(schema_query, sizeof(schema_query), + "SELECT count(*) FROM sqlite_master WHERE type='index' " + "AND name='%s'", + v16_indexes[index]); + CHECK(query_integer(true_v15_path, schema_query, 0)); + } + CHECK(query_integer(true_v15_path, + "SELECT count(*) FROM sqlite_master WHERE type='table' " + "AND name='track_builder_tasks'", + 1)); + CHECK(execute_test_sql( + true_v15_path, + "PRAGMA foreign_keys=ON;" + "INSERT INTO project(singleton,stable_id,name,created_at,updated_at) " + "VALUES(1,'legacy-project','Legacy',1,1);" + "INSERT INTO tasks(task_id,name,saved_state,recovery_state,progress," + "sequence_count,started_sec,started_nsec,finished_sec,finished_nsec," + "updated_at,task_kind,task_kind_version) VALUES(9,'Legacy task',1,0," + "100,1,10,20,30,40,2,'track_builder',1);" + "INSERT INTO checkpoints(task_id,path,format_version,durability," + "updated_at) VALUES(9,'legacy.chk',1,0,2);" + "INSERT INTO artifacts(artifact_id,kind,path,state,size_bytes," + "producer_task_id,created_at,updated_at) VALUES('legacy-artifact'," + "'legacy','legacy.bin',0,0,9,2,2);" + "INSERT INTO scansets(scanset_id,name,created_at,updated_at) " + "VALUES(1,'legacy-images',1,1);" + "INSERT INTO image_assets(asset_id,sha256,path,size_bytes,state," + "created_at) VALUES(1,zeroblob(32),'legacy/image-a',1,1,1);" + "INSERT INTO image_assets(asset_id,sha256,path,size_bytes,state," + "created_at) VALUES(2,randomblob(32),'legacy/image-b',1,1,1);" + "INSERT INTO images(image_id,scanset_id,asset_id,original_name," + "source_path,producer_task_id,imported_at) VALUES(1,1,1,'a.jpg'," + "'legacy/a.jpg',9,1);" + "INSERT INTO images(image_id,scanset_id,asset_id,original_name," + "source_path,producer_task_id,imported_at) VALUES(2,1,2,'b.jpg'," + "'legacy/b.jpg',9,1);" + "INSERT INTO feature_assets(feature_asset_id,sha256,path,size_bytes," + "durability,created_at) VALUES(1,zeroblob(32),'legacy/feature-a'," + "1,0,1);" + "INSERT INTO feature_assets(feature_asset_id,sha256,path,size_bytes," + "durability,created_at) VALUES(2,randomblob(32),'legacy/feature-b'," + "1,0,1);" + "INSERT INTO feature_sets(feature_set_id,image_id,feature_asset_id," + "extractor_kind,extractor_version,parameter_fingerprint," + "source_image_sha256,feature_count,descriptor_type,descriptor_dimension," + "producer_task_id,created_at) VALUES(1,1,1,'orb',1,zeroblob(32)," + "zeroblob(32),2,1,32,9,1);" + "INSERT INTO feature_sets(feature_set_id,image_id,feature_asset_id," + "extractor_kind,extractor_version,parameter_fingerprint," + "source_image_sha256,feature_count,descriptor_type,descriptor_dimension," + "producer_task_id,created_at) VALUES(2,2,2,'orb',1,randomblob(32)," + "randomblob(32),2,1,32,9,1);" + "INSERT INTO track_sets(track_set_id,builder_kind,builder_version," + "parameter_fingerprint,verifier_kind,verifier_version," + "verifier_fingerprint,input_scope_hash,gvr_count,track_count," + "created_at) VALUES(1,'track_builder',1,zeroblob(32),1,1," + "zeroblob(32),zeroblob(32),1,1,1);" + "INSERT INTO tracks(track_id,track_set_id,observation_count) " + "VALUES(1,1,2);" + "INSERT INTO track_observations(track_set_id,track_id,feature_set_id," + "feature_index,position_in_track) VALUES(1,1,1,0,0);" + "INSERT INTO track_observations(track_set_id,track_id,feature_set_id," + "feature_index,position_in_track) VALUES(1,1,2,0,1);" + "INSERT INTO track_builder_tasks(task_id,builder_kind," + "builder_version,builder_fingerprint,verifier_kind," + "verifier_version,verifier_fingerprint,input_scope_hash,gvr_count," + "scope_path,scope_size_bytes,scope_sha256,scope_format_version) " + "VALUES(9,'track_builder',1,zeroblob(32),1,1,zeroblob(32)," + "zeroblob(32),1,'.lardon3d/checkpoints/legacy.scope',128," + "zeroblob(32),1)")); + CHECK(query_integer(true_v15_path, + "SELECT count(*) FROM track_builder_tasks WHERE task_id=9", + 1)); + Lardon3DProjectDbResult true_v15_result = + lardon3d_project_db_open(true_v15_path, &database, error); + if (true_v15_result != LARDON3D_PROJECT_DB_OK) + fprintf(stderr, "true v15 upgrade: %d %s\n", true_v15_result, error); + CHECK(true_v15_result == LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_schema_version(database) == 16); + Lardon3DProjectDbProject migrated_project; + CHECK(lardon3d_project_db_get_project(database, &migrated_project) == + LARDON3D_PROJECT_DB_OK && + strcmp(migrated_project.stable_id, "legacy-project") == 0); + Lardon3DProjectDbTask migrated_task; + CHECK(lardon3d_project_db_load_task(database, 9, &migrated_task) == + LARDON3D_PROJECT_DB_OK && migrated_task.has_checkpoint && + strcmp(migrated_task.checkpoint.path, "legacy.chk") == 0); + Lardon3DProjectDbTrackBuilderTask migrated_builder; + CHECK(lardon3d_project_db_load_track_builder_task(database, 9, + &migrated_builder) == + LARDON3D_PROJECT_DB_OK); + CHECK(migrated_builder.gvr_count == 1 && + strcmp(migrated_builder.scope_path, + ".lardon3d/checkpoints/legacy.scope") == 0); + CHECK(query_integer(true_v15_path, "SELECT count(*) FROM scansets", 1)); + CHECK(query_integer(true_v15_path, "SELECT count(*) FROM images", 2)); + CHECK(query_integer(true_v15_path, "SELECT count(*) FROM feature_sets", 2)); + CHECK(query_integer(true_v15_path, "SELECT count(*) FROM track_sets", 1)); + CHECK(query_integer(true_v15_path, "SELECT count(*) FROM tracks", 1)); + CHECK(query_integer(true_v15_path, "SELECT count(*) FROM track_observations", 2)); + CHECK(query_integer( + true_v15_path, + "SELECT count(*) FROM track_observations AS o JOIN tracks AS t " + "ON t.track_id=o.track_id WHERE o.track_set_id=t.track_set_id AND " + "t.track_id=1", + 2)); + lardon3d_project_db_close(database); + database = NULL; + CHECK(lardon3d_project_db_open(database_path, &database, error) == + LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_schema_version(database) == 16); + lardon3d_project_db_close(database); + database = NULL; + CHECK(schema_compare(database_path, true_v15_path, true)); + CHECK(pragma_is_clean(database_path, "PRAGMA foreign_key_check", "")); + CHECK(pragma_is_clean(true_v15_path, "PRAGMA foreign_key_check", "")); + CHECK(pragma_is_clean(database_path, "PRAGMA quick_check", "ok")); + CHECK(pragma_is_clean(true_v15_path, "PRAGMA quick_check", "ok")); + CHECK(query_integer(database_path, + "SELECT count(*) FROM sqlite_master WHERE type IN " + "('trigger','view')", + 0)); + CHECK(query_integer(true_v15_path, + "SELECT count(*) FROM sqlite_master WHERE type IN " + "('trigger','view')", + 0)); + CHECK(execute_test_sql(true_v15_path, + "DROP INDEX sparse_reconstructions_scope_idx")); + CHECK(!schema_compare(database_path, true_v15_path, true)); + + CHECK(unlink(true_v15_path) == 0); + CHECK(create_true_v15_database(true_v15_path)); + CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V16", "1", 1) == 0); + CHECK(lardon3d_project_db_open(true_v15_path, &database, error) == + LARDON3D_PROJECT_DB_IO_ERROR); + CHECK(unsetenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V16") == 0); + CHECK(query_integer(true_v15_path, + "SELECT value FROM metadata WHERE key='schema_version'", + 15)); + CHECK(query_integer(true_v15_path, + "SELECT count(*) FROM sqlite_master WHERE type='table' " + "AND name='sparse_calibrations'", + 0)); + CHECK(lardon3d_project_db_open(true_v15_path, &database, error) == + LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_schema_version(database) == 16); lardon3d_project_db_close(database); database = NULL; - CHECK(query_integer(true_v14_path, - "SELECT count(*) FROM sqlite_master WHERE type='table' AND " - "name='track_builder_tasks'", 1)); CHECK(create_v13_database(failed_v14_path)); CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V14", "1", 1) == 0); @@ -961,13 +1740,16 @@ static bool run_test(void) { 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)); + "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) == 15); + LARDON3D_PROJECT_DB_OK && + lardon3d_project_db_schema_version(database) == 16); lardon3d_project_db_close(database); database = NULL; @@ -977,23 +1759,29 @@ static bool run_test(void) { LARDON3D_PROJECT_DB_IO_ERROR); CHECK(unsetenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V15") == 0); CHECK(query_integer(failed_v15_path, - "SELECT value FROM metadata WHERE key='schema_version'", 13)); - CHECK(query_integer(failed_v15_path, - "SELECT count(*) FROM sqlite_master WHERE type='table' AND " - "name='track_builder_tasks'", 0)); + "SELECT value FROM metadata WHERE key='schema_version'", + 13)); + CHECK( + query_integer(failed_v15_path, + "SELECT count(*) FROM sqlite_master WHERE type='table' AND " + "name='track_builder_tasks'", + 0)); CHECK(lardon3d_project_db_open(failed_v15_path, &database, error) == - LARDON3D_PROJECT_DB_OK && lardon3d_project_db_schema_version(database) == 15); + LARDON3D_PROJECT_DB_OK && + lardon3d_project_db_schema_version(database) == 16); 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); + 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) == + 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}; @@ -1004,13 +1792,13 @@ static bool run_test(void) { 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); + 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); + 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]; @@ -1025,15 +1813,15 @@ static bool run_test(void) { 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); + 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); + 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]; @@ -1041,104 +1829,125 @@ static bool run_test(void) { 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, + .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, + .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]}; + 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); + 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(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); + &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); + 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(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(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) == + 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, + 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); + 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(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(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]}; + 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); + 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); + 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); + 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); + 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); + 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); + 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)); @@ -1149,17 +1958,20 @@ static bool run_test(void) { .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(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); + 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_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), @@ -1170,45 +1982,53 @@ static bool run_test(void) { "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); + (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); + 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) == + 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); + (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, + 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); + (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); + (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) == + 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); @@ -1216,98 +2036,124 @@ static bool run_test(void) { 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); + 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_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); + 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); + (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_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('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('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('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 " + "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) " @@ -1325,25 +2171,27 @@ static bool run_test(void) { 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 + 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); + (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, insert_obs, NULL, NULL, NULL) == + SQLITE_CONSTRAINT); - CHECK(sqlite3_exec(raw_db, - "DELETE FROM track_sets WHERE track_set_id=2;", + CHECK(sqlite3_exec(raw_db, "DELETE FROM track_sets WHERE track_set_id=2;", NULL, NULL, NULL) == SQLITE_OK); sqlite3_stmt *cascade_check = NULL; @@ -1354,9 +2202,10 @@ static bool run_test(void) { 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_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); @@ -1386,11 +2235,10 @@ static bool run_test(void) { CHECK(unlink(v13_path) == 0); CHECK(unlink(failed_v14_path) == 0); CHECK(unlink(true_v14_path) == 0); + CHECK(unlink(true_v15_path) == 0); CHECK(unlink(failed_v15_path) == 0); CHECK(rmdir(directory) == 0); return true; } -int main(void) { - return run_test() ? EXIT_SUCCESS : EXIT_FAILURE; -} +int main(void) { return run_test() ? EXIT_SUCCESS : EXIT_FAILURE; } diff --git a/tests/test_sparse_sfm_model.c b/tests/test_sparse_sfm_model.c new file mode 100644 index 0000000..ea89c02 --- /dev/null +++ b/tests/test_sparse_sfm_model.c @@ -0,0 +1,1075 @@ +#include +#include +#include +#include +#include +#include + +#include +#include + +#define CHECK(value) \ + do { \ + if (!(value)) { \ + fprintf(stderr, "Sparse SfM check failed at line %d: %s\n", __LINE__, \ + #value); \ + return false; \ + } \ + } while (0) + +static int raw_exec(const char *path, const char *sql) { + sqlite3 *connection = NULL; + char *message = NULL; + int code = sqlite3_open_v2(path, &connection, + SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL); + if (code == SQLITE_OK) + code = sqlite3_exec(connection, "PRAGMA foreign_keys=ON", NULL, NULL, + &message); + if (code == SQLITE_OK) + code = sqlite3_exec(connection, sql, NULL, NULL, &message); + sqlite3_free(message); + if (connection) + sqlite3_close(connection); + return code; +} + +static bool raw_constraint(const char *path, const char *sql) { + return raw_exec(path, sql) == SQLITE_CONSTRAINT; +} + +static bool raw_scalar_equals(const char *path, const char *sql, + sqlite3_int64 expected) { + sqlite3 *connection = NULL; + sqlite3_stmt *statement = NULL; + bool matches = false; + if (sqlite3_open_v2(path, &connection, SQLITE_OPEN_READONLY, NULL) == + SQLITE_OK && + sqlite3_prepare_v2(connection, sql, -1, &statement, NULL) == SQLITE_OK && + sqlite3_step(statement) == SQLITE_ROW) + matches = sqlite3_column_int64(statement, 0) == expected; + if (statement) + sqlite3_finalize(statement); + if (connection) + sqlite3_close(connection); + return matches; +} + +static bool register_image(Lardon3DProjectDb *db, uint64_t scanset_id, + unsigned char seed, uint64_t *image_id) { + unsigned char hash[32] = {0}; + char asset_path[128]; + char hex[65]; + Lardon3DProjectDbImage image; + Lardon3DProjectDbImageRegisterStatus status; + hash[0] = seed; + static const char digits[] = "0123456789abcdef"; + for (size_t index = 0; index < 32; ++index) { + hex[index * 2] = digits[hash[index] >> 4]; + hex[index * 2 + 1] = digits[hash[index] & 15U]; + } + hex[64] = '\0'; + (void)snprintf(asset_path, sizeof(asset_path), "assets/images/%c%c/%s", + hex[0], hex[1], hex); + Lardon3DProjectDbResult result = lardon3d_project_db_register_image( + db, scanset_id, hash, asset_path, 1, "test.jpg", "/test.jpg", 0, 1, + &status, &image); + if (result != LARDON3D_PROJECT_DB_OK) { + char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]; + (void)lardon3d_project_db_last_error(db, error); + fprintf(stderr, "register image: %s (%d)\n", error, (int)result); + } + return result == LARDON3D_PROJECT_DB_OK && (*image_id = image.image_id) != 0; +} + +static bool run_test(void) { + char path[] = "/tmp/lardon3d-sparse-model-XXXXXX"; + int descriptor = mkstemp(path); + CHECK(descriptor >= 0); + close(descriptor); + unlink(path); + + Lardon3DProjectDb *db = NULL; + char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]; + CHECK(lardon3d_project_db_open(path, &db, error) == LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_schema_version(db) == 16); + + Lardon3DProjectDbScanSet scanset; + CHECK(lardon3d_project_db_create_scanset(db, "Sparse model", &scanset) == + LARDON3D_PROJECT_DB_OK); + uint64_t image_a = 0; + uint64_t image_b = 0; + CHECK(register_image(db, scanset.scanset_id, 1, &image_a)); + CHECK(register_image(db, scanset.scanset_id, 2, &image_b)); + + Lardon3DSparseCalibration calibration = {0}; + calibration.model_kind = LARDON3D_SPARSE_SFM_CALIBRATION_KIND_PINHOLE; + calibration.model_version = LARDON3D_SPARSE_SFM_CALIBRATION_VERSION; + calibration.width = 4000; + calibration.height = 3000; + calibration.fx = 2000.0; + calibration.fy = 2001.0; + calibration.cx = 2000.0; + calibration.cy = 1500.0; + calibration.provenance_kind = LARDON3D_SPARSE_SFM_PROVENANCE_USER_EXPLICIT; + Lardon3DSparseCalibration stored; + CHECK(lardon3d_sparse_calibration_create(db, &calibration, &stored) == + LARDON3D_PROJECT_DB_OK); + Lardon3DSparseCalibration alternate_calibration = calibration; + alternate_calibration.provenance_fingerprint[0] = 77; + Lardon3DSparseCalibration alternate_stored; + CHECK(lardon3d_sparse_calibration_create( + db, &alternate_calibration, &alternate_stored) == + LARDON3D_PROJECT_DB_OK); + Lardon3DSparseCalibration duplicate; + CHECK(lardon3d_sparse_calibration_create(db, &calibration, &duplicate) == + LARDON3D_PROJECT_DB_OK); + CHECK(stored.calibration_id == duplicate.calibration_id); + + Lardon3DSparseCalibrationMember members[2] = { + {.image_id = image_b, .calibration_id = stored.calibration_id}, + {.image_id = image_a, .calibration_id = stored.calibration_id}, + }; + memcpy(members[0].calibration_hash, stored.scientific_hash, 32); + memcpy(members[1].calibration_hash, stored.scientific_hash, 32); + Lardon3DSparseCalibrationMember mismatched_members[2]; + memcpy(mismatched_members, members, sizeof(mismatched_members)); + mismatched_members[0].calibration_hash[0] ^= 1U; + Lardon3DSparseCalibrationScope scope; + CHECK(lardon3d_sparse_calibration_scope_create( + db, mismatched_members, 2, &scope) != LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_sparse_calibration_scope_create(db, members, 2, &scope) == + LARDON3D_PROJECT_DB_OK); + Lardon3DSparseCalibrationMember page_members[2] = {members[0], members[1]}; + page_members[1].calibration_id = alternate_stored.calibration_id; + memcpy(page_members[1].calibration_hash, alternate_stored.scientific_hash, + 32); + Lardon3DSparseCalibrationScope page_scope; + CHECK(lardon3d_sparse_calibration_scope_create( + db, page_members, 2, &page_scope) == LARDON3D_PROJECT_DB_OK); + Lardon3DSparseCalibrationMember reordered[2] = {members[1], members[0]}; + Lardon3DSparseCalibrationScope same_scope; + CHECK(lardon3d_sparse_calibration_scope_create( + db, reordered, 2, &same_scope) == LARDON3D_PROJECT_DB_OK); + CHECK(scope.scope_id == same_scope.scope_id); + + Lardon3DSparseCalibrationMember page_items[2]; + size_t page_count = 0; + uint64_t cursor = 0; + CHECK(lardon3d_sparse_calibration_scope_list_members( + db, scope.scope_id, 0, page_items, 1, &page_count, &cursor) == + LARDON3D_PROJECT_DB_OK); + CHECK(page_count == 1 && cursor != 0); + CHECK(lardon3d_sparse_calibration_scope_list_members( + db, scope.scope_id, cursor, page_items, 1, &page_count, &cursor) == + LARDON3D_PROJECT_DB_OK); + CHECK(page_count == 1); + + unsigned char parameter_fingerprint[32] = {3}; + unsigned char feature_hash_a[32] = {4}; + unsigned char feature_hash_b[32] = {5}; + unsigned char source_hash_a[32] = {1}; + unsigned char source_hash_b[32] = {2}; + Lardon3DProjectDbFeatureSet feature_a; + Lardon3DProjectDbFeatureSet feature_b; + char feature_path_a[128]; + char feature_path_b[128]; + (void)snprintf(feature_path_a, sizeof(feature_path_a), + "assets/features/04/04%062x", 0U); + (void)snprintf(feature_path_b, sizeof(feature_path_b), + "assets/features/05/05%062x", 0U); + Lardon3DProjectDbResult feature_result = + lardon3d_project_db_register_feature_set( + db, image_a, "orb", 1, parameter_fingerprint, source_hash_a, 128, 1, + 32, feature_hash_a, feature_path_a, 128, + LARDON3D_DB_FEATURE_ASSET_DURABLE, 0, 1, &feature_a); + if (feature_result != LARDON3D_PROJECT_DB_OK) { + char feature_error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]; + (void)lardon3d_project_db_last_error(db, feature_error); + fprintf(stderr, "feature registration failed: %d %s\n", feature_result, + feature_error); + } + CHECK(feature_result == LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_register_feature_set( + db, image_b, "orb", 1, parameter_fingerprint, source_hash_b, 10, 1, + 32, feature_hash_b, feature_path_b, 128, + LARDON3D_DB_FEATURE_ASSET_DURABLE, 0, 1, + &feature_b) == LARDON3D_PROJECT_DB_OK); + + Lardon3DProjectDbTrackObservation track_observations[4] = { + {.feature_set_id = feature_a.feature_set_id, + .feature_index = 1, + .position_in_track = 0}, + {.feature_set_id = feature_b.feature_set_id, + .feature_index = 1, + .position_in_track = 1}, + {.feature_set_id = feature_a.feature_set_id, + .feature_index = 2, + .position_in_track = 0}, + {.feature_set_id = feature_b.feature_set_id, + .feature_index = 2, + .position_in_track = 1}, + }; + Lardon3DProjectDbTrack tracks[2] = { + {.observation_count = 2, .observations = &track_observations[0]}, + {.observation_count = 2, .observations = &track_observations[2]}, + }; + Lardon3DProjectDbTrackSet track_configuration = {0}; + (void)snprintf(track_configuration.builder_kind, + sizeof(track_configuration.builder_kind), "test_builder"); + track_configuration.builder_version = 1; + track_configuration.parameter_fingerprint[0] = 6; + track_configuration.verifier_kind = 1; + track_configuration.verifier_version = 1; + track_configuration.verifier_fingerprint[0] = 7; + track_configuration.input_scope_hash[0] = 8; + track_configuration.gvr_count = 1; + track_configuration.track_count = 2; + Lardon3DProjectDbTrackSet published_tracks; + CHECK(lardon3d_project_db_create_track_set(db, &track_configuration, tracks, + 2, &published_tracks) == + LARDON3D_PROJECT_DB_OK); + Lardon3DProjectDbTrack loaded_tracks[2]; + memset(loaded_tracks, 0, sizeof(loaded_tracks)); + size_t loaded_count = 0; + CHECK(lardon3d_project_db_list_tracks(db, published_tracks.track_set_id, 0, + loaded_tracks, 2, &loaded_count) == + LARDON3D_PROJECT_DB_OK); + CHECK(loaded_count == 2); + + Lardon3DSparseComponent component = {.component_key = image_a, + .registered_image_count = 2, + .landmark_count = 2}; + Lardon3DSparseRegisteredImage poses[2] = { + {.image_id = image_a, + .component_key = image_a, + .rotation_cw = {1, 0, 0, 0, 1, 0, 0, 0, 1}, + .translation_cw = {0, 0, 0}}, + {.image_id = image_b, + .component_key = image_a, + .rotation_cw = {1, 0, 0, 0, 1, 0, 0, 0, 1}, + .translation_cw = {1, 0, 0}}, + }; + Lardon3DSparseLandmark landmarks[2] = { + {.track_id = loaded_tracks[0].track_id, + .component_key = image_a, + .x = 0, + .y = 0, + .z = 4, + .reprojection_rmse_px = 0.1, + .reprojection_median_px = 0.1, + .observation_count = 2}, + {.track_id = loaded_tracks[1].track_id, + .component_key = image_a, + .x = 1, + .y = 0, + .z = 4, + .reprojection_rmse_px = 0.1, + .reprojection_median_px = 0.1, + .observation_count = 2}, + }; + Lardon3DSparseLandmarkObservation observations[4] = { + {.track_id = loaded_tracks[0].track_id, + .feature_set_id = feature_a.feature_set_id, + .feature_index = 1, + .position_in_track = 0}, + {.track_id = loaded_tracks[0].track_id, + .feature_set_id = feature_b.feature_set_id, + .feature_index = 1, + .position_in_track = 1}, + {.track_id = loaded_tracks[1].track_id, + .feature_set_id = feature_a.feature_set_id, + .feature_index = 2, + .position_in_track = 0}, + {.track_id = loaded_tracks[1].track_id, + .feature_set_id = feature_b.feature_set_id, + .feature_index = 2, + .position_in_track = 1}, + }; + Lardon3DSparsePublication publication = { + .track_set_id = published_tracks.track_set_id, + .calibration_scope_id = scope.scope_id, + .sfm_kind = LARDON3D_SPARSE_SFM_KIND_INCREMENTAL, + .sfm_version = LARDON3D_SPARSE_SFM_VERSION, + .components = &component, + .component_count = 1, + .registered_images = poses, + .registered_image_count = 2, + .landmarks = landmarks, + .landmark_count = 2, + .observations = observations, + .observation_count = 4, + .reprojection_rmse_px = 0.1, + .reprojection_median_px = 0.1, + }; + publication.parameter_fingerprint[0] = 9; + Lardon3DSparseReconstruction reconstruction; + CHECK(lardon3d_sparse_reconstruction_publish( + db, &publication, &reconstruction) == LARDON3D_PROJECT_DB_OK); + Lardon3DSparseLandmarkPage landmark_page = {0}; + Lardon3DSparseLandmark landmark_items[1]; + landmark_page.items = landmark_items; + Lardon3DProjectDbResult page_result = lardon3d_sparse_landmark_list( + db, reconstruction.reconstruction_id, 0, 1, &landmark_page); + if (page_result != LARDON3D_PROJECT_DB_OK) { + char page_error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]; + (void)lardon3d_project_db_last_error(db, page_error); + fprintf(stderr, "landmark page: %d %s\n", page_result, page_error); + } + CHECK(page_result == LARDON3D_PROJECT_DB_OK); + CHECK(landmark_page.count == 1); + CHECK(lardon3d_sparse_landmark_list( + db, reconstruction.reconstruction_id, landmark_page.next_track_id, + 1, &landmark_page) == LARDON3D_PROJECT_DB_OK); + CHECK(landmark_page.count == 1); + CHECK(lardon3d_sparse_landmark_list( + db, reconstruction.reconstruction_id, landmark_page.next_track_id, + 1, &landmark_page) == LARDON3D_PROJECT_DB_OK); + CHECK(landmark_page.count == 0); + CHECK(lardon3d_sparse_landmark_list(db, reconstruction.reconstruction_id, 0, + LARDON3D_SPARSE_SFM_PAGE_MAX + 1, + &landmark_page) == + LARDON3D_PROJECT_DB_INVALID_ARGUMENT); + Lardon3DSparseRegisteredImagePage camera_page = {0}; + Lardon3DSparseRegisteredImage camera_items[1]; + camera_page.items = camera_items; + Lardon3DProjectDbResult camera_result = lardon3d_sparse_registered_image_list( + db, reconstruction.reconstruction_id, 0, 1, &camera_page); + if (camera_result != LARDON3D_PROJECT_DB_OK) { + char camera_error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]; + (void)lardon3d_project_db_last_error(db, camera_error); + fprintf(stderr, "camera page: %d %s\n", camera_result, camera_error); + } + CHECK(camera_result == LARDON3D_PROJECT_DB_OK); + CHECK(camera_page.count == 1 && camera_page.next_image_id == image_a); + CHECK(lardon3d_sparse_registered_image_list( + db, reconstruction.reconstruction_id, camera_page.next_image_id, 1, + &camera_page) == LARDON3D_PROJECT_DB_OK); + CHECK(camera_page.count == 1 && camera_page.next_image_id == image_b); + + Lardon3DSparseObservationPage observation_page = {0}; + Lardon3DSparseLandmarkObservation observation_items[1]; + observation_page.items = observation_items; + CHECK(lardon3d_sparse_observation_list(db, reconstruction.reconstruction_id, + 0, 0, 1, &observation_page) == + LARDON3D_PROJECT_DB_OK); + CHECK(observation_page.count == 1); + CHECK(lardon3d_sparse_observation_list( + db, reconstruction.reconstruction_id, + observation_page.next_landmark_id, + observation_page.next_position_in_track, 1, + &observation_page) == LARDON3D_PROJECT_DB_OK); + CHECK(observation_page.count == 1); + + Lardon3DSparseComponentPage component_page = {0}; + Lardon3DSparseComponent component_items[1]; + component_page.items = component_items; + CHECK(lardon3d_sparse_component_list(db, reconstruction.reconstruction_id, 0, + 1, &component_page) == + LARDON3D_PROJECT_DB_OK); + CHECK(component_page.count == 1 && + component_page.next_component_key == image_a); + + Lardon3DSparseCalibration invalid = calibration; + invalid.width = 0; + CHECK(lardon3d_sparse_calibration_create(db, &invalid, &duplicate) == + LARDON3D_PROJECT_DB_INVALID_ARGUMENT); + invalid = calibration; + invalid.height = 0; + CHECK(lardon3d_sparse_calibration_create(db, &invalid, &duplicate) == + LARDON3D_PROJECT_DB_INVALID_ARGUMENT); + invalid = calibration; + invalid.fx = 0.0; + CHECK(lardon3d_sparse_calibration_create(db, &invalid, &duplicate) == + LARDON3D_PROJECT_DB_INVALID_ARGUMENT); + invalid = calibration; + invalid.fy = -1.0; + CHECK(lardon3d_sparse_calibration_create(db, &invalid, &duplicate) == + LARDON3D_PROJECT_DB_INVALID_ARGUMENT); + invalid = calibration; + invalid.model_kind = 99; + CHECK(lardon3d_sparse_calibration_create(db, &invalid, &duplicate) == + LARDON3D_PROJECT_DB_INVALID_ARGUMENT); + invalid = calibration; + invalid.fx = NAN; + CHECK(lardon3d_sparse_calibration_create(db, &invalid, &duplicate) == + LARDON3D_PROJECT_DB_INVALID_ARGUMENT); + invalid = calibration; + invalid.fy = INFINITY; + CHECK(lardon3d_sparse_calibration_create(db, &invalid, &duplicate) == + LARDON3D_PROJECT_DB_INVALID_ARGUMENT); + const char *lengths[] = {"0", "1", "31", "33", "64"}; + for (size_t index = 0; index < sizeof(lengths) / sizeof(lengths[0]); ++index) { + char sql[512]; + (void)snprintf( + sql, sizeof(sql), + "INSERT INTO sparse_calibrations " + "(scientific_hash,model_kind,model_version,width,height,fx,fy,cx,cy," + "k1,k2,p1,p2,provenance_kind,provenance_fingerprint) VALUES " + "(randomblob(32),1,1,4000,3000,2000,2000,2000,1500,0,0,0,0,1," + "zeroblob(%s))", + lengths[index]); + CHECK(raw_constraint(path, sql)); + (void)snprintf( + sql, sizeof(sql), + "INSERT INTO sparse_calibration_scopes " + "(scientific_hash,member_count) VALUES(randomblob(%s),1)", + lengths[index]); + CHECK(raw_constraint(path, sql)); + } + lardon3d_project_db_close(db); + db = NULL; + for (size_t index = 0; index < sizeof(lengths) / sizeof(lengths[0]); ++index) { + char sql[512]; + (void)snprintf( + sql, sizeof(sql), + "INSERT INTO sparse_reconstructions " + "(track_set_id,calibration_scope_id,sfm_kind,sfm_version," + "parameter_fingerprint,component_count,registered_image_count," + "landmark_count,reprojection_rmse_px,reprojection_median_px,created_at)" + " VALUES(%llu,%llu,1,1,zeroblob(%s),1,2,1,0.1,0.1,0)", + (unsigned long long)published_tracks.track_set_id, + (unsigned long long)scope.scope_id, lengths[index]); + CHECK(raw_constraint(path, sql)); + } + CHECK(lardon3d_project_db_open(path, &db, error) == LARDON3D_PROJECT_DB_OK); + lardon3d_project_db_close(db); + db = NULL; + CHECK(raw_exec(path, + "UPDATE sparse_calibration_scopes SET member_count=1") == + SQLITE_OK); + CHECK(lardon3d_project_db_open(path, &db, error) == LARDON3D_PROJECT_DB_OK); + Lardon3DSparseCalibrationScope corrupt_scope; + CHECK(lardon3d_sparse_calibration_scope_load(db, scope.scope_id, + &corrupt_scope) == + LARDON3D_PROJECT_DB_CORRUPT); + lardon3d_project_db_close(db); + db = NULL; + CHECK(raw_exec(path, + "UPDATE sparse_calibration_scopes SET member_count=2") == + SQLITE_OK); + CHECK(lardon3d_project_db_open(path, &db, error) == LARDON3D_PROJECT_DB_OK); + Lardon3DSparseReconstruction reopened; + CHECK( + lardon3d_sparse_reconstruction_load(db, reconstruction.reconstruction_id, + &reopened) == LARDON3D_PROJECT_DB_OK); + CHECK(reopened.landmark_count == reconstruction.landmark_count); + for (size_t index = 0; index < loaded_count; ++index) + lardon3d_project_db_free_track(&loaded_tracks[index]); + lardon3d_project_db_close(db); + db = NULL; + char corrupt_calibration_sql[128]; + (void)snprintf(corrupt_calibration_sql, sizeof(corrupt_calibration_sql), + "PRAGMA ignore_check_constraints=ON;UPDATE " + "sparse_calibrations SET width=0 WHERE calibration_id=%llu", + (unsigned long long)alternate_stored.calibration_id); + CHECK(raw_exec(path, corrupt_calibration_sql) == SQLITE_OK); + CHECK(lardon3d_project_db_open(path, &db, error) == LARDON3D_PROJECT_DB_OK); + Lardon3DSparseCalibrationMember scope_page_items[2]; + size_t scope_page_count = 0; + uint64_t scope_page_cursor = 0; + CHECK(lardon3d_sparse_calibration_scope_list_members( + db, page_scope.scope_id, 0, scope_page_items, 2, &scope_page_count, + &scope_page_cursor) == LARDON3D_PROJECT_DB_CORRUPT); + CHECK(scope_page_count == 0 && scope_page_cursor == 0); + lardon3d_project_db_close(db); + unlink(path); + return true; +} + +static bool register_fixture_feature_set(Lardon3DProjectDb *db, uint64_t image_id, + unsigned char seed, + Lardon3DProjectDbFeatureSet *output) { + unsigned char parameter_hash[32] = {11}; + unsigned char source_hash[32] = {0}; + unsigned char asset_hash[32] = {0}; + char path[128]; + char hex[65]; + static const char digits[] = "0123456789abcdef"; + source_hash[0] = seed; + asset_hash[0] = seed; + for (size_t index = 0; index < 32; ++index) { + hex[index * 2] = digits[asset_hash[index] >> 4]; + hex[index * 2 + 1] = digits[asset_hash[index] & 15U]; + } + hex[64] = '\0'; + (void)snprintf(path, sizeof(path), "assets/features/%c%c/%s", hex[0], hex[1], + hex); + return lardon3d_project_db_register_feature_set( + db, image_id, "synthetic", 1, parameter_hash, source_hash, 3, 1, + 32, asset_hash, path, 64, LARDON3D_DB_FEATURE_ASSET_DURABLE, 0, + 1, output) == LARDON3D_PROJECT_DB_OK; +} + +static bool page_fixture(Lardon3DProjectDb *db, uint64_t reconstruction_id, + const uint64_t image_ids[4], const uint64_t track_ids[4], + const uint64_t feature_set_ids[4]) { + Lardon3DSparseComponentPage components = {0}; + Lardon3DSparseComponent component_item; + components.items = &component_item; + uint64_t after_component = 0; + size_t component_index = 0; + for (;;) { + CHECK(lardon3d_sparse_component_list(db, reconstruction_id, + after_component, 1, &components) == + LARDON3D_PROJECT_DB_OK); + if (components.count == 0) + break; + CHECK(component_index < 2); + CHECK(component_item.component_key == image_ids[component_index * 2]); + CHECK(component_item.registered_image_count == 2); + CHECK(component_item.landmark_count == 2); + after_component = components.next_component_key; + ++component_index; + } + CHECK(component_index == 2); + + Lardon3DSparseRegisteredImagePage cameras = {0}; + Lardon3DSparseRegisteredImage camera_item; + cameras.items = &camera_item; + uint64_t after_image = 0; + size_t image_index = 0; + for (;;) { + CHECK(lardon3d_sparse_registered_image_list( + db, reconstruction_id, after_image, 1, &cameras) == + LARDON3D_PROJECT_DB_OK); + if (cameras.count == 0) + break; + CHECK(image_index < 4); + CHECK(camera_item.image_id == image_ids[image_index]); + CHECK(camera_item.component_key == image_ids[image_index < 2 ? 0 : 2]); + after_image = cameras.next_image_id; + ++image_index; + } + CHECK(image_index == 4); + + Lardon3DSparseLandmarkPage landmarks = {0}; + Lardon3DSparseLandmark landmark_item; + landmarks.items = &landmark_item; + uint64_t after_track = 0; + size_t landmark_index = 0; + for (;;) { + CHECK(lardon3d_sparse_landmark_list(db, reconstruction_id, after_track, 1, + &landmarks) == LARDON3D_PROJECT_DB_OK); + if (landmarks.count == 0) + break; + CHECK(landmark_index < 4); + CHECK(landmark_item.track_id == track_ids[landmark_index]); + CHECK(landmark_item.component_key == image_ids[landmark_index < 2 ? 0 : 2]); + CHECK(landmark_item.observation_count == 2); + after_track = landmarks.next_track_id; + ++landmark_index; + } + CHECK(landmark_index == 4); + + Lardon3DSparseObservationPage observations = {0}; + Lardon3DSparseLandmarkObservation observation_item; + observations.items = &observation_item; + uint64_t after_landmark = 0; + uint32_t after_position = 0; + size_t observation_index = 0; + for (;;) { + CHECK(lardon3d_sparse_observation_list( + db, reconstruction_id, after_landmark, after_position, 1, + &observations) == LARDON3D_PROJECT_DB_OK); + if (observations.count == 0) + break; + CHECK(observation_index < 8); + CHECK(observation_item.track_id == track_ids[observation_index / 2]); + CHECK(observation_item.feature_set_id == + feature_set_ids[(observation_index / 2) < 2 ? + observation_index % 2 : + 2 + observation_index % 2]); + CHECK(observation_item.position_in_track == observation_index % 2); + after_landmark = observations.next_landmark_id; + after_position = observations.next_position_in_track; + ++observation_index; + } + CHECK(observation_index == 8); + return true; +} + +static bool run_multi_component_test(void) { + char path[] = "/tmp/lardon3d-sparse-components-XXXXXX"; + int descriptor = mkstemp(path); + CHECK(descriptor >= 0); + close(descriptor); + unlink(path); + + Lardon3DProjectDb *db = NULL; + char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]; + CHECK(lardon3d_project_db_open(path, &db, error) == LARDON3D_PROJECT_DB_OK); + Lardon3DProjectDbScanSet scanset; + CHECK(lardon3d_project_db_create_scanset(db, "two components", &scanset) == + LARDON3D_PROJECT_DB_OK); + uint64_t image_ids[4] = {0}; + Lardon3DProjectDbFeatureSet features[4]; + for (size_t index = 0; index < 4; ++index) { + CHECK(register_image(db, scanset.scanset_id, (unsigned char)(20 + index), + &image_ids[index])); + CHECK(register_fixture_feature_set(db, image_ids[index], + (unsigned char)(20 + index), + &features[index])); + } + + Lardon3DSparseCalibration calibration = {0}; + calibration.model_kind = LARDON3D_SPARSE_SFM_CALIBRATION_KIND_PINHOLE; + calibration.model_version = LARDON3D_SPARSE_SFM_CALIBRATION_VERSION; + calibration.width = 4000; + calibration.height = 3000; + calibration.fx = 2000.0; + calibration.fy = 2000.0; + calibration.cx = 2000.0; + calibration.cy = 1500.0; + calibration.provenance_kind = LARDON3D_SPARSE_SFM_PROVENANCE_USER_EXPLICIT; + Lardon3DSparseCalibration stored; + CHECK(lardon3d_sparse_calibration_create(db, &calibration, &stored) == + LARDON3D_PROJECT_DB_OK); + Lardon3DSparseCalibrationMember members[4]; + for (size_t index = 0; index < 4; ++index) { + members[index].image_id = image_ids[index]; + members[index].calibration_id = stored.calibration_id; + memcpy(members[index].calibration_hash, stored.scientific_hash, 32); + } + Lardon3DSparseCalibrationScope scope; + CHECK(lardon3d_sparse_calibration_scope_create(db, members, 4, &scope) == + LARDON3D_PROJECT_DB_OK); + + Lardon3DProjectDbTrackObservation track_observations[8]; + Lardon3DProjectDbTrack tracks[4]; + for (size_t track = 0; track < 4; ++track) { + size_t first = track * 2; + size_t first_image = track < 2 ? 0 : 2; + track_observations[first] = (Lardon3DProjectDbTrackObservation){ + .feature_set_id = features[first_image].feature_set_id, + .feature_index = (uint32_t)(track % 2 + 1), .position_in_track = 0}; + track_observations[first + 1] = (Lardon3DProjectDbTrackObservation){ + .feature_set_id = features[first_image + 1].feature_set_id, + .feature_index = (uint32_t)(track % 2 + 1), .position_in_track = 1}; + tracks[track] = (Lardon3DProjectDbTrack){ + .observation_count = 2, .observations = &track_observations[first]}; + } + Lardon3DProjectDbTrackSet configuration = {0}; + (void)snprintf(configuration.builder_kind, sizeof(configuration.builder_kind), + "synthetic_components"); + configuration.builder_version = 1; + configuration.parameter_fingerprint[0] = 31; + configuration.verifier_kind = 1; + configuration.verifier_version = 1; + configuration.verifier_fingerprint[0] = 32; + configuration.input_scope_hash[0] = 33; + configuration.gvr_count = 1; + configuration.track_count = 4; + Lardon3DProjectDbTrackSet track_set; + Lardon3DProjectDbResult track_result = lardon3d_project_db_create_track_set( + db, &configuration, tracks, 4, &track_set); + if (track_result != LARDON3D_PROJECT_DB_OK) { + char track_error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]; + (void)lardon3d_project_db_last_error(db, track_error); + fprintf(stderr, "multi-component track set: %d %s\n", track_result, + track_error); + } + CHECK(track_result == LARDON3D_PROJECT_DB_OK); + Lardon3DProjectDbTrack loaded_tracks[4] = {0}; + size_t loaded_count = 0; + CHECK(lardon3d_project_db_list_tracks(db, track_set.track_set_id, 0, + loaded_tracks, 4, &loaded_count) == + LARDON3D_PROJECT_DB_OK); + CHECK(loaded_count == 4); + uint64_t track_ids[4]; + for (size_t index = 0; index < 4; ++index) + track_ids[index] = loaded_tracks[index].track_id; + for (size_t index = 0; index < 4; ++index) + lardon3d_project_db_free_track(&loaded_tracks[index]); + + Lardon3DSparseComponent components[2] = { + {.component_key = image_ids[0], .registered_image_count = 2, + .landmark_count = 2}, + {.component_key = image_ids[2], .registered_image_count = 2, + .landmark_count = 2}}; + Lardon3DSparseRegisteredImage poses[4]; + for (size_t index = 0; index < 4; ++index) { + poses[index] = (Lardon3DSparseRegisteredImage){ + .image_id = image_ids[index], + .component_key = image_ids[index < 2 ? 0 : 2], + .rotation_cw = {1, 0, 0, 0, 1, 0, 0, 0, 1}, + .translation_cw = {(double)index, 0, 0}}; + } + Lardon3DSparseLandmark landmarks[4]; + for (size_t index = 0; index < 4; ++index) + landmarks[index] = (Lardon3DSparseLandmark){ + .track_id = track_ids[index], + .component_key = image_ids[index < 2 ? 0 : 2], + .x = (double)index, + .y = 0, + .z = 4, + .reprojection_rmse_px = 0.1, + .reprojection_median_px = 0.1, + .observation_count = 2}; + Lardon3DSparseLandmarkObservation observations[8]; + for (size_t index = 0; index < 8; ++index) + observations[index] = (Lardon3DSparseLandmarkObservation){ + .track_id = track_ids[index / 2], + .feature_set_id = features[index / 2 < 2 ? index / 2 : 2 + index / 2 - 2] + .feature_set_id, + .feature_index = (uint32_t)(index / 2 % 2 + 1), + .position_in_track = (uint32_t)(index % 2)}; + for (size_t index = 0; index < 8; ++index) + observations[index].feature_set_id = + features[(index / 2 < 2 ? 0 : 2) + index % 2].feature_set_id; + + Lardon3DSparsePublication publication = { + .track_set_id = track_set.track_set_id, + .calibration_scope_id = scope.scope_id, + .sfm_kind = LARDON3D_SPARSE_SFM_KIND_INCREMENTAL, + .sfm_version = LARDON3D_SPARSE_SFM_VERSION, + .components = components, + .component_count = 2, + .registered_images = poses, + .registered_image_count = 4, + .landmarks = landmarks, + .landmark_count = 4, + .observations = observations, + .observation_count = 8, + .reprojection_rmse_px = 0.1, + .reprojection_median_px = 0.1}; + publication.parameter_fingerprint[0] = 34; + Lardon3DSparseReconstruction reconstruction; + CHECK(lardon3d_sparse_reconstruction_publish(db, &publication, + &reconstruction) == + LARDON3D_PROJECT_DB_OK); + publication.parameter_fingerprint[0] = 47; + Lardon3DSparseReconstruction deleted_reconstruction; + CHECK(lardon3d_sparse_reconstruction_publish( + db, &publication, &deleted_reconstruction) == + LARDON3D_PROJECT_DB_OK); + publication.parameter_fingerprint[0] = 34; + uint64_t deleted_id = deleted_reconstruction.reconstruction_id; + lardon3d_project_db_close(db); + db = NULL; + char lifecycle_sql[256]; + (void)snprintf(lifecycle_sql, sizeof(lifecycle_sql), + "DELETE FROM sparse_reconstructions WHERE reconstruction_id=%llu", + (unsigned long long)deleted_id); + CHECK(raw_exec(path, lifecycle_sql) == SQLITE_OK); + (void)snprintf(lifecycle_sql, sizeof(lifecycle_sql), + "SELECT COUNT(*) FROM sparse_reconstructions " + "WHERE reconstruction_id=%llu", + (unsigned long long)deleted_id); + CHECK(raw_scalar_equals(path, lifecycle_sql, 0)); + (void)snprintf(lifecycle_sql, sizeof(lifecycle_sql), + "SELECT COUNT(*) FROM sparse_reconstruction_components " + "WHERE reconstruction_id=%llu", + (unsigned long long)deleted_id); + CHECK(raw_scalar_equals(path, lifecycle_sql, 0)); + (void)snprintf(lifecycle_sql, sizeof(lifecycle_sql), + "SELECT COUNT(*) FROM sparse_registered_images " + "WHERE reconstruction_id=%llu", + (unsigned long long)deleted_id); + CHECK(raw_scalar_equals(path, lifecycle_sql, 0)); + (void)snprintf(lifecycle_sql, sizeof(lifecycle_sql), + "SELECT COUNT(*) FROM sparse_landmarks " + "WHERE reconstruction_id=%llu", + (unsigned long long)deleted_id); + CHECK(raw_scalar_equals(path, lifecycle_sql, 0)); + (void)snprintf(lifecycle_sql, sizeof(lifecycle_sql), + "SELECT COUNT(*) FROM sparse_landmark_observations AS o " + "JOIN sparse_landmarks AS l ON l.landmark_id=o.landmark_id " + "WHERE l.reconstruction_id=%llu", + (unsigned long long)deleted_id); + CHECK(raw_scalar_equals(path, lifecycle_sql, 0)); + CHECK(raw_scalar_equals( + path, + "SELECT COUNT(*) FROM sparse_reconstruction_components AS c " + "WHERE NOT EXISTS (SELECT 1 FROM sparse_reconstructions AS r " + "WHERE r.reconstruction_id=c.reconstruction_id)", + 0)); + CHECK(raw_scalar_equals( + path, + "SELECT COUNT(*) FROM sparse_registered_images AS p WHERE " + "NOT EXISTS (SELECT 1 FROM sparse_reconstructions AS r WHERE " + "r.reconstruction_id=p.reconstruction_id) OR NOT EXISTS " + "(SELECT 1 FROM sparse_reconstruction_components AS c WHERE " + "c.reconstruction_id=p.reconstruction_id AND c.component_id=" + "p.component_id)", + 0)); + CHECK(raw_scalar_equals( + path, + "SELECT COUNT(*) FROM sparse_landmarks AS l WHERE NOT EXISTS " + "(SELECT 1 FROM sparse_reconstructions AS r WHERE " + "r.reconstruction_id=l.reconstruction_id) OR NOT EXISTS " + "(SELECT 1 FROM sparse_reconstruction_components AS c WHERE " + "c.reconstruction_id=l.reconstruction_id AND c.component_id=" + "l.component_id)", + 0)); + CHECK(raw_scalar_equals( + path, + "SELECT COUNT(*) FROM sparse_landmark_observations AS o WHERE " + "NOT EXISTS (SELECT 1 FROM sparse_landmarks AS l WHERE " + "l.landmark_id=o.landmark_id)", + 0)); + CHECK(raw_constraint(path, "DELETE FROM tracks WHERE track_id=1")); + CHECK(raw_constraint(path, "DELETE FROM track_sets WHERE track_set_id=1")); + CHECK(raw_constraint(path, "DELETE FROM images WHERE image_id=1")); + CHECK(raw_constraint(path, "DELETE FROM feature_sets WHERE feature_set_id=1")); + CHECK(raw_constraint(path, "DELETE FROM sparse_calibrations WHERE calibration_id=1")); + CHECK(raw_constraint(path, "DELETE FROM sparse_calibration_scopes WHERE scope_id=1")); + CHECK(lardon3d_project_db_open(path, &db, error) == LARDON3D_PROJECT_DB_OK); + Lardon3DSparseCalibration surviving_calibration; + CHECK(lardon3d_sparse_calibration_load(db, stored.calibration_id, + &surviving_calibration) == + LARDON3D_PROJECT_DB_OK); + Lardon3DSparseCalibrationScope surviving_scope; + CHECK(lardon3d_sparse_calibration_scope_load(db, scope.scope_id, + &surviving_scope) == + LARDON3D_PROJECT_DB_OK); + Lardon3DProjectDbTrackSet surviving_track_set; + CHECK(lardon3d_project_db_load_track_set(db, track_set.track_set_id, + &surviving_track_set) == + LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_sparse_reconstruction_load(db, reconstruction.reconstruction_id, + &reconstruction) == + LARDON3D_PROJECT_DB_OK); + Lardon3DSparseRegisteredImage wrong_component_poses[4]; + memcpy(wrong_component_poses, poses, sizeof(wrong_component_poses)); + wrong_component_poses[0].component_key = image_ids[2]; + Lardon3DSparsePublication invalid_publication = publication; + invalid_publication.registered_images = wrong_component_poses; + invalid_publication.parameter_fingerprint[0] = 39; + CHECK(lardon3d_sparse_reconstruction_publish( + db, &invalid_publication, &reconstruction) == + LARDON3D_PROJECT_DB_INVALID_ARGUMENT); + Lardon3DSparseLandmark wrong_component_landmarks[4]; + memcpy(wrong_component_landmarks, landmarks, sizeof(wrong_component_landmarks)); + wrong_component_landmarks[0].component_key = image_ids[2]; + invalid_publication = publication; + invalid_publication.landmarks = wrong_component_landmarks; + invalid_publication.parameter_fingerprint[0] = 40; + CHECK(lardon3d_sparse_reconstruction_publish( + db, &invalid_publication, &reconstruction) == + LARDON3D_PROJECT_DB_INVALID_ARGUMENT); + memcpy(wrong_component_poses, poses, sizeof(wrong_component_poses)); + wrong_component_poses[1].image_id = image_ids[0]; + invalid_publication = publication; + invalid_publication.registered_images = wrong_component_poses; + invalid_publication.parameter_fingerprint[0] = 41; + CHECK(lardon3d_sparse_reconstruction_publish( + db, &invalid_publication, &reconstruction) == + LARDON3D_PROJECT_DB_INVALID_ARGUMENT); + invalid_publication = publication; + invalid_publication.track_set_id = track_set.track_set_id + 1000; + invalid_publication.parameter_fingerprint[0] = 42; + CHECK(lardon3d_sparse_reconstruction_publish( + db, &invalid_publication, &reconstruction) == + LARDON3D_PROJECT_DB_NOT_FOUND); + Lardon3DSparseLandmark duplicate_landmarks[4]; + memcpy(duplicate_landmarks, landmarks, sizeof(duplicate_landmarks)); + duplicate_landmarks[1].track_id = duplicate_landmarks[0].track_id; + invalid_publication = publication; + invalid_publication.landmarks = duplicate_landmarks; + invalid_publication.parameter_fingerprint[0] = 43; + CHECK(lardon3d_sparse_reconstruction_publish( + db, &invalid_publication, &reconstruction) == + LARDON3D_PROJECT_DB_INVALID_ARGUMENT); + Lardon3DSparseLandmarkObservation invalid_observations[8]; + memcpy(invalid_observations, observations, sizeof(invalid_observations)); + invalid_observations[0].track_id = track_ids[1]; + invalid_publication = publication; + invalid_publication.observations = invalid_observations; + invalid_publication.parameter_fingerprint[0] = 44; + CHECK(lardon3d_sparse_reconstruction_publish( + db, &invalid_publication, &reconstruction) != + LARDON3D_PROJECT_DB_OK); + memcpy(invalid_observations, observations, sizeof(invalid_observations)); + invalid_observations[0].feature_index = 99; + invalid_publication.parameter_fingerprint[0] = 45; + CHECK(lardon3d_sparse_reconstruction_publish( + db, &invalid_publication, &reconstruction) != + LARDON3D_PROJECT_DB_OK); + memcpy(invalid_observations, observations, sizeof(invalid_observations)); + invalid_observations[1] = invalid_observations[0]; + invalid_publication.parameter_fingerprint[0] = 46; + CHECK(lardon3d_sparse_reconstruction_publish( + db, &invalid_publication, &reconstruction) != + LARDON3D_PROJECT_DB_OK); + Lardon3DSparseRegisteredImage invalid_pose = poses[0]; + invalid_pose.rotation_cw[0] = 2.0; + Lardon3DSparsePublication invalid_pose_publication = publication; + invalid_pose_publication.registered_images = &invalid_pose; + invalid_pose_publication.registered_image_count = 1; + invalid_pose_publication.parameter_fingerprint[0] = 37; + CHECK(lardon3d_sparse_reconstruction_publish( + db, &invalid_pose_publication, &reconstruction) == + LARDON3D_PROJECT_DB_INVALID_ARGUMENT); + invalid_pose = poses[0]; + invalid_pose.translation_cw[0] = NAN; + invalid_pose_publication.registered_images = &invalid_pose; + invalid_pose_publication.parameter_fingerprint[0] = 38; + CHECK(lardon3d_sparse_reconstruction_publish( + db, &invalid_pose_publication, &reconstruction) == + LARDON3D_PROJECT_DB_INVALID_ARGUMENT); + CHECK(reconstruction.component_count == 2); + CHECK(reconstruction.registered_image_count == 4); + CHECK(reconstruction.landmark_count == 4); + CHECK(page_fixture(db, reconstruction.reconstruction_id, image_ids, track_ids, + (uint64_t[4]){features[0].feature_set_id, + features[1].feature_set_id, + features[2].feature_set_id, + features[3].feature_set_id})); + + Lardon3DSparseComponent unsorted_components[2] = {components[1], components[0]}; + Lardon3DSparsePublication unsorted_publication = publication; + unsorted_publication.components = unsorted_components; + unsorted_publication.parameter_fingerprint[0] = 36; + CHECK(lardon3d_sparse_reconstruction_publish(db, &unsorted_publication, + &reconstruction) == + LARDON3D_PROJECT_DB_INVALID_ARGUMENT); + + Lardon3DSparseLandmarkObservation invalid = observations[0]; + invalid.feature_set_id = features[2].feature_set_id; + invalid_publication = publication; + invalid_publication.observations = &invalid; + invalid_publication.observation_count = 1; + invalid_publication.parameter_fingerprint[0] = 35; + CHECK(lardon3d_sparse_reconstruction_publish(db, &invalid_publication, + &reconstruction) != + LARDON3D_PROJECT_DB_OK); + Lardon3DSparseReconstructionPage reconstruction_page = {0}; + Lardon3DSparseReconstruction reconstruction_items[2]; + reconstruction_page.items = reconstruction_items; + CHECK(lardon3d_sparse_reconstruction_list(db, 0, 2, &reconstruction_page) == + LARDON3D_PROJECT_DB_OK); + CHECK(reconstruction_page.count == 1); + + uint64_t reconstruction_id = reconstruction.reconstruction_id; + lardon3d_project_db_close(db); + db = NULL; + CHECK(lardon3d_project_db_open(path, &db, error) == LARDON3D_PROJECT_DB_OK); + Lardon3DSparseReconstruction reopened; + CHECK(lardon3d_sparse_reconstruction_load(db, reconstruction_id, &reopened) == + LARDON3D_PROJECT_DB_OK); + CHECK(reopened.component_count == 2 && reopened.registered_image_count == 4 && + reopened.landmark_count == 4); + CHECK(page_fixture(db, reconstruction_id, image_ids, track_ids, + (uint64_t[4]){features[0].feature_set_id, + features[1].feature_set_id, + features[2].feature_set_id, + features[3].feature_set_id})); + lardon3d_project_db_close(db); + db = NULL; + CHECK(raw_exec( + path, + "PRAGMA ignore_check_constraints=ON;" + "UPDATE sparse_reconstructions SET sfm_kind=99") == SQLITE_OK); + CHECK(lardon3d_project_db_open(path, &db, error) == LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_sparse_reconstruction_load(db, reconstruction_id, &reopened) == + LARDON3D_PROJECT_DB_CORRUPT); + lardon3d_project_db_close(db); + db = NULL; + CHECK(raw_exec( + path, + "PRAGMA ignore_check_constraints=ON;" + "UPDATE sparse_reconstructions SET sfm_kind=1") == SQLITE_OK); + CHECK(lardon3d_project_db_open(path, &db, error) == LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_sparse_reconstruction_load(db, reconstruction_id, &reopened) == + LARDON3D_PROJECT_DB_OK); + lardon3d_project_db_close(db); + db = NULL; + CHECK(raw_exec( + path, + "UPDATE sparse_registered_images SET component_id=" + "(SELECT component_id FROM sparse_reconstruction_components " + "WHERE reconstruction_id=(SELECT reconstruction_id FROM " + "sparse_registered_images LIMIT 1) ORDER BY component_key DESC " + "LIMIT 1) WHERE image_id=(SELECT MIN(image_id) FROM " + "sparse_registered_images)") == SQLITE_OK); + CHECK(lardon3d_project_db_open(path, &db, error) == LARDON3D_PROJECT_DB_OK); + Lardon3DSparseRegisteredImagePage wrong_component_camera_page = {0}; + Lardon3DSparseRegisteredImage wrong_component_camera; + wrong_component_camera_page.items = &wrong_component_camera; + CHECK(lardon3d_sparse_registered_image_list(db, reconstruction_id, 0, 1, + &wrong_component_camera_page) == + LARDON3D_PROJECT_DB_CORRUPT); + Lardon3DSparseComponentPage wrong_component_page = {0}; + Lardon3DSparseComponent wrong_component; + wrong_component_page.items = &wrong_component; + CHECK(lardon3d_sparse_component_list(db, reconstruction_id, 0, 1, + &wrong_component_page) == + LARDON3D_PROJECT_DB_CORRUPT); + lardon3d_project_db_close(db); + db = NULL; + CHECK(raw_exec( + path, + "UPDATE sparse_registered_images SET component_id=" + "(SELECT component_id FROM sparse_reconstruction_components " + "WHERE reconstruction_id=(SELECT reconstruction_id FROM " + "sparse_registered_images LIMIT 1) AND component_key=" + "(SELECT MIN(component_key) FROM sparse_reconstruction_components)) " + "WHERE image_id=(SELECT MIN(image_id) FROM sparse_registered_images);" + "UPDATE sparse_landmarks SET component_id=" + "(SELECT component_id FROM sparse_reconstruction_components " + "WHERE reconstruction_id=(SELECT reconstruction_id FROM " + "sparse_landmarks LIMIT 1) ORDER BY component_key DESC LIMIT 1) " + "WHERE landmark_id=(SELECT MIN(landmark_id) FROM sparse_landmarks)") == + SQLITE_OK); + CHECK(lardon3d_project_db_open(path, &db, error) == LARDON3D_PROJECT_DB_OK); + Lardon3DSparseLandmarkPage wrong_component_landmark_page = {0}; + Lardon3DSparseLandmark wrong_component_landmark; + wrong_component_landmark_page.items = &wrong_component_landmark; + CHECK(lardon3d_sparse_landmark_list(db, reconstruction_id, 0, 1, + &wrong_component_landmark_page) == + LARDON3D_PROJECT_DB_CORRUPT); + lardon3d_project_db_close(db); + db = NULL; + CHECK(raw_exec( + path, + "UPDATE sparse_landmarks SET component_id=" + "(SELECT component_id FROM sparse_reconstruction_components " + "WHERE reconstruction_id=(SELECT reconstruction_id FROM " + "sparse_landmarks LIMIT 1) AND component_key=" + "(SELECT MIN(f.image_id) FROM track_observations AS t JOIN " + "feature_sets AS f ON f.feature_set_id=t.feature_set_id JOIN " + "sparse_reconstructions AS r ON r.track_set_id=t.track_set_id " + "WHERE r.reconstruction_id=(SELECT reconstruction_id FROM " + "sparse_landmarks LIMIT 1) AND t.track_id=(SELECT track_id FROM " + "sparse_landmarks ORDER BY landmark_id LIMIT 1))) WHERE " + "landmark_id=(SELECT MIN(landmark_id) FROM sparse_landmarks)") == + SQLITE_OK); + CHECK(lardon3d_project_db_open(path, &db, error) == LARDON3D_PROJECT_DB_OK); + lardon3d_project_db_close(db); + db = NULL; + CHECK(raw_exec(path, + "UPDATE sparse_registered_images SET rotation_00=2.0") == + SQLITE_OK); + CHECK(lardon3d_project_db_open(path, &db, error) == LARDON3D_PROJECT_DB_OK); + Lardon3DSparseRegisteredImagePage corrupt_camera_page = {0}; + Lardon3DSparseRegisteredImage corrupt_camera; + corrupt_camera_page.items = &corrupt_camera; + CHECK(lardon3d_sparse_registered_image_list(db, reconstruction_id, 0, 1, + &corrupt_camera_page) == + LARDON3D_PROJECT_DB_CORRUPT); + lardon3d_project_db_close(db); + db = NULL; + CHECK(raw_exec( + path, + "UPDATE sparse_registered_images SET rotation_00=1.0;" + "UPDATE sparse_landmark_observations SET feature_set_id=" + "(SELECT feature_set_id FROM feature_sets ORDER BY feature_set_id " + "LIMIT 1 OFFSET 2) " + "WHERE landmark_id=(SELECT landmark_id FROM sparse_landmarks " + "ORDER BY landmark_id LIMIT 1) AND position_in_track=0") == + SQLITE_OK); + CHECK(lardon3d_project_db_open(path, &db, error) == LARDON3D_PROJECT_DB_OK); + Lardon3DSparseObservationPage corrupt_observation_page = {0}; + Lardon3DSparseLandmarkObservation corrupt_observation; + corrupt_observation_page.items = &corrupt_observation; + CHECK(lardon3d_sparse_observation_list(db, reconstruction_id, 0, 0, 1, + &corrupt_observation_page) == + LARDON3D_PROJECT_DB_CORRUPT); + lardon3d_project_db_close(db); + unlink(path); + return true; +} + +int main(void) { + return run_test() && run_multi_component_test() ? EXIT_SUCCESS : EXIT_FAILURE; +} diff --git a/tests/test_sparse_sfm_resource.c b/tests/test_sparse_sfm_resource.c new file mode 100644 index 0000000..fdb8239 --- /dev/null +++ b/tests/test_sparse_sfm_resource.c @@ -0,0 +1,313 @@ +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +static bool exec_sql(const char *path, const char *sql) { + sqlite3 *connection = NULL; + bool ok = sqlite3_open(path, &connection) == SQLITE_OK; + if (ok) + ok = sqlite3_exec(connection, "PRAGMA foreign_keys=ON", NULL, NULL, NULL) == + SQLITE_OK; + char *message = NULL; + if (ok) + ok = sqlite3_exec(connection, sql, NULL, NULL, &message) == SQLITE_OK; + if (!ok && connection) + fprintf(stderr, "resource SQL failed: %s\n", + message ? message : sqlite3_errmsg(connection)); + sqlite3_free(message); + if (connection) + ok = sqlite3_close(connection) == SQLITE_OK && ok; + return ok; +} + +static bool seed_upstream(const char *path, uint64_t landmark_count) { + char *sql = NULL; + int required = snprintf( + NULL, 0, + "PRAGMA foreign_keys=ON;" + "INSERT INTO scansets(scanset_id,name,created_at,updated_at) " + "VALUES(1,'resource',1,1);" + "WITH RECURSIVE n(x) AS (SELECT 1 UNION ALL SELECT x+1 FROM n WHERE x<500) " + "INSERT INTO image_assets(asset_id,sha256,path,size_bytes,state,created_at) " + "SELECT x,randomblob(32),printf('resource/image-%%d',x),1,1,1 FROM n;" + "WITH RECURSIVE n(x) AS (SELECT 1 UNION ALL SELECT x+1 FROM n WHERE x<500) " + "INSERT INTO images(image_id,scanset_id,asset_id,original_name,source_path," + "producer_task_id,imported_at) SELECT x,1,x,printf('image-%%d',x)," + "printf('resource/image-%%d',x),NULL,1 FROM n;" + "WITH RECURSIVE n(x) AS (SELECT 1 UNION ALL SELECT x+1 FROM n WHERE x<500) " + "INSERT INTO feature_assets(feature_asset_id,sha256,path,size_bytes,durability,created_at) " + "SELECT x,randomblob(32),printf('resource/feature-%%d',x),1,0,1 FROM n;" + "WITH RECURSIVE n(x) AS (SELECT 1 UNION ALL SELECT x+1 FROM n WHERE x<500) " + "INSERT INTO feature_sets(feature_set_id,image_id,feature_asset_id,extractor_kind," + "extractor_version,parameter_fingerprint,source_image_sha256,feature_count," + "descriptor_type,descriptor_dimension,producer_task_id,created_at) SELECT x,x,x," + "'resource',1,randomblob(32),randomblob(32),5,1,32,NULL,1 FROM n;" + "INSERT INTO track_sets(track_set_id,builder_kind,builder_version,parameter_fingerprint," + "verifier_kind,verifier_version,verifier_fingerprint,input_scope_hash,gvr_count," + "track_count,created_at) VALUES(1,'resource',1,zeroblob(32),1,1,zeroblob(32)," + "zeroblob(32),1,%llu,1);" + "WITH RECURSIVE n(x) AS (SELECT 1 UNION ALL SELECT x+1 FROM n WHERE x<%llu) " + "INSERT INTO tracks(track_id,track_set_id,observation_count) SELECT x,1,5 FROM n;" + "WITH RECURSIVE t(x) AS (SELECT 1 UNION ALL SELECT x+1 FROM t WHERE x<%llu), " + "p(x) AS (SELECT 0 UNION ALL SELECT x+1 FROM p WHERE x<4) " + "INSERT INTO track_observations(track_set_id,track_id,feature_set_id,feature_index," + "position_in_track) SELECT 1,t.x,CASE WHEN t.x<=%llu THEN 1+((((t.x-1)%%50)*5+p.x)%%250) " + "ELSE 251+((((t.x-1-%llu)%%50)*5+p.x)%%250) END,CASE WHEN t.x<=%llu " + "THEN ((t.x-1)/50) ELSE ((t.x-1-%llu)/50) END,p.x " + "FROM t CROSS JOIN p;", + (unsigned long long)landmark_count, + (unsigned long long)landmark_count, + (unsigned long long)landmark_count, + (unsigned long long)(landmark_count / 2), + (unsigned long long)(landmark_count / 2), + (unsigned long long)(landmark_count / 2), + (unsigned long long)(landmark_count / 2)); + if (required < 0) + return false; + sql = malloc((size_t)required + 1); + if (!sql) + return false; + (void)snprintf( + sql, (size_t)required + 1, + "PRAGMA foreign_keys=ON;" + "INSERT INTO scansets(scanset_id,name,created_at,updated_at) VALUES(1,'resource',1,1);" + "WITH RECURSIVE n(x) AS (SELECT 1 UNION ALL SELECT x+1 FROM n WHERE x<500) " + "INSERT INTO image_assets(asset_id,sha256,path,size_bytes,state,created_at) " + "SELECT x,randomblob(32),printf('resource/image-%%d',x),1,1,1 FROM n;" + "WITH RECURSIVE n(x) AS (SELECT 1 UNION ALL SELECT x+1 FROM n WHERE x<500) " + "INSERT INTO images(image_id,scanset_id,asset_id,original_name,source_path,producer_task_id,imported_at) " + "SELECT x,1,x,printf('image-%%d',x),printf('resource/image-%%d',x),NULL,1 FROM n;" + "WITH RECURSIVE n(x) AS (SELECT 1 UNION ALL SELECT x+1 FROM n WHERE x<500) " + "INSERT INTO feature_assets(feature_asset_id,sha256,path,size_bytes,durability,created_at) " + "SELECT x,randomblob(32),printf('resource/feature-%%d',x),1,0,1 FROM n;" + "WITH RECURSIVE n(x) AS (SELECT 1 UNION ALL SELECT x+1 FROM n WHERE x<500) " + "INSERT INTO feature_sets(feature_set_id,image_id,feature_asset_id," + "extractor_kind,extractor_version,parameter_fingerprint,source_image_sha256," + "feature_count,descriptor_type,descriptor_dimension,producer_task_id,created_at) " + "SELECT x,x,x,'resource',1,randomblob(32),randomblob(32),5,1,32,NULL,1 FROM n;" + "INSERT INTO track_sets(track_set_id,builder_kind,builder_version," + "parameter_fingerprint,verifier_kind,verifier_version," + "verifier_fingerprint,input_scope_hash,gvr_count,track_count,created_at) " + "VALUES(1,'resource',1,zeroblob(32),1,1,zeroblob(32),zeroblob(32),1,%llu,1);" + "WITH RECURSIVE n(x) AS (SELECT 1 UNION ALL SELECT x+1 FROM n WHERE x<%llu) " + "INSERT INTO tracks(track_id,track_set_id,observation_count) SELECT x,1,5 FROM n;" + "WITH RECURSIVE t(x) AS (SELECT 1 UNION ALL SELECT x+1 FROM t WHERE x<%llu)," + "p(x) AS (SELECT 0 UNION ALL SELECT x+1 FROM p WHERE x<4) " + "INSERT INTO track_observations(track_set_id,track_id,feature_set_id," + "feature_index,position_in_track) SELECT 1,t.x,CASE WHEN t.x<=%llu " + "THEN 1+((((t.x-1)%%50)*5+p.x)%%250) ELSE " + "251+((((t.x-1-%llu)%%50)*5+p.x)%%250) END,CASE WHEN t.x<=%llu " + "THEN ((t.x-1)/50) ELSE ((t.x-1-%llu)/50) END,p.x FROM t CROSS JOIN p;", + (unsigned long long)landmark_count, + (unsigned long long)landmark_count, + (unsigned long long)landmark_count, + (unsigned long long)(landmark_count / 2), + (unsigned long long)(landmark_count / 2), + (unsigned long long)(landmark_count / 2), + (unsigned long long)(landmark_count / 2)); + bool ok = exec_sql(path, sql); + free(sql); + return ok; +} + +static bool publish_fixture(const char *path, uint64_t landmark_count) { + char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]; + Lardon3DProjectDb *db = NULL; + if (lardon3d_project_db_open(path, &db, error) != LARDON3D_PROJECT_DB_OK) + return false; + Lardon3DSparseCalibration calibration = { + .model_kind = 1, .model_version = 1, .width = 4000, .height = 3000, + .fx = 2000, .fy = 2000, .cx = 2000, .cy = 1500, .provenance_kind = 1}; + Lardon3DSparseCalibration stored; + if (lardon3d_sparse_calibration_create(db, &calibration, &stored) != + LARDON3D_PROJECT_DB_OK) + return false; + Lardon3DSparseCalibrationMember *members = calloc(500, sizeof(*members)); + for (size_t index = 0; index < 500; ++index) { + members[index].image_id = index + 1; + members[index].calibration_id = stored.calibration_id; + memcpy(members[index].calibration_hash, stored.scientific_hash, 32); + } + Lardon3DSparseCalibrationScope scope; + bool ok = lardon3d_sparse_calibration_scope_create(db, members, 500, &scope) == + LARDON3D_PROJECT_DB_OK; + free(members); + if (!ok) + fprintf(stderr, "scope or allocation failed\n"); + if (!ok) + return false; + Lardon3DSparseComponent components[2] = { + {.component_key = 1, .registered_image_count = 250, + .landmark_count = landmark_count / 2}, + {.component_key = 251, .registered_image_count = 250, + .landmark_count = landmark_count / 2}}; + Lardon3DSparseRegisteredImage *poses = calloc(500, sizeof(*poses)); + Lardon3DSparseLandmark *landmarks = calloc(landmark_count, sizeof(*landmarks)); + Lardon3DSparseLandmarkObservation *observations = + calloc(landmark_count * 5, sizeof(*observations)); + if (!poses || !landmarks || !observations) + return false; + for (size_t index = 0; index < 500; ++index) + poses[index] = (Lardon3DSparseRegisteredImage){ + .image_id = index + 1, + .component_key = index < 250 ? 1 : 251, + .rotation_cw = {1, 0, 0, 0, 1, 0, 0, 0, 1}, + .translation_cw = {(double)index, 0, 0}}; + for (uint64_t track = 0; track < landmark_count; ++track) { + size_t component_base = track < landmark_count / 2 ? 0 : 250; + uint64_t local_track = track < landmark_count / 2 + ? track + : track - landmark_count / 2; + uint64_t component_key = component_base ? 251 : 1; + landmarks[track] = (Lardon3DSparseLandmark){ + .track_id = track + 1, .component_key = component_key, .x = 0, + .y = 0, .z = 1, .reprojection_rmse_px = 0.1, + .reprojection_median_px = 0.1, .observation_count = 5}; + for (uint32_t position = 0; position < 5; ++position) { + size_t offset = (size_t)track * 5 + position; + uint64_t feature_set = (uint64_t)component_base + + ((local_track % 50) * 5 + position) % 250 + 1; + observations[offset] = (Lardon3DSparseLandmarkObservation){ + .track_id = track + 1, + .feature_set_id = feature_set, + .feature_index = (uint32_t)(local_track / 50), + .position_in_track = position}; + } + } + Lardon3DSparsePublication publication = { + .track_set_id = 1, + .calibration_scope_id = scope.scope_id, + .sfm_kind = 1, + .sfm_version = 1, + .components = components, + .component_count = 2, + .registered_images = poses, + .registered_image_count = 500, + .landmarks = landmarks, + .landmark_count = landmark_count, + .observations = observations, + .observation_count = landmark_count * 5, + .reprojection_rmse_px = 0.1, + .reprojection_median_px = 0.1, + .created_at = 1}; + publication.parameter_fingerprint[0] = 91; + Lardon3DSparseReconstruction reconstruction; + Lardon3DProjectDbResult publication_result = + lardon3d_sparse_reconstruction_publish(db, &publication, &reconstruction); + ok = publication_result == LARDON3D_PROJECT_DB_OK; + if (!ok) { + char detail[LARDON3D_PROJECT_DB_ERROR_CAPACITY]; + (void)lardon3d_project_db_last_error(db, detail); + fprintf(stderr, "publish result %d: %s\n", publication_result, detail); + } + if (ok) + printf("reconstruction_id=%llu landmarks=%llu observations=%llu\n", + (unsigned long long)reconstruction.reconstruction_id, + (unsigned long long)landmark_count, + (unsigned long long)(landmark_count * 5)); + free(poses); + free(landmarks); + free(observations); + lardon3d_project_db_close(db); + return ok; +} + +static bool page_fixture(const char *path, uint64_t reconstruction_id, + unsigned passes) { + char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]; + Lardon3DProjectDb *db = NULL; + if (lardon3d_project_db_open(path, &db, error) != LARDON3D_PROJECT_DB_OK) + return false; + uint64_t totals[4] = {0}; + for (unsigned pass = 0; pass < passes; ++pass) { + Lardon3DSparseComponent components_items[64]; + Lardon3DSparseComponentPage components = {.items = components_items}; + uint64_t cursor = 0; + while (lardon3d_sparse_component_list(db, reconstruction_id, cursor, 64, + &components) == LARDON3D_PROJECT_DB_OK && + components.count) { + totals[0] += components.count; + cursor = components.next_component_key; + } + Lardon3DSparseRegisteredImage cameras[64]; + Lardon3DSparseRegisteredImagePage camera_page = {.items = cameras}; + cursor = 0; + while (lardon3d_sparse_registered_image_list( + db, reconstruction_id, cursor, 64, &camera_page) == + LARDON3D_PROJECT_DB_OK && + camera_page.count) { + totals[1] += camera_page.count; + cursor = camera_page.next_image_id; + } + Lardon3DSparseLandmark landmarks[64]; + Lardon3DSparseLandmarkPage landmark_page = {.items = landmarks}; + cursor = 0; + while (lardon3d_sparse_landmark_list(db, reconstruction_id, cursor, 64, + &landmark_page) == + LARDON3D_PROJECT_DB_OK && + landmark_page.count) { + totals[2] += landmark_page.count; + cursor = landmark_page.next_track_id; + } + Lardon3DSparseLandmarkObservation observations[64]; + Lardon3DSparseObservationPage observation_page = {.items = observations}; + uint64_t landmark_cursor = 0; + uint32_t position_cursor = 0; + while (lardon3d_sparse_observation_list( + db, reconstruction_id, landmark_cursor, position_cursor, 64, + &observation_page) == LARDON3D_PROJECT_DB_OK && + observation_page.count) { + totals[3] += observation_page.count; + landmark_cursor = observation_page.next_landmark_id; + position_cursor = observation_page.next_position_in_track; + } + } + printf("passes=%u components=%llu cameras=%llu landmarks=%llu observations=%llu\n", + passes, (unsigned long long)totals[0], (unsigned long long)totals[1], + (unsigned long long)totals[2], (unsigned long long)totals[3]); + lardon3d_project_db_close(db); + return true; +} + +int main(int argc, char **argv) { + if (argc < 3) + return 2; + const char *mode = argv[1]; + const char *path = argv[2]; + if (strcmp(mode, "publish") == 0) { + uint64_t landmarks = argc > 3 ? strtoull(argv[3], NULL, 10) : 100000; + char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]; + Lardon3DProjectDb *database = NULL; + if (lardon3d_project_db_open(path, &database, error) != + LARDON3D_PROJECT_DB_OK || + !database) { + fprintf(stderr, "open resource database failed: %s\n", error); + return 1; + } + lardon3d_project_db_close(database); + if (!seed_upstream(path, landmarks)) { + fprintf(stderr, "seed upstream failed\n"); + return 1; + } + if (!publish_fixture(path, landmarks)) { + fprintf(stderr, "publish fixture failed\n"); + return 1; + } + return 0; + } + if (strcmp(mode, "page") == 0) { + unsigned passes = argc > 4 ? (unsigned)strtoul(argv[4], NULL, 10) : 5; + return page_fixture(path, argc > 3 ? strtoull(argv[3], NULL, 10) : 1, + passes) + ? 0 + : 1; + } + return 2; +}