feat: data representations allow custom parsing and formatting of API fields.

See PR #2523. Most notable code changes:

- Load data representation casts into schema cache.
- Data representations for reads, filters, inserts, updates, views, over joins.
- `CoercibleField` represents name references in queries where coercion may be needed.
- `ResolverContext` help facilitate field resolution during planning.
- Planner 'resolves' names in the API query and pairs them with any implicit conversions to be used in the query builder stage.
- Tests for all of the above.
- More consistent naming (TypedX -> CoercibleX).

New: unit tests for more data representation use cases; helpful as examples as well.

New: update CHANGELOG with data representations feature description.

Fixed failing idempotence test.

New: replace date formatter test with one that does something.

Fixup: inadvertent CHANGELOG change after rebase.

Cleanup: `tfName` -> `cfName` and related.

Document what IRType means.

Formatting.

New: use a subquery to interpret `IN` literals requiring data rep transformation.

- With the previous method, very long queries such as `ANY (ARRAY[test.color('000100'), test.color('CAFE12'), test.color('01E240'), ...` could be generated. Consider the case where the parser function name is 45 characters and there's a hundred literals. That's 4.5kB of SQL just for the function name alone!
- New version uses `unnest`: `ANY (SELECT test.color(unnest('{000100,CAFE12,01E240,...}'::text[]))` to produce a much shorter query.
- This is likely to be more performant and either way much more readable and debuggable in the logs.
This commit is contained in:
Alexander Ljungberg
2023-06-29 15:01:58 -05:00
committed by Steve Chavez
parent 078c6ec08c
commit 0a1564ba5a
17 changed files with 1045 additions and 163 deletions
+142 -1
View File
@@ -2774,9 +2774,20 @@ BEGIN
LOAD 'safeupdate';
END; $$ LANGUAGE plpgsql SECURITY DEFINER;
-- This tests data representations over computed joins: even a lower case title should come back title cased.
DROP DOMAIN IF EXISTS public.titlecasetext CASCADE;
CREATE DOMAIN public.titlecasetext AS text;
CREATE OR REPLACE FUNCTION json(public.titlecasetext) RETURNS json AS $$
SELECT to_json(INITCAP($1::text));
$$ LANGUAGE SQL IMMUTABLE;
CREATE CAST (public.titlecasetext AS json) WITH FUNCTION json(public.titlecasetext) AS IMPLICIT;
-- End of data representations specific stuff except for where the domain is used in the table.
CREATE TABLE designers (
id int primary key
, name text
, name public.titlecasetext
);
CREATE TABLE videogames (
@@ -3091,6 +3102,136 @@ create table test.subscriptions(
primary key(subscriber, subscribed)
);
-- Data representations feature
DROP DOMAIN IF EXISTS public.color CASCADE;
CREATE DOMAIN public.color AS INTEGER CHECK (VALUE >= 0 AND VALUE <= 16777215);
CREATE OR REPLACE FUNCTION color(json) RETURNS public.color AS $$
SELECT color($1 #>> '{}');
$$ LANGUAGE SQL IMMUTABLE;
CREATE OR REPLACE FUNCTION color(text) RETURNS public.color AS $$
SELECT (('x' || lpad((CASE WHEN SUBSTRING($1::text, 1, 1) = '#' THEN SUBSTRING($1::text, 2) ELSE $1::text END), 8, '0'))::bit(32)::int)::public.color;
$$ LANGUAGE SQL IMMUTABLE;
CREATE OR REPLACE FUNCTION json(public.color) RETURNS json AS $$
SELECT
CASE WHEN $1 IS NULL THEN to_json(''::text)
ELSE to_json('#' || lpad(upper(to_hex($1)), 6, '0'))
END;
$$ LANGUAGE SQL IMMUTABLE;
CREATE CAST (public.color AS json) WITH FUNCTION json(public.color) AS IMPLICIT;
CREATE CAST (json AS public.color) WITH FUNCTION color(json) AS IMPLICIT;
CREATE CAST (text AS public.color) WITH FUNCTION color(text) AS IMPLICIT;
DROP DOMAIN IF EXISTS public.isodate CASCADE;
CREATE DOMAIN public.isodate AS timestamp with time zone;
CREATE OR REPLACE FUNCTION isodate(json) RETURNS public.isodate AS $$
SELECT isodate($1 #>> '{}');
$$ LANGUAGE SQL IMMUTABLE;
CREATE OR REPLACE FUNCTION isodate(text) RETURNS public.isodate AS $$
SELECT (replace($1, 'Z', '+00:00')::timestamp with time zone)::public.isodate;
$$ LANGUAGE SQL IMMUTABLE;
CREATE OR REPLACE FUNCTION json(public.isodate) RETURNS json AS $$
SELECT to_json(replace(to_json($1)#>>'{}', '+00:00', 'Z'));
$$ LANGUAGE SQL IMMUTABLE;
CREATE CAST (public.isodate AS json) WITH FUNCTION json(public.isodate) AS IMPLICIT;
CREATE CAST (json AS public.isodate) WITH FUNCTION isodate(json) AS IMPLICIT;
-- We intentionally don't have this in order to test query string parsing doesn't try to fall back on JSON parsing.
-- CREATE CAST (text AS public.isodate) WITH FUNCTION isodate(text) AS IMPLICIT;
-- bytea_b64 is a base64-encoded binary string
DROP DOMAIN IF EXISTS public.bytea_b64 CASCADE;
CREATE DOMAIN public.bytea_b64 AS bytea;
CREATE OR REPLACE FUNCTION bytea_b64(json) RETURNS public.bytea_b64 AS $$
SELECT bytea_b64($1 #>> '{}');
$$ LANGUAGE SQL IMMUTABLE;
CREATE OR REPLACE FUNCTION bytea_b64(text) RETURNS public.bytea_b64 AS $$
-- allow unpadded base64
SELECT decode($1 || repeat('=', 4 - (length($1) % 4)), 'base64')::public.bytea_b64;
$$ LANGUAGE SQL IMMUTABLE;
CREATE OR REPLACE FUNCTION json(public.bytea_b64) RETURNS json AS $$
SELECT to_json(translate(encode($1, 'base64'), E'\n', ''));
$$ LANGUAGE SQL IMMUTABLE;
CREATE CAST (public.bytea_b64 AS json) WITH FUNCTION json(public.bytea_b64) AS IMPLICIT;
CREATE CAST (json AS public.bytea_b64) WITH FUNCTION bytea_b64(json) AS IMPLICIT;
CREATE CAST (text AS public.bytea_b64) WITH FUNCTION bytea_b64(text) AS IMPLICIT;
-- unixtz is a timestamptz represented as an integer number of seconds since the Unix epoch
DROP DOMAIN IF EXISTS public.unixtz CASCADE;
CREATE DOMAIN public.unixtz AS timestamp with time zone;
CREATE OR REPLACE FUNCTION unixtz(json) RETURNS public.unixtz AS $$
SELECT unixtz($1 #>> '{}');
$$ LANGUAGE SQL IMMUTABLE;
CREATE OR REPLACE FUNCTION unixtz(text) RETURNS public.unixtz AS $$
SELECT (to_timestamp($1::numeric)::public.unixtz);
$$ LANGUAGE SQL IMMUTABLE;
CREATE OR REPLACE FUNCTION json(public.unixtz) RETURNS json AS $$
SELECT to_json(extract(epoch from $1)::bigint);
$$ LANGUAGE SQL IMMUTABLE;
CREATE CAST (public.unixtz AS json) WITH FUNCTION json(public.unixtz) AS IMPLICIT;
CREATE CAST (json AS public.unixtz) WITH FUNCTION unixtz(json) AS IMPLICIT;
CREATE CAST (text AS public.unixtz) WITH FUNCTION unixtz(text) AS IMPLICIT;
DROP DOMAIN IF EXISTS public.monetary CASCADE;
CREATE DOMAIN public.monetary AS numeric(17,2);
CREATE OR REPLACE FUNCTION monetary(json) RETURNS public.monetary AS $$
SELECT monetary($1 #>> '{}');
$$ LANGUAGE SQL IMMUTABLE;
CREATE OR REPLACE FUNCTION monetary(text) RETURNS public.monetary AS $$
SELECT ($1::numeric)::public.monetary;
$$ LANGUAGE SQL IMMUTABLE;
CREATE OR REPLACE FUNCTION json(public.monetary) RETURNS json AS $$
SELECT to_json($1::text);
$$ LANGUAGE SQL IMMUTABLE;
CREATE CAST (public.monetary AS json) WITH FUNCTION json(public.monetary) AS IMPLICIT;
CREATE CAST (json AS public.monetary) WITH FUNCTION monetary(json) AS IMPLICIT;
CREATE CAST (text AS public.monetary) WITH FUNCTION monetary(text) AS IMPLICIT;
CREATE TABLE datarep_todos (
id bigint primary key,
name text,
label_color public.color default 0,
due_at public.isodate default '2018-01-01'::date,
icon_image public.bytea_b64,
created_at public.unixtz default '2017-12-14 01:02:30'::timestamptz,
budget public.monetary default 0
);
CREATE TABLE datarep_next_two_todos (
id bigint primary key,
first_item_id bigint references datarep_todos(id),
second_item_id bigint references datarep_todos(id),
name text
);
CREATE VIEW datarep_todos_computed as (
SELECT id,
name,
label_color,
due_at,
(label_color / 2)::public.color as dark_color
FROM datarep_todos
);
-- view's name is alphabetically before projects
create view test.alpha_projects as
select c.id, p.name as pro_name, c.name as cli_name