forked from boos/BoosAPI
feat(db): generate schema artifacts and align sqlite baseline
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
CREATE TABLE IF NOT EXISTS `account_tokens` (`id` INT AUTO_INCREMENT NOT NULL PRIMARY KEY, `account_id` INT NOT NULL, `name` TEXT NOT NULL, `token` TEXT NOT NULL, `source` TEXT DEFAULT 'manual', `enabled` BOOLEAN DEFAULT true, `is_default` BOOLEAN DEFAULT false, `created_at` TEXT DEFAULT datetime('now'), `updated_at` TEXT DEFAULT datetime('now'), `token_group` TEXT, FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`) ON DELETE CASCADE);
|
||||
CREATE TABLE IF NOT EXISTS `accounts` (`id` INT AUTO_INCREMENT NOT NULL PRIMARY KEY, `site_id` INT NOT NULL, `username` TEXT, `access_token` TEXT NOT NULL, `api_token` TEXT, `balance` DOUBLE DEFAULT 0, `balance_used` DOUBLE DEFAULT 0, `quota` DOUBLE DEFAULT 0, `unit_cost` DOUBLE, `value_score` DOUBLE DEFAULT 0, `status` TEXT DEFAULT 'active', `checkin_enabled` BOOLEAN DEFAULT true, `last_checkin_at` TEXT, `last_balance_refresh` TEXT, `extra_config` TEXT, `created_at` TEXT DEFAULT datetime('now'), `updated_at` TEXT DEFAULT datetime('now'), `is_pinned` BOOLEAN DEFAULT false, `sort_order` INT DEFAULT 0, FOREIGN KEY (`site_id`) REFERENCES `sites`(`id`) ON DELETE CASCADE);
|
||||
CREATE TABLE IF NOT EXISTS `checkin_logs` (`id` INT AUTO_INCREMENT NOT NULL PRIMARY KEY, `account_id` INT NOT NULL, `status` TEXT NOT NULL, `message` TEXT, `reward` TEXT, `created_at` TEXT DEFAULT datetime('now'), FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`) ON DELETE CASCADE);
|
||||
CREATE TABLE IF NOT EXISTS `downstream_api_keys` (`id` INT AUTO_INCREMENT NOT NULL PRIMARY KEY, `name` TEXT NOT NULL, `key` TEXT NOT NULL, `description` TEXT, `enabled` BOOLEAN DEFAULT true, `expires_at` TEXT, `max_cost` DOUBLE, `used_cost` DOUBLE DEFAULT 0, `max_requests` INT, `used_requests` INT DEFAULT 0, `supported_models` TEXT, `allowed_route_ids` TEXT, `site_weight_multipliers` TEXT, `last_used_at` TEXT, `created_at` TEXT DEFAULT datetime('now'), `updated_at` TEXT DEFAULT datetime('now'));
|
||||
CREATE TABLE IF NOT EXISTS `events` (`id` INT AUTO_INCREMENT NOT NULL PRIMARY KEY, `type` TEXT NOT NULL, `title` TEXT NOT NULL, `message` TEXT, `level` TEXT NOT NULL DEFAULT 'info', `read` BOOLEAN DEFAULT false, `related_id` INT, `related_type` TEXT, `created_at` TEXT DEFAULT datetime('now'));
|
||||
CREATE TABLE IF NOT EXISTS `model_availability` (`id` INT AUTO_INCREMENT NOT NULL PRIMARY KEY, `account_id` INT NOT NULL, `model_name` TEXT NOT NULL, `available` BOOLEAN, `latency_ms` INT, `checked_at` TEXT DEFAULT datetime('now'), FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`) ON DELETE CASCADE);
|
||||
CREATE TABLE IF NOT EXISTS `proxy_files` (`id` INT AUTO_INCREMENT NOT NULL PRIMARY KEY, `public_id` TEXT NOT NULL, `owner_type` TEXT NOT NULL, `owner_id` TEXT NOT NULL, `filename` TEXT NOT NULL, `mime_type` TEXT NOT NULL, `purpose` TEXT, `byte_size` INT NOT NULL, `sha256` TEXT NOT NULL, `content_base64` TEXT NOT NULL, `created_at` TEXT DEFAULT datetime('now'), `updated_at` TEXT DEFAULT datetime('now'), `deleted_at` TEXT);
|
||||
CREATE TABLE IF NOT EXISTS `proxy_logs` (`id` INT AUTO_INCREMENT NOT NULL PRIMARY KEY, `route_id` INT, `channel_id` INT, `account_id` INT, `model_requested` TEXT, `model_actual` TEXT, `status` TEXT, `http_status` INT, `latency_ms` INT, `prompt_tokens` INT, `completion_tokens` INT, `total_tokens` INT, `estimated_cost` DOUBLE, `error_message` TEXT, `retry_count` INT DEFAULT 0, `created_at` TEXT DEFAULT datetime('now'), `billing_details` TEXT);
|
||||
CREATE TABLE IF NOT EXISTS `proxy_video_tasks` (`id` INT AUTO_INCREMENT NOT NULL PRIMARY KEY, `public_id` TEXT NOT NULL, `upstream_video_id` TEXT NOT NULL, `site_url` TEXT NOT NULL, `token_value` TEXT NOT NULL, `requested_model` TEXT, `actual_model` TEXT, `channel_id` INT, `account_id` INT, `status_snapshot` TEXT, `upstream_response_meta` TEXT, `last_upstream_status` INT, `last_polled_at` TEXT, `created_at` TEXT DEFAULT datetime('now'), `updated_at` TEXT DEFAULT datetime('now'));
|
||||
CREATE TABLE IF NOT EXISTS `route_channels` (`id` INT AUTO_INCREMENT NOT NULL PRIMARY KEY, `route_id` INT NOT NULL, `account_id` INT NOT NULL, `token_id` INT, `source_model` TEXT, `priority` INT DEFAULT 0, `weight` INT DEFAULT 10, `enabled` BOOLEAN DEFAULT true, `manual_override` BOOLEAN DEFAULT false, `success_count` INT DEFAULT 0, `fail_count` INT DEFAULT 0, `total_latency_ms` INT DEFAULT 0, `total_cost` DOUBLE DEFAULT 0, `last_used_at` TEXT, `last_selected_at` TEXT, `last_fail_at` TEXT, `consecutive_fail_count` INT NOT NULL DEFAULT 0, `cooldown_level` INT NOT NULL DEFAULT 0, `cooldown_until` TEXT, FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`) ON DELETE CASCADE, FOREIGN KEY (`route_id`) REFERENCES `token_routes`(`id`) ON DELETE CASCADE, FOREIGN KEY (`token_id`) REFERENCES `account_tokens`(`id`) ON DELETE SET NULL);
|
||||
CREATE TABLE IF NOT EXISTS `settings` (`key` TEXT NOT NULL PRIMARY KEY, `value` TEXT);
|
||||
CREATE TABLE IF NOT EXISTS `site_disabled_models` (`id` INT AUTO_INCREMENT NOT NULL PRIMARY KEY, `site_id` INT NOT NULL, `model_name` TEXT NOT NULL, `created_at` TEXT DEFAULT datetime('now'), FOREIGN KEY (`site_id`) REFERENCES `sites`(`id`) ON DELETE CASCADE);
|
||||
CREATE TABLE IF NOT EXISTS `sites` (`id` INT AUTO_INCREMENT NOT NULL PRIMARY KEY, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `platform` TEXT NOT NULL, `status` TEXT NOT NULL DEFAULT 'active', `api_key` TEXT, `created_at` TEXT DEFAULT datetime('now'), `updated_at` TEXT DEFAULT datetime('now'), `is_pinned` BOOLEAN DEFAULT false, `sort_order` INT DEFAULT 0, `proxy_url` TEXT, `use_system_proxy` BOOLEAN DEFAULT false, `custom_headers` TEXT, `external_checkin_url` TEXT, `global_weight` DOUBLE DEFAULT 1);
|
||||
CREATE TABLE IF NOT EXISTS `token_model_availability` (`id` INT AUTO_INCREMENT NOT NULL PRIMARY KEY, `token_id` INT NOT NULL, `model_name` TEXT NOT NULL, `available` BOOLEAN, `latency_ms` INT, `checked_at` TEXT DEFAULT datetime('now'), FOREIGN KEY (`token_id`) REFERENCES `account_tokens`(`id`) ON DELETE CASCADE);
|
||||
CREATE TABLE IF NOT EXISTS `token_routes` (`id` INT AUTO_INCREMENT NOT NULL PRIMARY KEY, `model_pattern` TEXT NOT NULL, `model_mapping` TEXT, `enabled` BOOLEAN DEFAULT true, `created_at` TEXT DEFAULT datetime('now'), `updated_at` TEXT DEFAULT datetime('now'), `display_name` TEXT, `display_icon` TEXT, `decision_snapshot` TEXT, `decision_refreshed_at` TEXT, `routing_strategy` TEXT DEFAULT 'weighted');
|
||||
CREATE UNIQUE INDEX `downstream_api_keys_key_unique` ON `downstream_api_keys` (`key`(191));
|
||||
CREATE UNIQUE INDEX `model_availability_account_model_unique` ON `model_availability` (`account_id`, `model_name`(191));
|
||||
CREATE UNIQUE INDEX `proxy_files_public_id_unique` ON `proxy_files` (`public_id`(191));
|
||||
CREATE UNIQUE INDEX `proxy_video_tasks_public_id_unique` ON `proxy_video_tasks` (`public_id`(191));
|
||||
CREATE UNIQUE INDEX `site_disabled_models_site_model_unique` ON `site_disabled_models` (`site_id`, `model_name`(191));
|
||||
CREATE UNIQUE INDEX `token_model_availability_token_model_unique` ON `token_model_availability` (`token_id`, `model_name`(191));
|
||||
CREATE INDEX `account_tokens_account_enabled_idx` ON `account_tokens` (`account_id`, `enabled`);
|
||||
CREATE INDEX `account_tokens_account_id_idx` ON `account_tokens` (`account_id`);
|
||||
CREATE INDEX `account_tokens_enabled_idx` ON `account_tokens` (`enabled`);
|
||||
CREATE INDEX `accounts_site_id_idx` ON `accounts` (`site_id`);
|
||||
CREATE INDEX `accounts_site_status_idx` ON `accounts` (`site_id`, `status`(191));
|
||||
CREATE INDEX `accounts_status_idx` ON `accounts` (`status`(191));
|
||||
CREATE INDEX `checkin_logs_account_created_at_idx` ON `checkin_logs` (`account_id`, `created_at`(191));
|
||||
CREATE INDEX `checkin_logs_created_at_idx` ON `checkin_logs` (`created_at`(191));
|
||||
CREATE INDEX `checkin_logs_status_idx` ON `checkin_logs` (`status`(191));
|
||||
CREATE INDEX `downstream_api_keys_enabled_idx` ON `downstream_api_keys` (`enabled`);
|
||||
CREATE INDEX `downstream_api_keys_expires_at_idx` ON `downstream_api_keys` (`expires_at`(191));
|
||||
CREATE INDEX `downstream_api_keys_name_idx` ON `downstream_api_keys` (`name`(191));
|
||||
CREATE INDEX `events_created_at_idx` ON `events` (`created_at`(191));
|
||||
CREATE INDEX `events_read_created_at_idx` ON `events` (`read`, `created_at`(191));
|
||||
CREATE INDEX `events_type_created_at_idx` ON `events` (`type`(191), `created_at`(191));
|
||||
CREATE INDEX `model_availability_account_available_idx` ON `model_availability` (`account_id`, `available`);
|
||||
CREATE INDEX `model_availability_model_name_idx` ON `model_availability` (`model_name`(191));
|
||||
CREATE INDEX `proxy_files_owner_lookup_idx` ON `proxy_files` (`owner_type`(191), `owner_id`(191), `deleted_at`(191));
|
||||
CREATE INDEX `proxy_logs_account_created_at_idx` ON `proxy_logs` (`account_id`, `created_at`(191));
|
||||
CREATE INDEX `proxy_logs_created_at_idx` ON `proxy_logs` (`created_at`(191));
|
||||
CREATE INDEX `proxy_logs_model_actual_created_at_idx` ON `proxy_logs` (`model_actual`(191), `created_at`(191));
|
||||
CREATE INDEX `proxy_logs_status_created_at_idx` ON `proxy_logs` (`status`(191), `created_at`(191));
|
||||
CREATE INDEX `proxy_video_tasks_created_at_idx` ON `proxy_video_tasks` (`created_at`(191));
|
||||
CREATE INDEX `proxy_video_tasks_upstream_video_id_idx` ON `proxy_video_tasks` (`upstream_video_id`(191));
|
||||
CREATE INDEX `route_channels_account_id_idx` ON `route_channels` (`account_id`);
|
||||
CREATE INDEX `route_channels_route_enabled_idx` ON `route_channels` (`route_id`, `enabled`);
|
||||
CREATE INDEX `route_channels_route_id_idx` ON `route_channels` (`route_id`);
|
||||
CREATE INDEX `route_channels_route_token_idx` ON `route_channels` (`route_id`, `token_id`);
|
||||
CREATE INDEX `route_channels_token_id_idx` ON `route_channels` (`token_id`);
|
||||
CREATE INDEX `site_disabled_models_site_id_idx` ON `site_disabled_models` (`site_id`);
|
||||
CREATE INDEX `sites_status_idx` ON `sites` (`status`(191));
|
||||
CREATE INDEX `token_model_availability_available_idx` ON `token_model_availability` (`available`);
|
||||
CREATE INDEX `token_model_availability_model_name_idx` ON `token_model_availability` (`model_name`(191));
|
||||
CREATE INDEX `token_model_availability_token_available_idx` ON `token_model_availability` (`token_id`, `available`);
|
||||
CREATE INDEX `token_routes_enabled_idx` ON `token_routes` (`enabled`);
|
||||
CREATE INDEX `token_routes_model_pattern_idx` ON `token_routes` (`model_pattern`(191));
|
||||
@@ -0,0 +1,27 @@
|
||||
CREATE TABLE IF NOT EXISTS `downstream_api_keys` (`id` INT AUTO_INCREMENT NOT NULL PRIMARY KEY, `name` TEXT NOT NULL, `key` TEXT NOT NULL, `description` TEXT, `enabled` BOOLEAN DEFAULT true, `expires_at` TEXT, `max_cost` DOUBLE, `used_cost` DOUBLE DEFAULT 0, `max_requests` INT, `used_requests` INT DEFAULT 0, `supported_models` TEXT, `allowed_route_ids` TEXT, `site_weight_multipliers` TEXT, `last_used_at` TEXT, `created_at` TEXT DEFAULT datetime('now'), `updated_at` TEXT DEFAULT datetime('now'));
|
||||
CREATE TABLE IF NOT EXISTS `proxy_files` (`id` INT AUTO_INCREMENT NOT NULL PRIMARY KEY, `public_id` TEXT NOT NULL, `owner_type` TEXT NOT NULL, `owner_id` TEXT NOT NULL, `filename` TEXT NOT NULL, `mime_type` TEXT NOT NULL, `purpose` TEXT, `byte_size` INT NOT NULL, `sha256` TEXT NOT NULL, `content_base64` TEXT NOT NULL, `created_at` TEXT DEFAULT datetime('now'), `updated_at` TEXT DEFAULT datetime('now'), `deleted_at` TEXT);
|
||||
CREATE TABLE IF NOT EXISTS `proxy_video_tasks` (`id` INT AUTO_INCREMENT NOT NULL PRIMARY KEY, `public_id` TEXT NOT NULL, `upstream_video_id` TEXT NOT NULL, `site_url` TEXT NOT NULL, `token_value` TEXT NOT NULL, `requested_model` TEXT, `actual_model` TEXT, `channel_id` INT, `account_id` INT, `status_snapshot` TEXT, `upstream_response_meta` TEXT, `last_upstream_status` INT, `last_polled_at` TEXT, `created_at` TEXT DEFAULT datetime('now'), `updated_at` TEXT DEFAULT datetime('now'));
|
||||
ALTER TABLE `route_channels` ADD COLUMN `source_model` TEXT;
|
||||
ALTER TABLE `route_channels` ADD COLUMN `last_selected_at` TEXT;
|
||||
ALTER TABLE `route_channels` ADD COLUMN `consecutive_fail_count` INT NOT NULL DEFAULT 0;
|
||||
ALTER TABLE `route_channels` ADD COLUMN `cooldown_level` INT NOT NULL DEFAULT 0;
|
||||
ALTER TABLE `sites` ADD COLUMN `proxy_url` TEXT;
|
||||
ALTER TABLE `sites` ADD COLUMN `use_system_proxy` BOOLEAN DEFAULT false;
|
||||
ALTER TABLE `sites` ADD COLUMN `custom_headers` TEXT;
|
||||
ALTER TABLE `sites` ADD COLUMN `external_checkin_url` TEXT;
|
||||
ALTER TABLE `sites` ADD COLUMN `global_weight` DOUBLE DEFAULT 1;
|
||||
ALTER TABLE `token_routes` ADD COLUMN `display_name` TEXT;
|
||||
ALTER TABLE `token_routes` ADD COLUMN `display_icon` TEXT;
|
||||
ALTER TABLE `token_routes` ADD COLUMN `decision_snapshot` TEXT;
|
||||
ALTER TABLE `token_routes` ADD COLUMN `decision_refreshed_at` TEXT;
|
||||
ALTER TABLE `token_routes` ADD COLUMN `routing_strategy` TEXT DEFAULT 'weighted';
|
||||
CREATE UNIQUE INDEX `downstream_api_keys_key_unique` ON `downstream_api_keys` (`key`(191));
|
||||
CREATE UNIQUE INDEX `model_availability_account_model_unique` ON `model_availability` (`account_id`, `model_name`(191));
|
||||
CREATE UNIQUE INDEX `proxy_files_public_id_unique` ON `proxy_files` (`public_id`(191));
|
||||
CREATE UNIQUE INDEX `proxy_video_tasks_public_id_unique` ON `proxy_video_tasks` (`public_id`(191));
|
||||
CREATE INDEX `downstream_api_keys_enabled_idx` ON `downstream_api_keys` (`enabled`);
|
||||
CREATE INDEX `downstream_api_keys_expires_at_idx` ON `downstream_api_keys` (`expires_at`(191));
|
||||
CREATE INDEX `downstream_api_keys_name_idx` ON `downstream_api_keys` (`name`(191));
|
||||
CREATE INDEX `proxy_files_owner_lookup_idx` ON `proxy_files` (`owner_type`(191), `owner_id`(191), `deleted_at`(191));
|
||||
CREATE INDEX `proxy_video_tasks_created_at_idx` ON `proxy_video_tasks` (`created_at`(191));
|
||||
CREATE INDEX `proxy_video_tasks_upstream_video_id_idx` ON `proxy_video_tasks` (`upstream_video_id`(191));
|
||||
@@ -0,0 +1,57 @@
|
||||
CREATE TABLE IF NOT EXISTS "account_tokens" ("id" INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL PRIMARY KEY, "account_id" INTEGER NOT NULL, "name" TEXT NOT NULL, "token" TEXT NOT NULL, "source" TEXT DEFAULT 'manual', "enabled" BOOLEAN DEFAULT true, "is_default" BOOLEAN DEFAULT false, "created_at" TEXT DEFAULT datetime('now'), "updated_at" TEXT DEFAULT datetime('now'), "token_group" TEXT, FOREIGN KEY ("account_id") REFERENCES "accounts"("id") ON DELETE CASCADE);
|
||||
CREATE TABLE IF NOT EXISTS "accounts" ("id" INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL PRIMARY KEY, "site_id" INTEGER NOT NULL, "username" TEXT, "access_token" TEXT NOT NULL, "api_token" TEXT, "balance" DOUBLE PRECISION DEFAULT 0, "balance_used" DOUBLE PRECISION DEFAULT 0, "quota" DOUBLE PRECISION DEFAULT 0, "unit_cost" DOUBLE PRECISION, "value_score" DOUBLE PRECISION DEFAULT 0, "status" TEXT DEFAULT 'active', "checkin_enabled" BOOLEAN DEFAULT true, "last_checkin_at" TEXT, "last_balance_refresh" TEXT, "extra_config" TEXT, "created_at" TEXT DEFAULT datetime('now'), "updated_at" TEXT DEFAULT datetime('now'), "is_pinned" BOOLEAN DEFAULT false, "sort_order" INTEGER DEFAULT 0, FOREIGN KEY ("site_id") REFERENCES "sites"("id") ON DELETE CASCADE);
|
||||
CREATE TABLE IF NOT EXISTS "checkin_logs" ("id" INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL PRIMARY KEY, "account_id" INTEGER NOT NULL, "status" TEXT NOT NULL, "message" TEXT, "reward" TEXT, "created_at" TEXT DEFAULT datetime('now'), FOREIGN KEY ("account_id") REFERENCES "accounts"("id") ON DELETE CASCADE);
|
||||
CREATE TABLE IF NOT EXISTS "downstream_api_keys" ("id" INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL PRIMARY KEY, "name" TEXT NOT NULL, "key" TEXT NOT NULL, "description" TEXT, "enabled" BOOLEAN DEFAULT true, "expires_at" TEXT, "max_cost" DOUBLE PRECISION, "used_cost" DOUBLE PRECISION DEFAULT 0, "max_requests" INTEGER, "used_requests" INTEGER DEFAULT 0, "supported_models" TEXT, "allowed_route_ids" TEXT, "site_weight_multipliers" TEXT, "last_used_at" TEXT, "created_at" TEXT DEFAULT datetime('now'), "updated_at" TEXT DEFAULT datetime('now'));
|
||||
CREATE TABLE IF NOT EXISTS "events" ("id" INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL PRIMARY KEY, "type" TEXT NOT NULL, "title" TEXT NOT NULL, "message" TEXT, "level" TEXT NOT NULL DEFAULT 'info', "read" BOOLEAN DEFAULT false, "related_id" INTEGER, "related_type" TEXT, "created_at" TEXT DEFAULT datetime('now'));
|
||||
CREATE TABLE IF NOT EXISTS "model_availability" ("id" INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL PRIMARY KEY, "account_id" INTEGER NOT NULL, "model_name" TEXT NOT NULL, "available" BOOLEAN, "latency_ms" INTEGER, "checked_at" TEXT DEFAULT datetime('now'), FOREIGN KEY ("account_id") REFERENCES "accounts"("id") ON DELETE CASCADE);
|
||||
CREATE TABLE IF NOT EXISTS "proxy_files" ("id" INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL PRIMARY KEY, "public_id" TEXT NOT NULL, "owner_type" TEXT NOT NULL, "owner_id" TEXT NOT NULL, "filename" TEXT NOT NULL, "mime_type" TEXT NOT NULL, "purpose" TEXT, "byte_size" INTEGER NOT NULL, "sha256" TEXT NOT NULL, "content_base64" TEXT NOT NULL, "created_at" TEXT DEFAULT datetime('now'), "updated_at" TEXT DEFAULT datetime('now'), "deleted_at" TEXT);
|
||||
CREATE TABLE IF NOT EXISTS "proxy_logs" ("id" INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL PRIMARY KEY, "route_id" INTEGER, "channel_id" INTEGER, "account_id" INTEGER, "model_requested" TEXT, "model_actual" TEXT, "status" TEXT, "http_status" INTEGER, "latency_ms" INTEGER, "prompt_tokens" INTEGER, "completion_tokens" INTEGER, "total_tokens" INTEGER, "estimated_cost" DOUBLE PRECISION, "error_message" TEXT, "retry_count" INTEGER DEFAULT 0, "created_at" TEXT DEFAULT datetime('now'), "billing_details" TEXT);
|
||||
CREATE TABLE IF NOT EXISTS "proxy_video_tasks" ("id" INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL PRIMARY KEY, "public_id" TEXT NOT NULL, "upstream_video_id" TEXT NOT NULL, "site_url" TEXT NOT NULL, "token_value" TEXT NOT NULL, "requested_model" TEXT, "actual_model" TEXT, "channel_id" INTEGER, "account_id" INTEGER, "status_snapshot" TEXT, "upstream_response_meta" TEXT, "last_upstream_status" INTEGER, "last_polled_at" TEXT, "created_at" TEXT DEFAULT datetime('now'), "updated_at" TEXT DEFAULT datetime('now'));
|
||||
CREATE TABLE IF NOT EXISTS "route_channels" ("id" INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL PRIMARY KEY, "route_id" INTEGER NOT NULL, "account_id" INTEGER NOT NULL, "token_id" INTEGER, "source_model" TEXT, "priority" INTEGER DEFAULT 0, "weight" INTEGER DEFAULT 10, "enabled" BOOLEAN DEFAULT true, "manual_override" BOOLEAN DEFAULT false, "success_count" INTEGER DEFAULT 0, "fail_count" INTEGER DEFAULT 0, "total_latency_ms" INTEGER DEFAULT 0, "total_cost" DOUBLE PRECISION DEFAULT 0, "last_used_at" TEXT, "last_selected_at" TEXT, "last_fail_at" TEXT, "consecutive_fail_count" INTEGER NOT NULL DEFAULT 0, "cooldown_level" INTEGER NOT NULL DEFAULT 0, "cooldown_until" TEXT, FOREIGN KEY ("account_id") REFERENCES "accounts"("id") ON DELETE CASCADE, FOREIGN KEY ("route_id") REFERENCES "token_routes"("id") ON DELETE CASCADE, FOREIGN KEY ("token_id") REFERENCES "account_tokens"("id") ON DELETE SET NULL);
|
||||
CREATE TABLE IF NOT EXISTS "settings" ("key" TEXT NOT NULL PRIMARY KEY, "value" TEXT);
|
||||
CREATE TABLE IF NOT EXISTS "site_disabled_models" ("id" INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL PRIMARY KEY, "site_id" INTEGER NOT NULL, "model_name" TEXT NOT NULL, "created_at" TEXT DEFAULT datetime('now'), FOREIGN KEY ("site_id") REFERENCES "sites"("id") ON DELETE CASCADE);
|
||||
CREATE TABLE IF NOT EXISTS "sites" ("id" INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL PRIMARY KEY, "name" TEXT NOT NULL, "url" TEXT NOT NULL, "platform" TEXT NOT NULL, "status" TEXT NOT NULL DEFAULT 'active', "api_key" TEXT, "created_at" TEXT DEFAULT datetime('now'), "updated_at" TEXT DEFAULT datetime('now'), "is_pinned" BOOLEAN DEFAULT false, "sort_order" INTEGER DEFAULT 0, "proxy_url" TEXT, "use_system_proxy" BOOLEAN DEFAULT false, "custom_headers" TEXT, "external_checkin_url" TEXT, "global_weight" DOUBLE PRECISION DEFAULT 1);
|
||||
CREATE TABLE IF NOT EXISTS "token_model_availability" ("id" INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL PRIMARY KEY, "token_id" INTEGER NOT NULL, "model_name" TEXT NOT NULL, "available" BOOLEAN, "latency_ms" INTEGER, "checked_at" TEXT DEFAULT datetime('now'), FOREIGN KEY ("token_id") REFERENCES "account_tokens"("id") ON DELETE CASCADE);
|
||||
CREATE TABLE IF NOT EXISTS "token_routes" ("id" INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL PRIMARY KEY, "model_pattern" TEXT NOT NULL, "model_mapping" TEXT, "enabled" BOOLEAN DEFAULT true, "created_at" TEXT DEFAULT datetime('now'), "updated_at" TEXT DEFAULT datetime('now'), "display_name" TEXT, "display_icon" TEXT, "decision_snapshot" TEXT, "decision_refreshed_at" TEXT, "routing_strategy" TEXT DEFAULT 'weighted');
|
||||
CREATE UNIQUE INDEX "downstream_api_keys_key_unique" ON "downstream_api_keys" ("key");
|
||||
CREATE UNIQUE INDEX "model_availability_account_model_unique" ON "model_availability" ("account_id", "model_name");
|
||||
CREATE UNIQUE INDEX "proxy_files_public_id_unique" ON "proxy_files" ("public_id");
|
||||
CREATE UNIQUE INDEX "proxy_video_tasks_public_id_unique" ON "proxy_video_tasks" ("public_id");
|
||||
CREATE UNIQUE INDEX "site_disabled_models_site_model_unique" ON "site_disabled_models" ("site_id", "model_name");
|
||||
CREATE UNIQUE INDEX "token_model_availability_token_model_unique" ON "token_model_availability" ("token_id", "model_name");
|
||||
CREATE INDEX "account_tokens_account_enabled_idx" ON "account_tokens" ("account_id", "enabled");
|
||||
CREATE INDEX "account_tokens_account_id_idx" ON "account_tokens" ("account_id");
|
||||
CREATE INDEX "account_tokens_enabled_idx" ON "account_tokens" ("enabled");
|
||||
CREATE INDEX "accounts_site_id_idx" ON "accounts" ("site_id");
|
||||
CREATE INDEX "accounts_site_status_idx" ON "accounts" ("site_id", "status");
|
||||
CREATE INDEX "accounts_status_idx" ON "accounts" ("status");
|
||||
CREATE INDEX "checkin_logs_account_created_at_idx" ON "checkin_logs" ("account_id", "created_at");
|
||||
CREATE INDEX "checkin_logs_created_at_idx" ON "checkin_logs" ("created_at");
|
||||
CREATE INDEX "checkin_logs_status_idx" ON "checkin_logs" ("status");
|
||||
CREATE INDEX "downstream_api_keys_enabled_idx" ON "downstream_api_keys" ("enabled");
|
||||
CREATE INDEX "downstream_api_keys_expires_at_idx" ON "downstream_api_keys" ("expires_at");
|
||||
CREATE INDEX "downstream_api_keys_name_idx" ON "downstream_api_keys" ("name");
|
||||
CREATE INDEX "events_created_at_idx" ON "events" ("created_at");
|
||||
CREATE INDEX "events_read_created_at_idx" ON "events" ("read", "created_at");
|
||||
CREATE INDEX "events_type_created_at_idx" ON "events" ("type", "created_at");
|
||||
CREATE INDEX "model_availability_account_available_idx" ON "model_availability" ("account_id", "available");
|
||||
CREATE INDEX "model_availability_model_name_idx" ON "model_availability" ("model_name");
|
||||
CREATE INDEX "proxy_files_owner_lookup_idx" ON "proxy_files" ("owner_type", "owner_id", "deleted_at");
|
||||
CREATE INDEX "proxy_logs_account_created_at_idx" ON "proxy_logs" ("account_id", "created_at");
|
||||
CREATE INDEX "proxy_logs_created_at_idx" ON "proxy_logs" ("created_at");
|
||||
CREATE INDEX "proxy_logs_model_actual_created_at_idx" ON "proxy_logs" ("model_actual", "created_at");
|
||||
CREATE INDEX "proxy_logs_status_created_at_idx" ON "proxy_logs" ("status", "created_at");
|
||||
CREATE INDEX "proxy_video_tasks_created_at_idx" ON "proxy_video_tasks" ("created_at");
|
||||
CREATE INDEX "proxy_video_tasks_upstream_video_id_idx" ON "proxy_video_tasks" ("upstream_video_id");
|
||||
CREATE INDEX "route_channels_account_id_idx" ON "route_channels" ("account_id");
|
||||
CREATE INDEX "route_channels_route_enabled_idx" ON "route_channels" ("route_id", "enabled");
|
||||
CREATE INDEX "route_channels_route_id_idx" ON "route_channels" ("route_id");
|
||||
CREATE INDEX "route_channels_route_token_idx" ON "route_channels" ("route_id", "token_id");
|
||||
CREATE INDEX "route_channels_token_id_idx" ON "route_channels" ("token_id");
|
||||
CREATE INDEX "site_disabled_models_site_id_idx" ON "site_disabled_models" ("site_id");
|
||||
CREATE INDEX "sites_status_idx" ON "sites" ("status");
|
||||
CREATE INDEX "token_model_availability_available_idx" ON "token_model_availability" ("available");
|
||||
CREATE INDEX "token_model_availability_model_name_idx" ON "token_model_availability" ("model_name");
|
||||
CREATE INDEX "token_model_availability_token_available_idx" ON "token_model_availability" ("token_id", "available");
|
||||
CREATE INDEX "token_routes_enabled_idx" ON "token_routes" ("enabled");
|
||||
CREATE INDEX "token_routes_model_pattern_idx" ON "token_routes" ("model_pattern");
|
||||
@@ -0,0 +1,27 @@
|
||||
CREATE TABLE IF NOT EXISTS "downstream_api_keys" ("id" INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL PRIMARY KEY, "name" TEXT NOT NULL, "key" TEXT NOT NULL, "description" TEXT, "enabled" BOOLEAN DEFAULT true, "expires_at" TEXT, "max_cost" DOUBLE PRECISION, "used_cost" DOUBLE PRECISION DEFAULT 0, "max_requests" INTEGER, "used_requests" INTEGER DEFAULT 0, "supported_models" TEXT, "allowed_route_ids" TEXT, "site_weight_multipliers" TEXT, "last_used_at" TEXT, "created_at" TEXT DEFAULT datetime('now'), "updated_at" TEXT DEFAULT datetime('now'));
|
||||
CREATE TABLE IF NOT EXISTS "proxy_files" ("id" INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL PRIMARY KEY, "public_id" TEXT NOT NULL, "owner_type" TEXT NOT NULL, "owner_id" TEXT NOT NULL, "filename" TEXT NOT NULL, "mime_type" TEXT NOT NULL, "purpose" TEXT, "byte_size" INTEGER NOT NULL, "sha256" TEXT NOT NULL, "content_base64" TEXT NOT NULL, "created_at" TEXT DEFAULT datetime('now'), "updated_at" TEXT DEFAULT datetime('now'), "deleted_at" TEXT);
|
||||
CREATE TABLE IF NOT EXISTS "proxy_video_tasks" ("id" INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL PRIMARY KEY, "public_id" TEXT NOT NULL, "upstream_video_id" TEXT NOT NULL, "site_url" TEXT NOT NULL, "token_value" TEXT NOT NULL, "requested_model" TEXT, "actual_model" TEXT, "channel_id" INTEGER, "account_id" INTEGER, "status_snapshot" TEXT, "upstream_response_meta" TEXT, "last_upstream_status" INTEGER, "last_polled_at" TEXT, "created_at" TEXT DEFAULT datetime('now'), "updated_at" TEXT DEFAULT datetime('now'));
|
||||
ALTER TABLE "route_channels" ADD COLUMN "source_model" TEXT;
|
||||
ALTER TABLE "route_channels" ADD COLUMN "last_selected_at" TEXT;
|
||||
ALTER TABLE "route_channels" ADD COLUMN "consecutive_fail_count" INTEGER NOT NULL DEFAULT 0;
|
||||
ALTER TABLE "route_channels" ADD COLUMN "cooldown_level" INTEGER NOT NULL DEFAULT 0;
|
||||
ALTER TABLE "sites" ADD COLUMN "proxy_url" TEXT;
|
||||
ALTER TABLE "sites" ADD COLUMN "use_system_proxy" BOOLEAN DEFAULT false;
|
||||
ALTER TABLE "sites" ADD COLUMN "custom_headers" TEXT;
|
||||
ALTER TABLE "sites" ADD COLUMN "external_checkin_url" TEXT;
|
||||
ALTER TABLE "sites" ADD COLUMN "global_weight" DOUBLE PRECISION DEFAULT 1;
|
||||
ALTER TABLE "token_routes" ADD COLUMN "display_name" TEXT;
|
||||
ALTER TABLE "token_routes" ADD COLUMN "display_icon" TEXT;
|
||||
ALTER TABLE "token_routes" ADD COLUMN "decision_snapshot" TEXT;
|
||||
ALTER TABLE "token_routes" ADD COLUMN "decision_refreshed_at" TEXT;
|
||||
ALTER TABLE "token_routes" ADD COLUMN "routing_strategy" TEXT DEFAULT 'weighted';
|
||||
CREATE UNIQUE INDEX "downstream_api_keys_key_unique" ON "downstream_api_keys" ("key");
|
||||
CREATE UNIQUE INDEX "model_availability_account_model_unique" ON "model_availability" ("account_id", "model_name");
|
||||
CREATE UNIQUE INDEX "proxy_files_public_id_unique" ON "proxy_files" ("public_id");
|
||||
CREATE UNIQUE INDEX "proxy_video_tasks_public_id_unique" ON "proxy_video_tasks" ("public_id");
|
||||
CREATE INDEX "downstream_api_keys_enabled_idx" ON "downstream_api_keys" ("enabled");
|
||||
CREATE INDEX "downstream_api_keys_expires_at_idx" ON "downstream_api_keys" ("expires_at");
|
||||
CREATE INDEX "downstream_api_keys_name_idx" ON "downstream_api_keys" ("name");
|
||||
CREATE INDEX "proxy_files_owner_lookup_idx" ON "proxy_files" ("owner_type", "owner_id", "deleted_at");
|
||||
CREATE INDEX "proxy_video_tasks_created_at_idx" ON "proxy_video_tasks" ("created_at");
|
||||
CREATE INDEX "proxy_video_tasks_upstream_video_id_idx" ON "proxy_video_tasks" ("upstream_video_id");
|
||||
@@ -222,6 +222,106 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"downstream_api_keys": {
|
||||
"columns": {
|
||||
"id": {
|
||||
"logicalType": "integer",
|
||||
"notNull": true,
|
||||
"defaultValue": null,
|
||||
"primaryKey": true
|
||||
},
|
||||
"name": {
|
||||
"logicalType": "text",
|
||||
"notNull": true,
|
||||
"defaultValue": null,
|
||||
"primaryKey": false
|
||||
},
|
||||
"key": {
|
||||
"logicalType": "text",
|
||||
"notNull": true,
|
||||
"defaultValue": null,
|
||||
"primaryKey": false
|
||||
},
|
||||
"description": {
|
||||
"logicalType": "text",
|
||||
"notNull": false,
|
||||
"defaultValue": null,
|
||||
"primaryKey": false
|
||||
},
|
||||
"enabled": {
|
||||
"logicalType": "boolean",
|
||||
"notNull": false,
|
||||
"defaultValue": "true",
|
||||
"primaryKey": false
|
||||
},
|
||||
"expires_at": {
|
||||
"logicalType": "datetime",
|
||||
"notNull": false,
|
||||
"defaultValue": null,
|
||||
"primaryKey": false
|
||||
},
|
||||
"max_cost": {
|
||||
"logicalType": "real",
|
||||
"notNull": false,
|
||||
"defaultValue": null,
|
||||
"primaryKey": false
|
||||
},
|
||||
"used_cost": {
|
||||
"logicalType": "real",
|
||||
"notNull": false,
|
||||
"defaultValue": "0",
|
||||
"primaryKey": false
|
||||
},
|
||||
"max_requests": {
|
||||
"logicalType": "integer",
|
||||
"notNull": false,
|
||||
"defaultValue": null,
|
||||
"primaryKey": false
|
||||
},
|
||||
"used_requests": {
|
||||
"logicalType": "integer",
|
||||
"notNull": false,
|
||||
"defaultValue": "0",
|
||||
"primaryKey": false
|
||||
},
|
||||
"supported_models": {
|
||||
"logicalType": "json",
|
||||
"notNull": false,
|
||||
"defaultValue": null,
|
||||
"primaryKey": false
|
||||
},
|
||||
"allowed_route_ids": {
|
||||
"logicalType": "json",
|
||||
"notNull": false,
|
||||
"defaultValue": null,
|
||||
"primaryKey": false
|
||||
},
|
||||
"site_weight_multipliers": {
|
||||
"logicalType": "json",
|
||||
"notNull": false,
|
||||
"defaultValue": null,
|
||||
"primaryKey": false
|
||||
},
|
||||
"last_used_at": {
|
||||
"logicalType": "datetime",
|
||||
"notNull": false,
|
||||
"defaultValue": null,
|
||||
"primaryKey": false
|
||||
},
|
||||
"created_at": {
|
||||
"logicalType": "datetime",
|
||||
"notNull": false,
|
||||
"defaultValue": "datetime('now')",
|
||||
"primaryKey": false
|
||||
},
|
||||
"updated_at": {
|
||||
"logicalType": "datetime",
|
||||
"notNull": false,
|
||||
"defaultValue": "datetime('now')",
|
||||
"primaryKey": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"events": {
|
||||
"columns": {
|
||||
"id": {
|
||||
@@ -320,6 +420,88 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"proxy_files": {
|
||||
"columns": {
|
||||
"id": {
|
||||
"logicalType": "integer",
|
||||
"notNull": true,
|
||||
"defaultValue": null,
|
||||
"primaryKey": true
|
||||
},
|
||||
"public_id": {
|
||||
"logicalType": "text",
|
||||
"notNull": true,
|
||||
"defaultValue": null,
|
||||
"primaryKey": false
|
||||
},
|
||||
"owner_type": {
|
||||
"logicalType": "text",
|
||||
"notNull": true,
|
||||
"defaultValue": null,
|
||||
"primaryKey": false
|
||||
},
|
||||
"owner_id": {
|
||||
"logicalType": "text",
|
||||
"notNull": true,
|
||||
"defaultValue": null,
|
||||
"primaryKey": false
|
||||
},
|
||||
"filename": {
|
||||
"logicalType": "text",
|
||||
"notNull": true,
|
||||
"defaultValue": null,
|
||||
"primaryKey": false
|
||||
},
|
||||
"mime_type": {
|
||||
"logicalType": "text",
|
||||
"notNull": true,
|
||||
"defaultValue": null,
|
||||
"primaryKey": false
|
||||
},
|
||||
"purpose": {
|
||||
"logicalType": "text",
|
||||
"notNull": false,
|
||||
"defaultValue": null,
|
||||
"primaryKey": false
|
||||
},
|
||||
"byte_size": {
|
||||
"logicalType": "integer",
|
||||
"notNull": true,
|
||||
"defaultValue": null,
|
||||
"primaryKey": false
|
||||
},
|
||||
"sha256": {
|
||||
"logicalType": "text",
|
||||
"notNull": true,
|
||||
"defaultValue": null,
|
||||
"primaryKey": false
|
||||
},
|
||||
"content_base64": {
|
||||
"logicalType": "text",
|
||||
"notNull": true,
|
||||
"defaultValue": null,
|
||||
"primaryKey": false
|
||||
},
|
||||
"created_at": {
|
||||
"logicalType": "datetime",
|
||||
"notNull": false,
|
||||
"defaultValue": "datetime('now')",
|
||||
"primaryKey": false
|
||||
},
|
||||
"updated_at": {
|
||||
"logicalType": "datetime",
|
||||
"notNull": false,
|
||||
"defaultValue": "datetime('now')",
|
||||
"primaryKey": false
|
||||
},
|
||||
"deleted_at": {
|
||||
"logicalType": "datetime",
|
||||
"notNull": false,
|
||||
"defaultValue": null,
|
||||
"primaryKey": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"proxy_logs": {
|
||||
"columns": {
|
||||
"id": {
|
||||
@@ -426,6 +608,100 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"proxy_video_tasks": {
|
||||
"columns": {
|
||||
"id": {
|
||||
"logicalType": "integer",
|
||||
"notNull": true,
|
||||
"defaultValue": null,
|
||||
"primaryKey": true
|
||||
},
|
||||
"public_id": {
|
||||
"logicalType": "text",
|
||||
"notNull": true,
|
||||
"defaultValue": null,
|
||||
"primaryKey": false
|
||||
},
|
||||
"upstream_video_id": {
|
||||
"logicalType": "text",
|
||||
"notNull": true,
|
||||
"defaultValue": null,
|
||||
"primaryKey": false
|
||||
},
|
||||
"site_url": {
|
||||
"logicalType": "text",
|
||||
"notNull": true,
|
||||
"defaultValue": null,
|
||||
"primaryKey": false
|
||||
},
|
||||
"token_value": {
|
||||
"logicalType": "text",
|
||||
"notNull": true,
|
||||
"defaultValue": null,
|
||||
"primaryKey": false
|
||||
},
|
||||
"requested_model": {
|
||||
"logicalType": "text",
|
||||
"notNull": false,
|
||||
"defaultValue": null,
|
||||
"primaryKey": false
|
||||
},
|
||||
"actual_model": {
|
||||
"logicalType": "text",
|
||||
"notNull": false,
|
||||
"defaultValue": null,
|
||||
"primaryKey": false
|
||||
},
|
||||
"channel_id": {
|
||||
"logicalType": "integer",
|
||||
"notNull": false,
|
||||
"defaultValue": null,
|
||||
"primaryKey": false
|
||||
},
|
||||
"account_id": {
|
||||
"logicalType": "integer",
|
||||
"notNull": false,
|
||||
"defaultValue": null,
|
||||
"primaryKey": false
|
||||
},
|
||||
"status_snapshot": {
|
||||
"logicalType": "json",
|
||||
"notNull": false,
|
||||
"defaultValue": null,
|
||||
"primaryKey": false
|
||||
},
|
||||
"upstream_response_meta": {
|
||||
"logicalType": "json",
|
||||
"notNull": false,
|
||||
"defaultValue": null,
|
||||
"primaryKey": false
|
||||
},
|
||||
"last_upstream_status": {
|
||||
"logicalType": "integer",
|
||||
"notNull": false,
|
||||
"defaultValue": null,
|
||||
"primaryKey": false
|
||||
},
|
||||
"last_polled_at": {
|
||||
"logicalType": "datetime",
|
||||
"notNull": false,
|
||||
"defaultValue": null,
|
||||
"primaryKey": false
|
||||
},
|
||||
"created_at": {
|
||||
"logicalType": "datetime",
|
||||
"notNull": false,
|
||||
"defaultValue": "datetime('now')",
|
||||
"primaryKey": false
|
||||
},
|
||||
"updated_at": {
|
||||
"logicalType": "datetime",
|
||||
"notNull": false,
|
||||
"defaultValue": "datetime('now')",
|
||||
"primaryKey": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"route_channels": {
|
||||
"columns": {
|
||||
"id": {
|
||||
@@ -446,6 +722,18 @@
|
||||
"defaultValue": null,
|
||||
"primaryKey": false
|
||||
},
|
||||
"token_id": {
|
||||
"logicalType": "integer",
|
||||
"notNull": false,
|
||||
"defaultValue": null,
|
||||
"primaryKey": false
|
||||
},
|
||||
"source_model": {
|
||||
"logicalType": "text",
|
||||
"notNull": false,
|
||||
"defaultValue": null,
|
||||
"primaryKey": false
|
||||
},
|
||||
"priority": {
|
||||
"logicalType": "integer",
|
||||
"notNull": false,
|
||||
@@ -500,20 +788,32 @@
|
||||
"defaultValue": null,
|
||||
"primaryKey": false
|
||||
},
|
||||
"last_selected_at": {
|
||||
"logicalType": "datetime",
|
||||
"notNull": false,
|
||||
"defaultValue": null,
|
||||
"primaryKey": false
|
||||
},
|
||||
"last_fail_at": {
|
||||
"logicalType": "datetime",
|
||||
"notNull": false,
|
||||
"defaultValue": null,
|
||||
"primaryKey": false
|
||||
},
|
||||
"cooldown_until": {
|
||||
"logicalType": "datetime",
|
||||
"notNull": false,
|
||||
"defaultValue": null,
|
||||
"consecutive_fail_count": {
|
||||
"logicalType": "integer",
|
||||
"notNull": true,
|
||||
"defaultValue": "0",
|
||||
"primaryKey": false
|
||||
},
|
||||
"token_id": {
|
||||
"cooldown_level": {
|
||||
"logicalType": "integer",
|
||||
"notNull": true,
|
||||
"defaultValue": "0",
|
||||
"primaryKey": false
|
||||
},
|
||||
"cooldown_until": {
|
||||
"logicalType": "datetime",
|
||||
"notNull": false,
|
||||
"defaultValue": null,
|
||||
"primaryKey": false
|
||||
@@ -625,6 +925,36 @@
|
||||
"notNull": false,
|
||||
"defaultValue": "0",
|
||||
"primaryKey": false
|
||||
},
|
||||
"proxy_url": {
|
||||
"logicalType": "text",
|
||||
"notNull": false,
|
||||
"defaultValue": null,
|
||||
"primaryKey": false
|
||||
},
|
||||
"use_system_proxy": {
|
||||
"logicalType": "boolean",
|
||||
"notNull": false,
|
||||
"defaultValue": "false",
|
||||
"primaryKey": false
|
||||
},
|
||||
"custom_headers": {
|
||||
"logicalType": "json",
|
||||
"notNull": false,
|
||||
"defaultValue": null,
|
||||
"primaryKey": false
|
||||
},
|
||||
"external_checkin_url": {
|
||||
"logicalType": "text",
|
||||
"notNull": false,
|
||||
"defaultValue": null,
|
||||
"primaryKey": false
|
||||
},
|
||||
"global_weight": {
|
||||
"logicalType": "real",
|
||||
"notNull": false,
|
||||
"defaultValue": "1",
|
||||
"primaryKey": false
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -705,6 +1035,36 @@
|
||||
"notNull": false,
|
||||
"defaultValue": "datetime('now')",
|
||||
"primaryKey": false
|
||||
},
|
||||
"display_name": {
|
||||
"logicalType": "text",
|
||||
"notNull": false,
|
||||
"defaultValue": null,
|
||||
"primaryKey": false
|
||||
},
|
||||
"display_icon": {
|
||||
"logicalType": "text",
|
||||
"notNull": false,
|
||||
"defaultValue": null,
|
||||
"primaryKey": false
|
||||
},
|
||||
"decision_snapshot": {
|
||||
"logicalType": "json",
|
||||
"notNull": false,
|
||||
"defaultValue": null,
|
||||
"primaryKey": false
|
||||
},
|
||||
"decision_refreshed_at": {
|
||||
"logicalType": "datetime",
|
||||
"notNull": false,
|
||||
"defaultValue": null,
|
||||
"primaryKey": false
|
||||
},
|
||||
"routing_strategy": {
|
||||
"logicalType": "text",
|
||||
"notNull": false,
|
||||
"defaultValue": "'weighted'",
|
||||
"primaryKey": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -785,6 +1145,38 @@
|
||||
],
|
||||
"unique": false
|
||||
},
|
||||
{
|
||||
"name": "downstream_api_keys_enabled_idx",
|
||||
"table": "downstream_api_keys",
|
||||
"columns": [
|
||||
"enabled"
|
||||
],
|
||||
"unique": false
|
||||
},
|
||||
{
|
||||
"name": "downstream_api_keys_expires_at_idx",
|
||||
"table": "downstream_api_keys",
|
||||
"columns": [
|
||||
"expires_at"
|
||||
],
|
||||
"unique": false
|
||||
},
|
||||
{
|
||||
"name": "downstream_api_keys_key_unique",
|
||||
"table": "downstream_api_keys",
|
||||
"columns": [
|
||||
"key"
|
||||
],
|
||||
"unique": true
|
||||
},
|
||||
{
|
||||
"name": "downstream_api_keys_name_idx",
|
||||
"table": "downstream_api_keys",
|
||||
"columns": [
|
||||
"name"
|
||||
],
|
||||
"unique": false
|
||||
},
|
||||
{
|
||||
"name": "events_created_at_idx",
|
||||
"table": "events",
|
||||
@@ -820,6 +1212,15 @@
|
||||
],
|
||||
"unique": false
|
||||
},
|
||||
{
|
||||
"name": "model_availability_account_model_unique",
|
||||
"table": "model_availability",
|
||||
"columns": [
|
||||
"account_id",
|
||||
"model_name"
|
||||
],
|
||||
"unique": true
|
||||
},
|
||||
{
|
||||
"name": "model_availability_model_name_idx",
|
||||
"table": "model_availability",
|
||||
@@ -828,6 +1229,24 @@
|
||||
],
|
||||
"unique": false
|
||||
},
|
||||
{
|
||||
"name": "proxy_files_owner_lookup_idx",
|
||||
"table": "proxy_files",
|
||||
"columns": [
|
||||
"owner_type",
|
||||
"owner_id",
|
||||
"deleted_at"
|
||||
],
|
||||
"unique": false
|
||||
},
|
||||
{
|
||||
"name": "proxy_files_public_id_unique",
|
||||
"table": "proxy_files",
|
||||
"columns": [
|
||||
"public_id"
|
||||
],
|
||||
"unique": true
|
||||
},
|
||||
{
|
||||
"name": "proxy_logs_account_created_at_idx",
|
||||
"table": "proxy_logs",
|
||||
@@ -863,6 +1282,30 @@
|
||||
],
|
||||
"unique": false
|
||||
},
|
||||
{
|
||||
"name": "proxy_video_tasks_created_at_idx",
|
||||
"table": "proxy_video_tasks",
|
||||
"columns": [
|
||||
"created_at"
|
||||
],
|
||||
"unique": false
|
||||
},
|
||||
{
|
||||
"name": "proxy_video_tasks_public_id_unique",
|
||||
"table": "proxy_video_tasks",
|
||||
"columns": [
|
||||
"public_id"
|
||||
],
|
||||
"unique": true
|
||||
},
|
||||
{
|
||||
"name": "proxy_video_tasks_upstream_video_id_idx",
|
||||
"table": "proxy_video_tasks",
|
||||
"columns": [
|
||||
"upstream_video_id"
|
||||
],
|
||||
"unique": false
|
||||
},
|
||||
{
|
||||
"name": "route_channels_account_id_idx",
|
||||
"table": "route_channels",
|
||||
@@ -982,6 +1425,35 @@
|
||||
}
|
||||
],
|
||||
"uniques": [
|
||||
{
|
||||
"name": "downstream_api_keys_key_unique",
|
||||
"table": "downstream_api_keys",
|
||||
"columns": [
|
||||
"key"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "model_availability_account_model_unique",
|
||||
"table": "model_availability",
|
||||
"columns": [
|
||||
"account_id",
|
||||
"model_name"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "proxy_files_public_id_unique",
|
||||
"table": "proxy_files",
|
||||
"columns": [
|
||||
"public_id"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "proxy_video_tasks_public_id_unique",
|
||||
"table": "proxy_video_tasks",
|
||||
"columns": [
|
||||
"public_id"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "site_disabled_models_site_model_unique",
|
||||
"table": "site_disabled_models",
|
||||
@@ -1066,6 +1538,17 @@
|
||||
],
|
||||
"onDelete": "CASCADE"
|
||||
},
|
||||
{
|
||||
"table": "route_channels",
|
||||
"columns": [
|
||||
"token_id"
|
||||
],
|
||||
"referencedTable": "account_tokens",
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "SET NULL"
|
||||
},
|
||||
{
|
||||
"table": "site_disabled_models",
|
||||
"columns": [
|
||||
|
||||
@@ -208,12 +208,82 @@ describe('sqlite migrate bootstrap', () => {
|
||||
sqlite.close();
|
||||
});
|
||||
|
||||
it('recovers duplicate-column errors inside multi-statement migrations by replaying the full migration', async () => {
|
||||
const dataDir = mkdtempSync(join(tmpdir(), 'metapi-migrate-recover-multi-'));
|
||||
process.env.DATA_DIR = dataDir;
|
||||
vi.resetModules();
|
||||
|
||||
const migrateModule = await import('./migrate.js');
|
||||
const { __migrateTestUtils } = migrateModule;
|
||||
|
||||
const sqlite = new Database(':memory:');
|
||||
sqlite.exec(`
|
||||
CREATE TABLE account_tokens (
|
||||
id integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
token_group text
|
||||
);
|
||||
`);
|
||||
|
||||
const tempMigrationsDir = mkdtempSync(join(tmpdir(), 'metapi-migration-files-multi-'));
|
||||
mkdirSync(join(tempMigrationsDir, 'meta'), { recursive: true });
|
||||
|
||||
writeFileSync(
|
||||
join(tempMigrationsDir, 'meta', '_journal.json'),
|
||||
JSON.stringify({
|
||||
entries: [
|
||||
{
|
||||
tag: '0008_sqlite_schema_backfill',
|
||||
when: 1772600000000,
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
writeFileSync(
|
||||
join(tempMigrationsDir, '0008_sqlite_schema_backfill.sql'),
|
||||
[
|
||||
'CREATE TABLE IF NOT EXISTS `downstream_api_keys` (`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL);',
|
||||
'ALTER TABLE `account_tokens` ADD `token_group` text;',
|
||||
].join('\n--> statement-breakpoint\n'),
|
||||
);
|
||||
|
||||
const duplicateColumnError = new Error(
|
||||
"DrizzleError: Failed to run the query 'ALTER TABLE `account_tokens` ADD `token_group` text;\n' duplicate column name: token_group",
|
||||
);
|
||||
|
||||
const recovered = __migrateTestUtils.tryRecoverDuplicateColumnMigrationError(
|
||||
sqlite,
|
||||
tempMigrationsDir,
|
||||
duplicateColumnError,
|
||||
);
|
||||
|
||||
expect(recovered).toBe(true);
|
||||
|
||||
const createdTable = sqlite
|
||||
.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'downstream_api_keys'")
|
||||
.get() as { name?: string } | undefined;
|
||||
const applied = sqlite
|
||||
.prepare('SELECT hash, created_at FROM __drizzle_migrations')
|
||||
.all() as Array<{ hash: string; created_at: number }>;
|
||||
|
||||
expect(createdTable?.name).toBe('downstream_api_keys');
|
||||
expect(applied).toHaveLength(1);
|
||||
expect(Number(applied[0].created_at)).toBe(1772600000000);
|
||||
|
||||
sqlite.close();
|
||||
});
|
||||
|
||||
it('replays missing migrations before marking a duplicate-column migration as applied', async () => {
|
||||
const dataDir = mkdtempSync(join(tmpdir(), 'metapi-migrate-partial-journal-'));
|
||||
const dbPath = join(dataDir, 'hub.db');
|
||||
const sqlite = new Database(dbPath);
|
||||
const journalEntries = readMigrationJournalEntries();
|
||||
const appliedEntries = journalEntries.filter((entry) => entry.tag !== '0006_site_disabled_models' && entry.tag !== '0007_account_token_group');
|
||||
const missingTags = new Set([
|
||||
'0006_site_disabled_models',
|
||||
'0007_account_token_group',
|
||||
'0008_sqlite_schema_backfill',
|
||||
]);
|
||||
const appliedEntries = journalEntries.filter((entry) => !missingTags.has(entry.tag));
|
||||
|
||||
for (const entry of appliedEntries) {
|
||||
const sqlText = readFileSync(join(migrationsDir, `${entry.tag}.sql`), 'utf8');
|
||||
@@ -239,8 +309,12 @@ describe('sqlite migrate bootstrap', () => {
|
||||
const disabledModelsTable = verified
|
||||
.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'site_disabled_models'")
|
||||
.get() as { name?: string } | undefined;
|
||||
const downstreamApiKeysTable = verified
|
||||
.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'downstream_api_keys'")
|
||||
.get() as { name?: string } | undefined;
|
||||
|
||||
expect(disabledModelsTable?.name).toBe('site_disabled_models');
|
||||
expect(downstreamApiKeysTable?.name).toBe('downstream_api_keys');
|
||||
expect(appliedRows.map((row) => Number(row.created_at))).toEqual(
|
||||
journalEntries.map((entry) => entry.when),
|
||||
);
|
||||
|
||||
@@ -174,6 +174,28 @@ function findMatchingSingleStatementMigration(
|
||||
return null;
|
||||
}
|
||||
|
||||
function findMatchingMigrationByStatement(
|
||||
migrationsFolder: string,
|
||||
failedSqlText: string,
|
||||
): RecoveryMigrationRecord | null {
|
||||
const normalizedFailedSql = normalizeSqlForMatch(failedSqlText);
|
||||
const migrations = readRecoveryMigrations(migrationsFolder);
|
||||
|
||||
for (const migration of migrations) {
|
||||
if (!migration.statements.some((statement) => normalizeSqlForMatch(statement) === normalizedFailedSql)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return {
|
||||
tag: migration.tag,
|
||||
createdAt: migration.createdAt,
|
||||
hash: migration.hash,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function readRecoveryMigrations(migrationsFolder: string): RecoveryMigration[] {
|
||||
const journalPath = resolve(migrationsFolder, 'meta', '_journal.json');
|
||||
const journal = JSON.parse(readFileSync(journalPath, 'utf8')) as MigrationJournalFile;
|
||||
@@ -320,7 +342,7 @@ function tryRecoverDuplicateColumnMigrationError(
|
||||
return false;
|
||||
}
|
||||
|
||||
const matchedMigration = findMatchingSingleStatementMigration(migrationsFolder, failedSqlText);
|
||||
const matchedMigration = findMatchingMigrationByStatement(migrationsFolder, failedSqlText);
|
||||
if (!matchedMigration) {
|
||||
return false;
|
||||
}
|
||||
@@ -337,6 +359,7 @@ export const __migrateTestUtils = {
|
||||
normalizeSqlForMatch,
|
||||
extractFailedSqlFromError,
|
||||
findMatchingSingleStatementMigration,
|
||||
findMatchingMigrationByStatement,
|
||||
readRecoveryMigrations,
|
||||
markMigrationRecordIfMissing,
|
||||
recoverMigrationSequence,
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { generateDialectArtifacts, type GeneratedDialectArtifacts } from './schemaArtifactGenerator.js';
|
||||
import type { SchemaContract, SchemaContractColumn } from './schemaContract.js';
|
||||
|
||||
const dbDir = dirname(fileURLToPath(import.meta.url));
|
||||
const schemaContractPath = resolve(dbDir, 'generated/schemaContract.json');
|
||||
|
||||
function readSchemaContract(): SchemaContract {
|
||||
return JSON.parse(readFileSync(schemaContractPath, 'utf8')) as SchemaContract;
|
||||
}
|
||||
|
||||
function makeColumn(overrides: Partial<SchemaContractColumn> = {}): SchemaContractColumn {
|
||||
return {
|
||||
logicalType: 'text',
|
||||
notNull: false,
|
||||
defaultValue: null,
|
||||
primaryKey: false,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('schema artifact generator', () => {
|
||||
it('generates bootstrap sql for mysql and postgres from the contract', () => {
|
||||
const artifacts = generateDialectArtifacts(readSchemaContract());
|
||||
|
||||
expect(artifacts.mysqlBootstrap).toContain('CREATE TABLE IF NOT EXISTS `sites`');
|
||||
expect(artifacts.mysqlBootstrap).toContain('CREATE TABLE IF NOT EXISTS `site_disabled_models`');
|
||||
expect(artifacts.postgresBootstrap).toContain('CREATE TABLE IF NOT EXISTS "account_tokens"');
|
||||
expect(artifacts.postgresBootstrap).toContain('"token_group"');
|
||||
});
|
||||
|
||||
it('generates additive upgrade sql for newly added tables, columns, indexes, and uniques', () => {
|
||||
const previousContract: SchemaContract = {
|
||||
tables: {
|
||||
sites: {
|
||||
columns: {
|
||||
id: makeColumn({ logicalType: 'integer', notNull: true, primaryKey: true }),
|
||||
},
|
||||
},
|
||||
},
|
||||
indexes: [],
|
||||
uniques: [],
|
||||
foreignKeys: [],
|
||||
};
|
||||
const currentContract: SchemaContract = {
|
||||
tables: {
|
||||
sites: {
|
||||
columns: {
|
||||
id: makeColumn({ logicalType: 'integer', notNull: true, primaryKey: true }),
|
||||
status: makeColumn({ notNull: true, defaultValue: "'active'" }),
|
||||
},
|
||||
},
|
||||
accounts: {
|
||||
columns: {
|
||||
id: makeColumn({ logicalType: 'integer', notNull: true, primaryKey: true }),
|
||||
site_id: makeColumn({ logicalType: 'integer', notNull: true }),
|
||||
email: makeColumn({ notNull: true }),
|
||||
},
|
||||
},
|
||||
},
|
||||
indexes: [
|
||||
{ name: 'sites_status_idx', table: 'sites', columns: ['status'], unique: false },
|
||||
],
|
||||
uniques: [
|
||||
{ name: 'accounts_site_email_unique', table: 'accounts', columns: ['site_id', 'email'] },
|
||||
],
|
||||
foreignKeys: [
|
||||
{
|
||||
table: 'accounts',
|
||||
columns: ['site_id'],
|
||||
referencedTable: 'sites',
|
||||
referencedColumns: ['id'],
|
||||
onDelete: 'cascade',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const artifacts: GeneratedDialectArtifacts = generateDialectArtifacts(currentContract, previousContract);
|
||||
|
||||
expect(artifacts.mysqlUpgrade).toContain('CREATE TABLE IF NOT EXISTS `accounts`');
|
||||
expect(artifacts.mysqlUpgrade).toContain('ALTER TABLE `sites` ADD COLUMN `status`');
|
||||
expect(artifacts.mysqlUpgrade).toContain('CREATE INDEX `sites_status_idx` ON `sites`');
|
||||
expect(artifacts.mysqlUpgrade).toContain('CREATE UNIQUE INDEX `accounts_site_email_unique` ON `accounts`');
|
||||
expect(artifacts.postgresUpgrade).toContain('CREATE TABLE IF NOT EXISTS "accounts"');
|
||||
expect(artifacts.postgresUpgrade).toContain('ALTER TABLE "sites" ADD COLUMN "status"');
|
||||
expect(artifacts.postgresUpgrade).toContain('CREATE INDEX "sites_status_idx" ON "sites"');
|
||||
expect(artifacts.postgresUpgrade).toContain('CREATE UNIQUE INDEX "accounts_site_email_unique" ON "accounts"');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,248 @@
|
||||
import { mkdirSync, writeFileSync } from 'node:fs';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import type {
|
||||
LogicalColumnType,
|
||||
SchemaContract,
|
||||
SchemaContractColumn,
|
||||
SchemaContractForeignKey,
|
||||
SchemaContractIndex,
|
||||
SchemaContractUnique,
|
||||
} from './schemaContract.js';
|
||||
|
||||
export interface GeneratedDialectArtifacts {
|
||||
mysqlBootstrap: string;
|
||||
postgresBootstrap: string;
|
||||
mysqlUpgrade: string;
|
||||
postgresUpgrade: string;
|
||||
}
|
||||
|
||||
type Dialect = 'mysql' | 'postgres';
|
||||
|
||||
function resolveDbDir(): string {
|
||||
return dirname(fileURLToPath(import.meta.url));
|
||||
}
|
||||
|
||||
export function resolveGeneratedArtifactPath(filename: string): string {
|
||||
return resolve(resolveDbDir(), 'generated', filename);
|
||||
}
|
||||
|
||||
function quoteIdentifier(dialect: Dialect, identifier: string): string {
|
||||
return dialect === 'mysql' ? `\`${identifier}\`` : `"${identifier}"`;
|
||||
}
|
||||
|
||||
function escapeMysqlTextPrefix(columnType: LogicalColumnType): string {
|
||||
return columnType === 'text' || columnType === 'datetime' || columnType === 'json' ? '(191)' : '';
|
||||
}
|
||||
|
||||
function mapColumnType(dialect: Dialect, columnName: string, column: SchemaContractColumn): string {
|
||||
if (dialect === 'mysql') {
|
||||
switch (column.logicalType) {
|
||||
case 'boolean':
|
||||
return 'BOOLEAN';
|
||||
case 'integer':
|
||||
return column.primaryKey && columnName === 'id' ? 'INT AUTO_INCREMENT' : 'INT';
|
||||
case 'real':
|
||||
return 'DOUBLE';
|
||||
case 'datetime':
|
||||
case 'json':
|
||||
case 'text':
|
||||
default:
|
||||
return 'TEXT';
|
||||
}
|
||||
}
|
||||
|
||||
switch (column.logicalType) {
|
||||
case 'boolean':
|
||||
return 'BOOLEAN';
|
||||
case 'integer':
|
||||
return column.primaryKey && columnName === 'id' ? 'INTEGER GENERATED BY DEFAULT AS IDENTITY' : 'INTEGER';
|
||||
case 'real':
|
||||
return 'DOUBLE PRECISION';
|
||||
case 'datetime':
|
||||
case 'json':
|
||||
case 'text':
|
||||
default:
|
||||
return 'TEXT';
|
||||
}
|
||||
}
|
||||
|
||||
function formatDefaultValue(defaultValue: string | null): string {
|
||||
if (defaultValue == null) {
|
||||
return '';
|
||||
}
|
||||
return ` DEFAULT ${defaultValue}`;
|
||||
}
|
||||
|
||||
function buildColumnDefinition(
|
||||
dialect: Dialect,
|
||||
columnName: string,
|
||||
column: SchemaContractColumn,
|
||||
): string {
|
||||
const sqlType = mapColumnType(dialect, columnName, column);
|
||||
const notNull = column.notNull ? ' NOT NULL' : '';
|
||||
const defaultValue = formatDefaultValue(column.defaultValue);
|
||||
const primaryKey = column.primaryKey ? ' PRIMARY KEY' : '';
|
||||
return `${quoteIdentifier(dialect, columnName)} ${sqlType}${notNull}${defaultValue}${primaryKey}`;
|
||||
}
|
||||
|
||||
function buildForeignKeyClause(dialect: Dialect, foreignKey: SchemaContractForeignKey): string {
|
||||
const sourceColumns = foreignKey.columns.map((column) => quoteIdentifier(dialect, column)).join(', ');
|
||||
const targetColumns = foreignKey.referencedColumns.map((column) => quoteIdentifier(dialect, column)).join(', ');
|
||||
const onDelete = foreignKey.onDelete ? ` ON DELETE ${foreignKey.onDelete.toUpperCase()}` : '';
|
||||
return `FOREIGN KEY (${sourceColumns}) REFERENCES ${quoteIdentifier(dialect, foreignKey.referencedTable)}(${targetColumns})${onDelete}`;
|
||||
}
|
||||
|
||||
function buildCreateTableStatement(
|
||||
dialect: Dialect,
|
||||
tableName: string,
|
||||
contract: SchemaContract,
|
||||
): string {
|
||||
const table = contract.tables[tableName];
|
||||
const columnEntries = Object.entries(table.columns);
|
||||
const foreignKeys = contract.foreignKeys.filter((foreignKey) => foreignKey.table === tableName);
|
||||
const parts = [
|
||||
...columnEntries.map(([columnName, column]) => buildColumnDefinition(dialect, columnName, column)),
|
||||
...foreignKeys.map((foreignKey) => buildForeignKeyClause(dialect, foreignKey)),
|
||||
];
|
||||
return `CREATE TABLE IF NOT EXISTS ${quoteIdentifier(dialect, tableName)} (${parts.join(', ')})`;
|
||||
}
|
||||
|
||||
function buildUniqueIndexStatement(dialect: Dialect, uniqueIndex: SchemaContractUnique, contract: SchemaContract): string {
|
||||
const columns = uniqueIndex.columns
|
||||
.map((columnName) => {
|
||||
const column = contract.tables[uniqueIndex.table]?.columns[columnName];
|
||||
const suffix = dialect === 'mysql' && column ? escapeMysqlTextPrefix(column.logicalType) : '';
|
||||
return `${quoteIdentifier(dialect, columnName)}${suffix}`;
|
||||
})
|
||||
.join(', ');
|
||||
return `CREATE UNIQUE INDEX ${quoteIdentifier(dialect, uniqueIndex.name)} ON ${quoteIdentifier(dialect, uniqueIndex.table)} (${columns})`;
|
||||
}
|
||||
|
||||
function buildIndexStatement(dialect: Dialect, index: SchemaContractIndex, contract: SchemaContract): string {
|
||||
const columns = index.columns
|
||||
.map((columnName) => {
|
||||
const column = contract.tables[index.table]?.columns[columnName];
|
||||
const suffix = dialect === 'mysql' && column ? escapeMysqlTextPrefix(column.logicalType) : '';
|
||||
return `${quoteIdentifier(dialect, columnName)}${suffix}`;
|
||||
})
|
||||
.join(', ');
|
||||
return `CREATE INDEX ${quoteIdentifier(dialect, index.name)} ON ${quoteIdentifier(dialect, index.table)} (${columns})`;
|
||||
}
|
||||
|
||||
function buildBootstrapSql(dialect: Dialect, contract: SchemaContract): string {
|
||||
const tableStatements = Object.keys(contract.tables)
|
||||
.sort((left, right) => left.localeCompare(right, 'en'))
|
||||
.map((tableName) => buildCreateTableStatement(dialect, tableName, contract));
|
||||
|
||||
const uniqueStatements = contract.uniques
|
||||
.slice()
|
||||
.sort((left, right) => left.name.localeCompare(right.name, 'en'))
|
||||
.map((uniqueIndex) => buildUniqueIndexStatement(dialect, uniqueIndex, contract));
|
||||
|
||||
const knownUniqueNames = new Set(contract.uniques.map((uniqueIndex) => uniqueIndex.name));
|
||||
const indexStatements = contract.indexes
|
||||
.filter((index) => !knownUniqueNames.has(index.name))
|
||||
.slice()
|
||||
.sort((left, right) => left.name.localeCompare(right.name, 'en'))
|
||||
.map((index) => buildIndexStatement(dialect, index, contract));
|
||||
|
||||
return `${[...tableStatements, ...uniqueStatements, ...indexStatements].join(';\n')};\n`;
|
||||
}
|
||||
|
||||
function buildAddColumnStatement(
|
||||
dialect: Dialect,
|
||||
tableName: string,
|
||||
columnName: string,
|
||||
column: SchemaContractColumn,
|
||||
): string {
|
||||
return `ALTER TABLE ${quoteIdentifier(dialect, tableName)} ADD COLUMN ${buildColumnDefinition(dialect, columnName, column)}`;
|
||||
}
|
||||
|
||||
function buildUpgradeSql(
|
||||
dialect: Dialect,
|
||||
currentContract: SchemaContract,
|
||||
previousContract?: SchemaContract | null,
|
||||
): string {
|
||||
if (!previousContract) {
|
||||
return `-- no previous schema contract available for ${dialect} additive upgrade generation\n`;
|
||||
}
|
||||
|
||||
const previousTableNames = new Set(Object.keys(previousContract.tables));
|
||||
const currentTableNames = Object.keys(currentContract.tables).sort((left, right) => left.localeCompare(right, 'en'));
|
||||
const addedTableStatements = currentTableNames
|
||||
.filter((tableName) => !previousTableNames.has(tableName))
|
||||
.map((tableName) => buildCreateTableStatement(dialect, tableName, currentContract));
|
||||
|
||||
const addColumnStatements: string[] = [];
|
||||
for (const tableName of currentTableNames) {
|
||||
if (!previousTableNames.has(tableName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const previousColumns = previousContract.tables[tableName]?.columns ?? {};
|
||||
const currentColumns = currentContract.tables[tableName]?.columns ?? {};
|
||||
for (const [columnName, column] of Object.entries(currentColumns)) {
|
||||
if (previousColumns[columnName]) {
|
||||
continue;
|
||||
}
|
||||
addColumnStatements.push(buildAddColumnStatement(dialect, tableName, columnName, column));
|
||||
}
|
||||
}
|
||||
|
||||
const previousUniqueNames = new Set(previousContract.uniques.map((unique) => unique.name));
|
||||
const uniqueStatements = currentContract.uniques
|
||||
.filter((unique) => !previousUniqueNames.has(unique.name))
|
||||
.slice()
|
||||
.sort((left, right) => left.name.localeCompare(right.name, 'en'))
|
||||
.map((unique) => buildUniqueIndexStatement(dialect, unique, currentContract));
|
||||
|
||||
const currentUniqueNames = new Set(currentContract.uniques.map((unique) => unique.name));
|
||||
const previousIndexNames = new Set(previousContract.indexes.map((index) => index.name));
|
||||
const indexStatements = currentContract.indexes
|
||||
.filter((index) => !currentUniqueNames.has(index.name))
|
||||
.filter((index) => !previousIndexNames.has(index.name))
|
||||
.slice()
|
||||
.sort((left, right) => left.name.localeCompare(right.name, 'en'))
|
||||
.map((index) => buildIndexStatement(dialect, index, currentContract));
|
||||
|
||||
const statements = [...addedTableStatements, ...addColumnStatements, ...uniqueStatements, ...indexStatements];
|
||||
if (statements.length === 0) {
|
||||
return `-- no schema changes detected for ${dialect}\n`;
|
||||
}
|
||||
|
||||
return `${statements.join(';\n')};\n`;
|
||||
}
|
||||
|
||||
export function generateDialectArtifacts(
|
||||
contract: SchemaContract,
|
||||
previousContract?: SchemaContract | null,
|
||||
): GeneratedDialectArtifacts {
|
||||
return {
|
||||
mysqlBootstrap: buildBootstrapSql('mysql', contract),
|
||||
postgresBootstrap: buildBootstrapSql('postgres', contract),
|
||||
mysqlUpgrade: buildUpgradeSql('mysql', contract, previousContract),
|
||||
postgresUpgrade: buildUpgradeSql('postgres', contract, previousContract),
|
||||
};
|
||||
}
|
||||
|
||||
export function writeDialectArtifactFiles(
|
||||
contract: SchemaContract,
|
||||
previousContract?: SchemaContract | null,
|
||||
): GeneratedDialectArtifacts {
|
||||
const artifacts = generateDialectArtifacts(contract, previousContract);
|
||||
const artifactEntries = [
|
||||
['mysql.bootstrap.sql', artifacts.mysqlBootstrap],
|
||||
['postgres.bootstrap.sql', artifacts.postgresBootstrap],
|
||||
['mysql.upgrade.sql', artifacts.mysqlUpgrade],
|
||||
['postgres.upgrade.sql', artifacts.postgresUpgrade],
|
||||
] as const;
|
||||
|
||||
for (const [filename, content] of artifactEntries) {
|
||||
const outputPath = resolveGeneratedArtifactPath(filename);
|
||||
mkdirSync(dirname(outputPath), { recursive: true });
|
||||
writeFileSync(outputPath, content, 'utf8');
|
||||
}
|
||||
|
||||
return artifacts;
|
||||
}
|
||||
@@ -12,6 +12,24 @@ describe('schema contract generation', () => {
|
||||
});
|
||||
expect(contract.tables.account_tokens.columns.token_group).toBeDefined();
|
||||
expect(contract.tables.site_disabled_models).toBeDefined();
|
||||
expect(contract.tables.downstream_api_keys).toBeDefined();
|
||||
expect(contract.tables.proxy_files).toBeDefined();
|
||||
expect(contract.tables.proxy_video_tasks).toBeDefined();
|
||||
expect(contract.tables.route_channels.columns.source_model).toBeDefined();
|
||||
expect(contract.tables.route_channels.columns.last_selected_at).toBeDefined();
|
||||
expect(contract.tables.route_channels.columns.consecutive_fail_count).toMatchObject({
|
||||
logicalType: 'integer',
|
||||
notNull: true,
|
||||
defaultValue: '0',
|
||||
});
|
||||
expect(contract.tables.sites.columns.use_system_proxy).toMatchObject({
|
||||
logicalType: 'boolean',
|
||||
defaultValue: 'false',
|
||||
});
|
||||
expect(contract.tables.token_routes.columns.routing_strategy).toMatchObject({
|
||||
logicalType: 'text',
|
||||
defaultValue: "'weighted'",
|
||||
});
|
||||
expect(contract.indexes).toContainEqual(
|
||||
expect.objectContaining({ name: 'sites_status_idx', table: 'sites', unique: false }),
|
||||
);
|
||||
@@ -22,6 +40,13 @@ describe('schema contract generation', () => {
|
||||
columns: ['site_id', 'model_name'],
|
||||
}),
|
||||
);
|
||||
expect(contract.uniques).toContainEqual(
|
||||
expect.objectContaining({
|
||||
name: 'model_availability_account_model_unique',
|
||||
table: 'model_availability',
|
||||
columns: ['account_id', 'model_name'],
|
||||
}),
|
||||
);
|
||||
expect(contract.foreignKeys).toContainEqual(
|
||||
expect.objectContaining({
|
||||
table: 'site_disabled_models',
|
||||
@@ -30,5 +55,13 @@ describe('schema contract generation', () => {
|
||||
referencedColumns: ['id'],
|
||||
}),
|
||||
);
|
||||
expect(contract.foreignKeys).toContainEqual(
|
||||
expect.objectContaining({
|
||||
table: 'route_channels',
|
||||
columns: ['token_id'],
|
||||
referencedTable: 'account_tokens',
|
||||
referencedColumns: ['id'],
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user