lardon3d/src/project_db.c

6628 lines
325 KiB
C

#include <errno.h>
#include <limits.h>
#include <math.h>
#include <pthread.h>
#include <sqlite3.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <time.h>
#include <lardon3d/project_db.h>
#include "project_db_internal.h"
static const char schema_v5[] =
"CREATE TABLE metadata(key TEXT PRIMARY KEY,value INTEGER NOT NULL);"
"INSERT INTO metadata(key,value) VALUES('schema_version',5);"
"INSERT INTO metadata(key,value) VALUES('next_task_id',1);"
"INSERT INTO metadata(key,value) VALUES('legacy_image_catalog_pending',0);"
"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,"
"task_kind TEXT,task_kind_version INTEGER,"
"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 NULL,"
"CHECK((task_kind IS NULL AND task_kind_version IS NULL) OR (task_kind IS NOT NULL AND "
"task_kind_version>0)));"
"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 "
"CASCADE,"
"path TEXT NOT NULL,format_version INTEGER NOT NULL 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 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);"
"CREATE TABLE scansets(scanset_id INTEGER PRIMARY KEY AUTOINCREMENT CHECK(scanset_id>0),"
"name TEXT NOT NULL CHECK(length(name)>0 AND length(name)<256),created_at INTEGER NOT NULL "
"CHECK(created_at>=0),"
"updated_at INTEGER NOT NULL CHECK(updated_at>=created_at));"
"CREATE TABLE image_assets(asset_id INTEGER PRIMARY KEY AUTOINCREMENT CHECK(asset_id>0),sha256 "
"BLOB NOT NULL UNIQUE CHECK(length(sha256)=32),"
"path TEXT NOT NULL UNIQUE CHECK(length(path)>0 AND length(path)<4096),size_bytes INTEGER NOT "
"NULL CHECK(size_bytes>=0),"
"state INTEGER NOT NULL CHECK(state=1),created_at INTEGER NOT NULL CHECK(created_at>=0));"
"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 NULL "
"CHECK(length(original_name)>0 AND length(original_name)<256),"
"source_path TEXT NOT NULL CHECK(length(source_path)>0 AND length(source_path)<4096),"
"producer_task_id INTEGER REFERENCES tasks(task_id),imported_at INTEGER NOT NULL "
"CHECK(imported_at>=0),"
"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);"
"CREATE TABLE image_import_tasks(task_id INTEGER PRIMARY KEY REFERENCES tasks(task_id) ON "
"DELETE CASCADE,"
"source_path TEXT NOT NULL,scanset_id INTEGER NOT NULL REFERENCES scansets(scanset_id));";
static const char schema_feature_v5[] =
"CREATE TABLE feature_assets(feature_asset_id INTEGER PRIMARY KEY AUTOINCREMENT "
"CHECK(feature_asset_id>0),sha256 BLOB NOT NULL UNIQUE CHECK(length(sha256)=32),path TEXT NOT "
"NULL UNIQUE CHECK(length(path)>0 AND length(path)<4096),size_bytes INTEGER NOT NULL "
"CHECK(size_bytes>=0),durability INTEGER NOT NULL CHECK(durability BETWEEN 0 AND 1),created_at "
"INTEGER NOT NULL CHECK(created_at>=0));"
"CREATE TABLE feature_sets(feature_set_id INTEGER PRIMARY KEY AUTOINCREMENT "
"CHECK(feature_set_id>0),image_id INTEGER NOT NULL REFERENCES "
"images(image_id),feature_asset_id INTEGER NOT NULL REFERENCES "
"feature_assets(feature_asset_id),extractor_kind TEXT NOT NULL CHECK(length(extractor_kind)>0 "
"AND length(extractor_kind)<65),extractor_version INTEGER NOT NULL "
"CHECK(extractor_version>0),parameter_fingerprint BLOB NOT NULL "
"CHECK(length(parameter_fingerprint)=32),source_image_sha256 BLOB NOT NULL "
"CHECK(length(source_image_sha256)=32),feature_count INTEGER NOT NULL "
"CHECK(feature_count BETWEEN 0 AND 8192),descriptor_type INTEGER NOT NULL "
"CHECK(descriptor_type "
"IN(1,2)),descriptor_dimension INTEGER NOT NULL "
"CHECK(descriptor_dimension BETWEEN 1 AND 4096),producer_task_id "
"INTEGER REFERENCES tasks(task_id),created_at INTEGER NOT NULL "
"CHECK(created_at>=0),UNIQUE(image_id,extractor_kind,extractor_version,parameter_fingerprint));"
"CREATE INDEX feature_sets_image_idx ON feature_sets(image_id,feature_set_id);"
"CREATE INDEX feature_sets_producer_idx ON feature_sets(producer_task_id,feature_set_id);"
"CREATE TABLE feature_extract_tasks(task_id INTEGER PRIMARY KEY REFERENCES tasks(task_id) ON "
"DELETE CASCADE,image_id INTEGER NOT NULL REFERENCES images(image_id),extractor_kind TEXT NOT "
"NULL,extractor_version INTEGER NOT NULL CHECK(extractor_version>0),max_features INTEGER NOT "
"NULL CHECK(max_features BETWEEN 1 AND 8192),pyramid_levels INTEGER NOT NULL "
"CHECK(pyramid_levels BETWEEN 1 AND 16),fast_threshold INTEGER NOT NULL "
"CHECK(fast_threshold BETWEEN 1 AND 255),parameter_fingerprint BLOB NOT NULL "
"CHECK(length(parameter_fingerprint)=32));";
static const char schema_visual_v6[] =
"CREATE TABLE visual_indexes(visual_index_id INTEGER PRIMARY KEY AUTOINCREMENT "
"CHECK(visual_index_id>0),index_kind TEXT NOT NULL CHECK(length(index_kind)>0 AND "
"length(index_kind)<65),index_version INTEGER NOT NULL CHECK(index_version>0),"
"descriptor_type INTEGER NOT NULL CHECK(descriptor_type IN(1,2)),descriptor_dimension "
"INTEGER NOT NULL CHECK(descriptor_dimension BETWEEN 1 AND 4096),extractor_kind TEXT NOT "
"NULL CHECK(length(extractor_kind)>0 AND length(extractor_kind)<65),extractor_version "
"INTEGER NOT NULL CHECK(extractor_version>0),feature_parameter_fingerprint BLOB NOT NULL "
"CHECK(length(feature_parameter_fingerprint)=32),index_parameter_fingerprint BLOB NOT NULL "
"CHECK(length(index_parameter_fingerprint)=32),table_count INTEGER NOT NULL CHECK(table_count "
"BETWEEN 1 AND 32),key_bits INTEGER NOT NULL CHECK(key_bits BETWEEN 8 AND 32),"
"max_features_per_set INTEGER NOT NULL CHECK(max_features_per_set BETWEEN 1 AND 1024),"
"max_bucket_postings INTEGER NOT NULL CHECK(max_bucket_postings BETWEEN 1 AND 4096),"
"created_at INTEGER NOT NULL CHECK(created_at>=0),UNIQUE(index_kind,index_version,"
"descriptor_type,descriptor_dimension,extractor_kind,extractor_version,"
"feature_parameter_fingerprint,index_parameter_fingerprint));"
"CREATE TABLE visual_index_segments(visual_index_segment_id INTEGER PRIMARY KEY "
"AUTOINCREMENT CHECK(visual_index_segment_id>0),visual_index_id INTEGER NOT NULL REFERENCES "
"visual_indexes(visual_index_id),generation INTEGER NOT NULL CHECK(generation>0),sha256 BLOB "
"NOT NULL CHECK(length(sha256)=32),path TEXT NOT NULL UNIQUE CHECK(length(path)>0 AND "
"length(path)<4096),size_bytes INTEGER NOT NULL CHECK(size_bytes>=128),posting_count INTEGER "
"NOT NULL CHECK(posting_count>=0),feature_set_count INTEGER NOT NULL CHECK(feature_set_count "
"BETWEEN 1 AND 16),durability INTEGER NOT NULL CHECK(durability BETWEEN 0 AND 1),"
"producer_task_id INTEGER REFERENCES tasks(task_id),created_at INTEGER NOT NULL "
"CHECK(created_at>=0),UNIQUE(visual_index_id,generation),UNIQUE(visual_index_id,sha256));"
"CREATE INDEX visual_index_segments_index_idx ON visual_index_segments(visual_index_id,"
"generation);"
"CREATE TABLE visual_index_memberships(visual_index_id INTEGER NOT NULL REFERENCES "
"visual_indexes(visual_index_id),feature_set_id INTEGER NOT NULL REFERENCES "
"feature_sets(feature_set_id),visual_index_segment_id INTEGER NOT NULL REFERENCES "
"visual_index_segments(visual_index_segment_id),PRIMARY KEY(visual_index_id,feature_set_id));"
"CREATE INDEX visual_index_memberships_segment_idx ON visual_index_memberships("
"visual_index_segment_id,feature_set_id);"
"CREATE TABLE visual_index_update_tasks(task_id INTEGER PRIMARY KEY REFERENCES tasks(task_id) "
"ON DELETE CASCADE,visual_index_id INTEGER NOT NULL REFERENCES visual_indexes(visual_index_id),"
"after_feature_set_id INTEGER NOT NULL CHECK(after_feature_set_id>=0));";
static const char schema_precision_v7[] =
"ALTER TABLE feature_sets ADD COLUMN occupied_cells INTEGER NOT NULL DEFAULT 0 "
"CHECK(occupied_cells>=0 AND occupied_cells<=1024);"
"ALTER TABLE feature_sets ADD COLUMN total_cells INTEGER NOT NULL DEFAULT 0 "
"CHECK(total_cells>=0 AND total_cells<=1024 AND occupied_cells<=total_cells);"
"ALTER TABLE feature_sets ADD COLUMN coverage_ratio REAL NOT NULL DEFAULT 0.0 "
"CHECK(coverage_ratio>=0.0 AND coverage_ratio<=1.0);"
"ALTER TABLE feature_sets ADD COLUMN feature_density_per_megapixel REAL NOT NULL DEFAULT 0.0 "
"CHECK(feature_density_per_megapixel>=0.0);"
"CREATE TABLE sift_extract_tasks(task_id INTEGER PRIMARY KEY REFERENCES tasks(task_id) ON "
"DELETE CASCADE,image_id INTEGER NOT NULL REFERENCES images(image_id),extractor_kind TEXT NOT "
"NULL CHECK(extractor_kind IN('sift','rootsift')),extractor_version INTEGER NOT NULL "
"CHECK(extractor_version=1),max_features INTEGER NOT NULL CHECK(max_features BETWEEN 1 AND "
"8192),octave_layers INTEGER NOT NULL CHECK(octave_layers BETWEEN 1 AND 8),"
"contrast_threshold REAL NOT NULL CHECK(contrast_threshold BETWEEN 0.001 AND 0.2),"
"edge_threshold REAL NOT NULL CHECK(edge_threshold BETWEEN 1.0 AND 100.0),sigma REAL NOT NULL "
"CHECK(sigma BETWEEN 0.5 AND 3.0),grid_rows INTEGER NOT NULL CHECK(grid_rows BETWEEN 1 AND 32),"
"grid_cols INTEGER NOT NULL CHECK(grid_cols BETWEEN 1 AND 32),max_features_per_cell INTEGER "
"NOT NULL CHECK(max_features_per_cell BETWEEN 1 AND 8192),parameter_fingerprint BLOB NOT NULL "
"CHECK(length(parameter_fingerprint)=32));"
"CREATE TABLE feature_support_sets(feature_support_set_id INTEGER PRIMARY KEY AUTOINCREMENT "
"CHECK(feature_support_set_id>0),image_id INTEGER NOT NULL REFERENCES images(image_id),"
"first_feature_set_id INTEGER NOT NULL REFERENCES feature_sets(feature_set_id),"
"second_feature_set_id INTEGER NOT NULL REFERENCES feature_sets(feature_set_id),radius_pixels "
"REAL NOT NULL CHECK(radius_pixels>0.0 AND radius_pixels<=64.0),parameter_fingerprint BLOB NOT "
"NULL CHECK(length(parameter_fingerprint)=32),created_at INTEGER NOT NULL CHECK(created_at>=0),"
"UNIQUE(image_id,first_feature_set_id,second_feature_set_id,parameter_fingerprint));"
"CREATE TABLE feature_support_groups(feature_support_group_id INTEGER PRIMARY KEY AUTOINCREMENT "
"CHECK(feature_support_group_id>0),feature_support_set_id INTEGER NOT NULL REFERENCES "
"feature_support_sets(feature_support_set_id) ON DELETE CASCADE,x REAL NOT NULL,y REAL NOT NULL,"
"distance_pixels REAL NOT NULL CHECK(distance_pixels>=0.0 AND distance_pixels<=64.0),"
"support_count INTEGER NOT NULL CHECK(support_count BETWEEN 1 AND 3));"
"CREATE INDEX feature_support_groups_set_idx ON feature_support_groups(feature_support_set_id,"
"feature_support_group_id);"
"CREATE TABLE feature_support_members(feature_support_group_id INTEGER NOT NULL REFERENCES "
"feature_support_groups(feature_support_group_id) ON DELETE CASCADE,feature_set_id INTEGER NOT "
"NULL REFERENCES feature_sets(feature_set_id),feature_index INTEGER NOT NULL CHECK(feature_index "
"BETWEEN 0 AND 8191),extractor_kind TEXT NOT NULL CHECK(length(extractor_kind)>0 AND "
"length(extractor_kind)<65),PRIMARY KEY(feature_support_group_id,feature_set_id));";
static const char schema_candidate_v8[] =
"CREATE TABLE candidate_pairs(candidate_pair_id INTEGER PRIMARY KEY AUTOINCREMENT "
"CHECK(candidate_pair_id>0),image_id_a INTEGER NOT NULL REFERENCES images(image_id),"
"image_id_b INTEGER NOT NULL REFERENCES images(image_id),created_at INTEGER NOT NULL "
"CHECK(created_at>=0),CHECK(image_id_a<image_id_b),UNIQUE(image_id_a,image_id_b));"
"CREATE INDEX candidate_pairs_image_a_idx ON candidate_pairs(image_id_a);"
"CREATE INDEX candidate_pairs_image_b_idx ON candidate_pairs(image_id_b);";
static const char schema_candidate_generate_v9[] =
"CREATE TABLE candidate_pair_generate_tasks(task_id INTEGER PRIMARY KEY REFERENCES "
"tasks(task_id) ON DELETE CASCADE,"
"visual_index_id INTEGER NOT NULL CHECK(visual_index_id>0),"
"after_feature_set_id INTEGER NOT NULL CHECK(after_feature_set_id>=0),"
"top_k INTEGER NOT NULL CHECK(top_k>0 AND top_k<=256),"
"minimum_evidence_count INTEGER NOT NULL CHECK(minimum_evidence_count>=0 "
"AND minimum_evidence_count<=1024),"
"scanset_filter INTEGER NOT NULL CHECK(scanset_filter>=0 AND scanset_filter<=2),"
"exclude_same_asset INTEGER NOT NULL CHECK(exclude_same_asset IN (0,1)));";
static const char schema_match_result_v10[] =
"CREATE TABLE match_results("
"match_result_id INTEGER PRIMARY KEY AUTOINCREMENT CHECK(match_result_id>0),"
"candidate_pair_id INTEGER NOT NULL REFERENCES candidate_pairs(candidate_pair_id),"
"feature_set_id_a INTEGER NOT NULL REFERENCES feature_sets(feature_set_id),"
"feature_set_id_b INTEGER NOT NULL REFERENCES feature_sets(feature_set_id),"
"matcher_kind TEXT NOT NULL CHECK(length(matcher_kind)>0 AND length(matcher_kind)<=64),"
"matcher_version INTEGER NOT NULL CHECK(matcher_version>0),"
"parameter_fingerprint BLOB NOT NULL CHECK(length(parameter_fingerprint)=32),"
"result_status INTEGER NOT NULL CHECK(result_status IN (0,1)),"
"match_count INTEGER NOT NULL CHECK(match_count>=0 AND match_count<=8192),"
"match_asset_sha256 BLOB CHECK(match_asset_sha256 IS NULL OR"
" length(match_asset_sha256)=32),"
"match_asset_path TEXT CHECK(match_asset_path IS NULL OR length(match_asset_path)>0),"
"match_asset_size_bytes INTEGER CHECK(match_asset_size_bytes IS NULL OR"
" match_asset_size_bytes>0),"
"created_at INTEGER NOT NULL CHECK(created_at>=0),"
"CHECK((result_status=0 AND match_count=0 AND match_asset_sha256 IS NULL AND "
"match_asset_path IS NULL AND match_asset_size_bytes IS NULL) OR "
"(result_status=1 AND match_count>0 AND match_asset_sha256 IS NOT NULL AND "
"match_asset_path IS NOT NULL AND match_asset_size_bytes IS NOT NULL)),"
"UNIQUE(candidate_pair_id,feature_set_id_a,feature_set_id_b,"
"matcher_kind,matcher_version,parameter_fingerprint));"
"CREATE INDEX match_results_candidate_pair_idx ON match_results(candidate_pair_id);"
"CREATE INDEX match_results_feature_set_a_idx ON match_results(feature_set_id_a);"
"CREATE INDEX match_results_feature_set_b_idx ON match_results(feature_set_id_b);";
static const char schema_matcher_task_v11[] =
"CREATE TABLE matcher_tasks("
"task_id INTEGER PRIMARY KEY REFERENCES tasks(task_id) ON DELETE CASCADE,"
"after_candidate_pair_id INTEGER NOT NULL CHECK(after_candidate_pair_id>=0),"
"feature_extractor_kind TEXT NOT NULL CHECK(length(feature_extractor_kind)>0 AND "
"length(feature_extractor_kind)<65),"
"feature_extractor_version INTEGER NOT NULL CHECK(feature_extractor_version>0),"
"feature_parameter_fingerprint BLOB NOT NULL "
"CHECK(length(feature_parameter_fingerprint)=32),"
"matcher_kind INTEGER NOT NULL CHECK(matcher_kind BETWEEN 0 AND 2),"
"ratio_threshold REAL NOT NULL CHECK(ratio_threshold>0.0 AND ratio_threshold<1.0));";
static const char schema_geometric_verification_v12[] =
"CREATE TABLE geometric_verification_results("
"geometric_verification_result_id INTEGER PRIMARY KEY AUTOINCREMENT "
"CHECK(geometric_verification_result_id>0),"
"match_result_id INTEGER NOT NULL REFERENCES match_results(match_result_id) ON DELETE CASCADE,"
"verifier_kind INTEGER NOT NULL CHECK(verifier_kind=1),"
"verifier_version INTEGER NOT NULL CHECK(verifier_version>0 AND verifier_version<=4294967295),"
"parameter_fingerprint BLOB NOT NULL CHECK(length(parameter_fingerprint)=32),"
"status INTEGER NOT NULL CHECK(status IN (1,2)),"
"inlier_count INTEGER NOT NULL CHECK(inlier_count>=0 AND inlier_count<=8192),"
"inlier_mask BLOB NOT NULL CHECK(length(inlier_mask)>=1 AND length(inlier_mask)<=1024),"
"model_m00 REAL,model_m01 REAL,model_m02 REAL,"
"model_m10 REAL,model_m11 REAL,model_m12 REAL,"
"model_m20 REAL,model_m21 REAL,model_m22 REAL,"
"created_at INTEGER NOT NULL CHECK(created_at>=0),"
"CHECK((status=1 AND model_m00 IS NULL AND model_m01 IS NULL AND model_m02 IS NULL AND "
"model_m10 IS NULL AND model_m11 IS NULL AND model_m12 IS NULL AND model_m20 IS NULL AND "
"model_m21 IS NULL AND model_m22 IS NULL) OR "
"(status=2 AND model_m00 IS NOT NULL AND model_m01 IS NOT NULL AND model_m02 IS NOT NULL AND "
"model_m10 IS NOT NULL AND model_m11 IS NOT NULL AND model_m12 IS NOT NULL AND "
"model_m20 IS NOT NULL AND model_m21 IS NOT NULL AND model_m22 IS NOT NULL)),"
"UNIQUE(match_result_id,verifier_kind,verifier_version,parameter_fingerprint));"
"CREATE INDEX geometric_verification_results_parent_idx ON "
"geometric_verification_results(match_result_id,geometric_verification_result_id);";
static const char schema_track_v14[] =
"CREATE TABLE track_sets(track_set_id INTEGER PRIMARY KEY AUTOINCREMENT "
"CHECK(track_set_id>0),builder_kind TEXT NOT NULL CHECK(length(builder_kind)>0 AND "
"length(builder_kind)<=64),builder_version INTEGER NOT NULL CHECK(builder_version>0),"
"parameter_fingerprint BLOB NOT NULL CHECK(length(parameter_fingerprint)=32),"
"verifier_kind INTEGER NOT NULL CHECK(verifier_kind>0),verifier_version INTEGER NOT NULL "
"CHECK(verifier_version>0),verifier_fingerprint BLOB NOT NULL "
"CHECK(length(verifier_fingerprint)=32),input_scope_hash BLOB NOT NULL "
"CHECK(length(input_scope_hash)=32),gvr_count INTEGER NOT NULL CHECK(gvr_count>=1),"
"track_count INTEGER NOT NULL CHECK(track_count>=0),created_at INTEGER NOT NULL "
"CHECK(created_at>=0),UNIQUE(builder_kind,builder_version,parameter_fingerprint,"
"verifier_kind,verifier_version,verifier_fingerprint,input_scope_hash));"
"CREATE TABLE tracks(track_id INTEGER PRIMARY KEY AUTOINCREMENT CHECK(track_id>0),"
"track_set_id INTEGER NOT NULL REFERENCES track_sets(track_set_id) ON DELETE CASCADE,"
"observation_count INTEGER NOT NULL CHECK(observation_count>=2));"
"CREATE INDEX tracks_set_idx ON tracks(track_set_id,track_id);"
"CREATE TABLE track_observations(track_set_id INTEGER NOT NULL,"
"track_id INTEGER NOT NULL REFERENCES tracks(track_id) ON DELETE CASCADE,"
"feature_set_id INTEGER NOT NULL REFERENCES feature_sets(feature_set_id),"
"feature_index INTEGER NOT NULL CHECK(feature_index>=0),"
"position_in_track INTEGER NOT NULL CHECK(position_in_track>=0),"
"PRIMARY KEY(track_set_id,feature_set_id,feature_index),"
"UNIQUE(track_id,position_in_track));"
"CREATE INDEX track_observations_lookup_idx ON "
"track_observations(feature_set_id,feature_index,track_set_id);";
static const char schema_geometric_verifier_task_v13[] =
"CREATE TABLE geometric_verifier_tasks("
"task_id INTEGER PRIMARY KEY REFERENCES tasks(task_id) ON DELETE CASCADE,"
"after_match_result_id INTEGER NOT NULL CHECK(after_match_result_id>=0),"
"threshold_pixels REAL NOT NULL CHECK(threshold_pixels>0.0),"
"confidence REAL NOT NULL CHECK(confidence>0.0 AND confidence<1.0),"
"max_iterations INTEGER NOT NULL CHECK(max_iterations>0 AND max_iterations<=2147483647),"
"min_inlier_count INTEGER NOT NULL CHECK(min_inlier_count BETWEEN 1 AND 8192),"
"min_inlier_ratio REAL NOT NULL CHECK(min_inlier_ratio>=0.0 AND min_inlier_ratio<=1.0),"
"seed_policy_version INTEGER NOT NULL CHECK(seed_policy_version>0),"
"canonicalization_version INTEGER NOT NULL CHECK(canonicalization_version>0),"
"parameter_fingerprint BLOB NOT NULL CHECK(length(parameter_fingerprint)=32));";
static const char schema_track_builder_task_v15[] =
"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));";
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 const char schema_sparse_sfm_task_v17[] =
"CREATE TABLE sparse_sfm_tasks("
"task_id INTEGER PRIMARY KEY REFERENCES tasks(task_id) ON DELETE CASCADE,"
"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),"
"minimum_seed_tracks INTEGER NOT NULL,minimum_seed_landmarks INTEGER NOT NULL,"
"minimum_pnp_correspondences INTEGER NOT NULL,maximum_seed_candidates INTEGER NOT NULL,"
"maximum_registration_rounds INTEGER NOT NULL,maximum_landmarks_per_round INTEGER NOT NULL,"
"maximum_images INTEGER NOT NULL,maximum_observations BLOB NOT NULL "
"CHECK(length(maximum_observations)=8),maximum_tracks BLOB NOT NULL "
"CHECK(length(maximum_tracks)=8),reprojection_threshold_px REAL NOT NULL,"
"minimum_track_parallax_rad REAL NOT NULL,relative_robust_threshold_px REAL NOT NULL,"
"relative_confidence REAL NOT NULL,relative_max_iterations INTEGER NOT NULL,"
"relative_minimum_inliers INTEGER NOT NULL,relative_minimum_inlier_ratio REAL NOT NULL,"
"relative_minimum_parallax_rad REAL NOT NULL,relative_minimum_cheirality_ratio REAL NOT NULL,"
"relative_deterministic_seed BLOB NOT NULL CHECK(length(relative_deterministic_seed)=8),"
"pnp_reprojection_threshold_px REAL NOT NULL,pnp_confidence REAL NOT NULL,"
"pnp_max_iterations INTEGER NOT NULL,pnp_minimum_inliers INTEGER NOT NULL,"
"pnp_minimum_inlier_ratio REAL NOT NULL,pnp_deterministic_seed BLOB NOT NULL "
"CHECK(length(pnp_deterministic_seed)=8),refinement_max_iterations INTEGER NOT NULL,"
"refinement_convergence_tolerance REAL NOT NULL);";
static const char schema_sparse_derivation_identity_v18[] =
"PRAGMA legacy_alter_table=ON;"
"DROP INDEX sparse_landmark_observations_page_idx;"
"DROP INDEX sparse_landmark_track_lookup_idx;"
"DROP INDEX sparse_landmarks_page_idx;"
"DROP INDEX sparse_registered_image_lookup_idx;"
"DROP INDEX sparse_registered_page_idx;"
"DROP INDEX sparse_components_page_idx;"
"DROP INDEX sparse_reconstructions_scope_idx;"
"DROP INDEX IF EXISTS sparse_reconstructions_gate_f_candidate_idx;"
"DROP INDEX IF EXISTS sparse_reconstructions_derivation_idx;"
"ALTER TABLE sparse_landmark_observations RENAME TO sparse_landmark_observations_v17;"
"ALTER TABLE sparse_landmarks RENAME TO sparse_landmarks_v17;"
"ALTER TABLE sparse_registered_images RENAME TO sparse_registered_images_v17;"
"ALTER TABLE sparse_reconstruction_components RENAME TO sparse_reconstruction_components_v17;"
"ALTER TABLE sparse_reconstructions RENAME TO sparse_reconstructions_v17;"
"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),derivation_identity BLOB "
"CHECK(derivation_identity IS NULL OR length(derivation_identity)=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);"
"INSERT INTO sparse_reconstructions("
"reconstruction_id,track_set_id,calibration_scope_id,sfm_kind,sfm_version,"
"parameter_fingerprint,derivation_identity,component_count,"
"registered_image_count,landmark_count,reprojection_rmse_px,"
"reprojection_median_px,created_at) SELECT reconstruction_id,track_set_id,"
"calibration_scope_id,sfm_kind,sfm_version,parameter_fingerprint,NULL,"
"component_count,registered_image_count,landmark_count,reprojection_rmse_px,"
"reprojection_median_px,created_at FROM sparse_reconstructions_v17;";
static const char schema_sparse_derivation_children_v18[] =
"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));"
"INSERT INTO sparse_reconstruction_components SELECT * FROM "
"sparse_reconstruction_components_v17;"
"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));"
"INSERT INTO sparse_registered_images SELECT * FROM sparse_registered_images_v17;"
"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));"
"INSERT INTO sparse_landmarks SELECT * FROM sparse_landmarks_v17;"
"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));"
"INSERT INTO sparse_landmark_observations SELECT * FROM "
"sparse_landmark_observations_v17;"
"DROP TABLE sparse_landmark_observations_v17;"
"DROP TABLE sparse_landmarks_v17;"
"DROP TABLE sparse_registered_images_v17;"
"DROP TABLE sparse_reconstruction_components_v17;"
"DROP TABLE sparse_reconstructions_v17;"
"CREATE UNIQUE INDEX sparse_reconstructions_gate_f_candidate_idx ON "
"sparse_reconstructions(track_set_id,calibration_scope_id,sfm_kind,sfm_version,"
"parameter_fingerprint) WHERE derivation_identity IS NULL;"
"CREATE UNIQUE INDEX sparse_reconstructions_derivation_idx ON "
"sparse_reconstructions(derivation_identity) WHERE derivation_identity IS NOT NULL;"
"CREATE INDEX sparse_reconstructions_scope_idx ON "
"sparse_reconstructions(calibration_scope_id);"
"CREATE INDEX sparse_components_page_idx ON sparse_reconstruction_components("
"reconstruction_id,component_key);"
"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 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 INDEX sparse_landmark_observations_page_idx ON "
"sparse_landmark_observations(landmark_id,position_in_track);"
"PRAGMA legacy_alter_table=OFF;";
static const char schema_incremental_reconstruction_v18[] =
"CREATE TABLE incremental_reconstructions("
"reconstruction_id INTEGER PRIMARY KEY REFERENCES sparse_reconstructions("
"reconstruction_id) ON DELETE CASCADE,"
"base_reconstruction_id INTEGER NOT NULL REFERENCES sparse_reconstructions("
"reconstruction_id),"
"extension_track_set_id INTEGER NOT NULL REFERENCES track_sets(track_set_id),"
"calibration_scope_id INTEGER NOT NULL REFERENCES sparse_calibration_scopes(scope_id),"
"incremental_kind INTEGER NOT NULL CHECK(incremental_kind=1),"
"incremental_version INTEGER NOT NULL CHECK(incremental_version=1),"
"parameter_fingerprint BLOB NOT NULL CHECK(length(parameter_fingerprint)=32),"
"scientific_identity BLOB NOT NULL CHECK(length(scientific_identity)=32) UNIQUE,"
"UNIQUE(base_reconstruction_id,extension_track_set_id,calibration_scope_id,"
"incremental_kind,incremental_version,parameter_fingerprint));"
"CREATE INDEX incremental_reconstructions_base_idx ON incremental_reconstructions("
"base_reconstruction_id,reconstruction_id);"
"CREATE TABLE incremental_reconstruction_tasks("
"task_id INTEGER PRIMARY KEY REFERENCES tasks(task_id) ON DELETE CASCADE,"
"base_reconstruction_id INTEGER NOT NULL REFERENCES sparse_reconstructions("
"reconstruction_id),"
"extension_track_set_id INTEGER NOT NULL REFERENCES track_sets(track_set_id),"
"calibration_scope_id INTEGER NOT NULL REFERENCES sparse_calibration_scopes(scope_id),"
"incremental_kind INTEGER NOT NULL CHECK(incremental_kind=1),"
"incremental_version INTEGER NOT NULL CHECK(incremental_version=1),"
"parameter_fingerprint BLOB NOT NULL CHECK(length(parameter_fingerprint)=32));";
static const char schema_capture_provenance_v19[] =
"CREATE TABLE captures("
"capture_id INTEGER PRIMARY KEY AUTOINCREMENT CHECK(capture_id>0),"
"scanset_id INTEGER NOT NULL REFERENCES scansets(scanset_id),"
"created_at INTEGER NOT NULL CHECK(created_at>=0));"
"CREATE INDEX captures_scanset_idx ON captures(scanset_id,capture_id);"
"CREATE TABLE capture_images("
"capture_id INTEGER NOT NULL REFERENCES captures(capture_id) ON DELETE CASCADE,"
"image_id INTEGER NOT NULL UNIQUE REFERENCES images(image_id),"
"PRIMARY KEY(capture_id,image_id));"
"CREATE TABLE capture_assets("
"capture_id INTEGER NOT NULL REFERENCES captures(capture_id) ON DELETE CASCADE,"
"asset_id INTEGER NOT NULL REFERENCES image_assets(asset_id),"
"role INTEGER NOT NULL CHECK(role IN(1,2)),"
"PRIMARY KEY(capture_id,asset_id));"
"CREATE INDEX capture_assets_page_idx ON capture_assets(capture_id,asset_id);"
"CREATE TABLE capture_selections("
"capture_id INTEGER PRIMARY KEY REFERENCES captures(capture_id) ON DELETE CASCADE,"
"image_id INTEGER NOT NULL UNIQUE REFERENCES images(image_id),"
"FOREIGN KEY(capture_id,image_id) REFERENCES capture_images(capture_id,image_id));"
"CREATE TABLE asset_derivations("
"child_asset_id INTEGER PRIMARY KEY REFERENCES image_assets(asset_id),"
"parent_asset_id INTEGER NOT NULL REFERENCES image_assets(asset_id),"
"derivation_kind INTEGER NOT NULL CHECK(derivation_kind=1),"
"derivation_version INTEGER NOT NULL CHECK(derivation_version>0),"
"parameter_fingerprint BLOB NOT NULL CHECK(length(parameter_fingerprint)=32),"
"producer_task_id INTEGER REFERENCES tasks(task_id),"
"created_at INTEGER NOT NULL CHECK(created_at>=0),"
"CHECK(parent_asset_id!=child_asset_id));";
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 : "");
}
}
Lardon3DProjectDbResult sqlite_result(Lardon3DProjectDb *database, int code,
const char *context) {
(void)snprintf(database->error, sizeof(database->error), "%s: %s", context,
sqlite3_errmsg(database->connection));
if (code == SQLITE_BUSY || code == SQLITE_LOCKED) {
return LARDON3D_PROJECT_DB_BUSY;
}
if (code == SQLITE_CONSTRAINT) {
return LARDON3D_PROJECT_DB_CONSTRAINT;
}
if (code == SQLITE_CORRUPT || code == SQLITE_NOTADB) {
return LARDON3D_PROJECT_DB_CORRUPT;
}
return LARDON3D_PROJECT_DB_IO_ERROR;
}
static bool bounded_text(const char *text, size_t capacity, bool allow_empty) {
if (!text) {
return false;
}
size_t length = strnlen(text, capacity);
return length < capacity && (allow_empty || length > 0);
}
static bool valid_task_id(uint64_t id) { return id > 0 && id <= INT64_MAX; }
static bool valid_state(Lardon3DTaskState state) {
return state >= TASK_PENDING && state <= TASK_COMPLETED;
}
static bool database_time(sqlite3_int64 value, time_t *output) {
if (value < 0) {
return false;
}
time_t converted = (time_t)value;
if (converted < 0 || (uint64_t)converted != (uint64_t)value) {
return false;
}
*output = converted;
return true;
}
Lardon3DProjectDbResult execute(Lardon3DProjectDb *database, const char *sql,
const char *context) {
char *message = NULL;
int code = sqlite3_exec(database->connection, sql, NULL, NULL, &message);
if (code == SQLITE_OK) {
return LARDON3D_PROJECT_DB_OK;
}
(void)snprintf(database->error, sizeof(database->error), "%s: %s", context,
message ? message : sqlite3_errmsg(database->connection));
sqlite3_free(message);
return sqlite_result(database, code, context);
}
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");
}
static bool table_exists(sqlite3 *connection, const char *name) {
sqlite3_stmt *statement = NULL;
if (sqlite3_prepare_v2(connection, "SELECT 1 FROM sqlite_master WHERE type='table' AND name=?1",
-1, &statement, NULL) != SQLITE_OK) {
return false;
}
(void)sqlite3_bind_text(statement, 1, name, -1, SQLITE_STATIC);
bool exists = sqlite3_step(statement) == SQLITE_ROW;
(void)sqlite3_finalize(statement);
return exists;
}
static Lardon3DProjectDbResult migrate(Lardon3DProjectDb *database, unsigned int from_version) {
if (from_version > LARDON3D_PROJECT_DB_SCHEMA_VERSION) {
copy_error(database->error, "Version de schéma future non supportée.");
return LARDON3D_PROJECT_DB_UNSUPPORTED_SCHEMA;
}
if (from_version == LARDON3D_PROJECT_DB_SCHEMA_VERSION) {
return LARDON3D_PROJECT_DB_OK;
}
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 != 15 && from_version != 16 && from_version != 17 && from_version != 18) {
return LARDON3D_PROJECT_DB_CORRUPT;
}
Lardon3DProjectDbResult result = execute(database, "BEGIN IMMEDIATE", "begin migration");
if (result == LARDON3D_PROJECT_DB_OK && from_version == 0) {
result = execute(database, schema_v5, "create schema v5");
if (result == LARDON3D_PROJECT_DB_OK) {
result = execute(database, schema_feature_v5, "create feature schema v5");
}
if (result == LARDON3D_PROJECT_DB_OK) {
result = execute(database, schema_visual_v6, "create visual index schema v6");
}
if (result == LARDON3D_PROJECT_DB_OK) {
result = execute(database,
"UPDATE metadata SET value=6 WHERE key='schema_version' AND value=5",
"finish new schema v6");
}
}
if (result == LARDON3D_PROJECT_DB_OK && from_version == 1) {
result = execute(database,
"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)",
"migrate schema v1 to v2");
#ifdef LARDON3D_PROJECT_DB_TESTING
const char *forced_failure = getenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V2");
if (result == LARDON3D_PROJECT_DB_OK && forced_failure && strcmp(forced_failure, "1") == 0) {
result =
execute(database, "INSERT INTO missing_test_table VALUES(1)", "forced migration failure");
}
#endif
if (result == LARDON3D_PROJECT_DB_OK) {
result =
execute(database, "UPDATE metadata SET value=2 WHERE key='schema_version' AND value=1",
"finish schema v2 migration");
}
}
if (result == LARDON3D_PROJECT_DB_OK && from_version != 0 && from_version < 3) {
result = execute(database,
"CREATE TABLE image_import_tasks(task_id INTEGER PRIMARY KEY REFERENCES "
"tasks(task_id) ON DELETE CASCADE,source_path TEXT NOT NULL);"
"INSERT INTO metadata(key,value) VALUES('next_task_id',(SELECT CASE WHEN "
"COALESCE(MAX(task_id),0)>=9223372036854775807 THEN 0 ELSE "
"COALESCE(MAX(task_id),0)+1 END FROM tasks))",
"migrate schema v2 to v3");
#ifdef LARDON3D_PROJECT_DB_TESTING
const char *forced_failure = getenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V3");
if (result == LARDON3D_PROJECT_DB_OK && forced_failure && strcmp(forced_failure, "1") == 0) {
result =
execute(database, "INSERT INTO missing_test_table VALUES(1)", "forced migration failure");
}
#endif
if (result == LARDON3D_PROJECT_DB_OK) {
result =
execute(database, "UPDATE metadata SET value=3 WHERE key='schema_version' AND value=2",
"finish schema v3 migration");
}
}
if (result == LARDON3D_PROJECT_DB_OK && from_version != 0 && from_version < 4) {
result = execute(
database,
"CREATE TABLE scansets(scanset_id INTEGER PRIMARY KEY AUTOINCREMENT "
"CHECK(scanset_id>0),name TEXT NOT NULL CHECK(length(name)>0 AND "
"length(name)<256),created_at INTEGER NOT NULL CHECK(created_at>=0),updated_at INTEGER "
"NOT NULL CHECK(updated_at>=created_at));"
"CREATE TABLE image_assets(asset_id INTEGER PRIMARY KEY AUTOINCREMENT "
"CHECK(asset_id>0),sha256 BLOB NOT NULL UNIQUE CHECK(length(sha256)=32),path TEXT NOT "
"NULL UNIQUE CHECK(length(path)>0 AND length(path)<4096),size_bytes INTEGER NOT NULL "
"CHECK(size_bytes>=0),state INTEGER NOT NULL CHECK(state=1),created_at INTEGER NOT "
"NULL CHECK(created_at>=0));"
"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 NULL CHECK(length(original_name)>0 AND "
"length(original_name)<256),source_path TEXT NOT NULL CHECK(length(source_path)>0 AND "
"length(source_path)<4096),producer_task_id INTEGER REFERENCES "
"tasks(task_id),imported_at INTEGER NOT NULL "
"CHECK(imported_at>=0),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 "
"scansets(scanset_id);"
"INSERT INTO scansets(name,created_at,updated_at) SELECT 'Imports antérieurs à ScanSet "
"v1',0,0 WHERE EXISTS(SELECT 1 FROM image_import_tasks);"
"UPDATE image_import_tasks SET scanset_id=(SELECT scanset_id FROM scansets WHERE "
"name='Imports antérieurs à ScanSet v1' ORDER BY scanset_id LIMIT 1) WHERE scanset_id "
"IS NULL;"
"INSERT INTO metadata(key,value) VALUES('legacy_image_catalog_pending',CASE WHEN "
"EXISTS(SELECT 1 FROM image_import_tasks) THEN 1 ELSE 0 END)",
"migrate schema v3 to v4");
#ifdef LARDON3D_PROJECT_DB_TESTING
const char *forced_failure = getenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V4");
if (result == LARDON3D_PROJECT_DB_OK && forced_failure && strcmp(forced_failure, "1") == 0) {
result =
execute(database, "INSERT INTO missing_test_table VALUES(1)", "forced migration failure");
}
#endif
if (result == LARDON3D_PROJECT_DB_OK) {
result =
execute(database, "UPDATE metadata SET value=4 WHERE key='schema_version' AND value=3",
"finish schema v4 migration");
}
}
if (result == LARDON3D_PROJECT_DB_OK && from_version != 0 && from_version < 5) {
result = execute(
database,
"CREATE TABLE feature_assets(feature_asset_id INTEGER PRIMARY KEY AUTOINCREMENT "
"CHECK(feature_asset_id>0),sha256 BLOB NOT NULL UNIQUE CHECK(length(sha256)=32),path "
"TEXT NOT NULL UNIQUE CHECK(length(path)>0 AND length(path)<4096),size_bytes INTEGER "
"NOT NULL CHECK(size_bytes>=0),durability INTEGER NOT NULL CHECK(durability BETWEEN 0 "
"AND 1),created_at INTEGER NOT NULL CHECK(created_at>=0));"
"CREATE TABLE feature_sets(feature_set_id INTEGER PRIMARY KEY AUTOINCREMENT "
"CHECK(feature_set_id>0),image_id INTEGER NOT NULL REFERENCES "
"images(image_id),feature_asset_id INTEGER NOT NULL REFERENCES "
"feature_assets(feature_asset_id),extractor_kind TEXT NOT NULL "
"CHECK(length(extractor_kind)>0 AND length(extractor_kind)<65),extractor_version "
"INTEGER NOT NULL CHECK(extractor_version>0),parameter_fingerprint BLOB NOT NULL "
"CHECK(length(parameter_fingerprint)=32),source_image_sha256 BLOB NOT NULL "
"CHECK(length(source_image_sha256)=32),feature_count INTEGER NOT NULL "
"CHECK(feature_count BETWEEN 0 AND 8192),descriptor_type INTEGER NOT NULL "
"CHECK(descriptor_type "
"IN(1,2)),descriptor_dimension INTEGER NOT NULL "
"CHECK(descriptor_dimension BETWEEN 1 AND 4096),producer_task_id INTEGER REFERENCES "
"tasks(task_id),created_at INTEGER NOT NULL "
"CHECK(created_at>=0),UNIQUE(image_id,extractor_kind,extractor_version,parameter_"
"fingerprint));"
"CREATE INDEX feature_sets_image_idx ON feature_sets(image_id,feature_set_id);"
"CREATE INDEX feature_sets_producer_idx ON "
"feature_sets(producer_task_id,feature_set_id);"
"CREATE TABLE feature_extract_tasks(task_id INTEGER PRIMARY KEY REFERENCES "
"tasks(task_id) ON DELETE CASCADE,image_id INTEGER NOT NULL REFERENCES "
"images(image_id),extractor_kind TEXT NOT NULL,extractor_version INTEGER NOT NULL "
"CHECK(extractor_version>0),max_features INTEGER NOT NULL "
"CHECK(max_features BETWEEN 1 AND 8192),pyramid_levels INTEGER NOT NULL "
"CHECK(pyramid_levels BETWEEN 1 AND 16),fast_threshold INTEGER NOT NULL "
"CHECK(fast_threshold BETWEEN 1 AND 255),parameter_fingerprint BLOB NOT NULL "
"CHECK(length(parameter_fingerprint)=32));",
"migrate schema v4 to v5");
#ifdef LARDON3D_PROJECT_DB_TESTING
const char *forced_failure = getenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V5");
if (result == LARDON3D_PROJECT_DB_OK && forced_failure && strcmp(forced_failure, "1") == 0) {
result = execute(database, "INSERT INTO missing_test_table VALUES(1)",
"forced migration v5 failure");
}
#endif
if (result == LARDON3D_PROJECT_DB_OK) {
result =
execute(database, "UPDATE metadata SET value=5 WHERE key='schema_version' AND value=4",
"finish schema v5 migration");
}
}
if (result == LARDON3D_PROJECT_DB_OK && from_version != 0 && from_version < 6) {
result = execute(database, schema_visual_v6, "migrate schema v5 to v6");
#ifdef LARDON3D_PROJECT_DB_TESTING
const char *forced_failure = getenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V6");
if (result == LARDON3D_PROJECT_DB_OK && forced_failure && strcmp(forced_failure, "1") == 0) {
result = execute(database, "INSERT INTO missing_test_table VALUES(1)",
"forced migration v6 failure");
}
#endif
if (result == LARDON3D_PROJECT_DB_OK) {
result = execute(database,
"UPDATE metadata SET value=6 WHERE key='schema_version' AND value=5",
"finish schema v6 migration");
}
}
if (result == LARDON3D_PROJECT_DB_OK && from_version < 7) {
result = execute(database, schema_precision_v7, "migrate schema v6 to v7");
#ifdef LARDON3D_PROJECT_DB_TESTING
const char *forced_failure = getenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V7");
if (result == LARDON3D_PROJECT_DB_OK && forced_failure && strcmp(forced_failure, "1") == 0) {
result = execute(database, "INSERT INTO missing_test_table VALUES(1)",
"forced migration v7 failure");
}
#endif
if (result == LARDON3D_PROJECT_DB_OK) {
result = execute(database,
"UPDATE metadata SET value=7 WHERE key='schema_version' AND value=6",
"finish schema v7 migration");
}
}
if (result == LARDON3D_PROJECT_DB_OK && from_version < 8) {
result = execute(database, schema_candidate_v8, "migrate schema v7 to v8");
#ifdef LARDON3D_PROJECT_DB_TESTING
const char *forced_failure = getenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V8");
if (result == LARDON3D_PROJECT_DB_OK && forced_failure && strcmp(forced_failure, "1") == 0) {
result = execute(database, "INSERT INTO missing_test_table VALUES(1)",
"forced migration v8 failure");
}
#endif
if (result == LARDON3D_PROJECT_DB_OK) {
result = execute(database,
"UPDATE metadata SET value=8 WHERE key='schema_version' AND value=7",
"finish schema v8 migration");
}
}
if (result == LARDON3D_PROJECT_DB_OK && from_version < 9) {
result = execute(database, schema_candidate_generate_v9, "migrate schema v8 to v9");
#ifdef LARDON3D_PROJECT_DB_TESTING
const char *forced_failure = getenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V9");
if (result == LARDON3D_PROJECT_DB_OK && forced_failure && strcmp(forced_failure, "1") == 0) {
result = execute(database, "INSERT INTO missing_test_table VALUES(1)",
"forced migration v9 failure");
}
#endif
if (result == LARDON3D_PROJECT_DB_OK) {
result = execute(database,
"UPDATE metadata SET value=9 WHERE key='schema_version' AND value=8",
"finish schema v9 migration");
}
}
if (result == LARDON3D_PROJECT_DB_OK && from_version < 10) {
result = execute(database, schema_match_result_v10, "migrate schema v9 to v10");
#ifdef LARDON3D_PROJECT_DB_TESTING
const char *forced_failure = getenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V10");
if (result == LARDON3D_PROJECT_DB_OK && forced_failure && strcmp(forced_failure, "1") == 0) {
result = execute(database, "INSERT INTO missing_test_table VALUES(1)",
"forced migration v10 failure");
}
#endif
if (result == LARDON3D_PROJECT_DB_OK) {
result = execute(database,
"UPDATE metadata SET value=10 WHERE key='schema_version' AND value=9",
"finish schema v10 migration");
}
}
if (result == LARDON3D_PROJECT_DB_OK && from_version < 11) {
result = execute(database, schema_matcher_task_v11, "migrate schema v10 to v11");
#ifdef LARDON3D_PROJECT_DB_TESTING
const char *forced_failure = getenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V11");
if (result == LARDON3D_PROJECT_DB_OK && forced_failure && strcmp(forced_failure, "1") == 0) {
result = execute(database, "INSERT INTO missing_test_table VALUES(1)",
"forced migration v11 failure");
}
#endif
if (result == LARDON3D_PROJECT_DB_OK) {
result = execute(database,
"UPDATE metadata SET value=11 WHERE key='schema_version' AND value=10",
"finish schema v11 migration");
}
}
if (result == LARDON3D_PROJECT_DB_OK && from_version < 12) {
result = execute(database, schema_geometric_verification_v12,
"migrate schema v11 to v12");
#ifdef LARDON3D_PROJECT_DB_TESTING
const char *forced_failure = getenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V12");
if (result == LARDON3D_PROJECT_DB_OK && forced_failure && strcmp(forced_failure, "1") == 0) {
result = execute(database, "INSERT INTO missing_test_table VALUES(1)",
"forced migration v12 failure");
}
#endif
if (result == LARDON3D_PROJECT_DB_OK) {
result = execute(database,
"UPDATE metadata SET value=12 WHERE key='schema_version' AND value=11",
"finish schema v12 migration");
}
}
if (result == LARDON3D_PROJECT_DB_OK && from_version < 13) {
result = execute(database, schema_geometric_verifier_task_v13,
"migrate schema v12 to v13");
#ifdef LARDON3D_PROJECT_DB_TESTING
const char *forced_failure = getenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V13");
if (result == LARDON3D_PROJECT_DB_OK && forced_failure && strcmp(forced_failure, "1") == 0) {
result = execute(database, "INSERT INTO missing_test_table VALUES(1)",
"forced migration v13 failure");
}
#endif
if (result == LARDON3D_PROJECT_DB_OK) {
result = execute(database,
"UPDATE metadata SET value=13 WHERE key='schema_version' AND value=12",
"finish schema v13 migration");
}
}
if (result == LARDON3D_PROJECT_DB_OK && from_version < 14) {
result = execute(database, schema_track_v14, "migrate schema v13 to v14");
#ifdef LARDON3D_PROJECT_DB_TESTING
const char *forced_failure = getenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V14");
if (result == LARDON3D_PROJECT_DB_OK && forced_failure && strcmp(forced_failure, "1") == 0) {
result = execute(database, "INSERT INTO missing_test_table VALUES(1)",
"forced migration v14 failure");
}
#endif
if (result == LARDON3D_PROJECT_DB_OK) {
result = execute(database,
"UPDATE metadata SET value=14 WHERE key='schema_version' AND value=13",
"finish schema v14 migration");
}
}
if (result == LARDON3D_PROJECT_DB_OK && from_version < 15) {
result = execute(database, schema_track_builder_task_v15,
"migrate schema v14 to v15");
#ifdef LARDON3D_PROJECT_DB_TESTING
const char *forced_failure = getenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V15");
if (result == LARDON3D_PROJECT_DB_OK && forced_failure && strcmp(forced_failure, "1") == 0) {
result = execute(database, "INSERT INTO missing_test_table VALUES(1)",
"forced migration v15 failure");
}
#endif
if (result == LARDON3D_PROJECT_DB_OK) {
result = execute(database,
"UPDATE metadata SET value=15 WHERE key='schema_version' AND value=14",
"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 && from_version < 17) {
result = execute(database, schema_sparse_sfm_task_v17,
"migrate schema v16 to v17");
#ifdef LARDON3D_PROJECT_DB_TESTING
if (result == LARDON3D_PROJECT_DB_OK &&
getenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V17")) {
result = execute(database, "INSERT INTO missing_v17_test_table VALUES(1)",
"forced migration v17 failure");
}
#endif
if (result == LARDON3D_PROJECT_DB_OK) {
result = execute(
database,
"UPDATE metadata SET value=17 WHERE key='schema_version' AND value=16",
"finish schema v17 migration");
}
}
if (result == LARDON3D_PROJECT_DB_OK && from_version < 18) {
result = execute(database, schema_sparse_derivation_identity_v18,
"migrate sparse derivation identity v17 to v18");
if (result == LARDON3D_PROJECT_DB_OK)
result = execute(database, schema_sparse_derivation_children_v18,
"migrate sparse derivation children v17 to v18");
if (result == LARDON3D_PROJECT_DB_OK)
result = execute(database, schema_incremental_reconstruction_v18,
"migrate incremental reconstruction v17 to v18");
#ifdef LARDON3D_PROJECT_DB_TESTING
if (result == LARDON3D_PROJECT_DB_OK &&
getenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V18")) {
result = execute(database, "INSERT INTO missing_v18_test_table VALUES(1)",
"forced migration v18 failure");
}
#endif
if (result == LARDON3D_PROJECT_DB_OK) {
result = execute(
database,
"UPDATE metadata SET value=18 WHERE key='schema_version' AND value=17",
"finish schema v18 migration");
}
}
if (result == LARDON3D_PROJECT_DB_OK && from_version < 19) {
result = execute(database, schema_capture_provenance_v19,
"migrate image catalog capture provenance v18 to v19");
if (result == LARDON3D_PROJECT_DB_OK) {
result = execute(
database,
"INSERT INTO captures(capture_id,scanset_id,created_at) "
"SELECT image_id,scanset_id,imported_at FROM images ORDER BY image_id;"
"INSERT INTO capture_images(capture_id,image_id) "
"SELECT image_id,image_id FROM images ORDER BY image_id;"
"INSERT INTO capture_assets(capture_id,asset_id,role) "
"SELECT image_id,asset_id,1 FROM images ORDER BY image_id;"
"INSERT INTO capture_selections(capture_id,image_id) "
"SELECT image_id,image_id FROM images ORDER BY image_id;",
"migrate legacy images to independent captures");
}
#ifdef LARDON3D_PROJECT_DB_TESTING
if (result == LARDON3D_PROJECT_DB_OK &&
getenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V19")) {
result = execute(database, "INSERT INTO missing_v19_test_table VALUES(1)",
"forced migration v19 failure");
}
#endif
if (result == LARDON3D_PROJECT_DB_OK) {
result = execute(
database,
"UPDATE metadata SET value=19 WHERE key='schema_version' AND value=18",
"finish schema v19 migration");
}
}
if (result == LARDON3D_PROJECT_DB_OK) {
result = execute(database, "COMMIT", "commit migration");
}
if (result != LARDON3D_PROJECT_DB_OK) {
(void)execute(database, "ROLLBACK", "rollback migration");
}
return result;
}
static Lardon3DProjectDbResult read_schema_version(Lardon3DProjectDb *database,
unsigned int *version) {
if (!table_exists(database->connection, "metadata")) {
sqlite3_stmt *statement = NULL;
int code = sqlite3_prepare_v2(database->connection,
"SELECT 1 FROM sqlite_master WHERE type='table' LIMIT 1", -1,
&statement, NULL);
if (code != SQLITE_OK) {
return sqlite_result(database, code, "inspect schema");
}
code = sqlite3_step(statement);
bool empty = code == SQLITE_DONE;
(void)sqlite3_finalize(statement);
if (!empty) {
copy_error(database->error, "Base incohérente sans metadata.");
return LARDON3D_PROJECT_DB_CORRUPT;
}
*version = 0;
return LARDON3D_PROJECT_DB_OK;
}
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result =
prepare(database, "SELECT value FROM metadata WHERE key='schema_version'", &statement);
if (result != LARDON3D_PROJECT_DB_OK) {
return result;
}
int code = sqlite3_step(statement);
if (code != SQLITE_ROW || sqlite3_column_type(statement, 0) != SQLITE_INTEGER) {
(void)sqlite3_finalize(statement);
copy_error(database->error, "schema_version absente ou invalide.");
return LARDON3D_PROJECT_DB_CORRUPT;
}
sqlite3_int64 value = sqlite3_column_int64(statement, 0);
code = sqlite3_step(statement);
(void)sqlite3_finalize(statement);
if (code != SQLITE_DONE || value < 1 || value > UINT_MAX) {
return LARDON3D_PROJECT_DB_CORRUPT;
}
*version = (unsigned int)value;
return LARDON3D_PROJECT_DB_OK;
}
Lardon3DProjectDbResult lardon3d_project_db_open(const char *path, Lardon3DProjectDb **output,
char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]) {
if (output) {
*output = NULL;
}
if (!path || !path[0] || !output ||
strnlen(path, LARDON3D_PROJECT_DB_PATH_CAPACITY) >= LARDON3D_PROJECT_DB_PATH_CAPACITY) {
copy_error(error, "Chemin de base invalide.");
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
Lardon3DProjectDb *database = calloc(1, sizeof(*database));
if (!database) {
copy_error(error, "Allocation impossible.");
return LARDON3D_PROJECT_DB_IO_ERROR;
}
if (pthread_mutex_init(&database->mutex, NULL) != 0) {
free(database);
copy_error(error, "Mutex impossible.");
return LARDON3D_PROJECT_DB_IO_ERROR;
}
int code =
sqlite3_open_v2(path, &database->connection,
SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_NOMUTEX, NULL);
if (code != SQLITE_OK) {
copy_error(error, database->connection ? sqlite3_errmsg(database->connection)
: "Ouverture SQLite impossible.");
if (database->connection) {
(void)sqlite3_close(database->connection);
}
(void)pthread_mutex_destroy(&database->mutex);
free(database);
return code == SQLITE_CANTOPEN ? LARDON3D_PROJECT_DB_IO_ERROR : LARDON3D_PROJECT_DB_CORRUPT;
}
Lardon3DProjectDbResult result =
execute(database,
"PRAGMA foreign_keys=ON;PRAGMA journal_mode=DELETE;PRAGMA synchronous=FULL;PRAGMA "
"busy_timeout=5000",
"configure SQLite");
unsigned int version = 0;
if (result == LARDON3D_PROJECT_DB_OK) {
result = read_schema_version(database, &version);
}
if (result == LARDON3D_PROJECT_DB_OK) {
result = migrate(database, version);
}
if (result == LARDON3D_PROJECT_DB_OK) {
const char *required[] = {"project",
"tasks",
"checkpoints",
"artifacts",
"image_import_tasks",
"scansets",
"image_assets",
"images",
"feature_assets",
"feature_sets",
"feature_extract_tasks",
"sift_extract_tasks",
"feature_support_sets",
"feature_support_groups",
"feature_support_members",
"candidate_pairs",
"matcher_tasks",
"match_results",
"geometric_verification_results",
"geometric_verifier_tasks",
"track_sets",
"tracks",
"track_observations",
"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",
"incremental_reconstructions",
"incremental_reconstruction_tasks",
"captures",
"capture_images",
"capture_assets",
"capture_selections",
"asset_derivations"};
for (size_t index = 0; index < sizeof(required) / sizeof(required[0]) &&
result == LARDON3D_PROJECT_DB_OK;
++index) {
if (!table_exists(database->connection, required[index])) {
copy_error(database->error, "Schéma v1 incomplet.");
result = LARDON3D_PROJECT_DB_CORRUPT;
}
}
}
if (result != LARDON3D_PROJECT_DB_OK) {
copy_error(error, database->error);
(void)sqlite3_close(database->connection);
(void)pthread_mutex_destroy(&database->mutex);
free(database);
return result;
}
database->error[0] = '\0';
copy_error(error, "");
*output = database;
return LARDON3D_PROJECT_DB_OK;
}
void lardon3d_project_db_close(Lardon3DProjectDb *database) {
if (!database) {
return;
}
(void)pthread_mutex_lock(&database->mutex);
(void)sqlite3_close(database->connection);
(void)pthread_mutex_unlock(&database->mutex);
(void)pthread_mutex_destroy(&database->mutex);
free(database);
}
bool lardon3d_project_db_last_error(Lardon3DProjectDb *database,
char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]) {
if (!database || !error) {
return false;
}
(void)pthread_mutex_lock(&database->mutex);
copy_error(error, database->error);
(void)pthread_mutex_unlock(&database->mutex);
return true;
}
unsigned int lardon3d_project_db_schema_version(Lardon3DProjectDb *database) {
return database ? LARDON3D_PROJECT_DB_SCHEMA_VERSION : 0U;
}
Lardon3DProjectDbResult lardon3d_project_db_legacy_catalog_pending(Lardon3DProjectDb *database,
bool *pending) {
if (pending) {
*pending = false;
}
if (!database || !pending) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
(void)pthread_mutex_lock(&database->mutex);
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result = prepare(
database, "SELECT value FROM metadata WHERE key='legacy_image_catalog_pending'", &statement);
if (result == LARDON3D_PROJECT_DB_OK) {
int code = sqlite3_step(statement);
sqlite3_int64 value = sqlite3_column_int64(statement, 0);
if (code != SQLITE_ROW || (value != 0 && value != 1) ||
sqlite3_step(statement) != SQLITE_DONE) {
result = LARDON3D_PROJECT_DB_CORRUPT;
} else {
*pending = value == 1;
}
(void)sqlite3_finalize(statement);
}
(void)pthread_mutex_unlock(&database->mutex);
return result;
}
Lardon3DProjectDbResult step_done(Lardon3DProjectDb *database, sqlite3_stmt *statement,
const char *context) {
int code = sqlite3_step(statement);
if (code != SQLITE_DONE) {
Lardon3DProjectDbResult result = sqlite_result(database, code, context);
(void)sqlite3_finalize(statement);
return result;
}
code = sqlite3_finalize(statement);
return code == SQLITE_OK ? LARDON3D_PROJECT_DB_OK : sqlite_result(database, code, context);
}
static bool copy_column(sqlite3_stmt *statement, int column, char *destination, size_t capacity) {
if (sqlite3_column_type(statement, column) != SQLITE_TEXT) {
return false;
}
int bytes = sqlite3_column_bytes(statement, column);
const unsigned char *text = sqlite3_column_text(statement, column);
if (!text || bytes < 0 || (size_t)bytes >= capacity) {
return false;
}
memcpy(destination, text, (size_t)bytes);
destination[bytes] = '\0';
return true;
}
Lardon3DProjectDbResult lardon3d_project_db_set_project(Lardon3DProjectDb *database,
const Lardon3DProjectDbProject *project) {
if (!database || !project ||
!bounded_text(project->stable_id, sizeof(project->stable_id), false) ||
!bounded_text(project->name, sizeof(project->name), false) || project->created_at < 0 ||
project->updated_at < project->created_at) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
(void)pthread_mutex_lock(&database->mutex);
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result = prepare(
database,
"INSERT INTO project(singleton,stable_id,name,created_at,updated_at) VALUES(1,?1,?2,?3,?4) "
"ON CONFLICT(singleton) DO UPDATE SET name=excluded.name,updated_at=excluded.updated_at "
"WHERE project.stable_id=excluded.stable_id AND project.created_at=excluded.created_at",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_text(statement, 1, project->stable_id, -1, SQLITE_TRANSIENT);
(void)sqlite3_bind_text(statement, 2, project->name, -1, SQLITE_TRANSIENT);
(void)sqlite3_bind_int64(statement, 3, project->created_at);
(void)sqlite3_bind_int64(statement, 4, project->updated_at);
result = step_done(database, statement, "set project");
if (result == LARDON3D_PROJECT_DB_OK && sqlite3_changes(database->connection) != 1) {
copy_error(database->error, "Identité de projet contradictoire.");
result = LARDON3D_PROJECT_DB_CONSTRAINT;
}
}
(void)pthread_mutex_unlock(&database->mutex);
return result;
}
Lardon3DProjectDbResult lardon3d_project_db_get_project(Lardon3DProjectDb *database,
Lardon3DProjectDbProject *project) {
if (!database || !project) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
(void)pthread_mutex_lock(&database->mutex);
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result = prepare(
database, "SELECT stable_id,name,created_at,updated_at FROM project WHERE singleton=1",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
int code = sqlite3_step(statement);
if (code == SQLITE_DONE) {
result = LARDON3D_PROJECT_DB_NOT_FOUND;
} else if (code != SQLITE_ROW ||
!copy_column(statement, 0, project->stable_id, sizeof(project->stable_id)) ||
!copy_column(statement, 1, project->name, sizeof(project->name))) {
result = LARDON3D_PROJECT_DB_CORRUPT;
} else {
project->created_at = sqlite3_column_int64(statement, 2);
project->updated_at = sqlite3_column_int64(statement, 3);
}
(void)sqlite3_finalize(statement);
}
(void)pthread_mutex_unlock(&database->mutex);
return result;
}
static bool valid_checkpoint(const Lardon3DProjectDbCheckpoint *checkpoint) {
return checkpoint && bounded_text(checkpoint->path, sizeof(checkpoint->path), false) &&
checkpoint->format_version > 0 && checkpoint->updated_at >= 0 &&
checkpoint->durability >= LARDON3D_DB_CHECKPOINT_DURABLE &&
checkpoint->durability <= LARDON3D_DB_CHECKPOINT_PUBLISHED_NOT_DURABLE;
}
static bool valid_durable_task(const Lardon3DTaskDurableSnapshot *snapshot, int64_t updated_at) {
return snapshot && valid_task_id(snapshot->id) &&
bounded_text(snapshot->name, sizeof(snapshot->name), false) &&
valid_state(snapshot->saved_state) && valid_state(snapshot->recovery_state) &&
snapshot->recovery_state ==
((snapshot->saved_state == TASK_RUNNING || snapshot->saved_state == TASK_PAUSED)
? TASK_PENDING
: snapshot->saved_state) &&
snapshot->progress <= 100 && snapshot->sequence_count <= INT_MAX &&
snapshot->started_at.tv_sec >= 0 && snapshot->started_at.tv_sec <= INT64_MAX &&
snapshot->started_at.tv_nsec >= 0 && snapshot->started_at.tv_nsec < 1000000000L &&
snapshot->finished_at.tv_sec >= 0 && snapshot->finished_at.tv_sec <= INT64_MAX &&
snapshot->finished_at.tv_nsec >= 0 && snapshot->finished_at.tv_nsec < 1000000000L &&
updated_at >= 0;
}
static bool valid_sparse_sfm_parameters(const Lardon3DSparseIncrementalParameters *p) {
return p && p->minimum_seed_tracks >= 2 && p->minimum_seed_landmarks > 0 &&
p->minimum_pnp_correspondences >= 4 && p->maximum_seed_candidates > 0 &&
p->maximum_registration_rounds > 0 && p->maximum_landmarks_per_round > 0 &&
p->maximum_images >= 2 && p->maximum_observations >= p->maximum_tracks &&
isfinite(p->reprojection_threshold_px) && p->reprojection_threshold_px > 0.0 &&
isfinite(p->minimum_track_parallax_rad) && p->minimum_track_parallax_rad >= 0.0 &&
isfinite(p->relative_pose.robust_threshold_px) &&
p->relative_pose.robust_threshold_px > 0.0 && isfinite(p->relative_pose.confidence) &&
p->relative_pose.confidence > 0.0 && p->relative_pose.confidence < 1.0 &&
p->relative_pose.max_iterations > 0 &&
p->relative_pose.minimum_inliers >= p->minimum_seed_tracks &&
isfinite(p->relative_pose.minimum_inlier_ratio) &&
p->relative_pose.minimum_inlier_ratio > 0.0 &&
p->relative_pose.minimum_inlier_ratio <= 1.0 &&
isfinite(p->relative_pose.minimum_parallax_rad) &&
p->relative_pose.minimum_parallax_rad >= 0.0 &&
isfinite(p->relative_pose.minimum_cheirality_ratio) &&
p->relative_pose.minimum_cheirality_ratio > 0.0 &&
p->relative_pose.minimum_cheirality_ratio <= 1.0 &&
isfinite(p->pnp.reprojection_threshold_px) && p->pnp.reprojection_threshold_px > 0.0 &&
isfinite(p->pnp.confidence) && p->pnp.confidence > 0.0 && p->pnp.confidence < 1.0 &&
p->pnp.max_iterations > 0 &&
p->pnp.minimum_inliers >= p->minimum_pnp_correspondences &&
isfinite(p->pnp.minimum_inlier_ratio) && p->pnp.minimum_inlier_ratio > 0.0 &&
p->pnp.minimum_inlier_ratio <= 1.0 && p->refinement.max_iterations > 0 &&
isfinite(p->refinement.convergence_tolerance) &&
p->refinement.convergence_tolerance > 0.0;
}
static Lardon3DProjectDbResult
record_task_internal(Lardon3DProjectDb *database, const Lardon3DTaskDurableSnapshot *snapshot,
const char *task_kind, uint32_t task_kind_version,
const Lardon3DProjectDbCheckpoint *checkpoint, const char *source_path,
uint64_t scanset_id, const Lardon3DProjectDbFeatureExtractTask *feature,
const Lardon3DProjectDbSiftExtractTask *sift,
const Lardon3DProjectDbVisualIndexUpdateTask *visual,
const Lardon3DProjectDbCandidatePairGenerateTask *candidate_pair,
const Lardon3DProjectDbMatcherTask *matcher,
const Lardon3DProjectDbGeometricVerifierTask *geometric_verifier,
const Lardon3DProjectDbSparseSfmTask *sparse_sfm,
int64_t updated_at) {
bool typed = task_kind != NULL;
if (!database || !valid_durable_task(snapshot, updated_at) ||
(typed && (!lardon3d_task_kind_is_valid(task_kind) || task_kind_version == 0)) ||
(!typed && task_kind_version != 0) ||
(source_path && !bounded_text(source_path, LARDON3D_PROJECT_DB_PATH_CAPACITY, false)) ||
(source_path && !valid_task_id(scanset_id)) ||
(feature &&
(!valid_task_id(feature->task_id) || feature->task_id != snapshot->id ||
!valid_task_id(feature->image_id) ||
!lardon3d_task_kind_is_valid(feature->extractor_kind) || feature->extractor_version == 0 ||
feature->max_features == 0 || feature->max_features > 8192 ||
feature->pyramid_levels == 0 || feature->pyramid_levels > 16 ||
feature->fast_threshold == 0 || feature->fast_threshold > 255)) ||
(sift &&
(!valid_task_id(sift->task_id) || sift->task_id != snapshot->id ||
!valid_task_id(sift->image_id) ||
(strcmp(sift->extractor_kind, "sift") != 0 &&
strcmp(sift->extractor_kind, "rootsift") != 0) ||
sift->extractor_version != 1 || sift->max_features == 0 ||
sift->max_features > 8192 || sift->octave_layers == 0 || sift->octave_layers > 8 ||
sift->contrast_threshold < 0.001 || sift->contrast_threshold > 0.2 ||
sift->edge_threshold < 1.0 || sift->edge_threshold > 100.0 || sift->sigma < 0.5 ||
sift->sigma > 3.0 || sift->grid_rows == 0 || sift->grid_rows > 32 ||
sift->grid_cols == 0 || sift->grid_cols > 32 || sift->max_features_per_cell == 0 ||
sift->max_features_per_cell > 8192)) ||
(visual &&
(!valid_task_id(visual->task_id) || visual->task_id != snapshot->id ||
!valid_task_id(visual->visual_index_id) || visual->after_feature_set_id > INT64_MAX)) ||
(candidate_pair &&
(!valid_task_id(candidate_pair->task_id) || candidate_pair->task_id != snapshot->id ||
!valid_task_id(candidate_pair->visual_index_id) ||
candidate_pair->after_feature_set_id > INT64_MAX ||
candidate_pair->top_k == 0 || candidate_pair->top_k > 256 ||
candidate_pair->minimum_evidence_count > 1024 ||
candidate_pair->scanset_filter < 0 || candidate_pair->scanset_filter > 2)) ||
(matcher &&
(!valid_task_id(matcher->task_id) || matcher->task_id != snapshot->id ||
matcher->after_candidate_pair_id > INT64_MAX ||
!lardon3d_task_kind_is_valid(matcher->feature_extractor_kind) ||
matcher->feature_extractor_version == 0 || matcher->matcher_kind < 0 ||
matcher->matcher_kind > 2 || !isfinite(matcher->ratio_threshold) ||
matcher->ratio_threshold <= 0.0F || matcher->ratio_threshold >= 1.0F)) ||
(geometric_verifier &&
(!valid_task_id(geometric_verifier->task_id) ||
geometric_verifier->task_id != snapshot->id ||
geometric_verifier->after_match_result_id > INT64_MAX ||
!isfinite(geometric_verifier->threshold_pixels) ||
geometric_verifier->threshold_pixels <= 0.0 ||
!isfinite(geometric_verifier->confidence) ||
geometric_verifier->confidence <= 0.0 || geometric_verifier->confidence >= 1.0 ||
geometric_verifier->max_iterations == 0 ||
geometric_verifier->max_iterations > INT_MAX ||
geometric_verifier->min_inlier_count == 0 ||
geometric_verifier->min_inlier_count > 8192 ||
!isfinite(geometric_verifier->min_inlier_ratio) ||
geometric_verifier->min_inlier_ratio < 0.0 ||
geometric_verifier->min_inlier_ratio > 1.0 ||
geometric_verifier->seed_policy_version == 0 ||
geometric_verifier->canonicalization_version == 0)) ||
(sparse_sfm &&
(!task_kind || strcmp(task_kind, "sparse_sfm.run") != 0 || task_kind_version != 1 ||
!valid_task_id(sparse_sfm->task_id) || sparse_sfm->task_id != snapshot->id ||
!valid_task_id(sparse_sfm->track_set_id) ||
!valid_task_id(sparse_sfm->calibration_scope_id) || sparse_sfm->sfm_kind != 1 ||
sparse_sfm->sfm_version != 1 || !valid_sparse_sfm_parameters(&sparse_sfm->parameters))) ||
(checkpoint && !valid_checkpoint(checkpoint))) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
#ifdef LARDON3D_PROJECT_DB_TESTING
const char *forced_busy = getenv("LARDON3D_TEST_PROJECT_DB_BUSY_CHECKPOINT");
if (forced_busy && checkpoint && strcmp(forced_busy, "1") == 0) {
copy_error(database->error, "Verrou DB injecté pour test.");
return LARDON3D_PROJECT_DB_BUSY;
}
#endif
(void)pthread_mutex_lock(&database->mutex);
Lardon3DProjectDbResult result = execute(database, "BEGIN IMMEDIATE", "begin task record");
sqlite3_stmt *statement = NULL;
if (result == LARDON3D_PROJECT_DB_OK) {
result = prepare(
database,
"INSERT INTO "
"tasks(task_id,name,task_kind,task_kind_version,saved_state,recovery_state,progress,"
"sequence_count,started_sec,started_nsec,finished_sec,finished_nsec,updated_at)"
"VALUES(?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13) ON CONFLICT(task_id) DO UPDATE SET "
"name=excluded.name,saved_state=excluded.saved_state,"
"recovery_state=excluded.recovery_state,progress=excluded.progress,sequence_count="
"excluded.sequence_count,started_sec=excluded.started_sec,"
"started_nsec=excluded.started_nsec,finished_sec=excluded.finished_sec,finished_nsec="
"excluded.finished_nsec,updated_at=excluded.updated_at "
"WHERE (tasks.task_kind IS NULL AND excluded.task_kind IS NULL) OR "
"(tasks.task_kind=excluded.task_kind AND "
"tasks.task_kind_version=excluded.task_kind_version)",
&statement);
}
if (result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)snapshot->id);
(void)sqlite3_bind_text(statement, 2, snapshot->name, -1, SQLITE_TRANSIENT);
if (typed) {
(void)sqlite3_bind_text(statement, 3, task_kind, -1, SQLITE_TRANSIENT);
(void)sqlite3_bind_int64(statement, 4, task_kind_version);
} else {
(void)sqlite3_bind_null(statement, 3);
(void)sqlite3_bind_null(statement, 4);
}
(void)sqlite3_bind_int(statement, 5, (int)snapshot->saved_state);
(void)sqlite3_bind_int(statement, 6, (int)snapshot->recovery_state);
(void)sqlite3_bind_int(statement, 7, (int)snapshot->progress);
(void)sqlite3_bind_int(statement, 8, (int)snapshot->sequence_count);
(void)sqlite3_bind_int64(statement, 9, snapshot->started_at.tv_sec);
(void)sqlite3_bind_int64(statement, 10, snapshot->started_at.tv_nsec);
(void)sqlite3_bind_int64(statement, 11, snapshot->finished_at.tv_sec);
(void)sqlite3_bind_int64(statement, 12, snapshot->finished_at.tv_nsec);
(void)sqlite3_bind_int64(statement, 13, updated_at);
result = step_done(database, statement, "upsert task");
if (result == LARDON3D_PROJECT_DB_OK && sqlite3_changes(database->connection) != 1) {
copy_error(database->error, "Type métier de tâche immuable.");
result = LARDON3D_PROJECT_DB_CONSTRAINT;
}
}
if (result == LARDON3D_PROJECT_DB_OK && checkpoint) {
#ifdef LARDON3D_PROJECT_DB_TESTING
const char *forced_failure = getenv("LARDON3D_TEST_PROJECT_DB_FAIL_CHECKPOINT");
if (forced_failure && strcmp(forced_failure, "1") == 0) {
result = execute(database, "INSERT INTO missing_test_table VALUES(1)",
"forced checkpoint failure");
}
#endif
}
if (result == LARDON3D_PROJECT_DB_OK) {
result =
prepare(database,
"UPDATE metadata SET value=CASE WHEN ?1=9223372036854775807 THEN 0 ELSE ?1+1 END "
"WHERE key='next_task_id' AND value>0 AND value<=?1",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)snapshot->id);
result = step_done(database, statement, "advance recorded task id");
}
}
if (result == LARDON3D_PROJECT_DB_OK && source_path) {
result =
prepare(database,
"INSERT INTO image_import_tasks(task_id,source_path,scanset_id) VALUES(?1,?2,?3) "
"ON CONFLICT(task_id) DO UPDATE SET source_path=excluded.source_path "
"WHERE image_import_tasks.source_path=excluded.source_path AND "
"image_import_tasks.scanset_id=excluded.scanset_id",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)snapshot->id);
(void)sqlite3_bind_text(statement, 2, source_path, -1, SQLITE_TRANSIENT);
(void)sqlite3_bind_int64(statement, 3, (sqlite3_int64)scanset_id);
result = step_done(database, statement, "upsert image import");
if (result == LARDON3D_PROJECT_DB_OK && sqlite3_changes(database->connection) != 1) {
copy_error(database->error, "Source d'import immuable.");
result = LARDON3D_PROJECT_DB_CONSTRAINT;
}
}
}
if (result == LARDON3D_PROJECT_DB_OK && feature) {
result = prepare(
database,
"INSERT INTO "
"feature_extract_tasks(task_id,image_id,extractor_kind,extractor_version,max_features,"
"pyramid_levels,fast_threshold,parameter_fingerprint) VALUES(?1,?2,?3,?4,?5,?6,?7,?8) "
"ON CONFLICT(task_id) DO UPDATE SET image_id=excluded.image_id WHERE "
"feature_extract_tasks.image_id=excluded.image_id AND "
"feature_extract_tasks.extractor_kind=excluded.extractor_kind AND "
"feature_extract_tasks.extractor_version=excluded.extractor_version AND "
"feature_extract_tasks.max_features=excluded.max_features AND "
"feature_extract_tasks.pyramid_levels=excluded.pyramid_levels AND "
"feature_extract_tasks.fast_threshold=excluded.fast_threshold AND "
"feature_extract_tasks.parameter_fingerprint=excluded.parameter_fingerprint",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)feature->task_id);
(void)sqlite3_bind_int64(statement, 2, (sqlite3_int64)feature->image_id);
(void)sqlite3_bind_text(statement, 3, feature->extractor_kind, -1, SQLITE_TRANSIENT);
(void)sqlite3_bind_int64(statement, 4, feature->extractor_version);
(void)sqlite3_bind_int64(statement, 5, feature->max_features);
(void)sqlite3_bind_int64(statement, 6, feature->pyramid_levels);
(void)sqlite3_bind_int64(statement, 7, feature->fast_threshold);
(void)sqlite3_bind_blob(statement, 8, feature->parameter_fingerprint, 32, SQLITE_TRANSIENT);
result = step_done(database, statement, "upsert feature extract");
statement = NULL;
if (result == LARDON3D_PROJECT_DB_OK && sqlite3_changes(database->connection) != 1) {
copy_error(database->error, "Paramètres feature immuables.");
result = LARDON3D_PROJECT_DB_CONSTRAINT;
}
}
}
if (result == LARDON3D_PROJECT_DB_OK && visual) {
result = prepare(database,
"INSERT INTO visual_index_update_tasks(task_id,visual_index_id,"
"after_feature_set_id) VALUES(?1,?2,?3) ON CONFLICT(task_id) DO UPDATE SET "
"after_feature_set_id=excluded.after_feature_set_id WHERE "
"visual_index_update_tasks.visual_index_id=excluded.visual_index_id",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(statement, 1, (sqlite3_int64)visual->task_id);
sqlite3_bind_int64(statement, 2, (sqlite3_int64)visual->visual_index_id);
sqlite3_bind_int64(statement, 3, (sqlite3_int64)visual->after_feature_set_id);
result = step_done(database, statement, "upsert visual index update");
if (result == LARDON3D_PROJECT_DB_OK && sqlite3_changes(database->connection) != 1) {
copy_error(database->error, "Index cible de tâche immuable.");
result = LARDON3D_PROJECT_DB_CONSTRAINT;
}
}
}
if (result == LARDON3D_PROJECT_DB_OK && candidate_pair) {
result = prepare(database,
"INSERT INTO candidate_pair_generate_tasks(task_id,visual_index_id,"
"after_feature_set_id,top_k,minimum_evidence_count,scanset_filter,"
"exclude_same_asset) VALUES(?1,?2,?3,?4,?5,?6,?7) ON CONFLICT(task_id) DO "
"UPDATE SET after_feature_set_id=excluded.after_feature_set_id WHERE "
"candidate_pair_generate_tasks.visual_index_id=excluded.visual_index_id",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(statement, 1, (sqlite3_int64)candidate_pair->task_id);
sqlite3_bind_int64(statement, 2, (sqlite3_int64)candidate_pair->visual_index_id);
sqlite3_bind_int64(statement, 3, (sqlite3_int64)candidate_pair->after_feature_set_id);
sqlite3_bind_int64(statement, 4, (sqlite3_int64)candidate_pair->top_k);
sqlite3_bind_int64(statement, 5, (sqlite3_int64)candidate_pair->minimum_evidence_count);
sqlite3_bind_int64(statement, 6, (sqlite3_int64)candidate_pair->scanset_filter);
sqlite3_bind_int64(statement, 7, candidate_pair->exclude_same_asset ? 1 : 0);
result = step_done(database, statement, "upsert candidate pair generate");
if (result == LARDON3D_PROJECT_DB_OK && sqlite3_changes(database->connection) != 1) {
copy_error(database->error, "Index cible de tâche immuable.");
result = LARDON3D_PROJECT_DB_CONSTRAINT;
}
}
}
if (result == LARDON3D_PROJECT_DB_OK && matcher) {
result = prepare(
database,
"INSERT INTO matcher_tasks(task_id,after_candidate_pair_id,feature_extractor_kind,"
"feature_extractor_version,feature_parameter_fingerprint,matcher_kind,ratio_threshold) "
"VALUES(?1,?2,?3,?4,?5,?6,?7) ON CONFLICT(task_id) DO UPDATE SET "
"after_candidate_pair_id=excluded.after_candidate_pair_id WHERE "
"matcher_tasks.feature_extractor_kind=excluded.feature_extractor_kind AND "
"matcher_tasks.feature_extractor_version=excluded.feature_extractor_version AND "
"matcher_tasks.feature_parameter_fingerprint=excluded.feature_parameter_fingerprint AND "
"matcher_tasks.matcher_kind=excluded.matcher_kind AND "
"matcher_tasks.ratio_threshold=excluded.ratio_threshold",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(statement, 1, (sqlite3_int64)matcher->task_id);
sqlite3_bind_int64(statement, 2, (sqlite3_int64)matcher->after_candidate_pair_id);
sqlite3_bind_text(statement, 3, matcher->feature_extractor_kind, -1, SQLITE_TRANSIENT);
sqlite3_bind_int64(statement, 4, matcher->feature_extractor_version);
sqlite3_bind_blob(statement, 5, matcher->feature_parameter_fingerprint, 32,
SQLITE_TRANSIENT);
sqlite3_bind_int(statement, 6, matcher->matcher_kind);
sqlite3_bind_double(statement, 7, matcher->ratio_threshold);
result = step_done(database, statement, "upsert matcher task");
if (result == LARDON3D_PROJECT_DB_OK && sqlite3_changes(database->connection) != 1) {
copy_error(database->error, "Configuration Matcher de tâche immuable.");
result = LARDON3D_PROJECT_DB_CONSTRAINT;
}
}
}
if (result == LARDON3D_PROJECT_DB_OK && geometric_verifier) {
result = prepare(
database,
"INSERT INTO geometric_verifier_tasks(task_id,after_match_result_id,threshold_pixels,"
"confidence,max_iterations,min_inlier_count,min_inlier_ratio,seed_policy_version,"
"canonicalization_version,parameter_fingerprint) VALUES(?1,?2,?3,?4,?5,?6,?7,?8,"
"?9,?10) ON CONFLICT(task_id) DO UPDATE SET after_match_result_id="
"excluded.after_match_result_id WHERE geometric_verifier_tasks.threshold_pixels="
"excluded.threshold_pixels AND geometric_verifier_tasks.confidence=excluded.confidence "
"AND geometric_verifier_tasks.max_iterations=excluded.max_iterations AND "
"geometric_verifier_tasks.min_inlier_count=excluded.min_inlier_count AND "
"geometric_verifier_tasks.min_inlier_ratio=excluded.min_inlier_ratio AND "
"geometric_verifier_tasks.seed_policy_version=excluded.seed_policy_version AND "
"geometric_verifier_tasks.canonicalization_version=excluded.canonicalization_version "
"AND geometric_verifier_tasks.parameter_fingerprint=excluded.parameter_fingerprint",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(statement, 1, (sqlite3_int64)geometric_verifier->task_id);
sqlite3_bind_int64(statement, 2,
(sqlite3_int64)geometric_verifier->after_match_result_id);
sqlite3_bind_double(statement, 3, geometric_verifier->threshold_pixels);
sqlite3_bind_double(statement, 4, geometric_verifier->confidence);
sqlite3_bind_int64(statement, 5, geometric_verifier->max_iterations);
sqlite3_bind_int64(statement, 6, geometric_verifier->min_inlier_count);
sqlite3_bind_double(statement, 7, geometric_verifier->min_inlier_ratio);
sqlite3_bind_int64(statement, 8, geometric_verifier->seed_policy_version);
sqlite3_bind_int64(statement, 9, geometric_verifier->canonicalization_version);
sqlite3_bind_blob(statement, 10, geometric_verifier->parameter_fingerprint, 32,
SQLITE_TRANSIENT);
result = step_done(database, statement, "upsert geometric verifier task");
if (result == LARDON3D_PROJECT_DB_OK && sqlite3_changes(database->connection) != 1) {
copy_error(database->error, "Configuration Geometric Verifier de tâche immuable.");
result = LARDON3D_PROJECT_DB_CONSTRAINT;
}
}
}
if (result == LARDON3D_PROJECT_DB_OK && sift) {
result = prepare(
database,
"INSERT INTO sift_extract_tasks(task_id,image_id,extractor_kind,extractor_version,"
"max_features,octave_layers,contrast_threshold,edge_threshold,sigma,grid_rows,grid_cols,"
"max_features_per_cell,parameter_fingerprint) VALUES(?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,"
"?11,?12,?13) ON CONFLICT(task_id) DO UPDATE SET image_id=excluded.image_id WHERE "
"sift_extract_tasks.image_id=excluded.image_id AND sift_extract_tasks.extractor_kind="
"excluded.extractor_kind AND sift_extract_tasks.extractor_version=excluded.extractor_version "
"AND sift_extract_tasks.max_features=excluded.max_features AND sift_extract_tasks."
"octave_layers=excluded.octave_layers AND sift_extract_tasks.contrast_threshold="
"excluded.contrast_threshold AND sift_extract_tasks.edge_threshold=excluded.edge_threshold "
"AND sift_extract_tasks.sigma=excluded.sigma AND sift_extract_tasks.grid_rows="
"excluded.grid_rows AND sift_extract_tasks.grid_cols=excluded.grid_cols AND "
"sift_extract_tasks.max_features_per_cell=excluded.max_features_per_cell AND "
"sift_extract_tasks.parameter_fingerprint=excluded.parameter_fingerprint",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(statement, 1, (sqlite3_int64)sift->task_id);
sqlite3_bind_int64(statement, 2, (sqlite3_int64)sift->image_id);
sqlite3_bind_text(statement, 3, sift->extractor_kind, -1, SQLITE_TRANSIENT);
sqlite3_bind_int64(statement, 4, sift->extractor_version);
sqlite3_bind_int64(statement, 5, sift->max_features);
sqlite3_bind_int64(statement, 6, sift->octave_layers);
sqlite3_bind_double(statement, 7, sift->contrast_threshold);
sqlite3_bind_double(statement, 8, sift->edge_threshold);
sqlite3_bind_double(statement, 9, sift->sigma);
sqlite3_bind_int64(statement, 10, sift->grid_rows);
sqlite3_bind_int64(statement, 11, sift->grid_cols);
sqlite3_bind_int64(statement, 12, sift->max_features_per_cell);
sqlite3_bind_blob(statement, 13, sift->parameter_fingerprint, 32, SQLITE_TRANSIENT);
result = step_done(database, statement, "upsert sift extract");
if (result == LARDON3D_PROJECT_DB_OK && sqlite3_changes(database->connection) != 1) {
result = LARDON3D_PROJECT_DB_CONSTRAINT;
}
}
}
if (result == LARDON3D_PROJECT_DB_OK && sparse_sfm) {
const Lardon3DSparseIncrementalParameters *p = &sparse_sfm->parameters;
unsigned char maximum_observations[8];
unsigned char maximum_tracks[8];
unsigned char relative_seed[8];
unsigned char pnp_seed[8];
for (unsigned int i = 0; i < 8; ++i) {
maximum_observations[i] = (unsigned char)(p->maximum_observations >> (8U * i));
maximum_tracks[i] = (unsigned char)(p->maximum_tracks >> (8U * i));
relative_seed[i] = (unsigned char)(p->relative_pose.deterministic_seed >> (8U * i));
pnp_seed[i] = (unsigned char)(p->pnp.deterministic_seed >> (8U * i));
}
result = prepare(
database,
"INSERT INTO sparse_sfm_tasks(task_id,track_set_id,calibration_scope_id,sfm_kind,"
"sfm_version,minimum_seed_tracks,minimum_seed_landmarks,minimum_pnp_correspondences,"
"maximum_seed_candidates,maximum_registration_rounds,maximum_landmarks_per_round,"
"maximum_images,maximum_observations,maximum_tracks,reprojection_threshold_px,"
"minimum_track_parallax_rad,relative_robust_threshold_px,relative_confidence,"
"relative_max_iterations,relative_minimum_inliers,relative_minimum_inlier_ratio,"
"relative_minimum_parallax_rad,relative_minimum_cheirality_ratio,"
"relative_deterministic_seed,pnp_reprojection_threshold_px,pnp_confidence,"
"pnp_max_iterations,pnp_minimum_inliers,pnp_minimum_inlier_ratio,"
"pnp_deterministic_seed,refinement_max_iterations,refinement_convergence_tolerance) "
"VALUES(?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14,?15,?16,?17,?18,?19,"
"?20,?21,?22,?23,?24,?25,?26,?27,?28,?29,?30,?31,?32) ON CONFLICT(task_id) DO "
"UPDATE SET track_set_id=excluded.track_set_id WHERE sparse_sfm_tasks.track_set_id="
"excluded.track_set_id AND sparse_sfm_tasks.calibration_scope_id="
"excluded.calibration_scope_id AND sparse_sfm_tasks.sfm_kind=excluded.sfm_kind AND "
"sparse_sfm_tasks.sfm_version=excluded.sfm_version AND sparse_sfm_tasks."
"minimum_seed_tracks=excluded.minimum_seed_tracks AND sparse_sfm_tasks."
"minimum_seed_landmarks=excluded.minimum_seed_landmarks AND sparse_sfm_tasks."
"minimum_pnp_correspondences=excluded.minimum_pnp_correspondences AND sparse_sfm_tasks."
"maximum_seed_candidates=excluded.maximum_seed_candidates AND sparse_sfm_tasks."
"maximum_registration_rounds=excluded.maximum_registration_rounds AND sparse_sfm_tasks."
"maximum_landmarks_per_round=excluded.maximum_landmarks_per_round AND sparse_sfm_tasks."
"maximum_images=excluded.maximum_images AND sparse_sfm_tasks.maximum_observations="
"excluded.maximum_observations AND sparse_sfm_tasks.maximum_tracks=excluded.maximum_tracks "
"AND sparse_sfm_tasks.reprojection_threshold_px=excluded.reprojection_threshold_px AND "
"sparse_sfm_tasks.minimum_track_parallax_rad=excluded.minimum_track_parallax_rad AND "
"sparse_sfm_tasks.relative_robust_threshold_px=excluded.relative_robust_threshold_px AND "
"sparse_sfm_tasks.relative_confidence=excluded.relative_confidence AND sparse_sfm_tasks."
"relative_max_iterations=excluded.relative_max_iterations AND sparse_sfm_tasks."
"relative_minimum_inliers=excluded.relative_minimum_inliers AND sparse_sfm_tasks."
"relative_minimum_inlier_ratio=excluded.relative_minimum_inlier_ratio AND sparse_sfm_tasks."
"relative_minimum_parallax_rad=excluded.relative_minimum_parallax_rad AND sparse_sfm_tasks."
"relative_minimum_cheirality_ratio=excluded.relative_minimum_cheirality_ratio AND "
"sparse_sfm_tasks.relative_deterministic_seed=excluded.relative_deterministic_seed AND "
"sparse_sfm_tasks.pnp_reprojection_threshold_px=excluded.pnp_reprojection_threshold_px AND "
"sparse_sfm_tasks.pnp_confidence=excluded.pnp_confidence AND sparse_sfm_tasks."
"pnp_max_iterations=excluded.pnp_max_iterations AND sparse_sfm_tasks."
"pnp_minimum_inliers=excluded.pnp_minimum_inliers AND sparse_sfm_tasks."
"pnp_minimum_inlier_ratio=excluded.pnp_minimum_inlier_ratio AND sparse_sfm_tasks."
"pnp_deterministic_seed=excluded.pnp_deterministic_seed AND sparse_sfm_tasks."
"refinement_max_iterations=excluded.refinement_max_iterations AND sparse_sfm_tasks."
"refinement_convergence_tolerance=excluded.refinement_convergence_tolerance",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(statement, 1, (sqlite3_int64)sparse_sfm->task_id);
sqlite3_bind_int64(statement, 2, (sqlite3_int64)sparse_sfm->track_set_id);
sqlite3_bind_int64(statement, 3, (sqlite3_int64)sparse_sfm->calibration_scope_id);
sqlite3_bind_int(statement, 4, (int)sparse_sfm->sfm_kind);
sqlite3_bind_int(statement, 5, (int)sparse_sfm->sfm_version);
sqlite3_bind_int64(statement, 6, p->minimum_seed_tracks);
sqlite3_bind_int64(statement, 7, p->minimum_seed_landmarks);
sqlite3_bind_int64(statement, 8, p->minimum_pnp_correspondences);
sqlite3_bind_int64(statement, 9, p->maximum_seed_candidates);
sqlite3_bind_int64(statement, 10, p->maximum_registration_rounds);
sqlite3_bind_int64(statement, 11, p->maximum_landmarks_per_round);
sqlite3_bind_int64(statement, 12, p->maximum_images);
sqlite3_bind_blob(statement, 13, maximum_observations, 8, SQLITE_TRANSIENT);
sqlite3_bind_blob(statement, 14, maximum_tracks, 8, SQLITE_TRANSIENT);
sqlite3_bind_double(statement, 15, p->reprojection_threshold_px);
sqlite3_bind_double(statement, 16, p->minimum_track_parallax_rad);
sqlite3_bind_double(statement, 17, p->relative_pose.robust_threshold_px);
sqlite3_bind_double(statement, 18, p->relative_pose.confidence);
sqlite3_bind_int64(statement, 19, p->relative_pose.max_iterations);
sqlite3_bind_int64(statement, 20, p->relative_pose.minimum_inliers);
sqlite3_bind_double(statement, 21, p->relative_pose.minimum_inlier_ratio);
sqlite3_bind_double(statement, 22, p->relative_pose.minimum_parallax_rad);
sqlite3_bind_double(statement, 23, p->relative_pose.minimum_cheirality_ratio);
sqlite3_bind_blob(statement, 24, relative_seed, 8, SQLITE_TRANSIENT);
sqlite3_bind_double(statement, 25, p->pnp.reprojection_threshold_px);
sqlite3_bind_double(statement, 26, p->pnp.confidence);
sqlite3_bind_int64(statement, 27, p->pnp.max_iterations);
sqlite3_bind_int64(statement, 28, p->pnp.minimum_inliers);
sqlite3_bind_double(statement, 29, p->pnp.minimum_inlier_ratio);
sqlite3_bind_blob(statement, 30, pnp_seed, 8, SQLITE_TRANSIENT);
sqlite3_bind_int64(statement, 31, p->refinement.max_iterations);
sqlite3_bind_double(statement, 32, p->refinement.convergence_tolerance);
result = step_done(database, statement, "upsert sparse SfM task");
if (result == LARDON3D_PROJECT_DB_OK && sqlite3_changes(database->connection) != 1) {
copy_error(database->error, "Payload Sparse SfM immuable.");
result = LARDON3D_PROJECT_DB_CONSTRAINT;
}
}
}
if (result == LARDON3D_PROJECT_DB_OK && checkpoint) {
result =
prepare(database,
"INSERT INTO checkpoints(task_id,path,format_version,durability,updated_at) "
"VALUES(?1,?2,?3,?4,?5) "
"ON CONFLICT(task_id) DO UPDATE SET "
"path=excluded.path,format_version=excluded.format_version,durability=excluded."
"durability,updated_at=excluded.updated_at",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)snapshot->id);
(void)sqlite3_bind_text(statement, 2, checkpoint->path, -1, SQLITE_TRANSIENT);
(void)sqlite3_bind_int64(statement, 3, checkpoint->format_version);
(void)sqlite3_bind_int(statement, 4, (int)checkpoint->durability);
(void)sqlite3_bind_int64(statement, 5, checkpoint->updated_at);
result = step_done(database, statement, "upsert checkpoint");
}
}
if (result == LARDON3D_PROJECT_DB_OK) {
result = execute(database, "COMMIT", "commit task record");
}
if (result != LARDON3D_PROJECT_DB_OK) {
(void)execute(database, "ROLLBACK", "rollback task record");
}
(void)pthread_mutex_unlock(&database->mutex);
return result;
}
Lardon3DProjectDbResult lardon3d_project_db_record_task(
Lardon3DProjectDb *database, const Lardon3DTaskDurableSnapshot *snapshot, const char *task_kind,
uint32_t task_kind_version, const Lardon3DProjectDbCheckpoint *checkpoint, int64_t updated_at) {
return record_task_internal(database, snapshot, task_kind, task_kind_version, checkpoint, NULL, 0,
NULL, NULL, NULL, NULL, NULL, NULL, NULL, updated_at);
}
Lardon3DProjectDbResult lardon3d_project_db_record_image_import_task(
Lardon3DProjectDb *database, const Lardon3DTaskDurableSnapshot *snapshot, const char *task_kind,
uint32_t task_kind_version, const Lardon3DProjectDbCheckpoint *checkpoint,
const char *source_path, uint64_t scanset_id, int64_t updated_at) {
if (!source_path) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
return record_task_internal(database, snapshot, task_kind, task_kind_version, checkpoint,
source_path, scanset_id, NULL, NULL, NULL, NULL, NULL, NULL,
NULL, updated_at);
}
Lardon3DProjectDbResult lardon3d_project_db_record_feature_extract_task(
Lardon3DProjectDb *database, const Lardon3DTaskDurableSnapshot *snapshot, const char *task_kind,
uint32_t task_kind_version, const Lardon3DProjectDbCheckpoint *checkpoint,
const Lardon3DProjectDbFeatureExtractTask *parameters, int64_t updated_at) {
if (!parameters) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
return record_task_internal(database, snapshot, task_kind, task_kind_version, checkpoint, NULL, 0,
parameters, NULL, NULL, NULL, NULL, NULL, NULL, updated_at);
}
Lardon3DProjectDbResult lardon3d_project_db_record_sift_extract_task(
Lardon3DProjectDb *database, const Lardon3DTaskDurableSnapshot *snapshot,
const char *task_kind, uint32_t task_kind_version,
const Lardon3DProjectDbCheckpoint *checkpoint,
const Lardon3DProjectDbSiftExtractTask *parameters, int64_t updated_at) {
if (!parameters) return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
return record_task_internal(database, snapshot, task_kind, task_kind_version, checkpoint, NULL, 0,
NULL, parameters, NULL, NULL, NULL, NULL, NULL, updated_at);
}
static bool read_task(sqlite3_stmt *statement, Lardon3DProjectDbTask *task) {
memset(task, 0, sizeof(*task));
sqlite3_int64 id = sqlite3_column_int64(statement, 0);
bool has_kind = sqlite3_column_type(statement, 2) != SQLITE_NULL;
bool has_kind_version = sqlite3_column_type(statement, 3) != SQLITE_NULL;
int progress = sqlite3_column_int(statement, 6);
sqlite3_int64 sequence_count = sqlite3_column_int64(statement, 7);
sqlite3_int64 started_nsec = sqlite3_column_int64(statement, 9);
sqlite3_int64 finished_nsec = sqlite3_column_int64(statement, 11);
if (id <= 0 || !copy_column(statement, 1, task->name, sizeof(task->name))) {
return false;
}
if (has_kind != has_kind_version) {
return false;
}
if (has_kind) {
sqlite3_int64 version = sqlite3_column_int64(statement, 3);
if (!copy_column(statement, 2, task->task_kind, sizeof(task->task_kind)) ||
!lardon3d_task_kind_is_valid(task->task_kind) || version <= 0 || version > UINT32_MAX) {
return false;
}
task->has_task_kind = true;
task->task_kind_version = (uint32_t)version;
}
task->task_id = (uint64_t)id;
task->saved_state = (Lardon3DTaskState)sqlite3_column_int(statement, 4);
task->recovery_state = (Lardon3DTaskState)sqlite3_column_int(statement, 5);
if (progress < 0 || progress > 100 || sequence_count < 0 || sequence_count > UINT_MAX ||
started_nsec < 0 || started_nsec >= 1000000000 || finished_nsec < 0 ||
finished_nsec >= 1000000000 ||
!database_time(sqlite3_column_int64(statement, 8), &task->started_at.tv_sec) ||
!database_time(sqlite3_column_int64(statement, 10), &task->finished_at.tv_sec)) {
return false;
}
task->progress = (unsigned int)progress;
task->sequence_count = (unsigned int)sequence_count;
task->started_at.tv_nsec = (long)started_nsec;
task->finished_at.tv_nsec = (long)finished_nsec;
task->updated_at = sqlite3_column_int64(statement, 12);
if (sqlite3_column_type(statement, 13) != SQLITE_NULL) {
task->has_checkpoint = true;
if (!copy_column(statement, 13, task->checkpoint.path, sizeof(task->checkpoint.path))) {
return false;
}
sqlite3_int64 format_version = sqlite3_column_int64(statement, 14);
if (format_version <= 0 || format_version > UINT32_MAX) {
return false;
}
task->checkpoint.format_version = (uint32_t)format_version;
task->checkpoint.durability =
(Lardon3DProjectDbCheckpointDurability)sqlite3_column_int(statement, 15);
task->checkpoint.updated_at = sqlite3_column_int64(statement, 16);
if (task->checkpoint.durability < LARDON3D_DB_CHECKPOINT_DURABLE ||
task->checkpoint.durability > LARDON3D_DB_CHECKPOINT_PUBLISHED_NOT_DURABLE) {
return false;
}
}
return valid_state(task->saved_state) && valid_state(task->recovery_state) &&
task->progress <= 100;
}
static const char task_select[] =
"SELECT "
"t.task_id,t.name,t.task_kind,t.task_kind_version,t.saved_state,t.recovery_state,t.progress,t."
"sequence_count,t.started_sec,t.started_nsec,"
"t.finished_sec,t.finished_nsec,t.updated_at,c.path,c.format_version,c.durability,c.updated_at "
"FROM tasks t LEFT JOIN checkpoints c ON c.task_id=t.task_id ";
Lardon3DProjectDbResult lardon3d_project_db_load_task(Lardon3DProjectDb *database, uint64_t task_id,
Lardon3DProjectDbTask *task) {
if (!database || !valid_task_id(task_id) || !task) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
(void)pthread_mutex_lock(&database->mutex);
sqlite3_stmt *statement = NULL;
char sql[768];
(void)snprintf(sql, sizeof(sql), "%s WHERE t.task_id=?1", task_select);
Lardon3DProjectDbResult result = prepare(database, sql, &statement);
if (result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)task_id);
int code = sqlite3_step(statement);
if (code == SQLITE_DONE) {
result = LARDON3D_PROJECT_DB_NOT_FOUND;
} else if (code != SQLITE_ROW || !read_task(statement, task)) {
result = LARDON3D_PROJECT_DB_CORRUPT;
}
(void)sqlite3_finalize(statement);
}
(void)pthread_mutex_unlock(&database->mutex);
return result;
}
Lardon3DProjectDbResult lardon3d_project_db_list_recoverable(Lardon3DProjectDb *database,
uint64_t after_task_id,
Lardon3DProjectDbTask *tasks,
size_t capacity, size_t *count) {
if (count) {
*count = 0;
}
if (!database || !tasks || !count || after_task_id > INT64_MAX || capacity == 0 ||
capacity > LARDON3D_PROJECT_DB_RECOVERY_PAGE_MAX) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
#ifdef LARDON3D_PROJECT_DB_TESTING
const char *forced_busy = getenv("LARDON3D_TEST_PROJECT_DB_BUSY_RECOVERY");
if (forced_busy && strcmp(forced_busy, "1") == 0) {
return LARDON3D_PROJECT_DB_BUSY;
}
#endif
(void)pthread_mutex_lock(&database->mutex);
sqlite3_stmt *statement = NULL;
char sql[896];
(void)snprintf(sql, sizeof(sql),
"%s WHERE t.recovery_state=?1 AND c.task_id IS NOT NULL AND t.task_id>?2 ORDER "
"BY t.task_id LIMIT ?3",
task_select);
Lardon3DProjectDbResult result = prepare(database, sql, &statement);
if (result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int(statement, 1, TASK_PENDING);
(void)sqlite3_bind_int64(statement, 2, (sqlite3_int64)after_task_id);
(void)sqlite3_bind_int64(statement, 3, (sqlite3_int64)capacity);
int code = SQLITE_DONE;
while (*count < capacity && (code = sqlite3_step(statement)) == SQLITE_ROW) {
if (!read_task(statement, &tasks[*count])) {
result = LARDON3D_PROJECT_DB_CORRUPT;
break;
}
++*count;
}
if (result == LARDON3D_PROJECT_DB_OK && *count < capacity && code != SQLITE_DONE) {
result = sqlite_result(database, code, "list recoverable");
}
(void)sqlite3_finalize(statement);
}
(void)pthread_mutex_unlock(&database->mutex);
return result;
}
Lardon3DProjectDbResult lardon3d_project_db_allocate_task_id(Lardon3DProjectDb *database,
uint64_t *task_id) {
if (task_id) {
*task_id = 0;
}
if (!database || !task_id) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
(void)pthread_mutex_lock(&database->mutex);
Lardon3DProjectDbResult result = execute(database, "BEGIN IMMEDIATE", "begin task id allocation");
sqlite3_stmt *statement = NULL;
sqlite3_int64 next = 0;
if (result == LARDON3D_PROJECT_DB_OK) {
result = prepare(database, "SELECT value FROM metadata WHERE key='next_task_id'", &statement);
}
if (result == LARDON3D_PROJECT_DB_OK) {
int code = sqlite3_step(statement);
if (code != SQLITE_ROW || sqlite3_column_type(statement, 0) != SQLITE_INTEGER) {
result = LARDON3D_PROJECT_DB_CORRUPT;
} else {
next = sqlite3_column_int64(statement, 0);
if (next <= 0 || next > INT64_MAX) {
result = LARDON3D_PROJECT_DB_CONSTRAINT;
}
}
(void)sqlite3_finalize(statement);
statement = NULL;
}
if (result == LARDON3D_PROJECT_DB_OK) {
result = prepare(database,
"UPDATE metadata SET value=CASE WHEN value=9223372036854775807 THEN 0 "
"ELSE value+1 END WHERE key='next_task_id' AND value=?1",
&statement);
}
if (result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, next);
result = step_done(database, statement, "advance task id");
statement = NULL;
if (result == LARDON3D_PROJECT_DB_OK && sqlite3_changes(database->connection) != 1) {
result = LARDON3D_PROJECT_DB_CONSTRAINT;
}
}
if (result == LARDON3D_PROJECT_DB_OK) {
result = execute(database, "COMMIT", "commit task id allocation");
}
if (result != LARDON3D_PROJECT_DB_OK) {
(void)execute(database, "ROLLBACK", "rollback task id allocation");
} else {
*task_id = (uint64_t)next;
}
(void)pthread_mutex_unlock(&database->mutex);
return result;
}
Lardon3DProjectDbResult
lardon3d_project_db_load_image_import(Lardon3DProjectDb *database, uint64_t task_id,
Lardon3DProjectDbImageImport *parameters) {
if (!database || !valid_task_id(task_id) || !parameters) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
memset(parameters, 0, sizeof(*parameters));
(void)pthread_mutex_lock(&database->mutex);
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result =
prepare(database, "SELECT source_path,scanset_id FROM image_import_tasks WHERE task_id=?1",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)task_id);
int code = sqlite3_step(statement);
if (code == SQLITE_DONE) {
result = LARDON3D_PROJECT_DB_NOT_FOUND;
} else if (code != SQLITE_ROW || !copy_column(statement, 0, parameters->source_path,
sizeof(parameters->source_path))) {
result = LARDON3D_PROJECT_DB_CORRUPT;
} else {
sqlite3_int64 scanset = sqlite3_column_int64(statement, 1);
if (scanset <= 0) {
result = LARDON3D_PROJECT_DB_CORRUPT;
} else {
parameters->task_id = task_id;
parameters->scanset_id = (uint64_t)scanset;
}
}
(void)sqlite3_finalize(statement);
}
(void)pthread_mutex_unlock(&database->mutex);
return result;
}
static bool valid_catalog_id(uint64_t value) { return value > 0 && value <= INT64_MAX; }
static bool valid_original_name(const char *name) {
return bounded_text(name, LARDON3D_PROJECT_DB_IMAGE_NAME_CAPACITY, false) && !strchr(name, '/') &&
!strchr(name, '\\') && !strchr(name, '\t') && !strchr(name, '\r') && !strchr(name, '\n');
}
static bool valid_relative_asset_path(const char *path) {
return bounded_text(path, LARDON3D_PROJECT_DB_PATH_CAPACITY, false) && path[0] != '/' &&
!strchr(path, '\\') && strncmp(path, "../", 3) != 0 && !strstr(path, "/../") &&
strcmp(path, "..") != 0;
}
static bool canonical_asset_path(const unsigned char hash[LARDON3D_PROJECT_DB_SHA256_SIZE],
const char *path) {
static const char digits[] = "0123456789abcdef";
char hex[65];
for (size_t index = 0; index < LARDON3D_PROJECT_DB_SHA256_SIZE; ++index) {
hex[index * 2] = digits[hash[index] >> 4];
hex[index * 2 + 1] = digits[hash[index] & 15U];
}
hex[64] = '\0';
char expected[LARDON3D_PROJECT_DB_PATH_CAPACITY];
int written = snprintf(expected, sizeof(expected), "assets/images/%c%c/%s", hex[0], hex[1], hex);
return written > 0 && (size_t)written < sizeof(expected) && strcmp(path, expected) == 0;
}
Lardon3DProjectDbResult lardon3d_project_db_create_scanset(Lardon3DProjectDb *database,
const char *name,
Lardon3DProjectDbScanSet *scanset) {
if (!database || !scanset ||
!bounded_text(name, LARDON3D_PROJECT_DB_SCANSET_NAME_CAPACITY, false)) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
int64_t now = (int64_t)time(NULL);
if (now < 0) {
return LARDON3D_PROJECT_DB_IO_ERROR;
}
memset(scanset, 0, sizeof(*scanset));
(void)pthread_mutex_lock(&database->mutex);
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result = prepare(
database, "INSERT INTO scansets(name,created_at,updated_at) VALUES(?1,?2,?2)", &statement);
if (result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_text(statement, 1, name, -1, SQLITE_TRANSIENT);
(void)sqlite3_bind_int64(statement, 2, now);
result = step_done(database, statement, "create scanset");
}
sqlite3_int64 id = sqlite3_last_insert_rowid(database->connection);
if (result == LARDON3D_PROJECT_DB_OK && id <= 0) {
result = LARDON3D_PROJECT_DB_CONSTRAINT;
}
if (result == LARDON3D_PROJECT_DB_OK) {
scanset->scanset_id = (uint64_t)id;
scanset->created_at = now;
scanset->updated_at = now;
(void)snprintf(scanset->name, sizeof(scanset->name), "%s", name);
}
(void)pthread_mutex_unlock(&database->mutex);
return result;
}
static bool read_scanset(sqlite3_stmt *statement, Lardon3DProjectDbScanSet *scanset) {
sqlite3_int64 id = sqlite3_column_int64(statement, 0);
sqlite3_int64 created = sqlite3_column_int64(statement, 2);
sqlite3_int64 updated = sqlite3_column_int64(statement, 3);
if (id <= 0 || created < 0 || updated < created ||
!copy_column(statement, 1, scanset->name, sizeof(scanset->name))) {
return false;
}
scanset->scanset_id = (uint64_t)id;
scanset->created_at = created;
scanset->updated_at = updated;
return true;
}
Lardon3DProjectDbResult lardon3d_project_db_load_scanset(Lardon3DProjectDb *database,
uint64_t scanset_id,
Lardon3DProjectDbScanSet *scanset) {
if (!database || !valid_catalog_id(scanset_id) || !scanset) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
memset(scanset, 0, sizeof(*scanset));
(void)pthread_mutex_lock(&database->mutex);
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result = prepare(
database, "SELECT scanset_id,name,created_at,updated_at FROM scansets WHERE scanset_id=?1",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)scanset_id);
int code = sqlite3_step(statement);
if (code == SQLITE_DONE) {
result = LARDON3D_PROJECT_DB_NOT_FOUND;
} else if (code != SQLITE_ROW || !read_scanset(statement, scanset)) {
result = LARDON3D_PROJECT_DB_CORRUPT;
}
(void)sqlite3_finalize(statement);
}
(void)pthread_mutex_unlock(&database->mutex);
return result;
}
Lardon3DProjectDbResult lardon3d_project_db_list_scansets(Lardon3DProjectDb *database,
uint64_t after_scanset_id,
Lardon3DProjectDbScanSet *scansets,
size_t capacity, size_t *count) {
if (count) {
*count = 0;
}
if (!database || !scansets || !count || after_scanset_id > INT64_MAX || capacity == 0 ||
capacity > LARDON3D_PROJECT_DB_CATALOG_PAGE_MAX) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
(void)pthread_mutex_lock(&database->mutex);
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result =
prepare(database,
"SELECT scanset_id,name,created_at,updated_at FROM scansets WHERE scanset_id>?1 "
"ORDER BY scanset_id LIMIT ?2",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)after_scanset_id);
(void)sqlite3_bind_int64(statement, 2, (sqlite3_int64)capacity);
int code = SQLITE_DONE;
while (*count < capacity && (code = sqlite3_step(statement)) == SQLITE_ROW) {
if (!read_scanset(statement, &scansets[*count])) {
result = LARDON3D_PROJECT_DB_CORRUPT;
break;
}
++*count;
}
if (result == LARDON3D_PROJECT_DB_OK && *count < capacity && code != SQLITE_DONE) {
result = sqlite_result(database, code, "list scansets");
}
(void)sqlite3_finalize(statement);
}
(void)pthread_mutex_unlock(&database->mutex);
return result;
}
static bool read_candidate_pair(sqlite3_stmt *statement, Lardon3DProjectDbCandidatePair *pair) {
sqlite3_int64 id = sqlite3_column_int64(statement, 0);
sqlite3_int64 image_a = sqlite3_column_int64(statement, 1);
sqlite3_int64 image_b = sqlite3_column_int64(statement, 2);
sqlite3_int64 created = sqlite3_column_int64(statement, 3);
if (id <= 0 || image_a <= 0 || image_b <= 0 || image_a >= image_b || created < 0) {
return false;
}
pair->candidate_pair_id = (uint64_t)id;
pair->image_id_a = (uint64_t)image_a;
pair->image_id_b = (uint64_t)image_b;
pair->created_at = created;
return true;
}
Lardon3DProjectDbResult lardon3d_project_db_create_candidate_pair(
Lardon3DProjectDb *database, uint64_t image_id_a, uint64_t image_id_b, int64_t created_at,
Lardon3DProjectDbCandidatePair *pair) {
if (!database || !valid_catalog_id(image_id_a) || !valid_catalog_id(image_id_b) ||
created_at < 0 || !pair) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
uint64_t canonical_a = image_id_a < image_id_b ? image_id_a : image_id_b;
uint64_t canonical_b = image_id_a < image_id_b ? image_id_b : image_id_a;
memset(pair, 0, sizeof(*pair));
(void)pthread_mutex_lock(&database->mutex);
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result = prepare(
database,
"INSERT INTO candidate_pairs(image_id_a,image_id_b,created_at) VALUES(?1,?2,?3)",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)canonical_a);
(void)sqlite3_bind_int64(statement, 2, (sqlite3_int64)canonical_b);
(void)sqlite3_bind_int64(statement, 3, created_at);
result = step_done(database, statement, "create candidate pair");
}
sqlite3_int64 id = sqlite3_last_insert_rowid(database->connection);
if (result == LARDON3D_PROJECT_DB_OK && id <= 0) {
result = LARDON3D_PROJECT_DB_CONSTRAINT;
}
if (result == LARDON3D_PROJECT_DB_OK) {
pair->candidate_pair_id = (uint64_t)id;
pair->image_id_a = canonical_a;
pair->image_id_b = canonical_b;
pair->created_at = created_at;
}
(void)pthread_mutex_unlock(&database->mutex);
return result;
}
Lardon3DProjectDbResult lardon3d_project_db_load_candidate_pair(
Lardon3DProjectDb *database, uint64_t candidate_pair_id,
Lardon3DProjectDbCandidatePair *pair) {
if (!database || !valid_catalog_id(candidate_pair_id) || !pair) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
memset(pair, 0, sizeof(*pair));
(void)pthread_mutex_lock(&database->mutex);
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result = prepare(
database,
"SELECT candidate_pair_id,image_id_a,image_id_b,created_at FROM candidate_pairs WHERE "
"candidate_pair_id=?1",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)candidate_pair_id);
int code = sqlite3_step(statement);
if (code == SQLITE_DONE) {
result = LARDON3D_PROJECT_DB_NOT_FOUND;
} else if (code != SQLITE_ROW || !read_candidate_pair(statement, pair)) {
result = LARDON3D_PROJECT_DB_CORRUPT;
}
(void)sqlite3_finalize(statement);
}
(void)pthread_mutex_unlock(&database->mutex);
return result;
}
Lardon3DProjectDbResult lardon3d_project_db_find_candidate_pair(
Lardon3DProjectDb *database, uint64_t image_id_a, uint64_t image_id_b,
Lardon3DProjectDbCandidatePair *pair) {
if (!database || !valid_catalog_id(image_id_a) || !valid_catalog_id(image_id_b) || !pair) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
uint64_t canonical_a = image_id_a < image_id_b ? image_id_a : image_id_b;
uint64_t canonical_b = image_id_a < image_id_b ? image_id_b : image_id_a;
memset(pair, 0, sizeof(*pair));
(void)pthread_mutex_lock(&database->mutex);
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result = prepare(
database,
"SELECT candidate_pair_id,image_id_a,image_id_b,created_at FROM candidate_pairs WHERE "
"image_id_a=?1 AND image_id_b=?2",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)canonical_a);
(void)sqlite3_bind_int64(statement, 2, (sqlite3_int64)canonical_b);
int code = sqlite3_step(statement);
if (code == SQLITE_DONE) {
result = LARDON3D_PROJECT_DB_NOT_FOUND;
} else if (code != SQLITE_ROW || !read_candidate_pair(statement, pair)) {
result = LARDON3D_PROJECT_DB_CORRUPT;
}
(void)sqlite3_finalize(statement);
}
(void)pthread_mutex_unlock(&database->mutex);
return result;
}
Lardon3DProjectDbResult lardon3d_project_db_list_candidate_pairs(
Lardon3DProjectDb *database, uint64_t after_candidate_pair_id,
Lardon3DProjectDbCandidatePair *pairs, size_t capacity, size_t *count) {
if (count) {
*count = 0;
}
if (!database || !pairs || !count || after_candidate_pair_id > INT64_MAX || capacity == 0 ||
capacity > LARDON3D_PROJECT_DB_CANDIDATE_PAIR_PAGE_MAX) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
(void)pthread_mutex_lock(&database->mutex);
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result = prepare(
database,
"SELECT candidate_pair_id,image_id_a,image_id_b,created_at FROM candidate_pairs WHERE "
"candidate_pair_id>?1 ORDER BY candidate_pair_id LIMIT ?2",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)after_candidate_pair_id);
(void)sqlite3_bind_int64(statement, 2, (sqlite3_int64)capacity);
int code = SQLITE_DONE;
while (*count < capacity && (code = sqlite3_step(statement)) == SQLITE_ROW) {
if (!read_candidate_pair(statement, &pairs[*count])) {
result = LARDON3D_PROJECT_DB_CORRUPT;
break;
}
++*count;
}
if (result == LARDON3D_PROJECT_DB_OK && *count < capacity && code != SQLITE_DONE) {
result = sqlite_result(database, code, "list candidate pairs");
}
(void)sqlite3_finalize(statement);
}
(void)pthread_mutex_unlock(&database->mutex);
return result;
}
static bool read_image(sqlite3_stmt *statement, Lardon3DProjectDbImage *image,
Lardon3DProjectDbImageAsset *asset) {
sqlite3_int64 image_id = sqlite3_column_int64(statement, 0);
sqlite3_int64 scanset_id = sqlite3_column_int64(statement, 1);
sqlite3_int64 asset_id = sqlite3_column_int64(statement, 2);
sqlite3_int64 size = sqlite3_column_int64(statement, 7);
int hash_size = sqlite3_column_bytes(statement, 5);
const void *hash = sqlite3_column_blob(statement, 5);
if (image_id <= 0 || scanset_id <= 0 || asset_id <= 0 || size < 0 ||
hash_size != LARDON3D_PROJECT_DB_SHA256_SIZE || !hash ||
!copy_column(statement, 3, image->original_name, sizeof(image->original_name)) ||
!copy_column(statement, 4, image->source_path, sizeof(image->source_path)) ||
!copy_column(statement, 6, asset->path, sizeof(asset->path)) ||
!valid_original_name(image->original_name) || !valid_relative_asset_path(asset->path)) {
return false;
}
image->image_id = (uint64_t)image_id;
image->scanset_id = (uint64_t)scanset_id;
image->asset_id = (uint64_t)asset_id;
asset->asset_id = (uint64_t)asset_id;
memcpy(asset->sha256, hash, LARDON3D_PROJECT_DB_SHA256_SIZE);
if (!canonical_asset_path(asset->sha256, asset->path)) {
return false;
}
asset->size_bytes = (uint64_t)size;
asset->state = (Lardon3DProjectDbImageAssetState)sqlite3_column_int(statement, 8);
asset->created_at = sqlite3_column_int64(statement, 9);
image->has_producer_task = sqlite3_column_type(statement, 10) != SQLITE_NULL;
sqlite3_int64 producer = sqlite3_column_int64(statement, 10);
if (image->has_producer_task && producer <= 0) {
return false;
}
image->producer_task_id = image->has_producer_task ? (uint64_t)producer : 0;
image->imported_at = sqlite3_column_int64(statement, 11);
return asset->state == LARDON3D_DB_IMAGE_ASSET_READY && asset->created_at >= 0 &&
image->imported_at >= 0;
}
static const char image_select[] =
"SELECT "
"i.image_id,i.scanset_id,i.asset_id,i.original_name,i.source_path,a.sha256,a.path,a.size_bytes,"
"a.state,a.created_at,i.producer_task_id,i.imported_at FROM images i JOIN image_assets a ON "
"a.asset_id=i.asset_id ";
static Lardon3DProjectDbResult ensure_capture_for_registered_image_locked(
Lardon3DProjectDb *database, uint64_t scanset_id, uint64_t asset_id, uint64_t image_id) {
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result = prepare(
database, "SELECT capture_id FROM capture_images WHERE image_id=?1", &statement);
if (result != LARDON3D_PROJECT_DB_OK) {
return result;
}
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)image_id);
int code = sqlite3_step(statement);
if (code == SQLITE_ROW) {
(void)sqlite3_finalize(statement);
return LARDON3D_PROJECT_DB_OK;
}
(void)sqlite3_finalize(statement);
if (code != SQLITE_DONE) {
return sqlite_result(database, code, "find capture for registered image");
}
result = prepare(
database, "INSERT INTO captures(scanset_id,created_at) "
"SELECT scanset_id,imported_at FROM images WHERE image_id=?1 AND scanset_id=?2",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)image_id);
(void)sqlite3_bind_int64(statement, 2, (sqlite3_int64)scanset_id);
result = step_done(database, statement, "create initial image capture");
statement = NULL;
}
if (result != LARDON3D_PROJECT_DB_OK) {
return result;
}
if (sqlite3_changes(database->connection) != 1) {
return LARDON3D_PROJECT_DB_CORRUPT;
}
sqlite3_int64 capture_id = sqlite3_last_insert_rowid(database->connection);
if (capture_id <= 0) {
return LARDON3D_PROJECT_DB_CORRUPT;
}
result = prepare(database,
"INSERT INTO capture_assets(capture_id,asset_id,role) VALUES(?1,?2,1)",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, capture_id);
(void)sqlite3_bind_int64(statement, 2, (sqlite3_int64)asset_id);
result = step_done(database, statement, "attach initial image source asset");
statement = NULL;
}
if (result == LARDON3D_PROJECT_DB_OK) {
result = prepare(database,
"INSERT INTO capture_images(capture_id,image_id) VALUES(?1,?2)",
&statement);
}
if (result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, capture_id);
(void)sqlite3_bind_int64(statement, 2, (sqlite3_int64)image_id);
result = step_done(database, statement, "attach initial image capture");
statement = NULL;
}
if (result == LARDON3D_PROJECT_DB_OK) {
result = prepare(database,
"INSERT INTO capture_selections(capture_id,image_id) VALUES(?1,?2)",
&statement);
}
if (result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, capture_id);
(void)sqlite3_bind_int64(statement, 2, (sqlite3_int64)image_id);
result = step_done(database, statement, "select initial capture image");
statement = NULL;
}
return result;
}
static Lardon3DProjectDbResult insert_or_find_image_locked(
Lardon3DProjectDb *database, uint64_t scanset_id, uint64_t asset_id,
const char *original_name, const char *source_path, uint64_t producer_task_id,
int64_t imported_at, int *inserted, uint64_t *image_id) {
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result = prepare(
database,
"INSERT INTO images(scanset_id,asset_id,original_name,source_path,producer_task_id,"
"imported_at) VALUES(?1,?2,?3,?4,?5,?6) ON CONFLICT(scanset_id,asset_id) DO NOTHING",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)scanset_id);
(void)sqlite3_bind_int64(statement, 2, (sqlite3_int64)asset_id);
(void)sqlite3_bind_text(statement, 3, original_name, -1, SQLITE_TRANSIENT);
(void)sqlite3_bind_text(statement, 4, source_path, -1, SQLITE_TRANSIENT);
if (producer_task_id) {
(void)sqlite3_bind_int64(statement, 5, (sqlite3_int64)producer_task_id);
} else {
(void)sqlite3_bind_null(statement, 5);
}
(void)sqlite3_bind_int64(statement, 6, imported_at);
result = step_done(database, statement, "insert logical image");
statement = NULL;
}
if (result == LARDON3D_PROJECT_DB_OK) {
*inserted = sqlite3_changes(database->connection);
result = prepare(database, "SELECT image_id FROM images WHERE scanset_id=?1 AND asset_id=?2",
&statement);
}
if (result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)scanset_id);
(void)sqlite3_bind_int64(statement, 2, (sqlite3_int64)asset_id);
int code = sqlite3_step(statement);
if (code == SQLITE_ROW) {
sqlite3_int64 stored_image_id = sqlite3_column_int64(statement, 0);
if (stored_image_id <= 0) {
result = LARDON3D_PROJECT_DB_CORRUPT;
} else {
*image_id = (uint64_t)stored_image_id;
}
} else {
result = code == SQLITE_DONE ? LARDON3D_PROJECT_DB_CORRUPT
: sqlite_result(database, code, "load registered image");
}
(void)sqlite3_finalize(statement);
}
return result;
}
Lardon3DProjectDbResult lardon3d_project_db_register_image_asset(
Lardon3DProjectDb *database,
const unsigned char sha256[LARDON3D_PROJECT_DB_SHA256_SIZE], const char *asset_path,
uint64_t size_bytes, int64_t created_at, Lardon3DProjectDbImageAsset *asset) {
if (!database || !sha256 || !valid_relative_asset_path(asset_path) ||
!canonical_asset_path(sha256, asset_path) || size_bytes > INT64_MAX || created_at < 0 ||
!asset) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
memset(asset, 0, sizeof(*asset));
(void)pthread_mutex_lock(&database->mutex);
Lardon3DProjectDbResult result = execute(database, "BEGIN IMMEDIATE", "begin image asset register");
sqlite3_stmt *statement = NULL;
if (result == LARDON3D_PROJECT_DB_OK) {
result = prepare(database, "INSERT INTO image_assets(sha256,path,size_bytes,state,created_at) "
"VALUES(?1,?2,?3,1,?4) ON CONFLICT(sha256) DO NOTHING",
&statement);
}
if (result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_blob(statement, 1, sha256, LARDON3D_PROJECT_DB_SHA256_SIZE,
SQLITE_TRANSIENT);
(void)sqlite3_bind_text(statement, 2, asset_path, -1, SQLITE_TRANSIENT);
(void)sqlite3_bind_int64(statement, 3, (sqlite3_int64)size_bytes);
(void)sqlite3_bind_int64(statement, 4, created_at);
result = step_done(database, statement, "insert image asset");
statement = NULL;
}
if (result == LARDON3D_PROJECT_DB_OK) {
result = prepare(database, "SELECT asset_id,sha256,path,size_bytes,state,created_at FROM "
"image_assets WHERE sha256=?1", &statement);
}
if (result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_blob(statement, 1, sha256, LARDON3D_PROJECT_DB_SHA256_SIZE,
SQLITE_TRANSIENT);
int code = sqlite3_step(statement);
if (code == SQLITE_DONE) {
result = LARDON3D_PROJECT_DB_CORRUPT;
} else if (code != SQLITE_ROW) {
result = sqlite_result(database, code, "load registered image asset");
} else {
sqlite3_int64 id = sqlite3_column_int64(statement, 0);
sqlite3_int64 stored_size = sqlite3_column_int64(statement, 3);
sqlite3_int64 stored_created = sqlite3_column_int64(statement, 5);
const void *stored_hash = sqlite3_column_blob(statement, 1);
if (id <= 0 || stored_size < 0 || stored_created < 0 ||
sqlite3_column_bytes(statement, 1) != LARDON3D_PROJECT_DB_SHA256_SIZE || !stored_hash ||
!copy_column(statement, 2, asset->path, sizeof(asset->path)) ||
strcmp(asset->path, asset_path) != 0 || stored_size != (sqlite3_int64)size_bytes ||
sqlite3_column_int(statement, 4) != LARDON3D_DB_IMAGE_ASSET_READY) {
result = LARDON3D_PROJECT_DB_CORRUPT;
} else {
asset->asset_id = (uint64_t)id;
memcpy(asset->sha256, stored_hash, LARDON3D_PROJECT_DB_SHA256_SIZE);
asset->size_bytes = (uint64_t)stored_size;
asset->state = LARDON3D_DB_IMAGE_ASSET_READY;
asset->created_at = stored_created;
}
}
(void)sqlite3_finalize(statement);
statement = NULL;
}
if (result == LARDON3D_PROJECT_DB_OK) {
result = execute(database, "COMMIT", "commit image asset register");
}
if (result != LARDON3D_PROJECT_DB_OK) {
(void)execute(database, "ROLLBACK", "rollback image asset register");
}
(void)pthread_mutex_unlock(&database->mutex);
return result;
}
Lardon3DProjectDbResult lardon3d_project_db_register_image(
Lardon3DProjectDb *database, uint64_t scanset_id,
const unsigned char sha256[LARDON3D_PROJECT_DB_SHA256_SIZE], const char *asset_path,
uint64_t size_bytes, const char *original_name, const char *source_path,
uint64_t producer_task_id, int64_t imported_at, Lardon3DProjectDbImageRegisterStatus *status,
Lardon3DProjectDbImage *image) {
if (!database || !valid_catalog_id(scanset_id) || !sha256 ||
!valid_relative_asset_path(asset_path) || !canonical_asset_path(sha256, asset_path) ||
size_bytes > INT64_MAX || !valid_original_name(original_name) ||
!bounded_text(source_path, LARDON3D_PROJECT_DB_PATH_CAPACITY, false) ||
(producer_task_id != 0 && !valid_task_id(producer_task_id)) || imported_at < 0 || !status ||
!image) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
#ifdef LARDON3D_PROJECT_DB_TESTING
const char *failure = getenv("LARDON3D_TEST_PROJECT_DB_FAIL_IMAGE_REGISTER");
if (failure && strcmp(failure, "1") == 0) {
return LARDON3D_PROJECT_DB_BUSY;
}
#endif
memset(image, 0, sizeof(*image));
*status = LARDON3D_PROJECT_DB_IMAGE_REGISTERED;
(void)pthread_mutex_lock(&database->mutex);
Lardon3DProjectDbResult result = execute(database, "BEGIN IMMEDIATE", "begin image register");
sqlite3_stmt *statement = NULL;
if (result == LARDON3D_PROJECT_DB_OK) {
result = prepare(database,
"INSERT INTO image_assets(sha256,path,size_bytes,state,created_at) "
"VALUES(?1,?2,?3,1,?4) ON CONFLICT(sha256) DO NOTHING",
&statement);
}
if (result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_blob(statement, 1, sha256, LARDON3D_PROJECT_DB_SHA256_SIZE,
SQLITE_TRANSIENT);
(void)sqlite3_bind_text(statement, 2, asset_path, -1, SQLITE_TRANSIENT);
(void)sqlite3_bind_int64(statement, 3, (sqlite3_int64)size_bytes);
(void)sqlite3_bind_int64(statement, 4, imported_at);
result = step_done(database, statement, "insert image asset");
statement = NULL;
}
sqlite3_int64 asset_id = 0;
if (result == LARDON3D_PROJECT_DB_OK) {
result =
prepare(database, "SELECT asset_id,path,size_bytes,state FROM image_assets WHERE sha256=?1",
&statement);
}
if (result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_blob(statement, 1, sha256, LARDON3D_PROJECT_DB_SHA256_SIZE,
SQLITE_TRANSIENT);
int code = sqlite3_step(statement);
char stored_path[LARDON3D_PROJECT_DB_PATH_CAPACITY];
if (code != SQLITE_ROW || !copy_column(statement, 1, stored_path, sizeof(stored_path)) ||
strcmp(stored_path, asset_path) != 0 ||
sqlite3_column_int64(statement, 2) != (sqlite3_int64)size_bytes ||
sqlite3_column_int(statement, 3) != LARDON3D_DB_IMAGE_ASSET_READY) {
result = LARDON3D_PROJECT_DB_CORRUPT;
} else {
asset_id = sqlite3_column_int64(statement, 0);
}
(void)sqlite3_finalize(statement);
statement = NULL;
}
int inserted = 0;
uint64_t image_id = 0;
if (result == LARDON3D_PROJECT_DB_OK) {
result = insert_or_find_image_locked(database, scanset_id, (uint64_t)asset_id, original_name,
source_path, producer_task_id, imported_at, &inserted,
&image_id);
}
if (result == LARDON3D_PROJECT_DB_OK) {
result = ensure_capture_for_registered_image_locked(database, scanset_id, (uint64_t)asset_id,
image_id);
}
if (result == LARDON3D_PROJECT_DB_OK) {
result = execute(database, "COMMIT", "commit image register");
}
if (result != LARDON3D_PROJECT_DB_OK) {
(void)execute(database, "ROLLBACK", "rollback image register");
}
(void)pthread_mutex_unlock(&database->mutex);
if (result != LARDON3D_PROJECT_DB_OK) {
return result;
}
*status = inserted == 1 ? LARDON3D_PROJECT_DB_IMAGE_REGISTERED
: LARDON3D_PROJECT_DB_IMAGE_ALREADY_PRESENT;
return lardon3d_project_db_load_image(database, image_id, image,
&(Lardon3DProjectDbImageAsset){0});
}
Lardon3DProjectDbResult lardon3d_project_db_publish_derived_capture_image(
Lardon3DProjectDb *database, uint64_t capture_id, uint64_t asset_id,
const char *original_name, const char *source_path, uint64_t producer_task_id,
int64_t imported_at, Lardon3DProjectDbImageRegisterStatus *status,
Lardon3DProjectDbImage *image) {
if (!database || !valid_catalog_id(capture_id) || !valid_catalog_id(asset_id) ||
!valid_original_name(original_name) ||
!bounded_text(source_path, LARDON3D_PROJECT_DB_PATH_CAPACITY, false) ||
(producer_task_id != 0 && !valid_task_id(producer_task_id)) || imported_at < 0 || !status ||
!image) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
memset(image, 0, sizeof(*image));
*status = LARDON3D_PROJECT_DB_IMAGE_REGISTERED;
(void)pthread_mutex_lock(&database->mutex);
Lardon3DProjectDbResult result =
execute(database, "BEGIN IMMEDIATE", "begin derived capture image publication");
sqlite3_stmt *statement = NULL;
sqlite3_int64 scanset_id = 0;
if (result == LARDON3D_PROJECT_DB_OK) {
result = prepare(database, "SELECT scanset_id FROM captures WHERE capture_id=?1", &statement);
}
if (result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)capture_id);
int code = sqlite3_step(statement);
if (code == SQLITE_DONE) {
result = LARDON3D_PROJECT_DB_NOT_FOUND;
} else if (code != SQLITE_ROW || (scanset_id = sqlite3_column_int64(statement, 0)) <= 0) {
result = code == SQLITE_ROW ? LARDON3D_PROJECT_DB_CORRUPT
: sqlite_result(database, code, "load derived image capture");
}
(void)sqlite3_finalize(statement);
statement = NULL;
}
if (result == LARDON3D_PROJECT_DB_OK) {
result = prepare(database, "SELECT 1 FROM image_assets WHERE asset_id=?1", &statement);
}
if (result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)asset_id);
int code = sqlite3_step(statement);
if (code == SQLITE_DONE) {
result = LARDON3D_PROJECT_DB_NOT_FOUND;
} else if (code != SQLITE_ROW) {
result = sqlite_result(database, code, "load derived image asset");
}
(void)sqlite3_finalize(statement);
statement = NULL;
}
if (result == LARDON3D_PROJECT_DB_OK) {
result = prepare(
database,
"SELECT 1 FROM asset_derivations d JOIN capture_assets p "
"ON p.asset_id=d.parent_asset_id WHERE d.child_asset_id=?1 AND p.capture_id=?2",
&statement);
}
if (result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)asset_id);
(void)sqlite3_bind_int64(statement, 2, (sqlite3_int64)capture_id);
int code = sqlite3_step(statement);
if (code == SQLITE_DONE) {
result = LARDON3D_PROJECT_DB_CONSTRAINT;
} else if (code != SQLITE_ROW) {
result = sqlite_result(database, code, "load derived image provenance");
}
(void)sqlite3_finalize(statement);
statement = NULL;
}
int inserted = 0;
uint64_t image_id = 0;
if (result == LARDON3D_PROJECT_DB_OK) {
result = insert_or_find_image_locked(database, (uint64_t)scanset_id, asset_id, original_name,
source_path, producer_task_id, imported_at, &inserted,
&image_id);
}
sqlite3_int64 mapped_capture_id = 0;
if (result == LARDON3D_PROJECT_DB_OK) {
result = prepare(database, "SELECT capture_id FROM capture_images WHERE image_id=?1", &statement);
}
if (result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)image_id);
int code = sqlite3_step(statement);
if (code == SQLITE_ROW) {
mapped_capture_id = sqlite3_column_int64(statement, 0);
if (mapped_capture_id <= 0) {
result = LARDON3D_PROJECT_DB_CORRUPT;
} else if ((uint64_t)mapped_capture_id != capture_id) {
result = LARDON3D_PROJECT_DB_CONSTRAINT;
}
} else if (code != SQLITE_DONE) {
result = sqlite_result(database, code, "find derived image capture");
}
(void)sqlite3_finalize(statement);
statement = NULL;
}
if (result == LARDON3D_PROJECT_DB_OK) {
result = prepare(database, "SELECT role FROM capture_assets WHERE capture_id=?1 AND asset_id=?2",
&statement);
}
if (result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)capture_id);
(void)sqlite3_bind_int64(statement, 2, (sqlite3_int64)asset_id);
int code = sqlite3_step(statement);
if (code == SQLITE_ROW) {
if (sqlite3_column_int(statement, 0) != LARDON3D_DB_CAPTURE_ASSET_DERIVED) {
result = LARDON3D_PROJECT_DB_CONSTRAINT;
}
} else if (code == SQLITE_DONE) {
(void)sqlite3_finalize(statement);
statement = NULL;
result = prepare(database,
"INSERT INTO capture_assets(capture_id,asset_id,role) VALUES(?1,?2,?3)",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)capture_id);
(void)sqlite3_bind_int64(statement, 2, (sqlite3_int64)asset_id);
(void)sqlite3_bind_int(statement, 3, LARDON3D_DB_CAPTURE_ASSET_DERIVED);
result = step_done(database, statement, "attach derived capture asset");
statement = NULL;
}
} else {
result = sqlite_result(database, code, "find derived capture asset");
}
if (statement) {
(void)sqlite3_finalize(statement);
statement = NULL;
}
}
if (result == LARDON3D_PROJECT_DB_OK && mapped_capture_id == 0) {
result = prepare(database, "INSERT INTO capture_images(capture_id,image_id) VALUES(?1,?2)",
&statement);
}
if (result == LARDON3D_PROJECT_DB_OK && mapped_capture_id == 0) {
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)capture_id);
(void)sqlite3_bind_int64(statement, 2, (sqlite3_int64)image_id);
result = step_done(database, statement, "attach derived capture image");
statement = NULL;
}
if (result == LARDON3D_PROJECT_DB_OK) {
result = execute(database, "COMMIT", "commit derived capture image publication");
}
if (result != LARDON3D_PROJECT_DB_OK) {
(void)execute(database, "ROLLBACK", "rollback derived capture image publication");
}
(void)pthread_mutex_unlock(&database->mutex);
if (result != LARDON3D_PROJECT_DB_OK) {
return result;
}
*status = inserted == 1 ? LARDON3D_PROJECT_DB_IMAGE_REGISTERED
: LARDON3D_PROJECT_DB_IMAGE_ALREADY_PRESENT;
return lardon3d_project_db_load_image(database, image_id, image,
&(Lardon3DProjectDbImageAsset){0});
}
Lardon3DProjectDbResult lardon3d_project_db_load_image(Lardon3DProjectDb *database,
uint64_t image_id,
Lardon3DProjectDbImage *image,
Lardon3DProjectDbImageAsset *asset) {
if (!database || !valid_catalog_id(image_id) || !image || !asset) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
memset(image, 0, sizeof(*image));
memset(asset, 0, sizeof(*asset));
(void)pthread_mutex_lock(&database->mutex);
char sql[768];
(void)snprintf(sql, sizeof(sql), "%s WHERE i.image_id=?1", image_select);
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result = prepare(database, sql, &statement);
if (result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)image_id);
int code = sqlite3_step(statement);
if (code == SQLITE_DONE) {
result = LARDON3D_PROJECT_DB_NOT_FOUND;
} else if (code != SQLITE_ROW || !read_image(statement, image, asset)) {
result = LARDON3D_PROJECT_DB_CORRUPT;
}
(void)sqlite3_finalize(statement);
}
(void)pthread_mutex_unlock(&database->mutex);
return result;
}
Lardon3DProjectDbResult lardon3d_project_db_list_images(Lardon3DProjectDb *database,
uint64_t scanset_id,
uint64_t after_image_id,
Lardon3DProjectDbImage *images,
Lardon3DProjectDbImageAsset *assets,
size_t capacity, size_t *count) {
if (count) {
*count = 0;
}
if (!database || !valid_catalog_id(scanset_id) || after_image_id > INT64_MAX || !images ||
!assets || !count || capacity == 0 || capacity > LARDON3D_PROJECT_DB_CATALOG_PAGE_MAX) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
Lardon3DProjectDbScanSet scanset;
Lardon3DProjectDbResult exists = lardon3d_project_db_load_scanset(database, scanset_id, &scanset);
if (exists != LARDON3D_PROJECT_DB_OK) {
return exists;
}
(void)pthread_mutex_lock(&database->mutex);
char sql[896];
(void)snprintf(sql, sizeof(sql),
"%s WHERE i.scanset_id=?1 AND i.image_id>?2 ORDER BY i.image_id LIMIT ?3",
image_select);
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result = prepare(database, sql, &statement);
if (result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)scanset_id);
(void)sqlite3_bind_int64(statement, 2, (sqlite3_int64)after_image_id);
(void)sqlite3_bind_int64(statement, 3, (sqlite3_int64)capacity);
int code = SQLITE_DONE;
while (*count < capacity && (code = sqlite3_step(statement)) == SQLITE_ROW) {
memset(&images[*count], 0, sizeof(images[*count]));
memset(&assets[*count], 0, sizeof(assets[*count]));
if (!read_image(statement, &images[*count], &assets[*count])) {
result = LARDON3D_PROJECT_DB_CORRUPT;
break;
}
++*count;
}
if (result == LARDON3D_PROJECT_DB_OK && *count < capacity && code != SQLITE_DONE) {
result = sqlite_result(database, code, "list images");
}
(void)sqlite3_finalize(statement);
}
(void)pthread_mutex_unlock(&database->mutex);
return result;
}
Lardon3DProjectDbResult lardon3d_project_db_count_images(Lardon3DProjectDb *database,
uint64_t scanset_id, uint64_t *count) {
if (count) {
*count = 0;
}
if (!database || !valid_catalog_id(scanset_id) || !count) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
Lardon3DProjectDbScanSet scanset;
Lardon3DProjectDbResult exists = lardon3d_project_db_load_scanset(database, scanset_id, &scanset);
if (exists != LARDON3D_PROJECT_DB_OK) {
return exists;
}
(void)pthread_mutex_lock(&database->mutex);
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result =
prepare(database, "SELECT count(*) FROM images WHERE scanset_id=?1", &statement);
if (result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)scanset_id);
int code = sqlite3_step(statement);
sqlite3_int64 value = sqlite3_column_int64(statement, 0);
if (code != SQLITE_ROW || value < 0) {
result = LARDON3D_PROJECT_DB_CORRUPT;
} else {
*count = (uint64_t)value;
}
(void)sqlite3_finalize(statement);
}
(void)pthread_mutex_unlock(&database->mutex);
return result;
}
static bool valid_capture_asset_role(Lardon3DProjectDbCaptureAssetRole role) {
return role == LARDON3D_DB_CAPTURE_ASSET_SOURCE || role == LARDON3D_DB_CAPTURE_ASSET_DERIVED;
}
static bool valid_asset_derivation_kind(Lardon3DProjectDbAssetDerivationKind kind) {
return kind == LARDON3D_DB_ASSET_DERIVATION_GENERIC_VERSIONED;
}
static bool read_capture(sqlite3_stmt *statement, Lardon3DProjectDbCapture *capture) {
sqlite3_int64 capture_id = sqlite3_column_int64(statement, 0);
sqlite3_int64 scanset_id = sqlite3_column_int64(statement, 1);
sqlite3_int64 created_at = sqlite3_column_int64(statement, 2);
if (capture_id <= 0 || scanset_id <= 0 || created_at < 0) {
return false;
}
capture->capture_id = (uint64_t)capture_id;
capture->scanset_id = (uint64_t)scanset_id;
capture->created_at = created_at;
return true;
}
Lardon3DProjectDbResult lardon3d_project_db_create_capture(
Lardon3DProjectDb *database, uint64_t scanset_id, int64_t created_at,
Lardon3DProjectDbCapture *capture) {
if (!database || !valid_catalog_id(scanset_id) || created_at < 0 || !capture) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
memset(capture, 0, sizeof(*capture));
(void)pthread_mutex_lock(&database->mutex);
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result = prepare(
database, "INSERT INTO captures(scanset_id,created_at) VALUES(?1,?2)", &statement);
if (result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)scanset_id);
(void)sqlite3_bind_int64(statement, 2, created_at);
result = step_done(database, statement, "create capture");
}
sqlite3_int64 capture_id = sqlite3_last_insert_rowid(database->connection);
if (result == LARDON3D_PROJECT_DB_OK && capture_id <= 0) {
result = LARDON3D_PROJECT_DB_CONSTRAINT;
}
if (result == LARDON3D_PROJECT_DB_OK) {
capture->capture_id = (uint64_t)capture_id;
capture->scanset_id = scanset_id;
capture->created_at = created_at;
}
(void)pthread_mutex_unlock(&database->mutex);
return result;
}
Lardon3DProjectDbResult lardon3d_project_db_load_capture(
Lardon3DProjectDb *database, uint64_t capture_id, Lardon3DProjectDbCapture *capture) {
if (!database || !valid_catalog_id(capture_id) || !capture) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
memset(capture, 0, sizeof(*capture));
(void)pthread_mutex_lock(&database->mutex);
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result = prepare(
database, "SELECT capture_id,scanset_id,created_at FROM captures WHERE capture_id=?1",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)capture_id);
int code = sqlite3_step(statement);
if (code == SQLITE_DONE) {
result = LARDON3D_PROJECT_DB_NOT_FOUND;
} else if (code != SQLITE_ROW || !read_capture(statement, capture)) {
result = LARDON3D_PROJECT_DB_CORRUPT;
}
(void)sqlite3_finalize(statement);
}
(void)pthread_mutex_unlock(&database->mutex);
return result;
}
Lardon3DProjectDbResult lardon3d_project_db_list_captures(
Lardon3DProjectDb *database, uint64_t scanset_id, uint64_t after_capture_id,
Lardon3DProjectDbCapture *captures, size_t capacity, size_t *count) {
if (count) {
*count = 0;
}
if (!database || !valid_catalog_id(scanset_id) || after_capture_id > INT64_MAX || !captures ||
!count || capacity == 0 || capacity > LARDON3D_PROJECT_DB_CATALOG_PAGE_MAX) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
(void)pthread_mutex_lock(&database->mutex);
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result = prepare(
database, "SELECT capture_id,scanset_id,created_at FROM captures WHERE scanset_id=?1 "
"AND capture_id>?2 ORDER BY capture_id LIMIT ?3", &statement);
if (result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)scanset_id);
(void)sqlite3_bind_int64(statement, 2, (sqlite3_int64)after_capture_id);
(void)sqlite3_bind_int64(statement, 3, (sqlite3_int64)capacity);
int code = SQLITE_DONE;
while (*count < capacity && (code = sqlite3_step(statement)) == SQLITE_ROW) {
if (!read_capture(statement, &captures[*count])) {
result = LARDON3D_PROJECT_DB_CORRUPT;
break;
}
++*count;
}
if (result == LARDON3D_PROJECT_DB_OK && *count < capacity && code != SQLITE_DONE) {
result = sqlite_result(database, code, "list captures");
}
(void)sqlite3_finalize(statement);
}
(void)pthread_mutex_unlock(&database->mutex);
return result;
}
Lardon3DProjectDbResult lardon3d_project_db_attach_capture_asset(
Lardon3DProjectDb *database, uint64_t capture_id, uint64_t asset_id,
Lardon3DProjectDbCaptureAssetRole role) {
if (!database || !valid_catalog_id(capture_id) || !valid_catalog_id(asset_id) ||
!valid_capture_asset_role(role)) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
(void)pthread_mutex_lock(&database->mutex);
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result = prepare(
database, "INSERT INTO capture_assets(capture_id,asset_id,role) VALUES(?1,?2,?3)",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)capture_id);
(void)sqlite3_bind_int64(statement, 2, (sqlite3_int64)asset_id);
(void)sqlite3_bind_int(statement, 3, (int)role);
result = step_done(database, statement, "attach capture asset");
}
(void)pthread_mutex_unlock(&database->mutex);
return result;
}
Lardon3DProjectDbResult lardon3d_project_db_list_capture_assets(
Lardon3DProjectDb *database, uint64_t capture_id, uint64_t after_asset_id,
Lardon3DProjectDbCaptureAsset *assets, size_t capacity, size_t *count) {
if (count) {
*count = 0;
}
if (!database || !valid_catalog_id(capture_id) || after_asset_id > INT64_MAX || !assets ||
!count || capacity == 0 || capacity > LARDON3D_PROJECT_DB_CATALOG_PAGE_MAX) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
(void)pthread_mutex_lock(&database->mutex);
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result = prepare(
database, "SELECT capture_id,asset_id,role FROM capture_assets WHERE capture_id=?1 "
"AND asset_id>?2 ORDER BY asset_id LIMIT ?3", &statement);
if (result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)capture_id);
(void)sqlite3_bind_int64(statement, 2, (sqlite3_int64)after_asset_id);
(void)sqlite3_bind_int64(statement, 3, (sqlite3_int64)capacity);
int code = SQLITE_DONE;
while (*count < capacity && (code = sqlite3_step(statement)) == SQLITE_ROW) {
sqlite3_int64 stored_capture = sqlite3_column_int64(statement, 0);
sqlite3_int64 stored_asset = sqlite3_column_int64(statement, 1);
int stored_role = sqlite3_column_int(statement, 2);
if (stored_capture <= 0 || stored_asset <= 0 ||
!valid_capture_asset_role((Lardon3DProjectDbCaptureAssetRole)stored_role)) {
result = LARDON3D_PROJECT_DB_CORRUPT;
break;
}
assets[*count] = (Lardon3DProjectDbCaptureAsset){
.capture_id = (uint64_t)stored_capture,
.asset_id = (uint64_t)stored_asset,
.role = (Lardon3DProjectDbCaptureAssetRole)stored_role};
++*count;
}
if (result == LARDON3D_PROJECT_DB_OK && *count < capacity && code != SQLITE_DONE) {
result = sqlite_result(database, code, "list capture assets");
}
(void)sqlite3_finalize(statement);
}
(void)pthread_mutex_unlock(&database->mutex);
return result;
}
Lardon3DProjectDbResult lardon3d_project_db_attach_capture_image(
Lardon3DProjectDb *database, uint64_t capture_id, uint64_t image_id) {
if (!database || !valid_catalog_id(capture_id) || !valid_catalog_id(image_id)) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
(void)pthread_mutex_lock(&database->mutex);
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result = prepare(
database, "INSERT INTO capture_images(capture_id,image_id) "
"SELECT ?1,?2 WHERE EXISTS(SELECT 1 FROM captures c JOIN images i "
"ON i.scanset_id=c.scanset_id WHERE c.capture_id=?1 AND i.image_id=?2)",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)capture_id);
(void)sqlite3_bind_int64(statement, 2, (sqlite3_int64)image_id);
result = step_done(database, statement, "attach capture image");
if (result == LARDON3D_PROJECT_DB_OK && sqlite3_changes(database->connection) != 1) {
result = LARDON3D_PROJECT_DB_NOT_FOUND;
}
}
(void)pthread_mutex_unlock(&database->mutex);
return result;
}
Lardon3DProjectDbResult lardon3d_project_db_find_capture_for_image(
Lardon3DProjectDb *database, uint64_t image_id, Lardon3DProjectDbCapture *capture) {
if (!database || !valid_catalog_id(image_id) || !capture) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
memset(capture, 0, sizeof(*capture));
(void)pthread_mutex_lock(&database->mutex);
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result = prepare(
database, "SELECT c.capture_id,c.scanset_id,c.created_at FROM captures c JOIN "
"capture_images ci ON ci.capture_id=c.capture_id WHERE ci.image_id=?1",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)image_id);
int code = sqlite3_step(statement);
if (code == SQLITE_DONE) {
result = LARDON3D_PROJECT_DB_NOT_FOUND;
} else if (code != SQLITE_ROW || !read_capture(statement, capture)) {
result = LARDON3D_PROJECT_DB_CORRUPT;
}
(void)sqlite3_finalize(statement);
}
(void)pthread_mutex_unlock(&database->mutex);
return result;
}
Lardon3DProjectDbResult lardon3d_project_db_record_asset_derivation(
Lardon3DProjectDb *database, const Lardon3DProjectDbAssetDerivation *derivation) {
if (!database || !derivation || !valid_catalog_id(derivation->parent_asset_id) ||
!valid_catalog_id(derivation->child_asset_id) ||
derivation->parent_asset_id == derivation->child_asset_id ||
!valid_asset_derivation_kind(derivation->kind) || derivation->version == 0 ||
derivation->created_at < 0 ||
(derivation->has_producer_task && !valid_task_id(derivation->producer_task_id))) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
(void)pthread_mutex_lock(&database->mutex);
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result = prepare(
database, "INSERT INTO asset_derivations(child_asset_id,parent_asset_id,derivation_kind,"
"derivation_version,parameter_fingerprint,producer_task_id,created_at) "
"VALUES(?1,?2,?3,?4,?5,?6,?7)", &statement);
if (result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)derivation->child_asset_id);
(void)sqlite3_bind_int64(statement, 2, (sqlite3_int64)derivation->parent_asset_id);
(void)sqlite3_bind_int(statement, 3, (int)derivation->kind);
(void)sqlite3_bind_int64(statement, 4, (sqlite3_int64)derivation->version);
(void)sqlite3_bind_blob(statement, 5, derivation->parameter_fingerprint,
LARDON3D_PROJECT_DB_SHA256_SIZE, SQLITE_TRANSIENT);
if (derivation->has_producer_task) {
(void)sqlite3_bind_int64(statement, 6, (sqlite3_int64)derivation->producer_task_id);
} else {
(void)sqlite3_bind_null(statement, 6);
}
(void)sqlite3_bind_int64(statement, 7, derivation->created_at);
result = step_done(database, statement, "record asset derivation");
}
(void)pthread_mutex_unlock(&database->mutex);
return result;
}
Lardon3DProjectDbResult lardon3d_project_db_load_asset_derivation(
Lardon3DProjectDb *database, uint64_t child_asset_id,
Lardon3DProjectDbAssetDerivation *derivation) {
if (!database || !valid_catalog_id(child_asset_id) || !derivation) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
memset(derivation, 0, sizeof(*derivation));
(void)pthread_mutex_lock(&database->mutex);
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result = prepare(
database, "SELECT parent_asset_id,child_asset_id,derivation_kind,derivation_version,"
"parameter_fingerprint,producer_task_id,created_at FROM asset_derivations "
"WHERE child_asset_id=?1", &statement);
if (result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)child_asset_id);
int code = sqlite3_step(statement);
if (code == SQLITE_DONE) {
result = LARDON3D_PROJECT_DB_NOT_FOUND;
} else if (code != SQLITE_ROW) {
result = sqlite_result(database, code, "load asset derivation");
} else {
sqlite3_int64 parent = sqlite3_column_int64(statement, 0);
sqlite3_int64 child = sqlite3_column_int64(statement, 1);
sqlite3_int64 version = sqlite3_column_int64(statement, 3);
sqlite3_int64 producer = sqlite3_column_int64(statement, 5);
sqlite3_int64 created_at = sqlite3_column_int64(statement, 6);
const void *fingerprint = sqlite3_column_blob(statement, 4);
if (parent <= 0 || child <= 0 || parent == child || version <= 0 || version > UINT32_MAX ||
created_at < 0 ||
!valid_asset_derivation_kind((Lardon3DProjectDbAssetDerivationKind)
sqlite3_column_int(statement, 2)) ||
sqlite3_column_bytes(statement, 4) != LARDON3D_PROJECT_DB_SHA256_SIZE || !fingerprint ||
(sqlite3_column_type(statement, 5) != SQLITE_NULL && producer <= 0)) {
result = LARDON3D_PROJECT_DB_CORRUPT;
} else {
derivation->parent_asset_id = (uint64_t)parent;
derivation->child_asset_id = (uint64_t)child;
derivation->kind = (Lardon3DProjectDbAssetDerivationKind)sqlite3_column_int(statement, 2);
derivation->version = (uint32_t)version;
memcpy(derivation->parameter_fingerprint, fingerprint,
LARDON3D_PROJECT_DB_SHA256_SIZE);
derivation->has_producer_task = sqlite3_column_type(statement, 5) != SQLITE_NULL;
derivation->producer_task_id = derivation->has_producer_task ? (uint64_t)producer : 0;
derivation->created_at = created_at;
}
}
(void)sqlite3_finalize(statement);
}
(void)pthread_mutex_unlock(&database->mutex);
return result;
}
Lardon3DProjectDbResult lardon3d_project_db_set_selected_capture_image(
Lardon3DProjectDb *database, uint64_t capture_id, uint64_t image_id) {
if (!database || !valid_catalog_id(capture_id) || !valid_catalog_id(image_id)) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
(void)pthread_mutex_lock(&database->mutex);
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result = prepare(
database, "INSERT INTO capture_selections(capture_id,image_id) VALUES(?1,?2) "
"ON CONFLICT(capture_id) DO UPDATE SET image_id=excluded.image_id", &statement);
if (result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)capture_id);
(void)sqlite3_bind_int64(statement, 2, (sqlite3_int64)image_id);
result = step_done(database, statement, "set selected capture image");
}
(void)pthread_mutex_unlock(&database->mutex);
return result;
}
Lardon3DProjectDbResult lardon3d_project_db_get_selected_capture_image(
Lardon3DProjectDb *database, uint64_t capture_id, uint64_t *image_id) {
if (image_id) {
*image_id = 0;
}
if (!database || !valid_catalog_id(capture_id) || !image_id) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
(void)pthread_mutex_lock(&database->mutex);
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result = prepare(
database, "SELECT image_id FROM capture_selections WHERE capture_id=?1", &statement);
if (result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)capture_id);
int code = sqlite3_step(statement);
if (code == SQLITE_DONE) {
result = LARDON3D_PROJECT_DB_NOT_FOUND;
} else if (code != SQLITE_ROW) {
result = sqlite_result(database, code, "get selected capture image");
} else {
sqlite3_int64 selected = sqlite3_column_int64(statement, 0);
if (selected <= 0) {
result = LARDON3D_PROJECT_DB_CORRUPT;
} else {
*image_id = (uint64_t)selected;
}
}
(void)sqlite3_finalize(statement);
}
(void)pthread_mutex_unlock(&database->mutex);
return result;
}
Lardon3DProjectDbResult
lardon3d_project_db_create_artifact(Lardon3DProjectDb *database,
const Lardon3DProjectDbArtifact *artifact) {
if (!database || !artifact ||
!bounded_text(artifact->artifact_id, sizeof(artifact->artifact_id), false) ||
!bounded_text(artifact->kind, sizeof(artifact->kind), false) ||
!bounded_text(artifact->path, sizeof(artifact->path), false) ||
artifact->state != LARDON3D_DB_ARTIFACT_STAGED || artifact->size_bytes != 0 ||
artifact->created_at < 0 || artifact->updated_at < artifact->created_at ||
(artifact->has_producer_task && !valid_task_id(artifact->producer_task_id))) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
(void)pthread_mutex_lock(&database->mutex);
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result =
prepare(database,
"INSERT INTO "
"artifacts(artifact_id,kind,path,state,size_bytes,producer_task_id,created_at,"
"updated_at) VALUES(?1,?2,?3,0,0,?4,?5,?6)",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_text(statement, 1, artifact->artifact_id, -1, SQLITE_TRANSIENT);
(void)sqlite3_bind_text(statement, 2, artifact->kind, -1, SQLITE_TRANSIENT);
(void)sqlite3_bind_text(statement, 3, artifact->path, -1, SQLITE_TRANSIENT);
if (artifact->has_producer_task) {
(void)sqlite3_bind_int64(statement, 4, (sqlite3_int64)artifact->producer_task_id);
} else {
(void)sqlite3_bind_null(statement, 4);
}
(void)sqlite3_bind_int64(statement, 5, artifact->created_at);
(void)sqlite3_bind_int64(statement, 6, artifact->updated_at);
result = step_done(database, statement, "create artifact");
}
(void)pthread_mutex_unlock(&database->mutex);
return result;
}
Lardon3DProjectDbResult lardon3d_project_db_mark_artifact_ready(Lardon3DProjectDb *database,
const char *artifact_id,
int64_t updated_at) {
if (!database || !bounded_text(artifact_id, LARDON3D_PROJECT_DB_ID_CAPACITY, false) ||
updated_at < 0) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
char path[LARDON3D_PROJECT_DB_PATH_CAPACITY];
Lardon3DProjectDbArtifact artifact;
Lardon3DProjectDbResult result =
lardon3d_project_db_load_artifact(database, artifact_id, &artifact);
if (result != LARDON3D_PROJECT_DB_OK) {
return result;
}
(void)snprintf(path, sizeof(path), "%s", artifact.path);
struct stat information;
if (stat(path, &information) != 0 || !S_ISREG(information.st_mode) || information.st_size < 0) {
return LARDON3D_PROJECT_DB_IO_ERROR;
}
(void)pthread_mutex_lock(&database->mutex);
sqlite3_stmt *statement = NULL;
result = prepare(
database,
"UPDATE artifacts SET state=1,size_bytes=?1,updated_at=?2 WHERE artifact_id=?3 AND path=?4",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, information.st_size);
(void)sqlite3_bind_int64(statement, 2, updated_at);
(void)sqlite3_bind_text(statement, 3, artifact_id, -1, SQLITE_TRANSIENT);
(void)sqlite3_bind_text(statement, 4, path, -1, SQLITE_TRANSIENT);
result = step_done(database, statement, "mark artifact ready");
if (result == LARDON3D_PROJECT_DB_OK && sqlite3_changes(database->connection) != 1) {
result = LARDON3D_PROJECT_DB_NOT_FOUND;
}
}
(void)pthread_mutex_unlock(&database->mutex);
return result;
}
Lardon3DProjectDbResult lardon3d_project_db_load_artifact(Lardon3DProjectDb *database,
const char *artifact_id,
Lardon3DProjectDbArtifact *artifact) {
if (!database || !artifact ||
!bounded_text(artifact_id, LARDON3D_PROJECT_DB_ID_CAPACITY, false)) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
(void)pthread_mutex_lock(&database->mutex);
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result = prepare(
database,
"SELECT artifact_id,kind,path,state,size_bytes,producer_task_id,created_at,updated_at FROM "
"artifacts WHERE artifact_id=?1",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_text(statement, 1, artifact_id, -1, SQLITE_TRANSIENT);
int code = sqlite3_step(statement);
if (code == SQLITE_DONE) {
result = LARDON3D_PROJECT_DB_NOT_FOUND;
} else if (code != SQLITE_ROW ||
!copy_column(statement, 0, artifact->artifact_id, sizeof(artifact->artifact_id)) ||
!copy_column(statement, 1, artifact->kind, sizeof(artifact->kind)) ||
!copy_column(statement, 2, artifact->path, sizeof(artifact->path))) {
result = LARDON3D_PROJECT_DB_CORRUPT;
} else {
sqlite3_int64 size = sqlite3_column_int64(statement, 4);
sqlite3_int64 producer = sqlite3_column_int64(statement, 5);
artifact->state = (Lardon3DProjectDbArtifactState)sqlite3_column_int(statement, 3);
artifact->size_bytes = size >= 0 ? (uint64_t)size : 0;
artifact->has_producer_task = sqlite3_column_type(statement, 5) != SQLITE_NULL;
artifact->producer_task_id =
artifact->has_producer_task && producer > 0 ? (uint64_t)producer : 0;
artifact->created_at = sqlite3_column_int64(statement, 6);
artifact->updated_at = sqlite3_column_int64(statement, 7);
}
(void)sqlite3_finalize(statement);
}
if (result == LARDON3D_PROJECT_DB_OK &&
(artifact->state < LARDON3D_DB_ARTIFACT_STAGED ||
artifact->state > LARDON3D_DB_ARTIFACT_READY ||
(artifact->has_producer_task && artifact->producer_task_id == 0))) {
result = LARDON3D_PROJECT_DB_CORRUPT;
}
(void)pthread_mutex_unlock(&database->mutex);
return result;
}
static bool canonical_feature_path(const unsigned char hash[32], const char *path) {
static const char digits[] = "0123456789abcdef";
char hex[65];
for (size_t i = 0; i < 32; ++i) {
hex[2 * i] = digits[hash[i] >> 4];
hex[2 * i + 1] = digits[hash[i] & 15];
}
hex[64] = '\0';
char expected[LARDON3D_PROJECT_DB_PATH_CAPACITY];
int n = snprintf(expected, sizeof(expected), "assets/features/%c%c/%s", hex[0], hex[1], hex);
return n > 0 && (size_t)n < sizeof(expected) && strcmp(expected, path) == 0;
}
static bool read_feature_set(sqlite3_stmt *s, Lardon3DProjectDbFeatureSet *set) {
memset(set, 0, sizeof(*set));
sqlite3_int64 id = sqlite3_column_int64(s, 0), image = sqlite3_column_int64(s, 1),
asset = sqlite3_column_int64(s, 2);
sqlite3_int64 version = sqlite3_column_int64(s, 4), count = sqlite3_column_int64(s, 7),
type = sqlite3_column_int64(s, 8), dim = sqlite3_column_int64(s, 9);
const void *fp = sqlite3_column_blob(s, 5), *source = sqlite3_column_blob(s, 6),
*hash = sqlite3_column_blob(s, 12);
sqlite3_int64 size = sqlite3_column_int64(s, 14), durability = sqlite3_column_int64(s, 15),
producer = sqlite3_column_int64(s, 10);
if (id <= 0 || image <= 0 || asset <= 0 || version <= 0 || version > UINT32_MAX || count < 0 ||
count > 8192 || type < 1 || type > 2 || dim <= 0 || dim > 4096 ||
sqlite3_column_bytes(s, 5) != 32 || sqlite3_column_bytes(s, 6) != 32 ||
sqlite3_column_bytes(s, 12) != 32 || !fp || !source || !hash || size < 0 || durability < 0 ||
durability > 1 || !copy_column(s, 3, set->extractor_kind, sizeof(set->extractor_kind)) ||
!lardon3d_task_kind_is_valid(set->extractor_kind) ||
!copy_column(s, 13, set->asset.path, sizeof(set->asset.path))) {
return false;
}
set->feature_set_id = (uint64_t)id;
set->image_id = (uint64_t)image;
set->feature_asset_id = (uint64_t)asset;
set->extractor_version = (uint32_t)version;
memcpy(set->parameter_fingerprint, fp, 32);
memcpy(set->source_image_sha256, source, 32);
set->feature_count = (uint32_t)count;
set->descriptor_type = (uint32_t)type;
set->descriptor_dimension = (uint32_t)dim;
set->has_producer_task = sqlite3_column_type(s, 10) != SQLITE_NULL;
if (set->has_producer_task && producer <= 0) {
return false;
}
set->producer_task_id = set->has_producer_task ? (uint64_t)producer : 0;
set->created_at = sqlite3_column_int64(s, 11);
set->asset.feature_asset_id = (uint64_t)asset;
memcpy(set->asset.sha256, hash, 32);
set->asset.size_bytes = (uint64_t)size;
set->asset.durability = (Lardon3DProjectDbFeatureDurability)durability;
set->asset.created_at = sqlite3_column_int64(s, 16);
sqlite3_int64 occupied = sqlite3_column_int64(s, 17);
sqlite3_int64 total_cells = sqlite3_column_int64(s, 18);
set->coverage_ratio = sqlite3_column_double(s, 19);
set->feature_density_per_megapixel = sqlite3_column_double(s, 20);
if (occupied < 0 || occupied > 1024 || total_cells < occupied || total_cells > 1024 ||
!isfinite(set->coverage_ratio) || set->coverage_ratio < 0.0 ||
set->coverage_ratio > 1.0 || !isfinite(set->feature_density_per_megapixel) ||
set->feature_density_per_megapixel < 0.0) {
return false;
}
set->occupied_cells = (uint32_t)occupied;
set->total_cells = (uint32_t)total_cells;
return set->created_at >= 0 && set->asset.created_at >= 0 &&
canonical_feature_path(set->asset.sha256, set->asset.path);
}
static const char feature_select[] =
"SELECT "
"f.feature_set_id,f.image_id,f.feature_asset_id,f.extractor_kind,f.extractor_version,f."
"parameter_fingerprint,f.source_image_sha256,f.feature_count,f.descriptor_type,f.descriptor_"
"dimension,f.producer_task_id,f.created_at,a.sha256,a.path,a.size_bytes,a.durability,a.created_"
"at,f.occupied_cells,f.total_cells,f.coverage_ratio,f.feature_density_per_megapixel FROM "
"feature_sets f JOIN feature_assets a ON a.feature_asset_id=f.feature_asset_id ";
static Lardon3DProjectDbResult verify_feature_source_locked(Lardon3DProjectDb *db,
uint64_t image_id,
const unsigned char source[32]) {
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result = prepare(db,
"SELECT a.sha256 FROM images i JOIN image_assets a ON "
"a.asset_id=i.asset_id WHERE i.image_id=?1",
&statement);
if (result != LARDON3D_PROJECT_DB_OK) {
return result;
}
sqlite3_bind_int64(statement, 1, (sqlite3_int64)image_id);
int code = sqlite3_step(statement);
if (code != SQLITE_ROW || sqlite3_column_bytes(statement, 0) != 32 ||
memcmp(sqlite3_column_blob(statement, 0), source, 32) != 0) {
result = code == SQLITE_DONE ? LARDON3D_PROJECT_DB_NOT_FOUND : LARDON3D_PROJECT_DB_CONSTRAINT;
}
sqlite3_finalize(statement);
return result;
}
static Lardon3DProjectDbResult find_or_insert_feature_asset_locked(
Lardon3DProjectDb *db, const unsigned char hash[32], const char *path, uint64_t size,
Lardon3DProjectDbFeatureDurability durability, int64_t created, sqlite3_int64 *asset_id) {
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result =
prepare(db,
"INSERT INTO feature_assets(sha256,path,size_bytes,durability,created_at) "
"VALUES(?1,?2,?3,?4,?5) ON CONFLICT(sha256) DO NOTHING",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_blob(statement, 1, hash, 32, SQLITE_TRANSIENT);
sqlite3_bind_text(statement, 2, path, -1, SQLITE_TRANSIENT);
sqlite3_bind_int64(statement, 3, (sqlite3_int64)size);
sqlite3_bind_int(statement, 4, (int)durability);
sqlite3_bind_int64(statement, 5, created);
result = step_done(db, statement, "insert feature asset");
statement = NULL;
}
if (result == LARDON3D_PROJECT_DB_OK) {
result = prepare(db,
"SELECT feature_asset_id,path,size_bytes,durability "
"FROM feature_assets WHERE sha256=?1",
&statement);
}
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_blob(statement, 1, hash, 32, SQLITE_TRANSIENT);
int code = sqlite3_step(statement);
char stored[4096];
sqlite3_int64 stored_durability = sqlite3_column_int64(statement, 3);
if (code != SQLITE_ROW || !copy_column(statement, 1, stored, sizeof(stored)) ||
strcmp(stored, path) != 0 || sqlite3_column_int64(statement, 2) != (sqlite3_int64)size ||
stored_durability < 0 || stored_durability > 1) {
result = LARDON3D_PROJECT_DB_CORRUPT;
} else {
*asset_id = sqlite3_column_int64(statement, 0);
}
sqlite3_finalize(statement);
statement = NULL;
}
if (result == LARDON3D_PROJECT_DB_OK && durability == LARDON3D_DB_FEATURE_ASSET_DURABLE) {
result = prepare(db,
"UPDATE feature_assets SET durability=?1 "
"WHERE feature_asset_id=?2 AND durability=?3",
&statement);
}
if (result == LARDON3D_PROJECT_DB_OK && durability == LARDON3D_DB_FEATURE_ASSET_DURABLE) {
sqlite3_bind_int(statement, 1, LARDON3D_DB_FEATURE_ASSET_DURABLE);
sqlite3_bind_int64(statement, 2, *asset_id);
sqlite3_bind_int(statement, 3, LARDON3D_DB_FEATURE_ASSET_PUBLISHED_NOT_DURABLE);
result = step_done(db, statement, "promote feature asset durability");
}
return result;
}
static Lardon3DProjectDbResult
insert_feature_set_locked(Lardon3DProjectDb *db, uint64_t image_id, sqlite3_int64 asset_id,
const char *kind, uint32_t version, const unsigned char fp[32],
const unsigned char source[32], uint32_t count, uint32_t type,
uint32_t dim, uint32_t occupied, uint32_t total_cells, double coverage,
double density, uint64_t producer, int64_t created) {
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result =
prepare(db,
"INSERT INTO feature_sets(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,occupied_cells,total_cells,"
"coverage_ratio,feature_density_per_megapixel) "
"VALUES(?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14,?15) ON CONFLICT("
"image_id,extractor_kind,"
"extractor_version,parameter_fingerprint) DO NOTHING",
&statement);
if (result != LARDON3D_PROJECT_DB_OK) {
return result;
}
sqlite3_bind_int64(statement, 1, (sqlite3_int64)image_id);
sqlite3_bind_int64(statement, 2, asset_id);
sqlite3_bind_text(statement, 3, kind, -1, SQLITE_TRANSIENT);
sqlite3_bind_int64(statement, 4, version);
sqlite3_bind_blob(statement, 5, fp, 32, SQLITE_TRANSIENT);
sqlite3_bind_blob(statement, 6, source, 32, SQLITE_TRANSIENT);
sqlite3_bind_int64(statement, 7, count);
sqlite3_bind_int64(statement, 8, type);
sqlite3_bind_int64(statement, 9, dim);
if (producer) {
sqlite3_bind_int64(statement, 10, (sqlite3_int64)producer);
} else {
sqlite3_bind_null(statement, 10);
}
sqlite3_bind_int64(statement, 11, created);
sqlite3_bind_int64(statement, 12, occupied);
sqlite3_bind_int64(statement, 13, total_cells);
sqlite3_bind_double(statement, 14, coverage);
sqlite3_bind_double(statement, 15, density);
return step_done(db, statement, "insert feature set");
}
Lardon3DProjectDbResult lardon3d_project_db_register_feature_set_quality(
Lardon3DProjectDb *db, uint64_t image_id, const char *kind, uint32_t version,
const unsigned char fp[32], const unsigned char source[32], uint32_t count, uint32_t type,
uint32_t dim, uint32_t occupied, uint32_t total_cells, double coverage, double density,
const unsigned char hash[32], const char *path, uint64_t size,
Lardon3DProjectDbFeatureDurability durability, uint64_t producer, int64_t created,
Lardon3DProjectDbFeatureSet *out) {
if (!db || !valid_catalog_id(image_id) || !kind || !lardon3d_task_kind_is_valid(kind) ||
version == 0 || !fp || !source || count > 8192 || type < 1 || type > 2 || dim == 0 ||
dim > 4096 || occupied > total_cells || total_cells > 1024 || !isfinite(coverage) ||
coverage < 0.0 || coverage > 1.0 || !isfinite(density) || density < 0.0 || !hash ||
!canonical_feature_path(hash, path) ||
size > INT64_MAX ||
durability < 0 || durability > 1 || (producer && !valid_task_id(producer)) || created < 0 ||
!out) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
#ifdef LARDON3D_PROJECT_DB_TESTING
const char *failure = getenv("LARDON3D_TEST_PROJECT_DB_FAIL_FEATURE_REGISTER");
if (failure && strcmp(failure, "1") == 0) {
return LARDON3D_PROJECT_DB_BUSY;
}
#endif
(void)pthread_mutex_lock(&db->mutex);
Lardon3DProjectDbResult result = execute(db, "BEGIN IMMEDIATE", "begin feature register");
if (result == LARDON3D_PROJECT_DB_OK) {
result = verify_feature_source_locked(db, image_id, source);
}
sqlite3_int64 asset_id = 0;
if (result == LARDON3D_PROJECT_DB_OK) {
result =
find_or_insert_feature_asset_locked(db, hash, path, size, durability, created, &asset_id);
}
if (result == LARDON3D_PROJECT_DB_OK) {
result = insert_feature_set_locked(db, image_id, asset_id, kind, version, fp, source, count,
type, dim, occupied, total_cells, coverage, density, producer,
created);
}
if (result == LARDON3D_PROJECT_DB_OK) {
result = execute(db, "COMMIT", "commit feature register");
}
if (result != LARDON3D_PROJECT_DB_OK) {
(void)execute(db, "ROLLBACK", "rollback feature register");
}
(void)pthread_mutex_unlock(&db->mutex);
return result == LARDON3D_PROJECT_DB_OK
? lardon3d_project_db_find_feature_set(db, image_id, kind, version, fp, out)
: result;
}
static bool read_support_set(sqlite3_stmt *s, Lardon3DProjectDbFeatureSupportSet *out) {
sqlite3_int64 id = sqlite3_column_int64(s, 0);
sqlite3_int64 image = sqlite3_column_int64(s, 1);
sqlite3_int64 first = sqlite3_column_int64(s, 2);
sqlite3_int64 second = sqlite3_column_int64(s, 3);
const void *fp = sqlite3_column_blob(s, 5);
sqlite3_int64 created = sqlite3_column_int64(s, 6);
sqlite3_int64 groups = sqlite3_column_int64(s, 7);
double radius = sqlite3_column_double(s, 4);
if (id <= 0 || image <= 0 || first <= 0 || second <= 0 || first == second || !fp ||
sqlite3_column_bytes(s, 5) != 32 || !isfinite(radius) || radius <= 0.0 || radius > 64.0 ||
created < 0 || groups < 0 || groups > 16384) {
return false;
}
memset(out, 0, sizeof(*out));
out->feature_support_set_id = (uint64_t)id;
out->image_id = (uint64_t)image;
out->first_feature_set_id = (uint64_t)first;
out->second_feature_set_id = (uint64_t)second;
out->radius_pixels = radius;
memcpy(out->parameter_fingerprint, fp, 32);
out->created_at = created;
out->group_count = (uint32_t)groups;
return true;
}
static Lardon3DProjectDbResult validate_support_sources_locked(
Lardon3DProjectDb *db, const Lardon3DProjectDbFeatureSupportSet *config,
const Lardon3DProjectDbFeatureSupportGroup *groups, size_t count) {
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result = prepare(
db, "SELECT image_id,feature_count,extractor_kind FROM feature_sets WHERE "
"feature_set_id IN(?1,?2) ORDER BY feature_set_id=?2",
&statement);
if (result != LARDON3D_PROJECT_DB_OK) return result;
sqlite3_bind_int64(statement, 1, (sqlite3_int64)config->first_feature_set_id);
sqlite3_bind_int64(statement, 2, (sqlite3_int64)config->second_feature_set_id);
uint32_t limits[2] = {0};
for (unsigned source = 0; source < 2; ++source) {
int step = sqlite3_step(statement);
const unsigned char *kind = step == SQLITE_ROW ? sqlite3_column_text(statement, 2) : NULL;
sqlite3_int64 image_id = step == SQLITE_ROW ? sqlite3_column_int64(statement, 0) : 0;
sqlite3_int64 feature_count = step == SQLITE_ROW ? sqlite3_column_int64(statement, 1) : -1;
bool expected_kind = source == 0 ? kind && strcmp((const char *)kind, "orb") == 0
: kind && (strcmp((const char *)kind, "sift") == 0 ||
strcmp((const char *)kind, "rootsift") == 0);
if (step != SQLITE_ROW || image_id != (sqlite3_int64)config->image_id || feature_count < 0 ||
feature_count > 8192 || !expected_kind) {
sqlite3_finalize(statement);
return LARDON3D_PROJECT_DB_CONSTRAINT;
}
limits[source] = (uint32_t)feature_count;
}
sqlite3_finalize(statement);
for (size_t i = 0; i < count; ++i) {
unsigned first_source = groups[i].first_member_from_second_set ? 1U : 0U;
if (groups[i].first_feature_index >= limits[first_source] ||
(groups[i].has_second_feature && groups[i].second_feature_index >= limits[1])) {
return LARDON3D_PROJECT_DB_CONSTRAINT;
}
}
return LARDON3D_PROJECT_DB_OK;
}
Lardon3DProjectDbResult lardon3d_project_db_load_feature_support(
Lardon3DProjectDb *db, uint64_t image_id, uint64_t first_id, uint64_t second_id,
const unsigned char fp[32], Lardon3DProjectDbFeatureSupportSet *out) {
if (!db || !valid_catalog_id(image_id) || !valid_catalog_id(first_id) ||
!valid_catalog_id(second_id) || first_id == second_id || !fp || !out) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
(void)pthread_mutex_lock(&db->mutex);
sqlite3_stmt *s = NULL;
Lardon3DProjectDbResult result = prepare(
db, "SELECT s.feature_support_set_id,s.image_id,s.first_feature_set_id,"
"s.second_feature_set_id,s.radius_pixels,s.parameter_fingerprint,s.created_at,"
"COUNT(g.feature_support_group_id) FROM feature_support_sets s LEFT JOIN "
"feature_support_groups g ON g.feature_support_set_id=s.feature_support_set_id WHERE "
"s.image_id=?1 AND s.first_feature_set_id=?2 AND s.second_feature_set_id=?3 AND "
"s.parameter_fingerprint=?4 GROUP BY s.feature_support_set_id",
&s);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(s, 1, (sqlite3_int64)image_id);
sqlite3_bind_int64(s, 2, (sqlite3_int64)first_id);
sqlite3_bind_int64(s, 3, (sqlite3_int64)second_id);
sqlite3_bind_blob(s, 4, fp, 32, SQLITE_TRANSIENT);
int step = sqlite3_step(s);
if (step == SQLITE_ROW) result = read_support_set(s, out) ? LARDON3D_PROJECT_DB_OK
: LARDON3D_PROJECT_DB_CORRUPT;
else result = step == SQLITE_DONE ? LARDON3D_PROJECT_DB_NOT_FOUND
: sqlite_result(db, step, "load feature support");
sqlite3_finalize(s);
}
(void)pthread_mutex_unlock(&db->mutex);
return result;
}
Lardon3DProjectDbResult lardon3d_project_db_list_feature_support_groups(
Lardon3DProjectDb *db, uint64_t support_set_id, uint64_t after_group_id,
Lardon3DProjectDbFeatureSupportGroup *groups, size_t capacity, size_t *count) {
if (count) *count = 0;
if (!db || !valid_catalog_id(support_set_id) || after_group_id > INT64_MAX || !groups ||
!count || capacity == 0 || capacity > 256) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
(void)pthread_mutex_lock(&db->mutex);
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result = prepare(
db, "SELECT g.feature_support_group_id,g.feature_support_set_id,g.x,g.y,g.distance_pixels,"
"g.support_count,first_member.feature_index,second_member.feature_index FROM "
"feature_support_groups g "
"JOIN feature_support_sets s ON s.feature_support_set_id=g.feature_support_set_id "
"LEFT JOIN feature_support_members first_member ON first_member."
"feature_support_group_id=g.feature_support_group_id AND first_member.feature_set_id="
"s.first_feature_set_id LEFT JOIN feature_support_members second_member ON "
"second_member.feature_support_group_id=g.feature_support_group_id AND second_member."
"feature_set_id=s.second_feature_set_id WHERE g.feature_support_set_id=?1 AND "
"g.feature_support_group_id>?2 ORDER BY g.feature_support_group_id LIMIT ?3",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(statement, 1, (sqlite3_int64)support_set_id);
sqlite3_bind_int64(statement, 2, (sqlite3_int64)after_group_id);
sqlite3_bind_int64(statement, 3, (sqlite3_int64)capacity);
while (*count < capacity) {
int step = sqlite3_step(statement);
if (step == SQLITE_DONE) break;
bool has_first = sqlite3_column_type(statement, 6) != SQLITE_NULL;
bool has_second = sqlite3_column_type(statement, 7) != SQLITE_NULL;
sqlite3_int64 id = sqlite3_column_int64(statement, 0);
sqlite3_int64 set_id = sqlite3_column_int64(statement, 1);
sqlite3_int64 support_count = sqlite3_column_int64(statement, 5);
sqlite3_int64 first_index = has_first ? sqlite3_column_int64(statement, 6) : 0;
sqlite3_int64 second_index = has_second ? sqlite3_column_int64(statement, 7) : 0;
double x = sqlite3_column_double(statement, 2), y = sqlite3_column_double(statement, 3);
double distance = sqlite3_column_double(statement, 4);
if (step != SQLITE_ROW || id <= 0 || set_id != (sqlite3_int64)support_set_id ||
!isfinite(x) || !isfinite(y) || !isfinite(distance) || distance < 0.0 ||
distance > 64.0 || support_count < 1 || support_count > 2 ||
has_first + has_second != support_count || first_index < 0 || first_index >= 8192 ||
second_index < 0 || second_index >= 8192) {
result = LARDON3D_PROJECT_DB_CORRUPT;
break;
}
Lardon3DProjectDbFeatureSupportGroup *group = &groups[(*count)++];
memset(group, 0, sizeof(*group));
group->feature_support_group_id = (uint64_t)id;
group->feature_support_set_id = support_set_id;
group->x = x;
group->y = y;
group->distance_pixels = distance;
group->support_count = (uint32_t)support_count;
group->first_member_from_second_set = !has_first;
group->first_feature_index = (uint32_t)(has_first ? first_index : second_index);
group->has_second_feature = has_first && has_second;
group->second_feature_index = (uint32_t)second_index;
}
sqlite3_finalize(statement);
}
(void)pthread_mutex_unlock(&db->mutex);
return result;
}
Lardon3DProjectDbResult lardon3d_project_db_publish_feature_support(
Lardon3DProjectDb *db, const Lardon3DProjectDbFeatureSupportSet *config,
const Lardon3DProjectDbFeatureSupportGroup *groups, size_t count,
Lardon3DProjectDbFeatureSupportSet *out) {
if (!db || !config || !out || !valid_catalog_id(config->image_id) ||
!valid_catalog_id(config->first_feature_set_id) ||
!valid_catalog_id(config->second_feature_set_id) ||
config->first_feature_set_id == config->second_feature_set_id ||
!isfinite(config->radius_pixels) || config->radius_pixels <= 0.0 ||
config->radius_pixels > 64.0 || count > 16384 || (count && !groups) ||
config->created_at < 0) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
for (size_t i = 0; i < count; ++i) {
if (!isfinite(groups[i].x) || !isfinite(groups[i].y) ||
!isfinite(groups[i].distance_pixels) || groups[i].distance_pixels < 0.0 ||
groups[i].distance_pixels > config->radius_pixels || groups[i].support_count < 1 ||
groups[i].support_count > 2 || groups[i].first_feature_index >= 8192 ||
(groups[i].support_count == 2 && groups[i].first_member_from_second_set) ||
groups[i].has_second_feature != (groups[i].support_count == 2) ||
(groups[i].has_second_feature && groups[i].second_feature_index >= 8192)) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
}
Lardon3DProjectDbFeatureSupportSet existing;
Lardon3DProjectDbResult found = lardon3d_project_db_load_feature_support(
db, config->image_id, config->first_feature_set_id, config->second_feature_set_id,
config->parameter_fingerprint, &existing);
if (found == LARDON3D_PROJECT_DB_OK) {
if (existing.radius_pixels != config->radius_pixels)
return LARDON3D_PROJECT_DB_CONSTRAINT;
*out = existing;
return LARDON3D_PROJECT_DB_OK;
}
if (found != LARDON3D_PROJECT_DB_NOT_FOUND) return found;
(void)pthread_mutex_lock(&db->mutex);
Lardon3DProjectDbResult result = execute(db, "BEGIN IMMEDIATE", "begin feature support");
sqlite3_stmt *s = NULL;
bool inserted_support_set = false;
if (result == LARDON3D_PROJECT_DB_OK)
result = validate_support_sources_locked(db, config, groups, count);
if (result == LARDON3D_PROJECT_DB_OK) {
result = prepare(db, "INSERT OR IGNORE INTO feature_support_sets(image_id,first_feature_set_id,"
"second_feature_set_id,radius_pixels,parameter_fingerprint,created_at) "
"SELECT ?1,?2,?3,?4,?5,?6 WHERE EXISTS(SELECT 1 FROM feature_sets a,"
"feature_sets b WHERE a.feature_set_id=?2 AND b.feature_set_id=?3 AND "
"a.image_id=?1 AND b.image_id=?1)", &s);
}
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(s, 1, (sqlite3_int64)config->image_id);
sqlite3_bind_int64(s, 2, (sqlite3_int64)config->first_feature_set_id);
sqlite3_bind_int64(s, 3, (sqlite3_int64)config->second_feature_set_id);
sqlite3_bind_double(s, 4, config->radius_pixels);
sqlite3_bind_blob(s, 5, config->parameter_fingerprint, 32, SQLITE_TRANSIENT);
sqlite3_bind_int64(s, 6, config->created_at);
result = step_done(db, s, "insert feature support set");
inserted_support_set = result == LARDON3D_PROJECT_DB_OK &&
sqlite3_changes(db->connection) == 1;
}
sqlite3_int64 support_id = 0;
if (result == LARDON3D_PROJECT_DB_OK) {
result = prepare(db, "SELECT feature_support_set_id FROM feature_support_sets WHERE image_id=?1 "
"AND first_feature_set_id=?2 AND second_feature_set_id=?3 AND "
"parameter_fingerprint=?4", &s);
}
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(s, 1, (sqlite3_int64)config->image_id);
sqlite3_bind_int64(s, 2, (sqlite3_int64)config->first_feature_set_id);
sqlite3_bind_int64(s, 3, (sqlite3_int64)config->second_feature_set_id);
sqlite3_bind_blob(s, 4, config->parameter_fingerprint, 32, SQLITE_TRANSIENT);
int step = sqlite3_step(s);
if (step == SQLITE_ROW) support_id = sqlite3_column_int64(s, 0);
else result = step == SQLITE_DONE ? LARDON3D_PROJECT_DB_NOT_FOUND
: sqlite_result(db, step, "select feature support");
sqlite3_finalize(s);
}
for (size_t i = 0; inserted_support_set && result == LARDON3D_PROJECT_DB_OK && i < count; ++i) {
result = prepare(db, "INSERT INTO feature_support_groups(feature_support_set_id,x,y,"
"distance_pixels,support_count) VALUES(?1,?2,?3,?4,?5)", &s);
if (result != LARDON3D_PROJECT_DB_OK) break;
sqlite3_bind_int64(s, 1, support_id);
sqlite3_bind_double(s, 2, groups[i].x);
sqlite3_bind_double(s, 3, groups[i].y);
sqlite3_bind_double(s, 4, groups[i].distance_pixels);
sqlite3_bind_int64(s, 5, groups[i].support_count);
result = step_done(db, s, "insert feature support group");
sqlite3_int64 group_id = sqlite3_last_insert_rowid(db->connection);
for (unsigned member = 0; result == LARDON3D_PROJECT_DB_OK && member < groups[i].support_count;
++member) {
result = prepare(db, "INSERT INTO feature_support_members(feature_support_group_id,"
"feature_set_id,feature_index,extractor_kind) SELECT ?1,?2,?3,"
"extractor_kind FROM feature_sets WHERE feature_set_id=?2", &s);
if (result != LARDON3D_PROJECT_DB_OK) break;
sqlite3_bind_int64(s, 1, group_id);
bool from_second = member != 0 || groups[i].first_member_from_second_set;
sqlite3_bind_int64(s, 2, (sqlite3_int64)(from_second ? config->second_feature_set_id
: config->first_feature_set_id));
sqlite3_bind_int64(s, 3, member ? groups[i].second_feature_index
: groups[i].first_feature_index);
result = step_done(db, s, "insert feature support member");
}
}
if (result == LARDON3D_PROJECT_DB_OK) result = execute(db, "COMMIT", "commit feature support");
if (result != LARDON3D_PROJECT_DB_OK) (void)execute(db, "ROLLBACK", "rollback feature support");
(void)pthread_mutex_unlock(&db->mutex);
return result == LARDON3D_PROJECT_DB_OK
? lardon3d_project_db_load_feature_support(
db, config->image_id, config->first_feature_set_id,
config->second_feature_set_id, config->parameter_fingerprint, out)
: result;
}
Lardon3DProjectDbResult lardon3d_project_db_register_feature_set(
Lardon3DProjectDb *db, uint64_t image_id, const char *kind, uint32_t version,
const unsigned char fp[32], const unsigned char source[32], uint32_t count, uint32_t type,
uint32_t dim, const unsigned char hash[32], const char *path, uint64_t size,
Lardon3DProjectDbFeatureDurability durability, uint64_t producer, int64_t created,
Lardon3DProjectDbFeatureSet *out) {
return lardon3d_project_db_register_feature_set_quality(
db, image_id, kind, version, fp, source, count, type, dim, 0, 0, 0.0, 0.0, hash, path, size,
durability, producer, created, out);
}
Lardon3DProjectDbResult lardon3d_project_db_find_feature_set(Lardon3DProjectDb *db,
uint64_t image_id, const char *kind,
uint32_t version,
const unsigned char fp[32],
Lardon3DProjectDbFeatureSet *out) {
if (!db || !valid_catalog_id(image_id) || !lardon3d_task_kind_is_valid(kind) || version == 0 ||
!fp || !out) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
(void)pthread_mutex_lock(&db->mutex);
char sql[1024];
snprintf(sql, sizeof(sql),
"%s WHERE f.image_id=?1 AND f.extractor_kind=?2 AND f.extractor_version=?3 AND "
"f.parameter_fingerprint=?4",
feature_select);
sqlite3_stmt *s = NULL;
Lardon3DProjectDbResult result = prepare(db, sql, &s);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(s, 1, (sqlite3_int64)image_id);
sqlite3_bind_text(s, 2, kind, -1, SQLITE_TRANSIENT);
sqlite3_bind_int64(s, 3, version);
sqlite3_bind_blob(s, 4, fp, 32, SQLITE_TRANSIENT);
int code = sqlite3_step(s);
if (code == SQLITE_DONE) {
result = LARDON3D_PROJECT_DB_NOT_FOUND;
} else if (code != SQLITE_ROW || !read_feature_set(s, out)) {
result = LARDON3D_PROJECT_DB_CORRUPT;
}
sqlite3_finalize(s);
}
(void)pthread_mutex_unlock(&db->mutex);
return result;
}
Lardon3DProjectDbResult lardon3d_project_db_load_feature_set(Lardon3DProjectDb *db, uint64_t id,
Lardon3DProjectDbFeatureSet *out) {
if (!db || !valid_catalog_id(id) || !out) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
(void)pthread_mutex_lock(&db->mutex);
char sql[1024];
snprintf(sql, sizeof(sql), "%s WHERE f.feature_set_id=?1", feature_select);
sqlite3_stmt *s = NULL;
Lardon3DProjectDbResult result = prepare(db, sql, &s);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(s, 1, (sqlite3_int64)id);
int code = sqlite3_step(s);
if (code == SQLITE_DONE) {
result = LARDON3D_PROJECT_DB_NOT_FOUND;
} else if (code != SQLITE_ROW || !read_feature_set(s, out)) {
result = LARDON3D_PROJECT_DB_CORRUPT;
}
sqlite3_finalize(s);
}
(void)pthread_mutex_unlock(&db->mutex);
return result;
}
Lardon3DProjectDbResult lardon3d_project_db_list_feature_sets(Lardon3DProjectDb *db, uint64_t after,
Lardon3DProjectDbFeatureSet *sets,
size_t capacity, size_t *count) {
if (count) {
*count = 0;
}
if (!db || after > INT64_MAX || !sets || !count || capacity == 0 || capacity > 256) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
(void)pthread_mutex_lock(&db->mutex);
char sql[1024];
snprintf(sql, sizeof(sql), "%s WHERE f.feature_set_id>?1 ORDER BY f.feature_set_id LIMIT ?2",
feature_select);
sqlite3_stmt *s = NULL;
Lardon3DProjectDbResult result = prepare(db, sql, &s);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(s, 1, (sqlite3_int64)after);
sqlite3_bind_int64(s, 2, (sqlite3_int64)capacity);
int code = SQLITE_DONE;
while (*count < capacity && (code = sqlite3_step(s)) == SQLITE_ROW) {
if (!read_feature_set(s, &sets[*count])) {
result = LARDON3D_PROJECT_DB_CORRUPT;
break;
}
++*count;
}
if (result == LARDON3D_PROJECT_DB_OK && *count < capacity && code != SQLITE_DONE) {
result = sqlite_result(db, code, "list feature sets");
}
sqlite3_finalize(s);
}
(void)pthread_mutex_unlock(&db->mutex);
return result;
}
Lardon3DProjectDbResult
lardon3d_project_db_load_feature_extract_task(Lardon3DProjectDb *db, uint64_t task_id,
Lardon3DProjectDbFeatureExtractTask *p) {
if (!db || !valid_task_id(task_id) || !p) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
memset(p, 0, sizeof(*p));
(void)pthread_mutex_lock(&db->mutex);
sqlite3_stmt *s = NULL;
Lardon3DProjectDbResult result =
prepare(db,
"SELECT "
"image_id,extractor_kind,extractor_version,max_features,pyramid_levels,fast_"
"threshold,parameter_fingerprint FROM feature_extract_tasks WHERE task_id=?1",
&s);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(s, 1, (sqlite3_int64)task_id);
int code = sqlite3_step(s);
sqlite3_int64 image = sqlite3_column_int64(s, 0), version = sqlite3_column_int64(s, 2),
max = sqlite3_column_int64(s, 3), levels = sqlite3_column_int64(s, 4),
threshold = sqlite3_column_int64(s, 5);
const void *fp = sqlite3_column_blob(s, 6);
if (code == SQLITE_DONE) {
result = LARDON3D_PROJECT_DB_NOT_FOUND;
} else if (code != SQLITE_ROW || image <= 0 || version <= 0 || version > UINT32_MAX ||
max <= 0 || max > 8192 || levels <= 0 || levels > 16 || threshold <= 0 ||
threshold > 255 ||
!copy_column(s, 1, p->extractor_kind, sizeof(p->extractor_kind)) ||
!lardon3d_task_kind_is_valid(p->extractor_kind) ||
sqlite3_column_bytes(s, 6) != 32 || !fp) {
result = LARDON3D_PROJECT_DB_CORRUPT;
} else {
p->task_id = task_id;
p->image_id = (uint64_t)image;
p->extractor_version = (uint32_t)version;
p->max_features = (uint32_t)max;
p->pyramid_levels = (uint32_t)levels;
p->fast_threshold = (uint32_t)threshold;
memcpy(p->parameter_fingerprint, fp, 32);
}
sqlite3_finalize(s);
}
(void)pthread_mutex_unlock(&db->mutex);
return result;
}
Lardon3DProjectDbResult lardon3d_project_db_load_sift_extract_task(
Lardon3DProjectDb *db, uint64_t task_id, Lardon3DProjectDbSiftExtractTask *p) {
if (!db || !valid_task_id(task_id) || !p) return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
memset(p, 0, sizeof(*p));
pthread_mutex_lock(&db->mutex);
sqlite3_stmt *s = NULL;
Lardon3DProjectDbResult result = prepare(
db, "SELECT image_id,extractor_kind,extractor_version,max_features,octave_layers,"
"contrast_threshold,edge_threshold,sigma,grid_rows,grid_cols,max_features_per_cell,"
"parameter_fingerprint FROM sift_extract_tasks WHERE task_id=?1", &s);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(s, 1, (sqlite3_int64)task_id);
int code = sqlite3_step(s);
if (code == SQLITE_DONE) {
result = LARDON3D_PROJECT_DB_NOT_FOUND;
} else if (code != SQLITE_ROW || sqlite3_column_int64(s, 0) <= 0 ||
!copy_column(s, 1, p->extractor_kind, sizeof(p->extractor_kind)) ||
sqlite3_column_bytes(s, 11) != 32) {
result = LARDON3D_PROJECT_DB_CORRUPT;
} else {
p->task_id = task_id;
p->image_id = (uint64_t)sqlite3_column_int64(s, 0);
p->extractor_version = (uint32_t)sqlite3_column_int64(s, 2);
p->max_features = (uint32_t)sqlite3_column_int64(s, 3);
p->octave_layers = (uint32_t)sqlite3_column_int64(s, 4);
p->contrast_threshold = sqlite3_column_double(s, 5);
p->edge_threshold = sqlite3_column_double(s, 6);
p->sigma = sqlite3_column_double(s, 7);
p->grid_rows = (uint32_t)sqlite3_column_int64(s, 8);
p->grid_cols = (uint32_t)sqlite3_column_int64(s, 9);
p->max_features_per_cell = (uint32_t)sqlite3_column_int64(s, 10);
memcpy(p->parameter_fingerprint, sqlite3_column_blob(s, 11), 32);
}
sqlite3_finalize(s);
}
if (result == LARDON3D_PROJECT_DB_OK &&
((strcmp(p->extractor_kind, "sift") != 0 && strcmp(p->extractor_kind, "rootsift") != 0) ||
p->extractor_version != 1 || p->max_features < 1 || p->max_features > 8192 ||
p->octave_layers < 1 || p->octave_layers > 8 || !isfinite(p->contrast_threshold) ||
p->contrast_threshold < 0.001 || p->contrast_threshold > 0.2 ||
!isfinite(p->edge_threshold) || p->edge_threshold < 1.0 || p->edge_threshold > 100.0 ||
!isfinite(p->sigma) || p->sigma < 0.5 || p->sigma > 3.0 || p->grid_rows < 1 ||
p->grid_rows > 32 || p->grid_cols < 1 || p->grid_cols > 32 ||
p->max_features_per_cell < 1 || p->max_features_per_cell > 8192)) {
result = LARDON3D_PROJECT_DB_CORRUPT;
}
pthread_mutex_unlock(&db->mutex);
return result;
}
static bool valid_visual_index(const Lardon3DProjectDbVisualIndex *index) {
return index && lardon3d_task_kind_is_valid(index->index_kind) && index->index_version > 0 &&
index->descriptor_type >= 1 && index->descriptor_type <= 2 &&
index->descriptor_dimension > 0 && index->descriptor_dimension <= 4096 &&
lardon3d_task_kind_is_valid(index->extractor_kind) && index->extractor_version > 0 &&
index->table_count > 0 && index->table_count <= 32 && index->key_bits >= 8 &&
index->key_bits <= 32 && index->max_features_per_set > 0 &&
index->max_features_per_set <= 1024 && index->max_bucket_postings > 0 &&
index->max_bucket_postings <= 4096 && index->created_at >= 0;
}
static bool read_visual_index(sqlite3_stmt *statement, Lardon3DProjectDbVisualIndex *index) {
memset(index, 0, sizeof(*index));
sqlite3_int64 id = sqlite3_column_int64(statement, 0);
const void *feature_fp = sqlite3_column_blob(statement, 7);
const void *index_fp = sqlite3_column_blob(statement, 8);
if (id <= 0 || !copy_column(statement, 1, index->index_kind, sizeof(index->index_kind)) ||
!copy_column(statement, 5, index->extractor_kind, sizeof(index->extractor_kind)) ||
sqlite3_column_bytes(statement, 7) != 32 || sqlite3_column_bytes(statement, 8) != 32 ||
!feature_fp || !index_fp) {
return false;
}
index->visual_index_id = (uint64_t)id;
index->index_version = (uint32_t)sqlite3_column_int64(statement, 2);
index->descriptor_type = (uint32_t)sqlite3_column_int64(statement, 3);
index->descriptor_dimension = (uint32_t)sqlite3_column_int64(statement, 4);
index->extractor_version = (uint32_t)sqlite3_column_int64(statement, 6);
memcpy(index->feature_parameter_fingerprint, feature_fp, 32);
memcpy(index->index_parameter_fingerprint, index_fp, 32);
index->table_count = (uint32_t)sqlite3_column_int64(statement, 9);
index->key_bits = (uint32_t)sqlite3_column_int64(statement, 10);
index->max_features_per_set = (uint32_t)sqlite3_column_int64(statement, 11);
index->max_bucket_postings = (uint32_t)sqlite3_column_int64(statement, 12);
index->created_at = sqlite3_column_int64(statement, 13);
return valid_visual_index(index);
}
static const char visual_index_select[] =
"SELECT visual_index_id,index_kind,index_version,descriptor_type,descriptor_dimension,"
"extractor_kind,extractor_version,feature_parameter_fingerprint,"
"index_parameter_fingerprint,table_count,key_bits,max_features_per_set,"
"max_bucket_postings,created_at FROM visual_indexes";
Lardon3DProjectDbResult lardon3d_project_db_create_visual_index(
Lardon3DProjectDb *db, const Lardon3DProjectDbVisualIndex *configuration,
Lardon3DProjectDbVisualIndex *out) {
if (!db || !valid_visual_index(configuration) || !out) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
(void)pthread_mutex_lock(&db->mutex);
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result = prepare(
db,
"INSERT INTO visual_indexes(index_kind,index_version,descriptor_type,descriptor_dimension,"
"extractor_kind,extractor_version,feature_parameter_fingerprint,"
"index_parameter_fingerprint,table_count,key_bits,max_features_per_set,"
"max_bucket_postings,created_at) VALUES(?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13) "
"ON CONFLICT(index_kind,index_version,descriptor_type,descriptor_dimension,extractor_kind,"
"extractor_version,feature_parameter_fingerprint,index_parameter_fingerprint) DO NOTHING",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_text(statement, 1, configuration->index_kind, -1, SQLITE_TRANSIENT);
sqlite3_bind_int64(statement, 2, configuration->index_version);
sqlite3_bind_int64(statement, 3, configuration->descriptor_type);
sqlite3_bind_int64(statement, 4, configuration->descriptor_dimension);
sqlite3_bind_text(statement, 5, configuration->extractor_kind, -1, SQLITE_TRANSIENT);
sqlite3_bind_int64(statement, 6, configuration->extractor_version);
sqlite3_bind_blob(statement, 7, configuration->feature_parameter_fingerprint, 32,
SQLITE_TRANSIENT);
sqlite3_bind_blob(statement, 8, configuration->index_parameter_fingerprint, 32,
SQLITE_TRANSIENT);
sqlite3_bind_int64(statement, 9, configuration->table_count);
sqlite3_bind_int64(statement, 10, configuration->key_bits);
sqlite3_bind_int64(statement, 11, configuration->max_features_per_set);
sqlite3_bind_int64(statement, 12, configuration->max_bucket_postings);
sqlite3_bind_int64(statement, 13, configuration->created_at);
result = step_done(db, statement, "create visual index");
}
if (result == LARDON3D_PROJECT_DB_OK) {
char sql[1024];
snprintf(sql, sizeof(sql),
"%s WHERE index_kind=?1 AND index_version=?2 AND descriptor_type=?3 AND "
"descriptor_dimension=?4 AND extractor_kind=?5 AND extractor_version=?6 AND "
"feature_parameter_fingerprint=?7 AND index_parameter_fingerprint=?8",
visual_index_select);
result = prepare(db, sql, &statement);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_text(statement, 1, configuration->index_kind, -1, SQLITE_TRANSIENT);
sqlite3_bind_int64(statement, 2, configuration->index_version);
sqlite3_bind_int64(statement, 3, configuration->descriptor_type);
sqlite3_bind_int64(statement, 4, configuration->descriptor_dimension);
sqlite3_bind_text(statement, 5, configuration->extractor_kind, -1, SQLITE_TRANSIENT);
sqlite3_bind_int64(statement, 6, configuration->extractor_version);
sqlite3_bind_blob(statement, 7, configuration->feature_parameter_fingerprint, 32,
SQLITE_TRANSIENT);
sqlite3_bind_blob(statement, 8, configuration->index_parameter_fingerprint, 32,
SQLITE_TRANSIENT);
int code = sqlite3_step(statement);
if (code != SQLITE_ROW || !read_visual_index(statement, out)) {
result = LARDON3D_PROJECT_DB_CORRUPT;
}
sqlite3_finalize(statement);
}
}
(void)pthread_mutex_unlock(&db->mutex);
return result;
}
Lardon3DProjectDbResult lardon3d_project_db_load_visual_index(
Lardon3DProjectDb *db, uint64_t id, Lardon3DProjectDbVisualIndex *out) {
if (!db || !valid_catalog_id(id) || !out) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
(void)pthread_mutex_lock(&db->mutex);
char sql[768];
snprintf(sql, sizeof(sql), "%s WHERE visual_index_id=?1", visual_index_select);
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result = prepare(db, sql, &statement);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(statement, 1, (sqlite3_int64)id);
int code = sqlite3_step(statement);
result = code == SQLITE_DONE ? LARDON3D_PROJECT_DB_NOT_FOUND
: code == SQLITE_ROW && read_visual_index(statement, out)
? LARDON3D_PROJECT_DB_OK
: LARDON3D_PROJECT_DB_CORRUPT;
sqlite3_finalize(statement);
}
(void)pthread_mutex_unlock(&db->mutex);
return result;
}
static bool canonical_visual_path(const unsigned char hash[32], const char *path) {
static const char digits[] = "0123456789abcdef";
char hex[65];
for (size_t i = 0; i < 32; ++i) {
hex[i * 2] = digits[hash[i] >> 4];
hex[i * 2 + 1] = digits[hash[i] & 15];
}
hex[64] = '\0';
char expected[LARDON3D_PROJECT_DB_PATH_CAPACITY];
int written = snprintf(expected, sizeof(expected), "assets/visual-index/%c%c/%s", hex[0],
hex[1], hex);
return written > 0 && (size_t)written < sizeof(expected) && strcmp(expected, path) == 0;
}
static bool read_visual_segment(sqlite3_stmt *statement,
Lardon3DProjectDbVisualIndexSegment *segment) {
memset(segment, 0, sizeof(*segment));
sqlite3_int64 id = sqlite3_column_int64(statement, 0);
sqlite3_int64 index_id = sqlite3_column_int64(statement, 1);
sqlite3_int64 generation = sqlite3_column_int64(statement, 2);
const void *hash = sqlite3_column_blob(statement, 3);
sqlite3_int64 size = sqlite3_column_int64(statement, 5);
sqlite3_int64 postings = sqlite3_column_int64(statement, 6);
sqlite3_int64 members = sqlite3_column_int64(statement, 7);
sqlite3_int64 durability = sqlite3_column_int64(statement, 8);
if (id <= 0 || index_id <= 0 || generation <= 0 || !hash ||
sqlite3_column_bytes(statement, 3) != 32 || size < 128 || postings < 0 || members < 1 ||
members > 16 || durability < 0 || durability > 1 ||
!copy_column(statement, 4, segment->path, sizeof(segment->path))) {
return false;
}
segment->visual_index_segment_id = (uint64_t)id;
segment->visual_index_id = (uint64_t)index_id;
segment->generation = (uint64_t)generation;
memcpy(segment->sha256, hash, 32);
segment->size_bytes = (uint64_t)size;
segment->posting_count = (uint64_t)postings;
segment->feature_set_count = (uint32_t)members;
segment->durability = (Lardon3DProjectDbVisualIndexDurability)durability;
segment->has_producer_task = sqlite3_column_type(statement, 9) != SQLITE_NULL;
segment->producer_task_id = segment->has_producer_task
? (uint64_t)sqlite3_column_int64(statement, 9)
: 0;
segment->created_at = sqlite3_column_int64(statement, 10);
return segment->created_at >= 0 && canonical_visual_path(segment->sha256, segment->path);
}
static const char visual_segment_select[] =
"SELECT visual_index_segment_id,visual_index_id,generation,sha256,path,size_bytes,"
"posting_count,feature_set_count,durability,producer_task_id,created_at "
"FROM visual_index_segments";
Lardon3DProjectDbResult lardon3d_project_db_list_visual_index_segments(
Lardon3DProjectDb *db, uint64_t index_id, uint64_t after,
Lardon3DProjectDbVisualIndexSegment *segments, size_t capacity, size_t *count) {
if (count) {
*count = 0;
}
if (!db || !valid_catalog_id(index_id) || after > INT64_MAX || !segments || !count ||
capacity == 0 || capacity > 256) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
(void)pthread_mutex_lock(&db->mutex);
char sql[768];
snprintf(sql, sizeof(sql), "%s WHERE visual_index_id=?1 AND generation>?2 ORDER BY generation "
"LIMIT ?3", visual_segment_select);
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result = prepare(db, sql, &statement);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(statement, 1, (sqlite3_int64)index_id);
sqlite3_bind_int64(statement, 2, (sqlite3_int64)after);
sqlite3_bind_int64(statement, 3, (sqlite3_int64)capacity);
int code = SQLITE_DONE;
while (*count < capacity && (code = sqlite3_step(statement)) == SQLITE_ROW) {
if (!read_visual_segment(statement, &segments[*count])) {
result = LARDON3D_PROJECT_DB_CORRUPT;
break;
}
++*count;
}
if (result == LARDON3D_PROJECT_DB_OK && code != SQLITE_DONE && *count < capacity) {
result = sqlite_result(db, code, "list visual index segments");
}
sqlite3_finalize(statement);
}
(void)pthread_mutex_unlock(&db->mutex);
return result;
}
Lardon3DProjectDbResult lardon3d_project_db_list_visual_index_pending(
Lardon3DProjectDb *db, uint64_t index_id, uint64_t after,
Lardon3DProjectDbFeatureSet *sets, size_t capacity, size_t *count) {
if (count) {
*count = 0;
}
if (!db || !valid_catalog_id(index_id) || after > INT64_MAX || !sets || !count ||
capacity == 0 || capacity > 16) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
(void)pthread_mutex_lock(&db->mutex);
char sql[2048];
snprintf(sql, sizeof(sql),
"%s JOIN visual_indexes v ON v.visual_index_id=?1 LEFT JOIN "
"visual_index_memberships m ON m.visual_index_id=v.visual_index_id AND "
"m.feature_set_id=f.feature_set_id WHERE f.feature_set_id>?2 AND m.feature_set_id IS "
"NULL AND f.descriptor_type=v.descriptor_type AND f.descriptor_dimension="
"v.descriptor_dimension AND f.extractor_kind=v.extractor_kind AND "
"f.extractor_version=v.extractor_version AND f.parameter_fingerprint="
"v.feature_parameter_fingerprint ORDER BY f.feature_set_id LIMIT ?3",
feature_select);
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result = prepare(db, sql, &statement);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(statement, 1, (sqlite3_int64)index_id);
sqlite3_bind_int64(statement, 2, (sqlite3_int64)after);
sqlite3_bind_int64(statement, 3, (sqlite3_int64)capacity);
int code = SQLITE_DONE;
while (*count < capacity && (code = sqlite3_step(statement)) == SQLITE_ROW) {
if (!read_feature_set(statement, &sets[*count])) {
result = LARDON3D_PROJECT_DB_CORRUPT;
break;
}
++*count;
}
if (result == LARDON3D_PROJECT_DB_OK && code != SQLITE_DONE && *count < capacity) {
result = sqlite_result(db, code, "list pending visual index features");
}
sqlite3_finalize(statement);
}
(void)pthread_mutex_unlock(&db->mutex);
return result;
}
Lardon3DProjectDbResult lardon3d_project_db_publish_visual_index_segment(
Lardon3DProjectDb *db, const Lardon3DProjectDbVisualIndexSegment *segment,
const uint64_t *ids, size_t count, Lardon3DProjectDbVisualIndexSegment *published) {
if (!db || !segment || !valid_catalog_id(segment->visual_index_id) || segment->generation == 0 ||
!canonical_visual_path(segment->sha256, segment->path) || segment->size_bytes < 128 ||
segment->size_bytes > INT64_MAX || segment->posting_count > INT64_MAX || !ids || count == 0 ||
count > 16 || segment->feature_set_count != count || segment->durability < 0 ||
segment->durability > 1 || (segment->producer_task_id &&
!valid_task_id(segment->producer_task_id)) ||
segment->created_at < 0 || !published) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
#ifdef LARDON3D_PROJECT_DB_TESTING
const char *fail_publish = getenv("LARDON3D_TEST_PROJECT_DB_FAIL_VISUAL_SEGMENT");
if (fail_publish && strcmp(fail_publish, "1") == 0) {
return LARDON3D_PROJECT_DB_BUSY;
}
#endif
(void)pthread_mutex_lock(&db->mutex);
Lardon3DProjectDbResult result = execute(db, "BEGIN IMMEDIATE", "begin visual segment publish");
sqlite3_stmt *statement = NULL;
if (result == LARDON3D_PROJECT_DB_OK) {
result = prepare(db, "INSERT INTO visual_index_segments(visual_index_id,generation,sha256,"
"path,size_bytes,posting_count,feature_set_count,durability,"
"producer_task_id,created_at) VALUES(?1,?2,?3,?4,?5,?6,?7,?8,?9,?10)",
&statement);
}
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(statement, 1, (sqlite3_int64)segment->visual_index_id);
sqlite3_bind_int64(statement, 2, (sqlite3_int64)segment->generation);
sqlite3_bind_blob(statement, 3, segment->sha256, 32, SQLITE_TRANSIENT);
sqlite3_bind_text(statement, 4, segment->path, -1, SQLITE_TRANSIENT);
sqlite3_bind_int64(statement, 5, (sqlite3_int64)segment->size_bytes);
sqlite3_bind_int64(statement, 6, (sqlite3_int64)segment->posting_count);
sqlite3_bind_int64(statement, 7, (sqlite3_int64)count);
sqlite3_bind_int(statement, 8, (int)segment->durability);
if (segment->producer_task_id) {
sqlite3_bind_int64(statement, 9, (sqlite3_int64)segment->producer_task_id);
} else {
sqlite3_bind_null(statement, 9);
}
sqlite3_bind_int64(statement, 10, segment->created_at);
result = step_done(db, statement, "insert visual segment");
}
sqlite3_int64 segment_id = sqlite3_last_insert_rowid(db->connection);
for (size_t i = 0; i < count && result == LARDON3D_PROJECT_DB_OK; ++i) {
if (!valid_catalog_id(ids[i])) {
result = LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
break;
}
result = prepare(db, "INSERT INTO visual_index_memberships(visual_index_id,feature_set_id,"
"visual_index_segment_id) VALUES(?1,?2,?3)", &statement);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(statement, 1, (sqlite3_int64)segment->visual_index_id);
sqlite3_bind_int64(statement, 2, (sqlite3_int64)ids[i]);
sqlite3_bind_int64(statement, 3, segment_id);
result = step_done(db, statement, "insert visual membership");
}
}
if (result == LARDON3D_PROJECT_DB_OK) {
result = execute(db, "COMMIT", "commit visual segment publish");
}
if (result != LARDON3D_PROJECT_DB_OK) {
(void)execute(db, "ROLLBACK", "rollback visual segment publish");
}
(void)pthread_mutex_unlock(&db->mutex);
if (result != LARDON3D_PROJECT_DB_OK) {
return result;
}
Lardon3DProjectDbVisualIndexSegment page[1];
size_t page_count = 0;
result = lardon3d_project_db_list_visual_index_segments(db, segment->visual_index_id,
segment->generation - 1, page, 1,
&page_count);
if (result == LARDON3D_PROJECT_DB_OK && page_count == 1 &&
page[0].generation == segment->generation) {
*published = page[0];
return LARDON3D_PROJECT_DB_OK;
}
return result == LARDON3D_PROJECT_DB_OK ? LARDON3D_PROJECT_DB_CORRUPT : result;
}
Lardon3DProjectDbResult lardon3d_project_db_record_visual_index_update_task(
Lardon3DProjectDb *db, const Lardon3DTaskDurableSnapshot *snapshot, const char *kind,
uint32_t version, const Lardon3DProjectDbCheckpoint *checkpoint,
const Lardon3DProjectDbVisualIndexUpdateTask *parameters, int64_t updated_at) {
if (!snapshot || !parameters || parameters->task_id != snapshot->id ||
!valid_catalog_id(parameters->visual_index_id) ||
parameters->after_feature_set_id > INT64_MAX) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
return record_task_internal(db, snapshot, kind, version, checkpoint, NULL, 0, NULL,
NULL, parameters, NULL, NULL, NULL, NULL, updated_at);
}
Lardon3DProjectDbResult lardon3d_project_db_load_visual_index_update_task(
Lardon3DProjectDb *db, uint64_t task_id,
Lardon3DProjectDbVisualIndexUpdateTask *parameters) {
if (!db || !valid_task_id(task_id) || !parameters) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
memset(parameters, 0, sizeof(*parameters));
(void)pthread_mutex_lock(&db->mutex);
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result = prepare(
db, "SELECT visual_index_id,after_feature_set_id FROM visual_index_update_tasks WHERE "
"task_id=?1", &statement);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(statement, 1, (sqlite3_int64)task_id);
int code = sqlite3_step(statement);
sqlite3_int64 index_id = sqlite3_column_int64(statement, 0);
sqlite3_int64 after = sqlite3_column_int64(statement, 1);
if (code == SQLITE_DONE) {
result = LARDON3D_PROJECT_DB_NOT_FOUND;
} else if (code != SQLITE_ROW || index_id <= 0 || after < 0) {
result = LARDON3D_PROJECT_DB_CORRUPT;
} else {
parameters->task_id = task_id;
parameters->visual_index_id = (uint64_t)index_id;
parameters->after_feature_set_id = (uint64_t)after;
}
sqlite3_finalize(statement);
}
(void)pthread_mutex_unlock(&db->mutex);
return result;
}
Lardon3DProjectDbResult lardon3d_project_db_record_candidate_pair_generate_task(
Lardon3DProjectDb *db, const Lardon3DTaskDurableSnapshot *snapshot, const char *kind,
uint32_t version, const Lardon3DProjectDbCheckpoint *checkpoint,
const Lardon3DProjectDbCandidatePairGenerateTask *parameters, int64_t updated_at) {
if (!snapshot || !parameters || parameters->task_id != snapshot->id ||
!valid_catalog_id(parameters->visual_index_id) ||
parameters->after_feature_set_id > INT64_MAX || parameters->top_k == 0 ||
parameters->top_k > 256 || parameters->minimum_evidence_count > 1024 ||
parameters->scanset_filter < 0 || parameters->scanset_filter > 2) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
return record_task_internal(db, snapshot, kind, version, checkpoint, NULL, 0, NULL,
NULL, NULL, parameters, NULL, NULL, NULL, updated_at);
}
Lardon3DProjectDbResult lardon3d_project_db_load_candidate_pair_generate_task(
Lardon3DProjectDb *db, uint64_t task_id,
Lardon3DProjectDbCandidatePairGenerateTask *parameters) {
if (!db || !valid_task_id(task_id) || !parameters) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
memset(parameters, 0, sizeof(*parameters));
(void)pthread_mutex_lock(&db->mutex);
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result = prepare(
db,
"SELECT visual_index_id,after_feature_set_id,top_k,minimum_evidence_count,"
"scanset_filter,exclude_same_asset FROM candidate_pair_generate_tasks WHERE task_id=?1",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(statement, 1, (sqlite3_int64)task_id);
int code = sqlite3_step(statement);
sqlite3_int64 index_id = sqlite3_column_int64(statement, 0);
sqlite3_int64 after = sqlite3_column_int64(statement, 1);
int top_k_val = sqlite3_column_int(statement, 2);
int evidence = sqlite3_column_int(statement, 3);
int filter_val = sqlite3_column_int(statement, 4);
int same_asset = sqlite3_column_int(statement, 5);
if (code == SQLITE_DONE) {
result = LARDON3D_PROJECT_DB_NOT_FOUND;
} else if (code != SQLITE_ROW || index_id <= 0 || after < 0 || top_k_val <= 0 ||
top_k_val > 256 || evidence < 0 || evidence > 1024 || filter_val < 0 ||
filter_val > 2) {
result = LARDON3D_PROJECT_DB_CORRUPT;
} else {
parameters->task_id = task_id;
parameters->visual_index_id = (uint64_t)index_id;
parameters->after_feature_set_id = (uint64_t)after;
parameters->top_k = (uint32_t)top_k_val;
parameters->minimum_evidence_count = (uint32_t)evidence;
parameters->scanset_filter = filter_val;
parameters->exclude_same_asset = same_asset != 0;
}
sqlite3_finalize(statement);
}
(void)pthread_mutex_unlock(&db->mutex);
return result;
}
Lardon3DProjectDbResult lardon3d_project_db_record_matcher_task(
Lardon3DProjectDb *db, const Lardon3DTaskDurableSnapshot *snapshot,
const char *kind, uint32_t version,
const Lardon3DProjectDbCheckpoint *checkpoint,
const Lardon3DProjectDbMatcherTask *parameters, int64_t updated_at) {
if (!snapshot || !parameters || parameters->task_id != snapshot->id) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
return record_task_internal(db, snapshot, kind, version, checkpoint, NULL, 0, NULL,
NULL, NULL, NULL, parameters, NULL, NULL, updated_at);
}
Lardon3DProjectDbResult lardon3d_project_db_load_matcher_task(
Lardon3DProjectDb *db, uint64_t task_id,
Lardon3DProjectDbMatcherTask *parameters) {
if (!db || !valid_task_id(task_id) || !parameters) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
memset(parameters, 0, sizeof(*parameters));
(void)pthread_mutex_lock(&db->mutex);
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result = prepare(
db,
"SELECT after_candidate_pair_id,feature_extractor_kind,feature_extractor_version,"
"feature_parameter_fingerprint,matcher_kind,ratio_threshold FROM matcher_tasks "
"WHERE task_id=?1",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(statement, 1, (sqlite3_int64)task_id);
int code = sqlite3_step(statement);
sqlite3_int64 after = sqlite3_column_int64(statement, 0);
sqlite3_int64 extractor_version = sqlite3_column_int64(statement, 2);
int matcher_kind = sqlite3_column_int(statement, 4);
double ratio = sqlite3_column_double(statement, 5);
if (code == SQLITE_DONE) {
result = LARDON3D_PROJECT_DB_NOT_FOUND;
} else if (code != SQLITE_ROW || after < 0 || extractor_version <= 0 ||
extractor_version > UINT32_MAX || matcher_kind < 0 || matcher_kind > 2 ||
!isfinite(ratio) || ratio <= 0.0 || ratio >= 1.0 ||
sqlite3_column_bytes(statement, 3) != LARDON3D_PROJECT_DB_SHA256_SIZE ||
!copy_column(statement, 1, parameters->feature_extractor_kind,
sizeof(parameters->feature_extractor_kind))) {
result = LARDON3D_PROJECT_DB_CORRUPT;
} else {
parameters->task_id = task_id;
parameters->after_candidate_pair_id = (uint64_t)after;
parameters->feature_extractor_version = (uint32_t)extractor_version;
memcpy(parameters->feature_parameter_fingerprint,
sqlite3_column_blob(statement, 3), LARDON3D_PROJECT_DB_SHA256_SIZE);
parameters->matcher_kind = matcher_kind;
parameters->ratio_threshold = (float)ratio;
}
sqlite3_finalize(statement);
}
(void)pthread_mutex_unlock(&db->mutex);
return result;
}
Lardon3DProjectDbResult lardon3d_project_db_record_geometric_verifier_task(
Lardon3DProjectDb *db, const Lardon3DTaskDurableSnapshot *snapshot,
const char *kind, uint32_t version,
const Lardon3DProjectDbCheckpoint *checkpoint,
const Lardon3DProjectDbGeometricVerifierTask *parameters, int64_t updated_at) {
if (!snapshot || !parameters || parameters->task_id != snapshot->id) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
return record_task_internal(db, snapshot, kind, version, checkpoint, NULL, 0, NULL,
NULL, NULL, NULL, NULL, parameters, NULL, updated_at);
}
Lardon3DProjectDbResult lardon3d_project_db_load_geometric_verifier_task(
Lardon3DProjectDb *db, uint64_t task_id,
Lardon3DProjectDbGeometricVerifierTask *parameters) {
if (!db || !valid_task_id(task_id) || !parameters) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
memset(parameters, 0, sizeof(*parameters));
(void)pthread_mutex_lock(&db->mutex);
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result = prepare(
db,
"SELECT after_match_result_id,threshold_pixels,confidence,max_iterations,"
"min_inlier_count,min_inlier_ratio,seed_policy_version,canonicalization_version,"
"parameter_fingerprint FROM geometric_verifier_tasks WHERE task_id=?1",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(statement, 1, (sqlite3_int64)task_id);
int code = sqlite3_step(statement);
sqlite3_int64 after = sqlite3_column_int64(statement, 0);
double threshold = sqlite3_column_double(statement, 1);
double confidence = sqlite3_column_double(statement, 2);
sqlite3_int64 iterations = sqlite3_column_int64(statement, 3);
sqlite3_int64 min_count = sqlite3_column_int64(statement, 4);
double min_ratio = sqlite3_column_double(statement, 5);
sqlite3_int64 seed_version = sqlite3_column_int64(statement, 6);
sqlite3_int64 canonical_version = sqlite3_column_int64(statement, 7);
if (code == SQLITE_DONE) {
result = LARDON3D_PROJECT_DB_NOT_FOUND;
} else if (code != SQLITE_ROW || after < 0 || !isfinite(threshold) || threshold <= 0.0 ||
!isfinite(confidence) || confidence <= 0.0 || confidence >= 1.0 ||
iterations <= 0 || iterations > INT_MAX || min_count <= 0 ||
min_count > 8192 || !isfinite(min_ratio) || min_ratio < 0.0 ||
min_ratio > 1.0 || seed_version <= 0 || seed_version > UINT32_MAX ||
canonical_version <= 0 || canonical_version > UINT32_MAX ||
sqlite3_column_bytes(statement, 8) != LARDON3D_PROJECT_DB_SHA256_SIZE) {
result = LARDON3D_PROJECT_DB_CORRUPT;
} else {
parameters->task_id = task_id;
parameters->after_match_result_id = (uint64_t)after;
parameters->threshold_pixels = threshold;
parameters->confidence = confidence;
parameters->max_iterations = (uint32_t)iterations;
parameters->min_inlier_count = (uint32_t)min_count;
parameters->min_inlier_ratio = min_ratio;
parameters->seed_policy_version = (uint32_t)seed_version;
parameters->canonicalization_version = (uint32_t)canonical_version;
memcpy(parameters->parameter_fingerprint, sqlite3_column_blob(statement, 8),
LARDON3D_PROJECT_DB_SHA256_SIZE);
}
sqlite3_finalize(statement);
}
(void)pthread_mutex_unlock(&db->mutex);
return result;
}
Lardon3DProjectDbResult lardon3d_project_db_record_track_builder_task(
Lardon3DProjectDb *db, const Lardon3DTaskDurableSnapshot *snapshot,
const char *kind, uint32_t version, const Lardon3DProjectDbCheckpoint *checkpoint,
const Lardon3DProjectDbTrackBuilderTask *parameters, int64_t updated_at) {
if (!db || !snapshot || !parameters || parameters->task_id != snapshot->id ||
strcmp(parameters->builder_kind, "track_builder") != 0 ||
parameters->builder_version == 0 || parameters->verifier_kind <= 0 ||
parameters->verifier_version == 0 || parameters->gvr_count == 0 ||
parameters->scope_format_version != 1 || parameters->scope_size_bytes == 0 ||
!bounded_text(parameters->scope_path, sizeof(parameters->scope_path), false) ||
strncmp(parameters->scope_path, ".lardon3d/checkpoints/", 21) != 0) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
Lardon3DProjectDbResult result = record_task_internal(
db, snapshot, kind, version, checkpoint, NULL, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
updated_at);
if (result != LARDON3D_PROJECT_DB_OK) return result;
(void)pthread_mutex_lock(&db->mutex);
result = execute(db, "BEGIN IMMEDIATE", "begin track builder task");
sqlite3_stmt *statement = NULL;
if (result == LARDON3D_PROJECT_DB_OK) {
result = prepare(
db,
"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(?1,?2,?3,?4,?5,?6,?7,?8,?9,"
"?10,?11,?12,?13) ON CONFLICT(task_id) DO UPDATE SET scope_path=excluded.scope_path,"
"scope_size_bytes=excluded.scope_size_bytes,scope_sha256=excluded.scope_sha256 "
"WHERE track_builder_tasks.builder_kind=excluded.builder_kind AND "
"track_builder_tasks.builder_version=excluded.builder_version AND "
"track_builder_tasks.builder_fingerprint=excluded.builder_fingerprint AND "
"track_builder_tasks.verifier_kind=excluded.verifier_kind AND "
"track_builder_tasks.verifier_version=excluded.verifier_version AND "
"track_builder_tasks.verifier_fingerprint=excluded.verifier_fingerprint AND "
"track_builder_tasks.input_scope_hash=excluded.input_scope_hash AND "
"track_builder_tasks.gvr_count=excluded.gvr_count",
&statement);
}
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(statement, 1, (sqlite3_int64)parameters->task_id);
sqlite3_bind_text(statement, 2, parameters->builder_kind, -1, SQLITE_TRANSIENT);
sqlite3_bind_int64(statement, 3, parameters->builder_version);
sqlite3_bind_blob(statement, 4, parameters->builder_fingerprint, 32, SQLITE_TRANSIENT);
sqlite3_bind_int(statement, 5, parameters->verifier_kind);
sqlite3_bind_int64(statement, 6, parameters->verifier_version);
sqlite3_bind_blob(statement, 7, parameters->verifier_fingerprint, 32, SQLITE_TRANSIENT);
sqlite3_bind_blob(statement, 8, parameters->input_scope_hash, 32, SQLITE_TRANSIENT);
sqlite3_bind_int64(statement, 9, (sqlite3_int64)parameters->gvr_count);
sqlite3_bind_text(statement, 10, parameters->scope_path, -1, SQLITE_TRANSIENT);
sqlite3_bind_int64(statement, 11, (sqlite3_int64)parameters->scope_size_bytes);
sqlite3_bind_blob(statement, 12, parameters->scope_sha256, 32, SQLITE_TRANSIENT);
sqlite3_bind_int64(statement, 13, parameters->scope_format_version);
result = step_done(db, statement, "upsert track builder task");
statement = NULL;
if (result == LARDON3D_PROJECT_DB_OK && sqlite3_changes(db->connection) != 1) {
result = LARDON3D_PROJECT_DB_CONSTRAINT;
}
}
if (statement) sqlite3_finalize(statement);
if (result == LARDON3D_PROJECT_DB_OK) result = execute(db, "COMMIT", "commit track builder task");
if (result != LARDON3D_PROJECT_DB_OK) (void)execute(db, "ROLLBACK", "rollback track builder task");
(void)pthread_mutex_unlock(&db->mutex);
return result;
}
Lardon3DProjectDbResult lardon3d_project_db_load_track_builder_task(
Lardon3DProjectDb *db, uint64_t task_id,
Lardon3DProjectDbTrackBuilderTask *parameters) {
if (!db || !valid_task_id(task_id) || !parameters) return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
memset(parameters, 0, sizeof(*parameters));
(void)pthread_mutex_lock(&db->mutex);
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result = prepare(
db,
"SELECT 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 FROM track_builder_tasks WHERE task_id=?1",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(statement, 1, (sqlite3_int64)task_id);
int code = sqlite3_step(statement);
sqlite3_int64 builder_version = sqlite3_column_int64(statement, 1);
sqlite3_int64 verifier_version = sqlite3_column_int64(statement, 4);
sqlite3_int64 gvr_count = sqlite3_column_int64(statement, 7);
sqlite3_int64 scope_size = sqlite3_column_int64(statement, 9);
sqlite3_int64 scope_format = sqlite3_column_int64(statement, 11);
if (code == SQLITE_DONE) {
result = LARDON3D_PROJECT_DB_NOT_FOUND;
} else if (code != SQLITE_ROW || builder_version <= 0 || verifier_version <= 0 ||
gvr_count <= 0 || scope_size <= 0 || scope_format != 1 ||
sqlite3_column_bytes(statement, 2) != 32 ||
sqlite3_column_bytes(statement, 5) != 32 ||
sqlite3_column_bytes(statement, 6) != 32 ||
sqlite3_column_bytes(statement, 10) != 32 ||
!copy_column(statement, 0, parameters->builder_kind,
sizeof(parameters->builder_kind)) ||
!copy_column(statement, 8, parameters->scope_path,
sizeof(parameters->scope_path)) ||
strcmp(parameters->builder_kind, "track_builder") != 0 ||
strncmp(parameters->scope_path, ".lardon3d/checkpoints/", 21) != 0) {
result = LARDON3D_PROJECT_DB_CORRUPT;
} else {
parameters->task_id = task_id;
parameters->builder_version = (uint32_t)builder_version;
memcpy(parameters->builder_fingerprint, sqlite3_column_blob(statement, 2), 32);
parameters->verifier_kind = sqlite3_column_int(statement, 3);
parameters->verifier_version = (uint32_t)verifier_version;
memcpy(parameters->verifier_fingerprint, sqlite3_column_blob(statement, 5), 32);
memcpy(parameters->input_scope_hash, sqlite3_column_blob(statement, 6), 32);
parameters->gvr_count = (uint64_t)gvr_count;
parameters->scope_size_bytes = (uint64_t)scope_size;
memcpy(parameters->scope_sha256, sqlite3_column_blob(statement, 10), 32);
parameters->scope_format_version = (uint32_t)scope_format;
}
sqlite3_finalize(statement);
}
(void)pthread_mutex_unlock(&db->mutex);
return result;
}
Lardon3DProjectDbResult lardon3d_project_db_record_sparse_sfm_task(
Lardon3DProjectDb *database, const Lardon3DTaskDurableSnapshot *snapshot,
const char *kind, uint32_t version, const Lardon3DProjectDbCheckpoint *checkpoint,
const Lardon3DProjectDbSparseSfmTask *parameters, int64_t updated_at) {
if (!parameters) return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
return record_task_internal(database, snapshot, kind, version, checkpoint, NULL, 0, NULL, NULL,
NULL, NULL, NULL, NULL, parameters, updated_at);
}
static bool read_u64_blob(sqlite3_stmt *statement, int column, uint64_t *value) {
if (sqlite3_column_type(statement, column) != SQLITE_BLOB ||
sqlite3_column_bytes(statement, column) != 8) {
return false;
}
const unsigned char *bytes = sqlite3_column_blob(statement, column);
uint64_t decoded = 0;
for (unsigned int i = 0; i < 8; ++i) decoded |= (uint64_t)bytes[i] << (8U * i);
*value = decoded;
return true;
}
Lardon3DProjectDbResult lardon3d_project_db_load_sparse_sfm_task(
Lardon3DProjectDb *database, uint64_t task_id,
Lardon3DProjectDbSparseSfmTask *parameters) {
if (!database || !valid_task_id(task_id) || !parameters) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
memset(parameters, 0, sizeof(*parameters));
(void)pthread_mutex_lock(&database->mutex);
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result = prepare(
database,
"SELECT s.track_set_id,s.calibration_scope_id,s.sfm_kind,s.sfm_version,"
"s.minimum_seed_tracks,s.minimum_seed_landmarks,s.minimum_pnp_correspondences,"
"s.maximum_seed_candidates,s.maximum_registration_rounds,s.maximum_landmarks_per_round,"
"s.maximum_images,s.maximum_observations,s.maximum_tracks,s.reprojection_threshold_px,"
"s.minimum_track_parallax_rad,s.relative_robust_threshold_px,s.relative_confidence,"
"s.relative_max_iterations,s.relative_minimum_inliers,s.relative_minimum_inlier_ratio,"
"s.relative_minimum_parallax_rad,s.relative_minimum_cheirality_ratio,"
"s.relative_deterministic_seed,s.pnp_reprojection_threshold_px,s.pnp_confidence,"
"s.pnp_max_iterations,s.pnp_minimum_inliers,s.pnp_minimum_inlier_ratio,"
"s.pnp_deterministic_seed,s.refinement_max_iterations,s.refinement_convergence_tolerance,"
"t.task_kind,t.task_kind_version FROM sparse_sfm_tasks s JOIN tasks t ON t.task_id=s.task_id "
"WHERE s.task_id=?1",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(statement, 1, (sqlite3_int64)task_id);
int code = sqlite3_step(statement);
bool corrupt = code != SQLITE_ROW;
if (code == SQLITE_DONE) {
result = LARDON3D_PROJECT_DB_NOT_FOUND;
} else if (!corrupt) {
const char *kind = (const char *)sqlite3_column_text(statement, 31);
sqlite3_int64 ids[2] = {sqlite3_column_int64(statement, 0),
sqlite3_column_int64(statement, 1)};
sqlite3_int64 integers[15];
const int integer_columns[15] = {2, 3, 4, 5, 6, 7, 8, 9, 10, 17, 18, 25, 26, 29, 32};
for (size_t i = 0; i < 15; ++i) integers[i] = sqlite3_column_int64(statement,
integer_columns[i]);
double doubles[11];
const int double_columns[11] = {13, 14, 15, 16, 19, 20, 21, 23, 24, 27, 30};
for (size_t i = 0; i < 11; ++i) doubles[i] = sqlite3_column_double(statement,
double_columns[i]);
corrupt = ids[0] <= 0 || ids[1] <= 0 || !kind || strcmp(kind, "sparse_sfm.run") != 0 ||
integers[0] != 1 || integers[1] != 1 || integers[14] != 1;
for (size_t i = 2; i < 14; ++i) {
if (integers[i] <= 0 || integers[i] > UINT32_MAX) corrupt = true;
}
for (size_t i = 0; i < 11; ++i) {
if (sqlite3_column_type(statement, double_columns[i]) != SQLITE_FLOAT ||
!isfinite(doubles[i])) {
corrupt = true;
}
}
parameters->task_id = task_id;
parameters->track_set_id = (uint64_t)ids[0];
parameters->calibration_scope_id = (uint64_t)ids[1];
parameters->sfm_kind = (uint32_t)integers[0];
parameters->sfm_version = (uint32_t)integers[1];
Lardon3DSparseIncrementalParameters *p = &parameters->parameters;
p->minimum_seed_tracks = (uint32_t)integers[2];
p->minimum_seed_landmarks = (uint32_t)integers[3];
p->minimum_pnp_correspondences = (uint32_t)integers[4];
p->maximum_seed_candidates = (uint32_t)integers[5];
p->maximum_registration_rounds = (uint32_t)integers[6];
p->maximum_landmarks_per_round = (uint32_t)integers[7];
p->maximum_images = (uint32_t)integers[8];
corrupt |= !read_u64_blob(statement, 11, &p->maximum_observations);
corrupt |= !read_u64_blob(statement, 12, &p->maximum_tracks);
p->reprojection_threshold_px = doubles[0];
p->minimum_track_parallax_rad = doubles[1];
p->relative_pose.robust_threshold_px = doubles[2];
p->relative_pose.confidence = doubles[3];
p->relative_pose.max_iterations = (uint32_t)integers[9];
p->relative_pose.minimum_inliers = (uint32_t)integers[10];
p->relative_pose.minimum_inlier_ratio = doubles[4];
p->relative_pose.minimum_parallax_rad = doubles[5];
p->relative_pose.minimum_cheirality_ratio = doubles[6];
corrupt |= !read_u64_blob(statement, 22, &p->relative_pose.deterministic_seed);
p->pnp.reprojection_threshold_px = doubles[7];
p->pnp.confidence = doubles[8];
p->pnp.max_iterations = (uint32_t)integers[11];
p->pnp.minimum_inliers = (uint32_t)integers[12];
p->pnp.minimum_inlier_ratio = doubles[9];
corrupt |= !read_u64_blob(statement, 28, &p->pnp.deterministic_seed);
p->refinement.max_iterations = (uint32_t)integers[13];
p->refinement.convergence_tolerance = doubles[10];
if (corrupt || !valid_sparse_sfm_parameters(p)) result = LARDON3D_PROJECT_DB_CORRUPT;
} else {
result = sqlite_result(database, code, "read sparse SfM task");
}
sqlite3_finalize(statement);
}
(void)pthread_mutex_unlock(&database->mutex);
return result;
}
Lardon3DProjectDbResult lardon3d_project_db_record_incremental_reconstruction_task(
Lardon3DProjectDb *database, const Lardon3DTaskDurableSnapshot *snapshot,
const char *kind, uint32_t version,
const Lardon3DProjectDbCheckpoint *checkpoint,
const Lardon3DProjectDbIncrementalReconstructionTask *parameters,
int64_t updated_at) {
if (!database || !snapshot || !parameters ||
!lardon3d_task_kind_is_valid(kind) ||
strcmp(kind, "incremental_reconstruction.run") != 0 || version != 1 ||
parameters->task_id != snapshot->id ||
!valid_task_id(parameters->base_reconstruction_id) ||
!valid_task_id(parameters->extension_track_set_id) ||
!valid_task_id(parameters->calibration_scope_id) ||
parameters->incremental_kind != 1 || parameters->incremental_version != 1)
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
Lardon3DProjectDbResult result = lardon3d_project_db_record_task(
database, snapshot, kind, version, checkpoint, updated_at);
if (result != LARDON3D_PROJECT_DB_OK) return result;
(void)pthread_mutex_lock(&database->mutex);
sqlite3_stmt *statement = NULL;
result = execute(database, "BEGIN IMMEDIATE", "begin incremental task payload");
if (result == LARDON3D_PROJECT_DB_OK)
result = prepare(
database,
"INSERT INTO incremental_reconstruction_tasks(task_id,base_reconstruction_id,"
"extension_track_set_id,calibration_scope_id,incremental_kind,incremental_version,"
"parameter_fingerprint) VALUES(?1,?2,?3,?4,?5,?6,?7) ON CONFLICT(task_id) DO "
"UPDATE SET task_id=excluded.task_id WHERE incremental_reconstruction_tasks."
"base_reconstruction_id=excluded.base_reconstruction_id AND "
"incremental_reconstruction_tasks.extension_track_set_id=excluded."
"extension_track_set_id AND incremental_reconstruction_tasks.calibration_scope_id="
"excluded.calibration_scope_id AND incremental_reconstruction_tasks.incremental_kind="
"excluded.incremental_kind AND incremental_reconstruction_tasks.incremental_version="
"excluded.incremental_version AND incremental_reconstruction_tasks."
"parameter_fingerprint=excluded.parameter_fingerprint",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(statement, 1, (sqlite3_int64)parameters->task_id);
sqlite3_bind_int64(statement, 2,
(sqlite3_int64)parameters->base_reconstruction_id);
sqlite3_bind_int64(statement, 3,
(sqlite3_int64)parameters->extension_track_set_id);
sqlite3_bind_int64(statement, 4,
(sqlite3_int64)parameters->calibration_scope_id);
sqlite3_bind_int(statement, 5, (int)parameters->incremental_kind);
sqlite3_bind_int(statement, 6, (int)parameters->incremental_version);
sqlite3_bind_blob(statement, 7, parameters->parameter_fingerprint, 32,
SQLITE_TRANSIENT);
result = step_done(database, statement, "upsert incremental task payload");
statement = NULL;
if (result == LARDON3D_PROJECT_DB_OK && sqlite3_changes(database->connection) != 1) {
copy_error(database->error, "Payload de reconstruction incrémentale immuable.");
result = LARDON3D_PROJECT_DB_CONSTRAINT;
}
}
if (statement) sqlite3_finalize(statement);
if (result == LARDON3D_PROJECT_DB_OK)
result = execute(database, "COMMIT", "commit incremental task payload");
if (result != LARDON3D_PROJECT_DB_OK)
(void)execute(database, "ROLLBACK", "rollback incremental task payload");
(void)pthread_mutex_unlock(&database->mutex);
return result;
}
Lardon3DProjectDbResult lardon3d_project_db_load_incremental_reconstruction_task(
Lardon3DProjectDb *database, uint64_t task_id,
Lardon3DProjectDbIncrementalReconstructionTask *parameters) {
if (!database || !valid_task_id(task_id) || !parameters)
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
memset(parameters, 0, sizeof(*parameters));
(void)pthread_mutex_lock(&database->mutex);
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result = prepare(
database,
"SELECT base_reconstruction_id,extension_track_set_id,calibration_scope_id,"
"incremental_kind,incremental_version,parameter_fingerprint FROM "
"incremental_reconstruction_tasks WHERE task_id=?1",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(statement, 1, (sqlite3_int64)task_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, 5) != 32)
result = LARDON3D_PROJECT_DB_CORRUPT;
else {
parameters->task_id = task_id;
parameters->base_reconstruction_id =
(uint64_t)sqlite3_column_int64(statement, 0);
parameters->extension_track_set_id =
(uint64_t)sqlite3_column_int64(statement, 1);
parameters->calibration_scope_id =
(uint64_t)sqlite3_column_int64(statement, 2);
parameters->incremental_kind = (uint32_t)sqlite3_column_int(statement, 3);
parameters->incremental_version = (uint32_t)sqlite3_column_int(statement, 4);
memcpy(parameters->parameter_fingerprint, sqlite3_column_blob(statement, 5), 32);
if (!valid_task_id(parameters->base_reconstruction_id) ||
!valid_task_id(parameters->extension_track_set_id) ||
!valid_task_id(parameters->calibration_scope_id) ||
parameters->incremental_kind != 1 || parameters->incremental_version != 1)
result = LARDON3D_PROJECT_DB_CORRUPT;
}
sqlite3_finalize(statement);
}
(void)pthread_mutex_unlock(&database->mutex);
if (result != LARDON3D_PROJECT_DB_OK) memset(parameters, 0, sizeof(*parameters));
return result;
}
static bool read_match_result(sqlite3_stmt *statement,
Lardon3DProjectDbMatchResult *result) {
sqlite3_int64 id = sqlite3_column_int64(statement, 0);
sqlite3_int64 cp_id = sqlite3_column_int64(statement, 1);
sqlite3_int64 fs_a = sqlite3_column_int64(statement, 2);
sqlite3_int64 fs_b = sqlite3_column_int64(statement, 3);
const char *kind = (const char *)sqlite3_column_text(statement, 4);
sqlite3_int64 version = sqlite3_column_int64(statement, 5);
const void *fp = sqlite3_column_blob(statement, 6);
sqlite3_int64 status = sqlite3_column_int64(statement, 7);
sqlite3_int64 match_count = sqlite3_column_int64(statement, 8);
const void *asset_sha256 = sqlite3_column_blob(statement, 9);
int asset_sha256_bytes = sqlite3_column_bytes(statement, 9);
const char *asset_path = (const char *)sqlite3_column_text(statement, 10);
sqlite3_int64 asset_size = sqlite3_column_int64(statement, 11);
sqlite3_int64 created = sqlite3_column_int64(statement, 12);
bool has_sha = sqlite3_column_type(statement, 9) != SQLITE_NULL;
bool has_path = sqlite3_column_type(statement, 10) != SQLITE_NULL;
bool has_size = sqlite3_column_type(statement, 11) != SQLITE_NULL;
if (id <= 0 || cp_id <= 0 || fs_a <= 0 || fs_b <= 0 ||
!kind || version <= 0 || !fp || sqlite3_column_bytes(statement, 6) != 32 ||
status < 0 || status > 1 || match_count < 0 || match_count > 8192 || created < 0 ||
(status == LARDON3D_MATCH_RESULT_STATUS_NO_MATCH &&
(match_count != 0 || has_sha || has_path || has_size)) ||
(status == LARDON3D_MATCH_RESULT_STATUS_MATCHED &&
(match_count == 0 || !has_sha || !has_path || !has_size || asset_size <= 0))) {
return false;
}
result->match_result_id = (uint64_t)id;
result->candidate_pair_id = (uint64_t)cp_id;
result->feature_set_id_a = (uint64_t)fs_a;
result->feature_set_id_b = (uint64_t)fs_b;
size_t len = strlen(kind);
if (len == 0 || len >= LARDON3D_PROJECT_DB_KIND_CAPACITY) return false;
memcpy(result->matcher_kind, kind, len + 1);
result->matcher_version = (uint32_t)version;
memcpy(result->parameter_fingerprint, fp, 32);
result->result_status = (int)status;
result->match_count = (uint32_t)match_count;
/* Asset fields */
if (asset_sha256 && asset_sha256_bytes == LARDON3D_PROJECT_DB_SHA256_SIZE) {
result->has_match_asset = true;
memcpy(result->match_asset_sha256, asset_sha256, LARDON3D_PROJECT_DB_SHA256_SIZE);
} else {
result->has_match_asset = false;
memset(result->match_asset_sha256, 0, LARDON3D_PROJECT_DB_SHA256_SIZE);
}
if (asset_path) {
size_t plen = strlen(asset_path);
if (plen < LARDON3D_PROJECT_DB_PATH_CAPACITY) {
memcpy(result->match_asset_path, asset_path, plen + 1);
} else {
memcpy(result->match_asset_path, asset_path, LARDON3D_PROJECT_DB_PATH_CAPACITY - 1);
result->match_asset_path[LARDON3D_PROJECT_DB_PATH_CAPACITY - 1] = '\0';
}
} else {
result->match_asset_path[0] = '\0';
}
result->match_asset_size_bytes = asset_size >= 0 ? (uint64_t)asset_size : 0;
result->created_at = created;
return true;
}
Lardon3DProjectDbResult lardon3d_project_db_create_match_result(
Lardon3DProjectDb *database, uint64_t candidate_pair_id, uint64_t feature_set_id_a,
uint64_t feature_set_id_b, const char *matcher_kind, uint32_t matcher_version,
const unsigned char parameter_fingerprint[LARDON3D_PROJECT_DB_SHA256_SIZE], int result_status,
uint32_t match_count,
const unsigned char *match_asset_sha256, const char *match_asset_path,
uint64_t match_asset_size_bytes, int64_t created_at,
Lardon3DProjectDbMatchResult *result) {
if (!database || !valid_catalog_id(candidate_pair_id) ||
!valid_catalog_id(feature_set_id_a) || !valid_catalog_id(feature_set_id_b) ||
!bounded_text(matcher_kind, LARDON3D_PROJECT_DB_KIND_CAPACITY, false) ||
matcher_version == 0 || !parameter_fingerprint || result_status < 0 || result_status > 1 ||
match_count > 8192 || created_at < 0 || !result ||
(result_status == LARDON3D_MATCH_RESULT_STATUS_NO_MATCH &&
(match_count != 0 || match_asset_sha256 || match_asset_path || match_asset_size_bytes != 0)) ||
(result_status == LARDON3D_MATCH_RESULT_STATUS_MATCHED &&
(match_count == 0 || !match_asset_sha256 || !match_asset_path ||
match_asset_size_bytes == 0))) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
/* Validate asset path if provided */
if (match_asset_path && match_asset_path[0] != '\0' &&
!bounded_text(match_asset_path, LARDON3D_PROJECT_DB_PATH_CAPACITY, false)) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
memset(result, 0, sizeof(*result));
(void)pthread_mutex_lock(&database->mutex);
/* Validate Feature Set → image ownership before INSERT */
sqlite3_stmt *check = NULL;
Lardon3DProjectDbResult db_result = LARDON3D_PROJECT_DB_OK;
sqlite3_int64 cp_image_a = 0, cp_image_b = 0;
sqlite3_int64 fs_a_image = 0, fs_b_image = 0;
/* Load candidate pair image_ids */
db_result = prepare(database,
"SELECT image_id_a,image_id_b FROM candidate_pairs WHERE "
"candidate_pair_id=?1",
&check);
if (db_result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(check, 1, (sqlite3_int64)candidate_pair_id);
int code = sqlite3_step(check);
if (code == SQLITE_DONE) {
db_result = LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
} else if (code != SQLITE_ROW) {
db_result = LARDON3D_PROJECT_DB_CORRUPT;
} else {
cp_image_a = sqlite3_column_int64(check, 0);
cp_image_b = sqlite3_column_int64(check, 1);
}
(void)sqlite3_finalize(check);
check = NULL;
}
/* Verify feature_set_id_a belongs to image_id_a */
if (db_result == LARDON3D_PROJECT_DB_OK) {
db_result = prepare(database,
"SELECT image_id FROM feature_sets WHERE feature_set_id=?1", &check);
if (db_result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(check, 1, (sqlite3_int64)feature_set_id_a);
int code = sqlite3_step(check);
if (code == SQLITE_DONE) {
db_result = LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
} else if (code != SQLITE_ROW) {
db_result = LARDON3D_PROJECT_DB_CORRUPT;
} else {
fs_a_image = sqlite3_column_int64(check, 0);
if (fs_a_image != cp_image_a) {
db_result = LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
}
(void)sqlite3_finalize(check);
check = NULL;
}
}
/* Verify feature_set_id_b belongs to image_id_b */
if (db_result == LARDON3D_PROJECT_DB_OK) {
db_result = prepare(database,
"SELECT image_id FROM feature_sets WHERE feature_set_id=?1", &check);
if (db_result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(check, 1, (sqlite3_int64)feature_set_id_b);
int code = sqlite3_step(check);
if (code == SQLITE_DONE) {
db_result = LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
} else if (code != SQLITE_ROW) {
db_result = LARDON3D_PROJECT_DB_CORRUPT;
} else {
fs_b_image = sqlite3_column_int64(check, 0);
if (fs_b_image != cp_image_b) {
db_result = LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
}
(void)sqlite3_finalize(check);
check = NULL;
}
}
/* INSERT */
sqlite3_stmt *statement = NULL;
if (db_result == LARDON3D_PROJECT_DB_OK) {
db_result = prepare(
database,
"INSERT INTO "
"match_results(candidate_pair_id,feature_set_id_a,feature_set_id_b,matcher_kind,"
"matcher_version,parameter_fingerprint,result_status,match_count,"
"match_asset_sha256,match_asset_path,match_asset_size_bytes,"
"created_at) VALUES(?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12)",
&statement);
}
if (db_result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)candidate_pair_id);
(void)sqlite3_bind_int64(statement, 2, (sqlite3_int64)feature_set_id_a);
(void)sqlite3_bind_int64(statement, 3, (sqlite3_int64)feature_set_id_b);
(void)sqlite3_bind_text(statement, 4, matcher_kind, -1, SQLITE_TRANSIENT);
(void)sqlite3_bind_int64(statement, 5, matcher_version);
(void)sqlite3_bind_blob(statement, 6, parameter_fingerprint, 32, SQLITE_TRANSIENT);
(void)sqlite3_bind_int(statement, 7, result_status);
(void)sqlite3_bind_int64(statement, 8, match_count);
if (match_asset_sha256) {
(void)sqlite3_bind_blob(statement, 9, match_asset_sha256,
LARDON3D_PROJECT_DB_SHA256_SIZE, SQLITE_TRANSIENT);
} else {
(void)sqlite3_bind_null(statement, 9);
}
if (match_asset_path && match_asset_path[0] != '\0') {
(void)sqlite3_bind_text(statement, 10, match_asset_path, -1, SQLITE_TRANSIENT);
} else {
(void)sqlite3_bind_null(statement, 10);
}
if (result_status == LARDON3D_MATCH_RESULT_STATUS_MATCHED) {
(void)sqlite3_bind_int64(statement, 11, (sqlite3_int64)match_asset_size_bytes);
} else {
(void)sqlite3_bind_null(statement, 11);
}
(void)sqlite3_bind_int64(statement, 12, created_at);
db_result = step_done(database, statement, "create match result");
}
sqlite3_int64 id = sqlite3_last_insert_rowid(database->connection);
if (db_result == LARDON3D_PROJECT_DB_OK && id <= 0) {
db_result = LARDON3D_PROJECT_DB_CONSTRAINT;
}
if (db_result == LARDON3D_PROJECT_DB_OK) {
result->match_result_id = (uint64_t)id;
result->candidate_pair_id = candidate_pair_id;
result->feature_set_id_a = feature_set_id_a;
result->feature_set_id_b = feature_set_id_b;
(void)snprintf(result->matcher_kind, sizeof(result->matcher_kind), "%s", matcher_kind);
result->matcher_version = matcher_version;
memcpy(result->parameter_fingerprint, parameter_fingerprint, 32);
result->result_status = result_status;
result->match_count = match_count;
if (match_asset_sha256) {
result->has_match_asset = true;
memcpy(result->match_asset_sha256, match_asset_sha256, LARDON3D_PROJECT_DB_SHA256_SIZE);
} else {
result->has_match_asset = false;
memset(result->match_asset_sha256, 0, LARDON3D_PROJECT_DB_SHA256_SIZE);
}
if (match_asset_path && match_asset_path[0] != '\0') {
size_t plen = strlen(match_asset_path);
if (plen < LARDON3D_PROJECT_DB_PATH_CAPACITY) {
memcpy(result->match_asset_path, match_asset_path, plen + 1);
} else {
memcpy(result->match_asset_path, match_asset_path,
LARDON3D_PROJECT_DB_PATH_CAPACITY - 1);
result->match_asset_path[LARDON3D_PROJECT_DB_PATH_CAPACITY - 1] = '\0';
}
} else {
result->match_asset_path[0] = '\0';
}
result->match_asset_size_bytes = match_asset_size_bytes;
result->created_at = created_at;
}
(void)pthread_mutex_unlock(&database->mutex);
return db_result;
}
Lardon3DProjectDbResult lardon3d_project_db_load_match_result(
Lardon3DProjectDb *database, uint64_t match_result_id,
Lardon3DProjectDbMatchResult *result) {
if (!database || !valid_catalog_id(match_result_id) || !result) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
memset(result, 0, sizeof(*result));
(void)pthread_mutex_lock(&database->mutex);
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult db_result = prepare(
database,
"SELECT "
"match_result_id,candidate_pair_id,feature_set_id_a,feature_set_id_b,matcher_kind,"
"matcher_version,parameter_fingerprint,result_status,match_count,"
"match_asset_sha256,match_asset_path,match_asset_size_bytes,"
"created_at FROM match_results WHERE match_result_id=?1",
&statement);
if (db_result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)match_result_id);
int code = sqlite3_step(statement);
if (code == SQLITE_DONE) {
db_result = LARDON3D_PROJECT_DB_NOT_FOUND;
} else if (code != SQLITE_ROW || !read_match_result(statement, result)) {
db_result = LARDON3D_PROJECT_DB_CORRUPT;
}
(void)sqlite3_finalize(statement);
}
(void)pthread_mutex_unlock(&database->mutex);
return db_result;
}
Lardon3DProjectDbResult lardon3d_project_db_repair_match_result(
Lardon3DProjectDb *database, uint64_t match_result_id, int result_status,
uint32_t match_count, const unsigned char *match_asset_sha256,
const char *match_asset_path, uint64_t match_asset_size_bytes,
Lardon3DProjectDbMatchResult *result) {
if (!database || !valid_catalog_id(match_result_id) || result_status < 0 ||
result_status > 1 || match_count > 8192 || !result ||
(result_status == LARDON3D_MATCH_RESULT_STATUS_NO_MATCH &&
(match_count != 0 || match_asset_sha256 || match_asset_path || match_asset_size_bytes != 0)) ||
(result_status == LARDON3D_MATCH_RESULT_STATUS_MATCHED &&
(match_count == 0 || !match_asset_sha256 || !match_asset_path ||
match_asset_size_bytes == 0)) ||
(match_asset_path &&
!bounded_text(match_asset_path, LARDON3D_PROJECT_DB_PATH_CAPACITY, false))) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
memset(result, 0, sizeof(*result));
(void)pthread_mutex_lock(&database->mutex);
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult db_result = prepare(
database,
"UPDATE match_results SET result_status=?2,match_count=?3,match_asset_sha256=?4,"
"match_asset_path=?5,match_asset_size_bytes=?6 WHERE match_result_id=?1",
&statement);
if (db_result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)match_result_id);
(void)sqlite3_bind_int(statement, 2, result_status);
(void)sqlite3_bind_int64(statement, 3, match_count);
if (match_asset_sha256) {
(void)sqlite3_bind_blob(statement, 4, match_asset_sha256, 32, SQLITE_TRANSIENT);
(void)sqlite3_bind_text(statement, 5, match_asset_path, -1, SQLITE_TRANSIENT);
(void)sqlite3_bind_int64(statement, 6, (sqlite3_int64)match_asset_size_bytes);
} else {
(void)sqlite3_bind_null(statement, 4);
(void)sqlite3_bind_null(statement, 5);
(void)sqlite3_bind_null(statement, 6);
}
db_result = step_done(database, statement, "repair match result");
}
if (db_result == LARDON3D_PROJECT_DB_OK && sqlite3_changes(database->connection) != 1) {
db_result = LARDON3D_PROJECT_DB_NOT_FOUND;
}
(void)pthread_mutex_unlock(&database->mutex);
if (db_result != LARDON3D_PROJECT_DB_OK) return db_result;
return lardon3d_project_db_load_match_result(database, match_result_id, result);
}
Lardon3DProjectDbResult lardon3d_project_db_find_match_result(
Lardon3DProjectDb *database, uint64_t candidate_pair_id,
uint64_t feature_set_id_a, uint64_t feature_set_id_b,
const char *matcher_kind, uint32_t matcher_version,
const unsigned char parameter_fingerprint[LARDON3D_PROJECT_DB_SHA256_SIZE],
Lardon3DProjectDbMatchResult *result) {
if (!database || !valid_catalog_id(candidate_pair_id) ||
!valid_catalog_id(feature_set_id_a) || !valid_catalog_id(feature_set_id_b) ||
!bounded_text(matcher_kind, LARDON3D_PROJECT_DB_KIND_CAPACITY, false) ||
matcher_version == 0 || !parameter_fingerprint || !result) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
memset(result, 0, sizeof(*result));
(void)pthread_mutex_lock(&database->mutex);
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult db_result = prepare(
database,
"SELECT "
"match_result_id,candidate_pair_id,feature_set_id_a,feature_set_id_b,matcher_kind,"
"matcher_version,parameter_fingerprint,result_status,match_count,"
"match_asset_sha256,match_asset_path,match_asset_size_bytes,"
"created_at FROM match_results WHERE candidate_pair_id=?1 AND feature_set_id_a=?2 AND "
"feature_set_id_b=?3 AND matcher_kind=?4 AND matcher_version=?5 AND "
"parameter_fingerprint=?6",
&statement);
if (db_result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)candidate_pair_id);
(void)sqlite3_bind_int64(statement, 2, (sqlite3_int64)feature_set_id_a);
(void)sqlite3_bind_int64(statement, 3, (sqlite3_int64)feature_set_id_b);
(void)sqlite3_bind_text(statement, 4, matcher_kind, -1, SQLITE_TRANSIENT);
(void)sqlite3_bind_int64(statement, 5, matcher_version);
(void)sqlite3_bind_blob(statement, 6, parameter_fingerprint, 32, SQLITE_TRANSIENT);
int code = sqlite3_step(statement);
if (code == SQLITE_DONE) {
db_result = LARDON3D_PROJECT_DB_NOT_FOUND;
} else if (code != SQLITE_ROW || !read_match_result(statement, result)) {
db_result = LARDON3D_PROJECT_DB_CORRUPT;
}
(void)sqlite3_finalize(statement);
}
(void)pthread_mutex_unlock(&database->mutex);
return db_result;
}
Lardon3DProjectDbResult lardon3d_project_db_list_match_results(
Lardon3DProjectDb *database, uint64_t after_match_result_id,
Lardon3DProjectDbMatchResult *results, size_t capacity, size_t *count) {
if (count) {
*count = 0;
}
if (!database || !results || !count || after_match_result_id > INT64_MAX || capacity == 0 ||
capacity > LARDON3D_PROJECT_DB_MATCH_RESULT_PAGE_MAX) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
(void)pthread_mutex_lock(&database->mutex);
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult db_result = prepare(
database,
"SELECT "
"match_result_id,candidate_pair_id,feature_set_id_a,feature_set_id_b,matcher_kind,"
"matcher_version,parameter_fingerprint,result_status,match_count,"
"match_asset_sha256,match_asset_path,match_asset_size_bytes,"
"created_at FROM match_results WHERE match_result_id>?1 ORDER BY match_result_id LIMIT ?2",
&statement);
if (db_result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)after_match_result_id);
(void)sqlite3_bind_int64(statement, 2, (sqlite3_int64)capacity);
int code = SQLITE_DONE;
while (*count < capacity && (code = sqlite3_step(statement)) == SQLITE_ROW) {
if (!read_match_result(statement, &results[*count])) {
db_result = LARDON3D_PROJECT_DB_CORRUPT;
break;
}
++*count;
}
if (db_result == LARDON3D_PROJECT_DB_OK && *count < capacity && code != SQLITE_DONE) {
db_result = sqlite_result(database, code, "list match results");
}
(void)sqlite3_finalize(statement);
}
(void)pthread_mutex_unlock(&database->mutex);
return db_result;
}
static size_t geometric_mask_size(uint32_t match_count) {
return ((size_t)match_count + 7U) / 8U;
}
static uint32_t geometric_mask_popcount(const unsigned char *mask, size_t size) {
uint32_t count = 0;
for (size_t byte_index = 0; byte_index < size; ++byte_index) {
unsigned int value = mask[byte_index];
while (value != 0U) {
value &= value - 1U;
++count;
}
}
return count;
}
static bool valid_geometric_mask(const unsigned char *mask, size_t size, uint32_t match_count,
uint32_t inlier_count) {
size_t expected_size = geometric_mask_size(match_count);
if (!mask || size != expected_size || expected_size == 0 ||
expected_size > LARDON3D_PROJECT_DB_INLIER_MASK_MAX || inlier_count > match_count) {
return false;
}
unsigned int used_bits = match_count % 8U;
if (used_bits != 0U) {
unsigned int padding_mask = 0xffU << used_bits;
if (((unsigned int)mask[size - 1] & padding_mask) != 0U) {
return false;
}
}
return geometric_mask_popcount(mask, size) == inlier_count;
}
static bool valid_geometric_model(Lardon3DGeometricVerificationStatus status,
const double *model) {
if (status == LARDON3D_GEOMETRIC_REJECTED) {
return model == NULL;
}
if (status != LARDON3D_GEOMETRIC_VERIFIED || !model) {
return false;
}
for (size_t index = 0; index < LARDON3D_PROJECT_DB_FUNDAMENTAL_COEFFICIENTS; ++index) {
if (!isfinite(model[index])) {
return false;
}
}
return true;
}
static const char geometric_result_columns[] =
"g.geometric_verification_result_id,g.match_result_id,g.verifier_kind,"
"g.verifier_version,g.parameter_fingerprint,g.status,g.inlier_count,g.inlier_mask,"
"g.model_m00,g.model_m01,g.model_m02,g.model_m10,g.model_m11,g.model_m12,"
"g.model_m20,g.model_m21,g.model_m22,g.created_at,m.result_status,m.match_count ";
static bool read_geometric_result(sqlite3_stmt *statement,
Lardon3DProjectDbGeometricVerificationResult *result) {
if (!statement || !result) {
return false;
}
for (int column = 0; column <= 7; ++column) {
int expected_type = column == 4 || column == 7 ? SQLITE_BLOB : SQLITE_INTEGER;
if (sqlite3_column_type(statement, column) != expected_type) {
return false;
}
}
if (sqlite3_column_type(statement, 17) != SQLITE_INTEGER ||
sqlite3_column_type(statement, 18) != SQLITE_INTEGER ||
sqlite3_column_type(statement, 19) != SQLITE_INTEGER) {
return false;
}
sqlite3_int64 id = sqlite3_column_int64(statement, 0);
sqlite3_int64 parent_id = sqlite3_column_int64(statement, 1);
sqlite3_int64 kind = sqlite3_column_int64(statement, 2);
sqlite3_int64 version = sqlite3_column_int64(statement, 3);
sqlite3_int64 status = sqlite3_column_int64(statement, 5);
sqlite3_int64 inlier_count = sqlite3_column_int64(statement, 6);
sqlite3_int64 created_at = sqlite3_column_int64(statement, 17);
sqlite3_int64 parent_status = sqlite3_column_int64(statement, 18);
sqlite3_int64 match_count = sqlite3_column_int64(statement, 19);
int fingerprint_size = sqlite3_column_bytes(statement, 4);
int mask_size = sqlite3_column_bytes(statement, 7);
const void *fingerprint = sqlite3_column_blob(statement, 4);
const void *mask = sqlite3_column_blob(statement, 7);
if (id <= 0 || parent_id <= 0 || kind != LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL ||
version <= 0 || version > UINT32_MAX ||
(status != LARDON3D_GEOMETRIC_REJECTED && status != LARDON3D_GEOMETRIC_VERIFIED) ||
inlier_count < 0 || inlier_count > 8192 || created_at < 0 ||
parent_status != LARDON3D_MATCH_RESULT_STATUS_MATCHED || match_count <= 0 ||
match_count > 8192 || fingerprint_size != LARDON3D_PROJECT_DB_SHA256_SIZE ||
!fingerprint || mask_size <= 0 || mask_size > LARDON3D_PROJECT_DB_INLIER_MASK_MAX || !mask ||
!valid_geometric_mask(mask, (size_t)mask_size, (uint32_t)match_count,
(uint32_t)inlier_count)) {
return false;
}
double model[LARDON3D_PROJECT_DB_FUNDAMENTAL_COEFFICIENTS] = {0};
bool has_model = status == LARDON3D_GEOMETRIC_VERIFIED;
for (int index = 0; index < LARDON3D_PROJECT_DB_FUNDAMENTAL_COEFFICIENTS; ++index) {
int type = sqlite3_column_type(statement, 8 + index);
if ((!has_model && type != SQLITE_NULL) ||
(has_model && type != SQLITE_FLOAT && type != SQLITE_INTEGER)) {
return false;
}
if (has_model) {
model[index] = sqlite3_column_double(statement, 8 + index);
if (!isfinite(model[index])) {
return false;
}
}
}
memset(result, 0, sizeof(*result));
result->geometric_verification_result_id = (uint64_t)id;
result->match_result_id = (uint64_t)parent_id;
result->verifier_kind = (Lardon3DGeometricVerifierKind)kind;
result->verifier_version = (uint32_t)version;
memcpy(result->parameter_fingerprint, fingerprint, LARDON3D_PROJECT_DB_SHA256_SIZE);
result->status = (Lardon3DGeometricVerificationStatus)status;
result->inlier_count = (uint32_t)inlier_count;
result->inlier_mask_size = (size_t)mask_size;
memcpy(result->inlier_mask, mask, (size_t)mask_size);
result->has_model = has_model;
memcpy(result->model, model, sizeof(model));
result->created_at = created_at;
return true;
}
static Lardon3DProjectDbResult read_single_geometric_result(
Lardon3DProjectDb *database, sqlite3_stmt *statement,
Lardon3DProjectDbGeometricVerificationResult *result) {
int code = sqlite3_step(statement);
if (code == SQLITE_DONE) {
(void)sqlite3_finalize(statement);
return LARDON3D_PROJECT_DB_NOT_FOUND;
}
if (code != SQLITE_ROW) {
Lardon3DProjectDbResult db_result = sqlite_result(database, code, "read geometric result");
(void)sqlite3_finalize(statement);
return db_result;
}
bool valid = read_geometric_result(statement, result);
code = sqlite3_step(statement);
(void)sqlite3_finalize(statement);
return valid && code == SQLITE_DONE ? LARDON3D_PROJECT_DB_OK : LARDON3D_PROJECT_DB_CORRUPT;
}
Lardon3DProjectDbResult lardon3d_project_db_create_geometric_verification_result(
Lardon3DProjectDb *database, uint64_t match_result_id,
Lardon3DGeometricVerifierKind verifier_kind, uint32_t verifier_version,
const unsigned char parameter_fingerprint[LARDON3D_PROJECT_DB_SHA256_SIZE],
Lardon3DGeometricVerificationStatus status, uint32_t inlier_count,
const unsigned char *inlier_mask, size_t inlier_mask_size, const double *model,
int64_t created_at, Lardon3DProjectDbGeometricVerificationResult *result) {
if (!database || !valid_catalog_id(match_result_id) ||
verifier_kind != LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL || verifier_version == 0 ||
!parameter_fingerprint || inlier_count > 8192 || !inlier_mask || inlier_mask_size == 0 ||
inlier_mask_size > LARDON3D_PROJECT_DB_INLIER_MASK_MAX ||
!valid_geometric_model(status, model) || created_at < 0 || !result) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
memset(result, 0, sizeof(*result));
(void)pthread_mutex_lock(&database->mutex);
Lardon3DProjectDbResult db_result =
execute(database, "BEGIN IMMEDIATE", "begin geometric result creation");
sqlite3_stmt *statement = NULL;
uint32_t match_count = 0;
if (db_result == LARDON3D_PROJECT_DB_OK) {
db_result = prepare(database,
"SELECT result_status,match_count FROM match_results "
"WHERE match_result_id=?1",
&statement);
}
if (db_result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)match_result_id);
int code = sqlite3_step(statement);
if (code == SQLITE_DONE) {
db_result = LARDON3D_PROJECT_DB_NOT_FOUND;
} else if (code != SQLITE_ROW || sqlite3_column_type(statement, 0) != SQLITE_INTEGER ||
sqlite3_column_type(statement, 1) != SQLITE_INTEGER) {
db_result = LARDON3D_PROJECT_DB_CORRUPT;
} else {
sqlite3_int64 parent_status = sqlite3_column_int64(statement, 0);
sqlite3_int64 parent_count = sqlite3_column_int64(statement, 1);
if (parent_status != LARDON3D_MATCH_RESULT_STATUS_MATCHED || parent_count <= 0 ||
parent_count > 8192) {
db_result = LARDON3D_PROJECT_DB_CONSTRAINT;
} else {
match_count = (uint32_t)parent_count;
}
}
(void)sqlite3_finalize(statement);
statement = NULL;
}
if (db_result == LARDON3D_PROJECT_DB_OK &&
!valid_geometric_mask(inlier_mask, inlier_mask_size, match_count, inlier_count)) {
db_result = LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
if (db_result == LARDON3D_PROJECT_DB_OK) {
db_result = prepare(
database,
"INSERT INTO geometric_verification_results("
"match_result_id,verifier_kind,verifier_version,parameter_fingerprint,status,"
"inlier_count,inlier_mask,model_m00,model_m01,model_m02,model_m10,model_m11,model_m12,"
"model_m20,model_m21,model_m22,created_at) "
"VALUES(?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14,?15,?16,?17)",
&statement);
}
if (db_result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)match_result_id);
(void)sqlite3_bind_int(statement, 2, (int)verifier_kind);
(void)sqlite3_bind_int64(statement, 3, (sqlite3_int64)verifier_version);
(void)sqlite3_bind_blob(statement, 4, parameter_fingerprint,
LARDON3D_PROJECT_DB_SHA256_SIZE, SQLITE_TRANSIENT);
(void)sqlite3_bind_int(statement, 5, (int)status);
(void)sqlite3_bind_int64(statement, 6, (sqlite3_int64)inlier_count);
(void)sqlite3_bind_blob(statement, 7, inlier_mask, (int)inlier_mask_size, SQLITE_TRANSIENT);
for (int index = 0; index < LARDON3D_PROJECT_DB_FUNDAMENTAL_COEFFICIENTS; ++index) {
if (model) {
(void)sqlite3_bind_double(statement, 8 + index, model[index]);
} else {
(void)sqlite3_bind_null(statement, 8 + index);
}
}
(void)sqlite3_bind_int64(statement, 17, created_at);
db_result = step_done(database, statement, "create geometric result");
statement = NULL;
}
uint64_t result_id = 0;
if (db_result == LARDON3D_PROJECT_DB_OK) {
sqlite3_int64 inserted_id = sqlite3_last_insert_rowid(database->connection);
if (inserted_id <= 0) {
db_result = LARDON3D_PROJECT_DB_CORRUPT;
} else {
result_id = (uint64_t)inserted_id;
db_result = execute(database, "COMMIT", "commit geometric result creation");
}
}
if (db_result != LARDON3D_PROJECT_DB_OK) {
(void)execute(database, "ROLLBACK", "rollback geometric result creation");
}
(void)pthread_mutex_unlock(&database->mutex);
if (db_result != LARDON3D_PROJECT_DB_OK) {
return db_result;
}
result->geometric_verification_result_id = result_id;
result->match_result_id = match_result_id;
result->verifier_kind = verifier_kind;
result->verifier_version = verifier_version;
memcpy(result->parameter_fingerprint, parameter_fingerprint,
LARDON3D_PROJECT_DB_SHA256_SIZE);
result->status = status;
result->inlier_count = inlier_count;
result->inlier_mask_size = inlier_mask_size;
memcpy(result->inlier_mask, inlier_mask, inlier_mask_size);
result->has_model = model != NULL;
if (model) {
memcpy(result->model, model, sizeof(result->model));
}
result->created_at = created_at;
return LARDON3D_PROJECT_DB_OK;
}
Lardon3DProjectDbResult lardon3d_project_db_load_geometric_verification_result(
Lardon3DProjectDb *database, uint64_t geometric_verification_result_id,
Lardon3DProjectDbGeometricVerificationResult *result) {
if (!database || !valid_catalog_id(geometric_verification_result_id) || !result) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
memset(result, 0, sizeof(*result));
(void)pthread_mutex_lock(&database->mutex);
sqlite3_stmt *statement = NULL;
char sql[1024];
(void)snprintf(sql, sizeof(sql), "SELECT %sFROM geometric_verification_results g "
"JOIN match_results m ON m.match_result_id=g.match_result_id "
"WHERE g.geometric_verification_result_id=?1",
geometric_result_columns);
Lardon3DProjectDbResult db_result = prepare(database, sql, &statement);
if (db_result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)geometric_verification_result_id);
db_result = read_single_geometric_result(database, statement, result);
}
(void)pthread_mutex_unlock(&database->mutex);
return db_result;
}
Lardon3DProjectDbResult lardon3d_project_db_find_geometric_verification_result(
Lardon3DProjectDb *database, uint64_t match_result_id,
Lardon3DGeometricVerifierKind verifier_kind, uint32_t verifier_version,
const unsigned char parameter_fingerprint[LARDON3D_PROJECT_DB_SHA256_SIZE],
Lardon3DProjectDbGeometricVerificationResult *result) {
if (!database || !valid_catalog_id(match_result_id) ||
verifier_kind != LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL || verifier_version == 0 ||
!parameter_fingerprint || !result) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
memset(result, 0, sizeof(*result));
(void)pthread_mutex_lock(&database->mutex);
sqlite3_stmt *statement = NULL;
char sql[1280];
(void)snprintf(sql, sizeof(sql), "SELECT %sFROM geometric_verification_results g "
"JOIN match_results m ON m.match_result_id=g.match_result_id "
"WHERE g.match_result_id=?1 AND g.verifier_kind=?2 AND "
"g.verifier_version=?3 AND g.parameter_fingerprint=?4",
geometric_result_columns);
Lardon3DProjectDbResult db_result = prepare(database, sql, &statement);
if (db_result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)match_result_id);
(void)sqlite3_bind_int(statement, 2, (int)verifier_kind);
(void)sqlite3_bind_int64(statement, 3, (sqlite3_int64)verifier_version);
(void)sqlite3_bind_blob(statement, 4, parameter_fingerprint,
LARDON3D_PROJECT_DB_SHA256_SIZE, SQLITE_TRANSIENT);
db_result = read_single_geometric_result(database, statement, result);
}
(void)pthread_mutex_unlock(&database->mutex);
return db_result;
}
Lardon3DProjectDbResult lardon3d_project_db_list_geometric_verification_results(
Lardon3DProjectDb *database, uint64_t match_result_id,
uint64_t after_geometric_verification_result_id,
Lardon3DProjectDbGeometricVerificationResult *results, size_t capacity, size_t *count) {
if (count) {
*count = 0;
}
if (!database || !valid_catalog_id(match_result_id) ||
after_geometric_verification_result_id > INT64_MAX || !results || !count || capacity == 0 ||
capacity > LARDON3D_PROJECT_DB_GEOMETRIC_RESULT_PAGE_MAX) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
(void)pthread_mutex_lock(&database->mutex);
sqlite3_stmt *statement = NULL;
char sql[1280];
(void)snprintf(sql, sizeof(sql), "SELECT %sFROM geometric_verification_results g "
"JOIN match_results m ON m.match_result_id=g.match_result_id "
"WHERE g.match_result_id=?1 AND "
"g.geometric_verification_result_id>?2 "
"ORDER BY g.geometric_verification_result_id LIMIT ?3",
geometric_result_columns);
Lardon3DProjectDbResult db_result = prepare(database, sql, &statement);
if (db_result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)match_result_id);
(void)sqlite3_bind_int64(statement, 2,
(sqlite3_int64)after_geometric_verification_result_id);
(void)sqlite3_bind_int64(statement, 3, (sqlite3_int64)capacity);
int code = SQLITE_DONE;
while (*count < capacity && (code = sqlite3_step(statement)) == SQLITE_ROW) {
if (!read_geometric_result(statement, &results[*count])) {
db_result = LARDON3D_PROJECT_DB_CORRUPT;
break;
}
++*count;
}
if (db_result == LARDON3D_PROJECT_DB_OK && *count < capacity && code != SQLITE_DONE) {
db_result = sqlite_result(database, code, "list geometric results");
}
(void)sqlite3_finalize(statement);
}
(void)pthread_mutex_unlock(&database->mutex);
return db_result;
}
/* Track persistence is deliberately kept as a small, self-contained API. The
* caller owns input arrays; loaded observation arrays are owned by the Track
* value until lardon3d_project_db_free_track(). */
static bool valid_track_identity(const Lardon3DProjectDbTrackSet *set) {
return set && set->builder_kind[0] != '\0' && strlen(set->builder_kind) < 65 &&
set->builder_version > 0 && set->verifier_kind > 0 && set->verifier_version > 0 &&
set->gvr_count > 0 &&
set->created_at >= 0;
}
static Lardon3DProjectDbResult read_track_set(sqlite3_stmt *statement,
Lardon3DProjectDbTrackSet *set) {
sqlite3_int64 id = sqlite3_column_int64(statement, 0);
sqlite3_int64 builder_version = sqlite3_column_int64(statement, 2);
sqlite3_int64 verifier_kind = sqlite3_column_int64(statement, 4);
sqlite3_int64 verifier_version = sqlite3_column_int64(statement, 5);
sqlite3_int64 gvr_count = sqlite3_column_int64(statement, 8);
sqlite3_int64 track_count = sqlite3_column_int64(statement, 9);
const unsigned char *builder = sqlite3_column_text(statement, 1);
const void *parameter = sqlite3_column_blob(statement, 3);
const void *verifier = sqlite3_column_blob(statement, 6);
const void *scope = sqlite3_column_blob(statement, 7);
if (id <= 0 || builder_version <= 0 || verifier_kind <= 0 || verifier_version <= 0 ||
gvr_count <= 0 || track_count < 0 || sqlite3_column_bytes(statement, 3) != 32 ||
sqlite3_column_bytes(statement, 6) != 32 || sqlite3_column_bytes(statement, 7) != 32 ||
!builder || !parameter || !verifier || !scope || strlen((const char *)builder) >= 65) {
return LARDON3D_PROJECT_DB_CORRUPT;
}
memset(set, 0, sizeof(*set));
set->track_set_id = (uint64_t)id;
(void)snprintf(set->builder_kind, sizeof(set->builder_kind), "%s", (const char *)builder);
set->builder_version = (uint32_t)builder_version;
memcpy(set->parameter_fingerprint, parameter, 32);
set->verifier_kind = (int)verifier_kind;
set->verifier_version = (uint32_t)verifier_version;
memcpy(set->verifier_fingerprint, verifier, 32);
memcpy(set->input_scope_hash, scope, 32);
set->gvr_count = (uint64_t)gvr_count;
set->track_count = (uint64_t)track_count;
set->created_at = sqlite3_column_int64(statement, 10);
return set->created_at >= 0 ? LARDON3D_PROJECT_DB_OK : LARDON3D_PROJECT_DB_CORRUPT;
}
void lardon3d_project_db_free_track(Lardon3DProjectDbTrack *track) {
if (!track) {
return;
}
free(track->observations);
memset(track, 0, sizeof(*track));
}
static void bind_track_identity(sqlite3_stmt *statement,
const Lardon3DProjectDbTrackSet *set, int first) {
(void)sqlite3_bind_text(statement, first, set->builder_kind, -1, SQLITE_TRANSIENT);
(void)sqlite3_bind_int64(statement, first + 1, set->builder_version);
(void)sqlite3_bind_blob(statement, first + 2, set->parameter_fingerprint, 32, SQLITE_TRANSIENT);
(void)sqlite3_bind_int(statement, first + 3, set->verifier_kind);
(void)sqlite3_bind_int64(statement, first + 4, set->verifier_version);
(void)sqlite3_bind_blob(statement, first + 5, set->verifier_fingerprint, 32, SQLITE_TRANSIENT);
(void)sqlite3_bind_blob(statement, first + 6, set->input_scope_hash, 32, SQLITE_TRANSIENT);
}
static Lardon3DProjectDbResult read_track_set_by_statement(
Lardon3DProjectDb *database, sqlite3_stmt *statement, Lardon3DProjectDbTrackSet *set) {
int code = sqlite3_step(statement);
Lardon3DProjectDbResult result = code == SQLITE_ROW ? read_track_set(statement, set)
: LARDON3D_PROJECT_DB_NOT_FOUND;
if (code != SQLITE_ROW && code != SQLITE_DONE) {
result = sqlite_result(database, code, "read track set");
}
(void)sqlite3_finalize(statement);
return result;
}
Lardon3DProjectDbResult lardon3d_project_db_load_track_set(
Lardon3DProjectDb *database, uint64_t track_set_id, Lardon3DProjectDbTrackSet *track_set) {
if (!database || !valid_catalog_id(track_set_id) || !track_set) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
memset(track_set, 0, sizeof(*track_set));
(void)pthread_mutex_lock(&database->mutex);
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result = prepare(
database,
"SELECT track_set_id,builder_kind,builder_version,parameter_fingerprint,verifier_kind,"
"verifier_version,verifier_fingerprint,input_scope_hash,gvr_count,track_count,created_at "
"FROM track_sets WHERE track_set_id=?1",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)track_set_id);
result = read_track_set_by_statement(database, statement, track_set);
}
(void)pthread_mutex_unlock(&database->mutex);
return result;
}
Lardon3DProjectDbResult lardon3d_project_db_find_track_set(
Lardon3DProjectDb *database, const Lardon3DProjectDbTrackSet *identity,
Lardon3DProjectDbTrackSet *track_set) {
if (!database || !valid_track_identity(identity) || !track_set) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
memset(track_set, 0, sizeof(*track_set));
(void)pthread_mutex_lock(&database->mutex);
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result = prepare(
database,
"SELECT track_set_id,builder_kind,builder_version,parameter_fingerprint,verifier_kind,"
"verifier_version,verifier_fingerprint,input_scope_hash,gvr_count,track_count,created_at "
"FROM track_sets WHERE builder_kind=?1 AND builder_version=?2 AND parameter_fingerprint=?3 "
"AND verifier_kind=?4 AND verifier_version=?5 AND verifier_fingerprint=?6 "
"AND input_scope_hash=?7",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
bind_track_identity(statement, identity, 1);
result = read_track_set_by_statement(database, statement, track_set);
}
(void)pthread_mutex_unlock(&database->mutex);
return result;
}
Lardon3DProjectDbResult lardon3d_project_db_list_track_sets(
Lardon3DProjectDb *database, uint64_t after_track_set_id, Lardon3DProjectDbTrackSet *track_sets,
size_t capacity, size_t *count) {
if (!database || !track_sets || !count || capacity == 0 ||
capacity > LARDON3D_PROJECT_DB_TRACK_PAGE_MAX) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
*count = 0;
(void)pthread_mutex_lock(&database->mutex);
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result = prepare(
database,
"SELECT track_set_id,builder_kind,builder_version,parameter_fingerprint,verifier_kind,"
"verifier_version,verifier_fingerprint,input_scope_hash,gvr_count,track_count,created_at "
"FROM track_sets WHERE track_set_id>?1 ORDER BY track_set_id LIMIT ?2",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)after_track_set_id);
(void)sqlite3_bind_int64(statement, 2, (sqlite3_int64)capacity);
int code;
while ((code = sqlite3_step(statement)) == SQLITE_ROW) {
result = read_track_set(statement, &track_sets[*count]);
if (result != LARDON3D_PROJECT_DB_OK) {
break;
}
++*count;
}
if (result == LARDON3D_PROJECT_DB_OK && code == SQLITE_DONE) {
result = LARDON3D_PROJECT_DB_OK;
} else if (result == LARDON3D_PROJECT_DB_OK) {
result = sqlite_result(database, code, "list track sets");
}
(void)sqlite3_finalize(statement);
}
(void)pthread_mutex_unlock(&database->mutex);
return result;
}
static Lardon3DProjectDbResult validate_track_observations_locked(
Lardon3DProjectDb *database, const Lardon3DProjectDbTrack *tracks, size_t track_count) {
for (size_t track_index = 0; track_index < track_count; ++track_index) {
const Lardon3DProjectDbTrack *track = &tracks[track_index];
if (!track->observations || track->observation_count < 2) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
for (uint32_t observation_index = 0; observation_index < track->observation_count;
++observation_index) {
const Lardon3DProjectDbTrackObservation *observation = &track->observations[observation_index];
if (observation->position_in_track != observation_index || observation->feature_set_id == 0) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result = prepare(
database, "SELECT image_id,feature_count FROM feature_sets WHERE feature_set_id=?1",
&statement);
if (result != LARDON3D_PROJECT_DB_OK) {
return result;
}
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)observation->feature_set_id);
int code = sqlite3_step(statement);
sqlite3_int64 image_id = code == SQLITE_ROW ? sqlite3_column_int64(statement, 0) : 0;
sqlite3_int64 feature_count = code == SQLITE_ROW ? sqlite3_column_int64(statement, 1) : 0;
(void)sqlite3_finalize(statement);
if (code != SQLITE_ROW) {
return code == SQLITE_DONE ? LARDON3D_PROJECT_DB_NOT_FOUND
: sqlite_result(database, code, "validate track feature set");
}
if (image_id <= 0 || feature_count < 0 || (uint64_t)observation->feature_index >=
(uint64_t)feature_count) {
return LARDON3D_PROJECT_DB_CONSTRAINT;
}
for (uint32_t prior = 0; prior < observation_index; ++prior) {
const Lardon3DProjectDbTrackObservation *old = &track->observations[prior];
if (old->feature_set_id == observation->feature_set_id &&
old->feature_index == observation->feature_index) {
return LARDON3D_PROJECT_DB_CONSTRAINT;
}
sqlite3_stmt *image_statement = NULL;
result = prepare(database, "SELECT image_id FROM feature_sets WHERE feature_set_id=?1",
&image_statement);
if (result != LARDON3D_PROJECT_DB_OK) {
return result;
}
(void)sqlite3_bind_int64(image_statement, 1, (sqlite3_int64)old->feature_set_id);
code = sqlite3_step(image_statement);
sqlite3_int64 old_image = code == SQLITE_ROW ? sqlite3_column_int64(image_statement, 0) : 0;
(void)sqlite3_finalize(image_statement);
if (code != SQLITE_ROW) {
return LARDON3D_PROJECT_DB_CORRUPT;
}
if (old_image == image_id) {
return LARDON3D_PROJECT_DB_CONSTRAINT;
}
}
}
}
for (size_t first = 0; first < track_count; ++first) {
for (size_t second = first + 1; second < track_count; ++second) {
for (uint32_t a = 0; a < tracks[first].observation_count; ++a) {
for (uint32_t b = 0; b < tracks[second].observation_count; ++b) {
if (tracks[first].observations[a].feature_set_id == tracks[second].observations[b].feature_set_id &&
tracks[first].observations[a].feature_index == tracks[second].observations[b].feature_index) {
return LARDON3D_PROJECT_DB_CONSTRAINT;
}
}
}
}
}
return LARDON3D_PROJECT_DB_OK;
}
Lardon3DProjectDbResult lardon3d_project_db_create_track_set(
Lardon3DProjectDb *database, const Lardon3DProjectDbTrackSet *configuration,
const Lardon3DProjectDbTrack *tracks, size_t track_count,
Lardon3DProjectDbTrackSet *published) {
if (!database || !valid_track_identity(configuration) || !published ||
(track_count > 0 && !tracks)) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
if (configuration->track_count != 0 && configuration->track_count != track_count) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
memset(published, 0, sizeof(*published));
(void)pthread_mutex_lock(&database->mutex);
Lardon3DProjectDbResult result = execute(database, "BEGIN IMMEDIATE", "begin track set publish");
int64_t publication_time = (int64_t)time(NULL);
if (result == LARDON3D_PROJECT_DB_OK && publication_time < 0) {
result = LARDON3D_PROJECT_DB_IO_ERROR;
}
bool reused = false;
sqlite3_stmt *statement = NULL;
if (result == LARDON3D_PROJECT_DB_OK) {
result = prepare(database,
"SELECT track_set_id,builder_kind,builder_version,parameter_fingerprint,"
"verifier_kind,verifier_version,verifier_fingerprint,input_scope_hash,gvr_count,"
"track_count,created_at FROM track_sets WHERE builder_kind=?1 AND "
"builder_version=?2 AND parameter_fingerprint=?3 AND verifier_kind=?4 AND "
"verifier_version=?5 AND verifier_fingerprint=?6 AND input_scope_hash=?7",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
bind_track_identity(statement, configuration, 1);
int code = sqlite3_step(statement);
if (code == SQLITE_ROW) {
result = read_track_set(statement, published);
if (result == LARDON3D_PROJECT_DB_OK &&
published->gvr_count != configuration->gvr_count) {
result = LARDON3D_PROJECT_DB_CONSTRAINT;
}
reused = result == LARDON3D_PROJECT_DB_OK;
} else if (code != SQLITE_DONE) {
result = sqlite_result(database, code, "find reusable track set");
} else {
result = validate_track_observations_locked(database, tracks, track_count);
}
(void)sqlite3_finalize(statement);
statement = NULL;
}
}
if (result == LARDON3D_PROJECT_DB_OK && published->track_set_id == 0) {
result = prepare(database,
"INSERT INTO track_sets(builder_kind,builder_version,parameter_fingerprint,"
"verifier_kind,verifier_version,verifier_fingerprint,input_scope_hash,gvr_count,"
"track_count,created_at) VALUES(?1,?2,?3,?4,?5,?6,?7,?8,?9,?10)",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
bind_track_identity(statement, configuration, 1);
(void)sqlite3_bind_int64(statement, 8, (sqlite3_int64)configuration->gvr_count);
(void)sqlite3_bind_int64(statement, 9, (sqlite3_int64)track_count);
(void)sqlite3_bind_int64(statement, 10, publication_time);
result = step_done(database, statement, "insert track set");
statement = NULL;
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_int64 id = sqlite3_last_insert_rowid(database->connection);
if (id <= 0) {
result = LARDON3D_PROJECT_DB_CORRUPT;
} else {
published->track_set_id = (uint64_t)id;
}
}
}
for (size_t index = 0; result == LARDON3D_PROJECT_DB_OK && index < track_count; ++index) {
const Lardon3DProjectDbTrack *track = &tracks[index];
result = prepare(database,
"INSERT INTO tracks(track_set_id,observation_count) VALUES(?1,?2)",
&statement);
if (result != LARDON3D_PROJECT_DB_OK) {
break;
}
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)published->track_set_id);
(void)sqlite3_bind_int64(statement, 2, track->observation_count);
result = step_done(database, statement, "insert track");
statement = NULL;
if (result != LARDON3D_PROJECT_DB_OK) {
break;
}
sqlite3_int64 track_id = sqlite3_last_insert_rowid(database->connection);
if (track_id <= 0) {
result = LARDON3D_PROJECT_DB_CORRUPT;
break;
}
for (uint32_t observation = 0;
result == LARDON3D_PROJECT_DB_OK && observation < track->observation_count; ++observation) {
const Lardon3DProjectDbTrackObservation *item = &track->observations[observation];
result = prepare(database,
"INSERT INTO track_observations(track_set_id,track_id,feature_set_id,"
"feature_index,position_in_track) VALUES(?1,?2,?3,?4,?5)",
&statement);
if (result != LARDON3D_PROJECT_DB_OK) {
break;
}
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)published->track_set_id);
(void)sqlite3_bind_int64(statement, 2, track_id);
(void)sqlite3_bind_int64(statement, 3, (sqlite3_int64)item->feature_set_id);
(void)sqlite3_bind_int64(statement, 4, item->feature_index);
(void)sqlite3_bind_int64(statement, 5, item->position_in_track);
result = step_done(database, statement, "insert track observation");
statement = NULL;
}
}
}
if (result == LARDON3D_PROJECT_DB_OK) {
result = execute(database, "COMMIT", "commit track set publish");
} else {
if (statement) {
(void)sqlite3_finalize(statement);
}
(void)execute(database, "ROLLBACK", "rollback track set publish");
}
uint64_t published_id = published->track_set_id;
(void)pthread_mutex_unlock(&database->mutex);
if (result == LARDON3D_PROJECT_DB_OK && published_id != 0 && !reused) {
*published = *configuration;
published->track_set_id = published_id;
published->track_count = track_count;
published->created_at = publication_time;
}
return result;
}
static Lardon3DProjectDbResult load_track_locked(Lardon3DProjectDb *database, uint64_t track_id,
Lardon3DProjectDbTrack *track) {
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result = prepare(
database, "SELECT track_id,track_set_id,observation_count FROM tracks WHERE track_id=?1",
&statement);
if (result != LARDON3D_PROJECT_DB_OK) {
return result;
}
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)track_id);
int code = sqlite3_step(statement);
if (code != SQLITE_ROW) {
result = code == SQLITE_DONE ? LARDON3D_PROJECT_DB_NOT_FOUND
: sqlite_result(database, code, "load track");
(void)sqlite3_finalize(statement);
return result;
}
sqlite3_int64 set_id = sqlite3_column_int64(statement, 1);
sqlite3_int64 count = sqlite3_column_int64(statement, 2);
(void)sqlite3_finalize(statement);
if (set_id <= 0 || count < 2 || (uint64_t)count > SIZE_MAX / sizeof(*track->observations)) {
return LARDON3D_PROJECT_DB_CORRUPT;
}
sqlite3_stmt *set_statement = NULL;
result = prepare(database, "SELECT 1 FROM track_sets WHERE track_set_id=?1", &set_statement);
if (result != LARDON3D_PROJECT_DB_OK) {
return result;
}
(void)sqlite3_bind_int64(set_statement, 1, set_id);
int set_code = sqlite3_step(set_statement);
(void)sqlite3_finalize(set_statement);
if (set_code != SQLITE_ROW) {
return set_code == SQLITE_DONE ? LARDON3D_PROJECT_DB_CORRUPT
: sqlite_result(database, set_code, "load track set parent");
}
track->observations = calloc((size_t)count, sizeof(*track->observations));
if (!track->observations) {
return LARDON3D_PROJECT_DB_IO_ERROR;
}
result = prepare(database,
"SELECT feature_set_id,feature_index,position_in_track FROM track_observations "
"WHERE track_id=?1 AND track_set_id=?2 ORDER BY position_in_track",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)track_id);
(void)sqlite3_bind_int64(statement, 2, set_id);
size_t index = 0;
while ((code = sqlite3_step(statement)) == SQLITE_ROW) {
if (index == (size_t)count || sqlite3_column_int64(statement, 2) != (sqlite3_int64)index) {
result = LARDON3D_PROJECT_DB_CORRUPT;
break;
}
sqlite3_int64 feature_set = sqlite3_column_int64(statement, 0);
sqlite3_int64 feature_index = sqlite3_column_int64(statement, 1);
if (feature_set <= 0 || feature_index < 0 || feature_index > UINT32_MAX) {
result = LARDON3D_PROJECT_DB_CORRUPT;
break;
}
sqlite3_stmt *feature_statement = NULL;
result = prepare(database,
"SELECT image_id,feature_count FROM feature_sets WHERE feature_set_id=?1",
&feature_statement);
if (result != LARDON3D_PROJECT_DB_OK) {
break;
}
(void)sqlite3_bind_int64(feature_statement, 1, feature_set);
int feature_code = sqlite3_step(feature_statement);
sqlite3_int64 image_id = feature_code == SQLITE_ROW ? sqlite3_column_int64(feature_statement, 0) : 0;
sqlite3_int64 feature_count = feature_code == SQLITE_ROW
? sqlite3_column_int64(feature_statement, 1)
: -1;
(void)sqlite3_finalize(feature_statement);
if (feature_code != SQLITE_ROW || image_id <= 0 || feature_count < 0 ||
(uint64_t)feature_index >= (uint64_t)feature_count) {
result = LARDON3D_PROJECT_DB_CORRUPT;
break;
}
for (size_t prior = 0; prior < index; ++prior) {
sqlite3_stmt *prior_statement = NULL;
result = prepare(database, "SELECT image_id FROM feature_sets WHERE feature_set_id=?1",
&prior_statement);
if (result != LARDON3D_PROJECT_DB_OK) {
break;
}
(void)sqlite3_bind_int64(prior_statement, 1,
(sqlite3_int64)track->observations[prior].feature_set_id);
int prior_code = sqlite3_step(prior_statement);
sqlite3_int64 prior_image = prior_code == SQLITE_ROW
? sqlite3_column_int64(prior_statement, 0)
: 0;
(void)sqlite3_finalize(prior_statement);
if (prior_code != SQLITE_ROW || prior_image <= 0) {
result = LARDON3D_PROJECT_DB_CORRUPT;
break;
}
if (prior_image == image_id) {
result = LARDON3D_PROJECT_DB_CORRUPT;
break;
}
}
if (result != LARDON3D_PROJECT_DB_OK) {
break;
}
track->observations[index] = (Lardon3DProjectDbTrackObservation){
.feature_set_id = (uint64_t)feature_set,
.feature_index = (uint32_t)feature_index,
.position_in_track = (uint32_t)index};
++index;
}
if (result == LARDON3D_PROJECT_DB_OK && code != SQLITE_DONE) {
result = sqlite_result(database, code, "read track observations");
}
if (result == LARDON3D_PROJECT_DB_OK && index != (size_t)count) {
result = LARDON3D_PROJECT_DB_CORRUPT;
}
(void)sqlite3_finalize(statement);
}
if (result != LARDON3D_PROJECT_DB_OK) {
lardon3d_project_db_free_track(track);
return result;
}
track->track_id = track_id;
track->track_set_id = (uint64_t)set_id;
track->observation_count = (uint32_t)count;
return LARDON3D_PROJECT_DB_OK;
}
Lardon3DProjectDbResult lardon3d_project_db_load_track(
Lardon3DProjectDb *database, uint64_t track_id, Lardon3DProjectDbTrack *track) {
if (!database || !valid_catalog_id(track_id) || !track) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
memset(track, 0, sizeof(*track));
(void)pthread_mutex_lock(&database->mutex);
Lardon3DProjectDbResult result = load_track_locked(database, track_id, track);
(void)pthread_mutex_unlock(&database->mutex);
return result;
}
Lardon3DProjectDbResult lardon3d_project_db_list_tracks(
Lardon3DProjectDb *database, uint64_t track_set_id, uint64_t after_track_id,
Lardon3DProjectDbTrack *tracks, size_t capacity, size_t *count) {
if (!database || !valid_catalog_id(track_set_id) || !tracks || !count || capacity == 0 ||
capacity > LARDON3D_PROJECT_DB_TRACK_PAGE_MAX) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
*count = 0;
(void)pthread_mutex_lock(&database->mutex);
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result = prepare(
database, "SELECT track_id FROM tracks WHERE track_set_id=?1 AND track_id>?2 "
"ORDER BY track_id LIMIT ?3", &statement);
if (result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)track_set_id);
(void)sqlite3_bind_int64(statement, 2, (sqlite3_int64)after_track_id);
(void)sqlite3_bind_int64(statement, 3, (sqlite3_int64)capacity);
int code;
while ((code = sqlite3_step(statement)) == SQLITE_ROW) {
sqlite3_int64 id = sqlite3_column_int64(statement, 0);
if (id <= 0) {
result = LARDON3D_PROJECT_DB_CORRUPT;
break;
}
result = load_track_locked(database, (uint64_t)id, &tracks[*count]);
if (result != LARDON3D_PROJECT_DB_OK || tracks[*count].track_set_id != track_set_id) {
if (result == LARDON3D_PROJECT_DB_OK) {
lardon3d_project_db_free_track(&tracks[*count]);
result = LARDON3D_PROJECT_DB_CORRUPT;
}
break;
}
++*count;
}
if (result == LARDON3D_PROJECT_DB_OK && code != SQLITE_DONE) {
result = sqlite_result(database, code, "list tracks");
}
if (result != LARDON3D_PROJECT_DB_OK) {
for (size_t index = 0; index < *count; ++index) {
lardon3d_project_db_free_track(&tracks[index]);
}
*count = 0;
}
(void)sqlite3_finalize(statement);
}
(void)pthread_mutex_unlock(&database->mutex);
return result;
}
Lardon3DProjectDbResult lardon3d_project_db_find_track_by_observation(
Lardon3DProjectDb *database, uint64_t track_set_id, uint64_t feature_set_id,
uint32_t feature_index, Lardon3DProjectDbTrack *track) {
if (!database || !valid_catalog_id(track_set_id) || !valid_catalog_id(feature_set_id) || !track) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
memset(track, 0, sizeof(*track));
(void)pthread_mutex_lock(&database->mutex);
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result = prepare(
database, "SELECT track_id FROM track_observations WHERE track_set_id=?1 AND "
"feature_set_id=?2 AND feature_index=?3", &statement);
if (result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)track_set_id);
(void)sqlite3_bind_int64(statement, 2, (sqlite3_int64)feature_set_id);
(void)sqlite3_bind_int64(statement, 3, feature_index);
int code = sqlite3_step(statement);
if (code == SQLITE_ROW) {
sqlite3_int64 id = sqlite3_column_int64(statement, 0);
result = id > 0 ? load_track_locked(database, (uint64_t)id, track)
: LARDON3D_PROJECT_DB_CORRUPT;
if (result == LARDON3D_PROJECT_DB_NOT_FOUND) {
result = LARDON3D_PROJECT_DB_CORRUPT;
}
if (result == LARDON3D_PROJECT_DB_OK && track->track_set_id != track_set_id) {
lardon3d_project_db_free_track(track);
result = LARDON3D_PROJECT_DB_CORRUPT;
}
} else {
result = code == SQLITE_DONE ? LARDON3D_PROJECT_DB_NOT_FOUND
: sqlite_result(database, code, "find track observation");
}
(void)sqlite3_finalize(statement);
}
(void)pthread_mutex_unlock(&database->mutex);
return result;
}
#ifdef LARDON3D_PROJECT_DB_TESTING
Lardon3DProjectDbResult lardon3d_project_db_test_delete_feature_identity(
Lardon3DProjectDb *database, uint64_t feature_set_id, uint64_t feature_asset_id) {
if (!database || !valid_catalog_id(feature_set_id) || !valid_catalog_id(feature_asset_id)) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
(void)pthread_mutex_lock(&database->mutex);
Lardon3DProjectDbResult result =
execute(database, "BEGIN IMMEDIATE", "begin delete feature identity");
sqlite3_stmt *s = NULL;
const char *sql[2] = {"DELETE FROM feature_sets WHERE feature_set_id=?1",
"DELETE FROM feature_assets WHERE feature_asset_id=?1"};
uint64_t ids[2] = {feature_set_id, feature_asset_id};
for (size_t i = 0; i < 2 && result == LARDON3D_PROJECT_DB_OK; ++i) {
result = prepare(database, sql[i], &s);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(s, 1, (sqlite3_int64)ids[i]);
result = step_done(database, s, "delete feature identity");
s = NULL;
if (result == LARDON3D_PROJECT_DB_OK && sqlite3_changes(database->connection) != 1) {
result = LARDON3D_PROJECT_DB_NOT_FOUND;
}
}
}
if (result == LARDON3D_PROJECT_DB_OK) {
result = execute(database, "COMMIT", "commit delete feature identity");
}
if (result != LARDON3D_PROJECT_DB_OK) {
(void)execute(database, "ROLLBACK", "rollback delete feature identity");
}
(void)pthread_mutex_unlock(&database->mutex);
return result;
}
Lardon3DProjectDbResult lardon3d_project_db_test_orphan_checkpoint(Lardon3DProjectDb *database) {
if (!database) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
(void)pthread_mutex_lock(&database->mutex);
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result =
prepare(database,
"INSERT INTO checkpoints(task_id,path,format_version,durability,updated_at) "
"VALUES(9223372036854775807,'orphan',1,0,0)",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
result = step_done(database, statement, "orphan checkpoint");
}
(void)pthread_mutex_unlock(&database->mutex);
return result;
}
Lardon3DProjectDbResult
lardon3d_project_db_test_delete_catalog_identity(Lardon3DProjectDb *database, uint64_t scanset_id,
uint64_t image_id, uint64_t asset_id) {
if (!database || !valid_catalog_id(scanset_id) || !valid_catalog_id(image_id) ||
!valid_catalog_id(asset_id)) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
(void)pthread_mutex_lock(&database->mutex);
Lardon3DProjectDbResult result =
execute(database, "BEGIN IMMEDIATE", "begin catalog identity deletion test");
const char *sql[] = {
("DELETE FROM captures WHERE capture_id IN "
"(SELECT capture_id FROM capture_images WHERE image_id=?1)"),
"DELETE FROM images WHERE image_id=?1",
"DELETE FROM image_assets WHERE asset_id=?1",
"DELETE FROM scansets WHERE scanset_id=?1",
};
const uint64_t ids[] = {image_id, image_id, asset_id, scanset_id};
for (size_t index = 0; index < 4 && result == LARDON3D_PROJECT_DB_OK; ++index) {
sqlite3_stmt *statement = NULL;
result = prepare(database, sql[index], &statement);
if (result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)ids[index]);
result = step_done(database, statement, "delete catalog identity test");
if (result == LARDON3D_PROJECT_DB_OK && sqlite3_changes(database->connection) != 1) {
result = LARDON3D_PROJECT_DB_NOT_FOUND;
}
}
}
if (result == LARDON3D_PROJECT_DB_OK) {
result = execute(database, "COMMIT", "commit catalog identity deletion test");
}
if (result != LARDON3D_PROJECT_DB_OK) {
(void)execute(database, "ROLLBACK", "rollback catalog identity deletion test");
}
(void)pthread_mutex_unlock(&database->mutex);
return result;
}
#endif