fix deducing Array(Nested(...)) and Nested(Array(...))

This commit is contained in:
Anton Popov 2022-02-23 04:28:11 +03:00
parent 0a7895ebb9
commit 9b5e702f6c
11 changed files with 205 additions and 132 deletions

View File

@ -327,14 +327,9 @@ namespace
void flattenTupleImpl(
PathInDataBuilder & builder,
DataTypePtr type,
size_t array_level,
PathsInData & new_paths,
std::vector<PathInData::Parts> & new_paths,
DataTypes & new_types)
{
bool is_nested = isNested(type);
if (is_nested)
type = assert_cast<const DataTypeArray &>(*type).getNestedType();
if (const auto * type_tuple = typeid_cast<const DataTypeTuple *>(type.get()))
{
const auto & tuple_names = type_tuple->getElementNames();
@ -342,19 +337,32 @@ void flattenTupleImpl(
for (size_t i = 0; i < tuple_names.size(); ++i)
{
builder.append(tuple_names[i], is_nested);
flattenTupleImpl(builder, tuple_types[i], array_level + is_nested, new_paths, new_types);
builder.append(tuple_names[i], false);
flattenTupleImpl(builder, tuple_types[i], new_paths, new_types);
builder.popBack();
}
}
else if (const auto * type_array = typeid_cast<const DataTypeArray *>(type.get()))
{
flattenTupleImpl(builder, type_array->getNestedType(), array_level + 1, new_paths, new_types);
PathInDataBuilder element_builder;
std::vector<PathInData::Parts> element_paths;
DataTypes element_types;
flattenTupleImpl(element_builder, type_array->getNestedType(), element_paths, element_types);
assert(element_paths.size() == element_types.size());
for (size_t i = 0; i < element_paths.size(); ++i)
{
builder.append(element_paths[i], true);
new_paths.emplace_back(builder.getParts());
new_types.emplace_back(std::make_shared<DataTypeArray>(element_types[i]));
builder.popBack(element_paths[i].size());
}
}
else
{
new_paths.emplace_back(builder.getParts());
new_types.push_back(createArrayOfType(type, array_level));
new_types.emplace_back(type);
}
}
@ -428,16 +436,16 @@ struct ColumnWithTypeAndDimensions
using SubcolumnsTreeWithTypes = SubcolumnsTree<ColumnWithTypeAndDimensions>;
using Node = SubcolumnsTreeWithTypes::Node;
std::pair<ColumnPtr, DataTypePtr> createTypeFromNode(const Node * node)
ColumnWithTypeAndDimensions createTypeFromNode(const Node * node)
{
auto collect_tuple_elemets = [](const auto & children)
{
std::vector<std::tuple<String, ColumnPtr, DataTypePtr>> tuple_elements;
std::vector<std::tuple<String, ColumnWithTypeAndDimensions>> tuple_elements;
tuple_elements.reserve(children.size());
for (const auto & [name, child] : children)
{
auto [column, type] = createTypeFromNode(child.get());
tuple_elements.emplace_back(name, std::move(column), std::move(type));
auto column = createTypeFromNode(child.get());
tuple_elements.emplace_back(name, std::move(column));
}
/// Sort to always create the same type for the same set of subcolumns.
@ -446,35 +454,44 @@ std::pair<ColumnPtr, DataTypePtr> createTypeFromNode(const Node * node)
auto tuple_names = extractVector<0>(tuple_elements);
auto tuple_columns = extractVector<1>(tuple_elements);
auto tuple_types = extractVector<2>(tuple_elements);
return std::make_tuple(tuple_names, tuple_columns, tuple_types);
return std::make_tuple(std::move(tuple_names), std::move(tuple_columns));
};
if (node->kind == Node::SCALAR)
{
return {node->data.column, node->data.type};
return node->data;
}
else if (node->kind == Node::NESTED)
{
auto [tuple_names, tuple_columns] = collect_tuple_elemets(node->children);
Columns offsets_columns;
ColumnPtr current_column = node->data.column;
offsets_columns.reserve(tuple_columns[0].array_dimensions + 1);
assert(node->data.array_dimensions > 0);
offsets_columns.reserve(node->data.array_dimensions);
const auto & current_array = assert_cast<const ColumnArray &>(*node->data.column);
offsets_columns.push_back(current_array.getOffsetsPtr());
for (size_t i = 0; i < node->data.array_dimensions; ++i)
for (size_t i = 0; i < tuple_columns[0].array_dimensions; ++i)
{
const auto & column_array = assert_cast<const ColumnArray &>(*current_column);
const auto & column_array = assert_cast<const ColumnArray &>(*tuple_columns[0].column);
offsets_columns.push_back(column_array.getOffsetsPtr());
current_column = column_array.getDataPtr();
tuple_columns[0].column = column_array.getDataPtr();
}
auto [tuple_names, tuple_columns, tuple_types] = collect_tuple_elemets(node->children);
size_t num_elements = tuple_columns.size();
Columns tuple_elements_columns(num_elements);
DataTypes tuple_elements_types(num_elements);
auto result_column = ColumnArray::create(ColumnTuple::create(tuple_columns), offsets_columns.back());
auto result_type = createNested(tuple_types, tuple_names);
for (size_t i = 0; i < num_elements; ++i)
{
assert(tuple_columns[i].array_dimensions == tuple_columns[0].array_dimensions);
tuple_elements_columns[i] = reduceNumberOfDimensions(tuple_columns[i].column, tuple_columns[i].array_dimensions);
tuple_elements_types[i] = reduceNumberOfDimensions(tuple_columns[i].type, tuple_columns[i].array_dimensions);
}
auto result_column = ColumnArray::create(ColumnTuple::create(tuple_elements_columns), offsets_columns.back());
auto result_type = createNested(tuple_elements_types, tuple_names);
for (auto it = offsets_columns.rbegin() + 1; it != offsets_columns.rend(); ++it)
{
@ -482,16 +499,27 @@ std::pair<ColumnPtr, DataTypePtr> createTypeFromNode(const Node * node)
result_type = std::make_shared<DataTypeArray>(result_type);
}
return {result_column, result_type};
return {result_column, result_type, tuple_columns[0].array_dimensions};
}
else
{
auto [tuple_names, tuple_columns, tuple_types] = collect_tuple_elemets(node->children);
auto [tuple_names, tuple_columns] = collect_tuple_elemets(node->children);
auto result_column = ColumnTuple::create(tuple_columns);
auto result_type = std::make_shared<DataTypeTuple>(tuple_types, tuple_names);
size_t num_elements = tuple_columns.size();
Columns tuple_elements_columns(num_elements);
DataTypes tuple_elements_types(num_elements);
return {result_column, result_type};
for (size_t i = 0; i < tuple_columns.size(); ++i)
{
assert(tuple_columns[i].array_dimensions == tuple_columns[0].array_dimensions);
tuple_elements_columns[i] = tuple_columns[i].column;
tuple_elements_types[i] = tuple_columns[i].type;
}
auto result_column = ColumnTuple::create(tuple_elements_columns);
auto result_type = std::make_shared<DataTypeTuple>(tuple_elements_types, tuple_names);
return {result_column, result_type, tuple_columns[0].array_dimensions};
}
}
@ -499,11 +527,13 @@ std::pair<ColumnPtr, DataTypePtr> createTypeFromNode(const Node * node)
std::pair<PathsInData, DataTypes> flattenTuple(const DataTypePtr & type)
{
PathsInData new_paths;
std::vector<PathInData::Parts> new_path_parts;
DataTypes new_types;
PathInDataBuilder builder;
flattenTupleImpl(builder, type, 0, new_paths, new_types);
flattenTupleImpl(builder, type, new_path_parts, new_types);
PathsInData new_paths(new_path_parts.begin(), new_path_parts.end());
return {new_paths, new_types};
}
@ -546,15 +576,7 @@ std::pair<ColumnPtr, DataTypePtr> unflattenTuple(
auto type = tuple_types[i];
const auto & parts = paths[i].getParts();
size_t num_parts = parts.size();
size_t nested_level = std::count_if(parts.begin(), parts.end(), [](const auto & part) { return part.is_nested; });
size_t array_level = getNumberOfDimensions(*type);
if (array_level < nested_level)
throw Exception(ErrorCodes::LOGICAL_ERROR,
"Number of dimensions ({}) is less than number Nested types ({}) for path {}",
array_level, nested_level, paths[i].getPath());
size_t pos = 0;
tree.add(paths[i], [&](Node::Kind kind, bool exists) -> std::shared_ptr<Node>
@ -564,27 +586,13 @@ std::pair<ColumnPtr, DataTypePtr> unflattenTuple(
"Not enough name parts for path {}. Expected at least {}, got {}",
paths[i].getPath(), pos + 1, num_parts);
ColumnWithTypeAndDimensions current_column;
if (kind == Node::NESTED)
size_t array_dimensions = kind == Node::NESTED ? 1 : parts[pos].anonymous_array_level;
ColumnWithTypeAndDimensions current_column{column, type, array_dimensions};
if (array_dimensions)
{
assert(parts[pos].is_nested);
size_t dimensions_to_reduce = array_level - nested_level + 1;
--nested_level;
current_column = ColumnWithTypeAndDimensions{column, type, dimensions_to_reduce};
if (dimensions_to_reduce)
{
type = reduceNumberOfDimensions(type, dimensions_to_reduce);
column = reduceNumberOfDimensions(column, dimensions_to_reduce);
}
array_level -= dimensions_to_reduce;
}
else
{
current_column = ColumnWithTypeAndDimensions{column, type, 0};
type = reduceNumberOfDimensions(type, array_dimensions);
column = reduceNumberOfDimensions(column, array_dimensions);
}
++pos;
@ -597,7 +605,8 @@ std::pair<ColumnPtr, DataTypePtr> unflattenTuple(
});
}
return createTypeFromNode(tree.getRoot());
const auto & [column, type, _] = createTypeFromNode(tree.getRoot());
return std::make_pair(std::move(column), std::move(type));
}
static void addConstantToWithClause(const ASTPtr & query, const String & column_name, const DataTypePtr & data_type)

View File

@ -153,13 +153,11 @@ private:
paths.reserve(paths.size() + arrays_by_path.size());
values.reserve(values.size() + arrays_by_path.size());
bool is_nested = arrays_by_path.size() > 1 || !arrays_by_path.begin()->getMapped().first.empty();
for (auto && [_, value] : arrays_by_path)
{
auto && [path, path_array] = value;
paths.push_back(builder.append(path, is_nested).getParts());
paths.push_back(builder.append(path, true).getParts());
values.push_back(std::move(path_array));
builder.popBack(path.size());

View File

@ -26,13 +26,13 @@ PathInData::PathInData(std::string_view path_)
if (*it == '.')
{
size_t size = static_cast<size_t>(it - begin);
parts.emplace_back(std::string_view{begin, size}, false);
parts.emplace_back(std::string_view{begin, size}, false, 0);
begin = it + 1;
}
}
size_t size = static_cast<size_t>(end - begin);
parts.emplace_back(std::string_view{begin, size}, false);
parts.emplace_back(std::string_view{begin, size}, false, 0.);
}
PathInData::PathInData(const Parts & parts_)
@ -65,6 +65,7 @@ UInt128 PathInData::getPartsHash(const Parts & parts_)
{
hash.update(part.key.data(), part.key.length());
hash.update(part.is_nested);
hash.update(part.anonymous_array_level);
}
UInt128 res;
@ -78,7 +79,8 @@ void PathInData::writeBinary(WriteBuffer & out) const
for (const auto & part : parts)
{
writeStringBinary(part.key, out);
writeVarUInt(static_cast<UInt8>(part.is_nested) , out);
writeVarUInt(part.is_nested, out);
writeVarUInt(part.anonymous_array_level, out);
}
}
@ -93,11 +95,14 @@ void PathInData::readBinary(ReadBuffer & in)
for (size_t i = 0; i < num_parts; ++i)
{
UInt8 is_nested;
bool is_nested;
UInt8 anonymous_array_level;
auto ref = readStringBinaryInto(arena, in);
readVarUInt(is_nested, in);
readVarUInt(anonymous_array_level, in);
temp_parts.emplace_back(static_cast<std::string_view>(ref), is_nested);
temp_parts.emplace_back(static_cast<std::string_view>(ref), is_nested, anonymous_array_level);
}
path = buildPath(temp_parts);
@ -122,16 +127,16 @@ String PathInData::buildPath(const Parts & other_parts)
return res;
}
PathInData::Parts PathInData::buildParts(const String & path, const Parts & other_parts)
PathInData::Parts PathInData::buildParts(const String & other_path, const Parts & other_parts)
{
if (other_parts.empty())
return {};
Parts res;
const char * begin = path.data();
const char * begin = other_path.data();
for (const auto & part : other_parts)
{
res.emplace_back(std::string_view{begin, part.key.length()}, part.is_nested);
res.emplace_back(std::string_view{begin, part.key.length()}, part.is_nested, part.anonymous_array_level);
begin += part.key.length() + 1;
}
return res;
@ -139,24 +144,43 @@ PathInData::Parts PathInData::buildParts(const String & path, const Parts & othe
size_t PathInData::Hash::operator()(const PathInData & value) const
{
return std::hash<String>{}(value.path);
auto hash = getPartsHash(value.parts);
return hash.items[0] ^ hash.items[1];
}
PathInDataBuilder & PathInDataBuilder::append(std::string_view key, bool is_nested)
PathInDataBuilder & PathInDataBuilder::append(std::string_view key, bool is_array)
{
if (!parts.empty())
parts.back().is_nested = is_nested;
if (parts.empty())
current_anonymous_array_level += is_array;
if (!key.empty())
{
if (!parts.empty())
parts.back().is_nested = is_array;
parts.emplace_back(key, false, current_anonymous_array_level);
current_anonymous_array_level = 0;
}
parts.emplace_back(key, false);
return *this;
}
PathInDataBuilder & PathInDataBuilder::append(const PathInData::Parts & path, bool is_nested)
PathInDataBuilder & PathInDataBuilder::append(const PathInData::Parts & path, bool is_array)
{
if (!parts.empty())
parts.back().is_nested = is_nested;
if (parts.empty())
current_anonymous_array_level += is_array;
if (!path.empty())
{
if (!parts.empty())
parts.back().is_nested = is_array;
auto it = parts.insert(parts.end(), path.begin(), path.end());
for (; it != parts.end(); ++it)
it->anonymous_array_level += current_anonymous_array_level;
current_anonymous_array_level = 0;
}
parts.insert(parts.end(), path.begin(), path.end());
return *this;
}

View File

@ -16,13 +16,16 @@ public:
struct Part
{
Part() = default;
Part(std::string_view key_, bool is_nested_)
: key(key_), is_nested(is_nested_)
Part(std::string_view key_, bool is_nested_, UInt8 anonymous_array_level_)
: key(key_), is_nested(is_nested_), anonymous_array_level(anonymous_array_level_)
{
}
std::string_view key;
bool is_nested = false;
UInt8 anonymous_array_level = 0;
bool operator==(const Part & other) const = default;
};
using Parts = std::vector<Part>;
@ -47,13 +50,12 @@ public:
void writeBinary(WriteBuffer & out) const;
void readBinary(ReadBuffer & in);
bool operator==(const PathInData & other) const { return path == other.path; }
bool operator!=(const PathInData & other) const { return !(*this == other); }
bool operator==(const PathInData & other) const { return parts == other.parts; }
struct Hash { size_t operator()(const PathInData & value) const; };
private:
static String buildPath(const Parts & other_parts);
static Parts buildParts(const String & path, const Parts & other_parts);
static Parts buildParts(const String & other_path, const Parts & other_parts);
String path;
Parts parts;
@ -64,14 +66,15 @@ class PathInDataBuilder
public:
const PathInData::Parts & getParts() const { return parts; }
PathInDataBuilder & append(std::string_view key, bool is_nested);
PathInDataBuilder & append(const PathInData::Parts & path, bool is_nested);
PathInDataBuilder & append(std::string_view key, bool is_array);
PathInDataBuilder & append(const PathInData::Parts & path, bool is_array);
void popBack();
void popBack(size_t n);
private:
PathInData::Parts parts;
size_t current_anonymous_array_level = 0;
};
using PathsInData = std::vector<PathInData>;

View File

@ -260,7 +260,7 @@ void SerializationObject<Parser>::serializeBinaryBulkWithMultipleStreams(
if (auto * stream = settings.getter(settings.path))
writeVarUInt(column_object.getSubcolumns().size(), *stream);
const auto & subcolumns = column_object.getSubcolumns().getLeaves();
const auto & subcolumns = column_object.getSubcolumns();
for (const auto & entry : subcolumns)
{
settings.path.back() = Substream::ObjectStructure;

View File

@ -1,9 +1,11 @@
#include <DataTypes/Serializations/JSONDataParser.h>
#include <Common/JSONParsers/SimdJSONParser.h>
#include <IO/ReadBufferFromString.h>
#include <gtest/gtest.h>
#include <Common/FieldVisitorToString.h>
#include <ostream>
#include <gtest/gtest.h>
#if USE_SIMDJSON
using namespace DB;
@ -49,26 +51,32 @@ TEST(JSONDataParser, ReadJSON)
struct JSONPathAndValue
{
String path;
PathInData path;
Field value;
std::vector<bool> is_nested;
JSONPathAndValue(const String & path_, const Field & value_, const std::vector<bool> & is_nested_)
: path(path_), value(value_), is_nested(is_nested_)
{
}
JSONPathAndValue(const PathInData & path_, const Field & value_)
: path(path_.getPath()), value(value_)
: path(path_), value(value_)
{
for (const auto & part : path_.getParts())
is_nested.push_back(part.is_nested);
}
bool operator==(const JSONPathAndValue & other) const = default;
bool operator<(const JSONPathAndValue & other) const { return path < other.path; }
bool operator<(const JSONPathAndValue & other) const { return path.getPath() < other.path.getPath(); }
};
static std::ostream & operator<<(std::ostream & ostr, const JSONPathAndValue & path_and_value)
{
ostr << "{ PathInData{";
bool first = true;
for (const auto & part : path_and_value.path.getParts())
{
ostr << (first ? "{" : ", {") << part.key << ", " << part.is_nested << ", " << part.anonymous_array_level << "}";
first = false;
}
ostr << "}, Field{" << applyVisitor(FieldVisitorToString(), path_and_value.value) << "} }";
return ostr;
}
using JSONValues = std::vector<JSONPathAndValue>;
static void check(
@ -100,17 +108,17 @@ TEST(JSONDataParser, Parse)
{
check(json1, "json1",
{
{"k1", 1, {false}},
{"k2.k3", "aa", {false, false}},
{"k2.k4", 2, {false, false}},
{ PathInData{{{"k1", false, 0}}}, 1 },
{ PathInData{{{"k2", false, 0}, {"k3", false, 0}}}, "aa" },
{ PathInData{{{"k2", false, 0}, {"k4", false, 0}}}, 2 },
});
}
{
check(json2, "json2",
{
{"k1.k2", Array{"aaa", "ddd"}, {true, false}},
{"k1.k3.k4", Array{Array{"bbb", "ccc"}, Array{"eee", "fff"}}, {true, true, false}},
{ PathInData{{{"k1", true, 0}, {"k2", false, 0}}}, Array{"aaa", "ddd"} },
{ PathInData{{{"k1", true, 0}, {"k3", true, 0}, {"k4", false, 0}}}, Array{Array{"bbb", "ccc"}, Array{"eee", "fff"}} },
});
}
@ -134,15 +142,11 @@ TEST(JSONDataParser, Parse)
}
]})";
Strings paths = {"k1.k2.k4", "k1.k5", "k1.k2.k3"};
auto k1k2k4 = Array{Array{3, 4}, Array{7, 8}};
check(json3, "json3",
{
{"k1.k5", Array{"foo", "bar"}, {true, false}},
{"k1.k2.k3", Array{Array{1, 2}, Array{5, 6}}, {true, false, false}},
{"k1.k2.k4", Array{Array{3, 4}, Array{7, 8}}, {true, false, false}},
{ PathInData{{{"k1", true, 0}, {"k5", false, 0}}}, Array{"foo", "bar"} },
{ PathInData{{{"k1", true, 0}, {"k2", false, 0}, {"k3", false, 0}}}, Array{Array{1, 2}, Array{5, 6}} },
{ PathInData{{{"k1", true, 0}, {"k2", false, 0}, {"k4", false, 0}}}, Array{Array{3, 4}, Array{7, 8}} },
});
}
@ -162,15 +166,18 @@ TEST(JSONDataParser, Parse)
check(json4, "json4",
{
{"k1.k5", Array{"foo", "bar"}, {true, false}},
{"k1.k2.k3", Array{Array{1, 2}, Array{5, 6}}, {true, true, false}},
{"k1.k2.k4", Array{Array{3, 4}, Array{7, 8}}, {true, true, false}},
{ PathInData{{{"k1", true, 0}, {"k5", false, 0}}}, Array{"foo", "bar"} },
{ PathInData{{{"k1", true, 0}, {"k2", true, 0}, {"k3", false, 0}}}, Array{Array{1, 2}, Array{5, 6}} },
{ PathInData{{{"k1", true, 0}, {"k2", true, 0}, {"k4", false, 0}}}, Array{Array{3, 4}, Array{7, 8}} },
});
}
{
const String json5 = R"({"k1": [[1, 2, 3], [4, 5], [6]]})";
check(json5, "json5", {{"k1", Array{Array{1, 2, 3}, Array{4, 5}, Array{6}}, {false}}});
check(json5, "json5",
{
{ PathInData{{{"k1", false, 0}}}, Array{Array{1, 2, 3}, Array{4, 5}, Array{6}} }
});
}
{
@ -182,15 +189,10 @@ TEST(JSONDataParser, Parse)
]
})";
Strings paths = {"k1.k2", "k1.k3"};
auto k1k2 = Array{Array{1, 3}, Array{5}};
auto k1k3 = Array{Array{2, 4}, Array{6}};
check(json6, "json6",
{
{"k1.k2", Array{Array{1, 3}, Array{5}}, {true, false}},
{"k1.k3", Array{Array{2, 4}, Array{6}}, {true, false}},
{ PathInData{{{"k1", true, 0}, {"k2", false, 1}}}, Array{Array{1, 3}, Array{5}} },
{ PathInData{{{"k1", true, 0}, {"k3", false, 1}}}, Array{Array{2, 4}, Array{6}} },
});
}
@ -205,8 +207,8 @@ TEST(JSONDataParser, Parse)
check(json7, "json7",
{
{"k1.k2", Array{Array{1, 3}, Array{5}}, {true, false}},
{"k1.k3", Array{Array{2, 4}, Array{6}}, {true, false}},
{ PathInData{{{"k1", true, 0}, {"k2", false, 0}}}, Array{Array{1, 3}, Array{5}} },
{ PathInData{{{"k1", true, 0}, {"k3", false, 0}}}, Array{Array{2, 4}, Array{6}} },
});
}
}

View File

@ -26,7 +26,6 @@ FROM system.parts_columns
WHERE table = 't_json' AND database = currentDatabase() AND active AND column = 'data'
ORDER BY name;
SELECT '============';
TRUNCATE TABLE t_json;

View File

@ -0,0 +1,2 @@
([[(1,2),(3,4)],[(5,6)]]) Tuple(k1 Array(Nested(k2 Int8, k3 Int8)))
([([1,3,4,5],[6,7]),([8],[9,10,11])]) Tuple(k1 Nested(k2 Array(Int8), k3 Array(Int8)))

View File

@ -0,0 +1,36 @@
#!/usr/bin/env bash
# Tags: no-fasttest
CURDIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# shellcheck source=../shell_config.sh
. "$CURDIR"/../shell_config.sh
$CLICKHOUSE_CLIENT -q "DROP TABLE IF EXISTS t_json_8"
$CLICKHOUSE_CLIENT -q "CREATE TABLE t_json_8 (data JSON) ENGINE = MergeTree ORDER BY tuple()"
cat <<EOF | $CLICKHOUSE_CLIENT -q "INSERT INTO t_json_8 FORMAT JSONAsObject"
{
"k1": [
[{"k2": 1, "k3": 2}, {"k2": 3, "k3": 4}],
[{"k2": 5, "k3": 6}]
]
}
EOF
$CLICKHOUSE_CLIENT -q "SELECT data, toTypeName(data) FROM t_json_8"
$CLICKHOUSE_CLIENT -q "TRUNCATE TABLE t_json_8"
cat <<EOF | $CLICKHOUSE_CLIENT -q "INSERT INTO t_json_8 FORMAT JSONAsObject"
{
"k1": [
{"k2": [1, 3, 4, 5], "k3": [6, 7]},
{"k2": [8], "k3": [9, 10, 11]}
]
}
EOF
$CLICKHOUSE_CLIENT -q "SELECT data, toTypeName(data) FROM t_json_8"
$CLICKHOUSE_CLIENT -q "TRUNCATE TABLE t_json_8"
$CLICKHOUSE_CLIENT -q "DROP TABLE t_json_8"

View File

@ -7,6 +7,6 @@ Philadelphia 76ers 57
Atlanta Hawks 55
Larry Bird 10
Clyde Drexler 4
Magic Johnson 3
Alvin Robertson 3
Fat Lever 2
Magic Johnson 3
Charles Barkley 2

View File

@ -34,7 +34,7 @@ ${CLICKHOUSE_CLIENT} -q \
SELECT arrayJoin(arrayJoin(data.teams.players)) as players from nbagames \
) \
) \
GROUP BY player ORDER BY triple_doubles DESC LIMIT 5"
GROUP BY player ORDER BY triple_doubles DESC, player LIMIT 5"
${CLICKHOUSE_CLIENT} -q "DROP TABLE IF EXISTS nbagames"