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"); }); it("generates Int32 type", () => { const type = t.int32(); expect(type._tinybirdType).toBe("Int32"); }); it("generates DateTime type", () => { const type = t.dateTime(); expect(type._tinybirdType).toBe("DateTime"); }); it("generates DateTime with timezone", () => { const type = t.dateTime("UTC"); expect(type._tinybirdType).toBe("DateTime('UTC')"); }); it("generates Bool type", () => { const type = t.bool(); expect(type._tinybirdType).toBe("Bool"); }); it("generates UUID type", () => { const type = t.uuid(); expect(type._tinybirdType).toBe("UUID"); }); it("generates Float64 type", () => { const type = t.float64(); expect(type._tinybirdType).toBe("Float64"); }); it("generates UInt64 type", () => { const type = t.uint64(); expect(type._tinybirdType).toBe("UInt64"); }); }); describe("Nullable modifier", () => { it("wraps type in Nullable", () => { const type = t.string().nullable(); expect(type._tinybirdType).toBe("Nullable(String)"); }); it("wraps Int32 in Nullable", () => { const type = t.int32().nullable(); expect(type._tinybirdType).toBe("Nullable(Int32)"); }); it("sets nullable modifier", () => { const type = t.string().nullable(); expect(type._modifiers.nullable).toBe(true); }); }); describe("Optional modifier", () => { it("marks field as optional without changing the Tinybird type", () => { const type = t.string().optional(); expect(type._tinybirdType).toBe("String"); expect(type._modifiers.optional).toBe(true); }); it("can be combined with nullable fields", () => { const type = t.string().nullable().optional(); expect(type._tinybirdType).toBe("Nullable(String)"); expect(type._modifiers.nullable).toBe(true); expect(type._modifiers.optional).toBe(true); }); }); describe("LowCardinality modifier", () => { it("wraps type in LowCardinality", () => { const type = t.string().lowCardinality(); expect(type._tinybirdType).toBe("LowCardinality(String)"); }); 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()", () => { const type = t.string().lowCardinality().nullable(); expect(type._tinybirdType).toBe("LowCardinality(Nullable(String))"); }); it("generates LowCardinality(Nullable(X)) when chaining .nullable().lowCardinality()", () => { const type = t.string().nullable().lowCardinality(); expect(type._tinybirdType).toBe("LowCardinality(Nullable(String))"); }); it("preserves lowCardinality modifier and omits nullable when combined (nullable is in the type string)", () => { const type = t.string().lowCardinality().nullable(); expect(type._modifiers.lowCardinality).toBe(true); expect(type._modifiers.nullable).toBeUndefined(); expect(type._tinybirdType).toBe("LowCardinality(Nullable(String))"); }); it("omits nullable modifier when nullable().lowCardinality() is chained", () => { const type = t.string().nullable().lowCardinality(); expect(type._modifiers.lowCardinality).toBe(true); expect(type._modifiers.nullable).toBeUndefined(); expect(type._tinybirdType).toBe("LowCardinality(Nullable(String))"); }); }); 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("works with numeric defaults", () => { const type = t.int32().default(42); expect(type._modifiers.defaultValue).toBe(42); }); it("stores default SQL expression in modifiers", () => { const type = t.uuid().defaultExpr("generateUUIDv4()"); expect(type._modifiers.hasDefault).toBe(true); expect(type._modifiers.defaultExpression).toBe("generateUUIDv4()"); expect(type._modifiers.defaultValue).toBeUndefined(); }); it("trims default SQL expression", () => { const type = t.uuid().defaultExpr(" generateUUIDv4() "); expect(type._modifiers.defaultExpression).toBe("generateUUIDv4()"); }); it("throws on empty default SQL expression", () => { expect(() => t.uuid().defaultExpr(" ")).toThrow( "Default expression cannot be empty." ); }); }); 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"); }); 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"); }); }); describe("Complex types", () => { it("generates Array type", () => { const type = t.array(t.string()); expect(type._tinybirdType).toBe("Array(String)"); }); it("generates nested Array type", () => { const type = t.array(t.int32()); expect(type._tinybirdType).toBe("Array(Int32)"); }); it("generates Map type", () => { const type = t.map(t.string(), t.int32()); expect(type._tinybirdType).toBe("Map(String, Int32)"); }); it("generates Decimal type", () => { const type = t.decimal(10, 2); expect(type._tinybirdType).toBe("Decimal(10, 2)"); }); it("generates FixedString type", () => { const type = t.fixedString(3); expect(type._tinybirdType).toBe("FixedString(3)"); }); it("generates Tuple type", () => { const type = t.tuple(t.string(), t.int32()); expect(type._tinybirdType).toBe("Tuple(String, Int32)"); }); it("generates DateTime64 type", () => { const type = t.dateTime64(3); expect(type._tinybirdType).toBe("DateTime64(3)"); }); it("generates DateTime64 with timezone", () => { const type = t.dateTime64(3, "UTC"); expect(type._tinybirdType).toBe("DateTime64(3, 'UTC')"); }); }); describe("Aggregate function types", () => { it("generates AggregateFunction with an explicit state type", () => { const type = t.aggregateFunction("uniq", t.string()); expect(type._tinybirdType).toBe("AggregateFunction(uniq, String)"); expectTypeOf(type._type).toEqualTypeOf(); }); it("generates AggregateFunction with multiple explicit state types", () => { const type = t.aggregateFunction("argMax", t.string(), t.dateTime()); expect(type._tinybirdType).toBe("AggregateFunction(argMax, String, DateTime)"); expectTypeOf(type._type).toEqualTypeOf(); }); it("generates AggregateFunction with multiple explicit state types for other functions", () => { expect(t.aggregateFunction("argMin", t.float64(), t.dateTime())._tinybirdType).toBe( "AggregateFunction(argMin, Float64, DateTime)" ); expect(t.aggregateFunction("corr", t.float64(), t.float64())._tinybirdType).toBe( "AggregateFunction(corr, Float64, Float64)" ); expect(t.aggregateFunction("sumMap", t.array(t.string()), t.array(t.uint64()))._tinybirdType).toBe( "AggregateFunction(sumMap, Array(String), Array(UInt64))" ); }); it("generates AggregateFunction with function parameters and multiple state types", () => { const type = t.aggregateFunction( "sequenceMatch('(?1)(?2)')", t.dateTime(), t.uint8(), t.uint8() ); expect(type._tinybirdType).toBe( "AggregateFunction(sequenceMatch('(?1)(?2)'), DateTime, UInt8, UInt8)" ); }); it("generates AggregateFunction without an explicit state type", () => { const type = t.aggregateFunction("count"); expect(type._tinybirdType).toBe("AggregateFunction(count)"); expectTypeOf(type._type).toEqualTypeOf(); }); }); 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); expect(isTypeValidator({})).toBe(false); expect(isTypeValidator(null)).toBe(false); }); it("getTinybirdType returns type string", () => { expect(getTinybirdType(t.string())).toBe("String"); }); 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)"); expect(type._modifiers.lowCardinality).toBe(true); expect(type._modifiers.hasDefault).toBe(true); 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)", ); }); 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"); expect(type._tinybirdType).toBe("Enum8('it\\'s ok' = 1, 'normal' = 2)"); }); 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(); }); }); }); });