@@ -130,20 +130,33 @@ public function extractLinkages(DatasetVersion $dv): void
130130 */
131131 protected function writeLinkages (DatasetVersion $ dv , array $ gwdm ): void
132132 {
133- $ linkage = $ gwdm ['linkage ' ] ?? [];
133+ // A `linkage` key absent entirely means this version's metadata never touched
134+ // linkage (e.g. a partial update) — leave existing junction rows untouched rather
135+ // than treating omission as an intentional clear. A present-but-empty sub-array
136+ // still clears, as before.
137+ if (! array_key_exists ('linkage ' , $ gwdm )) {
138+ return ;
139+ }
134140
135- $ datasetLinkages = $ linkage ['datasetLinkage ' ] ?? null ;
136- $ datasetLinkages = $ datasetLinkages !== '' ? $ datasetLinkages : null ;
141+ $ linkage = $ gwdm ['linkage ' ];
137142
138- $ aboutLinkages = $ linkage ['publicationAboutDataset ' ] ?? null ;
139- $ aboutLinkages = $ aboutLinkages !== '' ? $ aboutLinkages : null ;
143+ if (array_key_exists ('datasetLinkage ' , $ linkage )) {
144+ $ datasetLinkages = $ linkage ['datasetLinkage ' ];
145+ $ datasetLinkages = $ datasetLinkages !== '' ? $ datasetLinkages : null ;
146+ $ this ->processDatasetLinkages ($ dv ->id , $ datasetLinkages );
147+ }
140148
141- $ usingLinkages = $ linkage ['publicationUsingDataset ' ] ?? null ;
142- $ usingLinkages = $ usingLinkages !== '' ? $ usingLinkages : null ;
149+ if (array_key_exists ('publicationAboutDataset ' , $ linkage )) {
150+ $ aboutLinkages = $ linkage ['publicationAboutDataset ' ];
151+ $ aboutLinkages = $ aboutLinkages !== '' ? $ aboutLinkages : null ;
152+ $ this ->processPublicationLinkages ($ dv ->id , $ aboutLinkages , 'ABOUT ' );
153+ }
143154
144- $ this ->processDatasetLinkages ($ dv ->id , $ datasetLinkages );
145- $ this ->processPublicationLinkages ($ dv ->id , $ aboutLinkages , 'ABOUT ' );
146- $ this ->processPublicationLinkages ($ dv ->id , $ usingLinkages , 'USING ' );
155+ if (array_key_exists ('publicationUsingDataset ' , $ linkage )) {
156+ $ usingLinkages = $ linkage ['publicationUsingDataset ' ];
157+ $ usingLinkages = $ usingLinkages !== '' ? $ usingLinkages : null ;
158+ $ this ->processPublicationLinkages ($ dv ->id , $ usingLinkages , 'USING ' );
159+ }
147160 }
148161
149162 protected function processDatasetLinkages (int $ sourceVersionId , ?array $ datasetLinkages ): void
@@ -270,37 +283,10 @@ protected function findTargetPublication(string $doi): ?int
270283 */
271284 public function afterRead (DatasetVersion $ dv ): array
272285 {
273- $ resolvedDatasets = DB ::select (
274- 'SELECT
275- dataset_version_has_dataset_version.linkage_type,
276- dv.short_title,
277- d.pid,
278- d.id AS dataset_id
279- FROM dataset_version_has_dataset_version
280- INNER JOIN dataset_versions AS dv ON dv.id = dataset_version_has_dataset_version.dataset_version_target_id
281- INNER JOIN datasets AS d ON d.id = dv.dataset_id
282- WHERE dataset_version_has_dataset_version.dataset_version_source_id = ?
283- AND dataset_version_has_dataset_version.direct_linkage = ?
284- AND dataset_version_has_dataset_version.description = ? ' ,
285- [$ dv ->id , 1 , self ::LINKAGE_DESCRIPTION ]
286- );
286+ $ resolvedDatasets = $ this ->resolveDatasetLinkages ($ dv ->id , useLatestTitle: true );
287+ $ publications = $ this ->resolvePublicationLinkages ($ dv ->id );
287288
288- $ publications = DB ::select (
289- 'SELECT
290- publication_has_dataset_version.link_type,
291- publications.paper_doi
292- FROM publication_has_dataset_version
293- INNER JOIN publications ON publications.id = publication_has_dataset_version.publication_id
294- WHERE publication_has_dataset_version.dataset_version_id = ?
295- AND publication_has_dataset_version.description = ?
296- AND publication_has_dataset_version.deleted_at IS NULL ' ,
297- [$ dv ->id , self ::LINKAGE_DESCRIPTION ]
298- );
299-
300- $ hasExtractedRows = ! empty ($ resolvedDatasets )
301- || ! empty ($ publications );
302-
303- if (! $ hasExtractedRows ) {
289+ if (empty ($ resolvedDatasets ) && empty ($ publications )) {
304290 return [];
305291 }
306292
@@ -309,14 +295,14 @@ public function afterRead(DatasetVersion $dv): array
309295 $ datasetLinkage [$ row ->linkage_type ][] = [
310296 'url ' => config ('gateway.gateway_url ' ).'/en/dataset/ ' .$ row ->dataset_id ,
311297 'pid ' => $ row ->pid ,
312- 'title ' => $ row ->short_title ,
298+ 'title ' => $ row ->title ,
313299 ];
314300 }
315301
316302 $ aboutDataset = [];
317303 $ usingDataset = [];
318304 foreach ($ publications as $ row ) {
319- $ doi = $ row ->paper_doi ;
305+ $ doi = $ row ->doi ;
320306 if (! $ doi ) {
321307 continue ;
322308 }
@@ -335,4 +321,131 @@ public function afterRead(DatasetVersion $dv): array
335321 ],
336322 ];
337323 }
324+
325+ /**
326+ * Single source of truth for dataset-linkage row selection, shared by
327+ * afterRead() (GWDM metadata block) and getLinkages() (flat attribute).
328+ * SQL is authoritative for 2.x linkage data on reads.
329+ *
330+ * Rules:
331+ * - direct_linkage = 1 rows for the given source version.
332+ * - Target resolved via target version -> dataset_id -> datasets using LEFT
333+ * joins (robust to a missing/hard-deleted target version row).
334+ * - Only targets that exist (not soft-deleted) and are ACTIVE are returned;
335+ * unresolved / archived / deleted targets are dropped.
336+ * - Title tracks the target dataset's CURRENT latest short_title when
337+ * $useLatestTitle is true, so it stays consistent with the dataset-level URL;
338+ * otherwise (default) the frozen short_title captured at extraction time is
339+ * used as-is. When $useLatestTitle is true, the frozen short_title is still
340+ * the fallback if the latest lookup misses.
341+ *
342+ * @return array<int, object{linkage_type: string, dataset_id: int, pid: string, title: ?string}>
343+ */
344+ protected function resolveDatasetLinkages (int $ sourceVersionId , bool $ useLatestTitle = false ): array
345+ {
346+ $ rows = collect (DB ::select (
347+ 'SELECT
348+ dataset_version_has_dataset_version.linkage_type,
349+ datasets.id AS dataset_id,
350+ datasets.pid,
351+ dataset_versions.short_title AS frozen_short_title
352+ FROM dataset_version_has_dataset_version
353+ LEFT JOIN dataset_versions
354+ ON dataset_versions.id = dataset_version_has_dataset_version.dataset_version_target_id
355+ AND dataset_versions.deleted_at IS NULL
356+ LEFT JOIN datasets
357+ ON datasets.id = dataset_versions.dataset_id
358+ AND datasets.deleted_at IS NULL
359+ WHERE dataset_version_has_dataset_version.dataset_version_source_id = ?
360+ AND dataset_version_has_dataset_version.direct_linkage = ?
361+ AND datasets.status = ? ' ,
362+ [$ sourceVersionId , 1 , Dataset::STATUS_ACTIVE ]
363+ ));
364+
365+ // this is a refactor candidate
366+ // - there are inconsistencies with the dataset title in the linkages if a new dataset version
367+ // is published and changes the title (unlikely)
368+ // - all due to linkages being on dataset-version rather that dataset
369+ // - for now, keep at it is, but a method has been added to look up the latest tittle
370+ $ latestTitles = $ useLatestTitle
371+ ? $ this ->latestShortTitlesFor ($ rows ->pluck ('dataset_id ' )->filter ()->unique ()->all ())
372+ : [];
373+
374+ return $ rows
375+ ->map (fn ($ row ) => (object ) [
376+ 'linkage_type ' => $ row ->linkage_type ,
377+ 'dataset_id ' => (int ) $ row ->dataset_id ,
378+ 'pid ' => $ row ->pid ,
379+ 'title ' => $ latestTitles [$ row ->dataset_id ] ?? $ row ->frozen_short_title ,
380+ ])
381+ ->values ()
382+ ->all ();
383+ }
384+
385+ /**
386+ * Resolved publication linkages for the given source version, sourced from SQL.
387+ * Companion to resolveDatasetLinkages(); consumed by afterRead() to rebuild the
388+ * publicationAboutDataset / publicationUsingDataset arrays.
389+ *
390+ * @return array<int, object{link_type: string, doi: string}>
391+ */
392+ protected function resolvePublicationLinkages (int $ sourceVersionId ): array
393+ {
394+ return DB ::select (
395+ 'SELECT
396+ publication_has_dataset_version.link_type,
397+ publications.paper_doi AS doi
398+ FROM publication_has_dataset_version
399+ INNER JOIN publications
400+ ON publications.id = publication_has_dataset_version.publication_id
401+ AND publications.deleted_at IS NULL
402+ WHERE publication_has_dataset_version.dataset_version_id = ?
403+ AND publication_has_dataset_version.description = ?
404+ AND publication_has_dataset_version.deleted_at IS NULL ' ,
405+ [$ sourceVersionId , self ::LINKAGE_DESCRIPTION ]
406+ );
407+ }
408+
409+ /**
410+ * Flat dataset-linkage list for the given version (frontend `linkages` attribute).
411+ * Thin formatter over resolveDatasetLinkages() — see it for the selection rules.
412+ */
413+ public function getLinkages (int $ datasetVersionId ): array
414+ {
415+ return array_map (
416+ fn ($ row ) => [
417+ 'title ' => $ row ->title ,
418+ 'url ' => config ('gateway.gateway_url ' ).'/en/dataset/ ' .$ row ->dataset_id ,
419+ 'dataset_id ' => $ row ->dataset_id ,
420+ 'linkage_type ' => $ row ->linkage_type ,
421+ ],
422+ $ this ->resolveDatasetLinkages ($ datasetVersionId , useLatestTitle: true )
423+ );
424+ }
425+
426+ /**
427+ * Latest-version short_title for each given dataset id, keyed by dataset id.
428+ *
429+ * Linkage junction rows freeze a `dataset_version_target_id` (whatever was latest at
430+ * extraction time), but the read-back URL resolves to the dataset — i.e. its current
431+ * latest version. Resolving titles from the latest version here keeps the displayed
432+ * title consistent with the URL once the target dataset gains newer versions.
433+ *
434+ * Reuses Dataset::latestMetadata() (latestOfMany('version'), soft-delete aware).
435+ *
436+ * @param array<int, int> $datasetIds
437+ * @return array<int, string|null>
438+ */
439+ protected function latestShortTitlesFor (array $ datasetIds ): array
440+ {
441+ if (empty ($ datasetIds )) {
442+ return [];
443+ }
444+
445+ return Dataset::whereIn ('id ' , $ datasetIds )
446+ ->with ('latestMetadata ' )
447+ ->get ()
448+ ->mapWithKeys (fn (Dataset $ d ) => [$ d ->id => $ d ->latestMetadata ?->short_title])
449+ ->all ();
450+ }
338451}
0 commit comments