diff --git a/builds/win32/msvc15/engine_static.vcxproj b/builds/win32/msvc15/engine_static.vcxproj index c2b465b4cce..7143acd7d41 100644 --- a/builds/win32/msvc15/engine_static.vcxproj +++ b/builds/win32/msvc15/engine_static.vcxproj @@ -140,6 +140,7 @@ + diff --git a/builds/win32/msvc15/engine_static.vcxproj.filters b/builds/win32/msvc15/engine_static.vcxproj.filters index 7aa65c72ea2..5a5f4d4f7ee 100644 --- a/builds/win32/msvc15/engine_static.vcxproj.filters +++ b/builds/win32/msvc15/engine_static.vcxproj.filters @@ -111,7 +111,10 @@ JRD files\Data Access - + + JRD files\Data Access + + JRD files\Data Access diff --git a/doc/sql.extensions/README.unlist b/doc/sql.extensions/README.unlist new file mode 100644 index 00000000000..7ca4c5fd680 --- /dev/null +++ b/doc/sql.extensions/README.unlist @@ -0,0 +1,71 @@ +SQL Language Extension: UNLIST + +Function: + The function parses the input string using the specified delimiter (comma "," is implied by default) and returns the identified substrings as discrete records containing a single field. Additionally, the desired type of the returned field can be specified. If the specified data type conversion is impossible, an error is raised at runtime. + +Author: + Chudaykin Alex + +Format + ::= + UNLIST ( [, ] [, ] ) [AS] [ ( ) ] + + ::= + + ::= + + ::= RETURNING + +Syntax Rules: + +1) : any value expression that returns a string/blob of characters (or may be converted to a string), including string literals, table columns, constants, variables, expressions, etc. This parameter is mandatory. +2) : optional value expression that returns a string which is used as a delimiter (i.e. it separates one value from another inside the input string). It may also be a BLOB TEXT value, limited to 32KB. If an empty string is specified, the output will be just one record containing the input string. If omitted, the comma character is used as a delimiter. +2) : target data type to convert the output values into. Alternatively, a domain can be specified as the returned type. If omitted, VARCHAR(32) is implied. Feel free to suggest any better alternative default. +3) : alias of the record set returned by the UNLIST function. It is a mandatory parameter (per SQL standard). +4) : optional alias of the column returned by the UNLIST function. If omitted, UNLIST is used as an alias. + +Example: +A) + SELECT * FROM UNLIST('1,2,3,4,5') AS U; + +B) + SELECT * FROM UNLIST('100:200:300:400:500', ':' RETURNING INT) AS U; + +C) + SELECT U.* FROM UNLIST(‘text1, text2, text3’) AS U; + +D) + SELECT C0 FROM UNLIST(‘text1, text2, text3’) AS U(C0); + +E) + SELECT U.C0 FROM UNLIST(‘text1, text2, text3’) AS U(C0); + +F) + SET TERM ^ ; + RECREATE PROCEDURE TEST_PROC RETURNS (PROC_RETURN_INT INT) + AS + DECLARE VARIABLE text VARCHAR(11); + BEGIN + text = '123:123:123'; + FOR SELECT * FROM UNLIST( :text, ':' RETURNING INT) AS A INTO :PROC_RETURN_INT DO + SUSPEND; + END^ + SET TERM ; ^ + SELECT * FROM TEST_PROC; + +G) + CREATE DOMAIN D1 AS INT; + SELECT TEST_DOMAIN FROM UNLIST('1,2,3,4' RETURNING D1) AS A(TEST_DOMAIN); + + CREATE TABLE TABLE_TEST (COL1 INT); + SELECT TEST_TYPE_OF FROM UNLIST('1,2,3,4' RETURNING TYPE OF COLUMN TABLE_TEST.COL1) AS A(TEST_TYPE_OF); +H) + CREATE VIEW TEST_VIEW AS SELECT * FROM UNLIST('1,2,3,4') AS A(B); + SELECT B FROM TEST_VIEW; + +Unacceptable behavior: + SELECT UNLIST FROM UNLIST('UNLIST,A,S,A') AS A; + + + + diff --git a/src/common/ParserTokens.h b/src/common/ParserTokens.h index a45da1c81aa..3f2e569e42b 100644 --- a/src/common/ParserTokens.h +++ b/src/common/ParserTokens.h @@ -524,6 +524,7 @@ PARSER_TOKEN(TOK_UNICODE_CHAR, "UNICODE_CHAR", true) PARSER_TOKEN(TOK_UNICODE_VAL, "UNICODE_VAL", true) PARSER_TOKEN(TOK_UNION, "UNION", false) PARSER_TOKEN(TOK_UNIQUE, "UNIQUE", false) +PARSER_TOKEN(TOK_UNLIST, "UNLIST", true) PARSER_TOKEN(TOK_UNKNOWN, "UNKNOWN", false) PARSER_TOKEN(TOK_UPDATE, "UPDATE", false) PARSER_TOKEN(TOK_UPDATING, "UPDATING", false) diff --git a/src/dsql/DdlNodes.epp b/src/dsql/DdlNodes.epp index dc2a771a3d5..59d87f0eea4 100644 --- a/src/dsql/DdlNodes.epp +++ b/src/dsql/DdlNodes.epp @@ -9119,6 +9119,14 @@ void CreateAlterViewNode::execute(thread_db* tdbb, DsqlCompilerScratch* dsqlScra else updatable = false; + + if (field && context && (context->ctx_flags & CTX_blr_fields)) + { + field = nullptr; + context = nullptr; + updatable = false; + } + // If this is an expression, check to make sure there is a name specified. if (!ptr && !fieldStr) diff --git a/src/dsql/DsqlCompilerScratch.cpp b/src/dsql/DsqlCompilerScratch.cpp index 586dfdb9eec..8fe3eee0f21 100644 --- a/src/dsql/DsqlCompilerScratch.cpp +++ b/src/dsql/DsqlCompilerScratch.cpp @@ -945,6 +945,11 @@ bool DsqlCompilerScratch::pass1RelProcIsRecursive(RecordSourceNode* input) relName = relNode->dsqlName; relAlias = relNode->alias; } + else if (auto tableValueFunctionNode = nodeAs(input)) + { + relName = tableValueFunctionNode->dsqlName; + relAlias = tableValueFunctionNode->alias.c_str(); + } //// TODO: LocalTableSourceNode else return false; diff --git a/src/dsql/ExprNodes.cpp b/src/dsql/ExprNodes.cpp index 9d5d4849680..c651b0253ce 100644 --- a/src/dsql/ExprNodes.cpp +++ b/src/dsql/ExprNodes.cpp @@ -6027,6 +6027,11 @@ DmlNode* FieldNode::parse(thread_db* tdbb, MemoryPool& pool, CompilerScratch* cs Arg::Str(name) << Arg::Str(procedure->getName().toString())); } } + else if (tail->csb_table_value_fun) + { + csb->csb_blr_reader.getMetaName(name); + id = tail->csb_table_value_fun->getId(name); + } else { jrd_rel* relation = tail->csb_relation; @@ -6250,6 +6255,13 @@ ValueExprNode* FieldNode::internalDsqlPass(DsqlCompilerScratch* dsqlScratch, Rec procNode->dsqlContext = stackContext; *list = procNode; } + else if (context->ctx_table_value_fun) + { + auto tableValueFunctionNode = FB_NEW_POOL(*tdbb->getDefaultPool()) + TableValueFunctionSourceNode(*tdbb->getDefaultPool()); + tableValueFunctionNode->dsqlContext = stackContext; + *list = tableValueFunctionNode; + } //// TODO: LocalTableSourceNode fb_assert(*list); @@ -6446,9 +6458,28 @@ dsql_fld* FieldNode::resolveContext(DsqlCompilerScratch* dsqlScratch, const Meta return nullptr; } + const TEXT* dsqlName = nullptr; + dsql_fld* outputField = nullptr; dsql_rel* relation = context->ctx_relation; dsql_prc* procedure = context->ctx_procedure; - if (!relation && !procedure) + dsql_tab_func* tableValueFunctionContext = context->ctx_table_value_fun; + + if (relation) + { + dsqlName = relation->rel_name.c_str(); + outputField = relation->rel_fields; + } + else if (procedure) + { + dsqlName = procedure->prc_name.identifier.c_str(); + outputField = procedure->prc_outputs; + } + else if (tableValueFunctionContext) + { + dsqlName = tableValueFunctionContext->funName.c_str(); + outputField = tableValueFunctionContext->outputField; + } + else return nullptr; // if there is no qualifier, then we cannot match against @@ -6491,7 +6522,9 @@ dsql_fld* FieldNode::resolveContext(DsqlCompilerScratch* dsqlScratch, const Meta } if (aliasName.isEmpty()) - aliasName = relation ? relation->rel_name : procedure->prc_name.identifier; + { + aliasName = dsqlName; + } fb_assert(aliasName.hasData()); @@ -6501,7 +6534,7 @@ dsql_fld* FieldNode::resolveContext(DsqlCompilerScratch* dsqlScratch, const Meta // Lookup field in relation or procedure - return relation ? relation->rel_fields : procedure->prc_outputs; + return outputField; } bool FieldNode::dsqlAggregateFinder(AggregateFinder& visitor) diff --git a/src/dsql/Nodes.h b/src/dsql/Nodes.h index 4df8900d540..56bed32d86d 100644 --- a/src/dsql/Nodes.h +++ b/src/dsql/Nodes.h @@ -521,6 +521,7 @@ class ExprNode : public DmlNode TYPE_SELECT_EXPR, TYPE_UNION, TYPE_WINDOW, + TYPE_TABLE_VALUE_FUNCTION, // List types TYPE_REC_SOURCE_LIST, diff --git a/src/dsql/StmtNodes.cpp b/src/dsql/StmtNodes.cpp index f8a518da49c..f8174dbf352 100644 --- a/src/dsql/StmtNodes.cpp +++ b/src/dsql/StmtNodes.cpp @@ -10682,6 +10682,8 @@ static dsql_ctx* dsqlGetContext(const RecordSourceNode* node) return procNode->dsqlContext; else if (auto relNode = nodeAs(node)) return relNode->dsqlContext; + else if (auto tableValueFunctionNode = nodeAs(node)) + return tableValueFunctionNode->dsqlContext; //// TODO: LocalTableSourceNode else if (auto rseNode = nodeAs(node)) return rseNode->dsqlContext; @@ -10699,6 +10701,8 @@ static void dsqlGetContexts(DsqlContextStack& contexts, const RecordSourceNode* contexts.push(procNode->dsqlContext); else if (auto relNode = nodeAs(node)) contexts.push(relNode->dsqlContext); + else if (auto tableValueFunctionNode = nodeAs(node)) + contexts.push(tableValueFunctionNode->dsqlContext); //// TODO: LocalTableSourceNode else if (auto rseNode = nodeAs(node)) { diff --git a/src/dsql/dsql.h b/src/dsql/dsql.h index 422e36c3961..42ff206c6be 100644 --- a/src/dsql/dsql.h +++ b/src/dsql/dsql.h @@ -410,6 +410,21 @@ class dsql_intlsym : public pool_alloc USHORT intlsym_bytes_per_char = 0; }; + +// Table value function +class dsql_tab_func : public pool_alloc +{ +public: + explicit dsql_tab_func(MemoryPool& p) + : funName(p), + outputField(nullptr) + { + } + + MetaName funName; // Name of function + dsql_fld* outputField; // Output parameters +}; + // values used in intlsym_flags enum intlsym_flags_vals { @@ -454,6 +469,7 @@ class dsql_ctx : public pool_alloc dsql_rel* ctx_relation = nullptr; // Relation for context dsql_prc* ctx_procedure = nullptr; // Procedure for context + dsql_tab_func* ctx_table_value_fun = nullptr; // Table value function context NestConst ctx_proc_inputs; // Procedure input parameters dsql_map* ctx_map = nullptr; // Maps for aggregates and unions RseNode* ctx_rse = nullptr; // Sub-rse for aggregates @@ -475,6 +491,7 @@ class dsql_ctx : public pool_alloc { ctx_relation = v.ctx_relation; ctx_procedure = v.ctx_procedure; + ctx_table_value_fun = v.ctx_table_value_fun; ctx_proc_inputs = v.ctx_proc_inputs; ctx_map = v.ctx_map; ctx_rse = v.ctx_rse; @@ -518,6 +535,7 @@ const USHORT CTX_view_with_check_store = 0x20; // Context of WITH CHECK OPTION const USHORT CTX_view_with_check_modify = 0x40; // Context of WITH CHECK OPTION view's modify trigger const USHORT CTX_cursor = 0x80; // Context is a cursor const USHORT CTX_lateral = 0x100; // Context is a lateral derived table +const USHORT CTX_blr_fields = 0x200; // Fields of the context are defined inside BLR //! Aggregate/union map block to map virtual fields to their base //! TMN: NOTE! This datatype should definitely be renamed! diff --git a/src/dsql/make.cpp b/src/dsql/make.cpp index 0ee75a0428b..0d706dae630 100644 --- a/src/dsql/make.cpp +++ b/src/dsql/make.cpp @@ -423,6 +423,42 @@ FieldNode* MAKE_field(dsql_ctx* context, dsql_fld* field, ValueListNode* indices } +/** + + MAKE_field + + @brief Make up a dsql_fld from descriptor. + + + @param field + @param desc + + **/ +void MAKE_field(dsql_fld* field, const dsc* desc) +{ + DEV_BLKCHK(field, dsql_type_fld); + + field->dtype = desc->dsc_dtype; + field->scale = desc->dsc_scale; + field->subType = desc->dsc_sub_type; + field->length = desc->dsc_length; + + if (desc->dsc_dtype <= dtype_any_text) + { + field->collationId = DSC_GET_COLLATE(desc); + field->charSetId = DSC_GET_CHARSET(desc); + } + else if (desc->dsc_dtype == dtype_blob) + { + field->charSetId = desc->dsc_scale; + field->collationId = desc->dsc_flags >> 8; + } + + if (desc->dsc_flags & DSC_nullable) + field->flags |= FLD_nullable; +} + + /** MAKE_field_name diff --git a/src/dsql/make_proto.h b/src/dsql/make_proto.h index d68df999fb4..fd21fc4cf5d 100644 --- a/src/dsql/make_proto.h +++ b/src/dsql/make_proto.h @@ -84,6 +84,7 @@ Jrd::LiteralNode* MAKE_const_sint64(SINT64 value, SCHAR scale); Jrd::ValueExprNode* MAKE_constant(const char*, Jrd::dsql_constant_type, SSHORT = 0); Jrd::LiteralNode* MAKE_str_constant(const Jrd::IntlString*, SSHORT); Jrd::FieldNode* MAKE_field(Jrd::dsql_ctx*, Jrd::dsql_fld*, Jrd::ValueListNode*); +void MAKE_field(Jrd::dsql_fld*, const dsc*); Jrd::FieldNode* MAKE_field_name(const char*); Jrd::dsql_par* MAKE_parameter(Jrd::dsql_msg*, bool, bool, USHORT, const Jrd::ValueExprNode*); void MAKE_parameter_names(Jrd::dsql_par*, const Jrd::ValueExprNode*); diff --git a/src/dsql/parse-conflicts.txt b/src/dsql/parse-conflicts.txt index 612edc3bcf0..130f843da22 100644 --- a/src/dsql/parse-conflicts.txt +++ b/src/dsql/parse-conflicts.txt @@ -1 +1 @@ -117 shift/reduce conflicts, 22 reduce/reduce conflicts. +118 shift/reduce conflicts, 22 reduce/reduce conflicts. diff --git a/src/dsql/parse.y b/src/dsql/parse.y index cc51eea7dab..cfb03c27db7 100644 --- a/src/dsql/parse.y +++ b/src/dsql/parse.y @@ -708,6 +708,7 @@ using namespace Firebird; %token LTRIM %token NAMED_ARG_ASSIGN %token RTRIM +%token UNLIST // precedence declarations for expression evaluation @@ -6391,6 +6392,7 @@ table_primary | derived_table { $$ = $1; } | lateral_derived_table { $$ = $1; } | parenthesized_joined_table { $$ = $1; } + | table_value_function { $$ = $1; } ; %type parenthesized_joined_table @@ -6589,6 +6591,77 @@ outer_noise | OUTER ; +%type table_value_function +table_value_function + : table_value_function_clause table_value_function_correlation_name derived_column_list + { + auto node = nodeAs($1); + node->alias = *$2; + if ($3) + node->dsqlNameColumns = *$3; + $$ = node; + } + ; + +%type table_value_function_clause +table_value_function_clause + : table_value_function_unlist + { $$ = $1; } + ; + +%type table_value_function_unlist +table_value_function_unlist + : UNLIST '(' table_value_function_unlist_arg_list table_value_function_returning ')' + { + auto node = newNode(); + node->dsqlFlags |= RecordSourceNode::DFLAG_VALUE; + node->dsqlName = *$1; + node->inputList = $3; + node->dsqlField = $4; + $$ = node; + } + ; + +%type table_value_function_unlist_arg_list +table_value_function_unlist_arg_list + : value delimiter_opt + { + $$ = newNode($1); + $$->add($2); + } + ; + +%type table_value_function_returning +table_value_function_returning + : /*nothing*/ { $$ = NULL; } + | RETURNING data_type_descriptor { $$ = $2; } + ; + +%type table_value_function_correlation_name +table_value_function_correlation_name + : as_noise symbol_table_alias_name { $$ = $2; } + ; + +%type table_value_function_columns +table_value_function_columns + : table_value_function_column + { + ObjectsArray* node = newNode>(); + node->add(*$1); + $$ = node; + } + | table_value_function_columns ',' table_value_function_column + { + ObjectsArray* node = $1; + node->add(*$3); + $$ = node; + } + ; + +%type table_value_function_column +table_value_function_column + : symbol_column_name { $$ = $1; } + ; // other clauses in the select expression @@ -9684,6 +9757,7 @@ non_reserved_word | ANY_VALUE | FORMAT | OWNER + | UNLIST ; %% diff --git a/src/dsql/pass1.cpp b/src/dsql/pass1.cpp index ac7812eb4db..cfe48a840de 100644 --- a/src/dsql/pass1.cpp +++ b/src/dsql/pass1.cpp @@ -348,6 +348,7 @@ dsql_ctx* PASS1_make_context(DsqlCompilerScratch* dsqlScratch, RecordSourceNode* dsql_rel* relation = NULL; dsql_prc* procedure = NULL; + dsql_tab_func* tableValueFunctionContext = nullptr; // figure out whether this is a relation or a procedure // and give an error if it is neither @@ -356,11 +357,14 @@ dsql_ctx* PASS1_make_context(DsqlCompilerScratch* dsqlScratch, RecordSourceNode* ProcedureSourceNode* procNode = NULL; RelationSourceNode* relNode = NULL; SelectExprNode* selNode = NULL; + TableValueFunctionSourceNode* tableValueFunctionNode = nullptr; if ((procNode = nodeAs(relationNode))) relation_name = procNode->dsqlName.identifier; else if ((relNode = nodeAs(relationNode))) relation_name = relNode->dsqlName; + else if ((tableValueFunctionNode = nodeAs(relationNode))) + relation_name = tableValueFunctionNode->alias; //// TODO: LocalTableSourceNode else if ((selNode = nodeAs(relationNode))) relation_name = selNode->alias.c_str(); @@ -371,6 +375,11 @@ dsql_ctx* PASS1_make_context(DsqlCompilerScratch* dsqlScratch, RecordSourceNode* { // No processing needed here for derived tables. } + else if (tableValueFunctionNode) + { + tableValueFunctionContext = FB_NEW_POOL(*tdbb->getDefaultPool()) dsql_tab_func(*tdbb->getDefaultPool()); + tableValueFunctionContext->funName = tableValueFunctionNode->dsqlName; + } else if (procNode && (procNode->dsqlName.package.hasData() || procNode->inputSources)) { if (procNode->dsqlName.package.isEmpty()) @@ -430,6 +439,7 @@ dsql_ctx* PASS1_make_context(DsqlCompilerScratch* dsqlScratch, RecordSourceNode* const auto context = FB_NEW_POOL(*tdbb->getDefaultPool()) dsql_ctx(*tdbb->getDefaultPool()); context->ctx_relation = relation; context->ctx_procedure = procedure; + context->ctx_table_value_fun = tableValueFunctionContext; if (selNode) { @@ -467,6 +477,8 @@ dsql_ctx* PASS1_make_context(DsqlCompilerScratch* dsqlScratch, RecordSourceNode* // as one, I'll leave this assignment here. It will be set in PASS1_derived_table anyway. ///context->ctx_rse = selNode->querySpec; } + else if ((tableValueFunctionNode = nodeAs(relationNode))) + str = tableValueFunctionNode->alias.c_str(); if (str.hasData()) context->ctx_internal_alias = str; @@ -599,6 +611,15 @@ dsql_ctx* PASS1_make_context(DsqlCompilerScratch* dsqlScratch, RecordSourceNode* } } } + else if (tableValueFunctionNode) + { + context->ctx_flags |= CTX_blr_fields; + + fb_assert(tableValueFunctionContext); + tableValueFunctionContext->outputField = tableValueFunctionNode->makeField(dsqlScratch); + + context->ctx_proc_inputs = tableValueFunctionNode->inputList; + } // push the context onto the dsqlScratch context stack // for matching fields against @@ -959,7 +980,7 @@ void PASS1_expand_contexts(DsqlContextStack& contexts, dsql_ctx* context) { //// TODO: LocalTableSourceNode if (context->ctx_relation || context->ctx_procedure || - context->ctx_map || context->ctx_win_maps.hasData()) + context->ctx_map || context->ctx_win_maps.hasData() || context->ctx_table_value_fun) { if (context->ctx_parent) context = context->ctx_parent; @@ -1431,6 +1452,27 @@ void PASS1_expand_select_node(DsqlCompilerScratch* dsqlScratch, ExprNode* node, else list->add(value); } + else if (auto tableValueFunctionNode = nodeAs(node)) + { + auto context = tableValueFunctionNode->dsqlContext; + + if (const auto tableValueFunctionContext = context->ctx_table_value_fun) + { + tableValueFunctionNode->setDefaultNameField(dsqlScratch); + + for (dsql_fld* field = tableValueFunctionContext->outputField; field; field = field->fld_next) + { + DEV_BLKCHK(field, dsql_type_fld); + NestConst select_item = NULL; + if (!hide_using || context->getImplicitJoinField(field->fld_name, select_item)) + { + if (!select_item) + select_item = MAKE_field(context, field, NULL); + list->add(select_item); + } + } + } + } else { fb_assert(node->getKind() == DmlNode::KIND_VALUE); @@ -1728,6 +1770,13 @@ RecordSourceNode* PASS1_relation(DsqlCompilerScratch* dsqlScratch, RecordSourceN procNode->dsqlInputArgNames = nodeAs(input)->dsqlInputArgNames; return procNode; } + else if (context->ctx_table_value_fun) + { + const auto tableValueFunctionNode = FB_NEW_POOL(*tdbb->getDefaultPool()) + TableValueFunctionSourceNode(*tdbb->getDefaultPool()); + tableValueFunctionNode->dsqlContext = context; + return tableValueFunctionNode; + } //// TODO: LocalTableSourceNode fb_assert(false); @@ -2953,6 +3002,11 @@ static void remap_streams_to_parent_context(ExprNode* input, dsql_ctx* parent_co DEV_BLKCHK(relNode->dsqlContext, dsql_type_ctx); relNode->dsqlContext->ctx_parent = parent_context; } + else if (auto tableValueFunctionNode = nodeAs(input)) + { + DEV_BLKCHK(tableValueFunctionNode->dsqlContext, dsql_type_ctx); + tableValueFunctionNode->dsqlContext->ctx_parent = parent_context; + } //// TODO: LocalTableSourceNode else if (auto rseNode = nodeAs(input)) remap_streams_to_parent_context(rseNode->dsqlStreams, parent_context); diff --git a/src/include/fb_blk.h b/src/include/fb_blk.h index fec0ce090c8..852aa60316a 100644 --- a/src/include/fb_blk.h +++ b/src/include/fb_blk.h @@ -68,6 +68,7 @@ enum BlockType dsql_type_prc, dsql_type_intlsym, dsql_type_imp_join, + dsql_type_tab_func, alice_type_tdr, alice_type_str, diff --git a/src/include/firebird/impl/blr.h b/src/include/firebird/impl/blr.h index ecad7bb11a4..e190113f560 100644 --- a/src/include/firebird/impl/blr.h +++ b/src/include/firebird/impl/blr.h @@ -498,4 +498,8 @@ #define blr_cast_format (unsigned char) 228 +// Table value function +#define blr_table_value_fun (unsigned char) 229 +#define blr_table_value_fun_unlist (unsigned char) 1 + #endif // FIREBIRD_IMPL_BLR_H diff --git a/src/include/firebird/impl/msg/sqlerr.h b/src/include/firebird/impl/msg/sqlerr.h index 25a7014b0c4..63a2d897601 100644 --- a/src/include/firebird/impl/msg/sqlerr.h +++ b/src/include/firebird/impl/msg/sqlerr.h @@ -283,3 +283,4 @@ FB_IMPL_MSG(SQLERR, 1043, dsql_string_byte_length, -901, "42", "000", "String li FB_IMPL_MSG(SQLERR, 1044, dsql_string_char_length, -901, "42", "000", "String literal with @1 characters exceeds the maximum length of @2 characters for the @3 character set") FB_IMPL_MSG(SQLERR, 1045, dsql_max_nesting, -901, "07", "002", "Too many BEGIN...END nesting. Maximum level is @1") FB_IMPL_MSG(SQLERR, 1046, dsql_recreate_user_failed, -901, "42", "000", "RECREATE USER @1 failed") +FB_IMPL_MSG(SQLERR, 1047, dsql_table_value_many_columns, -104, "54", "001", "the number of fields exceeds the limit for the @1 operator. Expected @2, received @3") diff --git a/src/include/gen/Firebird.pas b/src/include/gen/Firebird.pas index 7c8ae81ce37..f8ffbefee3b 100644 --- a/src/include/gen/Firebird.pas +++ b/src/include/gen/Firebird.pas @@ -6165,6 +6165,7 @@ IProfilerStatsImpl = class(IProfilerStats) isc_dsql_string_char_length = 336397332; isc_dsql_max_nesting = 336397333; isc_dsql_recreate_user_failed = 336397334; + isc_dsql_table_value_many_columns = 336397335; isc_gsec_cant_open_db = 336723983; isc_gsec_switches_error = 336723984; isc_gsec_no_op_spec = 336723985; diff --git a/src/jrd/RecordSourceNodes.cpp b/src/jrd/RecordSourceNodes.cpp index f355b6bf1c1..a70ea955e95 100644 --- a/src/jrd/RecordSourceNodes.cpp +++ b/src/jrd/RecordSourceNodes.cpp @@ -243,6 +243,12 @@ PlanNode* PlanNode::dsqlPass(DsqlCompilerScratch* dsqlScratch) procNode->dsqlContext = context; node->recordSourceNode = procNode; } + else if (context->ctx_table_value_fun) + { + auto tableValueFunctionNode = FB_NEW_POOL(pool) TableValueFunctionSourceNode(pool); + tableValueFunctionNode->dsqlContext = context; + node->recordSourceNode = tableValueFunctionNode; + } //// TODO: LocalTableSourceNode // ASF: I think it's a error to let node->recordSourceNode be NULL here, but it happens @@ -3681,6 +3687,346 @@ RseNode* SelectExprNode::dsqlPass(DsqlCompilerScratch* dsqlScratch) return PASS1_derived_table(dsqlScratch, this, NULL); } +TableValueFunctionSourceNode* TableValueFunctionSourceNode::parse(thread_db* tdbb, + CompilerScratch* csb, + const SSHORT blrOp) +{ + fb_assert(blrOp == blr_table_value_fun); + + MemoryPool& pool = *tdbb->getDefaultPool(); + + const auto funcId = csb->csb_blr_reader.getByte(); + auto node = TableValueFunctionSourceNode::parseTableValueFunctions(tdbb, csb, funcId); + + node->stream = PAR_context(csb, nullptr); + + CompilerScratch::csb_repeat& element = csb->csb_rpt[node->stream]; + auto tableValueFunctionCsb = node->m_csbTableValueFun = FB_NEW_POOL(pool) jrd_table_value_fun(pool); + + tableValueFunctionCsb->funcId = funcId; + + string aliasString; + csb->csb_blr_reader.getString(aliasString); + if (aliasString.hasData()) + node->alias = aliasString; + else + fb_assert(false); + + tableValueFunctionCsb->name = aliasString; + + auto count = csb->csb_blr_reader.getWord(); + node->inputList = FB_NEW_POOL(pool) ValueListNode(pool, 0U); + while (count--) + node->inputList->add(PAR_parse_value(tdbb, csb)); + + count = csb->csb_blr_reader.getWord(); + + auto recordFormat = Format::newFormat(pool, count); + ULONG& offset = recordFormat->fmt_length = FLAG_BYTES(recordFormat->fmt_count); + Format::fmt_desc_iterator descIt = recordFormat->fmt_desc.begin(); + SSHORT fieldId = 0; + while (count--) + { + PAR_desc(tdbb, csb, descIt, nullptr); + + const USHORT align = type_alignments[descIt->dsc_dtype]; + if (align) + offset = FB_ALIGN(offset, align); + descIt->dsc_address = (UCHAR*)(IPTR)offset; + offset += descIt->dsc_length; + descIt++; + + string columnString; + csb->csb_blr_reader.getString(columnString); + fb_assert(columnString.hasData()); + + tableValueFunctionCsb->fields.put(columnString, fieldId++); + } + + element.csb_format = tableValueFunctionCsb->recordFormat = recordFormat; + element.csb_table_value_fun = tableValueFunctionCsb; + + return node; +} + +TableValueFunctionSourceNode* TableValueFunctionSourceNode::parseTableValueFunctions(thread_db* tdbb, + CompilerScratch* csb, + const SSHORT blrOp) +{ + MemoryPool& pool = *tdbb->getDefaultPool(); + TableValueFunctionSourceNode* node = nullptr; + switch (blrOp) + { + case blr_table_value_fun_unlist: + node = FB_NEW_POOL(pool) UnlistFunctionSourceNode(pool); + break; + + default: + PAR_syntax_error(csb, "blr_table_value_fun"); + } + + return node; +} + +Firebird::string TableValueFunctionSourceNode::internalPrint(NodePrinter& printer) const +{ + RecordSourceNode::internalPrint(printer); + + NODE_PRINT(printer, dsqlName); + NODE_PRINT(printer, inputList); + NODE_PRINT(printer, dsqlField); + NODE_PRINT(printer, alias); + NODE_PRINT(printer, dsqlNameColumns); + + return "TableValueFunctionsSourceNode"; +} + +RecordSourceNode* TableValueFunctionSourceNode::dsqlPass(DsqlCompilerScratch* dsqlScratch) +{ + return dsqlPassRelProc(dsqlScratch, this); +} + +bool TableValueFunctionSourceNode::dsqlMatch(DsqlCompilerScratch* /*dsqlScratch*/, + const ExprNode* other, bool /*ignoreMapCast*/) const +{ + const auto o = nodeAs(other); + return o && dsqlContext == o->dsqlContext; +} + +RecordSourceNode* TableValueFunctionSourceNode::pass1(thread_db* tdbb, CompilerScratch* csb) +{ + doPass1(tdbb, csb, inputList.getAddress()); + return this; +} + +RecordSourceNode* TableValueFunctionSourceNode::pass2(thread_db* tdbb, CompilerScratch* csb) +{ + ExprNode::doPass2(tdbb, csb, inputList.getAddress()); + return this; +} + +void TableValueFunctionSourceNode::genBlr(DsqlCompilerScratch* dsqlScratch) +{ + dsqlScratch->appendUChar(blr_table_value_fun); + + auto tableValueFunctionContext = dsqlContext->ctx_table_value_fun; + + if (tableValueFunctionContext->funName == UnlistFunctionSourceNode::FUNC_NAME) + dsqlScratch->appendUChar(blr_table_value_fun_unlist); + else + fb_assert(false); + + GEN_stuff_context(dsqlScratch, dsqlContext); + + dsqlScratch->appendMetaString(dsqlContext->ctx_alias.c_str()); + + dsqlScratch->appendUShort(dsqlContext->ctx_proc_inputs->items.getCount()); + for (auto& arg : dsqlContext->ctx_proc_inputs->items) + GEN_expr(dsqlScratch, arg); + + Array arrayFld; + for (const auto* field = tableValueFunctionContext->outputField; field; field = field->fld_next) + arrayFld.add(field); + + dsqlScratch->appendUShort(arrayFld.getCount()); + + for (const auto& fld : arrayFld) + { + dsqlScratch->putDtype(fld, true); + dsqlScratch->appendMetaString(fld->fld_name.c_str()); + } +} + +TableValueFunctionSourceNode* TableValueFunctionSourceNode::copy(thread_db* tdbb, + NodeCopier& copier) const +{ + if (!copier.remap) + BUGCHECK(221); // msg 221 (CMP) copy: cannot remap + + MemoryPool& pool = *tdbb->getDefaultPool(); + + auto newStream = copier.csb->nextStream(); + copier.remap[stream] = newStream; + + auto element = CMP_csb_element(copier.csb, newStream); + element->csb_view_stream = copier.remap[0]; + element->csb_format = m_csbTableValueFun->recordFormat; + element->csb_table_value_fun = m_csbTableValueFun; + if (alias.hasData()) + element->csb_alias = FB_NEW_POOL(pool) string(pool, alias.c_str()); + + auto newSource = TableValueFunctionSourceNode::parseTableValueFunctions( + tdbb, copier.csb, m_csbTableValueFun->funcId); + + newSource->inputList = copier.copy(tdbb, inputList); + newSource->m_csbTableValueFun = m_csbTableValueFun; + newSource->stream = newStream; + + return newSource; +} + +void TableValueFunctionSourceNode::pass1Source(thread_db* tdbb, CompilerScratch* csb, + RseNode* /*rse*/, BoolExprNode** /*boolean*/, + RecordSourceNodeStack& stack) +{ + stack.push(this); // Assume that the source will be used. Push it on the final stream stack. + + pass1(tdbb, csb); + + jrd_rel* const parentView = csb->csb_view; + const StreamType viewStream = csb->csb_view_stream; + + auto element = CMP_csb_element(csb, stream); + element->csb_view = parentView; + element->csb_view_stream = viewStream; + + // in the case where there is a parent view, find the context name + + if (parentView) + { + const ViewContexts& ctx = parentView->rel_view_contexts; + const USHORT key = stream; + FB_SIZE_T pos; + + if (ctx.find(key, pos)) + { + element->csb_alias = + FB_NEW_POOL(csb->csb_pool) string(csb->csb_pool, ctx[pos]->vcx_context_name); + } + } +} + +void TableValueFunctionSourceNode::pass2Rse(thread_db* tdbb, CompilerScratch* csb) +{ + csb->csb_rpt[stream].activate(); + + pass2(tdbb, csb); +} + +bool TableValueFunctionSourceNode::containsStream(StreamType checkStream) const +{ + return checkStream == stream; +} + +void TableValueFunctionSourceNode::computeDbKeyStreams(StreamList& /*streamList*/) const +{ +} + +RecordSource* TableValueFunctionSourceNode::compile(thread_db* /*tdbb*/, Optimizer* /*opt*/, + bool /*innerSubStream*/) +{ + fb_assert(false); // + return nullptr; +} + +bool TableValueFunctionSourceNode::computable(CompilerScratch* csb, StreamType stream, + bool allowOnlyCurrentStream, ValueExprNode* /*value*/) +{ + if (inputList && !inputList->computable(csb, stream, allowOnlyCurrentStream)) + return false; + + return true; +} + +void TableValueFunctionSourceNode::findDependentFromStreams(const CompilerScratch* csb, + StreamType currentStream, + SortedStreamList* streamList) +{ + if (inputList) + inputList->findDependentFromStreams(csb, currentStream, streamList); +} + +void TableValueFunctionSourceNode::collectStreams(SortedStreamList& streamList) const +{ + RecordSourceNode::collectStreams(streamList); + + if (inputList) + inputList->collectStreams(streamList); +} + +dsql_fld* TableValueFunctionSourceNode::makeField(DsqlCompilerScratch* /*dsqlScratch*/) +{ + fb_assert(false); + return nullptr; +} + +void TableValueFunctionSourceNode::setDefaultNameField(DsqlCompilerScratch* /*dsqlScratch*/) +{ + if (const auto tableValueFunctionContext = dsqlContext->ctx_table_value_fun) + { + MetaName nameFunc = tableValueFunctionContext->funName; + + auto i = 0U; + + if (nameFunc == UnlistFunctionSourceNode::FUNC_NAME) + { + dsql_fld* field = tableValueFunctionContext->outputField; + if (field->fld_name.isEmpty()) + field->fld_name = nameFunc; + } + else + fb_assert(false); + } + else + fb_assert(false); +} + +RecordSource* UnlistFunctionSourceNode::compile(thread_db* tdbb, Optimizer* opt, + bool /*innerSubStream*/) +{ + MemoryPool& pool = *tdbb->getDefaultPool(); + const auto csb = opt->getCompilerScratch(); + auto aliasOpt = opt->makeAlias(stream); + + return FB_NEW_POOL(pool) UnlistFunctionScan(csb, stream, aliasOpt, inputList); +} + +dsql_fld* UnlistFunctionSourceNode::makeField(DsqlCompilerScratch* dsqlScratch) +{ + if (inputList) + inputList = Node::doDsqlPass(dsqlScratch, inputList, false); + + dsc desc; + + auto inputItem = inputList->items.begin()->getObject(); + inputItem->setParameterType( + dsqlScratch, [](dsc* desc) { desc->makeVarying(1024, CS_dynamic); }, false); + + dsql_fld* field = dsqlField; + + if (!field) + { + auto newField = FB_NEW_POOL(dsqlScratch->getPool()) dsql_fld(dsqlScratch->getPool()); + field = newField; + + DsqlDescMaker::fromNode(dsqlScratch, &desc, inputItem); + + USHORT ttype = desc.getCharSet(); + + if (ttype == CS_NONE) + ttype = CS_ASCII; + + desc.makeText(32, ttype); + MAKE_field(newField, &desc); + newField->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(UnlistFunctionSourceNode::FUNC_NAME) + << Arg::Num(1) << Arg::Num(dsqlNameColumns.getCount())); + } + + field->fld_name = dsqlNameColumns[0]; + } + + field->resolve(dsqlScratch); + return field; +} //-------------------- @@ -3702,6 +4048,11 @@ static RecordSourceNode* dsqlPassRelProc(DsqlCompilerScratch* dsqlScratch, Recor relName = relNode->dsqlName; relAlias = relNode->alias; } + else if (const auto tblBasedFunNode = nodeAs(source)) + { + relName = tblBasedFunNode->dsqlName; + relAlias = tblBasedFunNode->alias.c_str(); + } //// TODO: LocalTableSourceNode else fb_assert(false); diff --git a/src/jrd/RecordSourceNodes.h b/src/jrd/RecordSourceNodes.h index ce4f7c0d51d..d7405d8c837 100644 --- a/src/jrd/RecordSourceNodes.h +++ b/src/jrd/RecordSourceNodes.h @@ -963,6 +963,70 @@ class SelectExprNode final : public TypedNode* columns; }; +class TableValueFunctionSourceNode + : public TypedNode +{ +public: + explicit TableValueFunctionSourceNode(MemoryPool& pool) + : TypedNode(pool), + dsqlName(pool), alias(pool), dsqlField(nullptr), dsqlNameColumns(pool), + m_csbTableValueFun(nullptr) + { + } + static TableValueFunctionSourceNode* parse(thread_db* tdbb, CompilerScratch* csb, + const SSHORT blrOp); + static TableValueFunctionSourceNode* parseTableValueFunctions(thread_db* tdbb, + CompilerScratch* csb, + const SSHORT blrOp); + + Firebird::string internalPrint(NodePrinter& printer) const override; + RecordSourceNode* dsqlPass(DsqlCompilerScratch* dsqlScratch) override; + bool dsqlMatch(DsqlCompilerScratch* dsqlScratch, const ExprNode* other, + bool ignoreMapCast) const override; + RecordSourceNode* pass1(thread_db* tdbb, CompilerScratch* csb) override; + RecordSourceNode* pass2(thread_db* tdbb, CompilerScratch* csb) override; + TableValueFunctionSourceNode* copy(thread_db* tdbb, NodeCopier& copier) const override; + void genBlr(DsqlCompilerScratch* dsqlScratch) override; + void pass1Source(thread_db* tdbb, CompilerScratch* csb, RseNode* rse, BoolExprNode** boolean, + RecordSourceNodeStack& stack) override; + void pass2Rse(thread_db* tdbb, CompilerScratch* csb) override; + bool containsStream(StreamType checkStream) const override; + void computeDbKeyStreams(StreamList& streamList) const override; + RecordSource* compile(thread_db* tdbb, Optimizer* opt, bool innerSubStream) override; + + bool computable(CompilerScratch* csb, StreamType stream, bool allowOnlyCurrentStream, + ValueExprNode* value) override; + void findDependentFromStreams(const CompilerScratch* csb, StreamType currentStream, + SortedStreamList* streamList) override; + + void collectStreams(SortedStreamList& streamList) const override; + + virtual dsql_fld* makeField(DsqlCompilerScratch* dsqlScratch); + void setDefaultNameField(DsqlCompilerScratch* dsqlScratch); + +public: + MetaName dsqlName; + MetaName alias; + NestConst inputList; + dsql_fld* dsqlField; + Firebird::ObjectsArray dsqlNameColumns; + +private: + jrd_table_value_fun* m_csbTableValueFun; +}; + +class UnlistFunctionSourceNode : public TableValueFunctionSourceNode +{ +public: + explicit UnlistFunctionSourceNode(MemoryPool& pool) : TableValueFunctionSourceNode(pool) + { + } + + RecordSource* compile(thread_db* tdbb, Optimizer* opt, bool innerSubStream) final; + dsql_fld* makeField(DsqlCompilerScratch* dsqlScratch) final; + + static constexpr char const* FUNC_NAME = "UNLIST"; +}; } // namespace Jrd diff --git a/src/jrd/cmp.cpp b/src/jrd/cmp.cpp index 87930ad8b64..af1d010b2a9 100644 --- a/src/jrd/cmp.cpp +++ b/src/jrd/cmp.cpp @@ -266,6 +266,8 @@ const Format* CMP_format(thread_db* tdbb, CompilerScratch* csb, StreamType strea tail->csb_format = MET_current(tdbb, tail->csb_relation); else if (tail->csb_procedure) tail->csb_format = tail->csb_procedure->prc_record_format; + else if (tail->csb_table_value_fun) + tail->csb_format = tail->csb_table_value_fun->recordFormat; //// TODO: LocalTableSourceNode else IBERROR(222); // msg 222 bad blr - invalid stream diff --git a/src/jrd/exe.h b/src/jrd/exe.h index b61833e7836..c9905106ea1 100644 --- a/src/jrd/exe.h +++ b/src/jrd/exe.h @@ -431,6 +431,28 @@ class ItemInfo : public Printable typedef Firebird::LeftPooledMap MapFieldInfo; typedef Firebird::RightPooledMap MapItemInfo; +// Table value function block + +class jrd_table_value_fun +{ +public: + explicit jrd_table_value_fun(MemoryPool& p) : recordFormat(nullptr), fields(p), name(p), funcId(0) + { + } + + SSHORT getId(const MetaName& fieldName) const + { + SSHORT* id = fields.get(fieldName); + fb_assert(id); + return *id; + } + + const Format* recordFormat; + Firebird::LeftPooledMap fields; + Firebird::string name; + USHORT funcId; +}; + // Compile scratch block class CompilerScratch : public pool_alloc @@ -610,6 +632,7 @@ class CompilerScratch : public pool_alloc void activate(); void deactivate(); + Firebird::string getName(bool allowEmpty = true) const; std::optional csb_cursor_number; // Cursor number for this stream StreamType csb_stream; // Map user context to internal stream @@ -630,6 +653,7 @@ class CompilerScratch : public pool_alloc PlanNode* csb_plan; // user-specified plan for this relation StreamType* csb_map; // Stream map for views RecordSource** csb_rsb_ptr; // point to rsb for nod_stream + jrd_table_value_fun* csb_table_value_fun; // Table value function }; typedef csb_repeat* rpt_itr; @@ -654,7 +678,8 @@ inline CompilerScratch::csb_repeat::csb_repeat() csb_cardinality(0.0), // TMN: Non-natural cardinality?! csb_plan(0), csb_map(0), - csb_rsb_ptr(0) + csb_rsb_ptr(0), + csb_table_value_fun(0) { } @@ -668,6 +693,22 @@ inline void CompilerScratch::csb_repeat::deactivate() csb_flags &= ~csb_active; } +inline Firebird::string CompilerScratch::csb_repeat::getName(bool allowEmpty) const +{ + if (csb_relation) + return csb_relation->rel_name.c_str(); + else if (csb_procedure) + return csb_procedure->getName().toString(); + else if (csb_table_value_fun) + return csb_table_value_fun->name.c_str(); + //// TODO: LocalTableSourceNode + else + { + fb_assert(allowEmpty); + return ""; + } +} + class AutoSetCurrentCursorId : private Firebird::AutoSetRestore { diff --git a/src/jrd/optimizer/Optimizer.cpp b/src/jrd/optimizer/Optimizer.cpp index f40713f4638..1562011ff2a 100644 --- a/src/jrd/optimizer/Optimizer.cpp +++ b/src/jrd/optimizer/Optimizer.cpp @@ -2887,12 +2887,7 @@ string Optimizer::getStreamName(StreamType stream) const auto procedure = tail->csb_procedure; const auto alias = tail->csb_alias; - string name; - - if (relation) - name = relation->rel_name.c_str(); - else if (procedure) - name = procedure->getName().toString(); + string name = tail->getName(); if (alias && alias->hasData()) { @@ -2941,13 +2936,8 @@ string Optimizer::makeAlias(StreamType stream) alias += ' '; } } - else if (csb_tail->csb_relation) - alias = csb_tail->csb_relation->rel_name.c_str(); - else if (csb_tail->csb_procedure) - alias = csb_tail->csb_procedure->getName().toString(); - //// TODO: LocalTableSourceNode else - fb_assert(false); + alias = csb_tail->getName(false); return alias; } diff --git a/src/jrd/par.cpp b/src/jrd/par.cpp index 3d7442d49d9..ef08ba112b2 100644 --- a/src/jrd/par.cpp +++ b/src/jrd/par.cpp @@ -136,6 +136,7 @@ namespace CompilerScratch::csb_repeat* t2 = CMP_csb_element(m_csb, stream); t2->csb_relation = ptr->csb_relation; t2->csb_procedure = ptr->csb_procedure; + t2->csb_table_value_fun = ptr->csb_table_value_fun; t2->csb_stream = ptr->csb_stream; t2->csb_flags = ptr->csb_flags & csb_used; } @@ -616,10 +617,12 @@ ValueExprNode* PAR_make_field(thread_db* tdbb, CompilerScratch* csb, USHORT cont jrd_rel* const relation = csb->csb_rpt[stream].csb_relation; jrd_prc* const procedure = csb->csb_rpt[stream].csb_procedure; + jrd_table_value_fun* const table_value_function = csb->csb_rpt[stream].csb_table_value_fun; const SSHORT id = relation ? MET_lookup_field(tdbb, relation, base_field) : - procedure ? PAR_find_proc_field(procedure, base_field) : -1; + procedure ? PAR_find_proc_field(procedure, base_field) : + table_value_function ? table_value_function->getId(base_field) : -1; if (id < 0) return NULL; @@ -1221,6 +1224,9 @@ RecordSourceNode* PAR_parseRecordSource(thread_db* tdbb, CompilerScratch* csb) case blr_aggregate: return AggregateSourceNode::parse(tdbb, csb); + case blr_table_value_fun: + return TableValueFunctionSourceNode::parse(tdbb, csb, blrOp); + default: PAR_syntax_error(csb, "record source"); } @@ -1555,6 +1561,7 @@ DmlNode* PAR_parse_node(thread_db* tdbb, CompilerScratch* csb) case blr_recurse: case blr_window: case blr_aggregate: + case blr_table_value_fun: csb->csb_blr_reader.seekBackward(1); return PAR_parseRecordSource(tdbb, csb); } diff --git a/src/jrd/recsrc/RecordSource.h b/src/jrd/recsrc/RecordSource.h index c6a1b7bf023..cfb4d685e2b 100644 --- a/src/jrd/recsrc/RecordSource.h +++ b/src/jrd/recsrc/RecordSource.h @@ -1462,6 +1462,64 @@ namespace Jrd NestConst const m_boolean; }; + class TableValueFunctionScan : public RecordStream + { + protected: + struct Impure : public RecordSource::Impure + { + RecordBuffer* m_recordBuffer; + }; + + public: + TableValueFunctionScan(CompilerScratch* csb, StreamType stream, + const Firebird::string& alias); + + bool refetchRecord(thread_db* tdbb) const override; + WriteLockResult lockRecord(thread_db* tdbb) const override; + void getLegacyPlan(thread_db* tdbb, Firebird::string& plan, unsigned level) const override; + + protected: + bool internalGetRecord(thread_db* tdbb) const override; + void assignParameter(thread_db* tdbb, dsc* fromDesc, const dsc* toDesc, SSHORT toId, + Record* record) const; + + virtual bool nextBuffer(thread_db* tdbb) const = 0; + + const Firebird::string m_alias; + }; + + class UnlistFunctionScan final : public TableValueFunctionScan + { + enum UnlistTypeItemIndex : unsigned + { + UNLIST_INDEX_STRING = 0, + UNLIST_INDEX_SEPARATOR = 1, + UNLIST_INDEX_LAST = 2 + }; + + struct Impure : public TableValueFunctionScan::Impure + { + blb* m_blob; + Firebird::string* m_separatorStr; + Firebird::string* m_resultStr; + }; + + public: + UnlistFunctionScan(CompilerScratch* csb, StreamType stream, const Firebird::string& alias, + ValueListNode* list); + + protected: + void close(thread_db* tdbb) const final; + void internalOpen(thread_db* tdbb) const final; + void internalGetPlan(thread_db* tdbb, PlanEntry& planEntry, unsigned level, + bool recurse) const final; + + bool nextBuffer(thread_db* tdbb) const final; + + private: + NestConst m_inputList; + }; + } // namespace #endif // JRD_RECORD_SOURCE_H diff --git a/src/jrd/recsrc/TableValueFunctionScan.cpp b/src/jrd/recsrc/TableValueFunctionScan.cpp new file mode 100644 index 00000000000..ecf0354d2ad --- /dev/null +++ b/src/jrd/recsrc/TableValueFunctionScan.cpp @@ -0,0 +1,328 @@ +/* + * The contents of this file are subject to the Initial + * Developer's Public License Version 1.0 (the "License"); + * you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * http://www.ibphoenix.com/main.nfs?a=ibphoenix&page=ibp_idpl. + * + * Software distributed under the License is distributed AS IS, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. + * See the License for the specific language governing rights + * and limitations under the License. + * + * The Original Code was created by Chudaykin Alexey + * for Red Soft Corporation. + * + * Copyright (c) 2025 Red Soft Corporation + * and all contributors signed below. + * + * All Rights Reserved. + * Contributor(s): + */ +#include "firebird.h" +#include "../dsql/ExprNodes.h" +#include "../dsql/StmtNodes.h" +#include "../jrd/jrd.h" +#include "../jrd/mov_proto.h" +#include "../jrd/optimizer/Optimizer.h" +#include "../jrd/req.h" +#include "../jrd/vio_proto.h" + +#include "RecordSource.h" +#include + +using namespace Firebird; +using namespace Jrd; + +TableValueFunctionScan::TableValueFunctionScan(CompilerScratch* csb, StreamType stream, + const string& alias) + : RecordStream(csb, stream), m_alias(csb->csb_pool, alias) +{ + m_cardinality = DEFAULT_CARDINALITY; +} + +bool TableValueFunctionScan::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(); + do + { + if (!impure->m_recordBuffer->fetch(rpb->rpb_number.getValue(), rpb->rpb_record)) + { + if (!nextBuffer(tdbb)) + { + rpb->rpb_number.setValid(false); + return false; + } + continue; + } + return true; + } while (true); +} + +bool TableValueFunctionScan::refetchRecord(thread_db* /*tdbb*/) const +{ + return true; +} + +WriteLockResult TableValueFunctionScan::lockRecord(thread_db* /*tdbb*/) const +{ + status_exception::raise(Arg::Gds(isc_record_lock_not_supp)); +} + +void TableValueFunctionScan::getLegacyPlan(thread_db* tdbb, string& plan, unsigned level) const +{ + if (!level) + plan += "("; + + plan += printName(tdbb, m_alias, false) + " NATURAL"; + + if (!level) + plan += ")"; +} + +void TableValueFunctionScan::assignParameter(thread_db* tdbb, dsc* fromDesc, const dsc* toDesc, + SSHORT toId, Record* record) const +{ + dsc toDescValue = *toDesc; + toDescValue.dsc_address = record->getData() + (IPTR)toDesc->dsc_address; + + if (fromDesc->isNull()) + { + record->setNull(toId); + return; + } + else + record->clearNull(toId); + + if (!DSC_EQUIV(fromDesc, &toDescValue, false)) + { + MOV_move(tdbb, fromDesc, &toDescValue); + return; + } + + 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) +{ + m_impure = csb->allocImpure(); +} + +void UnlistFunctionScan::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; + + if (impure->m_recordBuffer) + { + delete impure->m_recordBuffer; + impure->m_recordBuffer = nullptr; + } + + if (impure->m_blob) + { + impure->m_blob->BLB_close(tdbb); + impure->m_blob = nullptr; + } + + if (impure->m_separatorStr) + { + delete impure->m_separatorStr; + impure->m_separatorStr = nullptr; + } + + if (impure->m_resultStr) + { + delete impure->m_resultStr; + impure->m_resultStr = nullptr; + } + } +} + +void UnlistFunctionScan::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() >= UNLIST_INDEX_LAST); + + auto valueItem = m_inputList->items[UNLIST_INDEX_STRING]; + const auto valueDesc = EVL_expr(tdbb, request, valueItem); + if (valueDesc == nullptr) + { + rpb->rpb_number.setValid(false); + return; + } + + auto separatorItem = m_inputList->items[UNLIST_INDEX_SEPARATOR]; + const auto separatorDesc = EVL_expr(tdbb, request, separatorItem); + if (separatorDesc == nullptr) + { + rpb->rpb_number.setValid(false); + return; + } + + const auto impure = request->getImpure(m_impure); + impure->irsb_flags |= irsb_open; + impure->m_recordBuffer = FB_NEW_POOL(pool) RecordBuffer(pool, m_format); + impure->m_blob = nullptr; + impure->m_resultStr = nullptr; + + Record* const record = VIO_record(tdbb, rpb, m_format, &pool); + + auto toDesc = m_format->fmt_desc.begin(); + fb_assert(toDesc); + const auto textType = toDesc->getTextType(); + + impure->m_separatorStr = FB_NEW_POOL(pool) + string(pool, MOV_make_string2(tdbb, separatorDesc, textType, true)); + + if (impure->m_separatorStr->isEmpty()) + { + const string valueStr(MOV_make_string2(tdbb, valueDesc, textType, false)); + dsc fromDesc; + fromDesc.makeText(valueStr.size(), textType, (UCHAR*)(IPTR)(valueStr.c_str())); + assignParameter(tdbb, &fromDesc, toDesc, 0, record); + impure->m_recordBuffer->store(record); + return; + } + + if (valueDesc->isBlob()) + { + impure->m_blob = blb::open(tdbb, request->req_transaction, + reinterpret_cast(valueDesc->dsc_address)); + impure->m_resultStr = FB_NEW_POOL(pool) string(pool); + } + else + { + const string& separatorStr = *impure->m_separatorStr; + string valueStr(MOV_make_string2(tdbb, valueDesc, textType, true)); + auto end = AbstractString::npos; + do + { + auto size = end = valueStr.find(separatorStr); + if (end == AbstractString::npos) + { + if (valueStr.hasData()) + size = valueStr.size(); + else + break; + } + + if (size > 0) + { + dsc fromDesc; + fromDesc.makeText(size, textType, (UCHAR*)(IPTR)(valueStr.c_str())); + assignParameter(tdbb, &fromDesc, toDesc, 0, record); + impure->m_recordBuffer->store(record); + } + + valueStr.erase(valueStr.begin(), valueStr.begin() + end + separatorStr.length()); + } while (end != AbstractString::npos); + } +} + +void UnlistFunctionScan::internalGetPlan(thread_db* tdbb, PlanEntry& planEntry, unsigned /*level*/, + bool /*recurse*/) const +{ + planEntry.className = "FunctionScan"; + + planEntry.lines.add().text = "Functions " + printName(tdbb, "Unlist", m_alias) + " Scan"; + printOptInfo(planEntry.lines); + + if (m_alias.hasData()) + planEntry.alias = m_alias; +} + +bool UnlistFunctionScan::nextBuffer(thread_db* tdbb) const +{ + const auto request = tdbb->getRequest(); + const auto impure = request->getImpure(m_impure); + + if (impure->m_blob) + { + auto setStringToRecord = [&](const string& str) + { + Record* const record = request->req_rpb[m_stream].rpb_record; + + auto toDesc = m_format->fmt_desc.begin(); + const auto textType = toDesc->getTextType(); + + dsc fromDesc; + fromDesc.makeText(str.length(), textType, (UCHAR*)(IPTR)(str.c_str())); + assignParameter(tdbb, &fromDesc, toDesc, 0, record); + impure->m_recordBuffer->store(record); + }; + + MoveBuffer buffer; + const auto address = buffer.getBuffer(MAX_COLUMN_SIZE); + const auto length = impure->m_blob->BLB_get_data(tdbb, address, MAX_COLUMN_SIZE, false); + if (length > 0) + { + const std::string_view separatorView(impure->m_separatorStr->data(), + impure->m_separatorStr->length()); + std::string_view valueView(reinterpret_cast(address), length); + auto end = std::string_view::npos; + do + { + auto size = end = valueView.find(separatorView); + if (end == std::string_view::npos) + { + if (!valueView.empty()) + impure->m_resultStr->append(valueView.data(), valueView.length()); + + break; + } + + if (size > 0) + impure->m_resultStr->append(valueView.data(), size); + + valueView.remove_prefix(size + separatorView.length()); + + if (impure->m_resultStr->hasData()) + { + setStringToRecord(*impure->m_resultStr); + impure->m_resultStr->erase(); + } + } while (end != std::string_view::npos); + + return true; + } + + if (impure->m_blob->blb_flags & BLB_eof) + { + if (impure->m_resultStr->hasData()) + { + setStringToRecord(*impure->m_resultStr); + impure->m_resultStr->erase(); + return true; + } + } + } + + return false; +}