@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.
- package/README.md +53 -3
- package/dist/cli/commands/migrate.d.ts.map +1 -1
- package/dist/cli/commands/migrate.js +32 -0
- package/dist/cli/commands/migrate.js.map +1 -1
- package/dist/cli/commands/migrate.test.js +398 -1
- package/dist/cli/commands/migrate.test.js.map +1 -1
- package/dist/generator/pipe.d.ts.map +1 -1
- package/dist/generator/pipe.js +31 -1
- package/dist/generator/pipe.js.map +1 -1
- package/dist/generator/pipe.test.js +50 -1
- package/dist/generator/pipe.test.js.map +1 -1
- package/dist/index.d.ts +2 -2
- 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 +50 -9
- 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 +77 -49
- 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 +254 -44
- package/dist/migrate/parse-pipe.js.map +1 -1
- package/dist/migrate/parser-utils.d.ts +5 -0
- package/dist/migrate/parser-utils.d.ts.map +1 -1
- package/dist/migrate/parser-utils.js +22 -0
- package/dist/migrate/parser-utils.js.map +1 -1
- package/dist/migrate/types.d.ts +22 -3
- package/dist/migrate/types.d.ts.map +1 -1
- package/dist/schema/datasource.test.js +1 -0
- package/dist/schema/datasource.test.js.map +1 -1
- package/dist/schema/pipe.d.ts +90 -3
- package/dist/schema/pipe.d.ts.map +1 -1
- package/dist/schema/pipe.js +84 -0
- package/dist/schema/pipe.js.map +1 -1
- package/dist/schema/pipe.test.js +70 -1
- package/dist/schema/pipe.test.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/migrate.test.ts +580 -1
- package/src/cli/commands/migrate.ts +35 -0
- package/src/generator/pipe.test.ts +56 -1
- package/src/generator/pipe.ts +41 -1
- package/src/index.ts +9 -0
- package/src/migrate/emit-ts.ts +52 -10
- package/src/migrate/parse-datasource.ts +82 -68
- package/src/migrate/parse-pipe.ts +359 -66
- package/src/migrate/parser-utils.ts +36 -1
- package/src/migrate/types.ts +25 -3
- package/src/schema/datasource.test.ts +1 -0
- package/src/schema/pipe.test.ts +89 -0
- 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', {
|
package/src/generator/pipe.ts
CHANGED
|
@@ -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 {
|
|
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,
|
package/src/migrate/emit-ts.ts
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
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,
|