diff --git a/internal/repository/postgres/dataset.go b/internal/repository/postgres/dataset.go index 23e64b9..de51ca3 100644 --- a/internal/repository/postgres/dataset.go +++ b/internal/repository/postgres/dataset.go @@ -166,11 +166,22 @@ func (r *DatasetRepository) SetProperties(ctx context.Context, id uuid.UUID, pro return nil } -// MarkReady sets the dataset status to ready and clears any error. -func (r *DatasetRepository) MarkReady(ctx context.Context, id uuid.UUID) error { +// MarkReady sets the dataset status to ready, stores the dissolved feature +// geometry (GeoJSON in EPSG:4326; nil keeps the existing geometry, reduced to +// the union of all features via ST_UnaryUnion), and clears any error. +func (r *DatasetRepository) MarkReady(ctx context.Context, id uuid.UUID, geometry []byte) error { + var geom any // nil -> SQL NULL -> CASE keeps existing geometry + if len(geometry) > 0 { + geom = string(geometry) + } tag, err := r.pool.Exec(ctx, - `UPDATE datasets SET status = $2, parse_error = NULL, updated_at = now() WHERE id = $1`, - id, domain.DatasetStatusReady, + `UPDATE datasets + SET status = $2, + geometry = CASE WHEN $3::text IS NULL THEN geometry + ELSE ST_UnaryUnion(ST_SetSRID(ST_GeomFromGeoJSON($3), 4326)) END, + parse_error = NULL, updated_at = now() + WHERE id = $1`, + id, domain.DatasetStatusReady, geom, ) if err != nil { return mapError(err) diff --git a/internal/service/dataset.go b/internal/service/dataset.go index a6ee1fb..2f96cf7 100644 --- a/internal/service/dataset.go +++ b/internal/service/dataset.go @@ -31,7 +31,7 @@ type DatasetRepository interface { Delete(ctx context.Context, id uuid.UUID) error MarkParsed(ctx context.Context, id uuid.UUID, cols []domain.AttributeColumn) error MarkParseFailed(ctx context.Context, id uuid.UUID, reason string) error - MarkReady(ctx context.Context, id uuid.UUID) error + MarkReady(ctx context.Context, id uuid.UUID, geometry []byte) error MarkConverted(ctx context.Context, id uuid.UUID, cogKey string, footprint []byte) error SetProperties(ctx context.Context, id uuid.UUID, properties, geometry []byte) error SaveMapping(ctx context.Context, id uuid.UUID, katoColumn string, years []domain.YearColumn) (domain.Dataset, error) @@ -515,9 +515,11 @@ func (s *DatasetService) SaveMapping(ctx context.Context, id uuid.UUID, in Mappi } // Extract reads a mapped dataset's file, unpivots its attribute table into -// observations keyed by KATO code and date, and marks the dataset ready. It is -// invoked by the worker. Permanent failures (unparsable file) are recorded; -// transient failures (storage/DB) are returned for retry. +// observations keyed by KATO code and date, records the dissolved feature +// geometry, and marks the dataset ready. It is invoked by the worker. Permanent +// failures (unparsable file) are recorded; transient failures (storage/DB) are +// returned for retry. Geometry extraction is best-effort: a failure leaves +// geometry unset rather than failing the job. func (s *DatasetService) Extract(ctx context.Context, id uuid.UUID) error { dataset, err := s.repo.GetByID(ctx, id) if err != nil { @@ -541,7 +543,9 @@ func (s *DatasetService) Extract(ctx context.Context, id uuid.UUID) error { if err := s.repo.ReplaceObservations(ctx, id, obs); err != nil { return err // transient } - return s.repo.MarkReady(ctx, id) + + geometry := s.vectorGeometry(ctx, dataset.Filename, data) + return s.repo.MarkReady(ctx, id, geometry) } // buildObservations unpivots rows into observations. Rows without a KATO code diff --git a/internal/service/dataset_test.go b/internal/service/dataset_test.go index 547d601..7bb83fb 100644 --- a/internal/service/dataset_test.go +++ b/internal/service/dataset_test.go @@ -101,11 +101,14 @@ func (r *stubDatasetRepo) SaveMapping(_ context.Context, id uuid.UUID, kato stri return d, nil } -func (r *stubDatasetRepo) MarkReady(_ context.Context, id uuid.UUID) error { +func (r *stubDatasetRepo) MarkReady(_ context.Context, id uuid.UUID, geometry []byte) error { d, ok := r.store[id] if !ok { return domain.ErrNotFound } + if len(geometry) > 0 { + d.Geometry = geometry + } d.Status = domain.DatasetStatusReady r.store[id] = d return nil @@ -765,7 +768,9 @@ func TestDatasetService_Extract(t *testing.T) { } rows := []map[string]string{{"като": "751010000", "F_2023": "100"}} rp := RowParser(func(string, []byte) ([]map[string]string, error) { return rows, nil }) - svc := NewDatasetService(repo, &stubStore{}, stubCategoryReader{exists: true}, &stubEnqueuer{}, noopParser, rp, &stubConverter{}) + geom := []byte(`{"type":"GeometryCollection","geometries":[]}`) + conv := &stubConverter{vectorGeom: geom} + svc := NewDatasetService(repo, &stubStore{}, stubCategoryReader{exists: true}, &stubEnqueuer{}, noopParser, rp, conv) if err := svc.Extract(context.Background(), id); err != nil { t.Fatalf("unexpected error: %v", err) @@ -773,6 +778,9 @@ func TestDatasetService_Extract(t *testing.T) { if repo.store[id].Status != domain.DatasetStatusReady { t.Fatalf("want ready, got %q", repo.store[id].Status) } + if string(repo.store[id].Geometry) != string(geom) { + t.Fatalf("want geometry %s, got %s", geom, repo.store[id].Geometry) + } got := repo.observations[id] if len(got) != 1 || got[0].KatoCode != "751010000" || got[0].Value == nil || *got[0].Value != 100 { t.Fatalf("unexpected observations: %+v", got)