@tinybirdco/sdk 0.0.50 → 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/dist/cli/commands/migrate.test.js +247 -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/datasource.d.ts.map +1 -1
- package/dist/generator/datasource.js +19 -0
- package/dist/generator/datasource.js.map +1 -1
- package/dist/generator/datasource.test.js +16 -0
- 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 +1 -1
- package/dist/index.d.ts.map +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 +11 -0
- package/dist/migrate/emit-ts.js.map +1 -1
- package/dist/migrate/parse-datasource.d.ts.map +1 -1
- package/dist/migrate/parse-datasource.js +37 -0
- 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 +7 -0
- package/dist/migrate/types.d.ts.map +1 -1
- package/dist/schema/datasource.d.ts +16 -0
- package/dist/schema/datasource.d.ts.map +1 -1
- package/dist/schema/datasource.js +16 -0
- package/dist/schema/datasource.js.map +1 -1
- package/dist/schema/datasource.test.js +39 -0
- package/dist/schema/datasource.test.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/migrate.test.ts +357 -2
- package/src/codegen/type-mapper.test.ts +18 -0
- package/src/codegen/type-mapper.ts +79 -7
- package/src/generator/datasource.test.ts +22 -0
- package/src/generator/datasource.ts +25 -0
- package/src/generator/pipe.test.ts +21 -0
- package/src/generator/pipe.ts +119 -3
- package/src/index.ts +1 -0
- package/src/migrate/emit-ts.ts +13 -0
- package/src/migrate/parse-datasource.ts +72 -1
- package/src/migrate/parse-pipe.ts +250 -111
- package/src/migrate/parser-utils.ts +5 -1
- package/src/migrate/types.ts +8 -0
- package/src/schema/datasource.test.ts +53 -0
- package/src/schema/datasource.ts +38 -0
|
@@ -95,6 +95,28 @@ describe('Datasource Generator', () => {
|
|
|
95
95
|
expect(result.content).toContain('FORWARD_QUERY >');
|
|
96
96
|
expect(result.content).toContain(' SELECT id');
|
|
97
97
|
});
|
|
98
|
+
|
|
99
|
+
it("includes indexes block when provided", () => {
|
|
100
|
+
const ds = defineDatasource("test_ds", {
|
|
101
|
+
schema: {
|
|
102
|
+
id: t.string(),
|
|
103
|
+
pipe_name: t.string(),
|
|
104
|
+
},
|
|
105
|
+
indexes: [
|
|
106
|
+
{ name: "pipe_name_set", expr: "pipe_name", type: "set(100)", granularity: 1 },
|
|
107
|
+
{ name: "id_bf", expr: "lower(id)", type: "bloom_filter(0.001)", granularity: 4 },
|
|
108
|
+
],
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const result = generateDatasource(ds);
|
|
112
|
+
expect(result.content).toContain("INDEXES >");
|
|
113
|
+
expect(result.content).toContain(
|
|
114
|
+
"pipe_name_set pipe_name TYPE set(100) GRANULARITY 1"
|
|
115
|
+
);
|
|
116
|
+
expect(result.content).toContain(
|
|
117
|
+
"id_bf lower(id) TYPE bloom_filter(0.001) GRANULARITY 4"
|
|
118
|
+
);
|
|
119
|
+
});
|
|
98
120
|
});
|
|
99
121
|
|
|
100
122
|
describe('Column formatting', () => {
|
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
S3Config,
|
|
12
12
|
GCSConfig,
|
|
13
13
|
TokenConfig,
|
|
14
|
+
DatasourceIndex,
|
|
14
15
|
} from "../schema/datasource.js";
|
|
15
16
|
import type { AnyTypeValidator, TypeModifiers } from "../schema/types.js";
|
|
16
17
|
import { getColumnType, getColumnJsonPath } from "../schema/datasource.js";
|
|
@@ -211,6 +212,23 @@ function generateForwardQuery(forwardQuery?: string): string | null {
|
|
|
211
212
|
return ["FORWARD_QUERY >", ...lines.map((line) => ` ${line}`)].join("\n");
|
|
212
213
|
}
|
|
213
214
|
|
|
215
|
+
/**
|
|
216
|
+
* Generate INDEXES section
|
|
217
|
+
*/
|
|
218
|
+
function generateIndexes(indexes?: readonly DatasourceIndex[]): string | null {
|
|
219
|
+
if (!indexes || indexes.length === 0) {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const lines = ["INDEXES >"];
|
|
224
|
+
for (const index of indexes) {
|
|
225
|
+
lines.push(
|
|
226
|
+
` ${index.name} ${index.expr} TYPE ${index.type} GRANULARITY ${index.granularity}`
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
return lines.join("\n");
|
|
230
|
+
}
|
|
231
|
+
|
|
214
232
|
/**
|
|
215
233
|
* Generate SHARED_WITH section for sharing datasource with other workspaces
|
|
216
234
|
*/
|
|
@@ -317,6 +335,13 @@ export function generateDatasource(
|
|
|
317
335
|
// Add engine configuration
|
|
318
336
|
parts.push(generateEngineConfig(datasource.options.engine));
|
|
319
337
|
|
|
338
|
+
// Add indexes if present
|
|
339
|
+
const indexes = generateIndexes(datasource.options.indexes);
|
|
340
|
+
if (indexes) {
|
|
341
|
+
parts.push("");
|
|
342
|
+
parts.push(indexes);
|
|
343
|
+
}
|
|
344
|
+
|
|
320
345
|
// Add Kafka configuration if present
|
|
321
346
|
if (datasource.options.kafka) {
|
|
322
347
|
parts.push("");
|
|
@@ -164,6 +164,27 @@ describe('Pipe Generator', () => {
|
|
|
164
164
|
const result = generatePipe(pipe);
|
|
165
165
|
expect(result.content).toContain(' %\n');
|
|
166
166
|
});
|
|
167
|
+
|
|
168
|
+
it('injects param defaults into placeholders when SQL omits them', () => {
|
|
169
|
+
const pipe = definePipe('defaults_pipe', {
|
|
170
|
+
params: {
|
|
171
|
+
start_date: p.date().optional('2025-03-01'),
|
|
172
|
+
page: p.int32().optional(0),
|
|
173
|
+
},
|
|
174
|
+
nodes: [
|
|
175
|
+
node({
|
|
176
|
+
name: 'endpoint',
|
|
177
|
+
sql: 'SELECT * FROM events WHERE d >= {{Date(start_date)}} LIMIT 10 OFFSET {{Int32(page)}}',
|
|
178
|
+
}),
|
|
179
|
+
],
|
|
180
|
+
output: simpleOutput,
|
|
181
|
+
endpoint: true,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const result = generatePipe(pipe);
|
|
185
|
+
expect(result.content).toContain("{{ Date(start_date, '2025-03-01') }}");
|
|
186
|
+
expect(result.content).toContain('{{ Int32(page, 0) }}');
|
|
187
|
+
});
|
|
167
188
|
});
|
|
168
189
|
|
|
169
190
|
describe('Multiple nodes', () => {
|
package/src/generator/pipe.ts
CHANGED
|
@@ -18,6 +18,8 @@ import {
|
|
|
18
18
|
getCopyConfig,
|
|
19
19
|
getSinkConfig,
|
|
20
20
|
} from "../schema/pipe.js";
|
|
21
|
+
import type { AnyParamValidator } from "../schema/params.js";
|
|
22
|
+
import { getParamDefault } from "../schema/params.js";
|
|
21
23
|
|
|
22
24
|
/**
|
|
23
25
|
* Generated pipe content
|
|
@@ -36,10 +38,123 @@ function hasDynamicParameters(sql: string): boolean {
|
|
|
36
38
|
return /\{\{[^}]+\}\}/.test(sql) || /\{%[^%]+%\}/.test(sql);
|
|
37
39
|
}
|
|
38
40
|
|
|
41
|
+
function splitTopLevelComma(input: string): string[] {
|
|
42
|
+
const parts: string[] = [];
|
|
43
|
+
let current = "";
|
|
44
|
+
let depth = 0;
|
|
45
|
+
let inSingleQuote = false;
|
|
46
|
+
let inDoubleQuote = false;
|
|
47
|
+
|
|
48
|
+
for (let i = 0; i < input.length; i += 1) {
|
|
49
|
+
const char = input[i];
|
|
50
|
+
const prev = i > 0 ? input[i - 1] : "";
|
|
51
|
+
|
|
52
|
+
if (char === "'" && !inDoubleQuote && prev !== "\\") {
|
|
53
|
+
inSingleQuote = !inSingleQuote;
|
|
54
|
+
current += char;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (char === '"' && !inSingleQuote && prev !== "\\") {
|
|
59
|
+
inDoubleQuote = !inDoubleQuote;
|
|
60
|
+
current += char;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!inSingleQuote && !inDoubleQuote) {
|
|
65
|
+
if (char === "(") {
|
|
66
|
+
depth += 1;
|
|
67
|
+
current += char;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (char === ")" && depth > 0) {
|
|
71
|
+
depth -= 1;
|
|
72
|
+
current += char;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (char === "," && depth === 0) {
|
|
76
|
+
parts.push(current.trim());
|
|
77
|
+
current = "";
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
current += char;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (current.trim()) {
|
|
86
|
+
parts.push(current.trim());
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return parts;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function toTemplateDefaultLiteral(value: string | number | boolean): string {
|
|
93
|
+
if (typeof value === "string") {
|
|
94
|
+
return `'${value.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
|
|
95
|
+
}
|
|
96
|
+
return String(value);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function applyParamDefaultsToSql(
|
|
100
|
+
sql: string,
|
|
101
|
+
params?: Record<string, AnyParamValidator>
|
|
102
|
+
): string {
|
|
103
|
+
if (!params) {
|
|
104
|
+
return sql;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const defaults = new Map<string, string>();
|
|
108
|
+
for (const [name, validator] of Object.entries(params)) {
|
|
109
|
+
const defaultValue = getParamDefault(validator);
|
|
110
|
+
if (defaultValue !== undefined) {
|
|
111
|
+
defaults.set(name, toTemplateDefaultLiteral(defaultValue as string | number | boolean));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (defaults.size === 0) {
|
|
116
|
+
return sql;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const placeholderRegex = /\{\{\s*([^{}]+?)\s*\}\}/g;
|
|
120
|
+
return sql.replace(placeholderRegex, (fullMatch, rawExpression) => {
|
|
121
|
+
const expression = String(rawExpression);
|
|
122
|
+
const rewritten = expression.replace(
|
|
123
|
+
/([a-zA-Z_][a-zA-Z0-9_]*)\s*\(([^()]*)\)/g,
|
|
124
|
+
(call, _functionName, rawArgs) => {
|
|
125
|
+
const args = splitTopLevelComma(String(rawArgs));
|
|
126
|
+
if (args.length !== 1) {
|
|
127
|
+
return call;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const paramName = args[0]?.trim() ?? "";
|
|
131
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(paramName)) {
|
|
132
|
+
return call;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const defaultLiteral = defaults.get(paramName);
|
|
136
|
+
if (!defaultLiteral) {
|
|
137
|
+
return call;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return call.replace(/\)\s*$/, `, ${defaultLiteral})`);
|
|
141
|
+
}
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
if (rewritten === expression) {
|
|
145
|
+
return fullMatch;
|
|
146
|
+
}
|
|
147
|
+
return `{{ ${rewritten.trim()} }}`;
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
39
151
|
/**
|
|
40
152
|
* Generate a NODE section for the pipe
|
|
41
153
|
*/
|
|
42
|
-
function generateNode(
|
|
154
|
+
function generateNode(
|
|
155
|
+
node: NodeDefinition,
|
|
156
|
+
params?: Record<string, AnyParamValidator>
|
|
157
|
+
): string {
|
|
43
158
|
const parts: string[] = [];
|
|
44
159
|
|
|
45
160
|
parts.push(`NODE ${node._name}`);
|
|
@@ -57,7 +172,8 @@ function generateNode(node: NodeDefinition): string {
|
|
|
57
172
|
parts.push(` %`);
|
|
58
173
|
}
|
|
59
174
|
|
|
60
|
-
const
|
|
175
|
+
const sqlWithDefaults = applyParamDefaultsToSql(node.sql, params);
|
|
176
|
+
const sqlLines = sqlWithDefaults.trim().split("\n");
|
|
61
177
|
sqlLines.forEach((line) => {
|
|
62
178
|
parts.push(` ${line}`);
|
|
63
179
|
});
|
|
@@ -225,7 +341,7 @@ export function generatePipe(pipe: PipeDefinition): GeneratedPipe {
|
|
|
225
341
|
|
|
226
342
|
// Add all nodes
|
|
227
343
|
pipe.options.nodes.forEach((node, index) => {
|
|
228
|
-
parts.push(generateNode(node));
|
|
344
|
+
parts.push(generateNode(node, pipe.options.params as Record<string, AnyParamValidator> | undefined));
|
|
229
345
|
// Add empty line between nodes
|
|
230
346
|
if (index < pipe.options.nodes.length - 1) {
|
|
231
347
|
parts.push("");
|
package/src/index.ts
CHANGED
package/src/migrate/emit-ts.ts
CHANGED
|
@@ -133,6 +133,8 @@ function strictParamBaseValidator(type: string): string {
|
|
|
133
133
|
const map: Record<string, string> = {
|
|
134
134
|
String: "p.string()",
|
|
135
135
|
UUID: "p.uuid()",
|
|
136
|
+
Int: "p.int32()",
|
|
137
|
+
Integer: "p.int32()",
|
|
136
138
|
Int8: "p.int8()",
|
|
137
139
|
Int16: "p.int16()",
|
|
138
140
|
Int32: "p.int32()",
|
|
@@ -149,6 +151,8 @@ function strictParamBaseValidator(type: string): string {
|
|
|
149
151
|
DateTime: "p.dateTime()",
|
|
150
152
|
DateTime64: "p.dateTime64()",
|
|
151
153
|
Array: "p.array(p.string())",
|
|
154
|
+
column: "p.column()",
|
|
155
|
+
JSON: "p.json()",
|
|
152
156
|
};
|
|
153
157
|
const validator = map[type];
|
|
154
158
|
if (!validator) {
|
|
@@ -313,6 +317,15 @@ function emitDatasource(ds: DatasourceModel): string {
|
|
|
313
317
|
if (ds.engine) {
|
|
314
318
|
lines.push(` engine: ${emitEngineOptions(ds.engine)},`);
|
|
315
319
|
}
|
|
320
|
+
if (ds.indexes.length > 0) {
|
|
321
|
+
lines.push(" indexes: [");
|
|
322
|
+
for (const index of ds.indexes) {
|
|
323
|
+
lines.push(
|
|
324
|
+
` { name: ${escapeString(index.name)}, expr: ${escapeString(index.expr)}, type: ${escapeString(index.type)}, granularity: ${index.granularity} },`
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
lines.push(" ],");
|
|
328
|
+
}
|
|
316
329
|
|
|
317
330
|
if (ds.kafka) {
|
|
318
331
|
const connectionVar = toCamelCase(ds.kafka.connectionName);
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
DatasourceIndexModel,
|
|
3
|
+
DatasourceModel,
|
|
4
|
+
DatasourceTokenModel,
|
|
5
|
+
ResourceFile,
|
|
6
|
+
} from "./types.js";
|
|
2
7
|
import {
|
|
3
8
|
MigrationParseError,
|
|
4
9
|
isBlank,
|
|
@@ -14,6 +19,7 @@ const DATASOURCE_DIRECTIVES = new Set([
|
|
|
14
19
|
"DESCRIPTION",
|
|
15
20
|
"SCHEMA",
|
|
16
21
|
"FORWARD_QUERY",
|
|
22
|
+
"INDEXES",
|
|
17
23
|
"SHARED_WITH",
|
|
18
24
|
"ENGINE",
|
|
19
25
|
"ENGINE_SORTING_KEY",
|
|
@@ -241,9 +247,53 @@ function parseToken(filePath: string, resourceName: string, value: string): Data
|
|
|
241
247
|
return { name, scope };
|
|
242
248
|
}
|
|
243
249
|
|
|
250
|
+
function parseIndexLine(
|
|
251
|
+
filePath: string,
|
|
252
|
+
resourceName: string,
|
|
253
|
+
rawLine: string
|
|
254
|
+
): DatasourceIndexModel {
|
|
255
|
+
const line = rawLine.trim().replace(/,$/, "");
|
|
256
|
+
if (!line) {
|
|
257
|
+
throw new MigrationParseError(
|
|
258
|
+
filePath,
|
|
259
|
+
"datasource",
|
|
260
|
+
resourceName,
|
|
261
|
+
"Empty INDEXES line."
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const match = line.match(/^(\S+)\s+(.+?)\s+TYPE\s+(.+?)\s+GRANULARITY\s+(\d+)$/i);
|
|
266
|
+
if (!match) {
|
|
267
|
+
throw new MigrationParseError(
|
|
268
|
+
filePath,
|
|
269
|
+
"datasource",
|
|
270
|
+
resourceName,
|
|
271
|
+
`Invalid INDEXES definition: "${rawLine}"`
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const granularity = Number(match[4]);
|
|
276
|
+
if (!Number.isInteger(granularity) || granularity <= 0) {
|
|
277
|
+
throw new MigrationParseError(
|
|
278
|
+
filePath,
|
|
279
|
+
"datasource",
|
|
280
|
+
resourceName,
|
|
281
|
+
`Invalid INDEXES GRANULARITY value: "${match[4]}"`
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
name: match[1],
|
|
287
|
+
expr: match[2].trim(),
|
|
288
|
+
type: match[3].trim(),
|
|
289
|
+
granularity,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
244
293
|
export function parseDatasourceFile(resource: ResourceFile): DatasourceModel {
|
|
245
294
|
const lines = splitLines(resource.content);
|
|
246
295
|
const columns = [];
|
|
296
|
+
const indexes: DatasourceIndexModel[] = [];
|
|
247
297
|
const tokens: DatasourceTokenModel[] = [];
|
|
248
298
|
const sharedWith: string[] = [];
|
|
249
299
|
let description: string | undefined;
|
|
@@ -323,6 +373,26 @@ export function parseDatasourceFile(resource: ResourceFile): DatasourceModel {
|
|
|
323
373
|
continue;
|
|
324
374
|
}
|
|
325
375
|
|
|
376
|
+
if (line === "INDEXES >") {
|
|
377
|
+
const block = readDirectiveBlock(lines, i + 1, isDatasourceDirectiveLine);
|
|
378
|
+
if (block.lines.length === 0) {
|
|
379
|
+
throw new MigrationParseError(
|
|
380
|
+
resource.filePath,
|
|
381
|
+
"datasource",
|
|
382
|
+
resource.name,
|
|
383
|
+
"INDEXES block is empty."
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
for (const indexLine of block.lines) {
|
|
387
|
+
if (isBlank(indexLine) || indexLine.trim().startsWith("#")) {
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
indexes.push(parseIndexLine(resource.filePath, resource.name, indexLine));
|
|
391
|
+
}
|
|
392
|
+
i = block.nextIndex;
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
|
|
326
396
|
if (line === "SHARED_WITH >") {
|
|
327
397
|
const block = readDirectiveBlock(lines, i + 1, isDatasourceDirectiveLine);
|
|
328
398
|
for (const sharedLine of block.lines) {
|
|
@@ -552,6 +622,7 @@ export function parseDatasourceFile(resource: ResourceFile): DatasourceModel {
|
|
|
552
622
|
settings,
|
|
553
623
|
}
|
|
554
624
|
: undefined,
|
|
625
|
+
indexes,
|
|
555
626
|
kafka,
|
|
556
627
|
s3,
|
|
557
628
|
forwardQuery,
|