Skip to content

fix(spark): read shredded variants through the CDC and legacy streaming paths, and restore partition values there - #19583

Merged
voonhous merged 5 commits into
apache:masterfrom
voonhous:fix-19578-streaming-cdc
Aug 13, 2026
Merged

fix(spark): read shredded variants through the CDC and legacy streaming paths, and restore partition values there#19583
voonhous merged 5 commits into
apache:masterfrom
voonhous:fix-19578-streaming-cdc

Conversation

@voonhous

@voonhous voonhous commented Aug 11, 2026

Copy link
Copy Markdown
Member

Describe the issue this Pull Request addresses

Closes #19578, and fixes #19594.

#19578 asked for streaming and CDC round-trip coverage of shredded variant reads; writing the coverage surfaced three real defects on those paths, all of the #19556 null-read family (requesting native VariantType against a shredded parquet base file clips the shredded group to {metadata, value} and reads value=null). Review of the fix surfaced a fourth, unrelated defect on the same RDD, filed as #19594 and fixed here.

  1. HoodieMergeOnReadRDDV2 base-only splits (dataFileOnlySplit) bypass the internal reader context entirely and read through the relation's plain skip-merging base reader, so a shredded base file with no log files read null variants. Hit by the legacy (hoodie.file.group.reader.enabled=false) streaming/relation path, e.g. any streaming batch over a freshly compacted slice.
  2. CDCFileGroupIterator's BASE_FILE_INSERT case reads the new base file directly with the plain table schema, bypassing the context, so CDC after-images of insert commits read null variants. Hit under every supplemental logging mode.
  3. InternalRowToJsonStringConverter (CDC before/after images) had no VariantType handling, so a variant column fell through to Jackson bean serialization of the raw VariantVal bytes instead of the variant's JSON.
  4. [BUG] Legacy MOR read path returns null partition columns when they are not persisted in the data files #19594: HoodieMergeOnReadRDDV2's file-group-reader branch never appended partition values, so partition columns read back NULL whenever they are not persisted in the data files (drop.partition.columns, read-side extraction from the partition path, bootstrap data-queries-only). Pre-existing and independent of variants. It is fixed here rather than deferred because fix 1 re-routes base-only splits onto that same branch and would otherwise have widened the gap; carving it out would mean either shipping that regression or keeping a carve-out no test could reach.

Summary and Changelog

  • HoodieMergeOnReadRDDV2: splits whose required schema has variant columns skip the plain fast reader and take the file-group reader branch, whose reader context requests the full-variant projection shape (fix(spark): read shredded variants through internal write-side parquet reads #19558). Gated on SparkAdapter.buildFullVariantReadSchema(...).isDefined, which is None below Spark 4.1 where the file-group reader would read the same nulls. Base-only splits without variants are unchanged.
  • HoodieMergeOnReadRDDV2: the file-group-reader branch now appends partition values. The split carries the values parsed on the driver by the same routine the skip-merging reader relies on (off a log file's path when the slice has no base file), and they are bound in with one per-split UnsafeProjection over a JoinedRow. The output row shape was already correct -- TableSchemaResolver re-appends dropped partition columns, so the reader was merely filling them with nulls -- so this is a value substitution at existing ordinals, with no column added, moved or dropped. Inert unless it applies: no values on the split when the columns live in the data files, an empty ordinal array for a non-partitioned table (including the metadata table, whose Avro leg is untouched), and -1 for unprojected columns.
  • SparkFileFormatInternalRowReaderContext: the full-variant rewrite and restore projection are factored into reusable helpers (fullVariantReadSchemaWithOrdinals, variantRestoreProjection); behavior of the context itself is unchanged.
  • CDCFileGroupIterator: BASE_FILE_INSERT applies the same rewrite/restore around its direct base-file read (with a defensive copy, since the restore projection reuses its output buffer).
  • InternalRowToJsonStringConverter: variant columns are embedded into before/after images as real JSON nodes via VariantVal.toString, so get_json_object(after, '$.v.key') works as expected. The column is matched on DataType.typeName, not SparkAdapter.isVariantType: the guard runs for every non-string/array/map/struct field, and resolving the adapter needs a version module that is not on hudi-spark-common's own test classpath. readTree falls back to the raw rendering on JsonProcessingException, which is how a variant exceeding Jackson's default StreamReadConstraints (50k field names, 20M strings, 1000 nesting levels -- all reachable inside the variant size limit) arrives; VariantVal.toString stays outside that block so MALFORMED_VARIANT on corrupt bytes still surfaces.

Tests:

  • CDC round trip over a COW table in TestVariantDataType, sweeping shredded and unshredded layouts against OP_KEY_ONLY and DATA_BEFORE_AFTER, with the on-disk layout pinned per leg. Pinned to HoodieRecordType.SPARK, since a cdc table always writes through FileGroupReaderBasedMergeHandle and the merger's record type picks that handle's reader context; the AVRO leg's shredded base-file read is the separate defect tracked as [BUG] CoW small-file merge silently nulls shredded VARIANT values via the avro reader path #19567.
  • Legacy-path MOR streaming round trip in TestStreamingSource over a compacted shredded base file, asserting the executed plan is the RDD-backed Scan ExistingRDD so it cannot pass through a silent fallback to the file-group reader. One stream covers the base-only and log-only splits; a second, with its own checkpoint, replays from INIT so its first batch spans the compaction commit and the update together, which is the only way to land a base + log slice on this path.
  • MOR reads over a drop.partition.columns table in TestLegacyParquetReadPath, through the directly-constructed legacy relations. The snapshot case uses a partial upsert so one file group merges base plus log while the others stay base-only, exercising the merging branch and the fast path in one query; verified red before fix 4 and green after. A second case covers a log-only slice.
  • TestInternalRowToJsonStringConverter covers the JSON embedding and the constraints fallback. Before fix 3's detection change, 13 of its 14 cases errored with ClassNotFoundException: Spark4_2Adapter.

Impact

Wrong-results fixes on non-default or CDC paths, plus one image-format improvement. CDC after-images of insert commits and legacy-path reads of base-only shredded slices returned null variants before this change; legacy-path MOR reads of a table whose partition columns live only in the partition path returned null partition columns. CDC images of variant columns now carry the variant's JSON structure; previously they carried a Jackson bean rendering of the raw bytes, which no reader could reasonably consume, so this is considered a fix rather than a breaking format change.

One deliberate behaviour change to call out: with hoodie.datasource.read.extract.partition.values.from.path=true on a table that does persist its partition columns, this branch previously returned the file-persisted value and now returns the partition-path-parsed one. That matches the skip-merging reader, the file-format path and the documented flag semantics, including the TimestampBasedKeyGenerator caveat noted on HoodieBaseRelation.shouldExtractPartitionValuesFromPartitionPath.

Risk Level

low-medium. The variant routing change only triggers when the schema has variant columns and the adapter supports the rewrite; the CDC direct-read rewrite only rewrites on Spark 4.1+; the image converter change only affects variant columns. Fix 4 touches a shared read path, but HoodieMergeOnReadRDDV2 runs only for legacy MOR streaming (hoodie.file.group.reader.enabled=false) and directly-constructed relations -- batch MOR always scans through HoodieFileGroupReaderBasedFileFormat, which appends partition values itself -- and it is a no-op unless the partition columns are absent from the data files.

Documentation Update

none

Contributor's checklist

  • Read through contributor's guide
  • Enough context is provided in the sections above
  • Adequate tests were added if applicable
…ng paths

Writing the coverage apache#19578 asked for surfaced three defects of the apache#19556
null-read family on the query paths that do not go through catalyst:

- HoodieMergeOnReadRDDV2 base-only splits bypass the internal reader context
  and read through the plain skip-merging base reader, so a shredded base
  file with no logs read null variants on the legacy
  (hoodie.file.group.reader.enabled=false) streaming/relation path. Splits
  with variant columns now take the file-group reader branch instead.
- CDCFileGroupIterator's BASE_FILE_INSERT case reads the new base file
  directly with the plain table schema, so CDC after-images of insert
  commits read null variants under every supplemental logging mode. The
  direct read now applies the same full-variant rewrite and restore as the
  reader context (helpers factored out of
  SparkFileFormatInternalRowReaderContext; context behavior unchanged).
- InternalRowToJsonStringConverter had no VariantType case, so CDC images
  serialized the VariantVal bean (raw bytes as base64) instead of the
  variant's JSON. Variant columns now embed as real JSON nodes via
  VariantVal.toString.

Tests: CDC round trip over a shredded COW table (insert + update,
OP_KEY_ONLY and DATA_BEFORE_AFTER, shredded layout pinned; single-row table
so the in-flight avro merge-path fix apache#19582 is not a prerequisite), and a
legacy-path MOR streaming round trip covering both the base-only and merged
split branches over a compacted shredded base file.

@hudi-agent hudi-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ 🤖 This review was generated by an AI agent and may contain mistakes. Please verify any suggestions before applying.

Thanks for working on this! This PR fixes shredded-variant null reads on three Spark paths that bypass the internal reader context — the legacy MOR base-only split, CDC's BASE_FILE_INSERT direct base-file read, and the CDC before/after JSON image converter — by factoring the full-variant rewrite/restore into shared helpers and applying them consistently. No issues flagged from this automated pass — a Hudi committer or PMC member can take it from here for a final review.

cc @yihua

@voonhous

Copy link
Copy Markdown
Member Author

@wombatu-kun Can you please help to review this PR too? Thank you.

@github-actions github-actions Bot added the size:M PR with lines of changes in (100, 300] label Aug 11, 2026
@codecov-commenter

codecov-commenter commented Aug 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 84.12698% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 77.53%. Comparing base (3ba31dd) to head (b78ba3d).
⚠️ Report is 4 commits behind head on master.

Files with missing lines Patch % Lines
...scala/org/apache/hudi/HoodieMergeOnReadRDDV2.scala 84.00% 0 Missing and 4 partials ⚠️
...he/hudi/cdc/InternalRowToJsonStringConverter.scala 66.66% 1 Missing and 2 partials ⚠️
...ala/org/apache/hudi/cdc/CDCFileGroupIterator.scala 81.81% 0 Missing and 2 partials ⚠️
...hudi/SparkFileFormatInternalRowReaderContext.scala 88.88% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master   #19583      +/-   ##
============================================
+ Coverage     77.49%   77.53%   +0.03%     
- Complexity    32799    32887      +88     
============================================
  Files          2522     2524       +2     
  Lines        139179   139389     +210     
  Branches      16734    16781      +47     
============================================
+ Hits         107855   108071     +216     
- Misses        23748    23753       +5     
+ Partials       7576     7565      -11     
Components Coverage Δ
hudi-common 83.27% <ø> (+<0.01%) ⬆️
hudi-client 82.73% <88.88%> (+0.02%) ⬆️
hudi-flink 85.75% <ø> (+<0.01%) ⬆️
hudi-spark-datasource 70.72% <83.33%> (+0.11%) ⬆️
hudi-utilities 73.67% <ø> (+0.02%) ⬆️
hudi-cli 15.32% <ø> (ø)
hudi-hadoop 68.96% <ø> (+0.01%) ⬆️
hudi-sync 75.58% <ø> (+0.46%) ⬆️
hudi-io 79.37% <ø> (-0.10%) ⬇️
hudi-timeline-service 83.44% <ø> (ø)
hudi-cloud 64.06% <ø> (ø)
hudi-kafka-connect 53.20% <ø> (ø)
Flag Coverage Δ
common-and-other-modules 50.87% <0.00%> (+0.03%) ⬆️
flink-integration-tests 49.17% <ø> (-0.10%) ⬇️
hadoop-mr-java-client 43.86% <ø> (+0.02%) ⬆️
integration-tests 13.60% <0.00%> (-0.03%) ⬇️
spark-client-hadoop-common 50.46% <0.00%> (+<0.01%) ⬆️
spark-java-tests 51.67% <69.35%> (-0.02%) ⬇️
spark-scala-tests 46.06% <70.96%> (-0.02%) ⬇️
utilities 36.59% <30.15%> (-0.06%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...ain/scala/org/apache/hudi/HoodieBaseRelation.scala 79.49% <100.00%> (+0.06%) ⬆️
.../org/apache/hudi/MergeOnReadSnapshotRelation.scala 89.58% <100.00%> (+0.69%) ⬆️
...hudi/SparkFileFormatInternalRowReaderContext.scala 75.82% <88.88%> (+1.54%) ⬆️
...ala/org/apache/hudi/cdc/CDCFileGroupIterator.scala 79.07% <81.81%> (-0.04%) ⬇️
...he/hudi/cdc/InternalRowToJsonStringConverter.scala 72.41% <66.66%> (+0.41%) ⬆️
...scala/org/apache/hudi/HoodieMergeOnReadRDDV2.scala 79.53% <84.00%> (+13.53%) ⬆️

... and 25 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
Main code:
- HoodieMergeOnReadRDDV2: gate the base-only re-route on
  sparkAdapter.buildFullVariantReadSchema(...).isDefined instead of the mere
  presence of a variant column. It is None below Spark 4.1, where the file
  group reader reads the same nulls, so re-routing only lost the fast path.
- HoodieMergeOnReadRDDV2: keep the fast path for splits carrying partition
  values parsed off the partition path. Only requiredSchemaReaderSkipMerging
  appends those (drop.partition.columns, extract-from-path, bootstrap fast
  read); the file group reader branch builds its PartitionedFile with
  InternalRow.empty and an empty partition schema, so re-routing such a split
  would trade null variants for null partition columns. The same gap on the
  merged branch predates this change and is left for a follow-up.
- InternalRowToJsonStringConverter: match the variant column on
  dt.typeName rather than SparkAdapter.isVariantType. The guard runs for every
  non-string/array/map/struct field, and resolving the adapter needs a version
  module that is absent from hudi-spark-common's own test classpath: 13 of the
  14 TestInternalRowToJsonStringConverter cases errored with
  ClassNotFoundException: Spark4_2Adapter. Also fall back to the raw rendering
  when readTree rejects the variant JSON (a non-finite double renders as a bare
  NaN/Infinity token), so a CDC query degrades instead of failing.

Tests:
- TestInternalRowToJsonStringConverter: cover the JSON embedding and the
  malformed-JSON fallback; suite goes 14/14 (was 13 errors before the fix).
- TestStreamingSource: write through format("hudi"), not the fully qualified
  name. Only Spark4DefaultSource overrides supportsDataType to accept
  VariantType and it is reachable solely via the registered short name, so the
  write was dying with UNSUPPORTED_DATA_TYPE_FOR_DATASOURCE before the read.
  Add the testLegacyIncrementalStreamSource plan guard so the test cannot pass
  through a silent fallback to the file group reader. Correct the coverage
  comment (the second batch is a log-only split, not a merged one) and add a
  second testStream whose own checkpoint replays from INIT, which is what
  actually produces a base + log slice on this path.
- TestVariantDataType: sweep the CDC round trip over shredded and unshredded
  layouts rather than adding a second copy, so the rewrite branch is covered on
  both sides. Pin the test to HoodieRecordType.SPARK: a cdc table always writes
  through FileGroupReaderBasedMergeHandle, whose reader context follows the
  merger's record type, and the AVRO leg's shredded base-file read is the
  separate defect tracked as apache#19567/apache#19582. The previous claim that a single-row
  table removed that prerequisite was wrong: what fails is the CDC before image
  being written with value=null, which does not depend on carried-over rows.

Verified on Spark 4.1: TestVariantDataType 13/13 (2 cancelled as Spark-3 only),
TestStreamingSource 15/15. TestStreamingSource's suite-level leaked-file-stream
abort reproduces identically on the pre-PR revision, so it is not from this
change.

@hudi-agent hudi-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ 🤖 This review was generated by an AI agent and may contain mistakes. Please verify any suggestions before applying.

Thanks for the follow-up work here! This PR routes shredded-variant reads correctly through the legacy MOR streaming and CDC paths, fixing three null-read defects, and factors the full-variant rewrite/restore into shared helpers reused by the CDC BASE_FILE_INSERT direct read. I traced the reroute guard in HoodieMergeOnReadRDDV2, the copy-before-buffer handling in CDCFileGroupIterator, and the recursive variant handling in InternalRowToJsonStringConverter; the changed paths look internally consistent and the correctness concerns I can identify have all already been raised and addressed in the prior rounds. No new issues flagged from this automated pass — a Hudi committer or PMC member can take it from here for a final review.

cc @yihua

@github-actions github-actions Bot added size:L PR with lines of changes in (300, 1000] and removed size:M PR with lines of changes in (100, 300] labels Aug 12, 2026
…branch

HoodieMergeOnReadRDDV2's file-group-reader branch sourced every projected
column from the data files, so it returned NULL partition columns whenever
those columns are not persisted there: drop.partition.columns, read-side
extraction from the partition path, or a bootstrap data-queries-only read.
SparkFileFormatInternalRowReaderContext builds its PartitionedFile with
InternalRow.empty and an empty partition schema, and the builder's
withPartitionPath only feeds the bootstrap merge, so nothing injected them.
Only the skip-merging fast path ever appended them, via appendPartitionValues.

The output row shape was already correct -- TableSchemaResolver re-appends
dropped partition columns to the table schema, so the required schema keeps
them and the reader merely fills them with nulls -- which makes this a value
substitution at existing ordinals, with no column added, moved or dropped.

- Carry the parsed values on HoodieMergeOnReadFileSplit, resolved on the driver
  by the same routine the skip-merging reader relies on. A log-only slice has no
  base file to carry them, so they come off a log file's path; that uses
  HoodieLogFile#getPath, since pathInfo is transient and null there.
- Splice them in with one per-split UnsafeProjection over a JoinedRow. Bound
  references rather than literals: literals are inlined into generated code, so
  every distinct partition value would miss Spark's codegen cache. Mirrors
  HoodieFileGroupReaderBasedFileFormat.appendPartitionAndProject.
- No-op unless it has to run: the split carries no values when the columns are
  read from the data files, the ordinal array is empty for a non-partitioned
  table (including the metadata table), and unprojected partition columns
  resolve to -1. The metadata-table Avro leg is untouched.

With the branch fixed, the base-only re-route added earlier in this PR no
longer has to avoid it, so its partition-value escape hatch is dropped and
splits with shredded variant columns re-route unconditionally.

Reachable today only from legacy MOR streaming (hoodie.file.group.reader.enabled
=false); batch MOR always scans through HoodieFileGroupReaderBasedFileFormat,
which appends partition values itself.

Tests, in TestLegacyParquetReadPath since it already constructs the legacy
relations directly:
- MOR snapshot over drop.partition.columns with a partial upsert, so one file
  group merges base plus log while the others stay base-only. Verified red
  before this change (base-served p0 rows came back null) and green after.
- MOR incremental over a log-only slice. This one passes with or without the
  splicing above, so it is not a repro: its rows come from log records, which do
  carry the partition column because HUDI-6926 makes a MOR upsert ignore
  drop.partition.columns. It pins the log-only resolution off
  HoodieLogFile#getPath and agreement with the file-group-reader oracle. Noted
  as such in the test.
Round-2 review pointed out the stated trigger was wrong, and it is: Spark
guards the double and float arms of Variant.toJsonImpl with isFinite and sends
the non-finite one through appendQuoted, so a variant renders NaN and Infinity
QUOTED and readTree never sees a bare token. Verified against the bytecode and
by rendering hand-built variants on Spark 4.0.2 and 4.1.1.

The fallback still earns its place, for a different reason. Jackson's default
StreamReadConstraints cap field names at 50k chars, strings at 20M and nesting
at 1000 levels, while a variant may hold all three well inside its own 128MB
limit, and castToVariant applies no such gate. All three were reproduced and
all three arrive as StreamConstraintsException, a JsonProcessingException, so
the existing catch covers them.

- Reword the comment to name the real triggers.
- Record why value.toString stays outside the try: it throws MALFORMED_VARIANT
  (a SparkRuntimeException, which JsonProcessingException does not cover) on
  corrupt bytes. That is a data-integrity signal an operator needs, not a
  rendering quirk to swallow into a CDC image, and there would be no rendering
  left to fall back to anyway.
- Retarget the test, which asserted a rendering production cannot produce, onto
  over-deep nesting, and rename it accordingly. It expects the value as a JSON
  string, so it only passes when the fallback actually fires.
@voonhous voonhous changed the title fix(spark): read shredded variants through the CDC and legacy streami… Aug 12, 2026

@hudi-agent hudi-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ 🤖 This review was generated by an AI agent and may contain mistakes. Please verify any suggestions before applying.

Thanks for working on this! The PR routes shredded-variant reads through the CDC and legacy MOR streaming/relation paths (HoodieMergeOnReadRDDV2, CDCFileGroupIterator, InternalRowToJsonStringConverter) and restores partition values on the file-group-reader branch. I traced the partition-value splicing (ordinal/JoinedRow binding, the empty/non-projected guards, and the checkState), the variant reroute condition, the CDC BASE_FILE_INSERT rewrite+restore+copy, and the null/exception handling in the JSON converter, cross-checking ordering against getPartitionFields and the file-group-reader oracle used by the new tests. No new correctness issues flagged from this automated pass beyond what earlier rounds already covered — a Hudi committer or PMC member can take it from here for a final review.

cc @yihua

Round-3 review. The write-side gap is real: Jackson enforces a nesting cap on
output too (StreamWriteConstraints, also 1000 levels), inside convert's
writeValueAsString and therefore outside the branch's catch, so a variant deep
enough to clear the read limit but not the write limit once the image's own
object levels are added still failed the query. Embedding the validated
rendering as a RawValue closes it: that goes out through
JsonGenerator.writeRawValue, which keeps no nesting context at all.

Measured while confirming this, on jackson 2.18.2/2.20.0/2.21.2: the window is
one level per enclosing object for an object-rendered variant and one more for
an array-rendered one, because jackson-core's WriterBasedJsonGenerator
validates the PARENT depth in writeStartObject(Object) and so tolerates one
extra level. A variant at the top of the image rendering as an array fails at
exactly depth 1000, which is what the new test pins.

RawValue is byte-identical to the parsed-node path for every rendering
Variant.toJson can produce -- checked against the real renderer across
decimals, doubles, escaping, key order and all scalar shapes -- and it drops a
parse-then-rebuild round trip. It does mean unvalidated text would be spliced
verbatim, so the gate is tightened to match:

- FAIL_ON_TRAILING_TOKENS on the mapper, since readTree otherwise parses a
  valid prefix and leaves the rest unconsumed;
- an explicit MissingNode check, since readTree answers blank input with
  MissingNode rather than throwing, and RawValue would emit nothing at all.

Neither is producible by Variant.toJson, which always emits exactly one
complete value; they are defence in depth for the verbatim embed.

Also, from the same review:
- MergeOnReadSnapshotRelation: the split's partition values are non-empty on
  any of shouldExtractPartitionValuesFromPartitionPath's three triggers, not
  only when the columns are omitted from the data files -- the extract-from-path
  read option applies to tables that persist them too. Comment corrected.
- TestVariantDataType: the compaction test still pointed at apache#19578 as tracking
  the no-catalyst-schema legs, which this PR closes and covers. Repointed at the
  CDC round trip and the TestStreamingSource test.

Verified: TestInternalRowToJsonStringConverter 16/16, with the depth test red
before the RawValue change (StreamWriteConstraints exceeded) and green after;
TestVariantDataType 13/13, so the CDC round trip still reads its images back
through get_json_object.
@voonhous

Copy link
Copy Markdown
Member Author

@wombatu-kun Ready for another round of review.

@hudi-bot

Copy link
Copy Markdown
Collaborator

CI report:

Bot commands @hudi-bot supports the following commands:
  • @hudi-bot run azure re-run the last Azure build
@voonhous
voonhous merged commit b1639e0 into apache:master Aug 13, 2026
39 of 40 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L PR with lines of changes in (300, 1000]

5 participants