From 6c35e5126b5cd015d5e30bf01425d3fd778ef9ff Mon Sep 17 00:00:00 2001 From: gnzjgo Date: Wed, 25 Mar 2026 11:21:41 +0100 Subject: [PATCH] fix: don't set nullable modifier when nullable is embedded in LowCardinality type string When chaining .lowCardinality().nullable() or .nullable().lowCardinality(), the SDK correctly produces the type string LowCardinality(Nullable(X)). However, it also set nullable: true in the column modifiers metadata. The backend checks modifiers.nullable and wraps the type with Nullable() if the type string doesn't start with 'Nullable('. Since LowCardinality(Nullable(X)) starts with 'LowCardinality(', the backend would double-wrap it into Nullable(LowCardinality(Nullable(X))), which ClickHouse rejects. Fix: omit the nullable modifier flag when nullable is already encoded inside the LowCardinality type string wrapper. Co-authored-by: Amp Amp-Thread-ID: https://ampcode.com/threads/T-019d247b-1dfc-764a-ae09-697511e5209f --- src/generator/datasource.test.ts | 15 ++++++++++++++- src/schema/types.test.ts | 12 ++++++++++-- src/schema/types.ts | 4 ++-- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/generator/datasource.test.ts b/src/generator/datasource.test.ts index a1a477e..8b8a999 100644 --- a/src/generator/datasource.test.ts +++ b/src/generator/datasource.test.ts @@ -142,7 +142,7 @@ describe('Datasource Generator', () => { expect(result.content).toContain('country LowCardinality(String)'); }); - it('formats LowCardinality(Nullable) correctly', () => { + it('formats LowCardinality(Nullable) correctly with .lowCardinality().nullable()', () => { const ds = defineDatasource('test_ds', { schema: { country: t.string().lowCardinality().nullable(), @@ -151,6 +151,19 @@ describe('Datasource Generator', () => { const result = generateDatasource(ds); expect(result.content).toContain('country LowCardinality(Nullable(String))'); + expect(result.content).not.toContain('Nullable(LowCardinality'); + }); + + it('formats LowCardinality(Nullable) correctly with .nullable().lowCardinality()', () => { + const ds = defineDatasource('test_ds', { + schema: { + country: t.string().nullable().lowCardinality(), + }, + }); + + const result = generateDatasource(ds); + expect(result.content).toContain('country LowCardinality(Nullable(String))'); + expect(result.content).not.toContain('Nullable(LowCardinality'); }); it('includes default values', () => { diff --git a/src/schema/types.test.ts b/src/schema/types.test.ts index 60bda31..4a49386 100644 --- a/src/schema/types.test.ts +++ b/src/schema/types.test.ts @@ -87,10 +87,18 @@ describe("Type Validators (t.*)", () => { expect(type._tinybirdType).toBe("LowCardinality(Nullable(String))"); }); - it("preserves both modifiers when chained", () => { + 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).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))"); }); }); diff --git a/src/schema/types.ts b/src/schema/types.ts index 5eb05e4..80c89e9 100644 --- a/src/schema/types.ts +++ b/src/schema/types.ts @@ -106,7 +106,6 @@ function createValidator( `LowCardinality(Nullable(${string}))` >(newType as `LowCardinality(Nullable(${string}))`, { ...modifiers, - nullable: true, }) as unknown as TypeValidator< TType | null, `Nullable(${TTinybirdType})`, @@ -129,9 +128,10 @@ function createValidator( // 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}))`, - { ...modifiers, lowCardinality: true }, + { ...rest, lowCardinality: true }, ) as unknown as TypeValidator< TType, `LowCardinality(${TTinybirdType})`,