]> granicus.if.org Git - postgresql/commitdiff
Fix incorrect declaration of citext's regexp_matches() functions.
authorTom Lane <tgl@sss.pgh.pa.us>
Tue, 5 May 2015 19:50:53 +0000 (15:50 -0400)
committerTom Lane <tgl@sss.pgh.pa.us>
Tue, 5 May 2015 19:50:53 +0000 (15:50 -0400)
These functions should return SETOF TEXT[], like the core functions they
are wrappers for; but they were incorrectly declared as returning just
TEXT[].  This mistake had two results: first, if there was no match you got
a scalar null result, whereas what you should get is an empty set (zero
rows).  Second, the 'g' flag was effectively ignored, since you would get
only one result array even if there were multiple matches, as reported by
Jeff Certain.

While ignoring 'g' is a clear bug, the behavior for no matches might well
have been thought to be the intended behavior by people who hadn't compared
it carefully to the core regexp_matches() functions.  So we should tread
carefully about introducing this change in the back branches.  Still, it
clearly is a bug and so providing some fix is desirable.

After discussion, the conclusion was to introduce the change in a 1.1
version of the citext extension (as we would need to do anyway); 1.0 still
contains the incorrect behavior.  1.1 is the default and only available
version in HEAD, but it is optional in the back branches, where 1.0 remains
the default version.  People wishing to adopt the fix in back branches will
need to explicitly do ALTER EXTENSION citext UPDATE TO '1.1'.  (I also
provided a downgrade script in the back branches, so people could go back
to 1.0 if necessary.)

This should be called out as an incompatible change in the 9.5 release
notes, although we'll also document it in the next set of back-branch
release notes.  The notes should mention that any views or rules that use
citext's regexp_matches() functions will need to be dropped before
upgrading to 1.1, and then recreated again afterwards.

Back-patch to 9.1.  The bug goes all the way back to citext's introduction
in 8.4, but pre-9.1 there is no extension mechanism with which to manage
the change.  Given the lack of previous complaints it seems unnecessary to
change this behavior in 9.0, anyway.

contrib/citext/Makefile
contrib/citext/citext--1.0--1.1.sql [new file with mode: 0644]
contrib/citext/citext--1.1--1.0.sql [new file with mode: 0644]
contrib/citext/citext--1.1.sql [new file with mode: 0644]

index 65942528dd030090a6a41b994c59bb09b8226df4..e2ca7bb32c895656fb71ec5038135c3c504556e4 100644 (file)
@@ -3,7 +3,9 @@
 MODULES = citext
 
 EXTENSION = citext
-DATA = citext--1.0.sql citext--unpackaged--1.0.sql
+DATA = citext--1.0.sql citext--1.1.sql citext--1.0--1.1.sql \
+       citext--1.1--1.0.sql citext--unpackaged--1.0.sql
+PGFILEDESC = "citext - case-insensitive character string data type"
 
 REGRESS = citext
 
diff --git a/contrib/citext/citext--1.0--1.1.sql b/contrib/citext/citext--1.0--1.1.sql
new file mode 100644 (file)
index 0000000..e06627e
--- /dev/null
@@ -0,0 +1,21 @@
+/* contrib/citext/citext--1.0--1.1.sql */
+
+-- complain if script is sourced in psql, rather than via ALTER EXTENSION
+\echo Use "ALTER EXTENSION citext UPDATE TO '1.1'" to load this file. \quit
+
+/* First we have to remove them from the extension */
+ALTER EXTENSION citext DROP FUNCTION regexp_matches( citext, citext );
+ALTER EXTENSION citext DROP FUNCTION regexp_matches( citext, citext, text );
+
+/* Then we can drop them */
+DROP FUNCTION regexp_matches( citext, citext );
+DROP FUNCTION regexp_matches( citext, citext, text );
+
+/* Now redefine */
+CREATE FUNCTION regexp_matches( citext, citext ) RETURNS SETOF TEXT[] AS $$
+    SELECT pg_catalog.regexp_matches( $1::pg_catalog.text, $2::pg_catalog.text, 'i' );
+$$ LANGUAGE SQL IMMUTABLE STRICT ROWS 1;
+
+CREATE FUNCTION regexp_matches( citext, citext, text ) RETURNS SETOF TEXT[] AS $$
+    SELECT pg_catalog.regexp_matches( $1::pg_catalog.text, $2::pg_catalog.text, CASE WHEN pg_catalog.strpos($3, 'c') = 0 THEN  $3 || 'i' ELSE $3 END );
+$$ LANGUAGE SQL IMMUTABLE STRICT ROWS 10;
diff --git a/contrib/citext/citext--1.1--1.0.sql b/contrib/citext/citext--1.1--1.0.sql
new file mode 100644 (file)
index 0000000..81f243c
--- /dev/null
@@ -0,0 +1,21 @@
+/* contrib/citext/citext--1.1--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via ALTER EXTENSION
+\echo Use "ALTER EXTENSION citext UPDATE TO '1.0'" to load this file. \quit
+
+/* First we have to remove them from the extension */
+ALTER EXTENSION citext DROP FUNCTION regexp_matches( citext, citext );
+ALTER EXTENSION citext DROP FUNCTION regexp_matches( citext, citext, text );
+
+/* Then we can drop them */
+DROP FUNCTION regexp_matches( citext, citext );
+DROP FUNCTION regexp_matches( citext, citext, text );
+
+/* Now redefine */
+CREATE FUNCTION regexp_matches( citext, citext ) RETURNS TEXT[] AS $$
+    SELECT pg_catalog.regexp_matches( $1::pg_catalog.text, $2::pg_catalog.text, 'i' );
+$$ LANGUAGE SQL IMMUTABLE STRICT;
+
+CREATE FUNCTION regexp_matches( citext, citext, text ) RETURNS TEXT[] AS $$
+    SELECT pg_catalog.regexp_matches( $1::pg_catalog.text, $2::pg_catalog.text, CASE WHEN pg_catalog.strpos($3, 'c') = 0 THEN  $3 || 'i' ELSE $3 END );
+$$ LANGUAGE SQL IMMUTABLE STRICT;
diff --git a/contrib/citext/citext--1.1.sql b/contrib/citext/citext--1.1.sql
new file mode 100644 (file)
index 0000000..9ea7c64
--- /dev/null
@@ -0,0 +1,489 @@
+/* contrib/citext/citext--1.1.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION citext" to load this file. \quit
+
+--
+--  PostgreSQL code for CITEXT.
+--
+-- Most I/O functions, and a few others, piggyback on the "text" type
+-- functions via the implicit cast to text.
+--
+
+--
+-- Shell type to keep things a bit quieter.
+--
+
+CREATE TYPE citext;
+
+--
+--  Input and output functions.
+--
+CREATE FUNCTION citextin(cstring)
+RETURNS citext
+AS 'textin'
+LANGUAGE internal IMMUTABLE STRICT;
+
+CREATE FUNCTION citextout(citext)
+RETURNS cstring
+AS 'textout'
+LANGUAGE internal IMMUTABLE STRICT;
+
+CREATE FUNCTION citextrecv(internal)
+RETURNS citext
+AS 'textrecv'
+LANGUAGE internal STABLE STRICT;
+
+CREATE FUNCTION citextsend(citext)
+RETURNS bytea
+AS 'textsend'
+LANGUAGE internal STABLE STRICT;
+
+--
+--  The type itself.
+--
+
+CREATE TYPE citext (
+    INPUT          = citextin,
+    OUTPUT         = citextout,
+    RECEIVE        = citextrecv,
+    SEND           = citextsend,
+    INTERNALLENGTH = VARIABLE,
+    STORAGE        = extended,
+    -- make it a non-preferred member of string type category
+    CATEGORY       = 'S',
+    PREFERRED      = false,
+    COLLATABLE     = true
+);
+
+--
+-- Type casting functions for those situations where the I/O casts don't
+-- automatically kick in.
+--
+
+CREATE FUNCTION citext(bpchar)
+RETURNS citext
+AS 'rtrim1'
+LANGUAGE internal IMMUTABLE STRICT;
+
+CREATE FUNCTION citext(boolean)
+RETURNS citext
+AS 'booltext'
+LANGUAGE internal IMMUTABLE STRICT;
+
+CREATE FUNCTION citext(inet)
+RETURNS citext
+AS 'network_show'
+LANGUAGE internal IMMUTABLE STRICT;
+
+--
+--  Implicit and assignment type casts.
+--
+
+CREATE CAST (citext AS text)    WITHOUT FUNCTION AS IMPLICIT;
+CREATE CAST (citext AS varchar) WITHOUT FUNCTION AS IMPLICIT;
+CREATE CAST (citext AS bpchar)  WITHOUT FUNCTION AS ASSIGNMENT;
+CREATE CAST (text AS citext)    WITHOUT FUNCTION AS ASSIGNMENT;
+CREATE CAST (varchar AS citext) WITHOUT FUNCTION AS ASSIGNMENT;
+CREATE CAST (bpchar AS citext)  WITH FUNCTION citext(bpchar)  AS ASSIGNMENT;
+CREATE CAST (boolean AS citext) WITH FUNCTION citext(boolean) AS ASSIGNMENT;
+CREATE CAST (inet AS citext)    WITH FUNCTION citext(inet)    AS ASSIGNMENT;
+
+--
+-- Operator Functions.
+--
+
+CREATE FUNCTION citext_eq( citext, citext )
+RETURNS bool
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE STRICT;
+
+CREATE FUNCTION citext_ne( citext, citext )
+RETURNS bool
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE STRICT;
+
+CREATE FUNCTION citext_lt( citext, citext )
+RETURNS bool
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE STRICT;
+
+CREATE FUNCTION citext_le( citext, citext )
+RETURNS bool
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE STRICT;
+
+CREATE FUNCTION citext_gt( citext, citext )
+RETURNS bool
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE STRICT;
+
+CREATE FUNCTION citext_ge( citext, citext )
+RETURNS bool
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE STRICT;
+
+--
+-- Operators.
+--
+
+CREATE OPERATOR = (
+    LEFTARG    = CITEXT,
+    RIGHTARG   = CITEXT,
+    COMMUTATOR = =,
+    NEGATOR    = <>,
+    PROCEDURE  = citext_eq,
+    RESTRICT   = eqsel,
+    JOIN       = eqjoinsel,
+    HASHES,
+    MERGES
+);
+
+CREATE OPERATOR <> (
+    LEFTARG    = CITEXT,
+    RIGHTARG   = CITEXT,
+    NEGATOR    = =,
+    COMMUTATOR = <>,
+    PROCEDURE  = citext_ne,
+    RESTRICT   = neqsel,
+    JOIN       = neqjoinsel
+);
+
+CREATE OPERATOR < (
+    LEFTARG    = CITEXT,
+    RIGHTARG   = CITEXT,
+    NEGATOR    = >=,
+    COMMUTATOR = >,
+    PROCEDURE  = citext_lt,
+    RESTRICT   = scalarltsel,
+    JOIN       = scalarltjoinsel
+);
+
+CREATE OPERATOR <= (
+    LEFTARG    = CITEXT,
+    RIGHTARG   = CITEXT,
+    NEGATOR    = >,
+    COMMUTATOR = >=,
+    PROCEDURE  = citext_le,
+    RESTRICT   = scalarltsel,
+    JOIN       = scalarltjoinsel
+);
+
+CREATE OPERATOR >= (
+    LEFTARG    = CITEXT,
+    RIGHTARG   = CITEXT,
+    NEGATOR    = <,
+    COMMUTATOR = <=,
+    PROCEDURE  = citext_ge,
+    RESTRICT   = scalargtsel,
+    JOIN       = scalargtjoinsel
+);
+
+CREATE OPERATOR > (
+    LEFTARG    = CITEXT,
+    RIGHTARG   = CITEXT,
+    NEGATOR    = <=,
+    COMMUTATOR = <,
+    PROCEDURE  = citext_gt,
+    RESTRICT   = scalargtsel,
+    JOIN       = scalargtjoinsel
+);
+
+--
+-- Support functions for indexing.
+--
+
+CREATE FUNCTION citext_cmp(citext, citext)
+RETURNS int4
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT IMMUTABLE;
+
+CREATE FUNCTION citext_hash(citext)
+RETURNS int4
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT IMMUTABLE;
+
+--
+-- The btree indexing operator class.
+--
+
+CREATE OPERATOR CLASS citext_ops
+DEFAULT FOR TYPE CITEXT USING btree AS
+    OPERATOR    1   <  (citext, citext),
+    OPERATOR    2   <= (citext, citext),
+    OPERATOR    3   =  (citext, citext),
+    OPERATOR    4   >= (citext, citext),
+    OPERATOR    5   >  (citext, citext),
+    FUNCTION    1   citext_cmp(citext, citext);
+
+--
+-- The hash indexing operator class.
+--
+
+CREATE OPERATOR CLASS citext_ops
+DEFAULT FOR TYPE citext USING hash AS
+    OPERATOR    1   =  (citext, citext),
+    FUNCTION    1   citext_hash(citext);
+
+--
+-- Aggregates.
+--
+
+CREATE FUNCTION citext_smaller(citext, citext)
+RETURNS citext
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE STRICT;
+
+CREATE FUNCTION citext_larger(citext, citext)
+RETURNS citext
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE STRICT;
+
+CREATE AGGREGATE min(citext)  (
+    SFUNC = citext_smaller,
+    STYPE = citext,
+    SORTOP = <
+);
+
+CREATE AGGREGATE max(citext)  (
+    SFUNC = citext_larger,
+    STYPE = citext,
+    SORTOP = >
+);
+
+--
+-- CITEXT pattern matching.
+--
+
+CREATE FUNCTION texticlike(citext, citext)
+RETURNS bool AS 'texticlike'
+LANGUAGE internal IMMUTABLE STRICT;
+
+CREATE FUNCTION texticnlike(citext, citext)
+RETURNS bool AS 'texticnlike'
+LANGUAGE internal IMMUTABLE STRICT;
+
+CREATE FUNCTION texticregexeq(citext, citext)
+RETURNS bool AS 'texticregexeq'
+LANGUAGE internal IMMUTABLE STRICT;
+
+CREATE FUNCTION texticregexne(citext, citext)
+RETURNS bool AS 'texticregexne'
+LANGUAGE internal IMMUTABLE STRICT;
+
+CREATE OPERATOR ~ (
+    PROCEDURE = texticregexeq,
+    LEFTARG   = citext,
+    RIGHTARG  = citext,
+    NEGATOR   = !~,
+    RESTRICT  = icregexeqsel,
+    JOIN      = icregexeqjoinsel
+);
+
+CREATE OPERATOR ~* (
+    PROCEDURE = texticregexeq,
+    LEFTARG   = citext,
+    RIGHTARG  = citext,
+    NEGATOR   = !~*,
+    RESTRICT  = icregexeqsel,
+    JOIN      = icregexeqjoinsel
+);
+
+CREATE OPERATOR !~ (
+    PROCEDURE = texticregexne,
+    LEFTARG   = citext,
+    RIGHTARG  = citext,
+    NEGATOR   = ~,
+    RESTRICT  = icregexnesel,
+    JOIN      = icregexnejoinsel
+);
+
+CREATE OPERATOR !~* (
+    PROCEDURE = texticregexne,
+    LEFTARG   = citext,
+    RIGHTARG  = citext,
+    NEGATOR   = ~*,
+    RESTRICT  = icregexnesel,
+    JOIN      = icregexnejoinsel
+);
+
+CREATE OPERATOR ~~ (
+    PROCEDURE = texticlike,
+    LEFTARG   = citext,
+    RIGHTARG  = citext,
+    NEGATOR   = !~~,
+    RESTRICT  = iclikesel,
+    JOIN      = iclikejoinsel
+);
+
+CREATE OPERATOR ~~* (
+    PROCEDURE = texticlike,
+    LEFTARG   = citext,
+    RIGHTARG  = citext,
+    NEGATOR   = !~~*,
+    RESTRICT  = iclikesel,
+    JOIN      = iclikejoinsel
+);
+
+CREATE OPERATOR !~~ (
+    PROCEDURE = texticnlike,
+    LEFTARG   = citext,
+    RIGHTARG  = citext,
+    NEGATOR   = ~~,
+    RESTRICT  = icnlikesel,
+    JOIN      = icnlikejoinsel
+);
+
+CREATE OPERATOR !~~* (
+    PROCEDURE = texticnlike,
+    LEFTARG   = citext,
+    RIGHTARG  = citext,
+    NEGATOR   = ~~*,
+    RESTRICT  = icnlikesel,
+    JOIN      = icnlikejoinsel
+);
+
+--
+-- Matching citext to text.
+--
+
+CREATE FUNCTION texticlike(citext, text)
+RETURNS bool AS 'texticlike'
+LANGUAGE internal IMMUTABLE STRICT;
+
+CREATE FUNCTION texticnlike(citext, text)
+RETURNS bool AS 'texticnlike'
+LANGUAGE internal IMMUTABLE STRICT;
+
+CREATE FUNCTION texticregexeq(citext, text)
+RETURNS bool AS 'texticregexeq'
+LANGUAGE internal IMMUTABLE STRICT;
+
+CREATE FUNCTION texticregexne(citext, text)
+RETURNS bool AS 'texticregexne'
+LANGUAGE internal IMMUTABLE STRICT;
+
+CREATE OPERATOR ~ (
+    PROCEDURE = texticregexeq,
+    LEFTARG   = citext,
+    RIGHTARG  = text,
+    NEGATOR   = !~,
+    RESTRICT  = icregexeqsel,
+    JOIN      = icregexeqjoinsel
+);
+
+CREATE OPERATOR ~* (
+    PROCEDURE = texticregexeq,
+    LEFTARG   = citext,
+    RIGHTARG  = text,
+    NEGATOR   = !~*,
+    RESTRICT  = icregexeqsel,
+    JOIN      = icregexeqjoinsel
+);
+
+CREATE OPERATOR !~ (
+    PROCEDURE = texticregexne,
+    LEFTARG   = citext,
+    RIGHTARG  = text,
+    NEGATOR   = ~,
+    RESTRICT  = icregexnesel,
+    JOIN      = icregexnejoinsel
+);
+
+CREATE OPERATOR !~* (
+    PROCEDURE = texticregexne,
+    LEFTARG   = citext,
+    RIGHTARG  = text,
+    NEGATOR   = ~*,
+    RESTRICT  = icregexnesel,
+    JOIN      = icregexnejoinsel
+);
+
+CREATE OPERATOR ~~ (
+    PROCEDURE = texticlike,
+    LEFTARG   = citext,
+    RIGHTARG  = text,
+    NEGATOR   = !~~,
+    RESTRICT  = iclikesel,
+    JOIN      = iclikejoinsel
+);
+
+CREATE OPERATOR ~~* (
+    PROCEDURE = texticlike,
+    LEFTARG   = citext,
+    RIGHTARG  = text,
+    NEGATOR   = !~~*,
+    RESTRICT  = iclikesel,
+    JOIN      = iclikejoinsel
+);
+
+CREATE OPERATOR !~~ (
+    PROCEDURE = texticnlike,
+    LEFTARG   = citext,
+    RIGHTARG  = text,
+    NEGATOR   = ~~,
+    RESTRICT  = icnlikesel,
+    JOIN      = icnlikejoinsel
+);
+
+CREATE OPERATOR !~~* (
+    PROCEDURE = texticnlike,
+    LEFTARG   = citext,
+    RIGHTARG  = text,
+    NEGATOR   = ~~*,
+    RESTRICT  = icnlikesel,
+    JOIN      = icnlikejoinsel
+);
+
+--
+-- Matching citext in string comparison functions.
+-- XXX TODO Ideally these would be implemented in C.
+--
+
+CREATE FUNCTION regexp_matches( citext, citext ) RETURNS SETOF TEXT[] AS $$
+    SELECT pg_catalog.regexp_matches( $1::pg_catalog.text, $2::pg_catalog.text, 'i' );
+$$ LANGUAGE SQL IMMUTABLE STRICT ROWS 1;
+
+CREATE FUNCTION regexp_matches( citext, citext, text ) RETURNS SETOF TEXT[] AS $$
+    SELECT pg_catalog.regexp_matches( $1::pg_catalog.text, $2::pg_catalog.text, CASE WHEN pg_catalog.strpos($3, 'c') = 0 THEN  $3 || 'i' ELSE $3 END );
+$$ LANGUAGE SQL IMMUTABLE STRICT ROWS 10;
+
+CREATE FUNCTION regexp_replace( citext, citext, text ) returns TEXT AS $$
+    SELECT pg_catalog.regexp_replace( $1::pg_catalog.text, $2::pg_catalog.text, $3, 'i');
+$$ LANGUAGE SQL IMMUTABLE STRICT;
+
+CREATE FUNCTION regexp_replace( citext, citext, text, text ) returns TEXT AS $$
+    SELECT pg_catalog.regexp_replace( $1::pg_catalog.text, $2::pg_catalog.text, $3, CASE WHEN pg_catalog.strpos($4, 'c') = 0 THEN  $4 || 'i' ELSE $4 END);
+$$ LANGUAGE SQL IMMUTABLE STRICT;
+
+CREATE FUNCTION regexp_split_to_array( citext, citext ) RETURNS TEXT[] AS $$
+    SELECT pg_catalog.regexp_split_to_array( $1::pg_catalog.text, $2::pg_catalog.text, 'i' );
+$$ LANGUAGE SQL IMMUTABLE STRICT;
+
+CREATE FUNCTION regexp_split_to_array( citext, citext, text ) RETURNS TEXT[] AS $$
+    SELECT pg_catalog.regexp_split_to_array( $1::pg_catalog.text, $2::pg_catalog.text, CASE WHEN pg_catalog.strpos($3, 'c') = 0 THEN  $3 || 'i' ELSE $3 END );
+$$ LANGUAGE SQL IMMUTABLE STRICT;
+
+CREATE FUNCTION regexp_split_to_table( citext, citext ) RETURNS SETOF TEXT AS $$
+    SELECT pg_catalog.regexp_split_to_table( $1::pg_catalog.text, $2::pg_catalog.text, 'i' );
+$$ LANGUAGE SQL IMMUTABLE STRICT;
+
+CREATE FUNCTION regexp_split_to_table( citext, citext, text ) RETURNS SETOF TEXT AS $$
+    SELECT pg_catalog.regexp_split_to_table( $1::pg_catalog.text, $2::pg_catalog.text, CASE WHEN pg_catalog.strpos($3, 'c') = 0 THEN  $3 || 'i' ELSE $3 END );
+$$ LANGUAGE SQL IMMUTABLE STRICT;
+
+CREATE FUNCTION strpos( citext, citext ) RETURNS INT AS $$
+    SELECT pg_catalog.strpos( pg_catalog.lower( $1::pg_catalog.text ), pg_catalog.lower( $2::pg_catalog.text ) );
+$$ LANGUAGE SQL IMMUTABLE STRICT;
+
+CREATE FUNCTION replace( citext, citext, citext ) RETURNS TEXT AS $$
+    SELECT pg_catalog.regexp_replace( $1::pg_catalog.text, pg_catalog.regexp_replace($2::pg_catalog.text, '([^a-zA-Z_0-9])', E'\\\\\\1', 'g'), $3::pg_catalog.text, 'gi' );
+$$ LANGUAGE SQL IMMUTABLE STRICT;
+
+CREATE FUNCTION split_part( citext, citext, int ) RETURNS TEXT AS $$
+    SELECT (pg_catalog.regexp_split_to_array( $1::pg_catalog.text, pg_catalog.regexp_replace($2::pg_catalog.text, '([^a-zA-Z_0-9])', E'\\\\\\1', 'g'), 'i'))[$3];
+$$ LANGUAGE SQL IMMUTABLE STRICT;
+
+CREATE FUNCTION translate( citext, citext, text ) RETURNS TEXT AS $$
+    SELECT pg_catalog.translate( pg_catalog.translate( $1::pg_catalog.text, pg_catalog.lower($2::pg_catalog.text), $3), pg_catalog.upper($2::pg_catalog.text), $3);
+$$ LANGUAGE SQL IMMUTABLE STRICT;