feat: migrate from SQLite to PostgreSQL - complete schema and API updates

- Remove better-sqlite3, add pg driver
- Convert schema to PostgreSQL types (serial, timestamp, boolean, jsonb)
- Generate fresh PostgreSQL migrations
- Update database connection layer with pg.Pool
- Fix all API routes: remove JSON.parse/stringify, use native timestamps and booleans
- Update drizzle.config.ts and .env.example for PostgreSQL
This commit is contained in:
Marko Djordjevic 2026-02-17 13:43:06 +01:00
parent 4605283d2b
commit 5f70f13da3
37 changed files with 1164 additions and 1825 deletions

View file

@ -30,7 +30,6 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ message: 'Types already seeded' });
}
const now = Math.floor(Date.now() / 1000);
const defaultTypes = [
{
name: 'break_up',
@ -38,8 +37,7 @@ export async function POST(request: NextRequest) {
color: '#10b981',
category: 'marker',
icon: 'arrowUp',
is_active: 1,
created_at: now,
is_active: true,
},
{
name: 'break_down',
@ -47,8 +45,7 @@ export async function POST(request: NextRequest) {
color: '#ef4444',
category: 'marker',
icon: 'arrowDown',
is_active: 1,
created_at: now,
is_active: true,
},
{
name: 'line',
@ -56,8 +53,7 @@ export async function POST(request: NextRequest) {
color: '#3b82f6',
category: 'line',
icon: 'line',
is_active: 1,
created_at: now,
is_active: true,
},
];
@ -83,8 +79,7 @@ export async function POST(request: NextRequest) {
color,
category,
icon: icon || null,
is_active: 1,
created_at: Math.floor(Date.now() / 1000),
is_active: true,
})
.returning();

View file

@ -30,7 +30,7 @@ export async function PATCH(
const result = await db
.update(annotations)
.set({ geometry: JSON.stringify(geometry) })
.set({ geometry })
.where(eq(annotations.id, id))
.returning();
@ -41,11 +41,7 @@ export async function PATCH(
);
}
const updated = result[0];
return NextResponse.json({
...updated,
geometry: updated.geometry ? JSON.parse(updated.geometry as string) : null,
});
return NextResponse.json(result[0]);
} catch (error: any) {
return NextResponse.json(
{ error: error.message || 'Failed to update annotation' },

View file

@ -23,13 +23,7 @@ export async function GET(request: NextRequest) {
.from(annotations)
.where(eq(annotations.chart_id, parseInt(chartId, 10)));
// Parse geometry from JSON string
const parsed = allAnnotations.map((annotation) => ({
...annotation,
geometry: annotation.geometry ? JSON.parse(annotation.geometry) : null,
}));
return NextResponse.json(parsed);
return NextResponse.json(allAnnotations);
} catch (error: any) {
return NextResponse.json(
{ error: error.message || 'Failed to fetch annotations' },
@ -61,30 +55,18 @@ export async function POST(request: NextRequest) {
);
}
// Serialize geometry to JSON string if present
const geometryString = geometry ? JSON.stringify(geometry) : null;
const result = await db
.insert(annotations)
.values({
chart_id,
timestamp,
label_type,
geometry: geometryString,
geometry: geometry || null,
color: color || '#3b82f6',
created_at: Math.floor(Date.now() / 1000),
})
.returning();
const created = result[0];
return NextResponse.json(
{
...created,
geometry: created.geometry ? JSON.parse(created.geometry) : null,
},
{ status: 201 }
);
return NextResponse.json(result[0], { status: 201 });
} catch (error: any) {
return NextResponse.json(
{ error: error.message || 'Failed to create annotation' },

View file

@ -45,7 +45,7 @@ export async function GET(request: NextRequest) {
price = candleResult[0].close;
}
} else if (annotation.label_type === 'line' && annotation.geometry) {
const geometry = JSON.parse(annotation.geometry);
const geometry = annotation.geometry as any;
price = geometry.startPrice || null;
}

View file

@ -44,7 +44,7 @@ export async function GET(request: NextRequest) {
confidence: span.confidence,
outcome: span.outcome,
notes: span.notes,
sub_spans: span.sub_spans ? JSON.parse(span.sub_spans as string) : null,
sub_spans: span.sub_spans,
color: span.color,
created_at: span.created_at,
})),

View file

@ -34,7 +34,7 @@ export async function PATCH(
if (confidence !== undefined) updates.confidence = confidence;
if (outcome !== undefined) updates.outcome = outcome;
if (notes !== undefined) updates.notes = notes;
if (sub_spans !== undefined) updates.sub_spans = sub_spans ? JSON.stringify(sub_spans) : null;
if (sub_spans !== undefined) updates.sub_spans = sub_spans || null;
const result = await db
.update(spanAnnotations)

View file

@ -69,17 +69,17 @@ export async function GET(request: NextRequest) {
// Convert to ML pipeline format
const annotations = spans.map(span => ({
id: span.id,
start_time: new Date(span.start_time * 1000).toISOString(),
end_time: new Date(span.end_time * 1000).toISOString(),
start_time: span.start_time,
end_time: span.end_time,
label: span.label,
confidence: span.confidence,
outcome: span.outcome,
notes: span.notes,
sub_spans: span.sub_spans ? JSON.parse(span.sub_spans) : null,
sub_spans: span.sub_spans,
color: span.color,
source: span.source,
model_prediction: span.model_prediction ? JSON.parse(span.model_prediction) : null,
created_at: new Date(span.created_at * 1000).toISOString(),
model_prediction: span.model_prediction,
created_at: span.created_at,
}));
return NextResponse.json({
@ -93,12 +93,10 @@ export async function GET(request: NextRequest) {
const csvRows = ['id,start_time,end_time,label,confidence,outcome,notes'];
for (const span of spans) {
const startTime = new Date(span.start_time * 1000).toISOString();
const endTime = new Date(span.end_time * 1000).toISOString();
const notes = span.notes ? `"${span.notes.replace(/"/g, '""')}"` : '';
csvRows.push(
`${span.id},${startTime},${endTime},${span.label},${span.confidence || ''},${span.outcome || ''},${notes}`
`${span.id},${span.start_time},${span.end_time},${span.label},${span.confidence || ''},${span.outcome || ''},${notes}`
);
}

View file

@ -71,11 +71,10 @@ export async function POST(request: NextRequest) {
confidence: confidence || null,
outcome: outcome || null,
notes: notes || null,
sub_spans: sub_spans ? JSON.stringify(sub_spans) : null,
sub_spans: sub_spans || null,
color: color || '#2196F3',
source: source || 'human', // 'human', 'model', or 'human_correction'
model_prediction: model_prediction ? JSON.stringify(model_prediction) : null,
created_at: Math.floor(Date.now() / 1000),
model_prediction: model_prediction || null,
})
.returning();

View file

@ -9,7 +9,7 @@ export async function GET() {
const types = await db
.select()
.from(spanLabelTypes)
.where(eq(spanLabelTypes.is_active, 1))
.where(eq(spanLabelTypes.is_active, true))
.orderBy(spanLabelTypes.sort_order);
return NextResponse.json(types);
@ -42,9 +42,8 @@ export async function POST(request: NextRequest) {
display_name,
color,
hotkey: hotkey || null,
is_active: 1,
is_active: true,
sort_order: sort_order ?? 0,
created_at: Math.floor(Date.now() / 1000),
})
.returning();

View file

@ -85,7 +85,6 @@ export async function POST(request: NextRequest): Promise<NextResponse> {
// Create the chart
const [newChart] = await db.insert(charts).values({
name: chartName,
created_at: Math.floor(Date.now() / 1000),
}).returning();
// Parse and prepare candle data
@ -99,9 +98,10 @@ export async function POST(request: NextRequest): Promise<NextResponse> {
if (isNaN(date.getTime())) {
throw new Error(`Invalid date format: ${row.time}`);
}
timestamp = Math.floor(date.getTime() / 1000);
timestamp = date; // PostgreSQL timestamp type expects Date object or ISO string
} else if (typeof row.time === 'number') {
timestamp = row.time;
// If Unix timestamp (seconds), convert to Date
timestamp = new Date(row.time * 1000);
} else {
throw new Error(`Invalid time value: ${row.time}`);
}