@tinybirdco/sdk 0.0.48 → 0.0.50

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 (84) hide show
  1. package/README.md +71 -4
  2. package/dist/cli/commands/migrate.d.ts.map +1 -1
  3. package/dist/cli/commands/migrate.js +68 -1
  4. package/dist/cli/commands/migrate.js.map +1 -1
  5. package/dist/cli/commands/migrate.test.js +458 -1
  6. package/dist/cli/commands/migrate.test.js.map +1 -1
  7. package/dist/generator/connection.d.ts.map +1 -1
  8. package/dist/generator/connection.js +14 -1
  9. package/dist/generator/connection.js.map +1 -1
  10. package/dist/generator/connection.test.js +20 -4
  11. package/dist/generator/connection.test.js.map +1 -1
  12. package/dist/generator/datasource.d.ts.map +1 -1
  13. package/dist/generator/datasource.js +20 -10
  14. package/dist/generator/datasource.js.map +1 -1
  15. package/dist/generator/datasource.test.js +26 -1
  16. package/dist/generator/datasource.test.js.map +1 -1
  17. package/dist/generator/pipe.d.ts.map +1 -1
  18. package/dist/generator/pipe.js +31 -1
  19. package/dist/generator/pipe.js.map +1 -1
  20. package/dist/generator/pipe.test.js +50 -1
  21. package/dist/generator/pipe.test.js.map +1 -1
  22. package/dist/index.d.ts +5 -5
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +2 -2
  25. package/dist/index.js.map +1 -1
  26. package/dist/migrate/emit-ts.d.ts.map +1 -1
  27. package/dist/migrate/emit-ts.js +95 -20
  28. package/dist/migrate/emit-ts.js.map +1 -1
  29. package/dist/migrate/parse-connection.d.ts +2 -2
  30. package/dist/migrate/parse-connection.d.ts.map +1 -1
  31. package/dist/migrate/parse-connection.js +34 -4
  32. package/dist/migrate/parse-connection.js.map +1 -1
  33. package/dist/migrate/parse-datasource.d.ts.map +1 -1
  34. package/dist/migrate/parse-datasource.js +79 -51
  35. package/dist/migrate/parse-datasource.js.map +1 -1
  36. package/dist/migrate/parse-pipe.d.ts.map +1 -1
  37. package/dist/migrate/parse-pipe.js +254 -44
  38. package/dist/migrate/parse-pipe.js.map +1 -1
  39. package/dist/migrate/parser-utils.d.ts +5 -0
  40. package/dist/migrate/parser-utils.d.ts.map +1 -1
  41. package/dist/migrate/parser-utils.js +22 -0
  42. package/dist/migrate/parser-utils.js.map +1 -1
  43. package/dist/migrate/types.d.ts +37 -4
  44. package/dist/migrate/types.d.ts.map +1 -1
  45. package/dist/schema/connection.d.ts +34 -1
  46. package/dist/schema/connection.d.ts.map +1 -1
  47. package/dist/schema/connection.js +26 -0
  48. package/dist/schema/connection.js.map +1 -1
  49. package/dist/schema/connection.test.js +35 -1
  50. package/dist/schema/connection.test.js.map +1 -1
  51. package/dist/schema/datasource.d.ts +16 -1
  52. package/dist/schema/datasource.d.ts.map +1 -1
  53. package/dist/schema/datasource.js +3 -2
  54. package/dist/schema/datasource.js.map +1 -1
  55. package/dist/schema/datasource.test.js +33 -3
  56. package/dist/schema/datasource.test.js.map +1 -1
  57. package/dist/schema/pipe.d.ts +90 -3
  58. package/dist/schema/pipe.d.ts.map +1 -1
  59. package/dist/schema/pipe.js +84 -0
  60. package/dist/schema/pipe.js.map +1 -1
  61. package/dist/schema/pipe.test.js +70 -1
  62. package/dist/schema/pipe.test.js.map +1 -1
  63. package/package.json +1 -1
  64. package/src/cli/commands/migrate.test.ts +671 -1
  65. package/src/cli/commands/migrate.ts +74 -1
  66. package/src/generator/connection.test.ts +29 -4
  67. package/src/generator/connection.ts +25 -2
  68. package/src/generator/datasource.test.ts +30 -1
  69. package/src/generator/datasource.ts +22 -10
  70. package/src/generator/pipe.test.ts +56 -1
  71. package/src/generator/pipe.ts +41 -1
  72. package/src/index.ts +14 -0
  73. package/src/migrate/emit-ts.ts +106 -24
  74. package/src/migrate/parse-connection.ts +56 -6
  75. package/src/migrate/parse-datasource.ts +84 -70
  76. package/src/migrate/parse-pipe.ts +359 -66
  77. package/src/migrate/parser-utils.ts +36 -1
  78. package/src/migrate/types.ts +43 -4
  79. package/src/schema/connection.test.ts +48 -0
  80. package/src/schema/connection.ts +60 -1
  81. package/src/schema/datasource.test.ts +39 -3
  82. package/src/schema/datasource.ts +24 -3
  83. package/src/schema/pipe.test.ts +89 -0
  84. package/src/schema/pipe.ts +188 -4
@@ -3,6 +3,8 @@ import { toCamelCase } from "../codegen/utils.js";
3
3
  import { parseLiteralFromDatafile, toTsLiteral } from "./parser-utils.js";
4
4
  import type {
5
5
  DatasourceModel,
6
+ DatasourceEngineModel,
7
+ GCSConnectionModel,
6
8
  KafkaConnectionModel,
7
9
  ParsedResource,
8
10
  PipeModel,
@@ -58,11 +60,13 @@ function hasSecretTemplate(resources: ParsedResource[]): boolean {
58
60
  if (resource.secret) values.push(resource.secret);
59
61
  if (resource.sslCaPem) values.push(resource.sslCaPem);
60
62
  if (resource.schemaRegistryUrl) values.push(resource.schemaRegistryUrl);
61
- } else {
63
+ } else if (resource.connectionType === "s3") {
62
64
  values.push(resource.region);
63
65
  if (resource.arn) values.push(resource.arn);
64
66
  if (resource.accessKey) values.push(resource.accessKey);
65
67
  if (resource.secret) values.push(resource.secret);
68
+ } else {
69
+ values.push(resource.serviceAccountCredentialsJson);
66
70
  }
67
71
  continue;
68
72
  }
@@ -79,6 +83,11 @@ function hasSecretTemplate(resources: ParsedResource[]): boolean {
79
83
  if (resource.s3.schedule) values.push(resource.s3.schedule);
80
84
  if (resource.s3.fromTimestamp) values.push(resource.s3.fromTimestamp);
81
85
  }
86
+ if (resource.gcs) {
87
+ values.push(resource.gcs.bucketUri);
88
+ if (resource.gcs.schedule) values.push(resource.gcs.schedule);
89
+ if (resource.gcs.fromTimestamp) values.push(resource.gcs.fromTimestamp);
90
+ }
82
91
  continue;
83
92
  }
84
93
  }
@@ -151,7 +160,7 @@ function strictParamBaseValidator(type: string): string {
151
160
  function applyParamOptional(
152
161
  baseValidator: string,
153
162
  required: boolean,
154
- defaultValue: string | number | undefined
163
+ defaultValue: string | number | boolean | undefined
155
164
  ): string {
156
165
  const withDefault = defaultValue !== undefined;
157
166
  if (!withDefault && required) {
@@ -168,6 +177,13 @@ function applyParamOptional(
168
177
  return `${baseValidator}${optionalSuffix}`;
169
178
  }
170
179
 
180
+ function applyParamDescription(validator: string, description: string | undefined): string {
181
+ if (description === undefined) {
182
+ return validator;
183
+ }
184
+ return `${validator}.describe(${JSON.stringify(description)})`;
185
+ }
186
+
171
187
  function engineFunctionName(type: string): string {
172
188
  const map: Record<string, string> = {
173
189
  MergeTree: "mergeTree",
@@ -184,9 +200,8 @@ function engineFunctionName(type: string): string {
184
200
  return functionName;
185
201
  }
186
202
 
187
- function emitEngineOptions(ds: DatasourceModel): string {
203
+ function emitEngineOptions(engine: DatasourceEngineModel): string {
188
204
  const options: string[] = [];
189
- const { engine } = ds;
190
205
 
191
206
  if (engine.sortingKey.length === 1) {
192
207
  options.push(`sortingKey: ${escapeString(engine.sortingKey[0]!)}`);
@@ -256,7 +271,7 @@ function emitDatasource(ds: DatasourceModel): string {
256
271
  }
257
272
 
258
273
  lines.push(`export const ${variableName} = defineDatasource(${escapeString(ds.name)}, {`);
259
- if (ds.description) {
274
+ if (ds.description !== undefined) {
260
275
  lines.push(` description: ${emitStringOrSecret(ds.description)},`);
261
276
  }
262
277
  if (!hasJsonPath) {
@@ -295,7 +310,9 @@ function emitDatasource(ds: DatasourceModel): string {
295
310
  lines.push(` ${columnKey}: ${validator},`);
296
311
  }
297
312
  lines.push(" },");
298
- lines.push(` engine: ${emitEngineOptions(ds)},`);
313
+ if (ds.engine) {
314
+ lines.push(` engine: ${emitEngineOptions(ds.engine)},`);
315
+ }
299
316
 
300
317
  if (ds.kafka) {
301
318
  const connectionVar = toCamelCase(ds.kafka.connectionName);
@@ -328,6 +345,20 @@ function emitDatasource(ds: DatasourceModel): string {
328
345
  lines.push(" },");
329
346
  }
330
347
 
348
+ if (ds.gcs) {
349
+ const connectionVar = toCamelCase(ds.gcs.connectionName);
350
+ lines.push(" gcs: {");
351
+ lines.push(` connection: ${connectionVar},`);
352
+ lines.push(` bucketUri: ${emitStringOrSecret(ds.gcs.bucketUri)},`);
353
+ if (ds.gcs.schedule) {
354
+ lines.push(` schedule: ${emitStringOrSecret(ds.gcs.schedule)},`);
355
+ }
356
+ if (ds.gcs.fromTimestamp) {
357
+ lines.push(` fromTimestamp: ${emitStringOrSecret(ds.gcs.fromTimestamp)},`);
358
+ }
359
+ lines.push(" },");
360
+ }
361
+
331
362
  if (ds.forwardQuery) {
332
363
  lines.push(" forwardQuery: `");
333
364
  lines.push(ds.forwardQuery.replace(/`/g, "\\`").replace(/\${/g, "\\${"));
@@ -355,7 +386,9 @@ function emitDatasource(ds: DatasourceModel): string {
355
386
  return lines.join("\n");
356
387
  }
357
388
 
358
- function emitConnection(connection: KafkaConnectionModel | S3ConnectionModel): string {
389
+ function emitConnection(
390
+ connection: KafkaConnectionModel | S3ConnectionModel | GCSConnectionModel
391
+ ): string {
359
392
  const variableName = toCamelCase(connection.name);
360
393
  const lines: string[] = [];
361
394
 
@@ -387,19 +420,31 @@ function emitConnection(connection: KafkaConnectionModel | S3ConnectionModel): s
387
420
  return lines.join("\n");
388
421
  }
389
422
 
423
+ if (connection.connectionType === "s3") {
424
+ lines.push(
425
+ `export const ${variableName} = defineS3Connection(${escapeString(connection.name)}, {`
426
+ );
427
+ lines.push(` region: ${emitStringOrSecret(connection.region)},`);
428
+ if (connection.arn) {
429
+ lines.push(` arn: ${emitStringOrSecret(connection.arn)},`);
430
+ }
431
+ if (connection.accessKey) {
432
+ lines.push(` accessKey: ${emitStringOrSecret(connection.accessKey)},`);
433
+ }
434
+ if (connection.secret) {
435
+ lines.push(` secret: ${emitStringOrSecret(connection.secret)},`);
436
+ }
437
+ lines.push("});");
438
+ lines.push("");
439
+ return lines.join("\n");
440
+ }
441
+
390
442
  lines.push(
391
- `export const ${variableName} = defineS3Connection(${escapeString(connection.name)}, {`
443
+ `export const ${variableName} = defineGCSConnection(${escapeString(connection.name)}, {`
444
+ );
445
+ lines.push(
446
+ ` serviceAccountCredentialsJson: ${emitStringOrSecret(connection.serviceAccountCredentialsJson)},`
392
447
  );
393
- lines.push(` region: ${emitStringOrSecret(connection.region)},`);
394
- if (connection.arn) {
395
- lines.push(` arn: ${emitStringOrSecret(connection.arn)},`);
396
- }
397
- if (connection.accessKey) {
398
- lines.push(` accessKey: ${emitStringOrSecret(connection.accessKey)},`);
399
- }
400
- if (connection.secret) {
401
- lines.push(` secret: ${emitStringOrSecret(connection.secret)},`);
402
- }
403
448
  lines.push("});");
404
449
  lines.push("");
405
450
  return lines.join("\n");
@@ -423,24 +468,27 @@ function emitPipe(pipe: PipeModel): string {
423
468
  lines.push(`export const ${variableName} = defineMaterializedView(${escapeString(pipe.name)}, {`);
424
469
  } else if (pipe.type === "copy") {
425
470
  lines.push(`export const ${variableName} = defineCopyPipe(${escapeString(pipe.name)}, {`);
471
+ } else if (pipe.type === "sink") {
472
+ lines.push(`export const ${variableName} = defineSinkPipe(${escapeString(pipe.name)}, {`);
426
473
  } else {
427
474
  lines.push(`export const ${variableName} = definePipe(${escapeString(pipe.name)}, {`);
428
475
  }
429
476
 
430
- if (pipe.description) {
477
+ if (pipe.description !== undefined) {
431
478
  lines.push(` description: ${escapeString(pipe.description)},`);
432
479
  }
433
480
 
434
- if (pipe.type === "pipe" || pipe.type === "endpoint") {
481
+ if (pipe.type === "pipe" || pipe.type === "endpoint" || pipe.type === "sink") {
435
482
  if (pipe.params.length > 0) {
436
483
  lines.push(" params: {");
437
484
  for (const param of pipe.params) {
438
485
  const baseValidator = strictParamBaseValidator(param.type);
439
- const validator = applyParamOptional(
486
+ const validatorWithOptional = applyParamOptional(
440
487
  baseValidator,
441
488
  param.required,
442
489
  param.defaultValue
443
490
  );
491
+ const validator = applyParamDescription(validatorWithOptional, param.description);
444
492
  lines.push(` ${emitObjectKey(param.name)}: ${validator},`);
445
493
  }
446
494
  lines.push(" },");
@@ -464,11 +512,35 @@ function emitPipe(pipe: PipeModel): string {
464
512
  }
465
513
  }
466
514
 
515
+ if (pipe.type === "sink") {
516
+ if (!pipe.sink) {
517
+ throw new Error(`Sink pipe "${pipe.name}" is missing sink configuration.`);
518
+ }
519
+ lines.push(" sink: {");
520
+ lines.push(` connection: ${toCamelCase(pipe.sink.connectionName)},`);
521
+ if (pipe.sink.service === "kafka") {
522
+ lines.push(` topic: ${escapeString(pipe.sink.topic)},`);
523
+ lines.push(` schedule: ${escapeString(pipe.sink.schedule)},`);
524
+ } else {
525
+ lines.push(` bucketUri: ${escapeString(pipe.sink.bucketUri)},`);
526
+ lines.push(` fileTemplate: ${escapeString(pipe.sink.fileTemplate)},`);
527
+ lines.push(` schedule: ${escapeString(pipe.sink.schedule)},`);
528
+ lines.push(` format: ${escapeString(pipe.sink.format)},`);
529
+ if (pipe.sink.strategy) {
530
+ lines.push(` strategy: ${escapeString(pipe.sink.strategy)},`);
531
+ }
532
+ if (pipe.sink.compression) {
533
+ lines.push(` compression: ${escapeString(pipe.sink.compression)},`);
534
+ }
535
+ }
536
+ lines.push(" },");
537
+ }
538
+
467
539
  lines.push(" nodes: [");
468
540
  for (const node of pipe.nodes) {
469
541
  lines.push(" node({");
470
542
  lines.push(` name: ${escapeString(node.name)},`);
471
- if (node.description) {
543
+ if (node.description !== undefined) {
472
544
  lines.push(` description: ${escapeString(node.description)},`);
473
545
  }
474
546
  lines.push(" sql: `");
@@ -506,7 +578,7 @@ function emitPipe(pipe: PipeModel): string {
506
578
 
507
579
  export function emitMigrationFileContent(resources: ParsedResource[]): string {
508
580
  const connections = resources.filter(
509
- (resource): resource is KafkaConnectionModel | S3ConnectionModel =>
581
+ (resource): resource is KafkaConnectionModel | S3ConnectionModel | GCSConnectionModel =>
510
582
  resource.kind === "connection"
511
583
  );
512
584
  const datasources = resources.filter(
@@ -526,7 +598,6 @@ export function emitMigrationFileContent(resources: ParsedResource[]): string {
526
598
  "defineCopyPipe",
527
599
  "node",
528
600
  "t",
529
- "engine",
530
601
  ]);
531
602
  if (connections.some((connection) => connection.connectionType === "kafka")) {
532
603
  imports.add("defineKafkaConnection");
@@ -534,9 +605,18 @@ export function emitMigrationFileContent(resources: ParsedResource[]): string {
534
605
  if (connections.some((connection) => connection.connectionType === "s3")) {
535
606
  imports.add("defineS3Connection");
536
607
  }
608
+ if (connections.some((connection) => connection.connectionType === "gcs")) {
609
+ imports.add("defineGCSConnection");
610
+ }
537
611
  if (needsParams) {
538
612
  imports.add("p");
539
613
  }
614
+ if (pipes.some((pipe) => pipe.type === "sink")) {
615
+ imports.add("defineSinkPipe");
616
+ }
617
+ if (datasources.some((datasource) => datasource.engine !== undefined)) {
618
+ imports.add("engine");
619
+ }
540
620
  if (needsSecret) {
541
621
  imports.add("secret");
542
622
  }
@@ -544,10 +624,12 @@ export function emitMigrationFileContent(resources: ParsedResource[]): string {
544
624
  const orderedImports = [
545
625
  "defineKafkaConnection",
546
626
  "defineS3Connection",
627
+ "defineGCSConnection",
547
628
  "defineDatasource",
548
629
  "definePipe",
549
630
  "defineMaterializedView",
550
631
  "defineCopyPipe",
632
+ "defineSinkPipe",
551
633
  "node",
552
634
  "t",
553
635
  "engine",
@@ -1,4 +1,9 @@
1
- import type { KafkaConnectionModel, ResourceFile, S3ConnectionModel } from "./types.js";
1
+ import type {
2
+ GCSConnectionModel,
3
+ KafkaConnectionModel,
4
+ ResourceFile,
5
+ S3ConnectionModel,
6
+ } from "./types.js";
2
7
  import {
3
8
  MigrationParseError,
4
9
  isBlank,
@@ -9,7 +14,7 @@ import {
9
14
 
10
15
  export function parseConnectionFile(
11
16
  resource: ResourceFile
12
- ): KafkaConnectionModel | S3ConnectionModel {
17
+ ): KafkaConnectionModel | S3ConnectionModel | GCSConnectionModel {
13
18
  const lines = splitLines(resource.content);
14
19
  let connectionType: string | undefined;
15
20
 
@@ -34,6 +39,7 @@ export function parseConnectionFile(
34
39
  let arn: string | undefined;
35
40
  let accessKey: string | undefined;
36
41
  let accessSecret: string | undefined;
42
+ let serviceAccountCredentialsJson: string | undefined;
37
43
 
38
44
  for (const rawLine of lines) {
39
45
  const line = rawLine.trim();
@@ -100,6 +106,9 @@ export function parseConnectionFile(
100
106
  case "S3_SECRET":
101
107
  accessSecret = parseQuotedValue(value);
102
108
  break;
109
+ case "GCS_SERVICE_ACCOUNT_CREDENTIALS_JSON":
110
+ serviceAccountCredentialsJson = parseQuotedValue(value);
111
+ break;
103
112
  default:
104
113
  throw new MigrationParseError(
105
114
  resource.filePath,
@@ -120,12 +129,12 @@ export function parseConnectionFile(
120
129
  }
121
130
 
122
131
  if (connectionType === "kafka") {
123
- if (region || arn || accessKey || accessSecret) {
132
+ if (region || arn || accessKey || accessSecret || serviceAccountCredentialsJson) {
124
133
  throw new MigrationParseError(
125
134
  resource.filePath,
126
135
  "connection",
127
136
  resource.name,
128
- "S3 directives are not valid for kafka connections."
137
+ "S3/GCS directives are not valid for kafka connections."
129
138
  );
130
139
  }
131
140
 
@@ -161,13 +170,14 @@ export function parseConnectionFile(
161
170
  key ||
162
171
  secret ||
163
172
  schemaRegistryUrl ||
164
- sslCaPem
173
+ sslCaPem ||
174
+ serviceAccountCredentialsJson
165
175
  ) {
166
176
  throw new MigrationParseError(
167
177
  resource.filePath,
168
178
  "connection",
169
179
  resource.name,
170
- "Kafka directives are not valid for s3 connections."
180
+ "Kafka/GCS directives are not valid for s3 connections."
171
181
  );
172
182
  }
173
183
 
@@ -210,6 +220,46 @@ export function parseConnectionFile(
210
220
  };
211
221
  }
212
222
 
223
+ if (connectionType === "gcs") {
224
+ if (
225
+ bootstrapServers ||
226
+ securityProtocol ||
227
+ saslMechanism ||
228
+ key ||
229
+ secret ||
230
+ schemaRegistryUrl ||
231
+ sslCaPem ||
232
+ region ||
233
+ arn ||
234
+ accessKey ||
235
+ accessSecret
236
+ ) {
237
+ throw new MigrationParseError(
238
+ resource.filePath,
239
+ "connection",
240
+ resource.name,
241
+ "Kafka/S3 directives are not valid for gcs connections."
242
+ );
243
+ }
244
+
245
+ if (!serviceAccountCredentialsJson) {
246
+ throw new MigrationParseError(
247
+ resource.filePath,
248
+ "connection",
249
+ resource.name,
250
+ "GCS_SERVICE_ACCOUNT_CREDENTIALS_JSON is required for gcs connections."
251
+ );
252
+ }
253
+
254
+ return {
255
+ kind: "connection",
256
+ name: resource.name,
257
+ filePath: resource.filePath,
258
+ connectionType: "gcs",
259
+ serviceAccountCredentialsJson,
260
+ };
261
+ }
262
+
213
263
  throw new MigrationParseError(
214
264
  resource.filePath,
215
265
  "connection",
@@ -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",
@@ -506,7 +518,7 @@ export function parseDatasourceFile(resource: ResourceFile): DatasourceModel {
506
518
  resource.filePath,
507
519
  "datasource",
508
520
  resource.name,
509
- "IMPORT_CONNECTION_NAME and IMPORT_BUCKET_URI are required when S3 import directives are used."
521
+ "IMPORT_CONNECTION_NAME and IMPORT_BUCKET_URI are required when import directives are used."
510
522
  );
511
523
  }
512
524
 
@@ -515,7 +527,7 @@ export function parseDatasourceFile(resource: ResourceFile): DatasourceModel {
515
527
  resource.filePath,
516
528
  "datasource",
517
529
  resource.name,
518
- "Datasource cannot mix Kafka directives with S3 import directives."
530
+ "Datasource cannot mix Kafka directives with import directives."
519
531
  );
520
532
  }
521
533
 
@@ -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,