Guides / Extensions
Add typed expressions
Build custom expressions that preserve result types and query checks.
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.
Use the definition as a cast target
castType also makes this definition a cast target. Its SQL text is emitted
unchanged, 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.
Type individual expressions
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", "uuid")
const normalized = typedCall<SqlText, string>()("custom_text", users.name)
const rawNameAsText = typedCast<string, SqlText>()(users.name, "TEXT")
const generated = unsafeExpression<string, SqlText>("custom_text()")
Choose the helper for the operation:
typedCall()preserves source requirements from its arguments.typedCast()supplies a cast target when no reusable definition describes it. It preserves operand nullability and source metadata, and emits the supplied type name unchanged.typedValue()binds a parameter and declares its runtime SQL domain for the adapter. It does not choose a JavaScript result decoder; schema columns carry decoder metadata separately.unsafeExpression()emits its string unchanged. Use it only when the other helpers cannot express the syntax.
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 allows composition without SQL-domain checks. Declaring a
known domain opts the extension into incompatible-operation errors. See
SQL semantic types for the capability model and
its limits.
Declare a downstream aggregate
Declare reusable SQL aggregates with aggregateFunction() from qubu/core.
The returned function infers its arguments on each call and preserves their
source, dependency, nullability, and capability metadata:
import { aggregateFunction } from "qubu/core"
const jsonGroupArray = aggregateFunction<string>("json_group_array")
The helper adds both the runtime aggregate category and the type-level metadata
used by grouping checks. This avoids manually composing call() and
markExpressionCategory(): call<string>(...) defaults its remaining type
parameters, losing argument inference, and markExpressionCategory() alone
does not add type-level aggregate metadata.
For a SQLite query, declare the function once and use it in projections:
import { from, groupBy, select, table, text } from "qubu"
const gameCategory = table("game_category", {
gameId: text(),
name: text(),
})
const categoriesByGame = select(
{
gameId: gameCategory.gameId,
categories: jsonGroupArray(gameCategory.name),
},
from(gameCategory),
groupBy(gameCategory.gameId),
)
Qubu rejects a missing argument source, an ungrouped selected column, or an
aggregate used as a grouping key. The factory uses call()'s default
SqlUnknown result domain and argument-derived nullability; declare a different
result contract in the downstream helper when the SQL function requires one.
TOutput declares the JavaScript result type without decoding database values.
Keep the function catalog, argument restrictions, decoding, null filtering,
distinctness, and sorting policy downstream. An application can decode the
returned JSON text with its own parseCategories() helper after execution, or
attach its decoder with mapResult().
Read next
- Add sources and clauses covers custom relations and clause renderers.
- Use unsafe syntax covers the raw-SQL boundary.