Files
iAOP/core/data-bus/tdengine_schema.py

200 lines
6.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""TDengine 超级表 schema 自动生成 + 批量 INSERT —— 依据点位字典(子任务 #29)。
模型(TDengine 2.0/3.0 通用):
- 一张超级表 `{tpl}_points`:列 `ts / value / quality`,标签
`device_id / unit / data_type`;
- 点位字典每个测点自动生成一张子表 `{tpl}_pt_{point_id}`(点位维度,
字典 CSV 变更即重建子表集,内核零改动);
- 批量写入:按子表聚合多行 `INSERT INTO {sub} VALUES (...),(...);`,
单条样本约 512B 时对齐 PRD 5.2「5k 条/100ms」基线。
不依赖 taospy:本模块只负责生成 DDL / 批量 SQL;真实落库由
`batch_writer.StoreSink` 的客户端实现(缺失时降级内存 sink 联调)。
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Dict, List, Optional
from .templating import TemplateNaming, sanitize_sql, _sql_str
# dataType → TDengine 列类型(点位字典 schema.VALID_DATA_TYPES 子集)
TD_VALUE_TYPE: Dict[str, str] = {
"float": "DOUBLE",
"int": "BIGINT",
"bool": "BOOL",
}
DEFAULT_VALUE_TYPE = "DOUBLE"
# 样本记录字段(与 edge-gateway spool / kafka_sink 保持一致)
ROW_KEYS = ("device_id", "point_id", "value", "ts", "quality")
@dataclass(frozen=True)
class PointSpec:
"""点位字典中的最小维度信息(schema 生成所需)。"""
device_id: str
point_id: str
unit: str = ""
data_type: str = "float"
@classmethod
def from_dict(cls, raw: dict) -> "PointSpec":
return cls(
device_id=str(raw.get("device_id") or ""),
point_id=str(raw.get("point_id") or ""),
unit=str(raw.get("unit") or ""),
data_type=str(raw.get("data_type") or raw.get("dataType") or "float"),
)
def value_type(data_type: str) -> str:
return TD_VALUE_TYPE.get((data_type or "").lower(), DEFAULT_VALUE_TYPE)
def generate_supertable_ddl(
naming: TemplateNaming,
retention_days: Optional[int] = None,
) -> str:
"""生成超级表 DDL(自动建表,幂等 IF NOT EXISTS)。
Args:
naming: 模板命名器(决定超级表名)。
retention_days: 数据保留天数(TDengine KEEP 表选项,可选)。
Returns:
CREATE STABLE 语句。
"""
ddl = (
f"CREATE STABLE IF NOT EXISTS {naming.stable()} (\n"
" ts TIMESTAMP,\n"
" value DOUBLE,\n"
" quality TINYINT\n"
") TAGS (\n"
" device_id NCHAR(64),\n"
" unit NCHAR(16),\n"
" data_type NCHAR(16)\n"
")"
)
if retention_days and int(retention_days) > 0:
ddl += f" KEEP({int(retention_days)})"
return ddl + ";"
def generate_subtable_ddls(
naming: TemplateNaming,
points: List[PointSpec],
use_typed_value: bool = True,
) -> List[str]:
"""按点位字典为每个测点生成子表 DDL(依据点位字典自动生成)。
Args:
naming: 模板命名器。
points: 点位字典(测点维度集合)。
use_typed_value: 是否按 dataType 派生 value 列类型(TDengine 3.x
支持列级类型;2.x 超级表统一 DOUBLE,传 False 时忽略)。
Returns:
CREATE TABLE ... USING ... TAGS(...) 语句列表(顺序与 points 一致)。
"""
if use_typed_value:
# 说明:超级表 value 列按最宽类型 DOUBLE 建(TDengine 列类型在
# CREATE STABLE 时固定),子表继承,这里保留 data_type 标签供聚合。
pass
out: List[str] = []
seen: set = set()
for p in points:
sub = naming.subtable(p.point_id)
if sub in seen:
continue
seen.add(sub)
tags = ", ".join(
_sql_str(v) for v in (p.device_id, p.unit or "", (p.data_type or "float").lower())
)
out.append(
f"CREATE TABLE IF NOT EXISTS {sub} USING {naming.stable()} "
f"TAGS ({tags});"
)
return out
# ---------------------------------------------------------------------------
# 依据点位字典自动生成完整 schema(子任务 #29 交付入口)
# ---------------------------------------------------------------------------
def specs_from_point_dict_rows(rows: List[dict]) -> List[PointSpec]:
"""把点位字典 CSV 行(edge-gateway 点位字典表头)转换为 PointSpec 列表。
与 `core/edge-gateway/point_dict/loader.py` 的 CSV 表头对齐:
``device_id / point_id / unit / dataType``(兼容小写 ``data_type``)。
其余列(name / sampleRate / qualityCode / opcNode / protocol)与
schema 生成无关,宽松忽略;非法行按 ``from_dict`` 宽松兜底,
不做静默丢弃(保持与点位数一致,便于外层校验定位)。
Args:
rows: 点位字典行(每行一个测点,dict 键为 CSV 表头)。
Returns:
与输入顺序一致的 PointSpec 列表。
"""
return [PointSpec.from_dict(raw) for raw in rows]
def generate_schema(
naming: TemplateNaming,
points: List[PointSpec],
retention_days: Optional[int] = None,
) -> List[str]:
"""依据点位字典一次性生成完整 TDengine schema(超级表 + 每测点子表)。
Args:
naming: 模板命名器(决定超级表/子表名)。
points: 点位字典(测点维度集合,可由 specs_from_point_dict_rows 得到)。
retention_days: 数据保留天数(KEEP,可选)。
Returns:
DDL 语句列表:第 0 条为超级表 CREATE STABLE,其余为子表
CREATE TABLE ... USING ... TAGS(...),可直接按序执行。
"""
return [generate_supertable_ddl(naming, retention_days=retention_days)] + generate_subtable_ddls(
naming, points
)
def build_batch_insert(
naming: TemplateNaming,
rows: List[dict],
) -> List[str]:
"""把样本批聚合为按子表分组的批量 INSERT 语句。
Args:
naming: 模板命名器(决定子表名)。
rows: 样本列表,每条含 device_id/point_id/value/ts(/quality)。
Returns:
SQL 语句列表:每子表一条 `INSERT INTO {sub} VALUES (...),(...);`。
空输入返回空列表。
"""
grouped: Dict[str, List[dict]] = {}
for row in rows:
point_id = str(row.get("point_id") or "")
sub = naming.subtable(point_id)
grouped.setdefault(sub, []).append(row)
statements: List[str] = []
for sub in sorted(grouped):
tuples = []
for row in grouped[sub]:
ts = row.get("ts")
ts_ms = int(ts * 1000) if isinstance(ts, float) else int(ts)
value = row.get("value")
value_sql = "NULL" if value is None else repr(float(value))
quality = int(row.get("quality", 1))
tuples.append(f"({ts_ms}, {value_sql}, {quality})")
statements.append(
f"INSERT INTO {sub} VALUES " + ", ".join(tuples) + ";"
)
return statements