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;
+}