blob: cb477b76eda7ae2abb9c00c6a4b72f2503805722 [file] [edit]
/*
* Copyright (C) 2026 The Android Open Source Project
*
* Licensed 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 "src/perfetto_sql/analysis/relation.h"
#include <cstdint>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include <variant>
#include <vector>
#include "perfetto/ext/base/flat_hash_map.h"
#include "src/perfetto_sql/syntaqlite/syntaqlite_perfetto.h"
#include "test/gtest_and_gmock.h"
namespace perfetto::perfetto_sql::analysis {
namespace {
struct ParserDeleter {
void operator()(SyntaqliteParser* parser) const {
syntaqlite_parser_destroy(parser);
}
};
using ScopedParser = std::unique_ptr<SyntaqliteParser, ParserDeleter>;
class TestCatalog : public Catalog {
public:
void AddRelation(std::string name, std::vector<std::string> columns) {
relations_.Insert(std::move(name), TestLeafRelation{std::move(columns)});
}
void AddView(std::string name, std::string sql) {
relations_.Insert(std::move(name), ViewRelation{std::move(sql)});
}
std::optional<LeafRelation> FindLeafRelation(
std::string_view name) const override {
const Relation* relation = relations_.Find(std::string(name));
const auto* leaf =
relation ? std::get_if<TestLeafRelation>(relation) : nullptr;
if (!leaf) {
return std::nullopt;
}
LeafRelation result;
result.name = name;
result.columns.reserve(leaf->columns.size());
for (const std::string& column : leaf->columns) {
result.columns.push_back({column, std::nullopt});
}
return result;
}
std::optional<std::string> FindViewSql(std::string_view name) const override {
const Relation* relation = relations_.Find(std::string(name));
const auto* view = relation ? std::get_if<ViewRelation>(relation) : nullptr;
return view ? std::make_optional(view->sql) : std::nullopt;
}
private:
struct TestLeafRelation {
std::vector<std::string> columns;
};
struct ViewRelation {
std::string sql;
};
using Relation = std::variant<TestLeafRelation, ViewRelation>;
base::FlatHashMap<std::string, Relation> relations_;
};
std::vector<std::string> Show(const RelationLineage& lineage) {
std::vector<std::string> out;
for (const ColumnLineage& column : lineage.columns()) {
std::string value(column.output_name);
value += "=";
for (uint32_t i = 0; i < column.origins.size(); ++i) {
if (i) {
value += ",";
}
value += column.origins[i].relation_name;
value += ".";
value += column.origins[i].column_name;
}
out.push_back(std::move(value));
}
return out;
}
class RelationAnalyzerTest : public ::testing::Test {
protected:
RelationAnalyzerTest() {
catalog_.AddRelation("slice", {"id", "ts", "name"});
catalog_.AddRelation("thread", {"utid", "name"});
}
base::StatusOr<RelationLineage> Analyze(const std::string& sql) {
ScopedParser parser(syntaqlite_parser_create_perfetto(nullptr));
syntaqlite_parser_reset(parser.get(), sql.data(),
static_cast<uint32_t>(sql.size()));
if (syntaqlite_parser_next(parser.get()) != SYNTAQLITE_PARSE_OK) {
return base::ErrStatus("could not parse test query");
}
RelationAnalyzer analyzer(catalog_);
return analyzer.AnalyzeQuery(
{parser.get(), syntaqlite_result_root(parser.get())});
}
std::vector<std::string> Select(const std::string& sql) {
auto result = Analyze(sql);
EXPECT_TRUE(result.ok()) << sql << ": " << result.status().c_message();
return result.ok() ? Show(*result) : std::vector<std::string>{};
}
TestCatalog catalog_;
};
TEST_F(RelationAnalyzerTest, ResolvesColumnsAndAliases) {
EXPECT_THAT(Select("SELECT id AS slice_id, name FROM slice"),
testing::ElementsAre("slice_id=slice.id", "name=slice.name"));
}
TEST_F(RelationAnalyzerTest, ResultOutlivesParserAndAnalyzer) {
auto lineage = Analyze("SELECT id AS slice_id FROM slice");
ASSERT_TRUE(lineage.ok()) << lineage.status().c_message();
EXPECT_THAT(Show(*lineage), testing::ElementsAre("slice_id=slice.id"));
}
TEST_F(RelationAnalyzerTest, OriginsNameLeafRelations) {
auto result = Analyze("SELECT id AS harmless_name FROM slice");
ASSERT_TRUE(result.ok());
ASSERT_THAT(result->columns().front().origins, testing::SizeIs(1));
EXPECT_EQ(result->columns().front().origins.front().relation_name, "slice");
}
TEST_F(RelationAnalyzerTest, ExpressionsHaveUnknownOrigin) {
EXPECT_THAT(Select("SELECT id, ts * 2 AS doubled FROM slice"),
testing::ElementsAre("id=slice.id", "doubled="));
}
TEST_F(RelationAnalyzerTest, ExpandsStars) {
EXPECT_THAT(
Select("SELECT * FROM slice"),
testing::ElementsAre("id=slice.id", "ts=slice.ts", "name=slice.name"));
}
TEST_F(RelationAnalyzerTest, ExplicitViewColumnNamesReplaceBodyNames) {
catalog_.AddView("v", "CREATE VIEW v(public_id) AS SELECT id FROM slice");
EXPECT_THAT(Select("SELECT public_id FROM v"),
testing::ElementsAre("public_id=slice.id"));
}
TEST_F(RelationAnalyzerTest, UnqualifiedStarCoalescesUsingColumns) {
EXPECT_THAT(Select("SELECT * FROM slice JOIN thread USING(name)"),
testing::ElementsAre("id=slice.id", "ts=slice.ts",
"name=slice.name", "utid=thread.utid"));
EXPECT_THAT(Select("SELECT * FROM slice NATURAL JOIN thread"),
testing::ElementsAre("id=slice.id", "ts=slice.ts",
"name=slice.name", "utid=thread.utid"));
EXPECT_THAT(Select("SELECT thread.* FROM slice JOIN thread USING(name)"),
testing::ElementsAre("utid=thread.utid", "name=thread.name"));
}
TEST_F(RelationAnalyzerTest, FollowsAliasesSubqueriesAndViews) {
catalog_.AddView("v1", "CREATE VIEW v1 AS SELECT id, name FROM slice");
catalog_.AddView("v2", "CREATE VIEW v2 AS SELECT id AS renamed FROM v1");
EXPECT_THAT(Select("SELECT x.renamed FROM (SELECT renamed FROM v2) x"),
testing::ElementsAre("renamed=slice.id"));
}
TEST_F(RelationAnalyzerTest, KeepsEveryOriginOfAmbiguousJoinColumn) {
EXPECT_THAT(Select("SELECT name FROM slice, thread"),
testing::ElementsAre("name=slice.name,thread.name"));
}
TEST_F(RelationAnalyzerTest, KeepsEveryOriginAcrossCompoundSelect) {
EXPECT_THAT(Select("SELECT id FROM slice UNION ALL SELECT utid FROM thread"),
testing::ElementsAre("id=slice.id,thread.utid"));
}
TEST_F(RelationAnalyzerTest, UntraceableCompoundArmPoisonsOrigins) {
EXPECT_THAT(Select("SELECT id FROM slice UNION ALL SELECT 123"),
testing::ElementsAre("id="));
EXPECT_THAT(Select("SELECT 123 AS id UNION ALL SELECT id FROM slice"),
testing::ElementsAre("id="));
}
TEST_F(RelationAnalyzerTest, UnknownRelationsAreConservative) {
EXPECT_THAT(Select("SELECT id FROM unknown_table"),
testing::ElementsAre("id="));
}
TEST_F(RelationAnalyzerTest, IdentifiesRowOrigin) {
auto result = Analyze("SELECT id, name FROM slice");
ASSERT_TRUE(result.ok());
EXPECT_EQ(result->row_origin(),
std::make_optional<std::string_view>("slice"));
result = Analyze("SELECT s.id, t.utid FROM slice s, thread t");
ASSERT_TRUE(result.ok());
EXPECT_EQ(result->row_origin(), std::nullopt);
}
TEST_F(RelationAnalyzerTest, DetectsRowPreservingViewChains) {
catalog_.AddView("v1", "CREATE VIEW v1 AS SELECT id, name FROM slice");
catalog_.AddView("v2", "CREATE VIEW v2 AS SELECT id AS a, name AS b FROM v1");
RelationAnalyzer analyzer(catalog_);
auto result = analyzer.AnalyzeRelation("v2");
ASSERT_TRUE(result.ok());
EXPECT_EQ(result->row_origin(),
std::make_optional<std::string_view>("slice"));
}
TEST_F(RelationAnalyzerTest, RejectsFilteredViewAsRowOrigin) {
catalog_.AddView("v", "CREATE VIEW v AS SELECT id FROM slice WHERE ts > 5");
RelationAnalyzer analyzer(catalog_);
auto result = analyzer.AnalyzeRelation("v");
ASSERT_TRUE(result.ok());
EXPECT_EQ(result->row_origin(), std::nullopt);
}
TEST_F(RelationAnalyzerTest, RejectsOrderedViewAsRowOrigin) {
catalog_.AddView("v",
"CREATE VIEW v AS SELECT id FROM slice ORDER BY ts DESC");
RelationAnalyzer analyzer(catalog_);
auto result = analyzer.AnalyzeRelation("v");
ASSERT_TRUE(result.ok());
EXPECT_EQ(result->row_origin(), std::nullopt);
}
} // namespace
} // namespace perfetto::perfetto_sql::analysis