perf: Improve record and schema flattening performance#3687
Conversation
Reviewer's GuideOptimizes schema and record flattening performance by hoisting reusable constructs to module scope, switching from list-based to string-based key prefix handling with a fallback path for long keys, avoiding in-place schema mutations, and replacing sort-and-group duplicate detection with linear set-based checks while preserving behavior and snapshots. Sequence diagram for record flattening with parent_prefix and JSON dump checksequenceDiagram
participant Caller
participant flatten_record
participant _flatten_record
participant flatten_key
participant _should_jsondump_value
Caller->>flatten_record: flatten_record(record, flattened_schema, separator, max_level, max_key_length)
flatten_record->>_flatten_record: _flatten_record(record_node=record, flattened_schema=flattened_schema, parent_prefix="", separator, level=0, max_level, max_key_length)
loop for each k, v in record_node.items()
_flatten_record->>_flatten_record: Compute new_key = parent_prefix + k
_flatten_record->>_flatten_record: [check len(new_key) >= max_key_length]
alt long_key
_flatten_record->>_flatten_record: Reconstruct parent_keys from parent_prefix
_flatten_record->>flatten_key: flatten_key(k, parent_keys, separator, max_key_length)
flatten_key-->>_flatten_record: abbreviated key
end
_flatten_record->>_flatten_record: [decide recurse vs. append based on type and flattened_schema]
_flatten_record->>_flatten_record: [build child parent_prefix = parent_prefix + k + separator on recursion]
end
_flatten_record-->>Caller: flattened_record
Caller->>_should_jsondump_value: _should_jsondump_value(key, value, flattened_schema)
_should_jsondump_value->>_should_jsondump_value: Check frozenset(flattened_schema[key]["type"]) == _NULL_OBJECT_ARRAY_TYPES
_should_jsondump_value-->>Caller: bool
Flow diagram for incremental key flattening with fallback and duplicate detectionflowchart TD
A["_flatten_schema start"] --> B["Iterate schema_node.properties"]
B --> C["Compute new_key = parent_prefix + field_name"]
C --> D{"len(new_key) >= max_key_length"}
D -- "no" --> E["Use new_key as-is"]
D -- "yes" --> F["Reconstruct parent_keys from parent_prefix"]
F --> G["Call flatten_key(field_name, parent_keys, separator, max_key_length)"]
G --> H["Set new_key from flatten_key result"]
E --> I{"field_schema.type indicates nested object and level < max_level"}
H --> I
I -- "yes" --> J["Compute next_prefix = parent_prefix + field_name + separator"]
J --> K["Recurse _flatten_schema(field_schema, parent_prefix=next_prefix, ...)"]
I -- "no" --> L{"first_element.type in composite"}
L -- "string" --> M["Append (new_key, {**first_element, type=['null', 'string']})"]
L -- "array" --> N["Append (new_key, {**first_element, type=['null', 'array']})"]
L -- "object" --> O["Append (new_key, {**first_element, type=['null', 'object']})"]
L -- "other/typeless" --> P["Append (new_key, {type=['null', 'string']})"]
P --> Q["After loop: check duplicates with seen set"]
M --> Q
N --> Q
O --> Q
Q --> R{"key in seen"}
R -- "yes" --> S["Raise ValueError(duplicate column name)"]
R -- "no" --> T["Add key to seen"]
T --> U["Return dict(items)"]
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
frozenset({"null","object","array"})
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #3687 +/- ##
==========================================
- Coverage 94.21% 94.21% -0.01%
==========================================
Files 73 73
Lines 6208 6203 -5
Branches 763 761 -2
==========================================
- Hits 5849 5844 -5
Misses 267 267
Partials 92 92
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Merging this PR will improve performance by ×2.5
|
| Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|
| ⚡ | test_bench_flatten_schema_deep |
4,472.9 µs | 775.8 µs | ×5.8 |
| ⚡ | test_bench_flatten_schema_wide |
1,031.5 µs | 289.5 µs | ×3.6 |
| ⚡ | test_bench_flatten_key_short |
35.5 µs | 25.1 µs | +41.4% |
| ⚡ | test_bench_flatten_record_wide |
357.5 µs | 263 µs | +35.95% |
Tip
Curious why this is faster? Comment @codspeedbot explain why this is faster on this PR, or directly use the CodSpeed MCP with your agent.
Comparing perf/flattening (3b25ec7) with main (24237d2)
3a161eb to
6036d9e
Compare
3463696 to
9e586d4
Compare
|
@sourcery-ai review |
There was a problem hiding this comment.
Hey - I've left some high level feedback:
- The change from sorting items to returning them in insertion order in
_flatten_schemamay affect downstream consumers relying on deterministic sorted key order; consider either preserving sort or explicitly documenting this behavioral change. - The new
parent_prefixapproach assumes the prefix always ends with the separator when non-empty; it may be worth adding a small invariant check or helper to construct/validate prefixes to avoid subtle bugs if future changes accidentally omit the trailing separator.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The change from sorting items to returning them in insertion order in `_flatten_schema` may affect downstream consumers relying on deterministic sorted key order; consider either preserving sort or explicitly documenting this behavioral change.
- The new `parent_prefix` approach assumes the prefix always ends with the separator when non-empty; it may be worth adding a small invariant check or helper to construct/validate prefixes to avoid subtle bugs if future changes accidentally omit the trailing separator.Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
6036d9e to
be9c26d
Compare
9e586d4 to
47a0337
Compare
There was a problem hiding this comment.
Hey - I've found 1 issue, and left some high level feedback:
- The "rare slow path" logic for reconstructing parent keys from
parent_prefixis duplicated between_flatten_schemaand_flatten_record; consider extracting this into a small helper to keep the separator handling and edge cases centralized. - In
_flatten_schema,sep_len = len(separator)is computed for every call even though it is invariant within the function; you can move this out of the loop or compute it lazily only when the slow path is taken to avoid unnecessary work.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The "rare slow path" logic for reconstructing parent keys from `parent_prefix` is duplicated between `_flatten_schema` and `_flatten_record`; consider extracting this into a small helper to keep the separator handling and edge cases centralized.
- In `_flatten_schema`, `sep_len = len(separator)` is computed for every call even though it is invariant within the function; you can move this out of the loop or compute it lazily only when the slow path is taken to avoid unnecessary work.
## Individual Comments
### Comment 1
<location path="singer_sdk/helpers/_flattening.py" line_range="360" />
<code_context>
if "properties" not in schema_node:
return {}
+ sep_len = len(separator)
for field_name, field_schema in schema_node["properties"].items():
- new_key = flatten_key(
</code_context>
<issue_to_address>
**issue (bug_risk):** Reconstructing parent keys breaks when separator is an empty string.
This optimization assumes a non-empty `separator`. When `separator == ""`, `sep_len` is 0, `parent_prefix[:-sep_len]` is unchanged, and calling `.split("")` raises `ValueError`. The previous `parent_keys` approach worked regardless of separator length. Please either explicitly handle empty separators (e.g., reject them or skip this optimization) or adjust the reconstruction logic so it safely supports an empty separator.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
47a0337 to
3398663
Compare
Signed-off-by: Edgar Ramírez Mondragón <edgarrm358@gmail.com>
Signed-off-by: Edgar Ramírez Mondragón <edgarrm358@gmail.com>
Signed-off-by: Edgar Ramírez Mondragón <edgarrm358@gmail.com>
… of a list Signed-off-by: Edgar Ramírez Mondragón <edgarrm358@gmail.com>
3398663 to
b81c5fe
Compare
Documentation build overview
|
… instead of a list" This reverts commit b81c5fe.
frozenset({"null","object","array"})perf: Construct flattened field key incrementally as a string instead of a listnot worth itSummary by Sourcery
Improve performance of key flattening and JSON dumping by hoisting reusable constructs to module level.
Enhancements:
Summary by Sourcery
Optimize schema and record flattening to reduce overhead and improve performance.
Enhancements:
Tests: