Skip to content

Commit 9e39fcc

Browse files
jhfclaude
andcommitted
Add tests for set_era_ephemeral_columns and first() aggregate
Both are public API functions with zero dedicated test coverage. 008_era_ephemeral_columns: Tests setting/clearing ephemeral columns on an era, plus error paths for non-existent columns, system columns, and non-existent eras. 036_agg_first: Tests basic ordering, empty input (NULL), GROUP BY, multiple data types (integer, text, date), NULL handling, and single-row edge case. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 6231ccd commit 9e39fcc

4 files changed

Lines changed: 482 additions & 0 deletions

File tree

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
\i sql/include/test_setup.sql
2+
--
3+
-- test_setup.sql
4+
--
5+
-- Common setup for regression tests that need to be self-contained.
6+
-- This script creates the extension, a user role, and grants permissions.
7+
--
8+
SET datestyle = 'ISO, YMD';
9+
CREATE EXTENSION IF NOT EXISTS btree_gist;
10+
CREATE EXTENSION IF NOT EXISTS sql_saga CASCADE;
11+
DO $$
12+
BEGIN
13+
CREATE ROLE sql_saga_unprivileged_user;
14+
EXCEPTION WHEN duplicate_object THEN
15+
END
16+
$$;
17+
GRANT USAGE ON SCHEMA sql_saga TO sql_saga_unprivileged_user;
18+
GRANT SELECT ON ALL TABLES IN SCHEMA sql_saga TO sql_saga_unprivileged_user;
19+
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA sql_saga TO sql_saga_unprivileged_user;
20+
/*
21+
* Allow the unprivileged user to create tables in the public schema.
22+
* This is required for tests that create their own tables.
23+
* PG 15+ restricts this by default.
24+
*/
25+
GRANT CREATE ON SCHEMA public TO PUBLIC;
26+
BEGIN;
27+
SET ROLE TO sql_saga_unprivileged_user;
28+
-- =============================================================================
29+
-- Test: set_era_ephemeral_columns
30+
-- =============================================================================
31+
\echo '--- Setup: Create table with era ---'
32+
--- Setup: Create table with era ---
33+
CREATE TABLE public.ephemeral_test (
34+
id integer,
35+
name text,
36+
value numeric,
37+
edit_at timestamptz,
38+
edit_by text,
39+
valid_range daterange NOT NULL
40+
);
41+
SELECT sql_saga.add_era('public.ephemeral_test', 'valid_range');
42+
NOTICE: sql_saga: Created GIST index "ephemeral_test_valid_range_gist_idx" on column public.ephemeral_test.valid_range for temporal_merge performance
43+
add_era
44+
---------
45+
t
46+
(1 row)
47+
48+
-- =============================================================================
49+
\echo '--- Test 1: Set ephemeral columns and verify in sql_saga.era ---'
50+
--- Test 1: Set ephemeral columns and verify in sql_saga.era ---
51+
-- =============================================================================
52+
SELECT sql_saga.set_era_ephemeral_columns(
53+
table_class => 'public.ephemeral_test',
54+
era_name => 'valid',
55+
ephemeral_columns => ARRAY['edit_at', 'edit_by']::name[]
56+
);
57+
set_era_ephemeral_columns
58+
---------------------------
59+
60+
(1 row)
61+
62+
SELECT ephemeral_columns
63+
FROM sql_saga.era
64+
WHERE table_name = 'ephemeral_test' AND era_name = 'valid';
65+
ephemeral_columns
66+
-------------------
67+
{edit_at,edit_by}
68+
(1 row)
69+
70+
-- =============================================================================
71+
\echo '--- Test 2: Clear ephemeral columns by setting to NULL ---'
72+
--- Test 2: Clear ephemeral columns by setting to NULL ---
73+
-- =============================================================================
74+
SELECT sql_saga.set_era_ephemeral_columns(
75+
table_class => 'public.ephemeral_test',
76+
era_name => 'valid',
77+
ephemeral_columns => NULL
78+
);
79+
set_era_ephemeral_columns
80+
---------------------------
81+
82+
(1 row)
83+
84+
SELECT ephemeral_columns IS NULL AS is_cleared
85+
FROM sql_saga.era
86+
WHERE table_name = 'ephemeral_test' AND era_name = 'valid';
87+
is_cleared
88+
------------
89+
t
90+
(1 row)
91+
92+
-- =============================================================================
93+
\echo '--- Test 3: Error - non-existent column ---'
94+
--- Test 3: Error - non-existent column ---
95+
-- =============================================================================
96+
SAVEPOINT test3;
97+
SELECT sql_saga.set_era_ephemeral_columns(
98+
table_class => 'public.ephemeral_test',
99+
era_name => 'valid',
100+
ephemeral_columns => ARRAY['no_such_column']::name[]
101+
);
102+
ERROR: column "no_such_column" does not exist in table ephemeral_test
103+
CONTEXT: PL/pgSQL function set_era_ephemeral_columns(regclass,name,name[]) line 27 at RAISE
104+
ROLLBACK TO test3;
105+
-- =============================================================================
106+
\echo '--- Test 4: Error - system column as ephemeral ---'
107+
--- Test 4: Error - system column as ephemeral ---
108+
-- =============================================================================
109+
SAVEPOINT test4;
110+
SELECT sql_saga.set_era_ephemeral_columns(
111+
table_class => 'public.ephemeral_test',
112+
era_name => 'valid',
113+
ephemeral_columns => ARRAY['xmin']::name[]
114+
);
115+
ERROR: cannot mark system column "xmin" as ephemeral
116+
CONTEXT: PL/pgSQL function set_era_ephemeral_columns(regclass,name,name[]) line 37 at RAISE
117+
ROLLBACK TO test4;
118+
-- =============================================================================
119+
\echo '--- Test 5: Error - non-existent era ---'
120+
--- Test 5: Error - non-existent era ---
121+
-- =============================================================================
122+
SAVEPOINT test5;
123+
SELECT sql_saga.set_era_ephemeral_columns(
124+
table_class => 'public.ephemeral_test',
125+
era_name => 'nonexistent',
126+
ephemeral_columns => ARRAY['edit_at']::name[]
127+
);
128+
ERROR: Era "nonexistent" not found on table ephemeral_test
129+
CONTEXT: PL/pgSQL function set_era_ephemeral_columns(regclass,name,name[]) line 47 at RAISE
130+
ROLLBACK TO test5;
131+
ROLLBACK;
132+
\i sql/include/test_teardown.sql
133+
--
134+
-- test_teardown.sql
135+
--
136+
-- Common teardown for regression tests. This script drops the unprivileged
137+
-- user role created by test_setup.sql.
138+
--
139+
-- It is important to reset the role first, in case a test fails and
140+
-- leaves the session role set to the user that is about to be dropped.
141+
RESET ROLE;
142+
-- Drop the extensions to ensure a clean state for the next test.
143+
-- Use CASCADE to remove any dependent objects created by sql_saga.
144+
DROP EXTENSION IF EXISTS sql_saga CASCADE;
145+
DROP EXTENSION IF EXISTS btree_gist CASCADE;
146+
-- Revoke any privileges held by the test user and drop any objects they own.
147+
-- This is necessary before the role can be dropped.
148+
DROP OWNED BY sql_saga_unprivileged_user;
149+
DROP ROLE IF EXISTS sql_saga_unprivileged_user;

expected/036_agg_first.out

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
\i sql/include/test_setup.sql
2+
--
3+
-- test_setup.sql
4+
--
5+
-- Common setup for regression tests that need to be self-contained.
6+
-- This script creates the extension, a user role, and grants permissions.
7+
--
8+
SET datestyle = 'ISO, YMD';
9+
CREATE EXTENSION IF NOT EXISTS btree_gist;
10+
CREATE EXTENSION IF NOT EXISTS sql_saga CASCADE;
11+
DO $$
12+
BEGIN
13+
CREATE ROLE sql_saga_unprivileged_user;
14+
EXCEPTION WHEN duplicate_object THEN
15+
END
16+
$$;
17+
GRANT USAGE ON SCHEMA sql_saga TO sql_saga_unprivileged_user;
18+
GRANT SELECT ON ALL TABLES IN SCHEMA sql_saga TO sql_saga_unprivileged_user;
19+
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA sql_saga TO sql_saga_unprivileged_user;
20+
/*
21+
* Allow the unprivileged user to create tables in the public schema.
22+
* This is required for tests that create their own tables.
23+
* PG 15+ restricts this by default.
24+
*/
25+
GRANT CREATE ON SCHEMA public TO PUBLIC;
26+
BEGIN;
27+
SET ROLE TO sql_saga_unprivileged_user;
28+
-- =============================================================================
29+
-- Test: sql_saga.first() aggregate
30+
-- =============================================================================
31+
-- =============================================================================
32+
\echo '--- Test 1: Basic - first value ordered by id ---'
33+
--- Test 1: Basic - first value ordered by id ---
34+
-- =============================================================================
35+
SELECT sql_saga.first(val ORDER BY id)
36+
FROM (VALUES (1, 'a'), (2, 'b'), (3, 'c')) AS t(id, val);
37+
first
38+
-------
39+
a
40+
(1 row)
41+
42+
-- =============================================================================
43+
\echo '--- Test 2: Empty input returns NULL ---'
44+
--- Test 2: Empty input returns NULL ---
45+
-- =============================================================================
46+
SELECT sql_saga.first(val ORDER BY id)
47+
FROM (SELECT 1 AS val, 1 AS id WHERE false) AS t;
48+
first
49+
-------
50+
51+
(1 row)
52+
53+
-- =============================================================================
54+
\echo '--- Test 3: GROUP BY with multiple groups ---'
55+
--- Test 3: GROUP BY with multiple groups ---
56+
-- =============================================================================
57+
CREATE TEMP TABLE first_test_data (
58+
grp text,
59+
sort_key integer,
60+
val text
61+
);
62+
INSERT INTO first_test_data (grp, sort_key, val) VALUES
63+
('alpha', 3, 'third'),
64+
('alpha', 1, 'first'),
65+
('alpha', 2, 'second'),
66+
('beta', 2, 'two'),
67+
('beta', 1, 'one'),
68+
('beta', 3, 'three');
69+
SELECT grp, sql_saga.first(val ORDER BY sort_key)
70+
FROM first_test_data
71+
GROUP BY grp
72+
ORDER BY grp;
73+
grp | first
74+
-------+-------
75+
alpha | first
76+
beta | one
77+
(2 rows)
78+
79+
-- =============================================================================
80+
\echo '--- Test 4: Different types - integer ---'
81+
--- Test 4: Different types - integer ---
82+
-- =============================================================================
83+
SELECT sql_saga.first(val ORDER BY id)
84+
FROM (VALUES (1, 100), (2, 200), (3, 300)) AS t(id, val);
85+
first
86+
-------
87+
100
88+
(1 row)
89+
90+
-- =============================================================================
91+
\echo '--- Test 4b: Different types - date ---'
92+
--- Test 4b: Different types - date ---
93+
-- =============================================================================
94+
SELECT sql_saga.first(val ORDER BY id)
95+
FROM (VALUES (1, '2024-01-01'::date), (2, '2024-06-15'::date), (3, '2024-12-31'::date)) AS t(id, val);
96+
first
97+
------------
98+
2024-01-01
99+
(1 row)
100+
101+
-- =============================================================================
102+
\echo '--- Test 5: NULL handling - first value is NULL ---'
103+
--- Test 5: NULL handling - first value is NULL ---
104+
-- =============================================================================
105+
SELECT sql_saga.first(val ORDER BY id)
106+
FROM (VALUES (1, NULL::text), (2, 'b'), (3, 'c')) AS t(id, val);
107+
first
108+
-------
109+
b
110+
(1 row)
111+
112+
-- =============================================================================
113+
\echo '--- Test 5b: NULL handling - non-first values are NULL ---'
114+
--- Test 5b: NULL handling - non-first values are NULL ---
115+
-- =============================================================================
116+
SELECT sql_saga.first(val ORDER BY id)
117+
FROM (VALUES (1, 'a'), (2, NULL::text), (3, NULL::text)) AS t(id, val);
118+
first
119+
-------
120+
a
121+
(1 row)
122+
123+
-- =============================================================================
124+
\echo '--- Test 6: Single row ---'
125+
--- Test 6: Single row ---
126+
-- =============================================================================
127+
SELECT sql_saga.first(val ORDER BY id)
128+
FROM (VALUES (1, 'only')) AS t(id, val);
129+
first
130+
-------
131+
only
132+
(1 row)
133+
134+
ROLLBACK;
135+
\i sql/include/test_teardown.sql
136+
--
137+
-- test_teardown.sql
138+
--
139+
-- Common teardown for regression tests. This script drops the unprivileged
140+
-- user role created by test_setup.sql.
141+
--
142+
-- It is important to reset the role first, in case a test fails and
143+
-- leaves the session role set to the user that is about to be dropped.
144+
RESET ROLE;
145+
-- Drop the extensions to ensure a clean state for the next test.
146+
-- Use CASCADE to remove any dependent objects created by sql_saga.
147+
DROP EXTENSION IF EXISTS sql_saga CASCADE;
148+
DROP EXTENSION IF EXISTS btree_gist CASCADE;
149+
-- Revoke any privileges held by the test user and drop any objects they own.
150+
-- This is necessary before the role can be dropped.
151+
DROP OWNED BY sql_saga_unprivileged_user;
152+
DROP ROLE IF EXISTS sql_saga_unprivileged_user;

sql/008_era_ephemeral_columns.sql

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
\i sql/include/test_setup.sql
2+
3+
BEGIN;
4+
5+
SET ROLE TO sql_saga_unprivileged_user;
6+
7+
-- =============================================================================
8+
-- Test: set_era_ephemeral_columns
9+
-- =============================================================================
10+
11+
\echo '--- Setup: Create table with era ---'
12+
13+
CREATE TABLE public.ephemeral_test (
14+
id integer,
15+
name text,
16+
value numeric,
17+
edit_at timestamptz,
18+
edit_by text,
19+
valid_range daterange NOT NULL
20+
);
21+
22+
SELECT sql_saga.add_era('public.ephemeral_test', 'valid_range');
23+
24+
-- =============================================================================
25+
\echo '--- Test 1: Set ephemeral columns and verify in sql_saga.era ---'
26+
-- =============================================================================
27+
28+
SELECT sql_saga.set_era_ephemeral_columns(
29+
table_class => 'public.ephemeral_test',
30+
era_name => 'valid',
31+
ephemeral_columns => ARRAY['edit_at', 'edit_by']::name[]
32+
);
33+
34+
SELECT ephemeral_columns
35+
FROM sql_saga.era
36+
WHERE table_name = 'ephemeral_test' AND era_name = 'valid';
37+
38+
-- =============================================================================
39+
\echo '--- Test 2: Clear ephemeral columns by setting to NULL ---'
40+
-- =============================================================================
41+
42+
SELECT sql_saga.set_era_ephemeral_columns(
43+
table_class => 'public.ephemeral_test',
44+
era_name => 'valid',
45+
ephemeral_columns => NULL
46+
);
47+
48+
SELECT ephemeral_columns IS NULL AS is_cleared
49+
FROM sql_saga.era
50+
WHERE table_name = 'ephemeral_test' AND era_name = 'valid';
51+
52+
-- =============================================================================
53+
\echo '--- Test 3: Error - non-existent column ---'
54+
-- =============================================================================
55+
56+
SAVEPOINT test3;
57+
58+
SELECT sql_saga.set_era_ephemeral_columns(
59+
table_class => 'public.ephemeral_test',
60+
era_name => 'valid',
61+
ephemeral_columns => ARRAY['no_such_column']::name[]
62+
);
63+
64+
ROLLBACK TO test3;
65+
66+
-- =============================================================================
67+
\echo '--- Test 4: Error - system column as ephemeral ---'
68+
-- =============================================================================
69+
70+
SAVEPOINT test4;
71+
72+
SELECT sql_saga.set_era_ephemeral_columns(
73+
table_class => 'public.ephemeral_test',
74+
era_name => 'valid',
75+
ephemeral_columns => ARRAY['xmin']::name[]
76+
);
77+
78+
ROLLBACK TO test4;
79+
80+
-- =============================================================================
81+
\echo '--- Test 5: Error - non-existent era ---'
82+
-- =============================================================================
83+
84+
SAVEPOINT test5;
85+
86+
SELECT sql_saga.set_era_ephemeral_columns(
87+
table_class => 'public.ephemeral_test',
88+
era_name => 'nonexistent',
89+
ephemeral_columns => ARRAY['edit_at']::name[]
90+
);
91+
92+
ROLLBACK TO test5;
93+
94+
ROLLBACK;
95+
96+
\i sql/include/test_teardown.sql

0 commit comments

Comments
 (0)