From 111be91f64952a2adad9f90f592c3cc44650871c Mon Sep 17 00:00:00 2001 From: Ryan Blue Date: Mon, 1 Jun 2026 16:58:37 -0700 Subject: [PATCH 1/7] Spec: Add spec for expressions. --- format/expressions-spec.md | 284 +++++++++++++++++++++++++++++++++++++ 1 file changed, 284 insertions(+) create mode 100644 format/expressions-spec.md diff --git a/format/expressions-spec.md b/format/expressions-spec.md new file mode 100644 index 000000000000..08a258731d2b --- /dev/null +++ b/format/expressions-spec.md @@ -0,0 +1,284 @@ +--- +title: "Expressions Spec" +--- + + +# Iceberg Expressions + +This document defines the structure and behavior of expressions for use in Iceberg specifications. The purpose is to define a common structure that enables simple expressions to be stored and exchanged. + +Stored expressions are needed for use cases like data validations (`CHECK` constraints) and default values (for instance, `current_timestamp()`). Expressions are exchanged in use cases like server-side scan planning in the catalog protocol. + + +## Overview + +The goal of this specification is to define a simple expression structure and avoid complexity. + +To remain simple, the expressions that can be represented are deliberately constrained. Value expressions are constants, field references, or function calls with value expression arguments. Predicates are comparisons of value expressions that produce true or false. + +This approach is intended to keep focus on the logical structure of expressions. Complexity is pushed to the functions that are called, which can be a limited set of well-defined and portable functions (like Iceberg partition transforms) or could be user-defined functions that can use the full range of SQL capabilities. Multi-dialect UDFs are responsible for any SQL constructs that are specific to an engine, rather than importing and duplicating dialects in Iceberg expressions. + +This is consistent with Iceberg's conservative approach in other specs. Expressions and predicates are an important part of Iceberg implementation APIs, but have been deliberately limited in specifications. For example, sort orders and partition fields are strictly limited to a small set of transforms over well-defined inputs (source field IDs). This spec is widening what can be expressed, but depends on function calls for complex tasks. + +This specification covers the structure of Iceberg expressions and includes appendicies that specify serialization as JSON and a set of portable functions defined by Iceberg specifications. + + +## Structure + +Iceberg expressions have two types: + +* **Value expressions** represent data values and transformations of values (function calls) that produce any Iceberg type +* **Predicates** represent comparisons of value expressions and boolean logic that produce `true` or `false` + + +### Value expressions + +A value expression is an expression that produces a typed value + +Value expressions can be one of three types: a constant value, a field reference, or a function applied to zero or more value expressions. + + +#### Constant values + +A constant or literal is the simplest type of value expression that represents a specific typed value. + + +#### Field reference + +A field reference represents the value of a specific field in a row. When an expression is evaluated on a row, it returns the value of the field. + +Field references may be named references (unbound) or ID references (bound). ID references identify a field by field ID from a schema. Named references identify a field by name that must be resolved to an ID (bound to a schema) to access the field. + +ID references are used for stored expressions, where the identity of the column is determined when the stored expression is created. For example, column constraints are tied to field ID so that renaming a column does not drop its stored constraint. + +Named references are used when the identity of the column is determined when the expression is evaluated. For example, query filters are resolved each time a query runs so servers-side planning uses unbound named references. + +The context in which an expression is used determines the type of references that are valid. Iceberg specifications should document whether ID references, named references, or both are allowed. + + +#### Apply function + +An apply expression represents the result of a function applied to (or called on) zero or more values produced by child value expressions. + +Functions are identified by catalog, namespace, and name. + +* Function name is always required +* Namespace is optional and is assumed to be empty ([]) if it is not present or is null +* Catalog is optional and is assumed to be the catalog in which the referencing object is stored if it is not present or is null + +The catalog name is used to identify the catalog where the function definition can be loaded or it identifies a reserved function set. As in the view and UDF specs, catalog names represent connection configurations that may differ across environments. Omitting catalog names is recommended to avoid depending on consistent environments. For example, if a table has a CHECK constraint that references a UDF without a catalog name (missing or null), the UDF should be loaded from the table’s catalog. + +Reserved function set names are: + +* `sql_functions` is used for functions defined by the SQL standard +* `iceberg_functions` is used for functions defined in this specification + +Engines may document and use a catalog name to identify their built-in functions that are not part of the SQL spec, like `spark_builtin_functions.to_utc_timestamp`. + +Producers are responsible for resolving catalog, namespace, and name if the environment is relevant. For example, if a SQL engine uses its current catalog and namespace to find a function, the resolved catalog and namespace must be used to produce an unambiguous function identifier. + + +#### Value expression types + +The type produced by a value expression may change. For example, an ID reference may produce a widened type after the underlying column's type is promoted. + +Function calls may produce different types when function definitions change, and type changes may change the definition that is resolved for a function name. For example, `identity(int) -> int` will change to `identity(long) -> long` when an input field is promoted from `int` to `long`. + +A value expression's type is determined when it is bound to a specific input schema. + +If types are incompatible at runtime, implementations binding or evaluating expressions may apply type promotion to align types for predicates and to resolve functions. Implementations may choose when to promote values to accomodate engines that differ in casting behavior. However, implementations must fail rather than insert "unsafe" casts. + + +### Predicates + +A predicate is a boolean expression that produces true or false. + +Predicates can be constants (true or false), comparisons or tests of value expressions, or logical combinations of predicates (AND, OR, NOT). + +If value expression types in a predicate are incompatible, implementations should align types using type promotion. For instance, `int_col > 5.0` should promote int values to float. If the types cannot be aligned according to type promotion rules, the predicate must evaluate to false. For instance, `"goats" > -Infinity` should always be `false`. + +Value expressions are not valid predicates, even when the expression is expected to return a boolean value. Value expressions must be compared or tested to produce a predicate. For example, `is_empty("")` is not a valid predicate, but `is_empty("") = true` is a valid predicate. + + +#### Comparisons + +Comparisons are predicates that compare two value expressions with the same primitive type. Comparisons are: + +| Comparison | Description | +|-------------|-------------| +| `=` | Is equal | +| `!=` | Is not equal | +| `<` | Less than | +| `<=` | Less than or equal | +| `>` | Greater than | +| `>=` | Greater than or equal | + +Primitive types are compared using natural order, except for the following types: + +* `false` is less than `true` for `boolean` +* `fixed` and `binary` use unsigned byte-wise comparison +* `string` uses unsigned byte-wise comparison of the UTF-8 representation +* `uuid` uses unsigned byte-wise comparison of the UUID bytes +* `float` and `double` use IEEE 754 total order after normalizing NaN to the canonical NaN (sign bit 0, exponent bits all 1, matissa msb 1 followed by all 0) + * `NaN = NaN` is true for any two NaN values + * `val < NaN` is true for all non-NaN values + +Note type alignment produces `decimal` values with the same scale so that comparison is equivalent to the natural order of the unscaled numeric value. + +Tests are predicates that test a single value expression, optionally using a constant or set of constants. Constants must have the same type and must be non-null. Tests are: + +| Test | Allowed types | Constant type | Description | +|-------------------------|---------------|---------------|-------------| +| `IS NULL` | any | | true iff the value is null | +| `IS NOT NULL` | any | | true iff the value is not null | +| `IS NaN` | float, double | | true iff the value is an IEEE 754 NaN | +| `IS NOT NaN` | float, double | | true iff the value is not an IEEE 754 NaN | +| `STARTS WITH const` | string | string | true iff the constant is a prefix of the value | +| `NOT STARTS WITH const` | string | string | true iff the constant is not a prefix of the value | +| `IN (constant set)` | any | same as value | true iff the value is equal to any constant | +| `NOT IN (constant set)` | any | same as value | true iff the value is not equal to all constants | + + +#### Boolean logic + +Predicates must use 2-valued boolean logic. Evaluation of all predicates must produce `true` or `false`. + +Engines that implement SQL 3-valued boolean logic must add `IS NULL` and `NOT NULL` to produce the 2-valued equivalent. This avoids bugs in engines and languages that do not natively implement 3-valued logic. For example, the SQL predicate `x < 10` should be passed as `x < 10 AND x IS NOT NULL` for a SQL `WHERE` condition (or `x < 10`; see null-safe comparisons below). For a `CHECK` constraint, the expression is passed as `x < 10 OR x IS NULL`. This ensures that implementations will make the correct determination, rather than depending depending on context to interpret a null result (`WHERE` vs `CHECK`). + +Logical combinations are boolean operators applied to predicates. `AND` and `OR` are binary operations and `NOT` is a unary operation. + +Comparisons must be null-safe. For example: + +* `null = null` is `true` +* `34 = null` is `false` +* `null != null` is `false` +* `34 != null` is `true` +* `null < null` is `false` +* `null <= null` is `true` +* `34 < null` is `false` + +Comparisons must handle null values when value expressions evaluate to null. However, value expressions used to define a predicate should not directly contain null constants and may reject them. For example, `x = get_item(map, "key")` is valid although `get_item` may return a null value, but `x = null` should be rejected because `x IS NULL` is the recommended unambiguous predicate. + + +### Compatibility with REST catalog expressions + +Older clients use more restrictive forms of predicates and references that used a "term" for specific transforms and named references. These expressions should be supported for backward compatibility to allow older clients to interact with newer REST catalog services. + +Prior to this spec, deprecated expressions were passed in the REST API in 3 places: + +* As `filter` passed to server-side scan planning +* As `filter` passed to the service in `ScanReport` +* As `residual` passed to the client with a scan task + +Both server-side scan planning and the report endpoint can continue to accept filters from older clients without issues by parsing term-based expressions (see [Appendix B: JSON serialization](#appendix-b-json-serialization)). + +Residuals passed from services back to clients that do not use the new syntax would cause clients to fail, but services are allowed to omit the residual so that it is calculated on the client side (intended to avoid duplicating large IN filters). For compatibility, REST services should detect client versions and produce deprecated predicates, or omit residuals from tasks. + + +## Appendix A: Iceberg functions + +This section defines the functions in the `iceberg_functions` reserved catalog name. + +* `if_else(condition: predicate, when_true: T, when_false: T) -> T`: returns the value of `when_true` when `condition` is true and `when_false` otherwise + +### Partition transforms + +Iceberg partition transforms are also defined as functions (other than `void`). + +All partition transforms produce `null` for a `null` input value. + +| Function name | Description | Source types | Result type | +|-------------------|--------------------------------------------------------------|----------------------------------------------------------------------|-------------| +| `identity(value)` | Source value, unmodified | Any primitive except for `geometry`, `geography`, and `variant` | Source type | +| `year(value)` | Extract a date or timestamp year, as years from 1970 | `date`, `timestamp`, `timestamptz`, `timestamp_ns`, `timestamptz_ns` | `int` | +| `month(value)` | Extract a date or timestamp month, as months from 1970-01-01 | `date`, `timestamp`, `timestamptz`, `timestamp_ns`, `timestamptz_ns` | `int` | +| `day(value)` | Extract a date or timestamp day, as days from 1970-01-01 | `date`, `timestamp`, `timestamptz`, `timestamp_ns`, `timestamptz_ns` | `date` | +| `hour(value)` | Extract a timestamp hour, as hours from 1970-01-01 00:00:00 | `timestamp`, `timestamptz`, `timestamp_ns`, `timestamptz_ns` | `int` | + +Note that `year`, `month`, and `hour` transforms produce ordinal values and not human-readable values. For example, `year(2018-05-13)` produces `48`, not `2018`. + +Parameterized functions are called as 2-argument functions. The first argument is an `int` parameter (`N` or `W` from the table spec) and the second argument is the value to transform. For example, `bucket(256, id)` calls `bucket[256]`. + +| Parameterized function name | Description | Source types | Result type | +|-----------------------------|-----------------------------------------------|----------------------------------------------------------------------------------------------|-------------| +| `bucket(N, value)` | Hash of value, mod `N` (see table spec) | Any primitive except for `geometry`, `geography`, `variant`, `boolean`, `float`, or `double` | `int` | +| `truncate(W, value)` | Value truncated to width `W` (see table spec) | `int`, `long`, `decimal`, `string`, `binary` | Source type | + + +## Appendix B: JSON serialization + +Iceberg expressions are serialized as JSON objects in table, view, and UDF metadata, and in the REST protocol for catalogs. + +### Value expressions + +``` +EXPR: LITERAL | REFERENCE | APPLY + +LITERAL: VALUE + | { "type": "literal", "value": VALUE } + | { "type": "literal", "value": VALUE, "data-type": DATA_TYPE } +LITERALS: [ LITERAL* ] + +REFERENCE: BOUND_REF | UNBOUND_REF +BOUND_REF: ID | { "type": "reference", "id": ID } +UNBOUND_REF: NAME | { "type": "reference", "name": NAME } + +APPLY: { "type": "apply", "func-name": FUNC_ID, "arguments": [ EXPR* ] } +FUNC_ID: NAME + | { "catalog": NAME, "namespace": [ NAME* ], "name": NAME } + +ID: integer +NAME: string + +VALUE: non-null single value JSON from the table spec +DATA_TYPE: Iceberg type from the spec +``` + +If a function identifier is a string, that string is the function name, the namespace is empty ([]), and the catalog is missing/null. + +### Predicates + +``` +PREDICATE: true | false + | { "type": "not", "child": PREDICATE } + | { "type": BINARY_OP, "left": PREDICATE, "right": PREDICATE } + | { "type": UNARY_OP, "child": EXPR } + | { "type": CMP_OP, "left": EXPR, "right": EXPR } + | { "type": SET_OP, "child": EXPR, "values": LITERALS } + | DEPRECATED_PREDICATE + +BINARY_OP: "and" | "or" +UNARY_OP: "is-null" | "not-null" | "is-nan" | "not-nan" +CMP_OP: "lt" | "lt-eq" | "gt" | "gt-eq" | "eq" | "not-eq" + | "starts-with" | "not-starts-with" +SET_OP: "in" | "not-in" +``` + +### Backward compatibility + +``` +DEPRECATED_PREDICATE: + | { "type": UNARY_OP, "term": TERM } + | { "type": CMP_OP, "term": TERM, "value": LITERAL } + | { "type": SET_OP, "term": TERM, "values": LITERALS } + +DEPRECATED_REF: { "type": "reference", "term": NAME } + +TERM: NAME | DEPRECATED_REF | TRANSFORM +TRANSFORM: { "type": "transform", "transform": NAME, "term": TERM } +``` From 46d65dd2b387d75628a31765f6d3cf8f0c77ea25 Mon Sep 17 00:00:00 2001 From: Ryan Blue Date: Thu, 11 Jun 2026 17:02:25 -0700 Subject: [PATCH 2/7] Update from review feedback. --- format/expressions-spec.md | 44 ++++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/format/expressions-spec.md b/format/expressions-spec.md index 08a258731d2b..6d0ba362266f 100644 --- a/format/expressions-spec.md +++ b/format/expressions-spec.md @@ -29,26 +29,28 @@ Stored expressions are needed for use cases like data validations (`CHECK` const The goal of this specification is to define a simple expression structure and avoid complexity. -To remain simple, the expressions that can be represented are deliberately constrained. Value expressions are constants, field references, or function calls with value expression arguments. Predicates are comparisons of value expressions that produce true or false. +To remain simple, the expressions that can be represented are deliberately constrained to value expressions (constants, references, and function calls) and predicates (comparisons that produce true or false). -This approach is intended to keep focus on the logical structure of expressions. Complexity is pushed to the functions that are called, which can be a limited set of well-defined and portable functions (like Iceberg partition transforms) or could be user-defined functions that can use the full range of SQL capabilities. Multi-dialect UDFs are responsible for any SQL constructs that are specific to an engine, rather than importing and duplicating dialects in Iceberg expressions. +This approach is intended to keep focus on the logical structure of expressions. Complexity is pushed to the functions that are called, which are a limited set of well-defined and portable functions (like Iceberg partition transforms) or [user-defined functions][udf-spec] that can use the full range of SQL capabilities. Multi-dialect UDFs are responsible for any SQL constructs that are specific to an engine, rather than importing and duplicating dialects in Iceberg expressions. This is consistent with Iceberg's conservative approach in other specs. Expressions and predicates are an important part of Iceberg implementation APIs, but have been deliberately limited in specifications. For example, sort orders and partition fields are strictly limited to a small set of transforms over well-defined inputs (source field IDs). This spec is widening what can be expressed, but depends on function calls for complex tasks. This specification covers the structure of Iceberg expressions and includes appendicies that specify serialization as JSON and a set of portable functions defined by Iceberg specifications. +[udf-spec]: https://iceberg.apache.org/udf-spec + ## Structure Iceberg expressions have two types: * **Value expressions** represent data values and transformations of values (function calls) that produce any Iceberg type -* **Predicates** represent comparisons of value expressions and boolean logic that produce `true` or `false` +* **Predicates** represent comparisons of value expressions as well as combinations of predicates with boolean logic (and, or, not) ### Value expressions -A value expression is an expression that produces a typed value +A value expression is an expression that produces a typed value. Value expressions can be one of three types: a constant value, a field reference, or a function applied to zero or more value expressions. @@ -64,7 +66,7 @@ A field reference represents the value of a specific field in a row. When an exp Field references may be named references (unbound) or ID references (bound). ID references identify a field by field ID from a schema. Named references identify a field by name that must be resolved to an ID (bound to a schema) to access the field. -ID references are used for stored expressions, where the identity of the column is determined when the stored expression is created. For example, column constraints are tied to field ID so that renaming a column does not drop its stored constraint. +ID references are used for stored expressions, where the identity of the column is determined when the stored expression is created. For example, column constraints are tied to field IDs so that renaming a column does not invalidate the reference in its stored constraint. Named references are used when the identity of the column is determined when the expression is evaluated. For example, query filters are resolved each time a query runs so servers-side planning uses unbound named references. @@ -81,27 +83,27 @@ Functions are identified by catalog, namespace, and name. * Namespace is optional and is assumed to be empty ([]) if it is not present or is null * Catalog is optional and is assumed to be the catalog in which the referencing object is stored if it is not present or is null -The catalog name is used to identify the catalog where the function definition can be loaded or it identifies a reserved function set. As in the view and UDF specs, catalog names represent connection configurations that may differ across environments. Omitting catalog names is recommended to avoid depending on consistent environments. For example, if a table has a CHECK constraint that references a UDF without a catalog name (missing or null), the UDF should be loaded from the table’s catalog. +The catalog name identifies the catalog where the function definition can be loaded or is a reserved name that identifies a set of functions. As in the view and UDF specs, catalog names represent connection configurations that may differ across environments. Omitting catalog names is recommended to avoid depending on consistent environments. For example, if a table has a CHECK constraint that references a UDF without a catalog name (missing or null), the UDF should be loaded from the table’s catalog. -Reserved function set names are: +The reserved names used to identify sets are: * `sql_functions` is used for functions defined by the SQL standard * `iceberg_functions` is used for functions defined in this specification Engines may document and use a catalog name to identify their built-in functions that are not part of the SQL spec, like `spark_builtin_functions.to_utc_timestamp`. -Producers are responsible for resolving catalog, namespace, and name if the environment is relevant. For example, if a SQL engine uses its current catalog and namespace to find a function, the resolved catalog and namespace must be used to produce an unambiguous function identifier. +Function identifiers are unambiguous and are not interpreted using session context. Producers are responsible for resolving catalog, namespace, and name if the session is relevant. For example, if a SQL engine uses its current catalog and namespace to find a function, the resolved catalog and namespace must be used to produce an unambiguous function identifier. #### Value expression types The type produced by a value expression may change. For example, an ID reference may produce a widened type after the underlying column's type is promoted. -Function calls may produce different types when function definitions change, and type changes may change the definition that is resolved for a function name. For example, `identity(int) -> int` will change to `identity(long) -> long` when an input field is promoted from `int` to `long`. +A value expression's result type is determined when it is bound to a specific input schema. -A value expression's type is determined when it is bound to a specific input schema. +Function calls may produce different types when function definitions change, and type changes may change the definition that is resolved for a function name. For example, if the input field passed to `identity(int) -> int` is promoted from `int` to `long`, the resolved `identity` function can change to `identity(long) -> long` if it is defined. -If types are incompatible at runtime, implementations binding or evaluating expressions may apply type promotion to align types for predicates and to resolve functions. Implementations may choose when to promote values to accomodate engines that differ in casting behavior. However, implementations must fail rather than insert "unsafe" casts. +If types are incompatible at runtime, implementations binding or evaluating expressions may apply type promotion to align types for predicates and to resolve functions. Implementations may choose when to promote values to accomodate engines that differ in casting behavior. However, implementations must fail rather than insert unsafe casts. ### Predicates @@ -110,9 +112,9 @@ A predicate is a boolean expression that produces true or false. Predicates can be constants (true or false), comparisons or tests of value expressions, or logical combinations of predicates (AND, OR, NOT). -If value expression types in a predicate are incompatible, implementations should align types using type promotion. For instance, `int_col > 5.0` should promote int values to float. If the types cannot be aligned according to type promotion rules, the predicate must evaluate to false. For instance, `"goats" > -Infinity` should always be `false`. +If value expression types in a predicate are incompatible, implementations should align types using type promotion. For instance, `int_col > 5.0` should promote int values to float. If the types cannot be aligned according to type promotion rules (for instance, `"goats" > -Infinity`), the predicate cannot be evaluated and implementatinos must fail. -Value expressions are not valid predicates, even when the expression is expected to return a boolean value. Value expressions must be compared or tested to produce a predicate. For example, `is_empty("")` is not a valid predicate, but `is_empty("") = true` is a valid predicate. +Value expressions are not valid predicates, even when the expression is expected to return a boolean value. Value expressions must be compared or tested to produce a predicate. For example, `is_empty(str_col)` is not a valid predicate because it may produce `null`, but `is_empty(str_col) = true` is a valid predicate. #### Comparisons @@ -128,13 +130,13 @@ Comparisons are predicates that compare two value expressions with the same prim | `>` | Greater than | | `>=` | Greater than or equal | -Primitive types are compared using natural order, except for the following types: +Primitive types are compared using signed comparison, except for the following types: * `false` is less than `true` for `boolean` * `fixed` and `binary` use unsigned byte-wise comparison -* `string` uses unsigned byte-wise comparison of the UTF-8 representation +* `string` uses unsigned byte-wise comparison of the UTF-8 representation; it is not the Unicode Collation Algorithm * `uuid` uses unsigned byte-wise comparison of the UUID bytes -* `float` and `double` use IEEE 754 total order after normalizing NaN to the canonical NaN (sign bit 0, exponent bits all 1, matissa msb 1 followed by all 0) +* `float` and `double` use IEEE 754 total order after normalizing NaN to the canonical NaN (sign bit 0, exponent bits all 1, mantissa msb 1 followed by all 0) * `NaN = NaN` is true for any two NaN values * `val < NaN` is true for all non-NaN values @@ -150,15 +152,15 @@ Tests are predicates that test a single value expression, optionally using a con | `IS NOT NaN` | float, double | | true iff the value is not an IEEE 754 NaN | | `STARTS WITH const` | string | string | true iff the constant is a prefix of the value | | `NOT STARTS WITH const` | string | string | true iff the constant is not a prefix of the value | -| `IN (constant set)` | any | same as value | true iff the value is equal to any constant | -| `NOT IN (constant set)` | any | same as value | true iff the value is not equal to all constants | +| `IN (constant set)` | any primitive | same as value | true iff the value is equal to any constant | +| `NOT IN (constant set)` | any primitive | same as value | true iff the value is not equal to all constants | #### Boolean logic Predicates must use 2-valued boolean logic. Evaluation of all predicates must produce `true` or `false`. -Engines that implement SQL 3-valued boolean logic must add `IS NULL` and `NOT NULL` to produce the 2-valued equivalent. This avoids bugs in engines and languages that do not natively implement 3-valued logic. For example, the SQL predicate `x < 10` should be passed as `x < 10 AND x IS NOT NULL` for a SQL `WHERE` condition (or `x < 10`; see null-safe comparisons below). For a `CHECK` constraint, the expression is passed as `x < 10 OR x IS NULL`. This ensures that implementations will make the correct determination, rather than depending depending on context to interpret a null result (`WHERE` vs `CHECK`). +Engines that implement SQL 3-valued boolean logic must add `IS NULL` and `NOT NULL` to produce the 2-valued equivalent. This avoids bugs in engines and languages that do not natively implement 3-valued logic. For example, the SQL predicate `x < 10` should be passed as `x < 10 AND x IS NOT NULL` for a SQL `WHERE` condition (or `x < 10`; see null-safe comparisons below). For a `CHECK` constraint, the expression is passed as `x < 10 OR x IS NULL`. This ensures that implementations will make the correct determination, rather than depending on context to interpret a null result (`WHERE` vs `CHECK`). Logical combinations are boolean operators applied to predicates. `AND` and `OR` are binary operations and `NOT` is a unary operation. @@ -172,7 +174,7 @@ Comparisons must be null-safe. For example: * `null <= null` is `true` * `34 < null` is `false` -Comparisons must handle null values when value expressions evaluate to null. However, value expressions used to define a predicate should not directly contain null constants and may reject them. For example, `x = get_item(map, "key")` is valid although `get_item` may return a null value, but `x = null` should be rejected because `x IS NULL` is the recommended unambiguous predicate. +Comparisons must handle null values when value expressions evaluate to null. However, value expressions that are the direct child of a comparison must not be a null constant. For example, `x = get_item(map, "key")` is valid although `get_item` may return a null value, but `x = null` must be rejected because `x IS NULL` is the correct unambiguous predicate. ### Compatibility with REST catalog expressions @@ -212,7 +214,7 @@ All partition transforms produce `null` for a `null` input value. Note that `year`, `month`, and `hour` transforms produce ordinal values and not human-readable values. For example, `year(2018-05-13)` produces `48`, not `2018`. -Parameterized functions are called as 2-argument functions. The first argument is an `int` parameter (`N` or `W` from the table spec) and the second argument is the value to transform. For example, `bucket(256, id)` calls `bucket[256]`. +`bucket` and `truncate` are called as 2-argument functions. The first argument is an `int` parameter (`N` or `W` from the table spec) and the second argument is the value to transform. For example, `bucket(256, id)` calls `bucket[256]`. | Parameterized function name | Description | Source types | Result type | |-----------------------------|-----------------------------------------------|----------------------------------------------------------------------------------------------|-------------| From c35cd42e450711682f30194f849ef2dc83068bb1 Mon Sep 17 00:00:00 2001 From: Ryan Blue Date: Fri, 12 Jun 2026 10:39:40 -0700 Subject: [PATCH 3/7] More updates from reviews. --- format/expressions-spec.md | 56 +++++++++++++++++++++++--------------- 1 file changed, 34 insertions(+), 22 deletions(-) diff --git a/format/expressions-spec.md b/format/expressions-spec.md index 6d0ba362266f..4dc061a1bb11 100644 --- a/format/expressions-spec.md +++ b/format/expressions-spec.md @@ -77,10 +77,9 @@ The context in which an expression is used determines the type of references tha An apply expression represents the result of a function applied to (or called on) zero or more values produced by child value expressions. -Functions are identified by catalog, namespace, and name. +Functions are referenced using a catalog and a function identifer (list of strings). -* Function name is always required -* Namespace is optional and is assumed to be empty ([]) if it is not present or is null +* The function identifier consists of 0 or more namespace names followed by the function name. At least one part, the function name, is required. * Catalog is optional and is assumed to be the catalog in which the referencing object is stored if it is not present or is null The catalog name identifies the catalog where the function definition can be loaded or is a reserved name that identifies a set of functions. As in the view and UDF specs, catalog names represent connection configurations that may differ across environments. Omitting catalog names is recommended to avoid depending on consistent environments. For example, if a table has a CHECK constraint that references a UDF without a catalog name (missing or null), the UDF should be loaded from the table’s catalog. @@ -92,7 +91,7 @@ The reserved names used to identify sets are: Engines may document and use a catalog name to identify their built-in functions that are not part of the SQL spec, like `spark_builtin_functions.to_utc_timestamp`. -Function identifiers are unambiguous and are not interpreted using session context. Producers are responsible for resolving catalog, namespace, and name if the session is relevant. For example, if a SQL engine uses its current catalog and namespace to find a function, the resolved catalog and namespace must be used to produce an unambiguous function identifier. +Function references are unambiguous and are not interpreted using session context. Producers are responsible for resolving catalog, namespace, and name if the session is relevant. For example, if a SQL engine uses its current catalog and namespace to find a function, the resolved catalog and namespace must be used to produce an unambiguous function reference. #### Value expression types @@ -164,32 +163,41 @@ Engines that implement SQL 3-valued boolean logic must add `IS NULL` and `NOT NU Logical combinations are boolean operators applied to predicates. `AND` and `OR` are binary operations and `NOT` is a unary operation. -Comparisons must be null-safe. For example: +Comparisons must be null-safe. For any two operands a and b: -* `null = null` is `true` -* `34 = null` is `false` -* `null != null` is `false` -* `34 != null` is `true` -* `null < null` is `false` -* `null <= null` is `true` -* `34 < null` is `false` +* `a = b` is true if both are null, or both are non-null and equal; otherwise false +* `a != b` is the boolean negation of a = b +* `a < b` and `a > b` are false when either operand is null; otherwise they use the order defined above +* `a <= b` is `(a = b) OR (a < b)`; `a >= b` is `(a = b) OR (a > b)`; both are true when both operands are null and false when only one operand is null + +This table shows examples of these rules after evaluating value expressions to constants: + +| Comparison | Result | +|----------------|---------| +| `null = null` | `true` | +| `34 = null` | `false` | +| `null != null` | `false` | +| `34 != null` | `true` | +| `null < null` | `false` | +| `null <= null` | `true` | +| `34 < null` | `false` | Comparisons must handle null values when value expressions evaluate to null. However, value expressions that are the direct child of a comparison must not be a null constant. For example, `x = get_item(map, "key")` is valid although `get_item` may return a null value, but `x = null` must be rejected because `x IS NULL` is the correct unambiguous predicate. ### Compatibility with REST catalog expressions -Older clients use more restrictive forms of predicates and references that used a "term" for specific transforms and named references. These expressions should be supported for backward compatibility to allow older clients to interact with newer REST catalog services. +Prior to this spec, REST APIs used a more restrictive, term-based form of predicates and references. Those forms are now deprecated, but should be supported for backward compatibility to allow older clients to interact with newer REST catalog services. -Prior to this spec, deprecated expressions were passed in the REST API in 3 places: +The deprecated expressions were passed in 3 places: * As `filter` passed to server-side scan planning * As `filter` passed to the service in `ScanReport` * As `residual` passed to the client with a scan task -Both server-side scan planning and the report endpoint can continue to accept filters from older clients without issues by parsing term-based expressions (see [Appendix B: JSON serialization](#appendix-b-json-serialization)). +Both server-side scan planning and the report endpoint should continue to accept filters from older clients by parsing term-based expressions (see [Appendix B: JSON serialization](#backward-compatibility)). -Residuals passed from services back to clients that do not use the new syntax would cause clients to fail, but services are allowed to omit the residual so that it is calculated on the client side (intended to avoid duplicating large IN filters). For compatibility, REST services should detect client versions and produce deprecated predicates, or omit residuals from tasks. +Residuals passed from services back to clients that do not use the new syntax would cause clients to fail. Services are allowed to omit the residual so that it is calculated on the client side (intended to avoid duplicating large IN filters). For compatibility, REST services should omit residuals from tasks, but may include them if the service detects support for newer predicates (for example, via client version). ## Appendix A: Iceberg functions @@ -237,12 +245,15 @@ LITERAL: VALUE LITERALS: [ LITERAL* ] REFERENCE: BOUND_REF | UNBOUND_REF -BOUND_REF: ID | { "type": "reference", "id": ID } -UNBOUND_REF: NAME | { "type": "reference", "name": NAME } +BOUND_REF: { "type": "reference", "id": ID } +UNBOUND_REF: { "type": "reference", "name": NAME } -APPLY: { "type": "apply", "func-name": FUNC_ID, "arguments": [ EXPR* ] } -FUNC_ID: NAME - | { "catalog": NAME, "namespace": [ NAME* ], "name": NAME } +APPLY: { "type": "apply", "function": FUNC_REF, "arguments": [ FUNC_ARG* ] } +FUNC_ARG: EXPR | PREDICATE +FUNC_REF: NAME + | [ NAME* ] + | { "identifier": [ NAME* ] } + | { "catalog": NAME, "identifier": [ NAME* ] } ID: integer NAME: string @@ -251,7 +262,8 @@ VALUE: non-null single value JSON from the table spec DATA_TYPE: Iceberg type from the spec ``` -If a function identifier is a string, that string is the function name, the namespace is empty ([]), and the catalog is missing/null. +If a function reference is a string, that string is the one-part identifier and the catalog is missing/null. +If a function reference is a list of strings, it is the function identifier and the catalog is missing/null. ### Predicates From d3573938117dba5ebf98bb266176ebad185a4d1d Mon Sep 17 00:00:00 2001 From: Ryan Blue Date: Fri, 12 Jun 2026 10:49:27 -0700 Subject: [PATCH 4/7] Add references. --- format/expressions-spec.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/format/expressions-spec.md b/format/expressions-spec.md index 4dc061a1bb11..47bd5a3f55ae 100644 --- a/format/expressions-spec.md +++ b/format/expressions-spec.md @@ -224,10 +224,13 @@ Note that `year`, `month`, and `hour` transforms produce ordinal values and not `bucket` and `truncate` are called as 2-argument functions. The first argument is an `int` parameter (`N` or `W` from the table spec) and the second argument is the value to transform. For example, `bucket(256, id)` calls `bucket[256]`. -| Parameterized function name | Description | Source types | Result type | -|-----------------------------|-----------------------------------------------|----------------------------------------------------------------------------------------------|-------------| -| `bucket(N, value)` | Hash of value, mod `N` (see table spec) | Any primitive except for `geometry`, `geography`, `variant`, `boolean`, `float`, or `double` | `int` | -| `truncate(W, value)` | Value truncated to width `W` (see table spec) | `int`, `long`, `decimal`, `string`, `binary` | Source type | +| Parameterized function name | Description | Source types | Result type | +|-----------------------------|-----------------------------------------------------------------------|----------------------------------------------------------------------------------------------|-------------| +| `bucket(N, value)` | Hash of value, mod `N` (see [table spec details][bucket-ref]) | Any primitive except for `geometry`, `geography`, `variant`, `boolean`, `float`, or `double` | `int` | +| `truncate(W, value)` | Value truncated to width `W` (see [table spec details][truncate-ref]) | `int`, `long`, `decimal`, `string`, `binary` | Source type | + +[bucket-ref]: spec/#bucket-transform-details +[truncate-ref]: spec/#truncate-transform-details ## Appendix B: JSON serialization From 4f73422850c35f68e44883680463f8b6e0fcc29d Mon Sep 17 00:00:00 2001 From: Ryan Blue Date: Wed, 24 Jun 2026 15:20:02 -0700 Subject: [PATCH 5/7] Update NaN handling and relocate some chunks to make more sense. --- format/expressions-spec.md | 77 +++++++++++++++++++++----------------- 1 file changed, 43 insertions(+), 34 deletions(-) diff --git a/format/expressions-spec.md b/format/expressions-spec.md index 47bd5a3f55ae..c4a37fe6d01d 100644 --- a/format/expressions-spec.md +++ b/format/expressions-spec.md @@ -109,39 +109,14 @@ If types are incompatible at runtime, implementations binding or evaluating expr A predicate is a boolean expression that produces true or false. -Predicates can be constants (true or false), comparisons or tests of value expressions, or logical combinations of predicates (AND, OR, NOT). - -If value expression types in a predicate are incompatible, implementations should align types using type promotion. For instance, `int_col > 5.0` should promote int values to float. If the types cannot be aligned according to type promotion rules (for instance, `"goats" > -Infinity`), the predicate cannot be evaluated and implementatinos must fail. +Predicates can be constants (true or false), tests of a value expression, comparisons of value exprssions, or logical combinations of predicates (AND, OR, NOT). Value expressions are not valid predicates, even when the expression is expected to return a boolean value. Value expressions must be compared or tested to produce a predicate. For example, `is_empty(str_col)` is not a valid predicate because it may produce `null`, but `is_empty(str_col) = true` is a valid predicate. -#### Comparisons - -Comparisons are predicates that compare two value expressions with the same primitive type. Comparisons are: +#### Tests -| Comparison | Description | -|-------------|-------------| -| `=` | Is equal | -| `!=` | Is not equal | -| `<` | Less than | -| `<=` | Less than or equal | -| `>` | Greater than | -| `>=` | Greater than or equal | - -Primitive types are compared using signed comparison, except for the following types: - -* `false` is less than `true` for `boolean` -* `fixed` and `binary` use unsigned byte-wise comparison -* `string` uses unsigned byte-wise comparison of the UTF-8 representation; it is not the Unicode Collation Algorithm -* `uuid` uses unsigned byte-wise comparison of the UUID bytes -* `float` and `double` use IEEE 754 total order after normalizing NaN to the canonical NaN (sign bit 0, exponent bits all 1, mantissa msb 1 followed by all 0) - * `NaN = NaN` is true for any two NaN values - * `val < NaN` is true for all non-NaN values - -Note type alignment produces `decimal` values with the same scale so that comparison is equivalent to the natural order of the unscaled numeric value. - -Tests are predicates that test a single value expression, optionally using a constant or set of constants. Constants must have the same type and must be non-null. Tests are: +Tests are predicates that test a single value expression, optionally using a constant or set of constants. Constants must have the same type and must be non-null and non-NaN. Tests are: | Test | Allowed types | Constant type | Description | |-------------------------|---------------|---------------|-------------| @@ -155,18 +130,27 @@ Tests are predicates that test a single value expression, optionally using a con | `NOT IN (constant set)` | any primitive | same as value | true iff the value is not equal to all constants | -#### Boolean logic +#### Comparisons -Predicates must use 2-valued boolean logic. Evaluation of all predicates must produce `true` or `false`. +Comparisons are predicates that compare two value expressions with the same primitive type. -Engines that implement SQL 3-valued boolean logic must add `IS NULL` and `NOT NULL` to produce the 2-valued equivalent. This avoids bugs in engines and languages that do not natively implement 3-valued logic. For example, the SQL predicate `x < 10` should be passed as `x < 10 AND x IS NOT NULL` for a SQL `WHERE` condition (or `x < 10`; see null-safe comparisons below). For a `CHECK` constraint, the expression is passed as `x < 10 OR x IS NULL`. This ensures that implementations will make the correct determination, rather than depending on context to interpret a null result (`WHERE` vs `CHECK`). +If value expression types in a comparison are incompatible, implementations should align types using type promotion. For instance, `int_col > 5.0` should promote int values to float. If the types cannot be aligned according to type promotion rules (for instance, `"goats" > -Infinity`), the predicate cannot be evaluated and implementations must fail. + +Comparisons are: -Logical combinations are boolean operators applied to predicates. `AND` and `OR` are binary operations and `NOT` is a unary operation. +| Comparison | Description | +|-------------|-------------| +| `=` | Is equal | +| `!=` | Is not equal | +| `<` | Less than | +| `<=` | Less than or equal | +| `>` | Greater than | +| `>=` | Greater than or equal | Comparisons must be null-safe. For any two operands a and b: * `a = b` is true if both are null, or both are non-null and equal; otherwise false -* `a != b` is the boolean negation of a = b +* `a != b` is the boolean negation of `a = b` * `a < b` and `a > b` are false when either operand is null; otherwise they use the order defined above * `a <= b` is `(a = b) OR (a < b)`; `a >= b` is `(a = b) OR (a > b)`; both are true when both operands are null and false when only one operand is null @@ -182,7 +166,32 @@ This table shows examples of these rules after evaluating value expressions to c | `null <= null` | `true` | | `34 < null` | `false` | -Comparisons must handle null values when value expressions evaluate to null. However, value expressions that are the direct child of a comparison must not be a null constant. For example, `x = get_item(map, "key")` is valid although `get_item` may return a null value, but `x = null` must be rejected because `x IS NULL` is the correct unambiguous predicate. +Value expressions that are the direct child of a comparison must not be either a null or NaN constant. However, comparisons must handle null and NaN values that are the result of evaluating a value expression. For example, `x = get_item(map, "key")` is valid although `get_item` may return a null value, but `x = null` must be rejected because `x IS NULL` is the correct unambiguous predicate. Similarly, `multiply(a, b)` may produce NaN for `a=0.0` and `b=Infinity` and is valid, but `x = NaN` must be rejected because `x IS NaN` is the correct test. + +Primitive types are compared using signed comparison, except for the following types: + +* `false` is less than `true` for `boolean` +* `fixed` and `binary` use unsigned byte-wise comparison +* `string` uses unsigned byte-wise comparison of the UTF-8 representation; it is not the Unicode Collation Algorithm +* `uuid` uses unsigned byte-wise comparison of the UUID bytes +* `decimal` uses signed comparison independent of scale; this is equivalent to comparison of unscaled values because type alignment produces values with the same scale +* `float` and `double` use IEEE 754 total order for all non-NaN values; see below for NaN comparison rules + +For floating point values, comparison with NaN behaves similarly to comparison of values with null. NaN should be specifically handled using `IS NaN` and `IS NOT NaN` tests. However, when value expressions produce a NaN value, the following rules must be applied: + +* `a = b` is true if both are NaN, or both are non-NaN and equal; false otherwise +* `a != b` is the boolean negation of `a = b` +* `a < b` and `a > b` are false when either operand is NaN; otherwise the IEEE 754 total order is used +* `a <= b` is `(a = b) OR (a < b)`; `a >= b` is `(a = b) OR (a > b)`; both are true when both operands are NaN and false when only one operand is NaN + + +#### Boolean logic + +Predicates must use 2-valued boolean logic. Evaluation of all predicates must produce `true` or `false`. + +Engines that implement SQL 3-valued boolean logic must add `IS NULL` and `NOT NULL` to produce the 2-valued equivalent. This avoids bugs in engines and languages that do not natively implement 3-valued logic. For example, the SQL predicate `x < 10` should be passed as `x < 10 AND x IS NOT NULL` for a SQL `WHERE` condition (or `x < 10`; see null-safe comparisons below). For a `CHECK` constraint, the expression is passed as `x < 10 OR x IS NULL`. This ensures that implementations will make the correct determination, rather than depending on context to interpret a null result (`WHERE` vs `CHECK`). + +Logical combinations are boolean operators applied to predicates. `AND` and `OR` are binary operations and `NOT` is a unary operation. `AND`, `OR`, and `NOT` do not accept null values and must fail on any null operand. ### Compatibility with REST catalog expressions From ebd2ff0b4024112ef6396d78e3efbf9adb9a4ece Mon Sep 17 00:00:00 2001 From: Ryan Blue Date: Wed, 24 Jun 2026 16:08:32 -0700 Subject: [PATCH 6/7] Minor updates. --- format/expressions-spec.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/format/expressions-spec.md b/format/expressions-spec.md index c4a37fe6d01d..a321f4a80ea9 100644 --- a/format/expressions-spec.md +++ b/format/expressions-spec.md @@ -35,7 +35,7 @@ This approach is intended to keep focus on the logical structure of expressions. This is consistent with Iceberg's conservative approach in other specs. Expressions and predicates are an important part of Iceberg implementation APIs, but have been deliberately limited in specifications. For example, sort orders and partition fields are strictly limited to a small set of transforms over well-defined inputs (source field IDs). This spec is widening what can be expressed, but depends on function calls for complex tasks. -This specification covers the structure of Iceberg expressions and includes appendicies that specify serialization as JSON and a set of portable functions defined by Iceberg specifications. +This specification covers the structure of Iceberg expressions and includes appendices that specify serialization as JSON and a set of portable functions defined by Iceberg specifications. [udf-spec]: https://iceberg.apache.org/udf-spec @@ -68,21 +68,21 @@ Field references may be named references (unbound) or ID references (bound). ID ID references are used for stored expressions, where the identity of the column is determined when the stored expression is created. For example, column constraints are tied to field IDs so that renaming a column does not invalidate the reference in its stored constraint. -Named references are used when the identity of the column is determined when the expression is evaluated. For example, query filters are resolved each time a query runs so servers-side planning uses unbound named references. +Named references are used when the identity of the column is determined when the expression is evaluated. For example, query filters are resolved each time a query runs so server-side planning uses unbound named references. The context in which an expression is used determines the type of references that are valid. Iceberg specifications should document whether ID references, named references, or both are allowed. #### Apply function -An apply expression represents the result of a function applied to (or called on) zero or more values produced by child value expressions. +An apply expression represents the result of a function applied to (or called on) zero or more values produced by child value expressions or predicates. -Functions are referenced using a catalog and a function identifer (list of strings). +Functions are referenced using a catalog and a function identifier (list of strings). * The function identifier consists of 0 or more namespace names followed by the function name. At least one part, the function name, is required. * Catalog is optional and is assumed to be the catalog in which the referencing object is stored if it is not present or is null -The catalog name identifies the catalog where the function definition can be loaded or is a reserved name that identifies a set of functions. As in the view and UDF specs, catalog names represent connection configurations that may differ across environments. Omitting catalog names is recommended to avoid depending on consistent environments. For example, if a table has a CHECK constraint that references a UDF without a catalog name (missing or null), the UDF should be loaded from the table’s catalog. +The catalog name identifies the catalog where the function definition can be loaded or is a reserved name that identifies a set of functions. As in the view and UDF specs, catalog names represent connection configurations that may differ across environments. Omitting catalog names is recommended to avoid depending on consistent environments. For example, if a table has a CHECK constraint that references a UDF without a catalog name (missing or null), the UDF should be loaded from the table's catalog. The reserved names used to identify sets are: @@ -102,21 +102,21 @@ A value expression's result type is determined when it is bound to a specific in Function calls may produce different types when function definitions change, and type changes may change the definition that is resolved for a function name. For example, if the input field passed to `identity(int) -> int` is promoted from `int` to `long`, the resolved `identity` function can change to `identity(long) -> long` if it is defined. -If types are incompatible at runtime, implementations binding or evaluating expressions may apply type promotion to align types for predicates and to resolve functions. Implementations may choose when to promote values to accomodate engines that differ in casting behavior. However, implementations must fail rather than insert unsafe casts. +If types are incompatible at runtime, implementations binding or evaluating expressions may apply type promotion to align types for predicates and to resolve functions. Implementations may choose when to promote values to accommodate engines that differ in casting behavior. However, implementations must fail rather than insert unsafe casts. ### Predicates A predicate is a boolean expression that produces true or false. -Predicates can be constants (true or false), tests of a value expression, comparisons of value exprssions, or logical combinations of predicates (AND, OR, NOT). +Predicates can be constants (true or false), tests of a value expression, comparisons of value expressions, or logical combinations of predicates (AND, OR, NOT). Value expressions are not valid predicates, even when the expression is expected to return a boolean value. Value expressions must be compared or tested to produce a predicate. For example, `is_empty(str_col)` is not a valid predicate because it may produce `null`, but `is_empty(str_col) = true` is a valid predicate. #### Tests -Tests are predicates that test a single value expression, optionally using a constant or set of constants. Constants must have the same type and must be non-null and non-NaN. Tests are: +Tests are predicates that test a single value expression, optionally using a constant or set of constants. Constants must all have the same type and must be non-null and non-NaN. Tests are: | Test | Allowed types | Constant type | Description | |-------------------------|---------------|---------------|-------------| @@ -140,7 +140,7 @@ Comparisons are: | Comparison | Description | |-------------|-------------| -| `=` | Is equal | +| `=` | Is equal (is not distinct from) | | `!=` | Is not equal | | `<` | Less than | | `<=` | Less than or equal | @@ -175,7 +175,7 @@ Primitive types are compared using signed comparison, except for the following t * `string` uses unsigned byte-wise comparison of the UTF-8 representation; it is not the Unicode Collation Algorithm * `uuid` uses unsigned byte-wise comparison of the UUID bytes * `decimal` uses signed comparison independent of scale; this is equivalent to comparison of unscaled values because type alignment produces values with the same scale -* `float` and `double` use IEEE 754 total order for all non-NaN values; see below for NaN comparison rules +* `float` and `double` use IEEE 754 order for all non-NaN values; see below for NaN comparison rules For floating point values, comparison with NaN behaves similarly to comparison of values with null. NaN should be specifically handled using `IS NaN` and `IS NOT NaN` tests. However, when value expressions produce a NaN value, the following rules must be applied: @@ -191,7 +191,7 @@ Predicates must use 2-valued boolean logic. Evaluation of all predicates must pr Engines that implement SQL 3-valued boolean logic must add `IS NULL` and `NOT NULL` to produce the 2-valued equivalent. This avoids bugs in engines and languages that do not natively implement 3-valued logic. For example, the SQL predicate `x < 10` should be passed as `x < 10 AND x IS NOT NULL` for a SQL `WHERE` condition (or `x < 10`; see null-safe comparisons below). For a `CHECK` constraint, the expression is passed as `x < 10 OR x IS NULL`. This ensures that implementations will make the correct determination, rather than depending on context to interpret a null result (`WHERE` vs `CHECK`). -Logical combinations are boolean operators applied to predicates. `AND` and `OR` are binary operations and `NOT` is a unary operation. `AND`, `OR`, and `NOT` do not accept null values and must fail on any null operand. +Logical combinations are boolean operators applied to predicates. `AND` and `OR` are binary operations and `NOT` is a unary operation. `AND`, `OR`, and `NOT` do not accept null values because predicates cannot produce them. ### Compatibility with REST catalog expressions @@ -270,7 +270,7 @@ FUNC_REF: NAME ID: integer NAME: string -VALUE: non-null single value JSON from the table spec +VALUE: single value JSON from the table spec DATA_TYPE: Iceberg type from the spec ``` From a81282c32f0c4cd6956d1597bd7f981853d9c606 Mon Sep 17 00:00:00 2001 From: Ryan Blue Date: Mon, 29 Jun 2026 14:12:11 -0700 Subject: [PATCH 7/7] Minor updates and clarifications. --- format/expressions-spec.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/format/expressions-spec.md b/format/expressions-spec.md index a321f4a80ea9..01aa7d8b353d 100644 --- a/format/expressions-spec.md +++ b/format/expressions-spec.md @@ -77,7 +77,7 @@ The context in which an expression is used determines the type of references tha An apply expression represents the result of a function applied to (or called on) zero or more values produced by child value expressions or predicates. -Functions are referenced using a catalog and a function identifier (list of strings). +Functions are referenced using a catalog and a function identifier. * The function identifier consists of 0 or more namespace names followed by the function name. At least one part, the function name, is required. * Catalog is optional and is assumed to be the catalog in which the referencing object is stored if it is not present or is null @@ -127,7 +127,7 @@ Tests are predicates that test a single value expression, optionally using a con | `STARTS WITH const` | string | string | true iff the constant is a prefix of the value | | `NOT STARTS WITH const` | string | string | true iff the constant is not a prefix of the value | | `IN (constant set)` | any primitive | same as value | true iff the value is equal to any constant | -| `NOT IN (constant set)` | any primitive | same as value | true iff the value is not equal to all constants | +| `NOT IN (constant set)` | any primitive | same as value | true iff the value is not equal to any constant | #### Comparisons @@ -181,7 +181,7 @@ For floating point values, comparison with NaN behaves similarly to comparison o * `a = b` is true if both are NaN, or both are non-NaN and equal; false otherwise * `a != b` is the boolean negation of `a = b` -* `a < b` and `a > b` are false when either operand is NaN; otherwise the IEEE 754 total order is used +* `a < b` and `a > b` are false when either operand is NaN; otherwise the IEEE 754 order is used * `a <= b` is `(a = b) OR (a < b)`; `a >= b` is `(a = b) OR (a > b)`; both are true when both operands are NaN and false when only one operand is NaN @@ -189,7 +189,7 @@ For floating point values, comparison with NaN behaves similarly to comparison o Predicates must use 2-valued boolean logic. Evaluation of all predicates must produce `true` or `false`. -Engines that implement SQL 3-valued boolean logic must add `IS NULL` and `NOT NULL` to produce the 2-valued equivalent. This avoids bugs in engines and languages that do not natively implement 3-valued logic. For example, the SQL predicate `x < 10` should be passed as `x < 10 AND x IS NOT NULL` for a SQL `WHERE` condition (or `x < 10`; see null-safe comparisons below). For a `CHECK` constraint, the expression is passed as `x < 10 OR x IS NULL`. This ensures that implementations will make the correct determination, rather than depending on context to interpret a null result (`WHERE` vs `CHECK`). +Engines that implement SQL 3-valued boolean logic must add `IS NULL` and `IS NOT NULL` to produce the 2-valued equivalent. This avoids bugs in engines and languages that do not natively implement 3-valued logic. For example, the SQL predicate `x < 10` should be passed as `x < 10 AND x IS NOT NULL` for a SQL `WHERE` condition (or `x < 10`; see null-safe comparisons below). For a `CHECK` constraint, the expression is passed as `x < 10 OR x IS NULL`. This ensures that implementations will make the correct determination, rather than depending on context to interpret a null result (`WHERE` vs `CHECK`). Logical combinations are boolean operators applied to predicates. `AND` and `OR` are binary operations and `NOT` is a unary operation. `AND`, `OR`, and `NOT` do not accept null values because predicates cannot produce them. @@ -255,6 +255,7 @@ LITERAL: VALUE | { "type": "literal", "value": VALUE } | { "type": "literal", "value": VALUE, "data-type": DATA_TYPE } LITERALS: [ LITERAL* ] + | { "type": "literals", "values": [ VALUE* ], "data-type": DATA_TYPE } REFERENCE: BOUND_REF | UNBOUND_REF BOUND_REF: { "type": "reference", "id": ID }