/** * Column type validators for Tinybird datasources * Similar to Convex's `v.*` pattern, but for ClickHouse types */ // Symbol for brand typing - use Symbol.for() for global registry // This ensures the same symbol is used across module instances const VALIDATOR_BRAND = Symbol.for("tinybird.validator"); /** * Base interface for all type validators * The phantom types enable TypeScript to infer the correct types */ export interface TypeValidator< TType, TTinybirdType extends string = string, TModifiers extends TypeModifiers = TypeModifiers, > { readonly [VALIDATOR_BRAND]: true; /** The inferred TypeScript type */ readonly _type: TType; /** The Tinybird/ClickHouse type string */ readonly _tinybirdType: TTinybirdType; /** Metadata about modifiers applied */ readonly _modifiers: TModifiers; /** Mark this output field as optional (it may be absent from endpoint responses) */ optional(): TypeValidator< TType, TTinybirdType, TModifiers & { optional: true } >; /** Make this column nullable */ nullable(): TypeValidator< TType | null, `Nullable(${TTinybirdType})`, TModifiers & { nullable: true } >; /** Apply LowCardinality optimization (for strings with few unique values) */ lowCardinality(): TypeValidator< TType, `LowCardinality(${TTinybirdType})`, TModifiers & { lowCardinality: true } >; /** Set a default value for the column */ default( value: TType, ): TypeValidator< TType, TTinybirdType, TModifiers & { hasDefault: true; defaultValue: TType } >; /** Set a default SQL expression for the column (for example: generateUUIDv4()) */ defaultExpr( expression: string, ): TypeValidator< TType, TTinybirdType, TModifiers & { hasDefault: true; defaultExpression: string } >; /** Set a codec for compression */ codec( codec: string, ): TypeValidator; /** Set an explicit JSON path for extraction (overrides autogenerated path) */ jsonPath( path: string, ): TypeValidator; } export interface TypeModifiers { optional?: boolean; nullable?: boolean; lowCardinality?: boolean; hasDefault?: boolean; defaultValue?: unknown; defaultExpression?: string; codec?: string; jsonPath?: string; } // Internal implementation interface ValidatorImpl< TType, TTinybirdType extends string, TModifiers extends TypeModifiers, > extends TypeValidator { readonly tinybirdType: TTinybirdType; readonly modifiers: TModifiers; } function createValidator( tinybirdType: TTinybirdType, modifiers: TypeModifiers = {}, ): TypeValidator { const validator: ValidatorImpl = { [VALIDATOR_BRAND]: true, _type: undefined as unknown as TType, _tinybirdType: tinybirdType, _modifiers: modifiers, tinybirdType, modifiers, optional() { return createValidator(tinybirdType, { ...modifiers, optional: true, }) as TypeValidator< TType, TTinybirdType, TypeModifiers & { optional: true } >; }, nullable() { // If already has LowCardinality, we need to move Nullable inside // ClickHouse requires: LowCardinality(Nullable(X)), not Nullable(LowCardinality(X)) if (modifiers.lowCardinality) { // Extract base type from LowCardinality(X) and wrap as LowCardinality(Nullable(X)) const baseType = tinybirdType.replace(/^LowCardinality\((.+)\)$/, "$1"); const newType = `LowCardinality(Nullable(${baseType}))`; return createValidator< TType | null, `LowCardinality(Nullable(${string}))` >(newType as `LowCardinality(Nullable(${string}))`, { ...modifiers, }) as unknown as TypeValidator< TType | null, `Nullable(${TTinybirdType})`, TypeModifiers & { nullable: true } >; } return createValidator( `Nullable(${tinybirdType})` as `Nullable(${TTinybirdType})`, { ...modifiers, nullable: true }, ) as TypeValidator< TType | null, `Nullable(${TTinybirdType})`, TypeModifiers & { nullable: true } >; }, lowCardinality() { // If already nullable, wrap as LowCardinality(Nullable(X)) if (modifiers.nullable) { // Extract base type from Nullable(X) and wrap as LowCardinality(Nullable(X)) const baseType = tinybirdType.replace(/^Nullable\((.+)\)$/, "$1"); const newType = `LowCardinality(Nullable(${baseType}))`; const { nullable: _, ...rest } = modifiers; return createValidator( newType as `LowCardinality(Nullable(${string}))`, { ...rest, lowCardinality: true }, ) as unknown as TypeValidator< TType, `LowCardinality(${TTinybirdType})`, TypeModifiers & { lowCardinality: true } >; } return createValidator( `LowCardinality(${tinybirdType})` as `LowCardinality(${TTinybirdType})`, { ...modifiers, lowCardinality: true }, ) as TypeValidator< TType, `LowCardinality(${TTinybirdType})`, TypeModifiers & { lowCardinality: true } >; }, default(value: TType) { return createValidator(tinybirdType, { ...modifiers, hasDefault: true, defaultValue: value, defaultExpression: undefined, }) as TypeValidator< TType, TTinybirdType, TypeModifiers & { hasDefault: true; defaultValue: TType } >; }, defaultExpr(expression: string) { const trimmed = expression.trim(); if (!trimmed) { throw new Error("Default expression cannot be empty."); } return createValidator(tinybirdType, { ...modifiers, hasDefault: true, defaultValue: undefined, defaultExpression: trimmed, }) as TypeValidator< TType, TTinybirdType, TypeModifiers & { hasDefault: true; defaultExpression: string } >; }, codec(codec: string) { return createValidator(tinybirdType, { ...modifiers, codec, }) as TypeValidator< TType, TTinybirdType, TypeModifiers & { codec: string } >; }, jsonPath(path: string) { return createValidator(tinybirdType, { ...modifiers, jsonPath: path, }) as TypeValidator< TType, TTinybirdType, TypeModifiers & { jsonPath: string } >; }, }; return validator; } type AggregateFunctionValidator = { ( func: TFunc, ): TypeValidator; < TFunc extends string, TTypes extends readonly [ TypeValidator, ...TypeValidator[], ], >( func: TFunc, ...types: TTypes ): TypeValidator< AggregateFunctionValue, AggregateFunctionTinybirdType, TypeModifiers >; }; type AggregateFunctionValue< TTypes extends readonly TypeValidator[], > = TTypes extends readonly [ infer TFirst extends TypeValidator, ...TypeValidator[], ] ? TFirst["_type"] : unknown; type AggregateFunctionArgs< TTypes extends readonly TypeValidator[], > = TTypes extends readonly [ infer TFirst extends TypeValidator, ...infer TRest extends TypeValidator[], ] ? TRest extends [] ? TFirst["_tinybirdType"] : `${TFirst["_tinybirdType"]}, ${AggregateFunctionArgs}` : never; type AggregateFunctionTinybirdType< TFunc extends string, TTypes extends readonly TypeValidator[], > = TTypes extends [] ? `AggregateFunction(${TFunc})` : `AggregateFunction(${TFunc}, ${AggregateFunctionArgs})`; const aggregateFunction = (( func: string, ...types: TypeValidator[] ) => { if (types.length === 0) { return createValidator( `AggregateFunction(${func})`, ); } return createValidator( `AggregateFunction(${func}, ${types.map((type) => type._tinybirdType).join(", ")})`, ); }) as AggregateFunctionValidator; /** * Type validators for Tinybird columns * * @example * ```ts * import { t } from '@tinybirdco/sdk'; * * const schema = { * id: t.string(), * count: t.int32(), * timestamp: t.dateTime(), * tags: t.array(t.string()), * metadata: t.json(), * }; * * // With custom types for narrower type inference: * type UserId = string & { readonly __brand: 'UserId' }; * type Timestamp = string & { readonly __brand: 'Timestamp' }; * * const typedSchema = { * user_id: t.string(), * created_at: t.dateTime(), * }; * ``` */ export const t = { // ============ String Types ============ /** String type - variable length UTF-8 string */ string: () => createValidator("String"), /** FixedString(N) - fixed length string, padded with null bytes */ fixedString: (length: number) => createValidator(`FixedString(${length})`), /** UUID - 16-byte universally unique identifier */ uuid: () => createValidator("UUID"), // ============ Integer Types ============ /** Int8 - signed 8-bit integer (-128 to 127) */ int8: () => createValidator("Int8"), /** Int16 - signed 16-bit integer */ int16: () => createValidator("Int16"), /** Int32 - signed 32-bit integer */ int32: () => createValidator("Int32"), /** Int64 - signed 64-bit integer (represented as number, may lose precision) */ int64: () => createValidator("Int64"), /** Int128 - signed 128-bit integer (represented as bigint) */ int128: () => createValidator("Int128"), /** Int256 - signed 256-bit integer (represented as bigint) */ int256: () => createValidator("Int256"), /** UInt8 - unsigned 8-bit integer (0 to 255) */ uint8: () => createValidator("UInt8"), /** UInt16 - unsigned 16-bit integer */ uint16: () => createValidator("UInt16"), /** UInt32 - unsigned 32-bit integer */ uint32: () => createValidator("UInt32"), /** UInt64 - unsigned 64-bit integer (represented as number, may lose precision) */ uint64: () => createValidator("UInt64"), /** UInt128 - unsigned 128-bit integer (represented as bigint) */ uint128: () => createValidator("UInt128"), /** UInt256 - unsigned 256-bit integer (represented as bigint) */ uint256: () => createValidator("UInt256"), // ============ Float Types ============ /** Float32 - 32-bit floating point */ float32: () => createValidator("Float32"), /** Float64 - 64-bit floating point (double precision) */ float64: () => createValidator("Float64"), /** Decimal(precision, scale) - fixed-point decimal number */ decimal: (precision: number, scale: number) => createValidator( `Decimal(${precision}, ${scale})`, ), // ============ Boolean ============ /** Bool - boolean value (true/false) */ bool: () => createValidator("Bool"), // ============ Date/Time Types ============ /** Date - string in YYYY-MM-DD format (e.g. 2024-01-15) */ date: () => createValidator("Date"), /** Date32 - string in YYYY-MM-DD format (e.g. 2024-01-15, extended date range) */ date32: () => createValidator("Date32"), /** DateTime - string in YYYY-MM-DD HH:MM:SS format (e.g. 2024-01-15 10:30:00) */ dateTime: (timezone?: string) => timezone ? createValidator(`DateTime('${timezone}')`) : createValidator("DateTime"), /** DateTime64 - string in YYYY-MM-DD HH:MM:SS[.fraction] format (e.g. 2024-01-15 10:30:00.123) */ dateTime64: ( precision: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 = 3, timezone?: string, ) => timezone ? createValidator( `DateTime64(${precision}, '${timezone}')`, ) : createValidator(`DateTime64(${precision})`), // ============ Complex Types ============ /** Array(T) - array of elements of type T */ array: >( element: TElement, ): TypeValidator< TElement["_type"][], `Array(${TElement["_tinybirdType"]})`, TypeModifiers > => createValidator( `Array(${element._tinybirdType})` as `Array(${TElement["_tinybirdType"]})`, ), /** Tuple(T1, T2, ...) - tuple of heterogeneous types */ tuple: < TElements extends readonly TypeValidator[], >( ...elements: TElements ): TypeValidator< { [K in keyof TElements]: TElements[K]["_type"] }, `Tuple(${string})`, TypeModifiers > => createValidator< { [K in keyof TElements]: TElements[K]["_type"] }, `Tuple(${string})` >(`Tuple(${elements.map((e) => e._tinybirdType).join(", ")})`), /** Map(K, V) - dictionary/map type */ map: < TKey extends TypeValidator, TValue extends TypeValidator, >( keyType: TKey, valueType: TValue, ): TypeValidator< Map, `Map(${TKey["_tinybirdType"]}, ${TValue["_tinybirdType"]})`, TypeModifiers > => createValidator< Map, `Map(${TKey["_tinybirdType"]}, ${TValue["_tinybirdType"]})` >(`Map(${keyType._tinybirdType}, ${valueType._tinybirdType})`), /** JSON - semi-structured JSON data */ json: () => createValidator("JSON"), // ============ Enum Types ============ /** Enum8 - enumeration stored as Int8 */ enum8: (...values: TValues) => { const enumMapping = values .map((v, i) => `'${v.replace(/'/g, "\\'")}' = ${i + 1}`) .join(", "); return createValidator( `Enum8(${enumMapping})` as `Enum8(${string})`, ); }, /** Enum16 - enumeration stored as Int16 */ enum16: (...values: TValues) => { const enumMapping = values .map((v, i) => `'${v.replace(/'/g, "\\'")}' = ${i + 1}`) .join(", "); return createValidator( `Enum16(${enumMapping})` as `Enum16(${string})`, ); }, // ============ Special Types ============ /** IPv4 - IPv4 address */ ipv4: () => createValidator("IPv4"), /** IPv6 - IPv6 address */ ipv6: () => createValidator("IPv6"), // ============ Aggregate Function States ============ /** SimpleAggregateFunction - for materialized views with simple aggregates */ simpleAggregateFunction: < TFunc extends string, TType extends TypeValidator, >( func: TFunc, type: TType, ): TypeValidator< TType["_type"], `SimpleAggregateFunction(${TFunc}, ${TType["_tinybirdType"]})`, TypeModifiers > => createValidator< TType["_type"], `SimpleAggregateFunction(${TFunc}, ${TType["_tinybirdType"]})` >(`SimpleAggregateFunction(${func}, ${type._tinybirdType})`), /** AggregateFunction - for materialized views with complex aggregates */ aggregateFunction, } as const; /** Type alias for any type validator */ export type AnyTypeValidator = TypeValidator; /** Extract the TypeScript type from a type validator */ export type InferType = T["_type"]; /** Extract the Tinybird type string from a type validator */ export type TinybirdType = T["_tinybirdType"]; /** Helper to check if a value is a type validator */ export function isTypeValidator(value: unknown): value is AnyTypeValidator { return ( typeof value === "object" && value !== null && VALIDATOR_BRAND in value && (value as Record)[VALIDATOR_BRAND] === true ); } /** Get the Tinybird type string from a validator */ export function getTinybirdType(validator: AnyTypeValidator): string { return validator._tinybirdType; } /** Get the modifiers from a validator */ export function getModifiers(validator: AnyTypeValidator): TypeModifiers { return validator._modifiers; }