@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.
- package/METRICS.md +16 -17
- package/README.md +127 -442
- package/dist/annotations-BE74YmAO.d.mts +39 -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-C-L4QqG6.mjs +123 -0
- package/dist/connector-app-C-L4QqG6.mjs.map +1 -0
- package/dist/{constants-D113Ut1x.js → constants-D-GQzpHZ.mjs} +4 -5
- package/dist/constants-D-GQzpHZ.mjs.map +1 -0
- package/dist/demo/index.d.mts +2 -0
- package/dist/demo/index.mjs +2 -0
- package/dist/demo-B30Vlkk2.mjs +191 -0
- package/dist/demo-B30Vlkk2.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-D6L8I6vA.mjs +39 -0
- package/dist/iceberg-D6L8I6vA.mjs.map +1 -0
- package/dist/{index-VpHaZXZw.d.ts → index-BUHSGm9Q.d.mts} +11 -13
- package/dist/{index-DXa26Ry5.d.ts → index-BX8G7d41.d.mts} +1 -2
- package/dist/{index-bfjH7Yb0.d.ts → index-BfXpgxk9.d.mts} +9 -10
- package/dist/{index-BKw7jA2a.d.ts → index-CRVAwJ5-.d.mts} +10 -8
- package/dist/index-CUPP2A_m.d.mts +27 -0
- package/dist/{index-DYpEMJf-.d.ts → index-CgvFomO6.d.mts} +6 -7
- package/dist/index-DJhDp4Xc.d.mts +58 -0
- package/dist/index-Dk7urvpF.d.mts +76 -0
- package/dist/{index-DKoRwyFn.d.ts → index-DkVNO6ks.d.mts} +2 -5
- package/dist/{index-C5Ma7V7t.d.ts → index-NnCutIqM.d.mts} +62 -37
- package/dist/{index-CR9HJDdr.d.ts → index-eFuoMZZ3.d.mts} +18 -20
- package/dist/{index-ZSuiZX5H.d.ts → index-ygpml2kC.d.mts} +19 -31
- 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-q0WwqUSt.mjs +275 -0
- package/dist/ingestion-q0WwqUSt.mjs.map +1 -0
- package/dist/ingestor/index.d.mts +3 -0
- package/dist/ingestor/index.mjs +4 -0
- package/dist/ingestor-D4L-HPkg.mjs +26 -0
- package/dist/ingestor-D4L-HPkg.mjs.map +1 -0
- package/dist/instrumented-Bhp7fyvh.mjs +29 -0
- package/dist/instrumented-Bhp7fyvh.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-BGKxmKLy.mjs} +14 -19
- package/dist/manifest-BGKxmKLy.mjs.map +1 -0
- package/dist/{telemetry-j_nxwYfh.js → metrics-BaEX__MN.mjs} +170 -19
- package/dist/metrics-BaEX__MN.mjs.map +1 -0
- package/dist/{metrics-C4V1pm6v.d.ts → metrics-CVXFL7YT.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-Oa55gAew.mjs +97 -0
- package/dist/runtime-config-Oa55gAew.mjs.map +1 -0
- package/dist/server-CMnxz3h0.mjs +144 -0
- package/dist/server-CMnxz3h0.mjs.map +1 -0
- package/dist/service-DLkhuxYl.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-uFvHY7Ax.mjs} +15 -22
- package/dist/state-store-uFvHY7Ax.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-CKb9IhQ9.mjs} +14 -16
- package/dist/status-CKb9IhQ9.mjs.map +1 -0
- package/dist/table-V0tgUC5i.mjs +370 -0
- package/dist/table-V0tgUC5i.mjs.map +1 -0
- package/dist/types-xZtSmrGA.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/dist/wings-I56hGtNv.mjs +325 -0
- package/dist/wings-I56hGtNv.mjs.map +1 -0
- package/package.json +64 -51
- package/src/catalog/table.ts +201 -0
- package/src/connector-app/create-table.ts +47 -0
- package/src/connector-app/demo.ts +42 -0
- package/src/connector-app/index.ts +2 -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 +142 -0
- package/src/iceberg/annotations.ts +62 -0
- package/src/iceberg/error.ts +9 -0
- package/src/iceberg/index.ts +3 -0
- package/src/iceberg/table.ts +511 -0
- package/src/index.ts +3 -1
- package/src/ingestion/engine.ts +240 -60
- package/src/ingestor/arrow.ts +248 -0
- package/src/ingestor/console.ts +13 -0
- package/src/ingestor/index.ts +9 -0
- package/src/ingestor/instrumented.ts +61 -0
- package/src/ingestor/service.ts +30 -0
- package/src/ingestor/wings.ts +143 -0
- package/src/manifest/config.ts +3 -3
- package/src/metrics.ts +66 -20
- package/src/runtime-config/config.ts +6 -8
- package/src/runtime-config/constants.ts +2 -2
- package/src/runtime-config/index.ts +2 -7
- package/src/runtime-config/layer.ts +1 -1
- package/src/runtime-config/table-bindings.ts +48 -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 +10 -10
- 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,511 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CommitTableRequest,
|
|
3
|
+
IcebergType,
|
|
4
|
+
MapType,
|
|
5
|
+
StructField,
|
|
6
|
+
StructType,
|
|
7
|
+
TableSchema,
|
|
8
|
+
} from "iceberg-js";
|
|
9
|
+
|
|
10
|
+
import { Effect, Schema, type SchemaRepresentation } from "effect";
|
|
11
|
+
|
|
12
|
+
import { IcebergSchemaError } from "./error";
|
|
13
|
+
|
|
14
|
+
/** Options for creating a table. */
|
|
15
|
+
export type CreateTableCommitOptions = {
|
|
16
|
+
/** Where the table's files go. The catalog may choose this when omitted. */
|
|
17
|
+
readonly location?: string | undefined;
|
|
18
|
+
/** UUID for the table. Random by default. */
|
|
19
|
+
readonly uuid?: string | undefined;
|
|
20
|
+
/** Extra table properties. These win over the ones from the schema. */
|
|
21
|
+
readonly properties?: Readonly<Record<string, string>> | undefined;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type Representation = SchemaRepresentation.Representation;
|
|
25
|
+
type Resolved = Exclude<Representation, SchemaRepresentation.Reference>;
|
|
26
|
+
type Annotations = Schema.Annotations.Annotations;
|
|
27
|
+
|
|
28
|
+
type CompiledType = {
|
|
29
|
+
readonly type: IcebergType;
|
|
30
|
+
readonly nullable: boolean;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type CompiledTable = {
|
|
34
|
+
readonly schema: TableSchema;
|
|
35
|
+
readonly description: string | undefined;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Iceberg reserves its highest 200 field IDs.
|
|
39
|
+
const maxFieldId = 2_147_483_447;
|
|
40
|
+
|
|
41
|
+
const anonymousDeclaration =
|
|
42
|
+
"Schemas without a name, such as Schema.Class, do not work here; use Schema.Struct";
|
|
43
|
+
|
|
44
|
+
class CompilationFailure extends Error {
|
|
45
|
+
readonly path: string;
|
|
46
|
+
|
|
47
|
+
constructor(path: string, message: string) {
|
|
48
|
+
super(message);
|
|
49
|
+
this.path = path;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const fail = (path: string, message: string): never => {
|
|
54
|
+
throw new CompilationFailure(path, message);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const propertyPath = (path: string, name: string): string =>
|
|
58
|
+
/^[A-Za-z_$][\w$]*$/.test(name) ? `${path}.${name}` : `${path}[${JSON.stringify(name)}]`;
|
|
59
|
+
|
|
60
|
+
/** Reads annotations the way Effect does: if there are checks, the last one wins. */
|
|
61
|
+
const resolveAnnotations = (representation: Resolved): Annotations | undefined => {
|
|
62
|
+
const lastCheck = representation.checks.at(-1);
|
|
63
|
+
return lastCheck === undefined ? representation.annotations : lastCheck.annotations;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const description = (annotations: Annotations | undefined): string | undefined => {
|
|
67
|
+
const value = annotations?.description;
|
|
68
|
+
return typeof value === "string" ? value : undefined;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/** @internal */
|
|
72
|
+
export const checkHasRepresentation = (
|
|
73
|
+
checks: ReadonlyArray<SchemaRepresentation.Check>,
|
|
74
|
+
id: string,
|
|
75
|
+
): boolean =>
|
|
76
|
+
checks.some(
|
|
77
|
+
(check) =>
|
|
78
|
+
check.representation?.id === id ||
|
|
79
|
+
(check._tag === "FilterGroup" && checkHasRepresentation(check.checks, id)),
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const isAnonymousDeclaration = (representation: Resolved): boolean =>
|
|
83
|
+
representation._tag === "Declaration" && representation.representation?.id === undefined;
|
|
84
|
+
|
|
85
|
+
const literalType = (
|
|
86
|
+
literal: SchemaRepresentation.Literal["literal"],
|
|
87
|
+
path: string,
|
|
88
|
+
): IcebergType => {
|
|
89
|
+
switch (typeof literal) {
|
|
90
|
+
case "string":
|
|
91
|
+
return "string";
|
|
92
|
+
case "boolean":
|
|
93
|
+
return "boolean";
|
|
94
|
+
case "number":
|
|
95
|
+
return Number.isFinite(literal)
|
|
96
|
+
? "double"
|
|
97
|
+
: fail(path, "A number literal must be a real number, not Infinity or NaN");
|
|
98
|
+
case "bigint":
|
|
99
|
+
return "long";
|
|
100
|
+
default:
|
|
101
|
+
return fail(path, `A ${typeof literal} literal does not work here`);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const identifierId = (fields: ReadonlyArray<StructField>, name: string): number => {
|
|
106
|
+
const path = propertyPath("$", name);
|
|
107
|
+
const field = fields.find((candidate) => candidate.name === name);
|
|
108
|
+
|
|
109
|
+
if (!field) return fail(path, "Iceberg.identifier names a field that does not exist");
|
|
110
|
+
if (!field.required) return fail(path, "An identifier field cannot be optional or null");
|
|
111
|
+
if (typeof field.type !== "string" || field.type === "float" || field.type === "double") {
|
|
112
|
+
return fail(path, "An identifier field must be a primitive type other than float or double");
|
|
113
|
+
}
|
|
114
|
+
return field.id;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const assertNever = (_representation: never, path: string): never =>
|
|
118
|
+
fail(path, "This kind of schema does not work here");
|
|
119
|
+
|
|
120
|
+
class Compiler {
|
|
121
|
+
readonly #activeObjects = new Set<SchemaRepresentation.Objects>();
|
|
122
|
+
readonly #activeReferences = new Set<string>();
|
|
123
|
+
readonly #activeSuspends = new Set<SchemaRepresentation.Suspend>();
|
|
124
|
+
readonly #fieldIds = new Map<number, string>();
|
|
125
|
+
|
|
126
|
+
constructor(private readonly references: SchemaRepresentation.References) {}
|
|
127
|
+
|
|
128
|
+
compile(root: Representation): CompiledTable {
|
|
129
|
+
return this.#withReference(root, "$", (representation) => {
|
|
130
|
+
if (isAnonymousDeclaration(representation)) return fail("$", anonymousDeclaration);
|
|
131
|
+
if (representation._tag !== "Objects") {
|
|
132
|
+
return fail("$", "The top level of a table must be a Schema.Struct");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const annotations = resolveAnnotations(representation);
|
|
136
|
+
const fields = this.#compileFields(representation, "$");
|
|
137
|
+
const identifierIds = (annotations?.icebergIdentifierFields ?? []).map((name) =>
|
|
138
|
+
identifierId(fields, name),
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
schema:
|
|
143
|
+
identifierIds.length === 0
|
|
144
|
+
? { type: "struct", fields }
|
|
145
|
+
: {
|
|
146
|
+
type: "struct",
|
|
147
|
+
// Deletes only send the key and version, so other columns must be nullable.
|
|
148
|
+
fields: fields.map((field) => ({
|
|
149
|
+
...field,
|
|
150
|
+
required: identifierIds.includes(field.id),
|
|
151
|
+
})),
|
|
152
|
+
"identifier-field-ids": identifierIds,
|
|
153
|
+
},
|
|
154
|
+
description: description(annotations),
|
|
155
|
+
};
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
#readId(annotations: Annotations | undefined, path: string, source: string): number {
|
|
160
|
+
const id = annotations?.fieldId;
|
|
161
|
+
|
|
162
|
+
if (typeof id !== "number") {
|
|
163
|
+
return fail(path, `Missing fieldId; add it with ${source}`);
|
|
164
|
+
}
|
|
165
|
+
if (!Number.isInteger(id)) return fail(path, "fieldId must be a whole number");
|
|
166
|
+
if (id < 1 || id > maxFieldId) {
|
|
167
|
+
return fail(path, `fieldId must be between 1 and ${maxFieldId}`);
|
|
168
|
+
}
|
|
169
|
+
const previousPath = this.#fieldIds.get(id);
|
|
170
|
+
if (previousPath !== undefined) {
|
|
171
|
+
return fail(path, `fieldId ${id} is already used at ${previousPath}`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
this.#fieldIds.set(id, path);
|
|
175
|
+
return id;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
#readNodeId(representation: Representation, path: string): number {
|
|
179
|
+
return this.#withReference(representation, path, (resolved) =>
|
|
180
|
+
this.#readId(
|
|
181
|
+
resolveAnnotations(resolved),
|
|
182
|
+
path,
|
|
183
|
+
".annotate({ fieldId }) on the schema itself",
|
|
184
|
+
),
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
#withReference<A>(
|
|
189
|
+
representation: Representation,
|
|
190
|
+
path: string,
|
|
191
|
+
compile: (resolved: Resolved) => A,
|
|
192
|
+
): A {
|
|
193
|
+
if (representation._tag !== "Reference") return compile(representation);
|
|
194
|
+
|
|
195
|
+
const reference = representation.$ref;
|
|
196
|
+
const resolved = this.references[reference];
|
|
197
|
+
if (resolved === undefined) return fail(path, `Cannot find the schema named ${reference}`);
|
|
198
|
+
if (this.#activeReferences.has(reference)) {
|
|
199
|
+
return fail(path, "Iceberg cannot store a schema that contains itself");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
this.#activeReferences.add(reference);
|
|
203
|
+
try {
|
|
204
|
+
return this.#withReference(resolved, path, compile);
|
|
205
|
+
} finally {
|
|
206
|
+
this.#activeReferences.delete(reference);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
#checkRecursion(representation: Representation, path: string): void {
|
|
211
|
+
if (representation._tag === "Reference" && this.#activeReferences.has(representation.$ref)) {
|
|
212
|
+
fail(path, "Iceberg cannot store a schema that contains itself");
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
#compileFields(representation: SchemaRepresentation.Objects, path: string): Array<StructField> {
|
|
217
|
+
if (representation.indexSignatures.length > 0) {
|
|
218
|
+
return fail(
|
|
219
|
+
path,
|
|
220
|
+
"Schema.Record does not work here; name the keys with Schema.Struct, or store it as a JSON string",
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return representation.propertySignatures.map((property) => {
|
|
225
|
+
if (typeof property.name !== "string") {
|
|
226
|
+
return fail(
|
|
227
|
+
propertyPath(path, String(property.name)),
|
|
228
|
+
"Iceberg field names must be strings",
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const fieldPath = propertyPath(path, property.name);
|
|
233
|
+
this.#checkRecursion(property.type, fieldPath);
|
|
234
|
+
const id = this.#readId(property.annotations, fieldPath, "Iceberg.field(id)");
|
|
235
|
+
const compiled = this.#compileType(property.type, fieldPath);
|
|
236
|
+
const fieldDescription = description(property.annotations);
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
id,
|
|
240
|
+
name: property.name,
|
|
241
|
+
type: compiled.type,
|
|
242
|
+
required: !property.isOptional && !compiled.nullable,
|
|
243
|
+
...(fieldDescription === undefined ? {} : { doc: fieldDescription }),
|
|
244
|
+
};
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
#compileType(representation: Representation, path: string): CompiledType {
|
|
249
|
+
if (representation._tag === "Reference") {
|
|
250
|
+
return this.#withReference(representation, path, (resolved) =>
|
|
251
|
+
this.#compileType(resolved, path),
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
switch (representation._tag) {
|
|
256
|
+
case "String":
|
|
257
|
+
return { type: "string", nullable: false };
|
|
258
|
+
case "Boolean":
|
|
259
|
+
return { type: "boolean", nullable: false };
|
|
260
|
+
case "BigInt":
|
|
261
|
+
return { type: "long", nullable: false };
|
|
262
|
+
case "Number":
|
|
263
|
+
// Schema.Int can exceed Iceberg int. A double holds every safe integer
|
|
264
|
+
// exactly; a long would need bigint rows.
|
|
265
|
+
return checkHasRepresentation(representation.checks, "effect/schema/isFinite") ||
|
|
266
|
+
checkHasRepresentation(representation.checks, "effect/schema/isInt")
|
|
267
|
+
? { type: "double", nullable: false }
|
|
268
|
+
: fail(path, "Plain Schema.Number does not work here; use Schema.Finite or Schema.Int");
|
|
269
|
+
case "Literal":
|
|
270
|
+
return { type: literalType(representation.literal, path), nullable: false };
|
|
271
|
+
case "Objects": {
|
|
272
|
+
if (this.#activeObjects.has(representation)) {
|
|
273
|
+
return fail(path, "Iceberg cannot store a schema that contains itself");
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
this.#activeObjects.add(representation);
|
|
277
|
+
try {
|
|
278
|
+
return {
|
|
279
|
+
type: {
|
|
280
|
+
type: "struct",
|
|
281
|
+
fields: this.#compileFields(representation, path),
|
|
282
|
+
} satisfies StructType,
|
|
283
|
+
nullable: false,
|
|
284
|
+
};
|
|
285
|
+
} finally {
|
|
286
|
+
this.#activeObjects.delete(representation);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
case "Arrays":
|
|
290
|
+
return this.#compileArray(representation, path);
|
|
291
|
+
case "Declaration":
|
|
292
|
+
return this.#compileDeclaration(representation, path);
|
|
293
|
+
case "Union":
|
|
294
|
+
return this.#compileUnion(representation, path);
|
|
295
|
+
case "Suspend": {
|
|
296
|
+
if (this.#activeSuspends.has(representation)) {
|
|
297
|
+
return fail(path, "Iceberg cannot store a schema that contains itself");
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
this.#activeSuspends.add(representation);
|
|
301
|
+
try {
|
|
302
|
+
return this.#compileType(representation.thunk, path);
|
|
303
|
+
} finally {
|
|
304
|
+
this.#activeSuspends.delete(representation);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
case "Null":
|
|
308
|
+
case "Undefined":
|
|
309
|
+
return fail(path, "A field cannot contain only null or undefined");
|
|
310
|
+
case "Any":
|
|
311
|
+
case "Unknown":
|
|
312
|
+
case "ObjectKeyword":
|
|
313
|
+
case "Symbol":
|
|
314
|
+
case "UniqueSymbol":
|
|
315
|
+
case "Void":
|
|
316
|
+
case "Never":
|
|
317
|
+
case "Enum":
|
|
318
|
+
case "TemplateLiteral":
|
|
319
|
+
return fail(path, `A ${representation._tag} schema does not work here`);
|
|
320
|
+
default:
|
|
321
|
+
return assertNever(representation, path);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
#compileArray(representation: SchemaRepresentation.Arrays, path: string): CompiledType {
|
|
326
|
+
if (representation.elements.length > 0 || representation.rest.length !== 1) {
|
|
327
|
+
return fail(
|
|
328
|
+
path,
|
|
329
|
+
"A tuple does not work here; use a Schema.Array where every item has the same type",
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const elementPath = `${path}.element`;
|
|
334
|
+
const elementRepresentation = representation.rest[0]!;
|
|
335
|
+
const elementId = this.#readNodeId(elementRepresentation, elementPath);
|
|
336
|
+
const compiled = this.#compileType(elementRepresentation, elementPath);
|
|
337
|
+
|
|
338
|
+
return {
|
|
339
|
+
type: {
|
|
340
|
+
type: "list",
|
|
341
|
+
"element-id": elementId,
|
|
342
|
+
element: compiled.type,
|
|
343
|
+
"element-required": !compiled.nullable,
|
|
344
|
+
},
|
|
345
|
+
nullable: false,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
#compileDeclaration(
|
|
350
|
+
representation: SchemaRepresentation.Declaration,
|
|
351
|
+
path: string,
|
|
352
|
+
): CompiledType {
|
|
353
|
+
const id = representation.representation?.id;
|
|
354
|
+
|
|
355
|
+
switch (id) {
|
|
356
|
+
case "effect/schema/Date":
|
|
357
|
+
return { type: "timestamptz", nullable: false };
|
|
358
|
+
case "effect/schema/Uint8Array":
|
|
359
|
+
return { type: "binary", nullable: false };
|
|
360
|
+
case "effect/schema/ReadonlyMap":
|
|
361
|
+
return this.#compileMap(representation, path);
|
|
362
|
+
case undefined:
|
|
363
|
+
return fail(path, anonymousDeclaration);
|
|
364
|
+
default:
|
|
365
|
+
return fail(path, `The ${id} schema does not work here`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
#compileMap(representation: SchemaRepresentation.Declaration, path: string): CompiledType {
|
|
370
|
+
if (representation.typeParameters.length !== 2) {
|
|
371
|
+
return fail(path, "ReadonlyMap needs one key schema and one value schema");
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const keyPath = `${path}.key`;
|
|
375
|
+
const valuePath = `${path}.value`;
|
|
376
|
+
const keyRepresentation = representation.typeParameters[0]!;
|
|
377
|
+
const valueRepresentation = representation.typeParameters[1]!;
|
|
378
|
+
const keyId = this.#readNodeId(keyRepresentation, keyPath);
|
|
379
|
+
const compiledKey = this.#compileType(keyRepresentation, keyPath);
|
|
380
|
+
|
|
381
|
+
if (compiledKey.nullable) return fail(keyPath, "Map keys cannot be null");
|
|
382
|
+
|
|
383
|
+
const valueId = this.#readNodeId(valueRepresentation, valuePath);
|
|
384
|
+
const compiledValue = this.#compileType(valueRepresentation, valuePath);
|
|
385
|
+
|
|
386
|
+
return {
|
|
387
|
+
type: {
|
|
388
|
+
type: "map",
|
|
389
|
+
"key-id": keyId,
|
|
390
|
+
key: compiledKey.type,
|
|
391
|
+
"value-id": valueId,
|
|
392
|
+
value: compiledValue.type,
|
|
393
|
+
"value-required": !compiledValue.nullable,
|
|
394
|
+
} satisfies MapType,
|
|
395
|
+
nullable: false,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
#compileUnion(representation: SchemaRepresentation.Union, path: string): CompiledType {
|
|
400
|
+
let nullable = false;
|
|
401
|
+
const concrete: Array<Representation> = [];
|
|
402
|
+
|
|
403
|
+
for (const member of representation.types) {
|
|
404
|
+
if (member._tag === "Null" || member._tag === "Undefined") {
|
|
405
|
+
nullable = true;
|
|
406
|
+
} else {
|
|
407
|
+
concrete.push(member);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (concrete.length === 0) return fail(path, "A field cannot contain only null or undefined");
|
|
412
|
+
|
|
413
|
+
if (concrete.length === 1) {
|
|
414
|
+
const compiled = this.#compileType(concrete[0]!, path);
|
|
415
|
+
return { type: compiled.type, nullable: nullable || compiled.nullable };
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (!concrete.every((member) => member._tag === "Literal")) {
|
|
419
|
+
return fail(path, "A union only works when every member is a literal");
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const first = literalType(concrete[0]!.literal, path);
|
|
423
|
+
for (const member of concrete.slice(1)) {
|
|
424
|
+
if (literalType(member.literal, path) !== first) {
|
|
425
|
+
return fail(path, "Every literal in a union must be the same type");
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return { type: first, nullable };
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
export const compileTable = (
|
|
434
|
+
schema: Schema.Top,
|
|
435
|
+
): Effect.Effect<CompiledTable, IcebergSchemaError> =>
|
|
436
|
+
Effect.try({
|
|
437
|
+
try: () => {
|
|
438
|
+
const document = Schema.toRepresentation(Schema.toType(schema));
|
|
439
|
+
return new Compiler(document.references).compile(document.representation);
|
|
440
|
+
},
|
|
441
|
+
catch: (cause) =>
|
|
442
|
+
cause instanceof CompilationFailure
|
|
443
|
+
? new IcebergSchemaError({ path: cause.path, message: cause.message, cause })
|
|
444
|
+
: new IcebergSchemaError({
|
|
445
|
+
path: "$",
|
|
446
|
+
message: cause instanceof Error ? cause.message : String(cause),
|
|
447
|
+
cause,
|
|
448
|
+
}),
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
export const makeCommitRequest = (
|
|
452
|
+
table: CompiledTable,
|
|
453
|
+
options: CreateTableCommitOptions,
|
|
454
|
+
): CommitTableRequest => {
|
|
455
|
+
const properties = {
|
|
456
|
+
...(table.description === undefined ? {} : { comment: table.description }),
|
|
457
|
+
...options.properties,
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
return {
|
|
461
|
+
requirements: [{ type: "assert-create" }],
|
|
462
|
+
updates: [
|
|
463
|
+
{ action: "assign-uuid", uuid: options.uuid ?? crypto.randomUUID() },
|
|
464
|
+
{ action: "upgrade-format-version", "format-version": 2 },
|
|
465
|
+
{ action: "add-schema", schema: table.schema },
|
|
466
|
+
{ action: "set-current-schema", "schema-id": -1 },
|
|
467
|
+
{ action: "add-spec", spec: { "spec-id": 0, fields: [] } },
|
|
468
|
+
{ action: "set-default-spec", "spec-id": -1 },
|
|
469
|
+
{ action: "add-sort-order", "sort-order": { "order-id": 0, fields: [] } },
|
|
470
|
+
{ action: "set-default-sort-order", "sort-order-id": -1 },
|
|
471
|
+
...(options.location === undefined
|
|
472
|
+
? []
|
|
473
|
+
: [{ action: "set-location" as const, location: options.location }]),
|
|
474
|
+
...(Object.keys(properties).length === 0
|
|
475
|
+
? []
|
|
476
|
+
: [{ action: "set-properties" as const, updates: properties }]),
|
|
477
|
+
],
|
|
478
|
+
};
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Builds the commit that creates an Iceberg table from an Effect schema.
|
|
483
|
+
*
|
|
484
|
+
* The field IDs come from the schema, so the table has to keep them. Send the
|
|
485
|
+
* commit with `commitTable`. Then load the table and check the IDs before you
|
|
486
|
+
* write any rows. Don't use `createTable` here, because it can change the IDs.
|
|
487
|
+
*
|
|
488
|
+
* A root `description` becomes the table `comment`, and a field `description`
|
|
489
|
+
* becomes its `doc`. `title` and `examples` are dropped.
|
|
490
|
+
*
|
|
491
|
+
* `Iceberg.identifier` sets the `identifier-field-ids`, and makes every other
|
|
492
|
+
* top-level column nullable.
|
|
493
|
+
*
|
|
494
|
+
* @example
|
|
495
|
+
* ```ts
|
|
496
|
+
* const Row = Schema.Struct({
|
|
497
|
+
* id: Schema.String.pipe(Iceberg.field(1, { description: "Stable row identifier" })),
|
|
498
|
+
* })
|
|
499
|
+
* .annotate({ description: "Connector rows" })
|
|
500
|
+
* .pipe(Iceberg.identifier("id"))
|
|
501
|
+
*
|
|
502
|
+
* const request = yield* Iceberg.makeCreateTableCommitRequest(Row, {
|
|
503
|
+
* location: "s3://warehouse/rows"
|
|
504
|
+
* })
|
|
505
|
+
* ```
|
|
506
|
+
*/
|
|
507
|
+
export const makeCreateTableCommitRequest = (
|
|
508
|
+
schema: Schema.Top,
|
|
509
|
+
options: CreateTableCommitOptions,
|
|
510
|
+
): Effect.Effect<CommitTableRequest, IcebergSchemaError> =>
|
|
511
|
+
Effect.map(compileTable(schema), (table) => makeCommitRequest(table, options));
|
package/src/index.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
export * from "./core";
|
|
2
2
|
export * as Auth from "./auth";
|
|
3
3
|
export * as ConnectorApp from "./connector-app";
|
|
4
|
+
export * as Demo from "./demo";
|
|
5
|
+
export * as Iceberg from "./iceberg";
|
|
4
6
|
export * as Ingestion from "./ingestion";
|
|
7
|
+
export * as Ingestor from "./ingestor";
|
|
5
8
|
export * as Manifest from "./manifest";
|
|
6
9
|
export * as Metrics from "./metrics";
|
|
7
|
-
export * as Publisher from "./publisher";
|
|
8
10
|
export * as RuntimeConfig from "./runtime-config";
|
|
9
11
|
export * as StateStore from "./state-store";
|
|
10
12
|
export * as Status from "./status";
|