-
Notifications
You must be signed in to change notification settings - Fork 16
ECSQL: support IS / IS NOT operator between operands (null-safe comparison) #1476
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
9ce2554
8244641
7c28090
0c1b839
9b17160
a0bc9f2
5441fee
593ff15
e1fcba9
e2ba404
3d6aed2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2434,20 +2434,61 @@ BentleyStatus ECSqlParser::ParseSearchCondition(std::unique_ptr<BooleanExp>& exp | |
| } | ||
| case OSQLParseNode::boolean_test: | ||
| { | ||
| std::unique_ptr<BooleanExp> op1 = nullptr; | ||
| if (SUCCESS != ParseSearchCondition(op1, parseNode->getChild(0/*boolean_primary*/))) | ||
| return ERROR; | ||
|
|
||
| bool isNot = false; | ||
| if (SUCCESS != ParseNotToken(isNot, parseNode->getChild(2/*sql_not*/))) | ||
| return ERROR; | ||
|
|
||
| const BooleanSqlOperator op = isNot ? BooleanSqlOperator::IsNot : BooleanSqlOperator::Is; | ||
| OSQLParseNode const* lhsNode = parseNode->getChild(0/*boolean_primary*/); | ||
| OSQLParseNode const* rhsNode = parseNode->getChild(3/*truth_value | value_exp*/); | ||
|
|
||
| // A parenthesized qualified name on the right-hand side - e.g. 'X IS (alias.prop)' - parses as a | ||
| // '(ClassName)' type predicate because it is grammatically identical to one. If the name does not | ||
| // resolve to a class, reinterpret it as a property reference or enum literal so the comparison behaves | ||
| // like the null-safe 'X IS alias.prop' / 'X IS ts.Enum.Enumerator' instead of failing with a | ||
| // "class not found" error. | ||
| std::unique_ptr<ValueExp> rhsAsValueExp = nullptr; | ||
| if (SQL_ISRULE(rhsNode, type_predicate) && SUCCESS != TryParseParenthesizedNameAsValueExp(rhsAsValueExp, rhsNode)) | ||
| return ERROR; | ||
|
|
||
| // The right-hand operand of 'X IS [NOT] <rhs>' is either a "truth value" | ||
| // (NULL/TRUE/FALSE/UNKNOWN or the (ClassName) type predicate), which keeps the original | ||
| // boolean-predicate semantics, or any other value expression, which is a null-safe comparison. | ||
| const bool rhsIsTruthValue = rhsAsValueExp == nullptr | ||
| && (SQL_ISRULE(rhsNode, type_predicate) | ||
| || SQL_ISTOKEN(rhsNode, NULL) || SQL_ISTOKEN(rhsNode, TRUE) | ||
| || SQL_ISTOKEN(rhsNode, FALSE) || SQL_ISTOKEN(rhsNode, UNKNOWN)); | ||
|
|
||
| if (!rhsIsTruthValue) | ||
| { | ||
| // X IS [NOT] Y : both sides are value expressions compared with SQLite's null-safe IS operator. | ||
| // The left side is parsed as a value expression (not a boolean predicate) so that it keeps its real | ||
| // type. This matters for operands whose comparability depends on their type (e.g. points and | ||
| // navigation properties) and to detect a NULL literal operand for the column-wise expansion. | ||
| std::unique_ptr<ValueExp> lhsValueExp = nullptr; | ||
| if (SUCCESS != ParseValueExp(lhsValueExp, lhsNode)) | ||
| return ERROR; | ||
|
|
||
| std::unique_ptr<ValueExp> rhsValueExp = nullptr; | ||
| if (rhsAsValueExp != nullptr) | ||
| rhsValueExp = std::move(rhsAsValueExp); | ||
| else if (SUCCESS != ParseValueExp(rhsValueExp, rhsNode)) | ||
| return ERROR; | ||
|
|
||
| exp = std::make_unique<BinaryBooleanExp>(std::move(lhsValueExp), op, std::move(rhsValueExp)); | ||
| return SUCCESS; | ||
| } | ||
|
|
||
| // X IS [NOT] NULL|TRUE|FALSE|UNKNOWN|(ClassName) : the left side is a boolean predicate. | ||
| std::unique_ptr<BooleanExp> op1 = nullptr; | ||
| if (SUCCESS != ParseSearchCondition(op1, lhsNode)) | ||
| return ERROR; | ||
|
|
||
| std::unique_ptr<ValueExp> truthValueExp = nullptr; | ||
| if (SUCCESS != ParseTruthValue(truthValueExp, parseNode->getChild(3/*truth_value*/))) | ||
| if (SUCCESS != ParseValueExp(truthValueExp, rhsNode)) | ||
|
aruniverse marked this conversation as resolved.
|
||
| return ERROR; | ||
|
|
||
| // X IS [NOT] NULL|TRUE|FALSE|UNKNOWN | ||
| exp = std::make_unique<BinaryBooleanExp>(std::move(op1), isNot ? BooleanSqlOperator::IsNot : BooleanSqlOperator::Is, std::move(truthValueExp)); | ||
| exp = std::make_unique<BinaryBooleanExp>(std::move(op1), op, std::move(truthValueExp)); | ||
| return SUCCESS; | ||
| } | ||
| case OSQLParseNode::boolean_primary: | ||
|
|
@@ -3466,6 +3507,103 @@ BentleyStatus ECSqlParser::ParseTypePredicate(std::unique_ptr<ValueExp>& valueEx | |
| return SUCCESS; | ||
| } | ||
| //----------------------------------------------------------------------------------------- | ||
| // In an 'X IS (...)' expression a parenthesized qualified name such as '(alias.prop)' is | ||
| // grammatically indistinguishable from the '(ClassName)' type predicate: both reduce to a | ||
| // 'type_predicate' node. When the name does not resolve to an existing class, this reinterprets it as | ||
| // a property-reference or enum-literal value expression so the statement behaves like the | ||
| // null-safe comparison 'X IS alias.prop' / 'X IS ts.Enum.Enumerator' instead of failing with | ||
| // a "class not found" error. | ||
| // 'valueExp' is left null - so the caller keeps the type-predicate behavior - when the predicate uses | ||
| // an ONLY/ALL prefix, a comma-separated type list, the 'schema:class' (colon) form, or a name that | ||
| // does resolve to a class (e.g. 'ECClassId IS (ts.Foo)' or 'ECClassId IS (main.ts.Foo)'). | ||
| // @bsimethod | ||
| //+---------------+---------------+---------------+---------------+---------------+------ | ||
| BentleyStatus ECSqlParser::TryParseParenthesizedNameAsValueExp(std::unique_ptr<ValueExp>& valueExp, OSQLParseNode const* typePredicateNode) const | ||
| { | ||
| valueExp = nullptr; | ||
| if (!SQL_ISRULE(typePredicateNode, type_predicate)) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This method has no code path which returns in |
||
| return SUCCESS; | ||
|
|
||
| OSQLParseNode const* type_list = typePredicateNode->getChild(1/*type_list*/); | ||
| if (type_list->count() != 1) | ||
| return SUCCESS; // a comma-separated type list is always a genuine type predicate | ||
|
|
||
| OSQLParseNode const* type_list_item = type_list->getChild(0); | ||
| OSQLParseNode const* opt_only = type_list_item->getChild(0/*opt_only*/); | ||
| if (opt_only->count() != 0) | ||
| return SUCCESS; // an ONLY/ALL prefix is always a genuine type predicate | ||
|
|
||
| OSQLParseNode const* tableNode = type_list_item->getChild(1/*table_node*/); | ||
| if (!SQL_ISRULE(tableNode, table_node)) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the |
||
| return SUCCESS; | ||
|
|
||
| OSQLParseNode const* nameNode = tableNode->getChild(0); | ||
| PropertyPath propertyPath; | ||
| if (SQL_ISRULE(nameNode, qualified_class_name)) | ||
| { | ||
| // The 'schema:class' (colon) form is class-only syntax and is never a property path. | ||
| if (!nameNode->getChild(1/*'.' or ':'*/)->getTokenValue().Equals(".")) | ||
| return SUCCESS; | ||
|
|
||
| Utf8StringCR schemaNameOrAlias = nameNode->getChild(0)->getTokenValue(); | ||
| OSQLParseNode const* classNameNode = nameNode->getChild(2/*class_name*/); | ||
| Utf8StringCR propertyName = classNameNode->getChild(0)->getTokenValue(); | ||
| if (schemaNameOrAlias.empty() || propertyName.empty()) | ||
| return SUCCESS; | ||
|
|
||
| // If the name resolves to a class, keep the '(ClassName)' type-predicate meaning. | ||
| if (m_context->GetECDb().Schemas().GetClass(schemaNameOrAlias, propertyName, SchemaLookupMode::AutoDetect) != nullptr) | ||
| return SUCCESS; | ||
|
|
||
| propertyPath.Push(schemaNameOrAlias); | ||
| propertyPath.Push(propertyName); | ||
| } | ||
| else if (SQL_ISRULE(nameNode, tablespace_qualified_class_name)) | ||
| { | ||
| Utf8StringCR tableSpaceName = nameNode->getChild(0)->getTokenValue(); | ||
| OSQLParseNode const* qualifiedNameNode = nameNode->getChild(2/*qualified_class_name*/); | ||
| if (!SQL_ISRULE(qualifiedNameNode, qualified_class_name)) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here also same, if |
||
| return SUCCESS; | ||
|
|
||
| // The 'tablespace.schema:class' (colon) form is class-only syntax and is never a property path. | ||
| if (!qualifiedNameNode->getChild(1/*'.' or ':'*/)->getTokenValue().Equals(".")) | ||
| return SUCCESS; | ||
|
|
||
| Utf8StringCR schemaNameOrAlias = qualifiedNameNode->getChild(0)->getTokenValue(); | ||
| OSQLParseNode const* classNameNode = qualifiedNameNode->getChild(2/*class_name*/); | ||
| Utf8StringCR classNameOrPropertyName = classNameNode->getChild(0)->getTokenValue(); | ||
| if (tableSpaceName.empty() || schemaNameOrAlias.empty() || classNameOrPropertyName.empty()) | ||
| return SUCCESS; | ||
|
|
||
| // If the name resolves to a tablespace-qualified class, keep the '(ClassName)' type-predicate meaning. | ||
| if (m_context->GetECDb().Schemas().GetClass(schemaNameOrAlias, classNameOrPropertyName, SchemaLookupMode::AutoDetect, tableSpaceName.c_str()) != nullptr) | ||
| return SUCCESS; | ||
|
|
||
| propertyPath.Push(tableSpaceName); | ||
| propertyPath.Push(schemaNameOrAlias); | ||
| propertyPath.Push(classNameOrPropertyName); | ||
| } | ||
| else | ||
| return SUCCESS; | ||
|
|
||
| if (propertyPath.Size() == 3) | ||
| { | ||
| ECEnumerationCP ecEnum = m_context->GetECDb().Schemas().GetEnumeration(propertyPath[0].GetName(), propertyPath[1].GetName(), SchemaLookupMode::AutoDetect); | ||
| if (ecEnum != nullptr) | ||
| { | ||
| ECEnumeratorCP enumerator = ecEnum->FindEnumeratorByName(propertyPath[2].GetName().c_str()); | ||
| if (enumerator != nullptr) | ||
| { | ||
| valueExp = std::make_unique<EnumValueExp>(*enumerator, propertyPath); | ||
| return SUCCESS; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| valueExp = std::make_unique<PropertyNameExp>(propertyPath); | ||
| return SUCCESS; | ||
| } | ||
| //----------------------------------------------------------------------------------------- | ||
| // @bsimethod | ||
| //+---------------+---------------+---------------+---------------+---------------+------ | ||
| BentleyStatus ECSqlParser::ParseValueExp(std::unique_ptr<ValueExp>& valueExp, OSQLParseNode const* parseNode) const | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please narrow the fallback wording to dot-qualified names.
TryParseParenthesizedNameAsValueExpexplicitly excludes theschema:classandtablespace.schema:classcolon forms; an unresolved colon-form name remains a type predicate and fails class resolution instead of becoming a value expression. As written, "a parenthesized name that does not resolve to a class" overstates the behavior. Please say dot-qualified (for examplealias.prop) and mirror the constraint in the linked core docs.