Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion iModelCore/ECDb/CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,15 @@ This document including important changes to syntax or file format.
| Module | Version |
| ------- | --------- |
| Profile | `4.0.0.5` |
| ECSQL | `2.0.3.2` |
| ECSQL | `2.0.4.0` |

## ## `06/23/2026`: Added `IS` / `IS NOT` operator between operands
* ECSql version change `2.0.3.2` -> `2.0.4.0`.
* The `IS` and `IS NOT` operators can now be used between two operands, e.g. `prop1 IS [NOT] prop2`. Each operand may be any value expression — a property, the `NULL` literal, a constant, a parameter, a function call, an arithmetic expression, etc. These map to SQLite's null-safe comparison operators (`NULL IS NULL` is true, `1 IS NULL` is false).
* Previously `IS` / `IS NOT` only supported the right-hand operands `NULL`, the boolean literals `TRUE`/`FALSE`/`UNKNOWN`, and the class type predicate `IS (ClassName)`. Those forms are unchanged: a right-hand operand that is exactly `NULL`/`TRUE`/`FALSE`/`UNKNOWN`, or a parenthesized `(ClassName)`, keeps its original meaning. (Write a parenthesized value expression without the outer parentheses, e.g. `x IS y + 1` rather than `x IS (y + 1)`, when it could be mistaken for a type predicate.)
Comment thread
aruniverse marked this conversation as resolved.
Outdated
* For multi-column operands (e.g. `Point2d`/`Point3d` and navigation properties), the comparison is expanded column-wise: `IS` joins the per-column comparisons with `AND`, `IS NOT` joins them with `OR` (consistent with `=` and `<>`).
* Example: `SELECT * FROM bis.Element WHERE CodeValue IS NOT UserLabel`
* Example: `SELECT * FROM bis.Element WHERE CodeValue IS json_extract(JsonProperties, '$.code')`

## ## `04/24/2026`: Allow optional ON clause with CROSS JOIN
* ECSql version change `2.0.3.1` -> `2.0.3.2`.
Expand Down
43 changes: 36 additions & 7 deletions iModelCore/ECDb/ECDb/ECSql/ECSqlParser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2434,20 +2434,49 @@ 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*/);

// 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 = 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 (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))
Comment thread
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:
Expand Down
1 change: 0 additions & 1 deletion iModelCore/ECDb/ECDb/ECSql/ECSqlParser.h
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,6 @@ struct ECSqlParser final
BentleyStatus ParseTableNodeWithOptMemberCall(std::unique_ptr<ClassNameExp>&, connectivity::OSQLParseNode const&, ECSqlType, PolymorphicInfo polymorphic, bool disqualifyPrimaryJoin) const;
BentleyStatus ParseTableRef(std::unique_ptr<ClassRefExp>&, connectivity::OSQLParseNode const*, ECSqlType ecsqlType) const;
BentleyStatus ParseTerm(std::unique_ptr<ValueExp>&, connectivity::OSQLParseNode const*) const;
BentleyStatus ParseTruthValue(std::unique_ptr<ValueExp>& exp, connectivity::OSQLParseNode const* node) const { return ParseValueExp(exp, node); }

BentleyStatus ParseSearchCondition(std::unique_ptr<BooleanExp>&, connectivity::OSQLParseNode const*) const;
BentleyStatus ParseSetFct(std::unique_ptr<ValueExp>&, connectivity::OSQLParseNode const&, Utf8StringCR functionName, bool isStandardSetFunction) const;
Expand Down
Loading