| // 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. |
| |
| #include "kudu/common/schema.h" |
| |
| #include <cstddef> |
| #include <cstdint> |
| #include <string> |
| #include <tuple> // IWYU pragma: keep |
| #include <utility> |
| #include <vector> |
| |
| #include <glog/logging.h> // IWYU pragma: keep |
| #include <gtest/gtest.h> |
| |
| #include "kudu/common/common.pb.h" |
| #include "kudu/common/key_encoder.h" |
| #include "kudu/common/row.h" |
| #include "kudu/common/types.h" |
| #include "kudu/gutil/strings/stringpiece.h" |
| #include "kudu/gutil/strings/substitute.h" |
| #include "kudu/util/faststring.h" |
| #include "kudu/util/hexdump.h" |
| #include "kudu/util/int128.h" |
| #include "kudu/util/memory/arena.h" |
| #include "kudu/util/slice.h" |
| #include "kudu/util/status.h" |
| #include "kudu/util/stopwatch.h" // IWYU pragma: keep |
| #include "kudu/util/test_macros.h" |
| #include "kudu/util/test_util.h" |
| |
| using std::string; |
| using std::vector; |
| using strings::Substitute; |
| |
| namespace kudu { |
| |
| // Return true if the schemas have exactly the same set of columns |
| // and respective types. |
| bool EqualSchemas(const Schema& lhs, const Schema& rhs) { |
| if (lhs != rhs) { |
| return false; |
| } |
| |
| if (lhs.num_key_columns_ != rhs.num_key_columns_) { |
| return false; |
| } |
| if (lhs.num_columns() != rhs.num_columns()) { |
| return false; |
| } |
| for (size_t i = 0; i < rhs.num_columns(); ++i) { |
| if (!lhs.cols_[i].Equals(rhs.cols_[i])) { |
| return false; |
| } |
| } |
| |
| if (lhs.has_column_ids() != rhs.has_column_ids()) { |
| return false; |
| } |
| if (lhs.has_column_ids()) { |
| if (lhs.col_ids_ != rhs.col_ids_) { |
| return false; |
| } |
| if (lhs.max_col_id() != rhs.max_col_id()) { |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| namespace tablet { |
| |
| // Copy a row and its referenced data into the given Arena. |
| static Status CopyRowToArena(const Slice& row, |
| Arena* dst_arena, |
| ContiguousRow* copied) { |
| Slice row_data; |
| |
| // Copy the direct row data to arena |
| if (!dst_arena->RelocateSlice(row, &row_data)) { |
| return Status::IOError("no space for row data in arena"); |
| } |
| |
| copied->Reset(row_data.mutable_data()); |
| return RelocateIndirectDataToArena(copied, dst_arena); |
| } |
| |
| class TestSchema : public KuduTest {}; |
| |
| // Test basic functionality of Schema definition |
| TEST_F(TestSchema, TestSchema) { |
| Schema empty_schema; |
| ASSERT_GT(empty_schema.memory_footprint_excluding_this(), 0); |
| |
| ColumnSchema col1("key", STRING); |
| ColumnSchema col2("uint32val", UINT32, true); |
| ColumnSchema col3("int32val", INT32); |
| |
| vector<ColumnSchema> cols = { col1, col2, col3 }; |
| Schema schema(cols, 1); |
| |
| ASSERT_EQ(sizeof(Slice) + sizeof(uint32_t) + sizeof(int32_t), |
| schema.byte_size()); |
| ASSERT_EQ(3, schema.num_columns()); |
| ASSERT_EQ(0, schema.column_offset(0)); |
| ASSERT_EQ(sizeof(Slice), schema.column_offset(1)); |
| ASSERT_GT(schema.memory_footprint_excluding_this(), |
| empty_schema.memory_footprint_excluding_this()); |
| |
| EXPECT_EQ("(\n" |
| " key STRING NOT NULL,\n" |
| " uint32val UINT32 NULLABLE,\n" |
| " int32val INT32 NOT NULL,\n" |
| " PRIMARY KEY (key)\n" |
| ")", |
| schema.ToString()); |
| EXPECT_EQ("key STRING NOT NULL", schema.column(0).ToString()); |
| EXPECT_EQ("UINT32 NULLABLE", schema.column(1).TypeToString()); |
| } |
| |
| TEST_F(TestSchema, TestSchemaToStringMode) { |
| SchemaBuilder builder; |
| builder.AddKeyColumn("key", DataType::INT32); |
| const auto schema = builder.Build(); |
| EXPECT_EQ( |
| Substitute("(\n" |
| " $0:key INT32 NOT NULL,\n" |
| " PRIMARY KEY (key)\n" |
| ")", |
| schema.column_id(0)), |
| schema.ToString()); |
| EXPECT_EQ("(\n" |
| " key INT32 NOT NULL,\n" |
| " PRIMARY KEY (key)\n" |
| ")", |
| schema.ToString(Schema::ToStringMode::BASE_INFO)); |
| } |
| |
| enum IncludeColumnIds { |
| INCLUDE_COL_IDS, |
| NO_COL_IDS |
| }; |
| |
| class ParameterizedSchemaTest : public KuduTest, |
| public ::testing::WithParamInterface<IncludeColumnIds> {}; |
| |
| INSTANTIATE_TEST_SUITE_P(SchemaTypes, ParameterizedSchemaTest, |
| ::testing::Values(INCLUDE_COL_IDS, NO_COL_IDS)); |
| |
| TEST_P(ParameterizedSchemaTest, TestCopyAndMove) { |
| auto check_schema = [](const Schema& schema) { |
| ASSERT_EQ(sizeof(Slice) + sizeof(uint32_t) + sizeof(int32_t), |
| schema.byte_size()); |
| ASSERT_EQ(3, schema.num_columns()); |
| ASSERT_EQ(0, schema.column_offset(0)); |
| ASSERT_EQ(sizeof(Slice), schema.column_offset(1)); |
| |
| EXPECT_EQ(Substitute("(\n" |
| " $0key STRING NOT NULL,\n" |
| " $1uint32val UINT32 NULLABLE,\n" |
| " $2int32val INT32 NOT NULL,\n" |
| " PRIMARY KEY (key)\n" |
| ")", |
| schema.has_column_ids() ? "0:" : "", |
| schema.has_column_ids() ? "1:" : "", |
| schema.has_column_ids() ? "2:" : ""), |
| schema.ToString()); |
| EXPECT_EQ("key STRING NOT NULL", schema.column(0).ToString()); |
| EXPECT_EQ("UINT32 NULLABLE", schema.column(1).TypeToString()); |
| }; |
| |
| ColumnSchema col1("key", STRING); |
| ColumnSchema col2("uint32val", UINT32, true); |
| ColumnSchema col3("int32val", INT32); |
| |
| vector<ColumnSchema> cols = { col1, col2, col3 }; |
| vector<ColumnId> ids = { ColumnId(0), ColumnId(1), ColumnId(2) }; |
| constexpr int kNumKeyCols = 1; |
| |
| const auto& schema = GetParam() == INCLUDE_COL_IDS |
| ? Schema(cols, ids, kNumKeyCols) : Schema(cols, kNumKeyCols); |
| |
| NO_FATALS(check_schema(schema)); |
| |
| // Check copy- and move-assignment. |
| Schema moved_schema; |
| { |
| Schema copied_schema = schema; |
| NO_FATALS(check_schema(copied_schema)); |
| ASSERT_TRUE(EqualSchemas(schema, copied_schema)); |
| |
| // Move-assign to 'moved_to_schema' from 'copied_schema' and then let |
| // 'copied_schema' go out of scope to make sure none of the 'moved_schema' |
| // resources are incorrectly freed. |
| moved_schema = std::move(copied_schema); |
| |
| // 'copied_schema' is moved from so it should still be valid to call |
| // ToString(), though we can't expect any particular result. |
| copied_schema.ToString(); // NOLINT(*) |
| } |
| NO_FATALS(check_schema(moved_schema)); |
| ASSERT_TRUE(EqualSchemas(schema, moved_schema)); |
| |
| // Check copy- and move-construction. |
| { |
| Schema copied_schema(schema); |
| NO_FATALS(check_schema(copied_schema)); |
| ASSERT_TRUE(EqualSchemas(schema, copied_schema)); |
| |
| Schema moved_schema(std::move(copied_schema)); |
| copied_schema.ToString(); // NOLINT(*) |
| NO_FATALS(check_schema(moved_schema)); |
| ASSERT_TRUE(EqualSchemas(schema, moved_schema)); |
| } |
| } |
| |
| // Test basic functionality of Schema definition with decimal columns |
| TEST_F(TestSchema, TestSchemaWithDecimal) { |
| ColumnSchema col1("key", STRING); |
| ColumnSchema col2("decimal32val", DECIMAL32, false, false, |
| nullptr, nullptr, ColumnStorageAttributes(), |
| ColumnTypeAttributes(9, 4)); |
| ColumnSchema col3("decimal64val", DECIMAL64, true, false, |
| nullptr, nullptr, ColumnStorageAttributes(), |
| ColumnTypeAttributes(18, 10)); |
| ColumnSchema col4("decimal128val", DECIMAL128, true, false, |
| nullptr, nullptr, ColumnStorageAttributes(), |
| ColumnTypeAttributes(38, 2)); |
| |
| vector<ColumnSchema> cols = { col1, col2, col3, col4 }; |
| Schema schema(cols, 1); |
| |
| ASSERT_EQ(sizeof(Slice) + sizeof(int32_t) + |
| sizeof(int64_t) + sizeof(int128_t), |
| schema.byte_size()); |
| |
| EXPECT_EQ("(\n" |
| " key STRING NOT NULL,\n" |
| " decimal32val DECIMAL(9, 4) NOT NULL,\n" |
| " decimal64val DECIMAL(18, 10) NULLABLE,\n" |
| " decimal128val DECIMAL(38, 2) NULLABLE,\n" |
| " PRIMARY KEY (key)\n" |
| ")", |
| schema.ToString()); |
| |
| EXPECT_EQ("DECIMAL(9, 4) NOT NULL", schema.column(1).TypeToString()); |
| EXPECT_EQ("DECIMAL(18, 10) NULLABLE", schema.column(2).TypeToString()); |
| EXPECT_EQ("DECIMAL(38, 2) NULLABLE", schema.column(3).TypeToString()); |
| } |
| |
| // Test Schema::Equals respects decimal column attributes |
| TEST_F(TestSchema, TestSchemaEqualsWithDecimal) { |
| ColumnSchema col1("key", STRING); |
| ColumnSchema col_18_10("decimal64val", DECIMAL64, true, false, |
| nullptr, nullptr, ColumnStorageAttributes(), |
| ColumnTypeAttributes(18, 10)); |
| ColumnSchema col_18_9("decimal64val", DECIMAL64, true, false, |
| nullptr, nullptr, ColumnStorageAttributes(), |
| ColumnTypeAttributes(18, 9)); |
| ColumnSchema col_17_10("decimal64val", DECIMAL64, true, false, |
| nullptr, nullptr, ColumnStorageAttributes(), |
| ColumnTypeAttributes(17, 10)); |
| ColumnSchema col_17_9("decimal64val", DECIMAL64, true, false, |
| nullptr, nullptr, ColumnStorageAttributes(), |
| ColumnTypeAttributes(17, 9)); |
| |
| Schema schema_18_10({ col1, col_18_10 }, 1); |
| Schema schema_18_9({ col1, col_18_9 }, 1); |
| Schema schema_17_10({ col1, col_17_10 }, 1); |
| Schema schema_17_9({ col1, col_17_9 }, 1); |
| |
| EXPECT_EQ(schema_18_10, schema_18_10); |
| EXPECT_NE(schema_18_10, schema_18_9); |
| EXPECT_NE(schema_18_10, schema_17_10); |
| EXPECT_NE(schema_18_10, schema_17_9); |
| } |
| |
| TEST_F(TestSchema, TestColumnSchemaEquals) { |
| Slice default_str("read-write default"); |
| ColumnSchema col1("key", STRING); |
| ColumnSchema col2("key1", STRING); |
| ColumnSchema col3("key", STRING, true); |
| ColumnSchema col4("key", STRING, true, false, &default_str, &default_str); |
| |
| ASSERT_TRUE(col1.Equals(col1)); |
| ASSERT_FALSE(col1.Equals(col2, ColumnSchema::COMPARE_NAME)); |
| ASSERT_TRUE(col1.Equals(col2, ColumnSchema::COMPARE_TYPE)); |
| ASSERT_TRUE(col1.Equals(col3, ColumnSchema::COMPARE_NAME)); |
| ASSERT_FALSE(col1.Equals(col3, ColumnSchema::COMPARE_TYPE)); |
| ASSERT_TRUE(col1.Equals(col3, ColumnSchema::COMPARE_OTHER)); |
| ASSERT_FALSE(col3.Equals(col4, ColumnSchema::COMPARE_OTHER)); |
| ASSERT_TRUE(col4.Equals(col4, ColumnSchema::COMPARE_OTHER)); |
| } |
| |
| TEST_F(TestSchema, TestSchemaEquals) { |
| Schema schema1({ ColumnSchema("col1", STRING), |
| ColumnSchema("col2", STRING), |
| ColumnSchema("col3", UINT32) }, |
| 2); |
| Schema schema2({ ColumnSchema("newCol1", STRING), |
| ColumnSchema("newCol2", STRING), |
| ColumnSchema("newCol3", UINT32) }, |
| 2); |
| Schema schema3({ ColumnSchema("col1", STRING), |
| ColumnSchema("col2", UINT32), |
| ColumnSchema("col3", UINT32, true) }, |
| 2); |
| Schema schema4({ ColumnSchema("col1", STRING), |
| ColumnSchema("col2", UINT32), |
| ColumnSchema("col3", UINT32, false) }, |
| 2); |
| ASSERT_NE(schema1, schema2); |
| ASSERT_TRUE(schema1.KeyEquals(schema1)); |
| ASSERT_TRUE(schema1.KeyEquals(schema2, ColumnSchema::COMPARE_TYPE)); |
| ASSERT_FALSE(schema1.KeyEquals(schema2, ColumnSchema::COMPARE_NAME)); |
| ASSERT_TRUE(schema1.KeyTypeEquals(schema2)); |
| ASSERT_FALSE(schema2.KeyTypeEquals(schema3)); |
| ASSERT_NE(schema3, schema4); |
| ASSERT_EQ(schema4, schema4); |
| ASSERT_TRUE(schema3.KeyEquals(schema4, ColumnSchema::COMPARE_NAME_AND_TYPE)); |
| } |
| |
| TEST_F(TestSchema, TestReset) { |
| Schema schema; |
| ASSERT_FALSE(schema.initialized()); |
| |
| ASSERT_OK(schema.Reset({ ColumnSchema("col3", UINT32), |
| ColumnSchema("col2", STRING) }, |
| 1)); |
| ASSERT_TRUE(schema.initialized()); |
| |
| Schema schema1; |
| ASSERT_OK(schema1.Reset({ ColumnSchema("col3", UINT64), |
| ColumnSchema("col4", STRING), |
| ColumnSchema("col5", UINT32), |
| ColumnSchema("col6", STRING) }, 2)); |
| ASSERT_OK(schema.Reset(schema1.columns(), 2)); |
| ASSERT_TRUE(schema == schema1); |
| for (int i = 0; i < schema1.num_columns(); i++) { |
| ASSERT_EQ(schema.column_offset(i), schema1.column_offset(i)); |
| } |
| ASSERT_EQ(schema.key_byte_size(), schema1.key_byte_size()); |
| |
| // Move an uninitialized schema into the initialized schema. |
| Schema schema2; |
| schema = std::move(schema2); |
| ASSERT_FALSE(schema.initialized()); |
| } |
| |
| // Test for KUDU-943, a bug where we suspected that Variant didn't behave |
| // correctly with empty strings. |
| TEST_F(TestSchema, TestEmptyVariant) { |
| Slice empty_val(""); |
| Slice nonempty_val("test"); |
| |
| Variant v(STRING, &nonempty_val); |
| ASSERT_EQ("test", (static_cast<const Slice*>(v.value()))->ToString()); |
| v.Reset(STRING, &empty_val); |
| ASSERT_EQ("", (static_cast<const Slice*>(v.value()))->ToString()); |
| v.Reset(STRING, &nonempty_val); |
| ASSERT_EQ("test", (static_cast<const Slice*>(v.value()))->ToString()); |
| } |
| |
| TEST_F(TestSchema, TestProjectSubset) { |
| Schema schema1({ ColumnSchema("col1", STRING), |
| ColumnSchema("col2", STRING), |
| ColumnSchema("col3", UINT32) }, |
| 1); |
| |
| Schema schema2({ ColumnSchema("col3", UINT32), |
| ColumnSchema("col2", STRING) }, |
| 0); |
| |
| RowProjector row_projector(&schema1, &schema2); |
| ASSERT_OK(row_projector.Init()); |
| |
| // Verify the mapping |
| ASSERT_EQ(2, row_projector.base_cols_mapping().size()); |
| ASSERT_EQ(0, row_projector.projection_defaults().size()); |
| |
| const vector<RowProjector::ProjectionIdxMapping>& mapping = row_projector.base_cols_mapping(); |
| ASSERT_EQ(mapping[0].first, 0); // col3 schema2 |
| ASSERT_EQ(mapping[0].second, 2); // col3 schema1 |
| ASSERT_EQ(mapping[1].first, 1); // col2 schema2 |
| ASSERT_EQ(mapping[1].second, 1); // col2 schema1 |
| } |
| |
| // Test projection when the type of the projected column |
| // doesn't match the original type. |
| TEST_F(TestSchema, TestProjectTypeMismatch) { |
| Schema schema1({ ColumnSchema("key", STRING), |
| ColumnSchema("val", UINT32) }, |
| 1); |
| Schema schema2({ ColumnSchema("val", STRING) }, 0); |
| |
| RowProjector row_projector(&schema1, &schema2); |
| Status s = row_projector.Init(); |
| ASSERT_TRUE(s.IsInvalidArgument()) << s.ToString(); |
| ASSERT_STR_CONTAINS(s.message().ToString(), "must have type"); |
| } |
| |
| // Test projection when the some columns in the projection |
| // are not present in the base schema |
| TEST_F(TestSchema, TestProjectMissingColumn) { |
| Schema schema1({ ColumnSchema("key", STRING), ColumnSchema("val", UINT32) }, 1); |
| Schema schema2({ ColumnSchema("val", UINT32), ColumnSchema("non_present", STRING) }, 0); |
| Schema schema3({ ColumnSchema("val", UINT32), ColumnSchema("non_present", UINT32, true) }, 0); |
| uint32_t default_value = 15; |
| Schema schema4({ ColumnSchema("val", UINT32), |
| ColumnSchema("non_present", UINT32, false, false, &default_value) }, |
| 0); |
| |
| RowProjector row_projector(&schema1, &schema2); |
| Status s = row_projector.Init(); |
| ASSERT_TRUE(s.IsInvalidArgument()) << s.ToString(); |
| ASSERT_STR_CONTAINS(s.message().ToString(), |
| "does not exist in the projection, and it does not have a default value or a nullable type"); |
| |
| // Verify Default nullable column with no default value |
| ASSERT_OK(row_projector.Reset(&schema1, &schema3)); |
| |
| ASSERT_EQ(1, row_projector.base_cols_mapping().size()); |
| ASSERT_EQ(1, row_projector.projection_defaults().size()); |
| |
| ASSERT_EQ(row_projector.base_cols_mapping()[0].first, 0); // val schema2 |
| ASSERT_EQ(row_projector.base_cols_mapping()[0].second, 1); // val schema1 |
| ASSERT_EQ(row_projector.projection_defaults()[0], 1); // non_present schema3 |
| |
| // Verify Default non nullable column with default value |
| ASSERT_OK(row_projector.Reset(&schema1, &schema4)); |
| |
| ASSERT_EQ(1, row_projector.base_cols_mapping().size()); |
| ASSERT_EQ(1, row_projector.projection_defaults().size()); |
| |
| ASSERT_EQ(row_projector.base_cols_mapping()[0].first, 0); // val schema4 |
| ASSERT_EQ(row_projector.base_cols_mapping()[0].second, 1); // val schema1 |
| ASSERT_EQ(row_projector.projection_defaults()[0], 1); // non_present schema4 |
| } |
| |
| // Test projection mapping using IDs. |
| // This simulate a column rename ('val' -> 'val_renamed') |
| // and a new column added ('non_present') |
| TEST_F(TestSchema, TestProjectRename) { |
| SchemaBuilder builder; |
| ASSERT_OK(builder.AddKeyColumn("key", STRING)); |
| ASSERT_OK(builder.AddColumn("val", UINT32)); |
| Schema schema1 = builder.Build(); |
| |
| builder.Reset(schema1); |
| ASSERT_OK(builder.AddNullableColumn("non_present", UINT32)); |
| ASSERT_OK(builder.RenameColumn("val", "val_renamed")); |
| Schema schema2 = builder.Build(); |
| |
| RowProjector row_projector(&schema1, &schema2); |
| ASSERT_OK(row_projector.Init()); |
| |
| ASSERT_EQ(2, row_projector.base_cols_mapping().size()); |
| ASSERT_EQ(1, row_projector.projection_defaults().size()); |
| |
| ASSERT_EQ(row_projector.base_cols_mapping()[0].first, 0); // key schema2 |
| ASSERT_EQ(row_projector.base_cols_mapping()[0].second, 0); // key schema1 |
| |
| ASSERT_EQ(row_projector.base_cols_mapping()[1].first, 1); // val_renamed schema2 |
| ASSERT_EQ(row_projector.base_cols_mapping()[1].second, 1); // val schema1 |
| |
| ASSERT_EQ(row_projector.projection_defaults()[0], 2); // non_present schema2 |
| } |
| |
| // Test that we can map a projection schema (no column ids) onto a tablet |
| // schema (column ids). |
| TEST_F(TestSchema, TestGetMappedReadProjection) { |
| Schema tablet_schema({ ColumnSchema("key", STRING), |
| ColumnSchema("val", INT32) }, |
| { ColumnId(0), |
| ColumnId(1) }, |
| 1); |
| const bool kReadDefault = false; |
| Schema projection({ ColumnSchema("key", STRING), |
| ColumnSchema("deleted", IS_DELETED, |
| /*is_nullable=*/false, /*is_immutable=*/false, |
| /*read_default=*/&kReadDefault) }, |
| 1); |
| |
| Schema mapped; |
| ASSERT_OK(tablet_schema.GetMappedReadProjection(projection, &mapped)); |
| ASSERT_EQ(1, mapped.num_key_columns()); |
| ASSERT_EQ(2, mapped.num_columns()); |
| ASSERT_TRUE(mapped.has_column_ids()); |
| ASSERT_FALSE(EqualSchemas(mapped, projection)); |
| |
| // The column id for the 'key' column in the mapped projection should match |
| // the one from the tablet schema. |
| ASSERT_EQ("key", mapped.column(0).name()); |
| ASSERT_EQ(0, mapped.column_id(0)); |
| |
| // Since 'deleted' is a virtual column and thus does not appear in the tablet |
| // schema, in the mapped schema it should have been assigned a higher column |
| // id than the highest column id in the tablet schema. |
| ASSERT_EQ("deleted", mapped.column(1).name()); |
| ASSERT_GT(mapped.column_id(1), tablet_schema.column_id(1)); |
| ASSERT_GT(mapped.max_col_id(), tablet_schema.max_col_id()); |
| |
| // Ensure that virtual columns that are nullable or that do not have read |
| // defaults are rejected. |
| Schema nullable_projection; |
| Status s = nullable_projection.Reset({ ColumnSchema("key", STRING), |
| ColumnSchema("deleted", IS_DELETED, |
| /*is_nullable=*/true, |
| /*is_immutable=*/false, |
| /*read_default=*/&kReadDefault) }, |
| 1); |
| ASSERT_FALSE(s.ok()); |
| ASSERT_STR_CONTAINS(s.ToString(), "must not be nullable"); |
| |
| Schema no_default_projection; |
| s = no_default_projection.Reset({ ColumnSchema("key", STRING), |
| ColumnSchema("deleted", IS_DELETED, |
| /*is_nullable=*/false, |
| /*is_immutable=*/false, |
| /*read_default=*/nullptr) }, |
| 1); |
| ASSERT_FALSE(s.ok()); |
| ASSERT_STR_CONTAINS(s.ToString(), "must have a default value for read"); |
| } |
| |
| // Test that the schema can be used to compare and stringify rows. |
| TEST_F(TestSchema, TestRowOperations) { |
| Schema schema({ ColumnSchema("col1", STRING), |
| ColumnSchema("col2", STRING), |
| ColumnSchema("col3", UINT32), |
| ColumnSchema("col4", INT32) }, |
| 1); |
| |
| Arena arena(1024); |
| |
| RowBuilder rb(&schema); |
| rb.AddString(string("row_a_1")); |
| rb.AddString(string("row_a_2")); |
| rb.AddUint32(3); |
| rb.AddInt32(-3); |
| ContiguousRow row_a(&schema); |
| ASSERT_OK(CopyRowToArena(rb.data(), &arena, &row_a)); |
| |
| rb.Reset(); |
| rb.AddString(string("row_b_1")); |
| rb.AddString(string("row_b_2")); |
| rb.AddUint32(3); |
| rb.AddInt32(-3); |
| ContiguousRow row_b(&schema); |
| ASSERT_OK(CopyRowToArena(rb.data(), &arena, &row_b)); |
| |
| ASSERT_GT(schema.Compare(row_b, row_a), 0); |
| ASSERT_LT(schema.Compare(row_a, row_b), 0); |
| |
| ASSERT_EQ(R"((string col1="row_a_1", string col2="row_a_2", uint32 col3=3, int32 col4=-3))", |
| schema.DebugRow(row_a)); |
| } |
| |
| TEST(TestKeyEncoder, TestKeyEncoder) { |
| faststring fs; |
| const KeyEncoder<faststring>& encoder = GetKeyEncoder<faststring>(GetTypeInfo(STRING)); |
| |
| typedef std::tuple<vector<Slice>, Slice> test_pair; |
| |
| vector<test_pair> pairs; |
| |
| // Simple key |
| pairs.push_back(test_pair({ Slice("foo", 3) }, Slice("foo", 3))); |
| |
| // Simple compound key |
| pairs.push_back(test_pair({ Slice("foo", 3), Slice("bar", 3) }, |
| Slice("foo" "\x00\x00" "bar", 8))); |
| |
| // Compound key with a \x00 in it |
| pairs.push_back(test_pair({ Slice("xxx\x00yyy", 7), Slice("bar", 3) }, |
| Slice("xxx" "\x00\x01" "yyy" "\x00\x00" "bar", 13))); |
| |
| int i = 0; |
| for (const test_pair &t : pairs) { |
| const vector<Slice> &in = std::get<0>(t); |
| Slice expected = std::get<1>(t); |
| |
| fs.clear(); |
| for (int col = 0; col < in.size(); col++) { |
| encoder.Encode(&in[col], col == in.size() - 1, &fs); |
| } |
| |
| ASSERT_EQ(expected, Slice(fs)) |
| << "Failed encoding example " << i << ".\n" |
| << "Expected: " << HexDump(expected) << "\n" |
| << "Got: " << HexDump(Slice(fs)); |
| i++; |
| } |
| } |
| |
| TEST_F(TestSchema, TestDecodeKeys_CompoundStringKey) { |
| Schema schema({ ColumnSchema("col1", STRING), |
| ColumnSchema("col2", STRING), |
| ColumnSchema("col3", STRING) }, |
| 2); |
| |
| EXPECT_EQ(R"((string col1="foo", string col2="bar"))", |
| schema.DebugEncodedRowKey(Slice("foo\0\0bar", 8), Schema::START_KEY)); |
| EXPECT_EQ(R"((string col1="fo\000o", string col2="bar"))", |
| schema.DebugEncodedRowKey(Slice("fo\x00\x01o\0\0""bar", 10), Schema::START_KEY)); |
| EXPECT_EQ(R"((string col1="fo\000o", string col2="bar\000xy"))", |
| schema.DebugEncodedRowKey(Slice("fo\x00\x01o\0\0""bar\0xy", 13), Schema::START_KEY)); |
| |
| EXPECT_EQ("<start of table>", |
| schema.DebugEncodedRowKey("", Schema::START_KEY)); |
| EXPECT_EQ("<end of table>", |
| schema.DebugEncodedRowKey("", Schema::END_KEY)); |
| } |
| |
| // Test that appropriate statuses are returned when trying to decode an invalid |
| // encoded key. |
| TEST_F(TestSchema, TestDecodeKeys_InvalidKeys) { |
| Schema schema({ ColumnSchema("col1", STRING), |
| ColumnSchema("col2", UINT32), |
| ColumnSchema("col3", STRING) }, |
| 2); |
| |
| EXPECT_EQ("<invalid key: Invalid argument: Error decoding composite key component" |
| " 'col1': Missing separator after composite key string component: foo>", |
| schema.DebugEncodedRowKey(Slice("foo"), Schema::START_KEY)); |
| EXPECT_EQ("<invalid key: Invalid argument: Error decoding composite key component 'col2': " |
| "key too short>", |
| schema.DebugEncodedRowKey(Slice("foo\x00\x00", 5), Schema::START_KEY)); |
| EXPECT_EQ("<invalid key: Invalid argument: Error decoding composite key component 'col2': " |
| "key too short: \\xff\\xff>", |
| schema.DebugEncodedRowKey(Slice("foo\x00\x00\xff\xff", 7), Schema::START_KEY)); |
| } |
| |
| TEST_F(TestSchema, TestCreateProjection) { |
| Schema schema({ ColumnSchema("col1", STRING), |
| ColumnSchema("col2", STRING), |
| ColumnSchema("col3", STRING), |
| ColumnSchema("col4", STRING), |
| ColumnSchema("col5", STRING) }, |
| 2); |
| Schema schema_with_ids = SchemaBuilder(schema).Build(); |
| Schema partial_schema; |
| |
| // By names, without IDs |
| ASSERT_OK(schema.CreateProjectionByNames({ "col1", "col2", "col4" }, &partial_schema)); |
| EXPECT_EQ("(\n" |
| " col1 STRING NOT NULL,\n" |
| " col2 STRING NOT NULL,\n" |
| " col4 STRING NOT NULL,\n" |
| " PRIMARY KEY ()\n" |
| ")", |
| partial_schema.ToString()); |
| |
| // By names, with IDS |
| ASSERT_OK(schema_with_ids.CreateProjectionByNames({ "col1", "col2", "col4" }, &partial_schema)); |
| EXPECT_EQ(Substitute("(\n" |
| " $0:col1 STRING NOT NULL,\n" |
| " $1:col2 STRING NOT NULL,\n" |
| " $2:col4 STRING NOT NULL,\n" |
| " PRIMARY KEY ()\n" |
| ")", |
| schema_with_ids.column_id(0), |
| schema_with_ids.column_id(1), |
| schema_with_ids.column_id(3)), |
| partial_schema.ToString()); |
| |
| // By names, with missing names. |
| Status s = schema.CreateProjectionByNames({ "foobar" }, &partial_schema); |
| EXPECT_EQ("Not found: column not found: foobar", s.ToString()); |
| |
| // By IDs |
| ASSERT_OK(schema_with_ids.CreateProjectionByIdsIgnoreMissing({ schema_with_ids.column_id(0), |
| schema_with_ids.column_id(1), |
| ColumnId(1000), // missing column |
| schema_with_ids.column_id(3) }, |
| &partial_schema)); |
| EXPECT_EQ(Substitute("(\n" |
| " $0:col1 STRING NOT NULL,\n" |
| " $1:col2 STRING NOT NULL,\n" |
| " $2:col4 STRING NOT NULL,\n" |
| " PRIMARY KEY ()\n" |
| ")", |
| schema_with_ids.column_id(0), |
| schema_with_ids.column_id(1), |
| schema_with_ids.column_id(3)), |
| partial_schema.ToString()); |
| } |
| |
| TEST_F(TestSchema, TestFindColumn) { |
| Schema schema({ ColumnSchema("col1", STRING), |
| ColumnSchema("col2", INT32) }, |
| 1); |
| |
| int col_idx; |
| ASSERT_OK(schema.FindColumn("col1", &col_idx)); |
| ASSERT_EQ(0, col_idx); |
| ASSERT_OK(schema.FindColumn("col2", &col_idx)); |
| ASSERT_EQ(1, col_idx); |
| Status s = schema.FindColumn("col3", &col_idx); |
| ASSERT_TRUE(s.IsNotFound()) << s.ToString(); |
| ASSERT_EQ(s.ToString(), "Not found: No such column: col3"); |
| } |
| |
| #ifdef NDEBUG |
| TEST(TestKeyEncoder, BenchmarkSimpleKey) { |
| faststring fs; |
| Schema schema({ ColumnSchema("col1", STRING) }, 1); |
| |
| RowBuilder rb(&schema); |
| rb.AddString(Slice("hello world")); |
| ConstContiguousRow row(rb.schema(), rb.data()); |
| |
| LOG_TIMING(INFO, "Encoding") { |
| for (int i = 0; i < 10000000; i++) { |
| schema.EncodeComparableKey(row, &fs); |
| } |
| } |
| } |
| #endif |
| |
| } // namespace tablet |
| } // namespace kudu |