1--
2-- Tests for PL/pgSQL handling of composite (record) variables
3--
4
5create type two_int4s as (f1 int4, f2 int4);
6create type two_int8s as (q1 int8, q2 int8);
7
8-- base-case return of a composite type
9create function retc(int) returns two_int8s language plpgsql as
10$$ begin return row($1,1)::two_int8s; end $$;
11select retc(42);
12
13-- ok to return a matching record type
14create or replace function retc(int) returns two_int8s language plpgsql as
15$$ begin return row($1::int8, 1::int8); end $$;
16select retc(42);
17
18-- we don't currently support implicit casting
19create or replace function retc(int) returns two_int8s language plpgsql as
20$$ begin return row($1,1); end $$;
21select retc(42);
22
23-- nor extra columns
24create or replace function retc(int) returns two_int8s language plpgsql as
25$$ begin return row($1::int8, 1::int8, 42); end $$;
26select retc(42);
27
28-- same cases with an intermediate "record" variable
29create or replace function retc(int) returns two_int8s language plpgsql as
30$$ declare r record; begin r := row($1::int8, 1::int8); return r; end $$;
31select retc(42);
32
33create or replace function retc(int) returns two_int8s language plpgsql as
34$$ declare r record; begin r := row($1,1); return r; end $$;
35select retc(42);
36
37create or replace function retc(int) returns two_int8s language plpgsql as
38$$ declare r record; begin r := row($1::int8, 1::int8, 42); return r; end $$;
39select retc(42);
40
41-- but, for mostly historical reasons, we do convert when assigning
42-- to a named-composite-type variable
43create or replace function retc(int) returns two_int8s language plpgsql as
44$$ declare r two_int8s; begin r := row($1::int8, 1::int8, 42); return r; end $$;
45select retc(42);
46
47do $$ declare c two_int8s;
48begin c := row(1,2); raise notice 'c = %', c; end$$;
49
50do $$ declare c two_int8s;
51begin for c in select 1,2 loop raise notice 'c = %', c; end loop; end$$;
52
53do $$ declare c4 two_int4s; c8 two_int8s;
54begin
55  c8 := row(1,2);
56  c4 := c8;
57  c8 := c4;
58  raise notice 'c4 = %', c4;
59  raise notice 'c8 = %', c8;
60end$$;
61
62-- check passing composite result to another function
63create function getq1(two_int8s) returns int8 language plpgsql as $$
64declare r two_int8s; begin r := $1; return r.q1; end $$;
65
66select getq1(retc(344));
67select getq1(row(1,2));
68
69do $$
70declare r1 two_int8s; r2 record; x int8;
71begin
72  r1 := retc(345);
73  perform getq1(r1);
74  x := getq1(r1);
75  raise notice 'x = %', x;
76  r2 := retc(346);
77  perform getq1(r2);
78  x := getq1(r2);
79  raise notice 'x = %', x;
80end$$;
81
82-- check assignments of composites
83do $$
84declare r1 two_int8s; r2 two_int8s; r3 record; r4 record;
85begin
86  r1 := row(1,2);
87  raise notice 'r1 = %', r1;
88  r1 := r1;  -- shouldn't do anything
89  raise notice 'r1 = %', r1;
90  r2 := r1;
91  raise notice 'r1 = %', r1;
92  raise notice 'r2 = %', r2;
93  r2.q2 = r1.q1 + 3;  -- check that r2 has distinct storage
94  raise notice 'r1 = %', r1;
95  raise notice 'r2 = %', r2;
96  r1 := null;
97  raise notice 'r1 = %', r1;
98  raise notice 'r2 = %', r2;
99  r1 := row(7,11)::two_int8s;
100  r2 := r1;
101  raise notice 'r1 = %', r1;
102  raise notice 'r2 = %', r2;
103  r3 := row(1,2);
104  r4 := r3;
105  raise notice 'r3 = %', r3;
106  raise notice 'r4 = %', r4;
107  r4.f1 := r4.f1 + 3;  -- check that r4 has distinct storage
108  raise notice 'r3 = %', r3;
109  raise notice 'r4 = %', r4;
110  r1 := r3;
111  raise notice 'r1 = %', r1;
112  r4 := r1;
113  raise notice 'r4 = %', r4;
114  r4.q2 := r4.q2 + 1;  -- r4's field names have changed
115  raise notice 'r4 = %', r4;
116end$$;
117
118-- fields of named-type vars read as null if uninitialized
119do $$
120declare r1 two_int8s;
121begin
122  raise notice 'r1 = %', r1;
123  raise notice 'r1.q1 = %', r1.q1;
124  raise notice 'r1.q2 = %', r1.q2;
125  raise notice 'r1 = %', r1;
126end$$;
127
128do $$
129declare r1 two_int8s;
130begin
131  raise notice 'r1.q1 = %', r1.q1;
132  raise notice 'r1.q2 = %', r1.q2;
133  raise notice 'r1 = %', r1;
134  raise notice 'r1.nosuchfield = %', r1.nosuchfield;
135end$$;
136
137-- records, not so much
138do $$
139declare r1 record;
140begin
141  raise notice 'r1 = %', r1;
142  raise notice 'r1.f1 = %', r1.f1;
143  raise notice 'r1.f2 = %', r1.f2;
144  raise notice 'r1 = %', r1;
145end$$;
146
147-- but OK if you assign first
148do $$
149declare r1 record;
150begin
151  raise notice 'r1 = %', r1;
152  r1 := row(1,2);
153  raise notice 'r1.f1 = %', r1.f1;
154  raise notice 'r1.f2 = %', r1.f2;
155  raise notice 'r1 = %', r1;
156  raise notice 'r1.nosuchfield = %', r1.nosuchfield;
157end$$;
158
159-- check repeated assignments to composite fields
160create table some_table (id int, data text);
161
162do $$
163declare r some_table;
164begin
165  r := (23, 'skidoo');
166  for i in 1 .. 10 loop
167    r.id := r.id + i;
168    r.data := r.data || ' ' || i;
169  end loop;
170  raise notice 'r = %', r;
171end$$;
172
173-- check behavior of function declared to return "record"
174
175create function returnsrecord(int) returns record language plpgsql as
176$$ begin return row($1,$1+1); end $$;
177
178select returnsrecord(42);
179select * from returnsrecord(42) as r(x int, y int);
180select * from returnsrecord(42) as r(x int, y int, z int);  -- fail
181select * from returnsrecord(42) as r(x int, y bigint);  -- fail
182
183-- same with an intermediate record variable
184create or replace function returnsrecord(int) returns record language plpgsql as
185$$ declare r record; begin r := row($1,$1+1); return r; end $$;
186
187select returnsrecord(42);
188select * from returnsrecord(42) as r(x int, y int);
189select * from returnsrecord(42) as r(x int, y int, z int);  -- fail
190select * from returnsrecord(42) as r(x int, y bigint);  -- fail
191
192-- should work the same with a missing column in the actual result value
193create table has_hole(f1 int, f2 int, f3 int);
194alter table has_hole drop column f2;
195
196create or replace function returnsrecord(int) returns record language plpgsql as
197$$ begin return row($1,$1+1)::has_hole; end $$;
198
199select returnsrecord(42);
200select * from returnsrecord(42) as r(x int, y int);
201select * from returnsrecord(42) as r(x int, y int, z int);  -- fail
202select * from returnsrecord(42) as r(x int, y bigint);  -- fail
203
204-- same with an intermediate record variable
205create or replace function returnsrecord(int) returns record language plpgsql as
206$$ declare r record; begin r := row($1,$1+1)::has_hole; return r; end $$;
207
208select returnsrecord(42);
209select * from returnsrecord(42) as r(x int, y int);
210select * from returnsrecord(42) as r(x int, y int, z int);  -- fail
211select * from returnsrecord(42) as r(x int, y bigint);  -- fail
212
213-- check access to a field of an argument declared "record"
214create function getf1(x record) returns int language plpgsql as
215$$ begin return x.f1; end $$;
216select getf1(1);
217select getf1(row(1,2));
218-- a CLOBBER_CACHE_ALWAYS build will report this error with a different
219-- context stack than other builds, so suppress context output
220\set SHOW_CONTEXT never
221select getf1(row(1,2)::two_int8s);
222\set SHOW_CONTEXT errors
223select getf1(row(1,2));
224
225-- check behavior when assignment to FOR-loop variable requires coercion
226do $$
227declare r two_int8s;
228begin
229  for r in select i, i+1 from generate_series(1,4) i
230  loop
231    raise notice 'r = %', r;
232  end loop;
233end$$;
234
235-- check behavior when returning setof composite
236create function returnssetofholes() returns setof has_hole language plpgsql as
237$$
238declare r record;
239  h has_hole;
240begin
241  return next h;
242  r := (1,2);
243  h := (3,4);
244  return next r;
245  return next h;
246  return next row(5,6);
247  return next row(7,8)::has_hole;
248end$$;
249select returnssetofholes();
250
251create or replace function returnssetofholes() returns setof has_hole language plpgsql as
252$$
253declare r record;
254begin
255  return next r;  -- fails, not assigned yet
256end$$;
257select returnssetofholes();
258
259create or replace function returnssetofholes() returns setof has_hole language plpgsql as
260$$
261begin
262  return next row(1,2,3);  -- fails
263end$$;
264select returnssetofholes();
265
266-- check behavior with changes of a named rowtype
267create table mutable(f1 int, f2 text);
268
269create function sillyaddone(int) returns int language plpgsql as
270$$ declare r mutable; begin r.f1 := $1; return r.f1 + 1; end $$;
271select sillyaddone(42);
272
273-- test for change of type of column f1 should be here someday;
274-- for now see plpgsql_cache test
275
276alter table mutable drop column f1;
277select sillyaddone(42);  -- fail
278
279create function getf3(x mutable) returns int language plpgsql as
280$$ begin return x.f3; end $$;
281select getf3(null::mutable);  -- doesn't work yet
282alter table mutable add column f3 int;
283select getf3(null::mutable);  -- now it works
284alter table mutable drop column f3;
285-- a CLOBBER_CACHE_ALWAYS build will report this error with a different
286-- context stack than other builds, so suppress context output
287\set SHOW_CONTEXT never
288select getf3(null::mutable);  -- fails again
289\set SHOW_CONTEXT errors
290
291-- check behavior with creating/dropping a named rowtype
292set check_function_bodies = off;  -- else reference to nonexistent type fails
293
294create function sillyaddtwo(int) returns int language plpgsql as
295$$ declare r mutable2; begin r.f1 := $1; return r.f1 + 2; end $$;
296
297reset check_function_bodies;
298
299select sillyaddtwo(42);  -- fail
300create table mutable2(f1 int, f2 text);
301select sillyaddtwo(42);
302drop table mutable2;
303select sillyaddtwo(42);  -- fail
304create table mutable2(f0 text, f1 int, f2 text);
305select sillyaddtwo(42);
306select sillyaddtwo(43);
307
308-- check access to system columns in a record variable
309
310create function sillytrig() returns trigger language plpgsql as
311$$begin
312  raise notice 'old.ctid = %', old.ctid;
313  raise notice 'old.tableoid = %', old.tableoid::regclass;
314  return new;
315end$$;
316
317create trigger mutable_trig before update on mutable for each row
318execute procedure sillytrig();
319
320insert into mutable values ('foo'), ('bar');
321update mutable set f2 = f2 || ' baz';
322table mutable;
323
324-- check returning a composite datum from a trigger
325
326create or replace function sillytrig() returns trigger language plpgsql as
327$$begin
328  return row(new.*);
329end$$;
330
331update mutable set f2 = f2 || ' baz';
332table mutable;
333
334create or replace function sillytrig() returns trigger language plpgsql as
335$$declare r record;
336begin
337  r := row(new.*);
338  return r;
339end$$;
340
341update mutable set f2 = f2 || ' baz';
342table mutable;
343
344--
345-- Domains of composite
346--
347
348create domain ordered_int8s as two_int8s check((value).q1 <= (value).q2);
349
350create function read_ordered_int8s(p ordered_int8s) returns int8 as $$
351begin return p.q1 + p.q2; end
352$$ language plpgsql;
353
354select read_ordered_int8s(row(1, 2));
355select read_ordered_int8s(row(2, 1));  -- fail
356
357create function build_ordered_int8s(i int8, j int8) returns ordered_int8s as $$
358begin return row(i,j); end
359$$ language plpgsql;
360
361select build_ordered_int8s(1,2);
362select build_ordered_int8s(2,1);  -- fail
363
364create function build_ordered_int8s_2(i int8, j int8) returns ordered_int8s as $$
365declare r record; begin r := row(i,j); return r; end
366$$ language plpgsql;
367
368select build_ordered_int8s_2(1,2);
369select build_ordered_int8s_2(2,1);  -- fail
370
371create function build_ordered_int8s_3(i int8, j int8) returns ordered_int8s as $$
372declare r two_int8s; begin r := row(i,j); return r; end
373$$ language plpgsql;
374
375select build_ordered_int8s_3(1,2);
376select build_ordered_int8s_3(2,1);  -- fail
377
378create function build_ordered_int8s_4(i int8, j int8) returns ordered_int8s as $$
379declare r ordered_int8s; begin r := row(i,j); return r; end
380$$ language plpgsql;
381
382select build_ordered_int8s_4(1,2);
383select build_ordered_int8s_4(2,1);  -- fail
384
385create function build_ordered_int8s_a(i int8, j int8) returns ordered_int8s[] as $$
386begin return array[row(i,j), row(i,j+1)]; end
387$$ language plpgsql;
388
389select build_ordered_int8s_a(1,2);
390select build_ordered_int8s_a(2,1);  -- fail
391
392-- check field assignment
393do $$
394declare r ordered_int8s;
395begin
396  r.q1 := null;
397  r.q2 := 43;
398  r.q1 := 42;
399  r.q2 := 41;  -- fail
400end$$;
401
402-- check whole-row assignment
403do $$
404declare r ordered_int8s;
405begin
406  r := null;
407  r := row(null,null);
408  r := row(1,2);
409  r := row(2,1);  -- fail
410end$$;
411
412-- check assignment in for-loop
413do $$
414declare r ordered_int8s;
415begin
416  for r in values (1,2),(3,4),(6,5) loop
417    raise notice 'r = %', r;
418  end loop;
419end$$;
420
421-- check behavior with toastable fields, too
422
423create type two_texts as (f1 text, f2 text);
424create domain ordered_texts as two_texts check((value).f1 <= (value).f2);
425
426create table sometable (id int, a text, b text);
427-- b should be compressed, but in-line
428insert into sometable values (1, 'a', repeat('ffoob',1000));
429-- this b should be out-of-line
430insert into sometable values (2, 'a', repeat('ffoob',100000));
431-- this pair should fail the domain check
432insert into sometable values (3, 'z', repeat('ffoob',100000));
433
434do $$
435declare d ordered_texts;
436begin
437  for d in select a, b from sometable loop
438    raise notice 'succeeded at "%"', d.f1;
439  end loop;
440end$$;
441
442do $$
443declare r record; d ordered_texts;
444begin
445  for r in select * from sometable loop
446    raise notice 'processing row %', r.id;
447    d := row(r.a, r.b);
448  end loop;
449end$$;
450
451do $$
452declare r record; d ordered_texts;
453begin
454  for r in select * from sometable loop
455    raise notice 'processing row %', r.id;
456    d := null;
457    d.f1 := r.a;
458    d.f2 := r.b;
459  end loop;
460end$$;
461
462-- check coercion of a record result to named-composite function output type
463create function compresult(int8) returns two_int8s language plpgsql as
464$$ declare r record; begin r := row($1,$1); return r; end $$;
465
466create table two_int8s_tab (f1 two_int8s);
467insert into two_int8s_tab values (compresult(42));
468-- reconnect so we lose any local knowledge of anonymous record types
469\c -
470table two_int8s_tab;
471