From e6e72710945a75c21c6703f926853bd6dbe41a64 Mon Sep 17 00:00:00 2001 From: fy59 Date: Thu, 27 Aug 2026 10:49:04 +0200 Subject: [PATCH] feat: add multi-source capture association --- docs/architecture/project_database.md | 6 ++ include/lardon3d/project_db.h | 2 + src/project_db.c | 22 ++++++ tests/test_persistent_image_catalog.c | 100 ++++++++++++++++++++++++++ 4 files changed, 130 insertions(+) diff --git a/docs/architecture/project_database.md b/docs/architecture/project_database.md index 0c1deca..cdfda27 100644 --- a/docs/architecture/project_database.md +++ b/docs/architecture/project_database.md @@ -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 diff --git a/include/lardon3d/project_db.h b/include/lardon3d/project_db.h index 7b0cf54..bd223af 100644 --- a/include/lardon3d/project_db.h +++ b/include/lardon3d/project_db.h @@ -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); diff --git a/src/project_db.c b/src/project_db.c index 278897d..7308b18 100644 --- a/src/project_db.c +++ b/src/project_db.c @@ -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) { diff --git a/tests/test_persistent_image_catalog.c b/tests/test_persistent_image_catalog.c index 4bc5d60..c99eb73 100644 --- a/tests/test_persistent_image_catalog.c +++ b/tests/test_persistent_image_catalog.c @@ -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) {