@housekit/orm 0.1.22 → 0.1.23

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.
package/README.md CHANGED
@@ -13,8 +13,8 @@ HouseKit ORM is a modern database toolkit designed specifically for ClickHouse.
13
13
 
14
14
  ## 🚀 Key Features
15
15
 
16
- - **🛡️ First-Class TypeScript**: Full type inference for every query. If it compiles, the schema matches your DB.
17
- - **🏎️ Automatic Turbo Mode**: Native `RowBinary` serialization by default. Bypasses the overhead of JSON parsing for **5-10x faster inserts**.
16
+ - **🛡️ First-Class TypeScript**: Full type inference for every query. Schema definition acts as the single source of truth.
17
+ - **🏎️ Automatic Turbo Mode**: Native `RowBinary` serialization is used automatically when possible. Bypasses the overhead of JSON parsing for **5-10x faster inserts**.
18
18
  - **🏗️ ClickHouse Native Engines**: Fluent DSL for `MergeTree`, `ReplacingMergeTree`, `SummingMergeTree`, `Distributed`, `Buffer`, and more.
19
19
  - **🔍 Advanced Analytics**: Specialized support for `ASOF JOIN`, `ARRAY JOIN`, `PREWHERE`, and complex Window Functions.
20
20
  - **🤝 Smart Relational API**: Query relations using `groupArray` internally, preventing row duplication and keeping data transfer lean.
@@ -35,10 +35,11 @@ bun add @housekit/orm @clickhouse/client
35
35
 
36
36
  ## ⚡️ Quick Start
37
37
 
38
- ### 1. Define your Table
39
- Use the fluent `defineTable` API. All columns are **NOT NULL** by default, following ClickHouse best practices.
38
+ ### 1. Define your Table & Export Types
39
+ Use the fluent `defineTable` API. You can export the inferred types directly from the schema definition thanks to **Phantom Types**.
40
40
 
41
41
  ```typescript
42
+ // schema.ts
42
43
  import { defineTable, t, Engine } from '@housekit/orm';
43
44
 
44
45
  export const webEvents = defineTable('web_events', {
@@ -47,7 +48,7 @@ export const webEvents = defineTable('web_events', {
47
48
  url: t.string('url'),
48
49
  revenue: t.decimal('revenue', 18, 4).default(0),
49
50
  tags: t.array(t.string('tag')),
50
- metadata: t.json('metadata'), // Native JSON type support
51
+ metadata: t.json('metadata'),
51
52
  at: t.datetime('at').default('now()'),
52
53
  }, {
53
54
  engine: Engine.MergeTree(),
@@ -55,27 +56,34 @@ export const webEvents = defineTable('web_events', {
55
56
  partitionBy: 'toYYYYMM(at)',
56
57
  ttl: 'at + INTERVAL 1 MONTH'
57
58
  });
59
+
60
+ // ✨ Export inferred types directly
61
+ export type WebEvent = typeof webEvents.$inferSelect;
62
+ export type NewWebEvent = typeof webEvents.$inferInsert;
58
63
  ```
59
64
 
60
65
  ### 2. Connect and Query
61
66
  HouseKit automatically picks up configuration from your environment or `housekit.config.ts`.
62
67
 
63
68
  ```typescript
64
- import { createClient, eq, and, gte, sql } from '@housekit/orm';
69
+ import { createClient, sql } from '@housekit/orm';
70
+ import { webEvents } from './schema';
65
71
 
66
72
  const db = await createClient();
67
73
 
68
- // Fully typed result inference
74
+ // Fully typed result inference.
75
+ // No need to call .then() or .execute(), just await the builder!
69
76
  const results = await db.select({
70
77
  id: webEvents.id,
71
78
  path: webEvents.url,
72
79
  total: sql<number>`sum(${webEvents.revenue})`
73
80
  })
74
81
  .from(webEvents)
75
- .where(and(
76
- eq(webEvents.eventType, 'sale'),
77
- gte(webEvents.at, new Date('2024-01-01'))
78
- ))
82
+ .where({
83
+ // Object syntax implicitly uses AND operator
84
+ eventType: 'sale',
85
+ url: '/checkout'
86
+ })
79
87
  .groupBy(webEvents.id, webEvents.url)
80
88
  .limit(10);
81
89
  ```
@@ -106,7 +114,6 @@ export const users = defineTable('users', {
106
114
  engine: Engine.ReplacingMergeTree('version'),
107
115
 
108
116
  // Portability: '{cluster}' references the server-side macro.
109
- // This allows your schema to be environment-agnostic.
110
117
  onCluster: '{cluster}',
111
118
 
112
119
  orderBy: 'id'
@@ -134,9 +141,10 @@ export const userCache = defineDictionary('user_dict', {
134
141
  ## 🚀 High-Performance Data Ingestion
135
142
 
136
143
  ### Automatic Turbo Mode (RowBinary)
137
- When you call `db.insert()`, HouseKit analyzes your schema. If all types are compatible, it automatically switches to **Turbo Mode**, using native binary serialization instead of JSON.
144
+ When you call `db.insert()`, HouseKit analyzes your schema. If types are compatible, it automatically switches to **Turbo Mode**, using native binary serialization instead of JSON.
138
145
 
139
146
  ```typescript
147
+ // Clean syntax: No .execute() needed. Just await.
140
148
  await db.insert(webEvents).values([
141
149
  { id: '...', eventType: 'click', revenue: 0, metadata: { browser: 'chrome' } },
142
150
  { id: '...', eventType: 'purchase', revenue: 99.90, metadata: { browser: 'safari' } },
@@ -155,60 +163,29 @@ const builder = db.insert(webEvents)
155
163
  });
156
164
 
157
165
  // Add rows to the background queue.
158
- // Proccessing and flushing happen automatically.
166
+ // Processing and flushing happen automatically.
167
+ // Returns immediately (Fire-and-forget).
159
168
  await builder.append(row1);
160
169
  await builder.append(row2);
161
170
  ```
162
171
 
163
172
  ---
164
173
 
165
- ## 🛠️ Type-Safe Inserts
166
-
167
- ### Simple Repository Pattern
168
-
169
- ```typescript
170
- import type { InferInsert } from '@housekit/orm';
171
-
172
- async insertEvents(events: InferInsert<typeof auditEvents>[]) {
173
- return await db.insert(auditEvents).values(events);
174
- }
175
-
176
- // Usage
177
- await repository.insertEvents([
178
- { venueId: 'venue-1', ingredientId: 'ing-1', type: 'restock', quantity: 100, at: new Date() },
179
- { venueId: 'venue-2', ingredientId: 'ing-2', type: 'sale', quantity: -50, at: new Date(), referenceId: null }
180
- ]);
181
- ```
182
-
183
- ### Type Helpers
174
+ ## 🛠️ Zero-Config Type Safety
184
175
 
185
- ```typescript
186
- import type { TableInsertArray, InferInsert } from '@housekit/orm';
187
-
188
- // Using the direct helper
189
- async insertEvents(events: InferInsert<typeof salesEvents>[]) {
190
- return await db.insert(salesEvents).values(events);
191
- }
192
-
193
- // Using explicit type helper
194
- async insertEvents(events: TableInsertArray<typeof salesEvents>) {
195
- return await db.insert(salesEvents).values(events);
196
- }
197
- ```
176
+ Because we use **Phantom Types**, you don't need to import generic helpers like `Infer<T>` in your application code. You can use `typeof table.$infer...` or the types you exported from your schema file.
198
177
 
199
- ### Inline Types (No Extra Exports)
178
+ ### In Repository Functions
200
179
 
201
180
  ```typescript
202
- import { users } from './schema';
203
- import type { Infer } from '@housekit/orm';
181
+ import { webEvents, type NewWebEvent } from './schema';
204
182
 
205
- function UserCard({ user }: { user: Infer<typeof users> }) {
206
- return <div>{user.email}</div>;
183
+ async function logEvents(events: NewWebEvent[]) {
184
+ // Types match automatically
185
+ return await db.insert(webEvents).values(events);
207
186
  }
208
187
  ```
209
188
 
210
- **Note**: Autocomplete shows clean data types by default without exposing internal types.
211
-
212
189
  ---
213
190
 
214
191
  ## 🤝 Smart Relational API
@@ -216,27 +193,29 @@ function UserCard({ user }: { user: Infer<typeof users> }) {
216
193
  Traditional ORMs produce "Flat Joins" that duplicate data (the Cartesian Product problem). HouseKit's Relational API uses ClickHouse's `groupArray` internally to fetch related data as nested arrays in a single, efficient query.
217
194
 
218
195
  ```typescript
196
+ // Define relations in your schema first
197
+ import { relations } from '@housekit/orm';
198
+
199
+ relations(users, ({ many }) => ({
200
+ posts: many(posts, { fields: [users.id], references: [posts.userId] })
201
+ }));
202
+
203
+ // Query with nested data
219
204
  const usersWithData = await db.query.users.findMany({
205
+ where: { country: 'US' }, // Object syntax
220
206
  with: {
221
207
  posts: {
222
- where: (p) => eq(p.published, true),
223
- limit: 5
224
- },
225
- profile: true
208
+ limit: 5,
209
+ orderBy: { col: posts.createdAt, dir: 'DESC' }
210
+ }
226
211
  },
227
212
  limit: 10
228
213
  });
229
214
 
230
215
  // Result structure:
231
- // [{ id: 1, name: 'Alice', posts: [{ title: '...', ... }], profile: { bio: '...' } }]
216
+ // [{ id: 1, name: 'Alice', posts: [{ title: '...', ... }] }]
232
217
  ```
233
218
 
234
- ### Advanced Relational Engine
235
- HouseKit's relational API is optimized for ClickHouse:
236
- - **Filtered Relations**: Where clauses in `with` blocks are executed server-side using `groupUniqArrayIf`.
237
- - **Nested Pagination**: Control the size of related collections with `limit` and `offset` directly in the relation config.
238
- - **Smart Deduplication**: Merges results in-memory to handle row multiplication from complex joins.
239
-
240
219
  ---
241
220
 
242
221
  ## 🛠 SQL Utilities
@@ -250,7 +229,7 @@ const conditions = [
250
229
  gte(users.age, 18)
251
230
  ];
252
231
 
253
- const query = db.select()
232
+ const query = await db.select()
254
233
  .from(users)
255
234
  .where(sql.join(conditions, sql` AND `));
256
235
  ```
@@ -273,7 +252,9 @@ const matched = await db.select()
273
252
  Essential for distributed setups to avoid local-data-only results on sharded clusters.
274
253
 
275
254
  ```typescript
276
- db.select().from(distributedTable).globalJoin(rightTable, condition);
255
+ await db.select()
256
+ .from(distributedTable)
257
+ .globalJoin(rightTable, condition);
277
258
  ```
278
259
 
279
260
  ---
@@ -297,4 +278,4 @@ const db = await createClient({
297
278
 
298
279
  ## License
299
280
 
300
- MIT © [Pablo Fernandez Ruiz](https://github.com/pablofdezr)
281
+ MIT © [Pablo Fernandez Ruiz](https://github.com/pablofdezr)
@@ -219,10 +219,10 @@ export declare class ClickHouseQueryBuilder<TTable extends TableDefinition<any>
219
219
  kind: "cte";
220
220
  query?: string;
221
221
  }>;
222
- $inferSelect: InferSelectModel<{
222
+ readonly $inferSelect: InferSelectModel<{
223
223
  $columns: TSelection extends SelectionShape ? { [K in keyof (TSelection extends infer T_1 ? T_1 extends TSelection ? T_1 extends SelectionShape ? SelectResult<T_1> : Record<string, any> : never : never)]: ClickHouseColumn<(TSelection extends infer T_2 ? T_2 extends TSelection ? T_2 extends SelectionShape ? SelectResult<T_2> : Record<string, any> : never : never)[K], true, false>; } : Record<string, ClickHouseColumn<any, true, false>>;
224
224
  }>;
225
- $inferInsert: import("..").TableInsert<TSelection extends SelectionShape ? { [K in keyof (TSelection extends infer T_2 ? T_2 extends TSelection ? T_2 extends SelectionShape ? SelectResult<T_2> : Record<string, any> : never : never)]: ClickHouseColumn<(TSelection extends infer T_3 ? T_3 extends TSelection ? T_3 extends SelectionShape ? SelectResult<T_3> : Record<string, any> : never : never)[K], true, false>; } : Record<string, ClickHouseColumn<any, true, false>>>;
225
+ readonly $inferInsert: import("..").TableInsert<TSelection extends SelectionShape ? { [K in keyof (TSelection extends infer T_2 ? T_2 extends TSelection ? T_2 extends SelectionShape ? SelectResult<T_2> : Record<string, any> : never : never)]: ClickHouseColumn<(TSelection extends infer T_3 ? T_3 extends TSelection ? T_3 extends SelectionShape ? SelectResult<T_3> : Record<string, any> : never : never)[K], true, false>; } : Record<string, ClickHouseColumn<any, true, false>>>;
226
226
  } & (TSelection extends SelectionShape ? { [K in keyof (TSelection extends infer T_3 ? T_3 extends TSelection ? T_3 extends SelectionShape ? SelectResult<T_3> : Record<string, any> : never : never)]: ClickHouseColumn<(TSelection extends infer T_4 ? T_4 extends TSelection ? T_4 extends SelectionShape ? SelectResult<T_4> : Record<string, any> : never : never)[K], true, false>; } : Record<string, ClickHouseColumn<any, true, false>>);
227
227
  register: (mainQuery: ClickHouseQueryBuilder<any, any, any>) => ClickHouseQueryBuilder<any, any, any>;
228
228
  };
package/dist/index.js CHANGED
@@ -1005,8 +1005,6 @@ function chTable(tableName, columns, options = {}) {
1005
1005
  $table: tableName,
1006
1006
  $columns: columns,
1007
1007
  $options: finalOptions,
1008
- $inferSelect: undefined,
1009
- $inferInsert: undefined,
1010
1008
  toSQL: () => {
1011
1009
  if (finalOptions.externallyManaged) {
1012
1010
  return "";
package/dist/table.d.ts CHANGED
@@ -112,10 +112,10 @@ export type TableDefinition<TCols extends TableColumns, TOptions = TableOptions>
112
112
  toSQL(): string;
113
113
  toSQLs?(): string[];
114
114
  as(alias: string): TableDefinition<TCols, TOptions>;
115
- $inferSelect: InferSelectModel<{
115
+ readonly $inferSelect: InferSelectModel<{
116
116
  $columns: TCols;
117
117
  }>;
118
- $inferInsert: InferInsertModel<{
118
+ readonly $inferInsert: InferInsertModel<{
119
119
  $columns: TCols;
120
120
  }>;
121
121
  } & TCols;
@@ -127,8 +127,8 @@ export type TableRuntime<TInsert = any, TSelect = any, TOptions = TableOptions>
127
127
  toSQL(): string;
128
128
  toSQLs?(): string[];
129
129
  as(alias: string): TableRuntime<TInsert, TSelect, TOptions>;
130
- $inferSelect: TSelect;
131
- $inferInsert: TInsert;
130
+ readonly $inferSelect: TSelect;
131
+ readonly $inferInsert: TInsert;
132
132
  };
133
133
  export interface VersionedMeta {
134
134
  baseName: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@housekit/orm",
3
- "version": "0.1.22",
3
+ "version": "0.1.23",
4
4
  "description": "Type-safe ClickHouse ORM with modern DX and ClickHouse-specific optimizations. Features Turbo Mode (RowBinary), full engine support, and advanced query capabilities.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",