2018-11-28 11:37:12 +00:00
|
|
|
#include "FlatDictionary.h"
|
2021-01-10 21:15:55 +00:00
|
|
|
|
|
|
|
#include <Core/Defines.h>
|
2021-03-24 16:31:00 +00:00
|
|
|
#include <Common/HashTable/HashMap.h>
|
2021-04-03 12:47:01 +00:00
|
|
|
#include <Common/HashTable/HashSet.h>
|
2022-02-10 20:06:23 +00:00
|
|
|
#include <Common/ArenaUtils.h>
|
2021-03-24 16:31:00 +00:00
|
|
|
|
2021-01-10 21:15:55 +00:00
|
|
|
#include <DataTypes/DataTypesDecimal.h>
|
2018-06-05 19:46:49 +00:00
|
|
|
#include <IO/WriteHelpers.h>
|
2020-12-18 21:43:08 +00:00
|
|
|
#include <Columns/ColumnsNumber.h>
|
2021-01-02 22:08:54 +00:00
|
|
|
#include <Columns/ColumnNullable.h>
|
2020-12-18 21:43:08 +00:00
|
|
|
#include <Functions/FunctionHelpers.h>
|
2021-01-10 21:15:55 +00:00
|
|
|
|
2021-10-16 14:03:50 +00:00
|
|
|
#include <QueryPipeline/QueryPipelineBuilder.h>
|
2021-08-04 17:58:18 +00:00
|
|
|
#include <Processors/Executors/PullingPipelineExecutor.h>
|
|
|
|
|
2022-02-10 20:06:23 +00:00
|
|
|
#include <Dictionaries/DictionarySource.h>
|
2021-03-24 16:31:00 +00:00
|
|
|
#include <Dictionaries/DictionaryFactory.h>
|
|
|
|
#include <Dictionaries/HierarchyDictionariesUtils.h>
|
2016-06-07 21:07:44 +00:00
|
|
|
|
|
|
|
namespace DB
|
|
|
|
{
|
|
|
|
namespace ErrorCodes
|
|
|
|
{
|
2017-04-01 07:20:54 +00:00
|
|
|
extern const int ARGUMENT_OUT_OF_BOUND;
|
|
|
|
extern const int BAD_ARGUMENTS;
|
|
|
|
extern const int DICTIONARY_IS_EMPTY;
|
2018-11-28 11:37:12 +00:00
|
|
|
extern const int UNSUPPORTED_METHOD;
|
2016-06-07 21:07:44 +00:00
|
|
|
}
|
|
|
|
|
2018-12-10 15:25:45 +00:00
|
|
|
FlatDictionary::FlatDictionary(
|
2020-07-14 18:46:29 +00:00
|
|
|
const StorageID & dict_id_,
|
2019-08-03 11:02:40 +00:00
|
|
|
const DictionaryStructure & dict_struct_,
|
|
|
|
DictionarySourcePtr source_ptr_,
|
2021-04-04 13:30:01 +00:00
|
|
|
Configuration configuration_,
|
2021-04-30 22:23:22 +00:00
|
|
|
BlockPtr update_field_loaded_block_)
|
2020-07-14 18:46:29 +00:00
|
|
|
: IDictionary(dict_id_)
|
2019-08-03 11:02:40 +00:00
|
|
|
, dict_struct(dict_struct_)
|
|
|
|
, source_ptr{std::move(source_ptr_)}
|
2021-04-04 13:30:01 +00:00
|
|
|
, configuration(configuration_)
|
|
|
|
, loaded_keys(configuration.initial_array_size, false)
|
2021-04-30 22:23:22 +00:00
|
|
|
, update_field_loaded_block(std::move(update_field_loaded_block_))
|
2016-06-07 21:07:44 +00:00
|
|
|
{
|
2017-04-01 07:20:54 +00:00
|
|
|
createAttributes();
|
2019-07-17 08:39:36 +00:00
|
|
|
loadData();
|
2022-05-12 11:20:27 +00:00
|
|
|
buildHierarchyParentToChildIndexIfNeeded();
|
2019-07-17 08:39:36 +00:00
|
|
|
calculateBytesAllocated();
|
2016-06-07 21:07:44 +00:00
|
|
|
}
|
|
|
|
|
2020-12-19 13:24:51 +00:00
|
|
|
ColumnPtr FlatDictionary::getColumn(
|
2021-01-21 14:42:50 +00:00
|
|
|
const std::string & attribute_name,
|
|
|
|
const DataTypePtr & result_type,
|
2020-12-19 13:24:51 +00:00
|
|
|
const Columns & key_columns,
|
2020-12-21 14:39:15 +00:00
|
|
|
const DataTypes &,
|
2021-02-16 21:33:02 +00:00
|
|
|
const ColumnPtr & default_values_column) const
|
2016-06-07 21:07:44 +00:00
|
|
|
{
|
2020-12-18 21:43:08 +00:00
|
|
|
ColumnPtr result;
|
2016-06-07 21:07:44 +00:00
|
|
|
|
2021-03-24 16:31:00 +00:00
|
|
|
PaddedPODArray<UInt64> backup_storage;
|
2021-01-23 13:18:24 +00:00
|
|
|
const auto & ids = getColumnVectorData(this, key_columns.front(), backup_storage);
|
2020-12-29 15:21:49 +00:00
|
|
|
|
2021-01-02 22:08:54 +00:00
|
|
|
auto size = ids.size();
|
|
|
|
|
2021-01-21 14:42:50 +00:00
|
|
|
const auto & dictionary_attribute = dict_struct.getAttribute(attribute_name, result_type);
|
2020-12-18 21:43:08 +00:00
|
|
|
|
2021-03-24 16:31:00 +00:00
|
|
|
size_t attribute_index = dict_struct.attribute_name_to_index.find(attribute_name)->second;
|
|
|
|
const auto & attribute = attributes[attribute_index];
|
|
|
|
|
2021-06-12 10:53:03 +00:00
|
|
|
bool is_attribute_nullable = attribute.is_nullable_set.has_value();
|
|
|
|
ColumnUInt8::MutablePtr col_null_map_to;
|
|
|
|
ColumnUInt8::Container * vec_null_map_to = nullptr;
|
|
|
|
|
|
|
|
if (is_attribute_nullable)
|
|
|
|
{
|
|
|
|
col_null_map_to = ColumnUInt8::create(size, false);
|
|
|
|
vec_null_map_to = &col_null_map_to->getData();
|
|
|
|
}
|
|
|
|
|
2021-02-27 20:39:34 +00:00
|
|
|
auto type_call = [&](const auto & dictionary_attribute_type)
|
2020-12-18 21:43:08 +00:00
|
|
|
{
|
|
|
|
using Type = std::decay_t<decltype(dictionary_attribute_type)>;
|
|
|
|
using AttributeType = typename Type::AttributeType;
|
2021-01-23 13:18:24 +00:00
|
|
|
using ValueType = DictionaryValueType<AttributeType>;
|
|
|
|
using ColumnProvider = DictionaryAttributeColumnProvider<AttributeType>;
|
2021-01-23 16:47:33 +00:00
|
|
|
|
2021-06-12 10:53:03 +00:00
|
|
|
DictionaryDefaultValueExtractor<AttributeType> default_value_extractor(dictionary_attribute.null_value, default_values_column);
|
2020-12-18 21:43:08 +00:00
|
|
|
|
2021-01-23 13:18:24 +00:00
|
|
|
auto column = ColumnProvider::getColumn(dictionary_attribute, size);
|
2020-12-18 21:43:08 +00:00
|
|
|
|
2021-06-09 10:43:40 +00:00
|
|
|
if constexpr (std::is_same_v<ValueType, Array>)
|
2021-01-23 13:18:24 +00:00
|
|
|
{
|
|
|
|
auto * out = column.get();
|
2020-12-18 21:43:08 +00:00
|
|
|
|
2021-06-12 10:53:03 +00:00
|
|
|
getItemsImpl<ValueType, false>(
|
2021-01-22 14:54:51 +00:00
|
|
|
attribute,
|
|
|
|
ids,
|
2021-06-12 10:53:03 +00:00
|
|
|
[&](size_t, const Array & value, bool) { out->insert(value); },
|
2021-01-23 13:18:24 +00:00
|
|
|
default_value_extractor);
|
2020-12-18 21:43:08 +00:00
|
|
|
}
|
2021-06-09 10:43:40 +00:00
|
|
|
else if constexpr (std::is_same_v<ValueType, StringRef>)
|
2021-01-23 13:18:24 +00:00
|
|
|
{
|
|
|
|
auto * out = column.get();
|
2020-12-18 21:43:08 +00:00
|
|
|
|
2021-06-12 10:53:03 +00:00
|
|
|
if (is_attribute_nullable)
|
|
|
|
getItemsImpl<ValueType, true>(
|
|
|
|
attribute,
|
|
|
|
ids,
|
|
|
|
[&](size_t row, const StringRef value, bool is_null)
|
|
|
|
{
|
|
|
|
(*vec_null_map_to)[row] = is_null;
|
|
|
|
out->insertData(value.data, value.size);
|
|
|
|
},
|
|
|
|
default_value_extractor);
|
|
|
|
else
|
|
|
|
getItemsImpl<ValueType, false>(
|
|
|
|
attribute,
|
|
|
|
ids,
|
|
|
|
[&](size_t, const StringRef value, bool) { out->insertData(value.data, value.size); },
|
|
|
|
default_value_extractor);
|
2020-12-18 21:43:08 +00:00
|
|
|
}
|
2020-12-29 10:46:25 +00:00
|
|
|
else
|
2020-12-18 21:43:08 +00:00
|
|
|
{
|
2020-12-29 15:21:49 +00:00
|
|
|
auto & out = column->getData();
|
2020-12-18 21:43:08 +00:00
|
|
|
|
2021-06-12 10:53:03 +00:00
|
|
|
if (is_attribute_nullable)
|
|
|
|
getItemsImpl<ValueType, true>(
|
|
|
|
attribute,
|
|
|
|
ids,
|
|
|
|
[&](size_t row, const auto value, bool is_null)
|
|
|
|
{
|
|
|
|
(*vec_null_map_to)[row] = is_null;
|
|
|
|
out[row] = value;
|
|
|
|
},
|
|
|
|
default_value_extractor);
|
|
|
|
else
|
|
|
|
getItemsImpl<ValueType, false>(
|
|
|
|
attribute,
|
|
|
|
ids,
|
|
|
|
[&](size_t row, const auto value, bool) { out[row] = value; },
|
|
|
|
default_value_extractor);
|
2020-12-18 21:43:08 +00:00
|
|
|
}
|
2021-01-23 13:18:24 +00:00
|
|
|
|
|
|
|
result = std::move(column);
|
2020-12-18 21:43:08 +00:00
|
|
|
};
|
2018-12-10 15:25:45 +00:00
|
|
|
|
2020-12-18 21:43:08 +00:00
|
|
|
callOnDictionaryAttributeType(attribute.type, type_call);
|
2020-12-29 15:21:49 +00:00
|
|
|
|
2021-06-12 10:53:03 +00:00
|
|
|
if (attribute.is_nullable_set)
|
2022-03-02 17:22:12 +00:00
|
|
|
result = ColumnNullable::create(result, std::move(col_null_map_to));
|
2021-01-02 22:08:54 +00:00
|
|
|
|
2020-12-18 21:43:08 +00:00
|
|
|
return result;
|
2016-06-07 21:07:44 +00:00
|
|
|
}
|
|
|
|
|
2021-01-23 13:18:24 +00:00
|
|
|
ColumnUInt8::Ptr FlatDictionary::hasKeys(const Columns & key_columns, const DataTypes &) const
|
2016-06-07 21:07:44 +00:00
|
|
|
{
|
2021-03-24 16:31:00 +00:00
|
|
|
PaddedPODArray<UInt64> backup_storage;
|
2021-04-02 20:16:04 +00:00
|
|
|
const auto & keys = getColumnVectorData(this, key_columns.front(), backup_storage);
|
|
|
|
size_t keys_size = keys.size();
|
2020-12-19 13:24:51 +00:00
|
|
|
|
2021-04-02 20:16:04 +00:00
|
|
|
auto result = ColumnUInt8::create(keys_size);
|
|
|
|
auto & out = result->getData();
|
2020-12-19 13:24:51 +00:00
|
|
|
|
2021-05-06 06:09:47 +00:00
|
|
|
size_t keys_found = 0;
|
|
|
|
|
2021-04-02 20:16:04 +00:00
|
|
|
for (size_t key_index = 0; key_index < keys_size; ++key_index)
|
2017-04-01 07:20:54 +00:00
|
|
|
{
|
2021-04-02 20:16:04 +00:00
|
|
|
const auto key = keys[key_index];
|
|
|
|
out[key_index] = key < loaded_keys.size() && loaded_keys[key];
|
2021-05-06 06:09:47 +00:00
|
|
|
keys_found += out[key_index];
|
2017-04-01 07:20:54 +00:00
|
|
|
}
|
2020-12-18 21:43:08 +00:00
|
|
|
|
2021-04-02 20:16:04 +00:00
|
|
|
query_count.fetch_add(keys_size, std::memory_order_relaxed);
|
2021-05-06 06:09:47 +00:00
|
|
|
found_count.fetch_add(keys_found, std::memory_order_relaxed);
|
2016-06-07 21:07:44 +00:00
|
|
|
|
2020-12-19 13:24:51 +00:00
|
|
|
return result;
|
|
|
|
}
|
2016-06-07 21:07:44 +00:00
|
|
|
|
2021-03-24 16:31:00 +00:00
|
|
|
ColumnPtr FlatDictionary::getHierarchy(ColumnPtr key_column, const DataTypePtr &) const
|
|
|
|
{
|
|
|
|
PaddedPODArray<UInt64> keys_backup_storage;
|
|
|
|
const auto & keys = getColumnVectorData(this, key_column, keys_backup_storage);
|
|
|
|
|
|
|
|
size_t hierarchical_attribute_index = *dict_struct.hierarchical_attribute_index;
|
2021-06-12 10:53:03 +00:00
|
|
|
const auto & dictionary_attribute = dict_struct.attributes[hierarchical_attribute_index];
|
2021-03-24 16:31:00 +00:00
|
|
|
const auto & hierarchical_attribute = attributes[hierarchical_attribute_index];
|
|
|
|
|
2021-06-12 10:53:03 +00:00
|
|
|
const UInt64 null_value = dictionary_attribute.null_value.get<UInt64>();
|
2021-04-02 20:16:04 +00:00
|
|
|
const ContainerType<UInt64> & parent_keys = std::get<ContainerType<UInt64>>(hierarchical_attribute.container);
|
2021-03-24 16:31:00 +00:00
|
|
|
|
2021-04-02 20:16:04 +00:00
|
|
|
auto is_key_valid_func = [&, this](auto & key) { return key < loaded_keys.size() && loaded_keys[key]; };
|
2021-03-24 16:31:00 +00:00
|
|
|
|
2021-05-06 06:09:47 +00:00
|
|
|
size_t keys_found = 0;
|
|
|
|
|
2021-03-24 16:31:00 +00:00
|
|
|
auto get_parent_key_func = [&, this](auto & hierarchy_key)
|
|
|
|
{
|
2021-04-02 20:16:04 +00:00
|
|
|
bool is_key_valid = hierarchy_key < loaded_keys.size() && loaded_keys[hierarchy_key];
|
|
|
|
std::optional<UInt64> result = is_key_valid ? std::make_optional(parent_keys[hierarchy_key]) : std::nullopt;
|
2021-05-06 06:09:47 +00:00
|
|
|
keys_found += result.has_value();
|
2021-03-24 16:31:00 +00:00
|
|
|
return result;
|
|
|
|
};
|
|
|
|
|
|
|
|
auto dictionary_hierarchy_array = getKeysHierarchyArray(keys, null_value, is_key_valid_func, get_parent_key_func);
|
|
|
|
|
|
|
|
query_count.fetch_add(keys.size(), std::memory_order_relaxed);
|
2021-05-06 06:09:47 +00:00
|
|
|
found_count.fetch_add(keys_found, std::memory_order_relaxed);
|
2021-03-24 16:31:00 +00:00
|
|
|
|
|
|
|
return dictionary_hierarchy_array;
|
|
|
|
}
|
|
|
|
|
|
|
|
ColumnUInt8::Ptr FlatDictionary::isInHierarchy(
|
|
|
|
ColumnPtr key_column,
|
|
|
|
ColumnPtr in_key_column,
|
|
|
|
const DataTypePtr &) const
|
|
|
|
{
|
|
|
|
PaddedPODArray<UInt64> keys_backup_storage;
|
|
|
|
const auto & keys = getColumnVectorData(this, key_column, keys_backup_storage);
|
|
|
|
|
|
|
|
PaddedPODArray<UInt64> keys_in_backup_storage;
|
|
|
|
const auto & keys_in = getColumnVectorData(this, in_key_column, keys_in_backup_storage);
|
|
|
|
|
|
|
|
size_t hierarchical_attribute_index = *dict_struct.hierarchical_attribute_index;
|
2021-06-12 10:53:03 +00:00
|
|
|
const auto & dictionary_attribute = dict_struct.attributes[hierarchical_attribute_index];
|
2021-03-24 16:31:00 +00:00
|
|
|
const auto & hierarchical_attribute = attributes[hierarchical_attribute_index];
|
|
|
|
|
2021-06-12 10:53:03 +00:00
|
|
|
const UInt64 null_value = dictionary_attribute.null_value.get<UInt64>();
|
2021-04-02 20:16:04 +00:00
|
|
|
const ContainerType<UInt64> & parent_keys = std::get<ContainerType<UInt64>>(hierarchical_attribute.container);
|
2021-03-24 16:31:00 +00:00
|
|
|
|
2021-04-02 20:16:04 +00:00
|
|
|
auto is_key_valid_func = [&, this](auto & key) { return key < loaded_keys.size() && loaded_keys[key]; };
|
2021-03-24 16:31:00 +00:00
|
|
|
|
2021-05-06 06:09:47 +00:00
|
|
|
size_t keys_found = 0;
|
|
|
|
|
2021-03-24 16:31:00 +00:00
|
|
|
auto get_parent_key_func = [&, this](auto & hierarchy_key)
|
|
|
|
{
|
2021-04-02 20:16:04 +00:00
|
|
|
bool is_key_valid = hierarchy_key < loaded_keys.size() && loaded_keys[hierarchy_key];
|
|
|
|
std::optional<UInt64> result = is_key_valid ? std::make_optional(parent_keys[hierarchy_key]) : std::nullopt;
|
2021-05-06 06:09:47 +00:00
|
|
|
keys_found += result.has_value();
|
2021-03-24 16:31:00 +00:00
|
|
|
return result;
|
|
|
|
};
|
|
|
|
|
2021-03-25 21:47:43 +00:00
|
|
|
auto result = getKeysIsInHierarchyColumn(keys, keys_in, null_value, is_key_valid_func, get_parent_key_func);
|
2021-03-24 16:31:00 +00:00
|
|
|
|
2021-03-25 13:23:19 +00:00
|
|
|
query_count.fetch_add(keys.size(), std::memory_order_relaxed);
|
2021-05-06 06:09:47 +00:00
|
|
|
found_count.fetch_add(keys_found, std::memory_order_relaxed);
|
2021-03-25 13:23:19 +00:00
|
|
|
|
2021-03-24 16:31:00 +00:00
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2022-05-13 10:48:47 +00:00
|
|
|
DictionaryHierarchyParentToChildIndexPtr FlatDictionary::getHierarchicalIndex() const
|
2021-03-24 19:55:06 +00:00
|
|
|
{
|
2022-05-13 10:48:47 +00:00
|
|
|
if (hierarhical_index)
|
|
|
|
return hierarhical_index;
|
2022-05-12 11:20:27 +00:00
|
|
|
|
2021-03-24 19:55:06 +00:00
|
|
|
size_t hierarchical_attribute_index = *dict_struct.hierarchical_attribute_index;
|
|
|
|
const auto & hierarchical_attribute = attributes[hierarchical_attribute_index];
|
2021-04-02 20:16:04 +00:00
|
|
|
const ContainerType<UInt64> & parent_keys = std::get<ContainerType<UInt64>>(hierarchical_attribute.container);
|
2021-03-24 19:55:06 +00:00
|
|
|
|
2021-03-25 13:23:19 +00:00
|
|
|
HashMap<UInt64, PaddedPODArray<UInt64>> parent_to_child;
|
2022-05-25 20:40:19 +00:00
|
|
|
parent_to_child.reserve(element_count);
|
2021-03-24 19:55:06 +00:00
|
|
|
|
2022-05-25 20:40:19 +00:00
|
|
|
UInt64 child_keys_size = static_cast<UInt64>(parent_keys.size());
|
|
|
|
|
|
|
|
for (UInt64 child_key = 0; child_key < child_keys_size; ++child_key)
|
2021-03-24 19:55:06 +00:00
|
|
|
{
|
2022-05-25 20:40:19 +00:00
|
|
|
if (!loaded_keys[child_key])
|
|
|
|
continue;
|
2021-03-24 19:55:06 +00:00
|
|
|
|
2022-05-25 20:40:19 +00:00
|
|
|
auto parent_key = parent_keys[child_key];
|
|
|
|
parent_to_child[parent_key].emplace_back(child_key);
|
2021-03-25 07:31:12 +00:00
|
|
|
}
|
|
|
|
|
2022-05-13 10:48:47 +00:00
|
|
|
return std::make_shared<DictionaryHierarchicalParentToChildIndex>(parent_to_child);
|
2022-05-12 10:36:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
ColumnPtr FlatDictionary::getDescendants(
|
|
|
|
ColumnPtr key_column,
|
|
|
|
const DataTypePtr &,
|
|
|
|
size_t level,
|
2022-05-13 10:48:47 +00:00
|
|
|
DictionaryHierarchicalParentToChildIndexPtr parent_to_child_index) const
|
2022-05-12 10:36:32 +00:00
|
|
|
{
|
|
|
|
PaddedPODArray<UInt64> keys_backup;
|
|
|
|
const auto & keys = getColumnVectorData(this, key_column, keys_backup);
|
|
|
|
|
2021-05-06 07:55:27 +00:00
|
|
|
size_t keys_found;
|
2022-05-12 10:36:32 +00:00
|
|
|
auto result = getKeysDescendantsArray(keys, *parent_to_child_index, level, keys_found);
|
2021-03-24 19:55:06 +00:00
|
|
|
|
2021-03-25 13:23:19 +00:00
|
|
|
query_count.fetch_add(keys.size(), std::memory_order_relaxed);
|
2021-05-06 06:09:47 +00:00
|
|
|
found_count.fetch_add(keys_found, std::memory_order_relaxed);
|
2021-03-24 19:55:06 +00:00
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2016-06-07 21:07:44 +00:00
|
|
|
void FlatDictionary::createAttributes()
|
|
|
|
{
|
2017-04-01 07:20:54 +00:00
|
|
|
const auto size = dict_struct.attributes.size();
|
|
|
|
attributes.reserve(size);
|
|
|
|
|
|
|
|
for (const auto & attribute : dict_struct.attributes)
|
2021-06-12 10:53:03 +00:00
|
|
|
attributes.push_back(createAttribute(attribute));
|
2016-06-07 21:07:44 +00:00
|
|
|
}
|
|
|
|
|
2018-09-06 01:57:55 +00:00
|
|
|
void FlatDictionary::blockToAttributes(const Block & block)
|
2018-02-15 13:08:23 +00:00
|
|
|
{
|
2021-04-02 20:16:04 +00:00
|
|
|
const auto keys_column = block.safeGetByPosition(0).column;
|
2018-02-15 13:08:23 +00:00
|
|
|
|
2021-08-17 17:35:43 +00:00
|
|
|
DictionaryKeysArenaHolder<DictionaryKeyType::Simple> arena_holder;
|
|
|
|
DictionaryKeysExtractor<DictionaryKeyType::Simple> keys_extractor({ keys_column }, arena_holder.getComplexKeyArena());
|
2022-01-21 15:01:55 +00:00
|
|
|
size_t keys_size = keys_extractor.getKeysSize();
|
2021-04-02 20:16:04 +00:00
|
|
|
|
2022-01-21 15:01:55 +00:00
|
|
|
static constexpr size_t key_offset = 1;
|
2022-01-22 20:01:45 +00:00
|
|
|
|
|
|
|
size_t attributes_size = attributes.size();
|
|
|
|
|
|
|
|
if (unlikely(attributes_size == 0))
|
|
|
|
{
|
|
|
|
for (size_t i = 0; i < keys_size; ++i)
|
|
|
|
{
|
|
|
|
auto key = keys_extractor.extractCurrentKey();
|
|
|
|
|
|
|
|
if (unlikely(key >= configuration.max_array_size))
|
|
|
|
throw Exception(ErrorCodes::ARGUMENT_OUT_OF_BOUND,
|
|
|
|
"{}: identifier should be less than {}",
|
|
|
|
getFullName(),
|
|
|
|
toString(configuration.max_array_size));
|
|
|
|
|
|
|
|
if (key >= loaded_keys.size())
|
|
|
|
{
|
|
|
|
const size_t elements_count = key + 1;
|
|
|
|
loaded_keys.resize(elements_count, false);
|
|
|
|
}
|
|
|
|
|
|
|
|
loaded_keys[key] = true;
|
|
|
|
|
|
|
|
keys_extractor.rollbackCurrentKey();
|
|
|
|
}
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (size_t attribute_index = 0; attribute_index < attributes_size; ++attribute_index)
|
2018-02-15 13:08:23 +00:00
|
|
|
{
|
2021-04-02 20:16:04 +00:00
|
|
|
const IColumn & attribute_column = *block.safeGetByPosition(attribute_index + key_offset).column;
|
|
|
|
Attribute & attribute = attributes[attribute_index];
|
|
|
|
|
2022-01-21 15:01:55 +00:00
|
|
|
for (size_t i = 0; i < keys_size; ++i)
|
2021-04-02 20:16:04 +00:00
|
|
|
{
|
2022-01-21 15:01:55 +00:00
|
|
|
auto key = keys_extractor.extractCurrentKey();
|
2021-06-09 10:43:40 +00:00
|
|
|
|
2021-04-02 20:16:04 +00:00
|
|
|
setAttributeValue(attribute, key, attribute_column[i]);
|
2022-01-21 15:01:55 +00:00
|
|
|
keys_extractor.rollbackCurrentKey();
|
2021-04-02 20:16:04 +00:00
|
|
|
}
|
2021-04-03 12:47:01 +00:00
|
|
|
|
2022-01-21 15:01:55 +00:00
|
|
|
keys_extractor.reset();
|
2018-02-15 13:08:23 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-01-15 12:44:39 +00:00
|
|
|
void FlatDictionary::updateData()
|
2016-06-07 21:07:44 +00:00
|
|
|
{
|
2021-04-30 22:23:22 +00:00
|
|
|
if (!update_field_loaded_block || update_field_loaded_block->rows() == 0)
|
2018-01-15 12:44:39 +00:00
|
|
|
{
|
2021-09-16 17:40:42 +00:00
|
|
|
QueryPipeline pipeline(source_ptr->loadUpdatedAll());
|
2016-06-07 21:07:44 +00:00
|
|
|
|
2021-08-04 17:58:18 +00:00
|
|
|
PullingPipelineExecutor executor(pipeline);
|
|
|
|
Block block;
|
|
|
|
while (executor.pull(block))
|
2018-01-15 12:44:39 +00:00
|
|
|
{
|
2021-05-21 00:57:11 +00:00
|
|
|
convertToFullIfSparse(block);
|
2021-04-21 00:23:02 +00:00
|
|
|
|
2018-02-15 13:08:23 +00:00
|
|
|
/// We are using this to keep saved data if input stream consists of multiple blocks
|
2021-04-30 22:23:22 +00:00
|
|
|
if (!update_field_loaded_block)
|
|
|
|
update_field_loaded_block = std::make_shared<DB::Block>(block.cloneEmpty());
|
2021-04-02 20:16:04 +00:00
|
|
|
|
|
|
|
for (size_t column_index = 0; column_index < block.columns(); ++column_index)
|
2018-01-15 12:44:39 +00:00
|
|
|
{
|
2021-04-02 20:16:04 +00:00
|
|
|
const IColumn & update_column = *block.getByPosition(column_index).column.get();
|
2021-04-30 22:23:22 +00:00
|
|
|
MutableColumnPtr saved_column = update_field_loaded_block->getByPosition(column_index).column->assumeMutable();
|
2018-01-15 12:44:39 +00:00
|
|
|
saved_column->insertRangeFrom(update_column, 0, update_column.size());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
2017-04-01 07:20:54 +00:00
|
|
|
{
|
2021-08-04 17:58:18 +00:00
|
|
|
Pipe pipe(source_ptr->loadUpdatedAll());
|
2021-08-17 17:35:43 +00:00
|
|
|
mergeBlockWithPipe<DictionaryKeyType::Simple>(
|
2021-04-02 20:16:04 +00:00
|
|
|
dict_struct.getKeysSize(),
|
2021-04-30 22:23:22 +00:00
|
|
|
*update_field_loaded_block,
|
2021-08-04 17:58:18 +00:00
|
|
|
std::move(pipe));
|
2018-01-15 12:44:39 +00:00
|
|
|
}
|
2016-06-07 21:07:44 +00:00
|
|
|
|
2021-04-30 22:23:22 +00:00
|
|
|
if (update_field_loaded_block)
|
|
|
|
blockToAttributes(*update_field_loaded_block.get());
|
2018-01-15 12:44:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void FlatDictionary::loadData()
|
|
|
|
{
|
2018-05-07 02:01:11 +00:00
|
|
|
if (!source_ptr->hasUpdateField())
|
|
|
|
{
|
2021-09-16 17:40:42 +00:00
|
|
|
QueryPipeline pipeline(source_ptr->loadAll());
|
2021-08-04 17:58:18 +00:00
|
|
|
PullingPipelineExecutor executor(pipeline);
|
2016-06-07 21:07:44 +00:00
|
|
|
|
2021-08-04 17:58:18 +00:00
|
|
|
Block block;
|
|
|
|
while (executor.pull(block))
|
2018-02-15 13:08:23 +00:00
|
|
|
blockToAttributes(block);
|
2018-01-15 12:44:39 +00:00
|
|
|
}
|
|
|
|
else
|
|
|
|
updateData();
|
2016-06-07 21:07:44 +00:00
|
|
|
|
2022-01-21 19:22:55 +00:00
|
|
|
element_count = 0;
|
|
|
|
|
|
|
|
size_t loaded_keys_size = loaded_keys.size();
|
|
|
|
for (size_t i = 0; i < loaded_keys_size; ++i)
|
|
|
|
element_count += loaded_keys[i];
|
|
|
|
|
2021-04-04 13:30:01 +00:00
|
|
|
if (configuration.require_nonempty && 0 == element_count)
|
2022-01-16 00:06:10 +00:00
|
|
|
throw Exception(ErrorCodes::DICTIONARY_IS_EMPTY, "{}: dictionary source is empty and 'require_nonempty' property is set.", getFullName());
|
2016-06-07 21:07:44 +00:00
|
|
|
}
|
|
|
|
|
2022-05-12 11:20:27 +00:00
|
|
|
void FlatDictionary::buildHierarchyParentToChildIndexIfNeeded()
|
|
|
|
{
|
|
|
|
if (!dict_struct.hierarchical_attribute_index)
|
|
|
|
return;
|
|
|
|
|
|
|
|
if (dict_struct.attributes[*dict_struct.hierarchical_attribute_index].bidirectional)
|
2022-05-13 10:48:47 +00:00
|
|
|
hierarhical_index = getHierarchicalIndex();
|
2022-05-12 11:20:27 +00:00
|
|
|
}
|
|
|
|
|
2016-06-07 21:07:44 +00:00
|
|
|
void FlatDictionary::calculateBytesAllocated()
|
|
|
|
{
|
2017-04-01 07:20:54 +00:00
|
|
|
bytes_allocated += attributes.size() * sizeof(attributes.front());
|
|
|
|
|
|
|
|
for (const auto & attribute : attributes)
|
|
|
|
{
|
2020-12-18 21:43:08 +00:00
|
|
|
auto type_call = [&](const auto & dictionary_attribute_type)
|
2017-04-01 07:20:54 +00:00
|
|
|
{
|
2020-12-18 21:43:08 +00:00
|
|
|
using Type = std::decay_t<decltype(dictionary_attribute_type)>;
|
|
|
|
using AttributeType = typename Type::AttributeType;
|
2021-04-02 20:16:04 +00:00
|
|
|
using ValueType = DictionaryValueType<AttributeType>;
|
2017-04-01 07:20:54 +00:00
|
|
|
|
2021-04-02 20:16:04 +00:00
|
|
|
const auto & container = std::get<ContainerType<ValueType>>(attribute.container);
|
2021-06-09 10:43:40 +00:00
|
|
|
bytes_allocated += sizeof(ContainerType<ValueType>);
|
|
|
|
|
|
|
|
if constexpr (std::is_same_v<ValueType, Array>)
|
|
|
|
{
|
|
|
|
/// It is not accurate calculations
|
|
|
|
bytes_allocated += sizeof(Array) * container.size();
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
bytes_allocated += container.allocated_bytes();
|
|
|
|
}
|
|
|
|
|
2021-04-02 20:16:04 +00:00
|
|
|
bucket_count = container.capacity();
|
2020-12-18 21:43:08 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
callOnDictionaryAttributeType(attribute.type, type_call);
|
2021-10-15 15:26:56 +00:00
|
|
|
|
|
|
|
bytes_allocated += sizeof(attribute.is_nullable_set);
|
|
|
|
|
|
|
|
if (attribute.is_nullable_set.has_value())
|
|
|
|
bytes_allocated = attribute.is_nullable_set->getBufferSizeInBytes();
|
2017-04-01 07:20:54 +00:00
|
|
|
}
|
2021-04-30 22:23:22 +00:00
|
|
|
|
|
|
|
if (update_field_loaded_block)
|
|
|
|
bytes_allocated += update_field_loaded_block->allocatedBytes();
|
2022-01-08 10:26:11 +00:00
|
|
|
|
2022-05-13 10:48:47 +00:00
|
|
|
if (hierarhical_index)
|
|
|
|
{
|
|
|
|
hierarchical_index_bytes_allocated = hierarhical_index->getSizeInBytes();
|
|
|
|
bytes_allocated += hierarchical_index_bytes_allocated;
|
|
|
|
}
|
2022-05-12 11:20:27 +00:00
|
|
|
|
2022-01-08 10:26:11 +00:00
|
|
|
bytes_allocated += string_arena.size();
|
2016-06-07 21:07:44 +00:00
|
|
|
}
|
|
|
|
|
2021-06-12 10:53:03 +00:00
|
|
|
FlatDictionary::Attribute FlatDictionary::createAttribute(const DictionaryAttribute & dictionary_attribute)
|
2016-06-07 21:07:44 +00:00
|
|
|
{
|
2021-06-12 10:53:03 +00:00
|
|
|
auto is_nullable_set = dictionary_attribute.is_nullable ? std::make_optional<NullableSet>() : std::optional<NullableSet>{};
|
2022-01-08 10:26:11 +00:00
|
|
|
Attribute attribute{dictionary_attribute.underlying_type, std::move(is_nullable_set), {}};
|
2017-04-01 07:20:54 +00:00
|
|
|
|
2021-04-04 13:30:01 +00:00
|
|
|
auto type_call = [&](const auto & dictionary_attribute_type)
|
2017-04-01 07:20:54 +00:00
|
|
|
{
|
2020-12-18 21:43:08 +00:00
|
|
|
using Type = std::decay_t<decltype(dictionary_attribute_type)>;
|
2020-12-20 20:11:28 +00:00
|
|
|
using AttributeType = typename Type::AttributeType;
|
2021-04-04 13:30:01 +00:00
|
|
|
using ValueType = DictionaryValueType<AttributeType>;
|
|
|
|
|
2021-06-12 10:53:03 +00:00
|
|
|
attribute.container.emplace<ContainerType<ValueType>>(configuration.initial_array_size, ValueType());
|
2020-12-18 21:43:08 +00:00
|
|
|
};
|
|
|
|
|
2021-04-04 13:30:01 +00:00
|
|
|
callOnDictionaryAttributeType(dictionary_attribute.underlying_type, type_call);
|
2017-04-01 07:20:54 +00:00
|
|
|
|
2021-04-04 13:30:01 +00:00
|
|
|
return attribute;
|
2016-06-07 21:07:44 +00:00
|
|
|
}
|
|
|
|
|
2021-06-12 10:53:03 +00:00
|
|
|
template <typename AttributeType, bool is_nullable, typename ValueSetter, typename DefaultValueExtractor>
|
2016-06-07 21:07:44 +00:00
|
|
|
void FlatDictionary::getItemsImpl(
|
2021-01-22 14:54:51 +00:00
|
|
|
const Attribute & attribute,
|
2021-04-02 20:16:04 +00:00
|
|
|
const PaddedPODArray<UInt64> & keys,
|
2021-01-22 14:54:51 +00:00
|
|
|
ValueSetter && set_value,
|
2021-01-23 16:47:33 +00:00
|
|
|
DefaultValueExtractor & default_value_extractor) const
|
2016-06-07 21:07:44 +00:00
|
|
|
{
|
2021-04-02 20:16:04 +00:00
|
|
|
const auto & container = std::get<ContainerType<AttributeType>>(attribute.container);
|
|
|
|
const auto rows = keys.size();
|
2016-06-07 21:07:44 +00:00
|
|
|
|
2021-05-06 06:09:47 +00:00
|
|
|
size_t keys_found = 0;
|
|
|
|
|
2021-04-02 20:16:04 +00:00
|
|
|
for (size_t row = 0; row < rows; ++row)
|
2017-04-01 07:20:54 +00:00
|
|
|
{
|
2021-04-02 20:16:04 +00:00
|
|
|
const auto key = keys[row];
|
|
|
|
|
|
|
|
if (key < loaded_keys.size() && loaded_keys[key])
|
2021-05-06 06:09:47 +00:00
|
|
|
{
|
2021-06-12 10:53:03 +00:00
|
|
|
if constexpr (is_nullable)
|
|
|
|
set_value(row, container[key], attribute.is_nullable_set->find(key) != nullptr);
|
|
|
|
else
|
|
|
|
set_value(row, container[key], false);
|
|
|
|
|
2021-05-06 06:09:47 +00:00
|
|
|
++keys_found;
|
|
|
|
}
|
2021-04-02 20:16:04 +00:00
|
|
|
else
|
2021-06-12 10:53:03 +00:00
|
|
|
{
|
|
|
|
if constexpr (is_nullable)
|
|
|
|
set_value(row, default_value_extractor[row], default_value_extractor.isNullAt(row));
|
|
|
|
else
|
|
|
|
set_value(row, default_value_extractor[row], false);
|
|
|
|
}
|
2017-04-01 07:20:54 +00:00
|
|
|
}
|
2016-06-07 21:07:44 +00:00
|
|
|
|
2017-04-01 07:20:54 +00:00
|
|
|
query_count.fetch_add(rows, std::memory_order_relaxed);
|
2021-05-06 06:09:47 +00:00
|
|
|
found_count.fetch_add(keys_found, std::memory_order_relaxed);
|
2016-06-07 21:07:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
template <typename T>
|
2021-04-02 20:16:04 +00:00
|
|
|
void FlatDictionary::resize(Attribute & attribute, UInt64 key)
|
2016-06-07 21:07:44 +00:00
|
|
|
{
|
2021-04-04 13:30:01 +00:00
|
|
|
if (key >= configuration.max_array_size)
|
2021-04-02 20:16:04 +00:00
|
|
|
throw Exception(ErrorCodes::ARGUMENT_OUT_OF_BOUND,
|
2021-04-10 18:48:36 +00:00
|
|
|
"{}: identifier should be less than {}",
|
2022-01-16 00:06:10 +00:00
|
|
|
getFullName(),
|
2021-04-04 13:30:01 +00:00
|
|
|
toString(configuration.max_array_size));
|
2021-04-02 20:16:04 +00:00
|
|
|
|
|
|
|
auto & container = std::get<ContainerType<T>>(attribute.container);
|
2017-04-01 07:20:54 +00:00
|
|
|
|
2021-04-02 20:16:04 +00:00
|
|
|
if (key >= container.size())
|
2017-04-01 07:20:54 +00:00
|
|
|
{
|
2021-04-02 20:16:04 +00:00
|
|
|
const size_t elements_count = key + 1; //id=0 -> elements_count=1
|
|
|
|
loaded_keys.resize(elements_count, false);
|
2021-06-09 10:43:40 +00:00
|
|
|
|
|
|
|
if constexpr (std::is_same_v<T, Array>)
|
2021-06-12 10:53:03 +00:00
|
|
|
container.resize(elements_count, T{});
|
2021-06-09 10:43:40 +00:00
|
|
|
else
|
2021-06-12 10:53:03 +00:00
|
|
|
container.resize_fill(elements_count, T{});
|
2017-04-01 07:20:54 +00:00
|
|
|
}
|
2016-08-24 22:21:48 +00:00
|
|
|
}
|
|
|
|
|
2021-04-02 20:16:04 +00:00
|
|
|
void FlatDictionary::setAttributeValue(Attribute & attribute, const UInt64 key, const Field & value)
|
2016-06-07 21:07:44 +00:00
|
|
|
{
|
2021-06-09 10:43:40 +00:00
|
|
|
auto type_call = [&](const auto & dictionary_attribute_type)
|
2017-04-01 07:20:54 +00:00
|
|
|
{
|
2021-01-02 22:08:54 +00:00
|
|
|
using Type = std::decay_t<decltype(dictionary_attribute_type)>;
|
|
|
|
using AttributeType = typename Type::AttributeType;
|
2021-06-09 10:43:40 +00:00
|
|
|
using ValueType = DictionaryValueType<AttributeType>;
|
2021-01-09 21:23:59 +00:00
|
|
|
|
2021-06-09 10:43:40 +00:00
|
|
|
resize<ValueType>(attribute, key);
|
2021-01-02 22:08:54 +00:00
|
|
|
|
2022-01-21 19:22:55 +00:00
|
|
|
if (attribute.is_nullable_set && value.isNull())
|
2021-01-02 22:08:54 +00:00
|
|
|
{
|
2022-01-21 19:22:55 +00:00
|
|
|
attribute.is_nullable_set->insert(key);
|
|
|
|
loaded_keys[key] = true;
|
|
|
|
return;
|
2021-01-02 22:08:54 +00:00
|
|
|
}
|
|
|
|
|
2022-01-22 20:01:45 +00:00
|
|
|
auto & attribute_value = value.get<AttributeType>();
|
|
|
|
|
|
|
|
auto & container = std::get<ContainerType<ValueType>>(attribute.container);
|
|
|
|
loaded_keys[key] = true;
|
|
|
|
|
|
|
|
if constexpr (std::is_same_v<ValueType, StringRef>)
|
|
|
|
{
|
|
|
|
auto arena_value = copyStringInArena(string_arena, attribute_value);
|
|
|
|
container[key] = arena_value;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
container[key] = attribute_value;
|
|
|
|
}
|
2021-01-02 22:08:54 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
callOnDictionaryAttributeType(attribute.type, type_call);
|
2016-06-07 21:07:44 +00:00
|
|
|
}
|
|
|
|
|
2021-10-21 14:17:53 +00:00
|
|
|
Pipe FlatDictionary::read(const Names & column_names, size_t max_block_size, size_t num_streams) const
|
2017-04-28 18:33:31 +00:00
|
|
|
{
|
2021-04-02 20:16:04 +00:00
|
|
|
const auto keys_count = loaded_keys.size();
|
2017-04-28 18:33:31 +00:00
|
|
|
|
2021-04-02 20:16:04 +00:00
|
|
|
PaddedPODArray<UInt64> keys;
|
|
|
|
keys.reserve(keys_count);
|
2021-01-09 21:23:59 +00:00
|
|
|
|
2021-04-02 20:16:04 +00:00
|
|
|
for (size_t key_index = 0; key_index < keys_count; ++key_index)
|
|
|
|
if (loaded_keys[key_index])
|
|
|
|
keys.push_back(key_index);
|
2017-04-28 18:33:31 +00:00
|
|
|
|
2022-01-19 16:08:56 +00:00
|
|
|
auto keys_column = getColumnFromPODArray(std::move(keys));
|
2022-03-02 17:22:12 +00:00
|
|
|
ColumnsWithTypeAndName key_columns = {ColumnWithTypeAndName(keys_column, std::make_shared<DataTypeUInt64>(), dict_struct.id->name)};
|
2021-10-21 14:17:53 +00:00
|
|
|
|
|
|
|
std::shared_ptr<const IDictionary> dictionary = shared_from_this();
|
2022-04-19 20:47:29 +00:00
|
|
|
auto coordinator =std::make_shared<DictionarySourceCoordinator>(dictionary, column_names, std::move(key_columns), max_block_size);
|
2022-01-16 12:22:22 +00:00
|
|
|
auto result = coordinator->read(num_streams);
|
2021-10-21 14:17:53 +00:00
|
|
|
|
2022-01-16 12:22:22 +00:00
|
|
|
return result;
|
2017-04-28 18:33:31 +00:00
|
|
|
}
|
|
|
|
|
2018-11-28 11:37:12 +00:00
|
|
|
void registerDictionaryFlat(DictionaryFactory & factory)
|
|
|
|
{
|
2019-12-25 23:12:12 +00:00
|
|
|
auto create_layout = [=](const std::string & full_name,
|
2021-05-24 21:27:24 +00:00
|
|
|
const DictionaryStructure & dict_struct,
|
|
|
|
const Poco::Util::AbstractConfiguration & config,
|
|
|
|
const std::string & config_prefix,
|
|
|
|
DictionarySourcePtr source_ptr,
|
2021-08-12 15:16:55 +00:00
|
|
|
ContextPtr /* global_context */,
|
2021-05-24 21:27:24 +00:00
|
|
|
bool /* created_from_ddl */) -> DictionaryPtr
|
2018-12-10 15:50:58 +00:00
|
|
|
{
|
2018-11-28 11:37:12 +00:00
|
|
|
if (dict_struct.key)
|
2021-04-02 20:16:04 +00:00
|
|
|
throw Exception(ErrorCodes::UNSUPPORTED_METHOD, "'key' is not supported for dictionary of layout 'flat'");
|
2018-11-28 11:37:12 +00:00
|
|
|
|
|
|
|
if (dict_struct.range_min || dict_struct.range_max)
|
2021-04-02 20:16:04 +00:00
|
|
|
throw Exception(ErrorCodes::BAD_ARGUMENTS,
|
2021-04-10 18:48:36 +00:00
|
|
|
"{}: elements .structure.range_min and .structure.range_max should be defined only "
|
2021-04-02 20:16:04 +00:00
|
|
|
"for a dictionary of layout 'range_hashed'",
|
|
|
|
full_name);
|
2019-12-25 23:12:12 +00:00
|
|
|
|
2021-04-04 13:30:01 +00:00
|
|
|
static constexpr size_t default_initial_array_size = 1024;
|
|
|
|
static constexpr size_t default_max_array_size = 500000;
|
|
|
|
|
|
|
|
String dictionary_layout_prefix = config_prefix + ".layout" + ".flat";
|
2022-03-12 17:46:38 +00:00
|
|
|
const DictionaryLifetime dict_lifetime{config, config_prefix + ".lifetime"};
|
2021-04-04 13:30:01 +00:00
|
|
|
|
|
|
|
FlatDictionary::Configuration configuration
|
|
|
|
{
|
|
|
|
.initial_array_size = config.getUInt64(dictionary_layout_prefix + ".initial_array_size", default_initial_array_size),
|
|
|
|
.max_array_size = config.getUInt64(dictionary_layout_prefix + ".max_array_size", default_max_array_size),
|
2022-03-12 17:46:38 +00:00
|
|
|
.require_nonempty = config.getBool(config_prefix + ".require_nonempty", false),
|
|
|
|
.dict_lifetime = dict_lifetime
|
2021-04-04 13:30:01 +00:00
|
|
|
};
|
|
|
|
|
2020-07-14 19:19:17 +00:00
|
|
|
const auto dict_id = StorageID::fromDictionaryConfig(config, config_prefix);
|
2021-04-04 13:30:01 +00:00
|
|
|
|
2022-03-12 17:46:38 +00:00
|
|
|
return std::make_unique<FlatDictionary>(dict_id, dict_struct, std::move(source_ptr), std::move(configuration));
|
2018-11-28 11:37:12 +00:00
|
|
|
};
|
2021-04-02 20:16:04 +00:00
|
|
|
|
2019-10-21 16:05:45 +00:00
|
|
|
factory.registerLayout("flat", create_layout, false);
|
2018-11-28 11:37:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2016-06-07 21:07:44 +00:00
|
|
|
}
|