-- -- Edge case and crash-resistance tests for pgcollection 2.0.0 -- -- Focuses on: iterator stability, NULL handling, boundary conditions, -- single-element collections, memory safety under rapid mutation, -- and flatten/unflatten roundtrips. -- ---------------------------------------------------------------------- -- 1. Iterator stability after deleting the CURRENT element ---------------------------------------------------------------------- DO $$ DECLARE c collection; k text; BEGIN c := add(add(add(NULL::collection, 'a', '1'), 'b', '2'), 'c', '3'); c := first(c); c := next(c); -- now on 'b' ASSERT key(c) = 'b', 'should be on b'; c := delete(c, 'b'); -- delete current -- iterator should advance to next element, not crash ASSERT NOT isnull(c), 'iterator should not be null after deleting current'; RAISE NOTICE 'col delete-current key: %', key(c); END; $$; NOTICE: col delete-current key: c DO $$ DECLARE ic icollection; k bigint; BEGIN ic := add(add(add(NULL::icollection, 1, 'a'), 2, 'b'), 3, 'c'); ic := first(ic); ic := next(ic); -- now on 2 ASSERT key(ic) = 2, 'should be on 2'; ic := delete(ic, 2); -- delete current ASSERT NOT isnull(ic), 'ic iterator should not be null after deleting current'; RAISE NOTICE 'icol delete-current key: %', key(ic); END; $$; NOTICE: icol delete-current key: 3 ---------------------------------------------------------------------- -- 2. Single-element collection: delete only element ---------------------------------------------------------------------- DO $$ DECLARE c collection; BEGIN c := add(NULL::collection, 'only', 'val'); ASSERT count(c) = 1, 'single element count'; c := delete(c, 'only'); ASSERT count(c) = 0, 'count after deleting only element'; ASSERT isnull(c), 'iterator null after deleting only element'; ASSERT key(c) IS NULL, 'key null after deleting only element'; ASSERT value(c) IS NULL, 'value null after deleting only element'; END; $$; DO $$ DECLARE ic icollection; BEGIN ic := add(NULL::icollection, 42, 'val'); ASSERT count(ic) = 1, 'ic single element count'; ic := delete(ic, 42); ASSERT count(ic) = 0, 'ic count after deleting only element'; ASSERT isnull(ic), 'ic iterator null after deleting only element'; END; $$; ---------------------------------------------------------------------- -- 3. Single-element: copy, sort, iterate ---------------------------------------------------------------------- DO $$ DECLARE c collection; c2 collection; BEGIN c := add(NULL::collection, 'solo', 'data'); c2 := copy(c); ASSERT count(c2) = 1, 'copy of single-element'; ASSERT find(c2, 'solo') = 'data', 'copy value matches'; c := sort(c); ASSERT count(c) = 1, 'sort single-element'; c := first(c); ASSERT key(c) = 'solo', 'first on single-element'; c := last(c); ASSERT key(c) = 'solo', 'last on single-element'; c := next(c); ASSERT isnull(c), 'next past single-element'; END; $$; ---------------------------------------------------------------------- -- 4. Delete non-existent key (collection, text-keyed) ---------------------------------------------------------------------- DO $$ DECLARE c collection; BEGIN c := add(NULL::collection, 'a', '1'); c := delete(c, 'nonexistent'); ASSERT count(c) = 1, 'delete non-existent should not change count'; ASSERT find(c, 'a') = '1', 'original entry intact'; END; $$; ---------------------------------------------------------------------- -- 5. next/prev boundary on collection (text-keyed) ---------------------------------------------------------------------- DO $$ DECLARE c collection; BEGIN c := add(add(NULL::collection, 'a', '1'), 'b', '2'); c := sort(c); -- prev from first c := first(c); c := prev(c); ASSERT isnull(c), 'prev from first should give null iterator'; -- next from last c := add(NULL::collection, 'a', '1'); c := add(c, 'b', '2'); c := last(c); c := next(c); ASSERT isnull(c), 'next from last should give null iterator'; END; $$; ---------------------------------------------------------------------- -- 6. next_key/prev_key on collection (text-keyed) boundaries ---------------------------------------------------------------------- DO $$ DECLARE c collection; ok boolean; BEGIN c := add(add(add(NULL::collection, 'a', '1'), 'b', '2'), 'c', '3'); c := sort(c); ASSERT next_key(c, 'a') = 'b', 'next_key from a'; ASSERT prev_key(c, 'c') = 'b', 'prev_key from c'; ASSERT next_key(c, 'c') IS NULL, 'next_key from last is null'; ASSERT prev_key(c, 'a') IS NULL, 'prev_key from first is null'; -- next_key on missing key should error ok := false; BEGIN PERFORM next_key(c, 'zzz'); EXCEPTION WHEN no_data_found THEN ok := true; END; ASSERT ok, 'next_key on missing key should error'; -- prev_key on missing key should error ok := false; BEGIN PERFORM prev_key(c, 'zzz'); EXCEPTION WHEN no_data_found THEN ok := true; END; ASSERT ok, 'prev_key on missing key should error'; END; $$; ---------------------------------------------------------------------- -- 7. SRF on empty collection (text-keyed) — use add/delete to get empty ---------------------------------------------------------------------- DO $$ DECLARE c collection; n int; BEGIN c := add(NULL::collection, 'tmp', 'val'); c := delete(c, 'tmp'); ASSERT count(c) = 0, 'empty via delete'; SELECT count(*) INTO n FROM keys_to_table(c); ASSERT n = 0, 'keys_to_table on empty'; SELECT count(*) INTO n FROM values_to_table(c); ASSERT n = 0, 'values_to_table on empty'; SELECT count(*) INTO n FROM to_table(c); ASSERT n = 0, 'to_table on empty'; END; $$; DO $$ DECLARE ic icollection; n int; BEGIN ic := add(NULL::icollection, 1, 'val'); ic := delete(ic, 1); ASSERT count(ic) = 0, 'ic empty via delete'; SELECT count(*) INTO n FROM keys_to_table(ic); ASSERT n = 0, 'ic keys_to_table on empty'; SELECT count(*) INTO n FROM values_to_table(ic); ASSERT n = 0, 'ic values_to_table on empty'; SELECT count(*) INTO n FROM to_table(ic); ASSERT n = 0, 'ic to_table on empty'; END; $$; ---------------------------------------------------------------------- -- 8. Subscript assign to create collection from NULL variable ---------------------------------------------------------------------- DO $$ DECLARE c collection; ic icollection; BEGIN -- c is NULL, subscript assign should create a new collection c['hello'] := 'world'; ASSERT count(c) = 1, 'subscript assign from null collection'; ASSERT c['hello'] = 'world', 'subscript fetch after null assign'; -- ic is NULL, subscript assign should create a new icollection ic[1] := 'world'; ASSERT count(ic) = 1, 'subscript assign from null icollection'; ASSERT ic[1] = 'world', 'subscript fetch after null icollection assign'; END; $$; ---------------------------------------------------------------------- -- 9. Subscript with NULL key ---------------------------------------------------------------------- DO $$ DECLARE c collection; ok boolean; BEGIN c := add(NULL::collection, 'a', '1'); -- NULL subscript in assignment should error ok := false; BEGIN c[NULL] := 'val'; EXCEPTION WHEN null_value_not_allowed THEN ok := true; END; ASSERT ok, 'null subscript assign should error'; END; $$; DO $$ DECLARE ic icollection; ok boolean; BEGIN ic := add(NULL::icollection, 1, 'a'); ok := false; BEGIN ic[NULL] := 'val'; EXCEPTION WHEN null_value_not_allowed THEN ok := true; END; ASSERT ok, 'ic null subscript assign should error'; END; $$; ---------------------------------------------------------------------- -- 10. Flatten roundtrip: NULL values survive persist ---------------------------------------------------------------------- CREATE TABLE edge_persist_test(id serial, c collection, ic icollection); INSERT INTO edge_persist_test(c, ic) VALUES ( add(add(NULL::collection, 'k1', 'v1'), 'k2', NULL::text), add(add(NULL::icollection, 1, 'v1'), 2, NULL::text) ); DO $$ DECLARE r record; BEGIN SELECT c, ic INTO r FROM edge_persist_test WHERE id = 1; ASSERT find(r.c, 'k1') = 'v1', 'collection null-value roundtrip k1'; ASSERT find(r.c, 'k2') IS NULL, 'collection null-value roundtrip k2'; ASSERT find(r.ic, 1) = 'v1', 'icollection null-value roundtrip 1'; ASSERT find(r.ic, 2) IS NULL, 'icollection null-value roundtrip 2'; END; $$; DROP TABLE edge_persist_test; ---------------------------------------------------------------------- -- 11. Copy independence: mutating copy doesn't affect original ---------------------------------------------------------------------- DO $$ DECLARE c collection; c2 collection; BEGIN c := add(add(NULL::collection, 'a', '1'), 'b', '2'); c2 := copy(c); c2 := add(c2, 'c', '3'); c2 := delete(c2, 'a'); ASSERT count(c) = 2, 'original unchanged after copy mutation'; ASSERT count(c2) = 2, 'copy has correct count'; ASSERT exist(c, 'a'), 'original still has a'; ASSERT NOT exist(c2, 'a'), 'copy does not have a'; ASSERT exist(c2, 'c'), 'copy has c'; END; $$; DO $$ DECLARE ic icollection; ic2 icollection; BEGIN ic := add(add(NULL::icollection, 1, 'a'), 2, 'b'); ic2 := copy(ic); ic2 := add(ic2, 3, 'c'); ic2 := delete(ic2, 1); ASSERT count(ic) = 2, 'ic original unchanged after copy mutation'; ASSERT count(ic2) = 2, 'ic copy has correct count'; ASSERT exist(ic, 1), 'ic original still has 1'; ASSERT NOT exist(ic2, 1), 'ic copy does not have 1'; END; $$; ---------------------------------------------------------------------- -- 12. Rapid add/delete cycles (memory safety) ---------------------------------------------------------------------- DO $$ DECLARE c collection; i int; BEGIN c := NULL::collection; FOR i IN 1..500 LOOP c := add(c, 'key' || i, 'val' || i); END LOOP; ASSERT count(c) = 500, 'rapid add count'; FOR i IN 1..500 LOOP c := delete(c, 'key' || i); END LOOP; ASSERT count(c) = 0, 'rapid delete count'; -- re-add after full delete c := add(c, 'new', 'value'); ASSERT count(c) = 1, 'add after full rapid delete'; ASSERT find(c, 'new') = 'value', 'find after full rapid delete'; END; $$; DO $$ DECLARE ic icollection; i int; BEGIN ic := NULL::icollection; FOR i IN 1..500 LOOP ic := add(ic, i, 'val' || i); END LOOP; ASSERT count(ic) = 500, 'ic rapid add count'; FOR i IN 1..500 LOOP ic := delete(ic, i); END LOOP; ASSERT count(ic) = 0, 'ic rapid delete count'; ic := add(ic, 999, 'comeback'); ASSERT count(ic) = 1, 'ic add after full rapid delete'; END; $$; ---------------------------------------------------------------------- -- 13. Overwrite same key repeatedly ---------------------------------------------------------------------- DO $$ DECLARE c collection; i int; BEGIN c := NULL::collection; FOR i IN 1..100 LOOP c := add(c, 'same', 'val' || i); END LOOP; ASSERT count(c) = 1, 'overwrite same key count'; ASSERT find(c, 'same') = 'val100', 'overwrite same key value'; END; $$; DO $$ DECLARE ic icollection; i int; BEGIN ic := NULL::icollection; FOR i IN 1..100 LOOP ic := add(ic, 1, 'val' || i); END LOOP; ASSERT count(ic) = 1, 'ic overwrite same key count'; ASSERT find(ic, 1) = 'val100', 'ic overwrite same key value'; END; $$; ---------------------------------------------------------------------- -- 14. Type mismatch errors ---------------------------------------------------------------------- DO $$ DECLARE c collection; ok boolean; BEGIN c := add(NULL::collection, 'a', 42::int); ok := false; BEGIN c := add(c, 'b', 'text_value'::text); EXCEPTION WHEN datatype_mismatch THEN ok := true; END; ASSERT ok, 'type mismatch on add should error'; END; $$; DO $$ DECLARE ic icollection; ok boolean; BEGIN ic := add(NULL::icollection, 1, 42::int); ok := false; BEGIN ic := add(ic, 2, 'text_value'::text); EXCEPTION WHEN datatype_mismatch THEN ok := true; END; ASSERT ok, 'ic type mismatch on add should error'; END; $$; ---------------------------------------------------------------------- -- 15. find/exist on NULL collection arg ---------------------------------------------------------------------- DO $$ DECLARE ok boolean; BEGIN -- exist on NULL returns false ASSERT NOT exist(NULL::collection, 'a'), 'exist on null collection'; -- find on NULL should error ok := false; BEGIN PERFORM find(NULL::collection, 'a'); EXCEPTION WHEN no_data_found THEN ok := true; END; ASSERT ok, 'find on null collection should error'; END; $$; DO $$ DECLARE ok boolean; BEGIN ASSERT NOT exist(NULL::icollection, 1), 'exist on null icollection'; ok := false; BEGIN PERFORM find(NULL::icollection, 1); EXCEPTION WHEN no_data_found THEN ok := true; END; ASSERT ok, 'find on null icollection should error'; END; $$; ---------------------------------------------------------------------- -- 16. Iterate full cycle: first -> next -> ... -> null -> prev/next ---------------------------------------------------------------------- DO $$ DECLARE c collection; n int := 0; BEGIN c := add(add(add(NULL::collection, 'a', '1'), 'b', '2'), 'c', '3'); c := first(c); WHILE NOT isnull(c) LOOP n := n + 1; c := next(c); END LOOP; ASSERT n = 3, 'full iteration count'; -- now at null, prev should stay null (not crash) c := prev(c); ASSERT isnull(c), 'prev from null stays null'; -- next from null should also stay null c := next(c); ASSERT isnull(c), 'next from null stays null'; END; $$; DO $$ DECLARE ic icollection; n int := 0; BEGIN ic := add(add(add(NULL::icollection, 1, 'a'), 2, 'b'), 3, 'c'); ic := first(ic); WHILE NOT isnull(ic) LOOP n := n + 1; ic := next(ic); END LOOP; ASSERT n = 3, 'ic full iteration count'; ic := prev(ic); ASSERT isnull(ic), 'ic prev from null stays null'; END; $$; ---------------------------------------------------------------------- -- 17. Invalid JSON input ---------------------------------------------------------------------- -- Completely invalid JSON SELECT 'not json at all'::collection; ERROR: Invalid format LINE 1: SELECT 'not json at all'::collection; ^ -- Array instead of object SELECT '[1,2,3]'::collection; ERROR: Invalid collection format LINE 1: SELECT '[1,2,3]'::collection; ^ -- Missing closing brace SELECT '{"value_type": "text", "entries": {"a": "b"}'::collection; ERROR: Invalid format LINE 1: SELECT '{"value_type": "text", "entries": {"a": "b"}'::colle... ^ -- Entries as array instead of object SELECT '{"value_type": "text", "entries": [1,2,3]}'::collection; ERROR: Invalid collection format LINE 1: SELECT '{"value_type": "text", "entries": [1,2,3]}'::collect... ^ -- icollection with non-integer key SELECT '{"value_type": "text", "entries": {"notanumber": "val"}}'::icollection; ERROR: invalid input syntax for type bigint: "notanumber" LINE 1: SELECT '{"value_type": "text", "entries": {"notanumber": "va... ^ ---------------------------------------------------------------------- -- 18. Subscript fetch on missing key errors properly ---------------------------------------------------------------------- DO $$ DECLARE c collection; v text; ok boolean; BEGIN c := add(NULL::collection, 'a', '1'); ok := false; BEGIN v := c['missing']; EXCEPTION WHEN no_data_found THEN ok := true; END; ASSERT ok, 'subscript fetch missing key should error'; END; $$; DO $$ DECLARE ic icollection; v text; ok boolean; BEGIN ic := add(NULL::icollection, 1, 'a'); ok := false; BEGIN v := ic[999]; EXCEPTION WHEN no_data_found THEN ok := true; END; ASSERT ok, 'ic subscript fetch missing key should error'; END; $$; ---------------------------------------------------------------------- -- 19. Sort then iterate preserves order ---------------------------------------------------------------------- DO $$ DECLARE c collection; keys text[] := '{}'; BEGIN c := add(add(add(NULL::collection, 'cherry', '3'), 'apple', '1'), 'banana', '2'); c := sort(c); c := first(c); WHILE NOT isnull(c) LOOP keys := array_append(keys, key(c)); c := next(c); END LOOP; ASSERT keys = ARRAY['apple','banana','cherry'], format('sort order wrong: %s', keys::text); END; $$; DO $$ DECLARE ic icollection; keys bigint[] := '{}'; BEGIN ic := add(add(add(NULL::icollection, 30, 'c'), 10, 'a'), 20, 'b'); ic := sort(ic); ic := first(ic); WHILE NOT isnull(ic) LOOP keys := array_append(keys, key(ic)); ic := next(ic); END LOOP; ASSERT keys = ARRAY[10,20,30]::bigint[], format('ic sort order wrong: %s', keys::text); END; $$; ---------------------------------------------------------------------- -- 20. Add after sort maintains new entry ---------------------------------------------------------------------- DO $$ DECLARE c collection; BEGIN c := add(add(NULL::collection, 'b', '2'), 'a', '1'); c := sort(c); c := add(c, 'c', '3'); ASSERT count(c) = 3, 'count after add-after-sort'; ASSERT exist(c, 'c'), 'new key exists after add-after-sort'; END; $$; ---------------------------------------------------------------------- -- 21. value_type consistency ---------------------------------------------------------------------- DO $$ DECLARE c collection; BEGIN -- value_type is NULL until first add c := add(NULL::collection, 'tmp', 'x'); c := delete(c, 'tmp'); -- after delete-all, value_type returns NULL (head is NULL) ASSERT value_type(c) IS NULL, 'value_type null after delete-all'; -- re-adding same type works c := add(c, 'a', 'hello'); ASSERT value_type(c) = 'text'::regtype, 'value_type after re-add'; -- fresh collection with int c := add(NULL::collection, 'a', 42::int); ASSERT value_type(c) = 'integer'::regtype, 'value_type after first int add'; END; $$; ---------------------------------------------------------------------- -- 22. Large collection flatten/unflatten roundtrip ---------------------------------------------------------------------- DO $$ DECLARE c collection; r record; i int; BEGIN c := NULL::collection; FOR i IN 1..200 LOOP c := add(c, 'key' || lpad(i::text, 4, '0'), 'value' || i); END LOOP; CREATE TEMP TABLE edge_large_test(c collection); INSERT INTO edge_large_test VALUES (c); SELECT edge_large_test.c INTO r FROM edge_large_test; ASSERT count(r.c) = 200, 'large collection roundtrip count'; ASSERT find(r.c, 'key0001') = 'value1', 'large collection roundtrip first'; ASSERT find(r.c, 'key0200') = 'value200', 'large collection roundtrip last'; DROP TABLE edge_large_test; END; $$; ---------------------------------------------------------------------- -- 23. icollection with extreme int64 keys ---------------------------------------------------------------------- DO $$ DECLARE ic icollection; BEGIN ic := add(NULL::icollection, 9223372036854775807, 'max'); ic := add(ic, -9223372036854775808, 'min'); ic := add(ic, 0, 'zero'); ASSERT count(ic) = 3, 'extreme keys count'; ASSERT find(ic, 9223372036854775807) = 'max', 'max key find'; ASSERT find(ic, -9223372036854775808) = 'min', 'min key find'; ASSERT find(ic, 0) = 'zero', 'zero key find'; ASSERT exist(ic, 9223372036854775807), 'max key exist'; ASSERT exist(ic, -9223372036854775808), 'min key exist'; END; $$; -- Keys that would collide with 4-byte hash but not 8-byte DO $$ DECLARE ic icollection; BEGIN ic := add(NULL::icollection, 1, 'one'); ic := add(ic, 4294967297, 'big'); -- 0x100000001, same lower 32 bits as 1 ASSERT count(ic) = 2, 'keys with same lower 32 bits are distinct'; ASSERT find(ic, 1) = 'one', 'find key 1'; ASSERT find(ic, 4294967297) = 'big', 'find key 4294967297'; END; $$; ---------------------------------------------------------------------- -- 24. Delete then re-add then iterate ---------------------------------------------------------------------- DO $$ DECLARE c collection; n int := 0; BEGIN c := add(add(add(NULL::collection, 'a', '1'), 'b', '2'), 'c', '3'); c := delete(c, 'b'); c := add(c, 'b', 'new'); c := first(c); WHILE NOT isnull(c) LOOP n := n + 1; c := next(c); END LOOP; ASSERT n = 3, 'iterate after delete-readd count'; ASSERT find(c, 'b') = 'new', 'readded value correct'; END; $$; ---------------------------------------------------------------------- -- 25. first_key / last_key on collection (text-keyed) ---------------------------------------------------------------------- DO $$ DECLARE c collection; BEGIN c := add(add(NULL::collection, 'x', '1'), 'y', '2'); -- first_key/last_key reflect insertion order (not sorted) ASSERT first_key(c) IS NOT NULL, 'first_key not null'; ASSERT last_key(c) IS NOT NULL, 'last_key not null'; -- on empty (via delete) c := delete(c, 'x'); c := delete(c, 'y'); ASSERT first_key(c) IS NULL, 'first_key on empty'; ASSERT last_key(c) IS NULL, 'last_key on empty'; END; $$; ---------------------------------------------------------------------- -- 26. icollection large flatten/unflatten roundtrip ---------------------------------------------------------------------- DO $$ DECLARE ic icollection; r record; i int; BEGIN ic := NULL::icollection; FOR i IN 1..200 LOOP ic := add(ic, i, 'value' || i); END LOOP; CREATE TEMP TABLE edge_large_ic_test(ic icollection); INSERT INTO edge_large_ic_test VALUES (ic); SELECT edge_large_ic_test.ic INTO r FROM edge_large_ic_test; ASSERT count(r.ic) = 200, 'large icollection roundtrip count'; ASSERT find(r.ic, 1) = 'value1', 'large icollection roundtrip first'; ASSERT find(r.ic, 200) = 'value200', 'large icollection roundtrip last'; DROP TABLE edge_large_ic_test; END; $$; ---------------------------------------------------------------------- -- 27. Negative key operations on icollection ---------------------------------------------------------------------- DO $$ DECLARE ic icollection; BEGIN ic := add(NULL::icollection, -1, 'neg1'); ic := add(ic, -100, 'neg100'); ic := add(ic, -999999999999, 'bigneg'); ASSERT count(ic) = 3, 'negative keys count'; ASSERT find(ic, -1) = 'neg1', 'find -1'; ASSERT find(ic, -100) = 'neg100', 'find -100'; ASSERT exist(ic, -999999999999), 'exist big negative'; ic := delete(ic, -100); ASSERT count(ic) = 2, 'count after delete negative key'; ASSERT NOT exist(ic, -100), 'deleted negative key gone'; END; $$; -- ============================================================ -- Test 28: delete() with no args — NULL values in collection -- ============================================================ DO $$ DECLARE c collection; BEGIN c['a'] := 'value'; c['b'] := NULL; c['c'] := 'other'; ASSERT count(c) = 3, 'pre-delete with nulls'; c := delete(c); ASSERT count(c) = 0, 'delete all with null values'; END; $$; -- ============================================================ -- Test 29: delete() with no args — empty and NULL collection -- ============================================================ DO $$ DECLARE c collection; BEGIN -- delete all on NULL (creates empty, deletes nothing) c := delete(c); ASSERT count(c) = 0, 'delete all on null'; -- add, delete single to empty, then delete all on empty c['a'] := '1'; c := delete(c, 'a'); ASSERT count(c) = 0, 'empty after single delete'; c := delete(c); ASSERT count(c) = 0, 'delete all on already empty'; END; $$; -- ============================================================ -- Test 30: delete() with no args — rapid delete/refill cycle -- ============================================================ DO $$ DECLARE ic icollection('int4'); i int; BEGIN FOR cycle IN 1..5 LOOP FOR i IN 1..100 LOOP ic[i] := i * cycle; END LOOP; ASSERT count(ic) = 100, 'cycle ' || cycle || ' fill'; ic := delete(ic); ASSERT count(ic) = 0, 'cycle ' || cycle || ' empty'; END LOOP; -- final refill ic[1] := 999; ASSERT find(ic, 1, NULL::int4) = 999, 'final refill'; END; $$; -- ============================================================ -- Test 31: delete(collection, lo, hi) — edge cases -- ============================================================ -- lo > hi is a no-op DO $$ DECLARE c collection; BEGIN c['a'] := '1'; c['b'] := '2'; c['c'] := '3'; c := delete(c, 'd', 'a'); ASSERT count(c) = 3, 'lo>hi no-op'; END; $$; -- lo = hi deletes single key DO $$ DECLARE c collection; BEGIN c['a'] := '1'; c['b'] := '2'; c['c'] := '3'; c := delete(c, 'b', 'b'); ASSERT count(c) = 2, 'lo=hi single delete'; ASSERT NOT exist(c, 'b'), 'lo=hi key gone'; END; $$; -- NULL lo or hi is a no-op DO $$ DECLARE c collection; BEGIN c['a'] := '1'; c['b'] := '2'; c := delete(c, NULL::text, 'z'); ASSERT count(c) = 2, 'null lo no-op'; c := delete(c, 'a', NULL::text); ASSERT count(c) = 2, 'null hi no-op'; END; $$; -- range covers all keys DO $$ DECLARE c collection; BEGIN c['a'] := '1'; c['b'] := '2'; c['c'] := '3'; c := delete(c, 'a', 'z'); ASSERT count(c) = 0, 'range covers all'; END; $$; -- range on empty collection DO $$ DECLARE c collection; BEGIN c := delete(c, 'a', 'z'); ASSERT count(c) = 0, 'range on empty'; END; $$; -- icollection lo > hi is a no-op DO $$ DECLARE ic icollection('text'); BEGIN ic[1] := 'a'; ic[2] := 'b'; ic[3] := 'c'; ic := delete(ic, 10::bigint, 1::bigint); ASSERT count(ic) = 3, 'ic lo>hi no-op'; END; $$;