From 155508d05e4398bd9893c3478ad53c7f809c0e21 Mon Sep 17 00:00:00 2001 From: mrotteveel Date: Sat, 29 Nov 2014 15:39:59 +0000 Subject: [PATCH] CORE-4526 Support for SQL:2008 OFFSET and FETCH clauses --- doc/sql.extensions/README.offset_fetch.txt | 53 +++++++++++++++ doc/sql.extensions/README.rows | 22 ++++--- src/dsql/parse.y | 77 ++++++++++++++++++++-- src/include/gen/msgs.h | 6 +- src/msgs/messages2.sql | 6 +- src/yvalve/keywords.cpp | 2 + 6 files changed, 145 insertions(+), 21 deletions(-) create mode 100644 doc/sql.extensions/README.offset_fetch.txt diff --git a/doc/sql.extensions/README.offset_fetch.txt b/doc/sql.extensions/README.offset_fetch.txt new file mode 100644 index 0000000000..a30672037b --- /dev/null +++ b/doc/sql.extensions/README.offset_fetch.txt @@ -0,0 +1,53 @@ +----------------------- +OFFSET and FETCH clause +----------------------- + + Function: + SQL:2008 compliant equivalent for FIRST/SKIP, and an alternative for ROWS. + The OFFSET clause specifies the number of rows to skip. The FETCH clause + specifies the number of rows to fetch. + + OFFSET and FETCH can be applied independently on top-level and nested query + expressions. + + Author: + Mark Rotteveel + + Syntax rules: + SELECT ... [ ORDER BY ] + [ OFFSET { ROW | ROWS } ] + [ FETCH { FIRST | NEXT } [ ] { ROW | ROWS } ONLY ] + + Where is a (numeric) literal, a SQL parameter (?) or + PSQL named parameter (:namedparameter). + + Scope: + DSQL, PSQL + + Example(s): + 1. SELECT * FROM T1 + ORDER BY COL1 + OFFSET 10 ROWS + 2. SELECT * FROM T1 + ORDER BY COL1 + FETCH FIRST 10 ROWS ONLY + 3. SELECT * FROM ( + SELECT * FROM T1 + ORDER BY COL1 DESC + OFFSET 1 ROW + FETCH NEXT 10 ROWS ONLY + ) a + ORDER BY a.COL1 + FETCH FIRST ROW ONLY + + Note(s): + 1. Firebird doesn't support the percentage FETCH defined in the SQL + standard. + 2. Firebird doesn't support the FETCH ... WITH TIES defined in the SQL + standard. + 3. The FIRST/SKIP and ROWS clause are non-standard alternatives. + 4. The OFFSET and/or FETCH clauses cannot be combined with ROWS or + FIRST/SKIP on the same query expression. + 5. Expressions, column references, etc are not allowed within either clause. + 6. Contrary to the ROWS clause, OFFSET and FETCH are only available on + SELECT statements. \ No newline at end of file diff --git a/doc/sql.extensions/README.rows b/doc/sql.extensions/README.rows index 2b566cb2bf..73156282f4 100644 --- a/doc/sql.extensions/README.rows +++ b/doc/sql.extensions/README.rows @@ -3,8 +3,9 @@ ROWS clause ----------- Function: - Allow to limit a number of rows retrieved from a select expression. For a highest level - select statement, it would mean a number of rows sent to the host program. + Limits the number of rows retrieved from a select expression. For the + top-level select statement, it would mean a number of rows sent to the host + program. Author: Dmitry Yemanov @@ -28,10 +29,13 @@ ROWS clause ROWS 1 Note(s): - 1. ROWS is a more understandable alternative to the FIRST/SKIP clauses with some extra benefits. - It can be used in unions and all kind of subqueries. Also it's available in the UPDATE/DELETE - statements. - 2. When is omitted, then ROWS is a semantical equivalent for FIRST . When - both and are used, then ROWS TO means: - FIRST ( - + 1) SKIP ( - 1). Note that there's no semantical equivalent - for a SKIP clause used without a FIRST clause. + 1. ROWS is a more understandable alternative to the FIRST/SKIP clauses with + some extra benefits. It can be used in unions and all kind of subqueries. + It is also available in the UPDATE/DELETE statements. + 2. When is omitted, then ROWS is a semantical equivalent for + FIRST . When both and are used, then ROWS + TO means: + FIRST ( - + 1) SKIP ( - 1). Note that there's no + semantic equivalent for a SKIP clause used without a FIRST clause. + 3. The ROWS-clause is not defined in the SQL standard. For SELECT, consider + the alternative OFFSET and FETCH clauses defined in the SQL standard. diff --git a/src/dsql/parse.y b/src/dsql/parse.y index 6cefb803c0..1015b6a429 100644 --- a/src/dsql/parse.y +++ b/src/dsql/parse.y @@ -570,6 +570,8 @@ using namespace Firebird; %token SERVERWIDE %token INCREMENT %token TRUSTED +%token ROW +%token OFFSET // precedence declarations for expression evaluation @@ -3809,9 +3811,11 @@ keyword_or_column | KW_BOOLEAN // added in FB 3.0 | DETERMINISTIC | KW_FALSE + | OFFSET | OVER | RETURN | RDB_RECORD_VERSION + | ROW | SCROLL | SQLSTATE | KW_TRUE @@ -4898,6 +4902,21 @@ select_expr node->rowsClause = $4; node->withClause = $1; } + | with_clause select_expr_body order_clause result_offset_clause fetch_first_clause + { + SelectExprNode* node = $$ = newNode(); + node->querySpec = $2; + node->orderClause = $3; + if ($4 || $5) { + RowsClause* rowsNode = newNode(); + rowsNode->skip = $4; + rowsNode->length = $5; + node->rowsClause = rowsNode; + } else { + node->rowsClause = NULL; + } + node->withClause = $1; + } ; %type with_clause @@ -5451,14 +5470,13 @@ nulls_placement | LAST { $$ = OrderNode::NULLS_LAST; } ; -// ROWS clause +// ROWS clause - ROWS clause is a non-standard alternative to OFFSET .. FETCH .. +// Non-optional - for use in select_expr (so it doesn't cause conflicts with OFFSET .. FETCH ..) %type rows_clause rows_clause - : // nothing - { $$ = NULL; } // equivalent to FIRST value - | ROWS value + : ROWS value { $$ = newNode(); $$->length = $2; @@ -5473,6 +5491,45 @@ rows_clause } ; +// Optional - for use in delete_searched and update_searched +%type rows_clause_optional +rows_clause_optional + : // nothing + { $$ = NULL; } + | rows_clause + ; + +// OFFSET n {ROW | ROWS} + +row_noise + : ROW + | ROWS + ; + +%type result_offset_clause +result_offset_clause + : // nothing + { $$ = NULL; } + | OFFSET simple_value_spec row_noise + { $$ = $2; } + ; + +// FETCH {FIRST | NEXT} [ n ] {ROW | ROWS} ONLY + +first_next_noise + : FIRST + | NEXT + ; + +%type fetch_first_clause +fetch_first_clause + : // nothing + { $$ = NULL; } + | FETCH first_next_noise simple_value_spec row_noise ONLY + { $$ = $3; } + | FETCH first_next_noise row_noise ONLY + { $$ = MAKE_const_slong(1); } + ; // INSERT statement // IBO hack: replace column_parens_opt by ins_column_parens_opt. @@ -5588,7 +5645,7 @@ delete %type delete_searched delete_searched - : KW_DELETE FROM table_name where_clause plan_clause order_clause rows_clause returning_clause + : KW_DELETE FROM table_name where_clause plan_clause order_clause rows_clause_optional returning_clause { EraseNode* node = newNode(); node->dsqlRelation = $3; @@ -5625,7 +5682,7 @@ update %type update_searched update_searched : UPDATE table_name SET assignments where_clause plan_clause - order_clause rows_clause returning_clause + order_clause rows_clause_optional returning_clause { ModifyNode* node = newNode(); node->dsqlRelation = $2; @@ -6344,6 +6401,14 @@ value_primary | '(' value_primary ')' { $$ = $2; } ; +// Matches definition of in SQL standard +%type simple_value_spec +simple_value_spec + : constant + | variable + | parameter + ; + %type nonparenthesized_value nonparenthesized_value : column_name diff --git a/src/include/gen/msgs.h b/src/include/gen/msgs.h index b7d1a2e7c8..edbe49a389 100644 --- a/src/include/gen/msgs.h +++ b/src/include/gen/msgs.h @@ -518,8 +518,8 @@ static const struct { {335544814, "Services functionality will be supported in a later version of the product"}, /* service_not_supported */ {335544815, "GENERATOR @1"}, /* generator_name */ {335544816, "UDF @1"}, /* udf_name */ - {335544817, "Invalid parameter to FIRST. Only integers >= 0 are allowed."}, /* bad_limit_param */ - {335544818, "Invalid parameter to SKIP. Only integers >= 0 are allowed."}, /* bad_skip_param */ + {335544817, "Invalid parameter to FETCH or FIRST. Only integers >= 0 are allowed."}, /* bad_limit_param */ + {335544818, "Invalid parameter to OFFSET or SKIP. Only integers >= 0 are allowed."}, /* bad_skip_param */ {335544819, "File exceeded maximum size of 2GB. Add another database file or use a 64 bit I/O version of Firebird."}, /* io_32bit_exceeded_err */ {335544820, "Unable to find savepoint with name @1 in transaction context"}, /* invalid_savepoint */ {335544821, "Invalid column position used in the @1 clause"}, /* dsql_column_pos_err */ @@ -1158,7 +1158,7 @@ Data source : @4"}, /* eds_statement */ {336397324, "CREATE GENERATOR @1 failed"}, /* dsql_create_generator_failed */ {336397325, "SET GENERATOR @1 failed"}, /* dsql_set_generator_failed */ {336397326, "WITH LOCK can be used only with a single physical table"}, /* dsql_wlock_simple */ - {336397327, "FIRST/SKIP cannot be used with ROWS"}, /* dsql_firstskip_rows */ + {336397327, "FIRST/SKIP cannot be used with OFFSET/FETCH or ROWS"}, /* dsql_firstskip_rows */ {336397328, "WITH LOCK cannot be used with aggregates"}, /* dsql_wlock_aggregates */ {336397329, "WITH LOCK cannot be used with @1"}, /* dsql_wlock_conflict */ {336723983, "unable to open database"}, /* gsec_cant_open_db */ diff --git a/src/msgs/messages2.sql b/src/msgs/messages2.sql index 6fa18b3cfa..169436e90a 100644 --- a/src/msgs/messages2.sql +++ b/src/msgs/messages2.sql @@ -593,8 +593,8 @@ without specifying a character set.', NULL); ('service_not_supported', 'SVC_attach', 'svc.c', NULL, 0, 494, NULL, 'Services functionality will be supported in a later version of the product', NULL, NULL); ('generator_name', 'check_dependencies', 'dfw.e', NULL, 0, 495, NULL, 'GENERATOR @1', NULL, NULL); ('udf_name', 'check_dependencies', 'dfw.e', NULL, 0, 496, NULL, 'UDF @1', NULL, NULL); -('bad_limit_param', 'RSE_open', 'rse.c', NULL, 0, 497, NULL, 'Invalid parameter to FIRST. Only integers >= 0 are allowed.', NULL, NULL); -('bad_skip_param', 'RSE_open', 'rse.c', NULL, 0, 498, NULL, 'Invalid parameter to SKIP. Only integers >= 0 are allowed.', NULL, NULL); +('bad_limit_param', 'RSE_open', 'rse.c', NULL, 0, 497, NULL, 'Invalid parameter to FETCH or FIRST. Only integers >= 0 are allowed.', NULL, NULL); +('bad_skip_param', 'RSE_open', 'rse.c', NULL, 0, 498, NULL, 'Invalid parameter to OFFSET or SKIP. Only integers >= 0 are allowed.', NULL, NULL); ('io_32bit_exceeded_err', 'seek_file', 'unix.c', NULL, 0, 499, NULL, 'File exceeded maximum size of 2GB. Add another database file or use a 64 bit I/O version of Firebird.', NULL, NULL); ('invalid_savepoint', 'looper', 'exe.cpp', NULL, 0, 500, NULL, 'Unable to find savepoint with name @1 in transaction context', NULL, NULL); ('dsql_column_pos_err', '(several)', 'pass1.cpp', NULL, 0, 501, NULL, 'Invalid column position used in the @1 clause', NULL, NULL); @@ -2607,7 +2607,7 @@ ERROR: Backup incomplete', NULL, NULL); ('dsql_create_generator_failed', 'putErrorPrefix', 'DdlNodes.h', NULL, 13, 1036, NULL, 'CREATE GENERATOR @1 failed', NULL, NULL); ('dsql_set_generator_failed', 'putErrorPrefix', 'DdlNodes.h', NULL, 13, 1037, NULL, 'SET GENERATOR @1 failed', NULL, NULL); ('dsql_wlock_simple', 'pass1_rse_impl', 'pass1.cpp', NULL, 13, 1038, NULL, 'WITH LOCK can be used only with a single physical table', NULL, NULL); -('dsql_firstskip_rows', 'pass1_rse_impl', 'pass1.cpp', NULL, 13, 1039, NULL, 'FIRST/SKIP cannot be used with ROWS', NULL, NULL); +('dsql_firstskip_rows', 'pass1_rse_impl', 'pass1.cpp', NULL, 13, 1039, NULL, 'FIRST/SKIP cannot be used with OFFSET/FETCH or ROWS', NULL, NULL); ('dsql_wlock_aggregates', 'pass1_rse_impl', 'pass1.cpp', NULL, 13, 1040, NULL, 'WITH LOCK cannot be used with aggregates', NULL, NULL); ('dsql_wlock_conflict', NULL, 'pass1.cpp', NULL, 13, 1041, NULL, 'WITH LOCK cannot be used with @1', NULL, NULL); -- SQLWARN diff --git a/src/yvalve/keywords.cpp b/src/yvalve/keywords.cpp index 3ef7a50a36..ad899957cb 100644 --- a/src/yvalve/keywords.cpp +++ b/src/yvalve/keywords.cpp @@ -283,6 +283,7 @@ static const TOK tokens[] = {KW_NUMERIC, "NUMERIC", 1, false}, {OCTET_LENGTH, "OCTET_LENGTH", 2, false}, {OF, "OF", 1, false}, + {OFFSET, "OFFSET", 2, false}, {ON, "ON", 1, false}, {ONLY, "ONLY", 1, false}, {OPEN, "OPEN", 2, false}, @@ -348,6 +349,7 @@ static const TOK tokens[] = {ROLE, "ROLE", 1, true}, {ROLLBACK, "ROLLBACK", 1, false}, {ROUND, "ROUND", 2, false}, + {ROW, "ROW", 2, false}, {ROW_COUNT, "ROW_COUNT", 2, false}, {ROW_NUMBER, "ROW_NUMBER", 2, false}, {ROWS, "ROWS", 2, false},