airfoil 0.4.0 → 0.4.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/dist/index.js +401 -739
- package/package.json +9 -6
- package/src/commands/cluster/index.ts +13 -37
- package/src/commands/cluster/namespace/create.ts +278 -61
- package/src/commands/cluster/namespace/delete.ts +9 -13
- package/src/commands/cluster/namespace/get.ts +12 -14
- package/src/commands/cluster/namespace/list.ts +16 -34
- package/src/commands/cluster/{topic → table}/create.ts +79 -96
- package/src/commands/cluster/{tenant → table}/delete.ts +15 -17
- package/src/commands/cluster/table/get.ts +62 -0
- package/src/commands/cluster/table/list.ts +61 -0
- package/src/commands/dev.ts +17 -13
- package/src/commands/sql.ts +13 -23
- package/src/index.ts +6 -6
- package/src/utils/client.ts +3 -3
- package/src/utils/docker.ts +101 -7
- package/src/utils/options.ts +1 -1
- package/src/utils/wings.ts +18 -14
- package/src/commands/cluster/data-lake/create-iceberg.ts +0 -59
- package/src/commands/cluster/data-lake/create-parquet.ts +0 -59
- package/src/commands/cluster/data-lake/create.ts +0 -10
- package/src/commands/cluster/data-lake/delete.ts +0 -54
- package/src/commands/cluster/data-lake/get.ts +0 -40
- package/src/commands/cluster/data-lake/list.ts +0 -62
- package/src/commands/cluster/object-store/create-aws.ts +0 -104
- package/src/commands/cluster/object-store/create-azure.ts +0 -96
- package/src/commands/cluster/object-store/create-google.ts +0 -87
- package/src/commands/cluster/object-store/create-s3.ts +0 -114
- package/src/commands/cluster/object-store/create.ts +0 -21
- package/src/commands/cluster/object-store/delete.ts +0 -56
- package/src/commands/cluster/object-store/get.ts +0 -49
- package/src/commands/cluster/object-store/list.ts +0 -62
- package/src/commands/cluster/tenant/create.ts +0 -44
- package/src/commands/cluster/tenant/get.ts +0 -40
- package/src/commands/cluster/tenant/list.ts +0 -53
- package/src/commands/cluster/topic/delete.ts +0 -65
- package/src/commands/cluster/topic/get.ts +0 -51
- package/src/commands/cluster/topic/list.ts +0 -61
|
@@ -1,16 +1,13 @@
|
|
|
1
|
-
import type { FieldConfig } from "@useairfoil/wings";
|
|
2
|
-
|
|
3
1
|
import * as p from "@clack/prompts";
|
|
4
|
-
import {
|
|
2
|
+
import { Arrow, ClusterClient } from "@useairfoil/wings";
|
|
5
3
|
import { printTable } from "console-table-printer";
|
|
6
4
|
import { Effect, Option } from "effect";
|
|
7
5
|
import { Command, Flag } from "effect/unstable/cli";
|
|
8
6
|
import * as fs from "node:fs";
|
|
9
7
|
|
|
10
|
-
import {
|
|
11
|
-
import { hostOption, portOption } from "../../../utils/options
|
|
8
|
+
import { makeClusterClientLayer } from "../../../utils/client";
|
|
9
|
+
import { hostOption, portOption } from "../../../utils/options";
|
|
12
10
|
|
|
13
|
-
// Supported simple types (no config required)
|
|
14
11
|
const SUPPORTED_INLINE_TYPES = [
|
|
15
12
|
"Int8",
|
|
16
13
|
"Int16",
|
|
@@ -40,17 +37,18 @@ const SUPPORTED_INLINE_TYPES = [
|
|
|
40
37
|
] as const;
|
|
41
38
|
|
|
42
39
|
type SupportedInlineType = (typeof SUPPORTED_INLINE_TYPES)[number];
|
|
40
|
+
type FieldConfig = Arrow.FieldConfig;
|
|
43
41
|
|
|
44
42
|
const parentOption = Flag.string("parent").pipe(
|
|
45
|
-
Flag.withDescription("Parent namespace in format:
|
|
43
|
+
Flag.withDescription("Parent namespace in format: namespaces/{namespace}"),
|
|
46
44
|
);
|
|
47
45
|
|
|
48
|
-
const
|
|
49
|
-
Flag.withDescription("Unique identifier for the
|
|
46
|
+
const tableIdOption = Flag.string("table-id").pipe(
|
|
47
|
+
Flag.withDescription("Unique identifier for the table"),
|
|
50
48
|
);
|
|
51
49
|
|
|
52
50
|
const descriptionOption = Flag.string("description").pipe(
|
|
53
|
-
Flag.withDescription("
|
|
51
|
+
Flag.withDescription("Table description"),
|
|
54
52
|
Flag.optional,
|
|
55
53
|
);
|
|
56
54
|
|
|
@@ -64,29 +62,24 @@ const schemaFileOption = Flag.string("schema-file").pipe(
|
|
|
64
62
|
Flag.optional,
|
|
65
63
|
);
|
|
66
64
|
|
|
67
|
-
const
|
|
68
|
-
Flag.withDescription("Name of the field used
|
|
69
|
-
Flag.optional,
|
|
65
|
+
const keyFieldOption = Flag.string("key-field").pipe(
|
|
66
|
+
Flag.withDescription("Name of the field used as the primary key"),
|
|
70
67
|
);
|
|
71
68
|
|
|
72
|
-
const
|
|
73
|
-
Flag.withDescription("
|
|
74
|
-
Flag.withDefault(60),
|
|
69
|
+
const versionFieldOption = Flag.string("version-field").pipe(
|
|
70
|
+
Flag.withDescription("Name of the field used as the version/timestamp"),
|
|
75
71
|
);
|
|
76
72
|
|
|
77
|
-
const
|
|
78
|
-
Flag.withDescription("
|
|
73
|
+
const partitionFieldOption = Flag.string("partition-field").pipe(
|
|
74
|
+
Flag.withDescription("Name of the field used for partitioning"),
|
|
79
75
|
Flag.optional,
|
|
80
76
|
);
|
|
81
77
|
|
|
82
|
-
const
|
|
83
|
-
Flag.withDescription("Target
|
|
84
|
-
Flag.withDefault(
|
|
78
|
+
const freshnessSecondsOption = Flag.integer("freshness-seconds").pipe(
|
|
79
|
+
Flag.withDescription("Target freshness interval for compaction (seconds)"),
|
|
80
|
+
Flag.withDefault(60),
|
|
85
81
|
);
|
|
86
82
|
|
|
87
|
-
/**
|
|
88
|
-
* Parse a field string in format "name:Type" or "name:Type?" (nullable)
|
|
89
|
-
*/
|
|
90
83
|
function parseFieldString(fieldStr: string, index: number): FieldConfig {
|
|
91
84
|
const match = fieldStr.match(/^([^:]+):([^?]+)(\?)?$/);
|
|
92
85
|
if (!match) {
|
|
@@ -121,16 +114,10 @@ function parseFieldString(fieldStr: string, index: number): FieldConfig {
|
|
|
121
114
|
};
|
|
122
115
|
}
|
|
123
116
|
|
|
124
|
-
/**
|
|
125
|
-
* Parse multiple field strings into FieldConfig array
|
|
126
|
-
*/
|
|
127
117
|
function parseFieldsFromArgs(fields: ReadonlyArray<string>): FieldConfig[] {
|
|
128
118
|
return fields.map((field, index) => parseFieldString(field, index));
|
|
129
119
|
}
|
|
130
120
|
|
|
131
|
-
/**
|
|
132
|
-
* Load fields from a JSON schema file with basic validation
|
|
133
|
-
*/
|
|
134
121
|
function loadFieldsFromFile(filePath: string): FieldConfig[] {
|
|
135
122
|
if (!fs.existsSync(filePath)) {
|
|
136
123
|
throw new Error(`Schema file not found: "${filePath}"`);
|
|
@@ -162,36 +149,34 @@ function loadFieldsFromFile(filePath: string): FieldConfig[] {
|
|
|
162
149
|
});
|
|
163
150
|
}
|
|
164
151
|
|
|
165
|
-
export const
|
|
166
|
-
"create-
|
|
152
|
+
export const createTableCommand = Command.make(
|
|
153
|
+
"create-table",
|
|
167
154
|
{
|
|
168
155
|
parent: parentOption,
|
|
169
|
-
|
|
156
|
+
tableId: tableIdOption,
|
|
170
157
|
description: descriptionOption,
|
|
171
158
|
fields: fieldsOption,
|
|
172
159
|
schemaFile: schemaFileOption,
|
|
173
|
-
|
|
160
|
+
keyField: keyFieldOption,
|
|
161
|
+
versionField: versionFieldOption,
|
|
162
|
+
partitionField: partitionFieldOption,
|
|
174
163
|
freshnessSeconds: freshnessSecondsOption,
|
|
175
|
-
ttlSeconds: ttlSecondsOption,
|
|
176
|
-
targetFileSizeBytes: targetFileSizeBytesOption,
|
|
177
164
|
host: hostOption,
|
|
178
165
|
port: portOption,
|
|
179
166
|
},
|
|
180
167
|
({
|
|
181
168
|
parent,
|
|
182
|
-
|
|
169
|
+
tableId,
|
|
183
170
|
description,
|
|
184
171
|
fields,
|
|
185
172
|
schemaFile,
|
|
186
|
-
|
|
173
|
+
keyField,
|
|
174
|
+
versionField,
|
|
175
|
+
partitionField,
|
|
187
176
|
freshnessSeconds,
|
|
188
|
-
ttlSeconds,
|
|
189
|
-
targetFileSizeBytes,
|
|
190
|
-
host,
|
|
191
|
-
port,
|
|
192
177
|
}) =>
|
|
193
178
|
Effect.gen(function* () {
|
|
194
|
-
p.intro("📋 Create
|
|
179
|
+
p.intro("📋 Create Table");
|
|
195
180
|
|
|
196
181
|
const schemaFilePath = Option.getOrUndefined(schemaFile);
|
|
197
182
|
const hasFields = fields.length > 0;
|
|
@@ -206,84 +191,79 @@ export const createTopicCommand = Command.make(
|
|
|
206
191
|
);
|
|
207
192
|
}
|
|
208
193
|
|
|
209
|
-
const
|
|
194
|
+
const tableFields = yield* Effect.try({
|
|
210
195
|
try: () =>
|
|
211
196
|
schemaFilePath ? loadFieldsFromFile(schemaFilePath) : parseFieldsFromArgs(fields),
|
|
212
197
|
catch: (error) => (error instanceof Error ? error : new Error("Failed to parse fields")),
|
|
213
198
|
});
|
|
214
199
|
|
|
215
|
-
if (
|
|
200
|
+
if (tableFields.length === 0) {
|
|
216
201
|
return yield* Effect.fail(new Error("At least one field is required"));
|
|
217
202
|
}
|
|
218
203
|
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
return yield* Effect.fail(
|
|
225
|
-
new Error(
|
|
226
|
-
`Partition key field "${partitionKeyName}" not found in fields. Available fields: ${topicFields.map((field) => field.name).join(", ")}`,
|
|
227
|
-
),
|
|
204
|
+
const resolveField = (fieldName: string, role: string): bigint => {
|
|
205
|
+
const field = tableFields.find((f) => f.name === fieldName);
|
|
206
|
+
if (!field) {
|
|
207
|
+
throw new Error(
|
|
208
|
+
`${role} field "${fieldName}" not found. Available fields: ${tableFields.map((f) => f.name).join(", ")}`,
|
|
228
209
|
);
|
|
229
210
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
}
|
|
238
|
-
partitionKeyId = partitionField.id;
|
|
239
|
-
}
|
|
211
|
+
return field.id;
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const keyFieldId = yield* Effect.try({
|
|
215
|
+
try: () => resolveField(keyField, "Key"),
|
|
216
|
+
catch: (e) => (e instanceof Error ? e : new Error(String(e))),
|
|
217
|
+
});
|
|
240
218
|
|
|
241
|
-
const
|
|
219
|
+
const versionFieldId = yield* Effect.try({
|
|
220
|
+
try: () => resolveField(versionField, "Version"),
|
|
221
|
+
catch: (e) => (e instanceof Error ? e : new Error(String(e))),
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const partitionFieldName = Option.getOrUndefined(partitionField);
|
|
225
|
+
const partitionFieldId = partitionFieldName
|
|
226
|
+
? yield* Effect.try({
|
|
227
|
+
try: () => resolveField(partitionFieldName, "Partition"),
|
|
228
|
+
catch: (e) => (e instanceof Error ? e : new Error(String(e))),
|
|
229
|
+
})
|
|
230
|
+
: undefined;
|
|
242
231
|
|
|
243
232
|
const s = p.spinner();
|
|
244
|
-
s.start("Creating
|
|
233
|
+
s.start("Creating table...");
|
|
245
234
|
|
|
246
|
-
const
|
|
235
|
+
const table = yield* ClusterClient.createTable({
|
|
247
236
|
parent,
|
|
248
|
-
|
|
237
|
+
tableId,
|
|
249
238
|
description: Option.getOrUndefined(description),
|
|
250
|
-
fields:
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
Effect.provide(layer),
|
|
259
|
-
Effect.tapError(() => Effect.sync(() => s.stop("Failed to create topic"))),
|
|
260
|
-
);
|
|
261
|
-
|
|
262
|
-
s.stop("Topic created successfully");
|
|
239
|
+
fields: tableFields,
|
|
240
|
+
keyFieldId,
|
|
241
|
+
versionFieldId,
|
|
242
|
+
partitionFieldId,
|
|
243
|
+
targetFreshnessSeconds: BigInt(freshnessSeconds),
|
|
244
|
+
}).pipe(Effect.tapError(() => Effect.sync(() => s.stop("Failed to create table"))));
|
|
245
|
+
|
|
246
|
+
s.stop("Table created successfully");
|
|
263
247
|
|
|
264
248
|
yield* Effect.sync(() => {
|
|
265
249
|
printTable([
|
|
266
250
|
{
|
|
267
|
-
name:
|
|
268
|
-
description:
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
freshness_seconds: topic.compaction.freshnessSeconds.toString(),
|
|
275
|
-
ttl_seconds: topic.compaction.ttlSeconds?.toString() || "-",
|
|
276
|
-
target_file_size_bytes: topic.compaction.targetFileSizeBytes.toString(),
|
|
277
|
-
fields_count: topic.schema.fields.length.toString(),
|
|
251
|
+
name: table.name,
|
|
252
|
+
description: table.description || "-",
|
|
253
|
+
key_field_id: table.keyFieldId.toString(),
|
|
254
|
+
version_field_id: table.versionFieldId.toString(),
|
|
255
|
+
partition_field_id: table.partitionFieldId?.toString() || "-",
|
|
256
|
+
freshness_seconds: table.targetFreshnessSeconds.toString(),
|
|
257
|
+
fields_count: table.schema.fields.length.toString(),
|
|
278
258
|
},
|
|
279
259
|
]);
|
|
280
260
|
|
|
281
|
-
if (
|
|
261
|
+
if (table.schema.fields.length > 0) {
|
|
282
262
|
console.log("\nFields:");
|
|
283
263
|
printTable(
|
|
284
|
-
|
|
285
|
-
index: index.toString(),
|
|
264
|
+
table.schema.fields.map((field) => ({
|
|
286
265
|
name: field.name,
|
|
266
|
+
id: field.id.toString(),
|
|
287
267
|
type: field.arrowType?._tag ?? "unknown",
|
|
288
268
|
nullable: field.nullable ? "yes" : "no",
|
|
289
269
|
})),
|
|
@@ -293,4 +273,7 @@ export const createTopicCommand = Command.make(
|
|
|
293
273
|
p.outro("✓ Done");
|
|
294
274
|
});
|
|
295
275
|
}),
|
|
296
|
-
).pipe(
|
|
276
|
+
).pipe(
|
|
277
|
+
Command.withDescription("Create a new table belonging to a namespace"),
|
|
278
|
+
Command.provide(({ host, port }) => makeClusterClientLayer(host, port)),
|
|
279
|
+
);
|
|
@@ -1,32 +1,32 @@
|
|
|
1
1
|
import * as p from "@clack/prompts";
|
|
2
|
-
import {
|
|
2
|
+
import { ClusterClient } from "@useairfoil/wings";
|
|
3
3
|
import { Effect } from "effect";
|
|
4
4
|
import { Command, Flag } from "effect/unstable/cli";
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { makeClusterClientLayer } from "../../../utils/client";
|
|
7
7
|
import { forceOption, hostOption, portOption } from "../../../utils/options";
|
|
8
8
|
|
|
9
9
|
const nameOption = Flag.string("name").pipe(
|
|
10
|
-
Flag.withDescription("
|
|
10
|
+
Flag.withDescription("Table name in format: namespaces/{namespace}/tables/{table}"),
|
|
11
11
|
);
|
|
12
12
|
|
|
13
|
-
export const
|
|
14
|
-
"delete-
|
|
13
|
+
export const deleteTableCommand = Command.make(
|
|
14
|
+
"delete-table",
|
|
15
15
|
{
|
|
16
16
|
name: nameOption,
|
|
17
17
|
force: forceOption,
|
|
18
18
|
host: hostOption,
|
|
19
19
|
port: portOption,
|
|
20
20
|
},
|
|
21
|
-
({ name, force
|
|
21
|
+
({ name, force }) =>
|
|
22
22
|
Effect.gen(function* () {
|
|
23
|
-
p.intro("🗑️ Delete
|
|
23
|
+
p.intro("🗑️ Delete Table");
|
|
24
24
|
|
|
25
25
|
if (!force) {
|
|
26
26
|
const confirm = yield* Effect.tryPromise({
|
|
27
27
|
try: () =>
|
|
28
28
|
p.confirm({
|
|
29
|
-
message: `Are you sure you want to delete
|
|
29
|
+
message: `Are you sure you want to delete table ${name}?`,
|
|
30
30
|
initialValue: false,
|
|
31
31
|
}),
|
|
32
32
|
catch: () => new Error("Failed to confirm deletion"),
|
|
@@ -34,23 +34,21 @@ export const deleteTenantCommand = Command.make(
|
|
|
34
34
|
|
|
35
35
|
if (p.isCancel(confirm) || !confirm) {
|
|
36
36
|
p.cancel("Deletion cancelled");
|
|
37
|
-
|
|
37
|
+
return;
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
const layer = makeClusterMetadataLayer(host, port);
|
|
42
|
-
|
|
43
41
|
const s = p.spinner();
|
|
44
|
-
s.start("Deleting
|
|
42
|
+
s.start("Deleting table...");
|
|
45
43
|
|
|
46
|
-
yield*
|
|
47
|
-
Effect.
|
|
48
|
-
Effect.tapError(() => Effect.sync(() => s.stop("Failed to delete tenant"))),
|
|
44
|
+
yield* ClusterClient.deleteTable({ name }).pipe(
|
|
45
|
+
Effect.tapError(() => Effect.sync(() => s.stop("Failed to delete table"))),
|
|
49
46
|
);
|
|
50
47
|
|
|
51
|
-
s.stop("
|
|
48
|
+
s.stop("Table deleted successfully");
|
|
52
49
|
p.outro("✓ Done");
|
|
53
50
|
}),
|
|
54
51
|
).pipe(
|
|
55
|
-
Command.withDescription("Delete a
|
|
52
|
+
Command.withDescription("Delete a table from the cluster"),
|
|
53
|
+
Command.provide(({ host, port }) => makeClusterClientLayer(host, port)),
|
|
56
54
|
);
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import * as p from "@clack/prompts";
|
|
2
|
+
import { ClusterClient } from "@useairfoil/wings";
|
|
3
|
+
import { printTable } from "console-table-printer";
|
|
4
|
+
import { Effect } from "effect";
|
|
5
|
+
import { Command, Flag } from "effect/unstable/cli";
|
|
6
|
+
|
|
7
|
+
import { makeClusterClientLayer } from "../../../utils/client";
|
|
8
|
+
import { hostOption, portOption } from "../../../utils/options";
|
|
9
|
+
|
|
10
|
+
const nameOption = Flag.string("name").pipe(
|
|
11
|
+
Flag.withDescription("Table name in format: namespaces/{namespace}/tables/{table}"),
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
export const getTableCommand = Command.make(
|
|
15
|
+
"get-table",
|
|
16
|
+
{
|
|
17
|
+
name: nameOption,
|
|
18
|
+
host: hostOption,
|
|
19
|
+
port: portOption,
|
|
20
|
+
},
|
|
21
|
+
({ name }) =>
|
|
22
|
+
Effect.gen(function* () {
|
|
23
|
+
const s = p.spinner();
|
|
24
|
+
s.start("Fetching table...");
|
|
25
|
+
|
|
26
|
+
const table = yield* ClusterClient.getTable({ name }).pipe(
|
|
27
|
+
Effect.tapError(() => Effect.sync(() => s.stop("Failed to get table"))),
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
s.stop("Table retrieved");
|
|
31
|
+
|
|
32
|
+
yield* Effect.sync(() => {
|
|
33
|
+
printTable([
|
|
34
|
+
{
|
|
35
|
+
name: table.name,
|
|
36
|
+
description: table.description || "-",
|
|
37
|
+
key_field_id: table.keyFieldId.toString(),
|
|
38
|
+
version_field_id: table.versionFieldId.toString(),
|
|
39
|
+
partition_field_id: table.partitionFieldId?.toString() || "-",
|
|
40
|
+
freshness_seconds: table.targetFreshnessSeconds.toString(),
|
|
41
|
+
},
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
if (table.schema.fields.length > 0) {
|
|
45
|
+
console.log("\nFields:");
|
|
46
|
+
printTable(
|
|
47
|
+
table.schema.fields.map((field) => ({
|
|
48
|
+
name: field.name,
|
|
49
|
+
id: field.id.toString(),
|
|
50
|
+
type: field.arrowType?._tag ?? "unknown",
|
|
51
|
+
nullable: field.nullable ? "yes" : "no",
|
|
52
|
+
})),
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
p.outro("✓ Done");
|
|
57
|
+
});
|
|
58
|
+
}),
|
|
59
|
+
).pipe(
|
|
60
|
+
Command.withDescription("Get details of a specific table"),
|
|
61
|
+
Command.provide(({ host, port }) => makeClusterClientLayer(host, port)),
|
|
62
|
+
);
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import * as p from "@clack/prompts";
|
|
2
|
+
import { ClusterClient } from "@useairfoil/wings";
|
|
3
|
+
import { printTable } from "console-table-printer";
|
|
4
|
+
import { Effect, Option } from "effect";
|
|
5
|
+
import { Command, Flag } from "effect/unstable/cli";
|
|
6
|
+
|
|
7
|
+
import { makeClusterClientLayer } from "../../../utils/client";
|
|
8
|
+
import { hostOption, pageSizeOption, pageTokenOption, portOption } from "../../../utils/options";
|
|
9
|
+
|
|
10
|
+
const parentOption = Flag.string("parent").pipe(
|
|
11
|
+
Flag.withDescription("Parent namespace in format: namespaces/{namespace}"),
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
export const listTablesCommand = Command.make(
|
|
15
|
+
"list-tables",
|
|
16
|
+
{
|
|
17
|
+
parent: parentOption,
|
|
18
|
+
pageSize: pageSizeOption,
|
|
19
|
+
pageToken: pageTokenOption,
|
|
20
|
+
host: hostOption,
|
|
21
|
+
port: portOption,
|
|
22
|
+
},
|
|
23
|
+
({ parent, pageSize, pageToken }) =>
|
|
24
|
+
Effect.gen(function* () {
|
|
25
|
+
const s = p.spinner();
|
|
26
|
+
s.start("Fetching tables...");
|
|
27
|
+
|
|
28
|
+
const response = yield* ClusterClient.listTables({
|
|
29
|
+
parent,
|
|
30
|
+
pageSize,
|
|
31
|
+
pageToken: Option.getOrUndefined(pageToken),
|
|
32
|
+
}).pipe(Effect.tapError(() => Effect.sync(() => s.stop("Failed to list tables"))));
|
|
33
|
+
|
|
34
|
+
s.stop(`Found ${response.tables.length} table(s)`);
|
|
35
|
+
|
|
36
|
+
yield* Effect.sync(() => {
|
|
37
|
+
if (response.tables.length === 0) {
|
|
38
|
+
p.log.warn("No tables found");
|
|
39
|
+
} else {
|
|
40
|
+
printTable(
|
|
41
|
+
response.tables.map((table) => ({
|
|
42
|
+
name: table.name,
|
|
43
|
+
description: table.description || "-",
|
|
44
|
+
key_field_id: table.keyFieldId.toString(),
|
|
45
|
+
partition_field_id: table.partitionFieldId?.toString() || "-",
|
|
46
|
+
freshness_seconds: table.targetFreshnessSeconds.toString(),
|
|
47
|
+
})),
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (response.nextPageToken) {
|
|
52
|
+
p.log.info(`Next page token: ${response.nextPageToken}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
p.outro("✓ Done");
|
|
56
|
+
});
|
|
57
|
+
}),
|
|
58
|
+
).pipe(
|
|
59
|
+
Command.withDescription("List all tables belonging to a namespace"),
|
|
60
|
+
Command.provide(({ host, port }) => makeClusterClientLayer(host, port)),
|
|
61
|
+
);
|
package/src/commands/dev.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { Effect, FileSystem } from "effect";
|
|
2
2
|
import { Command, Flag, Prompt } from "effect/unstable/cli";
|
|
3
|
-
import { ChildProcess
|
|
4
|
-
|
|
5
|
-
import {
|
|
3
|
+
import { ChildProcess } from "effect/unstable/process";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
checkDockerVersion,
|
|
7
|
+
createDockerVolume,
|
|
8
|
+
pullDockerImage,
|
|
9
|
+
runDockerContainer,
|
|
10
|
+
} from "../utils/docker";
|
|
6
11
|
import { downloadWings, getWingsPath } from "../utils/wings";
|
|
7
12
|
|
|
8
13
|
const dockerOption = Flag.boolean("docker").pipe(
|
|
@@ -11,7 +16,7 @@ const dockerOption = Flag.boolean("docker").pipe(
|
|
|
11
16
|
);
|
|
12
17
|
|
|
13
18
|
const versionOption = Flag.string("version").pipe(
|
|
14
|
-
Flag.withDescription("Specify Wings version (e.g., v0.1.0-alpha.
|
|
19
|
+
Flag.withDescription("Specify Wings version (e.g., v0.1.0-alpha.14 or 'latest')"),
|
|
15
20
|
Flag.withDefault("latest"),
|
|
16
21
|
);
|
|
17
22
|
|
|
@@ -55,7 +60,6 @@ export const devCommand = Command.make(
|
|
|
55
60
|
|
|
56
61
|
const runWithBinary = (options: { version: string; yes: boolean; stress: boolean }) =>
|
|
57
62
|
Effect.gen(function* () {
|
|
58
|
-
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
59
63
|
const fs = yield* FileSystem.FileSystem;
|
|
60
64
|
|
|
61
65
|
yield* Effect.logInfo("🪽 Airfoil Dev");
|
|
@@ -90,17 +94,15 @@ const runWithBinary = (options: { version: string; yes: boolean; stress: boolean
|
|
|
90
94
|
|
|
91
95
|
yield* Effect.logInfo(`Starting Wings dev server...`);
|
|
92
96
|
|
|
93
|
-
const
|
|
97
|
+
const handle = yield* ChildProcess.make(wingsPath, ["dev"], {
|
|
94
98
|
stdin: "inherit",
|
|
95
99
|
stdout: "inherit",
|
|
96
100
|
});
|
|
97
101
|
|
|
98
|
-
const handle = yield* spawner.spawn(proc);
|
|
99
|
-
|
|
100
102
|
const exitCode = yield* handle.exitCode;
|
|
101
103
|
|
|
102
104
|
if (exitCode !== 0) {
|
|
103
|
-
yield* Effect.fail(new Error(`Process exited with code ${exitCode}`));
|
|
105
|
+
return yield* Effect.fail(new Error(`Process exited with code ${exitCode}`));
|
|
104
106
|
}
|
|
105
107
|
});
|
|
106
108
|
|
|
@@ -110,11 +112,8 @@ const runWithDocker = (options: { tag: string; forcePull: boolean }) =>
|
|
|
110
112
|
|
|
111
113
|
let tag = options.tag;
|
|
112
114
|
|
|
113
|
-
// If tag is not "latest" and doesn't include architecture, append it
|
|
114
|
-
// Format: 0.1.0-alpha.10-aarch64 or 0.1.0-alpha.10-x86_64
|
|
115
115
|
if (tag !== "latest" && !tag.includes("aarch64") && !tag.includes("x86_64")) {
|
|
116
116
|
const arch = process.arch === "arm64" ? "aarch64" : "x86_64";
|
|
117
|
-
// Remove 'v' prefix if present for docker tags
|
|
118
117
|
const cleanTag = tag.startsWith("v") ? tag.substring(1) : tag;
|
|
119
118
|
tag = `${cleanTag}-${arch}`;
|
|
120
119
|
}
|
|
@@ -122,10 +121,15 @@ const runWithDocker = (options: { tag: string; forcePull: boolean }) =>
|
|
|
122
121
|
const image = `docker.useairfoil.com/airfoil/wings:${tag}`;
|
|
123
122
|
|
|
124
123
|
yield* Effect.logInfo(`Using Docker image: ${image}`);
|
|
125
|
-
yield* Effect.logInfo("Ports: 7777 (gRPC)
|
|
124
|
+
yield* Effect.logInfo("Ports: 7777 (gRPC)");
|
|
126
125
|
|
|
127
126
|
yield* checkDockerVersion();
|
|
128
127
|
|
|
128
|
+
if (options.forcePull) {
|
|
129
|
+
yield* Effect.logInfo(`Pulling Docker image: ${image}`);
|
|
130
|
+
yield* pullDockerImage(image);
|
|
131
|
+
}
|
|
132
|
+
|
|
129
133
|
yield* createDockerVolume("wings-data");
|
|
130
134
|
|
|
131
135
|
yield* runDockerContainer(image, "wings-data");
|
package/src/commands/sql.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ArrowFlightSqlClient } from "@useairfoil/flight";
|
|
2
2
|
import { printTable } from "console-table-printer";
|
|
3
|
-
import { Effect, FileSystem, Option } from "effect";
|
|
3
|
+
import { Effect, FileSystem, Option, Stream } from "effect";
|
|
4
4
|
import { Argument, Command, Flag } from "effect/unstable/cli";
|
|
5
5
|
import { Metadata } from "nice-grpc-common";
|
|
6
6
|
|
|
@@ -20,7 +20,7 @@ const fileOption = Flag.string("file").pipe(
|
|
|
20
20
|
const namespaceOption = Flag.string("namespace").pipe(
|
|
21
21
|
Flag.withAlias("n"),
|
|
22
22
|
Flag.withDescription("Wings namespace"),
|
|
23
|
-
Flag.withDefault("
|
|
23
|
+
Flag.withDefault("namespaces/default"),
|
|
24
24
|
);
|
|
25
25
|
|
|
26
26
|
const jsonOption = Flag.boolean("json").pipe(
|
|
@@ -58,32 +58,22 @@ export const sqlCommand = Command.make(
|
|
|
58
58
|
|
|
59
59
|
yield* Effect.logInfo(`Namespace: ${namespace}`);
|
|
60
60
|
|
|
61
|
-
const client =
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
metadata: Metadata({
|
|
69
|
-
"x-wings-namespace": namespace,
|
|
70
|
-
}),
|
|
71
|
-
},
|
|
61
|
+
const client = yield* ArrowFlightSqlClient.make({
|
|
62
|
+
host: hostPort,
|
|
63
|
+
defaultCallOptions: {
|
|
64
|
+
"*": {
|
|
65
|
+
metadata: Metadata({
|
|
66
|
+
"x-wings-namespace": namespace,
|
|
67
|
+
}),
|
|
72
68
|
},
|
|
73
69
|
},
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
const flightInfo = yield* Effect.tryPromise({
|
|
77
|
-
try: () => client.executeQuery({ query: sqlQuery }),
|
|
78
|
-
catch: (error) => (error instanceof Error ? error : new Error("Query failed")),
|
|
79
70
|
});
|
|
80
71
|
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
});
|
|
72
|
+
const flightInfo = yield* client.executeQuery({ query: sqlQuery });
|
|
73
|
+
|
|
74
|
+
const batches = yield* client.executeFlightInfo(flightInfo).pipe(Stream.runCollect);
|
|
85
75
|
|
|
86
|
-
const data = batches.flatMap((batch) => batch.toArray());
|
|
76
|
+
const data = Array.from(batches).flatMap(({ batch }) => batch.toArray());
|
|
87
77
|
|
|
88
78
|
if (json) {
|
|
89
79
|
yield* Effect.log(JSON.stringify(data, null, 2));
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { NodeRuntime, NodeServices } from "@effect/platform-node";
|
|
3
|
-
import { Effect } from "effect";
|
|
3
|
+
import { Effect, Layer } from "effect";
|
|
4
4
|
import { Command } from "effect/unstable/cli";
|
|
5
5
|
import { FetchHttpClient } from "effect/unstable/http";
|
|
6
6
|
|
|
@@ -19,9 +19,9 @@ const cli = Command.run(program, {
|
|
|
19
19
|
version,
|
|
20
20
|
});
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
NodeRuntime.runMain(
|
|
23
|
+
cli.pipe(
|
|
24
|
+
Effect.provide(Layer.mergeAll(FetchHttpClient.layer, NodeServices.layer)),
|
|
25
|
+
Effect.scoped,
|
|
26
|
+
),
|
|
27
27
|
);
|
package/src/utils/client.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ClusterClient } from "@useairfoil/wings";
|
|
2
2
|
|
|
3
|
-
export const
|
|
4
|
-
|
|
3
|
+
export const makeClusterClientLayer = (host: string, port: number) =>
|
|
4
|
+
ClusterClient.layer({
|
|
5
5
|
host: `${host}:${port}`,
|
|
6
6
|
});
|