GH-34138: [C++][Parquet] Fix parsing stats from min_value/max_value - #34112

Merged
wjones127 merged 2 commits into
apache:mainfrom
wgtmac:parse_min_max
Feb 17, 2023
Merged

GH-34138: [C++][Parquet] Fix parsing stats from min_value/max_value#34112
wjones127 merged 2 commits into
apache:mainfrom
wgtmac:parse_min_max

Conversation

@wgtmac

@wgtmacwgtmac commented Feb 10, 2023

Copy link
Copy Markdown
Member

Rationale for this change

The code below does not read from stats.min_value/max_value at all.

// Extracts encoded statistics from V1 and V2 data page headerstemplate <typename H>
EncodedStatistics ExtractStatsFromHeader(const H& header) {
EncodedStatistics page_statistics;
if (!header.__isset.statistics) {
return page_statistics;
}
const format::Statistics& stats = header.statistics;
if (stats.__isset.max) {
page_statistics.set_max(stats.max);
}
if (stats.__isset.min) {
page_statistics.set_min(stats.min);
}
if (stats.__isset.null_count) {
page_statistics.set_null_count(stats.null_count);
}
if (stats.__isset.distinct_count) {
page_statistics.set_distinct_count(stats.distinct_count);
}
return page_statistics;
}

What changes are included in this PR?

Do similar thing from parquet-mr to check and read min_value/max_value from thrift stats.

Are these changes tested?

Some test cases fail after the fix. Fixed them to make sure it is covered.

Are there any user-facing changes?

No.

@github-actions

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #14870has been automatically assigned in GitHub to PR creator.

@wgtmac

Copy link
Copy Markdown
MemberAuthor

@pitrou@wjones127@westonpace Could you please take a look?

@mapleFUmapleFU left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Rest LGTM

}
const format::Statistics& stats = header.statistics;
if (stats.__isset.max) {
// Use the new V2 min-max statistics over the former one if it is filled

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Previously, page_statistics will handle min-max separately. This patch changes it to once have all min-max, otherwise, cannot use min-max

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Although in parquet.thrift, min-max can exist only one. But I think handling it like this is ok

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

So we revert back to previous mode that only has min or max is ok?

@wjones127wjones127 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This looks good. @wgtmac before we merge, could you create a new issue for this bug fix? Also, if you could create issues for the TODOs as well, that would be appreciated.

@westonpace

Copy link
Copy Markdown
Member

Does this mean the following (testing my understanding)?

If min is set but max is not set then we should ignore both min and max

@wjones127

Copy link
Copy Markdown
Member

Does this mean the following (testing my understanding)?

If min is set but max is not set then we should ignore both min and max

Yes, it does mean we will. Do you foresee that as an issue? It sounds like Java implementation takes the same approach.

@wgtmacwgtmac changed the title GH-14870: [C++][Parquet] Fix parsing stats from min_value/max_valueGH-34138: [C++][Parquet] Fix parsing stats from min_value/max_valueFeb 11, 2023
@github-actions

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #34138has been automatically assigned in GitHub to PR creator.

@wgtmac

Copy link
Copy Markdown
MemberAuthor

Does this mean the following (testing my understanding)?

If min is set but max is not set then we should ignore both min and max

When only one of min and max exists, it usually happens when a binary value has an extreme length or a floating value has NaN. In this case, the stats provide little value and make it tricker to use.

@wgtmac

Copy link
Copy Markdown
MemberAuthor

This looks good. @wgtmac before we merge, could you create a new issue for this bug fix? Also, if you could create issues for the TODOs as well, that would be appreciated.

I have created a new issue for this PR and updated the title.

A separate issue has been created for the TODO items: #34139

Thanks for the review! @wjones127

@mapleFUmapleFU left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Rest LGTM

page_statistics.set_max(stats.max_value);
page_statistics.set_min(stats.min_value);
} else if (stats.__isset.max && stats.__isset.min) {
// TODO: check created_by to see if it is corrupted for some types.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I've no problem here, but just curious, does this code meaning the parquet-mr's CorruptStatistics.shouldIgnoreStatistics? (It's really trickey...)

@wgtmac

Copy link
Copy Markdown
MemberAuthor

Gentle ping @wjones127@pitrou

Once this gets merged, I will rebase #34107 which is blocked by it.

@emkornfield

Copy link
Copy Markdown
Contributor

CC @fatemehp

@westonpace

Copy link
Copy Markdown
Member

Yes, it does mean we will. Do you foresee that as an issue? It sounds like Java implementation takes the same approach.

In datasets, for row group statistics, we recently added a check that was roughly...

if (is_nan(min) && is_nan(max)) {
// Ignore statistics
} else if (is_nan(min)) {
// Assume x <= max
} else if(is_nan(max)) {
// Assume x >= min
} else {
// Assume min <= x <= max
}

In other words, if one of min or max is NaN then we still use the other side of the equality. I think my primary concern is to validate that is a safe assumption. In other words, I want to make sure we aren't using garbage data in our handling of row groups.

@wjones127

Copy link
Copy Markdown
Member

@westonpace that makes sense.

When only one of min and max exists, it usually happens when a binary value has an extreme length or a floating value has NaN. In this case, the stats provide little value and make it tricker to use.

It seems like we do have handling for these two cases. See Weston's message for NaN handling and max_statistics_size on WriterProperties. Based on that, I'd actually prefer we keep the ability to parse just the min or max if only one is available.

Comment threadcpp/src/parquet/column_reader.cc Outdated
Comment on lines +215 to +218
if (stats.__isset.max_value && stats.__isset.min_value) {
// TODO: check if the column_order is TYPE_DEFINED_ORDER.
page_statistics.set_max(stats.max_value);
page_statistics.set_min(stats.min_value);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
if (stats.__isset.max_value && stats.__isset.min_value) {
// TODO: check if the column_order is TYPE_DEFINED_ORDER.
page_statistics.set_max(stats.max_value);
page_statistics.set_min(stats.min_value);
if (stats.__isset.max_value || stats.__isset.min_value) {
// TODO: check if the column_order is TYPE_DEFINED_ORDER.
if (stats.__isset.max_value) {
page_statistics.set_max(stats.max_value);
}
if (stats.__isset.min_value) {
page_statistics.set_min(stats.min_value);
}

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed. Please take a look again. Thanks @wjones127 !

@westonpace

Copy link
Copy Markdown
Member

It seems like we do have handling for these two cases.

Just for clarity, the PR I linked (and my thoughts) were about how we currently handle row group statistics. I'm not sure if the rules are identical for page statistics. I mainly wanted to make sure my understanding of the row group statistics wasn't invalid.

@wgtmac

Copy link
Copy Markdown
MemberAuthor

It seems like we do have handling for these two cases.

Just for clarity, the PR I linked (and my thoughts) were about how we currently handle row group statistics. I'm not sure if the rules are identical for page statistics. I mainly wanted to make sure my understanding of the row group statistics wasn't invalid.

IIUC, row group statistics are aggregated from page statistics so they should share the same rules. The parquet thrift message definition does allow only one side of min or max exist:

/** * Statistics per row group and per page * All fields are optional.*/structStatistics {
/** * DEPRECATED: min and max value of the column. Use min_value and max_value. * * Values are encoded using PLAIN encoding, except that variable-length byte * arrays do not include a length prefix. * * These fields encode min and max values determined by signed comparison * only. New files should use the correct order for a column's logical type * and store the values in the min_value and max_value fields. * * To support older readers, these may be set when the column order is * signed.*/1:optionalbinarymax;
2:optionalbinarymin;
/** count of null value in the column */3:optionali64null_count;
/** count of distinct values occurring */4:optionali64distinct_count;
/** * Min and max values for the column, determined by its ColumnOrder. * * Values are encoded using PLAIN encoding, except that variable-length byte * arrays do not include a length prefix.*/5:optionalbinarymax_value;
6:optionalbinarymin_value;
}

On the other side, the story of page index is different. The column index definition does require existence of both min and max values if it is not a null page:

/** * Description for ColumnIndex. * Each <array-field>[i] refers to the page at OffsetIndex.page_locations[i]*/structColumnIndex {
/** * A list of Boolean values to determine the validity of the corresponding * min and max values. If true, a page contains only null values, and writers * have to set the corresponding entries in min_values and max_values to * byte[0], so that all lists have the same length. If false, the * corresponding entries in min_values and max_values must be valid.*/1:requiredlist<bool> null_pages/** * Two lists containing lower and upper bounds for the values of each page * determined by the ColumnOrder of the column. These may be the actual * minimum and maximum values found on a page, but can also be (more compact) * values that do not exist on a page. For example, instead of storing ""Blart * Versenwald III", a writer may set min_values[i]="B", max_values[i]="C". * Such more compact values must still be valid values within the column's * logical type. Readers must make sure that list entries are populated before * using them by inspecting null_pages.*/2:requiredlist<binary> min_values3:requiredlist<binary> max_values/** * Stores whether both min_values and max_values are ordered and if so, in * which direction. This allows readers to perform binary searches in both * lists. Readers cannot assume that max_values[i] <= min_values[i+1], even * if the lists are ordered.*/4:requiredBoundaryOrderboundary_order/** A list containing the number of null values for each page **/5:optionallist<i64> null_counts
}

So I am fine with parsing only one side min or max values from page/row group statistics. @westonpace@wjones127

@wgtmac

wgtmac commented Feb 17, 2023

Copy link
Copy Markdown
MemberAuthor

The CI build is failed due to a recent branch rename and will be fixed by #34218

@wgtmac

Copy link
Copy Markdown
MemberAuthor

@westonpace Could you please take another pass?

@wjones127
wjones127 merged commit 8e5e438 into apache:mainFeb 17, 2023
@ursabot

Copy link
Copy Markdown

Benchmark runs are scheduled for baseline = 1264e40 and contender = 8e5e438. 8e5e438 is a master commit associated with this PR. Results will be available as each benchmark for each run completes.
Conbench compare runs links:
[Finished ⬇️0.0% ⬆️0.0%] ec2-t3-xlarge-us-east-2
[Failed ⬇️0.34% ⬆️0.03%] test-mac-arm
[Finished ⬇️1.02% ⬆️0.0%] ursa-i9-9960x
[Finished ⬇️0.54% ⬆️0.03%] ursa-thinkcentre-m75q
Buildkite builds:
[Finished] 8e5e438d ec2-t3-xlarge-us-east-2
[Failed] 8e5e438d test-mac-arm
[Finished] 8e5e438d ursa-i9-9960x
[Finished] 8e5e438d ursa-thinkcentre-m75q
[Finished] 1264e409 ec2-t3-xlarge-us-east-2
[Finished] 1264e409 test-mac-arm
[Finished] 1264e409 ursa-i9-9960x
[Finished] 1264e409 ursa-thinkcentre-m75q
Supported benchmarks:
ec2-t3-xlarge-us-east-2: Supported benchmark langs: Python, R. Runs only benchmarks with cloud = True
test-mac-arm: Supported benchmark langs: C++, Python, R
ursa-i9-9960x: Supported benchmark langs: Python, R, JavaScript
ursa-thinkcentre-m75q: Supported benchmark langs: C++, Java

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[C++][Parquet] Fix parsing stats from min_value/max_value

6 participants

@wgtmac@westonpace@wjones127@emkornfield@ursabot@mapleFU
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

GH-34138: [C++][Parquet] Fix parsing stats from min_value/max_value - #34112

Merged
wjones127 merged 2 commits into
apache:mainfrom
wgtmac:parse_min_max
Feb 17, 2023
Merged

GH-34138: [C++][Parquet] Fix parsing stats from min_value/max_value#34112
wjones127 merged 2 commits into
apache:mainfrom
wgtmac:parse_min_max

Conversation

@wgtmac

@wgtmacwgtmac commented Feb 10, 2023

Copy link
Copy Markdown
Member

Rationale for this change

The code below does not read from stats.min_value/max_value at all.

// Extracts encoded statistics from V1 and V2 data page headerstemplate <typename H>
EncodedStatistics ExtractStatsFromHeader(const H& header) {
EncodedStatistics page_statistics;
if (!header.__isset.statistics) {
return page_statistics;
}
const format::Statistics& stats = header.statistics;
if (stats.__isset.max) {
page_statistics.set_max(stats.max);
}
if (stats.__isset.min) {
page_statistics.set_min(stats.min);
}
if (stats.__isset.null_count) {
page_statistics.set_null_count(stats.null_count);
}
if (stats.__isset.distinct_count) {
page_statistics.set_distinct_count(stats.distinct_count);
}
return page_statistics;
}

What changes are included in this PR?

Do similar thing from parquet-mr to check and read min_value/max_value from thrift stats.

Are these changes tested?

Some test cases fail after the fix. Fixed them to make sure it is covered.

Are there any user-facing changes?

No.

@github-actions

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #14870has been automatically assigned in GitHub to PR creator.

@wgtmac

Copy link
Copy Markdown
MemberAuthor

@pitrou@wjones127@westonpace Could you please take a look?

@mapleFUmapleFU left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Rest LGTM

}
const format::Statistics& stats = header.statistics;
if (stats.__isset.max) {
// Use the new V2 min-max statistics over the former one if it is filled

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Previously, page_statistics will handle min-max separately. This patch changes it to once have all min-max, otherwise, cannot use min-max

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Although in parquet.thrift, min-max can exist only one. But I think handling it like this is ok

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

So we revert back to previous mode that only has min or max is ok?

@wjones127wjones127 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This looks good. @wgtmac before we merge, could you create a new issue for this bug fix? Also, if you could create issues for the TODOs as well, that would be appreciated.

@westonpace

Copy link
Copy Markdown
Member

Does this mean the following (testing my understanding)?

If min is set but max is not set then we should ignore both min and max

@wjones127

Copy link
Copy Markdown
Member

Does this mean the following (testing my understanding)?

If min is set but max is not set then we should ignore both min and max

Yes, it does mean we will. Do you foresee that as an issue? It sounds like Java implementation takes the same approach.

@wgtmacwgtmac changed the title GH-14870: [C++][Parquet] Fix parsing stats from min_value/max_valueGH-34138: [C++][Parquet] Fix parsing stats from min_value/max_valueFeb 11, 2023
@github-actions

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #34138has been automatically assigned in GitHub to PR creator.

@wgtmac

Copy link
Copy Markdown
MemberAuthor

Does this mean the following (testing my understanding)?

If min is set but max is not set then we should ignore both min and max

When only one of min and max exists, it usually happens when a binary value has an extreme length or a floating value has NaN. In this case, the stats provide little value and make it tricker to use.

@wgtmac

Copy link
Copy Markdown
MemberAuthor

This looks good. @wgtmac before we merge, could you create a new issue for this bug fix? Also, if you could create issues for the TODOs as well, that would be appreciated.

I have created a new issue for this PR and updated the title.

A separate issue has been created for the TODO items: #34139

Thanks for the review! @wjones127

@mapleFUmapleFU left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Rest LGTM

page_statistics.set_max(stats.max_value);
page_statistics.set_min(stats.min_value);
} else if (stats.__isset.max && stats.__isset.min) {
// TODO: check created_by to see if it is corrupted for some types.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I've no problem here, but just curious, does this code meaning the parquet-mr's CorruptStatistics.shouldIgnoreStatistics? (It's really trickey...)

@wgtmac

Copy link
Copy Markdown
MemberAuthor

Gentle ping @wjones127@pitrou

Once this gets merged, I will rebase #34107 which is blocked by it.

@emkornfield

Copy link
Copy Markdown
Contributor

CC @fatemehp

@westonpace

Copy link
Copy Markdown
Member

Yes, it does mean we will. Do you foresee that as an issue? It sounds like Java implementation takes the same approach.

In datasets, for row group statistics, we recently added a check that was roughly...

if (is_nan(min) && is_nan(max)) {
// Ignore statistics
} else if (is_nan(min)) {
// Assume x <= max
} else if(is_nan(max)) {
// Assume x >= min
} else {
// Assume min <= x <= max
}

In other words, if one of min or max is NaN then we still use the other side of the equality. I think my primary concern is to validate that is a safe assumption. In other words, I want to make sure we aren't using garbage data in our handling of row groups.

@wjones127

Copy link
Copy Markdown
Member

@westonpace that makes sense.

When only one of min and max exists, it usually happens when a binary value has an extreme length or a floating value has NaN. In this case, the stats provide little value and make it tricker to use.

It seems like we do have handling for these two cases. See Weston's message for NaN handling and max_statistics_size on WriterProperties. Based on that, I'd actually prefer we keep the ability to parse just the min or max if only one is available.

Comment threadcpp/src/parquet/column_reader.cc Outdated
Comment on lines +215 to +218
if (stats.__isset.max_value && stats.__isset.min_value) {
// TODO: check if the column_order is TYPE_DEFINED_ORDER.
page_statistics.set_max(stats.max_value);
page_statistics.set_min(stats.min_value);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
if (stats.__isset.max_value && stats.__isset.min_value) {
// TODO: check if the column_order is TYPE_DEFINED_ORDER.
page_statistics.set_max(stats.max_value);
page_statistics.set_min(stats.min_value);
if (stats.__isset.max_value || stats.__isset.min_value) {
// TODO: check if the column_order is TYPE_DEFINED_ORDER.
if (stats.__isset.max_value) {
page_statistics.set_max(stats.max_value);
}
if (stats.__isset.min_value) {
page_statistics.set_min(stats.min_value);
}

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed. Please take a look again. Thanks @wjones127 !

@westonpace

Copy link
Copy Markdown
Member

It seems like we do have handling for these two cases.

Just for clarity, the PR I linked (and my thoughts) were about how we currently handle row group statistics. I'm not sure if the rules are identical for page statistics. I mainly wanted to make sure my understanding of the row group statistics wasn't invalid.

@wgtmac

Copy link
Copy Markdown
MemberAuthor

It seems like we do have handling for these two cases.

Just for clarity, the PR I linked (and my thoughts) were about how we currently handle row group statistics. I'm not sure if the rules are identical for page statistics. I mainly wanted to make sure my understanding of the row group statistics wasn't invalid.

IIUC, row group statistics are aggregated from page statistics so they should share the same rules. The parquet thrift message definition does allow only one side of min or max exist:

/** * Statistics per row group and per page * All fields are optional.*/structStatistics {
/** * DEPRECATED: min and max value of the column. Use min_value and max_value. * * Values are encoded using PLAIN encoding, except that variable-length byte * arrays do not include a length prefix. * * These fields encode min and max values determined by signed comparison * only. New files should use the correct order for a column's logical type * and store the values in the min_value and max_value fields. * * To support older readers, these may be set when the column order is * signed.*/1:optionalbinarymax;
2:optionalbinarymin;
/** count of null value in the column */3:optionali64null_count;
/** count of distinct values occurring */4:optionali64distinct_count;
/** * Min and max values for the column, determined by its ColumnOrder. * * Values are encoded using PLAIN encoding, except that variable-length byte * arrays do not include a length prefix.*/5:optionalbinarymax_value;
6:optionalbinarymin_value;
}

On the other side, the story of page index is different. The column index definition does require existence of both min and max values if it is not a null page:

/** * Description for ColumnIndex. * Each <array-field>[i] refers to the page at OffsetIndex.page_locations[i]*/structColumnIndex {
/** * A list of Boolean values to determine the validity of the corresponding * min and max values. If true, a page contains only null values, and writers * have to set the corresponding entries in min_values and max_values to * byte[0], so that all lists have the same length. If false, the * corresponding entries in min_values and max_values must be valid.*/1:requiredlist<bool> null_pages/** * Two lists containing lower and upper bounds for the values of each page * determined by the ColumnOrder of the column. These may be the actual * minimum and maximum values found on a page, but can also be (more compact) * values that do not exist on a page. For example, instead of storing ""Blart * Versenwald III", a writer may set min_values[i]="B", max_values[i]="C". * Such more compact values must still be valid values within the column's * logical type. Readers must make sure that list entries are populated before * using them by inspecting null_pages.*/2:requiredlist<binary> min_values3:requiredlist<binary> max_values/** * Stores whether both min_values and max_values are ordered and if so, in * which direction. This allows readers to perform binary searches in both * lists. Readers cannot assume that max_values[i] <= min_values[i+1], even * if the lists are ordered.*/4:requiredBoundaryOrderboundary_order/** A list containing the number of null values for each page **/5:optionallist<i64> null_counts
}

So I am fine with parsing only one side min or max values from page/row group statistics. @westonpace@wjones127

@wgtmac

wgtmac commented Feb 17, 2023

Copy link
Copy Markdown
MemberAuthor

The CI build is failed due to a recent branch rename and will be fixed by #34218

@wgtmac

Copy link
Copy Markdown
MemberAuthor

@westonpace Could you please take another pass?

@wjones127
wjones127 merged commit 8e5e438 into apache:mainFeb 17, 2023
@ursabot

Copy link
Copy Markdown

Benchmark runs are scheduled for baseline = 1264e40 and contender = 8e5e438. 8e5e438 is a master commit associated with this PR. Results will be available as each benchmark for each run completes.
Conbench compare runs links:
[Finished ⬇️0.0% ⬆️0.0%] ec2-t3-xlarge-us-east-2
[Failed ⬇️0.34% ⬆️0.03%] test-mac-arm
[Finished ⬇️1.02% ⬆️0.0%] ursa-i9-9960x
[Finished ⬇️0.54% ⬆️0.03%] ursa-thinkcentre-m75q
Buildkite builds:
[Finished] 8e5e438d ec2-t3-xlarge-us-east-2
[Failed] 8e5e438d test-mac-arm
[Finished] 8e5e438d ursa-i9-9960x
[Finished] 8e5e438d ursa-thinkcentre-m75q
[Finished] 1264e409 ec2-t3-xlarge-us-east-2
[Finished] 1264e409 test-mac-arm
[Finished] 1264e409 ursa-i9-9960x
[Finished] 1264e409 ursa-thinkcentre-m75q
Supported benchmarks:
ec2-t3-xlarge-us-east-2: Supported benchmark langs: Python, R. Runs only benchmarks with cloud = True
test-mac-arm: Supported benchmark langs: C++, Python, R
ursa-i9-9960x: Supported benchmark langs: Python, R, JavaScript
ursa-thinkcentre-m75q: Supported benchmark langs: C++, Java

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[C++][Parquet] Fix parsing stats from min_value/max_value

6 participants

@wgtmac@westonpace@wjones127@emkornfield@ursabot@mapleFU
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

GH-34138: [C++][Parquet] Fix parsing stats from min_value/max_value - #34112

Merged
wjones127 merged 2 commits into
apache:mainfrom
wgtmac:parse_min_max
Feb 17, 2023
Merged

GH-34138: [C++][Parquet] Fix parsing stats from min_value/max_value#34112
wjones127 merged 2 commits into
apache:mainfrom
wgtmac:parse_min_max

Conversation

@wgtmac

@wgtmacwgtmac commented Feb 10, 2023

Copy link
Copy Markdown
Member

Rationale for this change

The code below does not read from stats.min_value/max_value at all.

// Extracts encoded statistics from V1 and V2 data page headerstemplate <typename H>
EncodedStatistics ExtractStatsFromHeader(const H& header) {
EncodedStatistics page_statistics;
if (!header.__isset.statistics) {
return page_statistics;
}
const format::Statistics& stats = header.statistics;
if (stats.__isset.max) {
page_statistics.set_max(stats.max);
}
if (stats.__isset.min) {
page_statistics.set_min(stats.min);
}
if (stats.__isset.null_count) {
page_statistics.set_null_count(stats.null_count);
}
if (stats.__isset.distinct_count) {
page_statistics.set_distinct_count(stats.distinct_count);
}
return page_statistics;
}

What changes are included in this PR?

Do similar thing from parquet-mr to check and read min_value/max_value from thrift stats.

Are these changes tested?

Some test cases fail after the fix. Fixed them to make sure it is covered.

Are there any user-facing changes?

No.

@github-actions

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #14870has been automatically assigned in GitHub to PR creator.

@wgtmac

Copy link
Copy Markdown
MemberAuthor

@pitrou@wjones127@westonpace Could you please take a look?

@mapleFUmapleFU left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Rest LGTM

}
const format::Statistics& stats = header.statistics;
if (stats.__isset.max) {
// Use the new V2 min-max statistics over the former one if it is filled

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Previously, page_statistics will handle min-max separately. This patch changes it to once have all min-max, otherwise, cannot use min-max

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Although in parquet.thrift, min-max can exist only one. But I think handling it like this is ok

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

So we revert back to previous mode that only has min or max is ok?

@wjones127wjones127 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This looks good. @wgtmac before we merge, could you create a new issue for this bug fix? Also, if you could create issues for the TODOs as well, that would be appreciated.

@westonpace

Copy link
Copy Markdown
Member

Does this mean the following (testing my understanding)?

If min is set but max is not set then we should ignore both min and max

@wjones127

Copy link
Copy Markdown
Member

Does this mean the following (testing my understanding)?

If min is set but max is not set then we should ignore both min and max

Yes, it does mean we will. Do you foresee that as an issue? It sounds like Java implementation takes the same approach.

@wgtmacwgtmac changed the title GH-14870: [C++][Parquet] Fix parsing stats from min_value/max_valueGH-34138: [C++][Parquet] Fix parsing stats from min_value/max_valueFeb 11, 2023
@github-actions

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #34138has been automatically assigned in GitHub to PR creator.

@wgtmac

Copy link
Copy Markdown
MemberAuthor

Does this mean the following (testing my understanding)?

If min is set but max is not set then we should ignore both min and max

When only one of min and max exists, it usually happens when a binary value has an extreme length or a floating value has NaN. In this case, the stats provide little value and make it tricker to use.

@wgtmac

Copy link
Copy Markdown
MemberAuthor

This looks good. @wgtmac before we merge, could you create a new issue for this bug fix? Also, if you could create issues for the TODOs as well, that would be appreciated.

I have created a new issue for this PR and updated the title.

A separate issue has been created for the TODO items: #34139

Thanks for the review! @wjones127

@mapleFUmapleFU left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Rest LGTM

page_statistics.set_max(stats.max_value);
page_statistics.set_min(stats.min_value);
} else if (stats.__isset.max && stats.__isset.min) {
// TODO: check created_by to see if it is corrupted for some types.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I've no problem here, but just curious, does this code meaning the parquet-mr's CorruptStatistics.shouldIgnoreStatistics? (It's really trickey...)

@wgtmac

Copy link
Copy Markdown
MemberAuthor

Gentle ping @wjones127@pitrou

Once this gets merged, I will rebase #34107 which is blocked by it.

@emkornfield

Copy link
Copy Markdown
Contributor

CC @fatemehp

@westonpace

Copy link
Copy Markdown
Member

Yes, it does mean we will. Do you foresee that as an issue? It sounds like Java implementation takes the same approach.

In datasets, for row group statistics, we recently added a check that was roughly...

if (is_nan(min) && is_nan(max)) {
// Ignore statistics
} else if (is_nan(min)) {
// Assume x <= max
} else if(is_nan(max)) {
// Assume x >= min
} else {
// Assume min <= x <= max
}

In other words, if one of min or max is NaN then we still use the other side of the equality. I think my primary concern is to validate that is a safe assumption. In other words, I want to make sure we aren't using garbage data in our handling of row groups.

@wjones127

Copy link
Copy Markdown
Member

@westonpace that makes sense.

When only one of min and max exists, it usually happens when a binary value has an extreme length or a floating value has NaN. In this case, the stats provide little value and make it tricker to use.

It seems like we do have handling for these two cases. See Weston's message for NaN handling and max_statistics_size on WriterProperties. Based on that, I'd actually prefer we keep the ability to parse just the min or max if only one is available.

Comment threadcpp/src/parquet/column_reader.cc Outdated
Comment on lines +215 to +218
if (stats.__isset.max_value && stats.__isset.min_value) {
// TODO: check if the column_order is TYPE_DEFINED_ORDER.
page_statistics.set_max(stats.max_value);
page_statistics.set_min(stats.min_value);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
if (stats.__isset.max_value && stats.__isset.min_value) {
// TODO: check if the column_order is TYPE_DEFINED_ORDER.
page_statistics.set_max(stats.max_value);
page_statistics.set_min(stats.min_value);
if (stats.__isset.max_value || stats.__isset.min_value) {
// TODO: check if the column_order is TYPE_DEFINED_ORDER.
if (stats.__isset.max_value) {
page_statistics.set_max(stats.max_value);
}
if (stats.__isset.min_value) {
page_statistics.set_min(stats.min_value);
}

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed. Please take a look again. Thanks @wjones127 !

@westonpace

Copy link
Copy Markdown
Member

It seems like we do have handling for these two cases.

Just for clarity, the PR I linked (and my thoughts) were about how we currently handle row group statistics. I'm not sure if the rules are identical for page statistics. I mainly wanted to make sure my understanding of the row group statistics wasn't invalid.

@wgtmac

Copy link
Copy Markdown
MemberAuthor

It seems like we do have handling for these two cases.

Just for clarity, the PR I linked (and my thoughts) were about how we currently handle row group statistics. I'm not sure if the rules are identical for page statistics. I mainly wanted to make sure my understanding of the row group statistics wasn't invalid.

IIUC, row group statistics are aggregated from page statistics so they should share the same rules. The parquet thrift message definition does allow only one side of min or max exist:

/** * Statistics per row group and per page * All fields are optional.*/structStatistics {
/** * DEPRECATED: min and max value of the column. Use min_value and max_value. * * Values are encoded using PLAIN encoding, except that variable-length byte * arrays do not include a length prefix. * * These fields encode min and max values determined by signed comparison * only. New files should use the correct order for a column's logical type * and store the values in the min_value and max_value fields. * * To support older readers, these may be set when the column order is * signed.*/1:optionalbinarymax;
2:optionalbinarymin;
/** count of null value in the column */3:optionali64null_count;
/** count of distinct values occurring */4:optionali64distinct_count;
/** * Min and max values for the column, determined by its ColumnOrder. * * Values are encoded using PLAIN encoding, except that variable-length byte * arrays do not include a length prefix.*/5:optionalbinarymax_value;
6:optionalbinarymin_value;
}

On the other side, the story of page index is different. The column index definition does require existence of both min and max values if it is not a null page:

/** * Description for ColumnIndex. * Each <array-field>[i] refers to the page at OffsetIndex.page_locations[i]*/structColumnIndex {
/** * A list of Boolean values to determine the validity of the corresponding * min and max values. If true, a page contains only null values, and writers * have to set the corresponding entries in min_values and max_values to * byte[0], so that all lists have the same length. If false, the * corresponding entries in min_values and max_values must be valid.*/1:requiredlist<bool> null_pages/** * Two lists containing lower and upper bounds for the values of each page * determined by the ColumnOrder of the column. These may be the actual * minimum and maximum values found on a page, but can also be (more compact) * values that do not exist on a page. For example, instead of storing ""Blart * Versenwald III", a writer may set min_values[i]="B", max_values[i]="C". * Such more compact values must still be valid values within the column's * logical type. Readers must make sure that list entries are populated before * using them by inspecting null_pages.*/2:requiredlist<binary> min_values3:requiredlist<binary> max_values/** * Stores whether both min_values and max_values are ordered and if so, in * which direction. This allows readers to perform binary searches in both * lists. Readers cannot assume that max_values[i] <= min_values[i+1], even * if the lists are ordered.*/4:requiredBoundaryOrderboundary_order/** A list containing the number of null values for each page **/5:optionallist<i64> null_counts
}

So I am fine with parsing only one side min or max values from page/row group statistics. @westonpace@wjones127

@wgtmac

wgtmac commented Feb 17, 2023

Copy link
Copy Markdown
MemberAuthor

The CI build is failed due to a recent branch rename and will be fixed by #34218

@wgtmac

Copy link
Copy Markdown
MemberAuthor

@westonpace Could you please take another pass?

@wjones127
wjones127 merged commit 8e5e438 into apache:mainFeb 17, 2023
@ursabot

Copy link
Copy Markdown

Benchmark runs are scheduled for baseline = 1264e40 and contender = 8e5e438. 8e5e438 is a master commit associated with this PR. Results will be available as each benchmark for each run completes.
Conbench compare runs links:
[Finished ⬇️0.0% ⬆️0.0%] ec2-t3-xlarge-us-east-2
[Failed ⬇️0.34% ⬆️0.03%] test-mac-arm
[Finished ⬇️1.02% ⬆️0.0%] ursa-i9-9960x
[Finished ⬇️0.54% ⬆️0.03%] ursa-thinkcentre-m75q
Buildkite builds:
[Finished] 8e5e438d ec2-t3-xlarge-us-east-2
[Failed] 8e5e438d test-mac-arm
[Finished] 8e5e438d ursa-i9-9960x
[Finished] 8e5e438d ursa-thinkcentre-m75q
[Finished] 1264e409 ec2-t3-xlarge-us-east-2
[Finished] 1264e409 test-mac-arm
[Finished] 1264e409 ursa-i9-9960x
[Finished] 1264e409 ursa-thinkcentre-m75q
Supported benchmarks:
ec2-t3-xlarge-us-east-2: Supported benchmark langs: Python, R. Runs only benchmarks with cloud = True
test-mac-arm: Supported benchmark langs: C++, Python, R
ursa-i9-9960x: Supported benchmark langs: Python, R, JavaScript
ursa-thinkcentre-m75q: Supported benchmark langs: C++, Java

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[C++][Parquet] Fix parsing stats from min_value/max_value

6 participants

@wgtmac@westonpace@wjones127@emkornfield@ursabot@mapleFU
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

GH-34138: [C++][Parquet] Fix parsing stats from min_value/max_value - #34112

Merged
wjones127 merged 2 commits into
apache:mainfrom
wgtmac:parse_min_max
Feb 17, 2023
Merged

GH-34138: [C++][Parquet] Fix parsing stats from min_value/max_value#34112
wjones127 merged 2 commits into
apache:mainfrom
wgtmac:parse_min_max

Conversation

@wgtmac

@wgtmacwgtmac commented Feb 10, 2023

Copy link
Copy Markdown
Member

Rationale for this change

The code below does not read from stats.min_value/max_value at all.

// Extracts encoded statistics from V1 and V2 data page headerstemplate <typename H>
EncodedStatistics ExtractStatsFromHeader(const H& header) {
EncodedStatistics page_statistics;
if (!header.__isset.statistics) {
return page_statistics;
}
const format::Statistics& stats = header.statistics;
if (stats.__isset.max) {
page_statistics.set_max(stats.max);
}
if (stats.__isset.min) {
page_statistics.set_min(stats.min);
}
if (stats.__isset.null_count) {
page_statistics.set_null_count(stats.null_count);
}
if (stats.__isset.distinct_count) {
page_statistics.set_distinct_count(stats.distinct_count);
}
return page_statistics;
}

What changes are included in this PR?

Do similar thing from parquet-mr to check and read min_value/max_value from thrift stats.

Are these changes tested?

Some test cases fail after the fix. Fixed them to make sure it is covered.

Are there any user-facing changes?

No.

@github-actions

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #14870has been automatically assigned in GitHub to PR creator.

@wgtmac

Copy link
Copy Markdown
MemberAuthor

@pitrou@wjones127@westonpace Could you please take a look?

@mapleFUmapleFU left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Rest LGTM

}
const format::Statistics& stats = header.statistics;
if (stats.__isset.max) {
// Use the new V2 min-max statistics over the former one if it is filled

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Previously, page_statistics will handle min-max separately. This patch changes it to once have all min-max, otherwise, cannot use min-max

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Although in parquet.thrift, min-max can exist only one. But I think handling it like this is ok

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

So we revert back to previous mode that only has min or max is ok?

@wjones127wjones127 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This looks good. @wgtmac before we merge, could you create a new issue for this bug fix? Also, if you could create issues for the TODOs as well, that would be appreciated.

@westonpace

Copy link
Copy Markdown
Member

Does this mean the following (testing my understanding)?

If min is set but max is not set then we should ignore both min and max

@wjones127

Copy link
Copy Markdown
Member

Does this mean the following (testing my understanding)?

If min is set but max is not set then we should ignore both min and max

Yes, it does mean we will. Do you foresee that as an issue? It sounds like Java implementation takes the same approach.

@wgtmacwgtmac changed the title GH-14870: [C++][Parquet] Fix parsing stats from min_value/max_valueGH-34138: [C++][Parquet] Fix parsing stats from min_value/max_valueFeb 11, 2023
@github-actions

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #34138has been automatically assigned in GitHub to PR creator.

@wgtmac

Copy link
Copy Markdown
MemberAuthor

Does this mean the following (testing my understanding)?

If min is set but max is not set then we should ignore both min and max

When only one of min and max exists, it usually happens when a binary value has an extreme length or a floating value has NaN. In this case, the stats provide little value and make it tricker to use.

@wgtmac

Copy link
Copy Markdown
MemberAuthor

This looks good. @wgtmac before we merge, could you create a new issue for this bug fix? Also, if you could create issues for the TODOs as well, that would be appreciated.

I have created a new issue for this PR and updated the title.

A separate issue has been created for the TODO items: #34139

Thanks for the review! @wjones127

@mapleFUmapleFU left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Rest LGTM

page_statistics.set_max(stats.max_value);
page_statistics.set_min(stats.min_value);
} else if (stats.__isset.max && stats.__isset.min) {
// TODO: check created_by to see if it is corrupted for some types.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I've no problem here, but just curious, does this code meaning the parquet-mr's CorruptStatistics.shouldIgnoreStatistics? (It's really trickey...)

@wgtmac

Copy link
Copy Markdown
MemberAuthor

Gentle ping @wjones127@pitrou

Once this gets merged, I will rebase #34107 which is blocked by it.

@emkornfield

Copy link
Copy Markdown
Contributor

CC @fatemehp

@westonpace

Copy link
Copy Markdown
Member

Yes, it does mean we will. Do you foresee that as an issue? It sounds like Java implementation takes the same approach.

In datasets, for row group statistics, we recently added a check that was roughly...

if (is_nan(min) && is_nan(max)) {
// Ignore statistics
} else if (is_nan(min)) {
// Assume x <= max
} else if(is_nan(max)) {
// Assume x >= min
} else {
// Assume min <= x <= max
}

In other words, if one of min or max is NaN then we still use the other side of the equality. I think my primary concern is to validate that is a safe assumption. In other words, I want to make sure we aren't using garbage data in our handling of row groups.

@wjones127

Copy link
Copy Markdown
Member

@westonpace that makes sense.

When only one of min and max exists, it usually happens when a binary value has an extreme length or a floating value has NaN. In this case, the stats provide little value and make it tricker to use.

It seems like we do have handling for these two cases. See Weston's message for NaN handling and max_statistics_size on WriterProperties. Based on that, I'd actually prefer we keep the ability to parse just the min or max if only one is available.

Comment threadcpp/src/parquet/column_reader.cc Outdated
Comment on lines +215 to +218
if (stats.__isset.max_value && stats.__isset.min_value) {
// TODO: check if the column_order is TYPE_DEFINED_ORDER.
page_statistics.set_max(stats.max_value);
page_statistics.set_min(stats.min_value);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
if (stats.__isset.max_value && stats.__isset.min_value) {
// TODO: check if the column_order is TYPE_DEFINED_ORDER.
page_statistics.set_max(stats.max_value);
page_statistics.set_min(stats.min_value);
if (stats.__isset.max_value || stats.__isset.min_value) {
// TODO: check if the column_order is TYPE_DEFINED_ORDER.
if (stats.__isset.max_value) {
page_statistics.set_max(stats.max_value);
}
if (stats.__isset.min_value) {
page_statistics.set_min(stats.min_value);
}

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed. Please take a look again. Thanks @wjones127 !

@westonpace

Copy link
Copy Markdown
Member

It seems like we do have handling for these two cases.

Just for clarity, the PR I linked (and my thoughts) were about how we currently handle row group statistics. I'm not sure if the rules are identical for page statistics. I mainly wanted to make sure my understanding of the row group statistics wasn't invalid.

@wgtmac

Copy link
Copy Markdown
MemberAuthor

It seems like we do have handling for these two cases.

Just for clarity, the PR I linked (and my thoughts) were about how we currently handle row group statistics. I'm not sure if the rules are identical for page statistics. I mainly wanted to make sure my understanding of the row group statistics wasn't invalid.

IIUC, row group statistics are aggregated from page statistics so they should share the same rules. The parquet thrift message definition does allow only one side of min or max exist:

/** * Statistics per row group and per page * All fields are optional.*/structStatistics {
/** * DEPRECATED: min and max value of the column. Use min_value and max_value. * * Values are encoded using PLAIN encoding, except that variable-length byte * arrays do not include a length prefix. * * These fields encode min and max values determined by signed comparison * only. New files should use the correct order for a column's logical type * and store the values in the min_value and max_value fields. * * To support older readers, these may be set when the column order is * signed.*/1:optionalbinarymax;
2:optionalbinarymin;
/** count of null value in the column */3:optionali64null_count;
/** count of distinct values occurring */4:optionali64distinct_count;
/** * Min and max values for the column, determined by its ColumnOrder. * * Values are encoded using PLAIN encoding, except that variable-length byte * arrays do not include a length prefix.*/5:optionalbinarymax_value;
6:optionalbinarymin_value;
}

On the other side, the story of page index is different. The column index definition does require existence of both min and max values if it is not a null page:

/** * Description for ColumnIndex. * Each <array-field>[i] refers to the page at OffsetIndex.page_locations[i]*/structColumnIndex {
/** * A list of Boolean values to determine the validity of the corresponding * min and max values. If true, a page contains only null values, and writers * have to set the corresponding entries in min_values and max_values to * byte[0], so that all lists have the same length. If false, the * corresponding entries in min_values and max_values must be valid.*/1:requiredlist<bool> null_pages/** * Two lists containing lower and upper bounds for the values of each page * determined by the ColumnOrder of the column. These may be the actual * minimum and maximum values found on a page, but can also be (more compact) * values that do not exist on a page. For example, instead of storing ""Blart * Versenwald III", a writer may set min_values[i]="B", max_values[i]="C". * Such more compact values must still be valid values within the column's * logical type. Readers must make sure that list entries are populated before * using them by inspecting null_pages.*/2:requiredlist<binary> min_values3:requiredlist<binary> max_values/** * Stores whether both min_values and max_values are ordered and if so, in * which direction. This allows readers to perform binary searches in both * lists. Readers cannot assume that max_values[i] <= min_values[i+1], even * if the lists are ordered.*/4:requiredBoundaryOrderboundary_order/** A list containing the number of null values for each page **/5:optionallist<i64> null_counts
}

So I am fine with parsing only one side min or max values from page/row group statistics. @westonpace@wjones127

@wgtmac

wgtmac commented Feb 17, 2023

Copy link
Copy Markdown
MemberAuthor

The CI build is failed due to a recent branch rename and will be fixed by #34218

@wgtmac

Copy link
Copy Markdown
MemberAuthor

@westonpace Could you please take another pass?

@wjones127
wjones127 merged commit 8e5e438 into apache:mainFeb 17, 2023
@ursabot

Copy link
Copy Markdown

Benchmark runs are scheduled for baseline = 1264e40 and contender = 8e5e438. 8e5e438 is a master commit associated with this PR. Results will be available as each benchmark for each run completes.
Conbench compare runs links:
[Finished ⬇️0.0% ⬆️0.0%] ec2-t3-xlarge-us-east-2
[Failed ⬇️0.34% ⬆️0.03%] test-mac-arm
[Finished ⬇️1.02% ⬆️0.0%] ursa-i9-9960x
[Finished ⬇️0.54% ⬆️0.03%] ursa-thinkcentre-m75q
Buildkite builds:
[Finished] 8e5e438d ec2-t3-xlarge-us-east-2
[Failed] 8e5e438d test-mac-arm
[Finished] 8e5e438d ursa-i9-9960x
[Finished] 8e5e438d ursa-thinkcentre-m75q
[Finished] 1264e409 ec2-t3-xlarge-us-east-2
[Finished] 1264e409 test-mac-arm
[Finished] 1264e409 ursa-i9-9960x
[Finished] 1264e409 ursa-thinkcentre-m75q
Supported benchmarks:
ec2-t3-xlarge-us-east-2: Supported benchmark langs: Python, R. Runs only benchmarks with cloud = True
test-mac-arm: Supported benchmark langs: C++, Python, R
ursa-i9-9960x: Supported benchmark langs: Python, R, JavaScript
ursa-thinkcentre-m75q: Supported benchmark langs: C++, Java

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[C++][Parquet] Fix parsing stats from min_value/max_value

6 participants

@wgtmac@westonpace@wjones127@emkornfield@ursabot@mapleFU
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

GH-34138: [C++][Parquet] Fix parsing stats from min_value/max_value - #34112

Merged
wjones127 merged 2 commits into
apache:mainfrom
wgtmac:parse_min_max
Feb 17, 2023
Merged

GH-34138: [C++][Parquet] Fix parsing stats from min_value/max_value#34112
wjones127 merged 2 commits into
apache:mainfrom
wgtmac:parse_min_max

Conversation

@wgtmac

@wgtmacwgtmac commented Feb 10, 2023

Copy link
Copy Markdown
Member

Rationale for this change

The code below does not read from stats.min_value/max_value at all.

// Extracts encoded statistics from V1 and V2 data page headerstemplate <typename H>
EncodedStatistics ExtractStatsFromHeader(const H& header) {
EncodedStatistics page_statistics;
if (!header.__isset.statistics) {
return page_statistics;
}
const format::Statistics& stats = header.statistics;
if (stats.__isset.max) {
page_statistics.set_max(stats.max);
}
if (stats.__isset.min) {
page_statistics.set_min(stats.min);
}
if (stats.__isset.null_count) {
page_statistics.set_null_count(stats.null_count);
}
if (stats.__isset.distinct_count) {
page_statistics.set_distinct_count(stats.distinct_count);
}
return page_statistics;
}

What changes are included in this PR?

Do similar thing from parquet-mr to check and read min_value/max_value from thrift stats.

Are these changes tested?

Some test cases fail after the fix. Fixed them to make sure it is covered.

Are there any user-facing changes?

No.

@github-actions

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #14870has been automatically assigned in GitHub to PR creator.

@wgtmac

Copy link
Copy Markdown
MemberAuthor

@pitrou@wjones127@westonpace Could you please take a look?

@mapleFUmapleFU left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Rest LGTM

}
const format::Statistics& stats = header.statistics;
if (stats.__isset.max) {
// Use the new V2 min-max statistics over the former one if it is filled

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Previously, page_statistics will handle min-max separately. This patch changes it to once have all min-max, otherwise, cannot use min-max

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Although in parquet.thrift, min-max can exist only one. But I think handling it like this is ok

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

So we revert back to previous mode that only has min or max is ok?

@wjones127wjones127 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This looks good. @wgtmac before we merge, could you create a new issue for this bug fix? Also, if you could create issues for the TODOs as well, that would be appreciated.

@westonpace

Copy link
Copy Markdown
Member

Does this mean the following (testing my understanding)?

If min is set but max is not set then we should ignore both min and max

@wjones127

Copy link
Copy Markdown
Member

Does this mean the following (testing my understanding)?

If min is set but max is not set then we should ignore both min and max

Yes, it does mean we will. Do you foresee that as an issue? It sounds like Java implementation takes the same approach.

@wgtmacwgtmac changed the title GH-14870: [C++][Parquet] Fix parsing stats from min_value/max_valueGH-34138: [C++][Parquet] Fix parsing stats from min_value/max_valueFeb 11, 2023
@github-actions

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #34138has been automatically assigned in GitHub to PR creator.

@wgtmac

Copy link
Copy Markdown
MemberAuthor

Does this mean the following (testing my understanding)?

If min is set but max is not set then we should ignore both min and max

When only one of min and max exists, it usually happens when a binary value has an extreme length or a floating value has NaN. In this case, the stats provide little value and make it tricker to use.

@wgtmac

Copy link
Copy Markdown
MemberAuthor

This looks good. @wgtmac before we merge, could you create a new issue for this bug fix? Also, if you could create issues for the TODOs as well, that would be appreciated.

I have created a new issue for this PR and updated the title.

A separate issue has been created for the TODO items: #34139

Thanks for the review! @wjones127

@mapleFUmapleFU left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Rest LGTM

page_statistics.set_max(stats.max_value);
page_statistics.set_min(stats.min_value);
} else if (stats.__isset.max && stats.__isset.min) {
// TODO: check created_by to see if it is corrupted for some types.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I've no problem here, but just curious, does this code meaning the parquet-mr's CorruptStatistics.shouldIgnoreStatistics? (It's really trickey...)

@wgtmac

Copy link
Copy Markdown
MemberAuthor

Gentle ping @wjones127@pitrou

Once this gets merged, I will rebase #34107 which is blocked by it.

@emkornfield

Copy link
Copy Markdown
Contributor

CC @fatemehp

@westonpace

Copy link
Copy Markdown
Member

Yes, it does mean we will. Do you foresee that as an issue? It sounds like Java implementation takes the same approach.

In datasets, for row group statistics, we recently added a check that was roughly...

if (is_nan(min) && is_nan(max)) {
// Ignore statistics
} else if (is_nan(min)) {
// Assume x <= max
} else if(is_nan(max)) {
// Assume x >= min
} else {
// Assume min <= x <= max
}

In other words, if one of min or max is NaN then we still use the other side of the equality. I think my primary concern is to validate that is a safe assumption. In other words, I want to make sure we aren't using garbage data in our handling of row groups.

@wjones127

Copy link
Copy Markdown
Member

@westonpace that makes sense.

When only one of min and max exists, it usually happens when a binary value has an extreme length or a floating value has NaN. In this case, the stats provide little value and make it tricker to use.

It seems like we do have handling for these two cases. See Weston's message for NaN handling and max_statistics_size on WriterProperties. Based on that, I'd actually prefer we keep the ability to parse just the min or max if only one is available.

Comment threadcpp/src/parquet/column_reader.cc Outdated
Comment on lines +215 to +218
if (stats.__isset.max_value && stats.__isset.min_value) {
// TODO: check if the column_order is TYPE_DEFINED_ORDER.
page_statistics.set_max(stats.max_value);
page_statistics.set_min(stats.min_value);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
if (stats.__isset.max_value && stats.__isset.min_value) {
// TODO: check if the column_order is TYPE_DEFINED_ORDER.
page_statistics.set_max(stats.max_value);
page_statistics.set_min(stats.min_value);
if (stats.__isset.max_value || stats.__isset.min_value) {
// TODO: check if the column_order is TYPE_DEFINED_ORDER.
if (stats.__isset.max_value) {
page_statistics.set_max(stats.max_value);
}
if (stats.__isset.min_value) {
page_statistics.set_min(stats.min_value);
}

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed. Please take a look again. Thanks @wjones127 !

@westonpace

Copy link
Copy Markdown
Member

It seems like we do have handling for these two cases.

Just for clarity, the PR I linked (and my thoughts) were about how we currently handle row group statistics. I'm not sure if the rules are identical for page statistics. I mainly wanted to make sure my understanding of the row group statistics wasn't invalid.

@wgtmac

Copy link
Copy Markdown
MemberAuthor

It seems like we do have handling for these two cases.

Just for clarity, the PR I linked (and my thoughts) were about how we currently handle row group statistics. I'm not sure if the rules are identical for page statistics. I mainly wanted to make sure my understanding of the row group statistics wasn't invalid.

IIUC, row group statistics are aggregated from page statistics so they should share the same rules. The parquet thrift message definition does allow only one side of min or max exist:

/** * Statistics per row group and per page * All fields are optional.*/structStatistics {
/** * DEPRECATED: min and max value of the column. Use min_value and max_value. * * Values are encoded using PLAIN encoding, except that variable-length byte * arrays do not include a length prefix. * * These fields encode min and max values determined by signed comparison * only. New files should use the correct order for a column's logical type * and store the values in the min_value and max_value fields. * * To support older readers, these may be set when the column order is * signed.*/1:optionalbinarymax;
2:optionalbinarymin;
/** count of null value in the column */3:optionali64null_count;
/** count of distinct values occurring */4:optionali64distinct_count;
/** * Min and max values for the column, determined by its ColumnOrder. * * Values are encoded using PLAIN encoding, except that variable-length byte * arrays do not include a length prefix.*/5:optionalbinarymax_value;
6:optionalbinarymin_value;
}

On the other side, the story of page index is different. The column index definition does require existence of both min and max values if it is not a null page:

/** * Description for ColumnIndex. * Each <array-field>[i] refers to the page at OffsetIndex.page_locations[i]*/structColumnIndex {
/** * A list of Boolean values to determine the validity of the corresponding * min and max values. If true, a page contains only null values, and writers * have to set the corresponding entries in min_values and max_values to * byte[0], so that all lists have the same length. If false, the * corresponding entries in min_values and max_values must be valid.*/1:requiredlist<bool> null_pages/** * Two lists containing lower and upper bounds for the values of each page * determined by the ColumnOrder of the column. These may be the actual * minimum and maximum values found on a page, but can also be (more compact) * values that do not exist on a page. For example, instead of storing ""Blart * Versenwald III", a writer may set min_values[i]="B", max_values[i]="C". * Such more compact values must still be valid values within the column's * logical type. Readers must make sure that list entries are populated before * using them by inspecting null_pages.*/2:requiredlist<binary> min_values3:requiredlist<binary> max_values/** * Stores whether both min_values and max_values are ordered and if so, in * which direction. This allows readers to perform binary searches in both * lists. Readers cannot assume that max_values[i] <= min_values[i+1], even * if the lists are ordered.*/4:requiredBoundaryOrderboundary_order/** A list containing the number of null values for each page **/5:optionallist<i64> null_counts
}

So I am fine with parsing only one side min or max values from page/row group statistics. @westonpace@wjones127

@wgtmac

wgtmac commented Feb 17, 2023

Copy link
Copy Markdown
MemberAuthor

The CI build is failed due to a recent branch rename and will be fixed by #34218

@wgtmac

Copy link
Copy Markdown
MemberAuthor

@westonpace Could you please take another pass?

@wjones127
wjones127 merged commit 8e5e438 into apache:mainFeb 17, 2023
@ursabot

Copy link
Copy Markdown

Benchmark runs are scheduled for baseline = 1264e40 and contender = 8e5e438. 8e5e438 is a master commit associated with this PR. Results will be available as each benchmark for each run completes.
Conbench compare runs links:
[Finished ⬇️0.0% ⬆️0.0%] ec2-t3-xlarge-us-east-2
[Failed ⬇️0.34% ⬆️0.03%] test-mac-arm
[Finished ⬇️1.02% ⬆️0.0%] ursa-i9-9960x
[Finished ⬇️0.54% ⬆️0.03%] ursa-thinkcentre-m75q
Buildkite builds:
[Finished] 8e5e438d ec2-t3-xlarge-us-east-2
[Failed] 8e5e438d test-mac-arm
[Finished] 8e5e438d ursa-i9-9960x
[Finished] 8e5e438d ursa-thinkcentre-m75q
[Finished] 1264e409 ec2-t3-xlarge-us-east-2
[Finished] 1264e409 test-mac-arm
[Finished] 1264e409 ursa-i9-9960x
[Finished] 1264e409 ursa-thinkcentre-m75q
Supported benchmarks:
ec2-t3-xlarge-us-east-2: Supported benchmark langs: Python, R. Runs only benchmarks with cloud = True
test-mac-arm: Supported benchmark langs: C++, Python, R
ursa-i9-9960x: Supported benchmark langs: Python, R, JavaScript
ursa-thinkcentre-m75q: Supported benchmark langs: C++, Java

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[C++][Parquet] Fix parsing stats from min_value/max_value

6 participants

@wgtmac@westonpace@wjones127@emkornfield@ursabot@mapleFU
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

GH-34138: [C++][Parquet] Fix parsing stats from min_value/max_value - #34112

Merged
wjones127 merged 2 commits into
apache:mainfrom
wgtmac:parse_min_max
Feb 17, 2023
Merged

GH-34138: [C++][Parquet] Fix parsing stats from min_value/max_value#34112
wjones127 merged 2 commits into
apache:mainfrom
wgtmac:parse_min_max

Conversation

@wgtmac

@wgtmacwgtmac commented Feb 10, 2023

Copy link
Copy Markdown
Member

Rationale for this change

The code below does not read from stats.min_value/max_value at all.

// Extracts encoded statistics from V1 and V2 data page headerstemplate <typename H>
EncodedStatistics ExtractStatsFromHeader(const H& header) {
EncodedStatistics page_statistics;
if (!header.__isset.statistics) {
return page_statistics;
}
const format::Statistics& stats = header.statistics;
if (stats.__isset.max) {
page_statistics.set_max(stats.max);
}
if (stats.__isset.min) {
page_statistics.set_min(stats.min);
}
if (stats.__isset.null_count) {
page_statistics.set_null_count(stats.null_count);
}
if (stats.__isset.distinct_count) {
page_statistics.set_distinct_count(stats.distinct_count);
}
return page_statistics;
}

What changes are included in this PR?

Do similar thing from parquet-mr to check and read min_value/max_value from thrift stats.

Are these changes tested?

Some test cases fail after the fix. Fixed them to make sure it is covered.

Are there any user-facing changes?

No.

@github-actions

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #14870has been automatically assigned in GitHub to PR creator.

@wgtmac

Copy link
Copy Markdown
MemberAuthor

@pitrou@wjones127@westonpace Could you please take a look?

@mapleFUmapleFU left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Rest LGTM

}
const format::Statistics& stats = header.statistics;
if (stats.__isset.max) {
// Use the new V2 min-max statistics over the former one if it is filled

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Previously, page_statistics will handle min-max separately. This patch changes it to once have all min-max, otherwise, cannot use min-max

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Although in parquet.thrift, min-max can exist only one. But I think handling it like this is ok

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

So we revert back to previous mode that only has min or max is ok?

@wjones127wjones127 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This looks good. @wgtmac before we merge, could you create a new issue for this bug fix? Also, if you could create issues for the TODOs as well, that would be appreciated.

@westonpace

Copy link
Copy Markdown
Member

Does this mean the following (testing my understanding)?

If min is set but max is not set then we should ignore both min and max

@wjones127

Copy link
Copy Markdown
Member

Does this mean the following (testing my understanding)?

If min is set but max is not set then we should ignore both min and max

Yes, it does mean we will. Do you foresee that as an issue? It sounds like Java implementation takes the same approach.

@wgtmacwgtmac changed the title GH-14870: [C++][Parquet] Fix parsing stats from min_value/max_valueGH-34138: [C++][Parquet] Fix parsing stats from min_value/max_valueFeb 11, 2023
@github-actions

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #34138has been automatically assigned in GitHub to PR creator.

@wgtmac

Copy link
Copy Markdown
MemberAuthor

Does this mean the following (testing my understanding)?

If min is set but max is not set then we should ignore both min and max

When only one of min and max exists, it usually happens when a binary value has an extreme length or a floating value has NaN. In this case, the stats provide little value and make it tricker to use.

@wgtmac

Copy link
Copy Markdown
MemberAuthor

This looks good. @wgtmac before we merge, could you create a new issue for this bug fix? Also, if you could create issues for the TODOs as well, that would be appreciated.

I have created a new issue for this PR and updated the title.

A separate issue has been created for the TODO items: #34139

Thanks for the review! @wjones127

@mapleFUmapleFU left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Rest LGTM

page_statistics.set_max(stats.max_value);
page_statistics.set_min(stats.min_value);
} else if (stats.__isset.max && stats.__isset.min) {
// TODO: check created_by to see if it is corrupted for some types.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I've no problem here, but just curious, does this code meaning the parquet-mr's CorruptStatistics.shouldIgnoreStatistics? (It's really trickey...)

@wgtmac

Copy link
Copy Markdown
MemberAuthor

Gentle ping @wjones127@pitrou

Once this gets merged, I will rebase #34107 which is blocked by it.

@emkornfield

Copy link
Copy Markdown
Contributor

CC @fatemehp

@westonpace

Copy link
Copy Markdown
Member

Yes, it does mean we will. Do you foresee that as an issue? It sounds like Java implementation takes the same approach.

In datasets, for row group statistics, we recently added a check that was roughly...

if (is_nan(min) && is_nan(max)) {
// Ignore statistics
} else if (is_nan(min)) {
// Assume x <= max
} else if(is_nan(max)) {
// Assume x >= min
} else {
// Assume min <= x <= max
}

In other words, if one of min or max is NaN then we still use the other side of the equality. I think my primary concern is to validate that is a safe assumption. In other words, I want to make sure we aren't using garbage data in our handling of row groups.

@wjones127

Copy link
Copy Markdown
Member

@westonpace that makes sense.

When only one of min and max exists, it usually happens when a binary value has an extreme length or a floating value has NaN. In this case, the stats provide little value and make it tricker to use.

It seems like we do have handling for these two cases. See Weston's message for NaN handling and max_statistics_size on WriterProperties. Based on that, I'd actually prefer we keep the ability to parse just the min or max if only one is available.

Comment threadcpp/src/parquet/column_reader.cc Outdated
Comment on lines +215 to +218
if (stats.__isset.max_value && stats.__isset.min_value) {
// TODO: check if the column_order is TYPE_DEFINED_ORDER.
page_statistics.set_max(stats.max_value);
page_statistics.set_min(stats.min_value);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
if (stats.__isset.max_value && stats.__isset.min_value) {
// TODO: check if the column_order is TYPE_DEFINED_ORDER.
page_statistics.set_max(stats.max_value);
page_statistics.set_min(stats.min_value);
if (stats.__isset.max_value || stats.__isset.min_value) {
// TODO: check if the column_order is TYPE_DEFINED_ORDER.
if (stats.__isset.max_value) {
page_statistics.set_max(stats.max_value);
}
if (stats.__isset.min_value) {
page_statistics.set_min(stats.min_value);
}

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed. Please take a look again. Thanks @wjones127 !

@westonpace

Copy link
Copy Markdown
Member

It seems like we do have handling for these two cases.

Just for clarity, the PR I linked (and my thoughts) were about how we currently handle row group statistics. I'm not sure if the rules are identical for page statistics. I mainly wanted to make sure my understanding of the row group statistics wasn't invalid.

@wgtmac

Copy link
Copy Markdown
MemberAuthor

It seems like we do have handling for these two cases.

Just for clarity, the PR I linked (and my thoughts) were about how we currently handle row group statistics. I'm not sure if the rules are identical for page statistics. I mainly wanted to make sure my understanding of the row group statistics wasn't invalid.

IIUC, row group statistics are aggregated from page statistics so they should share the same rules. The parquet thrift message definition does allow only one side of min or max exist:

/** * Statistics per row group and per page * All fields are optional.*/structStatistics {
/** * DEPRECATED: min and max value of the column. Use min_value and max_value. * * Values are encoded using PLAIN encoding, except that variable-length byte * arrays do not include a length prefix. * * These fields encode min and max values determined by signed comparison * only. New files should use the correct order for a column's logical type * and store the values in the min_value and max_value fields. * * To support older readers, these may be set when the column order is * signed.*/1:optionalbinarymax;
2:optionalbinarymin;
/** count of null value in the column */3:optionali64null_count;
/** count of distinct values occurring */4:optionali64distinct_count;
/** * Min and max values for the column, determined by its ColumnOrder. * * Values are encoded using PLAIN encoding, except that variable-length byte * arrays do not include a length prefix.*/5:optionalbinarymax_value;
6:optionalbinarymin_value;
}

On the other side, the story of page index is different. The column index definition does require existence of both min and max values if it is not a null page:

/** * Description for ColumnIndex. * Each <array-field>[i] refers to the page at OffsetIndex.page_locations[i]*/structColumnIndex {
/** * A list of Boolean values to determine the validity of the corresponding * min and max values. If true, a page contains only null values, and writers * have to set the corresponding entries in min_values and max_values to * byte[0], so that all lists have the same length. If false, the * corresponding entries in min_values and max_values must be valid.*/1:requiredlist<bool> null_pages/** * Two lists containing lower and upper bounds for the values of each page * determined by the ColumnOrder of the column. These may be the actual * minimum and maximum values found on a page, but can also be (more compact) * values that do not exist on a page. For example, instead of storing ""Blart * Versenwald III", a writer may set min_values[i]="B", max_values[i]="C". * Such more compact values must still be valid values within the column's * logical type. Readers must make sure that list entries are populated before * using them by inspecting null_pages.*/2:requiredlist<binary> min_values3:requiredlist<binary> max_values/** * Stores whether both min_values and max_values are ordered and if so, in * which direction. This allows readers to perform binary searches in both * lists. Readers cannot assume that max_values[i] <= min_values[i+1], even * if the lists are ordered.*/4:requiredBoundaryOrderboundary_order/** A list containing the number of null values for each page **/5:optionallist<i64> null_counts
}

So I am fine with parsing only one side min or max values from page/row group statistics. @westonpace@wjones127

@wgtmac

wgtmac commented Feb 17, 2023

Copy link
Copy Markdown
MemberAuthor

The CI build is failed due to a recent branch rename and will be fixed by #34218

@wgtmac

Copy link
Copy Markdown
MemberAuthor

@westonpace Could you please take another pass?

@wjones127
wjones127 merged commit 8e5e438 into apache:mainFeb 17, 2023
@ursabot

Copy link
Copy Markdown

Benchmark runs are scheduled for baseline = 1264e40 and contender = 8e5e438. 8e5e438 is a master commit associated with this PR. Results will be available as each benchmark for each run completes.
Conbench compare runs links:
[Finished ⬇️0.0% ⬆️0.0%] ec2-t3-xlarge-us-east-2
[Failed ⬇️0.34% ⬆️0.03%] test-mac-arm
[Finished ⬇️1.02% ⬆️0.0%] ursa-i9-9960x
[Finished ⬇️0.54% ⬆️0.03%] ursa-thinkcentre-m75q
Buildkite builds:
[Finished] 8e5e438d ec2-t3-xlarge-us-east-2
[Failed] 8e5e438d test-mac-arm
[Finished] 8e5e438d ursa-i9-9960x
[Finished] 8e5e438d ursa-thinkcentre-m75q
[Finished] 1264e409 ec2-t3-xlarge-us-east-2
[Finished] 1264e409 test-mac-arm
[Finished] 1264e409 ursa-i9-9960x
[Finished] 1264e409 ursa-thinkcentre-m75q
Supported benchmarks:
ec2-t3-xlarge-us-east-2: Supported benchmark langs: Python, R. Runs only benchmarks with cloud = True
test-mac-arm: Supported benchmark langs: C++, Python, R
ursa-i9-9960x: Supported benchmark langs: Python, R, JavaScript
ursa-thinkcentre-m75q: Supported benchmark langs: C++, Java

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[C++][Parquet] Fix parsing stats from min_value/max_value

6 participants

@wgtmac@westonpace@wjones127@emkornfield@ursabot@mapleFU
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

GH-34138: [C++][Parquet] Fix parsing stats from min_value/max_value - #34112

Merged
wjones127 merged 2 commits into
apache:mainfrom
wgtmac:parse_min_max
Feb 17, 2023
Merged

GH-34138: [C++][Parquet] Fix parsing stats from min_value/max_value#34112
wjones127 merged 2 commits into
apache:mainfrom
wgtmac:parse_min_max

Conversation

@wgtmac

@wgtmacwgtmac commented Feb 10, 2023

Copy link
Copy Markdown
Member

Rationale for this change

The code below does not read from stats.min_value/max_value at all.

// Extracts encoded statistics from V1 and V2 data page headerstemplate <typename H>
EncodedStatistics ExtractStatsFromHeader(const H& header) {
EncodedStatistics page_statistics;
if (!header.__isset.statistics) {
return page_statistics;
}
const format::Statistics& stats = header.statistics;
if (stats.__isset.max) {
page_statistics.set_max(stats.max);
}
if (stats.__isset.min) {
page_statistics.set_min(stats.min);
}
if (stats.__isset.null_count) {
page_statistics.set_null_count(stats.null_count);
}
if (stats.__isset.distinct_count) {
page_statistics.set_distinct_count(stats.distinct_count);
}
return page_statistics;
}

What changes are included in this PR?

Do similar thing from parquet-mr to check and read min_value/max_value from thrift stats.

Are these changes tested?

Some test cases fail after the fix. Fixed them to make sure it is covered.

Are there any user-facing changes?

No.

@github-actions

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #14870has been automatically assigned in GitHub to PR creator.

@wgtmac

Copy link
Copy Markdown
MemberAuthor

@pitrou@wjones127@westonpace Could you please take a look?

@mapleFUmapleFU left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Rest LGTM

}
const format::Statistics& stats = header.statistics;
if (stats.__isset.max) {
// Use the new V2 min-max statistics over the former one if it is filled

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Previously, page_statistics will handle min-max separately. This patch changes it to once have all min-max, otherwise, cannot use min-max

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Although in parquet.thrift, min-max can exist only one. But I think handling it like this is ok

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

So we revert back to previous mode that only has min or max is ok?

@wjones127wjones127 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This looks good. @wgtmac before we merge, could you create a new issue for this bug fix? Also, if you could create issues for the TODOs as well, that would be appreciated.

@westonpace

Copy link
Copy Markdown
Member

Does this mean the following (testing my understanding)?

If min is set but max is not set then we should ignore both min and max

@wjones127

Copy link
Copy Markdown
Member

Does this mean the following (testing my understanding)?

If min is set but max is not set then we should ignore both min and max

Yes, it does mean we will. Do you foresee that as an issue? It sounds like Java implementation takes the same approach.

@wgtmacwgtmac changed the title GH-14870: [C++][Parquet] Fix parsing stats from min_value/max_valueGH-34138: [C++][Parquet] Fix parsing stats from min_value/max_valueFeb 11, 2023
@github-actions

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #34138has been automatically assigned in GitHub to PR creator.

@wgtmac

Copy link
Copy Markdown
MemberAuthor

Does this mean the following (testing my understanding)?

If min is set but max is not set then we should ignore both min and max

When only one of min and max exists, it usually happens when a binary value has an extreme length or a floating value has NaN. In this case, the stats provide little value and make it tricker to use.

@wgtmac

Copy link
Copy Markdown
MemberAuthor

This looks good. @wgtmac before we merge, could you create a new issue for this bug fix? Also, if you could create issues for the TODOs as well, that would be appreciated.

I have created a new issue for this PR and updated the title.

A separate issue has been created for the TODO items: #34139

Thanks for the review! @wjones127

@mapleFUmapleFU left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Rest LGTM

page_statistics.set_max(stats.max_value);
page_statistics.set_min(stats.min_value);
} else if (stats.__isset.max && stats.__isset.min) {
// TODO: check created_by to see if it is corrupted for some types.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I've no problem here, but just curious, does this code meaning the parquet-mr's CorruptStatistics.shouldIgnoreStatistics? (It's really trickey...)

@wgtmac

Copy link
Copy Markdown
MemberAuthor

Gentle ping @wjones127@pitrou

Once this gets merged, I will rebase #34107 which is blocked by it.

@emkornfield

Copy link
Copy Markdown
Contributor

CC @fatemehp

@westonpace

Copy link
Copy Markdown
Member

Yes, it does mean we will. Do you foresee that as an issue? It sounds like Java implementation takes the same approach.

In datasets, for row group statistics, we recently added a check that was roughly...

if (is_nan(min) && is_nan(max)) {
// Ignore statistics
} else if (is_nan(min)) {
// Assume x <= max
} else if(is_nan(max)) {
// Assume x >= min
} else {
// Assume min <= x <= max
}

In other words, if one of min or max is NaN then we still use the other side of the equality. I think my primary concern is to validate that is a safe assumption. In other words, I want to make sure we aren't using garbage data in our handling of row groups.

@wjones127

Copy link
Copy Markdown
Member

@westonpace that makes sense.

When only one of min and max exists, it usually happens when a binary value has an extreme length or a floating value has NaN. In this case, the stats provide little value and make it tricker to use.

It seems like we do have handling for these two cases. See Weston's message for NaN handling and max_statistics_size on WriterProperties. Based on that, I'd actually prefer we keep the ability to parse just the min or max if only one is available.

Comment threadcpp/src/parquet/column_reader.cc Outdated
Comment on lines +215 to +218
if (stats.__isset.max_value && stats.__isset.min_value) {
// TODO: check if the column_order is TYPE_DEFINED_ORDER.
page_statistics.set_max(stats.max_value);
page_statistics.set_min(stats.min_value);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
if (stats.__isset.max_value && stats.__isset.min_value) {
// TODO: check if the column_order is TYPE_DEFINED_ORDER.
page_statistics.set_max(stats.max_value);
page_statistics.set_min(stats.min_value);
if (stats.__isset.max_value || stats.__isset.min_value) {
// TODO: check if the column_order is TYPE_DEFINED_ORDER.
if (stats.__isset.max_value) {
page_statistics.set_max(stats.max_value);
}
if (stats.__isset.min_value) {
page_statistics.set_min(stats.min_value);
}

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed. Please take a look again. Thanks @wjones127 !

@westonpace

Copy link
Copy Markdown
Member

It seems like we do have handling for these two cases.

Just for clarity, the PR I linked (and my thoughts) were about how we currently handle row group statistics. I'm not sure if the rules are identical for page statistics. I mainly wanted to make sure my understanding of the row group statistics wasn't invalid.

@wgtmac

Copy link
Copy Markdown
MemberAuthor

It seems like we do have handling for these two cases.

Just for clarity, the PR I linked (and my thoughts) were about how we currently handle row group statistics. I'm not sure if the rules are identical for page statistics. I mainly wanted to make sure my understanding of the row group statistics wasn't invalid.

IIUC, row group statistics are aggregated from page statistics so they should share the same rules. The parquet thrift message definition does allow only one side of min or max exist:

/** * Statistics per row group and per page * All fields are optional.*/structStatistics {
/** * DEPRECATED: min and max value of the column. Use min_value and max_value. * * Values are encoded using PLAIN encoding, except that variable-length byte * arrays do not include a length prefix. * * These fields encode min and max values determined by signed comparison * only. New files should use the correct order for a column's logical type * and store the values in the min_value and max_value fields. * * To support older readers, these may be set when the column order is * signed.*/1:optionalbinarymax;
2:optionalbinarymin;
/** count of null value in the column */3:optionali64null_count;
/** count of distinct values occurring */4:optionali64distinct_count;
/** * Min and max values for the column, determined by its ColumnOrder. * * Values are encoded using PLAIN encoding, except that variable-length byte * arrays do not include a length prefix.*/5:optionalbinarymax_value;
6:optionalbinarymin_value;
}

On the other side, the story of page index is different. The column index definition does require existence of both min and max values if it is not a null page:

/** * Description for ColumnIndex. * Each <array-field>[i] refers to the page at OffsetIndex.page_locations[i]*/structColumnIndex {
/** * A list of Boolean values to determine the validity of the corresponding * min and max values. If true, a page contains only null values, and writers * have to set the corresponding entries in min_values and max_values to * byte[0], so that all lists have the same length. If false, the * corresponding entries in min_values and max_values must be valid.*/1:requiredlist<bool> null_pages/** * Two lists containing lower and upper bounds for the values of each page * determined by the ColumnOrder of the column. These may be the actual * minimum and maximum values found on a page, but can also be (more compact) * values that do not exist on a page. For example, instead of storing ""Blart * Versenwald III", a writer may set min_values[i]="B", max_values[i]="C". * Such more compact values must still be valid values within the column's * logical type. Readers must make sure that list entries are populated before * using them by inspecting null_pages.*/2:requiredlist<binary> min_values3:requiredlist<binary> max_values/** * Stores whether both min_values and max_values are ordered and if so, in * which direction. This allows readers to perform binary searches in both * lists. Readers cannot assume that max_values[i] <= min_values[i+1], even * if the lists are ordered.*/4:requiredBoundaryOrderboundary_order/** A list containing the number of null values for each page **/5:optionallist<i64> null_counts
}

So I am fine with parsing only one side min or max values from page/row group statistics. @westonpace@wjones127

@wgtmac

wgtmac commented Feb 17, 2023

Copy link
Copy Markdown
MemberAuthor

The CI build is failed due to a recent branch rename and will be fixed by #34218

@wgtmac

Copy link
Copy Markdown
MemberAuthor

@westonpace Could you please take another pass?

@wjones127
wjones127 merged commit 8e5e438 into apache:mainFeb 17, 2023
@ursabot

Copy link
Copy Markdown

Benchmark runs are scheduled for baseline = 1264e40 and contender = 8e5e438. 8e5e438 is a master commit associated with this PR. Results will be available as each benchmark for each run completes.
Conbench compare runs links:
[Finished ⬇️0.0% ⬆️0.0%] ec2-t3-xlarge-us-east-2
[Failed ⬇️0.34% ⬆️0.03%] test-mac-arm
[Finished ⬇️1.02% ⬆️0.0%] ursa-i9-9960x
[Finished ⬇️0.54% ⬆️0.03%] ursa-thinkcentre-m75q
Buildkite builds:
[Finished] 8e5e438d ec2-t3-xlarge-us-east-2
[Failed] 8e5e438d test-mac-arm
[Finished] 8e5e438d ursa-i9-9960x
[Finished] 8e5e438d ursa-thinkcentre-m75q
[Finished] 1264e409 ec2-t3-xlarge-us-east-2
[Finished] 1264e409 test-mac-arm
[Finished] 1264e409 ursa-i9-9960x
[Finished] 1264e409 ursa-thinkcentre-m75q
Supported benchmarks:
ec2-t3-xlarge-us-east-2: Supported benchmark langs: Python, R. Runs only benchmarks with cloud = True
test-mac-arm: Supported benchmark langs: C++, Python, R
ursa-i9-9960x: Supported benchmark langs: Python, R, JavaScript
ursa-thinkcentre-m75q: Supported benchmark langs: C++, Java

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[C++][Parquet] Fix parsing stats from min_value/max_value

6 participants

@wgtmac@westonpace@wjones127@emkornfield@ursabot@mapleFU
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

GH-34138: [C++][Parquet] Fix parsing stats from min_value/max_value - #34112

Merged
wjones127 merged 2 commits into
apache:mainfrom
wgtmac:parse_min_max
Feb 17, 2023
Merged

GH-34138: [C++][Parquet] Fix parsing stats from min_value/max_value#34112
wjones127 merged 2 commits into
apache:mainfrom
wgtmac:parse_min_max

Conversation

@wgtmac

@wgtmacwgtmac commented Feb 10, 2023

Copy link
Copy Markdown
Member

Rationale for this change

The code below does not read from stats.min_value/max_value at all.

// Extracts encoded statistics from V1 and V2 data page headerstemplate <typename H>
EncodedStatistics ExtractStatsFromHeader(const H& header) {
EncodedStatistics page_statistics;
if (!header.__isset.statistics) {
return page_statistics;
}
const format::Statistics& stats = header.statistics;
if (stats.__isset.max) {
page_statistics.set_max(stats.max);
}
if (stats.__isset.min) {
page_statistics.set_min(stats.min);
}
if (stats.__isset.null_count) {
page_statistics.set_null_count(stats.null_count);
}
if (stats.__isset.distinct_count) {
page_statistics.set_distinct_count(stats.distinct_count);
}
return page_statistics;
}

What changes are included in this PR?

Do similar thing from parquet-mr to check and read min_value/max_value from thrift stats.

Are these changes tested?

Some test cases fail after the fix. Fixed them to make sure it is covered.

Are there any user-facing changes?

No.

@github-actions

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #14870has been automatically assigned in GitHub to PR creator.

@wgtmac

Copy link
Copy Markdown
MemberAuthor

@pitrou@wjones127@westonpace Could you please take a look?

@mapleFUmapleFU left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Rest LGTM

}
const format::Statistics& stats = header.statistics;
if (stats.__isset.max) {
// Use the new V2 min-max statistics over the former one if it is filled

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Previously, page_statistics will handle min-max separately. This patch changes it to once have all min-max, otherwise, cannot use min-max

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Although in parquet.thrift, min-max can exist only one. But I think handling it like this is ok

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

So we revert back to previous mode that only has min or max is ok?

@wjones127wjones127 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This looks good. @wgtmac before we merge, could you create a new issue for this bug fix? Also, if you could create issues for the TODOs as well, that would be appreciated.

@westonpace

Copy link
Copy Markdown
Member

Does this mean the following (testing my understanding)?

If min is set but max is not set then we should ignore both min and max

@wjones127

Copy link
Copy Markdown
Member

Does this mean the following (testing my understanding)?

If min is set but max is not set then we should ignore both min and max

Yes, it does mean we will. Do you foresee that as an issue? It sounds like Java implementation takes the same approach.

@wgtmacwgtmac changed the title GH-14870: [C++][Parquet] Fix parsing stats from min_value/max_valueGH-34138: [C++][Parquet] Fix parsing stats from min_value/max_valueFeb 11, 2023
@github-actions

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #34138has been automatically assigned in GitHub to PR creator.

@wgtmac

Copy link
Copy Markdown
MemberAuthor

Does this mean the following (testing my understanding)?

If min is set but max is not set then we should ignore both min and max

When only one of min and max exists, it usually happens when a binary value has an extreme length or a floating value has NaN. In this case, the stats provide little value and make it tricker to use.

@wgtmac

Copy link
Copy Markdown
MemberAuthor

This looks good. @wgtmac before we merge, could you create a new issue for this bug fix? Also, if you could create issues for the TODOs as well, that would be appreciated.

I have created a new issue for this PR and updated the title.

A separate issue has been created for the TODO items: #34139

Thanks for the review! @wjones127

@mapleFUmapleFU left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Rest LGTM

page_statistics.set_max(stats.max_value);
page_statistics.set_min(stats.min_value);
} else if (stats.__isset.max && stats.__isset.min) {
// TODO: check created_by to see if it is corrupted for some types.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I've no problem here, but just curious, does this code meaning the parquet-mr's CorruptStatistics.shouldIgnoreStatistics? (It's really trickey...)

@wgtmac

Copy link
Copy Markdown
MemberAuthor

Gentle ping @wjones127@pitrou

Once this gets merged, I will rebase #34107 which is blocked by it.

@emkornfield

Copy link
Copy Markdown
Contributor

CC @fatemehp

@westonpace

Copy link
Copy Markdown
Member

Yes, it does mean we will. Do you foresee that as an issue? It sounds like Java implementation takes the same approach.

In datasets, for row group statistics, we recently added a check that was roughly...

if (is_nan(min) && is_nan(max)) {
// Ignore statistics
} else if (is_nan(min)) {
// Assume x <= max
} else if(is_nan(max)) {
// Assume x >= min
} else {
// Assume min <= x <= max
}

In other words, if one of min or max is NaN then we still use the other side of the equality. I think my primary concern is to validate that is a safe assumption. In other words, I want to make sure we aren't using garbage data in our handling of row groups.

@wjones127

Copy link
Copy Markdown
Member

@westonpace that makes sense.

When only one of min and max exists, it usually happens when a binary value has an extreme length or a floating value has NaN. In this case, the stats provide little value and make it tricker to use.

It seems like we do have handling for these two cases. See Weston's message for NaN handling and max_statistics_size on WriterProperties. Based on that, I'd actually prefer we keep the ability to parse just the min or max if only one is available.

Comment threadcpp/src/parquet/column_reader.cc Outdated
Comment on lines +215 to +218
if (stats.__isset.max_value && stats.__isset.min_value) {
// TODO: check if the column_order is TYPE_DEFINED_ORDER.
page_statistics.set_max(stats.max_value);
page_statistics.set_min(stats.min_value);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
if (stats.__isset.max_value && stats.__isset.min_value) {
// TODO: check if the column_order is TYPE_DEFINED_ORDER.
page_statistics.set_max(stats.max_value);
page_statistics.set_min(stats.min_value);
if (stats.__isset.max_value || stats.__isset.min_value) {
// TODO: check if the column_order is TYPE_DEFINED_ORDER.
if (stats.__isset.max_value) {
page_statistics.set_max(stats.max_value);
}
if (stats.__isset.min_value) {
page_statistics.set_min(stats.min_value);
}

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed. Please take a look again. Thanks @wjones127 !

@westonpace

Copy link
Copy Markdown
Member

It seems like we do have handling for these two cases.

Just for clarity, the PR I linked (and my thoughts) were about how we currently handle row group statistics. I'm not sure if the rules are identical for page statistics. I mainly wanted to make sure my understanding of the row group statistics wasn't invalid.

@wgtmac

Copy link
Copy Markdown
MemberAuthor

It seems like we do have handling for these two cases.

Just for clarity, the PR I linked (and my thoughts) were about how we currently handle row group statistics. I'm not sure if the rules are identical for page statistics. I mainly wanted to make sure my understanding of the row group statistics wasn't invalid.

IIUC, row group statistics are aggregated from page statistics so they should share the same rules. The parquet thrift message definition does allow only one side of min or max exist:

/** * Statistics per row group and per page * All fields are optional.*/structStatistics {
/** * DEPRECATED: min and max value of the column. Use min_value and max_value. * * Values are encoded using PLAIN encoding, except that variable-length byte * arrays do not include a length prefix. * * These fields encode min and max values determined by signed comparison * only. New files should use the correct order for a column's logical type * and store the values in the min_value and max_value fields. * * To support older readers, these may be set when the column order is * signed.*/1:optionalbinarymax;
2:optionalbinarymin;
/** count of null value in the column */3:optionali64null_count;
/** count of distinct values occurring */4:optionali64distinct_count;
/** * Min and max values for the column, determined by its ColumnOrder. * * Values are encoded using PLAIN encoding, except that variable-length byte * arrays do not include a length prefix.*/5:optionalbinarymax_value;
6:optionalbinarymin_value;
}

On the other side, the story of page index is different. The column index definition does require existence of both min and max values if it is not a null page:

/** * Description for ColumnIndex. * Each <array-field>[i] refers to the page at OffsetIndex.page_locations[i]*/structColumnIndex {
/** * A list of Boolean values to determine the validity of the corresponding * min and max values. If true, a page contains only null values, and writers * have to set the corresponding entries in min_values and max_values to * byte[0], so that all lists have the same length. If false, the * corresponding entries in min_values and max_values must be valid.*/1:requiredlist<bool> null_pages/** * Two lists containing lower and upper bounds for the values of each page * determined by the ColumnOrder of the column. These may be the actual * minimum and maximum values found on a page, but can also be (more compact) * values that do not exist on a page. For example, instead of storing ""Blart * Versenwald III", a writer may set min_values[i]="B", max_values[i]="C". * Such more compact values must still be valid values within the column's * logical type. Readers must make sure that list entries are populated before * using them by inspecting null_pages.*/2:requiredlist<binary> min_values3:requiredlist<binary> max_values/** * Stores whether both min_values and max_values are ordered and if so, in * which direction. This allows readers to perform binary searches in both * lists. Readers cannot assume that max_values[i] <= min_values[i+1], even * if the lists are ordered.*/4:requiredBoundaryOrderboundary_order/** A list containing the number of null values for each page **/5:optionallist<i64> null_counts
}

So I am fine with parsing only one side min or max values from page/row group statistics. @westonpace@wjones127

@wgtmac

wgtmac commented Feb 17, 2023

Copy link
Copy Markdown
MemberAuthor

The CI build is failed due to a recent branch rename and will be fixed by #34218

@wgtmac

Copy link
Copy Markdown
MemberAuthor

@westonpace Could you please take another pass?

@wjones127
wjones127 merged commit 8e5e438 into apache:mainFeb 17, 2023
@ursabot

Copy link
Copy Markdown

Benchmark runs are scheduled for baseline = 1264e40 and contender = 8e5e438. 8e5e438 is a master commit associated with this PR. Results will be available as each benchmark for each run completes.
Conbench compare runs links:
[Finished ⬇️0.0% ⬆️0.0%] ec2-t3-xlarge-us-east-2
[Failed ⬇️0.34% ⬆️0.03%] test-mac-arm
[Finished ⬇️1.02% ⬆️0.0%] ursa-i9-9960x
[Finished ⬇️0.54% ⬆️0.03%] ursa-thinkcentre-m75q
Buildkite builds:
[Finished] 8e5e438d ec2-t3-xlarge-us-east-2
[Failed] 8e5e438d test-mac-arm
[Finished] 8e5e438d ursa-i9-9960x
[Finished] 8e5e438d ursa-thinkcentre-m75q
[Finished] 1264e409 ec2-t3-xlarge-us-east-2
[Finished] 1264e409 test-mac-arm
[Finished] 1264e409 ursa-i9-9960x
[Finished] 1264e409 ursa-thinkcentre-m75q
Supported benchmarks:
ec2-t3-xlarge-us-east-2: Supported benchmark langs: Python, R. Runs only benchmarks with cloud = True
test-mac-arm: Supported benchmark langs: C++, Python, R
ursa-i9-9960x: Supported benchmark langs: Python, R, JavaScript
ursa-thinkcentre-m75q: Supported benchmark langs: C++, Java

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[C++][Parquet] Fix parsing stats from min_value/max_value

6 participants

@wgtmac@westonpace@wjones127@emkornfield@ursabot@mapleFU