diff --git a/migrations/035_core_business_tables_baseline.sql b/migrations/035_core_business_tables_baseline.sql new file mode 100644 index 0000000..a65a3a8 --- /dev/null +++ b/migrations/035_core_business_tables_baseline.sql @@ -0,0 +1,548 @@ +-- ============================================================================= +-- Migration 035: core business tables baseline +-- 日期: 2026-05-12 台北 +-- ============================================================================= +-- 背景: +-- 歷史業務表長期依賴 SQLAlchemy Base.metadata.create_all() 起表;若新環境只跑 +-- migrations/,會缺少商品、匯入、使用者、趨勢、供應商、PPT cache 等表。 +-- +-- 設計: +-- 1. 僅 CREATE TABLE IF NOT EXISTS / CREATE INDEX IF NOT EXISTS,不 drop、不 replace。 +-- 2. 欄位對齊目前 ORM metadata,做為 migration-only 新環境的 baseline。 +-- 3. 依 FK dependency 排序,確保被參照表先建立。 +-- ============================================================================= + +CREATE TABLE IF NOT EXISTS ai_generation_history ( + id SERIAL NOT NULL, + generation_type VARCHAR(50) NOT NULL, + product_name VARCHAR(200), + input_keywords TEXT, + input_trend_topic VARCHAR(500), + output_content TEXT, + generation_duration FLOAT, + is_favorite BOOLEAN, + is_used BOOLEAN, + created_by VARCHAR(100), + created_at TIMESTAMP WITHOUT TIME ZONE, + PRIMARY KEY (id) +); + +CREATE TABLE IF NOT EXISTS ai_prompt_templates ( + id SERIAL NOT NULL, + name VARCHAR(200) NOT NULL, + description TEXT, + template_type VARCHAR(50) NOT NULL, + prompt_content TEXT NOT NULL, + variables TEXT, + is_active BOOLEAN, + is_system BOOLEAN, + created_by VARCHAR(100), + created_at TIMESTAMP WITHOUT TIME ZONE, + updated_at TIMESTAMP WITHOUT TIME ZONE, + PRIMARY KEY (id) +); + +CREATE TABLE IF NOT EXISTS ai_usage_tracking ( + id SERIAL NOT NULL, + user_id VARCHAR(100), + session_id VARCHAR(100), + service_type VARCHAR(50) NOT NULL, + model_name VARCHAR(100), + prompt_tokens INTEGER, + completion_tokens INTEGER, + total_tokens INTEGER, + cost_usd FLOAT, + request_type VARCHAR(50), + status VARCHAR(20), + error_message TEXT, + duration_ms FLOAT, + created_at TIMESTAMP WITHOUT TIME ZONE, + PRIMARY KEY (id) +); + +CREATE TABLE IF NOT EXISTS categories ( + id SERIAL NOT NULL, + name VARCHAR(50) NOT NULL, + PRIMARY KEY (id), + UNIQUE (name) +); + +CREATE TABLE IF NOT EXISTS import_config ( + id SERIAL NOT NULL, + config_key VARCHAR(100) NOT NULL, + config_value TEXT, + config_type VARCHAR(50), + description VARCHAR(500), + created_at TIMESTAMP WITHOUT TIME ZONE, + updated_at TIMESTAMP WITHOUT TIME ZONE, + PRIMARY KEY (id), + UNIQUE (config_key) +); + +CREATE TABLE IF NOT EXISTS import_jobs ( + id SERIAL NOT NULL, + job_type VARCHAR(50) NOT NULL, + status VARCHAR(20) NOT NULL, + drive_file_id VARCHAR(200), + drive_file_name VARCHAR(500), + drive_file_size INTEGER, + local_file_path VARCHAR(500), + progress_percent FLOAT, + current_step VARCHAR(200), + total_rows INTEGER, + processed_rows INTEGER, + success_rows INTEGER, + error_rows INTEGER, + created_at TIMESTAMP WITHOUT TIME ZONE, + started_at TIMESTAMP WITHOUT TIME ZONE, + completed_at TIMESTAMP WITHOUT TIME ZONE, + error_message TEXT, + import_summary TEXT, + PRIMARY KEY (id) +); + +CREATE TABLE IF NOT EXISTS monthly_summary_analysis ( + id SERIAL NOT NULL, + year INTEGER NOT NULL, + month INTEGER NOT NULL, + department VARCHAR(100), + category_3c VARCHAR(100), + division VARCHAR(100), + section VARCHAR(100), + area_id VARCHAR(50), + area_name VARCHAR(100), + pm_name VARCHAR(100), + brand_name VARCHAR(200), + vendor_id INTEGER, + vendor_name VARCHAR(200), + trade_type VARCHAR(20), + unit_price FLOAT, + sales_amt_curr INTEGER, + sales_amt_prev INTEGER, + sales_amt_yoa INTEGER, + profit_amt_curr INTEGER, + profit_amt_prev INTEGER, + profit_amt_yoa INTEGER, + discount_amt_curr INTEGER, + discount_amt_prev INTEGER, + discount_amt_yoa INTEGER, + coupon_amt_curr INTEGER, + coupon_amt_prev INTEGER, + coupon_amt_yoa INTEGER, + other_mkt_curr INTEGER, + other_mkt_prev INTEGER, + other_mkt_yoa INTEGER, + spot_disc_curr INTEGER, + spot_disc_prev INTEGER, + spot_disc_yoa INTEGER, + point_disc_curr INTEGER, + point_disc_prev INTEGER, + point_disc_yoa INTEGER, + sales_vol_curr INTEGER, + sales_vol_prev INTEGER, + sales_vol_yoa INTEGER, + conv_rate FLOAT, + views_curr INTEGER, + views_prev INTEGER, + views_yoa INTEGER, + created_at TIMESTAMP WITHOUT TIME ZONE, + PRIMARY KEY (id), + CONSTRAINT _monthly_summary_uc UNIQUE ( + year, month, department, category_3c, division, section, area_id, + pm_name, brand_name, vendor_id, trade_type + ) +); + +CREATE INDEX IF NOT EXISTS ix_monthly_summary_analysis_year ON monthly_summary_analysis (year); +CREATE INDEX IF NOT EXISTS ix_monthly_summary_analysis_month ON monthly_summary_analysis (month); +CREATE INDEX IF NOT EXISTS ix_monthly_summary_analysis_division ON monthly_summary_analysis (division); +CREATE INDEX IF NOT EXISTS ix_monthly_summary_analysis_pm_name ON monthly_summary_analysis (pm_name); +CREATE INDEX IF NOT EXISTS ix_monthly_summary_analysis_brand_name ON monthly_summary_analysis (brand_name); +CREATE INDEX IF NOT EXISTS ix_monthly_summary_analysis_vendor_id ON monthly_summary_analysis (vendor_id); + +CREATE TABLE IF NOT EXISTS notification_templates ( + id SERIAL NOT NULL, + code VARCHAR(50) NOT NULL, + name VARCHAR(100) NOT NULL, + category VARCHAR(50) NOT NULL, + channel VARCHAR(20), + title VARCHAR(200), + body TEXT NOT NULL, + emoji_prefix VARCHAR(10), + is_active BOOLEAN, + created_at TIMESTAMP WITHOUT TIME ZONE, + updated_at TIMESTAMP WITHOUT TIME ZONE, + PRIMARY KEY (id), + UNIQUE (code) +); + +CREATE TABLE IF NOT EXISTS permissions ( + id SERIAL NOT NULL, + code VARCHAR(50) NOT NULL, + name VARCHAR(100) NOT NULL, + category VARCHAR(50) NOT NULL, + description VARCHAR(200), + sort_order INTEGER, + PRIMARY KEY (id) +); + +CREATE UNIQUE INDEX IF NOT EXISTS ix_permissions_code ON permissions (code); + +CREATE TABLE IF NOT EXISTS ppt_reports ( + id SERIAL NOT NULL, + report_type VARCHAR(50) NOT NULL, + parameters TEXT, + file_path VARCHAR(500), + file_size INTEGER, + generated_at TIMESTAMP WITHOUT TIME ZONE, + expires_at TIMESTAMP WITHOUT TIME ZONE, + cached_data TEXT, + PRIMARY KEY (id) +); + +CREATE INDEX IF NOT EXISTS ix_ppt_reports_report_type ON ppt_reports (report_type); +CREATE INDEX IF NOT EXISTS ix_ppt_reports_generated_at ON ppt_reports (generated_at); +CREATE INDEX IF NOT EXISTS ix_ppt_reports_expires_at ON ppt_reports (expires_at); + +CREATE TABLE IF NOT EXISTS promo_products ( + id SERIAL NOT NULL, + batch_id VARCHAR(64), + crawled_at TIMESTAMP WITHOUT TIME ZONE, + time_slot VARCHAR(20), + activity_time_text VARCHAR(100), + session_time_text VARCHAR(100), + i_code VARCHAR(50), + name VARCHAR(255), + price INTEGER, + discount_text VARCHAR(20), + url TEXT, + image_url TEXT, + previous_price INTEGER, + remain_qty INTEGER, + status_change VARCHAR(20), + page_type VARCHAR(50), + PRIMARY KEY (id) +); + +CREATE INDEX IF NOT EXISTS ix_promo_products_batch_id ON promo_products (batch_id); +CREATE INDEX IF NOT EXISTS ix_promo_products_i_code ON promo_products (i_code); +CREATE INDEX IF NOT EXISTS ix_promo_products_page_type ON promo_products (page_type); + +CREATE TABLE IF NOT EXISTS realtime_sales_monthly ( + id SERIAL NOT NULL, + "日期" DATE, + "訂單編號" VARCHAR(50), + "商品ID" VARCHAR(100), + "商品編號" VARCHAR(100), + "商品名稱" TEXT, + "數量" INTEGER, + "總業績" NUMERIC(15, 2), + "總成本" NUMERIC(15, 2), + "毛利" NUMERIC(15, 2), + "退貨數量" INTEGER, + "商品單位售價" NUMERIC(15, 2), + "廠商名稱" VARCHAR(255), + "分類名稱" VARCHAR(255), + "商品館" VARCHAR(255), + "品牌名稱" VARCHAR(255), + "時間" VARCHAR(50), + "付款方式" VARCHAR(100), + "折扣活動名稱" VARCHAR(255), + "折價券折扣金額" NUMERIC(15, 2), + "折扣金額" NUMERIC(15, 2), + "滿額再折扣金額" NUMERIC(15, 2), + "分期手續費" NUMERIC(15, 2), + created_at TIMESTAMP WITHOUT TIME ZONE, + PRIMARY KEY (id) +); + +CREATE INDEX IF NOT EXISTS "ix_realtime_sales_monthly_日期" ON realtime_sales_monthly ("日期"); +CREATE INDEX IF NOT EXISTS "ix_realtime_sales_monthly_訂單編號" ON realtime_sales_monthly ("訂單編號"); +CREATE INDEX IF NOT EXISTS "ix_realtime_sales_monthly_商品ID" ON realtime_sales_monthly ("商品ID"); +CREATE INDEX IF NOT EXISTS "ix_realtime_sales_monthly_商品編號" ON realtime_sales_monthly ("商品編號"); +CREATE INDEX IF NOT EXISTS "ix_realtime_sales_monthly_廠商名稱" ON realtime_sales_monthly ("廠商名稱"); +CREATE INDEX IF NOT EXISTS "ix_realtime_sales_monthly_分類名稱" ON realtime_sales_monthly ("分類名稱"); +CREATE INDEX IF NOT EXISTS "ix_realtime_sales_monthly_商品館" ON realtime_sales_monthly ("商品館"); +CREATE INDEX IF NOT EXISTS "ix_realtime_sales_monthly_品牌名稱" ON realtime_sales_monthly ("品牌名稱"); + +CREATE TABLE IF NOT EXISTS trend_analysis ( + id SERIAL NOT NULL, + analysis_date DATE NOT NULL, + category VARCHAR(100), + analysis_type VARCHAR(50) NOT NULL, + summary TEXT NOT NULL, + hot_keywords TEXT, + hot_topics TEXT, + consumer_insights TEXT, + marketing_suggestions TEXT, + copywriting_hints TEXT, + source_stats TEXT, + record_count INTEGER, + model_used VARCHAR(50), + generation_time FLOAT, + created_at TIMESTAMP WITHOUT TIME ZONE, + PRIMARY KEY (id), + CONSTRAINT uq_analysis UNIQUE (analysis_date, category, analysis_type) +); + +CREATE INDEX IF NOT EXISTS ix_trend_analysis_analysis_date ON trend_analysis (analysis_date); +CREATE INDEX IF NOT EXISTS ix_trend_analysis_category ON trend_analysis (category); + +CREATE TABLE IF NOT EXISTS trend_keywords ( + id SERIAL NOT NULL, + keyword VARCHAR(100) NOT NULL, + keyword_type VARCHAR(50), + source VARCHAR(50) NOT NULL, + category VARCHAR(100), + mention_count INTEGER, + trend_date DATE NOT NULL, + sentiment_avg FLOAT, + related_keywords TEXT, + created_at TIMESTAMP WITHOUT TIME ZONE, + updated_at TIMESTAMP WITHOUT TIME ZONE, + PRIMARY KEY (id), + CONSTRAINT uq_keyword_source_date UNIQUE (keyword, source, trend_date) +); + +CREATE INDEX IF NOT EXISTS ix_trend_keywords_keyword ON trend_keywords (keyword); +CREATE INDEX IF NOT EXISTS ix_trend_keywords_category ON trend_keywords (category); +CREATE INDEX IF NOT EXISTS ix_trend_keywords_trend_date ON trend_keywords (trend_date); +CREATE INDEX IF NOT EXISTS idx_keyword_date_count ON trend_keywords (trend_date, mention_count); + +CREATE TABLE IF NOT EXISTS trend_records ( + id SERIAL NOT NULL, + source VARCHAR(50) NOT NULL, + source_board VARCHAR(100), + source_url VARCHAR(500), + source_id VARCHAR(100), + title VARCHAR(500) NOT NULL, + content TEXT, + author VARCHAR(100), + popularity_score INTEGER, + comment_count INTEGER, + category VARCHAR(100), + tags TEXT, + published_at TIMESTAMP WITHOUT TIME ZONE, + trend_date DATE NOT NULL, + created_at TIMESTAMP WITHOUT TIME ZONE, + sentiment VARCHAR(20), + ai_summary TEXT, + relevance_score FLOAT, + PRIMARY KEY (id), + CONSTRAINT uq_source_record UNIQUE (source, source_id) +); + +CREATE INDEX IF NOT EXISTS ix_trend_records_source ON trend_records (source); +CREATE INDEX IF NOT EXISTS ix_trend_records_category ON trend_records (category); +CREATE INDEX IF NOT EXISTS ix_trend_records_trend_date ON trend_records (trend_date); +CREATE INDEX IF NOT EXISTS idx_trend_source_date ON trend_records (source, trend_date); +CREATE INDEX IF NOT EXISTS idx_trend_category_date ON trend_records (category, trend_date); +CREATE INDEX IF NOT EXISTS idx_trend_popularity ON trend_records (popularity_score, trend_date); + +CREATE TABLE IF NOT EXISTS users ( + id SERIAL NOT NULL, + username VARCHAR(50) NOT NULL, + email VARCHAR(120), + password_hash VARCHAR(256) NOT NULL, + role VARCHAR(20), + display_name VARCHAR(100), + is_active BOOLEAN, + password_changed_at TIMESTAMP WITHOUT TIME ZONE, + created_at TIMESTAMP WITHOUT TIME ZONE, + updated_at TIMESTAMP WITHOUT TIME ZONE, + created_by INTEGER, + PRIMARY KEY (id), + UNIQUE (email), + FOREIGN KEY(created_by) REFERENCES users (id) +); + +CREATE UNIQUE INDEX IF NOT EXISTS ix_users_username ON users (username); +CREATE INDEX IF NOT EXISTS ix_users_role ON users (role); + +CREATE TABLE IF NOT EXISTS vendor_list ( + id SERIAL NOT NULL, + vendor_code VARCHAR(100) NOT NULL, + vendor_name VARCHAR(200) NOT NULL, + is_active BOOLEAN NOT NULL, + created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, + updated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, + PRIMARY KEY (id) +); + +CREATE UNIQUE INDEX IF NOT EXISTS ix_vendor_list_vendor_code ON vendor_list (vendor_code); +CREATE INDEX IF NOT EXISTS ix_vendor_list_is_active ON vendor_list (is_active); + +CREATE TABLE IF NOT EXISTS vendor_stockout ( + id SERIAL NOT NULL, + batch_id VARCHAR(50) NOT NULL, + import_date DATE NOT NULL, + import_time TIMESTAMP WITHOUT TIME ZONE NOT NULL, + department VARCHAR(100), + section VARCHAR(100), + pm_name VARCHAR(100), + zone_id VARCHAR(100), + zone_name VARCHAR(200), + product_code VARCHAR(100) NOT NULL, + product_name VARCHAR(500) NOT NULL, + product_spec TEXT, + borrow_transfer VARCHAR(100), + sale_price NUMERIC(10, 2), + cost_price NUMERIC(10, 2), + vendor_code VARCHAR(100) NOT NULL, + vendor_name VARCHAR(200) NOT NULL, + monthly_sales_qty INTEGER, + monthly_sales_amount NUMERIC(12, 2), + daily_avg_sales NUMERIC(10, 2), + current_stock INTEGER, + stockout_date DATE, + stockout_days INTEGER, + safe_stock_days INTEGER, + status VARCHAR(20) NOT NULL, + is_duplicate BOOLEAN, + duplicate_count INTEGER, + sent_date TIMESTAMP WITHOUT TIME ZONE, + sent_by VARCHAR(100), + error_message TEXT, + notes TEXT, + created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, + updated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, + PRIMARY KEY (id) +); + +CREATE INDEX IF NOT EXISTS ix_vendor_stockout_batch_id ON vendor_stockout (batch_id); +CREATE INDEX IF NOT EXISTS ix_vendor_stockout_import_date ON vendor_stockout (import_date); +CREATE INDEX IF NOT EXISTS ix_vendor_stockout_pm_name ON vendor_stockout (pm_name); +CREATE INDEX IF NOT EXISTS ix_vendor_stockout_product_code ON vendor_stockout (product_code); +CREATE INDEX IF NOT EXISTS ix_vendor_stockout_vendor_code ON vendor_stockout (vendor_code); +CREATE INDEX IF NOT EXISTS ix_vendor_stockout_vendor_name ON vendor_stockout (vendor_name); +CREATE INDEX IF NOT EXISTS ix_vendor_stockout_status ON vendor_stockout (status); +CREATE INDEX IF NOT EXISTS ix_vendor_stockout_is_duplicate ON vendor_stockout (is_duplicate); + +CREATE TABLE IF NOT EXISTS web_search_cache ( + id SERIAL NOT NULL, + query_hash VARCHAR(64) NOT NULL, + query VARCHAR(500) NOT NULL, + search_type VARCHAR(50), + result_json TEXT NOT NULL, + summary TEXT, + result_count INTEGER, + model_used VARCHAR(50), + generation_time FLOAT, + created_at TIMESTAMP WITHOUT TIME ZONE, + expires_at TIMESTAMP WITHOUT TIME ZONE, + PRIMARY KEY (id) +); + +CREATE UNIQUE INDEX IF NOT EXISTS ix_web_search_cache_query_hash ON web_search_cache (query_hash); +CREATE INDEX IF NOT EXISTS ix_web_search_cache_created_at ON web_search_cache (created_at); +CREATE INDEX IF NOT EXISTS idx_cache_query_type ON web_search_cache (query, search_type); +CREATE INDEX IF NOT EXISTS idx_cache_expires ON web_search_cache (expires_at); + +CREATE TABLE IF NOT EXISTS email_send_log ( + id SERIAL NOT NULL, + vendor_id INTEGER NOT NULL, + stockout_id INTEGER, + batch_id VARCHAR(50) NOT NULL, + sender_email VARCHAR(255) NOT NULL, + recipient_email VARCHAR(255) NOT NULL, + cc_emails TEXT, + bcc_emails TEXT, + subject VARCHAR(500) NOT NULL, + product_count INTEGER NOT NULL, + attachment_filename VARCHAR(255), + attachment_size INTEGER, + status VARCHAR(20) NOT NULL, + error_message TEXT, + retry_count INTEGER, + sent_at TIMESTAMP WITHOUT TIME ZONE, + created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, + PRIMARY KEY (id), + FOREIGN KEY(vendor_id) REFERENCES vendor_list (id), + FOREIGN KEY(stockout_id) REFERENCES vendor_stockout (id) +); + +CREATE INDEX IF NOT EXISTS ix_email_send_log_vendor_id ON email_send_log (vendor_id); +CREATE INDEX IF NOT EXISTS ix_email_send_log_stockout_id ON email_send_log (stockout_id); +CREATE INDEX IF NOT EXISTS ix_email_send_log_batch_id ON email_send_log (batch_id); +CREATE INDEX IF NOT EXISTS ix_email_send_log_status ON email_send_log (status); + +CREATE TABLE IF NOT EXISTS login_history ( + id SERIAL NOT NULL, + user_id INTEGER, + username_attempted VARCHAR(50), + login_time TIMESTAMP WITHOUT TIME ZONE, + ip_address VARCHAR(45), + user_agent VARCHAR(256), + status VARCHAR(20), + failure_reason VARCHAR(100), + PRIMARY KEY (id), + FOREIGN KEY(user_id) REFERENCES users (id) +); + +CREATE INDEX IF NOT EXISTS ix_login_history_login_time ON login_history (login_time); +CREATE INDEX IF NOT EXISTS ix_login_history_status ON login_history (status); + +CREATE TABLE IF NOT EXISTS products ( + id SERIAL NOT NULL, + i_code VARCHAR(50) NOT NULL, + name VARCHAR(255) NOT NULL, + url VARCHAR(500), + image_url TEXT, + category VARCHAR(100), + status VARCHAR(20), + updated_at TIMESTAMP WITHOUT TIME ZONE, + created_at TIMESTAMP WITHOUT TIME ZONE, + category_id INTEGER, + PRIMARY KEY (id), + FOREIGN KEY(category_id) REFERENCES categories (id) +); + +CREATE UNIQUE INDEX IF NOT EXISTS ix_products_i_code ON products (i_code); + +CREATE TABLE IF NOT EXISTS user_permissions ( + id SERIAL NOT NULL, + user_id INTEGER NOT NULL, + permission_code VARCHAR(50) NOT NULL, + granted_by INTEGER, + granted_at TIMESTAMP WITHOUT TIME ZONE, + PRIMARY KEY (id), + CONSTRAINT uq_user_permission UNIQUE (user_id, permission_code), + FOREIGN KEY(user_id) REFERENCES users (id) ON DELETE CASCADE, + FOREIGN KEY(granted_by) REFERENCES users (id) ON DELETE SET NULL +); + +CREATE INDEX IF NOT EXISTS ix_user_permissions_user_id ON user_permissions (user_id); +CREATE INDEX IF NOT EXISTS ix_user_permissions_permission_code ON user_permissions (permission_code); + +CREATE TABLE IF NOT EXISTS vendor_emails ( + id SERIAL NOT NULL, + vendor_id INTEGER NOT NULL, + email VARCHAR(255) NOT NULL, + contact_name VARCHAR(100), + email_type VARCHAR(20) NOT NULL, + is_active BOOLEAN NOT NULL, + notes TEXT, + created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, + updated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, + PRIMARY KEY (id), + FOREIGN KEY(vendor_id) REFERENCES vendor_list (id) +); + +CREATE INDEX IF NOT EXISTS ix_vendor_emails_vendor_id ON vendor_emails (vendor_id); +CREATE INDEX IF NOT EXISTS ix_vendor_emails_is_active ON vendor_emails (is_active); + +CREATE TABLE IF NOT EXISTS price_records ( + id SERIAL NOT NULL, + product_id INTEGER NOT NULL, + price FLOAT NOT NULL, + timestamp TIMESTAMP WITHOUT TIME ZONE, + PRIMARY KEY (id), + FOREIGN KEY(product_id) REFERENCES products (id) +); + +CREATE INDEX IF NOT EXISTS ix_price_records_timestamp ON price_records (timestamp); + +DO $$ +BEGIN + RAISE NOTICE 'Migration 035 done: core business metadata-only tables now have migration baseline'; +END $$; diff --git a/tests/test_migration_metadata_coverage.py b/tests/test_migration_metadata_coverage.py new file mode 100644 index 0000000..5505209 --- /dev/null +++ b/tests/test_migration_metadata_coverage.py @@ -0,0 +1,27 @@ +import re +from pathlib import Path + +from database.manager import Base + + +ROOT = Path(__file__).resolve().parents[1] + + +def _migration_created_tables(): + tables = set() + for path in (ROOT / "migrations").glob("*.sql"): + text = path.read_text(encoding="utf-8", errors="ignore") + for match in re.finditer( + r"CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?(?:public\.)?([\w_]+)", + text, + re.IGNORECASE, + ): + tables.add(match.group(1)) + return tables + + +def test_all_orm_metadata_tables_have_create_table_migration(): + metadata_tables = set(Base.metadata.tables) + migration_tables = _migration_created_tables() + + assert metadata_tables - migration_tables == set()