@useairfoil/connector-kit 0.6.0 → 0.7.0
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/LICENSE.txt +201 -0
- package/METRICS.md +38 -42
- package/README.md +111 -444
- package/dist/annotations-vzSzXtr-.d.mts +34 -0
- package/dist/arrow-flight-DDQ0ktx9.mjs +251 -0
- package/dist/arrow-flight-DDQ0ktx9.mjs.map +1 -0
- package/dist/auth/index.d.mts +2 -0
- package/dist/{auth-DvU_CZ7C.js → auth/index.mjs} +6 -8
- package/dist/auth/index.mjs.map +1 -0
- package/dist/connector-app/index.d.mts +2 -0
- package/dist/connector-app/index.mjs +2 -0
- package/dist/connector-app-C6_dZnhE.mjs +256 -0
- package/dist/connector-app-C6_dZnhE.mjs.map +1 -0
- package/dist/{constants-D113Ut1x.js → constants-v3UX-hf6.mjs} +5 -5
- package/dist/constants-v3UX-hf6.mjs.map +1 -0
- package/dist/demo/index.d.mts +2 -0
- package/dist/demo/index.mjs +2 -0
- package/dist/demo-DqTJ7TWI.mjs +204 -0
- package/dist/demo-DqTJ7TWI.mjs.map +1 -0
- package/dist/errors/index.d.mts +2 -0
- package/dist/errors/index.mjs +2 -0
- package/dist/{errors-DHWylqRZ.js → errors-pT0YVJU5.mjs} +2 -3
- package/dist/errors-pT0YVJU5.mjs.map +1 -0
- package/dist/iceberg/index.d.mts +3 -0
- package/dist/iceberg/index.mjs +3 -0
- package/dist/iceberg-Cj2PwlNm.mjs +30 -0
- package/dist/iceberg-Cj2PwlNm.mjs.map +1 -0
- package/dist/index-BTAbexBp.d.mts +55 -0
- package/dist/{index-DXa26Ry5.d.ts → index-BX8G7d41.d.mts} +1 -2
- package/dist/{index-VpHaZXZw.d.ts → index-Byr8NTdG.d.mts} +11 -13
- package/dist/index-CV6V6fyx.d.mts +79 -0
- package/dist/{index-C5Ma7V7t.d.ts → index-CZKx2Yce.d.mts} +72 -37
- package/dist/{index-DYpEMJf-.d.ts → index-CgvFomO6.d.mts} +6 -7
- package/dist/{index-bfjH7Yb0.d.ts → index-DeTwIUKl.d.mts} +9 -10
- package/dist/{index-DKoRwyFn.d.ts → index-DkVNO6ks.d.mts} +2 -5
- package/dist/{index-BKw7jA2a.d.ts → index-dHS59EZl.d.mts} +10 -8
- package/dist/{index-CR9HJDdr.d.ts → index-eFuoMZZ3.d.mts} +18 -20
- package/dist/{index-ZSuiZX5H.d.ts → index-fHIJJoRe.d.mts} +25 -30
- package/dist/index-uFa2wwYo.d.mts +27 -0
- package/dist/index.d.mts +16 -0
- package/dist/index.mjs +45 -0
- package/dist/index.mjs.map +1 -0
- package/dist/ingestion/index.d.mts +2 -0
- package/dist/ingestion/index.mjs +2 -0
- package/dist/ingestion-DoCUwm5N.mjs +275 -0
- package/dist/ingestion-DoCUwm5N.mjs.map +1 -0
- package/dist/ingestor/index.d.mts +3 -0
- package/dist/ingestor/index.mjs +4 -0
- package/dist/ingestor-DjCEzBzZ.mjs +26 -0
- package/dist/ingestor-DjCEzBzZ.mjs.map +1 -0
- package/dist/instrumented-Bq1WX2L5.mjs +29 -0
- package/dist/instrumented-Bq1WX2L5.mjs.map +1 -0
- package/dist/manifest/index.d.mts +2 -0
- package/dist/manifest/{index.js → index.mjs} +2 -4
- package/dist/{manifest-P7kBnch9.js → manifest-CABeIhAG.mjs} +14 -19
- package/dist/manifest-CABeIhAG.mjs.map +1 -0
- package/dist/{telemetry-j_nxwYfh.js → metrics-C901IIYZ.mjs} +172 -26
- package/dist/metrics-C901IIYZ.mjs.map +1 -0
- package/dist/{metrics-C4V1pm6v.d.ts → metrics-DoJ0WeKu.d.mts} +18 -13
- package/dist/metrics.d.mts +2 -0
- package/dist/metrics.mjs +2 -0
- package/dist/{chunk-Bp6m_JJh.js → rolldown-runtime-D7D4PA-g.mjs} +4 -4
- package/dist/runtime-config/index.d.mts +2 -0
- package/dist/runtime-config/index.mjs +3 -0
- package/dist/runtime-config-Bn8Fivt1.mjs +100 -0
- package/dist/runtime-config-Bn8Fivt1.mjs.map +1 -0
- package/dist/server-D1TTN0_D.mjs +144 -0
- package/dist/server-D1TTN0_D.mjs.map +1 -0
- package/dist/service-BRoy_EId.d.mts +27 -0
- package/dist/service-jWxxfEht.mjs +7 -0
- package/dist/service-jWxxfEht.mjs.map +1 -0
- package/dist/state-store/index.d.mts +2 -0
- package/dist/state-store/index.mjs +2 -0
- package/dist/{state-store-BdYulQOY.js → state-store-DhSrKVEH.mjs} +15 -22
- package/dist/state-store-DhSrKVEH.mjs.map +1 -0
- package/dist/status/{index.d.ts → index.d.mts} +1 -4
- package/dist/status/{index.js → index.mjs} +2 -6
- package/dist/{status-BTTd7AUR.js → status-4h6Oxh-O.mjs} +14 -16
- package/dist/status-4h6Oxh-O.mjs.map +1 -0
- package/dist/table-CxM09xf7.mjs +307 -0
- package/dist/table-CxM09xf7.mjs.map +1 -0
- package/dist/types-NtSv6gwl.d.mts +220 -0
- package/dist/webhook/index.d.mts +3 -0
- package/dist/webhook/index.mjs +12 -0
- package/dist/webhook/index.mjs.map +1 -0
- package/package.json +72 -58
- package/src/catalog/retry.ts +25 -0
- package/src/catalog/table.ts +237 -0
- package/src/connector-app/create-table.ts +47 -0
- package/src/connector-app/demo.ts +47 -0
- package/src/connector-app/index.ts +3 -0
- package/src/connector-app/setup.ts +69 -0
- package/src/connector-app/start.ts +1 -1
- package/src/core/builder.ts +15 -23
- package/src/core/index.ts +11 -1
- package/src/core/types.ts +114 -39
- package/src/demo/definition.ts +68 -0
- package/src/demo/generate.ts +167 -0
- package/src/demo/index.ts +3 -0
- package/src/demo/run.ts +172 -0
- package/src/iceberg/annotations.ts +49 -0
- package/src/iceberg/error.ts +9 -0
- package/src/iceberg/index.ts +3 -0
- package/src/iceberg/table.ts +461 -0
- package/src/index.ts +3 -1
- package/src/ingestion/engine.ts +240 -60
- package/src/ingestor/arrow-flight.ts +141 -0
- package/src/ingestor/arrow.ts +248 -0
- package/src/ingestor/console.ts +13 -0
- package/src/ingestor/index.ts +13 -0
- package/src/ingestor/instrumented.ts +61 -0
- package/src/ingestor/service.ts +30 -0
- package/src/manifest/config.ts +3 -3
- package/src/metrics.ts +66 -20
- package/src/runtime-config/config.ts +11 -8
- package/src/runtime-config/constants.ts +3 -2
- package/src/runtime-config/index.ts +2 -7
- package/src/runtime-config/layer.ts +1 -1
- package/src/runtime-config/table-bindings.ts +50 -136
- package/src/state-store/layer-key-value-store.ts +8 -3
- package/src/state-store/layer-memory.ts +1 -1
- package/src/state-store/layer-sql.ts +4 -4
- package/src/state-store/schema.ts +2 -2
- package/src/state-store/state.ts +14 -2
- package/src/status/route.ts +2 -2
- package/src/telemetry.ts +20 -28
- package/src/webhook/server.ts +39 -40
- package/dist/auth/index.d.ts +0 -2
- package/dist/auth/index.js +0 -3
- package/dist/auth-DvU_CZ7C.js.map +0 -1
- package/dist/connector-app/index.d.ts +0 -14
- package/dist/connector-app/index.js +0 -12
- package/dist/connector-app-BljHd0Pe.js +0 -64
- package/dist/connector-app-BljHd0Pe.js.map +0 -1
- package/dist/constants-D113Ut1x.js.map +0 -1
- package/dist/errors/index.d.ts +0 -2
- package/dist/errors/index.js +0 -3
- package/dist/errors-DHWylqRZ.js.map +0 -1
- package/dist/index-BA8ASHKr.d.ts +0 -31
- package/dist/index.d.ts +0 -14
- package/dist/index.js +0 -60
- package/dist/index.js.map +0 -1
- package/dist/ingestion/index.d.ts +0 -6
- package/dist/ingestion/index.js +0 -11
- package/dist/ingestion-QBGKrFr9.js +0 -200
- package/dist/ingestion-QBGKrFr9.js.map +0 -1
- package/dist/manifest/index.d.ts +0 -2
- package/dist/manifest-P7kBnch9.js.map +0 -1
- package/dist/metrics-inkCAxKq.js +0 -136
- package/dist/metrics-inkCAxKq.js.map +0 -1
- package/dist/metrics.d.ts +0 -4
- package/dist/metrics.js +0 -5
- package/dist/publisher/index.d.ts +0 -6
- package/dist/publisher/index.js +0 -8
- package/dist/publisher-BdWJOfer.js +0 -138
- package/dist/publisher-BdWJOfer.js.map +0 -1
- package/dist/runtime-config/index.d.ts +0 -4
- package/dist/runtime-config/index.js +0 -5
- package/dist/runtime-config-IbyCVpei.js +0 -60
- package/dist/runtime-config-IbyCVpei.js.map +0 -1
- package/dist/server-D-eLX3yN.js +0 -183
- package/dist/server-D-eLX3yN.js.map +0 -1
- package/dist/service-CUjGhe-7.d.ts +0 -32
- package/dist/service-DiP-XETo.js +0 -8
- package/dist/service-DiP-XETo.js.map +0 -1
- package/dist/state-store/index.d.ts +0 -3
- package/dist/state-store/index.js +0 -5
- package/dist/state-store-BdYulQOY.js.map +0 -1
- package/dist/status-BTTd7AUR.js.map +0 -1
- package/dist/table-bindings-Dp1DhTeS.js +0 -106
- package/dist/table-bindings-Dp1DhTeS.js.map +0 -1
- package/dist/telemetry-j_nxwYfh.js.map +0 -1
- package/dist/types-jVD5Ys43.d.ts +0 -174
- package/dist/webhook/index.d.ts +0 -7
- package/dist/webhook/index.js +0 -9
- package/dist/webhook-MhFcLW-o.js +0 -13
- package/dist/webhook-MhFcLW-o.js.map +0 -1
- package/src/publisher/console.ts +0 -18
- package/src/publisher/index.ts +0 -7
- package/src/publisher/instrumented.ts +0 -75
- package/src/publisher/service.ts +0 -37
- package/src/publisher/wings.ts +0 -204
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Bool,
|
|
3
|
+
DateDay,
|
|
4
|
+
Decimal,
|
|
5
|
+
Field,
|
|
6
|
+
FixedSizeBinary,
|
|
7
|
+
Float32,
|
|
8
|
+
Float64,
|
|
9
|
+
Int32,
|
|
10
|
+
Int64,
|
|
11
|
+
LargeBinary,
|
|
12
|
+
List,
|
|
13
|
+
Map_,
|
|
14
|
+
RecordBatch,
|
|
15
|
+
Schema as ArrowSchema,
|
|
16
|
+
Struct,
|
|
17
|
+
TimeMicrosecond,
|
|
18
|
+
TimestampMicrosecond,
|
|
19
|
+
Utf8,
|
|
20
|
+
makeBuilder,
|
|
21
|
+
type DataType,
|
|
22
|
+
type TypeMap,
|
|
23
|
+
} from "apache-arrow";
|
|
24
|
+
import { Effect, Schema } from "effect";
|
|
25
|
+
import {
|
|
26
|
+
parseDecimalType,
|
|
27
|
+
parseFixedType,
|
|
28
|
+
type IcebergType,
|
|
29
|
+
type PrimitiveType,
|
|
30
|
+
type TableSchema,
|
|
31
|
+
} from "iceberg-js";
|
|
32
|
+
|
|
33
|
+
import { ConnectorError } from "../errors";
|
|
34
|
+
|
|
35
|
+
// Nothing converts an Iceberg schema to Arrow for us, so we do it here.
|
|
36
|
+
// Each type also gets a schema for its values, because Arrow does not reject a bad
|
|
37
|
+
// value, it converts it: "1" becomes 1, and 1.5 becomes 1.
|
|
38
|
+
|
|
39
|
+
type RowSchema = Schema.Codec<unknown, unknown>;
|
|
40
|
+
|
|
41
|
+
/** One Iceberg type: the Arrow type, and the values it accepts. */
|
|
42
|
+
type Compiled = {
|
|
43
|
+
readonly type: DataType;
|
|
44
|
+
readonly row: RowSchema;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Arrow pads or cuts binary values to fit.
|
|
48
|
+
const bytes = (length: number) =>
|
|
49
|
+
Schema.Uint8Array.check(
|
|
50
|
+
Schema.makeFilter(
|
|
51
|
+
(value: Uint8Array) => value.byteLength === length || `expected ${length} bytes`,
|
|
52
|
+
),
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
// Arrow wraps big integers instead of failing, so check the range.
|
|
56
|
+
const int32 = Schema.Number.check(
|
|
57
|
+
Schema.isInt(),
|
|
58
|
+
Schema.isBetween({ minimum: -2_147_483_648, maximum: 2_147_483_647 }),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const int64 = Schema.BigInt.check(
|
|
62
|
+
Schema.isBetweenBigInt({ minimum: -(2n ** 63n), maximum: 2n ** 63n - 1n }),
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// Arrow turns a number outside the 32-bit range into Infinity.
|
|
66
|
+
const float32 = Schema.Finite.check(
|
|
67
|
+
Schema.makeFilter(
|
|
68
|
+
(value: number) => Number.isFinite(Math.fround(value)) || "expected a 32-bit number",
|
|
69
|
+
),
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const microsecondsInDay = Schema.BigInt.check(
|
|
73
|
+
Schema.isBetweenBigInt({ minimum: 0n, maximum: 86_399_999_999n }),
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const primitives: Record<string, Compiled> = {
|
|
77
|
+
boolean: { type: new Bool(), row: Schema.Boolean },
|
|
78
|
+
int: { type: new Int32(), row: int32 },
|
|
79
|
+
long: { type: new Int64(), row: int64 },
|
|
80
|
+
float: { type: new Float32(), row: float32 },
|
|
81
|
+
double: { type: new Float64(), row: Schema.Finite },
|
|
82
|
+
string: { type: new Utf8(), row: Schema.String },
|
|
83
|
+
uuid: { type: new FixedSizeBinary(16), row: bytes(16) },
|
|
84
|
+
binary: { type: new LargeBinary(), row: Schema.Uint8Array },
|
|
85
|
+
date: { type: new DateDay(), row: Schema.Date },
|
|
86
|
+
time: { type: new TimeMicrosecond(), row: microsecondsInDay },
|
|
87
|
+
timestamp: { type: new TimestampMicrosecond(), row: Schema.Date },
|
|
88
|
+
timestamptz: { type: new TimestampMicrosecond("UTC"), row: Schema.Date },
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const primitive = (type: PrimitiveType): Compiled => {
|
|
92
|
+
const known = primitives[type];
|
|
93
|
+
|
|
94
|
+
if (known) return known;
|
|
95
|
+
|
|
96
|
+
// Nothing checks the catalog metadata, and Arrow takes any precision.
|
|
97
|
+
// Better to fail here than at write time.
|
|
98
|
+
const decimal = parseDecimalType(type);
|
|
99
|
+
|
|
100
|
+
if (decimal) {
|
|
101
|
+
if (decimal.precision < 1 || decimal.precision > 38 || decimal.scale > decimal.precision) {
|
|
102
|
+
throw new Error(`Invalid Iceberg decimal type ${type}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
type: new Decimal(decimal.scale, decimal.precision),
|
|
107
|
+
row: Schema.instanceOf(Uint32Array).check(
|
|
108
|
+
Schema.makeFilter((value: Uint32Array) => value.length === 4 || "expected four words"),
|
|
109
|
+
),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const fixed = parseFixedType(type);
|
|
114
|
+
|
|
115
|
+
if (fixed) {
|
|
116
|
+
if (fixed.length < 1) throw new Error(`Invalid Iceberg fixed type ${type}`);
|
|
117
|
+
|
|
118
|
+
return { type: new FixedSizeBinary(fixed.length), row: bytes(fixed.length) };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
throw new Error(`Unsupported Iceberg type ${type}`);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/** Absent and null both become null in Arrow. */
|
|
125
|
+
const optional = (row: RowSchema) => Schema.optional(Schema.NullOr(row));
|
|
126
|
+
|
|
127
|
+
const fieldMetadata = (id: number): Map<string, string> =>
|
|
128
|
+
new Map([["PARQUET:field_id", String(id)]]);
|
|
129
|
+
|
|
130
|
+
/** Same as `primitive`, plus struct, list and map. */
|
|
131
|
+
const compileType = (type: IcebergType): Compiled => {
|
|
132
|
+
if (typeof type === "string") return primitive(type);
|
|
133
|
+
|
|
134
|
+
if (type.type === "struct") {
|
|
135
|
+
const fields = type.fields.map((field) => ({ ...field, compiled: compileType(field.type) }));
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
type: new Struct(
|
|
139
|
+
fields.map(
|
|
140
|
+
(field) =>
|
|
141
|
+
new Field(field.name, field.compiled.type, !field.required, fieldMetadata(field.id)),
|
|
142
|
+
),
|
|
143
|
+
),
|
|
144
|
+
row: Schema.Struct(
|
|
145
|
+
Object.fromEntries(
|
|
146
|
+
fields.map((field) => [
|
|
147
|
+
field.name,
|
|
148
|
+
field.required ? field.compiled.row : optional(field.compiled.row),
|
|
149
|
+
]),
|
|
150
|
+
),
|
|
151
|
+
),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (type.type === "list") {
|
|
156
|
+
const element = compileType(type.element);
|
|
157
|
+
const required = type["element-required"];
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
type: new List(
|
|
161
|
+
new Field("element", element.type, !required, fieldMetadata(type["element-id"])),
|
|
162
|
+
),
|
|
163
|
+
row: Schema.Array(required ? element.row : Schema.NullishOr(element.row)),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const key = compileType(type.key);
|
|
168
|
+
const value = compileType(type.value);
|
|
169
|
+
const required = type["value-required"];
|
|
170
|
+
const entries = new Struct([
|
|
171
|
+
new Field("key", key.type, false, fieldMetadata(type["key-id"])),
|
|
172
|
+
new Field("value", value.type, !required, fieldMetadata(type["value-id"])),
|
|
173
|
+
]);
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
type: new Map_(new Field("entries", entries, false)),
|
|
177
|
+
row: Schema.ReadonlyMap(key.row, required ? value.row : Schema.NullishOr(value.row)),
|
|
178
|
+
};
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
export type RowEncoder = (
|
|
182
|
+
rows: ReadonlyArray<object>,
|
|
183
|
+
) => Effect.Effect<RecordBatch<TypeMap>, ConnectorError>;
|
|
184
|
+
|
|
185
|
+
export const makeRowEncoder = (
|
|
186
|
+
tableSchema: TableSchema,
|
|
187
|
+
version: string,
|
|
188
|
+
): Effect.Effect<RowEncoder, ConnectorError> =>
|
|
189
|
+
Effect.try({
|
|
190
|
+
try: () => {
|
|
191
|
+
const fields = tableSchema.fields.map((field) => ({
|
|
192
|
+
id: field.id,
|
|
193
|
+
name: field.name,
|
|
194
|
+
...compileType(field.type),
|
|
195
|
+
}));
|
|
196
|
+
const fieldNames = new Set(fields.map((field) => field.name));
|
|
197
|
+
const identifierIds = new Set(tableSchema["identifier-field-ids"]);
|
|
198
|
+
const keys = tableSchema.fields
|
|
199
|
+
.filter((field) => identifierIds.has(field.id))
|
|
200
|
+
.map((field) => field.name);
|
|
201
|
+
// Other columns may be missing: a delete only sends the key and version.
|
|
202
|
+
const required = new Set([...keys, version]);
|
|
203
|
+
|
|
204
|
+
if (!fieldNames.has(version)) throw new Error(`Missing version column ${version}`);
|
|
205
|
+
|
|
206
|
+
const struct = new Struct(
|
|
207
|
+
fields.map((field) => new Field(field.name, field.type, true, fieldMetadata(field.id))),
|
|
208
|
+
);
|
|
209
|
+
const schema = new ArrowSchema(struct.children);
|
|
210
|
+
|
|
211
|
+
const decodeRow = Schema.decodeUnknownSync(
|
|
212
|
+
Schema.Struct(
|
|
213
|
+
Object.fromEntries(
|
|
214
|
+
fields.map((field) => [
|
|
215
|
+
field.name,
|
|
216
|
+
required.has(field.name) ? field.row : optional(field.row),
|
|
217
|
+
]),
|
|
218
|
+
),
|
|
219
|
+
),
|
|
220
|
+
{ onExcessProperty: "error" },
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
// Schema says which field is wrong. This adds the row number.
|
|
224
|
+
const decodeAt = (row: object, index: number) => {
|
|
225
|
+
try {
|
|
226
|
+
return decodeRow(row) as Record<string, unknown>;
|
|
227
|
+
} catch (cause) {
|
|
228
|
+
const detail = cause instanceof Error ? cause.message : String(cause);
|
|
229
|
+
throw new Error(`row ${index}: ${detail}`, { cause });
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
return (rows: ReadonlyArray<object>) =>
|
|
234
|
+
Effect.try({
|
|
235
|
+
try: () => {
|
|
236
|
+
const builder = makeBuilder({ type: struct, nullValues: [null, undefined] });
|
|
237
|
+
|
|
238
|
+
for (const [index, row] of rows.entries()) {
|
|
239
|
+
builder.append(decodeAt(row, index) as Struct<TypeMap>["TValue"]);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return new RecordBatch(schema, builder.finish().flush());
|
|
243
|
+
},
|
|
244
|
+
catch: (cause) => new ConnectorError({ message: "Failed to encode rows", cause }),
|
|
245
|
+
});
|
|
246
|
+
},
|
|
247
|
+
catch: (cause) => new ConnectorError({ message: "Failed to convert Iceberg schema", cause }),
|
|
248
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Effect, Layer } from "effect";
|
|
2
|
+
|
|
3
|
+
import { Ingestor } from "./service";
|
|
4
|
+
|
|
5
|
+
/** Logs batches without sending them over Arrow Flight. */
|
|
6
|
+
export const layerConsole = Layer.succeed(Ingestor)({
|
|
7
|
+
ingest: ({ resource, source, batch }) =>
|
|
8
|
+
Effect.logInfo(`[ingestor] -> Source: ${source} | Resource: ${resource}`).pipe(
|
|
9
|
+
Effect.annotateLogs({ rows: batch.rows.length, cursor: batch.cursor, source }),
|
|
10
|
+
// Nothing is encoded, so there are no bytes to count.
|
|
11
|
+
Effect.as({ rows: batch.rows.length, bytes: 0 }),
|
|
12
|
+
),
|
|
13
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { layerConsole } from "./console";
|
|
2
|
+
export {
|
|
3
|
+
Ingestor,
|
|
4
|
+
type IngestOptions,
|
|
5
|
+
type IngestorService,
|
|
6
|
+
type IngestResult,
|
|
7
|
+
type IngestSource,
|
|
8
|
+
} from "./service";
|
|
9
|
+
export {
|
|
10
|
+
layerArrowFlight,
|
|
11
|
+
layerArrowFlightConfig,
|
|
12
|
+
type ArrowFlightIngestorConfig,
|
|
13
|
+
} from "./arrow-flight";
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Effect, Metric } from "effect";
|
|
2
|
+
|
|
3
|
+
import type { ResourceBatch } from "../core/types";
|
|
4
|
+
|
|
5
|
+
import * as Metrics from "../metrics";
|
|
6
|
+
import { Attr, EventAttr, EventName, SpanName, addCurrentSpanEvent } from "../telemetry";
|
|
7
|
+
import { Ingestor, type IngestSource } from "./service";
|
|
8
|
+
|
|
9
|
+
export const ingestBatch = Effect.fnUntraced(function* (options: {
|
|
10
|
+
readonly connector?: string;
|
|
11
|
+
readonly resource: string;
|
|
12
|
+
readonly source: IngestSource;
|
|
13
|
+
readonly batch: ResourceBatch;
|
|
14
|
+
}) {
|
|
15
|
+
const metricAttributes = Metrics.withResourceAttributes({
|
|
16
|
+
connector: options.connector ?? "unknown",
|
|
17
|
+
resource: options.resource,
|
|
18
|
+
source: options.source,
|
|
19
|
+
});
|
|
20
|
+
const batchOutcome = (outcome: "success" | "error") =>
|
|
21
|
+
Metric.withAttributes(Metrics.batches, {
|
|
22
|
+
...metricAttributes,
|
|
23
|
+
[Attr.batchOutcome]: outcome,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const ingestor = yield* Ingestor;
|
|
27
|
+
|
|
28
|
+
const ingest = ingestor.ingest(options).pipe(
|
|
29
|
+
Effect.tap(() => Effect.annotateCurrentSpan({ [Attr.ingestionSuccess]: true })),
|
|
30
|
+
Effect.tapError(() =>
|
|
31
|
+
Effect.annotateCurrentSpan({
|
|
32
|
+
[Attr.ingestionSuccess]: false,
|
|
33
|
+
[Attr.errorPhase]: "ingest",
|
|
34
|
+
}),
|
|
35
|
+
),
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
// onError keeps the failure, so the engine still sees it and skips the checkpoint.
|
|
39
|
+
const result = yield* Effect.withSpan(ingest, SpanName.ingest, { kind: "producer" }).pipe(
|
|
40
|
+
Effect.trackDuration(Metric.withAttributes(Metrics.ingestDuration, metricAttributes)),
|
|
41
|
+
Effect.onError(() => Metric.update(batchOutcome("error"), 1)),
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
yield* Metric.update(batchOutcome("success"), 1);
|
|
45
|
+
yield* Metric.update(
|
|
46
|
+
Metric.withAttributes(Metrics.rowsIngested, metricAttributes),
|
|
47
|
+
options.batch.rows.length,
|
|
48
|
+
);
|
|
49
|
+
yield* Metric.update(
|
|
50
|
+
Metric.withAttributes(Metrics.batchSize, metricAttributes),
|
|
51
|
+
options.batch.rows.length,
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
if (options.batch.cursor !== undefined) {
|
|
55
|
+
yield* addCurrentSpanEvent(EventName.batchCheckpoint, {
|
|
56
|
+
[EventAttr.batchCursor]: options.batch.cursor,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return result;
|
|
61
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Context, type Effect } from "effect";
|
|
2
|
+
|
|
3
|
+
import type { ResourceBatch } from "../core/types";
|
|
4
|
+
import type { ConnectorError } from "../errors";
|
|
5
|
+
|
|
6
|
+
export type IngestSource = "backfill" | "changes" | "webhook" | "demo";
|
|
7
|
+
|
|
8
|
+
/** A resource batch and the source that produced it. */
|
|
9
|
+
export type IngestOptions = {
|
|
10
|
+
readonly resource: string;
|
|
11
|
+
readonly source: IngestSource;
|
|
12
|
+
readonly batch: ResourceBatch;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/** Rows written, and their size as uncompressed Arrow buffers. */
|
|
16
|
+
export type IngestResult = {
|
|
17
|
+
readonly rows: number;
|
|
18
|
+
readonly bytes: number;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** Handles batches from connector sources. */
|
|
22
|
+
export interface IngestorService {
|
|
23
|
+
readonly ingest: (options: IngestOptions) => Effect.Effect<IngestResult, ConnectorError>;
|
|
24
|
+
/** Resources it has tables for. Only these run. Without it, every resource runs. */
|
|
25
|
+
readonly resourceNames?: ReadonlyArray<string>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class Ingestor extends Context.Service<Ingestor, IngestorService>()(
|
|
29
|
+
"@useairfoil/connector-kit/Ingestor",
|
|
30
|
+
) {}
|
package/src/manifest/config.ts
CHANGED
|
@@ -111,7 +111,7 @@ const withDefault = <A extends string | number | boolean>(
|
|
|
111
111
|
|
|
112
112
|
/** Defines a required non-empty string, optionally with a default. */
|
|
113
113
|
export const string = (options: FieldOptions<string>): ConfigField<string> => ({
|
|
114
|
-
config: withDefault(Config.
|
|
114
|
+
config: withDefault(Config.NonEmptyString(options.runtimeKey), options.default),
|
|
115
115
|
spec: {
|
|
116
116
|
runtimeKey: options.runtimeKey,
|
|
117
117
|
type: "string",
|
|
@@ -145,7 +145,7 @@ export const number = (options: NumberFieldOptions): ConfigField<number> => {
|
|
|
145
145
|
|
|
146
146
|
/** Defines a boolean, optionally with a default. */
|
|
147
147
|
export const boolean = (options: FieldOptions<boolean>): ConfigField<boolean> => ({
|
|
148
|
-
config: withDefault(Config.
|
|
148
|
+
config: withDefault(Config.Boolean(options.runtimeKey), options.default),
|
|
149
149
|
spec: {
|
|
150
150
|
runtimeKey: options.runtimeKey,
|
|
151
151
|
type: "boolean",
|
|
@@ -160,7 +160,7 @@ export const boolean = (options: FieldOptions<boolean>): ConfigField<boolean> =>
|
|
|
160
160
|
export const select = <const Values extends readonly [string, ...Array<string>]>(
|
|
161
161
|
options: FieldOptions<Values[number]> & { readonly values: Values },
|
|
162
162
|
): ConfigField<Values[number]> => ({
|
|
163
|
-
config: withDefault(Config.
|
|
163
|
+
config: withDefault(Config.Literals(options.values, options.runtimeKey), options.default),
|
|
164
164
|
spec: {
|
|
165
165
|
runtimeKey: options.runtimeKey,
|
|
166
166
|
type: "select",
|
package/src/metrics.ts
CHANGED
|
@@ -1,11 +1,21 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
Cause,
|
|
3
|
+
DateTime,
|
|
4
|
+
type Duration,
|
|
5
|
+
Effect,
|
|
6
|
+
Metric,
|
|
7
|
+
Option,
|
|
8
|
+
Predicate,
|
|
9
|
+
Schedule,
|
|
10
|
+
} from "effect";
|
|
11
|
+
import { HttpClientError, HttpClientResponse } from "effect/http";
|
|
2
12
|
|
|
3
13
|
import type { SyncState } from "./core/types";
|
|
4
14
|
|
|
5
15
|
import { Attr } from "./telemetry";
|
|
6
16
|
|
|
7
|
-
export type BatchSource = "backfill" | "changes" | "webhook";
|
|
8
|
-
export type BatchOutcome = "
|
|
17
|
+
export type BatchSource = "backfill" | "changes" | "webhook" | "demo";
|
|
18
|
+
export type BatchOutcome = "success" | "error";
|
|
9
19
|
export type ApiRetryReason = "transport" | "timeout" | "rate_limit" | "server_error";
|
|
10
20
|
export type WebhookOutcome =
|
|
11
21
|
| "ok"
|
|
@@ -43,32 +53,26 @@ export type ApiRetryMetricAttrs = {
|
|
|
43
53
|
readonly reason: ApiRetryReason;
|
|
44
54
|
};
|
|
45
55
|
|
|
46
|
-
export const
|
|
47
|
-
description: "Total
|
|
48
|
-
incremental: true,
|
|
49
|
-
attributes: { unit: "{mutation}" },
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
export const entitiesDeleted = Metric.counter("airfoil.connector.entity.deletes", {
|
|
53
|
-
description: "Total entity delete mutations published by connector sources",
|
|
56
|
+
export const batches = Metric.counter("airfoil.connector.batches", {
|
|
57
|
+
description: "Total resource batches ingested by connector sources",
|
|
54
58
|
incremental: true,
|
|
55
|
-
attributes: { unit: "{
|
|
59
|
+
attributes: { unit: "{batch}" },
|
|
56
60
|
});
|
|
57
61
|
|
|
58
|
-
export const
|
|
59
|
-
description: "Total resource
|
|
62
|
+
export const rowsIngested = Metric.counter("airfoil.connector.rows.ingested", {
|
|
63
|
+
description: "Total resource rows successfully ingested",
|
|
60
64
|
incremental: true,
|
|
61
|
-
attributes: { unit: "{
|
|
65
|
+
attributes: { unit: "{row}" },
|
|
62
66
|
});
|
|
63
67
|
|
|
64
68
|
export const batchSize = Metric.histogram("airfoil.connector.batch.size", {
|
|
65
|
-
description: "Distribution of resource
|
|
69
|
+
description: "Distribution of resource row batch sizes",
|
|
66
70
|
boundaries: [1, 5, 10, 25, 50, 100, 250, 500, 1000],
|
|
67
|
-
attributes: { unit: "{
|
|
71
|
+
attributes: { unit: "{row}" },
|
|
68
72
|
});
|
|
69
73
|
|
|
70
|
-
export const
|
|
71
|
-
description: "
|
|
74
|
+
export const ingestDuration = Metric.timer("airfoil.connector.ingest.duration", {
|
|
75
|
+
description: "Ingestor call duration in milliseconds",
|
|
72
76
|
boundaries: [1, 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000],
|
|
73
77
|
attributes: { unit: "ms" },
|
|
74
78
|
});
|
|
@@ -80,7 +84,7 @@ export const webhookRequests = Metric.counter("airfoil.connector.webhook.request
|
|
|
80
84
|
});
|
|
81
85
|
|
|
82
86
|
export const webhookQueueDepth = Metric.gauge("airfoil.connector.webhook.queue.depth", {
|
|
83
|
-
description: "Current queued webhook batches waiting for
|
|
87
|
+
description: "Current queued webhook batches waiting for ingestion",
|
|
84
88
|
attributes: { unit: "{batch}" },
|
|
85
89
|
});
|
|
86
90
|
|
|
@@ -154,6 +158,48 @@ export const recordApiRetry = (attrs: ApiRetryMetricAttrs) =>
|
|
|
154
158
|
1,
|
|
155
159
|
);
|
|
156
160
|
|
|
161
|
+
// Same statuses as `HttpClient.retryTransient`.
|
|
162
|
+
const transientStatuses = new Set([408, 429, 500, 502, 503, 504]);
|
|
163
|
+
|
|
164
|
+
// A connector only retries a `403` when the provider uses it for rate limits.
|
|
165
|
+
const reasonForStatus = (status: number): ApiRetryReason =>
|
|
166
|
+
status === 408 ? "timeout" : status === 429 || status === 403 ? "rate_limit" : "server_error";
|
|
167
|
+
|
|
168
|
+
// Effect exports the response type ID, not a guard.
|
|
169
|
+
const isResponse = (value: unknown): value is HttpClientResponse.HttpClientResponse =>
|
|
170
|
+
Predicate.hasProperty(value, HttpClientResponse.TypeId);
|
|
171
|
+
|
|
172
|
+
/** Why `HttpClient.retryTransient` retries. Undefined for a response it keeps. */
|
|
173
|
+
const retryReason = (value: unknown): ApiRetryReason | undefined => {
|
|
174
|
+
if (isResponse(value)) {
|
|
175
|
+
return transientStatuses.has(value.status) ? reasonForStatus(value.status) : undefined;
|
|
176
|
+
}
|
|
177
|
+
if (HttpClientError.isHttpClientError(value) && value.reason._tag === "StatusCodeError") {
|
|
178
|
+
return reasonForStatus(value.reason.response.status);
|
|
179
|
+
}
|
|
180
|
+
return Cause.isTimeoutError(value) ? "timeout" : "transport";
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* A jittered exponential schedule for `HttpClient.retryTransient` that records
|
|
185
|
+
* each retry with its reason.
|
|
186
|
+
*/
|
|
187
|
+
export const retrySchedule = (options: {
|
|
188
|
+
readonly connector: string;
|
|
189
|
+
readonly baseDelay: Duration.Input;
|
|
190
|
+
readonly times: number;
|
|
191
|
+
}) =>
|
|
192
|
+
Schedule.exponential(options.baseDelay).pipe(
|
|
193
|
+
Schedule.jittered,
|
|
194
|
+
Schedule.upTo({ times: options.times }),
|
|
195
|
+
Schedule.tap(({ input }) => {
|
|
196
|
+
const reason = retryReason(input);
|
|
197
|
+
return reason === undefined
|
|
198
|
+
? Effect.void
|
|
199
|
+
: recordApiRetry({ connector: options.connector, reason });
|
|
200
|
+
}),
|
|
201
|
+
);
|
|
202
|
+
|
|
157
203
|
const syncStates = ["pending", "backfilling", "live", "error"] as const;
|
|
158
204
|
|
|
159
205
|
export const setSyncState = (attrs: SyncStateAttrs, current: SyncState) =>
|
|
@@ -1,16 +1,19 @@
|
|
|
1
|
-
import type { WingsClientOptions } from "@useairfoil/wings/wings-client";
|
|
2
|
-
|
|
3
1
|
import { Config } from "effect";
|
|
4
2
|
|
|
5
3
|
import { PlatformRuntimeDefault, PlatformRuntimeKey } from "./constants";
|
|
6
4
|
|
|
7
5
|
/** Shared HTTP port used by hosted and sandbox connector entrypoints. */
|
|
8
|
-
export const httpPort = Config.
|
|
6
|
+
export const httpPort = Config.Port(PlatformRuntimeKey.httpPort).pipe(
|
|
9
7
|
Config.withDefault(PlatformRuntimeDefault.httpPort),
|
|
10
8
|
);
|
|
11
9
|
|
|
12
|
-
/**
|
|
13
|
-
export const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
/** Engine URL, including the protocol. */
|
|
11
|
+
export const engineUri = Config.URL(PlatformRuntimeKey.engineUri);
|
|
12
|
+
|
|
13
|
+
/** Arrow Flight URL. Uses the engine URL when unset. */
|
|
14
|
+
export const flightUri = engineUri.pipe(
|
|
15
|
+
Config.flatMap((uri) => Config.URL(PlatformRuntimeKey.flightUri).pipe(Config.withDefault(uri))),
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
/** Catalog ID. */
|
|
19
|
+
export const catalog = Config.NonEmptyString(PlatformRuntimeKey.catalog);
|
|
@@ -8,8 +8,9 @@ export const PlatformRuntimeKey = {
|
|
|
8
8
|
connectorInstanceId: "AIRFOIL_CONNECTOR_INSTANCE_ID",
|
|
9
9
|
stateTable: "AIRFOIL_STATE_TABLE",
|
|
10
10
|
postgresConnectionString: "POSTGRES_CONNECTION_STRING",
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
engineUri: "ENGINE_URI",
|
|
12
|
+
flightUri: "FLIGHT_URI",
|
|
13
|
+
catalog: "AIRFOIL_CATALOG",
|
|
13
14
|
otelEnabled: "OTEL_ENABLED",
|
|
14
15
|
otelExporterOtlpEndpoint: "OTEL_EXPORTER_OTLP_ENDPOINT",
|
|
15
16
|
otelExporterOtlpHeaders: "OTEL_EXPORTER_OTLP_HEADERS",
|
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
export { RuntimeConfigError, type RuntimeConfigErrorCode } from "./error";
|
|
2
|
-
export {
|
|
2
|
+
export { catalog, engineUri, flightUri, httpPort } from "./config";
|
|
3
3
|
export { PlatformRuntimeDefault, PlatformRuntimeKey } from "./constants";
|
|
4
4
|
export { layerHosted } from "./layer";
|
|
5
|
-
export {
|
|
6
|
-
type HostedTableBinding,
|
|
7
|
-
type ResolvedTableBinding,
|
|
8
|
-
type ResolvedTableBindings,
|
|
9
|
-
loadTableBindings,
|
|
10
|
-
} from "./table-bindings";
|
|
5
|
+
export { type ResolvedTableBindings, type TableBinding, loadTableBindings } from "./table-bindings";
|
|
@@ -8,7 +8,7 @@ const providerFromFile: Effect.Effect<
|
|
|
8
8
|
RuntimeConfigError,
|
|
9
9
|
FileSystem.FileSystem
|
|
10
10
|
> = Effect.gen(function* () {
|
|
11
|
-
const configPath = yield* Config.
|
|
11
|
+
const configPath = yield* Config.NonEmptyString(PlatformRuntimeKey.configPath).pipe(
|
|
12
12
|
Effect.mapError(
|
|
13
13
|
(error) =>
|
|
14
14
|
new RuntimeConfigError({
|