From 7e61982e1c4e8dd1f7d7ea7c4d9c6109e1097f1b Mon Sep 17 00:00:00 2001 From: malinjawi <171935807+malinjawi@users.noreply.github.com> Date: Thu, 21 May 2026 12:49:22 +0300 Subject: [PATCH] feat: add unbound type for partially bound plans Adds a built-in placeholder type for plans serialized before all schema and type information is available: - add Type.Unbound to the protobuf type definitions - document the unbound type class as a pure placeholder: not a wildcard (distinct from `any`), no nullability or parameters, no literals, and no prescribed resolution policy - add UNBOUND to the dialect schema and dialect fixture coverage - remove the legacy extensions/unknown.yaml placeholder extension Unbound is a proto/plan-level type only. It is intentionally not added to the type or test-case grammars, since it should not appear in extension YAML files or function test cases. --- dialects/tests/types_test.yaml | 1 + extensions/unknown.yaml | 67 --------------------------------- proto/substrait/type.proto | 9 +++++ site/docs/types/type_classes.md | 14 +++++++ site/docs/types/type_system.md | 2 + text/dialect_schema.yaml | 14 +++++++ 6 files changed, 40 insertions(+), 67 deletions(-) delete mode 100644 extensions/unknown.yaml diff --git a/dialects/tests/types_test.yaml b/dialects/tests/types_test.yaml index e8d2395bc..84856da79 100644 --- a/dialects/tests/types_test.yaml +++ b/dialects/tests/types_test.yaml @@ -9,6 +9,7 @@ dependencies: supported_types: - I8 - I16 + - type: UNBOUND - type: I32 metadata: storage_bytes: 4 diff --git a/extensions/unknown.yaml b/extensions/unknown.yaml deleted file mode 100644 index 3603d6871..000000000 --- a/extensions/unknown.yaml +++ /dev/null @@ -1,67 +0,0 @@ -%YAML 1.2 ---- -urn: extension:io.substrait:unknown -types: - - name: unknown -scalar_functions: - - name: "add" - impls: - - args: - - value: unknown - - value: unknown - return: unknown - - name: "subtract" - impls: - - args: - - value: unknown - - value: unknown - return: unknown - - name: "multiply" - impls: - - args: - - value: unknown - - value: unknown - return: unknown - - name: "divide" - impls: - - args: - - value: unknown - - value: unknown - return: unknown - - name: "modulus" - impls: - - args: - - value: unknown - - value: unknown - return: unknown -aggregate_functions: - - name: "sum" - impls: - - args: - - value: unknown - intermediate: unknown - return: unknown - - name: "avg" - impls: - - args: - - value: unknown - intermediate: unknown - return: unknown - - name: "min" - impls: - - args: - - value: unknown - intermediate: unknown - return: unknown - - name: "max" - impls: - - args: - - value: unknown - intermediate: unknown - return: unknown - - name: "count" - impls: - - args: - - value: unknown - intermediate: unknown - return: unknown diff --git a/proto/substrait/type.proto b/proto/substrait/type.proto index 9a4da4e97..81a24859c 100644 --- a/proto/substrait/type.proto +++ b/proto/substrait/type.proto @@ -46,6 +46,13 @@ message Type { Map map = 28; Func func = 38; + // A placeholder type whose concrete type has not yet been bound. + // Unbound may be used where a concrete type would normally appear in + // partially bound plans and expressions, and must be bound to a + // concrete type before execution. Substrait defines no runtime semantics + // for values whose type is unbound. + Unbound unbound = 39; + UserDefined user_defined = 30; // Deprecated in favor of user_defined, which allows nullability and @@ -209,6 +216,8 @@ message Type { Nullability nullability = 4; } + message Unbound {} + // A function type for higher-order functions. // Represents a function that takes parameters of specified types and // returns a value of a specified type. diff --git a/site/docs/types/type_classes.md b/site/docs/types/type_classes.md index cf1a17068..e18d935d3 100644 --- a/site/docs/types/type_classes.md +++ b/site/docs/types/type_classes.md @@ -24,6 +24,20 @@ Simple type classes are those that don't support any form of configuration. For | date | A date within [1000-01-01..9999-12-31]. | `int32` days since `1970-01-01` | interval_year | Interval year to month. Supports a range of [-10,000..10,000] years with month precision (= [-120,000..120,000] months). Usually stored as separate integers for years and months, but only the total number of months is significant, i.e. `1y 0m` is considered equal to `0y 12m` or `1001y -12000m`. | `int32` years and `int32` months, with the added constraint that each component can never independently specify more than 10,000 years, even if the components have opposite signs (e.g. `-10000y 200000m` is **not** allowed) | uuid | A universally-unique identifier composed of 128 bits. Typically presented to users in the following hexadecimal format: `c48ffa9e-64f4-44cb-ae47-152b4e60e77b`. Any 128-bit value is allowed, without specific adherence to RFC4122. | 16-byte `binary` +| unbound | A placeholder for a type that has not yet been bound to a concrete type. May only appear in partially bound plans, which must be bound before execution. | n/a + +### Unbound Type + +The `unbound` type class is a placeholder for a type that has not yet been bound to a concrete type. It may appear wherever a concrete type would normally appear, for example as a field type within `ReadRel.base_schema`. This allows a producer to serialize the shape of a plan — its relations and the names and ordering of its fields — before the concrete types of those fields are known. A downstream binder later replaces each occurrence of `unbound` with a concrete type once the necessary schema or catalog information is available. + +A plan that contains `unbound` anywhere within it is *partially bound*. Partially bound plans may be serialized and exchanged, but they are not executable, and Substrait defines no runtime semantics for values of unbound type. Specifically: + +- `unbound` is not a wildcard: it does not unify with, or implicitly coerce to, any other type. In particular, `unbound` is distinct from `any`: `any` is a function-signature wildcard that participates in overload matching, while `unbound` is a plan-level placeholder for a type that is not yet known. +- `unbound` carries no nullability and accepts no parameters; those properties are determined when the type is bound to a concrete type. +- There is no literal of type `unbound`. +- Substrait prescribes no resolution policy for `unbound`. A function invocation whose argument types are unbound is part of a partially bound plan; how a binder assigns concrete types, and how it subsequently resolves function bindings, is left entirely to the system performing the binding. + +Consumers that do not support partially bound plans should reject any plan containing `unbound`. ### Compound Types diff --git a/site/docs/types/type_system.md b/site/docs/types/type_system.md index eef7ff507..f60ede4b5 100644 --- a/site/docs/types/type_system.md +++ b/site/docs/types/type_system.md @@ -15,3 +15,5 @@ Refer to [Type Parsing](type_parsing.md) for a description of the syntax used to !!! note "Note" Substrait employs a strict type system without any coercion rules. All changes in types must be made explicit via [cast expressions](../expressions/specialized_record_expressions.md). + + Partially bound plans and expressions may use the [`unbound`](type_classes.md#unbound-type) type as a placeholder until a downstream binder assigns a concrete type. diff --git a/text/dialect_schema.yaml b/text/dialect_schema.yaml index 3dbf0deec..2b4d24843 100644 --- a/text/dialect_schema.yaml +++ b/text/dialect_schema.yaml @@ -54,6 +54,7 @@ properties: - INTERVAL_DAY - INTERVAL_YEAR - UUID + - UNBOUND - DECIMAL - STRUCT - LIST @@ -78,6 +79,7 @@ properties: - $ref: "#/definitions/type_interval_day" - $ref: "#/definitions/type_interval_year" - $ref: "#/definitions/type_uuid" + - $ref: "#/definitions/type_unbound" - $ref: "#/definitions/type_decimal" - $ref: "#/definitions/type_struct" - $ref: "#/definitions/type_list" @@ -640,6 +642,18 @@ definitions: description: Arbitrary data created by the dialect author. system_metadata: $ref: "#/definitions/system_type_metadata" + type_unbound: + type: object + additionalProperties: false + required: [type] + properties: + type: + const: UNBOUND + metadata: + type: object + description: Arbitrary data created by the dialect author. + system_metadata: + $ref: "#/definitions/system_type_metadata" type_decimal: type: object additionalProperties: false