@housekit/orm 0.1.47 → 0.1.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/README.md +120 -5
  2. package/dist/builders/delete.js +112 -0
  3. package/dist/builders/insert.d.ts +0 -91
  4. package/dist/builders/insert.js +393 -0
  5. package/dist/builders/prepared.d.ts +1 -2
  6. package/dist/builders/prepared.js +30 -0
  7. package/dist/builders/select.d.ts +0 -161
  8. package/dist/builders/select.js +562 -0
  9. package/dist/builders/select.types.js +1 -0
  10. package/dist/builders/update.js +136 -0
  11. package/dist/client.d.ts +0 -6
  12. package/dist/client.js +140 -0
  13. package/dist/codegen/zod.js +107 -0
  14. package/dist/column.d.ts +1 -25
  15. package/dist/column.js +133 -0
  16. package/dist/compiler.d.ts +0 -7
  17. package/dist/compiler.js +513 -0
  18. package/dist/core.js +6 -0
  19. package/dist/data-types.d.ts +0 -61
  20. package/dist/data-types.js +127 -0
  21. package/dist/dictionary.d.ts +0 -149
  22. package/dist/dictionary.js +158 -0
  23. package/dist/engines.d.ts +0 -385
  24. package/dist/engines.js +292 -0
  25. package/dist/expressions.d.ts +0 -10
  26. package/dist/expressions.js +268 -0
  27. package/dist/external.d.ts +0 -112
  28. package/dist/external.js +224 -0
  29. package/dist/index.d.ts +0 -51
  30. package/dist/index.js +139 -6853
  31. package/dist/logger.js +36 -0
  32. package/dist/materialized-views.d.ts +0 -188
  33. package/dist/materialized-views.js +380 -0
  34. package/dist/metadata.js +59 -0
  35. package/dist/modules/aggregates.d.ts +0 -164
  36. package/dist/modules/aggregates.js +121 -0
  37. package/dist/modules/array.d.ts +0 -98
  38. package/dist/modules/array.js +71 -0
  39. package/dist/modules/conditional.d.ts +0 -84
  40. package/dist/modules/conditional.js +138 -0
  41. package/dist/modules/conversion.d.ts +0 -147
  42. package/dist/modules/conversion.js +109 -0
  43. package/dist/modules/geo.d.ts +0 -164
  44. package/dist/modules/geo.js +112 -0
  45. package/dist/modules/hash.js +4 -0
  46. package/dist/modules/index.js +12 -0
  47. package/dist/modules/json.d.ts +0 -106
  48. package/dist/modules/json.js +76 -0
  49. package/dist/modules/math.d.ts +0 -16
  50. package/dist/modules/math.js +16 -0
  51. package/dist/modules/string.d.ts +0 -136
  52. package/dist/modules/string.js +89 -0
  53. package/dist/modules/time.d.ts +0 -123
  54. package/dist/modules/time.js +91 -0
  55. package/dist/modules/types.d.ts +0 -133
  56. package/dist/modules/types.js +114 -0
  57. package/dist/modules/window.js +140 -0
  58. package/dist/relational.d.ts +0 -82
  59. package/dist/relational.js +290 -0
  60. package/dist/relations.js +21 -0
  61. package/dist/schema-builder.d.ts +0 -90
  62. package/dist/schema-builder.js +140 -0
  63. package/dist/table.d.ts +0 -42
  64. package/dist/table.js +406 -0
  65. package/dist/utils/background-batcher.js +75 -0
  66. package/dist/utils/batch-transform.js +51 -0
  67. package/dist/utils/binary-reader.d.ts +0 -6
  68. package/dist/utils/binary-reader.js +334 -0
  69. package/dist/utils/binary-serializer.d.ts +0 -125
  70. package/dist/utils/binary-serializer.js +637 -0
  71. package/dist/utils/binary-worker-code.js +1 -0
  72. package/dist/utils/binary-worker-pool.d.ts +0 -34
  73. package/dist/utils/binary-worker-pool.js +206 -0
  74. package/dist/utils/binary-worker.d.ts +0 -11
  75. package/dist/utils/binary-worker.js +63 -0
  76. package/dist/utils/insert-processing.d.ts +0 -2
  77. package/dist/utils/insert-processing.js +163 -0
  78. package/dist/utils/lru-cache.js +30 -0
  79. package/package.json +68 -3
package/dist/client.d.ts CHANGED
@@ -7,21 +7,15 @@ import { type TableDefinition, type TableRuntime, type CleanInsert } from './cor
7
7
  import { type HousekitLogger } from './logger';
8
8
  import { type RelationalAPI } from './relational';
9
9
  interface ConnectionPoolConfig {
10
- /** Maximum concurrent sockets (default: 100) */
11
10
  maxSockets?: number;
12
- /** Keep connections alive (default: true) */
13
11
  keepAlive?: boolean;
14
- /** Keep-alive initial delay in ms (default: 1000) */
15
12
  keepAliveInitialDelay?: number;
16
- /** Socket timeout in ms (default: 30000) */
17
13
  timeout?: number;
18
14
  }
19
15
  export type HousekitClientConfig = ClickHouseClientConfigOptions & {
20
16
  schema?: Record<string, TableDefinition<any>>;
21
17
  logger?: HousekitLogger;
22
- /** Connection pool configuration */
23
18
  pool?: ConnectionPoolConfig;
24
- /** Skip validation for maximum insert performance */
25
19
  skipValidation?: boolean;
26
20
  };
27
21
  export type ClientConfigWithSchema = HousekitClientConfig;
package/dist/client.js ADDED
@@ -0,0 +1,140 @@
1
+ import { createClient as createChClient } from '@clickhouse/client';
2
+ import { Readable } from 'stream';
3
+ import http from 'http';
4
+ import https from 'https';
5
+ import { ClickHouseQueryBuilder } from './builders/select';
6
+ import { ClickHouseInsertBuilder } from './builders/insert';
7
+ import { ClickHouseDeleteBuilder } from './builders/delete';
8
+ import { ClickHouseUpdateBuilder } from './builders/update';
9
+ import { buildInsertPlan, processRowsStream } from './utils/insert-processing';
10
+ import { wrapClientWithLogger } from './logger';
11
+ import { buildRelationalAPI } from './relational';
12
+ const agentPool = new Map();
13
+ function getOrCreateAgent(url, config = {}) {
14
+ const isHttps = url.startsWith('https');
15
+ const key = `${url}-${config.maxSockets ?? 100}`;
16
+ let agent = agentPool.get(key);
17
+ if (agent)
18
+ return agent;
19
+ const agentConfig = {
20
+ keepAlive: config.keepAlive ?? true,
21
+ keepAliveMsecs: config.keepAliveInitialDelay ?? 1000,
22
+ maxSockets: config.maxSockets ?? 100,
23
+ maxFreeSockets: Math.floor((config.maxSockets ?? 100) / 2),
24
+ timeout: config.timeout ?? 30000,
25
+ };
26
+ agent = isHttps
27
+ ? new https.Agent(agentConfig)
28
+ : new http.Agent(agentConfig);
29
+ agentPool.set(key, agent);
30
+ return agent;
31
+ }
32
+ export function createHousekitClient(config) {
33
+ const { schema, logger, pool, skipValidation, ...configRest } = config;
34
+ const normalizedConfig = { ...configRest };
35
+ if ('host' in normalizedConfig && !('url' in normalizedConfig)) {
36
+ const host = normalizedConfig.host;
37
+ normalizedConfig.url = host.startsWith('http://') || host.startsWith('https://')
38
+ ? host
39
+ : `http://${host}`;
40
+ delete normalizedConfig.host;
41
+ }
42
+ const urlStr = normalizedConfig.url?.toString() ?? '';
43
+ const keepAliveAgent = getOrCreateAgent(urlStr, pool);
44
+ const clientConfig = {
45
+ ...normalizedConfig,
46
+ clickhouse_settings: {
47
+ enable_http_compression: 1,
48
+ ...normalizedConfig.clickhouse_settings
49
+ }
50
+ };
51
+ clientConfig.http_agent = keepAliveAgent;
52
+ const baseChClient = createChClient(clientConfig);
53
+ const client = wrapClientWithLogger(baseChClient, logger);
54
+ const tableExists = async (tableName) => {
55
+ const result = await client.query({
56
+ query: `EXISTS TABLE \`${tableName}\``,
57
+ format: 'JSONEachRow'
58
+ });
59
+ const rows = await result.json();
60
+ const exists = rows[0]?.result === 1 || rows[0]?.exists === 1;
61
+ return Boolean(exists);
62
+ };
63
+ const ensureTable = async (table) => {
64
+ const exists = await tableExists(table.$table);
65
+ if (!exists) {
66
+ await client.query({ query: table.toSQL() });
67
+ }
68
+ };
69
+ const dropTable = async (tableName, options) => {
70
+ const clause = options?.ifExists === false ? '' : ' IF EXISTS';
71
+ await client.query({ query: `DROP TABLE${clause} \`${tableName}\`` });
72
+ };
73
+ const binaryInsertConfig = {
74
+ url: normalizedConfig.url?.toString() || 'http://localhost:8123',
75
+ username: normalizedConfig.username || 'default',
76
+ password: normalizedConfig.password || '',
77
+ database: normalizedConfig.database || 'default',
78
+ skipValidation,
79
+ };
80
+ const baseClient = {
81
+ rawClient: client,
82
+ select: (fieldsOrTable) => {
83
+ const builder = new ClickHouseQueryBuilder(client);
84
+ if (!fieldsOrTable)
85
+ return builder.select();
86
+ if (typeof fieldsOrTable === 'object' && fieldsOrTable !== null && '$table' in fieldsOrTable) {
87
+ return builder.select().from(fieldsOrTable);
88
+ }
89
+ return builder.select(fieldsOrTable);
90
+ },
91
+ with: (name, query) => {
92
+ const builder = new ClickHouseQueryBuilder(client);
93
+ return builder.with(name, query);
94
+ },
95
+ insert: (table) => new ClickHouseInsertBuilder(client, table, binaryInsertConfig),
96
+ insertMany: async (table, values, opts) => {
97
+ const blockSize = Math.max(opts?.maxBlockSize || 10000, 1);
98
+ const plan = buildInsertPlan(table);
99
+ const mode = plan.useCompact ? 'compact' : 'json';
100
+ const processedRows = processRowsStream(values, plan, mode);
101
+ const stream = Readable.from(processedRows, { objectMode: true, highWaterMark: blockSize });
102
+ const clickhouse_settings = opts?.asyncInsertWait !== undefined
103
+ ? { async_insert: 1, wait_for_async_insert: opts.asyncInsertWait ? 1 : 0 }
104
+ : undefined;
105
+ await client.insert({
106
+ table: table.$table,
107
+ values: stream,
108
+ format: mode === 'compact' ? 'JSONCompactEachRow' : 'JSONEachRow',
109
+ columns: mode === 'compact' ? plan.columnNames : undefined,
110
+ ...(clickhouse_settings ? { clickhouse_settings } : {})
111
+ });
112
+ },
113
+ update: (table) => new ClickHouseUpdateBuilder(client, table),
114
+ delete: (table) => new ClickHouseDeleteBuilder(client, table),
115
+ command: async (params) => {
116
+ return client.query(params);
117
+ },
118
+ raw: async (query, params) => {
119
+ const resultSet = await client.query({
120
+ query,
121
+ query_params: params,
122
+ format: 'JSONEachRow'
123
+ });
124
+ return resultSet.json();
125
+ },
126
+ tableExists,
127
+ ensureTable,
128
+ dropTable,
129
+ close: () => client.close(),
130
+ schema: schema,
131
+ _config: config,
132
+ query: undefined
133
+ };
134
+ const relationalAPI = buildRelationalAPI(client, schema);
135
+ if (relationalAPI) {
136
+ baseClient.query = relationalAPI;
137
+ }
138
+ return baseClient;
139
+ }
140
+ export const createClientFromConfigObject = createHousekitClient;
@@ -0,0 +1,107 @@
1
+ import { z } from 'zod';
2
+ import { ClickHouseColumn } from '../column';
3
+ function mapClickHouseTypeToZod(column) {
4
+ const type = column.type.toLowerCase();
5
+ let zodType;
6
+ if (type.startsWith('nullable(')) {
7
+ const innerType = column.type.slice('nullable('.length, -1);
8
+ const innerColumn = new ClickHouseColumn(column.name, innerType);
9
+ zodType = mapClickHouseTypeToZod(innerColumn).nullable();
10
+ }
11
+ else if (type.startsWith('array(')) {
12
+ const innerType = column.type.slice('array('.length, -1);
13
+ const innerColumn = new ClickHouseColumn(column.name, innerType);
14
+ zodType = z.array(mapClickHouseTypeToZod(innerColumn));
15
+ }
16
+ else if (type.startsWith('fixedstring')) {
17
+ zodType = z.string();
18
+ }
19
+ else if (type.startsWith('enum')) {
20
+ if (column.meta?.enumValues) {
21
+ zodType = z.enum(column.meta.enumValues);
22
+ }
23
+ else {
24
+ zodType = z.string();
25
+ }
26
+ }
27
+ else if (type.startsWith('datetime64')) {
28
+ zodType = z.date();
29
+ }
30
+ else if (type.startsWith('decimal')) {
31
+ zodType = z.string().refine((val) => !isNaN(parseFloat(val)), {
32
+ message: "Must be a valid decimal number string",
33
+ });
34
+ }
35
+ else {
36
+ switch (type) {
37
+ case 'uuid':
38
+ case 'string':
39
+ case 'json':
40
+ case 'ipv4':
41
+ case 'ipv6':
42
+ zodType = z.string();
43
+ break;
44
+ case 'int8':
45
+ case 'uint8':
46
+ case 'int16':
47
+ case 'uint16':
48
+ case 'int32':
49
+ case 'uint32':
50
+ case 'float32':
51
+ case 'float64':
52
+ zodType = z.number();
53
+ break;
54
+ case 'int64':
55
+ case 'uint64':
56
+ case 'int128':
57
+ case 'uint128':
58
+ case 'int256':
59
+ case 'uint256':
60
+ zodType = z.bigint();
61
+ break;
62
+ case 'boolean':
63
+ case 'bool':
64
+ zodType = z.boolean();
65
+ break;
66
+ case 'date':
67
+ case 'datetime':
68
+ zodType = z.date();
69
+ break;
70
+ default:
71
+ zodType = z.string();
72
+ break;
73
+ }
74
+ }
75
+ if (column.isNull && !type.startsWith('nullable(')) {
76
+ zodType = zodType.nullable();
77
+ }
78
+ return zodType;
79
+ }
80
+ export function generateSelectSchema(table) {
81
+ const shape = {};
82
+ for (const key in table.$columns) {
83
+ if (Object.prototype.hasOwnProperty.call(table.$columns, key)) {
84
+ const column = table.$columns[key];
85
+ shape[key] = mapClickHouseTypeToZod(column);
86
+ }
87
+ }
88
+ return z.object(shape);
89
+ }
90
+ export function generateInsertSchema(table) {
91
+ const shape = {};
92
+ for (const key in table.$columns) {
93
+ if (Object.prototype.hasOwnProperty.call(table.$columns, key)) {
94
+ const column = table.$columns[key];
95
+ let zodType = mapClickHouseTypeToZod(column);
96
+ const isOptionalBySchema = !column.isNull && (column.meta?.default !== undefined || column.meta?.defaultFn !== undefined || column.meta?.defaultExpr !== undefined || column.meta?.autoGenerate !== undefined);
97
+ const isOptionalByNullable = column.isNull;
98
+ if (isOptionalBySchema || isOptionalByNullable) {
99
+ zodType = zodType.optional();
100
+ }
101
+ else {
102
+ }
103
+ shape[key] = zodType;
104
+ }
105
+ }
106
+ return z.object(shape);
107
+ }
package/dist/column.d.ts CHANGED
@@ -26,8 +26,7 @@ export declare class ClickHouseColumn<TType = any, TNotNull extends boolean = tr
26
26
  isNull: boolean;
27
27
  meta?: ColumnMeta | undefined;
28
28
  tableName?: string;
29
- constructor(name: string, type: string, isNull?: boolean, // Columns are NOT NULL by default (isNull = false)
30
- meta?: ColumnMeta | undefined);
29
+ constructor(name: string, type: string, isNull?: boolean, meta?: ColumnMeta | undefined);
31
30
  clone<NTNotNull extends boolean = TNotNull, NAutoGenerated extends boolean = TAutoGenerated>(overrides?: {
32
31
  name?: string;
33
32
  type?: string;
@@ -40,37 +39,14 @@ export declare class ClickHouseColumn<TType = any, TNotNull extends boolean = tr
40
39
  version?: 1 | 3 | 4 | 5 | 6 | 7;
41
40
  }): ClickHouseColumn<TType, TNotNull, true>;
42
41
  primaryKey(): ClickHouseColumn<TType, true, TAutoGenerated>;
43
- /**
44
- * Define a default value for the column.
45
- * Can be a static value or a SQL expression like 'now()'.
46
- */
47
42
  default(value: any): ClickHouseColumn<TType, TNotNull, true>;
48
43
  references(getColumn: () => ClickHouseColumn, options?: {
49
44
  onDelete?: 'cascade' | 'set null' | 'restrict' | 'no action';
50
45
  onUpdate?: 'cascade' | 'set null' | 'restrict' | 'no action';
51
46
  }): ClickHouseColumn<TType, TNotNull, TAutoGenerated>;
52
- /**
53
- * Add a comment to the column in the database.
54
- */
55
47
  comment(text: string): ClickHouseColumn<TType, TNotNull, TAutoGenerated>;
56
48
  alias(expression: string): ClickHouseColumn<TType, TNotNull, TAutoGenerated>;
57
- /**
58
- * Apply a compression codec to this column.
59
- * Can be chained to apply multiple codecs (e.g., Delta then ZSTD).
60
- *
61
- * @example
62
- * ```typescript
63
- * // Optimize for time-series
64
- * timestamp.codec('DoubleDelta').codec('ZSTD', 3)
65
- * // Optimize for metrics
66
- * value.codec('Gorilla')
67
- * ```
68
- */
69
49
  codec(type: 'ZSTD' | 'LZ4' | 'Delta' | 'DoubleDelta' | 'Gorilla' | 'None', level?: number): ClickHouseColumn<TType, TNotNull, TAutoGenerated>;
70
- /**
71
- * Calculates the column value on the client side before insertion.
72
- * Useful for UUIDs, hashes, or computations based on other fields.
73
- */
74
50
  $defaultFn(fn: (row: Record<string, any>) => any): ClickHouseColumn<TType, TNotNull, true>;
75
51
  toSQL(): string;
76
52
  }
package/dist/column.js ADDED
@@ -0,0 +1,133 @@
1
+ export class ClickHouseColumn {
2
+ name;
3
+ type;
4
+ isNull;
5
+ meta;
6
+ tableName;
7
+ constructor(name, type, isNull = false, meta) {
8
+ this.name = name;
9
+ this.type = type;
10
+ this.isNull = isNull;
11
+ this.meta = meta;
12
+ }
13
+ clone(overrides = {}) {
14
+ const clonedMeta = overrides.meta ?? (this.meta ? { ...this.meta } : undefined);
15
+ const col = new ClickHouseColumn(overrides.name ?? this.name, overrides.type ?? this.type, overrides.isNull ?? this.isNull, clonedMeta);
16
+ col.tableName = this.tableName;
17
+ return col;
18
+ }
19
+ notNull() {
20
+ return this.clone({ isNull: false });
21
+ }
22
+ nullable() {
23
+ return this.clone({ isNull: true });
24
+ }
25
+ autoGenerate(options) {
26
+ if (this.type !== 'UUID') {
27
+ throw new Error('autoGenerate() can only be used with UUID columns');
28
+ }
29
+ const version = options?.version || 4;
30
+ const meta = { ...(this.meta ?? {}), autoGenerate: { type: 'uuid', version } };
31
+ return this.clone({ meta });
32
+ }
33
+ primaryKey() {
34
+ const meta = { ...(this.meta ?? {}), isPrimaryKey: true };
35
+ return this.clone({ isNull: false, meta });
36
+ }
37
+ default(value) {
38
+ const meta = { ...(this.meta ?? {}) };
39
+ if (typeof value === 'string') {
40
+ const trimmed = value.trim();
41
+ const isExpression = trimmed.includes('(') && trimmed.includes(')') ||
42
+ trimmed.match(/^[a-zA-Z_][a-zA-Z0-9_]*\s*\(/);
43
+ if (isExpression) {
44
+ meta.defaultExpr = trimmed;
45
+ delete meta.default;
46
+ }
47
+ else {
48
+ meta.default = value;
49
+ delete meta.defaultExpr;
50
+ }
51
+ }
52
+ else {
53
+ meta.default = value;
54
+ delete meta.defaultExpr;
55
+ }
56
+ return this.clone({ meta });
57
+ }
58
+ references(getColumn, options) {
59
+ const column = getColumn();
60
+ const meta = {
61
+ ...(this.meta ?? {}),
62
+ references: {
63
+ table: '',
64
+ column: column.name,
65
+ onDelete: options?.onDelete,
66
+ onUpdate: options?.onUpdate,
67
+ }
68
+ };
69
+ return this.clone({ meta });
70
+ }
71
+ comment(text) {
72
+ const meta = { ...(this.meta ?? {}), comment: text };
73
+ return this.clone({ meta });
74
+ }
75
+ alias(expression) {
76
+ const meta = { ...(this.meta ?? {}), alias: expression };
77
+ return this.clone({ meta });
78
+ }
79
+ codec(type, level) {
80
+ const codecStr = level ? `${type}(${level})` : type;
81
+ const meta = {
82
+ ...(this.meta ?? {}),
83
+ codec: this.meta?.codec ? `${this.meta.codec}, ${codecStr}` : codecStr
84
+ };
85
+ return this.clone({ meta });
86
+ }
87
+ $defaultFn(fn) {
88
+ const meta = { ...(this.meta ?? {}), defaultFn: fn };
89
+ return this.clone({ meta });
90
+ }
91
+ toSQL() {
92
+ const isComposite = this.type.startsWith('Array(') || this.type.startsWith('Map(') || this.type.startsWith('Tuple(');
93
+ const baseType = (this.isNull && !isComposite) ? `Nullable(${this.type})` : this.type;
94
+ const clauses = [baseType];
95
+ if (this.meta?.codec) {
96
+ clauses.push(`CODEC(${this.meta.codec})`);
97
+ }
98
+ if (this.meta?.defaultExpr) {
99
+ clauses.push(`DEFAULT ${this.meta.defaultExpr}`);
100
+ }
101
+ else if (this.meta?.default !== undefined) {
102
+ const defaultValue = this.meta.default;
103
+ let sqlValue;
104
+ if (typeof defaultValue === 'string') {
105
+ sqlValue = `'${defaultValue.replace(/'/g, "''")}'`;
106
+ }
107
+ else if (typeof defaultValue === 'number') {
108
+ sqlValue = String(defaultValue);
109
+ }
110
+ else if (typeof defaultValue === 'boolean') {
111
+ sqlValue = defaultValue ? '1' : '0';
112
+ }
113
+ else if (defaultValue === null) {
114
+ sqlValue = 'NULL';
115
+ }
116
+ else {
117
+ sqlValue = `'${String(defaultValue).replace(/'/g, "''")}'`;
118
+ }
119
+ clauses.push(`DEFAULT ${sqlValue}`);
120
+ }
121
+ if (this.meta?.materialized) {
122
+ clauses.push(`MATERIALIZED ${this.meta.materialized}`);
123
+ }
124
+ if (this.meta?.alias) {
125
+ clauses.push(`ALIAS ${this.meta.alias}`);
126
+ }
127
+ if (this.meta?.comment) {
128
+ const escapedComment = this.meta.comment.replace(/'/g, "''");
129
+ clauses.push(`COMMENT '${escapedComment}'`);
130
+ }
131
+ return clauses.join(' ');
132
+ }
133
+ }
@@ -4,17 +4,10 @@ export declare class QueryCompiler {
4
4
  private paramCounter;
5
5
  reset(): void;
6
6
  private getNextParamName;
7
- /**
8
- * Compile with caching support.
9
- * Returns a PreparedQuery ready for execution and the values to bind.
10
- */
11
7
  compileWithCache(state: QueryBuilderState, client: any): {
12
8
  cachedQuery: PreparedQuery<any>;
13
9
  values: any[];
14
10
  };
15
- /**
16
- * Legacy/Internal method for getting SQL + Params directly (used by toSQL)
17
- */
18
11
  compileSelect(state: QueryBuilderState): {
19
12
  sql: string;
20
13
  params: Record<string, unknown>;