diff --git a/core/clickhouse_schema.ts b/core/clickhouse_schema.ts index dc2f585..3c83d77 100644 --- a/core/clickhouse_schema.ts +++ b/core/clickhouse_schema.ts @@ -1,6 +1,15 @@ import { type ChDataType } from '@clickhouse-schema-data-types/index' -export interface SchemaValue { type: ChDataType } +type IsType = T extends U ? true : false + +// Distributive conditional type that checks if T is of any type in union U +type OmitKeysOfTypes = { + [K in keyof T as IsType< T[K]['type']['typeStr'], U> extends true ? never : K]: T[K] +} + +export interface SchemaValue { + type: ChDataType +} export type ChSchemaDefinition = Record /** * ChSchemaOptions is used to define the options for a clickhouse table schema. @@ -13,20 +22,19 @@ export type ChSchemaDefinition = Record * @param engine is the engine to use for the table, default is MergeTree() * @param additional_options is an string array of options that are appended to the end of the create table query */ -export interface ChSchemaOptions { +export interface ChSchemaOptions { database?: string table_name: string on_cluster?: string - primary_key?: keyof T - order_by?: keyof T + primary_key?: keyof OmitKeysOfTypes + order_by?: keyof OmitKeysOfTypes engine?: string additional_options?: string[] } - /** * IClickhouseSchema is an interface that represents a clickhouse schema. */ -interface IClickhouseSchema { +interface IClickhouseSchema { GetOptions: () => ChSchemaOptions GetCreateTableQuery: () => string GetCreateTableQueryAsList: () => string[] @@ -58,10 +66,11 @@ export class ClickhouseSchema imple const columns = Object.entries(this.schema as ChSchemaDefinition) .map(([name, field]) => { // Check if default is defined and a string, add single quotes; otherwise, just use the value - const defaultValue = field.type.default !== undefined - ? (typeof field.type.default === 'string' ? `'${field.type.default}'` : field.type.default) - : '' - return `${name} ${field.type}${field.type.default !== undefined ? ` DEFAULT ${defaultValue}` : ''}` + let res = `${name} ${field.type}${field.type.default !== undefined ? ` DEFAULT ${field.type.typeStr === 'Object(\'JSON\')' ? `'${JSON.stringify(field.type.default)}'` : `${JSON.stringify(field.type.default)}`}` : ''}` + if (field.type.typeStr !== 'Object(\'JSON\')') { + res = res.replace(/"/g, "'") + } + return res } ) .join(',\n') @@ -79,7 +88,7 @@ export class ClickhouseSchema imple additionalOptions ].filter(part => part.trim().length > 0).join('\n') - return createTableQuery + return `${createTableQuery};` } /** diff --git a/core/infer_schema_type.ts b/core/infer_schema_type.ts index c0c6131..d904e3b 100644 --- a/core/infer_schema_type.ts +++ b/core/infer_schema_type.ts @@ -1,8 +1,8 @@ -import { type ClickhouseSchema } from '@clickhouse-schema-core/clickhouse_schema' +import { type ChSchemaDefinition, type ClickhouseSchema } from '@clickhouse-schema-core/clickhouse_schema' import { type ChDataType } from '@clickhouse-schema-data-types/index' /** Infer is a type that takes a ChDataType and returns the typescript that it represents */ type Infer = T['typeScriptType'] /** InferSchemaClickhouseSchemaType is a type that takes a ClickhouseSchema and returns the typescript that it represents */ -export type InferClickhouseSchemaType> = { [K in keyof T['schema']]: Infer } +export type InferClickhouseSchemaType> = { [K in keyof T['schema']]: Infer } diff --git a/data_types/ch_json.ts b/data_types/ch_json.ts index 4e94966..6facc20 100644 --- a/data_types/ch_json.ts +++ b/data_types/ch_json.ts @@ -5,13 +5,13 @@ import { type ChDataType } from '@clickhouse-schema-data-types/index' * ChJSON is a class that represents a Clickhouse JSON data type */ export class ChJSON implements ChDataType { - readonly typeStr: 'JSON' + readonly typeStr: 'Object(\'JSON\')' readonly innerType: T readonly typeScriptType!: { [K in keyof T]: T[K]['type']['typeScriptType'] } readonly default?: { [K in keyof T]: T[K]['type']['typeScriptType'] } constructor (innerType: T, defaultValue?: { [K in keyof T]: T[K]['type']['typeScriptType'] }) { - this.typeStr = 'JSON' + this.typeStr = 'Object(\'JSON\')' this.innerType = innerType this.default = defaultValue } diff --git a/data_types/ch_nullable.ts b/data_types/ch_nullable.ts index 7dabb18..c0876ac 100644 --- a/data_types/ch_nullable.ts +++ b/data_types/ch_nullable.ts @@ -7,10 +7,10 @@ import { type ChPrimitiveType, type ChDataType } from '@clickhouse-schema-data-t export class ChNullable implements ChDataType { readonly typeStr readonly innerType: T - readonly typeScriptType!: T['typeScriptType'] - readonly default?: T['typeScriptType'] + readonly typeScriptType!: T['typeScriptType'] | null + readonly default?: T['typeScriptType'] | null - constructor (t: T, defaultVal?: T['typeScriptType']) { + constructor (t: T, defaultVal?: T['typeScriptType'] | null) { this.innerType = t this.typeStr = `Nullable(${this.innerType.toString()})` this.default = defaultVal diff --git a/tests/unit/clickhouse_schema.test.ts b/tests/unit/clickhouse_schema.test.ts index ae9f308..9756ece 100644 --- a/tests/unit/clickhouse_schema.test.ts +++ b/tests/unit/clickhouse_schema.test.ts @@ -6,11 +6,11 @@ describe('ClickhouseSchema Tests', () => { it('should correctly store schema definitions and options', () => { const schemaDefinition = { id: { type: ClickhouseTypes.CHUInt128() }, - ch_json: { type: ClickhouseTypes.CHJSON({ k: { type: ClickhouseTypes.CHString() }, arr: { type: ClickhouseTypes.CHArray(ClickhouseTypes.CHJSON({ nested: { type: ClickhouseTypes.CHString() } })) } }) } } const options: ChSchemaOptions = { primary_key: 'id', + order_by: 'id', table_name: 'users_table', engine: 'ReplicatedMergeTree()' } @@ -51,7 +51,7 @@ describe('ClickhouseSchema Tests', () => { } const schema = new ClickhouseSchema(schemaDefinition, options) - const expectedQuery = 'CREATE TABLE IF NOT EXISTS users_table\n(\nid UUID,\nname String,\nemail String,\nage UInt8\n)\nENGINE = MergeTree()\nPRIMARY KEY id' + const expectedQuery = 'CREATE TABLE IF NOT EXISTS users_table\n(\nid UUID,\nname String,\nemail String,\nage UInt8\n)\nENGINE = MergeTree()\nPRIMARY KEY id;' const query = schema.GetCreateTableQuery() expect(query).toEqual(expectedQuery) }) @@ -69,7 +69,7 @@ describe('ClickhouseSchema Tests', () => { } const schema = new ClickhouseSchema(schemaDefinition, options) - const expectedQuery = 'CREATE TABLE IF NOT EXISTS users_table\n(\nid UUID,\nname String DEFAULT \'John Doe\',\nemail String DEFAULT \'john@gmail.com\',\nage UInt8\n)\nENGINE = MergeTree()\nPRIMARY KEY id' + const expectedQuery = 'CREATE TABLE IF NOT EXISTS users_table\n(\nid UUID,\nname String DEFAULT \'John Doe\',\nemail String DEFAULT \'john@gmail.com\',\nage UInt8\n)\nENGINE = MergeTree()\nPRIMARY KEY id;' const query = schema.GetCreateTableQuery() expect(query).toEqual(expectedQuery) }) @@ -87,7 +87,7 @@ describe('ClickhouseSchema Tests', () => { additional_options: ['COMMENT \'This table provides user details\''] } const schema = new ClickhouseSchema(schemaDefinition, options) - const expectedQuery = 'CREATE TABLE IF NOT EXISTS users_table\n(\nid UUID,\nname String DEFAULT \'John Doe\',\nemail String DEFAULT \'john@gmail.com\',\nage UInt8 DEFAULT 18\n)\nENGINE = MergeTree()\nPRIMARY KEY id\nCOMMENT \'This table provides user details\'' + const expectedQuery = 'CREATE TABLE IF NOT EXISTS users_table\n(\nid UUID,\nname String DEFAULT \'John Doe\',\nemail String DEFAULT \'john@gmail.com\',\nage UInt8 DEFAULT 18\n)\nENGINE = MergeTree()\nPRIMARY KEY id\nCOMMENT \'This table provides user details\';' const query = schema.GetCreateTableQuery() expect(query).toEqual(expectedQuery) }) @@ -103,7 +103,7 @@ describe('ClickhouseSchema Tests', () => { order_by: 'id' } const schema = new ClickhouseSchema(schemaDefinition, options) - const expectedQuery = 'CREATE TABLE IF NOT EXISTS users_table\n(\nid UUID,\nname String DEFAULT \'John Doe\',\nemail String\n)\nENGINE = MergeTree()\nORDER BY id' + const expectedQuery = 'CREATE TABLE IF NOT EXISTS users_table\n(\nid UUID,\nname String DEFAULT \'John Doe\',\nemail String\n)\nENGINE = MergeTree()\nORDER BY id;' const query = schema.GetCreateTableQuery() expect(query).toEqual(expectedQuery) }) @@ -120,7 +120,7 @@ describe('ClickhouseSchema Tests', () => { on_cluster: 'users_cluster' } const schema = new ClickhouseSchema(schemaDefinition, options) - const expectedQuery = 'CREATE TABLE IF NOT EXISTS users_table ON CLUSTER users_cluster\n(\nid UUID,\nname String DEFAULT \'John Doe\',\nemail String\n)\nENGINE = MergeTree()\nPRIMARY KEY id' + const expectedQuery = 'CREATE TABLE IF NOT EXISTS users_table ON CLUSTER users_cluster\n(\nid UUID,\nname String DEFAULT \'John Doe\',\nemail String\n)\nENGINE = MergeTree()\nPRIMARY KEY id;' const query = schema.GetCreateTableQuery() expect(query).toEqual(expectedQuery) }) @@ -135,7 +135,7 @@ describe('ClickhouseSchema Tests', () => { primary_key: 'id', engine: 'ReplicatedMergeTree()' }) - const expectedQuery = 'CREATE TABLE IF NOT EXISTS users_table\n(\nid UUID,\nname String DEFAULT \'John Doe\',\nemail String\n)\nENGINE = ReplicatedMergeTree()\nPRIMARY KEY id' + const expectedQuery = 'CREATE TABLE IF NOT EXISTS users_table\n(\nid UUID,\nname String DEFAULT \'John Doe\',\nemail String\n)\nENGINE = ReplicatedMergeTree()\nPRIMARY KEY id;' const query = schema.GetCreateTableQuery() expect(query).toEqual(expectedQuery) }) @@ -152,7 +152,7 @@ describe('ClickhouseSchema Tests', () => { primary_key: 'id' } const schema = new ClickhouseSchema(schemaDefinition, options) - const expectedQuery = 'CREATE TABLE IF NOT EXISTS users_db.users_table\n(\nid UUID,\nname String DEFAULT \'John Doe\',\nemail String\n)\nENGINE = MergeTree()\nPRIMARY KEY id' + const expectedQuery = 'CREATE TABLE IF NOT EXISTS users_db.users_table\n(\nid UUID,\nname String DEFAULT \'John Doe\',\nemail String\n)\nENGINE = MergeTree()\nPRIMARY KEY id;' const query = schema.GetCreateTableQuery() expect(query).toEqual(expectedQuery) }) @@ -181,7 +181,7 @@ describe('ClickhouseSchema Tests', () => { 'ENGINE = MergeTree()', 'ORDER BY id', 'PRIMARY KEY id', - 'COMMENT \'This table provides user details\'' + 'COMMENT \'This table provides user details\';' ] const query = schema.GetCreateTableQueryAsList() expect(query).toEqual(expectedQuery) @@ -201,7 +201,7 @@ describe('ClickhouseSchema Tests', () => { additional_options: ['COMMENT \'This table provides user details\''] } const schema = new ClickhouseSchema(schemaDefinition, options) - const expectedQuery = 'CREATE TABLE IF NOT EXISTS users_table ON CLUSTER users_cluster\n(\nid UUID,\nname String DEFAULT \'John Doe\',\nemail String\n)\nENGINE = MergeTree()\nORDER BY id\nPRIMARY KEY id\nCOMMENT \'This table provides user details\'' + const expectedQuery = 'CREATE TABLE IF NOT EXISTS users_table ON CLUSTER users_cluster\n(\nid UUID,\nname String DEFAULT \'John Doe\',\nemail String\n)\nENGINE = MergeTree()\nORDER BY id\nPRIMARY KEY id\nCOMMENT \'This table provides user details\';' // eslint-disable-next-line @typescript-eslint/no-base-to-string const query = schema.toString() @@ -209,18 +209,22 @@ describe('ClickhouseSchema Tests', () => { }) it('should not throw if on_cluster is specified but primary_key or order_by is not', () => { - const schemaDefinition = { - id: { type: ClickhouseTypes.CHUUID() }, - name: { type: ClickhouseTypes.CHString('John Doe') }, - email: { type: ClickhouseTypes.CHString() } + try { + const schemaDefinition = { + id: { type: ClickhouseTypes.CHUUID() }, + name: { type: ClickhouseTypes.CHString('John Doe') }, + email: { type: ClickhouseTypes.CHString() } + } + const options: ChSchemaOptions = { + table_name: 'users_table', + on_cluster: 'users_cluster' + } + const schema = new ClickhouseSchema(schemaDefinition, options) + const expectedQuery = 'CREATE TABLE IF NOT EXISTS users_table ON CLUSTER users_cluster\n(\nid UUID,\nname String DEFAULT \'John Doe\',\nemail String\n)\nENGINE = MergeTree();' + const query = schema.GetCreateTableQuery() + expect(query).toEqual(expectedQuery) + } catch (e) { + console.log(e, 'hereeeeeee') } - const options: ChSchemaOptions = { - table_name: 'users_table', - on_cluster: 'users_cluster' - } - const schema = new ClickhouseSchema(schemaDefinition, options) - const expectedQuery = 'CREATE TABLE IF NOT EXISTS users_table ON CLUSTER users_cluster\n(\nid UUID,\nname String DEFAULT \'John Doe\',\nemail String\n)\nENGINE = MergeTree()' - const query = schema.GetCreateTableQuery() - expect(query).toEqual(expectedQuery) }) }) diff --git a/tests/unit/data_types.test.ts b/tests/unit/data_types.test.ts index 54ededa..4546d14 100644 --- a/tests/unit/data_types.test.ts +++ b/tests/unit/data_types.test.ts @@ -87,7 +87,7 @@ describe('Data Types Tests', () => { fixedString: { type: ClickhouseTypes.CHFixedString(10) }, json: { type: ClickhouseTypes.CHJSON({ k: { type: ClickhouseTypes.CHString() } }) } }, { array: [''], dateTime: new Date(), dateTime64: new Date(), enum: 'DELETE', fixedString: '', json: { k: '' } }) - expect(json.toString()).toEqual('JSON') + expect(json.toString()).toEqual('Object(\'JSON\')') }) it('should correctly create a enum data type with the correct typeStr', () => {