@tinybirdco/sdk 0.0.48 → 0.0.49

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 (52) hide show
  1. package/README.md +53 -3
  2. package/dist/cli/commands/migrate.d.ts.map +1 -1
  3. package/dist/cli/commands/migrate.js +32 -0
  4. package/dist/cli/commands/migrate.js.map +1 -1
  5. package/dist/cli/commands/migrate.test.js +398 -1
  6. package/dist/cli/commands/migrate.test.js.map +1 -1
  7. package/dist/generator/pipe.d.ts.map +1 -1
  8. package/dist/generator/pipe.js +31 -1
  9. package/dist/generator/pipe.js.map +1 -1
  10. package/dist/generator/pipe.test.js +50 -1
  11. package/dist/generator/pipe.test.js.map +1 -1
  12. package/dist/index.d.ts +2 -2
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +1 -1
  15. package/dist/index.js.map +1 -1
  16. package/dist/migrate/emit-ts.d.ts.map +1 -1
  17. package/dist/migrate/emit-ts.js +50 -9
  18. package/dist/migrate/emit-ts.js.map +1 -1
  19. package/dist/migrate/parse-datasource.d.ts.map +1 -1
  20. package/dist/migrate/parse-datasource.js +77 -49
  21. package/dist/migrate/parse-datasource.js.map +1 -1
  22. package/dist/migrate/parse-pipe.d.ts.map +1 -1
  23. package/dist/migrate/parse-pipe.js +254 -44
  24. package/dist/migrate/parse-pipe.js.map +1 -1
  25. package/dist/migrate/parser-utils.d.ts +5 -0
  26. package/dist/migrate/parser-utils.d.ts.map +1 -1
  27. package/dist/migrate/parser-utils.js +22 -0
  28. package/dist/migrate/parser-utils.js.map +1 -1
  29. package/dist/migrate/types.d.ts +22 -3
  30. package/dist/migrate/types.d.ts.map +1 -1
  31. package/dist/schema/datasource.test.js +1 -0
  32. package/dist/schema/datasource.test.js.map +1 -1
  33. package/dist/schema/pipe.d.ts +90 -3
  34. package/dist/schema/pipe.d.ts.map +1 -1
  35. package/dist/schema/pipe.js +84 -0
  36. package/dist/schema/pipe.js.map +1 -1
  37. package/dist/schema/pipe.test.js +70 -1
  38. package/dist/schema/pipe.test.js.map +1 -1
  39. package/package.json +1 -1
  40. package/src/cli/commands/migrate.test.ts +580 -1
  41. package/src/cli/commands/migrate.ts +35 -0
  42. package/src/generator/pipe.test.ts +56 -1
  43. package/src/generator/pipe.ts +41 -1
  44. package/src/index.ts +9 -0
  45. package/src/migrate/emit-ts.ts +52 -10
  46. package/src/migrate/parse-datasource.ts +82 -68
  47. package/src/migrate/parse-pipe.ts +359 -66
  48. package/src/migrate/parser-utils.ts +36 -1
  49. package/src/migrate/types.ts +25 -3
  50. package/src/schema/datasource.test.ts +1 -0
  51. package/src/schema/pipe.test.ts +89 -0
  52. package/src/schema/pipe.ts +188 -4
@@ -1,7 +1,8 @@
1
1
  import { describe, it, expect } from 'vitest';
2
2
  import { generatePipe, generateAllPipes } from './pipe.js';
3
- import { definePipe, defineMaterializedView, node } from '../schema/pipe.js';
3
+ import { definePipe, defineMaterializedView, defineSinkPipe, node } from '../schema/pipe.js';
4
4
  import { defineDatasource } from '../schema/datasource.js';
5
+ import { defineKafkaConnection, defineS3Connection } from '../schema/connection.js';
5
6
  import { defineToken } from '../schema/token.js';
6
7
  import { t } from '../schema/types.js';
7
8
  import { p } from '../schema/params.js';
@@ -472,6 +473,60 @@ GROUP BY day, country
472
473
  });
473
474
  });
474
475
 
476
+ describe('Sink configuration', () => {
477
+ it('generates Kafka sink directives', () => {
478
+ const kafka = defineKafkaConnection('events_kafka', {
479
+ bootstrapServers: 'localhost:9092',
480
+ });
481
+
482
+ const pipe = defineSinkPipe('events_sink', {
483
+ nodes: [node({ name: 'publish', sql: 'SELECT * FROM events' })],
484
+ sink: {
485
+ connection: kafka,
486
+ topic: 'events_out',
487
+ schedule: '@on-demand',
488
+ },
489
+ });
490
+
491
+ const result = generatePipe(pipe);
492
+ expect(result.content).toContain('TYPE sink');
493
+ expect(result.content).toContain('EXPORT_CONNECTION_NAME events_kafka');
494
+ expect(result.content).toContain('EXPORT_KAFKA_TOPIC events_out');
495
+ expect(result.content).toContain('EXPORT_SCHEDULE @on-demand');
496
+ expect(result.content).not.toContain('EXPORT_STRATEGY');
497
+ });
498
+
499
+ it('generates S3 sink directives', () => {
500
+ const s3 = defineS3Connection('exports_s3', {
501
+ region: 'us-east-1',
502
+ arn: 'arn:aws:iam::123456789012:role/tinybird-s3-access',
503
+ });
504
+
505
+ const pipe = defineSinkPipe('events_s3_sink', {
506
+ nodes: [node({ name: 'export', sql: 'SELECT * FROM events' })],
507
+ sink: {
508
+ connection: s3,
509
+ bucketUri: 's3://bucket/events/',
510
+ fileTemplate: 'events_{date}',
511
+ format: 'csv',
512
+ schedule: '@once',
513
+ compression: 'gzip',
514
+ strategy: 'replace',
515
+ },
516
+ });
517
+
518
+ const result = generatePipe(pipe);
519
+ expect(result.content).toContain('TYPE sink');
520
+ expect(result.content).toContain('EXPORT_CONNECTION_NAME exports_s3');
521
+ expect(result.content).toContain('EXPORT_BUCKET_URI s3://bucket/events/');
522
+ expect(result.content).toContain('EXPORT_FILE_TEMPLATE events_{date}');
523
+ expect(result.content).toContain('EXPORT_FORMAT csv');
524
+ expect(result.content).toContain('EXPORT_SCHEDULE @once');
525
+ expect(result.content).toContain('EXPORT_STRATEGY replace');
526
+ expect(result.content).toContain('EXPORT_COMPRESSION gzip');
527
+ });
528
+ });
529
+
475
530
  describe('Token generation', () => {
476
531
  it('generates TOKEN lines with inline config', () => {
477
532
  const pipe = definePipe('test_pipe', {
@@ -9,9 +9,15 @@ import type {
9
9
  EndpointConfig,
10
10
  MaterializedConfig,
11
11
  CopyConfig,
12
+ SinkConfig,
12
13
  PipeTokenConfig,
13
14
  } from "../schema/pipe.js";
14
- import { getEndpointConfig, getMaterializedConfig, getCopyConfig } from "../schema/pipe.js";
15
+ import {
16
+ getEndpointConfig,
17
+ getMaterializedConfig,
18
+ getCopyConfig,
19
+ getSinkConfig,
20
+ } from "../schema/pipe.js";
15
21
 
16
22
  /**
17
23
  * Generated pipe content
@@ -114,6 +120,33 @@ function generateCopy(config: CopyConfig): string {
114
120
  return parts.join("\n");
115
121
  }
116
122
 
123
+ /**
124
+ * Generate the TYPE sink section
125
+ */
126
+ function generateSink(config: SinkConfig): string {
127
+ const parts: string[] = ["TYPE sink"];
128
+
129
+ parts.push(`EXPORT_CONNECTION_NAME ${config.connection._name}`);
130
+
131
+ if ("topic" in config) {
132
+ parts.push(`EXPORT_KAFKA_TOPIC ${config.topic}`);
133
+ parts.push(`EXPORT_SCHEDULE ${config.schedule}`);
134
+ } else {
135
+ parts.push(`EXPORT_BUCKET_URI ${config.bucketUri}`);
136
+ parts.push(`EXPORT_FILE_TEMPLATE ${config.fileTemplate}`);
137
+ parts.push(`EXPORT_SCHEDULE ${config.schedule}`);
138
+ parts.push(`EXPORT_FORMAT ${config.format}`);
139
+ if (config.strategy) {
140
+ parts.push(`EXPORT_STRATEGY ${config.strategy}`);
141
+ }
142
+ if (config.compression) {
143
+ parts.push(`EXPORT_COMPRESSION ${config.compression}`);
144
+ }
145
+ }
146
+
147
+ return parts.join("\n");
148
+ }
149
+
117
150
  /**
118
151
  * Generate TOKEN lines for a pipe
119
152
  */
@@ -220,6 +253,13 @@ export function generatePipe(pipe: PipeDefinition): GeneratedPipe {
220
253
  parts.push(generateCopy(copyConfig));
221
254
  }
222
255
 
256
+ // Add sink configuration if this is a sink pipe
257
+ const sinkConfig = getSinkConfig(pipe);
258
+ if (sinkConfig) {
259
+ parts.push("");
260
+ parts.push(generateSink(sinkConfig));
261
+ }
262
+
223
263
  // Add tokens if present
224
264
  const tokenLines = generateTokens(pipe.options.tokens);
225
265
  if (tokenLines.length > 0) {
package/src/index.ts CHANGED
@@ -149,14 +149,17 @@ export {
149
149
  defineEndpoint,
150
150
  defineMaterializedView,
151
151
  defineCopyPipe,
152
+ defineSinkPipe,
152
153
  node,
153
154
  isPipeDefinition,
154
155
  isNodeDefinition,
155
156
  getEndpointConfig,
156
157
  getMaterializedConfig,
157
158
  getCopyConfig,
159
+ getSinkConfig,
158
160
  isMaterializedView,
159
161
  isCopyPipe,
162
+ isSinkPipe,
160
163
  getNodeNames,
161
164
  getNode,
162
165
  sql,
@@ -166,7 +169,13 @@ export type {
166
169
  PipeOptions,
167
170
  EndpointOptions,
168
171
  CopyPipeOptions,
172
+ SinkPipeOptions,
169
173
  CopyConfig,
174
+ SinkConfig,
175
+ SinkStrategy,
176
+ SinkCompression,
177
+ KafkaSinkConfig,
178
+ S3SinkConfig,
170
179
  NodeDefinition,
171
180
  NodeOptions,
172
181
  ParamsDefinition,
@@ -3,6 +3,7 @@ import { toCamelCase } from "../codegen/utils.js";
3
3
  import { parseLiteralFromDatafile, toTsLiteral } from "./parser-utils.js";
4
4
  import type {
5
5
  DatasourceModel,
6
+ DatasourceEngineModel,
6
7
  KafkaConnectionModel,
7
8
  ParsedResource,
8
9
  PipeModel,
@@ -151,7 +152,7 @@ function strictParamBaseValidator(type: string): string {
151
152
  function applyParamOptional(
152
153
  baseValidator: string,
153
154
  required: boolean,
154
- defaultValue: string | number | undefined
155
+ defaultValue: string | number | boolean | undefined
155
156
  ): string {
156
157
  const withDefault = defaultValue !== undefined;
157
158
  if (!withDefault && required) {
@@ -168,6 +169,13 @@ function applyParamOptional(
168
169
  return `${baseValidator}${optionalSuffix}`;
169
170
  }
170
171
 
172
+ function applyParamDescription(validator: string, description: string | undefined): string {
173
+ if (description === undefined) {
174
+ return validator;
175
+ }
176
+ return `${validator}.describe(${JSON.stringify(description)})`;
177
+ }
178
+
171
179
  function engineFunctionName(type: string): string {
172
180
  const map: Record<string, string> = {
173
181
  MergeTree: "mergeTree",
@@ -184,9 +192,8 @@ function engineFunctionName(type: string): string {
184
192
  return functionName;
185
193
  }
186
194
 
187
- function emitEngineOptions(ds: DatasourceModel): string {
195
+ function emitEngineOptions(engine: DatasourceEngineModel): string {
188
196
  const options: string[] = [];
189
- const { engine } = ds;
190
197
 
191
198
  if (engine.sortingKey.length === 1) {
192
199
  options.push(`sortingKey: ${escapeString(engine.sortingKey[0]!)}`);
@@ -256,7 +263,7 @@ function emitDatasource(ds: DatasourceModel): string {
256
263
  }
257
264
 
258
265
  lines.push(`export const ${variableName} = defineDatasource(${escapeString(ds.name)}, {`);
259
- if (ds.description) {
266
+ if (ds.description !== undefined) {
260
267
  lines.push(` description: ${emitStringOrSecret(ds.description)},`);
261
268
  }
262
269
  if (!hasJsonPath) {
@@ -295,7 +302,9 @@ function emitDatasource(ds: DatasourceModel): string {
295
302
  lines.push(` ${columnKey}: ${validator},`);
296
303
  }
297
304
  lines.push(" },");
298
- lines.push(` engine: ${emitEngineOptions(ds)},`);
305
+ if (ds.engine) {
306
+ lines.push(` engine: ${emitEngineOptions(ds.engine)},`);
307
+ }
299
308
 
300
309
  if (ds.kafka) {
301
310
  const connectionVar = toCamelCase(ds.kafka.connectionName);
@@ -423,24 +432,27 @@ function emitPipe(pipe: PipeModel): string {
423
432
  lines.push(`export const ${variableName} = defineMaterializedView(${escapeString(pipe.name)}, {`);
424
433
  } else if (pipe.type === "copy") {
425
434
  lines.push(`export const ${variableName} = defineCopyPipe(${escapeString(pipe.name)}, {`);
435
+ } else if (pipe.type === "sink") {
436
+ lines.push(`export const ${variableName} = defineSinkPipe(${escapeString(pipe.name)}, {`);
426
437
  } else {
427
438
  lines.push(`export const ${variableName} = definePipe(${escapeString(pipe.name)}, {`);
428
439
  }
429
440
 
430
- if (pipe.description) {
441
+ if (pipe.description !== undefined) {
431
442
  lines.push(` description: ${escapeString(pipe.description)},`);
432
443
  }
433
444
 
434
- if (pipe.type === "pipe" || pipe.type === "endpoint") {
445
+ if (pipe.type === "pipe" || pipe.type === "endpoint" || pipe.type === "sink") {
435
446
  if (pipe.params.length > 0) {
436
447
  lines.push(" params: {");
437
448
  for (const param of pipe.params) {
438
449
  const baseValidator = strictParamBaseValidator(param.type);
439
- const validator = applyParamOptional(
450
+ const validatorWithOptional = applyParamOptional(
440
451
  baseValidator,
441
452
  param.required,
442
453
  param.defaultValue
443
454
  );
455
+ const validator = applyParamDescription(validatorWithOptional, param.description);
444
456
  lines.push(` ${emitObjectKey(param.name)}: ${validator},`);
445
457
  }
446
458
  lines.push(" },");
@@ -464,11 +476,35 @@ function emitPipe(pipe: PipeModel): string {
464
476
  }
465
477
  }
466
478
 
479
+ if (pipe.type === "sink") {
480
+ if (!pipe.sink) {
481
+ throw new Error(`Sink pipe "${pipe.name}" is missing sink configuration.`);
482
+ }
483
+ lines.push(" sink: {");
484
+ lines.push(` connection: ${toCamelCase(pipe.sink.connectionName)},`);
485
+ if (pipe.sink.service === "kafka") {
486
+ lines.push(` topic: ${escapeString(pipe.sink.topic)},`);
487
+ lines.push(` schedule: ${escapeString(pipe.sink.schedule)},`);
488
+ } else {
489
+ lines.push(` bucketUri: ${escapeString(pipe.sink.bucketUri)},`);
490
+ lines.push(` fileTemplate: ${escapeString(pipe.sink.fileTemplate)},`);
491
+ lines.push(` schedule: ${escapeString(pipe.sink.schedule)},`);
492
+ lines.push(` format: ${escapeString(pipe.sink.format)},`);
493
+ if (pipe.sink.strategy) {
494
+ lines.push(` strategy: ${escapeString(pipe.sink.strategy)},`);
495
+ }
496
+ if (pipe.sink.compression) {
497
+ lines.push(` compression: ${escapeString(pipe.sink.compression)},`);
498
+ }
499
+ }
500
+ lines.push(" },");
501
+ }
502
+
467
503
  lines.push(" nodes: [");
468
504
  for (const node of pipe.nodes) {
469
505
  lines.push(" node({");
470
506
  lines.push(` name: ${escapeString(node.name)},`);
471
- if (node.description) {
507
+ if (node.description !== undefined) {
472
508
  lines.push(` description: ${escapeString(node.description)},`);
473
509
  }
474
510
  lines.push(" sql: `");
@@ -526,7 +562,6 @@ export function emitMigrationFileContent(resources: ParsedResource[]): string {
526
562
  "defineCopyPipe",
527
563
  "node",
528
564
  "t",
529
- "engine",
530
565
  ]);
531
566
  if (connections.some((connection) => connection.connectionType === "kafka")) {
532
567
  imports.add("defineKafkaConnection");
@@ -537,6 +572,12 @@ export function emitMigrationFileContent(resources: ParsedResource[]): string {
537
572
  if (needsParams) {
538
573
  imports.add("p");
539
574
  }
575
+ if (pipes.some((pipe) => pipe.type === "sink")) {
576
+ imports.add("defineSinkPipe");
577
+ }
578
+ if (datasources.some((datasource) => datasource.engine !== undefined)) {
579
+ imports.add("engine");
580
+ }
540
581
  if (needsSecret) {
541
582
  imports.add("secret");
542
583
  }
@@ -548,6 +589,7 @@ export function emitMigrationFileContent(resources: ParsedResource[]): string {
548
589
  "definePipe",
549
590
  "defineMaterializedView",
550
591
  "defineCopyPipe",
592
+ "defineSinkPipe",
551
593
  "node",
552
594
  "t",
553
595
  "engine",
@@ -4,45 +4,44 @@ import {
4
4
  isBlank,
5
5
  parseDirectiveLine,
6
6
  parseQuotedValue,
7
+ readDirectiveBlock,
7
8
  splitCommaSeparated,
8
9
  splitLines,
9
10
  splitTopLevelComma,
10
- stripIndent,
11
11
  } from "./parser-utils.js";
12
12
 
13
- interface BlockReadResult {
14
- lines: string[];
15
- nextIndex: number;
16
- }
17
-
18
- function readIndentedBlock(lines: string[], startIndex: number): BlockReadResult {
19
- const collected: string[] = [];
20
- let i = startIndex;
21
-
22
- while (i < lines.length) {
23
- const line = lines[i] ?? "";
24
- if (line.startsWith(" ")) {
25
- collected.push(stripIndent(line));
26
- i += 1;
27
- continue;
28
- }
29
-
30
- if (isBlank(line)) {
31
- let j = i + 1;
32
- while (j < lines.length && isBlank(lines[j] ?? "")) {
33
- j += 1;
34
- }
35
- if (j < lines.length && (lines[j] ?? "").startsWith(" ")) {
36
- collected.push("");
37
- i += 1;
38
- continue;
39
- }
40
- }
41
-
42
- break;
13
+ const DATASOURCE_DIRECTIVES = new Set([
14
+ "DESCRIPTION",
15
+ "SCHEMA",
16
+ "FORWARD_QUERY",
17
+ "SHARED_WITH",
18
+ "ENGINE",
19
+ "ENGINE_SORTING_KEY",
20
+ "ENGINE_PARTITION_KEY",
21
+ "ENGINE_PRIMARY_KEY",
22
+ "ENGINE_TTL",
23
+ "ENGINE_VER",
24
+ "ENGINE_SIGN",
25
+ "ENGINE_VERSION",
26
+ "ENGINE_SUMMING_COLUMNS",
27
+ "ENGINE_SETTINGS",
28
+ "KAFKA_CONNECTION_NAME",
29
+ "KAFKA_TOPIC",
30
+ "KAFKA_GROUP_ID",
31
+ "KAFKA_AUTO_OFFSET_RESET",
32
+ "IMPORT_CONNECTION_NAME",
33
+ "IMPORT_BUCKET_URI",
34
+ "IMPORT_SCHEDULE",
35
+ "IMPORT_FROM_TIMESTAMP",
36
+ "TOKEN",
37
+ ]);
38
+
39
+ function isDatasourceDirectiveLine(line: string): boolean {
40
+ if (!line) {
41
+ return false;
43
42
  }
44
-
45
- return { lines: collected, nextIndex: i };
43
+ const { key } = parseDirectiveLine(line);
44
+ return DATASOURCE_DIRECTIVES.has(key);
46
45
  }
47
46
 
48
47
  function findTokenOutsideContexts(input: string, token: string): number {
@@ -197,7 +196,15 @@ function parseEngineSettings(value: string): Record<string, string | number | bo
197
196
  }
198
197
 
199
198
  function parseToken(filePath: string, resourceName: string, value: string): DatasourceTokenModel {
200
- const parts = value.split(/\s+/).filter(Boolean);
199
+ const trimmed = value.trim();
200
+ const quotedMatch = trimmed.match(/^"([^"]+)"\s+(READ|APPEND)$/);
201
+ if (quotedMatch) {
202
+ const name = quotedMatch[1];
203
+ const scope = quotedMatch[2] as "READ" | "APPEND";
204
+ return { name, scope };
205
+ }
206
+
207
+ const parts = trimmed.split(/\s+/).filter(Boolean);
201
208
  if (parts.length < 2) {
202
209
  throw new MigrationParseError(
203
210
  filePath,
@@ -216,7 +223,11 @@ function parseToken(filePath: string, resourceName: string, value: string): Data
216
223
  );
217
224
  }
218
225
 
219
- const name = parts[0];
226
+ const rawName = parts[0] ?? "";
227
+ const name =
228
+ rawName.startsWith('"') && rawName.endsWith('"') && rawName.length >= 2
229
+ ? rawName.slice(1, -1)
230
+ : rawName;
220
231
  const scope = parts[1];
221
232
  if (scope !== "READ" && scope !== "APPEND") {
222
233
  throw new MigrationParseError(
@@ -271,22 +282,14 @@ export function parseDatasourceFile(resource: ResourceFile): DatasourceModel {
271
282
  }
272
283
 
273
284
  if (line === "DESCRIPTION >") {
274
- const block = readIndentedBlock(lines, i + 1);
275
- if (block.lines.length === 0) {
276
- throw new MigrationParseError(
277
- resource.filePath,
278
- "datasource",
279
- resource.name,
280
- "DESCRIPTION block is empty."
281
- );
282
- }
285
+ const block = readDirectiveBlock(lines, i + 1, isDatasourceDirectiveLine);
283
286
  description = block.lines.join("\n");
284
287
  i = block.nextIndex;
285
288
  continue;
286
289
  }
287
290
 
288
291
  if (line === "SCHEMA >") {
289
- const block = readIndentedBlock(lines, i + 1);
292
+ const block = readDirectiveBlock(lines, i + 1, isDatasourceDirectiveLine);
290
293
  if (block.lines.length === 0) {
291
294
  throw new MigrationParseError(
292
295
  resource.filePath,
@@ -306,7 +309,7 @@ export function parseDatasourceFile(resource: ResourceFile): DatasourceModel {
306
309
  }
307
310
 
308
311
  if (line === "FORWARD_QUERY >") {
309
- const block = readIndentedBlock(lines, i + 1);
312
+ const block = readDirectiveBlock(lines, i + 1, isDatasourceDirectiveLine);
310
313
  if (block.lines.length === 0) {
311
314
  throw new MigrationParseError(
312
315
  resource.filePath,
@@ -321,7 +324,7 @@ export function parseDatasourceFile(resource: ResourceFile): DatasourceModel {
321
324
  }
322
325
 
323
326
  if (line === "SHARED_WITH >") {
324
- const block = readIndentedBlock(lines, i + 1);
327
+ const block = readDirectiveBlock(lines, i + 1, isDatasourceDirectiveLine);
325
328
  for (const sharedLine of block.lines) {
326
329
  const normalized = sharedLine.trim().replace(/,$/, "");
327
330
  if (normalized) {
@@ -449,16 +452,25 @@ export function parseDatasourceFile(resource: ResourceFile): DatasourceModel {
449
452
  );
450
453
  }
451
454
 
452
- if (!engineType) {
453
- throw new MigrationParseError(
454
- resource.filePath,
455
- "datasource",
456
- resource.name,
457
- "ENGINE directive is required."
458
- );
455
+ const hasEngineDirectives =
456
+ sortingKey.length > 0 ||
457
+ partitionKey !== undefined ||
458
+ (primaryKey !== undefined && primaryKey.length > 0) ||
459
+ ttl !== undefined ||
460
+ ver !== undefined ||
461
+ isDeleted !== undefined ||
462
+ sign !== undefined ||
463
+ version !== undefined ||
464
+ (summingColumns !== undefined && summingColumns.length > 0) ||
465
+ settings !== undefined;
466
+
467
+ if (!engineType && hasEngineDirectives) {
468
+ // Tinybird defaults to MergeTree when ENGINE is omitted.
469
+ // If engine-specific options are present, preserve them by inferring MergeTree.
470
+ engineType = "MergeTree";
459
471
  }
460
472
 
461
- if (sortingKey.length === 0) {
473
+ if (engineType && sortingKey.length === 0) {
462
474
  throw new MigrationParseError(
463
475
  resource.filePath,
464
476
  "datasource",
@@ -525,19 +537,21 @@ export function parseDatasourceFile(resource: ResourceFile): DatasourceModel {
525
537
  filePath: resource.filePath,
526
538
  description,
527
539
  columns,
528
- engine: {
529
- type: engineType,
530
- sortingKey,
531
- partitionKey,
532
- primaryKey,
533
- ttl,
534
- ver,
535
- isDeleted,
536
- sign,
537
- version,
538
- summingColumns,
539
- settings,
540
- },
540
+ engine: engineType
541
+ ? {
542
+ type: engineType,
543
+ sortingKey,
544
+ partitionKey,
545
+ primaryKey,
546
+ ttl,
547
+ ver,
548
+ isDeleted,
549
+ sign,
550
+ version,
551
+ summingColumns,
552
+ settings,
553
+ }
554
+ : undefined,
541
555
  kafka,
542
556
  s3,
543
557
  forwardQuery,