diff --git a/packages/appkit/src/type-generator/query-registry.ts b/packages/appkit/src/type-generator/query-registry.ts index dd2202a3..9adcc5c5 100644 --- a/packages/appkit/src/type-generator/query-registry.ts +++ b/packages/appkit/src/type-generator/query-registry.ts @@ -168,10 +168,14 @@ export async function generateQueriesFromDescribe( })) as DatabricksStatementExecutionResponse; if (result.status.state === "FAILED") { + const sqlError = + result.status.error?.message || "Query execution failed"; spinner.stop(`✗ ${queryName} - failed`); + spinner.printDetail(`SQL Error: ${sqlError}`); + spinner.printDetail(`Query: ${cleanedSql.slice(0, 200)}`); failedQueries.push({ name: queryName, - error: "Query execution failed", + error: sqlError, }); continue; } diff --git a/packages/appkit/src/type-generator/spinner.ts b/packages/appkit/src/type-generator/spinner.ts index 4949a4e5..5a6e04b6 100644 --- a/packages/appkit/src/type-generator/spinner.ts +++ b/packages/appkit/src/type-generator/spinner.ts @@ -25,4 +25,8 @@ export class Spinner { // clear the line and write the final text process.stdout.write(`\x1b[2K\r ${finalText || this.text}\n`); } + + printDetail(text: string) { + process.stdout.write(`\x1b[2m ${text}\x1b[0m\n`); + } } diff --git a/packages/appkit/src/type-generator/tests/generate-queries.test.ts b/packages/appkit/src/type-generator/tests/generate-queries.test.ts new file mode 100644 index 00000000..81b583bf --- /dev/null +++ b/packages/appkit/src/type-generator/tests/generate-queries.test.ts @@ -0,0 +1,114 @@ +import { beforeEach, describe, expect, test, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ + readdirSync: vi.fn(), + readFileSync: vi.fn(), + executeStatement: vi.fn(), + spinnerStop: vi.fn(), + spinnerPrintDetail: vi.fn(), + loadCache: vi.fn(() => ({ version: "1", queries: {} })), + saveCache: vi.fn(), +})); + +vi.mock("node:fs", () => ({ + default: { + readdirSync: mocks.readdirSync, + readFileSync: mocks.readFileSync, + }, +})); + +vi.mock("@databricks/sdk-experimental", () => ({ + WorkspaceClient: vi.fn(() => ({ + statementExecution: { executeStatement: mocks.executeStatement }, + })), +})); + +vi.mock("../spinner", () => ({ + Spinner: vi.fn(() => ({ + start: vi.fn(), + stop: mocks.spinnerStop, + printDetail: mocks.spinnerPrintDetail, + })), +})); + +vi.mock("../cache", async (importOriginal) => { + const actual = (await importOriginal()) as Record; + return { ...actual, loadCache: mocks.loadCache, saveCache: mocks.saveCache }; +}); + +const { generateQueriesFromDescribe } = await import("../query-registry"); + +function succeededResult(columns: [string, string, string | null][]) { + return { + statement_id: "stmt-1", + status: { state: "SUCCEEDED" }, + result: { data_array: columns }, + }; +} + +describe("generateQueriesFromDescribe", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + test("success path — returns query schema", async () => { + mocks.readdirSync.mockReturnValue(["users.sql"]); + mocks.readFileSync.mockReturnValue( + "SELECT id, name FROM users WHERE status = :status", + ); + mocks.executeStatement.mockResolvedValue( + succeededResult([ + ["id", "INT", null], + ["name", "STRING", null], + ]), + ); + + const schemas = await generateQueriesFromDescribe("/queries", "wh-123"); + + expect(schemas).toHaveLength(1); + expect(schemas[0].name).toBe("users"); + expect(schemas[0].type).toContain("id: number"); + expect(schemas[0].type).toContain("name: string"); + expect(mocks.spinnerStop).toHaveBeenCalledWith("✓ users"); + }); + + test("FAILED status with error message — reports SQL error via spinner", async () => { + mocks.readdirSync.mockReturnValue(["bad_table.sql"]); + mocks.readFileSync.mockReturnValue("SELECT * FROM bad_table"); + mocks.executeStatement.mockResolvedValue({ + statement_id: "stmt-2", + status: { + state: "FAILED", + error: { message: "Table or view not found: bad_table" }, + }, + }); + + const schemas = await generateQueriesFromDescribe("/queries", "wh-123"); + + expect(schemas).toHaveLength(0); + expect(mocks.spinnerStop).toHaveBeenCalledWith("✗ bad_table - failed"); + expect(mocks.spinnerPrintDetail).toHaveBeenCalledWith( + "SQL Error: Table or view not found: bad_table", + ); + expect(mocks.spinnerPrintDetail).toHaveBeenCalledWith( + expect.stringContaining("Query:"), + ); + }); + + test("FAILED status without error message — uses fallback message", async () => { + mocks.readdirSync.mockReturnValue(["query.sql"]); + mocks.readFileSync.mockReturnValue("SELECT 1"); + mocks.executeStatement.mockResolvedValue({ + statement_id: "stmt-3", + status: { state: "FAILED" }, + }); + + const schemas = await generateQueriesFromDescribe("/queries", "wh-123"); + + expect(schemas).toHaveLength(0); + expect(mocks.spinnerStop).toHaveBeenCalledWith("✗ query - failed"); + expect(mocks.spinnerPrintDetail).toHaveBeenCalledWith( + "SQL Error: Query execution failed", + ); + }); +}); diff --git a/packages/appkit/src/type-generator/types.ts b/packages/appkit/src/type-generator/types.ts index 97338511..5af43591 100644 --- a/packages/appkit/src/type-generator/types.ts +++ b/packages/appkit/src/type-generator/types.ts @@ -6,7 +6,10 @@ */ export interface DatabricksStatementExecutionResponse { statement_id: string; - status: { state: string }; + status: { + state: string; + error?: { error_code?: string; message?: string }; + }; result?: { data_array?: (string | null)[][]; };