]> 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:51:22 +0000 (15:51 -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.sql [moved from contrib/citext/citext--1.0.sql with 97% similarity]
contrib/citext/citext.control
contrib/citext/expected/citext.out
contrib/citext/expected/citext_1.out
contrib/citext/sql/citext.sql

index 267854b5de7a0aaff9b25117923fd6d5c5d9cb6d..61e04bce7ae72679391680dffe0114f26022e1be 100644 (file)
@@ -3,7 +3,7 @@
 MODULES = citext
 
 EXTENSION = citext
-DATA = citext--1.0.sql citext--unpackaged--1.0.sql
+DATA = citext--1.1.sql citext--1.0--1.1.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;
similarity index 97%
rename from contrib/citext/citext--1.0.sql
rename to contrib/citext/citext--1.1.sql
index 7db2e8d0aec9cca5843ff070be2630fcac9f64d7..9ea7c64709a1093cce0cd0ef40f340b6906bec0d 100644 (file)
@@ -1,4 +1,4 @@
-/* contrib/citext/citext--1.0.sql */
+/* 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
@@ -440,13 +440,13 @@ CREATE OPERATOR !~~* (
 -- XXX TODO Ideally these would be implemented in C.
 --
 
-CREATE FUNCTION regexp_matches( citext, citext ) RETURNS TEXT[] AS $$
+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;
+$$ LANGUAGE SQL IMMUTABLE STRICT ROWS 1;
 
-CREATE FUNCTION regexp_matches( citext, citext, text ) RETURNS TEXT[] AS $$
+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;
+$$ 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');
index 3eb01a3360861472a62d6a59a3c03b18c3d8dcb3..ef90a97bc981907855ad2233d262a423cada9142 100644 (file)
@@ -1,5 +1,5 @@
 # citext extension
 comment = 'data type for case-insensitive character strings'
-default_version = '1.0'
+default_version = '1.1'
 module_pathname = '$libdir/citext'
 relocatable = true
index 411b689b4b9a95dacf62b89203afd38cab9e58fa..373fe6da545d0bb5b9f268896f08e4e4a34efac0 100644 (file)
@@ -1846,11 +1846,18 @@ SELECT regexp_matches('foobarbequebaz'::citext, '(BAR)(BEQUE)'::citext, ''::cite
 (1 row)
 
 -- c forces case-sensitive
-SELECT regexp_matches('foobarbequebaz'::citext, '(BAR)(BEQUE)'::citext, 'c'::citext) = ARRAY[ 'bar', 'beque' ] AS "null";
- null 
-------
-(1 row)
+SELECT regexp_matches('foobarbequebaz'::citext, '(BAR)(BEQUE)'::citext, 'c'::citext) = ARRAY[ 'bar', 'beque' ] AS "no rows";
+ no rows 
+---------
+(0 rows)
+
+-- g allows multiple output rows
+SELECT regexp_matches('foobarbequebazmorebarbequetoo'::citext, '(BAR)(BEQUE)'::citext, 'g'::citext) AS "two rows";
+  two rows   
+-------------
+ {bar,beque}
+ {bar,beque}
+(2 rows)
 
 SELECT regexp_replace('Thomas'::citext, '.[mN]a.',         'M') = 'ThM' AS t;
  t 
index da3862f49bac1df43d7917f84bd9a8ba9e65f327..fcadd8d3929ff6e54a3a9d9d150f36592e0d2486 100644 (file)
@@ -1846,11 +1846,18 @@ SELECT regexp_matches('foobarbequebaz'::citext, '(BAR)(BEQUE)'::citext, ''::cite
 (1 row)
 
 -- c forces case-sensitive
-SELECT regexp_matches('foobarbequebaz'::citext, '(BAR)(BEQUE)'::citext, 'c'::citext) = ARRAY[ 'bar', 'beque' ] AS "null";
- null 
-------
-(1 row)
+SELECT regexp_matches('foobarbequebaz'::citext, '(BAR)(BEQUE)'::citext, 'c'::citext) = ARRAY[ 'bar', 'beque' ] AS "no rows";
+ no rows 
+---------
+(0 rows)
+
+-- g allows multiple output rows
+SELECT regexp_matches('foobarbequebazmorebarbequetoo'::citext, '(BAR)(BEQUE)'::citext, 'g'::citext) AS "two rows";
+  two rows   
+-------------
+ {bar,beque}
+ {bar,beque}
+(2 rows)
 
 SELECT regexp_replace('Thomas'::citext, '.[mN]a.',         'M') = 'ThM' AS t;
  t 
index 27678fab5df669c01c6bb3868a2bbcf5c9f07206..950895baea7998e6ee6d2ec83f0767c754ee2caf 100644 (file)
@@ -601,7 +601,9 @@ SELECT regexp_matches('foobarbequebaz'::citext, '(BAR)(BEQUE)', '') = ARRAY[ 'ba
 SELECT regexp_matches('foobarbequebaz', '(BAR)(BEQUE)'::citext, '') = ARRAY[ 'bar', 'beque' ] AS t;
 SELECT regexp_matches('foobarbequebaz'::citext, '(BAR)(BEQUE)'::citext, ''::citext) = ARRAY[ 'bar', 'beque' ] AS t;
 -- c forces case-sensitive
-SELECT regexp_matches('foobarbequebaz'::citext, '(BAR)(BEQUE)'::citext, 'c'::citext) = ARRAY[ 'bar', 'beque' ] AS "null";
+SELECT regexp_matches('foobarbequebaz'::citext, '(BAR)(BEQUE)'::citext, 'c'::citext) = ARRAY[ 'bar', 'beque' ] AS "no rows";
+-- g allows multiple output rows
+SELECT regexp_matches('foobarbequebazmorebarbequetoo'::citext, '(BAR)(BEQUE)'::citext, 'g'::citext) AS "two rows";
 
 SELECT regexp_replace('Thomas'::citext, '.[mN]a.',         'M') = 'ThM' AS t;
 SELECT regexp_replace('Thomas'::citext, '.[MN]A.',         'M') = 'ThM' AS t;