Skip to content

Commit

Permalink
Port record summary fns over from closed PR #3561
Browse files Browse the repository at this point in the history
  • Loading branch information
seancolsen committed Oct 24, 2024
1 parent 1859317 commit 14db583
Showing 1 changed file with 210 additions and 0 deletions.
210 changes: 210 additions & 0 deletions db/sql/00_msar.sql
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,55 @@ $$ LANGUAGE plpgsql;
SELECT msar.drop_all_msar_functions();


----------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------
-- HELPER FUNCTIONS
--
-- Low-level utils functions used by other functions.
----------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------

CREATE OR REPLACE FUNCTION
msar.extract_smallints(v jsonb) RETURNS smallint[] AS $$/*
From the supplied JSONB value, extract all top-level JSONB array elements which can be successfully
cast to PostgreSQL smallint values. Return the resulting array of smallint values.
If the supplied jsonb value is not an array, this function will return an empty array.
If any jsonb array element cannot be cast to a smallint, it will be silently ignored.
This function does not raise any exceptions. It will always return an array.
This function should not be used on large arrays. It will be slow due to the performance
limitations[1] of EXCEPTION blocks.
[1]: https://www.postgresql.org/docs/current/plpgsql-control-structures.html#PLPGSQL-ERROR-TRAPPING
Args:
v: any JSONB value.
*/
DECLARE
result smallint[];
element jsonb;
BEGIN
FOR element IN SELECT jsonb_array_elements(v)
LOOP
BEGIN
result := result || (element::smallint);
EXCEPTION
-- Ignore any elements that can't be cast to smallint.
WHEN others THEN
CONTINUE;
END;
END LOOP;
RETURN result;
EXCEPTION
WHEN others THEN
RETURN '{}'::smallint[]; -- Return an empty array if the input is not an array.
END;
$$ LANGUAGE plpgsql IMMUTABLE RETURNS NULL ON NULL INPUT PARALLEL SAFE;


----------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------
-- GENERAL DDL FUNCTIONS
Expand Down Expand Up @@ -4715,6 +4764,167 @@ LIMIT 1;
$$ LANGUAGE SQL STABLE RETURNS NULL ON NULL INPUT;


CREATE OR REPLACE FUNCTION
msar.build_record_summary_query_from_template(
tab_id oid,
template jsonb
) RETURNS text AS $$/*
Given a table OID and a record summary template, this function returns a query that can be used to
generate record summaries for the table.
Args:
tab_id: The OID of the table for which to generate a record summary query.
template: A JSON array that represents the record summary template (described in detail below).
Example template:
[
"#",
[1],
" - ",
[2, 5],
" - ",
[2, 5, 10]
]
A string entry in the template represents static text to be included in the record summary
verbatim.
An array entry in the template represents a reference to data. Each element in the array is a
column attnum. The first column attnum refers to a column in the base table. If the array
contains more than one column reference, it represents a chain of FK columns starting from
the base table and ending with a non-FK column. This function follows the foreign keys to
produce the joins. Multi-column FK constraints are not supported.
Return value: a stringified query which produces a result set matching the structure described
in the return value of msar.get_record_summaries_via_query.
*/
DECLARE
base_alias CONSTANT text := 'base';
expr_parts text[] := ARRAY[]::text[];
expr text;
base_sch_name text := msar.get_relation_schema_name(tab_id);
base_tab_name text := msar.get_relation_name(tab_id);
base_pk_name text := msar.get_column_name(tab_id, msar.get_pk_column(tab_id));
template_part jsonb;
join_clauses text[] := ARRAY[]::text[];
join_section text;
BEGIN
IF base_pk_name IS NULL THEN
RAISE EXCEPTION 'Unable to find primary key column for table with oid %.', tab_id;
END IF;
IF jsonb_typeof(template) <> 'array' THEN
RAISE EXCEPTION 'Record summary template must be a JSON array.';
END IF;

<<template_parts_loop>>
FOR template_part IN SELECT jsonb_array_elements(template) LOOP
DECLARE
ref_chain smallint[] := msar.extract_smallints(template_part);
ref_chain_length integer := array_length(ref_chain, 1);
fk_col_id smallint;
contextual_tab_id oid := tab_id;
prev_alias text := base_alias;
ref_column_name text;
BEGIN
-- Column reference template parts
IF ref_chain_length > 0 THEN
-- Except for the final ref_chain element, process all array elements as attnums of FK
-- columns.
FOREACH fk_col_id IN ARRAY ref_chain[1:ref_chain_length-1] LOOP
DECLARE
fk_col_name text := msar.get_column_name(contextual_tab_id, fk_col_id);
ref_tab_id oid;
ref_col_id smallint;
ref_sch_name text;
ref_tab_name text;
ref_col_name text;
alias text;
join_clause text;
BEGIN
IF fk_col_name IS NULL THEN
-- Silently ignore references to non-existing FK columns. This can happen if a column
-- has been deleted.
CONTINUE template_parts_loop;
END IF;

SELECT confrelid, confkey[1] INTO ref_tab_id, ref_col_id
FROM pg_catalog.pg_constraint
WHERE contype = 'f' AND conrelid = contextual_tab_id AND conkey = array[fk_col_id];

IF ref_tab_id IS NULL THEN
-- Silently ignore references to non-FK columns. This can happen if the constraint
-- has been dropped.
CONTINUE template_parts_loop;
END IF;

ref_tab_name := msar.get_relation_name(ref_tab_id);
ref_sch_name := msar.get_relation_schema_name(ref_tab_id);
ref_col_name := msar.get_column_name(ref_tab_id, ref_col_id);
alias := concat(prev_alias, '_', fk_col_id);
join_clause := concat(
'LEFT JOIN ',
quote_ident(ref_sch_name), '.', quote_ident(ref_tab_name),
' AS ', alias,
' ON ',
alias, '.', quote_ident(ref_col_name),
' = ',
prev_alias, '.', quote_ident(fk_col_name)
);

IF NOT join_clauses @> ARRAY[join_clause] THEN
join_clauses := array_append(join_clauses, join_clause);
END IF;
prev_alias := alias;
contextual_tab_id := ref_tab_id;
END;
END LOOP;

ref_column_name := msar.get_column_name(contextual_tab_id, ref_chain[ref_chain_length]);
IF ref_column_name IS NOT NULL THEN
expr_parts := array_append(
expr_parts,
concat('cast(', prev_alias,'.', quote_ident(ref_column_name), ' AS text)')
);
END IF;

-- String literal template parts
ELSIF jsonb_typeof(template_part) = 'string' THEN
expr_parts := array_append(expr_parts, quote_literal(template_part #>> '{}'));
END IF;
END;
END LOOP;

IF cardinality(expr_parts) = 0 THEN
-- If the template didn't give us anything to render, then we show '?' as a fallback. This can
-- happen if (e.g.) the template only contains a reference which is no longer valid due to a
-- column being deleted.
expr_parts := array_append(expr_parts, quote_literal('?'));
END IF;

join_section := CASE
WHEN array_length(join_clauses, 1) = 0 THEN ''
ELSE chr(10) || array_to_string(join_clauses, chr(10))
END;

expr := array_to_string(expr_parts, E'\n || ');

RETURN concat(
'SELECT ', chr(10),
' ', base_alias, '.', quote_ident(base_pk_name), ' AS id, ', chr(10),
' ', expr, ' AS record_summary', chr(10),
'FROM ',
quote_ident(base_sch_name), '.', quote_ident(base_tab_name),
' AS ', base_alias,
join_section
);

-- TODO:
-- - Handle columns which can't be automatically cast to TEXT
END;
$$ LANGUAGE plpgsql;


CREATE OR REPLACE FUNCTION
msar.build_record_summary_query_for_table(tab_id oid, key_col_id smallint) RETURNS TEXT AS $$/*
Return text for an SQL query that will summarize records from a table.
Expand Down

0 comments on commit 14db583

Please sign in to comment.