@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.
Files changed (56) hide show
  1. package/dist/cli/commands/migrate.test.js +247 -2
  2. package/dist/cli/commands/migrate.test.js.map +1 -1
  3. package/dist/codegen/type-mapper.d.ts.map +1 -1
  4. package/dist/codegen/type-mapper.js +70 -7
  5. package/dist/codegen/type-mapper.js.map +1 -1
  6. package/dist/codegen/type-mapper.test.js +9 -0
  7. package/dist/codegen/type-mapper.test.js.map +1 -1
  8. package/dist/generator/datasource.d.ts.map +1 -1
  9. package/dist/generator/datasource.js +19 -0
  10. package/dist/generator/datasource.js.map +1 -1
  11. package/dist/generator/datasource.test.js +16 -0
  12. package/dist/generator/datasource.test.js.map +1 -1
  13. package/dist/generator/pipe.d.ts.map +1 -1
  14. package/dist/generator/pipe.js +92 -3
  15. package/dist/generator/pipe.js.map +1 -1
  16. package/dist/generator/pipe.test.js +19 -0
  17. package/dist/generator/pipe.test.js.map +1 -1
  18. package/dist/index.d.ts +1 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js.map +1 -1
  21. package/dist/migrate/emit-ts.d.ts.map +1 -1
  22. package/dist/migrate/emit-ts.js +11 -0
  23. package/dist/migrate/emit-ts.js.map +1 -1
  24. package/dist/migrate/parse-datasource.d.ts.map +1 -1
  25. package/dist/migrate/parse-datasource.js +37 -0
  26. package/dist/migrate/parse-datasource.js.map +1 -1
  27. package/dist/migrate/parse-pipe.d.ts.map +1 -1
  28. package/dist/migrate/parse-pipe.js +212 -93
  29. package/dist/migrate/parse-pipe.js.map +1 -1
  30. package/dist/migrate/parser-utils.d.ts.map +1 -1
  31. package/dist/migrate/parser-utils.js +3 -1
  32. package/dist/migrate/parser-utils.js.map +1 -1
  33. package/dist/migrate/types.d.ts +7 -0
  34. package/dist/migrate/types.d.ts.map +1 -1
  35. package/dist/schema/datasource.d.ts +16 -0
  36. package/dist/schema/datasource.d.ts.map +1 -1
  37. package/dist/schema/datasource.js +16 -0
  38. package/dist/schema/datasource.js.map +1 -1
  39. package/dist/schema/datasource.test.js +39 -0
  40. package/dist/schema/datasource.test.js.map +1 -1
  41. package/package.json +1 -1
  42. package/src/cli/commands/migrate.test.ts +357 -2
  43. package/src/codegen/type-mapper.test.ts +18 -0
  44. package/src/codegen/type-mapper.ts +79 -7
  45. package/src/generator/datasource.test.ts +22 -0
  46. package/src/generator/datasource.ts +25 -0
  47. package/src/generator/pipe.test.ts +21 -0
  48. package/src/generator/pipe.ts +119 -3
  49. package/src/index.ts +1 -0
  50. package/src/migrate/emit-ts.ts +13 -0
  51. package/src/migrate/parse-datasource.ts +72 -1
  52. package/src/migrate/parse-pipe.ts +250 -111
  53. package/src/migrate/parser-utils.ts +5 -1
  54. package/src/migrate/types.ts +8 -0
  55. package/src/schema/datasource.test.ts +53 -0
  56. 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', () => {
@@ -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(node: NodeDefinition): string {
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 sqlLines = node.sql.trim().split("\n");
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
@@ -114,6 +114,7 @@ export type {
114
114
  KafkaConfig,
115
115
  S3Config,
116
116
  GCSConfig,
117
+ DatasourceIndex,
117
118
  } from "./schema/datasource.js";
118
119
 
119
120
  // ============ Connection ============
@@ -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 { DatasourceModel, DatasourceTokenModel, ResourceFile } from "./types.js";
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,