Skip to content

Add ROW() record constructor support #1451

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open

Add ROW() record constructor support #1451

wants to merge 1 commit into from

Conversation

fulghum
Copy link
Contributor

@fulghum fulghum commented May 2, 2025

Initial support for the ROW() constructor expression, which creates anonymous records.

This PR adds support for basic uses of the ROW() constructor, such as creating records from groups of expressions, selecting records, comparing records, and using records in WHERE clauses. There are still several record features that don't work yet, such as using a table alias with the ROW() constructor.

Fixes: #1425

Copy link
Contributor

github-actions bot commented May 2, 2025

Main PR
covering_index_scan_postgres 342.43/s 345.83/s +0.9%
index_join_postgres 154.41/s 153.21/s -0.8%
index_join_scan_postgres 187.84/s 186.61/s -0.7%
index_scan_postgres 12.51/s 12.27/s -2.0%
oltp_point_select 2623.51/s 2581.66/s -1.6%
oltp_read_only 1843.66/s 1842.20/s -0.1%
select_random_points 115.67/s 115.84/s +0.1%
select_random_ranges 128.66/s 131.65/s +2.3%
table_scan_postgres 11.68/s 11.49/s -1.7%
types_table_scan_postgres 5.52/s 5.33/s -3.5%

Copy link
Contributor

github-actions bot commented May 2, 2025

Main PR
Total 42090 42090
Successful 16404 16430
Failures 25686 25660
Partial Successes1 5540 5545
Main PR
Successful 38.9736% 39.0354%
Failures 61.0264% 60.9646%

${\color{red}Regressions (1)}$

groupingsets

QUERY:          select v.c, (select count(*) from gstest2 group by () having v.c)
  from (values (false),(true)) v(c) order by v.c;
RECEIVED ERROR: incompatible conversion to SQL type: '[]'->record (errno 1105) (sqlstate HY000)

${\color{lightgreen}Progressions (28)}$

constraints

QUERY: CREATE TABLE error_tbl (i int DEFAULT (100, ));
QUERY: CREATE TABLE error_tbl (b1 bool DEFAULT (1 IN (1, 2)));

rowtypes

QUERY: select ROW(1,2) < ROW(1,3) as true;
QUERY: select ROW(1,2) < ROW(1,1) as false;
QUERY: select ROW(1,2) < ROW(1,NULL) as null;
QUERY: select ROW(1,2,3) < ROW(1,3,NULL) as true;
QUERY: select ROW(11,'ABC') < ROW(11,'DEF') as true;
QUERY: select ROW(11,'ABC') > ROW(11,'DEF') as false;
QUERY: select ROW(12,'ABC') > ROW(11,'DEF') as true;
QUERY: select ROW(1,2,3) < ROW(1,NULL,4) as null;
QUERY: select ROW(1,2,3) <> ROW(1,NULL,4) as true;
QUERY: select ROW(1,2) in (ROW(3,4), ROW(1,2));
QUERY: select thousand, tenthous from tenk1
where (thousand, tenthous) >= (997, 5000)
order by thousand, tenthous;
QUERY: select thousand, tenthous, four from tenk1
where (thousand, tenthous, four) > (998, 5000, 3)
order by thousand, tenthous;
QUERY: select thousand, tenthous from tenk1
where (998, 5000) < (thousand, tenthous)
order by thousand, tenthous;
QUERY: select thousand, hundred from tenk1
where (998, 5000) < (thousand, hundred)
order by thousand, hundred;
QUERY: select a,b from test_table where (a,b) > ('a','a') order by a,b;
QUERY: select ROW();
QUERY: select row(1,1.1) = any (array[ row(7,7.7), row(1,1.1), row(0,0.0) ]);
QUERY: select row(1,1.1) = any (array[ row(7,7.7), row(1,1.0), row(0,0.0) ]);
QUERY: select cast (row('Jim', 'Beam') as text);
QUERY: select (row('Jim', 'Beam'))::text;

union

QUERY: select x from (values (row(1, 2)), (row(1, 3))) _(x) union select x from (values (row(1, 2)), (row(1, 4))) _(x);
QUERY: select x from (values (row(1, 2)), (row(1, 3))) _(x) intersect select x from (values (row(1, 2)), (row(1, 4))) _(x);
QUERY: select x from (values (row(1, 2)), (row(1, 3))) _(x) except select x from (values (row(1, 2)), (row(1, 4))) _(x);
QUERY: select x from (values (row(1, 2)), (row(1, 3))) _(x) union select x from (values (row(1, 2)), (row(1, 4))) _(x);
QUERY: select x from (values (row(1, 2)), (row(1, 3))) _(x) intersect select x from (values (row(1, 2)), (row(1, 4))) _(x);
QUERY: select x from (values (row(1, 2)), (row(1, 3))) _(x) except select x from (values (row(1, 2)), (row(1, 4))) _(x);

Footnotes

  1. These are tests that we're marking as Successful, however they do not match the expected output in some way. This is due to small differences, such as different wording on the error messages, or the column names being incorrect while the data itself is correct.

Copy link
Collaborator

@Hydrocharged Hydrocharged left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't the right direction to head in. There isn't a tuple type in Postgres.

SELECT * FROM pg_type ORDER BY typname;

The ROW() expression works with composite and anonymous RECORD types, as described in the documentation here:
https://www.postgresql.org/docs/15/sql-expressions.html#SQL-SYNTAX-ROW-CONSTRUCTORS

With types, we are adhering to Postgres' documentation as close as possible. All of the functions for each type (bpcharin, timestamp_recv, etc.) are functions that exist within Postgres, and for those you can find them in pg_type. The comparison functions (int4gt, etc.) exist in the pg_operator table:

SELECT
	o.oprname,
	src_typ.typname AS oprleft,
	tgt_typ.typname AS oprright,
	res_typ.typname AS oprresult,
	o.oprcode::varchar
FROM
	pg_operator o 
JOIN 
    pg_catalog.pg_type src_typ ON o.oprleft = src_typ.oid
JOIN 
    pg_catalog.pg_type tgt_typ ON o.oprright = tgt_typ.oid
JOIN 
    pg_catalog.pg_type res_typ ON o.oprresult = res_typ.oid
WHERE
	o.oprname = '>'
ORDER BY
	src_typ.typname, tgt_typ.typname;

The best way to proceed would be to add support for composite types, and then follow that up with RECORD types (which you may remember seeing a taste of it in my triggers PR, which added enough of RECORD to work with the OLD and NEW values in trigger functions).

I've thought about how to properly implement RECORD types for a little bit (mainly while implementing triggers), and one potential idea for anonymous record types would be to have the row and types combined into a single value. So it would look something like:

type RecordValue struct {
	Value sql.Row
	Types []id.Type
}

This works on the assumption that an array of anonymous RECORD values could each have a different row representation, but that may not be true (would need to follow that up with a bit of reading).

@fulghum fulghum force-pushed the fulghum/tuple branch 4 times, most recently from 3a9f55b to 18f5ccd Compare May 5, 2025 21:33
@fulghum fulghum changed the title Add the tuple type for ROW() support Add ROW() record constructor support May 5, 2025
@fulghum fulghum marked this pull request as ready for review May 5, 2025 22:58
@fulghum
Copy link
Contributor Author

fulghum commented May 5, 2025

@Hydrocharged – ROW() now returns a record type, instead of a tuple type. I'm not opposed to refactoring to the form where a RecordValue contains its value and type together. That should be a fairly small change, too, just wanted to think through how that plays with user-defined composite types a little more, but I think it would all fit together fine. I'm out of the office starting tomorrow, so wanted to get over an update before I'm out.

@Hydrocharged
Copy link
Collaborator

This is definitely a step in the right direction, but there are some details that we need to nail down before we can merge this in. For example, right now comparisons are done by directly comparing the values. My hunch would be that they use the comparison functions for the appropriate types, which would need the type information available at that point.

When you're back next week, we can hop on a quick call and go over a few more details.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

ROW keyword for tuples not yet supported
2 participants