/** @type {import('node-pg-migrate').MigrationBuilder} */ exports.up = (pgm) => { pgm.sql(`CREATE EXTENSION IF NOT EXISTS timescaledb CASCADE`); pgm.createTable('signals', { real_time: { type: 'timestamptz', notNull: true }, game_tick: { type: 'bigint', notNull: true }, combinator: { type: 'text', notNull: true }, item_key: { type: 'text', notNull: true }, green: { type: 'integer', notNull: true, default: 0 }, red: { type: 'integer', notNull: true, default: 0 }, logistic: { type: 'integer' }, }, { ifNotExists: true }); pgm.sql(`SELECT create_hypertable('signals', 'real_time', if_not_exists => true)`); pgm.sql(`CREATE INDEX IF NOT EXISTS signals_combinator_item_key_real_time_idx ON signals (combinator, item_key, real_time DESC)`); pgm.sql(`CREATE INDEX IF NOT EXISTS signals_game_tick_idx ON signals (game_tick DESC)`); pgm.sql(`SELECT add_retention_policy('signals', INTERVAL '30 days', if_not_exists => true)`); pgm.createTable('tick_timing', { real_time: { type: 'timestamptz', notNull: true }, game_tick: { type: 'bigint', notNull: true }, combinator: { type: 'text', notNull: true }, }, { ifNotExists: true }); pgm.sql(`SELECT create_hypertable('tick_timing', 'real_time', if_not_exists => true)`); pgm.sql(`CREATE INDEX IF NOT EXISTS tick_timing_combinator_real_time_idx ON tick_timing (combinator, real_time DESC)`); pgm.sql(`SELECT add_retention_policy('tick_timing', INTERVAL '30 days', if_not_exists => true)`); pgm.createTable('charts', { id: { type: 'uuid', primaryKey: true, default: pgm.func('gen_random_uuid()') }, title: { type: 'text', notNull: true }, pos_x: { type: 'integer', notNull: true, default: 0 }, pos_y: { type: 'integer', notNull: true, default: 0 }, width: { type: 'integer', notNull: true, default: 2 }, height: { type: 'integer', notNull: true, default: 4 }, signal_type: { type: 'text', notNull: true, default: 'both' }, chart_type: { type: 'text', notNull: true, default: 'signals' }, viz_type: { type: 'text', notNull: true, default: 'line' }, filter_combinators: { type: 'text[]' }, filter_items: { type: 'text[]' }, filter_items_exclude: { type: 'text[]' }, filter_items_regex: { type: 'boolean', notNull: true, default: false }, y_min: { type: 'real' }, y_max: { type: 'real' }, series_limit: { type: 'integer', notNull: true, default: 20 }, order_by: { type: 'text', notNull: true, default: 'time' }, created_at: { type: 'timestamptz', notNull: true, default: pgm.func('now()') }, }, { ifNotExists: true }); // Use DO blocks so constraints are idempotent on existing DBs pgm.sql(`DO $$ BEGIN ALTER TABLE charts ADD CONSTRAINT charts_signal_type_check CHECK (signal_type IN ('green','red','both')); EXCEPTION WHEN duplicate_object THEN NULL; END $$`); pgm.sql(`DO $$ BEGIN ALTER TABLE charts ADD CONSTRAINT charts_chart_type_check CHECK (chart_type IN ('signals','ups')); EXCEPTION WHEN duplicate_object THEN NULL; END $$`); pgm.sql(`DO $$ BEGIN ALTER TABLE charts ADD CONSTRAINT charts_viz_type_check CHECK (viz_type IN ('line','stacked','table')); EXCEPTION WHEN duplicate_object THEN NULL; END $$`); pgm.sql(`DO $$ BEGIN ALTER TABLE charts ADD CONSTRAINT charts_order_by_check CHECK (order_by IN ('time','value_asc','value_desc','abs_desc','delta_asc','delta_desc')); EXCEPTION WHEN duplicate_object THEN NULL; END $$`); pgm.createTable('alerts', { id: { type: 'uuid', primaryKey: true, default: pgm.func('gen_random_uuid()') }, item_key: { type: 'text', notNull: true }, item_key_is_regex: { type: 'boolean', notNull: true, default: false }, combinator: { type: 'text' }, signal_type: { type: 'text', notNull: true, default: 'green' }, condition: { type: 'text', notNull: true }, threshold: { type: 'integer', notNull: true }, active: { type: 'boolean', notNull: true, default: true }, created_at: { type: 'timestamptz', notNull: true, default: pgm.func('now()') }, }, { ifNotExists: true }); pgm.sql(`DO $$ BEGIN ALTER TABLE alerts ADD CONSTRAINT alerts_signal_type_check CHECK (signal_type IN ('green','red')); EXCEPTION WHEN duplicate_object THEN NULL; END $$`); pgm.sql(`DO $$ BEGIN ALTER TABLE alerts ADD CONSTRAINT alerts_condition_check CHECK (condition IN ('above','below')); EXCEPTION WHEN duplicate_object THEN NULL; END $$`); pgm.createTable('settings', { key: { type: 'text', primaryKey: true }, value: { type: 'text', notNull: true }, }, { ifNotExists: true }); }; exports.down = () => Promise.resolve();