@tinybirdco/sdk 0.0.47 → 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 (107) 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 +585 -8
  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 +3 -0
  9. package/dist/generator/connection.js.map +1 -1
  10. package/dist/generator/connection.test.js +8 -0
  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 +3 -0
  14. package/dist/generator/datasource.js.map +1 -1
  15. package/dist/generator/datasource.test.js +50 -0
  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 +3 -2
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +3 -1
  25. package/dist/index.js.map +1 -1
  26. package/dist/index.test.js +3 -0
  27. package/dist/index.test.js.map +1 -1
  28. package/dist/migrate/emit-ts.d.ts.map +1 -1
  29. package/dist/migrate/emit-ts.js +159 -41
  30. package/dist/migrate/emit-ts.js.map +1 -1
  31. package/dist/migrate/parse-connection.d.ts.map +1 -1
  32. package/dist/migrate/parse-connection.js +13 -2
  33. package/dist/migrate/parse-connection.js.map +1 -1
  34. package/dist/migrate/parse-datasource.d.ts.map +1 -1
  35. package/dist/migrate/parse-datasource.js +115 -52
  36. package/dist/migrate/parse-datasource.js.map +1 -1
  37. package/dist/migrate/parse-pipe.d.ts.map +1 -1
  38. package/dist/migrate/parse-pipe.js +257 -46
  39. package/dist/migrate/parse-pipe.js.map +1 -1
  40. package/dist/migrate/parser-utils.d.ts +5 -0
  41. package/dist/migrate/parser-utils.d.ts.map +1 -1
  42. package/dist/migrate/parser-utils.js +22 -0
  43. package/dist/migrate/parser-utils.js.map +1 -1
  44. package/dist/migrate/types.d.ts +25 -3
  45. package/dist/migrate/types.d.ts.map +1 -1
  46. package/dist/schema/connection.d.ts +2 -0
  47. package/dist/schema/connection.d.ts.map +1 -1
  48. package/dist/schema/connection.js.map +1 -1
  49. package/dist/schema/datasource.d.ts +3 -1
  50. package/dist/schema/datasource.d.ts.map +1 -1
  51. package/dist/schema/datasource.js +8 -1
  52. package/dist/schema/datasource.js.map +1 -1
  53. package/dist/schema/datasource.test.js +13 -0
  54. package/dist/schema/datasource.test.js.map +1 -1
  55. package/dist/schema/engines.d.ts.map +1 -1
  56. package/dist/schema/engines.js +3 -0
  57. package/dist/schema/engines.js.map +1 -1
  58. package/dist/schema/engines.test.js +16 -0
  59. package/dist/schema/engines.test.js.map +1 -1
  60. package/dist/schema/pipe.d.ts +90 -3
  61. package/dist/schema/pipe.d.ts.map +1 -1
  62. package/dist/schema/pipe.js +84 -0
  63. package/dist/schema/pipe.js.map +1 -1
  64. package/dist/schema/pipe.test.js +70 -1
  65. package/dist/schema/pipe.test.js.map +1 -1
  66. package/dist/schema/secret.d.ts +6 -0
  67. package/dist/schema/secret.d.ts.map +1 -0
  68. package/dist/schema/secret.js +14 -0
  69. package/dist/schema/secret.js.map +1 -0
  70. package/dist/schema/secret.test.d.ts +2 -0
  71. package/dist/schema/secret.test.d.ts.map +1 -0
  72. package/dist/schema/secret.test.js +14 -0
  73. package/dist/schema/secret.test.js.map +1 -0
  74. package/dist/schema/types.d.ts +5 -0
  75. package/dist/schema/types.d.ts.map +1 -1
  76. package/dist/schema/types.js +6 -0
  77. package/dist/schema/types.js.map +1 -1
  78. package/dist/schema/types.test.js +12 -0
  79. package/dist/schema/types.test.js.map +1 -1
  80. package/package.json +1 -1
  81. package/src/cli/commands/migrate.test.ts +859 -8
  82. package/src/cli/commands/migrate.ts +35 -0
  83. package/src/generator/connection.test.ts +13 -0
  84. package/src/generator/connection.ts +4 -0
  85. package/src/generator/datasource.test.ts +60 -0
  86. package/src/generator/datasource.ts +3 -0
  87. package/src/generator/pipe.test.ts +56 -1
  88. package/src/generator/pipe.ts +41 -1
  89. package/src/index.test.ts +4 -0
  90. package/src/index.ts +12 -0
  91. package/src/migrate/emit-ts.ts +161 -48
  92. package/src/migrate/parse-connection.ts +15 -2
  93. package/src/migrate/parse-datasource.ts +134 -71
  94. package/src/migrate/parse-pipe.ts +364 -69
  95. package/src/migrate/parser-utils.ts +36 -1
  96. package/src/migrate/types.ts +28 -3
  97. package/src/schema/connection.ts +2 -0
  98. package/src/schema/datasource.test.ts +17 -0
  99. package/src/schema/datasource.ts +13 -2
  100. package/src/schema/engines.test.ts +18 -0
  101. package/src/schema/engines.ts +3 -0
  102. package/src/schema/pipe.test.ts +89 -0
  103. package/src/schema/pipe.ts +188 -4
  104. package/src/schema/secret.test.ts +19 -0
  105. package/src/schema/secret.ts +16 -0
  106. package/src/schema/types.test.ts +14 -0
  107. package/src/schema/types.ts +10 -0
@@ -15,7 +15,7 @@ const EXPECTED_COMPLEX_OUTPUT = `/**
15
15
  * Review endpoint output schemas and any defaults before production use.
16
16
  */
17
17
 
18
- import { defineKafkaConnection, defineDatasource, definePipe, defineMaterializedView, defineCopyPipe, node, t, engine, column, p } from "@tinybirdco/sdk";
18
+ import { defineKafkaConnection, defineDatasource, definePipe, defineMaterializedView, defineCopyPipe, node, t, engine, p } from "@tinybirdco/sdk";
19
19
 
20
20
  // Connections
21
21
 
@@ -36,12 +36,12 @@ export const stream = defineKafkaConnection("stream", {
36
36
  export const events = defineDatasource("events", {
37
37
  description: "Events from Kafka stream",
38
38
  schema: {
39
- event_id: column(t.string(), { jsonPath: "$.event_id" }),
40
- user_id: column(t.uint64(), { jsonPath: "$.user.id" }),
41
- env: column(t.string().default("prod"), { jsonPath: "$.env" }),
42
- is_test: column(t.bool().default(false), { jsonPath: "$.meta.is_test" }),
43
- updated_at: column(t.dateTime(), { jsonPath: "$.updated_at" }),
44
- payload: column(t.string().default("{}").codec("ZSTD(1)"), { jsonPath: "$.payload" }),
39
+ event_id: t.string().jsonPath("$.event_id"),
40
+ user_id: t.uint64().jsonPath("$.user.id"),
41
+ env: t.string().default("prod").jsonPath("$.env"),
42
+ is_test: t.bool().default(false).jsonPath("$.meta.is_test"),
43
+ updated_at: t.dateTime().jsonPath("$.updated_at"),
44
+ payload: t.string().default("{}").codec("ZSTD(1)").jsonPath("$.payload"),
45
45
  },
46
46
  engine: engine.replacingMergeTree({ sortingKey: ["event_id", "user_id"], partitionKey: "toYYYYMM(updated_at)", primaryKey: "event_id", ttl: "updated_at + toIntervalDay(30)", ver: "updated_at", settings: { "index_granularity": 8192, "enable_mixed_granularity_parts": true } }),
47
47
  kafka: {
@@ -107,7 +107,7 @@ export const eventsEndpoint = definePipe("events_endpoint", {
107
107
  SELECT event_id, user_id, payload
108
108
  FROM events
109
109
  WHERE user_id = {{UInt64(user_id)}}
110
- AND env = {{String(env, 'prod')}}
110
+ AND env = {{String(env, 'prod')}}
111
111
  \`,
112
112
  }),
113
113
  node({
@@ -615,4 +615,855 @@ IMPORT_FROM_TIMESTAMP 2024-01-01T00:00:00Z
615
615
  expect(output).toContain('schedule: "@auto"');
616
616
  expect(output).toContain('fromTimestamp: "2024-01-01T00:00:00Z"');
617
617
  });
618
+
619
+
620
+ it("migrates KAFKA_STORE_RAW_VALUE datasource directive", async () => {
621
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tinybird-migrate-"));
622
+ tempDirs.push(tempDir);
623
+
624
+ writeFile(
625
+ tempDir,
626
+ "stream.connection",
627
+ `TYPE kafka
628
+ KAFKA_BOOTSTRAP_SERVERS localhost:9092
629
+ `
630
+ );
631
+
632
+ writeFile(
633
+ tempDir,
634
+ "events.datasource",
635
+ `SCHEMA >
636
+ event_id String
637
+
638
+ ENGINE "MergeTree"
639
+ ENGINE_SORTING_KEY "event_id"
640
+ KAFKA_CONNECTION_NAME stream
641
+ KAFKA_TOPIC events_topic
642
+ KAFKA_STORE_RAW_VALUE True
643
+ `
644
+ );
645
+
646
+ const result = await runMigrate({
647
+ cwd: tempDir,
648
+ patterns: ["."],
649
+ strict: true,
650
+ });
651
+
652
+ expect(result.success).toBe(true);
653
+ expect(result.errors).toHaveLength(0);
654
+
655
+ const output = fs.readFileSync(result.outputPath, "utf-8");
656
+ expect(output).toContain("kafka: {");
657
+ expect(output).toContain("connection: stream");
658
+ expect(output).toContain('topic: "events_topic"');
659
+ expect(output).toContain("storeRawValue: true");
660
+ });
661
+
662
+ it("migrates kafka schema registry and engine is deleted directives", async () => {
663
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tinybird-migrate-"));
664
+ tempDirs.push(tempDir);
665
+
666
+ writeFile(
667
+ tempDir,
668
+ "stream.connection",
669
+ `TYPE kafka
670
+ KAFKA_BOOTSTRAP_SERVERS kafka.example.com:9092
671
+ KAFKA_SCHEMA_REGISTRY_URL https://registry-user:registry-pass@registry.example.com
672
+ # Optional registry auth details
673
+ `
674
+ );
675
+
676
+ writeFile(
677
+ tempDir,
678
+ "events.datasource",
679
+ `SCHEMA >
680
+ event_id String
681
+
682
+ ENGINE "MergeTree"
683
+ ENGINE_SORTING_KEY "event_id"
684
+ KAFKA_CONNECTION_NAME stream
685
+ KAFKA_TOPIC events_topic
686
+ KAFKA_STORE_RAW_VALUE True
687
+ `
688
+ );
689
+
690
+ writeFile(
691
+ tempDir,
692
+ "events_state.datasource",
693
+ `SCHEMA >
694
+ # logical delete marker
695
+ _is_deleted UInt8,
696
+ event_id String,
697
+ version_ts DateTime
698
+
699
+ ENGINE "ReplacingMergeTree"
700
+ ENGINE_SORTING_KEY "event_id"
701
+ ENGINE_VER "version_ts"
702
+ ENGINE_IS_DELETED "_is_deleted"
703
+ `
704
+ );
705
+
706
+ writeFile(
707
+ tempDir,
708
+ "events_state_mv.pipe",
709
+ `NODE latest
710
+ SQL >
711
+ SELECT
712
+ toUInt8(0) AS _is_deleted,
713
+ event_id,
714
+ now() AS version_ts
715
+ FROM events
716
+ # materialized definition
717
+ TYPE MATERIALIZED
718
+ DATASOURCE events_state
719
+ `
720
+ );
721
+
722
+ const result = await runMigrate({
723
+ cwd: tempDir,
724
+ patterns: ["."],
725
+ strict: true,
726
+ });
727
+
728
+ expect(result.success).toBe(true);
729
+ expect(result.errors).toHaveLength(0);
730
+
731
+ const output = fs.readFileSync(result.outputPath, "utf-8");
732
+ expect(output).toContain(
733
+ 'schemaRegistryUrl: "https://registry-user:registry-pass@registry.example.com"'
734
+ );
735
+ expect(output).toContain("storeRawValue: true");
736
+ expect(output).toContain(
737
+ 'engine: engine.replacingMergeTree({ sortingKey: "event_id", ver: "version_ts", isDeleted: "_is_deleted" })'
738
+ );
739
+ });
740
+
741
+ it("migrates TYPE copy in lowercase", async () => {
742
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tinybird-migrate-"));
743
+ tempDirs.push(tempDir);
744
+
745
+ writeFile(
746
+ tempDir,
747
+ "copy_target.datasource",
748
+ `SCHEMA >
749
+ id String
750
+
751
+ ENGINE "MergeTree"
752
+ ENGINE_SORTING_KEY "id"
753
+ `
754
+ );
755
+
756
+ writeFile(
757
+ tempDir,
758
+ "copy_lower.pipe",
759
+ `NODE copy_node
760
+ SQL >
761
+ SELECT id
762
+ FROM copy_target
763
+ TYPE copy
764
+ TARGET_DATASOURCE copy_target
765
+ `
766
+ );
767
+
768
+ const result = await runMigrate({
769
+ cwd: tempDir,
770
+ patterns: ["."],
771
+ strict: true,
772
+ });
773
+
774
+ expect(result.success).toBe(true);
775
+ expect(result.errors).toHaveLength(0);
776
+
777
+ const output = fs.readFileSync(result.outputPath, "utf-8");
778
+ expect(output).toContain('export const copyLower = defineCopyPipe("copy_lower", {');
779
+ expect(output).toContain("datasource: copyTarget,");
780
+ });
781
+
782
+ it("migrates TYPE ENDPOINT in uppercase", async () => {
783
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tinybird-migrate-"));
784
+ tempDirs.push(tempDir);
785
+
786
+ writeFile(
787
+ tempDir,
788
+ "endpoint_upper.pipe",
789
+ `NODE endpoint
790
+ SQL >
791
+ SELECT 1 AS id
792
+ FROM system.numbers
793
+ LIMIT 1
794
+ TYPE ENDPOINT
795
+ `
796
+ );
797
+
798
+ const result = await runMigrate({
799
+ cwd: tempDir,
800
+ patterns: ["."],
801
+ strict: true,
802
+ });
803
+
804
+ expect(result.success).toBe(true);
805
+ expect(result.errors).toHaveLength(0);
806
+
807
+ const output = fs.readFileSync(result.outputPath, "utf-8");
808
+ expect(output).toContain('export const endpointUpper = definePipe("endpoint_upper", {');
809
+ expect(output).toContain("endpoint: true,");
810
+ });
811
+
812
+ it("parses multiline datasource blocks with flexible indentation", async () => {
813
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tinybird-migrate-"));
814
+ tempDirs.push(tempDir);
815
+
816
+ writeFile(
817
+ tempDir,
818
+ "flex.datasource",
819
+ `DESCRIPTION >
820
+ Flexible indentation description
821
+
822
+ SCHEMA >
823
+ id String,
824
+ env String
825
+
826
+ ENGINE "MergeTree"
827
+ ENGINE_SORTING_KEY "id"
828
+ FORWARD_QUERY >
829
+ SELECT id, env
830
+ FROM upstream_ds
831
+ SHARED_WITH >
832
+ workspace_a,
833
+ workspace_b
834
+ `
835
+ );
836
+
837
+ const result = await runMigrate({
838
+ cwd: tempDir,
839
+ patterns: ["."],
840
+ strict: true,
841
+ });
842
+
843
+ expect(result.success).toBe(true);
844
+ expect(result.errors).toHaveLength(0);
845
+
846
+ const output = fs.readFileSync(result.outputPath, "utf-8");
847
+ expect(output).toContain('description: "Flexible indentation description"');
848
+ expect(output).toContain("forwardQuery: `");
849
+ expect(output).toContain("SELECT id, env");
850
+ expect(output).toContain("FROM upstream_ds");
851
+ expect(output).toContain('sharedWith: ["workspace_a", "workspace_b"],');
852
+ });
853
+
854
+ it("parses node SQL blocks with flexible indentation", async () => {
855
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tinybird-migrate-"));
856
+ tempDirs.push(tempDir);
857
+
858
+ writeFile(
859
+ tempDir,
860
+ "flex_pipe.pipe",
861
+ `NODE endpoint_node
862
+ DESCRIPTION >
863
+ Endpoint node description
864
+ SQL >
865
+ %
866
+ SELECT 1 AS id
867
+ FROM system.numbers
868
+ TYPE endpoint
869
+ `
870
+ );
871
+
872
+ const result = await runMigrate({
873
+ cwd: tempDir,
874
+ patterns: ["."],
875
+ strict: true,
876
+ });
877
+
878
+ expect(result.success).toBe(true);
879
+ expect(result.errors).toHaveLength(0);
880
+
881
+ const output = fs.readFileSync(result.outputPath, "utf-8");
882
+ expect(output).toContain('description: "Endpoint node description"');
883
+ expect(output).toContain("SELECT 1 AS id");
884
+ expect(output).toContain("FROM system.numbers");
885
+ expect(output).toContain("endpoint: true,");
886
+ });
887
+
888
+ it("migrates datasource without engine directives", async () => {
889
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tinybird-migrate-"));
890
+ tempDirs.push(tempDir);
891
+
892
+ writeFile(
893
+ tempDir,
894
+ "no_engine.datasource",
895
+ `SCHEMA >
896
+ id String,
897
+ name String
898
+ `
899
+ );
900
+
901
+ const result = await runMigrate({
902
+ cwd: tempDir,
903
+ patterns: ["."],
904
+ strict: true,
905
+ });
906
+
907
+ expect(result.success).toBe(true);
908
+ expect(result.errors).toHaveLength(0);
909
+
910
+ const output = fs.readFileSync(result.outputPath, "utf-8");
911
+ expect(output).toContain('export const noEngine = defineDatasource("no_engine", {');
912
+ expect(output).not.toContain("engine:");
913
+ expect(output).not.toContain(", engine,");
914
+ });
915
+
916
+ it("infers MergeTree when engine options exist without ENGINE directive", async () => {
917
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tinybird-migrate-"));
918
+ tempDirs.push(tempDir);
919
+
920
+ writeFile(
921
+ tempDir,
922
+ "implicit_engine.datasource",
923
+ `SCHEMA >
924
+ id String,
925
+ event_date Date
926
+
927
+ ENGINE_SORTING_KEY "id"
928
+ ENGINE_PARTITION_KEY "toYYYYMM(event_date)"
929
+ `
930
+ );
931
+
932
+ const result = await runMigrate({
933
+ cwd: tempDir,
934
+ patterns: ["."],
935
+ strict: true,
936
+ });
937
+
938
+ expect(result.success).toBe(true);
939
+ expect(result.errors).toHaveLength(0);
940
+
941
+ const output = fs.readFileSync(result.outputPath, "utf-8");
942
+ expect(output).toContain('export const implicitEngine = defineDatasource("implicit_engine", {');
943
+ expect(output).toContain(
944
+ 'engine: engine.mergeTree({ sortingKey: "id", partitionKey: "toYYYYMM(event_date)" }),'
945
+ );
946
+ });
947
+
948
+ it("supports quoted datasource token names with spaces", async () => {
949
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tinybird-migrate-"));
950
+ tempDirs.push(tempDir);
951
+
952
+ writeFile(
953
+ tempDir,
954
+ "token_spaces.datasource",
955
+ `TOKEN "ingestion (Data Source append)" APPEND
956
+
957
+ SCHEMA >
958
+ id String
959
+
960
+ ENGINE "MergeTree"
961
+ ENGINE_SORTING_KEY "id"
962
+ `
963
+ );
964
+
965
+ const result = await runMigrate({
966
+ cwd: tempDir,
967
+ patterns: ["."],
968
+ strict: true,
969
+ });
970
+
971
+ expect(result.success).toBe(true);
972
+ expect(result.errors).toHaveLength(0);
973
+
974
+ const output = fs.readFileSync(result.outputPath, "utf-8");
975
+ expect(output).toContain(
976
+ '{ name: "ingestion (Data Source append)", permissions: ["APPEND"] },'
977
+ );
978
+ });
979
+
980
+ it("rejects unquoted datasource token names with spaces", async () => {
981
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tinybird-migrate-"));
982
+ tempDirs.push(tempDir);
983
+
984
+ writeFile(
985
+ tempDir,
986
+ "bad_token.datasource",
987
+ `TOKEN ingestion data APPEND
988
+
989
+ SCHEMA >
990
+ id String
991
+
992
+ ENGINE "MergeTree"
993
+ ENGINE_SORTING_KEY "id"
994
+ `
995
+ );
996
+
997
+ const result = await runMigrate({
998
+ cwd: tempDir,
999
+ patterns: ["."],
1000
+ strict: true,
1001
+ });
1002
+
1003
+ expect(result.success).toBe(false);
1004
+ expect(result.errors).toHaveLength(1);
1005
+ expect(result.errors[0]?.message).toContain("Unsupported TOKEN syntax in strict mode");
1006
+ });
1007
+
1008
+ it("allows empty datasource DESCRIPTION block", async () => {
1009
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tinybird-migrate-"));
1010
+ tempDirs.push(tempDir);
1011
+
1012
+ writeFile(
1013
+ tempDir,
1014
+ "empty_ds_desc.datasource",
1015
+ `DESCRIPTION >
1016
+
1017
+ SCHEMA >
1018
+ id String
1019
+
1020
+ ENGINE "MergeTree"
1021
+ ENGINE_SORTING_KEY "id"
1022
+ `
1023
+ );
1024
+
1025
+ const result = await runMigrate({
1026
+ cwd: tempDir,
1027
+ patterns: ["."],
1028
+ strict: true,
1029
+ });
1030
+
1031
+ expect(result.success).toBe(true);
1032
+ expect(result.errors).toHaveLength(0);
1033
+
1034
+ const output = fs.readFileSync(result.outputPath, "utf-8");
1035
+ expect(output).toContain(
1036
+ 'export const emptyDsDesc = defineDatasource("empty_ds_desc", {'
1037
+ );
1038
+ expect(output).toContain('description: "",');
1039
+ });
1040
+
1041
+ it("allows empty node DESCRIPTION block", async () => {
1042
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tinybird-migrate-"));
1043
+ tempDirs.push(tempDir);
1044
+
1045
+ writeFile(
1046
+ tempDir,
1047
+ "empty_node_desc.pipe",
1048
+ `NODE helper
1049
+ DESCRIPTION >
1050
+
1051
+ SQL >
1052
+ SELECT 1 AS id
1053
+ TYPE endpoint
1054
+ `
1055
+ );
1056
+
1057
+ const result = await runMigrate({
1058
+ cwd: tempDir,
1059
+ patterns: ["."],
1060
+ strict: true,
1061
+ });
1062
+
1063
+ expect(result.success).toBe(true);
1064
+ expect(result.errors).toHaveLength(0);
1065
+
1066
+ const output = fs.readFileSync(result.outputPath, "utf-8");
1067
+ expect(output).toContain('export const emptyNodeDesc = definePipe("empty_node_desc", {');
1068
+ expect(output).toContain('description: "",');
1069
+ });
1070
+
1071
+ it("supports keyword-style pipe param arguments", async () => {
1072
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tinybird-migrate-"));
1073
+ tempDirs.push(tempDir);
1074
+
1075
+ writeFile(
1076
+ tempDir,
1077
+ "keyword_params.pipe",
1078
+ `NODE endpoint
1079
+ SQL >
1080
+ SELECT 1 AS id
1081
+ WHERE tenant_id = {{ String(tenant_id, description="Tenant ID to filter", default="") }}
1082
+ AND event_date >= {{ Date(from_date, description="Starting date", required=False) }}
1083
+ AND days >= {{ Int32(days, 1, description="Days to include", required=True) }}
1084
+ TYPE endpoint
1085
+ `
1086
+ );
1087
+
1088
+ const result = await runMigrate({
1089
+ cwd: tempDir,
1090
+ patterns: ["."],
1091
+ strict: true,
1092
+ });
1093
+
1094
+ expect(result.success).toBe(true);
1095
+ expect(result.errors).toHaveLength(0);
1096
+
1097
+ const output = fs.readFileSync(result.outputPath, "utf-8");
1098
+ expect(output).toContain('export const keywordParams = definePipe("keyword_params", {');
1099
+ expect(output).toContain(
1100
+ 'tenant_id: p.string().optional("").describe("Tenant ID to filter"),'
1101
+ );
1102
+ expect(output).toContain(
1103
+ 'from_date: p.date().optional().describe("Starting date"),'
1104
+ );
1105
+ expect(output).toContain(
1106
+ 'days: p.int32().optional(1).describe("Days to include"),'
1107
+ );
1108
+ });
1109
+
1110
+ it("migrates datasource with mixed explicit and default json paths", async () => {
1111
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tinybird-migrate-"));
1112
+ tempDirs.push(tempDir);
1113
+
1114
+ writeFile(
1115
+ tempDir,
1116
+ "mixed_paths.datasource",
1117
+ `SCHEMA >
1118
+ event_id String \`json:$.payload.id\`,
1119
+ event_type String
1120
+
1121
+ ENGINE "MergeTree"
1122
+ ENGINE_SORTING_KEY "event_id"
1123
+ `
1124
+ );
1125
+
1126
+ const result = await runMigrate({
1127
+ cwd: tempDir,
1128
+ patterns: ["."],
1129
+ strict: true,
1130
+ });
1131
+
1132
+ expect(result.success).toBe(true);
1133
+ expect(result.errors).toHaveLength(0);
1134
+
1135
+ const output = fs.readFileSync(result.outputPath, "utf-8");
1136
+ expect(output).toContain('event_id: t.string().jsonPath("$.payload.id")');
1137
+ expect(output).toContain("event_type: t.string()");
1138
+ expect(output).not.toContain("jsonPaths: false");
1139
+ });
1140
+
1141
+ it("normalizes backticked schema column names to valid object keys", async () => {
1142
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tinybird-migrate-"));
1143
+ tempDirs.push(tempDir);
1144
+
1145
+ writeFile(
1146
+ tempDir,
1147
+ "backticked.datasource",
1148
+ `SCHEMA >
1149
+ \`_is_deleted\` UInt8 \`json:$._is_deleted\`,
1150
+ \`id\` UUID \`json:$.id\`
1151
+
1152
+ ENGINE "MergeTree"
1153
+ ENGINE_SORTING_KEY "id"
1154
+ `
1155
+ );
1156
+
1157
+ const result = await runMigrate({
1158
+ cwd: tempDir,
1159
+ patterns: ["."],
1160
+ strict: true,
1161
+ });
1162
+
1163
+ expect(result.success).toBe(true);
1164
+ expect(result.errors).toHaveLength(0);
1165
+
1166
+ const output = fs.readFileSync(result.outputPath, "utf-8");
1167
+ expect(output).toContain('_is_deleted: t.uint8().jsonPath("$._is_deleted")');
1168
+ expect(output).toContain('id: t.uuid().jsonPath("$.id")');
1169
+ expect(output).not.toContain("`_is_deleted`:");
1170
+ expect(output).not.toContain("`id`:");
1171
+ });
1172
+
1173
+ it("emits secret helper for tb_secret template values", async () => {
1174
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tinybird-migrate-"));
1175
+ tempDirs.push(tempDir);
1176
+
1177
+ writeFile(
1178
+ tempDir,
1179
+ "stream.connection",
1180
+ `TYPE kafka
1181
+ KAFKA_BOOTSTRAP_SERVERS localhost:9092
1182
+ `
1183
+ );
1184
+
1185
+ writeFile(
1186
+ tempDir,
1187
+ "events.datasource",
1188
+ `SCHEMA >
1189
+ id UUID
1190
+
1191
+ ENGINE "MergeTree"
1192
+ ENGINE_SORTING_KEY "id"
1193
+ KAFKA_CONNECTION_NAME stream
1194
+ KAFKA_TOPIC events_topic
1195
+ KAFKA_GROUP_ID {{ tb_secret("KAFKA_GROUP_ID_LOCAL_ds_accounts", "accounts_1737295200") }}
1196
+ `
1197
+ );
1198
+
1199
+ const result = await runMigrate({
1200
+ cwd: tempDir,
1201
+ patterns: ["."],
1202
+ strict: true,
1203
+ });
1204
+
1205
+ expect(result.success).toBe(true);
1206
+ expect(result.errors).toHaveLength(0);
1207
+
1208
+ const output = fs.readFileSync(result.outputPath, "utf-8");
1209
+ expect(output).toContain('import {');
1210
+ expect(output).toContain('secret } from "@tinybirdco/sdk";');
1211
+ expect(output).not.toContain("const secret = (name: string, defaultValue?: string) =>");
1212
+ expect(output).toContain(
1213
+ 'groupId: secret("KAFKA_GROUP_ID_LOCAL_ds_accounts", "accounts_1737295200"),'
1214
+ );
1215
+ expect(output).not.toContain(
1216
+ 'groupId: "{{ tb_secret(\\"KAFKA_GROUP_ID_LOCAL_ds_accounts\\", \\"accounts_1737295200\\") }}",'
1217
+ );
1218
+ });
1219
+
1220
+ it("does not emit secret helper when no tb_secret template values are present", async () => {
1221
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tinybird-migrate-"));
1222
+ tempDirs.push(tempDir);
1223
+
1224
+ writeFile(
1225
+ tempDir,
1226
+ "stream.connection",
1227
+ `TYPE kafka
1228
+ KAFKA_BOOTSTRAP_SERVERS localhost:9092
1229
+ `
1230
+ );
1231
+
1232
+ writeFile(
1233
+ tempDir,
1234
+ "events.datasource",
1235
+ `SCHEMA >
1236
+ id UUID
1237
+
1238
+ ENGINE "MergeTree"
1239
+ ENGINE_SORTING_KEY "id"
1240
+ KAFKA_CONNECTION_NAME stream
1241
+ KAFKA_TOPIC events_topic
1242
+ KAFKA_GROUP_ID events-group
1243
+ `
1244
+ );
1245
+
1246
+ const result = await runMigrate({
1247
+ cwd: tempDir,
1248
+ patterns: ["."],
1249
+ strict: true,
1250
+ });
1251
+
1252
+ expect(result.success).toBe(true);
1253
+ expect(result.errors).toHaveLength(0);
1254
+
1255
+ const output = fs.readFileSync(result.outputPath, "utf-8");
1256
+ expect(output).not.toContain(", secret,");
1257
+ expect(output).not.toContain("const secret = (name: string, defaultValue?: string) =>");
1258
+ expect(output).toContain('groupId: "events-group",');
1259
+ });
1260
+
1261
+ it("migrates Kafka sink pipes and emits sink config in TypeScript", async () => {
1262
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tinybird-migrate-"));
1263
+ tempDirs.push(tempDir);
1264
+
1265
+ writeFile(
1266
+ tempDir,
1267
+ "events_kafka.connection",
1268
+ `TYPE kafka
1269
+ KAFKA_BOOTSTRAP_SERVERS localhost:9092
1270
+ `
1271
+ );
1272
+
1273
+ writeFile(
1274
+ tempDir,
1275
+ "events_sink.pipe",
1276
+ `NODE publish
1277
+ SQL >
1278
+ SELECT *
1279
+ FROM events
1280
+ WHERE env = {{String(env, 'prod')}}
1281
+ TYPE sink
1282
+ EXPORT_CONNECTION_NAME events_kafka
1283
+ EXPORT_KAFKA_TOPIC events_out
1284
+ EXPORT_SCHEDULE @on-demand
1285
+ `
1286
+ );
1287
+
1288
+ const result = await runMigrate({
1289
+ cwd: tempDir,
1290
+ patterns: ["."],
1291
+ strict: true,
1292
+ });
1293
+
1294
+ expect(result.success).toBe(true);
1295
+ expect(result.errors).toHaveLength(0);
1296
+ expect(result.migrated.filter((resource) => resource.kind === "connection")).toHaveLength(1);
1297
+ expect(result.migrated.filter((resource) => resource.kind === "pipe")).toHaveLength(1);
1298
+
1299
+ const output = fs.readFileSync(result.outputPath, "utf-8");
1300
+ expect(output).toContain('export const eventsSink = defineSinkPipe("events_sink", {');
1301
+ expect(output).toContain("params: {");
1302
+ expect(output).toContain('env: p.string().optional("prod"),');
1303
+ expect(output).toContain("sink: {");
1304
+ expect(output).toContain("connection: eventsKafka");
1305
+ expect(output).toContain('topic: "events_out"');
1306
+ expect(output).toContain('schedule: "@on-demand"');
1307
+ expect(output).not.toContain('strategy:');
1308
+ });
1309
+
1310
+ it("migrates S3 sink pipes and emits compression/strategy config in TypeScript", async () => {
1311
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tinybird-migrate-"));
1312
+ tempDirs.push(tempDir);
1313
+
1314
+ writeFile(
1315
+ tempDir,
1316
+ "exports_s3.connection",
1317
+ `TYPE s3
1318
+ S3_REGION "us-east-1"
1319
+ S3_ARN "arn:aws:iam::123456789012:role/tinybird-s3-access"
1320
+ `
1321
+ );
1322
+
1323
+ writeFile(
1324
+ tempDir,
1325
+ "events_s3_sink.pipe",
1326
+ `NODE export
1327
+ SQL >
1328
+ SELECT * FROM events
1329
+ TYPE sink
1330
+ EXPORT_CONNECTION_NAME exports_s3
1331
+ EXPORT_BUCKET_URI s3://exports/events/
1332
+ EXPORT_FILE_TEMPLATE events_{date}
1333
+ EXPORT_SCHEDULE @once
1334
+ EXPORT_FORMAT ndjson
1335
+ EXPORT_STRATEGY create_new
1336
+ EXPORT_COMPRESSION gzip
1337
+ `
1338
+ );
1339
+
1340
+ const result = await runMigrate({
1341
+ cwd: tempDir,
1342
+ patterns: ["."],
1343
+ strict: true,
1344
+ });
1345
+
1346
+ expect(result.success).toBe(true);
1347
+ expect(result.errors).toHaveLength(0);
1348
+
1349
+ const output = fs.readFileSync(result.outputPath, "utf-8");
1350
+ expect(output).toContain('export const eventsS3Sink = defineSinkPipe("events_s3_sink", {');
1351
+ expect(output).toContain("sink: {");
1352
+ expect(output).toContain("connection: exportsS3");
1353
+ expect(output).toContain('bucketUri: "s3://exports/events/"');
1354
+ expect(output).toContain('fileTemplate: "events_{date}"');
1355
+ expect(output).toContain('schedule: "@once"');
1356
+ expect(output).toContain('format: "ndjson"');
1357
+ expect(output).toContain('strategy: "create_new"');
1358
+ expect(output).toContain('compression: "gzip"');
1359
+ });
1360
+
1361
+ it("reports an error when sink pipe references a missing connection", async () => {
1362
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tinybird-migrate-"));
1363
+ tempDirs.push(tempDir);
1364
+
1365
+ writeFile(
1366
+ tempDir,
1367
+ "events_sink.pipe",
1368
+ `NODE publish
1369
+ SQL >
1370
+ SELECT * FROM events
1371
+ TYPE sink
1372
+ EXPORT_CONNECTION_NAME missing_connection
1373
+ EXPORT_KAFKA_TOPIC events_out
1374
+ EXPORT_SCHEDULE @on-demand
1375
+ `
1376
+ );
1377
+
1378
+ const result = await runMigrate({
1379
+ cwd: tempDir,
1380
+ patterns: ["."],
1381
+ strict: true,
1382
+ });
1383
+
1384
+ expect(result.success).toBe(false);
1385
+ expect(result.errors.map((error) => error.message)).toEqual(
1386
+ expect.arrayContaining([
1387
+ 'Sink pipe references missing/unmigrated connection "missing_connection".',
1388
+ ])
1389
+ );
1390
+ });
1391
+
1392
+ it("reports an error when sink connection type does not match sink service", async () => {
1393
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tinybird-migrate-"));
1394
+ tempDirs.push(tempDir);
1395
+
1396
+ writeFile(
1397
+ tempDir,
1398
+ "events_kafka.connection",
1399
+ `TYPE kafka
1400
+ KAFKA_BOOTSTRAP_SERVERS localhost:9092
1401
+ `
1402
+ );
1403
+
1404
+ writeFile(
1405
+ tempDir,
1406
+ "events_s3_sink.pipe",
1407
+ `NODE export
1408
+ SQL >
1409
+ SELECT * FROM events
1410
+ TYPE sink
1411
+ EXPORT_CONNECTION_NAME events_kafka
1412
+ EXPORT_BUCKET_URI s3://exports/events/
1413
+ EXPORT_FILE_TEMPLATE events_{date}
1414
+ EXPORT_SCHEDULE @once
1415
+ EXPORT_FORMAT csv
1416
+ `
1417
+ );
1418
+
1419
+ const result = await runMigrate({
1420
+ cwd: tempDir,
1421
+ patterns: ["."],
1422
+ strict: true,
1423
+ });
1424
+
1425
+ expect(result.success).toBe(false);
1426
+ expect(result.errors.map((error) => error.message)).toEqual(
1427
+ expect.arrayContaining([
1428
+ 'Sink pipe service "s3" is incompatible with connection "events_kafka" type "kafka".',
1429
+ ])
1430
+ );
1431
+ });
1432
+
1433
+ it("supports comments in sink pipe files", async () => {
1434
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tinybird-migrate-"));
1435
+ tempDirs.push(tempDir);
1436
+
1437
+ writeFile(
1438
+ tempDir,
1439
+ "events_kafka.connection",
1440
+ `TYPE kafka
1441
+ KAFKA_BOOTSTRAP_SERVERS localhost:9092
1442
+ `
1443
+ );
1444
+
1445
+ writeFile(
1446
+ tempDir,
1447
+ "events_sink.pipe",
1448
+ `# this pipe publishes records
1449
+ NODE publish
1450
+ SQL >
1451
+ SELECT * FROM events
1452
+ TYPE sink
1453
+ # Kafka target
1454
+ EXPORT_CONNECTION_NAME events_kafka
1455
+ EXPORT_KAFKA_TOPIC events_out
1456
+ EXPORT_SCHEDULE @on-demand
1457
+ `
1458
+ );
1459
+
1460
+ const result = await runMigrate({
1461
+ cwd: tempDir,
1462
+ patterns: ["."],
1463
+ strict: true,
1464
+ });
1465
+
1466
+ expect(result.success).toBe(true);
1467
+ expect(result.errors).toHaveLength(0);
1468
+ });
618
1469
  });