lardon3d/src/project_db.c
2026-08-09 19:06:52 +02:00

5029 lines
242 KiB
C

#include <errno.h>
#include <limits.h>
#include <math.h>
#include <pthread.h>
#include <sqlite3.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <time.h>
#include <lardon3d/project_db.h>
struct Lardon3DProjectDb {
sqlite3 *connection;
pthread_mutex_t mutex;
char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY];
};
static const char schema_v5[] =
"CREATE TABLE metadata(key TEXT PRIMARY KEY,value INTEGER NOT NULL);"
"INSERT INTO metadata(key,value) VALUES('schema_version',5);"
"INSERT INTO metadata(key,value) VALUES('next_task_id',1);"
"INSERT INTO metadata(key,value) VALUES('legacy_image_catalog_pending',0);"
"CREATE TABLE project(singleton INTEGER PRIMARY KEY CHECK(singleton=1),"
"stable_id TEXT NOT NULL UNIQUE,name TEXT NOT NULL,created_at INTEGER NOT NULL,updated_at "
"INTEGER NOT NULL);"
"CREATE TABLE tasks(task_id INTEGER PRIMARY KEY CHECK(task_id>0),name TEXT NOT NULL,"
"task_kind TEXT,task_kind_version INTEGER,"
"saved_state INTEGER NOT NULL CHECK(saved_state BETWEEN 0 AND 5),"
"recovery_state INTEGER NOT NULL CHECK(recovery_state BETWEEN 0 AND 5),"
"progress INTEGER NOT NULL CHECK(progress BETWEEN 0 AND 100),sequence_count INTEGER NOT NULL "
"CHECK(sequence_count>=0),"
"started_sec INTEGER NOT NULL,started_nsec INTEGER NOT NULL CHECK(started_nsec BETWEEN 0 AND "
"999999999),"
"finished_sec INTEGER NOT NULL,finished_nsec INTEGER NOT NULL CHECK(finished_nsec BETWEEN 0 "
"AND 999999999),updated_at INTEGER NOT NULL,"
"CHECK((task_kind IS NULL AND task_kind_version IS NULL) OR (task_kind IS NOT NULL AND "
"task_kind_version>0)));"
"CREATE INDEX tasks_recovery_state_idx ON tasks(recovery_state,task_id);"
"CREATE TABLE checkpoints(task_id INTEGER PRIMARY KEY REFERENCES tasks(task_id) ON DELETE "
"CASCADE,"
"path TEXT NOT NULL,format_version INTEGER NOT NULL CHECK(format_version>0),"
"durability INTEGER NOT NULL CHECK(durability BETWEEN 0 AND 1),updated_at INTEGER NOT NULL);"
"CREATE TABLE artifacts(artifact_id TEXT PRIMARY KEY,kind TEXT NOT NULL,path TEXT NOT NULL,"
"state INTEGER NOT NULL CHECK(state BETWEEN 0 AND 1),size_bytes INTEGER NOT NULL "
"CHECK(size_bytes>=0),"
"producer_task_id INTEGER REFERENCES tasks(task_id),created_at INTEGER NOT NULL,updated_at "
"INTEGER NOT NULL);"
"CREATE INDEX artifacts_state_idx ON artifacts(state,artifact_id);"
"CREATE INDEX artifacts_producer_idx ON artifacts(producer_task_id);"
"CREATE TABLE scansets(scanset_id INTEGER PRIMARY KEY AUTOINCREMENT CHECK(scanset_id>0),"
"name TEXT NOT NULL CHECK(length(name)>0 AND length(name)<256),created_at INTEGER NOT NULL "
"CHECK(created_at>=0),"
"updated_at INTEGER NOT NULL CHECK(updated_at>=created_at));"
"CREATE TABLE image_assets(asset_id INTEGER PRIMARY KEY AUTOINCREMENT CHECK(asset_id>0),sha256 "
"BLOB NOT NULL UNIQUE CHECK(length(sha256)=32),"
"path TEXT NOT NULL UNIQUE CHECK(length(path)>0 AND length(path)<4096),size_bytes INTEGER NOT "
"NULL CHECK(size_bytes>=0),"
"state INTEGER NOT NULL CHECK(state=1),created_at INTEGER NOT NULL CHECK(created_at>=0));"
"CREATE TABLE images(image_id INTEGER PRIMARY KEY AUTOINCREMENT CHECK(image_id>0),scanset_id "
"INTEGER NOT NULL REFERENCES scansets(scanset_id),"
"asset_id INTEGER NOT NULL REFERENCES image_assets(asset_id),original_name TEXT NOT NULL "
"CHECK(length(original_name)>0 AND length(original_name)<256),"
"source_path TEXT NOT NULL CHECK(length(source_path)>0 AND length(source_path)<4096),"
"producer_task_id INTEGER REFERENCES tasks(task_id),imported_at INTEGER NOT NULL "
"CHECK(imported_at>=0),"
"UNIQUE(scanset_id,asset_id));"
"CREATE INDEX images_scanset_idx ON images(scanset_id,image_id);"
"CREATE INDEX images_producer_idx ON images(producer_task_id,image_id);"
"CREATE TABLE image_import_tasks(task_id INTEGER PRIMARY KEY REFERENCES tasks(task_id) ON "
"DELETE CASCADE,"
"source_path TEXT NOT NULL,scanset_id INTEGER NOT NULL REFERENCES scansets(scanset_id));";
static const char schema_feature_v5[] =
"CREATE TABLE feature_assets(feature_asset_id INTEGER PRIMARY KEY AUTOINCREMENT "
"CHECK(feature_asset_id>0),sha256 BLOB NOT NULL UNIQUE CHECK(length(sha256)=32),path TEXT NOT "
"NULL UNIQUE CHECK(length(path)>0 AND length(path)<4096),size_bytes INTEGER NOT NULL "
"CHECK(size_bytes>=0),durability INTEGER NOT NULL CHECK(durability BETWEEN 0 AND 1),created_at "
"INTEGER NOT NULL CHECK(created_at>=0));"
"CREATE TABLE feature_sets(feature_set_id INTEGER PRIMARY KEY AUTOINCREMENT "
"CHECK(feature_set_id>0),image_id INTEGER NOT NULL REFERENCES "
"images(image_id),feature_asset_id INTEGER NOT NULL REFERENCES "
"feature_assets(feature_asset_id),extractor_kind TEXT NOT NULL CHECK(length(extractor_kind)>0 "
"AND length(extractor_kind)<65),extractor_version INTEGER NOT NULL "
"CHECK(extractor_version>0),parameter_fingerprint BLOB NOT NULL "
"CHECK(length(parameter_fingerprint)=32),source_image_sha256 BLOB NOT NULL "
"CHECK(length(source_image_sha256)=32),feature_count INTEGER NOT NULL "
"CHECK(feature_count BETWEEN 0 AND 8192),descriptor_type INTEGER NOT NULL "
"CHECK(descriptor_type "
"IN(1,2)),descriptor_dimension INTEGER NOT NULL "
"CHECK(descriptor_dimension BETWEEN 1 AND 4096),producer_task_id "
"INTEGER REFERENCES tasks(task_id),created_at INTEGER NOT NULL "
"CHECK(created_at>=0),UNIQUE(image_id,extractor_kind,extractor_version,parameter_fingerprint));"
"CREATE INDEX feature_sets_image_idx ON feature_sets(image_id,feature_set_id);"
"CREATE INDEX feature_sets_producer_idx ON feature_sets(producer_task_id,feature_set_id);"
"CREATE TABLE feature_extract_tasks(task_id INTEGER PRIMARY KEY REFERENCES tasks(task_id) ON "
"DELETE CASCADE,image_id INTEGER NOT NULL REFERENCES images(image_id),extractor_kind TEXT NOT "
"NULL,extractor_version INTEGER NOT NULL CHECK(extractor_version>0),max_features INTEGER NOT "
"NULL CHECK(max_features BETWEEN 1 AND 8192),pyramid_levels INTEGER NOT NULL "
"CHECK(pyramid_levels BETWEEN 1 AND 16),fast_threshold INTEGER NOT NULL "
"CHECK(fast_threshold BETWEEN 1 AND 255),parameter_fingerprint BLOB NOT NULL "
"CHECK(length(parameter_fingerprint)=32));";
static const char schema_visual_v6[] =
"CREATE TABLE visual_indexes(visual_index_id INTEGER PRIMARY KEY AUTOINCREMENT "
"CHECK(visual_index_id>0),index_kind TEXT NOT NULL CHECK(length(index_kind)>0 AND "
"length(index_kind)<65),index_version INTEGER NOT NULL CHECK(index_version>0),"
"descriptor_type INTEGER NOT NULL CHECK(descriptor_type IN(1,2)),descriptor_dimension "
"INTEGER NOT NULL CHECK(descriptor_dimension BETWEEN 1 AND 4096),extractor_kind TEXT NOT "
"NULL CHECK(length(extractor_kind)>0 AND length(extractor_kind)<65),extractor_version "
"INTEGER NOT NULL CHECK(extractor_version>0),feature_parameter_fingerprint BLOB NOT NULL "
"CHECK(length(feature_parameter_fingerprint)=32),index_parameter_fingerprint BLOB NOT NULL "
"CHECK(length(index_parameter_fingerprint)=32),table_count INTEGER NOT NULL CHECK(table_count "
"BETWEEN 1 AND 32),key_bits INTEGER NOT NULL CHECK(key_bits BETWEEN 8 AND 32),"
"max_features_per_set INTEGER NOT NULL CHECK(max_features_per_set BETWEEN 1 AND 1024),"
"max_bucket_postings INTEGER NOT NULL CHECK(max_bucket_postings BETWEEN 1 AND 4096),"
"created_at INTEGER NOT NULL CHECK(created_at>=0),UNIQUE(index_kind,index_version,"
"descriptor_type,descriptor_dimension,extractor_kind,extractor_version,"
"feature_parameter_fingerprint,index_parameter_fingerprint));"
"CREATE TABLE visual_index_segments(visual_index_segment_id INTEGER PRIMARY KEY "
"AUTOINCREMENT CHECK(visual_index_segment_id>0),visual_index_id INTEGER NOT NULL REFERENCES "
"visual_indexes(visual_index_id),generation INTEGER NOT NULL CHECK(generation>0),sha256 BLOB "
"NOT NULL CHECK(length(sha256)=32),path TEXT NOT NULL UNIQUE CHECK(length(path)>0 AND "
"length(path)<4096),size_bytes INTEGER NOT NULL CHECK(size_bytes>=128),posting_count INTEGER "
"NOT NULL CHECK(posting_count>=0),feature_set_count INTEGER NOT NULL CHECK(feature_set_count "
"BETWEEN 1 AND 16),durability INTEGER NOT NULL CHECK(durability BETWEEN 0 AND 1),"
"producer_task_id INTEGER REFERENCES tasks(task_id),created_at INTEGER NOT NULL "
"CHECK(created_at>=0),UNIQUE(visual_index_id,generation),UNIQUE(visual_index_id,sha256));"
"CREATE INDEX visual_index_segments_index_idx ON visual_index_segments(visual_index_id,"
"generation);"
"CREATE TABLE visual_index_memberships(visual_index_id INTEGER NOT NULL REFERENCES "
"visual_indexes(visual_index_id),feature_set_id INTEGER NOT NULL REFERENCES "
"feature_sets(feature_set_id),visual_index_segment_id INTEGER NOT NULL REFERENCES "
"visual_index_segments(visual_index_segment_id),PRIMARY KEY(visual_index_id,feature_set_id));"
"CREATE INDEX visual_index_memberships_segment_idx ON visual_index_memberships("
"visual_index_segment_id,feature_set_id);"
"CREATE TABLE visual_index_update_tasks(task_id INTEGER PRIMARY KEY REFERENCES tasks(task_id) "
"ON DELETE CASCADE,visual_index_id INTEGER NOT NULL REFERENCES visual_indexes(visual_index_id),"
"after_feature_set_id INTEGER NOT NULL CHECK(after_feature_set_id>=0));";
static const char schema_precision_v7[] =
"ALTER TABLE feature_sets ADD COLUMN occupied_cells INTEGER NOT NULL DEFAULT 0 "
"CHECK(occupied_cells>=0 AND occupied_cells<=1024);"
"ALTER TABLE feature_sets ADD COLUMN total_cells INTEGER NOT NULL DEFAULT 0 "
"CHECK(total_cells>=0 AND total_cells<=1024 AND occupied_cells<=total_cells);"
"ALTER TABLE feature_sets ADD COLUMN coverage_ratio REAL NOT NULL DEFAULT 0.0 "
"CHECK(coverage_ratio>=0.0 AND coverage_ratio<=1.0);"
"ALTER TABLE feature_sets ADD COLUMN feature_density_per_megapixel REAL NOT NULL DEFAULT 0.0 "
"CHECK(feature_density_per_megapixel>=0.0);"
"CREATE TABLE sift_extract_tasks(task_id INTEGER PRIMARY KEY REFERENCES tasks(task_id) ON "
"DELETE CASCADE,image_id INTEGER NOT NULL REFERENCES images(image_id),extractor_kind TEXT NOT "
"NULL CHECK(extractor_kind IN('sift','rootsift')),extractor_version INTEGER NOT NULL "
"CHECK(extractor_version=1),max_features INTEGER NOT NULL CHECK(max_features BETWEEN 1 AND "
"8192),octave_layers INTEGER NOT NULL CHECK(octave_layers BETWEEN 1 AND 8),"
"contrast_threshold REAL NOT NULL CHECK(contrast_threshold BETWEEN 0.001 AND 0.2),"
"edge_threshold REAL NOT NULL CHECK(edge_threshold BETWEEN 1.0 AND 100.0),sigma REAL NOT NULL "
"CHECK(sigma BETWEEN 0.5 AND 3.0),grid_rows INTEGER NOT NULL CHECK(grid_rows BETWEEN 1 AND 32),"
"grid_cols INTEGER NOT NULL CHECK(grid_cols BETWEEN 1 AND 32),max_features_per_cell INTEGER "
"NOT NULL CHECK(max_features_per_cell BETWEEN 1 AND 8192),parameter_fingerprint BLOB NOT NULL "
"CHECK(length(parameter_fingerprint)=32));"
"CREATE TABLE feature_support_sets(feature_support_set_id INTEGER PRIMARY KEY AUTOINCREMENT "
"CHECK(feature_support_set_id>0),image_id INTEGER NOT NULL REFERENCES images(image_id),"
"first_feature_set_id INTEGER NOT NULL REFERENCES feature_sets(feature_set_id),"
"second_feature_set_id INTEGER NOT NULL REFERENCES feature_sets(feature_set_id),radius_pixels "
"REAL NOT NULL CHECK(radius_pixels>0.0 AND radius_pixels<=64.0),parameter_fingerprint BLOB NOT "
"NULL CHECK(length(parameter_fingerprint)=32),created_at INTEGER NOT NULL CHECK(created_at>=0),"
"UNIQUE(image_id,first_feature_set_id,second_feature_set_id,parameter_fingerprint));"
"CREATE TABLE feature_support_groups(feature_support_group_id INTEGER PRIMARY KEY AUTOINCREMENT "
"CHECK(feature_support_group_id>0),feature_support_set_id INTEGER NOT NULL REFERENCES "
"feature_support_sets(feature_support_set_id) ON DELETE CASCADE,x REAL NOT NULL,y REAL NOT NULL,"
"distance_pixels REAL NOT NULL CHECK(distance_pixels>=0.0 AND distance_pixels<=64.0),"
"support_count INTEGER NOT NULL CHECK(support_count BETWEEN 1 AND 3));"
"CREATE INDEX feature_support_groups_set_idx ON feature_support_groups(feature_support_set_id,"
"feature_support_group_id);"
"CREATE TABLE feature_support_members(feature_support_group_id INTEGER NOT NULL REFERENCES "
"feature_support_groups(feature_support_group_id) ON DELETE CASCADE,feature_set_id INTEGER NOT "
"NULL REFERENCES feature_sets(feature_set_id),feature_index INTEGER NOT NULL CHECK(feature_index "
"BETWEEN 0 AND 8191),extractor_kind TEXT NOT NULL CHECK(length(extractor_kind)>0 AND "
"length(extractor_kind)<65),PRIMARY KEY(feature_support_group_id,feature_set_id));";
static const char schema_candidate_v8[] =
"CREATE TABLE candidate_pairs(candidate_pair_id INTEGER PRIMARY KEY AUTOINCREMENT "
"CHECK(candidate_pair_id>0),image_id_a INTEGER NOT NULL REFERENCES images(image_id),"
"image_id_b INTEGER NOT NULL REFERENCES images(image_id),created_at INTEGER NOT NULL "
"CHECK(created_at>=0),CHECK(image_id_a<image_id_b),UNIQUE(image_id_a,image_id_b));"
"CREATE INDEX candidate_pairs_image_a_idx ON candidate_pairs(image_id_a);"
"CREATE INDEX candidate_pairs_image_b_idx ON candidate_pairs(image_id_b);";
static const char schema_candidate_generate_v9[] =
"CREATE TABLE candidate_pair_generate_tasks(task_id INTEGER PRIMARY KEY REFERENCES "
"tasks(task_id) ON DELETE CASCADE,"
"visual_index_id INTEGER NOT NULL CHECK(visual_index_id>0),"
"after_feature_set_id INTEGER NOT NULL CHECK(after_feature_set_id>=0),"
"top_k INTEGER NOT NULL CHECK(top_k>0 AND top_k<=256),"
"minimum_evidence_count INTEGER NOT NULL CHECK(minimum_evidence_count>=0 "
"AND minimum_evidence_count<=1024),"
"scanset_filter INTEGER NOT NULL CHECK(scanset_filter>=0 AND scanset_filter<=2),"
"exclude_same_asset INTEGER NOT NULL CHECK(exclude_same_asset IN (0,1)));";
static const char schema_match_result_v10[] =
"CREATE TABLE match_results("
"match_result_id INTEGER PRIMARY KEY AUTOINCREMENT CHECK(match_result_id>0),"
"candidate_pair_id INTEGER NOT NULL REFERENCES candidate_pairs(candidate_pair_id),"
"feature_set_id_a INTEGER NOT NULL REFERENCES feature_sets(feature_set_id),"
"feature_set_id_b INTEGER NOT NULL REFERENCES feature_sets(feature_set_id),"
"matcher_kind TEXT NOT NULL CHECK(length(matcher_kind)>0 AND length(matcher_kind)<=64),"
"matcher_version INTEGER NOT NULL CHECK(matcher_version>0),"
"parameter_fingerprint BLOB NOT NULL CHECK(length(parameter_fingerprint)=32),"
"result_status INTEGER NOT NULL CHECK(result_status IN (0,1)),"
"match_count INTEGER NOT NULL CHECK(match_count>=0 AND match_count<=8192),"
"match_asset_sha256 BLOB CHECK(match_asset_sha256 IS NULL OR"
" length(match_asset_sha256)=32),"
"match_asset_path TEXT CHECK(match_asset_path IS NULL OR length(match_asset_path)>0),"
"match_asset_size_bytes INTEGER CHECK(match_asset_size_bytes IS NULL OR"
" match_asset_size_bytes>0),"
"created_at INTEGER NOT NULL CHECK(created_at>=0),"
"CHECK((result_status=0 AND match_count=0 AND match_asset_sha256 IS NULL AND "
"match_asset_path IS NULL AND match_asset_size_bytes IS NULL) OR "
"(result_status=1 AND match_count>0 AND match_asset_sha256 IS NOT NULL AND "
"match_asset_path IS NOT NULL AND match_asset_size_bytes IS NOT NULL)),"
"UNIQUE(candidate_pair_id,feature_set_id_a,feature_set_id_b,"
"matcher_kind,matcher_version,parameter_fingerprint));"
"CREATE INDEX match_results_candidate_pair_idx ON match_results(candidate_pair_id);"
"CREATE INDEX match_results_feature_set_a_idx ON match_results(feature_set_id_a);"
"CREATE INDEX match_results_feature_set_b_idx ON match_results(feature_set_id_b);";
static const char schema_matcher_task_v11[] =
"CREATE TABLE matcher_tasks("
"task_id INTEGER PRIMARY KEY REFERENCES tasks(task_id) ON DELETE CASCADE,"
"after_candidate_pair_id INTEGER NOT NULL CHECK(after_candidate_pair_id>=0),"
"feature_extractor_kind TEXT NOT NULL CHECK(length(feature_extractor_kind)>0 AND "
"length(feature_extractor_kind)<65),"
"feature_extractor_version INTEGER NOT NULL CHECK(feature_extractor_version>0),"
"feature_parameter_fingerprint BLOB NOT NULL "
"CHECK(length(feature_parameter_fingerprint)=32),"
"matcher_kind INTEGER NOT NULL CHECK(matcher_kind BETWEEN 0 AND 2),"
"ratio_threshold REAL NOT NULL CHECK(ratio_threshold>0.0 AND ratio_threshold<1.0));";
static const char schema_geometric_verification_v12[] =
"CREATE TABLE geometric_verification_results("
"geometric_verification_result_id INTEGER PRIMARY KEY AUTOINCREMENT "
"CHECK(geometric_verification_result_id>0),"
"match_result_id INTEGER NOT NULL REFERENCES match_results(match_result_id) ON DELETE CASCADE,"
"verifier_kind INTEGER NOT NULL CHECK(verifier_kind=1),"
"verifier_version INTEGER NOT NULL CHECK(verifier_version>0 AND verifier_version<=4294967295),"
"parameter_fingerprint BLOB NOT NULL CHECK(length(parameter_fingerprint)=32),"
"status INTEGER NOT NULL CHECK(status IN (1,2)),"
"inlier_count INTEGER NOT NULL CHECK(inlier_count>=0 AND inlier_count<=8192),"
"inlier_mask BLOB NOT NULL CHECK(length(inlier_mask)>=1 AND length(inlier_mask)<=1024),"
"model_m00 REAL,model_m01 REAL,model_m02 REAL,"
"model_m10 REAL,model_m11 REAL,model_m12 REAL,"
"model_m20 REAL,model_m21 REAL,model_m22 REAL,"
"created_at INTEGER NOT NULL CHECK(created_at>=0),"
"CHECK((status=1 AND model_m00 IS NULL AND model_m01 IS NULL AND model_m02 IS NULL AND "
"model_m10 IS NULL AND model_m11 IS NULL AND model_m12 IS NULL AND model_m20 IS NULL AND "
"model_m21 IS NULL AND model_m22 IS NULL) OR "
"(status=2 AND model_m00 IS NOT NULL AND model_m01 IS NOT NULL AND model_m02 IS NOT NULL AND "
"model_m10 IS NOT NULL AND model_m11 IS NOT NULL AND model_m12 IS NOT NULL AND "
"model_m20 IS NOT NULL AND model_m21 IS NOT NULL AND model_m22 IS NOT NULL)),"
"UNIQUE(match_result_id,verifier_kind,verifier_version,parameter_fingerprint));"
"CREATE INDEX geometric_verification_results_parent_idx ON "
"geometric_verification_results(match_result_id,geometric_verification_result_id);";
static const char schema_track_v14[] =
"CREATE TABLE track_sets(track_set_id INTEGER PRIMARY KEY AUTOINCREMENT "
"CHECK(track_set_id>0),builder_kind TEXT NOT NULL CHECK(length(builder_kind)>0 AND "
"length(builder_kind)<=64),builder_version INTEGER NOT NULL CHECK(builder_version>0),"
"parameter_fingerprint BLOB NOT NULL CHECK(length(parameter_fingerprint)=32),"
"verifier_kind INTEGER NOT NULL CHECK(verifier_kind>0),verifier_version INTEGER NOT NULL "
"CHECK(verifier_version>0),verifier_fingerprint BLOB NOT NULL "
"CHECK(length(verifier_fingerprint)=32),input_scope_hash BLOB NOT NULL "
"CHECK(length(input_scope_hash)=32),gvr_count INTEGER NOT NULL CHECK(gvr_count>=1),"
"track_count INTEGER NOT NULL CHECK(track_count>=0),created_at INTEGER NOT NULL "
"CHECK(created_at>=0),UNIQUE(builder_kind,builder_version,parameter_fingerprint,"
"verifier_kind,verifier_version,verifier_fingerprint,input_scope_hash));"
"CREATE TABLE tracks(track_id INTEGER PRIMARY KEY AUTOINCREMENT CHECK(track_id>0),"
"track_set_id INTEGER NOT NULL REFERENCES track_sets(track_set_id) ON DELETE CASCADE,"
"observation_count INTEGER NOT NULL CHECK(observation_count>=2));"
"CREATE INDEX tracks_set_idx ON tracks(track_set_id,track_id);"
"CREATE TABLE track_observations(track_set_id INTEGER NOT NULL,"
"track_id INTEGER NOT NULL REFERENCES tracks(track_id) ON DELETE CASCADE,"
"feature_set_id INTEGER NOT NULL REFERENCES feature_sets(feature_set_id),"
"feature_index INTEGER NOT NULL CHECK(feature_index>=0),"
"position_in_track INTEGER NOT NULL CHECK(position_in_track>=0),"
"PRIMARY KEY(track_set_id,feature_set_id,feature_index),"
"UNIQUE(track_id,position_in_track));"
"CREATE INDEX track_observations_lookup_idx ON "
"track_observations(feature_set_id,feature_index,track_set_id);";
static const char schema_geometric_verifier_task_v13[] =
"CREATE TABLE geometric_verifier_tasks("
"task_id INTEGER PRIMARY KEY REFERENCES tasks(task_id) ON DELETE CASCADE,"
"after_match_result_id INTEGER NOT NULL CHECK(after_match_result_id>=0),"
"threshold_pixels REAL NOT NULL CHECK(threshold_pixels>0.0),"
"confidence REAL NOT NULL CHECK(confidence>0.0 AND confidence<1.0),"
"max_iterations INTEGER NOT NULL CHECK(max_iterations>0 AND max_iterations<=2147483647),"
"min_inlier_count INTEGER NOT NULL CHECK(min_inlier_count BETWEEN 1 AND 8192),"
"min_inlier_ratio REAL NOT NULL CHECK(min_inlier_ratio>=0.0 AND min_inlier_ratio<=1.0),"
"seed_policy_version INTEGER NOT NULL CHECK(seed_policy_version>0),"
"canonicalization_version INTEGER NOT NULL CHECK(canonicalization_version>0),"
"parameter_fingerprint BLOB NOT NULL CHECK(length(parameter_fingerprint)=32));";
static void copy_error(char destination[LARDON3D_PROJECT_DB_ERROR_CAPACITY], const char *text) {
if (destination) {
(void)snprintf(destination, LARDON3D_PROJECT_DB_ERROR_CAPACITY, "%s", text ? text : "");
}
}
static Lardon3DProjectDbResult sqlite_result(Lardon3DProjectDb *database, int code,
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;
}
static 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);
}
static 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) {
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) {
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"};
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;
}
static 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 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,
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)) ||
(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 && 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, 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,
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, 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, 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 ";
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;
}
if (result == LARDON3D_PROJECT_DB_OK) {
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);
}
int inserted = 0;
if (result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)scanset_id);
(void)sqlite3_bind_int64(statement, 2, 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;
inserted = sqlite3_changes(database->connection);
}
sqlite3_int64 image_id = 0;
if (result == LARDON3D_PROJECT_DB_OK) {
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, asset_id);
int code = sqlite3_step(statement);
if (code != SQLITE_ROW || (image_id = sqlite3_column_int64(statement, 0)) <= 0) {
result = LARDON3D_PROJECT_DB_CORRUPT;
}
(void)sqlite3_finalize(statement);
statement = NULL;
}
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, (uint64_t)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;
}
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, 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, 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, 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, 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;
}
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 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, asset_id, scanset_id};
for (size_t index = 0; index < 3 && 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