Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).


## [UNRELEASED]

### Fixed
- Add datetime: null when an item has start_datetime and end_datetime but no datetime. Fixes (#158)


##[v0.9.9]

### Changed
Expand Down
53 changes: 22 additions & 31 deletions src/pgstac/sql/001a_jsonutils.sql
Original file line number Diff line number Diff line change
Expand Up @@ -108,12 +108,7 @@ BEGIN
THEN
RETURN j;
ELSE
includes := includes || (
CASE WHEN j ? 'collection' THEN
'["id","collection"]'
ELSE
'["id"]'
END)::jsonb;
includes := includes || '["id","collection"]'::jsonb;
FOR path IN SELECT explode_dotpaths(includes) LOOP
outj := jsonb_set_nested(outj, path, j #> path);
END LOOP;
Expand Down Expand Up @@ -151,21 +146,19 @@ CREATE OR REPLACE FUNCTION merge_jsonb(_a jsonb, _b jsonb) RETURNS jsonb AS $$
SELECT
CASE
WHEN _a = '"𒍟※"'::jsonb THEN NULL
WHEN _a IS NULL OR jsonb_typeof(_a) = 'null' THEN _b
WHEN _a IS NULL THEN _b
WHEN jsonb_typeof(_a) = 'object' AND jsonb_typeof(_b) = 'object' THEN
(
SELECT
jsonb_strip_nulls(
jsonb_object_agg(
key,
merge_jsonb(a.value, b.value)
)
)
FROM
jsonb_each(coalesce(_a,'{}'::jsonb)) as a
FULL JOIN
jsonb_each(coalesce(_b,'{}'::jsonb)) as b
USING (key)
SELECT jsonb_object_agg(key, val)
FROM (
SELECT key, merge_jsonb(a.value, b.value) AS val
FROM
jsonb_each(coalesce(_a,'{}'::jsonb)) as a
FULL JOIN
jsonb_each(coalesce(_b,'{}'::jsonb)) as b
USING (key)
) sub
WHERE val IS NOT NULL
)
WHEN
jsonb_typeof(_a) = 'array'
Expand Down Expand Up @@ -196,18 +189,16 @@ CREATE OR REPLACE FUNCTION strip_jsonb(_a jsonb, _b jsonb) RETURNS jsonb AS $$
WHEN _a = _b THEN NULL
WHEN jsonb_typeof(_a) = 'object' AND jsonb_typeof(_b) = 'object' THEN
(
SELECT
jsonb_strip_nulls(
jsonb_object_agg(
key,
strip_jsonb(a.value, b.value)
)
)
FROM
jsonb_each(_a) as a
FULL JOIN
jsonb_each(_b) as b
USING (key)
SELECT jsonb_object_agg(key, val)
FROM (
SELECT key, strip_jsonb(a.value, b.value) AS val
FROM
jsonb_each(_a) as a
FULL JOIN
jsonb_each(_b) as b
USING (key)
) sub
WHERE val IS NOT NULL
)
WHEN
jsonb_typeof(_a) = 'array'
Expand Down
23 changes: 23 additions & 0 deletions src/pgstac/tests/basic/hydration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
-- Test that JSON null values survive the dehydrate/hydrate round-trip
SET ROLE pgstac_ingest;

-- Setup: collection with empty base_item
INSERT INTO collections (content) VALUES ('{"id":"pgstactest-hydration"}');

-- Create item with explicit datetime:null (STAC-compliant temporal range)
SELECT create_item('{
"id": "temporal-range-item",
"collection": "pgstactest-hydration",
"geometry": {"type": "Point", "coordinates": [0, 0]},
"properties": {
"datetime": null,
"start_datetime": "2026-01-01T00:00:00Z",
"end_datetime": "2026-01-31T23:00:00Z"
}
}');

-- Verify datetime:null is preserved in stored content
SELECT content->'properties'->'datetime' FROM items WHERE id='temporal-range-item';

-- Verify datetime:null is preserved after hydration
SELECT get_item('temporal-range-item', 'pgstactest-hydration')->'properties'->'datetime';
25 changes: 25 additions & 0 deletions src/pgstac/tests/basic/hydration.sql.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
-- Test that JSON null values survive the dehydrate/hydrate round-trip
SET ROLE pgstac_ingest;
SET
-- Setup: collection with empty base_item
INSERT INTO collections (content) VALUES ('{"id":"pgstactest-hydration"}');
INSERT 0 1
-- Create item with explicit datetime:null (STAC-compliant temporal range)
SELECT create_item('{
"id": "temporal-range-item",
"collection": "pgstactest-hydration",
"geometry": {"type": "Point", "coordinates": [0, 0]},
"properties": {
"datetime": null,
"start_datetime": "2026-01-01T00:00:00Z",
"end_datetime": "2026-01-31T23:00:00Z"
}
}');

-- Verify datetime:null is preserved in stored content
SELECT content->'properties'->'datetime' FROM items WHERE id='temporal-range-item';
null

-- Verify datetime:null is preserved after hydration
SELECT get_item('temporal-range-item', 'pgstactest-hydration')->'properties'->'datetime';
null
19 changes: 19 additions & 0 deletions src/pgstac/tests/pgtap/003_items.sql
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,22 @@ SELECT results_eq($$
$$,
'Test delete_item function'
);

-- merge_jsonb and strip_jsonb must preserve JSON null values
SELECT results_eq(
$$ SELECT merge_jsonb(
'{"properties": {"datetime": null, "start_datetime": "2026-01-01T00:00:00Z", "end_datetime": "2026-01-31T23:00:00Z"}}'::jsonb,
'{"properties": {}}'::jsonb
)->'properties'->'datetime' $$,
$$ SELECT 'null'::jsonb $$,
'merge_jsonb preserves explicit JSON null values'
);

SELECT results_eq(
$$ SELECT strip_jsonb(
'{"properties": {"datetime": null, "start_datetime": "2026-01-01T00:00:00Z", "end_datetime": "2026-01-31T23:00:00Z"}}'::jsonb,
'{"properties": {}}'::jsonb
)->'properties'->'datetime' $$,
$$ SELECT 'null'::jsonb $$,
'strip_jsonb preserves explicit JSON null values'
);
Loading