Skip to content

Commit 0a173ab

Browse files
Merge pull request #3737 from liam-hq/supabase-repository-schema-bench
✨(schema-bench): Migrate to SupabaseRepositories for database persistence
2 parents ed5ab2f + 6cdde2d commit 0a173ab

File tree

5 files changed

+253
-17
lines changed

5 files changed

+253
-17
lines changed

frontend/internal-packages/schema-bench/README.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,11 +170,22 @@ benchmark-workspace/
170170
171171
## Environment Setup
172172
173-
For OpenAI executor, set your API key:
173+
### Required Environment Variables
174+
175+
For **LiamDB executor**, you need Supabase connection details:
176+
```bash
177+
export NEXT_PUBLIC_SUPABASE_URL="your-supabase-url"
178+
export SUPABASE_SERVICE_ROLE_KEY="your-service-role-key"
179+
export OPENAI_API_KEY="your-api-key" # Also required by LiamDB for AI operations
180+
```
181+
182+
For **OpenAI executor**, you only need:
174183
```bash
175184
export OPENAI_API_KEY="your-api-key"
176185
```
177186

187+
These variables should be set in a `.env` file at the repository root.
188+
178189
## Example Workflow
179190

180191
1) Clean and setup: `rm -rf benchmark-workspace && pnpm --filter @liam-hq/schema-bench setupWorkspace`

frontend/internal-packages/schema-bench/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"dependencies": {
77
"@huggingface/transformers": "3.3.3",
88
"@liam-hq/agent": "workspace:*",
9+
"@liam-hq/db": "workspace:*",
910
"@liam-hq/neverthrow": "workspace:*",
1011
"@liam-hq/schema": "workspace:*",
1112
"dotenv": "16.5.0",

frontend/internal-packages/schema-bench/src/executors/liamDb/liamDbExecutor.ts

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,46 @@
1-
import { deepModeling, InMemoryRepository } from '@liam-hq/agent'
1+
import { deepModeling } from '@liam-hq/agent'
22
import { aSchema } from '@liam-hq/schema'
33
import { err, ok, type Result } from 'neverthrow'
44
import { handleExecutionResult, logInputProcessing } from '../utils.ts'
5+
import { setupRepositories } from './setupRepositories.ts'
56
import type { LiamDbExecutorInput, LiamDbExecutorOutput } from './types.ts'
67

78
export async function execute(
89
input: LiamDbExecutorInput,
910
): Promise<Result<LiamDbExecutorOutput, Error>> {
1011
logInputProcessing(input.input)
1112

12-
// Setup InMemory repository
13-
const repositories = {
14-
schema: new InMemoryRepository({
15-
schemas: {
16-
'demo-design-session': aSchema({
17-
tables: {},
18-
}),
19-
},
20-
}),
13+
// Setup Supabase repositories
14+
const setupResult = await setupRepositories()
15+
if (setupResult.isErr()) {
16+
return err(
17+
new Error(`Failed to setup repositories: ${setupResult.error.message}`),
18+
)
2119
}
2220

21+
const {
22+
repositories,
23+
organizationId,
24+
buildingSchemaId,
25+
designSessionId,
26+
userId,
27+
} = setupResult.value
28+
2329
// Create workflow params
2430
const workflowParams = {
2531
userInput: input.input,
2632
schemaData: aSchema({ tables: {} }),
27-
organizationId: 'demo-org-id',
28-
designSessionId: 'demo-design-session',
29-
userId: 'demo-user-id',
33+
organizationId,
34+
buildingSchemaId,
35+
designSessionId,
36+
userId,
3037
signal: new AbortController().signal,
3138
}
3239

3340
const config = {
3441
configurable: {
3542
repositories,
36-
thread_id: 'demo-design-session',
43+
thread_id: designSessionId,
3744
},
3845
}
3946

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
import { createSupabaseRepositories } from '@liam-hq/agent'
2+
import {
3+
createClient,
4+
type SupabaseClientType,
5+
toResultAsync,
6+
} from '@liam-hq/db'
7+
import { err, ok, type Result, type ResultAsync } from 'neverthrow'
8+
9+
type Repositories = ReturnType<typeof createSupabaseRepositories>
10+
11+
type SetupResult = {
12+
repositories: Repositories
13+
organizationId: string
14+
buildingSchemaId: string
15+
designSessionId: string
16+
userId: string
17+
}
18+
19+
let cachedSetup: SetupResult | null = null
20+
21+
/**
22+
* Create Supabase client
23+
*/
24+
const createDatabaseConnection = (): Result<SupabaseClientType, Error> => {
25+
const supabaseUrl = process.env['NEXT_PUBLIC_SUPABASE_URL']
26+
const supabaseKey = process.env['SUPABASE_SERVICE_ROLE_KEY']
27+
28+
if (!supabaseUrl || !supabaseKey) {
29+
return err(
30+
new Error(
31+
'Missing required environment variables: NEXT_PUBLIC_SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY',
32+
),
33+
)
34+
}
35+
36+
const supabaseClient = createClient(supabaseUrl, supabaseKey)
37+
return ok(supabaseClient)
38+
}
39+
40+
/**
41+
* Get the first organization from database
42+
*/
43+
const getFirstOrganization = (
44+
supabaseClient: SupabaseClientType,
45+
): ResultAsync<{ id: string }, Error> => {
46+
return toResultAsync(
47+
supabaseClient.from('organizations').select('id').limit(1).single(),
48+
)
49+
}
50+
51+
/**
52+
* Get the first user from database
53+
*/
54+
const getUser = (
55+
supabaseClient: SupabaseClientType,
56+
): ResultAsync<{ id: string }, Error> => {
57+
return toResultAsync(
58+
supabaseClient.from('users').select('id').limit(1).single(),
59+
)
60+
}
61+
62+
const getDesignSession = (
63+
supabaseClient: SupabaseClientType,
64+
organizationId: string,
65+
): ResultAsync<{ id: string }, Error> => {
66+
return toResultAsync(
67+
supabaseClient
68+
.from('design_sessions')
69+
.select('id')
70+
.eq('organization_id', organizationId)
71+
.limit(1)
72+
.single(),
73+
)
74+
}
75+
76+
const createDesignSession = (
77+
supabaseClient: SupabaseClientType,
78+
organizationId: string,
79+
userId: string,
80+
): ResultAsync<{ id: string }, Error> => {
81+
return toResultAsync(
82+
supabaseClient
83+
.from('design_sessions')
84+
.insert({
85+
name: 'Schema Benchmark Session',
86+
project_id: null,
87+
organization_id: organizationId,
88+
created_by_user_id: userId,
89+
parent_design_session_id: null,
90+
})
91+
.select('id')
92+
.single(),
93+
)
94+
}
95+
96+
/**
97+
* Get or create a design session for benchmarking
98+
*/
99+
const getOrCreateDesignSession = (
100+
supabaseClient: SupabaseClientType,
101+
organizationId: string,
102+
userId: string,
103+
): ResultAsync<{ id: string }, Error> => {
104+
return getDesignSession(supabaseClient, organizationId).orElse(() =>
105+
createDesignSession(supabaseClient, organizationId, userId),
106+
)
107+
}
108+
109+
const getBuildingSchema = (
110+
supabaseClient: SupabaseClientType,
111+
designSessionId: string,
112+
): ResultAsync<{ id: string }, Error> => {
113+
return toResultAsync(
114+
supabaseClient
115+
.from('building_schemas')
116+
.select('id')
117+
.eq('design_session_id', designSessionId)
118+
.limit(1)
119+
.single(),
120+
)
121+
}
122+
123+
const createBuildingSchema = (
124+
supabaseClient: SupabaseClientType,
125+
designSessionId: string,
126+
organizationId: string,
127+
): ResultAsync<{ id: string }, Error> => {
128+
const initialSchema = { tables: {}, enums: {}, extensions: {} }
129+
return toResultAsync(
130+
supabaseClient
131+
.from('building_schemas')
132+
.insert({
133+
design_session_id: designSessionId,
134+
organization_id: organizationId,
135+
schema: structuredClone(initialSchema),
136+
initial_schema_snapshot: structuredClone(initialSchema),
137+
schema_file_path: null,
138+
git_sha: null,
139+
})
140+
.select('id')
141+
.single(),
142+
)
143+
}
144+
145+
/**
146+
* Get or create a building schema for the design session
147+
*/
148+
const getOrCreateBuildingSchema = (
149+
supabaseClient: SupabaseClientType,
150+
designSessionId: string,
151+
organizationId: string,
152+
): ResultAsync<{ id: string }, Error> => {
153+
return getBuildingSchema(supabaseClient, designSessionId).orElse(() =>
154+
createBuildingSchema(supabaseClient, designSessionId, organizationId),
155+
)
156+
}
157+
158+
/**
159+
* Setup Supabase repositories for schema benchmark
160+
* Uses singleton pattern to reuse connections across benchmark runs
161+
*/
162+
export const setupRepositories = async (): Promise<
163+
Result<SetupResult, Error>
164+
> => {
165+
// Return cached setup if available
166+
if (cachedSetup) {
167+
return ok(cachedSetup)
168+
}
169+
170+
// Create database connection
171+
const connectionResult = createDatabaseConnection()
172+
if (connectionResult.isErr()) {
173+
return err(connectionResult.error)
174+
}
175+
const supabaseClient = connectionResult.value
176+
177+
return await getFirstOrganization(supabaseClient)
178+
.andThen((organization) =>
179+
getUser(supabaseClient).andThen((user) =>
180+
getOrCreateDesignSession(
181+
supabaseClient,
182+
organization.id,
183+
user.id,
184+
).andThen((designSession) =>
185+
getOrCreateBuildingSchema(
186+
supabaseClient,
187+
designSession.id,
188+
organization.id,
189+
).map((buildingSchema) => ({
190+
organization,
191+
user,
192+
designSession,
193+
buildingSchema,
194+
})),
195+
),
196+
),
197+
)
198+
.map(({ organization, user, designSession, buildingSchema }) => {
199+
const repositories = createSupabaseRepositories(
200+
supabaseClient,
201+
organization.id,
202+
)
203+
204+
cachedSetup = {
205+
repositories,
206+
organizationId: organization.id,
207+
buildingSchemaId: buildingSchema.id,
208+
designSessionId: designSession.id,
209+
userId: user.id,
210+
}
211+
212+
return cachedSetup
213+
})
214+
}

pnpm-lock.yaml

Lines changed: 5 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)