]> granicus.if.org Git - postgresql/commitdiff
Fix lexing of standard multi-character operators in edge cases.
authorAndrew Gierth <rhodiumtoad@postgresql.org>
Thu, 23 Aug 2018 17:29:18 +0000 (18:29 +0100)
committerAndrew Gierth <rhodiumtoad@postgresql.org>
Thu, 23 Aug 2018 20:35:49 +0000 (21:35 +0100)
Commits c6b3c939b (which fixed the precedence of >=, <=, <> operators)
and 865f14a2d (which added support for the standard => notation for
named arguments) created a class of lexer tokens which look like
multi-character operators but which have their own token IDs distinct
from Op. However, longest-match rules meant that following any of
these tokens with another operator character, as in (1<>-1), would
cause them to be incorrectly returned as Op.

The error here isn't immediately obvious, because the parser would
usually still find the correct operator via the Op token, but there
were more subtle problems:

1. If immediately followed by a comment or +-, >= <= <> would be given
   the old precedence of Op rather than the correct new precedence;

2. If followed by a comment, != would be returned as Op rather than as
   NOT_EQUAL, causing it not to be found at all;

3. If followed by a comment or +-, the => token for named arguments
   would be lexed as Op, causing the argument to be mis-parsed as a
   simple expression, usually causing an error.

Fix by explicitly checking for the operators in the {operator} code
block in addition to all the existing special cases there.

Backpatch to 9.5 where the problem was introduced.

Analysis and patch by me; review by Tom Lane.
Discussion: https://postgr.es/m/87va851ppl.fsf@news-spur.riddles.org.uk

src/backend/parser/scan.l
src/fe_utils/psqlscan.l
src/interfaces/ecpg/preproc/pgc.l
src/test/regress/expected/create_operator.out
src/test/regress/expected/polymorphism.out
src/test/regress/sql/create_operator.sql
src/test/regress/sql/polymorphism.sql

index 967b2eaf9b99bb8a7be40cb703c4e307f9ad3585..6cf40ac6cb88ffd246dcec858099c14578ad2921 100644 (file)
@@ -335,6 +335,15 @@ identifier         {ident_start}{ident_cont}*
 typecast               "::"
 dot_dot                        \.\.
 colon_equals   ":="
+
+/*
+ * These operator-like tokens (unlike the above ones) also match the {operator}
+ * rule, which means that they might be overridden by a longer match if they
+ * are followed by a comment start or a + or - character. Accordingly, if you
+ * add to this list, you must also add corresponding code to the {operator}
+ * block to return the correct token in such cases. (This is not needed in
+ * psqlscan.l since the token value is ignored there.)
+ */
 equals_greater "=>"
 less_equals            "<="
 greater_equals ">="
@@ -925,6 +934,25 @@ other                      .
                                                if (nchars == 1 &&
                                                        strchr(",()[].;:+-*/%^<>=", yytext[0]))
                                                        return yytext[0];
+                                               /*
+                                                * Likewise, if what we have left is two chars, and
+                                                * those match the tokens ">=", "<=", "=>", "<>" or
+                                                * "!=", then we must return the appropriate token
+                                                * rather than the generic Op.
+                                                */
+                                               if (nchars == 2)
+                                               {
+                                                       if (yytext[0] == '=' && yytext[1] == '>')
+                                                               return EQUALS_GREATER;
+                                                       if (yytext[0] == '>' && yytext[1] == '=')
+                                                               return GREATER_EQUALS;
+                                                       if (yytext[0] == '<' && yytext[1] == '=')
+                                                               return LESS_EQUALS;
+                                                       if (yytext[0] == '<' && yytext[1] == '>')
+                                                               return NOT_EQUALS;
+                                                       if (yytext[0] == '!' && yytext[1] == '=')
+                                                               return NOT_EQUALS;
+                                               }
                                        }
 
                                        /*
index fe74abc1a6cb68a2ac9a7984078daca8efd1b778..73eafebefecbe8ad9745bc165db7fd68faafc7cd 100644 (file)
@@ -296,6 +296,15 @@ identifier         {ident_start}{ident_cont}*
 typecast               "::"
 dot_dot                        \.\.
 colon_equals   ":="
+
+/*
+ * These operator-like tokens (unlike the above ones) also match the {operator}
+ * rule, which means that they might be overridden by a longer match if they
+ * are followed by a comment start or a + or - character. Accordingly, if you
+ * add to this list, you must also add corresponding code to the {operator}
+ * block to return the correct token in such cases. (This is not needed in
+ * psqlscan.l since the token value is ignored there.)
+ */
 equals_greater "=>"
 less_equals            "<="
 greater_equals ">="
index bc47c23d03808ddaecfcec06c84129f68169f0b9..3645582b43982b1fef880a40b705add152365265 100644 (file)
@@ -242,6 +242,15 @@ array                      ({ident_cont}|{whitespace}|[\[\]\+\-\*\%\/\(\)\>\.])*
 typecast               "::"
 dot_dot                        \.\.
 colon_equals   ":="
+
+/*
+ * These operator-like tokens (unlike the above ones) also match the {operator}
+ * rule, which means that they might be overridden by a longer match if they
+ * are followed by a comment start or a + or - character. Accordingly, if you
+ * add to this list, you must also add corresponding code to the {operator}
+ * block to return the correct token in such cases. (This is not needed in
+ * psqlscan.l since the token value is ignored there.)
+ */
 equals_greater "=>"
 less_equals            "<="
 greater_equals ">="
@@ -729,6 +738,25 @@ cppline                    {space}*#([^i][A-Za-z]*|{if}|{ifdef}|{ifndef}|{import})((\/\*[^*/]*\*+
                                                        if (nchars == 1 &&
                                                                strchr(",()[].;:+-*/%^<>=", yytext[0]))
                                                                return yytext[0];
+                                                       /*
+                                                        * Likewise, if what we have left is two chars, and
+                                                        * those match the tokens ">=", "<=", "=>", "<>" or
+                                                        * "!=", then we must return the appropriate token
+                                                        * rather than the generic Op.
+                                                        */
+                                                       if (nchars == 2)
+                                                       {
+                                                               if (yytext[0] == '=' && yytext[1] == '>')
+                                                                       return EQUALS_GREATER;
+                                                               if (yytext[0] == '>' && yytext[1] == '=')
+                                                                       return GREATER_EQUALS;
+                                                               if (yytext[0] == '<' && yytext[1] == '=')
+                                                                       return LESS_EQUALS;
+                                                               if (yytext[0] == '<' && yytext[1] == '>')
+                                                                       return NOT_EQUALS;
+                                                               if (yytext[0] == '!' && yytext[1] == '=')
+                                                                       return NOT_EQUALS;
+                                                       }
                                                }
 
                                                base_yylval.str = mm_strdup(yytext);
index 3a216c2ca8fd1eb58e3385ba4d019596eb29c70f..f3e41777318418cdc35a3365a98aa7ffa6283851 100644 (file)
@@ -37,6 +37,80 @@ CREATE OPERATOR => (
 ERROR:  syntax error at or near "=>"
 LINE 1: CREATE OPERATOR => (
                         ^
+-- lexing of <=, >=, <>, != has a number of edge cases
+-- (=> is tested elsewhere)
+-- this is legal because ! is not allowed in sql ops
+CREATE OPERATOR !=- (
+   leftarg = int8,             -- right unary
+   procedure = numeric_fac
+);
+SELECT 2 !=-;
+ ?column? 
+----------
+        2
+(1 row)
+
+-- make sure lexer returns != as <> even in edge cases
+SELECT 2 !=/**/ 1, 2 !=/**/ 2;
+ ?column? | ?column? 
+----------+----------
+ t        | f
+(1 row)
+
+SELECT 2 !=-- comment to be removed by psql
+  1;
+ ?column? 
+----------
+ t
+(1 row)
+
+DO $$ -- use DO to protect -- from psql
+  declare r boolean;
+  begin
+    execute $e$ select 2 !=-- comment
+      1 $e$ into r;
+    raise info 'r = %', r;
+  end;
+$$;
+INFO:  r = t
+-- check that <= etc. followed by more operator characters are returned
+-- as the correct token with correct precedence
+SELECT true<>-1 BETWEEN 1 AND 1;  -- BETWEEN has prec. above <> but below Op
+ ?column? 
+----------
+ t
+(1 row)
+
+SELECT false<>/**/1 BETWEEN 1 AND 1;
+ ?column? 
+----------
+ t
+(1 row)
+
+SELECT false<=-1 BETWEEN 1 AND 1;
+ ?column? 
+----------
+ t
+(1 row)
+
+SELECT false>=-1 BETWEEN 1 AND 1;
+ ?column? 
+----------
+ t
+(1 row)
+
+SELECT 2<=/**/3, 3>=/**/2, 2<>/**/3;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT 3<=/**/2, 2>=/**/3, 2<>/**/2;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ f        | f        | f
+(1 row)
+
 -- Should fail. CREATE OPERATOR requires USAGE on SCHEMA
 BEGIN TRANSACTION;
 CREATE ROLE regress_rol_op1;
index 68b88d33a16b2a909f7476972a1549598e22d572..f04c28e1c370058115b0dce483e415dcb7c4fe4d 100644 (file)
@@ -1478,6 +1478,42 @@ select dfunc('a'::text, 'b', flag => true); -- mixed notation
  a
 (1 row)
 
+-- this tests lexer edge cases around =>
+select dfunc(a =>-1);
+ dfunc 
+-------
+    -1
+(1 row)
+
+select dfunc(a =>+1);
+ dfunc 
+-------
+     1
+(1 row)
+
+select dfunc(a =>/**/1);
+ dfunc 
+-------
+     1
+(1 row)
+
+select dfunc(a =>--comment to be removed by psql
+  1);
+ dfunc 
+-------
+     1
+(1 row)
+
+-- need DO to protect the -- from psql
+do $$
+  declare r integer;
+  begin
+    select dfunc(a=>-- comment
+      1) into r;
+    raise info 'r = %', r;
+  end;
+$$;
+INFO:  r = 1
 -- check reverse-listing of named-arg calls
 CREATE VIEW dfview AS
    SELECT q1, q2,
index 0e5d6356bc39e0c91e9907921f29fc9349e11b7c..0579cb95e1d425c55dd89b9967c199f435309793 100644 (file)
@@ -41,6 +41,37 @@ CREATE OPERATOR => (
    procedure = numeric_fac
 );
 
+-- lexing of <=, >=, <>, != has a number of edge cases
+-- (=> is tested elsewhere)
+
+-- this is legal because ! is not allowed in sql ops
+CREATE OPERATOR !=- (
+   leftarg = int8,             -- right unary
+   procedure = numeric_fac
+);
+SELECT 2 !=-;
+-- make sure lexer returns != as <> even in edge cases
+SELECT 2 !=/**/ 1, 2 !=/**/ 2;
+SELECT 2 !=-- comment to be removed by psql
+  1;
+DO $$ -- use DO to protect -- from psql
+  declare r boolean;
+  begin
+    execute $e$ select 2 !=-- comment
+      1 $e$ into r;
+    raise info 'r = %', r;
+  end;
+$$;
+
+-- check that <= etc. followed by more operator characters are returned
+-- as the correct token with correct precedence
+SELECT true<>-1 BETWEEN 1 AND 1;  -- BETWEEN has prec. above <> but below Op
+SELECT false<>/**/1 BETWEEN 1 AND 1;
+SELECT false<=-1 BETWEEN 1 AND 1;
+SELECT false>=-1 BETWEEN 1 AND 1;
+SELECT 2<=/**/3, 3>=/**/2, 2<>/**/3;
+SELECT 3<=/**/2, 2>=/**/3, 2<>/**/2;
+
 -- Should fail. CREATE OPERATOR requires USAGE on SCHEMA
 BEGIN TRANSACTION;
 CREATE ROLE regress_rol_op1;
index 45ae7a23aa7e1b9a126c42ec32e445e861b5c2d6..5cdfcd2e69c916944b1f94fa573ef544b5b99bd8 100644 (file)
@@ -785,6 +785,21 @@ select dfunc('a'::text, 'b', flag => false); -- mixed notation
 select dfunc('a'::text, 'b', true); -- full positional notation
 select dfunc('a'::text, 'b', flag => true); -- mixed notation
 
+-- this tests lexer edge cases around =>
+select dfunc(a =>-1);
+select dfunc(a =>+1);
+select dfunc(a =>/**/1);
+select dfunc(a =>--comment to be removed by psql
+  1);
+-- need DO to protect the -- from psql
+do $$
+  declare r integer;
+  begin
+    select dfunc(a=>-- comment
+      1) into r;
+    raise info 'r = %', r;
+  end;
+$$;
 
 -- check reverse-listing of named-arg calls
 CREATE VIEW dfview AS