diff --git a/.changeset/fix-r2-sql-nested-objects.md b/.changeset/fix-r2-sql-nested-objects.md new file mode 100644 index 000000000000..1b1b669ab413 --- /dev/null +++ b/.changeset/fix-r2-sql-nested-objects.md @@ -0,0 +1,7 @@ +--- +"wrangler": patch +--- + +Fix `wrangler r2 sql query` displaying `[object Object]` for nested values + +SQL functions that return complex types such as arrays of objects (e.g. `approx_top_k`) were rendered as `[object Object]` in the table output because `String()` was called directly on non-primitive values. These values are now serialized with `JSON.stringify` so they display as readable JSON strings. diff --git a/packages/wrangler/src/__tests__/r2/sql.test.ts b/packages/wrangler/src/__tests__/r2/sql.test.ts index dd7930a007b9..414741712876 100644 --- a/packages/wrangler/src/__tests__/r2/sql.test.ts +++ b/packages/wrangler/src/__tests__/r2/sql.test.ts @@ -223,6 +223,83 @@ describe("r2 sql", () => { ).rejects.toThrow("Received a malformed response from the API"); }); + it("should handle nested objects (as JSON with null converted to '') in query results", async () => { + const mockResponse = { + success: true, + errors: [], + messages: [], + result: { + request_id: "dqe-prod-test", + schema: [ + { + name: "approx_top_k(value, Int64(3))", + descriptor: { + type: { + name: "list", + item: { + type: { + name: "struct", + fields: [ + { + type: { name: "int64" }, + nullable: true, + name: "value", + }, + { + type: { name: "uint64" }, + nullable: false, + name: "count", + }, + ], + }, + nullable: true, + }, + }, + nullable: true, + }, + }, + ], + rows: [ + { + "approx_top_k(value, Int64(3))": [ + { value: 0, count: 961 }, + { value: 1, count: 485 }, + { value: 2, count: null }, + ], + }, + ], + metrics: { + r2_requests_count: 6, + files_scanned: 3, + bytes_scanned: 62878, + }, + }, + }; + + msw.use( + http.post( + "https://api.sql.cloudflarestorage.com/api/v1/accounts/:accountId/r2-sql/query/:bucketName", + async () => { + return HttpResponse.json(mockResponse); + }, + { once: true } + ) + ); + + await runWrangler(`r2 sql query ${mockWarehouse} "${mockQuery}"`); + + const startOfTable = std.out.indexOf("┌"); + const endOfTable = std.out.indexOf("┘") + 1; + + expect(std.out.slice(startOfTable, endOfTable)).toMatchInlineSnapshot(` + "┌─┐ + │ approx_top_k(value, Int64(3)) │ + ├─┤ + │ [{\\"value\\":0,\\"count\\":961},{\\"value\\":1,\\"count\\":485},{\\"value\\":2,\\"count\\":\\"\\"}] │ + └─┘" + `); + }); + it("should handle null values in query results", async () => { const mockResponse = { success: true, diff --git a/packages/wrangler/src/r2/sql.ts b/packages/wrangler/src/r2/sql.ts index c5138687168c..b7e59eb125cf 100644 --- a/packages/wrangler/src/r2/sql.ts +++ b/packages/wrangler/src/r2/sql.ts @@ -37,7 +37,19 @@ function formatSqlResults(data: SqlQueryResponse, duration: number): void { logger.table( rows.map((row) => Object.fromEntries( - column_order.map((column) => [column, String(row[column] ?? "")]) + column_order.map((column) => { + const value = row[column]; + if (value === null || value === undefined) { + return [column, ""]; + } + if (typeof value === "object") { + return [ + column, + JSON.stringify(value, (_k, v) => (v === null ? "" : v)), + ]; + } + return [column, String(value)]; + }) ) ), { wordWrap: true, head: column_order }