Просмотр исходного кода

feat(ods): 加 ods DDL 生成器 + 8 表 DDL(ADR-06 ods 部分 ✅)

- conf/pg-to-hive-type.ini:类型映射定稿(int 系→BIGINT、numeric→
  DECIMAL(20,4)、timestamp→TIMESTAMP、其他无争议;double precision
  补漏映射 DOUBLE)
- bin/hive-ddl-gen.py 加 -l ods 分支:5 个新函数 normalize_pg_type
  / load_type_mapping / map_pg_to_hive / reverse_ods_table_name /
  render_ods_ddl;ods DDL 末尾加 is_deleted BOOLEAN;不加 etl_time
  / src_sys / src_tbl 三件套(反悔 ADR-06 草案)
- 跑生成器出 8 张 ods DDL 落 manual/ddl/ods/{trd,usr,shp,prd}/
- tests/unit/datax/test_hive_ddl_gen.py 扩展 17 个 ods 测试(33 全过)
- kb/93 ADR-06 草案→已采纳;kb/30 加 §4.4 ods 整节 + 节号重排
  4.4/4.5/4.6 → 4.5/4.6/4.7;kb/92 加 changelog

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
tianyu.chu 7 часов назад
Родитель
Сommit
d330396598

+ 140 - 18
bin/hive-ddl-gen.py

@@ -1,26 +1,31 @@
 #!/usr/bin/env /usr/bin/python3
 # -*- coding:utf-8 -*-
 """
-Hive DDL 生成器(raw 层;ods 层占位待实施)。
+Hive DDL 生成器(raw / ods 双层)。
 
 **仅支持 PG 源**:reader.dataSource 必须是 `postgresql/{env}-{instance}`
 形式;mysql 等其他源由复用的 datax-sync-template-gen.resolve_datasource
 直接 NotImplementedError。
 
-输入 sync ini,从 PG 抽字段中文注释,按 reader.column 顺序渲染
-全字段 STRING + dt STRING 分区 + ORC + EXTERNAL TABLE,写到 stdout
-(传 -o 时额外落盘 {table_name}_create.sql)。
+输入 sync ini,从 PG 抽字段类型 + 中文注释:
+  - raw 层:按 reader.column 顺序渲染全字段 STRING + dt STRING 分区 + ORC + EXTERNAL
+  - ods 层:按 reader.column 顺序应用 conf/pg-to-hive-type.ini 类型映射,
+    末尾加 is_deleted BOOLEAN 软删归一字段,dt STRING 分区 + ORC + EXTERNAL;
+    不加 etl_time / src_sys / src_tbl 技术字段(详见 ADR-06)
+
+写到 stdout(传 -o 时额外落盘 {table_name}_create.sql)。
 
 CLI:
-  python3 bin/hive-ddl-gen.py -l raw -ini jobs/raw/{域}/{table}.ini [-o [DIR]]
+  python3 bin/hive-ddl-gen.py -l {raw|ods} -ini jobs/raw/{域}/{table}.ini [-o [DIR]]
 
 参数:
-  -l    层级(raw 必填;ods 暂未实施,传入直接 NotImplementedError
+  -l    层级(raw / ods,必填
   -ini  sync ini 路径(按项目根解析相对路径,与项目其他 bin 入口一致)
   -o    输出目录(任意三态 stdout 始终打印;不传仅 stdout;不带值额外落盘
         workspace/{yyyymmdd}/;带值额外落盘 <DIR>/)
 
-表名由 writer.path 末两段反推(path 末段必须是 dt=... 占位)。
+表名由 writer.path 末两段反推(path 末段必须是 dt=... 占位);
+ods 表名 = raw 表名首段 'raw_' 替换为 'ods_'。
 """
 import argparse
 import importlib.util
@@ -109,6 +114,19 @@ def fetch_column_comments(ds_ref, schema, table):
 
     复用 sync-template-gen 的 datasource 解析与 pg_catalog 查询,不另起一套。
     """
+    rows = _fetch_pg_column_rows(ds_ref, schema, table)
+    return {name: (comment or '') for _, name, comment, _, _ in rows}
+
+
+def fetch_column_full_rows(ds_ref, schema, table):
+    """连 PG 拿 schema.table 的全字段 [(attnum, attname, comment, pg_type, pk_flag), ...]。
+
+    ods 渲染需要 pg_type,raw 只用 comment。本函数返回原始 rows 给 ods 用。
+    """
+    return _fetch_pg_column_rows(ds_ref, schema, table)
+
+
+def _fetch_pg_column_rows(ds_ref, schema, table):
     ds = SYNC_GEN.resolve_datasource(ds_ref)
     ds_dict = ds.parse()
     jdbc_url = ds_dict[SYNC_GEN.DS_POSTGRE_SQL_JDBC_URL]
@@ -122,10 +140,56 @@ def fetch_column_comments(ds_ref, schema, table):
         user=user, password=password,
     )
     try:
-        rows = SYNC_GEN.query_columns_full(conn, schema, table)
+        return SYNC_GEN.query_columns_full(conn, schema, table)
     finally:
         conn.close()
-    return {name: (comment or '') for _, name, comment, _, _ in rows}
+
+
+def normalize_pg_type(pg_type):
+    """PG type → 映射 conf 查询用的 normalized key。
+
+    规则:
+      - 小写 + 去首尾空格
+      - 去括号参数:'numeric(12,2)' → 'numeric','character varying(64)' → 'character varying'
+      - 去时区后缀:'timestamp(6) without time zone' → 'timestamp'
+    """
+    t = pg_type.lower().strip()
+    if '(' in t and ')' in t:
+        before = t[:t.index('(')].strip()
+        after = t[t.index(')') + 1:].strip()
+        t = (before + ' ' + after).strip()
+    for suffix in ('without time zone', 'with time zone'):
+        if t.endswith(suffix):
+            t = t[:-len(suffix)].strip()
+    return t
+
+
+def load_type_mapping(conf_path):
+    """读 conf/pg-to-hive-type.ini 的 [mapping] 段,返回 {normalized_pg_type: hive_type}。"""
+    if not os.path.isfile(conf_path):
+        raise FileNotFoundError('类型映射 conf 不存在: ' + conf_path)
+    cp = ConfigParser()
+    cp.read(conf_path, encoding='utf-8')
+    if not cp.has_section('mapping'):
+        raise KeyError('类型映射 conf 缺 [mapping] 段: ' + conf_path)
+    return dict(cp.items('mapping'))
+
+
+def map_pg_to_hive(pg_type, type_mapping):
+    """PG 字段类型映射到 Hive 类型;未命中报错让人显式补规则。"""
+    key = normalize_pg_type(pg_type)
+    if key not in type_mapping:
+        raise KeyError(
+            "PG 类型 '{}'(normalized '{}')不在 conf/pg-to-hive-type.ini 映射表,"
+            "需显式补规则".format(pg_type, key))
+    return type_mapping[key]
+
+
+def reverse_ods_table_name(raw_table_name):
+    """raw_xxx → ods_xxx;首段必须是 'raw_'。"""
+    if not raw_table_name.startswith('raw_'):
+        raise ValueError("raw 表名首段必须是 'raw_': " + raw_table_name)
+    return 'ods_' + raw_table_name[len('raw_'):]
 
 
 def render_raw_ddl(table_name, columns, comment_dict):
@@ -166,14 +230,66 @@ def render_raw_ddl(table_name, columns, comment_dict):
     return '\n'.join(lines)
 
 
+def render_ods_ddl(raw_table_name, columns, full_rows, type_mapping):
+    """渲染 ods 层 DDL:typed 字段 + is_deleted 归一 + dt 分区 + ORC + EXTERNAL。
+
+    full_rows: [(attnum, attname, comment, pg_type, pk_flag), ...] 来自 query_columns_full
+    columns: sync ini reader.column 裁剪后字段列表(与 full_rows 字段名子集对齐)
+    type_mapping: load_type_mapping 返回的 {normalized_pg_type: hive_type}
+
+    字段顺序按 columns(不依赖 full_rows attnum);缺类型 / 缺注释报错。
+    末尾加 is_deleted BOOLEAN 软删归一字段(注释固定)。
+    """
+    today = datetime.now().strftime('%Y-%m-%d')
+    ods_table_name = reverse_ods_table_name(raw_table_name)
+    width = max(len(c) for c in columns + ['is_deleted']) + 4
+
+    by_name = {r[1]: (r[3], r[2] or '') for r in full_rows}
+    missing = [c for c in columns if c not in by_name]
+    if missing:
+        raise KeyError('reader.column 中字段 PG 元数据缺失: ' + ','.join(missing))
+
+    lines = [
+        '-- 作者:<TODO>',
+        '-- 日期:' + today,
+        '-- 工单:<TODO>',
+        '-- 目的:<TODO>',
+        '-- 状态:[待执行]',
+        '-- 备注:<TODO>',
+        '',
+        'DROP TABLE IF EXISTS ods.' + ods_table_name + ';',
+        '',
+        'CREATE EXTERNAL TABLE IF NOT EXISTS ods.' + ods_table_name + ' (',
+    ]
+    type_width = max(len(map_pg_to_hive(by_name[c][0], type_mapping)) for c in columns)
+    type_width = max(type_width, len('BOOLEAN')) + 2
+    for col in columns:
+        pg_type, comment = by_name[col]
+        hive_type = map_pg_to_hive(pg_type, type_mapping)
+        comment = comment.replace("'", "''")
+        lines.append("    {col:<{w}}{ht:<{tw}}COMMENT '{c}',".format(
+            col=col, w=width, ht=hive_type, tw=type_width, c=comment))
+    lines.append("    {col:<{w}}{ht:<{tw}}COMMENT '软删除归一(CASE WHEN del_* THEN TRUE)'".format(
+        col='is_deleted', w=width, ht='BOOLEAN', tw=type_width))
+    lines.extend([
+        ')',
+        "COMMENT '<TODO>'",
+        'PARTITIONED BY (dt STRING)',
+        'STORED AS ORC',
+        "LOCATION '/user/hive/warehouse/ods.db/" + ods_table_name + "';",
+        '',
+    ])
+    return '\n'.join(lines)
+
+
 def main():
     parser = argparse.ArgumentParser(
         prog='hive-ddl-gen',
-        description='Hive DDL 生成器(raw 层;ods 层占位待实施)',
+        description='Hive DDL 生成器(raw / ods 双层)',
     )
     parser.add_argument('-l', required=True, choices=['raw', 'ods'],
                         metavar='LAYER',
-                        help='层级(raw 必填;ods 暂未实施直接报错)')
+                        help='层级(raw / ods,必填)')
     parser.add_argument('-ini', required=True, metavar='PATH',
                         help='sync ini 路径(按项目根解析相对路径)')
     parser.add_argument('-o', nargs='?', const=WORKSPACE_DEFAULT, default=None,
@@ -181,22 +297,28 @@ def main():
                         help='输出目录(任意三态 stdout 始终打印;不传仅 stdout;不带值额外落盘 workspace/{yyyymmdd}/;带值额外落盘 <DIR>/)')
     args = parser.parse_args()
 
-    if args.l == 'ods':
-        raise NotImplementedError('ods 层 DDL 生成暂未实施(ADR-06)')
-
     ini_path = _resolve_to_project_root(args.ini)
     spec = parse_sync_ini(ini_path)
     table_name = reverse_table_name(spec['writer_path'])
-    comment_dict = fetch_column_comments(
-        spec['ds_ref'], spec['schema'], spec['table'])
 
-    ddl = render_raw_ddl(table_name, spec['columns'], comment_dict)
+    if args.l == 'raw':
+        comment_dict = fetch_column_comments(
+            spec['ds_ref'], spec['schema'], spec['table'])
+        ddl = render_raw_ddl(table_name, spec['columns'], comment_dict)
+        out_table_name = table_name
+    else:
+        full_rows = fetch_column_full_rows(
+            spec['ds_ref'], spec['schema'], spec['table'])
+        type_mapping = load_type_mapping(
+            os.path.join(project_root, 'conf', 'pg-to-hive-type.ini'))
+        ddl = render_ods_ddl(table_name, spec['columns'], full_rows, type_mapping)
+        out_table_name = reverse_ods_table_name(table_name)
 
     sys.stdout.write(ddl)
 
     if args.o is not None:
         os.makedirs(args.o, exist_ok=True)
-        out_path = os.path.join(args.o, table_name + '_create.sql')
+        out_path = os.path.join(args.o, out_table_name + '_create.sql')
         with open(out_path, 'w', encoding='utf-8') as f:
             f.write(ddl)
         print('已写入: ' + out_path, file=sys.stderr)

+ 55 - 0
conf/pg-to-hive-type.ini

@@ -0,0 +1,55 @@
+; PG 类型 → Hive 类型映射,给 bin/hive-ddl-gen.py -l ods 用。
+;
+; 规则:
+;   1. PG 类型先 normalize:去括号参数(numeric(12,2) → numeric)+ 去时区后缀
+;      (timestamp(6) without time zone → timestamp)
+;   2. normalize 后查本表,命中即用映射值;未命中报错让人显式补规则
+;   3. ConfigParser 接受带空格的 key(如 'character varying')
+;
+; 设计选择(详见 kb/93 ADR-06):
+;   - int 系(smallint/integer/bigint)统一 BIGINT:吸收 PG 字段类型升级,下游聚合无 INT 溢出风险
+;   - numeric/decimal 统一 DECIMAL(20,4):所有金额同精度,下游 SQL JOIN 无类型不一致;极端高精度场景本项目无
+;   - timestamp 走 Hive 原生 TIMESTAMP:日期函数原生用,本项目集群单一时区无迁移风险
+;   - text / varchar / json / jsonb → STRING:Hive 无原生 JSON
+;   - bytea → BINARY:本项目暂无 bytea 字段,规则保留备用
+
+[mapping]
+; 整数类
+smallint = BIGINT
+integer = BIGINT
+int = BIGINT
+int2 = BIGINT
+int4 = BIGINT
+int8 = BIGINT
+bigint = BIGINT
+
+; 数值类
+numeric = DECIMAL(20,4)
+decimal = DECIMAL(20,4)
+
+; 文本类
+text = STRING
+varchar = STRING
+character varying = STRING
+character = STRING
+char = STRING
+
+; 时间类
+timestamp = TIMESTAMP
+date = DATE
+
+; 布尔
+boolean = BOOLEAN
+bool = BOOLEAN
+
+; JSON(Hive 无原生类型,存原字符串,下游 get_json_object 解析)
+json = STRING
+jsonb = STRING
+
+; 浮点类(精度敏感字段必须用 numeric→DECIMAL,浮点仅用于评分 / 经纬度等近似值)
+real = FLOAT
+double precision = DOUBLE
+float = FLOAT
+
+; 二进制
+bytea = BINARY

+ 25 - 4
kb/30-开发规范.md

@@ -424,7 +424,28 @@ raw 层的 `jobs/` 有两类主要任务,根据源数据形态选择:
 - `USING csv OPTIONS(...)` 本身就是 Spark 的声明式 CSV 读取语法,YAML 再封装一层是多余的
 - 与其他分层文件类型一致(除 raw DataX ini 外,其他都是 `.sql`),读者不需要切换上下文
 
-### 4.4 ads 层(SQL + 导出 ini 并存)
+### 4.4 ods 层(类型转换 + 软删归一)
+
+ods 与 raw 一对一对应(表名 `ods_<域>_<源表名>_<快照类型>_<周期>`,详见 kb/21 §3.2),承担类型转换 / 软删归一 / 跨日漂移修正职责,下游 dwd / dim / dws 全部从 ods 取数。
+
+**类型映射**:`conf/pg-to-hive-type.ini` 集中维护(详见 kb/93 ADR-06),核心规则:int 系统一 BIGINT、numeric → DECIMAL(20,4)、timestamp → Hive TIMESTAMP、text/varchar/json → STRING;未命中类型报错显式补 conf。
+
+**软删归一**:ods DDL 末尾加 `is_deleted BOOLEAN` 字段,原 `del_flg` / `del_time` / `del_flag` 保留;CASE WHEN 转换在 ods 同步 SQL 里实现。
+
+**不加技术字段**:不加 `etl_time` / `src_sys` / `src_tbl` 三件套(详见 ADR-06 反悔记录);多源 UNION 时在 SELECT 加 literal 字段即可。
+
+**分区 / 写入模式**(ADR-03):动态分区 `PARTITION (dt)`,按每行 `update_time` 真实日期归位;`INSERT OVERWRITE` + 双源 union(raw dt=T-1 + raw dt=T-2)+ filter `WHERE DATE(update_time)=T-1` + dedupe `(pk, max(update_time))`;跨 dt 不去重(同 pk 多 dt 并存 = 拉链表底层)。
+
+**调度依赖**:8 张 ods 工作流分别 1 对 1 依赖前置 raw 工作流(DS DEPENDENT 任务),不靠 cron 时间。
+
+**接入流程(4 步)**:
+
+1. **跑 DDL 生成器**:`python3 bin/hive-ddl-gen.py -l ods -ini jobs/raw/<域>/<table>.ini -o manual/ddl/ods/<域>/`
+2. **手工微调 DDL**:补作者 / 工单 / 目的 / 备注 / 表 COMMENT;其他字段类型 / is_deleted / 分区 / 路径生成器已就位
+3. **写 ods 同步 SQL**:落 `jobs/ods/<域>/<table>_inc_d.sql`,单文件按 §4.2 简单表风格,含双源 union + 动态分区 + dedupe + is_deleted CASE WHEN
+4. **DS 工作流**:1 对 1 依赖前置 raw 工作流,task script `python3 bin/spark-sql-starter.py -f <sql> -dt ${dt} -p tdt=${tdt}`
+
+### 4.5 ads 层(SQL + 导出 ini 并存)
 
 ```
 manual/ddl/
@@ -437,7 +458,7 @@ jobs/ads/trd/
 
 **命名规则**:导出 ini 文件名 = `{ads 表名}_export.ini`,便于一眼对应。
 
-### 4.5 文件命名速查
+### 4.6 文件命名速查
 
 | 目录 | 文件后缀 | 文件名规则 | 说明 |
 |---|---|---|---|
@@ -450,7 +471,7 @@ jobs/ads/trd/
 | `manual/imports/{yyyymmdd}/` | `.ini` / `.sql` | `{任务描述}.ini` 或 `.sql` | 一次性入仓任务(离线硬盘、历史 dump、外部 CSV 等),按执行日期归档 |
 | `manual/exports/{yyyymmdd}/` | `.ini` | `{任务描述}.ini` | 一次性出仓任务,按执行日期归档 |
 
-### 4.6 表结构变更流程(migration 模式)
+### 4.7 表结构变更流程(migration 模式)
 
 当要给某张表加列 / 改字段时,**只写新文件,不改老文件**:
 
@@ -477,7 +498,7 @@ jobs/ads/trd/
 3. **文件头必须声明元信息**:作者、日期、工单号、目的、执行状态(待执行 / 已执行 yyyy-mm-dd)
 4. **`fix/` 和 `backfill/` 强制 Review**:涉及线上数据订正和历史回刷的脚本,合并前至少 1 人 Review
 5. **与 `jobs/` 的边界**:
-   - 表结构变更 → **只在 `manual/ddl/` 写一个新的 ALTER 文件**,**不要回头改 `manual/ddl/{表名}.sql`**(migration 模式,详见 §4.6)。
+   - 表结构变更 → **只在 `manual/ddl/` 写一个新的 ALTER 文件**,**不要回头改 `manual/ddl/{表名}.sql`**(migration 模式,详见 §4.7)。
    - 历史数据回刷 → 优先复用 `jobs/` 原 SQL + 日期参数,`manual/backfill/` 只放调用包装
    - 临时取数给业务方 → `manual/adhoc/`
    - 脏数据订正 → `manual/fix/`,必须附 TPAD 工单号

+ 1 - 0
kb/92-重构进度.md

@@ -200,3 +200,4 @@
 | 2026-04-28 | **kb/20 §5.5 整节改写 + §7 并入 inbox 分层分区语义 + §21 + §7.1 配套修正**:(a) §5.5 老"合 vs 拆"二分(含 acc 拼团/订单举例)整节重写为"事件 vs 状态"框架——DWD 默认 `_apd_d` 事件,实体状态进 DIM `_zip_d` 拉链,循环状态机天然支持;末尾留"何时可考虑 acc"小节给固定线性里程碑场景留口子(commits c9b58ba + e3761c2)。(b) `kb/inbox/关于分层的分区语义.md` 整篇并入 §7 新增 §7.2 各层分区语义、§7.3 各层职责与设计要点、§7.4 6 条分区与建模设计原则;inbox 原文保留不删(commit e575250)。(c) 配套 cascading 修正:`kb/21 §2.2` acc 表行加默认 event+zip 标注 + 链 §5.5、zip vs acc 选择依据保留思路并加"暂时默认 zip 路线"标注、删"状态机用 acc"一句、循环 fallback 从 "inc 主表 + apd 流水" 改齐为 "apd 事件 + DIM zip";`kb/21 §3.4` dwd 快照类型表删 "里程碑→acc" 行、加 "严格不循环固定线性里程碑→acc" + 链 §5.5,示例删 `dwd_trd_group_buy_acc_d`、`dwd_trd_order_pay_inc_d` 改 `_apd_d`、加订单履约事件示例;`kb/20 §7.1` 拼团 DWD 快照策略改为 "事件 _apd_d + DIM _zip_d 组合"。**未在本批**:CLAUDE.md 不加 acc 禁用锚定(acc 看情况评估,非禁用) | — |
 | 2026-04-29 | **`datax-gc-generator` 从零重写为 `datax-sync-template-gen`(kb/90 §2.7 完成)**:(a) 老 798 行 `bin/datax-gc-generator.py` 整文件删(mysql 链路 + 6 种 from × to 组合,全项目零外部引用);(b) 新 `bin/datax-sync-template-gen.py`(200+ 行)—— 仅 PG → HDFS,按 sync ini `dataSource` 字段格式 `postgresql/{env}-{instance}` 走 `../datasource/{ref}.ini`,复用 `DataSourceFactory` + `PostgreSQLDataSource` 拿 jdbc/user/pwd,pg8000 直连查 `pg_catalog.pg_attribute` 拿全字段 + 注释,查 `pg_index` 单字段 PK,渲染 sync ini 模板(全字段 column / where 用 update_time / writer.path 用源表名 `_TODO_d` 占位 / `-o` nargs='?' 三态:不传 stdout / 不带值 `workspace/{yyyymmdd}/` / 带值自定义);(c) `requirements.txt` 加 `pg8000~=1.30`;(d) 单测 `tests/unit/datax/test_sync_template_gen.py` 9 条覆盖渲染 + JDBC 解析 + PK auto-detect 边界(mock conn);(e) 配套死代码清理(kb/90 §2.7 拆除清单 scope 收紧):`dw_base/datax/datax_utils.py` + `dw_base/common/template_constants.py` 两整文件删(零外部引用),`mysql_reader.py` 删 `generate_hive_ddl` + `generate_hive_over_hbase_ddl` 两方法 + 顶部 `template_constants` import;**保留**:`mysql_data_source.py` / `mysql_reader.py` 整文件 / `mysql_writer.py` / `data_source_factory.py` mysql 分支 / `plugin_factory.py` mysql 分支 / `MYSQL_KEYWORDS` / `generate_definition`(mysql 运行时同步代码,与 PG 同等地位,未来 mysql 同步可能用,scope 收紧 vs 老拆除清单) | — |
 | 2026-05-05 | **kb/01 §5 集群层外部 jar 节新建**:登记 `elasticsearch-hadoop-7.17.25.jar`(落点 `/opt/cloudera/parcels/CDH/lib/spark/jars/`,所有节点,运维分发,不入 git),ES 集群版本 7.17.25 | — |
+| 2026-05-07 | **ods 启动(ADR-06 ods 部分 ✅ + kb/30 §4.4 新增)**:(a) ADR-06 状态草案 → 已采纳,标题日期补 ods ✅ 2026-05-06;ods 层定稿条款写入:类型映射(int 系→BIGINT、numeric→DECIMAL(20,4)、timestamp→TIMESTAMP、其他无争议)、is_deleted BOOLEAN 软删归一、不加技术字段(反悔早先 etl_time/src_sys/src_tbl 三件套);反悔条件加 2 条(跨时区集群迁移、numeric 高精度场景)。(b) 新建 `conf/pg-to-hive-type.ini` 类型映射 conf。(c) `bin/hive-ddl-gen.py` 加 -l ods 分支:normalize_pg_type / load_type_mapping / map_pg_to_hive / reverse_ods_table_name / render_ods_ddl 5 函数;ods DDL 末尾加 is_deleted;不加技术字段;LOCATION /user/hive/warehouse/ods.db/。(d) 单测 `tests/unit/datax/test_hive_ddl_gen.py` 扩展 17 个 ods 测试,33 全过。(e) 跑生成器出 8 张 ods DDL 落 `manual/ddl/ods/{trd,usr,shp,prd}/`,作者统一 tianyu.chu 头其他 TODO 字段保留。(f) kb/30 新增 §4.4 ods 层(类型转换 + 软删归一)整节,节号重排 4.4 ads → 4.5、4.5 文件命名速查 → 4.6、4.6 表结构变更 → 4.7,line 480 内部引用 §4.6 → §4.7 同步 | — |

+ 17 - 10
kb/93-架构决策.md

@@ -193,9 +193,9 @@
   - DIM 拉链表实现复杂度成为团队瓶颈(开发提效需要)
   - 实际场景中跨表 JOIN 性能 / 维护成本超出预期
 
-### ADR-06 DDL 生成器 raw + ods 双层方案(raw ✅ 2026-04-29,ods 待实施
+### ADR-06 DDL 生成器 raw + ods 双层方案(raw ✅ 2026-04-29,ods ✅ 2026-05-06
 
-- **状态**:草案
+- **状态**:已采纳
 
   raw / ods 表 DDL 长期手写(参考 sync ini column 列表 + 类型映射)易错且重复——80+ 字段表抄一遍 PG schema 已经痛苦,未来 50 张业务表 × 2 层(raw + ods)无法维护。需要工具自动出 DDL,开发者只做表注释 / LOCATION / 字段注释微调。
 
@@ -204,18 +204,23 @@
   - 新建 `bin/hive-ddl-gen.py` 一脚本两层产出(独立于 `datax-sync-template-gen.py`,单脚本一职责)
   - 命令 `python3 bin/hive-ddl-gen.py -l {raw|ods} -ini <sync.ini path> [-o [DIR]]`
   - **必须显式 `-l` 指定层级**——避免 raw / ods 误生成;不做"同时生成两层"
-  - **raw 层(本轮范围)**:
+  - **raw 层(2026-04-29 落地)**:
     - 输入:裁剪后 sync.ini(reader.column 已是入仓字段集)
     - 复用 `datax-sync-template-gen.py` 的 `resolve_datasource` + `query_columns_full` 拿字段中文注释 + PG 类型
     - 输出:全字段 STRING + dt STRING 分区 + ORC + LOCATION 从 ini writer.path 推
     - 头注释自己生成 placeholder(作者 / 工单 / 状态 / 配套 ini 路径),不解析 ini 头
-  - **ods 层(暂未实施)**:
-    - 输入:sync.ini + 类型映射 conf
-    - 类型映射 conf 落 `conf/pg-to-hive-type.ini`,PG type → Hive type 规则(参考 `kb/20 §8.4.1`)
-    - 输出:typed 字段 + dt STRING 分区 + 技术字段(`etl_time` / `src_sys` / `src_tbl`)+ ORC
-    - `numeric(p,s)` 是否保留原精度还是统一 `DECIMAL(20,4)`:实施时拍板
-    - mask conf 中字段类型可能因脱敏方法变化(如 `month_trunc` 时间 → STRING),实施时考虑
-  - `-l ods` 在 ods 实施前报"未实现"
+  - **ods 层(2026-05-06 落地)**:
+    - 输入:sync.ini + 类型映射 conf(`conf/pg-to-hive-type.ini`)
+    - 类型映射定稿条款:
+      - **int 系(smallint / integer / bigint)统一 BIGINT**:吸收 PG 字段类型升级;下游聚合无 INT 21 亿溢出风险;ORC 列存 RLE/delta encoding 实际占用看值范围不看声明类型,存储差异 < 5%
+      - **numeric / decimal 统一 DECIMAL(20,4)**:所有金额同精度避免下游 JOIN/聚合 CAST;本项目无极端高精度(>20,4)字段
+      - **timestamp → TIMESTAMP**:Hive 原生类型,日期函数原生用;本项目集群单一时区无迁移风险(跨时区集群迁移有 ±8h 风险,迁移时再改)
+      - text / varchar / character / json / jsonb → STRING;boolean → BOOLEAN;date → DATE;real / float → FLOAT;double precision → DOUBLE;bytea → BINARY
+      - PG 类型 normalize:去括号参数(`numeric(12,2)` → `numeric`)+ 去时区后缀(`timestamp(6) without time zone` → `timestamp`)后查表
+      - 未命中规则报错让人显式补,不静默 fallback
+    - 输出:typed 字段 + **末尾加 `is_deleted BOOLEAN` 软删归一字段**(保留原 `del_flg` / `del_time` / `del_flag`,CASE WHEN 转换在 ods SQL 里做)+ dt STRING 分区 + ORC + EXTERNAL TABLE + LOCATION `/user/hive/warehouse/ods.db/{ods 表名}`
+    - **不加技术字段**(反悔早先草案中的 `etl_time` / `src_sys` / `src_tbl` 三件套):单源情况下技术字段是常量浪费存储;多源 UNION 时(如订单表 his_o + inc_d)在 SELECT 加 literal 字段即可,不必建表时固化;追溯需求由 dt 分区 + DS 实例日志 + git history 满足
+    - ods 表名 = raw 表名首段 `raw_` 替换为 `ods_`(kb/21 §3.2 一对一对应)
 
 - **后果**:
 
@@ -231,6 +236,8 @@
 - **反悔条件**:
   - 业务表数稳定 < 5 张永远不再加:手写也能撑,工具失去 ROI
   - 引擎换非 Hive(Doris / ClickHouse),DDL 生成需重写
+  - 跨时区集群迁移:timestamp 映射需改 STRING 避免 ±8h 偏移
+  - 出现 numeric 精度 > (20,4) 的金融场景:DECIMAL 统一精度反悔回保留原精度
 
 ### ADR-07 数据质量配置化方案(mask conf 双消费 + dq 注册表)
 

+ 52 - 0
manual/ddl/ods/prd/ods_prd_checklist_base_info_inc_d_create.sql

@@ -0,0 +1,52 @@
+-- 作者:tianyu.chu
+-- 日期:2026-05-06
+-- 工单:<TODO>
+-- 目的:<TODO>
+-- 状态:[待执行]
+-- 备注:<TODO>
+
+DROP TABLE IF EXISTS ods.ods_prd_checklist_base_info_inc_d;
+
+CREATE EXTERNAL TABLE IF NOT EXISTS ods.ods_prd_checklist_base_info_inc_d (
+    id                           BIGINT     COMMENT '主键',
+    code                         STRING     COMMENT '编码',
+    year                         STRING     COMMENT '年份(赛季)',
+    sport                        STRING     COMMENT '运动类型',
+    manufacturer                 STRING     COMMENT '厂商(发行商)',
+    sets                         STRING     COMMENT '系列',
+    display_name                 STRING     COMMENT '别名(显示名)',
+    type                         STRING     COMMENT '可组类型',
+    num                          BIGINT     COMMENT 'list数量',
+    use_num                      BIGINT     COMMENT '使用次数',
+    status                       BIGINT     COMMENT '状态',
+    remark                       STRING     COMMENT '描述',
+    create_time                  TIMESTAMP  COMMENT '',
+    create_by                    STRING     COMMENT '',
+    update_time                  TIMESTAMP  COMMENT '',
+    update_by                    STRING     COMMENT '',
+    lot                          BIGINT     COMMENT '是否是lot',
+    del_flg                      BIGINT     COMMENT '是否删除:0=正常,1=删除',
+    act_point_type               STRING     COMMENT '额外积分类型,SP等',
+    parent_id                    BIGINT     COMMENT '父id',
+    sets_version                 STRING     COMMENT '系列版本',
+    merchant_id                  BIGINT     COMMENT '商家id',
+    merchant_name                STRING     COMMENT '商家名',
+    share                        BIGINT     COMMENT '是否分享:0=否,1= 是',
+    share_to_merchant            STRING     COMMENT '指定分享到商家',
+    custom                       BIGINT     COMMENT '是否自定义',
+    panini_config_id             BIGINT     COMMENT '系列版本配置id',
+    import_failure_cause         STRING     COMMENT '导入失败原因',
+    check_failure_cause          STRING     COMMENT '检查失败原因',
+    import_status                BIGINT     COMMENT 'checklist文件状态',
+    review_msg                   STRING     COMMENT '审核理由',
+    sub_type                     STRING     COMMENT '可组类型子类型:选随随机用',
+    title                        STRING     COMMENT '拼团checklist描述:',
+    data_version                 STRING     COMMENT '数据版本',
+    sport_blend                  BIGINT     COMMENT '是否单一运动类型:0否',
+    display_name_translations    STRING     COMMENT '别名-国际版',
+    is_deleted                   BOOLEAN    COMMENT '软删除归一(CASE WHEN del_* THEN TRUE)'
+)
+COMMENT '<TODO>'
+PARTITIONED BY (dt STRING)
+STORED AS ORC
+LOCATION '/user/hive/warehouse/ods.db/ods_prd_checklist_base_info_inc_d';

+ 49 - 0
manual/ddl/ods/prd/ods_prd_panini_checklist_base_info_inc_d_create.sql

@@ -0,0 +1,49 @@
+-- 作者:tianyu.chu
+-- 日期:2026-05-06
+-- 工单:<TODO>
+-- 目的:<TODO>
+-- 状态:[待执行]
+-- 备注:<TODO>
+
+DROP TABLE IF EXISTS ods.ods_prd_panini_checklist_base_info_inc_d;
+
+CREATE EXTERNAL TABLE IF NOT EXISTS ods.ods_prd_panini_checklist_base_info_inc_d (
+    id                     BIGINT         COMMENT '主键',
+    code                   STRING         COMMENT '编码',
+    year                   STRING         COMMENT '年份(赛季)',
+    sport                  STRING         COMMENT '运动类型',
+    manufacturer           STRING         COMMENT '厂商(发行商)',
+    sets                   STRING         COMMENT '系列',
+    sets_version           STRING         COMMENT '版本',
+    display_name           STRING         COMMENT '别名(显示名)',
+    type                   STRING         COMMENT '可组类型',
+    num                    BIGINT         COMMENT 'list数量',
+    use_num                BIGINT         COMMENT '使用次数',
+    status                 BIGINT         COMMENT '状态',
+    remark                 STRING         COMMENT '描述',
+    lot                    BIGINT         COMMENT '是否是lot',
+    del_flg                BIGINT         COMMENT '是否删除:0=正常,1=删除',
+    carmichael_img_type    STRING         COMMENT '卡密图片适用类型:球员,球队,other,all',
+    create_time            TIMESTAMP      COMMENT '',
+    create_by              STRING         COMMENT '',
+    update_time            TIMESTAMP      COMMENT '',
+    update_by              STRING         COMMENT '',
+    import_type            STRING         COMMENT '导入类型',
+    report_flag            BIGINT         COMMENT '是否强制使用新版报告形式,默认0:非,1:强制',
+    base_config            STRING         COMMENT '配置',
+    base_price             DECIMAL(20,4)  COMMENT '指导价: 元/箱',
+    min_reference_price    DECIMAL(20,4)  COMMENT '参考价范围-最小',
+    max_reference_price    DECIMAL(20,4)  COMMENT '参考价范围-最大',
+    prop1                  STRING         COMMENT '',
+    prop2                  STRING         COMMENT '',
+    prop3                  STRING         COMMENT '',
+    prop4                  STRING         COMMENT '',
+    data_version           STRING         COMMENT '数据版本',
+    first_sport            STRING         COMMENT '大分类-品类(运动类型)',
+    sets_display_name      STRING         COMMENT '系列简称',
+    is_deleted             BOOLEAN        COMMENT '软删除归一(CASE WHEN del_* THEN TRUE)'
+)
+COMMENT '<TODO>'
+PARTITIONED BY (dt STRING)
+STORED AS ORC
+LOCATION '/user/hive/warehouse/ods.db/ods_prd_panini_checklist_base_info_inc_d';

+ 49 - 0
manual/ddl/ods/prd/ods_prd_panini_checklist_version_config_inc_d_create.sql

@@ -0,0 +1,49 @@
+-- 作者:tianyu.chu
+-- 日期:2026-05-06
+-- 工单:<TODO>
+-- 目的:<TODO>
+-- 状态:[待执行]
+-- 备注:<TODO>
+
+DROP TABLE IF EXISTS ods.ods_prd_panini_checklist_version_config_inc_d;
+
+CREATE EXTERNAL TABLE IF NOT EXISTS ods.ods_prd_panini_checklist_version_config_inc_d (
+    id                           BIGINT         COMMENT '',
+    panini_list_id               BIGINT         COMMENT '基础库beseid',
+    set_version                  STRING         COMMENT '版本',
+    carmichael_img_type          STRING         COMMENT '卡密图片适用类型:球员,球队,other,all',
+    import_type                  STRING         COMMENT '导入类型',
+    report_flag                  BIGINT         COMMENT '是否强制使用新版报告形式,默认0:非,1:强制',
+    base_config                  STRING         COMMENT '配置',
+    base_price                   DECIMAL(20,4)  COMMENT '指导价: 元/箱',
+    min_reference_price          DECIMAL(20,4)  COMMENT '参考价范围-最小',
+    max_reference_price          DECIMAL(20,4)  COMMENT '参考价范围-最大',
+    num                          BIGINT         COMMENT 'list数量',
+    use_num                      BIGINT         COMMENT '使用次数',
+    status                       BIGINT         COMMENT '状态',
+    remark                       STRING         COMMENT '描述',
+    del_flg                      BIGINT         COMMENT '是否删除:0=正常,1=删除',
+    create_time                  TIMESTAMP      COMMENT '',
+    create_by                    STRING         COMMENT '',
+    update_time                  TIMESTAMP      COMMENT '',
+    update_by                    STRING         COMMENT '',
+    prop1                        STRING         COMMENT '',
+    prop2                        STRING         COMMENT '',
+    prop3                        STRING         COMMENT '',
+    prop4                        STRING         COMMENT '',
+    tag                          STRING         COMMENT '标签,关键字',
+    display_name                 STRING         COMMENT '别名',
+    presale_time                 TIMESTAMP      COMMENT '预售时间开播时间',
+    max_box                      BIGINT         COMMENT '规格限制:最多 盒',
+    league                       STRING         COMMENT '联赛',
+    issuing_time                 TIMESTAMP      COMMENT '发行日期',
+    issuing_price                DECIMAL(20,4)  COMMENT '发行价格:单价元/箱',
+    display_name_translations    STRING         COMMENT '国际版配置 别名',
+    sale_time                    TIMESTAMP      COMMENT '预售上架时间',
+    open_time                    BIGINT         COMMENT '最晚开拆时间',
+    is_deleted                   BOOLEAN        COMMENT '软删除归一(CASE WHEN del_* THEN TRUE)'
+)
+COMMENT '<TODO>'
+PARTITIONED BY (dt STRING)
+STORED AS ORC
+LOCATION '/user/hive/warehouse/ods.db/ods_prd_panini_checklist_version_config_inc_d';

+ 66 - 0
manual/ddl/ods/shp/ods_shp_tzy_merchant_info_inc_d_create.sql

@@ -0,0 +1,66 @@
+-- 作者:tianyu.chu
+-- 日期:2026-05-06
+-- 工单:<TODO>
+-- 目的:<TODO>
+-- 状态:[待执行]
+-- 备注:<TODO>
+
+DROP TABLE IF EXISTS ods.ods_shp_tzy_merchant_info_inc_d;
+
+CREATE EXTERNAL TABLE IF NOT EXISTS ods.ods_shp_tzy_merchant_info_inc_d (
+    id                      BIGINT     COMMENT 'id',
+    appid                   STRING     COMMENT '授权程序',
+    user_id                 BIGINT     COMMENT '后台用户账号id',
+    username                STRING     COMMENT '后台账户(统一登陆账号)',
+    name                    STRING     COMMENT '名称',
+    status                  BIGINT     COMMENT '状态',
+    remark                  STRING     COMMENT '备注',
+    create_by               STRING     COMMENT '创建人',
+    create_time             TIMESTAMP  COMMENT '创建时间',
+    update_by               STRING     COMMENT '更新人',
+    update_time             TIMESTAMP  COMMENT '更细时间',
+    code                    STRING     COMMENT '',
+    fans                    BIGINT     COMMENT '粉丝数量',
+    sale_num                BIGINT     COMMENT '销量',
+    applet_auth             BOOLEAN    COMMENT '是否绑定小程序-',
+    applet_lives_auth       BOOLEAN    COMMENT '是否授权小程序直播',
+    applet_lives_role       STRING     COMMENT '小程序角色',
+    commission_rate         STRING     COMMENT '佣金比例(百分比)',
+    prop_json               STRING     COMMENT '',
+    sort_rate               BIGINT     COMMENT '权重比',
+    check_status            BIGINT     COMMENT '',
+    live_type               STRING     COMMENT '直播类型,默认小程序applet,企业微信:work_wx;融云:rongcloud',
+    living_auth_config      STRING     COMMENT '商家主播配置',
+    goods_sold_num          BIGINT     COMMENT '商城商品售出数量',
+    hot_config              STRING     COMMENT '热度配置',
+    tag_config              STRING     COMMENT '标签配置',
+    mall_role               BIGINT     COMMENT '是否是APP商城用户:0否,1是',
+    living_time             STRING     COMMENT '直播时间范围',
+    express_level           STRING     COMMENT '快递等级:',
+    del_flg                 BIGINT     COMMENT '删除标记:默认0 :1=删除',
+    group_show_name         STRING     COMMENT '商家精美卡片展示名称',
+    main_business           STRING     COMMENT '主营业务:篮球,足球,其他运动(棒球、冰球、橄榄球),tcg,其他收藏',
+    min_card_num            BIGINT     COMMENT '精美卡片最小发货单位',
+    dy_name                 STRING     COMMENT '抖音信息',
+    current_month_score     DOUBLE     COMMENT '当月综合评分(前三个月最高评分)',
+    member_level            BIGINT     COMMENT '商家会员等级',
+    member_name             STRING     COMMENT '商家会员等级名称',
+    member_medal            STRING     COMMENT '商家会员等级勋章',
+    prefer_valid_time       TIMESTAMP  COMMENT '优选商家过期时间',
+    tag_id                  STRING     COMMENT '商家分类标签id:逗号分割',
+    show_status             BIGINT     COMMENT '是否搜索展示,默认0,1:不展示',
+    point_type              STRING     COMMENT '',
+    refund_limit_day        BIGINT     COMMENT '拼豆退款申请次数限制天数,默认14',
+    open_act_discount       BIGINT     COMMENT '是否打开无余额模式,默认0:关闭,打开:1',
+    reputation_score        BIGINT     COMMENT '信誉评分',
+    hide_stock              BIGINT     COMMENT '隐藏库存',
+    version                 BIGINT     COMMENT '',
+    total_sold_num          BIGINT     COMMENT '累计上架数量',
+    shipping_cost_config    STRING     COMMENT '运费配置',
+    merchant_group_id       BIGINT     COMMENT '集团id',
+    is_deleted              BOOLEAN    COMMENT '软删除归一(CASE WHEN del_* THEN TRUE)'
+)
+COMMENT '<TODO>'
+PARTITIONED BY (dt STRING)
+STORED AS ORC
+LOCATION '/user/hive/warehouse/ods.db/ods_shp_tzy_merchant_info_inc_d';

+ 131 - 0
manual/ddl/ods/trd/ods_trd_card_group_info_inc_d_create.sql

@@ -0,0 +1,131 @@
+-- 作者:tianyu.chu
+-- 日期:2026-05-06
+-- 工单:<TODO>
+-- 目的:<TODO>
+-- 状态:[待执行]
+-- 备注:<TODO>
+
+DROP TABLE IF EXISTS ods.ods_trd_card_group_info_inc_d;
+
+CREATE EXTERNAL TABLE IF NOT EXISTS ods.ods_trd_card_group_info_inc_d (
+    id                          BIGINT         COMMENT 'id',
+    merchant_id                 BIGINT         COMMENT '商家id',
+    appid                       STRING         COMMENT '所属程序',
+    name                        STRING         COMMENT '名称',
+    code                        STRING         COMMENT '编码',
+    status                      BIGINT         COMMENT '状态',
+    specs                       STRING         COMMENT '商品规格',
+    type                        STRING         COMMENT '组团方式',
+    random_type                 STRING         COMMENT '随机方式',
+    total_price                 DECIMAL(20,4)  COMMENT '商品总价',
+    copies                      BIGINT         COMMENT '商品份数',
+    unit_price                  DECIMAL(20,4)  COMMENT '单份售价',
+    sold_copies                 BIGINT         COMMENT '售出份数',
+    release_time                STRING         COMMENT '发布时间',
+    cycle                       STRING         COMMENT '销售周期',
+    show_applet                 STRING         COMMENT '是否在小程序显示',
+    title                       STRING         COMMENT '商品子标题',
+    msg                         STRING         COMMENT '详情描述',
+    remark                      STRING         COMMENT '备注',
+    create_time                 TIMESTAMP      COMMENT '创建时间',
+    update_by                   STRING         COMMENT '更新人',
+    update_time                 TIMESTAMP      COMMENT '更新时间',
+    order_quota_min             BIGINT         COMMENT '每笔订单最少购买',
+    order_quota_max             BIGINT         COMMENT '每笔订单最多购买',
+    user_quota_max              BIGINT         COMMENT '用户最多购买',
+    start_time                  TIMESTAMP      COMMENT '计划开售时间',
+    marketing_info              STRING         COMMENT '营销信息',
+    reviewmsg                   STRING         COMMENT '审核描述',
+    lock                        BOOLEAN        COMMENT '锁定状态',
+    commission_rate             STRING         COMMENT '佣金比例(百分比)',
+    year                        STRING         COMMENT '',
+    sport                       STRING         COMMENT '',
+    manufacturer                STRING         COMMENT '',
+    sets                        STRING         COMMENT '',
+    act                         STRING         COMMENT '',
+    config                      STRING         COMMENT '',
+    info_config                 STRING         COMMENT '',
+    total_num                   BIGINT         COMMENT '',
+    banner_end_time             TIMESTAMP      COMMENT '',
+    add_banner                  STRING         COMMENT '',
+    finished_time               TIMESTAMP      COMMENT '结束时间',
+    display_name                STRING         COMMENT '系列别名(显示名)',
+    group_sets_no               BIGINT         COMMENT '同商家,同系列 序号',
+    close_payment_time          TIMESTAMP      COMMENT '打款日期(确认发货日期 后5个工作日)',
+    confirm_send_time           TIMESTAMP      COMMENT '确认发货日期',
+    close_payment_status        BIGINT         COMMENT '结算状态:100=未结束,101=待申请,201=审核中,202=驳回,203=审核通过/待打款,301=打款成功/结束结算,302=部分打款成功,501=已开票',
+    open_card                   BIGINT         COMMENT '开卡动作,0:无开卡动作,1:有',
+    close_payment_record        STRING         COMMENT '结款记录',
+    group_full_time             TIMESTAMP      COMMENT '组齐时间',
+    live_create_time            TIMESTAMP      COMMENT '直播创建时间',
+    live_start_time             TIMESTAMP      COMMENT '直播开播时间',
+    live_end_time               TIMESTAMP      COMMENT '直播结束时间',
+    report_start_time           TIMESTAMP      COMMENT '报告开始时间',
+    report_end_time             TIMESTAMP      COMMENT '报告结束时间',
+    report_review_num           BIGINT         COMMENT '报告审核次数',
+    report_review_first_time    TIMESTAMP      COMMENT '报告第一次审核时间',
+    report_review_end_time      TIMESTAMP      COMMENT '报告最后一次审核时间',
+    review_hold_time            TIMESTAMP      COMMENT '组队提交审核时间',
+    review_approval_time        TIMESTAMP      COMMENT '组队审核通过时间',
+    review_num                  BIGINT         COMMENT '组队审核次数(驳回次数)',
+    config_json                 STRING         COMMENT '',
+    free_flag                   BIGINT         COMMENT '免单标记',
+    mer_name                    STRING         COMMENT '商户名称',
+    change_type                 STRING         COMMENT '买对玩法,改变之后的组队方式',
+    act_price                   DECIMAL(20,4)  COMMENT '私域分享优惠价',
+    act_config_json             STRING         COMMENT '新营销活动配置',
+    real_sold_num               BIGINT         COMMENT '实际销售数量',
+    weight                      BIGINT         COMMENT '重量',
+    hot_type                    STRING         COMMENT '查询type',
+    team_first                  BIGINT         COMMENT '开启包队优先玩法',
+    prop1                       STRING         COMMENT '积分标记,1代表需要统计',
+    prop2                       STRING         COMMENT '搜索别名',
+    prop3                       STRING         COMMENT '精美卡片',
+    point_rate                  BIGINT         COMMENT '积分转换比例,默认1:100',
+    point_max                   BIGINT         COMMENT '积分兑换最大值',
+    point_min                   BIGINT         COMMENT '积分兑换最小值',
+    list_id                     BIGINT         COMMENT 'checklistId',
+    list_code                   STRING         COMMENT 'checklistCode',
+    mix_copies                  BIGINT         COMMENT '组合加倍,默认1',
+    sub_type                    STRING         COMMENT '组队方式子方式',
+    act_point_type              STRING         COMMENT '额外积分类型,SP等',
+    payment_method              BIGINT         COMMENT '打款方式:0 = 线下打款(默认),1=线上打款',
+    payment_total_price         DECIMAL(20,4)  COMMENT '打款总金额',
+    payment_commission          DECIMAL(20,4)  COMMENT '佣金金额',
+    payment_finished_price      DECIMAL(20,4)  COMMENT '已打款金额',
+    payment_remain_price        DECIMAL(20,4)  COMMENT '剩余打款金额',
+    payment_online_price        DECIMAL(20,4)  COMMENT '线上打款金额',
+    exclusive                   BIGINT         COMMENT '是否使用专属支付通道:0否(默认),1是',
+    has_bg                      BIGINT         COMMENT '是否有背景图:0否(默认),1=是',
+    merchant_sort               BIGINT         COMMENT '商家自定义排序',
+    del_flg                     BIGINT         COMMENT '删除标记',
+    del_time                    TIMESTAMP      COMMENT '删除时间',
+    review_account              STRING         COMMENT '审核账号',
+    act_id                      BIGINT         COMMENT '关联活动id',
+    sold_end_time               TIMESTAMP      COMMENT '售卖结束时间',
+    panini_list_id              BIGINT         COMMENT '帕尼尼list表id',
+    hot_type_config             STRING         COMMENT 'hotType 配置',
+    goods_type                  BIGINT         COMMENT '卡密类型,默认0,1:新库',
+    report_flag                 BIGINT         COMMENT '是否强制使用新版报告形式,默认0:非,1:强制',
+    use_coupon                  BIGINT         COMMENT '是否可用优惠劵,默认0:可以,1:不允许',
+    user_level                  BIGINT         COMMENT '用户可看等级',
+    custom                      BIGINT         COMMENT '是否自定义,1=是',
+    gift_card_id                BIGINT         COMMENT '关联精美卡片id',
+    group_show_name             STRING         COMMENT '精美卡片名称',
+    min_card_num                BIGINT         COMMENT '免运费达标数量',
+    act_type                    STRING         COMMENT '活动标签',
+    waring_type                 STRING         COMMENT '风险异常类型',
+    compensation_status         BIGINT         COMMENT '赔付状态:1未补偿、2已补偿',
+    point_type                  STRING         COMMENT '兑换积分类型,搭配point使用',
+    first_act_config            STRING         COMMENT '首次购买活动',
+    gift_config                 STRING         COMMENT '赠礼活动配置',
+    version                     BIGINT         COMMENT '',
+    extra_prop                  STRING         COMMENT '额外配置json信息',
+    use_member_discount         BIGINT         COMMENT '参与会员折扣:默认0不参与 平台会员折扣 1 品牌会员折扣 2',
+    merchant_open               BIGINT         COMMENT '支持商家代开卡密:默认0不支持,1支持',
+    is_deleted                  BOOLEAN        COMMENT '软删除归一(CASE WHEN del_* THEN TRUE)'
+)
+COMMENT '<TODO>'
+PARTITIONED BY (dt STRING)
+STORED AS ORC
+LOCATION '/user/hive/warehouse/ods.db/ods_trd_card_group_info_inc_d';

+ 107 - 0
manual/ddl/ods/trd/ods_trd_card_group_order_info_inc_d_create.sql

@@ -0,0 +1,107 @@
+-- 作者:tianyu.chu
+-- 日期:2026-05-06
+-- 工单:<TODO>
+-- 目的:<TODO>
+-- 状态:[待执行]
+-- 备注:<TODO>
+
+DROP TABLE IF EXISTS ods.ods_trd_card_group_order_info_inc_d;
+
+CREATE EXTERNAL TABLE IF NOT EXISTS ods.ods_trd_card_group_order_info_inc_d (
+    id                        BIGINT         COMMENT 'id',
+    group_info_id             BIGINT         COMMENT '组团信息id',
+    merchant_id               BIGINT         COMMENT '商家id',
+    user_id                   BIGINT         COMMENT '用户id',
+    shipping_address_id       BIGINT         COMMENT '收货地址id',
+    purchase_count            BIGINT         COMMENT '购买数量',
+    order_no                  STRING         COMMENT '订单编码',
+    accounts_payable          DECIMAL(20,4)  COMMENT '应付款',
+    actual_payment            DECIMAL(20,4)  COMMENT '实付款',
+    payment_type              STRING         COMMENT '支付方式-交易类型',
+    payment_time              TIMESTAMP      COMMENT '支付时间',
+    coupon                    BIGINT         COMMENT '优惠券',
+    discount                  DECIMAL(20,4)  COMMENT '折扣',
+    status                    BIGINT         COMMENT '订单状态',
+    remark                    STRING         COMMENT '备注',
+    create_time               TIMESTAMP      COMMENT '创建时间',
+    create_by                 STRING         COMMENT '创建人',
+    update_time               TIMESTAMP      COMMENT '更新时间',
+    update_by                 STRING         COMMENT '更新人',
+    payment_status            STRING         COMMENT '交易状态',
+    payment_status_desc       STRING         COMMENT '交易状态描述',
+    payment_success_time      TIMESTAMP      COMMENT '支付完成时间',
+    del_flg                   BIGINT         COMMENT '删除标记:0=正常,1=删除',
+    curier_company            STRING         COMMENT '快递公司',
+    refund_fee                DECIMAL(20,4)  COMMENT '退款金额',
+    refund_time               TIMESTAMP      COMMENT '退款时间',
+    anonymous                 BOOLEAN        COMMENT '是否匿名',
+    pick_up_type              STRING         COMMENT '提货方式',
+    ship_time                 TIMESTAMP      COMMENT '发货时间',
+    refund_success_time       TIMESTAMP      COMMENT '退款成功时间',
+    refund_recv_accout        STRING         COMMENT '退款入账账户',
+    refund_account            STRING         COMMENT '退款资金来源',
+    refund_request_source     STRING         COMMENT '退款发起来源',
+    card_price                DECIMAL(20,4)  COMMENT '应付款',
+    act_price                 DECIMAL(20,4)  COMMENT '应付款',
+    goods_price_json          STRING         COMMENT '价格json',
+    payment_sub_type          STRING         COMMENT '支付 子分类:杉德支付(微信),宝付支付(微信)等',
+    team_first                STRING         COMMENT '买队优先队伍',
+    refuse_status             BIGINT         COMMENT '是否接受累积发货,0申请,1拒绝,2同意',
+    prop1                     STRING         COMMENT '备用',
+    prop2                     STRING         COMMENT '备用',
+    prop3                     STRING         COMMENT '备用',
+    point                     BIGINT         COMMENT '消耗积分',
+    order_type                STRING         COMMENT '订单类型',
+    trade_amount              DECIMAL(20,4)  COMMENT '订单交易金额',
+    refund_type               STRING         COMMENT '仅退款refund_amount,退货退款refund_goods,换货change',
+    refund_reason             STRING         COMMENT '订单退换原因',
+    evaluation                STRING         COMMENT '订单评价',
+    user_refund_time          TIMESTAMP      COMMENT '退换申请时间',
+    refund_status             BIGINT         COMMENT '1:申请  2同意 3拒绝 ,4退回货物  5商家收到货',
+    merchant_refund_reason    STRING         COMMENT '商家拒绝原因',
+    point_deduct              DECIMAL(20,4)  COMMENT '积分抵扣金额',
+    shipping_cost             DECIMAL(20,4)  COMMENT '运费',
+    merchant_remark           STRING         COMMENT '商家备注',
+    pay_record                BIGINT         COMMENT '是否重复支付:1=是',
+    order_sub_type            STRING         COMMENT '订单子类型,赠与:receive',
+    give_user_code            STRING         COMMENT '赠与人',
+    give_order_id             BIGINT         COMMENT '赠与关联订单id',
+    read_flag                 BIGINT         COMMENT '赠送未读0和1',
+    give_num                  BIGINT         COMMENT '赠送个数',
+    invoice_id                BIGINT         COMMENT '发票记录id',
+    combination_no            STRING         COMMENT '拆分订单关联编号',
+    open_self                 BIGINT         COMMENT '是否用户自己拆卡,默认0,1:商家待拆',
+    refund_desc               STRING         COMMENT '退款原因详细描述',
+    goods_allocate            BIGINT         COMMENT '卡密是否分配,默认0:未分配,1已分配',
+    close_payment_status      BIGINT         COMMENT '打款状态:0=未打款,1=申请中,2=已打款',
+    close_payment_time        TIMESTAMP      COMMENT '打款时间,签收时间后5个工作日',
+    finished_time             TIMESTAMP      COMMENT '订单结束时间',
+    expire_time               TIMESTAMP      COMMENT '过期时间',
+    settlement_amount         DECIMAL(20,4)  COMMENT '结算金额,实付款-退款(预售组队为已开卡总金额)',
+    platform_coupon           BIGINT         COMMENT '平台优惠券id',
+    platform_discount         DECIMAL(20,4)  COMMENT '平台优惠劵折扣',
+    discount_amount           DECIMAL(20,4)  COMMENT '折扣金额',
+    member_discount           DECIMAL(20,4)  COMMENT '会员折扣',
+    shipping_free_id          BIGINT         COMMENT '运费券id',
+    shipping_free_amount      DECIMAL(20,4)  COMMENT '运费券金额',
+    discount_point            BIGINT         COMMENT '折扣积分',
+    un_shipped_num            BIGINT         COMMENT '精美卡片未发货数量',
+    pre_un_shipped_num        BIGINT         COMMENT '拼豆订单提醒用户申请时间',
+    wait_shipped_num          BIGINT         COMMENT '精美卡片等待发货数量',
+    pre_wait_shipped_num      BIGINT         COMMENT '用户支付拼豆订单运费时间',
+    refuse_time               TIMESTAMP      COMMENT '用户同意累计发货时间',
+    refuse_notice             BIGINT         COMMENT '累计发货通知提醒:0未发送 1已发送',
+    pickup_time               TIMESTAMP      COMMENT '揽收时间',
+    waring_type               STRING         COMMENT '风险异常类型',
+    waring_status             BIGINT         COMMENT '风险异常状态:1风险预警、2违规',
+    point_type                STRING         COMMENT '使用积分类型',
+    delivery_end_time         TIMESTAMP      COMMENT '发货截止时间',
+    serve_status              BIGINT         COMMENT '订单业务状态,业务之间,间隔100',
+    self_pickup_time          TIMESTAMP      COMMENT '申请自提时间,24小时内有效',
+    act_discount              DECIMAL(20,4)  COMMENT '平台折扣(拼豆无余额折扣)',
+    is_deleted                BOOLEAN        COMMENT '软删除归一(CASE WHEN del_* THEN TRUE)'
+)
+COMMENT '<TODO>'
+PARTITIONED BY (dt STRING)
+STORED AS ORC
+LOCATION '/user/hive/warehouse/ods.db/ods_trd_card_group_order_info_inc_d';

+ 68 - 0
manual/ddl/ods/usr/ods_usr_app_base_user_inc_d_create.sql

@@ -0,0 +1,68 @@
+-- 作者:tianyu.chu
+-- 日期:2026-05-06
+-- 工单:<TODO>
+-- 目的:<TODO>
+-- 状态:[待执行]
+-- 备注:<TODO>
+
+DROP TABLE IF EXISTS ods.ods_usr_app_base_user_inc_d;
+
+CREATE EXTERNAL TABLE IF NOT EXISTS ods.ods_usr_app_base_user_inc_d (
+    id                      BIGINT         COMMENT 'id',
+    appid                   STRING         COMMENT '所属程序',
+    point                   BIGINT         COMMENT '积分',
+    level                   BIGINT         COMMENT '会员等级',
+    register_channel        STRING         COMMENT '注册渠道',
+    status                  BIGINT         COMMENT '状态',
+    del_flg                 BIGINT         COMMENT '删除标记',
+    remark                  STRING         COMMENT '备注',
+    create_by               STRING         COMMENT '创建人',
+    create_time             TIMESTAMP      COMMENT '创建时间',
+    update_by               STRING         COMMENT '更新人',
+    update_time             TIMESTAMP      COMMENT '更新时间',
+    username                STRING         COMMENT '账号',
+    growth_num              BIGINT         COMMENT '会员成长值',
+    code                    STRING         COMMENT '会员码',
+    notify_flag             BIGINT         COMMENT '推送是否接受',
+    user_id                 BIGINT         COMMENT '',
+    notify_type             STRING         COMMENT '',
+    face_verify             BIGINT         COMMENT '通过人脸识别标志:1',
+    open_psd                BIGINT         COMMENT '支付开关',
+    refuse_pick_up          BIGINT         COMMENT '是否拒绝自提,1拒绝0同意',
+    prop1                   STRING         COMMENT '额外配置',
+    prop2                   STRING         COMMENT '备用',
+    prop3                   STRING         COMMENT '备用',
+    prop4                   STRING         COMMENT '备用',
+    window_open             BIGINT         COMMENT '悬浮窗口开关',
+    open_invoice            BIGINT         COMMENT '开票权限',
+    blacklist               BIGINT         COMMENT '编辑黑名单,1不允许编辑,2:限制云闪付',
+    id_card                 STRING         COMMENT '身份证号',
+    member_level            BIGINT         COMMENT '会员等级',
+    member_name             STRING         COMMENT '会员名称',
+    current_month_growth    BIGINT         COMMENT '当月已获取成长值',
+    member_init_flag        BIGINT         COMMENT '月初初始化标志,默认0:未开始,1:已初始化',
+    member_keep_growth      BIGINT         COMMENT '当前会员保级所需成长值',
+    register_ip_addr        STRING         COMMENT '用户注册ip',
+    register_addr           STRING         COMMENT '用户注册省区',
+    login_ip_addr           STRING         COMMENT '用户上一次登陆ip',
+    login_addr              STRING         COMMENT '用户上一次登陆省区',
+    notify_top_show         BIGINT         COMMENT 'app顶部横幅通知',
+    voice_reminder          BIGINT         COMMENT '声音提醒,默认1:开启,0:关闭',
+    vibrate_reminder        BIGINT         COMMENT '震动提醒,默认1:开启,0:关闭',
+    consume_amount          DECIMAL(20,4)  COMMENT '消费总金额',
+    order_total_num         BIGINT         COMMENT '订单总数',
+    open_card_show          BIGINT         COMMENT '开卡特效开关,默认1:开,0:关',
+    effects_type            STRING         COMMENT '',
+    live_config_json        STRING         COMMENT '直播间配置json',
+    cancel_verify_num       BIGINT         COMMENT '重置实名认证次数',
+    version                 BIGINT         COMMENT '',
+    daily_limit             BIGINT         COMMENT '每日限额提醒',
+    weekly_limit            BIGINT         COMMENT '每周限额提醒',
+    monthly_limit           BIGINT         COMMENT '每月限额',
+    live_anonymous          BIGINT         COMMENT '直播间匿名观看',
+    is_deleted              BOOLEAN        COMMENT '软删除归一(CASE WHEN del_* THEN TRUE)'
+)
+COMMENT '<TODO>'
+PARTITIONED BY (dt STRING)
+STORED AS ORC
+LOCATION '/user/hive/warehouse/ods.db/ods_usr_app_base_user_inc_d';

+ 27 - 0
manual/ddl/ods/usr/ods_usr_app_user_cert_info_inc_d_create.sql

@@ -0,0 +1,27 @@
+-- 作者:tianyu.chu
+-- 日期:2026-05-06
+-- 工单:<TODO>
+-- 目的:<TODO>
+-- 状态:[待执行]
+-- 备注:<TODO>
+
+DROP TABLE IF EXISTS ods.ods_usr_app_user_cert_info_inc_d;
+
+CREATE EXTERNAL TABLE IF NOT EXISTS ods.ods_usr_app_user_cert_info_inc_d (
+    id               BIGINT     COMMENT '',
+    user_id          BIGINT     COMMENT '用户id',
+    cert_birthday    TIMESTAMP  COMMENT '证件生日',
+    cert_sex         BIGINT     COMMENT '证件性别',
+    cert_province    STRING     COMMENT '证件所在省',
+    cert_city        STRING     COMMENT '证件所在市',
+    version          BIGINT     COMMENT '版本',
+    status           BIGINT     COMMENT '状态:0正常',
+    del_flag         BIGINT     COMMENT '删除标记:0正常;1删除',
+    create_time      TIMESTAMP  COMMENT '创建时间',
+    update_time      TIMESTAMP  COMMENT '更新时间',
+    is_deleted       BOOLEAN    COMMENT '软删除归一(CASE WHEN del_* THEN TRUE)'
+)
+COMMENT '<TODO>'
+PARTITIONED BY (dt STRING)
+STORED AS ORC
+LOCATION '/user/hive/warehouse/ods.db/ods_usr_app_user_cert_info_inc_d';

+ 159 - 9
tests/unit/datax/test_hive_ddl_gen.py

@@ -181,15 +181,6 @@ def _patch_main_dependencies(monkeypatch, tmp_path):
     return str(sync_ini)
 
 
-def test_main_l_ods_raises_not_implemented(monkeypatch, tmp_path):
-    sync_ini = _patch_main_dependencies(monkeypatch, tmp_path)
-    monkeypatch.setattr(sys, 'argv', [
-        'hive-ddl-gen.py', '-l', 'ods', '-ini', sync_ini,
-    ])
-    with pytest.raises(NotImplementedError, match='ods'):
-        GEN.main()
-
-
 def test_main_stdout_only_when_no_o(monkeypatch, capsys, tmp_path):
     sync_ini = _patch_main_dependencies(monkeypatch, tmp_path)
     monkeypatch.setattr(sys, 'argv', [
@@ -225,3 +216,162 @@ def test_main_stdout_and_disk_when_o_no_value(monkeypatch, capsys, tmp_path):
     captured = capsys.readouterr()
     assert 'CREATE EXTERNAL TABLE IF NOT EXISTS raw.raw_usr_users_inc_d (' in captured.out
     assert '已写入' in captured.err
+
+
+# ----------------------------------------------------------------------------
+# ods 层测试
+# ----------------------------------------------------------------------------
+
+
+def test_normalize_pg_type_strips_paren_args():
+    assert GEN.normalize_pg_type('numeric(12,2)') == 'numeric'
+    assert GEN.normalize_pg_type('character varying(64)') == 'character varying'
+
+
+def test_normalize_pg_type_strips_timezone_suffix():
+    assert GEN.normalize_pg_type('timestamp(6) without time zone') == 'timestamp'
+    assert GEN.normalize_pg_type('timestamp with time zone') == 'timestamp'
+
+
+def test_normalize_pg_type_lower_strip():
+    assert GEN.normalize_pg_type('  BIGINT  ') == 'bigint'
+
+
+def test_normalize_pg_type_passthrough_simple():
+    assert GEN.normalize_pg_type('text') == 'text'
+    assert GEN.normalize_pg_type('boolean') == 'boolean'
+
+
+def test_load_type_mapping_basic(tmp_path):
+    p = tmp_path / 'pg-to-hive-type.ini'
+    p.write_text(
+        '[mapping]\ninteger = BIGINT\ntext = STRING\n', encoding='utf-8')
+    m = GEN.load_type_mapping(str(p))
+    assert m == {'integer': 'BIGINT', 'text': 'STRING'}
+
+
+def test_load_type_mapping_missing_file_raises():
+    with pytest.raises(FileNotFoundError, match='类型映射 conf 不存在'):
+        GEN.load_type_mapping('/nonexistent.ini')
+
+
+def test_load_type_mapping_missing_section_raises(tmp_path):
+    p = tmp_path / 'bad.ini'
+    p.write_text('[other]\nx = y\n', encoding='utf-8')
+    with pytest.raises(KeyError, match='\\[mapping\\]'):
+        GEN.load_type_mapping(str(p))
+
+
+def test_map_pg_to_hive_hits():
+    m = {'integer': 'BIGINT', 'numeric': 'DECIMAL(20,4)', 'timestamp': 'TIMESTAMP'}
+    assert GEN.map_pg_to_hive('integer', m) == 'BIGINT'
+    assert GEN.map_pg_to_hive('numeric(12,2)', m) == 'DECIMAL(20,4)'
+    assert GEN.map_pg_to_hive('timestamp(6) without time zone', m) == 'TIMESTAMP'
+
+
+def test_map_pg_to_hive_miss_raises():
+    with pytest.raises(KeyError, match='不在 conf'):
+        GEN.map_pg_to_hive('xml', {'integer': 'BIGINT'})
+
+
+def test_reverse_ods_table_name_basic():
+    assert GEN.reverse_ods_table_name('raw_trd_card_group_info_inc_d') == 'ods_trd_card_group_info_inc_d'
+
+
+def test_reverse_ods_table_name_no_raw_prefix_raises():
+    with pytest.raises(ValueError, match="raw_"):
+        GEN.reverse_ods_table_name('ods_x')
+
+
+def _ods_type_mapping():
+    return {
+        'integer': 'BIGINT', 'bigint': 'BIGINT', 'smallint': 'BIGINT',
+        'numeric': 'DECIMAL(20,4)',
+        'character varying': 'STRING', 'text': 'STRING',
+        'timestamp': 'TIMESTAMP', 'boolean': 'BOOLEAN',
+    }
+
+
+def test_render_ods_ddl_field_types_mapped():
+    columns = ['id', 'amount', 'create_time', 'is_active', 'name']
+    full_rows = [
+        (1, 'id', 'id', 'bigint', 'PK'),
+        (2, 'amount', '金额', 'numeric(12,2)', ''),
+        (3, 'create_time', '创建时间', 'timestamp(6) without time zone', ''),
+        (4, 'is_active', '是否启用', 'boolean', ''),
+        (5, 'name', '姓名', 'character varying(64)', ''),
+    ]
+    out = GEN.render_ods_ddl(
+        'raw_usr_users_inc_d', columns, full_rows, _ods_type_mapping())
+    assert 'CREATE EXTERNAL TABLE IF NOT EXISTS ods.ods_usr_users_inc_d (' in out
+    assert 'BIGINT' in out
+    assert 'DECIMAL(20,4)' in out
+    assert 'TIMESTAMP' in out
+    assert 'BOOLEAN' in out
+    assert 'STRING' in out
+    assert "LOCATION '/user/hive/warehouse/ods.db/ods_usr_users_inc_d';" in out
+
+
+def test_render_ods_ddl_appends_is_deleted_at_end():
+    columns = ['id', 'name']
+    full_rows = [
+        (1, 'id', 'id', 'bigint', 'PK'),
+        (2, 'name', '姓名', 'character varying', ''),
+    ]
+    out = GEN.render_ods_ddl(
+        'raw_usr_x_inc_d', columns, full_rows, _ods_type_mapping())
+    name_idx = out.index("'姓名'")
+    is_deleted_idx = out.index('is_deleted')
+    assert name_idx < is_deleted_idx
+    assert 'is_deleted' in out
+    assert 'BOOLEAN' in out
+
+
+def test_render_ods_ddl_no_tech_fields():
+    columns = ['id']
+    full_rows = [(1, 'id', 'id', 'bigint', 'PK')]
+    out = GEN.render_ods_ddl(
+        'raw_x_inc_d', columns, full_rows, _ods_type_mapping())
+    assert 'etl_time' not in out
+    assert 'src_sys' not in out
+    assert 'src_tbl' not in out
+
+
+def test_render_ods_ddl_partition_orc_external():
+    out = GEN.render_ods_ddl(
+        'raw_x_inc_d', ['id'], [(1, 'id', '', 'bigint', 'PK')], _ods_type_mapping())
+    assert 'PARTITIONED BY (dt STRING)' in out
+    assert 'STORED AS ORC' in out
+    assert 'CREATE EXTERNAL TABLE IF NOT EXISTS ods.ods_x_inc_d (' in out
+    assert 'DROP TABLE IF EXISTS ods.ods_x_inc_d;' in out
+
+
+def test_render_ods_ddl_missing_column_pg_meta_raises():
+    """sync ini reader.column 里有但 PG 元数据没有的字段,应该报错。"""
+    with pytest.raises(KeyError, match='元数据缺失'):
+        GEN.render_ods_ddl(
+            'raw_x_inc_d', ['id', 'ghost'],
+            [(1, 'id', '', 'bigint', 'PK')], _ods_type_mapping())
+
+
+def test_main_l_ods_writes_ods_ddl_with_ods_filename(monkeypatch, capsys, tmp_path):
+    sync_ini = _patch_main_dependencies(monkeypatch, tmp_path)
+    out_dir = tmp_path / 'out'
+    type_conf = tmp_path / 'pg-to-hive-type.ini'
+    type_conf.write_text(
+        '[mapping]\nbigint = BIGINT\ncharacter varying = STRING\n',
+        encoding='utf-8',
+    )
+    # 让 main 读这个 tmp 的 type conf 而不是项目 conf
+    monkeypatch.setattr(GEN, 'project_root', str(tmp_path))
+    (tmp_path / 'conf').mkdir()
+    (tmp_path / 'conf' / 'pg-to-hive-type.ini').write_text(
+        type_conf.read_text(encoding='utf-8'), encoding='utf-8')
+
+    monkeypatch.setattr(sys, 'argv', [
+        'hive-ddl-gen.py', '-l', 'ods', '-ini', sync_ini, '-o', str(out_dir),
+    ])
+    GEN.main()
+    captured = capsys.readouterr()
+    assert 'CREATE EXTERNAL TABLE IF NOT EXISTS ods.ods_usr_users_inc_d (' in captured.out
+    assert (out_dir / 'ods_usr_users_inc_d_create.sql').exists()