chore: reinitialize project with vite architecture
This commit is contained in:
344
tools/generate_documentation.py
Normal file
344
tools/generate_documentation.py
Normal file
@@ -0,0 +1,344 @@
|
||||
"""
|
||||
生成 MES 数据库参考文档
|
||||
用于报表开发参考
|
||||
"""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
# 读取表结构信息
|
||||
ROOT_DIR = Path(__file__).resolve().parent.parent
|
||||
DATA_FILE = ROOT_DIR / 'data' / 'table_schema_info.json'
|
||||
with open(DATA_FILE, 'r', encoding='utf-8') as f:
|
||||
table_info = json.load(f)
|
||||
|
||||
# 表用途描述(根据表名推断)
|
||||
TABLE_DESCRIPTIONS = {
|
||||
'DW_MES_CONTAINER': '容器/批次主檔 - 目前在製容器狀態、數量與流程資訊',
|
||||
'DW_MES_HOLDRELEASEHISTORY': 'Hold/Release 歷史表 - 批次停工與解除紀錄',
|
||||
'DW_MES_JOB': '設備維修工單表 - 維修工單的當前狀態與流程',
|
||||
'DW_MES_LOTREJECTHISTORY': '批次不良/報廢歷史表 - 不良原因與數量',
|
||||
'DW_MES_LOTWIPDATAHISTORY': '在製數據採集歷史表 - 製程量測/參數紀錄',
|
||||
'DW_MES_LOTWIPHISTORY': '在製流轉歷史表 - 批次進出站與流程軌跡',
|
||||
'DW_MES_MAINTENANCE': '設備保養/維護紀錄表 - 保養計畫與點檢數據',
|
||||
'DW_MES_PARTREQUESTORDER': '維修用料請求表 - 維修/設備零件請領',
|
||||
'DW_MES_PJ_COMBINEDASSYLOTS': '併批紀錄表 - 合批/合併批次關聯與數量資訊',
|
||||
'DW_MES_RESOURCESTATUS': '設備狀態變更歷史表 - 狀態切換與原因',
|
||||
'DW_MES_RESOURCESTATUS_SHIFT': '設備狀態班次彙總表 - 班次級狀態/工時',
|
||||
'DW_MES_WIP': '在製品現況表(含歷史累積)- 當前 WIP 狀態/數量',
|
||||
'DW_MES_HM_LOTMOVEOUT': '批次出站事件歷史表 - 出站/移出交易',
|
||||
'DW_MES_JOBTXNHISTORY': '維修工單交易歷史表 - 工單狀態變更紀錄',
|
||||
'DW_MES_LOTMATERIALSHISTORY': '批次物料消耗歷史表 - 用料與批次關聯',
|
||||
'DW_MES_RESOURCE': '資源表 - 設備/載具等資源基本資料(OBJECTCATEGORY=ASSEMBLY 時,RESOURCENAME 為設備編號)'
|
||||
}
|
||||
|
||||
# 常见字段说明
|
||||
COMMON_FIELD_NOTES = {
|
||||
'ID': '唯一标识符',
|
||||
'NAME': '名称',
|
||||
'STATUS': '状态',
|
||||
'TIMESTAMP': '时间戳',
|
||||
'CREATEDATE': '创建日期',
|
||||
'UPDATEDATE': '更新日期',
|
||||
'LOTID': '批次ID',
|
||||
'CONTAINERID': '容器ID',
|
||||
'RESOURCEID': '资源ID',
|
||||
'EQUIPMENTID': '设备ID',
|
||||
'OPERATIONID': '工序ID',
|
||||
'JOBID': '工单ID',
|
||||
'PRODUCTID': '产品ID',
|
||||
'CUSTOMERID': '客户ID',
|
||||
'QTY': '数量',
|
||||
'QUANTITY': '数量'
|
||||
}
|
||||
|
||||
|
||||
def generate_markdown():
|
||||
"""生成 Markdown 文档"""
|
||||
|
||||
md = []
|
||||
|
||||
# 标题和简介
|
||||
md.append("# MES 数据库报表开发参考文档\n")
|
||||
md.append(f"**生成时间**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
|
||||
md.append("---\n")
|
||||
|
||||
# 目录
|
||||
md.append("## 目录\n")
|
||||
md.append("1. [数据库连接信息](#数据库连接信息)")
|
||||
md.append("2. [数据库概览](#数据库概览)")
|
||||
md.append("3. [表结构详细说明](#表结构详细说明)")
|
||||
md.append("4. [报表开发注意事项](#报表开发注意事项)")
|
||||
md.append("5. [常用查询示例](#常用查询示例)\n")
|
||||
md.append("---\n")
|
||||
|
||||
# 1. 数据库连接信息
|
||||
md.append("## 数据库连接信息\n")
|
||||
md.append("### 连接参数\n")
|
||||
md.append("| 参数 | 值 |")
|
||||
md.append("|------|------|")
|
||||
md.append("| 数据库类型 | Oracle Database 19c Enterprise Edition |")
|
||||
md.append("| 主机地址 | 請參考 .env 檔案 (DB_HOST) |")
|
||||
md.append("| 端口 | 請參考 .env 檔案 (DB_PORT) |")
|
||||
md.append("| 服务名 | 請參考 .env 檔案 (DB_SERVICE) |")
|
||||
md.append("| 用户名 | 請參考 .env 檔案 (DB_USER) |")
|
||||
md.append("| 密码 | 請參考 .env 檔案 (DB_PASSWORD) |\n")
|
||||
|
||||
md.append("### Python 连接示例\n")
|
||||
md.append("```python")
|
||||
md.append("import os")
|
||||
md.append("import oracledb")
|
||||
md.append("from dotenv import load_dotenv")
|
||||
md.append("")
|
||||
md.append("# 載入環境變數")
|
||||
md.append("load_dotenv()")
|
||||
md.append("")
|
||||
md.append("# 连接配置 (從環境變數讀取)")
|
||||
md.append("DB_CONFIG = {")
|
||||
md.append(" 'user': os.getenv('DB_USER'),")
|
||||
md.append(" 'password': os.getenv('DB_PASSWORD'),")
|
||||
md.append(" 'dsn': f\"(DESCRIPTION=(ADDRESS_LIST=(ADDRESS=(PROTOCOL=TCP)(HOST={os.getenv('DB_HOST')})(PORT={os.getenv('DB_PORT')})))(CONNECT_DATA=(SERVICE_NAME={os.getenv('DB_SERVICE')})))\"")
|
||||
md.append("}")
|
||||
md.append("")
|
||||
md.append("# 建立连接")
|
||||
md.append("connection = oracledb.connect(**DB_CONFIG)")
|
||||
md.append("cursor = connection.cursor()")
|
||||
md.append("")
|
||||
md.append("# 执行查询")
|
||||
md.append("cursor.execute('SELECT * FROM DW_MES_WIP WHERE ROWNUM <= 10')")
|
||||
md.append("results = cursor.fetchall()")
|
||||
md.append("")
|
||||
md.append("# 关闭连接")
|
||||
md.append("cursor.close()")
|
||||
md.append("connection.close()")
|
||||
md.append("```\n")
|
||||
|
||||
md.append("### JDBC 连接字符串\n")
|
||||
md.append("```")
|
||||
md.append("jdbc:oracle:thin:@${DB_HOST}:${DB_PORT}:${DB_SERVICE}")
|
||||
md.append("```\n")
|
||||
|
||||
# 2. 数据库概览
|
||||
md.append("---\n")
|
||||
md.append("## 数据库概览\n")
|
||||
md.append("### 表统计信息\n")
|
||||
md.append("| # | 表名 | 用途 | 数据量 |")
|
||||
md.append("|---|------|------|--------|")
|
||||
|
||||
for idx, (table_name, info) in enumerate(sorted(table_info.items()), 1):
|
||||
if 'error' not in info:
|
||||
row_count = f"{info['row_count']:,}"
|
||||
description = TABLE_DESCRIPTIONS.get(table_name, '待补充')
|
||||
md.append(f"| {idx} | `{table_name}` | {description} | {row_count} |")
|
||||
|
||||
md.append("")
|
||||
|
||||
# 计算总数据量
|
||||
total_rows = sum(info['row_count'] for info in table_info.values() if 'error' not in info)
|
||||
md.append(f"**总数据量**: {total_rows:,} 行\n")
|
||||
|
||||
# 3. 表结构详细说明
|
||||
md.append("---\n")
|
||||
md.append("## 表结构详细说明\n")
|
||||
|
||||
for table_name in sorted(table_info.keys()):
|
||||
info = table_info[table_name]
|
||||
|
||||
if 'error' in info:
|
||||
continue
|
||||
|
||||
md.append(f"### {table_name}\n")
|
||||
|
||||
# 表说明
|
||||
md.append(f"**用途**: {TABLE_DESCRIPTIONS.get(table_name, '待补充')}\n")
|
||||
md.append(f"**数据量**: {info['row_count']:,} 行\n")
|
||||
|
||||
if info.get('table_comment'):
|
||||
md.append(f"**表注释**: {info['table_comment']}\n")
|
||||
|
||||
# 字段列表
|
||||
md.append("#### 字段列表\n")
|
||||
md.append("| # | 字段名 | 数据类型 | 长度 | 可空 | 说明 |")
|
||||
md.append("|---|--------|----------|------|------|------|")
|
||||
|
||||
schema = info.get('schema', [])
|
||||
for col in schema:
|
||||
col_num = col['column_id']
|
||||
col_name = col['column_name']
|
||||
|
||||
# 构建数据类型显示
|
||||
if col['data_type'] in ['VARCHAR2', 'CHAR']:
|
||||
data_type = f"{col['data_type']}({col['data_length']})"
|
||||
elif col['data_type'] == 'NUMBER' and col['data_precision']:
|
||||
if col['data_scale']:
|
||||
data_type = f"NUMBER({col['data_precision']},{col['data_scale']})"
|
||||
else:
|
||||
data_type = f"NUMBER({col['data_precision']})"
|
||||
else:
|
||||
data_type = col['data_type']
|
||||
|
||||
nullable = "是" if col['nullable'] == 'Y' else "否"
|
||||
|
||||
# 获取字段说明
|
||||
column_comments = info.get('column_comments', {})
|
||||
comment = column_comments.get(col_name, '')
|
||||
|
||||
# 如果没有注释,尝试从常见字段说明中获取
|
||||
if not comment:
|
||||
for key, value in COMMON_FIELD_NOTES.items():
|
||||
if key in col_name:
|
||||
comment = value
|
||||
break
|
||||
|
||||
md.append(f"| {col_num} | `{col_name}` | {data_type} | {col.get('data_length', '-')} | {nullable} | {comment} |")
|
||||
|
||||
md.append("")
|
||||
|
||||
# 索引信息
|
||||
indexes = info.get('indexes', [])
|
||||
if indexes:
|
||||
md.append("#### 索引\n")
|
||||
md.append("| 索引名 | 类型 | 字段 |")
|
||||
md.append("|--------|------|------|")
|
||||
for idx_info in indexes:
|
||||
idx_type = "唯一索引" if idx_info[1] == 'UNIQUE' else "普通索引"
|
||||
md.append(f"| `{idx_info[0]}` | {idx_type} | {idx_info[2]} |")
|
||||
md.append("")
|
||||
|
||||
md.append("---\n")
|
||||
|
||||
# 4. 报表开发注意事项
|
||||
md.append("## 报表开发注意事项\n")
|
||||
md.append("### 性能优化建议\n")
|
||||
md.append("1. **大数据量表查询优化**")
|
||||
md.append(" - 以下表数据量较大,查询时务必添加时间范围限制:")
|
||||
|
||||
large_tables = [(name, info['row_count']) for name, info in table_info.items()
|
||||
if 'error' not in info and info['row_count'] > 10000000]
|
||||
large_tables.sort(key=lambda x: x[1], reverse=True)
|
||||
|
||||
for table_name, count in large_tables:
|
||||
md.append(f" - `{table_name}`: {count:,} 行")
|
||||
|
||||
md.append("")
|
||||
md.append("2. **索引使用**")
|
||||
md.append(" - 查询时尽量使用已建立索引的字段作为查询条件")
|
||||
md.append(" - 避免在索引字段上使用函数,会导致索引失效")
|
||||
md.append("")
|
||||
md.append("3. **连接池配置**")
|
||||
md.append(" - 建议使用连接池管理数据库连接")
|
||||
md.append(" - 推荐连接池大小:5-10 个连接")
|
||||
md.append("")
|
||||
md.append("4. **查询超时设置**")
|
||||
md.append(" - 建议设置查询超时时间为 30-60 秒")
|
||||
md.append(" - 避免长时间运行的查询影响系统性能")
|
||||
md.append("")
|
||||
|
||||
md.append("### 数据时效性\n")
|
||||
md.append("- **实时数据表**: `DW_MES_WIP`(含歷史累積), `DW_MES_RESOURCESTATUS`")
|
||||
md.append("- **历史数据表**: 带有 `HISTORY` 后缀的表")
|
||||
md.append("- **主数据表**: `DW_MES_RESOURCE`, `DW_MES_CONTAINER`")
|
||||
md.append("")
|
||||
|
||||
md.append("### 常用时间字段\n")
|
||||
md.append("大多数历史表包含以下时间相关字段:")
|
||||
md.append("- `CREATEDATE` / `CREATETIMESTAMP`: 记录创建时间")
|
||||
md.append("- `UPDATEDATE` / `UPDATETIMESTAMP`: 记录更新时间")
|
||||
md.append("- `TRANSACTIONDATE`: 交易发生时间")
|
||||
md.append("")
|
||||
|
||||
md.append("### 数据权限\n")
|
||||
md.append("- 當前帳號為唯讀帳號 (詳見 .env 中的 DB_USER)")
|
||||
md.append("- 仅可执行 SELECT 查询")
|
||||
md.append("- 无法进行 INSERT, UPDATE, DELETE 操作")
|
||||
md.append("")
|
||||
|
||||
# 5. 常用查询示例
|
||||
md.append("---\n")
|
||||
md.append("## 常用查询示例\n")
|
||||
|
||||
md.append("### 1. 查询当前在制品数量\n")
|
||||
md.append("```sql")
|
||||
md.append("SELECT COUNT(*) as WIP_COUNT")
|
||||
md.append("FROM DW_MES_WIP")
|
||||
md.append("WHERE CURRENTSTATUSID IS NOT NULL;")
|
||||
md.append("```\n")
|
||||
|
||||
md.append("### 2. 查询设备状态统计\n")
|
||||
md.append("```sql")
|
||||
md.append("SELECT")
|
||||
md.append(" CURRENTSTATUSID,")
|
||||
md.append(" COUNT(*) as COUNT")
|
||||
md.append("FROM DW_MES_RESOURCESTATUS")
|
||||
md.append("GROUP BY CURRENTSTATUSID")
|
||||
md.append("ORDER BY COUNT DESC;")
|
||||
md.append("```\n")
|
||||
|
||||
md.append("### 3. 查询最近 7 天的批次历史\n")
|
||||
md.append("```sql")
|
||||
md.append("SELECT *")
|
||||
md.append("FROM DW_MES_LOTWIPHISTORY")
|
||||
md.append("WHERE CREATEDATE >= SYSDATE - 7")
|
||||
md.append("ORDER BY CREATEDATE DESC;")
|
||||
md.append("```\n")
|
||||
|
||||
md.append("### 4. 查询工单完成情况\n")
|
||||
md.append("```sql")
|
||||
md.append("SELECT")
|
||||
md.append(" JOBID,")
|
||||
md.append(" JOBSTATUS,")
|
||||
md.append(" COUNT(*) as COUNT")
|
||||
md.append("FROM DW_MES_JOB")
|
||||
md.append("GROUP BY JOBID, JOBSTATUS")
|
||||
md.append("ORDER BY JOBID;")
|
||||
md.append("```\n")
|
||||
|
||||
md.append("### 5. 按日期统计生产数量\n")
|
||||
md.append("```sql")
|
||||
md.append("SELECT")
|
||||
md.append(" TRUNC(CREATEDATE) as PRODUCTION_DATE,")
|
||||
md.append(" COUNT(*) as LOT_COUNT")
|
||||
md.append("FROM DW_MES_HM_LOTMOVEOUT")
|
||||
md.append("WHERE CREATEDATE >= SYSDATE - 30")
|
||||
md.append("GROUP BY TRUNC(CREATEDATE)")
|
||||
md.append("ORDER BY PRODUCTION_DATE DESC;")
|
||||
md.append("```\n")
|
||||
|
||||
md.append("### 6. 联表查询示例(批次与容器)\n")
|
||||
md.append("```sql")
|
||||
md.append("SELECT")
|
||||
md.append(" w.LOTID,")
|
||||
md.append(" w.CONTAINERNAME,")
|
||||
md.append(" c.CURRENTSTATUSID,")
|
||||
md.append(" c.CUSTOMERID")
|
||||
md.append("FROM DW_MES_WIP w")
|
||||
md.append("LEFT JOIN DW_MES_CONTAINER c ON w.CONTAINERID = c.CONTAINERID")
|
||||
md.append("WHERE w.CREATEDATE >= SYSDATE - 1")
|
||||
md.append("ORDER BY w.CREATEDATE DESC;")
|
||||
md.append("```\n")
|
||||
|
||||
md.append("---\n")
|
||||
md.append("## 附录\n")
|
||||
md.append("### 文档更新记录\n")
|
||||
md.append(f"- {datetime.now().strftime('%Y-%m-%d')}: 初始版本创建")
|
||||
md.append("")
|
||||
md.append("### 联系方式\n")
|
||||
md.append("如有疑问或需要补充信息,请联系数据库管理员。\n")
|
||||
|
||||
return '\n'.join(md)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Generating documentation...")
|
||||
markdown_content = generate_markdown()
|
||||
|
||||
output_file = ROOT_DIR / 'docs' / 'MES_Database_Reference.md'
|
||||
with open(output_file, 'w', encoding='utf-8') as f:
|
||||
f.write(markdown_content)
|
||||
|
||||
print(f"[OK] Documentation generated: {output_file}")
|
||||
|
||||
|
||||
|
||||
|
||||
261
tools/query_table_schema.py
Normal file
261
tools/query_table_schema.py
Normal file
@@ -0,0 +1,261 @@
|
||||
"""
|
||||
查询 MES 表结构信息脚本
|
||||
用于生成报表开发参考文档
|
||||
"""
|
||||
|
||||
import sys
|
||||
import io
|
||||
import os
|
||||
import json
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
import oracledb
|
||||
|
||||
# 设置 UTF-8 编码输出
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
||||
|
||||
# Load .env file
|
||||
try:
|
||||
from dotenv import load_dotenv
|
||||
env_path = Path(__file__).resolve().parent.parent / '.env'
|
||||
load_dotenv(env_path)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# 数据库连接信息 (从环境变量读取,必须在 .env 中设置)
|
||||
DB_HOST = os.getenv('DB_HOST', '')
|
||||
DB_PORT = os.getenv('DB_PORT', '1521')
|
||||
DB_SERVICE = os.getenv('DB_SERVICE', '')
|
||||
DB_USER = os.getenv('DB_USER', '')
|
||||
DB_PASSWORD = os.getenv('DB_PASSWORD', '')
|
||||
|
||||
DB_CONFIG = {
|
||||
'user': DB_USER,
|
||||
'password': DB_PASSWORD,
|
||||
'dsn': f'(DESCRIPTION=(ADDRESS_LIST=(ADDRESS=(PROTOCOL=TCP)(HOST={DB_HOST})(PORT={DB_PORT})))(CONNECT_DATA=(SERVICE_NAME={DB_SERVICE})))'
|
||||
}
|
||||
|
||||
# MES 表列表(預設清單)
|
||||
MES_TABLES = [
|
||||
'DW_MES_CONTAINER',
|
||||
'DW_MES_HOLDRELEASEHISTORY',
|
||||
'DW_MES_JOB',
|
||||
'DW_MES_LOTREJECTHISTORY',
|
||||
'DW_MES_LOTWIPDATAHISTORY',
|
||||
'DW_MES_LOTWIPHISTORY',
|
||||
'DW_MES_MAINTENANCE',
|
||||
'DW_MES_PARTREQUESTORDER',
|
||||
'DW_MES_PJ_COMBINEDASSYLOTS',
|
||||
'DW_MES_RESOURCESTATUS',
|
||||
'DW_MES_RESOURCESTATUS_SHIFT',
|
||||
'DW_MES_WIP',
|
||||
'DW_MES_HM_LOTMOVEOUT',
|
||||
'DW_MES_JOBTXNHISTORY',
|
||||
'DW_MES_LOTMATERIALSHISTORY',
|
||||
'DW_MES_RESOURCE'
|
||||
]
|
||||
|
||||
def get_table_schema(cursor, table_name, owner=None):
|
||||
"""获取表的结构信息"""
|
||||
query = """
|
||||
SELECT
|
||||
COLUMN_NAME,
|
||||
DATA_TYPE,
|
||||
DATA_LENGTH,
|
||||
DATA_PRECISION,
|
||||
DATA_SCALE,
|
||||
NULLABLE,
|
||||
DATA_DEFAULT,
|
||||
COLUMN_ID
|
||||
FROM ALL_TAB_COLUMNS
|
||||
WHERE TABLE_NAME = :table_name
|
||||
"""
|
||||
if owner:
|
||||
query += " AND OWNER = :owner ORDER BY COLUMN_ID"
|
||||
cursor.execute(query, table_name=table_name, owner=owner)
|
||||
else:
|
||||
query += " ORDER BY COLUMN_ID"
|
||||
cursor.execute(query, table_name=table_name)
|
||||
columns = cursor.fetchall()
|
||||
|
||||
schema = []
|
||||
for col in columns:
|
||||
col_info = {
|
||||
'column_name': col[0],
|
||||
'data_type': col[1],
|
||||
'data_length': col[2],
|
||||
'data_precision': col[3],
|
||||
'data_scale': col[4],
|
||||
'nullable': col[5],
|
||||
'default_value': col[6],
|
||||
'column_id': col[7]
|
||||
}
|
||||
schema.append(col_info)
|
||||
|
||||
return schema
|
||||
|
||||
|
||||
def get_table_comments(cursor, table_name, owner=None):
|
||||
"""获取表和列的注释"""
|
||||
# 获取表注释
|
||||
table_query = """
|
||||
SELECT COMMENTS
|
||||
FROM ALL_TAB_COMMENTS
|
||||
WHERE TABLE_NAME = :table_name
|
||||
"""
|
||||
if owner:
|
||||
table_query += " AND OWNER = :owner"
|
||||
cursor.execute(table_query, table_name=table_name, owner=owner)
|
||||
else:
|
||||
cursor.execute(table_query, table_name=table_name)
|
||||
table_comment = cursor.fetchone()
|
||||
|
||||
# 获取列注释
|
||||
col_query = """
|
||||
SELECT COLUMN_NAME, COMMENTS
|
||||
FROM ALL_COL_COMMENTS
|
||||
WHERE TABLE_NAME = :table_name
|
||||
"""
|
||||
if owner:
|
||||
col_query += " AND OWNER = :owner ORDER BY COLUMN_NAME"
|
||||
cursor.execute(col_query, table_name=table_name, owner=owner)
|
||||
else:
|
||||
col_query += " ORDER BY COLUMN_NAME"
|
||||
cursor.execute(col_query, table_name=table_name)
|
||||
column_comments = {row[0]: row[1] for row in cursor.fetchall()}
|
||||
|
||||
return table_comment[0] if table_comment else None, column_comments
|
||||
|
||||
|
||||
def get_table_indexes(cursor, table_name, owner=None):
|
||||
"""获取表的索引信息"""
|
||||
query = """
|
||||
SELECT
|
||||
i.INDEX_NAME,
|
||||
i.UNIQUENESS,
|
||||
LISTAGG(ic.COLUMN_NAME, ', ') WITHIN GROUP (ORDER BY ic.COLUMN_POSITION) as COLUMNS
|
||||
FROM ALL_INDEXES i
|
||||
JOIN ALL_IND_COLUMNS ic ON i.INDEX_NAME = ic.INDEX_NAME AND i.TABLE_NAME = ic.TABLE_NAME
|
||||
WHERE i.TABLE_NAME = :table_name
|
||||
GROUP BY i.INDEX_NAME, i.UNIQUENESS
|
||||
ORDER BY i.INDEX_NAME
|
||||
"""
|
||||
if owner:
|
||||
query = query.replace(
|
||||
"WHERE i.TABLE_NAME = :table_name",
|
||||
"WHERE i.TABLE_NAME = :table_name AND i.TABLE_OWNER = :owner",
|
||||
)
|
||||
cursor.execute(query, table_name=table_name, owner=owner)
|
||||
else:
|
||||
cursor.execute(query, table_name=table_name)
|
||||
return cursor.fetchall()
|
||||
|
||||
|
||||
def get_sample_data(cursor, table_name, owner=None, limit=5):
|
||||
"""获取表的示例数据"""
|
||||
try:
|
||||
if owner:
|
||||
cursor.execute(f"SELECT * FROM {owner}.{table_name} WHERE ROWNUM <= {limit}")
|
||||
else:
|
||||
cursor.execute(f"SELECT * FROM {table_name} WHERE ROWNUM <= {limit}")
|
||||
columns = [col[0] for col in cursor.description]
|
||||
rows = cursor.fetchall()
|
||||
return columns, rows
|
||||
except Exception as e:
|
||||
return None, str(e)
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
parser = argparse.ArgumentParser(description="Query Oracle table/view schema information")
|
||||
parser.add_argument(
|
||||
"--schema",
|
||||
help="Schema/owner to scan (e.g. DWH). If set, scans all TABLE/VIEW in that schema.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output",
|
||||
help="Output JSON path (default: data/table_schema_info.json)",
|
||||
default=None,
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
print("Connecting to database...")
|
||||
connection = oracledb.connect(**DB_CONFIG)
|
||||
cursor = connection.cursor()
|
||||
|
||||
all_table_info = {}
|
||||
|
||||
owner = args.schema.strip().upper() if args.schema else None
|
||||
if owner:
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT OBJECT_NAME
|
||||
FROM ALL_OBJECTS
|
||||
WHERE OWNER = :owner
|
||||
AND OBJECT_TYPE IN ('TABLE', 'VIEW')
|
||||
ORDER BY OBJECT_NAME
|
||||
""",
|
||||
owner=owner,
|
||||
)
|
||||
table_list = [row[0] for row in cursor.fetchall()]
|
||||
else:
|
||||
table_list = MES_TABLES
|
||||
|
||||
print(f"\nQuerying schema information for {len(table_list)} objects...\n")
|
||||
|
||||
for idx, table_name in enumerate(table_list, 1):
|
||||
print(f"[{idx}/{len(table_list)}] Processing {table_name}...")
|
||||
|
||||
try:
|
||||
# 获取表结构
|
||||
schema = get_table_schema(cursor, table_name, owner=owner)
|
||||
|
||||
# 获取注释
|
||||
table_comment, column_comments = get_table_comments(cursor, table_name, owner=owner)
|
||||
|
||||
# 获取索引
|
||||
indexes = get_table_indexes(cursor, table_name, owner=owner)
|
||||
|
||||
# 获取行数
|
||||
if owner:
|
||||
cursor.execute(f"SELECT COUNT(*) FROM {owner}.{table_name}")
|
||||
else:
|
||||
cursor.execute(f"SELECT COUNT(*) FROM {table_name}")
|
||||
row_count = cursor.fetchone()[0]
|
||||
|
||||
# 获取示例数据
|
||||
sample_columns, sample_data = get_sample_data(cursor, table_name, owner=owner, limit=3)
|
||||
|
||||
all_table_info[table_name] = {
|
||||
'owner': owner,
|
||||
'table_comment': table_comment,
|
||||
'row_count': row_count,
|
||||
'schema': schema,
|
||||
'column_comments': column_comments,
|
||||
'indexes': indexes,
|
||||
'sample_columns': sample_columns,
|
||||
'sample_data': sample_data
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print(f" Error: {str(e)}")
|
||||
all_table_info[table_name] = {'error': str(e)}
|
||||
|
||||
# 保存到 JSON 文件
|
||||
if args.output:
|
||||
output_file = Path(args.output)
|
||||
else:
|
||||
output_file = Path(__file__).resolve().parent.parent / 'data' / 'table_schema_info.json'
|
||||
with open(output_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(all_table_info, f, ensure_ascii=False, indent=2, default=str)
|
||||
|
||||
print(f"\n[OK] Schema information saved to {output_file}")
|
||||
|
||||
cursor.close()
|
||||
connection.close()
|
||||
print("[OK] Connection closed")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
152
tools/test_oracle_connection.py
Normal file
152
tools/test_oracle_connection.py
Normal file
@@ -0,0 +1,152 @@
|
||||
"""
|
||||
Oracle Database Connection Test Script
|
||||
测试连接到 DWDB 数据库并验证 MES 表访问权限
|
||||
"""
|
||||
|
||||
import sys
|
||||
import io
|
||||
import os
|
||||
import oracledb
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
# 设置 UTF-8 编码输出
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
||||
|
||||
# Load .env file
|
||||
try:
|
||||
from dotenv import load_dotenv
|
||||
env_path = Path(__file__).resolve().parent.parent / '.env'
|
||||
load_dotenv(env_path)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# 数据库连接信息 (从环境变量读取,必须在 .env 中设置)
|
||||
DB_HOST = os.getenv('DB_HOST', '')
|
||||
DB_PORT = os.getenv('DB_PORT', '1521')
|
||||
DB_SERVICE = os.getenv('DB_SERVICE', '')
|
||||
DB_USER = os.getenv('DB_USER', '')
|
||||
DB_PASSWORD = os.getenv('DB_PASSWORD', '')
|
||||
|
||||
DB_CONFIG = {
|
||||
'user': DB_USER,
|
||||
'password': DB_PASSWORD,
|
||||
'dsn': f'(DESCRIPTION=(ADDRESS_LIST=(ADDRESS=(PROTOCOL=TCP)(HOST={DB_HOST})(PORT={DB_PORT})))(CONNECT_DATA=(SERVICE_NAME={DB_SERVICE})))'
|
||||
}
|
||||
|
||||
# MES 表列表
|
||||
MES_TABLES = [
|
||||
'DW_MES_CONTAINER',
|
||||
'DW_MES_HOLDRELEASEHISTORY',
|
||||
'DW_MES_JOB',
|
||||
'DW_MES_LOTREJECTHISTORY',
|
||||
'DW_MES_LOTWIPDATAHISTORY',
|
||||
'DW_MES_LOTWIPHISTORY',
|
||||
'DW_MES_MAINTENANCE',
|
||||
'DW_MES_PARTREQUESTORDER',
|
||||
'DW_MES_PJ_COMBINEDASSYLOTS',
|
||||
'DW_MES_RESOURCESTATUS',
|
||||
'DW_MES_RESOURCESTATUS_SHIFT',
|
||||
'DW_MES_WIP',
|
||||
'DW_MES_HM_LOTMOVEOUT',
|
||||
'DW_MES_JOBTXNHISTORY',
|
||||
'DW_MES_LOTMATERIALSHISTORY',
|
||||
'DW_MES_RESOURCE'
|
||||
]
|
||||
|
||||
|
||||
def test_connection():
|
||||
"""测试数据库连接"""
|
||||
print("=" * 60)
|
||||
print("Oracle Database Connection Test")
|
||||
print("=" * 60)
|
||||
print(f"Test Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
print(f"Host: {DB_HOST}:{DB_PORT}")
|
||||
print(f"Service Name: {DB_SERVICE}")
|
||||
print(f"User: {DB_USER}")
|
||||
print("=" * 60)
|
||||
|
||||
try:
|
||||
# 尝试连接数据库
|
||||
print("\n[1/3] Attempting to connect to database...")
|
||||
connection = oracledb.connect(**DB_CONFIG)
|
||||
print("[OK] Connection successful!")
|
||||
|
||||
# 获取数据库版本信息
|
||||
print("\n[2/3] Retrieving database version...")
|
||||
cursor = connection.cursor()
|
||||
cursor.execute("SELECT * FROM v$version WHERE banner LIKE 'Oracle%'")
|
||||
version = cursor.fetchone()
|
||||
if version:
|
||||
print(f"[OK] Database Version: {version[0]}")
|
||||
|
||||
# 测试每个表的访问权限
|
||||
print("\n[3/3] Testing access to MES tables...")
|
||||
print("-" * 60)
|
||||
|
||||
accessible_tables = []
|
||||
inaccessible_tables = []
|
||||
|
||||
for table_name in MES_TABLES:
|
||||
try:
|
||||
# 尝试查询表的行数
|
||||
cursor.execute(f"SELECT COUNT(*) FROM {table_name}")
|
||||
count = cursor.fetchone()[0]
|
||||
print(f"[OK] {table_name:<35} - {count:,} rows")
|
||||
accessible_tables.append(table_name)
|
||||
except oracledb.DatabaseError as e:
|
||||
error_obj, = e.args
|
||||
print(f"[FAIL] {table_name:<35} - Error: {error_obj.message}")
|
||||
inaccessible_tables.append((table_name, error_obj.message))
|
||||
|
||||
# 汇总结果
|
||||
print("\n" + "=" * 60)
|
||||
print("Test Summary")
|
||||
print("=" * 60)
|
||||
print(f"Total tables tested: {len(MES_TABLES)}")
|
||||
print(f"Accessible tables: {len(accessible_tables)}")
|
||||
print(f"Inaccessible tables: {len(inaccessible_tables)}")
|
||||
|
||||
if inaccessible_tables:
|
||||
print("\nInaccessible Tables:")
|
||||
for table, error in inaccessible_tables:
|
||||
print(f" - {table}: {error}")
|
||||
|
||||
# 关闭连接
|
||||
cursor.close()
|
||||
connection.close()
|
||||
print("\n[OK] Connection closed successfully")
|
||||
|
||||
return len(inaccessible_tables) == 0
|
||||
|
||||
except oracledb.DatabaseError as e:
|
||||
error_obj, = e.args
|
||||
print(f"\n[FAIL] Database Error: {error_obj.message}")
|
||||
print(f" Error Code: {error_obj.code}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n[FAIL] Unexpected Error: {str(e)}")
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
try:
|
||||
success = test_connection()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
if success:
|
||||
print("[SUCCESS] All tests passed successfully!")
|
||||
else:
|
||||
print("[WARNING] Some tests failed. Please check the output above.")
|
||||
print("=" * 60)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n\nTest interrupted by user.")
|
||||
except Exception as e:
|
||||
print(f"\n\nFatal error: {str(e)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
194
tools/update_oracle_authorized_objects.py
Normal file
194
tools/update_oracle_authorized_objects.py
Normal file
@@ -0,0 +1,194 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generate a list of accessible TABLE/VIEW objects under a specific owner (default: DWH)
|
||||
and update docs/Oracle_Authorized_Objects.md.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from collections import Counter
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import oracledb
|
||||
|
||||
|
||||
def load_env() -> None:
|
||||
"""Load .env if available (best-effort)."""
|
||||
try:
|
||||
from dotenv import load_dotenv # type: ignore
|
||||
|
||||
env_path = Path(__file__).resolve().parent.parent / ".env"
|
||||
load_dotenv(env_path)
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
env_path = Path(__file__).resolve().parent.parent / ".env"
|
||||
if not env_path.exists():
|
||||
return
|
||||
for line in env_path.read_text().splitlines():
|
||||
if not line or line.strip().startswith("#") or "=" not in line:
|
||||
continue
|
||||
key, value = line.split("=", 1)
|
||||
os.environ.setdefault(key.strip(), value.strip())
|
||||
|
||||
|
||||
def get_connection():
|
||||
host = os.getenv("DB_HOST", "")
|
||||
port = os.getenv("DB_PORT", "1521")
|
||||
service = os.getenv("DB_SERVICE", "")
|
||||
user = os.getenv("DB_USER", "")
|
||||
password = os.getenv("DB_PASSWORD", "")
|
||||
dsn = (
|
||||
"(DESCRIPTION=(ADDRESS_LIST=(ADDRESS=(PROTOCOL=TCP)"
|
||||
f"(HOST={host})(PORT={port})))(CONNECT_DATA=(SERVICE_NAME={service})))"
|
||||
)
|
||||
return oracledb.connect(user=user, password=password, dsn=dsn)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
owner = "DWH"
|
||||
output_path = Path("docs/Oracle_Authorized_Objects.md")
|
||||
if len(sys.argv) > 1:
|
||||
owner = sys.argv[1].strip().upper()
|
||||
|
||||
load_env()
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
cur.execute("SELECT USER FROM DUAL")
|
||||
user = cur.fetchone()[0]
|
||||
|
||||
# Roles
|
||||
cur.execute("SELECT GRANTED_ROLE FROM USER_ROLE_PRIVS")
|
||||
roles = [r[0] for r in cur.fetchall()]
|
||||
|
||||
# Accessible objects under owner
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT OBJECT_NAME, OBJECT_TYPE
|
||||
FROM ALL_OBJECTS
|
||||
WHERE OWNER = :p_owner
|
||||
AND OBJECT_TYPE IN ('TABLE', 'VIEW')
|
||||
ORDER BY OBJECT_NAME
|
||||
""",
|
||||
p_owner=owner,
|
||||
)
|
||||
objects = cur.fetchall()
|
||||
|
||||
# Direct + PUBLIC grants
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT o.object_name, o.object_type, p.privilege,
|
||||
CASE WHEN p.grantee = 'PUBLIC' THEN 'PUBLIC' ELSE 'DIRECT' END AS source
|
||||
FROM all_tab_privs p
|
||||
JOIN all_objects o
|
||||
ON o.owner = p.table_schema
|
||||
AND o.object_name = p.table_name
|
||||
WHERE p.grantee IN (:p_user, 'PUBLIC')
|
||||
AND o.owner = :p_owner
|
||||
AND o.object_type IN ('TABLE', 'VIEW')
|
||||
""",
|
||||
p_user=user,
|
||||
p_owner=owner,
|
||||
)
|
||||
direct_rows = cur.fetchall()
|
||||
|
||||
# Role grants
|
||||
role_rows = []
|
||||
for role in roles:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT o.object_name, o.object_type, p.privilege, p.role AS source
|
||||
FROM role_tab_privs p
|
||||
JOIN all_objects o
|
||||
ON o.owner = p.owner
|
||||
AND o.object_name = p.table_name
|
||||
WHERE p.role = :p_role
|
||||
AND o.owner = :p_owner
|
||||
AND o.object_type IN ('TABLE', 'VIEW')
|
||||
""",
|
||||
p_role=role,
|
||||
p_owner=owner,
|
||||
)
|
||||
role_rows.extend(cur.fetchall())
|
||||
|
||||
# Aggregate privileges by object
|
||||
info = {}
|
||||
for name, otype in objects:
|
||||
info[(name, otype)] = {"privs": set(), "sources": set()}
|
||||
|
||||
for name, otype, priv, source in direct_rows + role_rows:
|
||||
key = (name, otype)
|
||||
if key not in info:
|
||||
info[key] = {"privs": set(), "sources": set()}
|
||||
info[key]["privs"].add(priv)
|
||||
info[key]["sources"].add(source)
|
||||
|
||||
# Fill in missing privilege/source if object is visible but not in grants
|
||||
for key, data in info.items():
|
||||
if not data["privs"]:
|
||||
data["privs"].add("UNKNOWN")
|
||||
if not data["sources"]:
|
||||
data["sources"].add("SYSTEM")
|
||||
|
||||
type_counts = Counter(k[1] for k in info.keys())
|
||||
source_counts = Counter()
|
||||
for data in info.values():
|
||||
for s in data["sources"]:
|
||||
if s in ("DIRECT", "PUBLIC"):
|
||||
source_counts[s] += 1
|
||||
elif s == "SYSTEM":
|
||||
source_counts["SYSTEM"] += 1
|
||||
else:
|
||||
source_counts["ROLE"] += 1
|
||||
|
||||
# Render markdown
|
||||
lines = []
|
||||
lines.append("# Oracle 可使用 TABLE/VIEW 清單(DWH)")
|
||||
lines.append("")
|
||||
lines.append(f"**產生時間**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
lines.append(f"**使用者**: {user}")
|
||||
lines.append(f"**Schema**: {owner}")
|
||||
lines.append("")
|
||||
lines.append("## 摘要")
|
||||
lines.append("")
|
||||
lines.append(f"- 可使用物件總數: {len(info):,}")
|
||||
lines.append(f"- TABLE: {type_counts.get('TABLE', 0):,}")
|
||||
lines.append(f"- VIEW: {type_counts.get('VIEW', 0):,}")
|
||||
lines.append(
|
||||
"- 來源 (去重後物件數): "
|
||||
f"DIRECT {source_counts.get('DIRECT', 0):,}, "
|
||||
f"PUBLIC {source_counts.get('PUBLIC', 0):,}, "
|
||||
f"ROLE {source_counts.get('ROLE', 0):,}, "
|
||||
f"SYSTEM {source_counts.get('SYSTEM', 0):,}"
|
||||
)
|
||||
lines.append("")
|
||||
lines.append("## 物件清單")
|
||||
lines.append("")
|
||||
lines.append("| 物件 | 類型 | 權限 | 授權來源 |")
|
||||
lines.append("|------|------|------|----------|")
|
||||
|
||||
for name, otype in sorted(info.keys()):
|
||||
data = info[(name, otype)]
|
||||
obj = f"{owner}.{name}"
|
||||
privs = ", ".join(sorted(data["privs"]))
|
||||
sources = ", ".join(
|
||||
sorted(
|
||||
"ROLE" if s not in ("DIRECT", "PUBLIC", "SYSTEM") else s
|
||||
for s in data["sources"]
|
||||
)
|
||||
)
|
||||
lines.append(f"| `{obj}` | {otype} | {privs} | {sources} |")
|
||||
|
||||
output_path.write_text("\n".join(lines), encoding="utf-8")
|
||||
|
||||
cur.close()
|
||||
conn.close()
|
||||
print(f"Wrote {output_path} ({len(info)} objects)")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user