Database functions provide an effective way to create parameterized, reusable reports in Metadb. Instead of writing the same query with different values, you can define a function once and call it with different parameters.
Metadb does not support database views. Use database functions instead, as they provide better flexibility and don’t create blocking dependencies during schema updates.
Let’s walk through creating a parameterized report function.
1
Start with a base query
Begin with a working SQL query:
SELECT item_id, COUNT(*) AS loan_countFROM folio_circulation.loan__tWHERE '2023-01-01' <= loan_date AND loan_date < '2024-01-01'GROUP BY item_id;
This counts loans for each item in 2023.
2
Identify parameters
Determine which values should be parameterized. In this example:
Start date: '2023-01-01'
End date: '2024-01-01'
3
Create the function
Convert the query into a function with parameters:
CREATE FUNCTION lisa.count_loans( start_date DATE DEFAULT '2000-01-01', end_date DATE DEFAULT '2050-01-01')RETURNS TABLE( item_id UUID, loan_count INTEGER)AS $$ SELECT item_id, COUNT(*) AS loan_count FROM folio_circulation.loan__t WHERE start_date <= loan_date AND loan_date < end_date GROUP BY item_id$$LANGUAGE SQLSTABLEPARALLEL SAFE;
4
Call the function
Execute the function with different parameters:
-- Loans from 2022SELECT * FROM lisa.count_loans( start_date => '2022-01-01', end_date => '2023-01-01');-- Loans from 2023 onward (uses default end_date)SELECT * FROM lisa.count_loans( start_date => '2023-01-01');
Using named parameters (start_date =>) makes function calls more readable and allows you to omit parameters that have defaults.
CREATE FUNCTION analytics.patron_activity( origin_filter TEXT DEFAULT '%', group_filter TEXT DEFAULT '%', min_loan_count INTEGER DEFAULT 0)RETURNS TABLE( patron_id UUID, patron_group TEXT, origin TEXT, total_loans BIGINT)AS $$ SELECT p.id AS patron_id, pg.groupname AS patron_group, p.__origin AS origin, COUNT(l.id) AS total_loans FROM library.patron p JOIN library.patrongroup pg ON p.patron_group_id = pg.id LEFT JOIN folio_circulation.loan__t l ON l.patron_id = p.id WHERE p.__origin LIKE origin_filter AND pg.groupname LIKE group_filter GROUP BY p.id, pg.groupname, p.__origin HAVING COUNT(l.id) >= min_loan_count ORDER BY total_loans DESC$$LANGUAGE SQLSTABLEPARALLEL SAFE;
Usage:
-- All patrons with at least 10 loansSELECT * FROM analytics.patron_activity(min_loan_count => 10);-- Graduate students from east campusSELECT * FROM analytics.patron_activity( origin_filter => 'east', group_filter => 'graduate');
CREATE FUNCTION reports.circulation_summary( report_year INTEGER)RETURNS TABLE( month DATE, total_loans BIGINT, unique_items BIGINT, unique_patrons BIGINT)AS $$ SELECT DATE_TRUNC('month', loan_date)::DATE AS month, COUNT(*) AS total_loans, COUNT(DISTINCT item_id) AS unique_items, COUNT(DISTINCT patron_id) AS unique_patrons FROM folio_circulation.loan__t WHERE EXTRACT(YEAR FROM loan_date) = report_year GROUP BY DATE_TRUNC('month', loan_date) ORDER BY month$$LANGUAGE SQLSTABLEPARALLEL SAFE;
CREATE FUNCTION analytics.compare_periods( metric_date DATE, comparison_date DATE)RETURNS TABLE( group_name TEXT, current_count BIGINT, comparison_count BIGINT, change_count BIGINT, percent_change NUMERIC(10,2))AS $$ WITH current_data AS ( SELECT groupname, COUNT(*) AS cnt FROM library.patrongroup__ WHERE __start <= metric_date AND metric_date < __end GROUP BY groupname ), comparison_data AS ( SELECT groupname, COUNT(*) AS cnt FROM library.patrongroup__ WHERE __start <= comparison_date AND comparison_date < __end GROUP BY groupname ) SELECT COALESCE(c.groupname, p.groupname) AS group_name, COALESCE(c.cnt, 0) AS current_count, COALESCE(p.cnt, 0) AS comparison_count, COALESCE(c.cnt, 0) - COALESCE(p.cnt, 0) AS change_count, CASE WHEN COALESCE(p.cnt, 0) = 0 THEN NULL ELSE ROUND(((COALESCE(c.cnt, 0)::NUMERIC - p.cnt) / p.cnt * 100), 2) END AS percent_change FROM current_data c FULL OUTER JOIN comparison_data p ON c.groupname = p.groupname ORDER BY group_name$$LANGUAGE SQLSTABLEPARALLEL SAFE;
For analytical queries on Metadb data, use STABLE. This is appropriate for data that changes between transactions but remains consistent within a single transaction.