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:
parent
4605283d2b
commit
5f70f13da3
37 changed files with 1164 additions and 1825 deletions
|
|
@ -1,28 +1,30 @@
|
|||
import Database from 'better-sqlite3';
|
||||
import { drizzle } from 'drizzle-orm/better-sqlite3';
|
||||
import { migrate } from 'drizzle-orm/better-sqlite3/migrator';
|
||||
import { Pool } from 'pg';
|
||||
import { drizzle } from 'drizzle-orm/node-postgres';
|
||||
import { migrate } from 'drizzle-orm/node-postgres/migrator';
|
||||
import * as schema from './schema';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
|
||||
// Ensure data directory exists
|
||||
const dataDir = path.join(process.cwd(), 'data');
|
||||
if (!fs.existsSync(dataDir)) {
|
||||
fs.mkdirSync(dataDir, { recursive: true });
|
||||
// Read DATABASE_URL from environment
|
||||
const DATABASE_URL = process.env.DATABASE_URL;
|
||||
|
||||
if (!DATABASE_URL) {
|
||||
throw new Error('DATABASE_URL environment variable is not set');
|
||||
}
|
||||
|
||||
const dbPath = path.join(dataDir, 'candles.db');
|
||||
const sqlite = new Database(dbPath);
|
||||
export const db = drizzle(sqlite, { schema });
|
||||
// Create PostgreSQL connection pool
|
||||
const pool = new Pool({
|
||||
connectionString: DATABASE_URL,
|
||||
max: 10,
|
||||
});
|
||||
|
||||
// Run migrations at startup (for local dev).
|
||||
// In Docker, migrations are run by scripts/run-migrations.js before the app starts,
|
||||
// so this will be a no-op (all migrations already applied).
|
||||
export const db = drizzle(pool, { schema });
|
||||
|
||||
// Run migrations at startup (skip during build phase)
|
||||
const isBuildTime = process.env.NEXT_PHASE === 'phase-production-build' || process.env.NEXT_PHASE === 'phase-development-build';
|
||||
|
||||
if (!isBuildTime) {
|
||||
try {
|
||||
migrate(db, { migrationsFolder: path.join(process.cwd(), 'drizzle') });
|
||||
await migrate(db, { migrationsFolder: path.join(process.cwd(), 'drizzle') });
|
||||
console.log('✅ Database migrations completed');
|
||||
} catch (error) {
|
||||
console.error('❌ Migration failed:', error);
|
||||
|
|
|
|||
|
|
@ -1,67 +1,67 @@
|
|||
import { sqliteTable, integer, real, text, uniqueIndex } from 'drizzle-orm/sqlite-core';
|
||||
import { pgTable, serial, text, timestamp, doublePrecision, integer, boolean, jsonb, uniqueIndex } from 'drizzle-orm/pg-core';
|
||||
|
||||
export const charts = sqliteTable('charts', {
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
export const charts = pgTable('charts', {
|
||||
id: serial('id').primaryKey(),
|
||||
name: text('name').notNull().unique(),
|
||||
created_at: integer('created_at').notNull(),
|
||||
created_at: timestamp('created_at').notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export const candles = sqliteTable('candles', {
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
export const candles = pgTable('candles', {
|
||||
id: serial('id').primaryKey(),
|
||||
chart_id: integer('chart_id').notNull().references(() => charts.id),
|
||||
time: integer('time').notNull(),
|
||||
open: real('open').notNull(),
|
||||
high: real('high').notNull(),
|
||||
low: real('low').notNull(),
|
||||
close: real('close').notNull(),
|
||||
time: timestamp('time').notNull(),
|
||||
open: doublePrecision('open').notNull(),
|
||||
high: doublePrecision('high').notNull(),
|
||||
low: doublePrecision('low').notNull(),
|
||||
close: doublePrecision('close').notNull(),
|
||||
}, (table) => [
|
||||
uniqueIndex('candles_chart_time_unique').on(table.chart_id, table.time),
|
||||
]);
|
||||
|
||||
export const annotationTypes = sqliteTable('annotation_types', {
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
export const annotationTypes = pgTable('annotation_types', {
|
||||
id: serial('id').primaryKey(),
|
||||
name: text('name').notNull().unique(), // internal name (e.g., 'break_up')
|
||||
display_name: text('display_name').notNull(), // display name (e.g., 'Break Up')
|
||||
color: text('color').notNull(), // hex color code
|
||||
category: text('category').notNull(), // 'marker' or 'line'
|
||||
icon: text('icon'), // icon name or symbol
|
||||
is_active: integer('is_active').notNull().default(1), // 1 = active, 0 = inactive
|
||||
created_at: integer('created_at').notNull(),
|
||||
is_active: boolean('is_active').notNull().default(true), // true = active, false = inactive
|
||||
created_at: timestamp('created_at').notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export const annotations = sqliteTable('annotations', {
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
export const annotations = pgTable('annotations', {
|
||||
id: serial('id').primaryKey(),
|
||||
chart_id: integer('chart_id').notNull().references(() => charts.id),
|
||||
timestamp: integer('timestamp').notNull(),
|
||||
timestamp: timestamp('timestamp').notNull(),
|
||||
label_type: text('label_type').notNull(),
|
||||
geometry: text('geometry'), // JSON string for line coordinates
|
||||
geometry: jsonb('geometry'), // JSON for line coordinates
|
||||
color: text('color').default('#3b82f6'), // hex color code
|
||||
created_at: integer('created_at').notNull(),
|
||||
created_at: timestamp('created_at').notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export const spanLabelTypes = sqliteTable('span_label_types', {
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
export const spanLabelTypes = pgTable('span_label_types', {
|
||||
id: serial('id').primaryKey(),
|
||||
name: text('name').notNull().unique(), // internal name (e.g., 'bull_flag')
|
||||
display_name: text('display_name').notNull(), // UI label (e.g., 'Bull Flag')
|
||||
color: text('color').notNull(), // hex color for rectangle fill
|
||||
hotkey: text('hotkey'), // keyboard shortcut (e.g., '1')
|
||||
is_active: integer('is_active').notNull().default(1), // 1 = active, 0 = inactive
|
||||
is_active: boolean('is_active').notNull().default(true), // true = active, false = inactive
|
||||
sort_order: integer('sort_order').notNull().default(0), // display order
|
||||
created_at: integer('created_at').notNull(),
|
||||
created_at: timestamp('created_at').notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export const spanAnnotations = sqliteTable('span_annotations', {
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
export const spanAnnotations = pgTable('span_annotations', {
|
||||
id: serial('id').primaryKey(),
|
||||
chart_id: integer('chart_id').notNull().references(() => charts.id),
|
||||
start_time: integer('start_time').notNull(), // Unix timestamp of first candle
|
||||
end_time: integer('end_time').notNull(), // Unix timestamp of last candle
|
||||
start_time: timestamp('start_time').notNull(), // timestamp of first candle
|
||||
end_time: timestamp('end_time').notNull(), // timestamp of last candle
|
||||
label: text('label').notNull(), // pattern name referencing span_label_types.name
|
||||
confidence: integer('confidence'), // 1-5 scale, nullable
|
||||
outcome: text('outcome'), // 'win'|'loss'|'breakeven'|null
|
||||
notes: text('notes'), // free-text, nullable
|
||||
sub_spans: text('sub_spans'), // JSON array of sub-span objects, nullable
|
||||
sub_spans: jsonb('sub_spans'), // JSON array of sub-span objects, nullable
|
||||
color: text('color').notNull().default('#2196F3'), // hex color
|
||||
source: text('source').notNull().default('human'), // 'human'|'model'|'human_correction'
|
||||
model_prediction: text('model_prediction'), // JSON metadata when confirming/correcting predictions
|
||||
created_at: integer('created_at').notNull(),
|
||||
model_prediction: jsonb('model_prediction'), // JSON metadata when confirming/correcting predictions
|
||||
created_at: timestamp('created_at').notNull().defaultNow(),
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue