Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fix SQL export generating multiple PRIMARY KEY constraints for composite keys (#1026)
- Preserve parametrized physicalTypes for SQL export (#1086)
- Fix incorrect SQL type mappings: SQL Server `double`/`jsonb`, MySQL bare `varchar`, missing Trino types (#1110)
- Fix markdown export breaking table structure when extra field values contain pipe characters (#832)

## [0.11.7] - 2026-03-24

Expand Down
15 changes: 12 additions & 3 deletions datacontract/export/markdown_exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -366,11 +366,20 @@ def render_header(key: str) -> str:

if isinstance(value_extra, list) and len(value_extra):
if isinstance(value_extra[0], dict):
parts.append(array_of_dict_to_markdown(value_extra))
raw = array_of_dict_to_markdown(value_extra)
if is_in_table_cell:
raw = raw.replace("|", "&#124;").replace("\n", "<br />")
parts.append(raw)
elif isinstance(value_extra[0], str):
parts.append(array_to_markdown(value_extra))
raw = array_to_markdown(value_extra)
if is_in_table_cell:
raw = raw.replace("|", "&#124;").replace("\n", "<br />")
parts.append(raw)
elif isinstance(value_extra, dict):
parts.append(dict_to_markdown(value_extra))
raw = dict_to_markdown(value_extra)
if is_in_table_cell:
raw = raw.replace("|", "&#124;").replace("\n", "<br />")
parts.append(raw)
else:
parts.append(f"{str(value_extra)}{value_line_ending}")

Expand Down
23 changes: 23 additions & 0 deletions tests/fixtures/markdown/export/multicolumn.odcs.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
id: test-multicolumn-export
name: Multicolumn Test Contract
version: 1.0.0
schema:
- name: orders
description: Order records
properties:
- name: order_id
physicalType: string
quality:
- type: sql
description: Row count must be positive
query: SELECT count(*) FROM orders
mustBeGreaterThan: 0
- type: text
description: Non-null check
- name: amount
physicalType: double
quality:
- type: sql
description: Amount must be positive
query: SELECT count(*) FROM orders WHERE amount <= 0
mustBe: 0
27 changes: 27 additions & 0 deletions tests/test_export_markdown.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import yaml
from datacontract_specification.model import DataContractSpecification
from open_data_contract_standard.model import OpenDataContractStandard
from typer.testing import CliRunner

from datacontract.cli import app
Expand Down Expand Up @@ -30,3 +32,28 @@ def test_to_markdown():

with open("fixtures/markdown/export/expected.md", "r") as file:
assert result == file.read()


def test_pipe_chars_escaped_in_table_cells():
"""Regression test for #832: pipe chars in extra field values should be escaped when inside table cells."""
contract_yaml = """
id: test-pipe-escape
schema:
- name: orders
properties:
- name: order_id
logicalType: string
config:
mapping: "a | b | c"
"""
contract = OpenDataContractStandard.model_validate(yaml.safe_load(contract_yaml))
result = to_markdown(contract)

# Find the table row for order_id
lines = [line for line in result.split("\n") if "order_id" in line and line.startswith("|")]
assert lines, "order_id table row not found"
row = lines[0]
# The row must have exactly 4 pipe chars as table delimiters (| col1 | col2 | col3 |)
assert row.count("|") == 4, (
f"Expected 4 pipe delimiters in row, got {row.count('|')}: {row!r}"
)
Loading