diff --git a/doc/sql.extensions/README.generate_series.md b/doc/sql.extensions/README.generate_series.md new file mode 100644 index 00000000000..01db7638d53 --- /dev/null +++ b/doc/sql.extensions/README.generate_series.md @@ -0,0 +1,56 @@ +# GENERATE_SERIES function + +The `GENERATE_SERIES` function creates a series of numbers within a specified interval. +The interval and the step between series values ​​are defined by the user. + +## Syntax + +``` + ::= + GENERATE_SERIES(, [, ]) [AS] [ ( ) ] +``` + +## Arguments + +* `start` - The first value in the interval. `start` is specified as a variable, a literal, or a scalar expression of type +`SMALLINT`, `INTEGER`, `BIGINT`, `INT128` or `NUMERIC/DECIMAL`. + +* `finish` - The last value in the interval. `finish` is specified as a variable, a literal, or a scalar expression of +type `SMALLINT`, `INTEGER`, `BIGINT`, `INT128` or `NUMERIC/DECIMAL`. The series stops once the last generated step value +exceeds the `finish` value. + +* `step` - Indicates the number of values to increment or decrement between steps in the series. `step` is an expression +of type `SMALLINT`, `INTEGER`, `BIGINT`, `INT128` or `NUMERIC/DECIMAL`. +`step` can be either negative or positive, but can't be zero (0). This argument is optional. The default value for `step` is 1. + +## Returning type + +The function `GENERATE_SERIES` returns a set with `BIGINT`, `INT128` or `NUMERIC(18, x)/NUMERIC(38, x)` column, +where the scale is determined by the maximum of the scales of the function arguments. + +## Rules + +* If `start > finish` and a negative `step` value is specified, an empty set is returned. + +* If `start < finish` and a positive `step` value is specified, an empty set is returned. + +* If the `step` argument is zero, an error is thrown. + +## Examples + +``` +SELECT n +FROM GENERATE_SERIES(1, 3) AS S(n); + +SELECT n +FROM GENERATE_SERIES(3, 1, -1) AS S(n); + +SELECT n +FROM GENERATE_SERIES(0, 9.9, 0.1) AS S(n); + +SELECT + DATEADD(n MINUTE TO timestamp '2025-01-01 12:00') AS START_TIME, + DATEADD(n MINUTE TO timestamp '2025-01-01 12:00:59.9999') AS FINISH_TIME +FROM GENERATE_SERIES(0, 59) AS S(n); +``` + diff --git a/src/common/ParserTokens.h b/src/common/ParserTokens.h index 8a44da58a38..a74b37d5e6d 100644 --- a/src/common/ParserTokens.h +++ b/src/common/ParserTokens.h @@ -235,6 +235,7 @@ PARSER_TOKEN(TOK_FROM, "FROM", false) PARSER_TOKEN(TOK_FULL, "FULL", false) PARSER_TOKEN(TOK_FUNCTION, "FUNCTION", false) PARSER_TOKEN(TOK_GDSCODE, "GDSCODE", false) +PARSER_TOKEN(TOK_GENERATE_SERIES, "GENERATE_SERIES", true) PARSER_TOKEN(TOK_GENERATED, "GENERATED", true) PARSER_TOKEN(TOK_GENERATOR, "GENERATOR", true) PARSER_TOKEN(TOK_GEN_ID, "GEN_ID", true) diff --git a/src/dsql/parse-conflicts.txt b/src/dsql/parse-conflicts.txt index dea420bc00c..d18086526cc 100644 --- a/src/dsql/parse-conflicts.txt +++ b/src/dsql/parse-conflicts.txt @@ -1 +1 @@ -133 shift/reduce conflicts, 13 reduce/reduce conflicts. +134 shift/reduce conflicts, 13 reduce/reduce conflicts. diff --git a/src/dsql/parse.y b/src/dsql/parse.y index c8f2204af17..b385ac94550 100644 --- a/src/dsql/parse.y +++ b/src/dsql/parse.y @@ -710,6 +710,7 @@ using namespace Firebird; %token CURRENT_SCHEMA %token DOWNTO %token FORMAT +%token GENERATE_SERIES %token GREATEST %token LEAST %token LTRIM @@ -6798,6 +6799,8 @@ table_value_function table_value_function_clause : table_value_function_unlist { $$ = $1; } + | table_value_function_gen_series + { $$ = $1; } ; %type table_value_function_unlist @@ -6833,6 +6836,34 @@ table_value_function_correlation_name : as_noise symbol_item_alias_name { $$ = $2; } ; +%type table_value_function_gen_series +table_value_function_gen_series + : GENERATE_SERIES '(' table_value_function_gen_series_arg_list ')' + { + auto node = newNode(); + node->dsqlFlags |= RecordSourceNode::DFLAG_VALUE; + node->dsqlName = *$1; + node->inputList = $3; + node->dsqlField = nullptr; + $$ = node; + } + ; + +%type table_value_function_gen_series_arg_list +table_value_function_gen_series_arg_list + : value ',' value gen_series_step_opt + { + $$ = newNode($1); + $$->add($3); + $$->add($4); + } + ; + +%type gen_series_step_opt +gen_series_step_opt + : /* nothing */ { $$ = MAKE_const_sint64(1, 0); } + | ',' value { $$ = $2; } + ; // other clauses in the select expression @@ -9967,6 +9998,7 @@ non_reserved_word | BIN_XOR_AGG | DOWNTO | FORMAT + | GENERATE_SERIES | OWNER | SEARCH_PATH | SCHEMA diff --git a/src/include/firebird/impl/blr.h b/src/include/firebird/impl/blr.h index 6c4c9573aae..bb7825e3d4a 100644 --- a/src/include/firebird/impl/blr.h +++ b/src/include/firebird/impl/blr.h @@ -508,6 +508,7 @@ // Table value function #define blr_table_value_fun (unsigned char) 229 #define blr_table_value_fun_unlist (unsigned char) 1 +#define blr_table_value_fun_gen_series (unsigned char) 2 #define blr_for_range (unsigned char) 230 #define blr_for_range_variable (unsigned char) 1 diff --git a/src/include/firebird/impl/msg/jrd.h b/src/include/firebird/impl/msg/jrd.h index cef871febd6..7ba26ca0ac2 100644 --- a/src/include/firebird/impl/msg/jrd.h +++ b/src/include/firebird/impl/msg/jrd.h @@ -998,3 +998,5 @@ FB_IMPL_MSG(JRD, 995, missing_value_for_format_pattern, -901, "HY", "000", "Cann FB_IMPL_MSG(JRD, 996, invalid_name, -901, "HY", "000", "Invalid name: @1") FB_IMPL_MSG(JRD, 997, invalid_unqualified_name_list, -901, "HY", "000", "Invalid list of unqualified names: @1") FB_IMPL_MSG(JRD, 998, no_user_att_while_restore, -901, "HY", "000", "User attachments are not allowed for the database being restored") +FB_IMPL_MSG(JRD, 999, genseq_stepmustbe_nonzero, -833, "42", "000", "Argument STEP must be different than zero for function @1") +FB_IMPL_MSG(JRD, 1000, argmustbe_exact_function, -833, "42", "000", "Arguments for @1 function must be exact numeric types") diff --git a/src/include/gen/Firebird.pas b/src/include/gen/Firebird.pas index 15fb8074f2c..0ff161ae362 100644 --- a/src/include/gen/Firebird.pas +++ b/src/include/gen/Firebird.pas @@ -5844,6 +5844,8 @@ IProfilerStatsImpl = class(IProfilerStats) isc_invalid_name = 335545316; isc_invalid_unqualified_name_list = 335545317; isc_no_user_att_while_restore = 335545318; + isc_genseq_stepmustbe_nonzero = 335545319; + isc_argmustbe_exact_function = 335545320; isc_gfix_db_name = 335740929; isc_gfix_invalid_sw = 335740930; isc_gfix_incmp_sw = 335740932; diff --git a/src/jrd/RecordSourceNodes.cpp b/src/jrd/RecordSourceNodes.cpp index cd72088503e..9eee2d4fd34 100644 --- a/src/jrd/RecordSourceNodes.cpp +++ b/src/jrd/RecordSourceNodes.cpp @@ -4109,6 +4109,10 @@ TableValueFunctionSourceNode* TableValueFunctionSourceNode::parseFunction(thread node = FB_NEW_POOL(pool) UnlistFunctionSourceNode(pool); break; + case blr_table_value_fun_gen_series: + node = FB_NEW_POOL(pool) GenSeriesFunctionSourceNode(pool); + break; + default: PAR_syntax_error(csb, "blr_table_value_fun"); } @@ -4161,6 +4165,8 @@ void TableValueFunctionSourceNode::genBlr(DsqlCompilerScratch* dsqlScratch) if (tableValueFunctionContext->funName == UnlistFunctionSourceNode::FUNC_NAME) dsqlScratch->appendUChar(blr_table_value_fun_unlist); + else if (tableValueFunctionContext->funName == GenSeriesFunctionSourceNode::FUNC_NAME) + dsqlScratch->appendUChar(blr_table_value_fun_gen_series); else fb_assert(false); @@ -4320,6 +4326,8 @@ void TableValueFunctionSourceNode::setDefaultNameField(DsqlCompilerScratch* /*ds fb_assert(false); } +//-------------------- + RecordSource* UnlistFunctionSourceNode::compile(thread_db* tdbb, Optimizer* opt, bool /*innerSubStream*/) { @@ -4378,6 +4386,88 @@ dsql_fld* UnlistFunctionSourceNode::makeField(DsqlCompilerScratch* dsqlScratch) //-------------------- +RecordSource* GenSeriesFunctionSourceNode::compile(thread_db* tdbb, Optimizer* opt, + bool /*innerSubStream*/) +{ + MemoryPool& pool = *tdbb->getDefaultPool(); + const auto csb = opt->getCompilerScratch(); + const auto alias = opt->makeAlias(stream); + + return FB_NEW_POOL(pool) GenSeriesFunctionScan(csb, stream, alias, inputList); +} + +dsql_fld* GenSeriesFunctionSourceNode::makeField(DsqlCompilerScratch* dsqlScratch) +{ + if (inputList) + inputList = Node::doDsqlPass(dsqlScratch, inputList, false); + + const auto startItem = inputList->items[0].getObject(); + startItem->setParameterType( + dsqlScratch, [](dsc* desc) { desc->makeInt64(0); }, false); + + const auto finishItem = inputList->items[1].getObject(); + finishItem->setParameterType( + dsqlScratch, [](dsc* desc) { desc->makeInt64(0); }, false); + + const auto stepItem = inputList->items[2].getObject(); + stepItem->setParameterType( + dsqlScratch, [](dsc* desc) { desc->makeInt64(0); }, false); + + dsc startDesc; + DsqlDescMaker::fromNode(dsqlScratch, &startDesc, startItem, true); + if (!startDesc.isExact() && !startDesc.isNull()) + status_exception::raise(Arg::Gds(isc_argmustbe_exact_function) << Arg::Str(getName())); + + dsc finishDesc; + DsqlDescMaker::fromNode(dsqlScratch, &finishDesc, finishItem, true); + if (!finishDesc.isExact() && !finishDesc.isNull()) + status_exception::raise(Arg::Gds(isc_argmustbe_exact_function) << Arg::Str(getName())); + + dsc stepDesc; + DsqlDescMaker::fromNode(dsqlScratch, &stepDesc, stepItem, true); + if (!stepDesc.isExact() && !stepDesc.isNull()) + status_exception::raise(Arg::Gds(isc_argmustbe_exact_function) << Arg::Str(getName())); + + // common scale + const auto scale = MIN(MIN(startDesc.dsc_scale, finishDesc.dsc_scale), stepDesc.dsc_scale); + // common type + const auto dtype = MAX(MAX(startDesc.dsc_dtype, finishDesc.dsc_dtype), stepDesc.dsc_dtype); + + dsql_fld* field = dsqlField; + + if (!field) + { + field = FB_NEW_POOL(dsqlScratch->getPool()) dsql_fld(dsqlScratch->getPool()); + + dsc desc; + if (dtype == dtype_int128) + desc.makeInt128(scale); + else + desc.makeInt64(scale); + + MAKE_field(field, &desc); + field->fld_id = 0; + } + + if (dsqlNameColumns.hasData()) + { + if (dsqlNameColumns.getCount() > 1) + { + ERRD_post(Arg::Gds(isc_sqlerr) << Arg::Num(-104) << Arg::Gds(isc_dsql_command_err) + << Arg::Gds(isc_dsql_table_value_many_columns) + << Arg::Str(getName()) + << Arg::Num(1) << Arg::Num(dsqlNameColumns.getCount())); + } + + field->fld_name = dsqlNameColumns[0]; + } + + field->resolve(dsqlScratch); + return field; +} + +//-------------------- + static RecordSourceNode* dsqlPassRelProc(DsqlCompilerScratch* dsqlScratch, RecordSourceNode* source) { diff --git a/src/jrd/RecordSourceNodes.h b/src/jrd/RecordSourceNodes.h index 7630539a20c..9e78e5415c0 100644 --- a/src/jrd/RecordSourceNodes.h +++ b/src/jrd/RecordSourceNodes.h @@ -1081,6 +1081,25 @@ class UnlistFunctionSourceNode : public TableValueFunctionSourceNode } }; +class GenSeriesFunctionSourceNode final : public TableValueFunctionSourceNode +{ +public: + explicit GenSeriesFunctionSourceNode(MemoryPool& pool) + : TableValueFunctionSourceNode(pool) + { + } + + RecordSource* compile(thread_db* tdbb, Optimizer* opt, bool innerSubStream) override; + dsql_fld* makeField(DsqlCompilerScratch* dsqlScratch) override; + + static constexpr char const* FUNC_NAME = "GENERATE_SERIES"; + + const char* getName() const override + { + return FUNC_NAME; + } +}; + } // namespace Jrd #endif // JRD_RECORD_SOURCE_NODES_H diff --git a/src/jrd/recsrc/RecordSource.h b/src/jrd/recsrc/RecordSource.h index bc5df3dfed2..ad8756327b2 100644 --- a/src/jrd/recsrc/RecordSource.h +++ b/src/jrd/recsrc/RecordSource.h @@ -1615,6 +1615,63 @@ namespace Jrd NestConst m_inputList; }; + class GenSeriesFunctionScan final : public TableValueFunctionScan + { + enum GenSeriesTypeItemIndex : UCHAR + { + GEN_SERIES_INDEX_START = 0, + GEN_SERIES_INDEX_FINISH = 1, + GEN_SERIES_INDEX_STEP = 2, + GEN_SERIES_INDEX_LAST = 3 + }; + + struct Impure : public TableValueFunctionScan::Impure + { + union + { + SINT64 vlu_int64; + Firebird::Int128 vlu_int128; + } m_start; + + union + { + SINT64 vlu_int64; + Firebird::Int128 vlu_int128; + } m_finish; + + union + { + SINT64 vlu_int64; + Firebird::Int128 vlu_int128; + } m_step; + + union + { + SINT64 vlu_int64; + Firebird::Int128 vlu_int128; + } m_result; + + UCHAR m_dtype; + SCHAR m_scale; + }; + + public: + GenSeriesFunctionScan(CompilerScratch* csb, StreamType stream, const Firebird::string& alias, + ValueListNode* list); + + protected: + void close(thread_db* tdbb) const override; + void internalOpen(thread_db* tdbb) const override; + void internalGetPlan(thread_db* tdbb, PlanEntry& planEntry, unsigned level, + bool recurse) const override; + bool internalGetRecord(thread_db* tdbb) const override; + + bool nextBuffer(thread_db* tdbb) const override; + + private: + NestConst m_inputList; + }; + } // namespace #endif // JRD_RECORD_SOURCE_H diff --git a/src/jrd/recsrc/TableValueFunctionScan.cpp b/src/jrd/recsrc/TableValueFunctionScan.cpp index 326caf89b6e..51e44fed7ef 100644 --- a/src/jrd/recsrc/TableValueFunctionScan.cpp +++ b/src/jrd/recsrc/TableValueFunctionScan.cpp @@ -118,6 +118,8 @@ void TableValueFunctionScan::assignParameter(thread_db* tdbb, dsc* fromDesc, con memcpy(toDescValue.dsc_address, fromDesc->dsc_address, fromDesc->dsc_length); } +//-------------------- + UnlistFunctionScan::UnlistFunctionScan(CompilerScratch* csb, StreamType stream, const string& alias, ValueListNode* list) : TableValueFunctionScan(csb, stream, alias), m_inputList(list) @@ -346,3 +348,200 @@ bool UnlistFunctionScan::nextBuffer(thread_db* tdbb) const return false; } + +//-------------------- + +GenSeriesFunctionScan::GenSeriesFunctionScan(CompilerScratch* csb, StreamType stream, const string& alias, + ValueListNode* list) + : TableValueFunctionScan(csb, stream, alias), m_inputList(list) +{ + m_impure = csb->allocImpure(); +} + +void GenSeriesFunctionScan::close(thread_db* tdbb) const +{ + const auto request = tdbb->getRequest(); + + invalidateRecords(request); + + const auto impure = request->getImpure(m_impure); + + if (impure->irsb_flags & irsb_open) + impure->irsb_flags &= ~irsb_open; +} + + +void GenSeriesFunctionScan::internalOpen(thread_db* tdbb) const +{ + const auto request = tdbb->getRequest(); + const auto rpb = &request->req_rpb[m_stream]; + MemoryPool& pool = *tdbb->getDefaultPool(); + + rpb->rpb_number.setValue(BOF_NUMBER); + + fb_assert(m_inputList->items.getCount() >= GEN_SERIES_INDEX_LAST); + + auto startItem = m_inputList->items[GEN_SERIES_INDEX_START]; + const auto startDesc = EVL_expr(tdbb, request, startItem); + if (startDesc == nullptr) + return; + + auto finishItem = m_inputList->items[GEN_SERIES_INDEX_FINISH]; + const auto finishDesc = EVL_expr(tdbb, request, finishItem); + if (finishDesc == nullptr) + return; + + auto stepItem = m_inputList->items[GEN_SERIES_INDEX_STEP]; + const auto stepDesc = EVL_expr(tdbb, request, stepItem); + if (stepDesc == nullptr) + return; + + const auto impure = request->getImpure(m_impure); + impure->m_recordBuffer = nullptr; + + // common scale + impure->m_scale = MIN(MIN(startDesc->dsc_scale, finishDesc->dsc_scale), stepDesc->dsc_scale); + // common type + impure->m_dtype = MAX(MAX(startDesc->dsc_dtype, finishDesc->dsc_dtype), stepDesc->dsc_dtype); + + if (impure->m_dtype != dtype_int128) + { + const auto start = MOV_get_int64(tdbb, startDesc, impure->m_scale); + const auto finish = MOV_get_int64(tdbb, finishDesc, impure->m_scale); + const auto step = MOV_get_int64(tdbb, stepDesc, impure->m_scale); + + if (step == 0) + status_exception::raise(Arg::Gds(isc_genseq_stepmustbe_nonzero) << Arg::Str(m_name)); + + // validate parameter value + if (((step > 0) && (start > finish)) || + ((step < 0) && (start < finish))) + { + return; + } + + impure->m_start.vlu_int64 = start; + impure->m_finish.vlu_int64 = finish; + impure->m_step.vlu_int64 = step; + impure->m_result.vlu_int64 = start; + } + else + { + const auto start = MOV_get_int128(tdbb, startDesc, impure->m_scale); + const auto finish = MOV_get_int128(tdbb, finishDesc, impure->m_scale); + const auto step = MOV_get_int128(tdbb, stepDesc, impure->m_scale); + + if (step.sign() == 0) + status_exception::raise(Arg::Gds(isc_genseq_stepmustbe_nonzero) << Arg::Str(m_name)); + + // validate parameter value + if (((step.sign() > 0) && (start.compare(finish) > 0)) || + ((step.sign() < 0) && (start.compare(finish) < 0))) + { + return; + } + + impure->m_start.vlu_int128 = start; + impure->m_finish.vlu_int128 = finish; + impure->m_step.vlu_int128 = step; + impure->m_result.vlu_int128 = start; + } + + impure->irsb_flags |= irsb_open; + + VIO_record(tdbb, rpb, m_format, &pool); +} + +void GenSeriesFunctionScan::internalGetPlan(thread_db* tdbb, PlanEntry& planEntry, unsigned /*level*/, + bool /*recurse*/) const +{ + planEntry.className = "FunctionScan"; + + planEntry.lines.add().text = "Function " + + printName(tdbb, m_name.toQuotedString(), m_alias) + " Scan"; + + printOptInfo(planEntry.lines); + + if (m_alias.hasData()) + planEntry.alias = m_alias; +} + +bool GenSeriesFunctionScan::internalGetRecord(thread_db* tdbb) const +{ + JRD_reschedule(tdbb); + + const auto request = tdbb->getRequest(); + const auto impure = request->getImpure(m_impure); + const auto rpb = &request->req_rpb[m_stream]; + + if (!(impure->irsb_flags & irsb_open)) + { + rpb->rpb_number.setValid(false); + return false; + } + + rpb->rpb_number.increment(); + + if (nextBuffer(tdbb)) + { + rpb->rpb_number.setValid(true); + return true; + } + + rpb->rpb_number.setValid(false); + return false; +} + +bool GenSeriesFunctionScan::nextBuffer(thread_db* tdbb) const +{ + const auto request = tdbb->getRequest(); + const auto impure = request->getImpure(m_impure); + + if (impure->m_dtype != dtype_int128) + { + auto result = impure->m_result.vlu_int64; + const auto finish = impure->m_finish.vlu_int64; + const auto step = impure->m_step.vlu_int64; + + if (((step > 0) && (result <= finish)) || + ((step < 0) && (result >= finish))) + { + Record* const record = request->req_rpb[m_stream].rpb_record; + + auto toDesc = m_format->fmt_desc.begin(); + + dsc fromDesc; + fromDesc.makeInt64(impure->m_scale, &result); + assignParameter(tdbb, &fromDesc, toDesc, 0, record); + + result += step; + impure->m_result.vlu_int64 = result; + + return true; + } + } + else { + auto result = impure->m_result.vlu_int128; + const auto finish = impure->m_finish.vlu_int128; + const auto step = impure->m_step.vlu_int128; + + if (((step.sign() > 0) && (result.compare(finish) <= 0)) || + ((step.sign() < 0) && (result.compare(finish) >= 0))) + { + Record* const record = request->req_rpb[m_stream].rpb_record; + + auto toDesc = m_format->fmt_desc.begin(); + + dsc fromDesc; + fromDesc.makeInt128(impure->m_scale, &result); + assignParameter(tdbb, &fromDesc, toDesc, 0, record); + + result = result.add(step); + impure->m_result.vlu_int128 = result; + + return true; + } + } + + return false; +}