Skip to main content

Data Types

Types describe values, declared storage, and query results. Use schema type names in .conjure source; a generated C# carrier is not an alternative source grammar. See Schema Language for declarations and Query Language for expressions and pipelines.

Type System Overview​

There are three distinct choices:

  1. Value type: a scalar, enum, declared record, entity reference, or collection element.
  2. Absence: T? admits an absent value; T does not silently discard absence at a declared assignment boundary.
  3. Query result surface: T[], pooled<T>, seq<T>, and map<K, V> choose different ownership and generated API contracts.

A nominal type alias does not create a new type. A collection result constructor does not make that result type valid as a stored field.

Numeric Types​

Integer Types​

Schema typeWidthRange
sbyteSigned 8-bit−128 to 127
byteUnsigned 8-bit0 to 255
shortSigned 16-bit−32,768 to 32,767
ushortUnsigned 16-bit0 to 65,535
intSigned 32-bit−2³¹ to 2³¹−1
uintUnsigned 32-bit0 to 2³²−1
longSigned 64-bit−2⁶³ to 2⁶³−1
ulongUnsigned 64-bit0 to 2⁶⁴−1

Use suffixes to select a literal's type: 1u, 1L, and 1ul. A sign is an expression operator, as in -1L. A constant assignment may fit a smaller declared integral type; a runtime narrowing is not justified merely because the value usually fits.

Floating-Point Types​

float is IEEE 754 binary32; double is IEEE 754 binary64. An unsuffixed decimal real literal is double. Use 1f or 0.5f for float, and 1d for an explicit double literal. Do not rely on a double literal implicitly narrowing to float.

Decimal Type​

decimal is the CLR decimal value type. Use m, as in 12.50m. Decimal and binary floating-point arithmetic are not interchangeable numeric families; choose conversions deliberately rather than assuming a common numeric rank.

Fixed-Point Types​

Schema typeCarrierFormatLiteral example
fixed32ConjureDB.Numerics.Fixed32Q16.161.5fx32
fixed64ConjureDB.Numerics.Fixed64Q32.321.5fx64

Fixed-point values do not silently mix with integer or IEEE floating-point values. Use their supported typed operations and explicit conversions. The exact fixed32 to fixed64 widening is supported; the reverse is not implicit. fixed is not a canonical substitute for fixed64.

Numeric Promotion Hierarchy​

Promotion is governed by the actual operand types and the operation. It is not simply “the larger width wins”: signed/unsigned ranges, decimal, fixed point, nullability, and potentially lossy conversions matter. Typical lossless writes include int to long and float to double; a long result cannot be assigned to int without an explicit admitted conversion.

Aggregate Type Inference​

An aggregate's result type and empty-input behavior are part of its contract. count() returns long, including zero for empty input. Value folds such as sum, min, max, and avg can be absent on empty input; do not declare a non-nullable result unless the actual operation and input prove non-absence.

table Sale(persistence: none) {
Id: int @id
Amount: int
}

query SaleCount() -> long {
from Sale s | aggregate count()
}

query AverageAmount() -> double? {
from Sale s | aggregate avg(s.Amount)
}

A scalar output aggregate is unnamed. A row-member aggregate has an explicit assignment name. Adding a select to a scalar aggregate is not permission to use an unnamed aggregate as an authored row member.

Boolean Type​

bool has the values true and false; bool? also admits absence.

Two-Valued Boolean Logic​

Comparisons use two-valued results rather than SQL UNKNOWN. Null equality is explicit: null == null is true, and an absent value differs from a non-absent one. Ordered comparisons involving absence do not make the value present. Use &&, ||, and !; short-circuiting and the actual expression's effects are preserved. An absence-admitting boolean must be handled where a definite boolean is required.

Character and String Types​

char is one UTF-16 code unit; string is a reference string. String comparison is ordinal. The declaration distinguishes string from string?, even though both use a reference-type CLR carrier. A CLR reference representation does not make a declared non-nullable result or field nullable.

table Player(persistence: none) {
Id: int @id
Nickname: string?
}

query DisplayNames() -> string[] {
from Player p | select Name = p.Nickname ?? "anonymous"
}

query UnnamedPlayers() -> Player[] {
from Player p | filter p.Nickname == null
}

Use canonical text.* functions for string operations; see Custom Functions. Do not substitute C# instance methods or retired collection/string call spellings.

Temporal Types​

Schema typeC# carrier
datetimeSystem.DateTime
datetimeoffsetSystem.DateTimeOffset
dateonlySystem.DateOnly
timeonlySystem.TimeOnly
timespanSystem.TimeSpan

A temporal type is not a number merely because its carrier has ticks. Comparisons and supported arithmetic require appropriate temporal operands; different date/time types are not automatically interchangeable.

Calendar construction uses expressions such as datetime(2024, 1, 1), not an @-prefixed date literal. Duration literals include s, ms, min, and h. Their typed value is a timespan.

const Epoch: datetime = datetime(2024, 1, 1)
const RetryDelay: timespan = 1s + 500ms

table EventRecord(persistence: none) {
Id: int @id
CreatedAt: datetime @default(Epoch)
}

Runtime clocks are not closed schema constants or closed range endpoints. Temporal conversion and date functions are described in Custom Functions.

Binary Type​

binary is a binary value carried by byte[]. It is semantically distinct from the general array type byte[]; do not assume that equal CLR representations make the two source types interchangeable in every operation.

table Payload(persistence: none) {
Id: int @id
Content: binary
OptionalContent: binary?
}

Binary values support compatible equality, not general scalar ordering. Storage-kind restrictions still apply: a managed binary payload is not an arbitrary ephemeral-row field.

Identifier Types​

Guid​

guid maps to System.Guid. It is not a stringly typed entity reference. Use a typed parameter or the supported conversion contract when crossing a textual boundary. Domain-specific frame references use ref T, described below.

Nullable Types​

Syntax​

FormMeaning
int?An integer or absence
string?A string declaration that admits absence
Player?An optional entity/result value
int[]An array of non-nullable integers
int?[]An array of optional integers

The suffix order matters. T?[] does not mean an absent array container. Do not rewrite optional many T mechanically to that form: it changes which value can be absent. Direct type syntax does not admit T[]? as an alternative spelling for optional elements.

Null Handling Operators and Functions​

  • Test absence with value == null or value != null.
  • Use value ?? replacement to choose an alternative for absence.
  • Use condition ? left : right for a conditional expression. Its branches must have an admitted common type and nullability.

An explicit cast does not automatically prove that an absent value is present. A declared write must satisfy both the target's type and its nullability.

Optional Results and Row Cardinality​

The question “can this value be absent?” differs from “how many rows does this query produce?” An ungrouped avg produces one scalar row whose value can be absent. A key lookup can produce zero or one entity row. The signature and completed pipeline are checked together.

Completed row cardinalityRequired -> TOptional -> T?Collection result
Exactly one, provedNo row terminalNo row terminalNo row terminal
At most one, provedsingleNo row terminalNo row terminal
Many or not provedsingle, or ordered firstsingle_or_default, or ordered first_or_defaultNo row terminal

The result value must still fit its declared type in every cell of the table. first and first_or_default require an actual qualifying order. A terminal that is redundant or conflicts with the signature is rejected, rather than silently changing the API. single checks multiplicity; it does not choose an arbitrary row.

Enum Types​

Schema Definition​

enum PlayerState: byte {
Idle = 0,
Active = 1,
Disabled = 2,
}

table Player(persistence: none) {
Id: int @id
State: PlayerState @default(PlayerState.Idle)
}

query ActivePlayers() -> Player[] {
from Player p | filter p.State == PlayerState.Active
}

Backing Types​

The default is int. Explicit integral backing types are sbyte, byte, short, ushort, int, uint, long, and ulong. Values must fit the backing range. An omitted member value increments from its predecessor.

Enum Type Compatibility​

An enum has its own nominal identity. Two independently declared enums are not interchangeable merely because they have identical backing types and values. Supported equality with an enum's backing integer type does not make that integer an assignable enum value or authorize an integer-to-enum cast. Enum ordering is not a general numeric operation.

Enum Literals​

Write a declared member, such as PlayerState.Active. A field/default typed as that enum accepts its own members, not the same-valued member of another enum. Do not manufacture an enum value by writing 1 as PlayerState.

Flags Enums​

An enum can declare powers of two and named combinations, but that declaration does not create a separate flags-expression grammar:

enum Permissions {
None = 0,
Read = 1,
Write = 2,
ReadWrite = 3,
}

Use the declared combination Permissions.ReadWrite when needed. A bare | is a pipeline separator, not an enum-combination operator. Do not infer C# [Flags] behavior from these member values.

Collection Types​

Collection Kinds​

An ordinary query's result signature selects one generated consumer surface:

Source resultOwnership and consumptionGenerated method shape
T[]Owned array copy; the caller can modify that copyRead(...) returns an array
pooled<T>Stable pooled copy, read-only, valid until DisposeRead(...) returns ReadOnlyPooledQueryResult<T>
seq<T>Rows pushed to a typed consumer during executionReadForEach<TConsumer>(..., ref consumer)
map<K, V>Owned keyed result from a final scoped groupRead(...) returns a dictionary

Only the selected method surface is generated. An array result does not also request ReadNoAlloc, and a sequence does not add an array-returning Read. Choosing a different surface can therefore change the public C# API.

A pooled result is a copy, not a borrowed view into mutable table storage. Source writes do not change that copy. Copies of a pooled result handle share its disposal/lifetime; do not use any copy after disposal. Its public API does not expose writable backing storage. A sequence is a push API, not a promise of an IEnumerable<T> object that the caller can enumerate later.

Schema Syntax​

This complete example selects each supported surface explicitly:

table Row(plural: Rows, persistence: none) {
Id: int @id
Bucket: int
Score: int
}

using PlayerRow = (Id: long, Score: int)

query ArrayRows() -> int[] {
from Row r | select r.Id
}

query PooledRows() -> pooled<int> {
from Row r | select r.Id
}

query SequenceRows() -> seq<int> {
from Row r | select r.Id
}

query MapRows() -> map<int, int[]> {
from Row r | group r.Bucket (sort r.Id | select r.Id)
}

query MapCounts() -> map<int, long> {
from Row r | group r.Bucket (aggregate count())
}

query MapTuples() -> map<int, PlayerRow[]> {
from Row r | group r.Bucket (sort r.Id | select { r.Id, r.Score })
}

A map query must end in the actual group Key (pipeline). The key supplies K; the inner output supplies V. A collection value retains the partition's rows; a scalar value requires the supported scalar reduction. A nullable key, a scalar value manufactured from an arbitrary multi-row select, or a pipeline suffix after the final group is rejected. An inner filter can leave an existing partition with an empty array value.

Selected result constructors cannot be nested arbitrarily: pooled<T> and seq<T> require non-array elements, a map value may be an array but not another selected result constructor, and a map key cannot be optional or a collection.

These constructors are result-only. They do not invent a storage or parameter ABI for pooled<T>, seq<T>, or map<K, V>. Stored arrays use T[] where the storage kind permits arrays. A nominal alias cannot bypass storage restrictions.

Migration from CLR Collection Signatures​

Use the source result constructors to state collection policy, not a CLR list interface. The following is an intentional migration example, not the recommended signature for a new query:

-> extern System.Collections.Generic.IReadOnlyList<int>

There is no universally API-preserving replacement: choose int[], pooled<int>, or seq<int> according to ownership and consumption, then review the actual generated return type and method set. A type alias alone does not prove equality of those public APIs.

Composite Types​

Declared Records​

type Name declares a nominal reference record; struct type Name declares a nominal value record. Both describe named members. A named tuple alias is a structural tuple spelling, not a new nominal record declaration.

struct type Pair {
First: int
Second: int?
}

using PairAlias = Pair

table Row(persistence: none) {
Id: int @id
}

query Pairs() -> Pair[] {
from Row r | select (PairAlias { First = r.Id, Second = null })
}

PairAlias { ... } constructs the actual Pair. Member names, duplicates, required members, conversions, and nullability are checked against its declared shape. Source order is retained for expression evaluation; it is not an excuse to bind values to whichever member happens to occupy that position.

A contextual record value { First = expression, Second = expression } needs an actual declared target shape. A nominal value explicitly names that shape. In contrast, select { r.Id, Score = expression } is a named result-field projection. One scalar output is written without tuple braces; parenthesize a whole nominal record value as shown above.

Composite Type Compatibility​

Nominal identity matters: a different record with the same field names is not implicitly the same type. Aliases preserve identity. An enum, table entity, struct record, reference record, and named tuple are not interchangeable just because generated code happens to expose similar members.

Extern Types​

extern type Alias = Qualified.ClrType declares a C# bridge type. The owning project must provide the actual referenced symbol; a guessed name or an unrelated same-named type is not sufficient. It is also possible to use an explicit extern Qualified.ClrType at an admitted type-reference boundary.

These are syntax templates, requiring a real project reference and actual host members, rather than self-contained schema examples:

extern type Position = Game.Geometry.Position
extern function project(value: Position) -> float = Game.Geometry.Project

External construction/projection must match the real accessible C# constructor and members. A private or mismatched constructor is not made usable by an alias. For functions and project integration, see Custom Functions and Schema Workflow.

Entity References​

The source type ref Entity carries an ephemeral entity domain. It is an opaque generational reference, not a numeric key. Equality and supported domain-preserving uses do not imply arithmetic, ordering, or casts to/from integers.

ephemeral table Actor(capacity: 16) {
Id: int @id
Health: int
}

event table Hit(capacity: 16, lifetime: next_frame) {
Target: ref Actor
Damage: int
}

A reference must name an admitted entity/table domain. An optional reference is represented by the handle's none contract, not by adding ? to a reference field. Reference arrays, persisted reference fields, and references nested into unsupported stored record layouts are rejected. See Ephemeral Tables.

Type Coercion and Conversion​

Implicit Conversions​

Declared assignments admit supported implicit conversions, not arbitrary reinterpretation. Non-absence is checked independently of the CLR type: string? cannot become a required string merely because both carriers are references. A narrowing aggregate result also needs explicit intent.

Explicit Casts​

Use the expression form value as Type; the historical cast(value as Type) form is not the canonical syntax.

table Player(persistence: none) {
Id: int @id
Score: int
}

query WideScores() -> long[] {
from Player p | select Score = p.Score as long
}

Casts must belong to a supported conversion. They do not grant arbitrary integer-to-enum, integer-to-reference, or unrelated nominal conversions. Use checked(...) and unchecked(...) where the supported arithmetic/cast contract requires explicit overflow behavior. Function-based parsing and conversion have their own failure/nullability contracts; see Custom Functions.

Ranges​

lower..upper is lower-inclusive and upper-exclusive. A predicate such as Value in 0..10 includes zero and excludes ten. @range(0..10) declares typed range metadata using the same endpoint convention.

Range endpoints must be ordered, closed, and compatible with the field's supported numeric or temporal domain. Constants and supported calendar/duration expressions can supply endpoints; runtime parameters and clocks cannot.

A declared range is not a universal mutable-storage invariant. It does not prove that every host/API write was range-checked. Do not rely on the annotation alone to remove validation, narrow a result type, or assume an out-of-range write cannot occur. Query execution contracts such as a dense-sort hint are separate and can enforce their own range checks.

Schema Type Names vs C# Names​

Canonical sourceC# carrier
sbyte, byte, short, ushortCorresponding C# integral type
int, uint, long, ulongCorresponding C# integral type
float, double, decimalCorresponding C# numeric type
bool, char, stringCorresponding C# scalar/reference type
datetime, datetimeoffsetSystem.DateTime, System.DateTimeOffset
dateonly, timeonly, timespanCorresponding System temporal type
guidSystem.Guid
binarybyte[], with binary semantics
fixed32, fixed64Corresponding ConjureDB.Numerics fixed-point type
objectSystem.Object, only where the operation/storage admits it
Declared enumGenerated enum with its declared backing type
type Name / struct type NameGenerated reference/value record
T[]Array of the actual element type
ref EntityDomain-typed ephemeral entity handle

CLR names and carriers belong at integration boundaries. They do not replace canonical schema spellings or override lifetime, storage, and nullability rules. The owning project/runtime must support the carrier; a schema spelling alone does not provide an unavailable framework or external type.

Intentional Negative Migration Examples​

The following old spellings are shown only to explain rejection/migration:

int32 -> int
boolean -> bool
single -> float
timestamp -> datetime
interval -> timespan
fixed -> fixed64
optional T -> T?
many T -> T[]

This complete intentionally rejected schema uses the retired wrapper/type spellings (SCH2173). Its canonical field type is int?[], not an optional array container.

type LegacyShape {
Values: many optional int32
}

Compound wrapper edits must preserve which value is optional. If that cannot be proved, a diagnostic may deliberately omit an automatic edit. See Error Codes and Schema Migration.