Use JSON objects to generate SQL WHERE
clause expressions.
Clone this repo and from within the base directory, run
>>> make install
to install the JSON_QUERY PostgreSQL extension. Once this is done, the extension can be added
to a database via PostgreSQL's CREATE EXTENSION
:
template1=# CREATE EXTENSION json_query;
NOTE: requires PostgreSQL 9.4 or higher.
PostgreSQL provides no simple way to parameterize a query's WHERE
clause
which severly limits the reusability of many types of SQL functions.
For example, given a table tracking customer purchases, it might be useful to create a function that returns the total of all outstanding amounts:
CREATE FUNCTION total_outstanding() RETURNS NUMERIC AS $$
SELECT SUM(outstanding_amount)
FROM purchase
WHERE NOT written_off;
$$ LANGUAGE SQL STABLE;
It might also be useful to apply some restrictions on the purchases included in the sum:
-- Includes only purchases made on or after the given date
CREATE FUNCTION total_outstanding_since(since_date DATE) RETURNS NUMERIC AS $$
SELECT SUM(outstanding_amount)
FROM purchase
WHERE NOT written_off AND purchase_date >= since_date;
$$ LANGUAGE SQL STABLE;
-- Includes only purchases made by the specified customer
CREATE FUNCTION total_outstanding_for_customer(cid TEXT) RETURNS NUMERIC AS $$
SELECT SUM(outstanding_amount)
FROM purchase
WHERE NOT written_off AND customer_id = cid;
$$ LANGUAGE SQL STABLE;
-- ... Etc.
Creating all of these repetitive variations on a function is tiresome and so applications will typically construct the query using some programming language specific query-builder.
Query-builders complicate SQL development by forcing database interaction to take place via the client language and make debugging/profiling more difficult since they must deal with programmatically generated queries.
This extension provides an alternative by allowing the WHERE
part of a query to be parameterized:
-- This procedure takes an optional argument customizing which rows are included in the aggregate.
CREATE FUNCTION total_outstanding(filters JSONB DEFAULT '{}') RETURNS NUMERIC AS $$
SELECT SUM(outstanding_amount)
FROM purchase p
WHERE NOT written_off AND jq_filter(p, filters);
$$ LANGUAGE SQL STABLE;
This function replaces all of the previous functions:
-- Equivalent to the original total_outstanding() procedure:
SELECT total_outstanding('{}');
-- Equivalent to total_outstanding_since_date('2015-01-01'):
SELECT total_outstanding('{"purchase_date__ge": "2015-01-01"}');
-- Equivalent to total_outstanding_for_customer('a'):
SELECT total_outstanding('{"customer_id": "a"}');
And can also perform more complex variations of the original query:
-- Total outstanding payments due from customers 'a' or 'b' for purchases on
-- or after Jan. 1, 2015:
SELECT total_outstanding('{"purchase_date__ge": "2015-01-01",
"customer_id__in": ["a","b"]}');
Before any table (or other composite types) can be used with JSON_QUERY, they must first be registered
with this function. type_name
should be the full name of the table (i.e., 'schema_name.table_name'
if the table is not in the public schema; otherwise, simply 'table_name'
).
Unregister a previously registered row type.
NOTE: If the table definition of a previously registered table changes, it should be unregistered and then registered again.
Returns a boolean indicating whether or not the record matched against the filter object. rec
should
be a row value previously registered via jq_register_row_type()
. filter_object
should be a JSONB
object specifying a where-clause using Django-style filter syntax.
Performs a filter using a JSONB array of raw filter objects. The raw filter format is more verbose than the Django filter syntax but more flexible.
A Django-style filter object is a JSONB object of the form {"<column>__<op>": <value>, ...}
generating
a whereclause similar to WHERE <column> <op> <value> AND ...
.
This is the internal format, which is exposed in the API as it is easier to work with programmatically.
These should be JSONB arrays of objects of the form [{"field": "<column>", "op": "<op>", "value": <value>}, ...]
and generate a whereclause similar to WHERE <column> <op> <value> AND ...
.
The allowed values for the <op>
expression (and the SQL operator they represent) are as follows:
eq
(=
when value is non-NULL,IS
for NULL value)ne
(<>
when vaue is non-NULL,IS NOT
for NULL value)gt
(>
)lt
(<
)le
(<=
)ge
(>=
)in
(IN
)notin
(NOT IN
)like
(LIKE
)ilike
(ILIKE
)startswith
(equivalent to<column> LIKE '<value>%'
)istartswith
(equivalent to<column> ILIKE '<value>%'
)endswith
(equivalent to<column> LIKE '%<value>'
)iendswith
(equivalent to<column> ILIKE '%<value>'
)exists
(?
)notexists
(equivalent toNOT (<column> ? <value>)
)existsany
(?|
)notexistsany
(equivalent toNOT (<column> ?| <value>)
)existsall
(?&
)notexistsall
(equivalent toNOT (<column> ?& <value>)
)contains
(@>
)contained
(<@
)
In the case of JSONB columns, the <column>
argument can reference a value nested within the JSONB object
via the JSONB lookup operators
->
(lookup as JSONB) or ->>
(lookup as TEXT).
All queries made using jq_filter()
are simplified by Postgres'
query optimizer to the same expression as would be used with the equivalent
inline query. So the only performance difference between jq_filter()
and its inline equivalent is the time it takes the query optimizer to simplify
the expression, which is typically in the low single-digit milliseconds.
For example, both of the these queries actually use the same query plan, both
making use of the primary key index. Using jq_filter()
:
EXPLAIN SELECT * FROM customer c WHERE jq_filter(c, '{"id": 1000}');
-- Output:
-- QUERY PLAN
-- ---------------------------------------------------------------------------------
-- Index Scan using customer_pkey on customer c (cost=0.29..8.31 rows=1 width=68)
-- Index Cond: (id = 1000)
and it's inline equivalent (same query plan as above):
EXPLAIN SELECT * FROM customer c WHERE id = 1000;
-- Output:
-- QUERY PLAN
-- ---------------------------------------------------------------------------------
-- Index Scan using customer_pkey on customer c (cost=0.29..8.31 rows=1 width=68)
-- Index Cond: (id = 1000)
See example.sql.
The json_query
PostgreSQL extension is released under the
PostgreSQL Licence, a
permissive Open Source license, similar to the BSD or MIT licenses.