@tinybirdco/sdk 0.0.49 → 0.0.51
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 +19 -2
- package/dist/cli/commands/migrate.d.ts.map +1 -1
- package/dist/cli/commands/migrate.js +36 -1
- package/dist/cli/commands/migrate.js.map +1 -1
- package/dist/cli/commands/migrate.test.js +307 -2
- package/dist/cli/commands/migrate.test.js.map +1 -1
- package/dist/codegen/type-mapper.d.ts.map +1 -1
- package/dist/codegen/type-mapper.js +70 -7
- package/dist/codegen/type-mapper.js.map +1 -1
- package/dist/codegen/type-mapper.test.js +9 -0
- package/dist/codegen/type-mapper.test.js.map +1 -1
- package/dist/generator/connection.d.ts.map +1 -1
- package/dist/generator/connection.js +14 -1
- package/dist/generator/connection.js.map +1 -1
- package/dist/generator/connection.test.js +20 -4
- package/dist/generator/connection.test.js.map +1 -1
- package/dist/generator/datasource.d.ts.map +1 -1
- package/dist/generator/datasource.js +39 -10
- package/dist/generator/datasource.js.map +1 -1
- package/dist/generator/datasource.test.js +42 -1
- package/dist/generator/datasource.test.js.map +1 -1
- package/dist/generator/pipe.d.ts.map +1 -1
- package/dist/generator/pipe.js +92 -3
- package/dist/generator/pipe.js.map +1 -1
- package/dist/generator/pipe.test.js +19 -0
- package/dist/generator/pipe.test.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/migrate/emit-ts.d.ts.map +1 -1
- package/dist/migrate/emit-ts.js +56 -11
- package/dist/migrate/emit-ts.js.map +1 -1
- package/dist/migrate/parse-connection.d.ts +2 -2
- package/dist/migrate/parse-connection.d.ts.map +1 -1
- package/dist/migrate/parse-connection.js +34 -4
- package/dist/migrate/parse-connection.js.map +1 -1
- package/dist/migrate/parse-datasource.d.ts.map +1 -1
- package/dist/migrate/parse-datasource.js +39 -2
- package/dist/migrate/parse-datasource.js.map +1 -1
- package/dist/migrate/parse-pipe.d.ts.map +1 -1
- package/dist/migrate/parse-pipe.js +212 -93
- package/dist/migrate/parse-pipe.js.map +1 -1
- package/dist/migrate/parser-utils.d.ts.map +1 -1
- package/dist/migrate/parser-utils.js +3 -1
- package/dist/migrate/parser-utils.js.map +1 -1
- package/dist/migrate/types.d.ts +22 -1
- package/dist/migrate/types.d.ts.map +1 -1
- package/dist/schema/connection.d.ts +34 -1
- package/dist/schema/connection.d.ts.map +1 -1
- package/dist/schema/connection.js +26 -0
- package/dist/schema/connection.js.map +1 -1
- package/dist/schema/connection.test.js +35 -1
- package/dist/schema/connection.test.js.map +1 -1
- package/dist/schema/datasource.d.ts +32 -1
- package/dist/schema/datasource.d.ts.map +1 -1
- package/dist/schema/datasource.js +19 -2
- package/dist/schema/datasource.js.map +1 -1
- package/dist/schema/datasource.test.js +71 -3
- package/dist/schema/datasource.test.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/migrate.test.ts +448 -2
- package/src/cli/commands/migrate.ts +39 -1
- package/src/codegen/type-mapper.test.ts +18 -0
- package/src/codegen/type-mapper.ts +79 -7
- package/src/generator/connection.test.ts +29 -4
- package/src/generator/connection.ts +25 -2
- package/src/generator/datasource.test.ts +52 -1
- package/src/generator/datasource.ts +47 -10
- package/src/generator/pipe.test.ts +21 -0
- package/src/generator/pipe.ts +119 -3
- package/src/index.ts +6 -0
- package/src/migrate/emit-ts.ts +67 -14
- package/src/migrate/parse-connection.ts +56 -6
- package/src/migrate/parse-datasource.ts +74 -3
- package/src/migrate/parse-pipe.ts +250 -111
- package/src/migrate/parser-utils.ts +5 -1
- package/src/migrate/types.ts +26 -1
- package/src/schema/connection.test.ts +48 -0
- package/src/schema/connection.ts +60 -1
- package/src/schema/datasource.test.ts +91 -3
- package/src/schema/datasource.ts +62 -3
|
@@ -70,35 +70,41 @@ function inferOutputColumnsFromSql(sql: string): string[] {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
function mapTemplateFunctionToParamType(func: string): string | null {
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
"
|
|
76
|
-
"
|
|
77
|
-
"
|
|
78
|
-
"Int32",
|
|
79
|
-
"
|
|
80
|
-
"
|
|
81
|
-
"
|
|
82
|
-
"
|
|
83
|
-
"
|
|
84
|
-
"
|
|
85
|
-
"
|
|
86
|
-
"
|
|
87
|
-
"
|
|
88
|
-
"
|
|
89
|
-
"
|
|
90
|
-
"
|
|
91
|
-
"
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
73
|
+
const lower = func.toLowerCase();
|
|
74
|
+
const aliases: Record<string, string> = {
|
|
75
|
+
string: "String",
|
|
76
|
+
uuid: "UUID",
|
|
77
|
+
int: "Int32",
|
|
78
|
+
integer: "Int32",
|
|
79
|
+
int8: "Int8",
|
|
80
|
+
int16: "Int16",
|
|
81
|
+
int32: "Int32",
|
|
82
|
+
int64: "Int64",
|
|
83
|
+
uint8: "UInt8",
|
|
84
|
+
uint16: "UInt16",
|
|
85
|
+
uint32: "UInt32",
|
|
86
|
+
uint64: "UInt64",
|
|
87
|
+
float32: "Float32",
|
|
88
|
+
float64: "Float64",
|
|
89
|
+
boolean: "Boolean",
|
|
90
|
+
bool: "Boolean",
|
|
91
|
+
date: "Date",
|
|
92
|
+
datetime: "DateTime",
|
|
93
|
+
datetime64: "DateTime64",
|
|
94
|
+
array: "Array",
|
|
95
|
+
column: "column",
|
|
96
|
+
json: "JSON",
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const mapped = aliases[lower];
|
|
100
|
+
if (mapped) {
|
|
101
|
+
return mapped;
|
|
96
102
|
}
|
|
97
103
|
|
|
98
|
-
if (
|
|
104
|
+
if (lower.startsWith("datetime64")) {
|
|
99
105
|
return "DateTime64";
|
|
100
106
|
}
|
|
101
|
-
if (
|
|
107
|
+
if (lower.startsWith("datetime")) {
|
|
102
108
|
return "DateTime";
|
|
103
109
|
}
|
|
104
110
|
|
|
@@ -157,8 +163,7 @@ function parseParamOptions(rawArgs: string[]): {
|
|
|
157
163
|
required?: boolean;
|
|
158
164
|
description?: string;
|
|
159
165
|
} {
|
|
160
|
-
let
|
|
161
|
-
let keywordDefault: string | number | boolean | undefined;
|
|
166
|
+
let defaultValue: string | number | boolean | undefined;
|
|
162
167
|
let required: boolean | undefined;
|
|
163
168
|
let description: string | undefined;
|
|
164
169
|
|
|
@@ -170,15 +175,13 @@ function parseParamOptions(rawArgs: string[]): {
|
|
|
170
175
|
|
|
171
176
|
const keyword = parseKeywordArgument(trimmed);
|
|
172
177
|
if (!keyword) {
|
|
173
|
-
|
|
174
|
-
positionalDefault = parseParamDefault(trimmed);
|
|
175
|
-
}
|
|
178
|
+
defaultValue = parseParamDefault(trimmed);
|
|
176
179
|
continue;
|
|
177
180
|
}
|
|
178
181
|
|
|
179
182
|
const keyLower = keyword.key.toLowerCase();
|
|
180
183
|
if (keyLower === "default") {
|
|
181
|
-
|
|
184
|
+
defaultValue = parseParamDefault(keyword.value);
|
|
182
185
|
continue;
|
|
183
186
|
}
|
|
184
187
|
if (keyLower === "required") {
|
|
@@ -195,17 +198,122 @@ function parseParamOptions(rawArgs: string[]): {
|
|
|
195
198
|
}
|
|
196
199
|
}
|
|
197
200
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
201
|
+
return { defaultValue, required, description };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function extractTemplateFunctionCalls(expression: string): Array<{
|
|
205
|
+
functionName: string;
|
|
206
|
+
argsRaw: string;
|
|
207
|
+
fullCall: string;
|
|
208
|
+
start: number;
|
|
209
|
+
end: number;
|
|
210
|
+
}> {
|
|
211
|
+
const maskParenthesesInsideQuotes = (value: string): string => {
|
|
212
|
+
let output = "";
|
|
213
|
+
let inSingleQuote = false;
|
|
214
|
+
let inDoubleQuote = false;
|
|
215
|
+
|
|
216
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
217
|
+
const char = value[i] ?? "";
|
|
218
|
+
const prev = i > 0 ? value[i - 1] ?? "" : "";
|
|
219
|
+
|
|
220
|
+
if (char === "'" && !inDoubleQuote && prev !== "\\") {
|
|
221
|
+
inSingleQuote = !inSingleQuote;
|
|
222
|
+
output += char;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (char === '"' && !inSingleQuote && prev !== "\\") {
|
|
226
|
+
inDoubleQuote = !inDoubleQuote;
|
|
227
|
+
output += char;
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if ((inSingleQuote || inDoubleQuote) && (char === "(" || char === ")")) {
|
|
232
|
+
output += " ";
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
output += char;
|
|
204
237
|
}
|
|
205
|
-
|
|
238
|
+
|
|
239
|
+
return output;
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const maskedExpression = maskParenthesesInsideQuotes(expression);
|
|
243
|
+
const callRegex = /([a-zA-Z_][a-zA-Z0-9_]*)\s*\(([^()]*)\)/g;
|
|
244
|
+
const calls: Array<{
|
|
245
|
+
functionName: string;
|
|
246
|
+
argsRaw: string;
|
|
247
|
+
fullCall: string;
|
|
248
|
+
start: number;
|
|
249
|
+
end: number;
|
|
250
|
+
}> = [];
|
|
251
|
+
let match: RegExpExecArray | null = callRegex.exec(maskedExpression);
|
|
252
|
+
while (match) {
|
|
253
|
+
const start = match.index;
|
|
254
|
+
const fullCall = expression.slice(start, start + (match[0]?.length ?? 0));
|
|
255
|
+
const openParen = fullCall.indexOf("(");
|
|
256
|
+
const closeParen = fullCall.lastIndexOf(")");
|
|
257
|
+
|
|
258
|
+
calls.push({
|
|
259
|
+
functionName: match[1] ?? "",
|
|
260
|
+
argsRaw: openParen >= 0 && closeParen > openParen ? fullCall.slice(openParen + 1, closeParen) : "",
|
|
261
|
+
fullCall,
|
|
262
|
+
start,
|
|
263
|
+
end: start + fullCall.length,
|
|
264
|
+
});
|
|
265
|
+
match = callRegex.exec(maskedExpression);
|
|
206
266
|
}
|
|
267
|
+
return calls;
|
|
268
|
+
}
|
|
207
269
|
|
|
208
|
-
|
|
270
|
+
function shouldParseTemplateFunctionAsParam(mappedType: string): boolean {
|
|
271
|
+
return mappedType !== "Array";
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function normalizeSqlPlaceholders(sql: string): string {
|
|
275
|
+
const placeholderRegex = /\{\{\s*([^{}]+?)\s*\}\}/g;
|
|
276
|
+
return sql.replace(placeholderRegex, (fullMatch, rawExpression) => {
|
|
277
|
+
const expression = String(rawExpression);
|
|
278
|
+
const calls = extractTemplateFunctionCalls(expression);
|
|
279
|
+
if (calls.length === 0) {
|
|
280
|
+
return fullMatch;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
let rewritten = "";
|
|
284
|
+
let cursor = 0;
|
|
285
|
+
let changed = false;
|
|
286
|
+
for (const call of calls) {
|
|
287
|
+
rewritten += expression.slice(cursor, call.start);
|
|
288
|
+
|
|
289
|
+
let replacement = call.fullCall;
|
|
290
|
+
const normalizedFunction = String(call.functionName).toLowerCase();
|
|
291
|
+
if (normalizedFunction !== "error" && normalizedFunction !== "custom_error") {
|
|
292
|
+
const mappedType = mapTemplateFunctionToParamType(String(call.functionName));
|
|
293
|
+
if (mappedType && shouldParseTemplateFunctionAsParam(mappedType)) {
|
|
294
|
+
const args = splitTopLevelComma(String(call.argsRaw));
|
|
295
|
+
if (args.length > 0) {
|
|
296
|
+
const paramName = args[0]?.trim() ?? "";
|
|
297
|
+
if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(paramName)) {
|
|
298
|
+
replacement = `${String(call.functionName)}(${paramName})`;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (replacement !== call.fullCall) {
|
|
305
|
+
changed = true;
|
|
306
|
+
}
|
|
307
|
+
rewritten += replacement;
|
|
308
|
+
cursor = call.end;
|
|
309
|
+
}
|
|
310
|
+
rewritten += expression.slice(cursor);
|
|
311
|
+
|
|
312
|
+
if (!changed) {
|
|
313
|
+
return fullMatch;
|
|
314
|
+
}
|
|
315
|
+
return `{{ ${rewritten.trim()} }}`;
|
|
316
|
+
});
|
|
209
317
|
}
|
|
210
318
|
|
|
211
319
|
function inferParamsFromSql(
|
|
@@ -213,100 +321,105 @@ function inferParamsFromSql(
|
|
|
213
321
|
filePath: string,
|
|
214
322
|
resourceName: string
|
|
215
323
|
): PipeParamModel[] {
|
|
216
|
-
const regex = /\{\{\s*([
|
|
324
|
+
const regex = /\{\{\s*([^{}]+?)\s*\}\}/g;
|
|
217
325
|
const params = new Map<string, PipeParamModel>();
|
|
218
326
|
let match: RegExpExecArray | null = regex.exec(sql);
|
|
219
327
|
|
|
220
328
|
while (match) {
|
|
221
|
-
const
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const paramName = args[0]?.trim();
|
|
234
|
-
if (!paramName || !/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(paramName)) {
|
|
235
|
-
throw new MigrationParseError(
|
|
236
|
-
filePath,
|
|
237
|
-
"pipe",
|
|
238
|
-
resourceName,
|
|
239
|
-
`Unsupported parameter name in placeholder: "${match[0]}"`
|
|
240
|
-
);
|
|
241
|
-
}
|
|
329
|
+
const expression = match[1] ?? "";
|
|
330
|
+
const calls = extractTemplateFunctionCalls(expression);
|
|
331
|
+
|
|
332
|
+
for (const call of calls) {
|
|
333
|
+
const templateFunction = call.functionName;
|
|
334
|
+
const normalizedTemplateFunction = templateFunction.toLowerCase();
|
|
335
|
+
if (normalizedTemplateFunction === "error" || normalizedTemplateFunction === "custom_error") {
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
242
338
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
339
|
+
const mappedType = mapTemplateFunctionToParamType(templateFunction);
|
|
340
|
+
if (!mappedType) {
|
|
341
|
+
throw new MigrationParseError(
|
|
342
|
+
filePath,
|
|
343
|
+
"pipe",
|
|
344
|
+
resourceName,
|
|
345
|
+
`Unsupported placeholder function in strict mode: "${templateFunction}"`
|
|
346
|
+
);
|
|
347
|
+
}
|
|
252
348
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
let description: string | undefined;
|
|
256
|
-
if (args.length > 1) {
|
|
257
|
-
try {
|
|
258
|
-
const parsedOptions = parseParamOptions(args.slice(1));
|
|
259
|
-
defaultValue = parsedOptions.defaultValue;
|
|
260
|
-
required = parsedOptions.required;
|
|
261
|
-
description = parsedOptions.description;
|
|
262
|
-
} catch (error) {
|
|
349
|
+
const args = splitTopLevelComma(call.argsRaw);
|
|
350
|
+
if (args.length === 0) {
|
|
263
351
|
throw new MigrationParseError(
|
|
264
352
|
filePath,
|
|
265
353
|
"pipe",
|
|
266
354
|
resourceName,
|
|
267
|
-
|
|
355
|
+
`Invalid template placeholder: "${call.fullCall}"`
|
|
268
356
|
);
|
|
269
357
|
}
|
|
270
|
-
}
|
|
271
358
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
if (
|
|
359
|
+
const paramName = args[0]?.trim() ?? "";
|
|
360
|
+
const isIdentifier = /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(paramName);
|
|
361
|
+
if (!isIdentifier) {
|
|
362
|
+
if (mappedType === "column") {
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
275
365
|
throw new MigrationParseError(
|
|
276
366
|
filePath,
|
|
277
367
|
"pipe",
|
|
278
368
|
resourceName,
|
|
279
|
-
`
|
|
369
|
+
`Unsupported parameter name in placeholder: "{{ ${call.fullCall} }}"`
|
|
280
370
|
);
|
|
281
371
|
}
|
|
282
|
-
|
|
283
|
-
|
|
372
|
+
|
|
373
|
+
let defaultValue: string | number | boolean | undefined;
|
|
374
|
+
let required: boolean | undefined;
|
|
375
|
+
let description: string | undefined;
|
|
376
|
+
if (args.length > 1 && shouldParseTemplateFunctionAsParam(mappedType)) {
|
|
377
|
+
try {
|
|
378
|
+
const parsedOptions = parseParamOptions(args.slice(1));
|
|
379
|
+
defaultValue = parsedOptions.defaultValue;
|
|
380
|
+
required = parsedOptions.required;
|
|
381
|
+
description = parsedOptions.description;
|
|
382
|
+
} catch (error) {
|
|
284
383
|
throw new MigrationParseError(
|
|
285
384
|
filePath,
|
|
286
385
|
"pipe",
|
|
287
386
|
resourceName,
|
|
288
|
-
|
|
387
|
+
(error as Error).message
|
|
289
388
|
);
|
|
290
389
|
}
|
|
291
390
|
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
391
|
+
|
|
392
|
+
const existing = params.get(paramName);
|
|
393
|
+
if (existing) {
|
|
394
|
+
if (existing.type !== mappedType) {
|
|
395
|
+
// Keep the last explicit type seen in SQL.
|
|
396
|
+
existing.type = mappedType;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Match backend merge semantics: prefer the latest truthy value.
|
|
400
|
+
if (defaultValue !== undefined || existing.defaultValue !== undefined) {
|
|
401
|
+
existing.defaultValue =
|
|
402
|
+
(defaultValue as string | number | boolean | undefined) || existing.defaultValue;
|
|
403
|
+
}
|
|
404
|
+
if (description !== undefined || existing.description !== undefined) {
|
|
405
|
+
existing.description = description || existing.description;
|
|
406
|
+
}
|
|
407
|
+
const optionalInAnyUsage =
|
|
408
|
+
existing.required === false ||
|
|
409
|
+
required === false ||
|
|
410
|
+
existing.defaultValue !== undefined ||
|
|
411
|
+
defaultValue !== undefined;
|
|
412
|
+
existing.required = !optionalInAnyUsage;
|
|
413
|
+
} else {
|
|
414
|
+
const isRequired = required ?? defaultValue === undefined;
|
|
415
|
+
params.set(paramName, {
|
|
416
|
+
name: paramName,
|
|
417
|
+
type: mappedType,
|
|
418
|
+
required: isRequired,
|
|
419
|
+
defaultValue,
|
|
420
|
+
description,
|
|
421
|
+
});
|
|
297
422
|
}
|
|
298
|
-
const optionalInAnyUsage =
|
|
299
|
-
existing.required === false || required === false || defaultValue !== undefined;
|
|
300
|
-
existing.required = !optionalInAnyUsage;
|
|
301
|
-
} else {
|
|
302
|
-
const isRequired = required ?? defaultValue === undefined;
|
|
303
|
-
params.set(paramName, {
|
|
304
|
-
name: paramName,
|
|
305
|
-
type: mappedType,
|
|
306
|
-
required: isRequired,
|
|
307
|
-
defaultValue,
|
|
308
|
-
description,
|
|
309
|
-
});
|
|
310
423
|
}
|
|
311
424
|
|
|
312
425
|
match = regex.exec(sql);
|
|
@@ -355,9 +468,21 @@ function parseToken(filePath: string, resourceName: string, value: string): Pipe
|
|
|
355
468
|
return { name: tokenName, scope: "READ" };
|
|
356
469
|
}
|
|
357
470
|
|
|
471
|
+
function normalizeExportStrategy(rawValue: string): "create_new" | "replace" {
|
|
472
|
+
const normalized = parseQuotedValue(rawValue).toLowerCase();
|
|
473
|
+
if (normalized === "create_new") {
|
|
474
|
+
return "create_new";
|
|
475
|
+
}
|
|
476
|
+
if (normalized === "replace" || normalized === "truncate") {
|
|
477
|
+
return "replace";
|
|
478
|
+
}
|
|
479
|
+
throw new Error(`Unsupported sink strategy in strict mode: "${rawValue}"`);
|
|
480
|
+
}
|
|
481
|
+
|
|
358
482
|
export function parsePipeFile(resource: ResourceFile): PipeModel {
|
|
359
483
|
const lines = splitLines(resource.content);
|
|
360
484
|
const nodes: PipeModel["nodes"] = [];
|
|
485
|
+
const rawNodeSqls: string[] = [];
|
|
361
486
|
const tokens: PipeTokenModel[] = [];
|
|
362
487
|
let description: string | undefined;
|
|
363
488
|
let pipeType: PipeModel["type"] = "pipe";
|
|
@@ -458,10 +583,11 @@ export function parsePipeFile(resource: ResourceFile): PipeModel {
|
|
|
458
583
|
);
|
|
459
584
|
}
|
|
460
585
|
|
|
586
|
+
rawNodeSqls.push(sql);
|
|
461
587
|
nodes.push({
|
|
462
588
|
name: nodeName,
|
|
463
589
|
description: nodeDescription,
|
|
464
|
-
sql,
|
|
590
|
+
sql: normalizeSqlPlaceholders(sql),
|
|
465
591
|
});
|
|
466
592
|
|
|
467
593
|
i = sqlBlock.nextIndex;
|
|
@@ -566,8 +692,9 @@ export function parsePipeFile(resource: ResourceFile): PipeModel {
|
|
|
566
692
|
exportSchedule = parseQuotedValue(value);
|
|
567
693
|
break;
|
|
568
694
|
case "EXPORT_STRATEGY": {
|
|
569
|
-
|
|
570
|
-
|
|
695
|
+
try {
|
|
696
|
+
exportStrategy = normalizeExportStrategy(value);
|
|
697
|
+
} catch {
|
|
571
698
|
throw new MigrationParseError(
|
|
572
699
|
resource.filePath,
|
|
573
700
|
"pipe",
|
|
@@ -575,7 +702,19 @@ export function parsePipeFile(resource: ResourceFile): PipeModel {
|
|
|
575
702
|
`Unsupported EXPORT_STRATEGY in strict mode: "${value}"`
|
|
576
703
|
);
|
|
577
704
|
}
|
|
578
|
-
|
|
705
|
+
break;
|
|
706
|
+
}
|
|
707
|
+
case "EXPORT_WRITE_STRATEGY": {
|
|
708
|
+
try {
|
|
709
|
+
exportStrategy = normalizeExportStrategy(value);
|
|
710
|
+
} catch {
|
|
711
|
+
throw new MigrationParseError(
|
|
712
|
+
resource.filePath,
|
|
713
|
+
"pipe",
|
|
714
|
+
resource.name,
|
|
715
|
+
`Unsupported EXPORT_WRITE_STRATEGY in strict mode: "${value}"`
|
|
716
|
+
);
|
|
717
|
+
}
|
|
579
718
|
break;
|
|
580
719
|
}
|
|
581
720
|
case "EXPORT_COMPRESSION": {
|
|
@@ -784,7 +923,7 @@ export function parsePipeFile(resource: ResourceFile): PipeModel {
|
|
|
784
923
|
pipeType === "materialized" || pipeType === "copy"
|
|
785
924
|
? []
|
|
786
925
|
: inferParamsFromSql(
|
|
787
|
-
|
|
926
|
+
rawNodeSqls.join("\n"),
|
|
788
927
|
resource.filePath,
|
|
789
928
|
resource.name
|
|
790
929
|
);
|
|
@@ -72,7 +72,11 @@ export function splitCommaSeparated(input: string): string[] {
|
|
|
72
72
|
|
|
73
73
|
export function parseQuotedValue(input: string): string {
|
|
74
74
|
const trimmed = input.trim();
|
|
75
|
-
if (
|
|
75
|
+
if (
|
|
76
|
+
trimmed.length >= 2 &&
|
|
77
|
+
((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
|
78
|
+
(trimmed.startsWith("'") && trimmed.endsWith("'")))
|
|
79
|
+
) {
|
|
76
80
|
return trimmed.slice(1, -1);
|
|
77
81
|
}
|
|
78
82
|
return trimmed;
|
package/src/migrate/types.ts
CHANGED
|
@@ -52,11 +52,25 @@ export interface DatasourceS3Model {
|
|
|
52
52
|
fromTimestamp?: string;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
export interface DatasourceGCSModel {
|
|
56
|
+
connectionName: string;
|
|
57
|
+
bucketUri: string;
|
|
58
|
+
schedule?: string;
|
|
59
|
+
fromTimestamp?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
55
62
|
export interface DatasourceTokenModel {
|
|
56
63
|
name: string;
|
|
57
64
|
scope: "READ" | "APPEND";
|
|
58
65
|
}
|
|
59
66
|
|
|
67
|
+
export interface DatasourceIndexModel {
|
|
68
|
+
name: string;
|
|
69
|
+
expr: string;
|
|
70
|
+
type: string;
|
|
71
|
+
granularity: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
60
74
|
export interface DatasourceModel {
|
|
61
75
|
kind: "datasource";
|
|
62
76
|
name: string;
|
|
@@ -64,8 +78,10 @@ export interface DatasourceModel {
|
|
|
64
78
|
description?: string;
|
|
65
79
|
columns: DatasourceColumnModel[];
|
|
66
80
|
engine?: DatasourceEngineModel;
|
|
81
|
+
indexes: DatasourceIndexModel[];
|
|
67
82
|
kafka?: DatasourceKafkaModel;
|
|
68
83
|
s3?: DatasourceS3Model;
|
|
84
|
+
gcs?: DatasourceGCSModel;
|
|
69
85
|
forwardQuery?: string;
|
|
70
86
|
tokens: DatasourceTokenModel[];
|
|
71
87
|
sharedWith: string[];
|
|
@@ -156,11 +172,20 @@ export interface S3ConnectionModel {
|
|
|
156
172
|
secret?: string;
|
|
157
173
|
}
|
|
158
174
|
|
|
175
|
+
export interface GCSConnectionModel {
|
|
176
|
+
kind: "connection";
|
|
177
|
+
name: string;
|
|
178
|
+
filePath: string;
|
|
179
|
+
connectionType: "gcs";
|
|
180
|
+
serviceAccountCredentialsJson: string;
|
|
181
|
+
}
|
|
182
|
+
|
|
159
183
|
export type ParsedResource =
|
|
160
184
|
| DatasourceModel
|
|
161
185
|
| PipeModel
|
|
162
186
|
| KafkaConnectionModel
|
|
163
|
-
| S3ConnectionModel
|
|
187
|
+
| S3ConnectionModel
|
|
188
|
+
| GCSConnectionModel;
|
|
164
189
|
|
|
165
190
|
export interface MigrationResult {
|
|
166
191
|
success: boolean;
|
|
@@ -2,9 +2,11 @@ import { describe, it, expect } from "vitest";
|
|
|
2
2
|
import {
|
|
3
3
|
defineKafkaConnection,
|
|
4
4
|
defineS3Connection,
|
|
5
|
+
defineGCSConnection,
|
|
5
6
|
isConnectionDefinition,
|
|
6
7
|
isKafkaConnectionDefinition,
|
|
7
8
|
isS3ConnectionDefinition,
|
|
9
|
+
isGCSConnectionDefinition,
|
|
8
10
|
getConnectionType,
|
|
9
11
|
} from "./connection.js";
|
|
10
12
|
|
|
@@ -153,6 +155,29 @@ describe("Connection Schema", () => {
|
|
|
153
155
|
});
|
|
154
156
|
});
|
|
155
157
|
|
|
158
|
+
describe("defineGCSConnection", () => {
|
|
159
|
+
it("creates a GCS connection with required fields", () => {
|
|
160
|
+
const conn = defineGCSConnection("my_gcs", {
|
|
161
|
+
serviceAccountCredentialsJson: '{{ tb_secret("GCS_SERVICE_ACCOUNT_CREDENTIALS_JSON") }}',
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
expect(conn._name).toBe("my_gcs");
|
|
165
|
+
expect(conn._type).toBe("connection");
|
|
166
|
+
expect(conn._connectionType).toBe("gcs");
|
|
167
|
+
expect(conn.options.serviceAccountCredentialsJson).toBe(
|
|
168
|
+
'{{ tb_secret("GCS_SERVICE_ACCOUNT_CREDENTIALS_JSON") }}'
|
|
169
|
+
);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("throws when credentials json is empty", () => {
|
|
173
|
+
expect(() =>
|
|
174
|
+
defineGCSConnection("my_gcs", {
|
|
175
|
+
serviceAccountCredentialsJson: " ",
|
|
176
|
+
})
|
|
177
|
+
).toThrow("GCS connection `serviceAccountCredentialsJson` is required.");
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
156
181
|
describe("isConnectionDefinition", () => {
|
|
157
182
|
it("returns true for valid connection", () => {
|
|
158
183
|
const conn = defineKafkaConnection("my_kafka", {
|
|
@@ -203,6 +228,21 @@ describe("Connection Schema", () => {
|
|
|
203
228
|
});
|
|
204
229
|
});
|
|
205
230
|
|
|
231
|
+
describe("isGCSConnectionDefinition", () => {
|
|
232
|
+
it("returns true for GCS connection", () => {
|
|
233
|
+
const conn = defineGCSConnection("my_gcs", {
|
|
234
|
+
serviceAccountCredentialsJson: '{{ tb_secret("GCS_SERVICE_ACCOUNT_CREDENTIALS_JSON") }}',
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
expect(isGCSConnectionDefinition(conn)).toBe(true);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("returns false for non-GCS objects", () => {
|
|
241
|
+
expect(isGCSConnectionDefinition({})).toBe(false);
|
|
242
|
+
expect(isGCSConnectionDefinition(null)).toBe(false);
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
206
246
|
describe("getConnectionType", () => {
|
|
207
247
|
it("returns the connection type", () => {
|
|
208
248
|
const conn = defineKafkaConnection("my_kafka", {
|
|
@@ -220,5 +260,13 @@ describe("Connection Schema", () => {
|
|
|
220
260
|
|
|
221
261
|
expect(getConnectionType(conn)).toBe("s3");
|
|
222
262
|
});
|
|
263
|
+
|
|
264
|
+
it("returns the gcs connection type", () => {
|
|
265
|
+
const conn = defineGCSConnection("my_gcs", {
|
|
266
|
+
serviceAccountCredentialsJson: '{{ tb_secret("GCS_SERVICE_ACCOUNT_CREDENTIALS_JSON") }}',
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
expect(getConnectionType(conn)).toBe("gcs");
|
|
270
|
+
});
|
|
223
271
|
});
|
|
224
272
|
});
|