Skip to content

Commit 248c5de

Browse files
calmacxclaude
andauthored
fix(GAT-9222): dataset-version linkage cleanup (#1704)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent b0874f9 commit 248c5de

4 files changed

Lines changed: 468 additions & 84 deletions

File tree

app/Services/DatasetService.php

Lines changed: 8 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
use App\Models\DataAccessTemplate;
99
use App\Models\Dataset;
1010
use App\Models\DatasetVersion;
11-
use App\Models\DatasetVersionHasDatasetVersion;
1211
use App\Models\DatasetVersionHasSpatialCoverage;
1312
use App\Models\SpatialCoverage;
1413
use App\Models\Team;
@@ -36,8 +35,7 @@ class DatasetService
3635
public function __construct(
3736
private readonly GwdmVersionContext $gwdmVersionContext,
3837
private readonly GwdmHandlerFactory $handlerFactory,
39-
) {
40-
}
38+
) {}
4139

4240
public function list(
4341
?string $filterStatus,
@@ -209,7 +207,13 @@ public function prepareForShow(
209207
$dataset->setRelation('versions', collect([$withLinks]));
210208
}
211209

212-
$dataset->linkages = $this->getLinkages($latestVersionId);
210+
if ($latestVersionId !== null) {
211+
$latestGwdmVersion = $allVersions->firstWhere('id', $latestVersionId)?->gwdm_version
212+
?? $this->gwdmVersionContext->targetVersion();
213+
$dataset->linkages = $this->handlerFactory->resolve($latestGwdmVersion)->getLinkages($latestVersionId);
214+
} else {
215+
$dataset->linkages = [];
216+
}
213217

214218
$dataset->team->has_published_dar_template = DataAccessTemplate::where('team_id', $dataset->team->id)
215219
->where('published', 1)
@@ -582,44 +586,6 @@ public function delete(int $id): void
582586
MMC::deleteDataset($id);
583587
}
584588

585-
/**
586-
/**
587-
* Return dataset linkages for the given version, sourced entirely from SQL.
588-
* Resolved rows are joined to datasets; unresolved rows carry the raw reference
589-
* data stored during extraction. Only resolved linkages whose target is ACTIVE
590-
* are included; unresolved rows are always included.
591-
*/
592-
public function getLinkages(int $datasetVersionId): array
593-
{
594-
// SQL is the complete source of truth for linkage data. Rows are joined
595-
// to dataset_versions/datasets to read the current title/status.
596-
return DatasetVersionHasDatasetVersion::query()
597-
->where('dataset_version_has_dataset_version.dataset_version_source_id', $datasetVersionId)
598-
->where('dataset_version_has_dataset_version.direct_linkage', 1)
599-
->leftJoin('dataset_versions', 'dataset_versions.id', '=', 'dataset_version_has_dataset_version.dataset_version_target_id')
600-
->leftJoin('datasets', 'datasets.id', '=', 'dataset_versions.dataset_id')
601-
->where(function ($q) {
602-
$q->whereNull('datasets.id')
603-
->orWhere('datasets.status', Dataset::STATUS_ACTIVE);
604-
})
605-
->select([
606-
'dataset_version_has_dataset_version.linkage_type',
607-
'dataset_versions.short_title',
608-
'datasets.id as target_dataset_id',
609-
])
610-
->get()
611-
->map(fn ($row) => [
612-
'title' => $row->short_title,
613-
'url' => $row->target_dataset_id
614-
? config('gateway.gateway_url').'/en/dataset/'.$row->target_dataset_id
615-
: null,
616-
'dataset_id' => $row->target_dataset_id,
617-
'linkage_type' => $row->linkage_type,
618-
])
619-
->values()
620-
->toArray();
621-
}
622-
623589
/**
624590
* V2 (legacy) version persistence — overwrites the existing version row.
625591
* Mirrors MetadataVersioning@updateMetadataVersion for the service layer.

app/Services/Gwdm/Gwdm2xHandler.php

Lines changed: 155 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -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
}

app/Services/Gwdm/GwdmMetadataHandler.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,21 @@ public function extractLinkages(DatasetVersion $dv): void
189189
// no-op for versions without linkage extraction support
190190
}
191191

192+
/**
193+
* Return the flat dataset-linkage list for a version (the frontend
194+
* `linkages` attribute: {title, url, dataset_id, linkage_type}).
195+
*
196+
* No-op for the base class. Gwdm2xHandler reads it from the
197+
* dataset_version_has_dataset_version junction table. Companion to
198+
* extractLinkages() (write) and afterRead() (nested metadata block) — all
199+
* three read/write the same handler-owned junction tables, so a
200+
* version-specific handler overrides them together.
201+
*/
202+
public function getLinkages(int $datasetVersionId): array
203+
{
204+
return [];
205+
}
206+
192207
// ── Onboarding form defaults ──────────────────────────────────────────────
193208

194209
/**

0 commit comments

Comments
 (0)