This commit is contained in:
548
migrations/035_core_business_tables_baseline.sql
Normal file
548
migrations/035_core_business_tables_baseline.sql
Normal file
@@ -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 $$;
|
||||
27
tests/test_migration_metadata_coverage.py
Normal file
27
tests/test_migration_metadata_coverage.py
Normal file
@@ -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()
|
||||
Reference in New Issue
Block a user