From 2cc08dd7fa0d16a3c026c108620941a1b92c06f5 Mon Sep 17 00:00:00 2001 From: Wyatt Schulte Date: Tue, 24 Feb 2026 17:21:30 -0500 Subject: [PATCH] implement generic types on all column types --- src/schema/types.test.ts | 390 +++++++++++++++++++++++++++++---------- src/schema/types.ts | 213 ++++++++++++++------- 2 files changed, 439 insertions(+), 164 deletions(-) diff --git a/src/schema/types.test.ts b/src/schema/types.test.ts index 1177300..17a3903 100644 --- a/src/schema/types.test.ts +++ b/src/schema/types.test.ts @@ -1,226 +1,424 @@ -import { describe, it, expect } from 'vitest'; -import { t, isTypeValidator, getTinybirdType, getModifiers } from './types.js'; - -describe('Type Validators (t.*)', () => { - describe('Basic types', () => { - it('generates String type', () => { +import { describe, it, expect, expectTypeOf } from "vitest"; +import { t, isTypeValidator, getTinybirdType, getModifiers } from "./types.js"; +import { defineDatasource } from "./datasource.js"; +import { engine } from "./engines.js"; +import type { InferRow } from "../infer/index.js"; + +describe("Type Validators (t.*)", () => { + describe("Basic types", () => { + it("generates String type", () => { const type = t.string(); - expect(type._tinybirdType).toBe('String'); + expect(type._tinybirdType).toBe("String"); }); - it('generates Int32 type', () => { + it("generates Int32 type", () => { const type = t.int32(); - expect(type._tinybirdType).toBe('Int32'); + expect(type._tinybirdType).toBe("Int32"); }); - it('generates DateTime type', () => { + it("generates DateTime type", () => { const type = t.dateTime(); - expect(type._tinybirdType).toBe('DateTime'); + expect(type._tinybirdType).toBe("DateTime"); }); - it('generates DateTime with timezone', () => { - const type = t.dateTime('UTC'); + it("generates DateTime with timezone", () => { + const type = t.dateTime("UTC"); expect(type._tinybirdType).toBe("DateTime('UTC')"); }); - it('generates Bool type', () => { + it("generates Bool type", () => { const type = t.bool(); - expect(type._tinybirdType).toBe('Bool'); + expect(type._tinybirdType).toBe("Bool"); }); - it('generates UUID type', () => { + it("generates UUID type", () => { const type = t.uuid(); - expect(type._tinybirdType).toBe('UUID'); + expect(type._tinybirdType).toBe("UUID"); }); - it('generates Float64 type', () => { + it("generates Float64 type", () => { const type = t.float64(); - expect(type._tinybirdType).toBe('Float64'); + expect(type._tinybirdType).toBe("Float64"); }); - it('generates UInt64 type', () => { + it("generates UInt64 type", () => { const type = t.uint64(); - expect(type._tinybirdType).toBe('UInt64'); + expect(type._tinybirdType).toBe("UInt64"); }); }); - describe('Nullable modifier', () => { - it('wraps type in Nullable', () => { + describe("Nullable modifier", () => { + it("wraps type in Nullable", () => { const type = t.string().nullable(); - expect(type._tinybirdType).toBe('Nullable(String)'); + expect(type._tinybirdType).toBe("Nullable(String)"); }); - it('wraps Int32 in Nullable', () => { + it("wraps Int32 in Nullable", () => { const type = t.int32().nullable(); - expect(type._tinybirdType).toBe('Nullable(Int32)'); + expect(type._tinybirdType).toBe("Nullable(Int32)"); }); - it('sets nullable modifier', () => { + it("sets nullable modifier", () => { const type = t.string().nullable(); expect(type._modifiers.nullable).toBe(true); }); }); - describe('LowCardinality modifier', () => { - it('wraps type in LowCardinality', () => { + describe("LowCardinality modifier", () => { + it("wraps type in LowCardinality", () => { const type = t.string().lowCardinality(); - expect(type._tinybirdType).toBe('LowCardinality(String)'); + expect(type._tinybirdType).toBe("LowCardinality(String)"); }); - it('sets lowCardinality modifier', () => { + it("sets lowCardinality modifier", () => { const type = t.string().lowCardinality(); expect(type._modifiers.lowCardinality).toBe(true); }); }); - describe('LowCardinality + Nullable ordering', () => { - it('generates LowCardinality(Nullable(X)) when chaining .lowCardinality().nullable()', () => { + describe("LowCardinality + Nullable ordering", () => { + it("generates LowCardinality(Nullable(X)) when chaining .lowCardinality().nullable()", () => { const type = t.string().lowCardinality().nullable(); - expect(type._tinybirdType).toBe('LowCardinality(Nullable(String))'); + expect(type._tinybirdType).toBe("LowCardinality(Nullable(String))"); }); - it('generates LowCardinality(Nullable(X)) when chaining .nullable().lowCardinality()', () => { + it("generates LowCardinality(Nullable(X)) when chaining .nullable().lowCardinality()", () => { const type = t.string().nullable().lowCardinality(); - expect(type._tinybirdType).toBe('LowCardinality(Nullable(String))'); + expect(type._tinybirdType).toBe("LowCardinality(Nullable(String))"); }); - it('preserves both modifiers when chained', () => { + it("preserves both modifiers when chained", () => { const type = t.string().lowCardinality().nullable(); expect(type._modifiers.lowCardinality).toBe(true); expect(type._modifiers.nullable).toBe(true); }); }); - describe('Default values', () => { - it('sets hasDefault modifier', () => { - const type = t.string().default('test'); + describe("Default values", () => { + it("sets hasDefault modifier", () => { + const type = t.string().default("test"); expect(type._modifiers.hasDefault).toBe(true); }); - it('stores defaultValue in modifiers', () => { - const type = t.string().default('test'); - expect(type._modifiers.defaultValue).toBe('test'); + it("stores defaultValue in modifiers", () => { + const type = t.string().default("test"); + expect(type._modifiers.defaultValue).toBe("test"); }); - it('works with numeric defaults', () => { + it("works with numeric defaults", () => { const type = t.int32().default(42); expect(type._modifiers.defaultValue).toBe(42); }); }); - describe('Codec modifier', () => { - it('sets codec in modifiers', () => { - const type = t.string().codec('LZ4'); - expect(type._modifiers.codec).toBe('LZ4'); + describe("Codec modifier", () => { + it("sets codec in modifiers", () => { + const type = t.string().codec("LZ4"); + expect(type._modifiers.codec).toBe("LZ4"); }); }); - describe('jsonPath modifier', () => { - it('sets jsonPath in modifiers', () => { - const type = t.string().jsonPath('$.payload.id'); - expect(type._modifiers.jsonPath).toBe('$.payload.id'); + describe("jsonPath modifier", () => { + it("sets jsonPath in modifiers", () => { + const type = t.string().jsonPath("$.payload.id"); + expect(type._modifiers.jsonPath).toBe("$.payload.id"); }); - it('supports chaining with other modifiers', () => { - const type = t.string().nullable().jsonPath('$.user.name'); - expect(type._tinybirdType).toBe('Nullable(String)'); + it("supports chaining with other modifiers", () => { + const type = t.string().nullable().jsonPath("$.user.name"); + expect(type._tinybirdType).toBe("Nullable(String)"); expect(type._modifiers.nullable).toBe(true); - expect(type._modifiers.jsonPath).toBe('$.user.name'); + expect(type._modifiers.jsonPath).toBe("$.user.name"); }); }); - describe('Complex types', () => { - it('generates Array type', () => { + describe("Complex types", () => { + it("generates Array type", () => { const type = t.array(t.string()); - expect(type._tinybirdType).toBe('Array(String)'); + expect(type._tinybirdType).toBe("Array(String)"); }); - it('generates nested Array type', () => { + it("generates nested Array type", () => { const type = t.array(t.int32()); - expect(type._tinybirdType).toBe('Array(Int32)'); + expect(type._tinybirdType).toBe("Array(Int32)"); }); - it('generates Map type', () => { + it("generates Map type", () => { const type = t.map(t.string(), t.int32()); - expect(type._tinybirdType).toBe('Map(String, Int32)'); + expect(type._tinybirdType).toBe("Map(String, Int32)"); }); - it('generates Decimal type', () => { + it("generates Decimal type", () => { const type = t.decimal(10, 2); - expect(type._tinybirdType).toBe('Decimal(10, 2)'); + expect(type._tinybirdType).toBe("Decimal(10, 2)"); }); - it('generates FixedString type', () => { + it("generates FixedString type", () => { const type = t.fixedString(3); - expect(type._tinybirdType).toBe('FixedString(3)'); + expect(type._tinybirdType).toBe("FixedString(3)"); }); - it('generates Tuple type', () => { + it("generates Tuple type", () => { const type = t.tuple(t.string(), t.int32()); - expect(type._tinybirdType).toBe('Tuple(String, Int32)'); + expect(type._tinybirdType).toBe("Tuple(String, Int32)"); }); - it('generates DateTime64 type', () => { + it("generates DateTime64 type", () => { const type = t.dateTime64(3); - expect(type._tinybirdType).toBe('DateTime64(3)'); + expect(type._tinybirdType).toBe("DateTime64(3)"); }); - it('generates DateTime64 with timezone', () => { - const type = t.dateTime64(3, 'UTC'); + it("generates DateTime64 with timezone", () => { + const type = t.dateTime64(3, "UTC"); expect(type._tinybirdType).toBe("DateTime64(3, 'UTC')"); }); }); - describe('Helper functions', () => { - it('isTypeValidator returns true for validators', () => { + describe("Helper functions", () => { + it("isTypeValidator returns true for validators", () => { expect(isTypeValidator(t.string())).toBe(true); }); - it('isTypeValidator returns false for non-validators', () => { - expect(isTypeValidator('string')).toBe(false); + it("isTypeValidator returns false for non-validators", () => { + expect(isTypeValidator("string")).toBe(false); expect(isTypeValidator({})).toBe(false); expect(isTypeValidator(null)).toBe(false); }); - it('getTinybirdType returns type string', () => { - expect(getTinybirdType(t.string())).toBe('String'); + it("getTinybirdType returns type string", () => { + expect(getTinybirdType(t.string())).toBe("String"); }); - it('getModifiers returns modifiers object', () => { + it("getModifiers returns modifiers object", () => { const modifiers = getModifiers(t.string().nullable()); expect(modifiers.nullable).toBe(true); }); }); - describe('Chained modifiers', () => { - it('supports multiple modifiers', () => { - const type = t.string().lowCardinality().default('test'); - expect(type._tinybirdType).toBe('LowCardinality(String)'); + describe("Chained modifiers", () => { + it("supports multiple modifiers", () => { + const type = t.string().lowCardinality().default("test"); + expect(type._tinybirdType).toBe("LowCardinality(String)"); expect(type._modifiers.lowCardinality).toBe(true); expect(type._modifiers.hasDefault).toBe(true); - expect(type._modifiers.defaultValue).toBe('test'); + expect(type._modifiers.defaultValue).toBe("test"); }); }); - describe('Enum types', () => { - it('generates Enum8 with value mapping', () => { - const type = t.enum8('active', 'inactive', 'pending'); - expect(type._tinybirdType).toBe("Enum8('active' = 1, 'inactive' = 2, 'pending' = 3)"); + describe("Enum types", () => { + it("generates Enum8 with value mapping", () => { + const type = t.enum8("active", "inactive", "pending"); + expect(type._tinybirdType).toBe( + "Enum8('active' = 1, 'inactive' = 2, 'pending' = 3)", + ); }); - it('generates Enum16 with value mapping', () => { - const type = t.enum16('draft', 'published', 'archived'); - expect(type._tinybirdType).toBe("Enum16('draft' = 1, 'published' = 2, 'archived' = 3)"); + it("generates Enum16 with value mapping", () => { + const type = t.enum16("draft", "published", "archived"); + expect(type._tinybirdType).toBe( + "Enum16('draft' = 1, 'published' = 2, 'archived' = 3)", + ); }); - it('escapes single quotes in enum values', () => { - const type = t.enum8("it's ok", 'normal'); + it("escapes single quotes in enum values", () => { + const type = t.enum8("it's ok", "normal"); expect(type._tinybirdType).toBe("Enum8('it\\'s ok' = 1, 'normal' = 2)"); }); - it('handles single enum value', () => { - const type = t.enum8('only'); + it("handles single enum value", () => { + const type = t.enum8("only"); expect(type._tinybirdType).toBe("Enum8('only' = 1)"); }); }); + + describe("Custom type generics", () => { + // Branded/nominal type helpers for testing + type UserId = string & { readonly __brand: "UserId" }; + type TraceId = string & { readonly __brand: "TraceId" }; + type Timestamp = string & { readonly __brand: "Timestamp" }; + type Count = number & { readonly __brand: "Count" }; + type Price = number & { readonly __brand: "Price" }; + type BigId = bigint & { readonly __brand: "BigId" }; + type IsActive = boolean & { readonly __brand: "IsActive" }; + + describe("runtime behavior unchanged", () => { + it("string with generic produces same _tinybirdType", () => { + expect(t.string()._tinybirdType).toBe(t.string()._tinybirdType); + expect(t.string()._tinybirdType).toBe("String"); + }); + + it("int32 with generic produces same _tinybirdType", () => { + expect(t.int32()._tinybirdType).toBe(t.int32()._tinybirdType); + }); + + it("uuid with generic produces same _tinybirdType", () => { + expect(t.uuid()._tinybirdType).toBe("UUID"); + }); + + it("dateTime with generic produces same _tinybirdType", () => { + expect(t.dateTime()._tinybirdType).toBe("DateTime"); + expect(t.dateTime("UTC")._tinybirdType).toBe( + "DateTime('UTC')", + ); + }); + + it("bool with generic produces same _tinybirdType", () => { + expect(t.bool()._tinybirdType).toBe("Bool"); + }); + + it("int128 with generic produces same _tinybirdType", () => { + expect(t.int128()._tinybirdType).toBe("Int128"); + }); + + it("decimal with generic produces same _tinybirdType", () => { + expect(t.decimal(10, 2)._tinybirdType).toBe("Decimal(10, 2)"); + }); + + it("fixedString with generic produces same _tinybirdType", () => { + type CountryCode = string & { readonly __brand: "CountryCode" }; + expect(t.fixedString(2)._tinybirdType).toBe( + "FixedString(2)", + ); + }); + }); + + describe("modifiers work with custom generics", () => { + it("nullable", () => { + const v = t.string().nullable(); + expect(v._tinybirdType).toBe("Nullable(String)"); + expect(v._modifiers.nullable).toBe(true); + }); + + it("lowCardinality", () => { + expect(t.string().lowCardinality()._tinybirdType).toBe( + "LowCardinality(String)", + ); + }); + + it("default", () => { + const v = t.string().default("fallback" as UserId); + expect(v._modifiers.hasDefault).toBe(true); + expect(v._modifiers.defaultValue).toBe("fallback"); + }); + }); + + describe("type inference", () => { + it("validators without generics still infer base types", () => { + expectTypeOf(t.string()._type).toEqualTypeOf(); + expectTypeOf(t.int32()._type).toEqualTypeOf(); + expectTypeOf(t.bool()._type).toEqualTypeOf(); + expectTypeOf(t.int128()._type).toEqualTypeOf(); + expectTypeOf(t.uuid()._type).toEqualTypeOf(); + }); + + it("validators with generics infer the custom type", () => { + expectTypeOf(t.string()._type).toEqualTypeOf(); + expectTypeOf(t.uuid()._type).toEqualTypeOf(); + expectTypeOf(t.int32()._type).toEqualTypeOf(); + expectTypeOf(t.bool()._type).toEqualTypeOf(); + expectTypeOf(t.int128()._type).toEqualTypeOf(); + expectTypeOf(t.dateTime()._type).toEqualTypeOf(); + expectTypeOf( + t.dateTime("UTC")._type, + ).toEqualTypeOf(); + expectTypeOf( + t.dateTime64(3)._type, + ).toEqualTypeOf(); + expectTypeOf(t.decimal(10, 2)._type).toEqualTypeOf(); + }); + + it("custom types flow through nullable", () => { + expectTypeOf( + t.string().nullable()._type, + ).toEqualTypeOf(); + expectTypeOf( + t.int32().nullable()._type, + ).toEqualTypeOf(); + }); + + it("custom types flow through lowCardinality", () => { + expectTypeOf( + t.string().lowCardinality()._type, + ).toEqualTypeOf(); + }); + + it("custom types flow through InferRow", () => { + const ds = defineDatasource("test_custom_types", { + schema: { + user_id: t.string(), + event_count: t.int32(), + created_at: t.dateTime(), + name: t.string(), + }, + engine: engine.mergeTree({ sortingKey: ["user_id"] }), + }); + + type Row = InferRow; + + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + }); + + it("rejects generics that violate base type constraint", () => { + // @ts-expect-error - number does not extend string + t.string(); + + // @ts-expect-error - string does not extend number + t.int32(); + + // @ts-expect-error - string does not extend boolean + t.bool(); + + // @ts-expect-error - number does not extend bigint + t.int128(); + }); + }); + + describe("all validators accept custom generics", () => { + it("string-based validators", () => { + type S = string & { readonly __brand: "S" }; + expectTypeOf(t.string()._type).toEqualTypeOf(); + expectTypeOf(t.fixedString(10)._type).toEqualTypeOf(); + expectTypeOf(t.uuid()._type).toEqualTypeOf(); + expectTypeOf(t.ipv4()._type).toEqualTypeOf(); + expectTypeOf(t.ipv6()._type).toEqualTypeOf(); + expectTypeOf(t.date()._type).toEqualTypeOf(); + expectTypeOf(t.date32()._type).toEqualTypeOf(); + expectTypeOf(t.dateTime()._type).toEqualTypeOf(); + expectTypeOf(t.dateTime("UTC")._type).toEqualTypeOf(); + expectTypeOf(t.dateTime64()._type).toEqualTypeOf(); + expectTypeOf(t.dateTime64(6, "UTC")._type).toEqualTypeOf(); + }); + + it("number-based validators", () => { + type N = number & { readonly __brand: "N" }; + expectTypeOf(t.int8()._type).toEqualTypeOf(); + expectTypeOf(t.int16()._type).toEqualTypeOf(); + expectTypeOf(t.int32()._type).toEqualTypeOf(); + expectTypeOf(t.int64()._type).toEqualTypeOf(); + expectTypeOf(t.uint8()._type).toEqualTypeOf(); + expectTypeOf(t.uint16()._type).toEqualTypeOf(); + expectTypeOf(t.uint32()._type).toEqualTypeOf(); + expectTypeOf(t.uint64()._type).toEqualTypeOf(); + expectTypeOf(t.float32()._type).toEqualTypeOf(); + expectTypeOf(t.float64()._type).toEqualTypeOf(); + expectTypeOf(t.decimal(10, 2)._type).toEqualTypeOf(); + }); + + it("bigint-based validators", () => { + type B = bigint & { readonly __brand: "B" }; + expectTypeOf(t.int128()._type).toEqualTypeOf(); + expectTypeOf(t.int256()._type).toEqualTypeOf(); + expectTypeOf(t.uint128()._type).toEqualTypeOf(); + expectTypeOf(t.uint256()._type).toEqualTypeOf(); + }); + + it("boolean-based validators", () => { + type Bool = boolean & { readonly __brand: "Bool" }; + expectTypeOf(t.bool()._type).toEqualTypeOf(); + }); + }); + }); }); diff --git a/src/schema/types.ts b/src/schema/types.ts index 22e5f17..b3cf660 100644 --- a/src/schema/types.ts +++ b/src/schema/types.ts @@ -14,7 +14,7 @@ const VALIDATOR_BRAND = Symbol.for("tinybird.validator"); export interface TypeValidator< TType, TTinybirdType extends string = string, - TModifiers extends TypeModifiers = TypeModifiers + TModifiers extends TypeModifiers = TypeModifiers, > { readonly [VALIDATOR_BRAND]: true; /** The inferred TypeScript type */ @@ -25,15 +25,33 @@ export interface TypeValidator< readonly _modifiers: TModifiers; /** Make this column nullable */ - nullable(): TypeValidator; + nullable(): TypeValidator< + TType | null, + `Nullable(${TTinybirdType})`, + TModifiers & { nullable: true } + >; /** Apply LowCardinality optimization (for strings with few unique values) */ - lowCardinality(): TypeValidator; + lowCardinality(): TypeValidator< + TType, + `LowCardinality(${TTinybirdType})`, + TModifiers & { lowCardinality: true } + >; /** Set a default value for the column */ - default(value: TType): TypeValidator; + default( + value: TType, + ): TypeValidator< + TType, + TTinybirdType, + TModifiers & { hasDefault: true; defaultValue: TType } + >; /** Set a codec for compression */ - codec(codec: string): TypeValidator; + codec( + codec: string, + ): TypeValidator; /** Set an explicit JSON path for extraction (overrides autogenerated path) */ - jsonPath(path: string): TypeValidator; + jsonPath( + path: string, + ): TypeValidator; } export interface TypeModifiers { @@ -46,15 +64,18 @@ export interface TypeModifiers { } // Internal implementation -interface ValidatorImpl - extends TypeValidator { +interface ValidatorImpl< + TType, + TTinybirdType extends string, + TModifiers extends TypeModifiers, +> extends TypeValidator { readonly tinybirdType: TTinybirdType; readonly modifiers: TModifiers; } function createValidator( tinybirdType: TTinybirdType, - modifiers: TypeModifiers = {} + modifiers: TypeModifiers = {}, ): TypeValidator { const validator: ValidatorImpl = { [VALIDATOR_BRAND]: true, @@ -69,34 +90,53 @@ function createValidator( // 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 baseType = tinybirdType.replace(/^LowCardinality\((.+)\)$/, "$1"); const newType = `LowCardinality(Nullable(${baseType}))`; - return createValidator( - newType as `LowCardinality(Nullable(${string}))`, - { ...modifiers, nullable: true } - ) as unknown as TypeValidator; + return createValidator< + TType | null, + `LowCardinality(Nullable(${string}))` + >(newType as `LowCardinality(Nullable(${string}))`, { + ...modifiers, + nullable: true, + }) as unknown as TypeValidator< + TType | null, + `Nullable(${TTinybirdType})`, + TypeModifiers & { nullable: true } + >; } return createValidator( `Nullable(${tinybirdType})` as `Nullable(${TTinybirdType})`, - { ...modifiers, nullable: true } - ) as TypeValidator; + { ...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 baseType = tinybirdType.replace(/^Nullable\((.+)\)$/, "$1"); const newType = `LowCardinality(Nullable(${baseType}))`; return createValidator( newType as `LowCardinality(Nullable(${string}))`, - { ...modifiers, lowCardinality: true } - ) as unknown as TypeValidator; + { ...modifiers, lowCardinality: true }, + ) as unknown as TypeValidator< + TType, + `LowCardinality(${TTinybirdType})`, + TypeModifiers & { lowCardinality: true } + >; } return createValidator( `LowCardinality(${tinybirdType})` as `LowCardinality(${TTinybirdType})`, - { ...modifiers, lowCardinality: true } - ) as TypeValidator; + { ...modifiers, lowCardinality: true }, + ) as TypeValidator< + TType, + `LowCardinality(${TTinybirdType})`, + TypeModifiers & { lowCardinality: true } + >; }, default(value: TType) { @@ -104,21 +144,33 @@ function createValidator( ...modifiers, hasDefault: true, defaultValue: value, - }) as TypeValidator; + }) as TypeValidator< + TType, + TTinybirdType, + TypeModifiers & { hasDefault: true; defaultValue: TType } + >; }, codec(codec: string) { return createValidator(tinybirdType, { ...modifiers, codec, - }) as TypeValidator; + }) as TypeValidator< + TType, + TTinybirdType, + TypeModifiers & { codec: string } + >; }, jsonPath(path: string) { return createValidator(tinybirdType, { ...modifiers, jsonPath: path, - }) as TypeValidator; + }) as TypeValidator< + TType, + TTinybirdType, + TypeModifiers & { jsonPath: string } + >; }, }; @@ -139,116 +191,141 @@ function createValidator( * 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"), + string: () => + createValidator("String"), /** FixedString(N) - fixed length string, padded with null bytes */ - fixedString: (length: number) => - createValidator(`FixedString(${length})`), + fixedString: (length: number) => + createValidator(`FixedString(${length})`), /** UUID - 16-byte universally unique identifier */ - uuid: () => createValidator("UUID"), + uuid: () => createValidator("UUID"), // ============ Integer Types ============ /** Int8 - signed 8-bit integer (-128 to 127) */ - int8: () => createValidator("Int8"), + int8: () => createValidator("Int8"), /** Int16 - signed 16-bit integer */ - int16: () => createValidator("Int16"), + int16: () => createValidator("Int16"), /** Int32 - signed 32-bit integer */ - int32: () => createValidator("Int32"), + int32: () => createValidator("Int32"), /** Int64 - signed 64-bit integer (represented as number, may lose precision) */ - int64: () => createValidator("Int64"), + int64: () => createValidator("Int64"), /** Int128 - signed 128-bit integer (represented as bigint) */ - int128: () => createValidator("Int128"), + int128: () => + createValidator("Int128"), /** Int256 - signed 256-bit integer (represented as bigint) */ - int256: () => createValidator("Int256"), + int256: () => + createValidator("Int256"), /** UInt8 - unsigned 8-bit integer (0 to 255) */ - uint8: () => createValidator("UInt8"), + uint8: () => createValidator("UInt8"), /** UInt16 - unsigned 16-bit integer */ - uint16: () => createValidator("UInt16"), + uint16: () => + createValidator("UInt16"), /** UInt32 - unsigned 32-bit integer */ - uint32: () => createValidator("UInt32"), + uint32: () => + createValidator("UInt32"), /** UInt64 - unsigned 64-bit integer (represented as number, may lose precision) */ - uint64: () => createValidator("UInt64"), + uint64: () => + createValidator("UInt64"), /** UInt128 - unsigned 128-bit integer (represented as bigint) */ - uint128: () => createValidator("UInt128"), + uint128: () => + createValidator("UInt128"), /** UInt256 - unsigned 256-bit integer (represented as bigint) */ - uint256: () => createValidator("UInt256"), + uint256: () => + createValidator("UInt256"), // ============ Float Types ============ /** Float32 - 32-bit floating point */ - float32: () => createValidator("Float32"), + float32: () => + createValidator("Float32"), /** Float64 - 64-bit floating point (double precision) */ - float64: () => createValidator("Float64"), + float64: () => + createValidator("Float64"), /** Decimal(precision, scale) - fixed-point decimal number */ - decimal: (precision: number, scale: number) => - createValidator( - `Decimal(${precision}, ${scale})` + decimal: (precision: number, scale: number) => + createValidator( + `Decimal(${precision}, ${scale})`, ), // ============ Boolean ============ /** Bool - boolean value (true/false) */ - bool: () => createValidator("Bool"), + bool: () => createValidator("Bool"), // ============ Date/Time Types ============ /** Date - string in YYYY-MM-DD format (e.g. 2024-01-15) */ - date: () => createValidator("Date"), + date: () => createValidator("Date"), /** Date32 - string in YYYY-MM-DD format (e.g. 2024-01-15, extended date range) */ - date32: () => createValidator("Date32"), + date32: () => + createValidator("Date32"), /** DateTime - string in YYYY-MM-DD HH:MM:SS format (e.g. 2024-01-15 10:30:00) */ - dateTime: (timezone?: string) => + dateTime: (timezone?: string) => timezone - ? createValidator(`DateTime('${timezone}')`) - : createValidator("DateTime"), + ? 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) => + dateTime64: ( + precision: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 = 3, + timezone?: string, + ) => timezone - ? createValidator( - `DateTime64(${precision}, '${timezone}')` + ? createValidator( + `DateTime64(${precision}, '${timezone}')`, ) - : createValidator(`DateTime64(${precision})`), + : createValidator(`DateTime64(${precision})`), // ============ Complex Types ============ /** Array(T) - array of elements of type T */ array: >( - element: TElement + element: TElement, ): TypeValidator< TElement["_type"][], `Array(${TElement["_tinybirdType"]})`, TypeModifiers > => createValidator( - `Array(${element._tinybirdType})` as `Array(${TElement["_tinybirdType"]})` + `Array(${element._tinybirdType})` as `Array(${TElement["_tinybirdType"]})`, ), /** Tuple(T1, T2, ...) - tuple of heterogeneous types */ - tuple: []>( + tuple: < + TElements extends readonly TypeValidator[], + >( ...elements: TElements ): TypeValidator< { [K in keyof TElements]: TElements[K]["_type"] }, @@ -263,10 +340,10 @@ export const t = { /** Map(K, V) - dictionary/map type */ map: < TKey extends TypeValidator, - TValue extends TypeValidator + TValue extends TypeValidator, >( keyType: TKey, - valueType: TValue + valueType: TValue, ): TypeValidator< Map, `Map(${TKey["_tinybirdType"]}, ${TValue["_tinybirdType"]})`, @@ -288,7 +365,7 @@ export const t = { .map((v, i) => `'${v.replace(/'/g, "\\'")}' = ${i + 1}`) .join(", "); return createValidator( - `Enum8(${enumMapping})` as `Enum8(${string})` + `Enum8(${enumMapping})` as `Enum8(${string})`, ); }, @@ -298,27 +375,27 @@ export const t = { .map((v, i) => `'${v.replace(/'/g, "\\'")}' = ${i + 1}`) .join(", "); return createValidator( - `Enum16(${enumMapping})` as `Enum16(${string})` + `Enum16(${enumMapping})` as `Enum16(${string})`, ); }, // ============ Special Types ============ /** IPv4 - IPv4 address */ - ipv4: () => createValidator("IPv4"), + ipv4: () => createValidator("IPv4"), /** IPv6 - IPv6 address */ - ipv6: () => createValidator("IPv6"), + ipv6: () => createValidator("IPv6"), // ============ Aggregate Function States ============ /** SimpleAggregateFunction - for materialized views with simple aggregates */ simpleAggregateFunction: < TFunc extends string, - TType extends TypeValidator + TType extends TypeValidator, >( func: TFunc, - type: TType + type: TType, ): TypeValidator< TType["_type"], `SimpleAggregateFunction(${TFunc}, ${TType["_tinybirdType"]})`, @@ -332,10 +409,10 @@ export const t = { /** AggregateFunction - for materialized views with complex aggregates */ aggregateFunction: < TFunc extends string, - TType extends TypeValidator + TType extends TypeValidator, >( func: TFunc, - type: TType + type: TType, ): TypeValidator< TType["_type"], `AggregateFunction(${TFunc}, ${TType["_tinybirdType"]})`,