#include #include #include #include #include #include #include #include #include #include #include #include #include #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_a0)," "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 = ¶meters->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