Natural Language to PostgreSQL: Querying with AI
Natural-language SQL is useful when someone knows the question but not the schema or syntax. The model can turn “show me users who signed up last month” into a query, but I would never let generated SQL run without validation and database permissions doing the final work.
What the model needs to know
The model needs enough information to write a sensible query:
- Complex Syntax: JOINs, subqueries, window functions
- Schema Knowledge: Understanding table structures and relationships
- Performance Optimization: Writing efficient queries
- Error Handling: Debugging syntax and logic errors
- Accessibility: Making databases usable by non-technical users
AI can reduce the amount of SQL a person has to write, but it does not remove the need to understand the data or protect the database. A plausible query can still answer the wrong question, scan too much data or expose something it should not.
The request path
A concrete example
Implementation with the Vercel AI SDK
Start with a read-only converter
// lib/nl2sql/basicConverter.ts
import { openai } from '@ai-sdk/openai'
import { streamText } from 'ai'
interface DatabaseSchema {
tables: TableSchema[]
relationships: Relationship[]
}
interface TableSchema {
name: string
description: string
columns: ColumnSchema[]
}
interface ColumnSchema {
name: string
type: string
description: string
nullable: boolean
primaryKey?: boolean
foreignKey?: {
table: string
column: string
}
}
interface Relationship {
from: { table: string; column: string }
to: { table: string; column: string }
type: 'one-to-one' | 'one-to-many' | 'many-to-many'
}
interface SQLQuery {
query: string
explanation: string
tables: string[]
estimatedCost: 'low' | 'medium' | 'high'
}
class BasicNL2SQLConverter {
private schema: DatabaseSchema
constructor(schema: DatabaseSchema) {
this.schema = schema
}
async convertToSQL(naturalLanguage: string): Promise<SQLQuery> {
const prompt = this.buildPrompt(naturalLanguage)
const result = await streamText({
model: openai('gpt-4o'),
messages: [{ role: 'user', content: prompt }],
temperature: 0.1,
maxTokens: 1000,
})
try {
const parsed = this.parseSQLResponse(result.text)
return parsed
} catch (error) {
console.error('Failed to parse SQL response:', error)
throw new Error('Failed to convert natural language to SQL')
}
}
private buildPrompt(naturalLanguage: string): string {
const schemaDescription = this.buildSchemaDescription()
return `Convert this natural language question to a PostgreSQL query:
Question: "${naturalLanguage}"
Database Schema:
${schemaDescription}
Requirements:
- Use proper PostgreSQL syntax
- Include appropriate JOINs when needed
- Add comments explaining the query logic
- Optimize for performance
- Handle edge cases (NULL values, etc.)
- Use meaningful column aliases
- Format the query for readability
Respond with a JSON object:
{
"query": "The complete SQL query",
"explanation": "Explanation of what the query does",
"tables": ["table1", "table2"],
"estimatedCost": "low|medium|high"
}
Generate a clean, production-ready PostgreSQL query.`
}
private buildSchemaDescription(): string {
let description = ''
for (const table of this.schema.tables) {
description += `\nTable: ${table.name}\n`
description += `Description: ${table.description}\n`
description += 'Columns:\n'
for (const column of table.columns) {
description += ` - ${column.name} (${column.type})`
if (column.primaryKey) description += ' [PRIMARY KEY]'
if (column.foreignKey) description += ` [FK -> ${column.foreignKey.table}.${column.foreignKey.column}]`
if (!column.nullable) description += ' [NOT NULL]'
description += ` - ${column.description}\n`
}
}
if (this.schema.relationships.length > 0) {
description += '\nRelationships:\n'
for (const rel of this.schema.relationships) {
description += ` - ${rel.from.table}.${rel.from.column} -> ${rel.to.table}.${rel.to.column} (${rel.type})\n`
}
}
return description
}
private parseSQLResponse(text: string): SQLQuery {
// Try to extract JSON from the response
const jsonMatch = text.match(/\{[\s\S]*\}/)
if (!jsonMatch) {
throw new Error('No valid JSON found in response')
}
const parsed = JSON.parse(jsonMatch[0])
return {
query: parsed.query || text,
explanation: parsed.explanation || 'Generated SQL query',
tables: parsed.tables || [],
estimatedCost: parsed.estimatedCost || 'medium',
}
}
async convertMultiple(questions: string[]): Promise<SQLQuery[]> {
const queries: SQLQuery[] = []
for (const question of questions) {
try {
const query = await this.convertToSQL(question)
queries.push(query)
} catch (error) {
console.error(`Failed to convert: ${question}`, error)
queries.push({
query: '-- Failed to generate query',
explanation: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
tables: [],
estimatedCost: 'high',
})
}
}
return queries
}
}
// Example schema
const sampleSchema: DatabaseSchema = {
tables: [
{
name: 'users',
description: 'User accounts and profile information',
columns: [
{ name: 'id', type: 'SERIAL', description: 'Unique user identifier', nullable: false, primaryKey: true },
{ name: 'email', type: 'VARCHAR(255)', description: 'User email address', nullable: false },
{ name: 'name', type: 'VARCHAR(100)', description: 'User full name', nullable: false },
{ name: 'created_at', type: 'TIMESTAMP', description: 'Account creation date', nullable: false },
{ name: 'status', type: 'VARCHAR(20)', description: 'Account status (active, inactive, suspended)', nullable: false },
],
},
{
name: 'orders',
description: 'Customer orders and transactions',
columns: [
{ name: 'id', type: 'SERIAL', description: 'Unique order identifier', nullable: false, primaryKey: true },
{ name: 'user_id', type: 'INTEGER', description: 'Reference to users table', nullable: false, foreignKey: { table: 'users', column: 'id' } },
{ name: 'total_amount', type: 'DECIMAL(10,2)', description: 'Order total amount', nullable: false },
{ name: 'status', type: 'VARCHAR(20)', description: 'Order status (pending, completed, cancelled)', nullable: false },
{ name: 'created_at', type: 'TIMESTAMP', description: 'Order creation date', nullable: false },
],
},
{
name: 'products',
description: 'Product catalog information',
columns: [
{ name: 'id', type: 'SERIAL', description: 'Unique product identifier', nullable: false, primaryKey: true },
{ name: 'name', type: 'VARCHAR(200)', description: 'Product name', nullable: false },
{ name: 'price', type: 'DECIMAL(10,2)', description: 'Product price', nullable: false },
{ name: 'category', type: 'VARCHAR(50)', description: 'Product category', nullable: true },
{ name: 'in_stock', type: 'BOOLEAN', description: 'Product availability', nullable: false },
],
},
],
relationships: [
{
from: { table: 'orders', column: 'user_id' },
to: { table: 'users', column: 'id' },
type: 'many-to-one',
},
],
}
// Usage example
const converter = new BasicNL2SQLConverter(sampleSchema)
const query = await converter.convertToSQL('Show me all users who signed up last month and their total order amounts')
console.log('Generated SQL:', query.query)
console.log('Explanation:', query.explanation)
Advanced NL2SQL with Query Optimization
// lib/nl2sql/advancedConverter.ts
import { Pool } from 'pg'
interface QueryAnalysis {
query: string
explanation: string
tables: string[]
estimatedCost: 'low' | 'medium' | 'high'
optimizationSuggestions: string[]
executionPlan?: any
estimatedRows?: number
}
interface QueryResult {
data: any[]
metadata: {
rowCount: number
executionTime: number
columns: string[]
}
}
class AdvancedNL2SQLConverter extends BasicNL2SQLConverter {
private dbPool: Pool
private queryCache: Map<string, QueryAnalysis> = new Map()
constructor(schema: DatabaseSchema, dbPool: Pool) {
super(schema)
this.dbPool = dbPool
}
async convertWithOptimization(naturalLanguage: string): Promise<QueryAnalysis> {
// Check cache first
const cacheKey = this.generateCacheKey(naturalLanguage)
const cached = this.queryCache.get(cacheKey)
if (cached) {
return cached
}
// Generate initial query
const initialQuery = await this.convertToSQL(naturalLanguage)
// Analyze and optimize
const analysis = await this.analyzeAndOptimize(initialQuery)
// Cache the result
this.queryCache.set(cacheKey, analysis)
return analysis
}
private async analyzeAndOptimize(query: SQLQuery): Promise<QueryAnalysis> {
const analysis: QueryAnalysis = {
query: query.query,
explanation: query.explanation,
tables: query.tables,
estimatedCost: query.estimatedCost,
optimizationSuggestions: [],
}
try {
// Get execution plan
const planResult = await this.dbPool.query(`EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) ${query.query}`)
analysis.executionPlan = planResult.rows[0]['QUERY PLAN']
// Analyze execution plan
const suggestions = this.analyzeExecutionPlan(analysis.executionPlan)
analysis.optimizationSuggestions = suggestions
// Estimate row count
const countQuery = `SELECT COUNT(*) FROM (${query.query}) as subquery`
const countResult = await this.dbPool.query(countQuery)
analysis.estimatedRows = parseInt(countResult.rows[0].count)
// Update cost estimation
analysis.estimatedCost = this.estimateCost(analysis.executionPlan, analysis.estimatedRows)
} catch (error) {
console.error('Failed to analyze query:', error)
analysis.optimizationSuggestions.push('Unable to analyze query execution plan')
}
return analysis
}
private analyzeExecutionPlan(plan: any): string[] {
const suggestions: string[] = []
if (!plan || !plan[0]) {
return ['Unable to analyze execution plan']
}
const analyzeNode = (node: any): void => {
if (node['Node Type'] === 'Seq Scan') {
suggestions.push('Consider adding an index for sequential scan operations')
}
if (node['Node Type'] === 'Sort' && node['Sort Method'] === 'external') {
suggestions.push('Consider adding an index to avoid external sorting')
}
if (node['Node Type'] === 'Hash Join') {
suggestions.push('Hash join detected - consider index optimization for better performance')
}
if (node['Actual Rows'] && node['Estimated Rows']) {
const actual = node['Actual Rows']
const estimated = node['Estimated Rows']
const ratio = actual / estimated
if (ratio > 10 || ratio < 0.1) {
suggestions.push('Row count estimation is significantly off - consider updating table statistics')
}
}
// Recursively analyze child nodes
if (node['Plans']) {
node['Plans'].forEach(analyzeNode)
}
}
analyzeNode(plan[0])
return suggestions
}
private estimateCost(plan: any, rowCount: number): 'low' | 'medium' | 'high' {
if (!plan || !plan[0]) return 'medium'
const totalCost = plan[0]['Total Cost'] || 0
const actualTime = plan[0]['Actual Total Time'] || 0
if (totalCost < 1000 && actualTime < 100) return 'low'
if (totalCost < 10000 && actualTime < 1000) return 'medium'
return 'high'
}
async executeQuery(analysis: QueryAnalysis): Promise<QueryResult> {
const startTime = Date.now()
try {
const result = await this.dbPool.query(analysis.query)
const executionTime = Date.now() - startTime
return {
data: result.rows,
metadata: {
rowCount: result.rowCount,
executionTime,
columns: result.fields.map(field => field.name),
},
}
} catch (error) {
throw new Error(`Query execution failed: ${error instanceof Error ? error.message : 'Unknown error'}`)
}
}
async suggestOptimizations(query: string): Promise<string[]> {
const optimizationPrompt = `Analyze this PostgreSQL query and suggest optimizations:
\`\`\`sql
${query}
\`\`\`
Consider:
- Index usage
- JOIN optimization
- WHERE clause efficiency
- Subquery optimization
- Query structure improvements
Provide specific, actionable suggestions.`
const result = await streamText({
model: openai('gpt-4o'),
messages: [{ role: 'user', content: optimizationPrompt }],
temperature: 0.3,
maxTokens: 500,
})
return result.text.split('\n').filter(line => line.trim().length > 0)
}
private generateCacheKey(naturalLanguage: string): string {
return Buffer.from(naturalLanguage.toLowerCase().trim()).toString('base64')
}
clearCache(): void {
this.queryCache.clear()
}
}
// Usage example
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
})
const advancedConverter = new AdvancedNL2SQLConverter(sampleSchema, pool)
const analysis = await advancedConverter.convertWithOptimization(
'Find users who spent more than $1000 in the last 3 months'
)
console.log('Optimized query:', analysis.query)
console.log('Suggestions:', analysis.optimizationSuggestions)
const result = await advancedConverter.executeQuery(analysis)
console.log('Query results:', result.data)
Interactive Query Builder
// lib/nl2sql/interactiveBuilder.ts
interface QueryBuilderState {
naturalLanguage: string
generatedSQL: string
explanation: string
suggestions: string[]
isExecuting: boolean
results: any[]
error: string | null
}
class InteractiveQueryBuilder {
private converter: AdvancedNL2SQLConverter
private state: QueryBuilderState
constructor(converter: AdvancedNL2SQLConverter) {
this.converter = converter
this.state = {
naturalLanguage: '',
generatedSQL: '',
explanation: '',
suggestions: [],
isExecuting: false,
results: [],
error: null,
}
}
async buildQuery(naturalLanguage: string): Promise<void> {
this.state.naturalLanguage = naturalLanguage
this.state.error = null
try {
const analysis = await this.converter.convertWithOptimization(naturalLanguage)
this.state.generatedSQL = analysis.query
this.state.explanation = analysis.explanation
this.state.suggestions = analysis.optimizationSuggestions
} catch (error) {
this.state.error = error instanceof Error ? error.message : 'Failed to generate query'
}
}
async executeQuery(): Promise<void> {
if (!this.state.generatedSQL) {
this.state.error = 'No query to execute'
return
}
this.state.isExecuting = true
this.state.error = null
try {
const analysis = await this.converter.convertWithOptimization(this.state.naturalLanguage)
const result = await this.converter.executeQuery(analysis)
this.state.results = result.data
} catch (error) {
this.state.error = error instanceof Error ? error.message : 'Query execution failed'
} finally {
this.state.isExecuting = false
}
}
async refineQuery(feedback: string): Promise<void> {
const refinementPrompt = `Refine this SQL query based on the user feedback:
Original question: "${this.state.naturalLanguage}"
Generated SQL: \`\`\`sql
${this.state.generatedSQL}
\`\`\`
User feedback: "${feedback}"
Please provide an improved SQL query that addresses the feedback.`
const result = await streamText({
model: openai('gpt-4o'),
messages: [{ role: 'user', content: refinementPrompt }],
temperature: 0.3,
maxTokens: 1000,
})
try {
const jsonMatch = result.text.match(/\{[\s\S]*\}/)
if (jsonMatch) {
const parsed = JSON.parse(jsonMatch[0])
this.state.generatedSQL = parsed.query
this.state.explanation = parsed.explanation || this.state.explanation
} else {
this.state.generatedSQL = result.text
}
} catch (error) {
this.state.error = 'Failed to refine query'
}
}
getState(): QueryBuilderState {
return { ...this.state }
}
async getQueryHistory(): Promise<Array<{
question: string
query: string
timestamp: Date
executionTime?: number
}>> {
// This would typically come from a database
return []
}
}
// React component for the interactive builder
// components/InteractiveQueryBuilder.tsx
'use client'
import { useState, useEffect } from 'react'
interface InteractiveQueryBuilderProps {
converter: AdvancedNL2SQLConverter
}
export function InteractiveQueryBuilder({ converter }: InteractiveQueryBuilderProps) {
const [builder] = useState(() => new InteractiveQueryBuilder(converter))
const [state, setState] = useState(builder.getState())
const [feedback, setFeedback] = useState('')
const handleBuildQuery = async () => {
await builder.buildQuery(state.naturalLanguage)
setState(builder.getState())
}
const handleExecuteQuery = async () => {
await builder.executeQuery()
setState(builder.getState())
}
const handleRefineQuery = async () => {
await builder.refineQuery(feedback)
setState(builder.getState())
setFeedback('')
}
return (
<div className="max-w-6xl mx-auto p-6">
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 mb-4">
Natural Language to SQL Builder
</h1>
<p className="text-gray-600">
Ask questions in plain English and get optimized PostgreSQL queries.
</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Input Section */}
<div className="space-y-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Your Question
</label>
<textarea
value={state.naturalLanguage}
onChange={(e) => setState({ ...state, naturalLanguage: e.target.value })}
placeholder="Ask a question about your data... (e.g., 'Show me users who signed up last month')"
className="w-full h-32 p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<div className="flex space-x-4">
<button
onClick={handleBuildQuery}
disabled={!state.naturalLanguage.trim()}
className="flex-1 bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 disabled:opacity-50"
>
Generate SQL
</button>
<button
onClick={handleExecuteQuery}
disabled={!state.generatedSQL || state.isExecuting}
className="flex-1 bg-green-600 text-white py-2 px-4 rounded-md hover:bg-green-700 disabled:opacity-50"
>
{state.isExecuting ? 'Executing...' : 'Execute Query'}
</button>
</div>
{state.error && (
<div className="p-3 bg-red-50 border border-red-200 rounded-md">
<p className="text-red-600 text-sm">{state.error}</p>
</div>
)}
</div>
{/* Output Section */}
<div className="space-y-6">
{state.generatedSQL && (
<>
<div className="bg-white border border-gray-200 rounded-lg p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">
Generated SQL
</h3>
<div className="bg-gray-50 rounded-md p-4">
<pre className="text-sm text-gray-800 overflow-x-auto">
<code>{state.generatedSQL}</code>
</pre>
</div>
{state.explanation && (
<p className="mt-3 text-sm text-gray-600">
{state.explanation}
</p>
)}
</div>
{state.suggestions.length > 0 && (
<div className="bg-white border border-gray-200 rounded-lg p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">
Optimization Suggestions
</h3>
<ul className="space-y-2">
{state.suggestions.map((suggestion, index) => (
<li key={index} className="text-sm text-gray-600 flex items-start">
<span className="text-blue-500 mr-2">•</span>
{suggestion}
</li>
))}
</ul>
</div>
)}
<div className="bg-white border border-gray-200 rounded-lg p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">
Refine Query
</h3>
<textarea
value={feedback}
onChange={(e) => setFeedback(e.target.value)}
placeholder="Provide feedback to improve the query..."
className="w-full h-20 p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
<button
onClick={handleRefineQuery}
disabled={!feedback.trim()}
className="mt-3 bg-purple-600 text-white py-2 px-4 rounded-md hover:bg-purple-700 disabled:opacity-50"
>
Refine Query
</button>
</div>
</>
)}
{state.results.length > 0 && (
<div className="bg-white border border-gray-200 rounded-lg p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">
Query Results ({state.results.length} rows)
</h3>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
{Object.keys(state.results[0] || {}).map((column) => (
<th key={column} className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
{column}
</th>
))}
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{state.results.slice(0, 10).map((row, index) => (
<tr key={index}>
{Object.values(row).map((value, colIndex) => (
<td key={colIndex} className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{String(value)}
</td>
))}
</tr>
))}
</tbody>
</table>
{state.results.length > 10 && (
<p className="mt-3 text-sm text-gray-500">
Showing first 10 of {state.results.length} results
</p>
)}
</div>
</div>
)}
</div>
</div>
</div>
)
}
Best Practices
1. Query Validation and Safety
class SafeNL2SQLConverter extends AdvancedNL2SQLConverter {
private readonly allowedOperations = new Set(['SELECT', 'WITH'])
private readonly blockedKeywords = new Set(['DROP', 'DELETE', 'UPDATE', 'INSERT', 'TRUNCATE', 'ALTER', 'CREATE'])
async convertToSQL(naturalLanguage: string): Promise<SQLQuery> {
const query = await super.convertToSQL(naturalLanguage)
// Validate query safety
const validation = this.validateQuery(query.query)
if (!validation.safe) {
throw new Error(`Query blocked for safety: ${validation.reason}`)
}
return query
}
private validateQuery(query: string): { safe: boolean; reason?: string } {
const upperQuery = query.toUpperCase().trim()
// Check for blocked operations
for (const blocked of this.blockedKeywords) {
if (upperQuery.includes(blocked)) {
return { safe: false, reason: `Operation '${blocked}' is not allowed` }
}
}
// Ensure query starts with allowed operation
const startsWithAllowed = Array.from(this.allowedOperations).some(op =>
upperQuery.startsWith(op)
)
if (!startsWithAllowed) {
return { safe: false, reason: 'Query must start with SELECT or WITH' }
}
// Check for potential SQL injection patterns
const suspiciousPatterns = [
/;\s*$/,
/--\s*$/,
/\/\*.*\*\//,
/UNION\s+ALL/i,
/UNION\s+SELECT/i,
]
for (const pattern of suspiciousPatterns) {
if (pattern.test(query)) {
return { safe: false, reason: 'Query contains suspicious patterns' }
}
}
return { safe: true }
}
async executeQuery(analysis: QueryAnalysis): Promise<QueryResult> {
// Add query timeout
const timeout = setTimeout(() => {
throw new Error('Query execution timeout')
}, 30000) // 30 seconds
try {
const result = await super.executeQuery(analysis)
clearTimeout(timeout)
return result
} catch (error) {
clearTimeout(timeout)
throw error
}
}
}
2. Performance Monitoring
class MonitoredNL2SQLConverter extends SafeNL2SQLConverter {
private queryMetrics: Array<{
question: string
query: string
executionTime: number
rowCount: number
timestamp: Date
success: boolean
}> = []
async convertWithOptimization(naturalLanguage: string): Promise<QueryAnalysis> {
const startTime = Date.now()
try {
const analysis = await super.convertWithOptimization(naturalLanguage)
this.queryMetrics.push({
question: naturalLanguage,
query: analysis.query,
executionTime: Date.now() - startTime,
rowCount: analysis.estimatedRows || 0,
timestamp: new Date(),
success: true,
})
return analysis
} catch (error) {
this.queryMetrics.push({
question: naturalLanguage,
query: '',
executionTime: Date.now() - startTime,
rowCount: 0,
timestamp: new Date(),
success: false,
})
throw error
}
}
getPerformanceStats(): {
totalQueries: number
averageExecutionTime: number
successRate: number
mostCommonQuestions: Array<{ question: string; count: number }>
} {
const successful = this.queryMetrics.filter(m => m.success)
const totalTime = successful.reduce((sum, m) => sum + m.executionTime, 0)
const questionCounts = this.queryMetrics.reduce((acc, m) => {
acc[m.question] = (acc[m.question] || 0) + 1
return acc
}, {} as Record<string, number>)
const mostCommon = Object.entries(questionCounts)
.sort(([,a], [,b]) => b - a)
.slice(0, 5)
.map(([question, count]) => ({ question, count }))
return {
totalQueries: this.queryMetrics.length,
averageExecutionTime: successful.length > 0 ? totalTime / successful.length : 0,
successRate: this.queryMetrics.length > 0 ? successful.length / this.queryMetrics.length : 0,
mostCommonQuestions: mostCommon,
}
}
}
3. Error Handling and Recovery
class RobustNL2SQLConverter extends MonitoredNL2SQLConverter {
async convertToSQL(naturalLanguage: string): Promise<SQLQuery> {
try {
return await super.convertToSQL(naturalLanguage)
} catch (error) {
// Try to provide a helpful fallback
return this.generateFallbackQuery(naturalLanguage, error)
}
}
private async generateFallbackQuery(naturalLanguage: string, originalError: any): Promise<SQLQuery> {
const fallbackPrompt = `The original query generation failed with error: ${originalError.message}
Original question: "${naturalLanguage}"
Please generate a simple, safe SELECT query that might help answer this question.
Focus on basic operations and avoid complex JOINs or subqueries.
Respond with a JSON object:
{
"query": "Simple SELECT query",
"explanation": "What this query does",
"tables": ["table1"],
"estimatedCost": "low"
}`
const result = await streamText({
model: openai('gpt-4o'),
messages: [{ role: 'user', content: fallbackPrompt }],
temperature: 0.1,
maxTokens: 500,
})
try {
const jsonMatch = result.text.match(/\{[\s\S]*\}/)
if (jsonMatch) {
const parsed = JSON.parse(jsonMatch[0])
return {
query: parsed.query,
explanation: `${parsed.explanation} (Fallback query due to original error)`,
tables: parsed.tables || [],
estimatedCost: parsed.estimatedCost || 'low',
}
}
} catch (parseError) {
// If even the fallback fails, return a basic query
return {
query: 'SELECT 1 as result',
explanation: 'Fallback query - unable to generate specific query',
tables: [],
estimatedCost: 'low',
}
}
throw originalError
}
}
Natural language to PostgreSQL can make database-backed products easier to explore. Instead of writing SQL directly, users can ask questions in plain English and receive a safe, reviewed query. The hard part is building the guardrails: schema context, validation, permissions, query limits and clear feedback.
With the right implementation, you can make your database accessible to everyone while maintaining the power and flexibility of SQL.