@tinybirdco/sdk 0.0.1

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 (258) hide show
  1. package/README.md +518 -0
  2. package/bin/tinybird.js +7 -0
  3. package/dist/api/branches.d.ts +98 -0
  4. package/dist/api/branches.d.ts.map +1 -0
  5. package/dist/api/branches.js +203 -0
  6. package/dist/api/branches.js.map +1 -0
  7. package/dist/api/branches.test.d.ts +2 -0
  8. package/dist/api/branches.test.d.ts.map +1 -0
  9. package/dist/api/branches.test.js +286 -0
  10. package/dist/api/branches.test.js.map +1 -0
  11. package/dist/api/build.d.ts +130 -0
  12. package/dist/api/build.d.ts.map +1 -0
  13. package/dist/api/build.js +143 -0
  14. package/dist/api/build.js.map +1 -0
  15. package/dist/api/build.test.d.ts +2 -0
  16. package/dist/api/build.test.d.ts.map +1 -0
  17. package/dist/api/build.test.js +138 -0
  18. package/dist/api/build.test.js.map +1 -0
  19. package/dist/api/deploy.d.ts +39 -0
  20. package/dist/api/deploy.d.ts.map +1 -0
  21. package/dist/api/deploy.js +135 -0
  22. package/dist/api/deploy.js.map +1 -0
  23. package/dist/api/deploy.test.d.ts +2 -0
  24. package/dist/api/deploy.test.d.ts.map +1 -0
  25. package/dist/api/deploy.test.js +118 -0
  26. package/dist/api/deploy.test.js.map +1 -0
  27. package/dist/api/workspaces.d.ts +46 -0
  28. package/dist/api/workspaces.d.ts.map +1 -0
  29. package/dist/api/workspaces.js +39 -0
  30. package/dist/api/workspaces.js.map +1 -0
  31. package/dist/api/workspaces.test.d.ts +2 -0
  32. package/dist/api/workspaces.test.d.ts.map +1 -0
  33. package/dist/api/workspaces.test.js +65 -0
  34. package/dist/api/workspaces.test.js.map +1 -0
  35. package/dist/cli/auth.d.ts +86 -0
  36. package/dist/cli/auth.d.ts.map +1 -0
  37. package/dist/cli/auth.js +284 -0
  38. package/dist/cli/auth.js.map +1 -0
  39. package/dist/cli/branch-store.d.ts +53 -0
  40. package/dist/cli/branch-store.d.ts.map +1 -0
  41. package/dist/cli/branch-store.js +91 -0
  42. package/dist/cli/branch-store.js.map +1 -0
  43. package/dist/cli/branch-store.test.d.ts +2 -0
  44. package/dist/cli/branch-store.test.d.ts.map +1 -0
  45. package/dist/cli/branch-store.test.js +115 -0
  46. package/dist/cli/branch-store.test.js.map +1 -0
  47. package/dist/cli/commands/branch.d.ts +82 -0
  48. package/dist/cli/commands/branch.d.ts.map +1 -0
  49. package/dist/cli/commands/branch.js +215 -0
  50. package/dist/cli/commands/branch.js.map +1 -0
  51. package/dist/cli/commands/build.d.ts +43 -0
  52. package/dist/cli/commands/build.d.ts.map +1 -0
  53. package/dist/cli/commands/build.js +138 -0
  54. package/dist/cli/commands/build.js.map +1 -0
  55. package/dist/cli/commands/dev.d.ts +78 -0
  56. package/dist/cli/commands/dev.d.ts.map +1 -0
  57. package/dist/cli/commands/dev.js +226 -0
  58. package/dist/cli/commands/dev.js.map +1 -0
  59. package/dist/cli/commands/init.d.ts +45 -0
  60. package/dist/cli/commands/init.d.ts.map +1 -0
  61. package/dist/cli/commands/init.js +277 -0
  62. package/dist/cli/commands/init.js.map +1 -0
  63. package/dist/cli/commands/init.test.d.ts +2 -0
  64. package/dist/cli/commands/init.test.d.ts.map +1 -0
  65. package/dist/cli/commands/init.test.js +158 -0
  66. package/dist/cli/commands/init.test.js.map +1 -0
  67. package/dist/cli/commands/login.d.ts +37 -0
  68. package/dist/cli/commands/login.d.ts.map +1 -0
  69. package/dist/cli/commands/login.js +64 -0
  70. package/dist/cli/commands/login.js.map +1 -0
  71. package/dist/cli/config.d.ts +114 -0
  72. package/dist/cli/config.d.ts.map +1 -0
  73. package/dist/cli/config.js +258 -0
  74. package/dist/cli/config.js.map +1 -0
  75. package/dist/cli/config.test.d.ts +2 -0
  76. package/dist/cli/config.test.d.ts.map +1 -0
  77. package/dist/cli/config.test.js +243 -0
  78. package/dist/cli/config.test.js.map +1 -0
  79. package/dist/cli/env.d.ts +29 -0
  80. package/dist/cli/env.d.ts.map +1 -0
  81. package/dist/cli/env.js +66 -0
  82. package/dist/cli/env.js.map +1 -0
  83. package/dist/cli/git.d.ts +29 -0
  84. package/dist/cli/git.d.ts.map +1 -0
  85. package/dist/cli/git.js +114 -0
  86. package/dist/cli/git.js.map +1 -0
  87. package/dist/cli/git.test.d.ts +2 -0
  88. package/dist/cli/git.test.d.ts.map +1 -0
  89. package/dist/cli/git.test.js +125 -0
  90. package/dist/cli/git.test.js.map +1 -0
  91. package/dist/cli/index.d.ts +7 -0
  92. package/dist/cli/index.d.ts.map +1 -0
  93. package/dist/cli/index.js +337 -0
  94. package/dist/cli/index.js.map +1 -0
  95. package/dist/cli/utils/schema-validation.d.ts +95 -0
  96. package/dist/cli/utils/schema-validation.d.ts.map +1 -0
  97. package/dist/cli/utils/schema-validation.js +175 -0
  98. package/dist/cli/utils/schema-validation.js.map +1 -0
  99. package/dist/cli/utils/schema-validation.test.d.ts +5 -0
  100. package/dist/cli/utils/schema-validation.test.d.ts.map +1 -0
  101. package/dist/cli/utils/schema-validation.test.js +173 -0
  102. package/dist/cli/utils/schema-validation.test.js.map +1 -0
  103. package/dist/client/base.d.ts +116 -0
  104. package/dist/client/base.d.ts.map +1 -0
  105. package/dist/client/base.js +328 -0
  106. package/dist/client/base.js.map +1 -0
  107. package/dist/client/types.d.ts +137 -0
  108. package/dist/client/types.d.ts.map +1 -0
  109. package/dist/client/types.js +43 -0
  110. package/dist/client/types.js.map +1 -0
  111. package/dist/generator/client.d.ts +44 -0
  112. package/dist/generator/client.d.ts.map +1 -0
  113. package/dist/generator/client.js +144 -0
  114. package/dist/generator/client.js.map +1 -0
  115. package/dist/generator/datasource.d.ts +57 -0
  116. package/dist/generator/datasource.d.ts.map +1 -0
  117. package/dist/generator/datasource.js +169 -0
  118. package/dist/generator/datasource.js.map +1 -0
  119. package/dist/generator/datasource.test.d.ts +2 -0
  120. package/dist/generator/datasource.test.d.ts.map +1 -0
  121. package/dist/generator/datasource.test.js +254 -0
  122. package/dist/generator/datasource.test.js.map +1 -0
  123. package/dist/generator/index.d.ts +131 -0
  124. package/dist/generator/index.d.ts.map +1 -0
  125. package/dist/generator/index.js +121 -0
  126. package/dist/generator/index.js.map +1 -0
  127. package/dist/generator/index.test.d.ts +2 -0
  128. package/dist/generator/index.test.d.ts.map +1 -0
  129. package/dist/generator/index.test.js +175 -0
  130. package/dist/generator/index.test.js.map +1 -0
  131. package/dist/generator/loader.d.ts +156 -0
  132. package/dist/generator/loader.d.ts.map +1 -0
  133. package/dist/generator/loader.js +295 -0
  134. package/dist/generator/loader.js.map +1 -0
  135. package/dist/generator/pipe.d.ts +72 -0
  136. package/dist/generator/pipe.d.ts.map +1 -0
  137. package/dist/generator/pipe.js +174 -0
  138. package/dist/generator/pipe.js.map +1 -0
  139. package/dist/generator/pipe.test.d.ts +2 -0
  140. package/dist/generator/pipe.test.d.ts.map +1 -0
  141. package/dist/generator/pipe.test.js +393 -0
  142. package/dist/generator/pipe.test.js.map +1 -0
  143. package/dist/index.d.ts +74 -0
  144. package/dist/index.d.ts.map +1 -0
  145. package/dist/index.js +73 -0
  146. package/dist/index.js.map +1 -0
  147. package/dist/infer/index.d.ts +202 -0
  148. package/dist/infer/index.d.ts.map +1 -0
  149. package/dist/infer/index.js +5 -0
  150. package/dist/infer/index.js.map +1 -0
  151. package/dist/schema/datasource.d.ts +135 -0
  152. package/dist/schema/datasource.d.ts.map +1 -0
  153. package/dist/schema/datasource.js +105 -0
  154. package/dist/schema/datasource.js.map +1 -0
  155. package/dist/schema/datasource.test.d.ts +2 -0
  156. package/dist/schema/datasource.test.d.ts.map +1 -0
  157. package/dist/schema/datasource.test.js +142 -0
  158. package/dist/schema/datasource.test.js.map +1 -0
  159. package/dist/schema/engines.d.ts +157 -0
  160. package/dist/schema/engines.d.ts.map +1 -0
  161. package/dist/schema/engines.js +155 -0
  162. package/dist/schema/engines.js.map +1 -0
  163. package/dist/schema/engines.test.d.ts +2 -0
  164. package/dist/schema/engines.test.d.ts.map +1 -0
  165. package/dist/schema/engines.test.js +221 -0
  166. package/dist/schema/engines.test.js.map +1 -0
  167. package/dist/schema/params.d.ts +106 -0
  168. package/dist/schema/params.d.ts.map +1 -0
  169. package/dist/schema/params.js +138 -0
  170. package/dist/schema/params.js.map +1 -0
  171. package/dist/schema/params.test.d.ts +2 -0
  172. package/dist/schema/params.test.d.ts.map +1 -0
  173. package/dist/schema/params.test.js +175 -0
  174. package/dist/schema/params.test.js.map +1 -0
  175. package/dist/schema/pipe.d.ts +436 -0
  176. package/dist/schema/pipe.d.ts.map +1 -0
  177. package/dist/schema/pipe.js +484 -0
  178. package/dist/schema/pipe.js.map +1 -0
  179. package/dist/schema/pipe.test.d.ts +2 -0
  180. package/dist/schema/pipe.test.d.ts.map +1 -0
  181. package/dist/schema/pipe.test.js +488 -0
  182. package/dist/schema/pipe.test.js.map +1 -0
  183. package/dist/schema/project.d.ts +202 -0
  184. package/dist/schema/project.d.ts.map +1 -0
  185. package/dist/schema/project.js +188 -0
  186. package/dist/schema/project.js.map +1 -0
  187. package/dist/schema/project.test.d.ts +2 -0
  188. package/dist/schema/project.test.d.ts.map +1 -0
  189. package/dist/schema/project.test.js +180 -0
  190. package/dist/schema/project.test.js.map +1 -0
  191. package/dist/schema/types.d.ts +140 -0
  192. package/dist/schema/types.d.ts.map +1 -0
  193. package/dist/schema/types.js +174 -0
  194. package/dist/schema/types.js.map +1 -0
  195. package/dist/schema/types.test.d.ts +2 -0
  196. package/dist/schema/types.test.d.ts.map +1 -0
  197. package/dist/schema/types.test.js +176 -0
  198. package/dist/schema/types.test.js.map +1 -0
  199. package/dist/test/handlers.d.ts +58 -0
  200. package/dist/test/handlers.d.ts.map +1 -0
  201. package/dist/test/handlers.js +62 -0
  202. package/dist/test/handlers.js.map +1 -0
  203. package/dist/test/setup.d.ts +5 -0
  204. package/dist/test/setup.d.ts.map +1 -0
  205. package/dist/test/setup.js +11 -0
  206. package/dist/test/setup.js.map +1 -0
  207. package/package.json +57 -0
  208. package/src/api/branches.test.ts +377 -0
  209. package/src/api/branches.ts +334 -0
  210. package/src/api/build.test.ts +216 -0
  211. package/src/api/build.ts +266 -0
  212. package/src/api/deploy.test.ts +193 -0
  213. package/src/api/deploy.ts +163 -0
  214. package/src/api/workspaces.test.ts +81 -0
  215. package/src/api/workspaces.ts +77 -0
  216. package/src/cli/auth.ts +358 -0
  217. package/src/cli/branch-store.test.ts +139 -0
  218. package/src/cli/branch-store.ts +137 -0
  219. package/src/cli/commands/branch.ts +306 -0
  220. package/src/cli/commands/build.ts +183 -0
  221. package/src/cli/commands/dev.ts +334 -0
  222. package/src/cli/commands/init.test.ts +249 -0
  223. package/src/cli/commands/init.ts +323 -0
  224. package/src/cli/commands/login.ts +98 -0
  225. package/src/cli/config.test.ts +359 -0
  226. package/src/cli/config.ts +335 -0
  227. package/src/cli/env.ts +86 -0
  228. package/src/cli/git.test.ts +147 -0
  229. package/src/cli/git.ts +125 -0
  230. package/src/cli/index.ts +382 -0
  231. package/src/cli/utils/schema-validation.test.ts +222 -0
  232. package/src/cli/utils/schema-validation.ts +272 -0
  233. package/src/client/base.ts +414 -0
  234. package/src/client/types.ts +165 -0
  235. package/src/generator/client.ts +194 -0
  236. package/src/generator/datasource.test.ts +297 -0
  237. package/src/generator/datasource.ts +217 -0
  238. package/src/generator/index.test.ts +209 -0
  239. package/src/generator/index.ts +203 -0
  240. package/src/generator/loader.ts +406 -0
  241. package/src/generator/pipe.test.ts +441 -0
  242. package/src/generator/pipe.ts +220 -0
  243. package/src/index.ts +191 -0
  244. package/src/infer/index.ts +247 -0
  245. package/src/schema/datasource.test.ts +187 -0
  246. package/src/schema/datasource.ts +195 -0
  247. package/src/schema/engines.test.ts +247 -0
  248. package/src/schema/engines.ts +271 -0
  249. package/src/schema/params.test.ts +208 -0
  250. package/src/schema/params.ts +249 -0
  251. package/src/schema/pipe.test.ts +588 -0
  252. package/src/schema/pipe.ts +832 -0
  253. package/src/schema/project.test.ts +236 -0
  254. package/src/schema/project.ts +394 -0
  255. package/src/schema/types.test.ts +212 -0
  256. package/src/schema/types.ts +366 -0
  257. package/src/test/handlers.ts +79 -0
  258. package/src/test/setup.ts +13 -0
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Datasource definition for Tinybird
3
+ * Define table schemas as TypeScript with full type safety
4
+ */
5
+
6
+ import type { AnyTypeValidator } from "./types.js";
7
+ import type { EngineConfig } from "./engines.js";
8
+
9
+ // Symbol for brand typing
10
+ const DATASOURCE_BRAND = Symbol("tinybird.datasource");
11
+
12
+ /**
13
+ * A column can be defined as just a type validator,
14
+ * or with additional options like JSON path or default value
15
+ */
16
+ export interface ColumnDefinition<T extends AnyTypeValidator = AnyTypeValidator> {
17
+ /** The column type */
18
+ type: T;
19
+ /** JSON path for extracting from nested JSON (e.g., '$.user.id') */
20
+ jsonPath?: string;
21
+ }
22
+
23
+ /**
24
+ * Schema definition is a record of column names to type validators or column definitions
25
+ */
26
+ export type SchemaDefinition = Record<string, AnyTypeValidator | ColumnDefinition>;
27
+
28
+ /**
29
+ * Token configuration for datasource access
30
+ */
31
+ export interface TokenConfig {
32
+ /** Token name */
33
+ name: string;
34
+ /** Permissions granted to this token */
35
+ permissions: readonly ("READ" | "APPEND")[];
36
+ }
37
+
38
+ /**
39
+ * Options for defining a datasource
40
+ */
41
+ export interface DatasourceOptions<TSchema extends SchemaDefinition> {
42
+ /** Human-readable description of the datasource */
43
+ description?: string;
44
+ /** Column schema definition */
45
+ schema: TSchema;
46
+ /** Table engine configuration */
47
+ engine?: EngineConfig;
48
+ /** Access tokens for this datasource */
49
+ tokens?: readonly TokenConfig[];
50
+ /** Workspaces to share this datasource with */
51
+ sharedWith?: readonly string[];
52
+ /**
53
+ * Whether to generate JSON path expressions for columns.
54
+ * Set to false for datasources that are targets of materialized views.
55
+ * Defaults to true.
56
+ */
57
+ jsonPaths?: boolean;
58
+ }
59
+
60
+ /**
61
+ * A datasource definition with full type information
62
+ */
63
+ export interface DatasourceDefinition<TSchema extends SchemaDefinition = SchemaDefinition> {
64
+ readonly [DATASOURCE_BRAND]: true;
65
+ /** Datasource name */
66
+ readonly _name: string;
67
+ /** Type marker for inference */
68
+ readonly _type: "datasource";
69
+ /** Schema definition */
70
+ readonly _schema: TSchema;
71
+ /** Full options */
72
+ readonly options: DatasourceOptions<TSchema>;
73
+ }
74
+
75
+ /**
76
+ * Define a Tinybird datasource
77
+ *
78
+ * @param name - The datasource name (must be valid identifier)
79
+ * @param options - Datasource configuration including schema and engine
80
+ * @returns A datasource definition that can be used in a project
81
+ *
82
+ * @example
83
+ * ```ts
84
+ * import { defineDatasource, t, engine } from '@tinybirdco/sdk';
85
+ *
86
+ * export const events = defineDatasource('events', {
87
+ * description: 'User event tracking data',
88
+ * schema: {
89
+ * timestamp: t.dateTime(),
90
+ * event_id: t.uuid(),
91
+ * user_id: t.string(),
92
+ * event_type: t.string().lowCardinality(),
93
+ * properties: t.json(),
94
+ * session_id: t.string().nullable(),
95
+ * },
96
+ * engine: engine.mergeTree({
97
+ * sortingKey: ['user_id', 'timestamp'],
98
+ * partitionKey: 'toYYYYMM(timestamp)',
99
+ * ttl: 'timestamp + INTERVAL 90 DAY',
100
+ * }),
101
+ * });
102
+ * ```
103
+ */
104
+ export function defineDatasource<TSchema extends SchemaDefinition>(
105
+ name: string,
106
+ options: DatasourceOptions<TSchema>
107
+ ): DatasourceDefinition<TSchema> {
108
+ // Validate name is a valid identifier
109
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
110
+ throw new Error(
111
+ `Invalid datasource name: "${name}". Must start with a letter or underscore and contain only alphanumeric characters and underscores.`
112
+ );
113
+ }
114
+
115
+ return {
116
+ [DATASOURCE_BRAND]: true,
117
+ _name: name,
118
+ _type: "datasource",
119
+ _schema: options.schema,
120
+ options,
121
+ };
122
+ }
123
+
124
+ /**
125
+ * Check if a value is a datasource definition
126
+ */
127
+ export function isDatasourceDefinition(value: unknown): value is DatasourceDefinition {
128
+ return (
129
+ typeof value === "object" &&
130
+ value !== null &&
131
+ DATASOURCE_BRAND in value &&
132
+ (value as Record<symbol, unknown>)[DATASOURCE_BRAND] === true
133
+ );
134
+ }
135
+
136
+ /**
137
+ * Get the column type for a schema entry (handles both raw validators and column definitions)
138
+ */
139
+ export function getColumnType(column: AnyTypeValidator | ColumnDefinition): AnyTypeValidator {
140
+ if ("type" in column && typeof column.type === "object") {
141
+ return column.type;
142
+ }
143
+ return column as AnyTypeValidator;
144
+ }
145
+
146
+ /**
147
+ * Get the JSON path for a column if defined
148
+ */
149
+ export function getColumnJsonPath(column: AnyTypeValidator | ColumnDefinition): string | undefined {
150
+ if ("jsonPath" in column) {
151
+ return column.jsonPath;
152
+ }
153
+ return undefined;
154
+ }
155
+
156
+ /**
157
+ * Get all column names from a schema
158
+ */
159
+ export function getColumnNames<TSchema extends SchemaDefinition>(
160
+ schema: TSchema
161
+ ): (keyof TSchema)[] {
162
+ return Object.keys(schema) as (keyof TSchema)[];
163
+ }
164
+
165
+ /**
166
+ * Helper type to extract the schema from a datasource definition
167
+ */
168
+ export type ExtractSchema<T> = T extends DatasourceDefinition<infer S> ? S : never;
169
+
170
+ /**
171
+ * Column definition helper for complex column configurations
172
+ *
173
+ * @example
174
+ * ```ts
175
+ * import { defineDatasource, t, column } from '@tinybirdco/sdk';
176
+ *
177
+ * export const events = defineDatasource('events', {
178
+ * schema: {
179
+ * // Simple column
180
+ * id: t.string(),
181
+ * // Column with JSON extraction
182
+ * user_id: column(t.string(), { jsonPath: '$.user.id' }),
183
+ * },
184
+ * });
185
+ * ```
186
+ */
187
+ export function column<T extends AnyTypeValidator>(
188
+ type: T,
189
+ options?: Omit<ColumnDefinition<T>, "type">
190
+ ): ColumnDefinition<T> {
191
+ return {
192
+ type,
193
+ ...options,
194
+ };
195
+ }
@@ -0,0 +1,247 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { engine, getEngineClause, getSortingKey, getPrimaryKey } from './engines.js';
3
+
4
+ describe('Engine Configurations', () => {
5
+ describe('MergeTree', () => {
6
+ it('creates MergeTree config', () => {
7
+ const config = engine.mergeTree({ sortingKey: ['id'] });
8
+ expect(config.type).toBe('MergeTree');
9
+ expect(config.sortingKey).toEqual(['id']);
10
+ });
11
+
12
+ it('supports string sortingKey', () => {
13
+ const config = engine.mergeTree({ sortingKey: 'id' });
14
+ expect(config.sortingKey).toBe('id');
15
+ });
16
+
17
+ it('supports partitionKey', () => {
18
+ const config = engine.mergeTree({
19
+ sortingKey: ['id'],
20
+ partitionKey: 'toYYYYMM(timestamp)',
21
+ });
22
+ expect(config.partitionKey).toBe('toYYYYMM(timestamp)');
23
+ });
24
+
25
+ it('supports TTL', () => {
26
+ const config = engine.mergeTree({
27
+ sortingKey: ['id'],
28
+ ttl: 'timestamp + INTERVAL 90 DAY',
29
+ });
30
+ expect(config.ttl).toBe('timestamp + INTERVAL 90 DAY');
31
+ });
32
+ });
33
+
34
+ describe('ReplacingMergeTree', () => {
35
+ it('creates ReplacingMergeTree config', () => {
36
+ const config = engine.replacingMergeTree({ sortingKey: ['id'] });
37
+ expect(config.type).toBe('ReplacingMergeTree');
38
+ });
39
+
40
+ it('supports version column', () => {
41
+ const config = engine.replacingMergeTree({
42
+ sortingKey: ['id'],
43
+ ver: 'updated_at',
44
+ });
45
+ expect(config.ver).toBe('updated_at');
46
+ });
47
+ });
48
+
49
+ describe('SummingMergeTree', () => {
50
+ it('creates SummingMergeTree config', () => {
51
+ const config = engine.summingMergeTree({ sortingKey: ['id'] });
52
+ expect(config.type).toBe('SummingMergeTree');
53
+ });
54
+
55
+ it('supports columns to sum', () => {
56
+ const config = engine.summingMergeTree({
57
+ sortingKey: ['date'],
58
+ columns: ['count', 'total'],
59
+ });
60
+ expect(config.columns).toEqual(['count', 'total']);
61
+ });
62
+ });
63
+
64
+ describe('AggregatingMergeTree', () => {
65
+ it('creates AggregatingMergeTree config', () => {
66
+ const config = engine.aggregatingMergeTree({ sortingKey: ['id'] });
67
+ expect(config.type).toBe('AggregatingMergeTree');
68
+ });
69
+ });
70
+
71
+ describe('CollapsingMergeTree', () => {
72
+ it('creates CollapsingMergeTree config', () => {
73
+ const config = engine.collapsingMergeTree({
74
+ sortingKey: ['id'],
75
+ sign: 'sign_col',
76
+ });
77
+ expect(config.type).toBe('CollapsingMergeTree');
78
+ expect(config.sign).toBe('sign_col');
79
+ });
80
+ });
81
+
82
+ describe('VersionedCollapsingMergeTree', () => {
83
+ it('creates VersionedCollapsingMergeTree config', () => {
84
+ const config = engine.versionedCollapsingMergeTree({
85
+ sortingKey: ['id'],
86
+ sign: 'sign_col',
87
+ version: 'version_col',
88
+ });
89
+ expect(config.type).toBe('VersionedCollapsingMergeTree');
90
+ expect(config.sign).toBe('sign_col');
91
+ expect(config.version).toBe('version_col');
92
+ });
93
+ });
94
+
95
+ describe('getEngineClause', () => {
96
+ it('generates basic MergeTree clause', () => {
97
+ const config = engine.mergeTree({ sortingKey: ['id'] });
98
+ const clause = getEngineClause(config);
99
+ expect(clause).toContain('ENGINE "MergeTree"');
100
+ expect(clause).toContain('ENGINE_SORTING_KEY "id"');
101
+ });
102
+
103
+ it('includes partition key', () => {
104
+ const config = engine.mergeTree({
105
+ sortingKey: ['id'],
106
+ partitionKey: 'toYYYYMM(timestamp)',
107
+ });
108
+ const clause = getEngineClause(config);
109
+ expect(clause).toContain('ENGINE_PARTITION_KEY "toYYYYMM(timestamp)"');
110
+ });
111
+
112
+ it('includes TTL', () => {
113
+ const config = engine.mergeTree({
114
+ sortingKey: ['id'],
115
+ ttl: 'timestamp + INTERVAL 90 DAY',
116
+ });
117
+ const clause = getEngineClause(config);
118
+ expect(clause).toContain('ENGINE_TTL "timestamp + INTERVAL 90 DAY"');
119
+ });
120
+
121
+ it('includes primary key when different from sorting key', () => {
122
+ const config = engine.mergeTree({
123
+ sortingKey: ['id', 'timestamp'],
124
+ primaryKey: ['id'],
125
+ });
126
+ const clause = getEngineClause(config);
127
+ expect(clause).toContain('ENGINE_SORTING_KEY "id, timestamp"');
128
+ expect(clause).toContain('ENGINE_PRIMARY_KEY "id"');
129
+ });
130
+
131
+ it('includes ReplacingMergeTree version column', () => {
132
+ const config = engine.replacingMergeTree({
133
+ sortingKey: ['id'],
134
+ ver: 'updated_at',
135
+ });
136
+ const clause = getEngineClause(config);
137
+ expect(clause).toContain('ENGINE "ReplacingMergeTree"');
138
+ expect(clause).toContain('ENGINE_VER "updated_at"');
139
+ });
140
+
141
+ it('includes SummingMergeTree columns', () => {
142
+ const config = engine.summingMergeTree({
143
+ sortingKey: ['date'],
144
+ columns: ['count', 'total'],
145
+ });
146
+ const clause = getEngineClause(config);
147
+ expect(clause).toContain('ENGINE "SummingMergeTree"');
148
+ expect(clause).toContain('ENGINE_SUMMING_COLUMNS "count, total"');
149
+ });
150
+
151
+ it('includes CollapsingMergeTree sign column', () => {
152
+ const config = engine.collapsingMergeTree({
153
+ sortingKey: ['id'],
154
+ sign: 'sign_col',
155
+ });
156
+ const clause = getEngineClause(config);
157
+ expect(clause).toContain('ENGINE "CollapsingMergeTree"');
158
+ expect(clause).toContain('ENGINE_SIGN "sign_col"');
159
+ });
160
+
161
+ it('includes VersionedCollapsingMergeTree sign and version', () => {
162
+ const config = engine.versionedCollapsingMergeTree({
163
+ sortingKey: ['id'],
164
+ sign: 'sign_col',
165
+ version: 'version_col',
166
+ });
167
+ const clause = getEngineClause(config);
168
+ expect(clause).toContain('ENGINE "VersionedCollapsingMergeTree"');
169
+ expect(clause).toContain('ENGINE_SIGN "sign_col"');
170
+ expect(clause).toContain('ENGINE_VERSION "version_col"');
171
+ });
172
+ });
173
+
174
+ describe('Helper functions', () => {
175
+ it('getSortingKey returns array from string', () => {
176
+ const config = engine.mergeTree({ sortingKey: 'id' });
177
+ expect(getSortingKey(config)).toEqual(['id']);
178
+ });
179
+
180
+ it('getSortingKey returns array from array', () => {
181
+ const config = engine.mergeTree({ sortingKey: ['id', 'timestamp'] });
182
+ expect(getSortingKey(config)).toEqual(['id', 'timestamp']);
183
+ });
184
+
185
+ it('getPrimaryKey defaults to sorting key', () => {
186
+ const config = engine.mergeTree({ sortingKey: ['id'] });
187
+ expect(getPrimaryKey(config)).toEqual(['id']);
188
+ });
189
+
190
+ it('getPrimaryKey returns explicit primary key', () => {
191
+ const config = engine.mergeTree({
192
+ sortingKey: ['id', 'timestamp'],
193
+ primaryKey: ['id'],
194
+ });
195
+ expect(getPrimaryKey(config)).toEqual(['id']);
196
+ });
197
+ });
198
+
199
+ describe('ENGINE_SETTINGS string escaping', () => {
200
+ it('quotes string values in settings', () => {
201
+ const config = engine.mergeTree({
202
+ sortingKey: ['id'],
203
+ settings: {
204
+ storage_policy: 'tiered',
205
+ },
206
+ });
207
+ const clause = getEngineClause(config);
208
+ expect(clause).toContain("storage_policy='tiered'");
209
+ });
210
+
211
+ it('does not quote numeric values in settings', () => {
212
+ const config = engine.mergeTree({
213
+ sortingKey: ['id'],
214
+ settings: {
215
+ index_granularity: 8192,
216
+ },
217
+ });
218
+ const clause = getEngineClause(config);
219
+ expect(clause).toContain('index_granularity=8192');
220
+ expect(clause).not.toContain("index_granularity='8192'");
221
+ });
222
+
223
+ it('escapes single quotes in string values', () => {
224
+ const config = engine.mergeTree({
225
+ sortingKey: ['id'],
226
+ settings: {
227
+ comment: "it's a test",
228
+ },
229
+ });
230
+ const clause = getEngineClause(config);
231
+ expect(clause).toContain("comment='it\\'s a test'");
232
+ });
233
+
234
+ it('handles mixed string and numeric settings', () => {
235
+ const config = engine.mergeTree({
236
+ sortingKey: ['id'],
237
+ settings: {
238
+ storage_policy: 'default',
239
+ index_granularity: 8192,
240
+ },
241
+ });
242
+ const clause = getEngineClause(config);
243
+ expect(clause).toContain("storage_policy='default'");
244
+ expect(clause).toContain('index_granularity=8192');
245
+ });
246
+ });
247
+ });
@@ -0,0 +1,271 @@
1
+ /**
2
+ * Engine configurations for Tinybird datasources
3
+ * ClickHouse table engines determine how data is stored and queried
4
+ */
5
+
6
+ /**
7
+ * Base configuration shared by all MergeTree engines
8
+ */
9
+ export interface BaseMergeTreeConfig {
10
+ /** Columns used for sorting data within parts (required for all MergeTree engines) */
11
+ sortingKey: string | readonly string[];
12
+ /** Expression for partitioning data (e.g., 'toYYYYMM(timestamp)') */
13
+ partitionKey?: string;
14
+ /** Primary key columns (defaults to sortingKey if not specified) */
15
+ primaryKey?: string | readonly string[];
16
+ /** TTL expression for automatic data expiration (e.g., 'timestamp + INTERVAL 90 DAY') */
17
+ ttl?: string;
18
+ /** Additional engine settings */
19
+ settings?: Record<string, string | number | boolean>;
20
+ }
21
+
22
+ /**
23
+ * MergeTree engine configuration
24
+ * The most universal and functional table engine for high-load tasks
25
+ */
26
+ export interface MergeTreeConfig extends BaseMergeTreeConfig {
27
+ type: "MergeTree";
28
+ }
29
+
30
+ /**
31
+ * ReplacingMergeTree engine configuration
32
+ * Removes duplicate rows with the same sorting key during merges
33
+ */
34
+ export interface ReplacingMergeTreeConfig extends BaseMergeTreeConfig {
35
+ type: "ReplacingMergeTree";
36
+ /** Optional version column - rows with highest version are kept */
37
+ ver?: string;
38
+ /** Optional flag to enable clean mode (ClickHouse 23.2+) */
39
+ isDeleted?: string;
40
+ }
41
+
42
+ /**
43
+ * SummingMergeTree engine configuration
44
+ * Sums numeric columns for rows with the same sorting key during merges
45
+ */
46
+ export interface SummingMergeTreeConfig extends BaseMergeTreeConfig {
47
+ type: "SummingMergeTree";
48
+ /** Columns to sum (if not specified, all numeric columns are summed) */
49
+ columns?: readonly string[];
50
+ }
51
+
52
+ /**
53
+ * AggregatingMergeTree engine configuration
54
+ * For incremental data aggregation with AggregateFunction columns
55
+ */
56
+ export interface AggregatingMergeTreeConfig extends BaseMergeTreeConfig {
57
+ type: "AggregatingMergeTree";
58
+ }
59
+
60
+ /**
61
+ * CollapsingMergeTree engine configuration
62
+ * For collapsing rows that cancel each other out
63
+ */
64
+ export interface CollapsingMergeTreeConfig extends BaseMergeTreeConfig {
65
+ type: "CollapsingMergeTree";
66
+ /** Column containing sign (1 for state, -1 for cancel) */
67
+ sign: string;
68
+ }
69
+
70
+ /**
71
+ * VersionedCollapsingMergeTree engine configuration
72
+ * For collapsing with versioning when events may arrive out of order
73
+ */
74
+ export interface VersionedCollapsingMergeTreeConfig extends BaseMergeTreeConfig {
75
+ type: "VersionedCollapsingMergeTree";
76
+ /** Column containing sign (1 for state, -1 for cancel) */
77
+ sign: string;
78
+ /** Column containing version number */
79
+ version: string;
80
+ }
81
+
82
+ /**
83
+ * Union type of all engine configurations
84
+ */
85
+ export type EngineConfig =
86
+ | MergeTreeConfig
87
+ | ReplacingMergeTreeConfig
88
+ | SummingMergeTreeConfig
89
+ | AggregatingMergeTreeConfig
90
+ | CollapsingMergeTreeConfig
91
+ | VersionedCollapsingMergeTreeConfig;
92
+
93
+ /**
94
+ * Helper to normalize sorting key to array format
95
+ */
96
+ function normalizeSortingKey(key: string | readonly string[]): readonly string[] {
97
+ return typeof key === "string" ? [key] : key;
98
+ }
99
+
100
+ /**
101
+ * Engine configuration builders
102
+ *
103
+ * @example
104
+ * ```ts
105
+ * import { engine } from '@tinybirdco/sdk';
106
+ *
107
+ * // Basic MergeTree
108
+ * engine.mergeTree({
109
+ * sortingKey: ['user_id', 'timestamp'],
110
+ * partitionKey: 'toYYYYMM(timestamp)',
111
+ * });
112
+ *
113
+ * // ReplacingMergeTree for upserts
114
+ * engine.replacingMergeTree({
115
+ * sortingKey: ['id'],
116
+ * ver: 'updated_at',
117
+ * });
118
+ *
119
+ * // SummingMergeTree for counters
120
+ * engine.summingMergeTree({
121
+ * sortingKey: ['date', 'metric_name'],
122
+ * columns: ['value'],
123
+ * });
124
+ * ```
125
+ */
126
+ export const engine = {
127
+ /**
128
+ * MergeTree - The most universal engine for high-load tasks
129
+ * Best for: General-purpose analytics, logs, events
130
+ */
131
+ mergeTree: (config: Omit<MergeTreeConfig, "type">): MergeTreeConfig => ({
132
+ type: "MergeTree",
133
+ ...config,
134
+ }),
135
+
136
+ /**
137
+ * ReplacingMergeTree - Removes duplicates during background merges
138
+ * Best for: Maintaining latest state, upserts, slowly changing dimensions
139
+ *
140
+ * @param config.ver - Optional version column. Rows with highest version are kept.
141
+ */
142
+ replacingMergeTree: (
143
+ config: Omit<ReplacingMergeTreeConfig, "type">
144
+ ): ReplacingMergeTreeConfig => ({
145
+ type: "ReplacingMergeTree",
146
+ ...config,
147
+ }),
148
+
149
+ /**
150
+ * SummingMergeTree - Sums numeric columns during background merges
151
+ * Best for: Counters, metrics aggregation, pre-aggregated data
152
+ *
153
+ * @param config.columns - Columns to sum. If not specified, all numeric columns are summed.
154
+ */
155
+ summingMergeTree: (
156
+ config: Omit<SummingMergeTreeConfig, "type">
157
+ ): SummingMergeTreeConfig => ({
158
+ type: "SummingMergeTree",
159
+ ...config,
160
+ }),
161
+
162
+ /**
163
+ * AggregatingMergeTree - For incremental aggregation with AggregateFunction columns
164
+ * Best for: Materialized views, incremental aggregation pipelines
165
+ */
166
+ aggregatingMergeTree: (
167
+ config: Omit<AggregatingMergeTreeConfig, "type">
168
+ ): AggregatingMergeTreeConfig => ({
169
+ type: "AggregatingMergeTree",
170
+ ...config,
171
+ }),
172
+
173
+ /**
174
+ * CollapsingMergeTree - For collapsing state/cancel row pairs
175
+ * Best for: Changelog-style updates, mutable data with deletes
176
+ *
177
+ * @param config.sign - Column containing 1 (state) or -1 (cancel)
178
+ */
179
+ collapsingMergeTree: (
180
+ config: Omit<CollapsingMergeTreeConfig, "type">
181
+ ): CollapsingMergeTreeConfig => ({
182
+ type: "CollapsingMergeTree",
183
+ ...config,
184
+ }),
185
+
186
+ /**
187
+ * VersionedCollapsingMergeTree - Collapsing with versioning for out-of-order events
188
+ * Best for: Changelog-style updates with potential out-of-order arrival
189
+ *
190
+ * @param config.sign - Column containing 1 (state) or -1 (cancel)
191
+ * @param config.version - Column containing version number for ordering
192
+ */
193
+ versionedCollapsingMergeTree: (
194
+ config: Omit<VersionedCollapsingMergeTreeConfig, "type">
195
+ ): VersionedCollapsingMergeTreeConfig => ({
196
+ type: "VersionedCollapsingMergeTree",
197
+ ...config,
198
+ }),
199
+ } as const;
200
+
201
+ /**
202
+ * Get the sorting key as an array
203
+ */
204
+ export function getSortingKey(config: EngineConfig): readonly string[] {
205
+ return normalizeSortingKey(config.sortingKey);
206
+ }
207
+
208
+ /**
209
+ * Get the primary key as an array (defaults to sorting key)
210
+ */
211
+ export function getPrimaryKey(config: EngineConfig): readonly string[] {
212
+ if (config.primaryKey) {
213
+ return normalizeSortingKey(config.primaryKey);
214
+ }
215
+ return getSortingKey(config);
216
+ }
217
+
218
+ /**
219
+ * Generate the engine clause for a datasource file
220
+ */
221
+ export function getEngineClause(config: EngineConfig): string {
222
+ const parts: string[] = [`ENGINE "${config.type}"`];
223
+
224
+ if (config.partitionKey) {
225
+ parts.push(`ENGINE_PARTITION_KEY "${config.partitionKey}"`);
226
+ }
227
+
228
+ const sortingKey = getSortingKey(config);
229
+ parts.push(`ENGINE_SORTING_KEY "${sortingKey.join(", ")}"`);
230
+
231
+ if (config.primaryKey) {
232
+ const primaryKey = getPrimaryKey(config);
233
+ parts.push(`ENGINE_PRIMARY_KEY "${primaryKey.join(", ")}"`);
234
+ }
235
+
236
+ if (config.ttl) {
237
+ parts.push(`ENGINE_TTL "${config.ttl}"`);
238
+ }
239
+
240
+ // Engine-specific options
241
+ if (config.type === "ReplacingMergeTree" && config.ver) {
242
+ parts.push(`ENGINE_VER "${config.ver}"`);
243
+ }
244
+
245
+ if (config.type === "CollapsingMergeTree" || config.type === "VersionedCollapsingMergeTree") {
246
+ parts.push(`ENGINE_SIGN "${config.sign}"`);
247
+ }
248
+
249
+ if (config.type === "VersionedCollapsingMergeTree") {
250
+ parts.push(`ENGINE_VERSION "${config.version}"`);
251
+ }
252
+
253
+ if (config.type === "SummingMergeTree" && config.columns && config.columns.length > 0) {
254
+ parts.push(`ENGINE_SUMMING_COLUMNS "${config.columns.join(", ")}"`);
255
+ }
256
+
257
+ if (config.settings && Object.keys(config.settings).length > 0) {
258
+ const settingsStr = Object.entries(config.settings)
259
+ .map(([k, v]) => {
260
+ if (typeof v === "string") {
261
+ const escaped = v.replace(/'/g, "\\'");
262
+ return `${k}='${escaped}'`;
263
+ }
264
+ return `${k}=${v}`;
265
+ })
266
+ .join(", ");
267
+ parts.push(`ENGINE_SETTINGS "${settingsStr}"`);
268
+ }
269
+
270
+ return parts.join("\n");
271
+ }