GDAL RFC 94 解析:用宽度/精度元数据在栅格向量格式间安全换算数字字段(DMD_NUMERIC_FIELD_WIDTH 机制)

原创2026-10-09 14:38:311,974 阅读
文章标签:GIS数据处理

GDAL RFC 94 解析:用宽度/精度元数据在栅格向量格式间安全换算数字字段(DMD_NUMERIC_FIELD_WIDTH 机制)

本文围绕 GDAL 官方设计文档 RFC 94: Numeric fields width/precision metadata 展开,讲清楚向量驱动在"字段宽度(width)与精度(precision)"上长期存在的语义歧义问题:不同驱动对 width 的计算是否包含小数分隔符和负号并不统一,跨格式转换(如 DBF 转 SQL)时可能因此丢失宽度或精度。读完本篇,你将掌握 GDAL 3.7 起引入的两个驱动级元数据常量(DMD_NUMERIC_FIELD_WIDTH_INCLUDES_DECIMAL_SEPARATOR / DMD_NUMERIC_FIELD_WIDTH_INCLUDES_SIGN)的确切含义、各驱动的实际取值,以及 ogr2ogr 如何利用这些元数据自动修正数字字段宽度的完整调用链,并能在自己的应用中读取这些元数据来做格式转换或输入校验。

问题背景:OGR 的 width 与 SQL 的 precision 不是同一个东西

RFC 94 的动因章节指出:应用程序(例如 OGR/GDAL 工具链和 QGIS)经常需要获取字段的 width 和 precision 信息,以在格式之间做转换,或者在编辑时校验用户输入。但不同驱动计算 width 和 precision 的方式并不一致——有的把小数分隔符计入长度,有的把负号(minus sign)计入长度,有的两者都计、有的都不计。在 GDAL 3.7 之前,应用程序没有任何途径获知某驱动采用哪种口径,这会导致两类问题:

  1. 跨格式转换时 width 或 precision 被悄悄丢失;
  2. 应用为保险起见主动压缩 GDAL 报告的 width,白白牺牲了字段可容纳的位数。

RFC 特别强调了一个极易混淆的术语对应关系:

  • OGR 中的 width 对应 SQL 中的 precision(有效位数);
  • OGR 中的 precision 对应 SQL 中的 scale(小数位数)。

这个映射是理解整篇 RFC 数值换算示例的钥匙。

现状调查:各驱动 width 的口径差异

RFC 对当时主要驱动做了一次"口径普查",结论如下表(对数据库类驱动,以 NUMERIC 数据类型为参照):

驱动 width 是否包含小数分隔符 width 是否包含负号
Shapefile YES YES
MapInfo YES YES
PostgreSQL NO NO
MySQL NO NO
MSSQL NO NO
OCI NO NO
GPKG N/A N/A
CSV(来自 .csvt) YES YES
HANA NO NO
FlatGeoBuf NO NO
FileGDB N/A N/A
GML NO NO
MEM N/A N/A

RFC 还补充了几个值得注意的单驱动说明:

  • GPKG:SQLite 的列亲和性存储本质是 8 字节 IEEE 浮点数,因此 width/precision 只是逻辑约定,记为 N/A;
  • GML:对应 xsd:decimal 的 totalDigits(小数点左右两侧的数字总数,不计小数点本身)与 fractionDigits(小数点右侧的数字位数);
  • FlatGeoBuf:对 Float 字段,OGR_width = flatgeobuf_precision,OGR_precision = flatgeobuf_scale(若 flatgeobuf_scale == -1 则 OGR_precision 取 0);
  • FileGDB:Scale 是"小数点右侧的位数"(如 56.78 的 scale 为 2),只适用于 double 类型字段;Precision 是"数字的总位数"(56.78 的 precision 为 4),仅对数值字段有效。且 personal/File 地理数据库字段两者均恒返回 0。

这些差异正是 RFC 要解决的:让驱动把"自己的口径"显式暴露出来。

技术方案:两个驱动级元数据常量

RFC 94 的设计非常克制——不改动任何数据结构,只是在**驱动元数据(driver metadata)**层面新增两个能力标志(capability flags),由驱动声明自己的 width 口径。RFC 中给出的 API 设计为:

/** Capability set by a vector driver that supports field width and precision.
 *
 * This capability reflects that a vector driver includes the decimal separator
 * in the field width.
 *
 * See GDAL_DMD_NUMERIC_FIELD_WIDTH_INCLUDES_SIGN for a related capability flag.
 * @since GDAL 3.7
 */
#define GDAL_DMD_NUMERIC_FIELD_WIDTH_INCLUDES_DECIMAL_SEPARATOR "DMD_NUMERIC_FIELD_WIDTH_INCLUDES_DECIMAL_SEPARATOR"

/** Capability set by a vector driver that supports field width and precision.
 *
 * This capability reflects that a vector driver includes the (minus) sign
 * in the field width.
 *
 * See GDAL_DMD_NUMERIC_FIELD_WIDTH_INCLUDES_DECIMAL_SEPARATOR for a related capability flag.
 * @since GDAL 3.7
 */
#define GDAL_DMD_NUMERIC_FIELD_WIDTH_INCLUDES_SIGN "DMD_NUMERIC_FIELD_WIDTH_INCLUDES_SIGN"

语义约定:

  • 元数据值为 YES 表示该驱动的数字字段 width 计算中包含对应字符(小数点或负号);
  • 元数据未定义则视为不支持该特性,即该驱动对数字字段没有可供客户端访问的 width/precision 约束(对应上表中的 N/A 驱动,如 GPKG、FileGDB、MEM)。

这一设计与 GDAL 驱动能力元数据(GDAL_DMD_* 系列)的既有模式完全一致,属于典型的增量式、向后兼容的扩展:RFC 在"效率影响"和"向后兼容性"两节均明确标注为 None。

仓库中的实际落地:常量定义与驱动声明

在仓库中,这两个常量定义于 gcore/gdal.h#L671-L691,注释进一步收窄了适用范围——仅针对 OFTReal 类型的字段:

/** Capability set by a vector driver that supports field width and precision.
 *
 * This capability reflects that a vector driver includes the decimal separator
 * in the field width of fields of type OFTReal.
 *
 * See GDAL_DMD_NUMERIC_FIELD_WIDTH_INCLUDES_SIGN for a related capability flag.
 * @since GDAL 3.7
 */
#define GDAL_DMD_NUMERIC_FIELD_WIDTH_INCLUDES_DECIMAL_SEPARATOR                \
    "DMD_NUMERIC_FIELD_WIDTH_INCLUDES_DECIMAL_SEPARATOR"

声明该元数据为 YES 的驱动(即"width 计入小数点且计入负号"的口径)可在驱动注册代码中查到,与 RFC 普查表完全对应:

例如 CSV 驱动在构造函数中执行:

poDriver->SetMetadataItem(GDAL_DMD_NUMERIC_FIELD_WIDTH_INCLUDES_SIGN,
                           "YES");
poDriver->SetMetadataItem(
    GDAL_DMD_NUMERIC_FIELD_WIDTH_INCLUDES_DECIMAL_SEPARATOR, "YES");

而 PostgreSQL、MySQL、GML 等数据库/GML 类驱动(width 口径为"纯数字位数")则不设置这两个键,应用侧读到 NULL 即按 RFC 约定理解为"width 不含小数点与负号"或"无约束"。

ogr2ogr 中的核心用法:跨口径自动换算 width

RFC 的技术细节部分明确指出,该特性要通过 ogr2ogr 暴露出来:当转换的源/目标格式对 width 的口径不同(一方计入负号或小数点、另一方不计入)时,应当对数字字段的 width 做相应增减。RFC 给出的定量结论是:

  • 从 DBF 转 SQL:width 减 1;
  • 从 SQL 转 DBF:width 加 2。

并给出两个经典算例:

示例值 DBF 表示 SQL 表示
-1.23 width=5, precision=2 precision=3, scale=2
12.34 width=5, precision=2 precision=4, scale=2

RFC 的推理是:DBF→SQL 方向,由于"至少有一个字符是小数点"是确定事实,可以安全地把 width 减 1;但负号是否存在依赖数据本身,所以不能无条件再减 1(除非确认所有值都是负数)。反向(SQL→DBF)则相反:必须为小数点与可能出现的负号各预留 1 位,因此加 2。

源码实现:iChangeWidthBy 的计算与应用

在 apps/ogr2ogr_lib.cpp#L5256-L5302 中可以找到完整的实现。ogr2ogr 在为目标层创建字段前,先分别读取源、目标两个驱动的四项元数据并转成布尔量:

/* Determine if NUMERIC field width narrowing is allowed */
auto poSrcDriver = m_poSrcDS->GetDriver();
const char *pszSrcWidthIncludesDecimalSeparator{
    poSrcDriver ? poSrcDriver->GetMetadataItem(
                      "DMD_NUMERIC_FIELD_WIDTH_INCLUDES_DECIMAL_SEPARATOR")
                : nullptr};
const bool bSrcWidthIncludesDecimalSeparator{
    pszSrcWidthIncludesDecimalSeparator &&
    EQUAL(pszSrcWidthIncludesDecimalSeparator, "YES")};
// …(目标驱动同样读取 DECIMAL_SEPARATOR 与 SIGN 两项)…
const bool bDstWidthIncludesMinusSign{
    pszDstWidthIncludesMinusSign &&
    EQUAL(pszDstWidthIncludesMinusSign, "YES")};

// Calculate width delta
int iChangeWidthBy{0};

if (bSrcWidthIncludesDecimalSeparator && !bDstWidthIncludesDecimalSeparator)
{
    iChangeWidthBy--;
}
else if (!bSrcWidthIncludesDecimalSeparator &&
         bDstWidthIncludesDecimalSeparator)
{
    iChangeWidthBy++;
}

// We cannot assume there is no minus sign, we can only inflate here
if (!bSrcWidthIncludesMinusSign && bDstWidthIncludesMinusSign)
{
    iChangeWidthBy++;
}

换算规则可以整理成一张表:

场景 修正量 依据
源计入小数点、目标不计入 width - 1 小数点必然存在,可安全扣除
源不计入小数点、目标计入 width + 1 目标口径需要为小数点预留 1 位
源不计入负号、目标计入 width + 1 注释说明:"We cannot assume there is no minus sign, we can only inflate here"——负号可能不存在,只能膨胀不能收缩
源计入负号、目标不计入 不修正(保守处理) 源码中未实现该方向的收缩

随后在两个字段创建分支(按字段子集创建、全量字段复制)中,对每个字段执行 apps/ogr2ogr_lib.cpp#L5519-L5522:

if (iChangeWidthBy != 0 && oFieldDefn.GetType() == OFTReal &&
    oFieldDefn.GetWidth() != 0)
{
    oFieldDefn.SetWidth(oFieldDefn.GetWidth() + iChangeWidthBy);
}

注意两个约束条件:仅当字段类型为 OFTReal(与 gdal.h 中常量注释一致)且 width 非 0 时才做修正。对于 DBF(Shapefile/CSV)→ SQL 类方向:iChangeWidthBy = -1,正好实现 RFC 的"减 1";SQL → DBF 方向:iChangeWidthBy = +1(小数点)+1(负号)= +2,对应 RFC 的"加 2"。

需要说明的是,RFC 文本中曾设想给 ogr2ogr 增加一个开关来启用/禁用 width 收缩;从当前仓库 apps/ogr2ogr_lib.cpp 的代码结构看,上述宽度修正完全由两个驱动的元数据自动驱动,未见额外的命令行开关参与该环节。

测试验证:参数化的跨驱动宽度换算矩阵

RFC 承诺"将向测试套件添加 C++ 测试"。当前仓库中,该特性的回归验证落在 Python 测试 autotest/utilities/test_ogr2ogr_lib.py 的 test_width_precision_flags(约 L1902-L1947)中,采用参数化矩阵覆盖 Shapefile、GML、CSV、PGDump 等多个"源驱动 × 目标驱动"组合,例如:

("GML", ".gml", "CSV", ".csv", 6, 2, False, False),
("CSV", ".csv", "CSV", ".csv", 5, 2, True, True),  # round trip
("CSV", ".csv", "ESRI Shapefile", ".shp", 5, 2, True, True),
("CSV", ".shp", "PGDump", ".sql", 4, 2, True, True),

测试逻辑与 RFC 的口径分析一一对应:

  1. 先用 dr.GetMetadata_Dict().get("DMD_NUMERIC_FIELD_WIDTH_INCLUDES_SIGN", "NO") 断言源驱动声明的口径与预期参数一致;
  2. 构造一个 width=5, precision=2 的 OFTReal 字段写入 12.34(测试注释说明:若 width 计入符号与小数点,width=5 可容纳 12.34 或 -1.23,但不能容纳 -12.34);
  3. 按源驱动是否计入小数点决定是否先把 width 减 1,再执行转换并断言目标字段上的 width/precision。

测试中还留有两处很有价值的工程注记,值得转换数据时留意:

  • 被标注为"精度不可靠"的驱动列表 PRECISION_UNRELIABLE_DRIVERS = ("GML", "CSV")——它们会在 schema 里正确设置 width/precision,但实际写入时可能按全精度存储而不告警;
  • MapInfo 相关组合被注释掉,因为该驱动遇到溢出会中止写入,而其他驱动会继续写。

在应用与绑定中使用这些元数据

RFC 声明该实现会暴露到 SWIG 绑定中;仓库中 swig/include/gdalconst.i 已包含 DMD_NUMERIC_FIELD_WIDTH_INCLUDES_DECIMAL_SEPARATOR 与 DMD_NUMERIC_FIELD_WIDTH_INCLUDES_SIGN 两个常量,Python 侧可以直接使用。一个典型用法(对应测试中的写法):

from osgeo import ogr

dr = ogr.GetDriverByName("ESRI Shapefile")
meta = dr.GetMetadata_Dict()
includes_sign    = meta.get("DMD_NUMERIC_FIELD_WIDTH_INCLUDES_SIGN", "NO") == "YES"
includes_decimal = meta.get("DMD_NUMERIC_FIELD_WIDTH_INCLUDES_DECIMAL_SEPARATOR", "NO") == "YES"

据此,应用可以在做字段迁移、导入前的长度校验、或向数据库 DDL 生成 NUMERIC(precision, scale) 声明时,先归一化两侧口径,再执行 RFC 给出的换算:DBF 口径的 width 对应 SQL precision,DBF 口径的 precision 对应 SQL scale,跨口径时按"小数点 ±1、负号仅膨胀 +1"的规则修正。

小结

RFC 94 用最小的改动面(两个 GDAL_DMD_* 驱动元数据,GDAL 3.7 起可用,@since 标注见 gcore/gdal.h#L671-L691)解决了向量字段 width/precision 跨格式转换中的口径歧义问题。其价值链条清晰:

  1. 驱动侧:CSV、Shapefile、MapInfo 等"width 含小数点与负号"的驱动显式声明两个元数据为 YES(如 ogr/ogrsf_frmts/shape/ogrshapedriver.cpp#L330-L333);未声明者按"不含或无约束"处理;
  2. 工具侧:ogr2ogr 依据源/目标驱动元数据自动计算 iChangeWidthBy 并只作用于 width 非 0 的 OFTReal 字段(apps/ogr2ogr_lib.cpp#L5256-L5302);
  3. 测试侧:参数化矩阵回归验证多驱动组合下的换算结果(autotest/utilities/test_ogr2ogr_lib.py)。

对开发者而言,这套机制给出的实践准则是:处理数字字段的 width/precision 时,永远先查询驱动元数据确认口径,再按"小数点必然存在可扣减、负号只能预留不能扣减"的原则做换算,并记住 OGR width ↔ SQL precision、OGR precision ↔ SQL scale 的术语映射,才能避免转换过程中的精度丢失或无谓的宽度收缩。

登录后查看全文
gdal