Guides / Extensions
Add typed expressions
Extend Qubu with expressions that retain source, nullability, result, and SQL-domain metadata.
Build expressions from public helpers
The examples use the users table from Sources and clauses.
fragment(), makeExpression(), parameter(), identifier(), syntax(),
customClause(), and customSource() are public extension building blocks.
Import fragment, dialect, and expression constructors from qubu/core; import
custom source and schema metadata constructors from qubu/schema. Preserve the
same metadata model that built-ins use:
- use
RequiresSourceMeta<Source>for every source that the expression reads; - use
ResultMeta<Output, NullableFrom, SqlType>when the fragment exposes a typed result, or accept its defaultSqlUnknowndomain intentionally; - inherit child source and nullability facts when composing fragments; and
- use
context.parameter()for values instead of concatenating them into SQL.
sequence() is useful for a reusable fragment assembled from arbitrary child
fragments. Its const type parameter preserves the children's metadata, so
source-scope checking continues to work without as const at the call site.
Declare a custom SQL domain
Extend SqlSemanticType and only the portable capabilities the database type
actually supports. Compatibility groups allow a dialect-specific domain to
interoperate with a built-in family:
import { cast, column } from "qubu"
import type { SqlEqualityComparable, SqlOrderable, SqlSemanticType, SqlTextLike } from "qubu"
interface SqlCitext
extends
SqlSemanticType<"postgres.citext">,
SqlTextLike,
SqlOrderable<"text">,
SqlEqualityComparable<"text"> {}
const citext = column<string, string, string, SqlCitext>({
castType: "CITEXT",
})
const nameAsCitext = cast(users.name, citext)
The first three column type arguments are output, insert, and update values;
the fourth is the SQL domain. The text equality and ordering groups make the
custom domain compatible with SqlText. Use a distinct group when cross-type
comparison is not portable. castType also makes this definition a cast
target; its SQL text is emitted verbatim, so keep it in trusted extension code.
Definitions with schema flags are not accepted as cast targets because cast
nullability comes from the operand and write flags have no cast meaning.
Declare result domains at other extension boundaries too:
import { typedCall, typedCast, typedValue, unsafeExpression } from "qubu/core"
import type { SqlText, SqlUuid } from "qubu"
const id = typedValue<SqlUuid, string>("108cb836-20d2-41b2-8c23-f0c94700aa7e")
const normalized = typedCall<SqlText, string>()("custom_text", users.name)
const rawNameAsText = typedCast<string, SqlText>()(users.name, "TEXT")
const generated = unsafeExpression<string, SqlText>("custom_text()")
typedCall() preserves source requirements from its arguments. typedCast()
is the fallback when no reusable definition describes the target. It preserves
operand nullability and source metadata while emitting its supplied type name
verbatim. typedValue() binds a parameter. unsafeExpression() emits its
string unchanged and should remain a last resort.
The lower-level forms also expose the SQL domain in their generic lists:
call<Output, Name, Arguments, NullableFrom, SqlType>() and
cast<Output, SqlType>(). They are useful when an extension already computes
argument or nullability types in its own generic signature.
Untyped column(), value(), call(), and custom expressions use
SqlUnknown, which stays permissive for backward compatibility. Declaring a
known domain opts the extension into incompatible-operation errors. See
SQL semantic types for the capability model and
its limits.
Read next
- Add sources and clauses covers custom relations and clause renderers.
- Use unsafe syntax covers the raw-SQL boundary.