From 269aee48bb0b6a68b6ae1a190f2339e7f5cadb5e Mon Sep 17 00:00:00 2001 From: ChudaykinAlex Date: Tue, 3 Feb 2026 12:03:03 +0300 Subject: [PATCH 1/5] Feature implementation --- src/dsql/parse.y | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/dsql/parse.y b/src/dsql/parse.y index 140aa27c903..9275d616a8c 100644 --- a/src/dsql/parse.y +++ b/src/dsql/parse.y @@ -7756,8 +7756,36 @@ in_predicate ComparativeBoolNode::DFLAG_ANSI_ANY, $4); $$ = newNode(node); } + | value IN table_value_function_unlist_short + { + $$ = newNode(blr_eql, $1, + ComparativeBoolNode::DFLAG_ANSI_ANY, $3); + } + | value NOT IN table_value_function_unlist_short + { + const auto node = newNode(blr_eql, $1, + ComparativeBoolNode::DFLAG_ANSI_ANY, $4); + $$ = newNode(node); + } ; +%type table_value_function_unlist_short +table_value_function_unlist_short +: table_value_function_unlist + { + const auto unlistNode = nodeAs($1); + unlistNode->alias = UnlistFunctionSourceNode::FUNC_NAME; + + const auto rseNode = newNode(); + rseNode->dsqlFlags |= RecordSourceNode::DFLAG_BODY_WRAPPER; + rseNode->dsqlFrom = newNode(unlistNode); + + const auto selectNode = newNode(); + selectNode->querySpec = rseNode; + + $$ = selectNode; + } + %type exists_predicate exists_predicate : EXISTS '(' select_expr ')' From af90afa3e4715ef2ae161fe9225142e81c4e93ca Mon Sep 17 00:00:00 2001 From: ChudaykinAlex Date: Tue, 3 Feb 2026 12:49:49 +0300 Subject: [PATCH 2/5] Addition to documentation --- doc/sql.extensions/README.unlist | 40 ++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/doc/sql.extensions/README.unlist b/doc/sql.extensions/README.unlist index 7ca4c5fd680..40847b4ca07 100644 --- a/doc/sql.extensions/README.unlist +++ b/doc/sql.extensions/README.unlist @@ -67,5 +67,45 @@ Unacceptable behavior: SELECT UNLIST FROM UNLIST('UNLIST,A,S,A') AS A; +Short syntax for UNLIST with IN operator: + Since Firebird 6.0, UNLIST can be used directly with the IN operator without requiring a full subquery. + Instead of writing IN (SELECT * FROM UNLIST(...) AS U), you can now use the shorter syntax IN UNLIST(...). + +Syntax: + ::= + [NOT] IN + +
::= + UNLIST ( [, ] [, ] ) + +Examples: + +A) + SELECT * FROM EMPLOYEE WHERE EMP_NO IN UNLIST('2,4,5,7,11'); +B) + SELECT * FROM EMPLOYEE WHERE JOB_COUNTRY IN UNLIST('France,Italy,England'); +C) + SELECT EMP_NO FROM EMPLOYEE WHERE JOB_COUNTRY NOT IN UNLIST('USA'); +D) + SELECT * FROM EMPLOYEE WHERE DEPT_NO IN UNLIST('100:110:115:120', ':' RETURNING INT); +E) + SET AUTOTERM; + RECREATE PROCEDURE GET_EMPLOYEES_BY_PHONE_EXT (PHONE VARCHAR(1000)) + RETURNS (EMPLOYEE_NAME VARCHAR(100)) + AS + BEGIN + FOR + SELECT FIRST_NAME FROM EMPLOYEE + WHERE PHONE_EXT IN UNLIST(:PHONE, ',' RETURNING INT) + INTO :EMPLOYEE_NAME + DO + SUSPEND; + END; + + SELECT A.EMPLOYEE_NAME FROM GET_EMPLOYEES_BY_PHONE_EXT('2,3,7') AS A; + +Note: + The short syntax automatically provides the correlation name and column name, + so they cannot be specified explicitly when using this form. From e89764abeda64bbba46c7a8cd222b7e8ced70ba0 Mon Sep 17 00:00:00 2001 From: "alexey.chudaykin" Date: Tue, 3 Mar 2026 12:24:54 +0300 Subject: [PATCH 3/5] Documentation adjustments --- doc/sql.extensions/README.unlist | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/sql.extensions/README.unlist b/doc/sql.extensions/README.unlist index 863b0a2e2a9..1cab515be50 100644 --- a/doc/sql.extensions/README.unlist +++ b/doc/sql.extensions/README.unlist @@ -75,8 +75,8 @@ Unacceptable behavior: Short syntax for UNLIST with IN operator: - Since Firebird 6.0, UNLIST can be used directly with the IN operator without requiring a full subquery. - Instead of writing IN (SELECT * FROM UNLIST(...) AS U), you can now use the shorter syntax IN UNLIST(...). + UNLIST can be used directly with the IN operator without requiring a full subquery. + Instead of writing IN (SELECT * FROM UNLIST(...) AS U), you can use the shorter syntax IN UNLIST(...). Syntax: ::= From 32875c751eb15b14de7dd62fc9c3e4c90f64fabe Mon Sep 17 00:00:00 2001 From: "alexey.chudaykin" Date: Tue, 3 Mar 2026 16:42:06 +0300 Subject: [PATCH 4/5] The code is consistent with the description. --- src/jrd/RecordSourceNodes.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/jrd/RecordSourceNodes.cpp b/src/jrd/RecordSourceNodes.cpp index 764effa6f47..e49d3a3e215 100644 --- a/src/jrd/RecordSourceNodes.cpp +++ b/src/jrd/RecordSourceNodes.cpp @@ -4387,7 +4387,7 @@ dsql_fld* UnlistFunctionSourceNode::makeField(DsqlCompilerScratch* dsqlScratch) ttype = CS_ASCII; const auto bytesPerChar = DSqlDataTypeUtil(dsqlScratch).maxBytesPerChar(ttype); - desc.makeText(bytesPerChar * DEFAULT_UNLIST_TEXT_LENGTH, ttype); + desc.makeVarying(bytesPerChar * DEFAULT_UNLIST_TEXT_LENGTH, ttype); MAKE_field(newField, &desc); newField->fld_id = 0; } From be1b532bdcfb85a66ca1795fbd16c9ded204ecb0 Mon Sep 17 00:00:00 2001 From: "alexey.chudaykin" Date: Wed, 4 Mar 2026 14:24:34 +0300 Subject: [PATCH 5/5] Type inference based on comparison parameter --- src/dsql/parse.y | 35 ++++++++++++++++++++--------------- src/jrd/RecordSourceNodes.cpp | 20 ++++++++++++++------ src/jrd/RecordSourceNodes.h | 8 ++++++-- 3 files changed, 40 insertions(+), 23 deletions(-) diff --git a/src/dsql/parse.y b/src/dsql/parse.y index 7828716ba95..61363c34c85 100644 --- a/src/dsql/parse.y +++ b/src/dsql/parse.y @@ -7801,12 +7801,12 @@ in_predicate ComparativeBoolNode::DFLAG_ANSI_ANY, $4); $$ = newNode(node); } - | value IN table_value_function_unlist_short + | value IN table_value_function_unlist_short(NOTRIAL($1)) { $$ = newNode(blr_eql, $1, ComparativeBoolNode::DFLAG_ANSI_ANY, $3); } - | value NOT IN table_value_function_unlist_short + | value NOT IN table_value_function_unlist_short(NOTRIAL($1)) { const auto node = newNode(blr_eql, $1, ComparativeBoolNode::DFLAG_ANSI_ANY, $4); @@ -7814,22 +7814,27 @@ in_predicate } ; -%type table_value_function_unlist_short -table_value_function_unlist_short -: table_value_function_unlist - { - const auto unlistNode = nodeAs($1); - unlistNode->alias = UnlistFunctionSourceNode::FUNC_NAME; +%type table_value_function_unlist_short() +table_value_function_unlist_short($autoTypeFromValue) + : table_value_function_unlist + { + const auto unlistNode = nodeAs($1); + unlistNode->alias = UnlistFunctionSourceNode::FUNC_NAME; + unlistNode->shortEntry = true; + + if (unlistNode->dsqlField == nullptr) + unlistNode->dsqlAutoTypeFromValue = $autoTypeFromValue; - const auto rseNode = newNode(); - rseNode->dsqlFlags |= RecordSourceNode::DFLAG_BODY_WRAPPER; - rseNode->dsqlFrom = newNode(unlistNode); + const auto rseNode = newNode(); + rseNode->dsqlFlags |= RecordSourceNode::DFLAG_BODY_WRAPPER; + rseNode->dsqlFrom = newNode(unlistNode); - const auto selectNode = newNode(); - selectNode->querySpec = rseNode; + const auto selectNode = newNode(); + selectNode->querySpec = rseNode; - $$ = selectNode; - } + $$ = selectNode; + } + ; %type exists_predicate exists_predicate diff --git a/src/jrd/RecordSourceNodes.cpp b/src/jrd/RecordSourceNodes.cpp index e49d3a3e215..ab1f08095c7 100644 --- a/src/jrd/RecordSourceNodes.cpp +++ b/src/jrd/RecordSourceNodes.cpp @@ -4380,14 +4380,22 @@ dsql_fld* UnlistFunctionSourceNode::makeField(DsqlCompilerScratch* dsqlScratch) field = newField; dsc desc; - DsqlDescMaker::fromNode(dsqlScratch, &desc, inputItem); - auto ttype = desc.getCharSet(); + if (dsqlAutoTypeFromValue && shortEntry) + { + dsqlAutoTypeFromValue = Node::doDsqlPass(dsqlScratch, dsqlAutoTypeFromValue, false); + DsqlDescMaker::fromNode(dsqlScratch, &desc, dsqlAutoTypeFromValue); + } + else + { + DsqlDescMaker::fromNode(dsqlScratch, &desc, inputItem); + auto ttype = desc.getCharSet(); - if (ttype == CS_NONE && !desc.isText() && !desc.isBlob()) - ttype = CS_ASCII; + if (ttype == CS_NONE && !desc.isText() && !desc.isBlob()) + ttype = CS_ASCII; - const auto bytesPerChar = DSqlDataTypeUtil(dsqlScratch).maxBytesPerChar(ttype); - desc.makeVarying(bytesPerChar * DEFAULT_UNLIST_TEXT_LENGTH, ttype); + const auto bytesPerChar = DSqlDataTypeUtil(dsqlScratch).maxBytesPerChar(ttype); + desc.makeVarying(bytesPerChar * DEFAULT_UNLIST_TEXT_LENGTH, ttype); + } MAKE_field(newField, &desc); newField->fld_id = 0; } diff --git a/src/jrd/RecordSourceNodes.h b/src/jrd/RecordSourceNodes.h index bb2f0d66236..a9f9c470523 100644 --- a/src/jrd/RecordSourceNodes.h +++ b/src/jrd/RecordSourceNodes.h @@ -1056,12 +1056,12 @@ class TableValueFunctionSourceNode class UnlistFunctionSourceNode : public TableValueFunctionSourceNode { public: - explicit UnlistFunctionSourceNode(MemoryPool& pool) : TableValueFunctionSourceNode(pool) + explicit UnlistFunctionSourceNode(MemoryPool& pool) : TableValueFunctionSourceNode(pool) { } RecordSource* compile(thread_db* tdbb, Optimizer* opt, bool innerSubStream) final; - dsql_fld* makeField(DsqlCompilerScratch* dsqlScratch) final; + dsql_fld* makeField(DsqlCompilerScratch* dsqlScratch) override; static constexpr char const* FUNC_NAME = "UNLIST"; static constexpr USHORT DEFAULT_UNLIST_TEXT_LENGTH = 32; @@ -1070,6 +1070,10 @@ class UnlistFunctionSourceNode : public TableValueFunctionSourceNode { return FUNC_NAME; } + +public: + NestConst dsqlAutoTypeFromValue; + bool shortEntry = false; }; class GenSeriesFunctionSourceNode final : public TableValueFunctionSourceNode