This is an automated email from the ASF dual-hosted git repository.
Gabriel39 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new a22aca4f6cf [fix](be) Normalize Iceberg Parquet decimal manifest
bounds (#68548)
a22aca4f6cf is described below
commit a22aca4f6cf73ae78ad1aa7e1354b7c232e21b37
Author: Gabriel <[email protected]>
AuthorDate: Tue Sep 29 09:37:33 2026 +0800
[fix](be) Normalize Iceberg Parquet decimal manifest bounds (#68548)
### What problem does this PR solve?
Iceberg Parquet writes copy the encoded Parquet decimal min/max
statistics directly into manifest bounds. For example, the unscaled
value 128 can be written as `00000080` instead of `0080`. These padded
bounds do not follow [Iceberg's single-value
serialization](https://iceberg.apache.org/spec/#appendix-d-single-value-serialization),
which requires the minimal signed big-endian representation.
Normalize DECIMAL bounds after merging row-group statistics. Preserve
the necessary sign byte and convert integer physical representations
from little endian. Other logical types retain their existing encoding.
Add writer unit coverage for precisions 1–38 and sign boundaries, an
Iceberg write regression for DECIMAL32/64/128 metadata and filtered
reads, and refresh the existing Parquet statistics expectations.
### Release note
Fix Iceberg Parquet DECIMAL manifest bounds to use the minimal signed
big-endian encoding required by the Iceberg specification.
Validation: clang-format 16, clang-tidy, and build hygiene checks passed
after rebasing. C++ compilation completed, but the local writer
unit-test executable could not link: the upstream CDC test now
references `posix_spawn_file_actions_addclose`, which pulls in toolchain
glibc compatibility definitions that conflict with Doris definitions. No
unit tests executed in this post-rebase attempt. On the original PR
commit, the ASAN BE/FE build, all 4 writer unit tests, and both Iceberg
regression suites passed; the new unit test failed with the original
implementation and passed with the fix.
### Check List (For Author)
- Test:
- [x] Regression test: `test_iceberg_write_decimal_bounds`,
`test_iceberg_write_stats2`
- [x] Unit Test: `VParquetWriterTest.*`
- Behavior changed:
- [x] Yes. Newly written Parquet DECIMAL bounds omit redundant
sign-extension bytes while preserving their values.
- Does this need documentation?
- [x] No.
### Check List (For Reviewer who merge this PR)
- [ ] Confirm the release note
- [ ] Confirm test cases
- [ ] Confirm document
- [ ] Add branch pick label
---
.../format/transformer/viceberg_parquet_writer.cpp | 30 +++++++-
.../format/transformer/vparquet_writer_test.cpp | 76 +++++++++++++++++++
.../write/test_iceberg_write_decimal_bounds.out | 55 ++++++++++++++
.../iceberg/write/test_iceberg_write_stats2.out | 6 +-
.../write/test_iceberg_write_decimal_bounds.groovy | 88 ++++++++++++++++++++++
5 files changed, 250 insertions(+), 5 deletions(-)
diff --git a/be/src/format/transformer/viceberg_parquet_writer.cpp
b/be/src/format/transformer/viceberg_parquet_writer.cpp
index 10fde7aece3..57f329014d0 100644
--- a/be/src/format/transformer/viceberg_parquet_writer.cpp
+++ b/be/src/format/transformer/viceberg_parquet_writer.cpp
@@ -20,12 +20,38 @@
#include <parquet/api/reader.h>
#include <parquet/schema.h>
+#include <algorithm>
+
#include "format/table/iceberg/iceberg_arrow_block_convertor.h"
#include "format/table/parquet_utils.h"
#include "runtime/runtime_state.h"
namespace doris {
+static std::string encode_iceberg_bound(const ::parquet::Statistics& stats,
std::string encoded) {
+ if (!stats.descr()->logical_type()->is_decimal()) {
+ return encoded;
+ }
+ // Parquet integer statistics are little-endian, while binary decimals
already use big-endian.
+ // Iceberg bounds require the unscaled value's shortest signed big-endian
representation.
+ if (stats.physical_type() == ::parquet::Type::INT32 ||
+ stats.physical_type() == ::parquet::Type::INT64) {
+ std::reverse(encoded.begin(), encoded.end());
+ }
+ size_t start = 0;
+ while (start + 1 < encoded.size()) {
+ const auto byte = static_cast<uint8_t>(encoded[start]);
+ const bool next_is_negative = (static_cast<uint8_t>(encoded[start +
1]) & 0x80) != 0;
+ if ((byte == 0 && !next_is_negative) || (byte == 0xff &&
next_is_negative)) {
+ ++start;
+ } else {
+ break;
+ }
+ }
+ encoded.erase(0, start);
+ return encoded;
+}
+
VIcebergParquetWriter::VIcebergParquetWriter(RuntimeState* state,
io::FileWriter* file_writer,
const VExprContextSPtrs&
output_vexpr_ctxs,
std::vector<std::string>
column_names,
@@ -90,8 +116,8 @@ Status
VIcebergParquetWriter::collect_file_statistics_after_close(TIcebergColumn
}
if (column_stat->HasMinMax()) {
has_any_min_max = true;
- lower_bounds[field_id] = column_stat->EncodeMin();
- upper_bounds[field_id] = column_stat->EncodeMax();
+ lower_bounds[field_id] = encode_iceberg_bound(*column_stat,
column_stat->EncodeMin());
+ upper_bounds[field_id] = encode_iceberg_bound(*column_stat,
column_stat->EncodeMax());
}
}
diff --git a/be/test/format/transformer/vparquet_writer_test.cpp
b/be/test/format/transformer/vparquet_writer_test.cpp
index f5a5b10c2b9..e7b446630f3 100644
--- a/be/test/format/transformer/vparquet_writer_test.cpp
+++ b/be/test/format/transformer/vparquet_writer_test.cpp
@@ -24,13 +24,16 @@
#include <parquet/arrow/reader.h>
#include <parquet/schema.h>
+#include <algorithm>
#include <optional>
#include <string_view>
#include "core/block/block.h"
#include "core/column/column_array.h"
+#include "core/column/column_decimal.h"
#include "core/column/column_nullable.h"
#include "core/data_type/data_type_array.h"
+#include "core/data_type/data_type_decimal.h"
#include "core/data_type/data_type_factory.hpp"
#include "core/data_type/data_type_nullable.h"
#include "core/data_type/data_type_number.h"
@@ -64,6 +67,62 @@ protected:
static_cast<void>(_fs->delete_file(_file_path));
}
+ void check_decimal_bounds(int64_t unscaled, const std::string& expected) {
+ const int min_precision = unscaled > -10 && unscaled < 10 ? 1 : 3;
+ DataTypes types;
+ std::vector<std::string> names;
+ std::string json = R"({"type":"struct","fields":[)";
+ Block block;
+ for (int precision = min_precision; precision <= 38; ++precision) {
+ const auto name = "d" + std::to_string(precision);
+ const int scale = std::min(precision, 2);
+ auto type = make_nullable(create_decimal(precision, scale, false));
+ auto column = type->create_column();
+ if (precision <= 9) {
+
column->insert(Field::create_field<TYPE_DECIMAL32>(Decimal32(unscaled)));
+ } else if (precision <= 18) {
+
column->insert(Field::create_field<TYPE_DECIMAL64>(Decimal64(unscaled)));
+ } else {
+
column->insert(Field::create_field<TYPE_DECIMAL128I>(Decimal128V3(unscaled)));
+ }
+ column->insert_default();
+ block.insert({std::move(column), type, name});
+ types.push_back(type);
+ names.push_back(name);
+ if (precision != min_precision) {
+ json += ",";
+ }
+ json += "{\"id\":" + std::to_string(precision) + ",\"name\":\"" +
name +
+ "\",\"required\":false,\"type\":\"decimal(" +
std::to_string(precision) + "," +
+ std::to_string(scale) + ")\"}";
+ }
+ json += "]}";
+ auto schema = iceberg::SchemaParser::from_json(json);
+ auto output_exprs = MockSlotRef::create_mock_contexts(types);
+ io::FileWriterPtr file_writer;
+ ASSERT_TRUE(_fs->create_file(_file_path, &file_writer).ok());
+ RuntimeState state;
+ state.set_timezone("UTC");
+ ParquetFileOptions options {.compression_type =
TParquetCompressionType::UNCOMPRESSED,
+ .parquet_version =
TParquetVersion::PARQUET_1_0,
+ .parquet_disable_dictionary = false,
+ .enable_int96_timestamps = false};
+ VIcebergParquetWriter writer(&state, file_writer.get(), output_exprs,
names, false, options,
+ &json, *schema);
+ ASSERT_TRUE(writer.open().ok());
+ ASSERT_TRUE(writer.write(block).ok());
+ ASSERT_TRUE(writer.close().ok());
+ TIcebergColumnStats stats;
+ ASSERT_TRUE(writer.collect_file_statistics_after_close(&stats).ok());
+ for (int precision = min_precision; precision <= 38; ++precision) {
+ SCOPED_TRACE("precision=" + std::to_string(precision));
+ EXPECT_EQ(expected, stats.lower_bounds.at(precision));
+ EXPECT_EQ(expected, stats.upper_bounds.at(precision));
+ EXPECT_EQ(2, stats.value_counts.at(precision));
+ EXPECT_EQ(1, stats.null_value_counts.at(precision));
+ }
+ }
+
arrow::MemoryPool* _previous_pool = nullptr;
std::string _file_path;
std::shared_ptr<io::FileSystem> _fs;
@@ -105,6 +164,23 @@ TEST_F(VParquetWriterTest,
WritesIcebergPrimitiveAndCollectsMetrics) {
EXPECT_EQ(std::string("\x03\x00\x00\x00", 4), stats.upper_bounds.at(7));
}
+TEST_F(VParquetWriterTest, IcebergDecimalBoundsUseMinimalSignedBytes) {
+ for (const auto& [unscaled, expected] :
+ std::vector<std::pair<int64_t, std::string>> {{0, std::string("\x00",
1)},
+ {1, std::string("\x01",
1)},
+ {127,
std::string("\x7f", 1)},
+ {128,
std::string("\x00\x80", 2)},
+ {255,
std::string("\x00\xff", 2)},
+ {256,
std::string("\x01\x00", 2)},
+ {-1,
std::string("\xff", 1)},
+ {-128,
std::string("\x80", 1)},
+ {-129,
std::string("\xff\x7f", 2)},
+ {-256,
std::string("\xff\x00", 2)}}) {
+ SCOPED_TRACE("unscaled=" + std::to_string(unscaled));
+ check_decimal_bounds(unscaled, expected);
+ }
+}
+
// NOLINTNEXTLINE(readability-function-cognitive-complexity): keep schema and
value assertions together.
TEST_F(VParquetWriterTest, WritesInt64TimestampSemantics) {
DataTypes types
{DataTypeFactory::instance().create_data_type(TYPE_DATETIMEV2, false, 0, 6),
diff --git
a/regression-test/data/external_table_p0/iceberg/write/test_iceberg_write_decimal_bounds.out
b/regression-test/data/external_table_p0/iceberg/write/test_iceberg_write_decimal_bounds.out
new file mode 100644
index 00000000000..1b7b03ea0a9
--- /dev/null
+++
b/regression-test/data/external_table_p0/iceberg/write/test_iceberg_write_decimal_bounds.out
@@ -0,0 +1,55 @@
+-- This file is automatically generated. You should know what you did if you
want to edit this
+-- !bounds --
+0 1 00 00 00 00 00 00
+1 1 01 01 01 01 01 01
+2 1 7F 7F 7F 7F 7F 7F
+3 1 0080 0080 0080 0080 0080 0080
+4 1 00FF 00FF 00FF 00FF 00FF 00FF
+5 1 0100 0100 0100 0100 0100 0100
+6 1 FF FF FF FF FF FF
+7 1 80 80 80 80 80 80
+8 1 FF7F FF7F FF7F FF7F FF7F FF7F
+9 1 FF00 FF00 FF00 FF00 FF00 FF00
+10 1 8000 8000 8000 8000 8000 8000
+11 1 FF7FFF FF7FFF FF7FFF FF7FFF FF7FFF FF7FFF
+12 1 \N \N \N \N \N \N
+13 1 3B9AC9FF 3B9AC9FF 0DE0B6B3A763FFFF
0DE0B6B3A763FFFF 4B3B4CA85A86C47A098A223FFFFFFFFF
4B3B4CA85A86C47A098A223FFFFFFFFF
+14 1 C4653601 C4653601 F21F494C589C0001
F21F494C589C0001 B4C4B357A5793B85F675DDC000000001
B4C4B357A5793B85F675DDC000000001
+15 2 FF 0080 FF 0080 FF 0080
+
+-- !values --
+0 0.00 0.00 0.00
+1 0.01 0.01 0.01
+2 1.27 1.27 1.27
+3 1.28 1.28 1.28
+4 2.55 2.55 2.55
+5 2.56 2.56 2.56
+6 -0.01 -0.01 -0.01
+7 -1.28 -1.28 -1.28
+8 -1.29 -1.29 -1.29
+9 -2.56 -2.56 -2.56
+10 -327.68 -327.68 -327.68
+11 -327.69 -327.69 -327.69
+12 \N \N \N
+13 9999999.99 9999999999999999.99
999999999999999999999999999999999999.99
+14 -9999999.99 -9999999999999999.99
-999999999999999999999999999999999999.99
+15 -0.01 -0.01 -0.01
+15 1.28 1.28 1.28
+
+-- !negative --
+6 -0.01 -0.01 -0.01
+7 -1.28 -1.28 -1.28
+8 -1.29 -1.29 -1.29
+9 -2.56 -2.56 -2.56
+10 -327.68 -327.68 -327.68
+11 -327.69 -327.69 -327.69
+14 -9999999.99 -9999999999999999.99
-999999999999999999999999999999999999.99
+15 -0.01 -0.01 -0.01
+
+-- !positive --
+3 1.28 1.28 1.28
+4 2.55 2.55 2.55
+5 2.56 2.56 2.56
+13 9999999.99 9999999999999999.99
999999999999999999999999999999999999.99
+15 1.28 1.28 1.28
+
diff --git
a/regression-test/data/external_table_p0/iceberg/write/test_iceberg_write_stats2.out
b/regression-test/data/external_table_p0/iceberg/write/test_iceberg_write_stats2.out
index 0094b0f105f..56233ea9861 100644
---
a/regression-test/data/external_table_p0/iceberg/write/test_iceberg_write_stats2.out
+++
b/regression-test/data/external_table_p0/iceberg/write/test_iceberg_write_stats2.out
@@ -4,7 +4,7 @@ false 22 222 2.2 2.2 2222 8765.4321
8765.432100 987654321098765432.987654321099
true 11 111 1.1 1.1 1111 1234.5678 1234.567890
123456789012345678.123456789012 aaa 2023-01-01 2023-01-01T12:34:56
-- !sql_1 --
-0 PARQUET 2 {1:2, 2:2, 3:2, 4:2, 5:2, 6:2, 7:2, 8:2, 9:2, 10:2,
11:2, 12:2} {1:0, 2:0, 3:0, 4:0, 5:0, 6:0, 7:0, 8:0, 9:0, 10:0, 11:0, 12:0}
{1:0x00, 2:0x0B000000, 3:0x6F00000000000000, 4:0xCDCC8C3F,
5:0x9A9999999999F13F, 6:0x00000457, 7:0x00BC614E, 8:0x00000000499602D2,
9:0x000000018EE90FF6C373E0393713FA14, 10:0x616161, 11:0x9E4B0000,
12:0x005CE70F33F10500} {1:0x01, 2:0x16000000, 3:0xDE00000000000000,
4:0xCDCC0C40, 5:0x9A99999999990140, 6:0x000008AE, 7:0x05397FB1,
8:0x000000020A75E124, 9 [...]
+0 PARQUET 2 {1:2, 2:2, 3:2, 4:2, 5:2, 6:2, 7:2, 8:2, 9:2, 10:2,
11:2, 12:2} {1:0, 2:0, 3:0, 4:0, 5:0, 6:0, 7:0, 8:0, 9:0, 10:0, 11:0, 12:0}
{1:0x00, 2:0x0B000000, 3:0x6F00000000000000, 4:0xCDCC8C3F,
5:0x9A9999999999F13F, 6:0x0457, 7:0x00BC614E, 8:0x499602D2,
9:0x018EE90FF6C373E0393713FA14, 10:0x616161, 11:0x9E4B0000,
12:0x005CE70F33F10500} {1:0x01, 2:0x16000000, 3:0xDE00000000000000,
4:0xCDCC0C40, 5:0x9A99999999990140, 6:0x08AE, 7:0x05397FB1, 8:0x020A75E124,
9:0x0C7748819DFFB62505316873C [...]
-- !sql_2 --
{"bigint_col":{"column_size":74, "value_count":2, "null_value_count":0,
"nan_value_count":null, "lower_bound":111, "upper_bound":222},
"boolean_col":{"column_size":33, "value_count":2, "null_value_count":0,
"nan_value_count":null, "lower_bound":0, "upper_bound":1},
"date_col":{"column_size":66, "value_count":2, "null_value_count":0,
"nan_value_count":null, "lower_bound":"2023-01-01",
"upper_bound":"2023-06-15"}, "datetime_col1":{"column_size":74,
"value_count":2, "null_value_count":0, "n [...]
@@ -20,13 +20,13 @@ true 11 111 1.1 1.1 1111
1234.5678 1234.567890 123456789012345678.123456789012 a
{"bigint_col":{"column_size":null, "value_count":2, "null_value_count":null,
"nan_value_count":null, "lower_bound":111, "upper_bound":222},
"boolean_col":{"column_size":null, "value_count":2, "null_value_count":null,
"nan_value_count":null, "lower_bound":0, "upper_bound":1},
"date_col":{"column_size":null, "value_count":2, "null_value_count":null,
"nan_value_count":null, "lower_bound":"2023-01-01",
"upper_bound":"2023-06-15"}, "datetime_col1":{"column_size":null,
"value_count":2, "null_v [...]
-- !sql_6 --
-{1:0x00, 2:0x0B000000, 3:0x6F00000000000000, 4:0xCDCC8C3F,
5:0x9A9999999999F13F, 6:0x00000457, 7:0x00BC614E, 8:0x00000000499602D2,
9:0x000000018EE90FF6C373E0393713FA14, 10:0x616161, 11:0x9E4B0000,
12:0x005CE70F33F10500}
+{1:0x00, 2:0x0B000000, 3:0x6F00000000000000, 4:0xCDCC8C3F,
5:0x9A9999999999F13F, 6:0x0457, 7:0x00BC614E, 8:0x499602D2,
9:0x018EE90FF6C373E0393713FA14, 10:0x616161, 11:0x9E4B0000,
12:0x005CE70F33F10500}
-- !sql_7 --
{1:0x00, 2:0x0B00000000000000, 3:0x6F00000000000000, 4:0xCDCC8C3F,
5:0x9A9999999999F13F, 6:0x0457, 7:0x00BC614E, 8:0x075BCD15,
9:0x018EE90FF6C373E0393713FA14, 10:0x616161, 11:0x9E4B0000,
12:0x005CE70F33F10500}
-- !sql_8 --
-{1:0x01, 2:0x16000000, 3:0xDE00000000000000, 4:0xCDCC0C40,
5:0x9A99999999990140, 6:0x000008AE, 7:0x05397FB1, 8:0x000000020A75E124,
9:0x0000000C7748819DFFB62505316873CB, 10:0x626262, 11:0x434C0000,
12:0x40D91FA833FE0500}
+{1:0x01, 2:0x16000000, 3:0xDE00000000000000, 4:0xCDCC0C40,
5:0x9A99999999990140, 6:0x08AE, 7:0x05397FB1, 8:0x020A75E124,
9:0x0C7748819DFFB62505316873CB, 10:0x626262, 11:0x434C0000,
12:0x40D91FA833FE0500}
-- !sql_9 --
{1:0x01, 2:0x1600000000000000, 3:0xDE00000000000000, 4:0xCDCC0C40,
5:0x9A99999999990140, 6:0x08AE, 7:0x05397FB1, 8:0x05397FB1,
9:0x0C7748819DFFB62505316873CB, 10:0x626262, 11:0x434C0000,
12:0x40D91FA833FE0500}
diff --git
a/regression-test/suites/external_table_p0/iceberg/write/test_iceberg_write_decimal_bounds.groovy
b/regression-test/suites/external_table_p0/iceberg/write/test_iceberg_write_decimal_bounds.groovy
new file mode 100644
index 00000000000..2b45de13ad6
--- /dev/null
+++
b/regression-test/suites/external_table_p0/iceberg/write/test_iceberg_write_decimal_bounds.groovy
@@ -0,0 +1,88 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+suite("test_iceberg_write_decimal_bounds", "p0,external,iceberg") {
+ if
(!context.config.otherConfigs.get("enableIcebergTest")?.equalsIgnoreCase("true"))
{
+ logger.info("disable iceberg test.")
+ return
+ }
+
+ String catalogName = "test_iceberg_write_decimal_bounds"
+ String externalEnvIp = context.config.otherConfigs.get("externalEnvIp")
+ String restPort = context.config.otherConfigs.get("iceberg_rest_uri_port")
+ String minioPort = context.config.otherConfigs.get("iceberg_minio_port")
+ sql "DROP CATALOG IF EXISTS ${catalogName}"
+ sql """
+ CREATE CATALOG ${catalogName} PROPERTIES (
+ "type" = "iceberg",
+ "iceberg.catalog.type" = "rest",
+ "uri" = "http://${externalEnvIp}:${restPort}",
+ "s3.access_key" = "admin",
+ "s3.secret_key" = "password",
+ "s3.endpoint" = "http://${externalEnvIp}:${minioPort}",
+ "s3.region" = "us-east-1",
+ "enable.mapping.varbinary" = "true"
+ )
+ """
+ sql "SWITCH ${catalogName}"
+ sql "CREATE DATABASE IF NOT EXISTS test_decimal_bounds"
+ sql "USE test_decimal_bounds"
+ sql "DROP TABLE IF EXISTS decimal_bounds"
+ sql """
+ CREATE TABLE decimal_bounds (
+ case_id INT,
+ d32 DECIMAL(9,2),
+ d64 DECIMAL(18,2),
+ d128 DECIMAL(38,2)
+ )
+ PARTITION BY LIST (case_id) ()
+ PROPERTIES ("write-format" = "parquet")
+ """
+ // Separate partitions expose every sign boundary as a manifest bound, not
just a data value.
+ sql """
+ INSERT INTO decimal_bounds VALUES
+ (0, 0.00, 0.00, 0.00),
+ (1, 0.01, 0.01, 0.01),
+ (2, 1.27, 1.27, 1.27),
+ (3, 1.28, 1.28, 1.28),
+ (4, 2.55, 2.55, 2.55),
+ (5, 2.56, 2.56, 2.56),
+ (6, -0.01, -0.01, -0.01),
+ (7, -1.28, -1.28, -1.28),
+ (8, -1.29, -1.29, -1.29),
+ (9, -2.56, -2.56, -2.56),
+ (10, -327.68, -327.68, -327.68),
+ (11, -327.69, -327.69, -327.69),
+ (12, NULL, NULL, NULL),
+ (13, 9999999.99, 9999999999999999.99,
999999999999999999999999999999999999.99),
+ (14, -9999999.99, -9999999999999999.99,
-999999999999999999999999999999999999.99),
+ (15, -0.01, -0.01, -0.01),
+ (15, 1.28, 1.28, 1.28)
+ """
+ // Reading the data alone would not detect redundant sign-extension bytes
in metadata.
+ qt_bounds """
+ SELECT `partition`.case_id, record_count,
+ from_hex(lower_bounds[2]), from_hex(upper_bounds[2]),
+ from_hex(lower_bounds[3]), from_hex(upper_bounds[3]),
+ from_hex(lower_bounds[4]), from_hex(upper_bounds[4])
+ FROM decimal_bounds\$files
+ ORDER BY `partition`.case_id
+ """
+ qt_values "SELECT * FROM decimal_bounds ORDER BY case_id, d32"
+ qt_negative "SELECT * FROM decimal_bounds WHERE d128 < 0 ORDER BY case_id,
d32"
+ qt_positive "SELECT * FROM decimal_bounds WHERE d32 >= 1.28 ORDER BY
case_id, d32"
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]