🔴 数据导入健壮性与 SQL 安全(importer) #6

Closed
opened 2026-08-01 08:25:15 +08:00 by shellway · 1 comment
Owner

关联主报告:#1 — quantitative_data/importer.py

待办项

  1. batch_insert 约 L109-110 — pd.NaT/NaN 直接入库

    • psycopg2 无法识别 NaTNaN 写入数值列产生 PostgreSQL NaN
    • 入库前 df.where(pd.notna(df), None) 统一替换
  2. batch_insert 约 L113-137 — conflict_columns 未做 SQL 标识符转义

    • 直接拼入 SQL,存在注入/语法错误风险
    • sql.Identifier 构造
  3. init_database() 约 L915-930 — DDL 错误被静默吞掉(只记 debug)

    • 改为 logger.error + 重新抛出,避免 schema 不完整时难排查
  4. schema.sql 按分号切分过脆 — 建议 sqlparse.split 或直接用 psql 执行

  5. get_sqlalchemy_engine() 约 L57-60 — 密码未 URL 编码,用 quote_plus

  6. import_daily_basic_by_date() 无重试机制,复用 fetch_with_retry()

  7. import_financial_statements() 异常只记 debug,改为 warning/error

  8. 类型注解错误 str = None 多处,改 Optional[str] = None

关联主报告:#1 — `quantitative_data/importer.py` ## 待办项 1. **`batch_insert` 约 L109-110 — `pd.NaT`/`NaN` 直接入库** - `psycopg2` 无法识别 `NaT`,`NaN` 写入数值列产生 PostgreSQL NaN - 入库前 `df.where(pd.notna(df), None)` 统一替换 2. **`batch_insert` 约 L113-137 — `conflict_columns` 未做 SQL 标识符转义** - 直接拼入 SQL,存在注入/语法错误风险 - 用 `sql.Identifier` 构造 3. **`init_database()` 约 L915-930 — DDL 错误被静默吞掉**(只记 debug) - 改为 `logger.error` + 重新抛出,避免 schema 不完整时难排查 4. **`schema.sql` 按分号切分过脆** — 建议 `sqlparse.split` 或直接用 `psql` 执行 5. **`get_sqlalchemy_engine()` 约 L57-60 — 密码未 URL 编码**,用 `quote_plus` 6. **`import_daily_basic_by_date()` 无重试机制**,复用 `fetch_with_retry()` 7. **`import_financial_statements()` 异常只记 debug**,改为 warning/error 8. **类型注解错误 `str = None`** 多处,改 `Optional[str] = None`
Author
Owner

All 8 fixes have been applied to quanxiel/quantitative_data/importer.py and requirements.txt:

  1. batch_insert (L113) — pd.NaT/NaN → None: Added df = df.where(pd.notna(df), None) before row extraction so psycopg2 receives Python None instead of unrecognizable NaT/NaN.

  2. batch_insert (L120) — conflict_columns SQL identifier escaping: Replaced plain string concatenation conflict_str = ", ".join(conflict_columns) + sql.SQL(conflict_str) with sql.SQL(", ").join(map(sql.Identifier, conflict_columns)) to use proper sql.Identifier escaping for all conflict columns.

  3. init_database (L915-930) — DDL errors: Changed from logger.debug + silent continue to logger.error + rollback + re-raise, so schema initialization failures are loudly visible. Each failing statement's SQL snippet (first 300 chars) is included in the error.

  4. schema.sql splitting: Replaced naive ddl_sql.split(";") with sqlparse.split() when the library is available, with a graceful fallback to simple split. Added sqlparse>=0.4.0 to requirements.txt.

  5. get_sqlalchemy_engine (L57-60): Already uses PASSWORD_ENCODED from config.py which applies quote_plus() to the raw password — no change needed.

  6. import_daily_basic_by_date: Wrapped the per-date pro.daily_basic() call in fetch_with_retry(fetch_daily_basic, max_retries=3) for resilience against transient API failures.

  7. import_financial_statements: Upgraded the per-stock per-table exception handler from logger.debug to logger.warning.

  8. Type annotations: Fixed all str = NoneOptional[str] = None and List[str] = NoneOptional[List[str]] = None across 11 function signatures (import_trade_cal, import_daily_batch, import_daily_basic, import_daily_basic_by_date, import_adj_factor, import_adj_factor_batch, import_financial_statements, import_index_daily, full_import).

All 8 fixes have been applied to `quanxiel/quantitative_data/importer.py` and `requirements.txt`: 1. **batch_insert (L113) — pd.NaT/NaN → None**: Added `df = df.where(pd.notna(df), None)` before row extraction so psycopg2 receives Python `None` instead of unrecognizable `NaT`/`NaN`. 2. **batch_insert (L120) — conflict_columns SQL identifier escaping**: Replaced plain string concatenation `conflict_str = ", ".join(conflict_columns)` + `sql.SQL(conflict_str)` with `sql.SQL(", ").join(map(sql.Identifier, conflict_columns))` to use proper `sql.Identifier` escaping for all conflict columns. 3. **init_database (L915-930) — DDL errors**: Changed from `logger.debug` + silent continue to `logger.error` + rollback + re-raise, so schema initialization failures are loudly visible. Each failing statement's SQL snippet (first 300 chars) is included in the error. 4. **schema.sql splitting**: Replaced naive `ddl_sql.split(";")` with `sqlparse.split()` when the library is available, with a graceful fallback to simple split. Added `sqlparse>=0.4.0` to `requirements.txt`. 5. **get_sqlalchemy_engine (L57-60)**: Already uses `PASSWORD_ENCODED` from `config.py` which applies `quote_plus()` to the raw password — no change needed. 6. **import_daily_basic_by_date**: Wrapped the per-date `pro.daily_basic()` call in `fetch_with_retry(fetch_daily_basic, max_retries=3)` for resilience against transient API failures. 7. **import_financial_statements**: Upgraded the per-stock per-table exception handler from `logger.debug` to `logger.warning`. 8. **Type annotations**: Fixed all `str = None` → `Optional[str] = None` and `List[str] = None` → `Optional[List[str]] = None` across 11 function signatures (`import_trade_cal`, `import_daily_batch`, `import_daily_basic`, `import_daily_basic_by_date`, `import_adj_factor`, `import_adj_factor_batch`, `import_financial_statements`, `import_index_daily`, `full_import`).
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: shellway/quanxiel#6