Skip to content

Conversation

@alecgrieser
Copy link
Collaborator

@alecgrieser alecgrieser commented Dec 17, 2025

This begins the work necessary to fix internal errors related to VersionValue queries. The ultimate goal is to remove VersionValue, but for now, what this does is it modifies the way the various operators that sit on top of record fetches and has them copy out the version into a field, called __ROW_VERSION. Once that's done, then in a future version (say, 4.9), we'll be able to modify the PlanGenerator so that it references that field. Note that we'll need to wait for that rather than do it immediately for backwards compatibility reasons. If we took the final version right away, then any plan that wants the version when sent to an older version that doesn't do this copying will be unable to process that FieldValue.

This is in furtherance of #3796.

This also fixes #3734. This is because the type repository updates that are done to make the new copied-to types present in the type repository also put in the enum fields that were previously missing (and causing #3734).

This begins the work necessary to fix internal errors related to `VersionValue` queries. The ultimate goal is to remove `VersionValue`, but for now, what this does is it modifies the way the various operators that sit on top of record fetches and has them copy out the version into a field, called `__ROW_VERSION`. Once that's done, then in a future version (say, 4.9), we'll be able to modify the `PlanGenerator` so that it references that field. Note that we'll need to wait for that rather than do it immediately for backwards compatibility reasons. If we took the final version right away, then any plan that wants the version when sent to an older version that doesn't do this copying will be unable to process that `FieldValue`. This is in furtherance of FoundationDB#3796.
@alecgrieser alecgrieser added the bug fix Change that fixes a bug label Dec 17, 2025
return ImmutableSet.of(p.getResultType());
final Type resultType = p.getResultType();
if (!resultType.isPrimitive() && !resultType.isUuid()) {
return ImmutableSet.of(resultType);
Copy link
Contributor

Choose a reason for hiding this comment

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

Did this really return a type even if the type is not a record? I guess something using the set that is returned here would then filter it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I'm not sure I follow. This returns the type only if it's a record or an enum. Primitive types (and UUIDs) get filtered out. This is necessary so that the type repository has the type when it goes to copy it. It also fixed #3734

Copy link
Contributor

Choose a reason for hiding this comment

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

This method only really needs to return types that are Type.Record. Before your change, it seems it added primitive types as well which made me think about what happens here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ah. I believe that before, because it was basing its choice on whether the type was a CreatesDynamicTypesValue, which was mostly RecordConstructorValue and AbstractArrayConstructorValue. So, it would have returned a primitive if that was somehow the return type of one of those (like, say, PromoteValue). That no longer works as more things require dynamic types. No one is actually using that marker interface any more, so it could be removed, I suppose.


@Nonnull
private static Map<String, Descriptors.FieldDescriptor> getFieldDescriptorMap(@Nonnull final Stream<RecordType> recordTypeStream) {
public Type.Record getPlannerType(@Nonnull Collection<String> recordTypeNames) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't we cache this?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It would be nice (or at least, it would be nice to cache the single-type variant returned by getPlannerType(String)). My initial version of this constructed these structures during RecordMetaData.build and then just retrieves it from a Map. However, the descriptor to Type.Record logic cannot handle cyclic type relationships (e.g., a type X with a field of type Y which has a field of type X), and there are (it turns out) a bunch of tests in our code base with that kind of structure.

We could make this a memoized thing, and then we'd avoid re-creating the same object multiple times. We weren't doing that before, though, in the old code path, so this is at least treading water.

Copy link
Contributor

Choose a reason for hiding this comment

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

However, the descriptor to Type.Record logic cannot handle cyclic type relationships (e.g., a type X with a field of type Y which has a field of type X), and there are (it turns out) a bunch of tests in our code base with that kind of structure.

How is that possible? Type is immutable so how can you create those cyclic types. Can you point to a test case that does this?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

In some sense, it's the immutability of Type that is the problem. We have test cases like UnnestedRecordTypeTest.unnestDoubleNestedMapType that use test_records_double_nested.proto as the basis of their meta-data definition: https://github.com/FoundationDB/fdb-record-layer/blob/main/fdb-record-layer-core/src/test/proto/test_records_double_nested.proto

Note that the types in that file (like, say MiddleRecord, which has a field of type MiddleRecord) have a cyclic graph. I don't think you can construct a Type.Record that models this correctly, and if you try to, you get a StackOverflowError. FWIW, I think you would always have gotten a stack overflow error if you tried to use cascades to query this type, and trying to do that eagerly just overturned the bug

1. The normalize fields method was resetting any field numbers if they were already set. This can cause `deepCopy` problems as the message structures may not align if the original types had out of order field numbers 1. Update could get weird. It expects the old and new messages to have the same format, which means that either they both needed to reference pseudo-fields or they both needed to not. However, the way it was structured, we could wind up placing two types with the same name (one with and one without the pseudo-fields) into the type repository. This adjusts this to not re-use the type, but we may have to think about what that means for the result set meta-data
@alecgrieser alecgrieser force-pushed the 03796-be-explicit-about-versions-prelim branch from 9824821 to 50f02fc Compare December 19, 2025 17:31
@github-actions
Copy link

📊 Metrics Diff Analysis Report

Summary

  • New queries: 52
  • Dropped queries: 13
  • Plan changed + metrics changed: 21
  • Plan unchanged + metrics changed: 16
ℹ️ About this analysis

This automated analysis compares query planner metrics between the base branch and this PR. It categorizes changes into:

  • New queries: Queries added in this PR
  • Dropped queries: Queries removed in this PR. These should be reviewed to ensure we are not losing coverage.
  • Plan changed + metrics changed: The query plan has changed along with planner metrics.
  • Metrics only changed: Same plan but different metrics

The last category in particular may indicate planner regressions that should be investigated.

New Queries

Count of new queries by file:

  • yaml-tests/src/test/resources/pseudo-field-clash.metrics.yaml: 19
  • yaml-tests/src/test/resources/versions-tests.metrics.yaml: 33

Dropped Queries

The following queries with metrics were removed:

The reviewer should double check that these queries were removed intentionally to avoid a loss of coverage.

Plan and Metrics Changed

These queries experienced both plan and metrics changes. This generally indicates that there was some planner change
that means the planning for this query may be substantially different. Some amount of query plan metrics change is expected,
but the reviewer should still validate that these changes are not excessive.

Total: 21 queries

Statistical Summary (Plan and Metrics Changed)

task_count:

  • Average change: -60.1
  • Median change: -31
  • Standard deviation: 41.0
  • Range: -112 to -6
  • Queries changed: 17
  • No regressions! 🎉

transform_count:

  • Average change: -9.2
  • Average regression: +2.0
  • Median change: -5
  • Median regression: +2
  • Standard deviation: 7.3
  • Standard deviation of regressions: 0.0
  • Range: -21 to +2
  • Range of regressions: +2 to +2
  • Queries changed: 17
  • Queries regressed: 1

transform_yield_count:

  • Average change: -4.4
  • Median change: -3
  • Standard deviation: 2.4
  • Range: -8 to -2
  • Queries changed: 14
  • No regressions! 🎉

insert_new_count:

  • Average change: -5.3
  • Median change: -3
  • Standard deviation: 4.7
  • Range: -14 to -1
  • Queries changed: 21
  • No regressions! 🎉

insert_reused_count:

  • Average change: +2.4
  • Average regression: +2.4
  • Median change: +2
  • Median regression: +2
  • Standard deviation: 1.6
  • Standard deviation of regressions: 1.6
  • Range: +1 to +5
  • Range of regressions: +1 to +5
  • Queries changed: 14
  • Queries regressed: 14

There were no queries with significant regressions detected.

Minor Changes (Plan and Metrics Changed)

In addition, there were 21 queries with minor changes.

Only Metrics Changed

These queries experienced only metrics changes without any plan changes. If these metrics have substantially changed,
then a planner change has been made which affects planner performance but does not correlate with any new outcomes,
which could indicate a regression.

Total: 16 queries

Statistical Summary (Only Metrics Changed)

task_count:

  • Average change: -71.2
  • Median change: -53
  • Standard deviation: 50.4
  • Range: -234 to -32
  • Queries changed: 16
  • No regressions! 🎉

transform_count:

  • Average change: -11.0
  • Median change: -9
  • Standard deviation: 6.4
  • Range: -30 to -6
  • Queries changed: 16
  • No regressions! 🎉

transform_yield_count:

  • Average change: -3.8
  • Median change: -3
  • Standard deviation: 2.7
  • Range: -12 to -1
  • Queries changed: 16
  • No regressions! 🎉

insert_new_count:

  • Average change: -7.2
  • Median change: -5
  • Standard deviation: 5.0
  • Range: -23 to -3
  • Queries changed: 16
  • No regressions! 🎉

insert_reused_count:

  • Average change: +2.3
  • Average regression: +2.3
  • Median change: +2
  • Median regression: +2
  • Standard deviation: 1.0
  • Standard deviation of regressions: 1.0
  • Range: +1 to +5
  • Range of regressions: +1 to +5
  • Queries changed: 16
  • Queries regressed: 16

Significant Regressions (Only Metrics Changed)

There was 1 outlier detected. Outlier queries have a significant regression in at least one field. Statistically, this represents either an increase of more than two standard deviations above the mean or a large absolute increase (e.g., 100).

Minor Changes (Only Metrics Changed)

In addition, there were 15 queries with minor changes.

@alecgrieser alecgrieser added the DO NOT MERGE do not merge label Dec 19, 2025
@alecgrieser
Copy link
Collaborator Author

Adding DO NOT MERGE. We have some things regarding the way type names are handled that we may want to work out. Once that's agreed upon, we'll want to take this, possibly with some adjustments for the type naming

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug fix Change that fixes a bug DO NOT MERGE do not merge

2 participants