Skip to content

Commit 9012b67

Browse files
committed
Merge branch 'main' into feat/upload-db
2 parents 3e7e733 + b877770 commit 9012b67

File tree

18 files changed

+271
-70
lines changed

18 files changed

+271
-70
lines changed

README.md

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,33 @@
1-
# postgres-new
1+
# postgres.new
22

33
In-browser Postgres sandbox with AI assistance.
44

5+
![github-repo-hero](https://github.com/user-attachments/assets/e55f7c0d-a817-4aeb-838e-728aabda3a5d)
6+
7+
With [postgres.new](https://postgres.new), you can instantly spin up an unlimited number of Postgres databases that run directly in your browser (and soon, deploy them to S3).
8+
9+
Each database is paired with a large language model (LLM) which opens the door to some interesting use cases:
10+
11+
- Drag-and-drop CSV import (generate table on the fly)
12+
- Generate and export reports
13+
- Generate charts
14+
- Build database diagrams
15+
16+
## How it works
17+
All queries in postgres.new run directly in your browser. There’s no remote Postgres container or WebSocket proxy.
18+
19+
How is this possible? [PGlite](https://pglite.dev/), a WASM version of Postgres that can run directly in your browser. Every database that you create spins up a new instance of PGlite that exposes a fully-functional Postgres database. Data is stored in IndexedDB so that changes persist after refresh.
20+
521
## Monorepo
622

723
This is a monorepo split into the following projects:
824

9-
- [Frontend (Next.js)](./apps/postgres-new/)
10-
- [Backend (pg-gateway)](./apps/db-service/)
25+
- [Frontend (Next.js)](./apps/postgres-new/): This contains the primary web app built with Next.js
26+
- [Backend (pg-gateway)](./apps/db-service/): This serves S3-backed PGlite databases over the PG wire protocol using [pg-gateway](https://github.com/supabase-community/pg-gateway)
27+
28+
## Video
29+
30+
[![image](https://github.com/user-attachments/assets/9da04785-d813-4e9c-a400-4e00c63381a1)](https://youtu.be/ooWaPVvljlU)
1131

1232
## License
1333

apps/db-service/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"psql": "docker compose run --rm -i psql psql"
1010
},
1111
"dependencies": {
12-
"@electric-sql/pglite": "0.2.0-alpha.6",
12+
"@electric-sql/pglite": "0.2.0-alpha.9",
1313
"pg-gateway": "^0.2.5-alpha.2",
1414
"tar": "^7.4.3"
1515
},

apps/postgres-new/README.md

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,32 @@
22

33
In-browser Postgres sandbox with AI assistance. Built on Next.js.
44

5+
## Architecture
6+
7+
We use PGlite for 2 purposes:
8+
9+
1. A "meta" DB that keeps track of all of the user databases along with their message history
10+
2. A "user" DB for each database the user creates along with whatever tables/data they've created
11+
12+
Both databases are stored locally in the browser via IndexedDB. This means that these databases are not persisted to the cloud and cannot be accessed from multiple devices (though this is on the roadmap).
13+
14+
Every PGlite instance runs in a Web Worker so that the main thread is not blocked.
15+
16+
## AI
17+
18+
The AI component is powered by OpenAI's GPT-4o model. The project uses [Vercel's AI SDK ](https://sdk.vercel.ai/docs/introduction) to simplify message streams and tool calls.
19+
20+
## Authentication
21+
22+
Because LLMs cost money, a lightweight auth wall exists to prevent abuse. It is currently only used to validate that the user has a legitimate GitHub account, but in the future it could be used to save private/public databases to the cloud.
23+
24+
Authentication and users are managed by a [Supabase](https://supabase.com/) database. You can find the migrations and other configuration for this in the root [`./supabase`](../../supabase/) directory.
25+
526
## Development
627

7-
1. Install deps:
28+
From this directory (`./apps/postgres-new`):
29+
30+
1. Install dependencies:
831
```shell
932
npm i
1033
```

apps/postgres-new/app/api/chat/route.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ export async function POST(req: Request) {
4141
When querying data, limit to 5 by default.
4242
4343
When performing FTS, always use 'simple' (languages aren't available).
44+
45+
When importing CSVs try to solve the problem yourself (eg. use a generic text column, then refine)
46+
vs. asking the user to change the CSV.
4447
4548
You also know math. All math equations and expressions must be written in KaTex and must be wrapped in double dollar \`$$\`:
4649
- Inline: $$\\sqrt{26}$$

apps/postgres-new/app/page.tsx

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,18 +68,51 @@ export default function Page() {
6868
<Workspace
6969
databaseId={nextDatabaseId}
7070
visibility="local"
71-
onStart={async () => {
71+
onMessage={async () => {
7272
// Make the DB no longer hidden
7373
await updateDatabase({ id: nextDatabaseId, name: null, isHidden: false })
7474

75-
// Navigate to this DB's path
76-
router.push(`/db/${nextDatabaseId}`)
77-
7875
// Pre-load the next DB
7976
const nextId = uniqueId()
8077
localStorage.setItem('next-db-id', JSON.stringify(nextId))
8178
preloadDb(nextId)
8279
}}
80+
onReply={async (message, append) => {
81+
if (!dbManager) {
82+
throw new Error('dbManager is not available')
83+
}
84+
85+
const messages = await dbManager.getMessages(nextDatabaseId)
86+
const isFirstReplyComplete =
87+
!messages.some((message) => message.role === 'assistant' && !message.toolInvocations) &&
88+
message.role === 'assistant' &&
89+
!message.toolInvocations
90+
91+
// The model might run multiple tool calls before ending with a message, so
92+
// we only want to redirect after all of these back-to-back calls finish
93+
if (isFirstReplyComplete) {
94+
router.push(`/db/${nextDatabaseId}`)
95+
96+
append({
97+
role: 'user',
98+
content: 'Name this conversation. No need to reply.',
99+
data: {
100+
automated: true,
101+
},
102+
})
103+
}
104+
}}
105+
onCancelReply={(append) => {
106+
router.push(`/db/${nextDatabaseId}`)
107+
108+
append({
109+
role: 'user',
110+
content: 'Name this conversation. No need to reply.',
111+
data: {
112+
automated: true,
113+
},
114+
})
115+
}}
83116
/>
84117
)
85118
}

apps/postgres-new/components/chat.tsx

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,28 @@ export default function Chat() {
7373

7474
const sendCsv = useCallback(
7575
async (file: File) => {
76+
if (file.type !== 'text/csv') {
77+
// Add an artificial tool call requesting the CSV
78+
// with an error indicating the file wasn't a CSV
79+
appendMessage({
80+
role: 'assistant',
81+
content: '',
82+
toolInvocations: [
83+
{
84+
state: 'result',
85+
toolCallId: generateId(),
86+
toolName: 'requestCsv',
87+
args: {},
88+
result: {
89+
success: false,
90+
error: `The file has type '${file.type}'. Let the user know that only CSV imports are currently supported.`,
91+
},
92+
},
93+
],
94+
})
95+
return
96+
}
97+
7698
const fileId = generateId()
7799

78100
await saveFile(fileId, file)
@@ -120,7 +142,7 @@ export default function Chat() {
120142

121143
const [file] = files
122144

123-
if (file && file.type === 'text/csv') {
145+
if (file) {
124146
await sendCsv(file)
125147
}
126148
},
@@ -280,7 +302,7 @@ export default function Chat() {
280302
{user ? (
281303
<m.h3
282304
layout
283-
className="text-2xl font-light"
305+
className="text-2xl font-light text-center"
284306
variants={{
285307
hidden: { opacity: 0, y: 10 },
286308
show: { opacity: 1, y: 0 },
@@ -402,7 +424,7 @@ export default function Chat() {
402424
const changeEvent = event as unknown as ChangeEvent<HTMLInputElement>
403425
const [file] = Array.from(changeEvent.target?.files ?? [])
404426

405-
if (file && file.type === 'text/csv') {
427+
if (file) {
406428
await sendCsv(file)
407429
}
408430

apps/postgres-new/components/providers.tsx

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,7 @@ const queryClient = new QueryClient()
1010

1111
export default function Providers({ children }: PropsWithChildren) {
1212
return (
13-
// Force theme until we implement dark mode
14-
<ThemeProvider
15-
attribute="class"
16-
defaultTheme="system"
17-
storageKey="jonny"
18-
disableTransitionOnChange
19-
>
13+
<ThemeProvider attribute="class" defaultTheme="system" disableTransitionOnChange>
2014
<QueryClientProvider client={queryClient}>
2115
<AppProvider>{children}</AppProvider>
2216
</QueryClientProvider>

apps/postgres-new/components/schema/table-graph.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,10 @@ export default function TablesGraph({
7373

7474
// `fitView` needs to happen during next event tick
7575
setTimeout(() => fitView(isFirstLoad ? 0 : 500), 0)
76-
setIsFirstLoad(false)
76+
77+
if (tables.length > 0) {
78+
setIsFirstLoad(false)
79+
}
7780
})
7881
}
7982
}, [reactFlowInstance, tables, resolvedTheme, fitView, isFirstLoad])

apps/postgres-new/components/sidebar.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ function DatabaseMenuItem({ database, isActive }: DatabaseMenuItemProps) {
273273
<AnimatePresence initial={false}>
274274
{!isOnDeployWaitlist ? (
275275
<button
276-
className="px-4 py-3 bg-black text-white rounded-md"
276+
className="px-4 py-3 bg-foreground text-background rounded-md"
277277
onClick={async () => {
278278
await joinDeployWaitlist()
279279
}}
@@ -399,7 +399,11 @@ function DatabaseMenuItem({ database, isActive }: DatabaseMenuItemProps) {
399399
downloadFile(file)
400400
}}
401401
>
402-
<Download size={16} strokeWidth={2} className="flex-shrink-0" />
402+
<Download
403+
size={16}
404+
strokeWidth={2}
405+
className="flex-shrink-0 text-muted-foreground"
406+
/>
403407

404408
<span>Download</span>
405409
</DropdownMenuItem>
@@ -418,7 +422,7 @@ function DatabaseMenuItem({ database, isActive }: DatabaseMenuItemProps) {
418422
strokeWidth={2}
419423
className="flex-shrink-0 text-muted-foreground"
420424
/>
421-
<span>Publish</span>
425+
<span>Deploy</span>
422426
</DropdownMenuItem>
423427
<DropdownMenuSeparator />
424428
<DropdownMenuItem

apps/postgres-new/components/tools/csv-export.tsx

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { m } from 'framer-motion'
22
import { Download } from 'lucide-react'
3+
import { useMemo } from 'react'
4+
import { format } from 'sql-formatter'
35
import { loadFile } from '~/lib/files'
46
import { ToolInvocation } from '~/lib/tools'
57
import { downloadFile } from '~/lib/util'
@@ -10,19 +12,40 @@ export type CsvExportProps = {
1012
}
1113

1214
export default function CsvExport({ toolInvocation }: CsvExportProps) {
15+
const { sql } = toolInvocation.args
16+
17+
const formattedSql = useMemo(
18+
() =>
19+
format(sql, {
20+
language: 'postgresql',
21+
keywordCase: 'lower',
22+
identifierCase: 'lower',
23+
dataTypeCase: 'lower',
24+
functionCase: 'lower',
25+
}),
26+
[sql]
27+
)
28+
1329
if (!('result' in toolInvocation)) {
1430
return null
1531
}
1632

1733
const { result } = toolInvocation
1834

1935
if (!result.success) {
20-
return <div className="bg-destructive-300 px-6 py-4 rounded-md">Error executing SQL</div>
36+
return (
37+
<CodeAccordion
38+
title="Error executing SQL"
39+
language="sql"
40+
code={formattedSql}
41+
error={result.error}
42+
/>
43+
)
2144
}
2245

2346
return (
2447
<>
25-
<CodeAccordion title="Executed SQL" language="sql" code={toolInvocation.args.sql} />
48+
<CodeAccordion title="Executed SQL" language="sql" code={formattedSql} />
2649
<m.div
2750
layoutId={toolInvocation.toolCallId}
2851
className="self-start px-5 py-2.5 text-base rounded-full bg-border flex gap-2 items-center text-lighter italic"

0 commit comments

Comments
 (0)