Skip to content

Commit e9cebfb

Browse files
committed
Restore PostgreSQL file metadata on export
1 parent 0279621 commit e9cebfb

2 files changed

Lines changed: 89 additions & 8 deletions

File tree

‎src/Sql/PostgreSqlWideTableExporter.php‎

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,17 +69,22 @@ public function export(string $datasetName, string $targetFormat = 'sav'): array
6969
);
7070
}
7171

72+
$metadata = new FileMetadata(
73+
label: $this->fileLabel($datasetName),
74+
documents: $this->documents($datasetName),
75+
);
76+
7277
return [
7378
'dataset' => new Dataset(
7479
new VariableDictionary($typedVariables),
7580
$rows,
76-
new FileMetadata(),
77-
new FileTechnicalMetadata(sourceFormat: $targetFormat, compression: $targetFormat === 'zsav' ? 2 : 1),
81+
$metadata,
82+
$this->technicalMetadata($datasetName, $targetFormat),
7883
),
7984
'caseCount' => count($rows),
8085
'diagnostics' => [new FidelityDiagnostic(
8186
'postgresql_dictionary_metadata_deferred',
82-
'PostgreSQL export preserves strict-wide case values, core variable definitions, value labels, user-missing rules, and display settings. File-level metadata, attributes, variable sets, and multiple-response sets require later catalogue-export work.',
87+
'PostgreSQL export preserves strict-wide case values, core variable definitions, value labels, user-missing rules, display settings, file label, ordered documents, and file technical metadata. File attributes, variable attributes, variable sets, and multiple-response sets require later catalogue-export work.',
8388
)],
8489
];
8590
}
@@ -255,6 +260,45 @@ private function numeric(int|float|string $value): int|float
255260
throw new UnsupportedOperation(DiagnosticCode::InvalidSourceDataset, 'SPSS missing-value ranges require numeric endpoints.');
256261
}
257262

263+
private function fileLabel(string $datasetName): ?string
264+
{
265+
$statement = $this->statement('SELECT meta_value FROM dataset_metadata WHERE dataset_name = ? AND meta_key = ?');
266+
$statement->execute([$datasetName, 'file_label']);
267+
$label = $statement->fetchColumn();
268+
269+
return is_string($label) ? $label : null;
270+
}
271+
272+
/** @return list<string> */
273+
private function documents(string $datasetName): array
274+
{
275+
$statement = $this->statement('SELECT text FROM documents WHERE dataset_name = ? ORDER BY ordinal');
276+
$statement->execute([$datasetName]);
277+
278+
return array_values(array_filter($statement->fetchAll(PDO::FETCH_COLUMN), 'is_string'));
279+
}
280+
281+
/** Rebuild the V3 file fields whose SAV/ZSAV writer representation is supported. */
282+
private function technicalMetadata(string $datasetName, string $targetFormat): FileTechnicalMetadata
283+
{
284+
$statement = $this->statement(
285+
'SELECT source_version, provenance, encoding, product_name FROM file_technical_metadata WHERE dataset_name = ?',
286+
);
287+
$statement->execute([$datasetName]);
288+
$row = $statement->fetch(PDO::FETCH_ASSOC);
289+
$targetIsZsav = $targetFormat === 'zsav';
290+
291+
return new FileTechnicalMetadata(
292+
sourceFormat: $targetFormat,
293+
recordType: $targetIsZsav ? '$FL3' : '$FL2',
294+
sourceVersion: is_array($row) && is_string($row['source_version'] ?? null) && $row['source_version'] !== '' ? $row['source_version'] : null,
295+
provenance: is_array($row) && is_string($row['provenance'] ?? null) && $row['provenance'] !== '' ? $row['provenance'] : null,
296+
encoding: is_array($row) && is_string($row['encoding'] ?? null) && $row['encoding'] !== '' ? $row['encoding'] : 'UTF-8',
297+
productName: is_array($row) && is_string($row['product_name'] ?? null) && $row['product_name'] !== '' ? $row['product_name'] : null,
298+
compression: $targetIsZsav ? 2 : 1,
299+
);
300+
}
301+
258302
private function caseValue(mixed $value, string $storageKind): int|float|string|null
259303
{
260304
if ($storageKind === 'string') {

‎tests/Spss/PostgreSqlProfileSelectionTest.php‎

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ public function testPostgreSqlDriverSelectsProfileAndExportsStrictWideDatasetThr
7474
$dataset = $this->createMock(PDOStatement::class);
7575
$variables = $this->createMock(PDOStatement::class);
7676
$cases = $this->createMock(PDOStatement::class);
77+
$fileLabel = $this->createMock(PDOStatement::class);
78+
$documents = $this->createMock(PDOStatement::class);
79+
$technical = $this->createMock(PDOStatement::class);
7780
$scoreLabels = $this->createMock(PDOStatement::class);
7881
$scoreMissing = $this->createMock(PDOStatement::class);
7982
$commentLabels = $this->createMock(PDOStatement::class);
@@ -82,9 +85,9 @@ public function testPostgreSqlDriverSelectsProfileAndExportsStrictWideDatasetThr
8285
$commentDisplay = $this->createMock(PDOStatement::class);
8386
$engine = $this->createMock(SpssEngine::class);
8487

85-
$pdo->expects(self::exactly(9))
88+
$pdo->expects(self::exactly(12))
8689
->method('prepare')
87-
->willReturnOnConsecutiveCalls($dataset, $variables, $cases, $scoreLabels, $scoreMissing, $scoreDisplay, $commentLabels, $commentMissing, $commentDisplay);
90+
->willReturnOnConsecutiveCalls($dataset, $variables, $cases, $scoreLabels, $scoreMissing, $scoreDisplay, $commentLabels, $commentMissing, $commentDisplay, $fileLabel, $documents, $technical);
8891
$dataset->expects(self::once())->method('execute')->with(['fixture'])->willReturn(true);
8992
$dataset->expects(self::once())->method('fetch')->willReturn(['table_name' => 'dataset_fixture']);
9093
$variables->expects(self::once())->method('execute')->with(['fixture'])->willReturn(true);
@@ -98,6 +101,17 @@ public function testPostgreSqlDriverSelectsProfileAndExportsStrictWideDatasetThr
98101
['score' => null, 'comment' => 'green'],
99102
false,
100103
);
104+
$fileLabel->expects(self::once())->method('execute')->with(['fixture', 'file_label'])->willReturn(true);
105+
$fileLabel->expects(self::once())->method('fetchColumn')->willReturn('Customer survey source');
106+
$documents->expects(self::once())->method('execute')->with(['fixture'])->willReturn(true);
107+
$documents->expects(self::once())->method('fetchAll')->with(PDO::FETCH_COLUMN)->willReturn(['First document line', 'Second document line']);
108+
$technical->expects(self::once())->method('execute')->with(['fixture'])->willReturn(true);
109+
$technical->expects(self::once())->method('fetch')->willReturn([
110+
'source_version' => '31.0',
111+
'provenance' => 'unit-test',
112+
'encoding' => 'UTF-8',
113+
'product_name' => 'SPSS Statistics',
114+
]);
101115
$scoreLabels->expects(self::once())->method('execute')->with(['fixture', 1])->willReturn(true);
102116
$scoreLabels->expects(self::once())->method('fetch')->willReturn(false);
103117
$scoreMissing->expects(self::once())->method('execute')->with(['fixture', 1])->willReturn(true);
@@ -122,7 +136,13 @@ public function testPostgreSqlDriverSelectsProfileAndExportsStrictWideDatasetThr
122136
self::assertSame(Measure::NOMINAL, $written->variables()[1]->measure);
123137
self::assertSame(24, $written->variables()[1]->columns);
124138
self::assertSame(Alignment::LEFT, $written->variables()[1]->alignment);
139+
self::assertSame('Customer survey source', $written->metadata->label);
140+
self::assertSame(['First document line', 'Second document line'], $written->metadata->documents());
125141
self::assertSame('zsav', $written->technicalMetadata->sourceFormat);
142+
self::assertSame('31.0', $written->technicalMetadata->sourceVersion);
143+
self::assertSame('unit-test', $written->technicalMetadata->provenance);
144+
self::assertSame('UTF-8', $written->technicalMetadata->encoding);
145+
self::assertSame('SPSS Statistics', $written->technicalMetadata->productName);
126146

127147
return true;
128148
}));
@@ -132,7 +152,8 @@ public function testPostgreSqlDriverSelectsProfileAndExportsStrictWideDatasetThr
132152
self::assertSame(2, $result->caseCount);
133153
self::assertSame('postgresql_dictionary_metadata_deferred', $result->diagnostics[0]->code);
134154
self::assertStringContainsString('display settings', $result->diagnostics[0]->message);
135-
self::assertStringNotContainsString('Display settings require', $result->diagnostics[0]->message);
155+
self::assertStringContainsString('file label, ordered documents, and file technical metadata', $result->diagnostics[0]->message);
156+
self::assertStringNotContainsString('File-level metadata', $result->diagnostics[0]->message);
136157
}
137158

138159
public function testPostgreSqlExportRestoresOrderedTypedLabelsAndAllUserMissingRuleForms(): void
@@ -141,6 +162,9 @@ public function testPostgreSqlExportRestoresOrderedTypedLabelsAndAllUserMissingR
141162
$dataset = $this->createMock(PDOStatement::class);
142163
$variables = $this->createMock(PDOStatement::class);
143164
$cases = $this->createMock(PDOStatement::class);
165+
$fileLabel = $this->createMock(PDOStatement::class);
166+
$documents = $this->createMock(PDOStatement::class);
167+
$technical = $this->createMock(PDOStatement::class);
144168
$numericLabels = $this->createMock(PDOStatement::class);
145169
$numericRule = $this->createMock(PDOStatement::class);
146170
$numericValues = $this->createMock(PDOStatement::class);
@@ -151,7 +175,7 @@ public function testPostgreSqlExportRestoresOrderedTypedLabelsAndAllUserMissingR
151175
$textDisplay = $this->createMock(PDOStatement::class);
152176
$engine = $this->createMock(SpssEngine::class);
153177

154-
$pdo->expects(self::exactly(11))
178+
$pdo->expects(self::exactly(14))
155179
->method('prepare')
156180
->willReturnOnConsecutiveCalls(
157181
$dataset,
@@ -165,6 +189,9 @@ public function testPostgreSqlExportRestoresOrderedTypedLabelsAndAllUserMissingR
165189
$textRule,
166190
$textValues,
167191
$textDisplay,
192+
$fileLabel,
193+
$documents,
194+
$technical,
168195
);
169196
$dataset->expects(self::once())->method('execute')->with(['fixture'])->willReturn(true);
170197
$dataset->expects(self::once())->method('fetch')->willReturn(['table_name' => 'dataset_fixture']);
@@ -176,6 +203,13 @@ public function testPostgreSqlExportRestoresOrderedTypedLabelsAndAllUserMissingR
176203
$cases->expects(self::once())->method('execute')->with()->willReturn(true);
177204
$cases->expects(self::exactly(2))->method('fetch')->willReturnOnConsecutiveCalls(['score' => '2', 'reason' => 'MISSING'], false);
178205

206+
$fileLabel->expects(self::once())->method('execute')->with(['fixture', 'file_label'])->willReturn(true);
207+
$fileLabel->expects(self::once())->method('fetchColumn')->willReturn(false);
208+
$documents->expects(self::once())->method('execute')->with(['fixture'])->willReturn(true);
209+
$documents->expects(self::once())->method('fetchAll')->with(PDO::FETCH_COLUMN)->willReturn([]);
210+
$technical->expects(self::once())->method('execute')->with(['fixture'])->willReturn(true);
211+
$technical->expects(self::once())->method('fetch')->willReturn(false);
212+
179213
$numericLabels->expects(self::once())->method('execute')->with(['fixture', 1])->willReturn(true);
180214
$numericLabels->expects(self::exactly(3))->method('fetch')->willReturnOnConsecutiveCalls(
181215
['value_kind' => 'numeric', 'numeric_value' => '2', 'text_value' => null, 'label' => 'No'],
@@ -225,6 +259,9 @@ public function testPostgreSqlExportRestoresOrderedTypedLabelsAndAllUserMissingR
225259
new ValueLabel('REFUSED', 'Refused'),
226260
], $variables[1]->valueLabels->labels());
227261
self::assertEquals(MissingValues::discrete('MISSING', 'REFUSED'), $variables[1]->missingValues);
262+
self::assertNull($written->metadata->label);
263+
self::assertSame([], $written->metadata->documents());
264+
self::assertSame('UTF-8', $written->technicalMetadata->encoding);
228265

229266
return true;
230267
}));
@@ -233,7 +270,7 @@ public function testPostgreSqlExportRestoresOrderedTypedLabelsAndAllUserMissingR
233270

234271
self::assertSame(1, $result->caseCount);
235272
self::assertSame('postgresql_dictionary_metadata_deferred', $result->diagnostics[0]->code);
236-
self::assertStringContainsString('value labels, user-missing rules, and display settings', $result->diagnostics[0]->message);
273+
self::assertStringContainsString('value labels, user-missing rules, display settings', $result->diagnostics[0]->message);
237274
self::assertStringContainsString('display settings', $result->diagnostics[0]->message);
238275
}
239276

0 commit comments

Comments
 (0)