feat: add multi-source capture association

This commit is contained in:
fy59 2026-08-27 10:49:04 +02:00
parent c8fb2e4094
commit e6e7271094
4 changed files with 130 additions and 0 deletions

View file

@ -11,6 +11,12 @@ d'image logique. Une sélection courante optionnelle référence exactement une
image déjà associée au Capture et ne modifie jamais cette image, son asset, ni
les résultats scientifiques existants.
L'association explicite d'un asset `SOURCE` existant à un Capture existant est
idempotente : si cette même association existe déjà avec le rôle `SOURCE`, elle
est acceptée sans nouvelle ligne ni migration de schéma. Elle ne crée aucune
image logique, ne modifie aucune sélection et ne réalise aucun appariement
automatique.
`asset_derivations` est volontairement limité à un parent asset et un enfant
asset, avec kind/version et fingerprint canonique de 32 octets. Il n'est pas un
DAG générique et ne réalise aucun développement RAW ni extraction vidéo. La

View file

@ -533,6 +533,8 @@ Lardon3DProjectDbResult lardon3d_project_db_list_captures(
Lardon3DProjectDbResult lardon3d_project_db_attach_capture_asset(
Lardon3DProjectDb *database, uint64_t capture_id, uint64_t asset_id,
Lardon3DProjectDbCaptureAssetRole role);
Lardon3DProjectDbResult lardon3d_project_db_attach_capture_source_asset(
Lardon3DProjectDb *database, uint64_t capture_id, uint64_t asset_id);
Lardon3DProjectDbResult lardon3d_project_db_list_capture_assets(
Lardon3DProjectDb *database, uint64_t capture_id, uint64_t after_asset_id,
Lardon3DProjectDbCaptureAsset *assets, size_t capacity, size_t *count);

View file

@ -3191,6 +3191,28 @@ Lardon3DProjectDbResult lardon3d_project_db_attach_capture_asset(
return result;
}
Lardon3DProjectDbResult lardon3d_project_db_attach_capture_source_asset(
Lardon3DProjectDb *database, uint64_t capture_id, uint64_t asset_id) {
Lardon3DProjectDbResult result = lardon3d_project_db_attach_capture_asset(
database, capture_id, asset_id, LARDON3D_DB_CAPTURE_ASSET_SOURCE);
if (result != LARDON3D_PROJECT_DB_CONSTRAINT) {
return result;
}
Lardon3DProjectDbCaptureAsset existing;
size_t count = 0;
result = lardon3d_project_db_list_capture_assets(database, capture_id, asset_id - 1,
&existing, 1, &count);
if (result != LARDON3D_PROJECT_DB_OK) {
return result;
}
if (count == 1 && existing.asset_id == asset_id &&
existing.role == LARDON3D_DB_CAPTURE_ASSET_SOURCE) {
return LARDON3D_PROJECT_DB_OK;
}
return LARDON3D_PROJECT_DB_CONSTRAINT;
}
Lardon3DProjectDbResult lardon3d_project_db_list_capture_assets(
Lardon3DProjectDb *database, uint64_t capture_id, uint64_t after_asset_id,
Lardon3DProjectDbCaptureAsset *assets, size_t capacity, size_t *count) {

View file

@ -105,6 +105,13 @@ typedef struct {
uint64_t image_id;
} ImportThread;
typedef struct {
Lardon3DProjectDb *database;
uint64_t capture_id;
uint64_t asset_id;
Lardon3DProjectDbResult result;
} AttachSourceThread;
static void *
import_thread(void *userdata)
{
@ -117,6 +124,15 @@ import_thread(void *userdata)
return NULL;
}
static void *
attach_source_thread(void *userdata)
{
AttachSourceThread *thread = userdata;
thread->result = lardon3d_project_db_attach_capture_source_asset(thread->database,
thread->capture_id, thread->asset_id);
return NULL;
}
static bool
has_initial_capture(Lardon3DProjectDb *database, const Lardon3DProjectDbImage *image,
const Lardon3DProjectDbImageAsset *asset, uint64_t scanset_id,
@ -271,6 +287,90 @@ run_test(void)
&& contexts[0].result == LARDON3D_IMAGE_CATALOG_ALREADY_PRESENT))
&& contexts[0].image_id == contexts[1].image_id);
/* S3-C: explicit, idempotent SOURCE association does not mutate catalog policy. */
Lardon3DProjectDbImage concurrent_image;
Lardon3DProjectDbImageAsset concurrent_asset;
CHECK(lardon3d_project_db_load_image(database, contexts[0].image_id,
&concurrent_image, &concurrent_asset) == LARDON3D_PROJECT_DB_OK);
uint64_t image_count_a_before = 0, image_count_b_before = 0;
CHECK(lardon3d_project_db_count_images(database, a.scanset_id,
&image_count_a_before) == LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_count_images(database, b.scanset_id,
&image_count_b_before) == LARDON3D_PROJECT_DB_OK);
uint64_t selection_before = 0;
CHECK(lardon3d_project_db_get_selected_capture_image(database, capture_a.capture_id,
&selection_before) == LARDON3D_PROJECT_DB_OK
&& selection_before == image_a.image_id);
CHECK(lardon3d_project_db_attach_capture_source_asset(database, capture_a.capture_id,
different_asset.asset_id) == LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_attach_capture_source_asset(database, capture_a.capture_id,
different_asset.asset_id) == LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_attach_capture_source_asset(database, capture_a.capture_id,
concurrent_asset.asset_id) == LARDON3D_PROJECT_DB_OK);
Lardon3DProjectDbCaptureAsset source_page[4];
size_t source_count = 0;
CHECK(lardon3d_project_db_list_capture_assets(database, capture_a.capture_id, 0,
source_page, 4, &source_count) == LARDON3D_PROJECT_DB_OK && source_count == 3);
for (size_t index = 0; index < source_count; ++index) {
CHECK(source_page[index].capture_id == capture_a.capture_id
&& source_page[index].role == LARDON3D_DB_CAPTURE_ASSET_SOURCE
&& (index == 0 || source_page[index - 1].asset_id < source_page[index].asset_id));
}
AttachSourceThread attach_contexts[2] = {
{.database = database, .capture_id = preexisting_capture.capture_id,
.asset_id = asset_a.asset_id},
{.database = database, .capture_id = preexisting_capture.capture_id,
.asset_id = asset_a.asset_id},
};
pthread_t attach_threads[2];
CHECK(pthread_create(&attach_threads[0], NULL, attach_source_thread,
&attach_contexts[0]) == 0);
CHECK(pthread_create(&attach_threads[1], NULL, attach_source_thread,
&attach_contexts[1]) == 0);
CHECK(pthread_join(attach_threads[0], NULL) == 0
&& pthread_join(attach_threads[1], NULL) == 0
&& attach_contexts[0].result == LARDON3D_PROJECT_DB_OK
&& attach_contexts[1].result == LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_attach_capture_source_asset(database, capture_b.capture_id,
asset_a.asset_id) == LARDON3D_PROJECT_DB_OK);
Lardon3DProjectDbCapture conflict_capture;
CHECK(lardon3d_project_db_create_capture(database, a.scanset_id, 2,
&conflict_capture) == LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_attach_capture_asset(database, conflict_capture.capture_id,
concurrent_asset.asset_id, LARDON3D_DB_CAPTURE_ASSET_DERIVED)
== LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_attach_capture_source_asset(database,
conflict_capture.capture_id, concurrent_asset.asset_id)
== LARDON3D_PROJECT_DB_CONSTRAINT);
size_t conflict_count = 0;
CHECK(lardon3d_project_db_list_capture_assets(database, conflict_capture.capture_id, 0,
source_page, 1, &conflict_count) == LARDON3D_PROJECT_DB_OK
&& conflict_count == 1
&& source_page[0].role == LARDON3D_DB_CAPTURE_ASSET_DERIVED);
lardon3d_project_db_close(database);
database = NULL;
state.project_db = NULL;
CHECK(lardon3d_project_db_open(database_path, &database, error)
== LARDON3D_PROJECT_DB_OK);
state.project_db = database;
CHECK(lardon3d_project_db_attach_capture_source_asset(database, capture_a.capture_id,
different_asset.asset_id) == LARDON3D_PROJECT_DB_OK);
source_count = 0;
CHECK(lardon3d_project_db_list_capture_assets(database, capture_a.capture_id, 0,
source_page, 4, &source_count) == LARDON3D_PROJECT_DB_OK && source_count == 3);
uint64_t selection_after = 0, image_count_a_after = 0, image_count_b_after = 0;
CHECK(lardon3d_project_db_get_selected_capture_image(database, capture_a.capture_id,
&selection_after) == LARDON3D_PROJECT_DB_OK && selection_after == selection_before);
CHECK(lardon3d_project_db_count_images(database, a.scanset_id,
&image_count_a_after) == LARDON3D_PROJECT_DB_OK
&& image_count_a_after == image_count_a_before);
CHECK(lardon3d_project_db_count_images(database, b.scanset_id,
&image_count_b_after) == LARDON3D_PROJECT_DB_OK
&& image_count_b_after == image_count_b_before);
char bulk_directory[PATH_MAX];
CHECK(join_path(bulk_directory, root, "bulk") && mkdir(bulk_directory, 0700) == 0);
for (uint64_t index = 1; index <= 2048; ++index) {