)
Airbyte 规范化引擎嵌套流测试场景深度解析主键、标识符截断与命名冲突test_nested_streams【免费下载链接】airbyteOpen-source data movement for ELT pipelines and AI agents — from APIs, databases files to warehouses, lakes, and AI applications. Both self-hosted and Cloud.项目地址: https://gitcode.com/gh_mirrors/ai/airbyteAirbyte 的base-normalization模块负责把同步到原始表_airbyte_raw_*的 JSON 记录展开成规范化normalized的关系表。本文围绕其集成测试资源test_nested_streams展开剖析该测试套件如何系统性验证三类高难度场景在含嵌套对象、嵌套数组、数组嵌套数组等复杂类型字段的流上定义主键让流名故意突破目标数据库标识符长度上限以制造截断碰撞以及人为构造流名与列名冲突、含特殊字符的列名来检验规范化 SQL 的健壮性。读完本文你将理解 Airbyte 规范化引擎面对真实世界脏数据时的降级策略并能复现、扩展这套跨数据库的测试方法论。测试资源在仓库中的位置与作用test_nested_streams是base-normalization集成测试的三大 Git 版本化测试资源之一位于 airbyte-integrations/bases/base-normalization/integration_tests/resources/test_nested_streams。在 test_normalization.py 中可以看到版本化测试的完整清单# dbt models and final sql outputs from the following git versioned tests will be written in a folder included in # airbyte git repository. git_versioned_tests [test_simple_streams, test_nested_streams]所谓Git 版本化含义见test_normalization.py中setup_test_dir的注释这些测试生成的 dbt 模型与最终 SQL 产物会写入仓库内的normalization_test_output/目录便于对下游变更做最小规模的差异追踪非版本化测试的产物则写入/tmp临时目录。该测试通过pytest.mark.parametrize对destination_type × test_resource_name做笛卡尔积式遍历test_normalization.py其中对 Oracle 与 ClickHouse 两个目标做了显式跳过——它们不支持嵌套流测试if ( destination_type.value in (DestinationType.ORACLE.value, DestinationType.CLICKHOUSE.value) and test_resource_name test_nested_streams ): pytest.skip(fDestinations {destination_type} does not support nested streams)整个资源目录结构如下test_nested_streams/ ├── README.md ├── data_input/ │ ├── catalog.json # 9 个流的连接目录schema 同步模式 主键 │ ├── messages.txt # 第一轮同步的 RECORD 消息 │ ├── messages_incremental.txt # 第二轮增量同步的 RECORD 消息 │ └── replace_identifiers.json # 各目标数据库的标识符截断/改写映射 └── dbt_test_config/ ├── dbt_data_tests/ # 第一轮行数断言引用 tmp 模型 ├── dbt_data_tests_incremental/ # 第二轮行数断言 ├── dbt_data_tests_tmp/ # 第一轮行数统计模型 ├── dbt_data_tests_tmp_incremental/ # 第二轮行数统计模型 ├── dbt_schema_tests/ # 第一轮 schema 断言expression_is_true 等 └── dbt_schema_tests_incremental/ # 第二轮 schema 断言场景一在复杂嵌套类型字段上定义主键测试的主体流nested_stream_with_complex_columns_resulting_into_long_names在 catalog.json 中被设计为包含三种复杂嵌套结构嵌套对象nested objectpartition字段是一个[null, object]其下继续挂载子字段嵌套数组nested arraypartition.DATA是[null, array]元素为对象含currency字段数组的数组array of arraypartition.double_array_data是array且items又是array最内层元素为含id的对象。该流的关键配置是在嵌套流上定义主键sync_mode: incremental, cursor_field: [date], destination_sync_mode: append_dedup, primary_key: [[id]]primary_key: [[id]]表示id是主键且id的 JSON Schema 类型被定义为[null, number, string]——即数值与字符串都合法。配合 messages.txt 中的真实数据{type: RECORD, record: {stream: nested_stream_with_complex_columns_resulting_into_long_names, emitted_at: 1602638599000, data: { id: 4.2, date: 2020-08-29T00:00:00Z, partition: { double_array_data: [[ { id: EUR } ]], DATA: [ {currency: EUR } ], column_with\_quotes: [ {currency: EUR } ] } }}} {type: RECORD, record: {stream: nested_stream_with_complex_columns_resulting_into_long_names, emitted_at: 1602638599100, data: { id: test record, date: 2020-08-31T00:00:00Z, partition: { double_array_data: [[ { id: USD } ], [ { id: GBP } ]], DATA: [ {currency: EUR } ], column_with\_quotes: [ {currency: EUR } ] } }}}这里id分别是数值4.2与字符串test record而date作为游标字段驱动增量同步append_dedup模式下规范化引擎需要依据主键对嵌套展开后的多张表做去重。测试验证的正是主键定义不会被嵌套结构干扰且主键列的类型多样性number/string不会破坏行数统计与去重语义。场景二流名碰撞——突破 Postgres 64 字符标识符上限这是test_nested_streams最核心、也最贴近真实生产环境的测试。README 明确指出以下三个流被故意命名为超长描述用来打破 Postgres 64 字符的标识符限制即使它们被放在不同的 schema 中test_normalization_nested_stream_with_complex_columns_resulting_into_long_namestest_normalization_non_nested_stream_without_namespace_resulting_into_long_namestest_normalization_namespace_simple_stream_with_namespace_resulting_into_long_names这三个全名截断到 64 字符后都可能变成同一个名字test_normalization_n__lting_into_long_names从而制造出命名碰撞collision。这直接考验规范化引擎的标识符消歧策略截断后如果冲突引擎必须通过追加哈希、编号或其他规则为冲突的标识符生成唯一名称否则下游 dbt 模型会生成失败或互相覆盖。跨数据库的实际截断映射记录在 replace_identifiers.json 中。以 Postgres 为例postgres: [ { nested_stream_with_complex_columns_resulting_into_long_names_partition_double_array_data: nested_stream_with_c__ion_double_array_data }, { nested_stream_with_complex_columns_resulting_into_long_names_partition_data: nested_stream_with_c___names_partition_data }, { nested_stream_with_complex_columns_resulting_into_long_names_partition: nested_stream_with_c___long_names_partition }, { nested_stream_with_complex_columns_resulting_into_long_names: nested_stream_with_c__lting_into_long_names }, { non_nested_stream_without_namespace_resulting_into_long_names: non_nested_stream_wi__lting_into_long_names }, { expression: \DATA is not null\: expression: \\\\DATA\\\ is not null\ } ]可以看到截断并不是简单粗暴地切前 64 个字符而是保留足够多的可辨识前缀与后缀中间用下划线连接例如nested_stream_with_complex_columns_resulting_into_long_names→nested_stream_with_c__lting_into_long_names。MySQL、MSSQL、TiDB、DuckDB 的条目结构与此一致体现各数据库标识符上限如 MySQL 64、MSSQL 128下不同的截断策略。此外replace_identifiers.json还包含目标侧断言 SQL 的改写Postgres 需要把DATA is not null改成带双引号的\DATA\ is not null说明DATA这类保留字/大写字段名在规范化输出中必须被正确引用。测试套件中的non_nested_stream_without_namespace_resulting_into_long_names与simple_stream_with_namespace_resulting_into_long_names带namespace: test_normalization_namespace见 catalog.json分别覆盖无 namespace 的非嵌套长名流与带 namespace 的简单长名流从两个维度逼近命名空间长度双重的极限。场景三流名与列名冲突conflict_stream_* 系列README 指出conflict_stream_name_*系列表与unnest_alias专门用来测试流名与列名在结合嵌套展开时产生的命名冲突。catalog 中构造了三个自指流conflict_stream_name流内部嵌套了与流同名的对象字段conflict_stream_name且该字段的items.properties里还有一个同名整数列conflict_stream_namecatalog.json。对应数据为三层同名嵌套{type:RECORD,record:{stream:conflict_stream_name,data:{id:1,conflict_stream_name:{conflict_stream_name: {groups: 1, custom_fields: [{id:1, value:3}, {id:2, value:4}], conflict_stream_name: 3}}},emitted_at:1623861660}}该流还同时出现于首轮与增量消息文件中用于验证同名冲突在多次同步下的稳定性。conflict_stream_scalar流内直接放了一个与流同名的标量字段conflict_stream_scalar类型integer测试流名 列名的顶层冲突。conflict_stream_array流内放置与流同名的数组字段conflict_stream_array其properties.conflict_stream_name又是数组元素含id测试数组展开路径上的同名冲突。这一组场景验证规范化引擎在生成表名与列名时如何避免同名覆盖流级表名与嵌套展开后的子表、列名之间必须保持可区分否则会产生语义错误或 SQL 编译失败。场景四unnest_alias 与特殊字符列名unnest_alias流专门测试展开unnest时使用别名以及含特殊字符的列名。其 JSON Schema 中定义了一个极具挑战性的字段名column_with\_quotes: { type: [null, array], items: { properties: { currency: { type: [null, string] } } } }即字段名里同时包含反引号、单引号、双引号catalog.json。对应消息数据为{type:RECORD,record:{stream:unnest_alias,data:{id:1, children: [{ab_id: 1, owner: {owner_id: 1, column_with\_quotes: [ {currency: EUR } ]}},{ab_id: 2, owner: {owner_id: 2, column_with\_quotes: [ {currency: EUR } ]}}]},emitted_at:1623861660}}schema_test.yml中针对该展开产物unnest_alias_children_owner断言了特殊列的存在与非空schema_test.yml- name: unnest_alias_children_owner tests: - dbt_utils.expression_is_true: expression: \column_with\\_quotes\ is not null注意表达式中对引号的转义——dbt 渲染 SQL 时列名必须被正确引用双引号内再转义双引号。同时replace_identifiers.json中 BigQuery 与 MySQL 对column___with__quotes/column__with\_quotes的映射说明各数据库对特殊字符的清洗与引用规则各不相同BigQuery 把特殊字符替换为下划线MySQL 则用反引号包裹并保留单引号。此外unnest_alias流的cursor_field被设为[]空数组且destination_sync_mode为overwrite意味着它走的是全量覆盖 无游标展开路径用于隔离展开别名这一变量避免与增量语义耦合。场景五空流到有数据的增量过渡与数组展开some_stream_that_was_empty流在 catalog.json 中定义sync_mode: incremental、append_dedup、主键id验证了一个微妙场景第一轮同步该流没有任何记录第二轮增量同步才出现 3 条数据。首轮messages.txt中确实没有它的任何 RECORD而 messages_incremental.txt 中补入了 3 条带date游标的记录。这考验规范化引擎对空流 SCD缓慢变化维度表的建表与后续填充行为。arrays流则补充测试普通数组的展开array_of_strings含null元素的字符串数组与nested_array_parent.nested_array对象内嵌套字符串数组验证数组展开对null元素与嵌套父级的处理。dbt 断言体系如何验证规范化结果正确整个测试资源的验证由两层 dbt 测试构成Schema 层断言schema_test.yml对展开表nested_stream_with_complex_columns_resulting_into_long_names_partition断言double_array_data is not null与DATA is not null即嵌套数组展开后应有非空数据对..._partition_DATA表的currency列做not_null校验对unnest_alias_children_owner断言特殊字符列非空。注意其中被注释掉的..._partition_double_array_data.id的not_null测试并标注# TODO Fix bug here——这是测试资源中真实存在的待修复事项也从侧面说明嵌套数组展开后的列约束曾是已知薄弱点。行数层断言data tests采用统计模型 断言查询的两段式结构。统计模型 nested_streams_first_run_row_counts.sql 对每一张关键表用union all汇总row_count与expected_count例如select distinct _airbyte_raw_nested_stream_with_complex_columns_resulting_into_long_names as label, count(*) as row_count, 2 as expected_count from {{ source(test_normalization, _airbyte_raw_nested_stream_with_complex_columns_resulting_into_long_names) }} union all select distinct nested_stream_with_complex_columns_resulting_into_long_names as label, count(*) as row_count, 2 as expected_count from {{ ref(nested_stream_with_complex_columns_resulting_into_long_names) }}首轮对主表、partition展开表、partition_DATA表按currency去重后期望 1 行逐一核对 2 行的期望值同时断言空流的some_stream_that_was_empty_scd与some_stream_that_was_empty为 0 行、arrays展开为 1 行。断言查询 test_check_first_run_row_counts.sql 则简单直接select * from {{ ref(nested_streams_first_run_row_counts) }} where row_count ! expected_count第二轮增量统计模型 nested_streams_second_run_row_counts.sql 把主表期望值提升到 3 行并把空流的_scd与主表都改为期望 3 行从而验证空流在增量轮次中正确产出数据以及append_dedup的去重结果。跨数据库适配replace_identifiers 的工程意义replace_identifiers.json 按目标数据库bigquery、oracle、postgres、snowflake、redshift、mysql、mssql、tidb、duckdb组织其职责是把测试 SQL 中预期的长标识符改写为对应数据库实际截断后的名称。从中可以提炼出可复用的经验数据库标识符上限典型值该文件中的代表改写Postgres63 字节nested_stream_with_complex_columns_resulting_into_long_names→nested_stream_with_c__lting_into_long_namesMySQL / TiDB64 字符额外改写_airbyte_raw_...前缀表名并用json_length()替代array_length()MSSQL128 字符与 Postgres 类似的结构化截断DuckDB较长沿用与 MySQL 相同的截断映射BigQuery宽松主要改写array_length()断言与特殊字符清洗column___with__quotesSnowflake宽松主要改写大写表名NESTED_STREAMS_FIRST_RUN_ROW_COUNTS→ 小写引用该文件同时说明了断言 SQL 本身的方言差异MySQL/TiDB 用coalesce(json_length(...), 0) 0判断数组非空BigQuery 用array_length(...) 0Postgres 则需要给DATA这样的保留字加引号。这意味着同一份测试资源在不同目标上断言语义完全一致但表达必须方言化——这正是 Airbyte 规范化层一次建模、多方言渲染设计目标的直接体现。如何复现与扩展这套测试复现需要先让目标数据库出现在NORMALIZATION_TEST_TARGET环境变量中未设置时测试会直接pytest.skip。核心入口是# 在 base-normalization 目录下对指定目标运行嵌套流测试 NORMALIZATION_TEST_TARGETpostgres pytest integration_tests/test_normalization.py -k test_nested_streams测试夹具会自动完成以下流程对应 test_normalization.py 描述的setup_test_dir用 catalog.json 生成规范化后的 dbt 模型 → 用 messages.txt 模拟首轮同步 → 运行 dbt 并执行首轮 schema/data 测试 → 再用messages_incremental.txt模拟第二轮增量同步 → 执行增量轮测试。由于test_nested_streams属于git_versioned_tests中间产物会落在normalization_test_output/目录便于 diff 检查规范化 SQL 的每一次变更。若要扩展新的边界场景例如更深层的对象嵌套、更长的嵌套路径、更多保留字冲突可以在本资源目录中按现有结构新增 catalog 流、补充 RECORD 消息与replace_identifiers.json映射再在dbt_test_config下增加对应的行数与 schema 断言即可复用整套 dbt 测试基建无需改动 test_normalization.py 的框架逻辑。小结test_nested_streams以不足 30 行的 README 定义了五个极具实战价值的规范化测试方向复杂嵌套类型上的主键、超长标识符截断碰撞、流名与列名同名冲突、特殊字符列名、空流增量过渡。配合catalog.json的 9 个精心设计的流、两轮 RECORD 消息、方言化的replace_identifiers.json映射以及 dbt 双层断言它系统性地验证了 Airbyte 规范化引擎在多数据库下的标识符降级与命名消歧能力。这套资源目录 版本化产物 跨方言断言的组织方式本身也是一份可直接借鉴的数据库适配测试模板。【免费下载链接】airbyteOpen-source data movement for ELT pipelines and AI agents — from APIs, databases files to warehouses, lakes, and AI applications. Both self-hosted and Cloud.项目地址: https://gitcode.com/gh_mirrors/ai/airbyte创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考