Repository navigation
Implement PostgreSQL COPY escape sequence handling (fixes #7) - #17
Conversation
This commit implements full support for PostgreSQL COPY text format escape sequences, matching the behavior of PostgreSQL's CopyReadAttributesText() function in copyfromparse.c. Changes: - Added unescape_copy_text() function that handles all PostgreSQL escape sequences: * \\b, \\f, \\n, \\r, \\t, \\v - single character escapes * \\NNN - octal byte values (1-3 digits) * \\xNN - hex byte values (1-2 digits) * \\\\ - backslash * \\X - any other character taken literally - Updated DataConverter and SmartDataConverter to unescape data after splitting by tabs but before type conversion - NULL marker (\\N) is checked before unescaping, matching PostgreSQL's behavior - Added comprehensive test suite with 16 new test cases covering: * All escape sequence types * Edge cases (octal/hex digit combinations) * Integration with existing converters * NULL marker preservation - Updated module documentation to document escape sequence support This resolves issue #7 where backslash sequences other than \\N were not being interpreted correctly. Implementation is based directly on PostgreSQL source code at: build/postgres/src/backend/commands/copyfromparse.c lines 1657-1749 Resolves #7
WalkthroughAdds an unescape_copy_text(field: str) utility to decode PostgreSQL COPY text escapes and updates DataConverter and SmartDataConverter to unescape fields before type conversion while preserving Changes
Sequence Diagram(s)sequenceDiagram
participant Raw as Raw Field Data
participant Conv as DataConverter / SmartDataConverter
participant Unesc as unescape_copy_text()
participant Result as Output
Raw->>Conv: receive field (string)
Conv->>Conv: check if field == "\\N"
alt field is "\N"
Conv-->>Result: return None
else
Conv->>Unesc: unescape_copy_text(field)
Note over Unesc: handle \b \f \n \r \t \v \\ \xNN \NNN\n(unknown \X left literal)
Unesc-->>Conv: unescaped string
Conv->>Conv: (SmartDataConverter) perform type conversions on unescaped string
Conv-->>Result: return converted/unescaped value
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes
Poem
Pre-merge checks and finishing touches✅ Passed checks (5 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (2)
pgdumplib/converters.py (1)
40-124: unescape_copy_text matches PostgreSQL COPY semantics; drop unusednoqaand consider a couple more edge-case testsThe implementation correctly handles:
- Single-char escapes (
\b,\f,\n,\r,\t,\v).- Octal escapes with 1–3 digits, masking to a single byte.
- Hex escapes with 1–2 digits, consuming only valid hex chars and leaving the rest literal.
\\→\and generic\X→Xbehavior via the finalelsebranch.- Early return when no backslash is present, and safe handling of a trailing backslash.
Behavior for
\\Nintentionally yields'N', with call sites checking for\\Nbefore unescaping, which matches PostgreSQL’sCopyReadAttributesText()usage pattern.Static analysis notes that the
# noqa: C901on Line 40 is unused. Since this function is well within reasonable complexity, it’s safe to remove the directive.Apply this diff to satisfy Ruff:
-def unescape_copy_text(field: str) -> str: # noqa: C901 +def unescape_copy_text(field: str) -> str:Optionally, you could add a couple of micro edge-case tests (e.g.,
\\xwith no digits and a trailing backslash) to lock in current behavior:
unescape_copy_text('\\x') -> 'x'unescape_copy_text('\\xG') -> 'xG'unescape_copy_text('foo\\') -> 'foo\\'tests/test_converters.py (1)
80-240: New escape-sequence tests are thorough and aligned with the implementation; a couple of optional edge cases could be addedThe
UnescapeCopyTextTestCasesuite does a good job of pinning down behavior:
- All advertised escapes (
\\b,\\f,\\n,\\r,\\t,\\v, octal, hex,\\\\, and literal\\X) are exercised.- Octal/hex expectations (e.g.,
\\101 -> 'A',\\377 -> '\xff',\\x41 -> 'A') match the implemented parsing.test_null_marker_not_unescapedcorrectly documents and verifies thatunescape_copy_text('\\N') == 'N', with converter tests ensuring\\Nis special-cased before unescaping.- Converter integration tests confirm DataConverter and SmartDataConverter both unescape content while preserving NULL markers and type inference.
Everything here looks consistent with the new core logic.
As a small optional enhancement, you could add tests for the edge cases that the implementation already handles:
self.assertEqual(converters.unescape_copy_text('\\x'), 'x')self.assertEqual(converters.unescape_copy_text('\\xG'), 'xG')self.assertEqual(converters.unescape_copy_text('foo\\'), 'foo\\')to make the intended behavior around incomplete hex escapes and trailing backslashes explicit.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
pgdumplib/converters.py(4 hunks)tests/test_converters.py(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
pgdumplib/converters.py (1)
pgdumplib/dump.py (1)
append(65-78)
tests/test_converters.py (1)
pgdumplib/converters.py (6)
unescape_copy_text(40-124)DataConverter(127-150)convert(140-150)convert(157-164)convert(203-207)SmartDataConverter(181-246)
🪛 Ruff (0.14.5)
pgdumplib/converters.py
40-40: Unused noqa directive (non-enabled: C901)
Remove unused noqa directive
(RUF100)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (28)
- GitHub Check: test (3.14, 17)
- GitHub Check: test (3.11, 16)
- GitHub Check: test (3.14, 18)
- GitHub Check: test (3.14, 15)
- GitHub Check: test (3.13, 18)
- GitHub Check: test (3.12, 16)
- GitHub Check: test (3.13, 16)
- GitHub Check: test (3.13, 17)
- GitHub Check: test (3.14, 16)
- GitHub Check: test (3.13, 15)
- GitHub Check: test (3.12, 18)
- GitHub Check: test (3.11, 17)
- GitHub Check: test (3.12, 15)
- GitHub Check: test (3.12, 17)
- GitHub Check: test (3.11, 15)
- GitHub Check: test (3.11, 18)
- GitHub Check: test (3.13, 18)
- GitHub Check: test (3.12, 18)
- GitHub Check: test (3.13, 17)
- GitHub Check: test (3.13, 15)
- GitHub Check: test (3.13, 16)
- GitHub Check: test (3.12, 15)
- GitHub Check: test (3.12, 16)
- GitHub Check: test (3.11, 18)
- GitHub Check: test (3.12, 17)
- GitHub Check: test (3.11, 16)
- GitHub Check: test (3.11, 15)
- GitHub Check: test (3.11, 17)
🔇 Additional comments (3)
pgdumplib/converters.py (3)
6-24: Module-level documentation accurately describes new COPY escape behaviorThe description of DataConverter/SmartDataConverter and the supported COPY text escape sequences aligns with the implementation, including NULL (
\\N) handling and unescaping order. No changes needed here.
133-150: DataConverter: correct ordering of NULL check vs unescapingThe comprehension now:
- Splits on actual tabs (matching COPY text format).
- Checks
e == '\\N'before callingunescape_copy_text, so the NULL marker is converted toNoneand never misinterpreted as'N'.- Unescapes all other fields, giving callers the fully decoded COPY payload.
This is the right behavior and aligns with the new tests and module docs.
210-246: SmartDataConverter: NULL-first logic and unescape-before-type-detection look solid
_convert_columnnow:
- Detects
column == '\\N'up front and returnsNone, preserving PostgreSQL’s NULL semantics.- Unescapes the remaining values before running numeric/Decimal/IP/UUID/datetime detection, ensuring type inference sees the true content (e.g.,
'42'rather than'4\\2','hello\nworld'instead of'hello\\nworld').This preserves existing behavior for bad dates and other non-matching values while improving correctness for escaped content. No changes needed.
Per CodeRabbit feedback, add tests for: - Incomplete hex escapes (\x, \xG, \xZ9) - Trailing backslashes (foo\, \) The noqa directive is needed for local pre-commit hooks (C901 enabled with threshold 15, function complexity 18). The complexity is inherent to matching PostgreSQL's implementation.
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (1)
tests/test_converters.py (1)
135-171: Octal and hex escape sequences well-tested.The tests comprehensively cover all digit ranges and verify correct numeric conversions. Case sensitivity for hex is properly tested.
Optional: Consider adding a test for invalid octal digits to demonstrate edge-case handling:
def test_invalid_octal_escapes(self): """Test that non-octal digits after backslash are literal""" self.assertEqual(converters.unescape_copy_text('\\8'), '8') self.assertEqual(converters.unescape_copy_text('\\9'), '9') # Octal stops at non-octal digit self.assertEqual(converters.unescape_copy_text('\\078'), '\x078')
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
tests/test_converters.py(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
tests/test_converters.py (1)
pgdumplib/converters.py (6)
unescape_copy_text(40-124)DataConverter(127-150)convert(140-150)convert(157-164)convert(203-207)SmartDataConverter(181-246)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (29)
- GitHub Check: test (3.14, 18)
- GitHub Check: test (3.14, 15)
- GitHub Check: test (3.13, 18)
- GitHub Check: test (3.14, 17)
- GitHub Check: test (3.13, 15)
- GitHub Check: test (3.13, 17)
- GitHub Check: test (3.12, 17)
- GitHub Check: test (3.12, 16)
- GitHub Check: test (3.13, 16)
- GitHub Check: test (3.11, 18)
- GitHub Check: test (3.12, 18)
- GitHub Check: test (3.11, 15)
- GitHub Check: test (3.11, 17)
- GitHub Check: test (3.12, 15)
- GitHub Check: test (3.11, 16)
- GitHub Check: test (3.12, 17)
- GitHub Check: test (3.14, 16)
- GitHub Check: test (3.13, 17)
- GitHub Check: test (3.14, 17)
- GitHub Check: test (3.13, 18)
- GitHub Check: test (3.12, 18)
- GitHub Check: test (3.12, 15)
- GitHub Check: test (3.13, 16)
- GitHub Check: test (3.11, 15)
- GitHub Check: test (3.13, 15)
- GitHub Check: test (3.12, 16)
- GitHub Check: test (3.11, 16)
- GitHub Check: test (3.11, 17)
- GitHub Check: test (3.11, 18)
🔇 Additional comments (6)
tests/test_converters.py (6)
80-86: Excellent test class documentation.The docstring clearly establishes the purpose and scope of these tests, explicitly referencing PostgreSQL's behavior. This provides valuable context for future maintainers.
88-133: Comprehensive coverage of single-character escape sequences.All documented single-character escape sequences are tested thoroughly, including edge cases with multiple occurrences and embedded contexts.
173-190: Excellent edge case coverage.The tests correctly verify PostgreSQL's behavior for:
- Unknown escape sequences (taken literally without the backslash)
- Incomplete hex escapes (falling back to literal 'x')
- Trailing backslashes (preserved as-is)
These edge cases align perfectly with the implementation logic.
192-212: Critical test: NULL marker handling is correct.The
test_null_marker_not_unescapedtest correctly verifies thatunescape_copy_text('\\N')returns'N', notNone. This is the proper design because converters must check for the NULL marker BEFORE unescaping, matching PostgreSQL'sCopyReadAttributesText()behavior.The combined escapes test ensures state is properly maintained across sequential escape sequences.
214-253: Robust integration testing with converters.These tests verify the critical interaction between unescaping and converter logic:
- NULL marker precedence: Both converters correctly check for
\NBEFORE unescaping (lines 220-221, 252)- Escape-separator interaction: Line 238-241 excellently tests that escaped tabs within field data (
\t) don't interfere with actual tab field separators- Type conversion order: Line 251 confirms SmartDataConverter unescapes before type detection, ensuring escape sequences don't break type inference
80-253: Outstanding test coverage for PostgreSQL COPY escape sequences.This test suite demonstrates excellent software engineering:
- Comprehensive: All documented escape sequences plus edge cases
- Well-organized: Logical grouping from basic to complex scenarios
- Integration-focused: Tests both the function and its use in converters
- PostgreSQL-aligned: Explicitly matches PostgreSQL's behavior, including the critical NULL-marker-before-unescaping sequence
The 16 new tests bring the total to 121, all passing. This is production-ready code.
Summary
This PR implements full support for PostgreSQL COPY text format escape sequences, resolving issue #7. The implementation is based directly on PostgreSQL's canonical source code.
Changes
Core Implementation
unescape_copy_text()function that handles all PostgreSQL COPY escape sequences:\b,\f,\n,\r,\t,\v- single character escapes\NNN- octal byte values (1-3 digits)\xNN- hex byte values (1-2 digits)\\- backslash\X- any other character taken literallyConverter Updates
DataConverterto unescape data after splitting by tabsSmartDataConverterto unescape before type conversion\N) is checked before unescaping (matching PostgreSQL behavior)Testing
Added comprehensive test suite with 16 new test cases:
Documentation
Implementation Details
The implementation is a direct port of PostgreSQL's escape sequence handling from:
This ensures 100% compatibility with how PostgreSQL itself handles escape sequences in COPY text format.
Testing
All 121 tests pass, including:
Breaking Changes
None - this is purely additive functionality. Previously, escape sequences were returned as literal strings (e.g.,
"\\n"as two characters). Now they are properly unescaped to their actual values (e.g.,"\n"as a newline character).This matches PostgreSQL's behavior and is the expected behavior for COPY text format.
Resolves #7
🤖 Generated with Claude Code
Summary by CodeRabbit
Bug Fixes
New Features
Tests
✏️ Tip: You can customize this high-level summary in your review settings.