@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.
- package/README.md +52 -13
- package/dist/api/branches.d.ts.map +1 -1
- package/dist/api/branches.js +6 -5
- package/dist/api/branches.js.map +1 -1
- package/dist/api/branches.test.js +32 -6
- package/dist/api/branches.test.js.map +1 -1
- package/dist/api/build.d.ts.map +1 -1
- package/dist/api/build.js +2 -1
- package/dist/api/build.js.map +1 -1
- package/dist/api/deploy.d.ts +42 -3
- package/dist/api/deploy.d.ts.map +1 -1
- package/dist/api/deploy.js +162 -19
- package/dist/api/deploy.js.map +1 -1
- package/dist/api/deploy.test.js +83 -31
- package/dist/api/deploy.test.js.map +1 -1
- package/dist/api/fetcher.d.ts +6 -0
- package/dist/api/fetcher.d.ts.map +1 -0
- package/dist/api/fetcher.js +13 -0
- package/dist/api/fetcher.js.map +1 -0
- package/dist/api/local.d.ts.map +1 -1
- package/dist/api/local.js +5 -4
- package/dist/api/local.js.map +1 -1
- package/dist/api/local.test.js.map +1 -1
- package/dist/api/resources.d.ts +178 -0
- package/dist/api/resources.d.ts.map +1 -0
- package/dist/api/resources.js +245 -0
- package/dist/api/resources.js.map +1 -0
- package/dist/api/resources.test.d.ts +2 -0
- package/dist/api/resources.test.d.ts.map +1 -0
- package/dist/api/resources.test.js +255 -0
- package/dist/api/resources.test.js.map +1 -0
- package/dist/api/workspaces.d.ts.map +1 -1
- package/dist/api/workspaces.js +2 -1
- package/dist/api/workspaces.js.map +1 -1
- package/dist/api/workspaces.test.js +9 -1
- package/dist/api/workspaces.test.js.map +1 -1
- package/dist/cli/auth.d.ts.map +1 -1
- package/dist/cli/auth.js +2 -1
- package/dist/cli/auth.js.map +1 -1
- package/dist/cli/commands/build.d.ts +3 -4
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/build.js +23 -25
- package/dist/cli/commands/build.js.map +1 -1
- package/dist/cli/commands/deploy.d.ts +41 -0
- package/dist/cli/commands/deploy.d.ts.map +1 -0
- package/dist/cli/commands/deploy.js +92 -0
- package/dist/cli/commands/deploy.js.map +1 -0
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +7 -3
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/init.d.ts +38 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +434 -23
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/init.test.js +190 -30
- package/dist/cli/commands/init.test.js.map +1 -1
- package/dist/cli/index.js +80 -15
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/utils/package-manager.d.ts +8 -0
- package/dist/cli/utils/package-manager.d.ts.map +1 -0
- package/dist/cli/utils/package-manager.js +45 -0
- package/dist/cli/utils/package-manager.js.map +1 -0
- package/dist/cli/utils/package-manager.test.d.ts +2 -0
- package/dist/cli/utils/package-manager.test.d.ts.map +1 -0
- package/dist/cli/utils/package-manager.test.js +85 -0
- package/dist/cli/utils/package-manager.test.js.map +1 -0
- package/dist/client/base.d.ts.map +1 -1
- package/dist/client/base.js +2 -1
- package/dist/client/base.js.map +1 -1
- package/dist/codegen/index.d.ts +39 -0
- package/dist/codegen/index.d.ts.map +1 -0
- package/dist/codegen/index.js +300 -0
- package/dist/codegen/index.js.map +1 -0
- package/dist/codegen/index.test.d.ts +2 -0
- package/dist/codegen/index.test.d.ts.map +1 -0
- package/dist/codegen/index.test.js +310 -0
- package/dist/codegen/index.test.js.map +1 -0
- package/dist/codegen/type-mapper.d.ts +20 -0
- package/dist/codegen/type-mapper.d.ts.map +1 -0
- package/dist/codegen/type-mapper.js +238 -0
- package/dist/codegen/type-mapper.js.map +1 -0
- package/dist/codegen/type-mapper.test.d.ts +2 -0
- package/dist/codegen/type-mapper.test.d.ts.map +1 -0
- package/dist/codegen/type-mapper.test.js +167 -0
- package/dist/codegen/type-mapper.test.js.map +1 -0
- package/dist/codegen/utils.d.ts +46 -0
- package/dist/codegen/utils.d.ts.map +1 -0
- package/dist/codegen/utils.js +141 -0
- package/dist/codegen/utils.js.map +1 -0
- package/dist/codegen/utils.test.d.ts +2 -0
- package/dist/codegen/utils.test.d.ts.map +1 -0
- package/dist/codegen/utils.test.js +178 -0
- package/dist/codegen/utils.test.js.map +1 -0
- package/dist/generator/index.d.ts +3 -0
- package/dist/generator/index.d.ts.map +1 -1
- package/dist/generator/index.js +17 -1
- package/dist/generator/index.js.map +1 -1
- package/dist/generator/index.test.js +104 -1
- package/dist/generator/index.test.js.map +1 -1
- package/dist/generator/loader.d.ts +15 -0
- package/dist/generator/loader.d.ts.map +1 -1
- package/dist/generator/loader.js +24 -0
- package/dist/generator/loader.js.map +1 -1
- package/dist/schema/connection.d.ts.map +1 -1
- package/dist/schema/connection.js +3 -2
- package/dist/schema/connection.js.map +1 -1
- package/dist/schema/datasource.d.ts.map +1 -1
- package/dist/schema/datasource.js +3 -2
- package/dist/schema/datasource.js.map +1 -1
- package/dist/schema/params.d.ts.map +1 -1
- package/dist/schema/params.js +3 -2
- package/dist/schema/params.js.map +1 -1
- package/dist/schema/pipe.d.ts +2 -2
- package/dist/schema/pipe.d.ts.map +1 -1
- package/dist/schema/pipe.js +4 -4
- package/dist/schema/pipe.js.map +1 -1
- package/dist/schema/project.d.ts.map +1 -1
- package/dist/schema/project.js +3 -2
- package/dist/schema/project.js.map +1 -1
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema/types.js +3 -2
- package/dist/schema/types.js.map +1 -1
- package/dist/test/handlers.d.ts +49 -0
- package/dist/test/handlers.d.ts.map +1 -1
- package/dist/test/handlers.js +45 -0
- package/dist/test/handlers.js.map +1 -1
- package/package.json +4 -2
- package/src/api/branches.test.ts +65 -57
- package/src/api/branches.ts +7 -5
- package/src/api/build.ts +2 -1
- package/src/api/deploy.test.ts +141 -36
- package/src/api/deploy.ts +231 -23
- package/src/api/fetcher.ts +17 -0
- package/src/api/local.test.ts +43 -31
- package/src/api/local.ts +5 -4
- package/src/api/resources.test.ts +332 -0
- package/src/api/resources.ts +555 -0
- package/src/api/workspaces.test.ts +15 -9
- package/src/api/workspaces.ts +3 -1
- package/src/cli/auth.ts +2 -1
- package/src/cli/commands/build.ts +29 -33
- package/src/cli/commands/deploy.ts +131 -0
- package/src/cli/commands/dev.ts +10 -3
- package/src/cli/commands/init.test.ts +239 -30
- package/src/cli/commands/init.ts +548 -26
- package/src/cli/index.ts +117 -20
- package/src/cli/utils/package-manager.test.ts +118 -0
- package/src/cli/utils/package-manager.ts +44 -0
- package/src/client/base.ts +3 -2
- package/src/codegen/index.test.ts +367 -0
- package/src/codegen/index.ts +379 -0
- package/src/codegen/type-mapper.test.ts +224 -0
- package/src/codegen/type-mapper.ts +265 -0
- package/src/codegen/utils.test.ts +221 -0
- package/src/codegen/utils.ts +174 -0
- package/src/generator/index.test.ts +121 -1
- package/src/generator/index.ts +19 -1
- package/src/generator/loader.ts +43 -0
- package/src/schema/connection.ts +3 -2
- package/src/schema/datasource.ts +3 -2
- package/src/schema/params.ts +3 -2
- package/src/schema/pipe.ts +4 -4
- package/src/schema/project.ts +3 -2
- package/src/schema/types.ts +3 -2
- 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
|
+
}
|