@tinybirdco/sdk 0.0.4 → 0.0.7

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 (165) hide show
  1. package/README.md +52 -13
  2. package/dist/api/branches.d.ts.map +1 -1
  3. package/dist/api/branches.js +6 -5
  4. package/dist/api/branches.js.map +1 -1
  5. package/dist/api/branches.test.js +32 -6
  6. package/dist/api/branches.test.js.map +1 -1
  7. package/dist/api/build.d.ts.map +1 -1
  8. package/dist/api/build.js +2 -1
  9. package/dist/api/build.js.map +1 -1
  10. package/dist/api/deploy.d.ts +42 -3
  11. package/dist/api/deploy.d.ts.map +1 -1
  12. package/dist/api/deploy.js +162 -19
  13. package/dist/api/deploy.js.map +1 -1
  14. package/dist/api/deploy.test.js +83 -31
  15. package/dist/api/deploy.test.js.map +1 -1
  16. package/dist/api/fetcher.d.ts +6 -0
  17. package/dist/api/fetcher.d.ts.map +1 -0
  18. package/dist/api/fetcher.js +13 -0
  19. package/dist/api/fetcher.js.map +1 -0
  20. package/dist/api/local.d.ts.map +1 -1
  21. package/dist/api/local.js +5 -4
  22. package/dist/api/local.js.map +1 -1
  23. package/dist/api/local.test.js.map +1 -1
  24. package/dist/api/resources.d.ts +178 -0
  25. package/dist/api/resources.d.ts.map +1 -0
  26. package/dist/api/resources.js +245 -0
  27. package/dist/api/resources.js.map +1 -0
  28. package/dist/api/resources.test.d.ts +2 -0
  29. package/dist/api/resources.test.d.ts.map +1 -0
  30. package/dist/api/resources.test.js +255 -0
  31. package/dist/api/resources.test.js.map +1 -0
  32. package/dist/api/workspaces.d.ts.map +1 -1
  33. package/dist/api/workspaces.js +2 -1
  34. package/dist/api/workspaces.js.map +1 -1
  35. package/dist/api/workspaces.test.js +9 -1
  36. package/dist/api/workspaces.test.js.map +1 -1
  37. package/dist/cli/auth.d.ts.map +1 -1
  38. package/dist/cli/auth.js +2 -1
  39. package/dist/cli/auth.js.map +1 -1
  40. package/dist/cli/commands/build.d.ts +3 -4
  41. package/dist/cli/commands/build.d.ts.map +1 -1
  42. package/dist/cli/commands/build.js +23 -25
  43. package/dist/cli/commands/build.js.map +1 -1
  44. package/dist/cli/commands/deploy.d.ts +41 -0
  45. package/dist/cli/commands/deploy.d.ts.map +1 -0
  46. package/dist/cli/commands/deploy.js +92 -0
  47. package/dist/cli/commands/deploy.js.map +1 -0
  48. package/dist/cli/commands/dev.d.ts.map +1 -1
  49. package/dist/cli/commands/dev.js +7 -3
  50. package/dist/cli/commands/dev.js.map +1 -1
  51. package/dist/cli/commands/init.d.ts +38 -1
  52. package/dist/cli/commands/init.d.ts.map +1 -1
  53. package/dist/cli/commands/init.js +434 -23
  54. package/dist/cli/commands/init.js.map +1 -1
  55. package/dist/cli/commands/init.test.js +190 -30
  56. package/dist/cli/commands/init.test.js.map +1 -1
  57. package/dist/cli/index.js +80 -15
  58. package/dist/cli/index.js.map +1 -1
  59. package/dist/cli/utils/package-manager.d.ts +8 -0
  60. package/dist/cli/utils/package-manager.d.ts.map +1 -0
  61. package/dist/cli/utils/package-manager.js +45 -0
  62. package/dist/cli/utils/package-manager.js.map +1 -0
  63. package/dist/cli/utils/package-manager.test.d.ts +2 -0
  64. package/dist/cli/utils/package-manager.test.d.ts.map +1 -0
  65. package/dist/cli/utils/package-manager.test.js +85 -0
  66. package/dist/cli/utils/package-manager.test.js.map +1 -0
  67. package/dist/client/base.d.ts.map +1 -1
  68. package/dist/client/base.js +2 -1
  69. package/dist/client/base.js.map +1 -1
  70. package/dist/codegen/index.d.ts +39 -0
  71. package/dist/codegen/index.d.ts.map +1 -0
  72. package/dist/codegen/index.js +300 -0
  73. package/dist/codegen/index.js.map +1 -0
  74. package/dist/codegen/index.test.d.ts +2 -0
  75. package/dist/codegen/index.test.d.ts.map +1 -0
  76. package/dist/codegen/index.test.js +310 -0
  77. package/dist/codegen/index.test.js.map +1 -0
  78. package/dist/codegen/type-mapper.d.ts +20 -0
  79. package/dist/codegen/type-mapper.d.ts.map +1 -0
  80. package/dist/codegen/type-mapper.js +238 -0
  81. package/dist/codegen/type-mapper.js.map +1 -0
  82. package/dist/codegen/type-mapper.test.d.ts +2 -0
  83. package/dist/codegen/type-mapper.test.d.ts.map +1 -0
  84. package/dist/codegen/type-mapper.test.js +167 -0
  85. package/dist/codegen/type-mapper.test.js.map +1 -0
  86. package/dist/codegen/utils.d.ts +46 -0
  87. package/dist/codegen/utils.d.ts.map +1 -0
  88. package/dist/codegen/utils.js +141 -0
  89. package/dist/codegen/utils.js.map +1 -0
  90. package/dist/codegen/utils.test.d.ts +2 -0
  91. package/dist/codegen/utils.test.d.ts.map +1 -0
  92. package/dist/codegen/utils.test.js +178 -0
  93. package/dist/codegen/utils.test.js.map +1 -0
  94. package/dist/generator/index.d.ts +3 -0
  95. package/dist/generator/index.d.ts.map +1 -1
  96. package/dist/generator/index.js +17 -1
  97. package/dist/generator/index.js.map +1 -1
  98. package/dist/generator/index.test.js +104 -1
  99. package/dist/generator/index.test.js.map +1 -1
  100. package/dist/generator/loader.d.ts +15 -0
  101. package/dist/generator/loader.d.ts.map +1 -1
  102. package/dist/generator/loader.js +24 -0
  103. package/dist/generator/loader.js.map +1 -1
  104. package/dist/schema/connection.d.ts.map +1 -1
  105. package/dist/schema/connection.js +3 -2
  106. package/dist/schema/connection.js.map +1 -1
  107. package/dist/schema/datasource.d.ts.map +1 -1
  108. package/dist/schema/datasource.js +3 -2
  109. package/dist/schema/datasource.js.map +1 -1
  110. package/dist/schema/params.d.ts.map +1 -1
  111. package/dist/schema/params.js +3 -2
  112. package/dist/schema/params.js.map +1 -1
  113. package/dist/schema/pipe.d.ts +2 -2
  114. package/dist/schema/pipe.d.ts.map +1 -1
  115. package/dist/schema/pipe.js +4 -4
  116. package/dist/schema/pipe.js.map +1 -1
  117. package/dist/schema/project.d.ts.map +1 -1
  118. package/dist/schema/project.js +3 -2
  119. package/dist/schema/project.js.map +1 -1
  120. package/dist/schema/types.d.ts.map +1 -1
  121. package/dist/schema/types.js +3 -2
  122. package/dist/schema/types.js.map +1 -1
  123. package/dist/test/handlers.d.ts +49 -0
  124. package/dist/test/handlers.d.ts.map +1 -1
  125. package/dist/test/handlers.js +45 -0
  126. package/dist/test/handlers.js.map +1 -1
  127. package/package.json +4 -2
  128. package/src/api/branches.test.ts +65 -57
  129. package/src/api/branches.ts +7 -5
  130. package/src/api/build.ts +2 -1
  131. package/src/api/deploy.test.ts +141 -36
  132. package/src/api/deploy.ts +231 -23
  133. package/src/api/fetcher.ts +17 -0
  134. package/src/api/local.test.ts +43 -31
  135. package/src/api/local.ts +5 -4
  136. package/src/api/resources.test.ts +332 -0
  137. package/src/api/resources.ts +555 -0
  138. package/src/api/workspaces.test.ts +15 -9
  139. package/src/api/workspaces.ts +3 -1
  140. package/src/cli/auth.ts +2 -1
  141. package/src/cli/commands/build.ts +29 -33
  142. package/src/cli/commands/deploy.ts +131 -0
  143. package/src/cli/commands/dev.ts +10 -3
  144. package/src/cli/commands/init.test.ts +239 -30
  145. package/src/cli/commands/init.ts +548 -26
  146. package/src/cli/index.ts +117 -20
  147. package/src/cli/utils/package-manager.test.ts +118 -0
  148. package/src/cli/utils/package-manager.ts +44 -0
  149. package/src/client/base.ts +3 -2
  150. package/src/codegen/index.test.ts +367 -0
  151. package/src/codegen/index.ts +379 -0
  152. package/src/codegen/type-mapper.test.ts +224 -0
  153. package/src/codegen/type-mapper.ts +265 -0
  154. package/src/codegen/utils.test.ts +221 -0
  155. package/src/codegen/utils.ts +174 -0
  156. package/src/generator/index.test.ts +121 -1
  157. package/src/generator/index.ts +19 -1
  158. package/src/generator/loader.ts +43 -0
  159. package/src/schema/connection.ts +3 -2
  160. package/src/schema/datasource.ts +3 -2
  161. package/src/schema/params.ts +3 -2
  162. package/src/schema/pipe.ts +4 -4
  163. package/src/schema/project.ts +3 -2
  164. package/src/schema/types.ts +3 -2
  165. package/src/test/handlers.ts +58 -0
@@ -0,0 +1,265 @@
1
+ /**
2
+ * Type mapping from ClickHouse types to TypeScript SDK validators
3
+ */
4
+
5
+ /**
6
+ * Parse enum values from an enum type string
7
+ * e.g., "'a' = 1, 'b' = 2" -> ["a", "b"]
8
+ */
9
+ function parseEnumValues(enumContent: string): string[] {
10
+ const values: string[] = [];
11
+ const regex = /'([^']+)'\s*=\s*\d+/g;
12
+ let match;
13
+ while ((match = regex.exec(enumContent)) !== null) {
14
+ values.push(match[1]);
15
+ }
16
+ return values;
17
+ }
18
+
19
+ /**
20
+ * Map a ClickHouse type to a t.* validator call
21
+ *
22
+ * Handles:
23
+ * - Basic types: String, Int32, Float64, DateTime, etc.
24
+ * - Nullable wrapper: Nullable(String) -> t.string().nullable()
25
+ * - LowCardinality wrapper: LowCardinality(String) -> t.string().lowCardinality()
26
+ * - Parameterized types: DateTime('UTC'), FixedString(10), Decimal(10, 2)
27
+ * - Complex types: Array(String), Map(String, Int32)
28
+ * - Aggregate functions: SimpleAggregateFunction(sum, UInt64)
29
+ */
30
+ export function clickhouseTypeToValidator(chType: string): string {
31
+ // Trim whitespace
32
+ chType = chType.trim();
33
+
34
+ // Handle Nullable wrapper
35
+ const nullableMatch = chType.match(/^Nullable\((.+)\)$/);
36
+ if (nullableMatch) {
37
+ const innerType = clickhouseTypeToValidator(nullableMatch[1]);
38
+ return `${innerType}.nullable()`;
39
+ }
40
+
41
+ // Handle LowCardinality wrapper
42
+ const lowCardMatch = chType.match(/^LowCardinality\((.+)\)$/);
43
+ if (lowCardMatch) {
44
+ const innerType = clickhouseTypeToValidator(lowCardMatch[1]);
45
+ // If inner type already has .nullable(), we need to handle this specially
46
+ // LowCardinality(Nullable(X)) should become t.X().nullable().lowCardinality()
47
+ // But the recursive call already returns t.X().nullable()
48
+ return `${innerType}.lowCardinality()`;
49
+ }
50
+
51
+ // Simple type mappings
52
+ const simpleTypeMap: Record<string, string> = {
53
+ String: "t.string()",
54
+ UUID: "t.uuid()",
55
+ Int8: "t.int8()",
56
+ Int16: "t.int16()",
57
+ Int32: "t.int32()",
58
+ Int64: "t.int64()",
59
+ Int128: "t.int128()",
60
+ Int256: "t.int256()",
61
+ UInt8: "t.uint8()",
62
+ UInt16: "t.uint16()",
63
+ UInt32: "t.uint32()",
64
+ UInt64: "t.uint64()",
65
+ UInt128: "t.uint128()",
66
+ UInt256: "t.uint256()",
67
+ Float32: "t.float32()",
68
+ Float64: "t.float64()",
69
+ Bool: "t.bool()",
70
+ Boolean: "t.bool()",
71
+ Date: "t.date()",
72
+ Date32: "t.date32()",
73
+ DateTime: "t.dateTime()",
74
+ JSON: "t.json()",
75
+ Object: "t.json()",
76
+ IPv4: "t.ipv4()",
77
+ IPv6: "t.ipv6()",
78
+ };
79
+
80
+ if (simpleTypeMap[chType]) {
81
+ return simpleTypeMap[chType];
82
+ }
83
+
84
+ // DateTime with timezone: DateTime('UTC')
85
+ const dtTzMatch = chType.match(/^DateTime\('([^']+)'\)$/);
86
+ if (dtTzMatch) {
87
+ return `t.dateTime("${dtTzMatch[1]}")`;
88
+ }
89
+
90
+ // DateTime64 with precision and optional timezone
91
+ const dt64Match = chType.match(/^DateTime64\((\d+)(?:,\s*'([^']+)')?\)$/);
92
+ if (dt64Match) {
93
+ const precision = dt64Match[1];
94
+ const tz = dt64Match[2];
95
+ if (tz) {
96
+ return `t.dateTime64(${precision}, "${tz}")`;
97
+ }
98
+ return `t.dateTime64(${precision})`;
99
+ }
100
+
101
+ // DateTime64 without precision (defaults to 3)
102
+ if (chType === "DateTime64") {
103
+ return "t.dateTime64(3)";
104
+ }
105
+
106
+ // FixedString(N)
107
+ const fixedMatch = chType.match(/^FixedString\((\d+)\)$/);
108
+ if (fixedMatch) {
109
+ return `t.fixedString(${fixedMatch[1]})`;
110
+ }
111
+
112
+ // Decimal(P, S) or Decimal(P)
113
+ const decMatch = chType.match(/^Decimal\((\d+)(?:,\s*(\d+))?\)$/);
114
+ if (decMatch) {
115
+ const precision = decMatch[1];
116
+ const scale = decMatch[2] ?? "0";
117
+ return `t.decimal(${precision}, ${scale})`;
118
+ }
119
+
120
+ // Decimal32, Decimal64, Decimal128, Decimal256 with scale
121
+ const decNMatch = chType.match(/^Decimal(32|64|128|256)\((\d+)\)$/);
122
+ if (decNMatch) {
123
+ const bits = decNMatch[1];
124
+ const scale = decNMatch[2];
125
+ const precisionMap: Record<string, number> = {
126
+ "32": 9,
127
+ "64": 18,
128
+ "128": 38,
129
+ "256": 76,
130
+ };
131
+ return `t.decimal(${precisionMap[bits]}, ${scale})`;
132
+ }
133
+
134
+ // Array(T)
135
+ const arrMatch = chType.match(/^Array\((.+)\)$/);
136
+ if (arrMatch) {
137
+ const innerType = clickhouseTypeToValidator(arrMatch[1]);
138
+ return `t.array(${innerType})`;
139
+ }
140
+
141
+ // Tuple(T1, T2, ...) - simplified handling
142
+ const tupleMatch = chType.match(/^Tuple\((.+)\)$/);
143
+ if (tupleMatch) {
144
+ // For complex tuples, we just use a JSON type
145
+ // TODO: Could parse and generate t.tuple() for simple cases
146
+ return `t.json() /* Tuple: ${chType} */`;
147
+ }
148
+
149
+ // Map(K, V)
150
+ const mapMatch = chType.match(/^Map\(([^,]+),\s*(.+)\)$/);
151
+ if (mapMatch) {
152
+ const keyType = clickhouseTypeToValidator(mapMatch[1]);
153
+ const valueType = clickhouseTypeToValidator(mapMatch[2]);
154
+ return `t.map(${keyType}, ${valueType})`;
155
+ }
156
+
157
+ // Enum8('a' = 1, 'b' = 2)
158
+ const enum8Match = chType.match(/^Enum8\((.+)\)$/);
159
+ if (enum8Match) {
160
+ const values = parseEnumValues(enum8Match[1]);
161
+ if (values.length > 0) {
162
+ return `t.enum8(${values.map((v) => `"${v}"`).join(", ")})`;
163
+ }
164
+ return `t.string() /* Enum8 */`;
165
+ }
166
+
167
+ // Enum16('a' = 1, 'b' = 2)
168
+ const enum16Match = chType.match(/^Enum16\((.+)\)$/);
169
+ if (enum16Match) {
170
+ const values = parseEnumValues(enum16Match[1]);
171
+ if (values.length > 0) {
172
+ return `t.enum16(${values.map((v) => `"${v}"`).join(", ")})`;
173
+ }
174
+ return `t.string() /* Enum16 */`;
175
+ }
176
+
177
+ // SimpleAggregateFunction(func, T)
178
+ const simpleAggMatch = chType.match(/^SimpleAggregateFunction\((\w+),\s*(.+)\)$/);
179
+ if (simpleAggMatch) {
180
+ const func = simpleAggMatch[1];
181
+ const innerType = clickhouseTypeToValidator(simpleAggMatch[2]);
182
+ return `t.simpleAggregateFunction("${func}", ${innerType})`;
183
+ }
184
+
185
+ // AggregateFunction(func, T)
186
+ const aggMatch = chType.match(/^AggregateFunction\((\w+),\s*(.+)\)$/);
187
+ if (aggMatch) {
188
+ const func = aggMatch[1];
189
+ const innerType = clickhouseTypeToValidator(aggMatch[2]);
190
+ return `t.aggregateFunction("${func}", ${innerType})`;
191
+ }
192
+
193
+ // Nested - treat as JSON
194
+ if (chType.startsWith("Nested(")) {
195
+ return `t.json() /* ${chType} */`;
196
+ }
197
+
198
+ // Fallback for unknown types
199
+ return `t.string() /* TODO: Unknown type: ${chType} */`;
200
+ }
201
+
202
+ /**
203
+ * Map a pipe parameter type to a p.* validator call
204
+ */
205
+ export function paramTypeToValidator(
206
+ paramType: string,
207
+ defaultValue?: string | number,
208
+ required: boolean = true
209
+ ): string {
210
+ // Normalize type
211
+ paramType = paramType.trim();
212
+
213
+ // Simple type mappings
214
+ const typeMap: Record<string, string> = {
215
+ String: "p.string()",
216
+ UUID: "p.uuid()",
217
+ Int8: "p.int8()",
218
+ Int16: "p.int16()",
219
+ Int32: "p.int32()",
220
+ Int64: "p.int64()",
221
+ UInt8: "p.uint8()",
222
+ UInt16: "p.uint16()",
223
+ UInt32: "p.uint32()",
224
+ UInt64: "p.uint64()",
225
+ Float32: "p.float32()",
226
+ Float64: "p.float64()",
227
+ Boolean: "p.boolean()",
228
+ Bool: "p.boolean()",
229
+ Date: "p.date()",
230
+ DateTime: "p.dateTime()",
231
+ DateTime64: "p.dateTime64()",
232
+ };
233
+
234
+ let validator = typeMap[paramType];
235
+
236
+ // Handle parameterized DateTime types
237
+ if (!validator) {
238
+ if (paramType.startsWith("DateTime64")) {
239
+ validator = "p.dateTime64()";
240
+ } else if (paramType.startsWith("DateTime")) {
241
+ validator = "p.dateTime()";
242
+ } else if (paramType.startsWith("Array")) {
243
+ // Array parameters - default to string array
244
+ validator = "p.array(p.string())";
245
+ } else {
246
+ // Default to string for unknown types
247
+ validator = "p.string()";
248
+ }
249
+ }
250
+
251
+ // Add optional with default if not required or has a default value
252
+ if (!required || defaultValue !== undefined) {
253
+ if (defaultValue !== undefined) {
254
+ const formattedDefault =
255
+ typeof defaultValue === "string" ? `"${defaultValue}"` : defaultValue;
256
+ // Replace () with .optional(value)
257
+ validator = validator.replace(/\(\)$/, `().optional(${formattedDefault})`);
258
+ } else {
259
+ // Just make it optional without a default
260
+ validator = validator.replace(/\(\)$/, "().optional()");
261
+ }
262
+ }
263
+
264
+ return validator;
265
+ }
@@ -0,0 +1,221 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ toCamelCase,
4
+ toPascalCase,
5
+ escapeString,
6
+ parseSortingKey,
7
+ generateEngineCode,
8
+ formatSqlForTemplate,
9
+ } from "./utils.js";
10
+
11
+ describe("toCamelCase", () => {
12
+ it("converts snake_case to camelCase", () => {
13
+ expect(toCamelCase("page_views")).toBe("pageViews");
14
+ expect(toCamelCase("user_session_data")).toBe("userSessionData");
15
+ });
16
+
17
+ it("converts kebab-case to camelCase", () => {
18
+ expect(toCamelCase("page-views")).toBe("pageViews");
19
+ expect(toCamelCase("user-session-data")).toBe("userSessionData");
20
+ });
21
+
22
+ it("lowercases first character of PascalCase", () => {
23
+ expect(toCamelCase("PageViews")).toBe("pageViews");
24
+ });
25
+
26
+ it("handles single word", () => {
27
+ expect(toCamelCase("events")).toBe("events");
28
+ });
29
+
30
+ it("prefixes reserved keywords with underscore", () => {
31
+ expect(toCamelCase("class")).toBe("_class");
32
+ expect(toCamelCase("function")).toBe("_function");
33
+ expect(toCamelCase("return")).toBe("_return");
34
+ expect(toCamelCase("import")).toBe("_import");
35
+ expect(toCamelCase("export")).toBe("_export");
36
+ });
37
+
38
+ it("prefixes names starting with numbers", () => {
39
+ expect(toCamelCase("123_test")).toBe("_123Test");
40
+ expect(toCamelCase("1events")).toBe("_1events");
41
+ });
42
+ });
43
+
44
+ describe("toPascalCase", () => {
45
+ it("converts snake_case to PascalCase", () => {
46
+ expect(toPascalCase("page_views")).toBe("PageViews");
47
+ expect(toPascalCase("user_session_data")).toBe("UserSessionData");
48
+ });
49
+
50
+ it("converts kebab-case to PascalCase", () => {
51
+ expect(toPascalCase("page-views")).toBe("PageViews");
52
+ });
53
+
54
+ it("uppercases first character", () => {
55
+ expect(toPascalCase("events")).toBe("Events");
56
+ });
57
+ });
58
+
59
+ describe("escapeString", () => {
60
+ it("escapes double quotes", () => {
61
+ expect(escapeString('hello "world"')).toBe('hello \\"world\\"');
62
+ });
63
+
64
+ it("escapes backslashes", () => {
65
+ expect(escapeString("path\\to\\file")).toBe("path\\\\to\\\\file");
66
+ });
67
+
68
+ it("escapes newlines", () => {
69
+ expect(escapeString("line1\nline2")).toBe("line1\\nline2");
70
+ });
71
+
72
+ it("escapes tabs", () => {
73
+ expect(escapeString("col1\tcol2")).toBe("col1\\tcol2");
74
+ });
75
+
76
+ it("handles combined escapes", () => {
77
+ expect(escapeString('say "hello\\world"\n')).toBe('say \\"hello\\\\world\\"\\n');
78
+ });
79
+ });
80
+
81
+ describe("parseSortingKey", () => {
82
+ it("parses single column", () => {
83
+ expect(parseSortingKey("timestamp")).toEqual(["timestamp"]);
84
+ });
85
+
86
+ it("parses multiple columns", () => {
87
+ expect(parseSortingKey("user_id, timestamp")).toEqual(["user_id", "timestamp"]);
88
+ });
89
+
90
+ it("trims whitespace", () => {
91
+ expect(parseSortingKey(" user_id , timestamp ")).toEqual(["user_id", "timestamp"]);
92
+ });
93
+
94
+ it("returns empty array for undefined", () => {
95
+ expect(parseSortingKey(undefined)).toEqual([]);
96
+ });
97
+
98
+ it("returns empty array for empty string", () => {
99
+ expect(parseSortingKey("")).toEqual([]);
100
+ });
101
+ });
102
+
103
+ describe("generateEngineCode", () => {
104
+ it("generates MergeTree engine code", () => {
105
+ const code = generateEngineCode({
106
+ type: "MergeTree",
107
+ sorting_key: "timestamp",
108
+ });
109
+ expect(code).toContain("engine.mergeTree");
110
+ expect(code).toContain('sortingKey: "timestamp"');
111
+ });
112
+
113
+ it("generates MergeTree with multiple sorting keys", () => {
114
+ const code = generateEngineCode({
115
+ type: "MergeTree",
116
+ sorting_key: "user_id, timestamp",
117
+ });
118
+ expect(code).toContain("engine.mergeTree");
119
+ expect(code).toContain('sortingKey: ["user_id", "timestamp"]');
120
+ });
121
+
122
+ it("includes partition key", () => {
123
+ const code = generateEngineCode({
124
+ type: "MergeTree",
125
+ sorting_key: "timestamp",
126
+ partition_key: "toYYYYMM(timestamp)",
127
+ });
128
+ expect(code).toContain('partitionKey: "toYYYYMM(timestamp)"');
129
+ });
130
+
131
+ it("includes TTL", () => {
132
+ const code = generateEngineCode({
133
+ type: "MergeTree",
134
+ sorting_key: "timestamp",
135
+ ttl: "timestamp + INTERVAL 90 DAY",
136
+ });
137
+ expect(code).toContain('ttl: "timestamp + INTERVAL 90 DAY"');
138
+ });
139
+
140
+ it("generates ReplacingMergeTree with ver column", () => {
141
+ const code = generateEngineCode({
142
+ type: "ReplacingMergeTree",
143
+ sorting_key: "id",
144
+ ver: "updated_at",
145
+ });
146
+ expect(code).toContain("engine.replacingMergeTree");
147
+ expect(code).toContain('ver: "updated_at"');
148
+ });
149
+
150
+ it("generates SummingMergeTree with columns", () => {
151
+ const code = generateEngineCode({
152
+ type: "SummingMergeTree",
153
+ sorting_key: "date, category",
154
+ summing_columns: "count, total",
155
+ });
156
+ expect(code).toContain("engine.summingMergeTree");
157
+ expect(code).toContain('columns: ["count", "total"]');
158
+ });
159
+
160
+ it("generates AggregatingMergeTree", () => {
161
+ const code = generateEngineCode({
162
+ type: "AggregatingMergeTree",
163
+ sorting_key: "date",
164
+ });
165
+ expect(code).toContain("engine.aggregatingMergeTree");
166
+ });
167
+
168
+ it("generates CollapsingMergeTree with sign column", () => {
169
+ const code = generateEngineCode({
170
+ type: "CollapsingMergeTree",
171
+ sorting_key: "id, timestamp",
172
+ sign: "sign",
173
+ });
174
+ expect(code).toContain("engine.collapsingMergeTree");
175
+ expect(code).toContain('sign: "sign"');
176
+ });
177
+
178
+ it("generates VersionedCollapsingMergeTree", () => {
179
+ const code = generateEngineCode({
180
+ type: "VersionedCollapsingMergeTree",
181
+ sorting_key: "id",
182
+ sign: "sign",
183
+ version: "version",
184
+ });
185
+ expect(code).toContain("engine.versionedCollapsingMergeTree");
186
+ expect(code).toContain('sign: "sign"');
187
+ expect(code).toContain('version: "version"');
188
+ });
189
+
190
+ it("defaults to mergeTree for unknown engine types", () => {
191
+ const code = generateEngineCode({
192
+ type: "UnknownEngine",
193
+ sorting_key: "id",
194
+ });
195
+ expect(code).toContain("engine.mergeTree");
196
+ });
197
+ });
198
+
199
+ describe("formatSqlForTemplate", () => {
200
+ it("escapes backticks", () => {
201
+ expect(formatSqlForTemplate("SELECT `column` FROM table")).toBe(
202
+ "SELECT \\`column\\` FROM table"
203
+ );
204
+ });
205
+
206
+ it("escapes template literal interpolations", () => {
207
+ expect(formatSqlForTemplate("SELECT ${column} FROM table")).toBe(
208
+ "SELECT \\${column} FROM table"
209
+ );
210
+ });
211
+
212
+ it("preserves newlines", () => {
213
+ const sql = "SELECT *\nFROM table\nWHERE id = 1";
214
+ expect(formatSqlForTemplate(sql)).toBe(sql);
215
+ });
216
+
217
+ it("preserves Tinybird template syntax", () => {
218
+ const sql = "SELECT * FROM table WHERE id = {{Int32(id)}}";
219
+ expect(formatSqlForTemplate(sql)).toBe(sql);
220
+ });
221
+ });
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Utility functions for code generation
3
+ */
4
+
5
+ /**
6
+ * Convert a string to camelCase
7
+ * Handles snake_case and kebab-case
8
+ */
9
+ export function toCamelCase(str: string): string {
10
+ // Handle reserved keywords
11
+ const reserved = new Set([
12
+ "break", "case", "catch", "class", "const", "continue", "debugger",
13
+ "default", "delete", "do", "else", "enum", "export", "extends",
14
+ "false", "finally", "for", "function", "if", "import", "in",
15
+ "instanceof", "new", "null", "return", "super", "switch", "this",
16
+ "throw", "true", "try", "typeof", "undefined", "var", "void",
17
+ "while", "with", "yield", "let", "static", "implements", "interface",
18
+ "package", "private", "protected", "public", "await", "async",
19
+ ]);
20
+
21
+ const result = str
22
+ .replace(/[-_](.)/g, (_, char) => char.toUpperCase())
23
+ .replace(/^[A-Z]/, (char) => char.toLowerCase());
24
+
25
+ // If the result is a reserved keyword or starts with a number, prefix with underscore
26
+ if (reserved.has(result) || /^\d/.test(result)) {
27
+ return `_${result}`;
28
+ }
29
+
30
+ return result;
31
+ }
32
+
33
+ /**
34
+ * Convert a string to PascalCase
35
+ * Handles snake_case and kebab-case
36
+ */
37
+ export function toPascalCase(str: string): string {
38
+ const camel = toCamelCase(str);
39
+ return camel.charAt(0).toUpperCase() + camel.slice(1);
40
+ }
41
+
42
+ /**
43
+ * Escape a string for use in JavaScript/TypeScript code
44
+ */
45
+ export function escapeString(str: string): string {
46
+ return str
47
+ .replace(/\\/g, "\\\\")
48
+ .replace(/"/g, '\\"')
49
+ .replace(/\n/g, "\\n")
50
+ .replace(/\r/g, "\\r")
51
+ .replace(/\t/g, "\\t");
52
+ }
53
+
54
+ /**
55
+ * Parse a sorting key string into an array
56
+ * Handles comma-separated values and quoted identifiers
57
+ */
58
+ export function parseSortingKey(sortingKey?: string): string[] {
59
+ if (!sortingKey) {
60
+ return [];
61
+ }
62
+
63
+ // Split by comma, trim whitespace
64
+ return sortingKey
65
+ .split(",")
66
+ .map((s) => s.trim())
67
+ .filter((s) => s.length > 0);
68
+ }
69
+
70
+ /**
71
+ * Generate engine code from engine info
72
+ */
73
+ export function generateEngineCode(engine: {
74
+ type: string;
75
+ sorting_key?: string;
76
+ partition_key?: string;
77
+ primary_key?: string;
78
+ ttl?: string;
79
+ ver?: string;
80
+ sign?: string;
81
+ version?: string;
82
+ summing_columns?: string;
83
+ }): string {
84
+ const sortingKey = parseSortingKey(engine.sorting_key);
85
+
86
+ // Build options object
87
+ const options: string[] = [];
88
+
89
+ // Sorting key is required for all MergeTree engines
90
+ if (sortingKey.length === 1) {
91
+ options.push(`sortingKey: "${sortingKey[0]}"`);
92
+ } else if (sortingKey.length > 1) {
93
+ options.push(`sortingKey: [${sortingKey.map((k) => `"${k}"`).join(", ")}]`);
94
+ }
95
+
96
+ // Optional fields
97
+ if (engine.partition_key) {
98
+ options.push(`partitionKey: "${escapeString(engine.partition_key)}"`);
99
+ }
100
+
101
+ if (engine.primary_key && engine.primary_key !== engine.sorting_key) {
102
+ const primaryKey = parseSortingKey(engine.primary_key);
103
+ if (primaryKey.length === 1) {
104
+ options.push(`primaryKey: "${primaryKey[0]}"`);
105
+ } else if (primaryKey.length > 1) {
106
+ options.push(`primaryKey: [${primaryKey.map((k) => `"${k}"`).join(", ")}]`);
107
+ }
108
+ }
109
+
110
+ if (engine.ttl) {
111
+ options.push(`ttl: "${escapeString(engine.ttl)}"`);
112
+ }
113
+
114
+ // Engine-specific options
115
+ if (engine.type === "ReplacingMergeTree" && engine.ver) {
116
+ options.push(`ver: "${engine.ver}"`);
117
+ }
118
+
119
+ if (
120
+ (engine.type === "CollapsingMergeTree" ||
121
+ engine.type === "VersionedCollapsingMergeTree") &&
122
+ engine.sign
123
+ ) {
124
+ options.push(`sign: "${engine.sign}"`);
125
+ }
126
+
127
+ if (engine.type === "VersionedCollapsingMergeTree" && engine.version) {
128
+ options.push(`version: "${engine.version}"`);
129
+ }
130
+
131
+ if (engine.type === "SummingMergeTree" && engine.summing_columns) {
132
+ const columns = parseSortingKey(engine.summing_columns);
133
+ if (columns.length > 0) {
134
+ options.push(`columns: [${columns.map((c) => `"${c}"`).join(", ")}]`);
135
+ }
136
+ }
137
+
138
+ // Map engine type to function name
139
+ const engineFunctionMap: Record<string, string> = {
140
+ MergeTree: "engine.mergeTree",
141
+ ReplacingMergeTree: "engine.replacingMergeTree",
142
+ SummingMergeTree: "engine.summingMergeTree",
143
+ AggregatingMergeTree: "engine.aggregatingMergeTree",
144
+ CollapsingMergeTree: "engine.collapsingMergeTree",
145
+ VersionedCollapsingMergeTree: "engine.versionedCollapsingMergeTree",
146
+ };
147
+
148
+ const engineFunc = engineFunctionMap[engine.type] ?? "engine.mergeTree";
149
+
150
+ if (options.length === 0) {
151
+ return `${engineFunc}({ sortingKey: [] })`;
152
+ }
153
+
154
+ return `${engineFunc}({\n ${options.join(",\n ")},\n })`;
155
+ }
156
+
157
+ /**
158
+ * Indent a multi-line string
159
+ */
160
+ export function indent(str: string, spaces: number): string {
161
+ const prefix = " ".repeat(spaces);
162
+ return str
163
+ .split("\n")
164
+ .map((line) => (line.trim() ? prefix + line : line))
165
+ .join("\n");
166
+ }
167
+
168
+ /**
169
+ * Format SQL for inclusion in template literal
170
+ * Preserves newlines and indentation but escapes backticks
171
+ */
172
+ export function formatSqlForTemplate(sql: string): string {
173
+ return sql.replace(/`/g, "\\`").replace(/\${/g, "\\${");
174
+ }