@useairfoil/connector-kit 0.6.0 → 0.6.1

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 (179) hide show
  1. package/METRICS.md +16 -17
  2. package/README.md +127 -442
  3. package/dist/annotations-BE74YmAO.d.mts +39 -0
  4. package/dist/auth/index.d.mts +2 -0
  5. package/dist/{auth-DvU_CZ7C.js → auth/index.mjs} +6 -8
  6. package/dist/auth/index.mjs.map +1 -0
  7. package/dist/connector-app/index.d.mts +2 -0
  8. package/dist/connector-app/index.mjs +2 -0
  9. package/dist/connector-app-C-L4QqG6.mjs +123 -0
  10. package/dist/connector-app-C-L4QqG6.mjs.map +1 -0
  11. package/dist/{constants-D113Ut1x.js → constants-D-GQzpHZ.mjs} +4 -5
  12. package/dist/constants-D-GQzpHZ.mjs.map +1 -0
  13. package/dist/demo/index.d.mts +2 -0
  14. package/dist/demo/index.mjs +2 -0
  15. package/dist/demo-B30Vlkk2.mjs +191 -0
  16. package/dist/demo-B30Vlkk2.mjs.map +1 -0
  17. package/dist/errors/index.d.mts +2 -0
  18. package/dist/errors/index.mjs +2 -0
  19. package/dist/{errors-DHWylqRZ.js → errors-pT0YVJU5.mjs} +2 -3
  20. package/dist/errors-pT0YVJU5.mjs.map +1 -0
  21. package/dist/iceberg/index.d.mts +3 -0
  22. package/dist/iceberg/index.mjs +3 -0
  23. package/dist/iceberg-D6L8I6vA.mjs +39 -0
  24. package/dist/iceberg-D6L8I6vA.mjs.map +1 -0
  25. package/dist/{index-VpHaZXZw.d.ts → index-BUHSGm9Q.d.mts} +11 -13
  26. package/dist/{index-DXa26Ry5.d.ts → index-BX8G7d41.d.mts} +1 -2
  27. package/dist/{index-bfjH7Yb0.d.ts → index-BfXpgxk9.d.mts} +9 -10
  28. package/dist/{index-BKw7jA2a.d.ts → index-CRVAwJ5-.d.mts} +10 -8
  29. package/dist/index-CUPP2A_m.d.mts +27 -0
  30. package/dist/{index-DYpEMJf-.d.ts → index-CgvFomO6.d.mts} +6 -7
  31. package/dist/index-DJhDp4Xc.d.mts +58 -0
  32. package/dist/index-Dk7urvpF.d.mts +76 -0
  33. package/dist/{index-DKoRwyFn.d.ts → index-DkVNO6ks.d.mts} +2 -5
  34. package/dist/{index-C5Ma7V7t.d.ts → index-NnCutIqM.d.mts} +62 -37
  35. package/dist/{index-CR9HJDdr.d.ts → index-eFuoMZZ3.d.mts} +18 -20
  36. package/dist/{index-ZSuiZX5H.d.ts → index-ygpml2kC.d.mts} +19 -31
  37. package/dist/index.d.mts +16 -0
  38. package/dist/index.mjs +45 -0
  39. package/dist/index.mjs.map +1 -0
  40. package/dist/ingestion/index.d.mts +2 -0
  41. package/dist/ingestion/index.mjs +2 -0
  42. package/dist/ingestion-q0WwqUSt.mjs +275 -0
  43. package/dist/ingestion-q0WwqUSt.mjs.map +1 -0
  44. package/dist/ingestor/index.d.mts +3 -0
  45. package/dist/ingestor/index.mjs +4 -0
  46. package/dist/ingestor-D4L-HPkg.mjs +26 -0
  47. package/dist/ingestor-D4L-HPkg.mjs.map +1 -0
  48. package/dist/instrumented-Bhp7fyvh.mjs +29 -0
  49. package/dist/instrumented-Bhp7fyvh.mjs.map +1 -0
  50. package/dist/manifest/index.d.mts +2 -0
  51. package/dist/manifest/{index.js → index.mjs} +2 -4
  52. package/dist/{manifest-P7kBnch9.js → manifest-BGKxmKLy.mjs} +14 -19
  53. package/dist/manifest-BGKxmKLy.mjs.map +1 -0
  54. package/dist/{telemetry-j_nxwYfh.js → metrics-BaEX__MN.mjs} +170 -19
  55. package/dist/metrics-BaEX__MN.mjs.map +1 -0
  56. package/dist/{metrics-C4V1pm6v.d.ts → metrics-CVXFL7YT.d.mts} +18 -13
  57. package/dist/metrics.d.mts +2 -0
  58. package/dist/metrics.mjs +2 -0
  59. package/dist/{chunk-Bp6m_JJh.js → rolldown-runtime-D7D4PA-g.mjs} +4 -4
  60. package/dist/runtime-config/index.d.mts +2 -0
  61. package/dist/runtime-config/index.mjs +3 -0
  62. package/dist/runtime-config-Oa55gAew.mjs +97 -0
  63. package/dist/runtime-config-Oa55gAew.mjs.map +1 -0
  64. package/dist/server-CMnxz3h0.mjs +144 -0
  65. package/dist/server-CMnxz3h0.mjs.map +1 -0
  66. package/dist/service-DLkhuxYl.d.mts +27 -0
  67. package/dist/service-jWxxfEht.mjs +7 -0
  68. package/dist/service-jWxxfEht.mjs.map +1 -0
  69. package/dist/state-store/index.d.mts +2 -0
  70. package/dist/state-store/index.mjs +2 -0
  71. package/dist/{state-store-BdYulQOY.js → state-store-uFvHY7Ax.mjs} +15 -22
  72. package/dist/state-store-uFvHY7Ax.mjs.map +1 -0
  73. package/dist/status/{index.d.ts → index.d.mts} +1 -4
  74. package/dist/status/{index.js → index.mjs} +2 -6
  75. package/dist/{status-BTTd7AUR.js → status-CKb9IhQ9.mjs} +14 -16
  76. package/dist/status-CKb9IhQ9.mjs.map +1 -0
  77. package/dist/table-V0tgUC5i.mjs +370 -0
  78. package/dist/table-V0tgUC5i.mjs.map +1 -0
  79. package/dist/types-xZtSmrGA.d.mts +220 -0
  80. package/dist/webhook/index.d.mts +3 -0
  81. package/dist/webhook/index.mjs +12 -0
  82. package/dist/webhook/index.mjs.map +1 -0
  83. package/dist/wings-I56hGtNv.mjs +325 -0
  84. package/dist/wings-I56hGtNv.mjs.map +1 -0
  85. package/package.json +64 -51
  86. package/src/catalog/table.ts +201 -0
  87. package/src/connector-app/create-table.ts +47 -0
  88. package/src/connector-app/demo.ts +42 -0
  89. package/src/connector-app/index.ts +2 -0
  90. package/src/connector-app/start.ts +1 -1
  91. package/src/core/builder.ts +15 -23
  92. package/src/core/index.ts +11 -1
  93. package/src/core/types.ts +114 -39
  94. package/src/demo/definition.ts +68 -0
  95. package/src/demo/generate.ts +167 -0
  96. package/src/demo/index.ts +3 -0
  97. package/src/demo/run.ts +142 -0
  98. package/src/iceberg/annotations.ts +62 -0
  99. package/src/iceberg/error.ts +9 -0
  100. package/src/iceberg/index.ts +3 -0
  101. package/src/iceberg/table.ts +511 -0
  102. package/src/index.ts +3 -1
  103. package/src/ingestion/engine.ts +240 -60
  104. package/src/ingestor/arrow.ts +248 -0
  105. package/src/ingestor/console.ts +13 -0
  106. package/src/ingestor/index.ts +9 -0
  107. package/src/ingestor/instrumented.ts +61 -0
  108. package/src/ingestor/service.ts +30 -0
  109. package/src/ingestor/wings.ts +143 -0
  110. package/src/manifest/config.ts +3 -3
  111. package/src/metrics.ts +66 -20
  112. package/src/runtime-config/config.ts +6 -8
  113. package/src/runtime-config/constants.ts +2 -2
  114. package/src/runtime-config/index.ts +2 -7
  115. package/src/runtime-config/layer.ts +1 -1
  116. package/src/runtime-config/table-bindings.ts +48 -136
  117. package/src/state-store/layer-key-value-store.ts +8 -3
  118. package/src/state-store/layer-memory.ts +1 -1
  119. package/src/state-store/layer-sql.ts +4 -4
  120. package/src/state-store/schema.ts +2 -2
  121. package/src/state-store/state.ts +14 -2
  122. package/src/status/route.ts +2 -2
  123. package/src/telemetry.ts +10 -10
  124. package/src/webhook/server.ts +39 -40
  125. package/dist/auth/index.d.ts +0 -2
  126. package/dist/auth/index.js +0 -3
  127. package/dist/auth-DvU_CZ7C.js.map +0 -1
  128. package/dist/connector-app/index.d.ts +0 -14
  129. package/dist/connector-app/index.js +0 -12
  130. package/dist/connector-app-BljHd0Pe.js +0 -64
  131. package/dist/connector-app-BljHd0Pe.js.map +0 -1
  132. package/dist/constants-D113Ut1x.js.map +0 -1
  133. package/dist/errors/index.d.ts +0 -2
  134. package/dist/errors/index.js +0 -3
  135. package/dist/errors-DHWylqRZ.js.map +0 -1
  136. package/dist/index-BA8ASHKr.d.ts +0 -31
  137. package/dist/index.d.ts +0 -14
  138. package/dist/index.js +0 -60
  139. package/dist/index.js.map +0 -1
  140. package/dist/ingestion/index.d.ts +0 -6
  141. package/dist/ingestion/index.js +0 -11
  142. package/dist/ingestion-QBGKrFr9.js +0 -200
  143. package/dist/ingestion-QBGKrFr9.js.map +0 -1
  144. package/dist/manifest/index.d.ts +0 -2
  145. package/dist/manifest-P7kBnch9.js.map +0 -1
  146. package/dist/metrics-inkCAxKq.js +0 -136
  147. package/dist/metrics-inkCAxKq.js.map +0 -1
  148. package/dist/metrics.d.ts +0 -4
  149. package/dist/metrics.js +0 -5
  150. package/dist/publisher/index.d.ts +0 -6
  151. package/dist/publisher/index.js +0 -8
  152. package/dist/publisher-BdWJOfer.js +0 -138
  153. package/dist/publisher-BdWJOfer.js.map +0 -1
  154. package/dist/runtime-config/index.d.ts +0 -4
  155. package/dist/runtime-config/index.js +0 -5
  156. package/dist/runtime-config-IbyCVpei.js +0 -60
  157. package/dist/runtime-config-IbyCVpei.js.map +0 -1
  158. package/dist/server-D-eLX3yN.js +0 -183
  159. package/dist/server-D-eLX3yN.js.map +0 -1
  160. package/dist/service-CUjGhe-7.d.ts +0 -32
  161. package/dist/service-DiP-XETo.js +0 -8
  162. package/dist/service-DiP-XETo.js.map +0 -1
  163. package/dist/state-store/index.d.ts +0 -3
  164. package/dist/state-store/index.js +0 -5
  165. package/dist/state-store-BdYulQOY.js.map +0 -1
  166. package/dist/status-BTTd7AUR.js.map +0 -1
  167. package/dist/table-bindings-Dp1DhTeS.js +0 -106
  168. package/dist/table-bindings-Dp1DhTeS.js.map +0 -1
  169. package/dist/telemetry-j_nxwYfh.js.map +0 -1
  170. package/dist/types-jVD5Ys43.d.ts +0 -174
  171. package/dist/webhook/index.d.ts +0 -7
  172. package/dist/webhook/index.js +0 -9
  173. package/dist/webhook-MhFcLW-o.js +0 -13
  174. package/dist/webhook-MhFcLW-o.js.map +0 -1
  175. package/src/publisher/console.ts +0 -18
  176. package/src/publisher/index.ts +0 -7
  177. package/src/publisher/instrumented.ts +0 -75
  178. package/src/publisher/service.ts +0 -37
  179. package/src/publisher/wings.ts +0 -204
@@ -0,0 +1,68 @@
1
+ import type { Schema } from "effect";
2
+
3
+ import type { ResourceRequiredField, ResourceSchema } from "../core/types";
4
+ import type { DemoContext } from "./generate";
5
+
6
+ /** A table a demo writes: the same name, row schema and version as its resource. */
7
+ export type DemoTable<
8
+ Name extends string = string,
9
+ S extends ResourceSchema = ResourceSchema,
10
+ Version extends string = string,
11
+ > = {
12
+ readonly name: Name;
13
+ readonly rowSchema: S;
14
+ readonly version: Version;
15
+ };
16
+
17
+ export const table = <
18
+ const Name extends string,
19
+ S extends ResourceSchema,
20
+ const Version extends ResourceRequiredField<S>,
21
+ >(
22
+ definition: DemoTable<Name, S, Version>,
23
+ ): DemoTable<Name, S, Version> => definition;
24
+
25
+ /** Rows by table. Required fields a row leaves out get placeholder values. */
26
+ export type DemoRows<Tables extends ReadonlyArray<DemoTable>> = {
27
+ readonly [Table in Tables[number] as Table["name"]]?: ReadonlyArray<
28
+ Partial<Schema.Schema.Type<Table["rowSchema"]>>
29
+ >;
30
+ };
31
+
32
+ export type DemoDefinition<
33
+ Setup extends ReadonlyArray<DemoTable> = ReadonlyArray<DemoTable>,
34
+ Group extends ReadonlyArray<DemoTable> = ReadonlyArray<DemoTable>,
35
+ > = {
36
+ readonly name: string;
37
+ /** Tables written once, before any group, such as a product catalog. */
38
+ readonly setup: {
39
+ readonly tables: Setup;
40
+ readonly rows: (context: DemoContext) => DemoRows<Setup>;
41
+ };
42
+ /**
43
+ * Tables for one related unit, such as a customer and their orders. The run
44
+ * adds groups until it reaches the size. Build keys from the index, so they
45
+ * are unique and other groups can point to them.
46
+ */
47
+ readonly group: {
48
+ readonly tables: Group;
49
+ readonly rows: (index: number, context: DemoContext) => DemoRows<Group>;
50
+ /**
51
+ * Groups written per chunk, 100 by default. A run can go over the size by
52
+ * one chunk, so lower it when a group is big.
53
+ */
54
+ readonly groupsPerChunk?: number;
55
+ /**
56
+ * The number of groups there are, when the provider only keeps so much
57
+ * history. The run stops there, even below the size.
58
+ */
59
+ readonly count?: number;
60
+ };
61
+ };
62
+
63
+ export const define = <
64
+ const Setup extends ReadonlyArray<DemoTable>,
65
+ const Group extends ReadonlyArray<DemoTable>,
66
+ >(
67
+ definition: DemoDefinition<Setup, Group>,
68
+ ): DemoDefinition<Setup, Group> => definition;
@@ -0,0 +1,167 @@
1
+ import type { Faker } from "@faker-js/faker";
2
+
3
+ import { Duration, Effect, Schema, type SchemaRepresentation } from "effect";
4
+
5
+ import { ConnectorError } from "../errors";
6
+ import { checkHasRepresentation } from "../iceberg/table";
7
+
8
+ /** What one run shares: the seeded Faker and the time the run started. */
9
+ export type DemoContext = {
10
+ readonly faker: Faker;
11
+ readonly now: Date;
12
+ };
13
+
14
+ /** Completes a row: keeps what it has, and fills the required fields it lacks. */
15
+ export type RowFiller = (context: DemoContext, row: object) => Record<string, unknown>;
16
+
17
+ type Representation = SchemaRepresentation.Representation;
18
+ type Value = (context: DemoContext) => unknown;
19
+
20
+ const twoYears = Duration.toMillis(Duration.days(730));
21
+
22
+ class Unsupported extends Error {}
23
+
24
+ const unsupported = (path: string, what: string): never => {
25
+ throw new Unsupported(`${path}: ${what} is not supported in demo data`);
26
+ };
27
+
28
+ /**
29
+ * Compiles a row schema into a filler for demo rows. It reads the same
30
+ * representation as `compileTable`, so values fit the Iceberg types. It fills
31
+ * only required fields the row lacks, so optional ones such as `_deleted` stay
32
+ * out.
33
+ */
34
+ export const compileRow = (schema: Schema.Top): Effect.Effect<RowFiller, ConnectorError> =>
35
+ Effect.try({
36
+ try: () => {
37
+ const { representation, references } = Schema.toRepresentation(Schema.toType(schema));
38
+
39
+ const resolve = (node: Representation, path: string): Representation => {
40
+ if (node._tag !== "Reference") return node;
41
+ const target = references[node.$ref];
42
+
43
+ return target === undefined ? unsupported(path, `reference ${node.$ref}`) : target;
44
+ };
45
+
46
+ const requiredFields = (node: Representation, path: string) => {
47
+ const resolved = resolve(node, path);
48
+
49
+ if (resolved._tag !== "Objects" || resolved.indexSignatures.length > 0) {
50
+ return unsupported(path, resolved._tag);
51
+ }
52
+
53
+ return resolved.propertySignatures
54
+ .filter((property) => !property.isOptional)
55
+ .map((property) => {
56
+ const name = String(property.name);
57
+ return [name, value(property.type, `${path}.${name}`)] as const;
58
+ });
59
+ };
60
+
61
+ const object = (node: Representation, path: string): Value => {
62
+ const fields = requiredFields(node, path);
63
+ return (context) => Object.fromEntries(fields.map(([name, make]) => [name, make(context)]));
64
+ };
65
+
66
+ const value = (node: Representation, path: string): Value => {
67
+ const resolved = resolve(node, path);
68
+
69
+ switch (resolved._tag) {
70
+ case "String":
71
+ return ({ faker }) => faker.word.words({ count: { min: 1, max: 3 } });
72
+ case "Boolean":
73
+ return ({ faker }) => faker.datatype.boolean();
74
+ case "BigInt":
75
+ return ({ faker }) => BigInt(faker.number.int({ max: 100_000 }));
76
+ case "Number":
77
+ return checkHasRepresentation(resolved.checks, "effect/schema/isInt")
78
+ ? ({ faker }) => faker.number.int({ max: 1000 })
79
+ : ({ faker }) => faker.number.float({ max: 1000, fractionDigits: 2 });
80
+ case "Literal": {
81
+ const literal = resolved.literal;
82
+ return () => literal;
83
+ }
84
+ case "Union": {
85
+ // Fill every field, so nullable unions get their value type.
86
+ const members = resolved.types.filter(
87
+ (member) => member._tag !== "Null" && member._tag !== "Undefined",
88
+ );
89
+
90
+ const literals = members.flatMap((member) =>
91
+ member._tag === "Literal" ? [member.literal] : [],
92
+ );
93
+
94
+ if (literals.length > 0 && literals.length === members.length) {
95
+ return ({ faker }) => faker.helpers.arrayElement(literals);
96
+ }
97
+
98
+ const [only] = members;
99
+
100
+ return only !== undefined && members.length === 1
101
+ ? value(only, path)
102
+ : unsupported(path, "a union of several types");
103
+ }
104
+ case "Objects":
105
+ return object(resolved, path);
106
+ case "Arrays": {
107
+ const [element] = resolved.rest;
108
+
109
+ if (resolved.elements.length > 0 || resolved.rest.length !== 1 || !element) {
110
+ return unsupported(path, "a tuple");
111
+ }
112
+
113
+ const item = value(element, `${path}[]`);
114
+
115
+ return (context) =>
116
+ Array.from({ length: context.faker.number.int({ max: 3 }) }, () => item(context));
117
+ }
118
+ case "Declaration":
119
+ return declaration(resolved, path);
120
+ case "Suspend":
121
+ return value(resolved.thunk, path);
122
+ default:
123
+ return unsupported(path, resolved._tag);
124
+ }
125
+ };
126
+
127
+ const declaration = (node: SchemaRepresentation.Declaration, path: string): Value => {
128
+ switch (node.representation?.id) {
129
+ case "effect/schema/Date":
130
+ return ({ faker, now }) =>
131
+ faker.date.between({ from: now.getTime() - twoYears, to: now });
132
+ case "effect/schema/Uint8Array":
133
+ return ({ faker }) => Uint8Array.from({ length: 8 }, () => faker.number.int(255));
134
+ case "effect/schema/ReadonlyMap": {
135
+ const [keyNode, valueNode] = node.typeParameters;
136
+ if (!keyNode || !valueNode) return unsupported(path, "a map without key or value");
137
+ const key = value(keyNode, `${path}.key`);
138
+ const item = value(valueNode, `${path}.value`);
139
+
140
+ return (context) =>
141
+ new Map(
142
+ Array.from({ length: context.faker.number.int({ max: 3 }) }, () => [
143
+ key(context),
144
+ item(context),
145
+ ]),
146
+ );
147
+ }
148
+ default:
149
+ return unsupported(path, node.representation?.id ?? "an unnamed declaration");
150
+ }
151
+ };
152
+
153
+ const fields = requiredFields(representation, "$");
154
+
155
+ return (context, row) => ({
156
+ ...Object.fromEntries(
157
+ fields.filter(([name]) => !(name in row)).map(([name, make]) => [name, make(context)]),
158
+ ),
159
+ ...row,
160
+ });
161
+ },
162
+ catch: (cause) =>
163
+ new ConnectorError({
164
+ message: cause instanceof Unsupported ? cause.message : "Failed to compile demo rows",
165
+ cause,
166
+ }),
167
+ });
@@ -0,0 +1,3 @@
1
+ export { type DemoContext } from "./generate";
2
+ export { define, table, type DemoDefinition, type DemoRows, type DemoTable } from "./definition";
3
+ export { run, type DemoTotals, type RunOptions } from "./run";
@@ -0,0 +1,142 @@
1
+ import { Faker, base, en } from "@faker-js/faker";
2
+ import { Array as Arr, Effect, Option, Schema, Stream } from "effect";
3
+
4
+ import type { DemoDefinition, DemoTable } from "./definition";
5
+
6
+ import { ConnectorError } from "../errors";
7
+ import { ingestBatch } from "../ingestor/instrumented";
8
+ import { Ingestor, type IngestResult } from "../ingestor/service";
9
+ import { compileRow, type DemoContext } from "./generate";
10
+
11
+ export type RunOptions = {
12
+ /** Stop once this many Arrow bytes are written. */
13
+ readonly bytes: number;
14
+ readonly seed: number;
15
+ /** The time data is made around. Same seed, time and tables give the same rows. */
16
+ readonly now: Date;
17
+ /** Groups per chunk. Overrides the demo's. */
18
+ readonly groupsPerChunk?: number;
19
+ };
20
+
21
+ export type DemoTotals = IngestResult & {
22
+ readonly tables: Readonly<Record<string, IngestResult>>;
23
+ };
24
+
25
+ const empty: DemoTotals = { rows: 0, bytes: 0, tables: {} };
26
+
27
+ // Wings rejects Flight messages over 4 MiB. 500 rows keeps a batch of the
28
+ // widest demo rows well under that.
29
+ const batchRows = 500;
30
+
31
+ const sum = (results: ReadonlyArray<IngestResult>): IngestResult => ({
32
+ rows: results.reduce((total, result) => total + result.rows, 0),
33
+ bytes: results.reduce((total, result) => total + result.bytes, 0),
34
+ });
35
+
36
+ const add = (totals: DemoTotals, written: ReadonlyArray<readonly [string, IngestResult]>) =>
37
+ written.reduce<DemoTotals>(
38
+ (current, [name, result]) => ({
39
+ rows: current.rows + result.rows,
40
+ bytes: current.bytes + result.bytes,
41
+ tables: {
42
+ ...current.tables,
43
+ [name]: {
44
+ rows: (current.tables[name]?.rows ?? 0) + result.rows,
45
+ bytes: (current.tables[name]?.bytes ?? 0) + result.bytes,
46
+ },
47
+ },
48
+ }),
49
+ totals,
50
+ );
51
+
52
+ /**
53
+ * Writes the setup rows, then groups, until it reaches the size or the demo's
54
+ * count. It can go over the size by one chunk. Only bound tables are
55
+ * written; keys still point to the others.
56
+ */
57
+ export const run = Effect.fnUntraced(function* (demo: DemoDefinition, options: RunOptions) {
58
+ const ingestor = yield* Ingestor;
59
+
60
+ const bound = (table: DemoTable) =>
61
+ ingestor.resourceNames === undefined || ingestor.resourceNames.includes(table.name);
62
+
63
+ // A fresh Faker per run, so its sequence depends only on the seed.
64
+ const faker = new Faker({ locale: [en, base] });
65
+ faker.seed(options.seed);
66
+ const context: DemoContext = { faker, now: options.now };
67
+
68
+ const prepare = (tables: ReadonlyArray<DemoTable>) =>
69
+ Effect.forEach(tables.filter(bound), (table) =>
70
+ compileRow(table.rowSchema).pipe(
71
+ Effect.map((fill) => ({
72
+ table,
73
+ fill,
74
+ decode: Schema.decodeUnknownEffect(Schema.toType(table.rowSchema)),
75
+ })),
76
+ ),
77
+ );
78
+
79
+ const write = (
80
+ tables: Effect.Success<ReturnType<typeof prepare>>,
81
+ rowsOf: (name: string) => ReadonlyArray<object>,
82
+ ) =>
83
+ Effect.forEach(tables, ({ table, fill, decode }) =>
84
+ Effect.forEach(rowsOf(table.name), (row) =>
85
+ decode(fill(context, row)).pipe(
86
+ Effect.mapError(
87
+ (cause) => new ConnectorError({ message: `Invalid demo row for ${table.name}`, cause }),
88
+ ),
89
+ ),
90
+ ).pipe(
91
+ Effect.flatMap((rows) =>
92
+ Effect.forEach(Arr.chunksOf(rows, batchRows), (batch) =>
93
+ ingestBatch({
94
+ connector: demo.name,
95
+ resource: table.name,
96
+ source: "demo",
97
+ batch: { rows: batch },
98
+ }),
99
+ ),
100
+ ),
101
+ Effect.map((results) => [table.name, sum(results)] as const),
102
+ ),
103
+ );
104
+
105
+ const setupTables = yield* prepare(demo.setup.tables);
106
+ const groupTables = yield* prepare(demo.group.tables);
107
+
108
+ const setup = demo.setup.rows(context);
109
+ const afterSetup = add(empty, yield* write(setupTables, (name) => setup[name] ?? []));
110
+
111
+ const totals =
112
+ afterSetup.bytes >= options.bytes || groupTables.length === 0
113
+ ? afterSetup
114
+ : yield* Stream.range(0, (demo.group.count ?? Infinity) - 1).pipe(
115
+ Stream.grouped(options.groupsPerChunk ?? demo.group.groupsPerChunk ?? 100),
116
+ Stream.mapEffect((indexes) => {
117
+ const groups = indexes.map((index) => demo.group.rows(index, context));
118
+ return write(groupTables, (name) => groups.flatMap((group) => group[name] ?? []));
119
+ }),
120
+ Stream.scan(() => afterSetup, add),
121
+ Stream.takeUntil((current) => current.bytes >= options.bytes),
122
+ Stream.tap((current) =>
123
+ Effect.logInfo("demo progress").pipe(
124
+ Effect.annotateLogs({ rows: current.rows, bytes: current.bytes }),
125
+ ),
126
+ ),
127
+ Stream.runLast,
128
+ Effect.map(Option.getOrElse(() => afterSetup)),
129
+ );
130
+
131
+ if (totals.bytes < options.bytes) {
132
+ yield* Effect.logWarning(
133
+ "The selected tables ran out of rows, so the run wrote less than the size asked for",
134
+ ).pipe(Effect.annotateLogs({ bytes: totals.bytes, target: options.bytes }));
135
+ }
136
+
137
+ yield* Effect.logInfo("demo done").pipe(
138
+ Effect.annotateLogs({ rows: totals.rows, bytes: totals.bytes, tables: totals.tables }),
139
+ );
140
+
141
+ return totals;
142
+ });
@@ -0,0 +1,62 @@
1
+ import { Schema } from "effect";
2
+
3
+ declare module "effect/Schema" {
4
+ namespace Annotations {
5
+ interface Annotations {
6
+ readonly fieldId?: number | undefined;
7
+ readonly icebergIdentifierFields?: ReadonlyArray<string> | undefined;
8
+ }
9
+ }
10
+ }
11
+
12
+ /** Sets a field's Iceberg ID, and any other annotations. */
13
+ export const field =
14
+ <A = unknown>(fieldId: number, annotations: Schema.Annotations.Key<A> = {}) =>
15
+ <S extends Schema.Schema<A>>(schema: S): S["Rebuild"] =>
16
+ schema.pipe(Schema.annotateKey({ ...annotations, fieldId }));
17
+
18
+ /** Adds an offset to a base ID for reusable schemas. */
19
+ export const ids =
20
+ (baseId: number) =>
21
+ (offset: number): number =>
22
+ baseId + offset;
23
+
24
+ type KnownField<S extends Schema.Top> = keyof S["Type"] & string;
25
+
26
+ /** A row field that cannot be null or undefined. */
27
+ export type RequiredField<S extends Schema.Top> = [KnownField<S>] extends [never]
28
+ ? string
29
+ : {
30
+ readonly [Field in KnownField<S>]-?: undefined extends S["Type"][Field]
31
+ ? never
32
+ : null extends S["Type"][Field]
33
+ ? never
34
+ : Field;
35
+ }[KnownField<S>];
36
+
37
+ /** A row schema whose identifier fields are `Key`. Only the type has it. */
38
+ export interface Identified<Key extends string> {
39
+ readonly "~iceberg.identifier": Key;
40
+ }
41
+
42
+ /**
43
+ * Marks the fields that identify a row. Wings upserts rows by them.
44
+ *
45
+ * Put it last. A later `.annotate()` keeps the runtime annotation, but the type
46
+ * loses the key, so `Resource.entity` stops compiling.
47
+ *
48
+ * @example
49
+ * ```ts
50
+ * const Post = Schema.Struct({
51
+ * id: Schema.String.pipe(Iceberg.field(1)),
52
+ * updatedAt: Schema.String.pipe(Iceberg.field(2)),
53
+ * }).pipe(Iceberg.identifier("id"));
54
+ * ```
55
+ */
56
+ export const identifier =
57
+ <S extends Schema.Top, const Key extends RequiredField<S>>(
58
+ ...keys: readonly [Key, ...ReadonlyArray<Key>]
59
+ ) =>
60
+ (schema: S): S["Rebuild"] & Identified<Key> =>
61
+ // Effect has no slot for the key in the schema type, so we add it.
62
+ schema.annotate({ icebergIdentifierFields: keys }) as S["Rebuild"] & Identified<Key>;
@@ -0,0 +1,9 @@
1
+ import { Data } from "effect";
2
+
3
+ /** A schema could not be turned into an Iceberg table. */
4
+ export class IcebergSchemaError extends Data.TaggedError("IcebergSchemaError")<{
5
+ /** Schema path, such as `$.orders.element`. */
6
+ readonly path: string;
7
+ readonly message: string;
8
+ readonly cause?: unknown;
9
+ }> {}
@@ -0,0 +1,3 @@
1
+ export { field, identifier, ids } from "./annotations";
2
+ export { IcebergSchemaError } from "./error";
3
+ export { makeCreateTableCommitRequest, type CreateTableCommitOptions } from "./table";