Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 0 additions & 9 deletions .env.example

This file was deleted.

62 changes: 53 additions & 9 deletions app/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,60 @@

import { sql } from "@vercel/postgres";
import { Result } from "@/lib/types";
import { generateObject } from 'ai';
import { openai } from '@ai-sdk/openai';

import { explanationSchema } from "@/lib/types";
import { z } from 'zod';


export const generateQuery = async (input: string) => {
'use server';
try {
const result = await generateObject({
model: openai('gpt-4o'),
system: `You are a SQL (postgres) ...

[full system prompt here.]`,
prompt: `Generate the query necessary to retrieve the data the user wants: ${input}`,
schema: z.object({
query: z.string(),
}),
});
return result.object.query;
} catch (e) {
console.error(e);
throw new Error('Failed to generate query');
}
};

export const explainQuery = async (input: string, sqlQuery: string) => {
'use server';
try {
const result = await generateObject({
model: openai('gpt-4o'),
system: `You are a SQL (postgres) expert. ...

[full system prompt here.]`,
prompt: `Explain the SQL query you generated to retrieve the data the user wanted. Assume the user is not an expert in SQL. Break down the query into steps. Be concise.

User Query:
${input}

Generated SQL Query:
${sqlQuery}`,
schema: explanationSchema,
output: 'array',
});
return result.object;
} catch (e) {
console.error(e);
throw new Error('Failed to generate query');
}
};

/**
* Executes a SQL query and returns the result data
* @param {string} query - The SQL query to execute
* @returns {Promise<Result[]>} Array of query results
* @throws {Error} If query is not a SELECT statement or table doesn't exist
*/
export const runGeneratedSQLQuery = async (query: string) => {
"use server";
// Ensure the query is a SELECT statement. Otherwise, throw an error
if (
!query.trim().toLowerCase().startsWith("select") ||
query.trim().toLowerCase().includes("drop") ||
Expand All @@ -31,7 +75,7 @@ export const runGeneratedSQLQuery = async (query: string) => {
try {
data = await sql.query(query);
} catch (e: any) {
if (e.message.includes('relation "unicorns" does not exist')) {
if (e.message.includes('relation "relation" does not exist')) {
Copy link

@vercel vercel bot Nov 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (e.message.includes('relation "relation" does not exist')) {
if (e.message.includes('relation "unicorns" does not exist')) {

The table name in the error message check was changed from "unicorns" to "relation", which will prevent the error handler from catching the expected error condition. This is likely a copy-paste error from a template.

View Details

Analysis

Incorrect table name in error handler prevents missing table detection

What fails: The runGeneratedSQLQuery() function fails to catch missing table errors because it checks for 'relation "relation" does not exist' instead of the actual table name 'relation "unicorns" does not exist'

How to reproduce:

  1. The application only supports queries against the "unicorns" table (see lib/seed.ts)
  2. Delete or drop the unicorns table from PostgreSQL
  3. Run any SELECT query through the UI that would query the table
  4. The error is re-thrown instead of being caught

Result: PostgreSQL returns error message: 'relation "unicorns" does not exist' (includes actual table name per PostgreSQL error format)

The condition e.message.includes('relation "relation" does not exist') never matches, so the error falls through to else { throw e; } and propagates to the user instead of being handled gracefully.

Expected: According to PostgreSQL documentation and error message format specifications, when a table doesn't exist, the error message includes the actual table name: 'relation "unicorns" does not exist'. The check should match this specific error to catch it and handle the missing table condition.

Fix: Changed line 78 from checking for 'relation "relation" does not exist' to the correct 'relation "unicorns" does not exist'

console.log(
"Table does not exist, creating and seeding it with dummy data now...",
);
Expand All @@ -43,4 +87,4 @@ export const runGeneratedSQLQuery = async (query: string) => {
}

return data.rows as Result[];
};
};
4 changes: 2 additions & 2 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Loader2 } from "lucide-react";
import { toast } from "sonner";

import { Config, Result } from "@/lib/types";
import { runGeneratedSQLQuery } from "./actions";
import { runGeneratedSQLQuery, generateQuery } from "./actions";

import { Header } from "@/components/header";
import { QueryViewer } from "@/components/query-viewer";
Expand Down Expand Up @@ -45,7 +45,7 @@ export default function Page() {
setActiveQuery("");

try {
const query = "TODO - IMPLEMENT ABOVE"; // placeholder value
const query = await generateQuery(question);

if (query === undefined) {
toast.error("An error occurred. Please try again.");
Expand Down
26 changes: 0 additions & 26 deletions components/project-info.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,11 @@
import { Info } from "lucide-react";
import { DeployButton } from "./deploy-button";
import { Alert, AlertDescription } from "./ui/alert";
import Link from "next/link";

export const ProjectInfo = () => {
return (
<div className="bg-muted p-4 mt-auto">
<Alert className="bg-muted text-muted-foreground border-0">
<Info className="h-4 w-4 text-primary" />
<AlertDescription>
This application uses the{" "}
<Link
target="_blank"
className="text-primary hover:text-primary/90 underline"
href="https://sdk.vercel.ai"
>
AI SDK
</Link>{" "}
to allow you to query a PostgreSQL database with natural language. The
dataset is CB Insights&apos; list of all unicorn companies. Learn more
at{" "}
<Link
href="https://www.cbinsights.com/research-unicorn-companies"
target="_blank"
className="text-primary hover:text-primary/90 underline"
>
CB Insights
</Link>
.
<div className="mt-4 sm:hidden">
<DeployButton />
</div>
</AlertDescription>
</Alert>
</div>
);
Expand Down
5 changes: 3 additions & 2 deletions components/query-viewer.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useState } from "react";
import { Button } from "./ui/button";
import { QueryWithTooltips } from "./ui/query-with-tooltips";
import { explainQuery } from '@/app/actions';
import { QueryExplanation } from "@/lib/types";
import { CircleHelp, Loader2 } from "lucide-react";

Expand All @@ -21,8 +22,8 @@ export const QueryViewer = ({
setQueryExpanded(true);
setLoadingExplanation(true);

// TODO: generate explanation and update state
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The handleExplainQuery function lacks error handling. If explainQuery throws an error, setLoadingExplanation(false) won't be called, leaving the UI in a loading state indefinitely.

View Details
📝 Patch Details
diff --git a/components/query-viewer.tsx b/components/query-viewer.tsx
index 216d2ca..7646f9b 100644
--- a/components/query-viewer.tsx
+++ b/components/query-viewer.tsx
@@ -4,6 +4,7 @@ import { QueryWithTooltips } from "./ui/query-with-tooltips";
 import { explainQuery } from '@/app/actions';
 import { QueryExplanation } from "@/lib/types";
 import { CircleHelp, Loader2 } from "lucide-react";
+import { toast } from "sonner";
 
 export const QueryViewer = ({
   activeQuery,
@@ -22,9 +23,15 @@ export const QueryViewer = ({
     setQueryExpanded(true);
     setLoadingExplanation(true);
 
-const explanations = await explainQuery(inputValue, activeQuery);
-  setQueryExplanations(explanations);
-    setLoadingExplanation(false);
+    try {
+      const explanations = await explainQuery(inputValue, activeQuery);
+      setQueryExplanations(explanations);
+    } catch (error) {
+      console.error('Failed to generate explanation:', error);
+      toast.error('Failed to generate explanation. Please try again.');
+    } finally {
+      setLoadingExplanation(false);
+    }
   };
 
   if (activeQuery.length === 0) return null;

Analysis

Missing error handling in handleExplainQuery leaves UI in loading state

What fails: QueryViewer.handleExplainQuery() lacks error handling. When explainQuery() throws an error, setLoadingExplanation(false) is never called, leaving the spinner visible indefinitely.

How to reproduce:

  1. Navigate to the query explanation feature
  2. Click "Explain query" button when explainQuery fails (e.g., API error, network failure)
  3. The loading spinner remains visible
  4. No error message displayed to the user

Result: Unhandled promise rejection; loading state remains true; spinner never stops; user receives no feedback about the failure.

Expected: Errors should be caught, loading state should always be cleared, and user should receive error feedback via toast notification.

Fix implemented: Added try-catch-finally block to handleExplainQuery that:

  • Catches errors from explainQuery()
  • Displays error toast notification to user
  • Always clears loading state in finally block

This follows the same error handling pattern used elsewhere in the codebase (see app/page.tsx for toast error patterns).


const explanations = await explainQuery(inputValue, activeQuery);
setQueryExplanations(explanations);
setLoadingExplanation(false);
};

Expand Down
10 changes: 10 additions & 0 deletions lib/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { z } from 'zod';


export type Unicorn = {
id: number;
company: string;
Expand All @@ -9,6 +12,13 @@ export type Unicorn = {
select_investors: string;
};

export const explanationSchema = z.object({
section: z.string(),
explanation: z.string(),
});

export type QueryExplanation = z.infer<typeof explanationSchema>;

export type Result = Record<string, string | number>;

export type Config = any;