diff --git a/Android.bp b/Android.bp index 8f070f2dc3d..a1c4ede2dcc 100644 --- a/Android.bp +++ b/Android.bp @@ -17770,6 +17770,18 @@ filegroup { // GN: //src/trace_processor/core/common:common filegroup { name: "perfetto_src_trace_processor_core_common_common", + srcs: [ + "src/trace_processor/core/common/string_ranks.cc", + ], +} + +// GN: //src/trace_processor/core/common:unittests +filegroup { + name: "perfetto_src_trace_processor_core_common_unittests", + srcs: [ + "src/trace_processor/core/common/row_layout_unittest.cc", + "src/trace_processor/core/common/string_ranks_unittest.cc", + ], } // GN: //src/trace_processor/core/dataframe:dataframe @@ -24437,6 +24449,7 @@ cc_test { ":perfetto_src_trace_processor_containers_containers", ":perfetto_src_trace_processor_containers_unittests", ":perfetto_src_trace_processor_core_common_common", + ":perfetto_src_trace_processor_core_common_unittests", ":perfetto_src_trace_processor_core_dataframe_dataframe", ":perfetto_src_trace_processor_core_dataframe_unittests", ":perfetto_src_trace_processor_core_exec_exec", diff --git a/BUILD b/BUILD index 6740c5841b2..cbbcb8f39ea 100644 --- a/BUILD +++ b/BUILD @@ -2528,9 +2528,12 @@ perfetto_filegroup( "src/trace_processor/core/common/duplicate_types.h", "src/trace_processor/core/common/null_types.h", "src/trace_processor/core/common/op_types.h", + "src/trace_processor/core/common/row_layout.h", "src/trace_processor/core/common/schema.h", "src/trace_processor/core/common/sort_types.h", "src/trace_processor/core/common/storage_types.h", + "src/trace_processor/core/common/string_ranks.cc", + "src/trace_processor/core/common/string_ranks.h", "src/trace_processor/core/common/value_fetcher.h", ], ) diff --git a/src/perfetto_sql/analysis/relation.cc b/src/perfetto_sql/analysis/relation.cc index 73ddf17c46a..0ae3f90d8b6 100644 --- a/src/perfetto_sql/analysis/relation.cc +++ b/src/perfetto_sql/analysis/relation.cc @@ -103,6 +103,22 @@ bool IsNatural(SyntaqliteJoinType type) { type == SYNTAQLITE_JOIN_TYPE_NATURAL_FULL; } +// The CTE definitions of `with`, in order. +std::vector CteDefinitions( + SyntaqliteParser* p, + const SyntaqliteWithClause& with) { + std::vector out; + const void* list = syntaqlite_parser_node(p, with.ctes); + uint32_t count = list ? syntaqlite_list_count(list) : 0; + for (uint32_t i = 0; i < count; ++i) { + const SyntaqliteNode* node = Node(p, syntaqlite_list_child_id(list, i)); + if (node && node->tag == SYNTAQLITE_NODE_CTE_DEFINITION) { + out.push_back(&node->cte_definition); + } + } + return out; +} + bool ContainsName(const std::vector& names, std::string_view name) { return std::any_of(names.begin(), names.end(), [&](std::string_view n) { @@ -157,6 +173,7 @@ class RelationAnalyzer::Impl { preserves_rows_ = true; views_.clear(); leaves_.clear(); + ctes_.clear(); } // The columns of the relation `name`. Its hidden columns, if any, are added @@ -165,9 +182,24 @@ class RelationAnalyzer::Impl { std::string_view name, int depth, std::vector& hidden); + // The columns of `name` as a query reads it: the innermost CTE of that name + // in scope, or else the relation `name`. + base::StatusOr> + Read(std::string_view name, int depth, std::vector& hidden); base::StatusOr> Select(SyntaqliteParser* p, uint32_t id, int depth); + // Brings into scope the CTEs visible at `at`. Fails if `at` is not where a + // query can be read, as the CTEs in scope there are not known. + base::Status EnterScope(SqlNode at) { + if (!Within(at.parser, syntaqlite_result_root(at.parser), at.id)) { + return base::ErrStatus( + "relation analysis: could not find the scope of the node being read"); + } + // Defining the CTEs says nothing about the rows of what is read at `at`. + preserves_rows_ = true; + return base::OkStatus(); + } bool preserves_rows() const { return preserves_rows_; } @@ -182,6 +214,12 @@ class RelationAnalyzer::Impl { std::vector hidden; }; using Scope = std::vector; + // A common table expression in scope. No columns means they could not be + // worked out, as for a recursive one while it is still being defined. + struct Cte { + std::string_view name; + std::optional> columns; + }; base::Status Sources(SyntaqliteParser* p, uint32_t id, @@ -192,6 +230,32 @@ class RelationAnalyzer::Impl { static ColumnLineage Lookup(const Scope&, std::string_view table, std::string_view column); + // Brings every CTE of `with` into scope, of unknown shape, and returns the + // index in `ctes_` of the first. As in SQLite, each CTE body sees all the + // CTEs of its WITH clause, later ones and itself included, so the names + // must all be in scope before any body is analyzed. One read before it is + // worked out has unknown shape, so it fails rather than reading a relation + // of the same name. + size_t DeclareCtes(SyntaqliteParser* p, const SyntaqliteWithClause& with); + // Works out the columns of the CTE declared at `ctes_[slot]`. + void DefineCte(SyntaqliteParser* p, + const SyntaqliteCteDefinition& cte, + size_t slot, + int depth); + // Brings into scope the CTEs defined on the way from `id` down to `at`, + // through the places a query can be read: statements, WITH clauses, + // compound selects, FROM clauses and pipeline sources. Returns whether `at` + // was reached; if not, the CTEs in scope are unchanged. + bool Within(SyntaqliteParser* p, uint32_t id, uint32_t at); + bool WithinSources(SyntaqliteParser* p, uint32_t id, uint32_t at); + // The innermost CTE in scope named `name`, if any. + const Cte* FindCte(std::string_view name) const; + // Renames `columns` to the names listed at `names`, if there is a list, as + // `CREATE VIEW v(a, b)` and `WITH t(a, b)` do. + static base::Status NameColumns(SyntaqliteParser* p, + uint32_t names, + std::string_view relation, + std::vector& columns); const Catalog& catalog_; // Lineage string_views point into each view's sql string and parse tree, so @@ -201,9 +265,174 @@ class RelationAnalyzer::Impl { // Lineage string_views point into each leaf relation's strings, so they are // kept at stable addresses for the same reason. std::vector> leaves_; + // The CTEs in scope of what is being analyzed, innermost last. + std::vector ctes_; bool preserves_rows_ = true; }; +size_t RelationAnalyzer::Impl::DeclareCtes(SyntaqliteParser* p, + const SyntaqliteWithClause& with) { + size_t first = ctes_.size(); + for (const SyntaqliteCteDefinition* cte : CteDefinitions(p, with)) { + ctes_.push_back({Text(p, cte->cte_name), std::nullopt}); + } + return first; +} + +void RelationAnalyzer::Impl::DefineCte(SyntaqliteParser* p, + const SyntaqliteCteDefinition& cte, + size_t slot, + int depth) { + base::StatusOr> columns = + Select(p, cte.select, depth); + if (columns.ok() && + NameColumns(p, cte.columns, ctes_[slot].name, *columns).ok()) { + ctes_[slot].columns = std::move(*columns); + } +} + +bool RelationAnalyzer::Impl::Within(SyntaqliteParser* p, + uint32_t id, + uint32_t at) { + if (id == at) { + return true; + } + const SyntaqliteNode* node = Node(p, id); + if (!node) { + return false; + } + switch (static_cast(node->tag)) { + case SYNTAQLITE_NODE_CREATE_VIEW_STMT: + return Within(p, node->create_view_stmt.select, at); + case SYNTAQLITE_NODE_CREATE_PERFETTO_VIEW_STMT: + return Within(p, node->create_perfetto_view_stmt.select, at); + case SYNTAQLITE_NODE_CREATE_PERFETTO_TABLE_STMT: + return Within(p, node->create_perfetto_table_stmt.select, at) || + Within(p, node->create_perfetto_table_stmt.pipeline, at); + case SYNTAQLITE_NODE_CREATE_PERFETTO_FUNCTION_STMT: + return Within(p, node->create_perfetto_function_stmt.select, at); + case SYNTAQLITE_NODE_CREATE_TABLE_STMT: + return Within(p, node->create_table_stmt.as_select, at); + case SYNTAQLITE_NODE_SELECT_STMT: + return WithinSources(p, node->select_stmt.from_clause, at); + case SYNTAQLITE_NODE_COMPOUND_SELECT: + return Within(p, node->compound_select.left, at) || + Within(p, node->compound_select.right, at); + case SYNTAQLITE_NODE_PERFETTO_PIPELINE: + return Within(p, node->perfetto_pipeline.from, at) || + Within(p, node->perfetto_pipeline.intersection, at); + case SYNTAQLITE_NODE_PERFETTO_INTERVAL_INTERSECTION: { + const void* list = syntaqlite_parser_node( + p, node->perfetto_interval_intersection.operands); + uint32_t count = list ? syntaqlite_list_count(list) : 0; + for (uint32_t i = 0; i < count; ++i) { + if (Within(p, syntaqlite_list_child_id(list, i), at)) { + return true; + } + } + return false; + } + case SYNTAQLITE_NODE_PERFETTO_PIPE_SOURCE: + return Within(p, node->perfetto_pipe_source.select, at); + case SYNTAQLITE_NODE_WITH_CLAUSE: { + const SyntaqliteWithClause& with = node->with_clause; + size_t outer = DeclareCtes(p, with); + size_t slot = outer; + for (const SyntaqliteCteDefinition* cte : CteDefinitions(p, with)) { + if (Within(p, cte->select, at)) { + return true; + } + DefineCte(p, *cte, slot++, 0); + } + if (Within(p, with.select, at)) { + return true; + } + ctes_.resize(outer); + return false; + } + default: + return false; + } +} + +bool RelationAnalyzer::Impl::WithinSources(SyntaqliteParser* p, + uint32_t id, + uint32_t at) { + if (id == at) { + return true; + } + const SyntaqliteNode* node = Node(p, id); + if (!node) { + return false; + } + switch (static_cast(node->tag)) { + case SYNTAQLITE_NODE_JOIN_CLAUSE: + return WithinSources(p, node->join_clause.left, at) || + WithinSources(p, node->join_clause.right, at); + case SYNTAQLITE_NODE_JOIN_PREFIX: + return WithinSources(p, node->join_prefix.source, at); + case SYNTAQLITE_NODE_SUBQUERY_TABLE_SOURCE: + return Within(p, node->subquery_table_source.select, at); + default: + return false; + } +} + +base::StatusOr> RelationAnalyzer::Impl::Read( + std::string_view name, + int depth, + std::vector& hidden) { + const Cte* cte = FindCte(name); + if (!cte) { + return Relation(name, depth, hidden); + } + // A CTE can filter or reorder its rows. + preserves_rows_ = false; + if (!cte->columns) { + return base::ErrStatus("relation analysis: CTE '%.*s' has unknown shape", + static_cast(name.size()), name.data()); + } + return *cte->columns; +} + +const RelationAnalyzer::Impl::Cte* RelationAnalyzer::Impl::FindCte( + std::string_view name) const { + for (auto it = ctes_.rbegin(); it != ctes_.rend(); ++it) { + if (base::CaseInsensitiveEqual(it->name, name)) { + return &*it; + } + } + return nullptr; +} + +base::Status RelationAnalyzer::Impl::NameColumns( + SyntaqliteParser* p, + uint32_t names, + std::string_view relation, + std::vector& columns) { + if (!syntaqlite_node_is_present(names)) { + return base::OkStatus(); + } + const void* list = syntaqlite_parser_node(p, names); + uint32_t count = syntaqlite_list_count(list); + if (count != columns.size()) { + return base::ErrStatus( + "relation analysis: '%.*s' names %u columns for %u results", + static_cast(relation.size()), relation.data(), count, + static_cast(columns.size())); + } + for (uint32_t i = 0; i < count; ++i) { + const SyntaqliteNode* column = Node(p, syntaqlite_list_child_id(list, i)); + if (!column || column->tag != SYNTAQLITE_NODE_COLUMN_REF) { + return base::ErrStatus("relation analysis: invalid column name in '%.*s'", + static_cast(relation.size()), + relation.data()); + } + columns[i].output_name = Text(p, column->column_ref.column); + } + return base::OkStatus(); +} + ColumnLineage RelationAnalyzer::Impl::Lookup(const Scope& scope, std::string_view table, std::string_view column) { @@ -300,9 +529,13 @@ base::Status RelationAnalyzer::Impl::Sources(SyntaqliteParser* p, if (const SyntaqliteNode* a = Node(p, node->table_ref.alias)) { alias = Text(p, a->ident_name.source); } + // A CTE hides any relation of the same name. It is never qualified by + // a schema. std::vector hidden; base::StatusOr> columns = - Relation(name, depth, hidden); + Text(p, node->table_ref.schema).empty() + ? Read(name, depth, hidden) + : Relation(name, depth, hidden); if (!columns.ok()) { scope->push_back({alias, std::nullopt, {}, {}}); return base::OkStatus(); @@ -421,9 +654,19 @@ RelationAnalyzer::Impl::Select(SyntaqliteParser* p, uint32_t id, int depth) { switch (static_cast(node->tag)) { case SYNTAQLITE_NODE_SELECT_STMT: return SelectStmt(p, node->select_stmt, depth); - case SYNTAQLITE_NODE_WITH_CLAUSE: + case SYNTAQLITE_NODE_WITH_CLAUSE: { preserves_rows_ = false; - return Select(p, node->with_clause.select, depth); + size_t outer = DeclareCtes(p, node->with_clause); + size_t slot = outer; + for (const SyntaqliteCteDefinition* cte : + CteDefinitions(p, node->with_clause)) { + DefineCte(p, *cte, slot++, depth); + } + base::StatusOr> columns = + Select(p, node->with_clause.select, depth); + ctes_.resize(outer); + return columns; + } case SYNTAQLITE_NODE_COMPOUND_SELECT: { preserves_rows_ = false; base::StatusOr> left = @@ -512,28 +755,13 @@ base::StatusOr> RelationAnalyzer::Impl::Relation( return base::ErrStatus("relation analysis: '%.*s' is not a view", static_cast(name.size()), name.data()); } + // A view sees none of the CTEs of the query reading it. + std::vector outer = std::exchange(ctes_, {}); base::StatusOr> columns = Select(p, select, depth + 1); + ctes_ = std::move(outer); RETURN_IF_ERROR(columns.status()); - if (syntaqlite_node_is_present(column_names)) { - const void* list = syntaqlite_parser_node(p, column_names); - uint32_t count = syntaqlite_list_count(list); - if (count != columns->size()) { - return base::ErrStatus( - "relation analysis: view '%.*s' names %u columns for %u results", - static_cast(name.size()), name.data(), count, - static_cast(columns->size())); - } - for (uint32_t i = 0; i < count; ++i) { - const SyntaqliteNode* column = Node(p, syntaqlite_list_child_id(list, i)); - if (!column || column->tag != SYNTAQLITE_NODE_COLUMN_REF) { - return base::ErrStatus( - "relation analysis: invalid column name in '%.*s'", - static_cast(name.size()), name.data()); - } - (*columns)[i].output_name = Text(p, column->column_ref.column); - } - } + RETURN_IF_ERROR(NameColumns(p, column_names, name, *columns)); return {columns}; } @@ -573,17 +801,20 @@ RelationAnalyzer::~RelationAnalyzer() = default; base::StatusOr RelationAnalyzer::AnalyzeQuery(SqlNode query) { impl_->Begin(); + RETURN_IF_ERROR(impl_->EnterScope(query)); ASSIGN_OR_RETURN(auto columns, impl_->Select(query.parser, query.id, 0)); return RelationLineage(std::make_unique( std::move(columns), impl_->preserves_rows())); } base::StatusOr RelationAnalyzer::AnalyzeRelation( + SqlNode at, std::string_view name) { impl_->Begin(); + RETURN_IF_ERROR(impl_->EnterScope(at)); // Hidden columns are still columns of the relation itself. std::vector hidden; - ASSIGN_OR_RETURN(auto columns, impl_->Relation(name, 0, hidden)); + ASSIGN_OR_RETURN(auto columns, impl_->Read(name, 0, hidden)); return RelationLineage(std::make_unique( std::move(columns), impl_->preserves_rows())); } diff --git a/src/perfetto_sql/analysis/relation.h b/src/perfetto_sql/analysis/relation.h index a8afbb3d0eb..6ec2b34c032 100644 --- a/src/perfetto_sql/analysis/relation.h +++ b/src/perfetto_sql/analysis/relation.h @@ -31,7 +31,7 @@ struct SyntaqliteParser; namespace perfetto::perfetto_sql::analysis { -// A node in a caller-owned Syntaqlite parse tree. +// A node of the statement a caller-owned Syntaqlite parser last parsed. struct SqlNode { SyntaqliteParser* parser; uint32_t id; @@ -133,8 +133,13 @@ class RelationAnalyzer { RelationAnalyzer(const RelationAnalyzer&) = delete; RelationAnalyzer& operator=(const RelationAnalyzer&) = delete; - base::StatusOr AnalyzeQuery(SqlNode); - base::StatusOr AnalyzeRelation(std::string_view name); + // The lineage of `query`, read where it is in its statement: it sees the + // CTEs in scope there. + base::StatusOr AnalyzeQuery(SqlNode query); + // The lineage of the relation `name`, read at the node `at`: a CTE in scope + // there hides any relation of the same name. + base::StatusOr AnalyzeRelation(SqlNode at, + std::string_view name); private: class Impl; diff --git a/src/perfetto_sql/analysis/relation_unittest.cc b/src/perfetto_sql/analysis/relation_unittest.cc index a83762dc243..db05d131539 100644 --- a/src/perfetto_sql/analysis/relation_unittest.cc +++ b/src/perfetto_sql/analysis/relation_unittest.cc @@ -134,6 +134,45 @@ class RelationAnalyzerTest : public ::testing::Test { return result.ok() ? Show(*result) : std::vector{}; } + // Analyzes `name`, or with `name` empty the query itself, as read at the + // select in `sql` whose text is `at`. An error is shown as "!". + std::vector ReadAt(const std::string& sql, + const std::string& at, + const std::string& name) { + ScopedParser parser(syntaqlite_parser_create_perfetto(nullptr)); + syntaqlite_parser_set_collect_node_extents(parser.get(), 1); + syntaqlite_parser_reset(parser.get(), sql.data(), + static_cast(sql.size())); + if (syntaqlite_parser_next(parser.get()) != SYNTAQLITE_PARSE_OK) { + return {"!could not parse test query"}; + } + std::optional found; + for (uint32_t id = 0; id < syntaqlite_parser_node_count(parser.get()); + ++id) { + const auto* node = static_cast( + syntaqlite_parser_node(parser.get(), id)); + SyntaqliteLength len = 0; + SyntaqliteStmtOffset offset = 0; + const char* text = + syntaqlite_parser_node_text(parser.get(), id, &len, &offset); + if (node && node->tag == SYNTAQLITE_NODE_SELECT_STMT && text && + std::string_view(text, len) == at) { + found = id; + } + } + if (!found) { + return {"!no select '" + at + "'"}; + } + RelationAnalyzer analyzer(catalog_); + auto result = name.empty() + ? analyzer.AnalyzeQuery({parser.get(), *found}) + : analyzer.AnalyzeRelation({parser.get(), *found}, name); + if (!result.ok()) { + return {"!" + result.status().message()}; + } + return Show(*result); + } + TestCatalog catalog_; }; @@ -201,6 +240,114 @@ TEST_F(RelationAnalyzerTest, FollowsAliasesSubqueriesAndViews) { testing::ElementsAre("renamed=slice.id")); } +// CTEs are in scope for the rest of their WITH clause and its query, hiding +// relations of the same name there, as in SQLite. +TEST_F(RelationAnalyzerTest, Ctes) { + // Each sees those before it, and is renamed by its list of names. + EXPECT_THAT(Select(R"( + WITH + a(x) AS (SELECT id FROM slice), + b AS (SELECT x AS y, name FROM a, slice) + SELECT * FROM (SELECT * FROM b) + )"), + testing::ElementsAre("y=slice.id", "name=slice.name")); + // A CTE hides a table of the same name in its query, but not outside the + // query or in a view the query reads. + catalog_.AddView("v", "CREATE VIEW v AS SELECT * FROM thread"); + EXPECT_THAT(Select(R"( + SELECT * + FROM ( + WITH thread AS (SELECT id FROM slice) + SELECT * FROM thread, v + ), + thread + )"), + testing::ElementsAre("id=slice.id", "utid=thread.utid", + "name=thread.name", "utid=thread.utid", + "name=thread.name")); + // Every CTE of a WITH clause is in scope in all of its definitions, so a + // CTE read before its columns are known, by itself or by an earlier CTE, + // is never mistaken for a table of the same name. + EXPECT_THAT(Select(R"( + WITH + a AS (SELECT utid FROM thread), + thread AS (SELECT id AS utid FROM slice) + SELECT * FROM a + )"), + testing::ElementsAre("utid=")); + EXPECT_THAT(Select(R"( + WITH thread AS (SELECT utid FROM thread) + SELECT * FROM thread + )"), + testing::ElementsAre("utid=")); + // A recursive CTE reads itself before its columns are known. + EXPECT_THAT(Select(R"( + WITH RECURSIVE n(i) AS ( + SELECT id FROM slice + UNION ALL + SELECT i + 1 FROM n WHERE i < 3 + ) + SELECT * FROM n + )"), + testing::ElementsAre("i=")); + // A CTE can filter its rows, so what reads one has no row origin. + auto result = + Analyze("WITH t AS (SELECT id FROM slice WHERE ts > 5) SELECT id FROM t"); + ASSERT_TRUE(result.ok()); + EXPECT_EQ(result->row_origin(), std::nullopt); +} + +// What is read at a node of a statement sees the CTEs in scope there, and only +// those. +TEST_F(RelationAnalyzerTest, ReadsSeeTheCtesInScope) { + const std::string kHere = "SELECT 1 AS here"; + // Through statements, compound selects and joins; the innermost CTE of a + // name hides the others. + const std::string kNested = R"( + CREATE VIEW v AS + WITH t AS (SELECT utid FROM thread) + SELECT 1, 2, 3 + UNION ALL + SELECT * FROM thread JOIN ( + WITH t AS (SELECT id FROM slice) + SELECT * FROM (SELECT 1 AS here) + ) + )"; + EXPECT_THAT(ReadAt(kNested, kHere, "t"), testing::ElementsAre("id=slice.id")); + // A query read there. + EXPECT_THAT(ReadAt(R"( + WITH t AS (SELECT id FROM slice) + SELECT * FROM (SELECT * FROM t) + )", + "SELECT * FROM t", ""), + testing::ElementsAre("id=slice.id")); + // Inside a CTE's definition, all the CTEs of its WITH clause, but only those + // before it are known yet; and none of another subquery's. + const std::string kDefinition = R"( + SELECT * + FROM (WITH s AS (SELECT id FROM slice) SELECT * FROM s), + ( + WITH + a AS (SELECT id FROM slice), + b AS (SELECT * FROM (SELECT 1 AS here)), + c AS (SELECT utid FROM thread) + SELECT * FROM b + ) + )"; + EXPECT_THAT(ReadAt(kDefinition, kHere, "a"), + testing::ElementsAre("id=slice.id")); + EXPECT_THAT( + ReadAt(kDefinition, kHere, "c"), + testing::ElementsAre("!relation analysis: CTE 'c' has unknown shape")); + EXPECT_THAT(ReadAt(kDefinition, kHere, "s"), + testing::ElementsAre("!relation analysis: 's' is not known")); + // A node where no query can be read has no known scope. + EXPECT_THAT( + ReadAt("SELECT * FROM slice WHERE id IN (SELECT 1 AS here)", kHere, "t"), + testing::ElementsAre("!relation analysis: could not find the scope of " + "the node being read")); +} + TEST_F(RelationAnalyzerTest, KeepsEveryOriginOfAmbiguousJoinColumn) { EXPECT_THAT(Select("SELECT name FROM slice, thread"), testing::ElementsAre("name=slice.name,thread.name")); @@ -237,8 +384,7 @@ TEST_F(RelationAnalyzerTest, IdentifiesRowOrigin) { 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"); + auto result = Analyze("SELECT * FROM v2"); ASSERT_TRUE(result.ok()); EXPECT_EQ(result->row_origin(), std::make_optional("slice")); @@ -246,8 +392,7 @@ TEST_F(RelationAnalyzerTest, DetectsRowPreservingViewChains) { 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"); + auto result = Analyze("SELECT * FROM v"); ASSERT_TRUE(result.ok()); EXPECT_EQ(result->row_origin(), std::nullopt); } @@ -255,8 +400,7 @@ TEST_F(RelationAnalyzerTest, RejectsFilteredViewAsRowOrigin) { 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"); + auto result = Analyze("SELECT * FROM v"); ASSERT_TRUE(result.ok()); EXPECT_EQ(result->row_origin(), std::nullopt); } diff --git a/src/trace_processor/BUILD.gn b/src/trace_processor/BUILD.gn index 931ebebc9c3..79237bb49da 100644 --- a/src/trace_processor/BUILD.gn +++ b/src/trace_processor/BUILD.gn @@ -446,6 +446,7 @@ perfetto_unittest_source_set("unittests") { deps = [ ":top_level_unittests", "containers:unittests", + "core/common:unittests", "core/dataframe:unittests", "core/exec:unittests", "core/interpreter:unittests", diff --git a/src/trace_processor/core/common/BUILD.gn b/src/trace_processor/core/common/BUILD.gn index a467725e9dd..6af99b51ae4 100644 --- a/src/trace_processor/core/common/BUILD.gn +++ b/src/trace_processor/core/common/BUILD.gn @@ -19,9 +19,12 @@ source_set("common") { "duplicate_types.h", "null_types.h", "op_types.h", + "row_layout.h", "schema.h", "sort_types.h", "storage_types.h", + "string_ranks.cc", + "string_ranks.h", "value_fetcher.h", ] deps = [ @@ -31,3 +34,18 @@ source_set("common") { "../util", ] } + +perfetto_unittest_source_set("unittests") { + testonly = true + sources = [ + "row_layout_unittest.cc", + "string_ranks_unittest.cc", + ] + deps = [ + ":common", + "../../../../gn:default_deps", + "../../../../gn:gtest_and_gmock", + "../../../base", + "../../containers", + ] +} diff --git a/src/trace_processor/core/common/row_layout.h b/src/trace_processor/core/common/row_layout.h new file mode 100644 index 00000000000..85766ba7a9f --- /dev/null +++ b/src/trace_processor/core/common/row_layout.h @@ -0,0 +1,144 @@ +/* + * 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. + */ + +#ifndef SRC_TRACE_PROCESSOR_CORE_COMMON_ROW_LAYOUT_H_ +#define SRC_TRACE_PROCESSOR_CORE_COMMON_ROW_LAYOUT_H_ + +#include +#include +#include +#include + +#include "perfetto/base/compiler.h" +#include "perfetto/base/endian.h" +#include "perfetto/base/logging.h" + +namespace perfetto::trace_processor::core { + +// Lays rows out in fixed-width slots whose bytes compare as their values do. +// A nullable slot starts with 0xFF if the row has a value and 0 if not; +// a descending slot's bits are inverted. +class RowLayout { + public: + enum class Type : uint8_t { kUint32, kInt32, kInt64, kDouble }; + struct Column { + Type type; + bool nullable = false; + bool descending = false; + }; + struct Slot { + uint32_t offset; + uint32_t stride; + bool nullable; + bool descending; + }; + + RowLayout() = default; + explicit RowLayout(const std::vector& columns) { + for (const Column& column : columns) { + slots_.push_back({stride_, 0, column.nullable, column.descending}); + stride_ += (column.nullable ? 1u : 0u) + ValueSize(column.type); + } + for (Slot& slot : slots_) { + slot.stride = stride_; + } + } + + uint32_t stride() const { return stride_; } + + const Slot& slot(uint32_t column) const { return slots_[column]; } + + // `get(i, &value)` returns false if row i has no value. + template + PERFETTO_ALWAYS_INLINE static void Write(const Slot& slot, + uint32_t count, + Get get, + uint8_t* rows) { + // Any other type would be implicitly converted to one of these by + // Encode, writing a value of a different size to its slot. + static_assert(std::is_same_v || std::is_same_v || + std::is_same_v || std::is_same_v, + "Unsupported type for row layout representation"); + uint8_t* to = rows + slot.offset; + if (slot.nullable) { + for (uint32_t i = 0; i < count; ++i, to += slot.stride) { + T value{}; + bool present = get(i, &value); + auto byte = static_cast(present ? 0xFF : 0); + *to = slot.descending ? static_cast(~byte) : byte; + if (present) { + WriteValue(value, slot.descending, to + 1); + } else { + memset(to + 1, 0, sizeof(T)); + } + } + } else { + for (uint32_t i = 0; i < count; ++i, to += slot.stride) { + T value{}; + [[maybe_unused]] bool present = get(i, &value); + PERFETTO_DCHECK(present); + WriteValue(value, slot.descending, to); + } + } + } + + private: + static uint32_t ValueSize(Type type) { + switch (type) { + case Type::kUint32: + case Type::kInt32: + return sizeof(uint32_t); + case Type::kInt64: + case Type::kDouble: + return sizeof(uint64_t); + } + PERFETTO_FATAL("Unknown type"); + } + + template + PERFETTO_ALWAYS_INLINE static void WriteValue(T x, bool invert, uint8_t* to) { + auto bits = Encode(x); + bits = invert ? static_cast(~bits) : bits; + memcpy(to, &bits, sizeof(bits)); + } + + // The inspiration behind these functions comes from: + // https://arrow.apache.org/blog/2022/11/07/multi-column-sorts-in-arrow-rust-part-2/ + PERFETTO_ALWAYS_INLINE static uint32_t Encode(uint32_t x) { + return base::HostToBE32(x); + } + PERFETTO_ALWAYS_INLINE static uint32_t Encode(int32_t x) { + return base::HostToBE32( + static_cast(x ^ static_cast(0x80000000))); + } + PERFETTO_ALWAYS_INLINE static uint64_t Encode(int64_t x) { + return base::HostToBE64( + static_cast(x ^ static_cast(0x8000000000000000))); + } + PERFETTO_ALWAYS_INLINE static uint64_t Encode(double x) { + int64_t bits; + memcpy(&bits, &x, sizeof(double)); + bits ^= static_cast(static_cast(bits >> 63) >> 1); + return Encode(bits); + } + + std::vector slots_; + uint32_t stride_ = 0; +}; + +} // namespace perfetto::trace_processor::core + +#endif // SRC_TRACE_PROCESSOR_CORE_COMMON_ROW_LAYOUT_H_ diff --git a/src/trace_processor/core/common/row_layout_unittest.cc b/src/trace_processor/core/common/row_layout_unittest.cc new file mode 100644 index 00000000000..9ff6bb70ef6 --- /dev/null +++ b/src/trace_processor/core/common/row_layout_unittest.cc @@ -0,0 +1,107 @@ +/* + * 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/trace_processor/core/common/row_layout.h" + +#include +#include +#include +#include +#include + +#include "test/gtest_and_gmock.h" + +namespace perfetto::trace_processor::core { +namespace { + +template +std::vector LayOut(RowLayout::Type type, + const std::vector>& values, + bool nullable, + bool descending) { + RowLayout layout({{type, nullable, descending}}); + std::vector rows(layout.stride() * values.size()); + RowLayout::Write( + layout.slot(0), static_cast(values.size()), + [&](uint32_t i, T* out) { + if (!values[i]) { + return false; + } + *out = *values[i]; + return true; + }, + rows.data()); + return rows; +} + +template +void ExpectOrdered(RowLayout::Type type, + const std::vector>& ascending, + bool nullable) { + for (bool descending : {false, true}) { + std::vector rows = LayOut(type, ascending, nullable, descending); + size_t stride = rows.size() / ascending.size(); + for (size_t i = 0; i + 1 < ascending.size(); ++i) { + int cmp = memcmp(&rows[i * stride], &rows[(i + 1) * stride], stride); + if (descending) { + EXPECT_GT(cmp, 0) << i; + } else { + EXPECT_LT(cmp, 0) << i; + } + } + } +} + +TEST(RowLayoutTest, BytesSortAsValues) { + ExpectOrdered( + RowLayout::Type::kUint32, + {0u, 1u, 255u, 256u, std::numeric_limits::max()}, false); + ExpectOrdered(RowLayout::Type::kInt32, + {std::numeric_limits::min(), -256, -1, 0, 1, + 256, std::numeric_limits::max()}, + false); + ExpectOrdered( + RowLayout::Type::kInt64, + {std::numeric_limits::min(), -(int64_t{1} << 40), int64_t{-1}, + int64_t{0}, int64_t{1}, int64_t{1} << 40, + std::numeric_limits::max()}, + false); + ExpectOrdered( + RowLayout::Type::kDouble, + {-std::numeric_limits::infinity(), -1e300, -1.5, -0.0, 1e-300, + 1.5, 1e300, std::numeric_limits::infinity()}, + false); +} + +TEST(RowLayoutTest, NullsSortFirst) { + ExpectOrdered( + RowLayout::Type::kInt64, + {std::nullopt, std::numeric_limits::min(), int64_t{0}}, true); +} + +TEST(RowLayoutTest, SlotsSitOneAfterAnother) { + RowLayout layout({{RowLayout::Type::kInt32, false}, + {RowLayout::Type::kDouble, true}, + {RowLayout::Type::kUint32, true}}); + EXPECT_EQ(layout.slot(0).offset, 0u); + EXPECT_EQ(layout.slot(1).offset, 4u); + EXPECT_EQ(layout.slot(2).offset, 13u); + EXPECT_EQ(layout.stride(), 18u); + EXPECT_EQ(layout.slot(1).stride, 18u); +} + +} // namespace +} // namespace perfetto::trace_processor::core diff --git a/src/trace_processor/core/common/string_ranks.cc b/src/trace_processor/core/common/string_ranks.cc new file mode 100644 index 00000000000..db0728b68fa --- /dev/null +++ b/src/trace_processor/core/common/string_ranks.cc @@ -0,0 +1,68 @@ +/* + * 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/trace_processor/core/common/string_ranks.h" + +#include +#include +#include + +#include "perfetto/base/logging.h" +#include "perfetto/ext/base/string_view.h" +#include "src/trace_processor/containers/string_pool.h" +#include "src/trace_processor/core/util/sort.h" + +namespace perfetto::trace_processor::core { +namespace { + +struct StringSortToken { + std::string_view str_view; + StringPool::Id id; +}; + +struct StringSortKey { + std::string_view operator()(const StringSortToken& token) const { + return token.str_view; + } +}; + +} // namespace + +void StringRanks::Rank(const StringPool& pool) { + // Initially do *not* default initialize the array for performance. + std::unique_ptr ids_to_sort( + new StringSortToken[ranks_.size()]); + std::unique_ptr scratch( + new StringSortToken[ranks_.size()]); + uint32_t i = 0; + for (auto it = ranks_.GetIterator(); it; ++it) { + base::StringView str_view = pool.Get(it.key()); + ids_to_sort[i++] = StringSortToken{ + std::string_view(str_view.data(), str_view.size()), + it.key(), + }; + } + auto* sorted = + core::MsdRadixSort(ids_to_sort.get(), ids_to_sort.get() + ranks_.size(), + scratch.get(), StringSortKey{}); + for (uint32_t rank = 0; rank < ranks_.size(); ++rank) { + uint32_t* it = ranks_.Find(sorted[rank].id); + PERFETTO_DCHECK(it); + *it = rank; + } +} + +} // namespace perfetto::trace_processor::core diff --git a/src/trace_processor/core/common/string_ranks.h b/src/trace_processor/core/common/string_ranks.h new file mode 100644 index 00000000000..b97244919c0 --- /dev/null +++ b/src/trace_processor/core/common/string_ranks.h @@ -0,0 +1,47 @@ +/* + * 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. + */ + +#ifndef SRC_TRACE_PROCESSOR_CORE_COMMON_STRING_RANKS_H_ +#define SRC_TRACE_PROCESSOR_CORE_COMMON_STRING_RANKS_H_ + +#include +#include + +#include "perfetto/ext/base/flat_hash_map.h" +#include "src/trace_processor/containers/string_pool.h" + +namespace perfetto::trace_processor::core { + +// Ranks strings by their contents. +class StringRanks { + public: + void Add(StringPool::Id id) { ranks_.Insert(id, 0); } + + // Sorts the string IDs added and assigns each its rank. + void Rank(const StringPool& pool); + + const uint32_t* Find(StringPool::Id id) const { return ranks_.Find(id); } + + size_t size() const { return ranks_.size(); } + void Clear() { ranks_.Clear(); } + + private: + base::FlatHashMap ranks_; +}; + +} // namespace perfetto::trace_processor::core + +#endif // SRC_TRACE_PROCESSOR_CORE_COMMON_STRING_RANKS_H_ diff --git a/src/trace_processor/core/common/string_ranks_unittest.cc b/src/trace_processor/core/common/string_ranks_unittest.cc new file mode 100644 index 00000000000..2fc61a3084a --- /dev/null +++ b/src/trace_processor/core/common/string_ranks_unittest.cc @@ -0,0 +1,50 @@ +/* + * 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/trace_processor/core/common/string_ranks.h" + +#include + +#include "src/trace_processor/containers/string_pool.h" +#include "test/gtest_and_gmock.h" + +namespace perfetto::trace_processor::core { +namespace { + +using testing::Pointee; + +TEST(StringRanksTest, RanksByContents) { + StringPool pool; + StringPool::Id cherry = pool.InternString("cherry"); + StringPool::Id apple = pool.InternString("apple"); + StringPool::Id banana = pool.InternString("banana"); + + StringRanks ranks; + ranks.Add(cherry); + ranks.Add(apple); + ranks.Add(banana); + ranks.Add(apple); + ranks.Rank(pool); + + EXPECT_EQ(ranks.size(), 3u); + EXPECT_THAT(ranks.Find(apple), Pointee(0u)); + EXPECT_THAT(ranks.Find(banana), Pointee(1u)); + EXPECT_THAT(ranks.Find(cherry), Pointee(2u)); + EXPECT_EQ(ranks.Find(pool.InternString("durian")), nullptr); +} + +} // namespace +} // namespace perfetto::trace_processor::core diff --git a/src/trace_processor/core/dataframe/bytecode_lowering.cc b/src/trace_processor/core/dataframe/bytecode_lowering.cc index 2e5c7165e6f..dcf8ec7460a 100644 --- a/src/trace_processor/core/dataframe/bytecode_lowering.cc +++ b/src/trace_processor/core/dataframe/bytecode_lowering.cc @@ -30,6 +30,7 @@ #include "perfetto/ext/base/type_set.h" #include "perfetto/ext/base/variant.h" #include "perfetto/public/compiler.h" +#include "src/trace_processor/core/common/row_layout.h" #include "src/trace_processor/core/common/storage_types.h" #include "src/trace_processor/core/dataframe/dataframe.h" #include "src/trace_processor/core/dataframe/logical_plan.h" @@ -88,18 +89,19 @@ GetSortedFilterArgs(const RangeOp& op) { } } -// Helper to get byte size of storage types for layout calculation. -uint8_t GetDataSize(StorageType type) { +// Helper to get the row layout type of storage types for layout calculation. +RowLayout::Type GetRowLayoutType(StorageType type) { switch (type.index()) { case StorageType::GetTypeIndex(): case StorageType::GetTypeIndex(): - case StorageType::GetTypeIndex(): case StorageType::GetTypeIndex(): - return sizeof(uint32_t); + return RowLayout::Type::kUint32; + case StorageType::GetTypeIndex(): + return RowLayout::Type::kInt32; case StorageType::GetTypeIndex(): - return sizeof(int64_t); + return RowLayout::Type::kInt64; case StorageType::GetTypeIndex(): - return sizeof(double); + return RowLayout::Type::kDouble; default: PERFETTO_FATAL("Invalid storage type"); } @@ -476,10 +478,10 @@ void BytecodeLowering::LowerDistinct(const logical::Distinct& d) { for (uint32_t col : d.cols) { row_layout_params.push_back({col, false}); } - uint16_t total_row_stride = CalculateRowLayoutStride(row_layout_params); + RowLayout layout = MakeRowLayout(row_layout_params); + auto total_row_stride = static_cast(layout.stride()); i::RwHandle> indices = EnsureIndicesAreInSlab(); - auto buffer_reg = - CopyToRowLayout(total_row_stride, indices, {}, row_layout_params); + auto buffer_reg = CopyToRowLayout(layout, indices, {}, row_layout_params); { using B = i::Distinct; auto& bc = AddOpcode(); @@ -589,9 +591,10 @@ void BytecodeLowering::LowerSort(const logical::Sort& sort) { {spec.col, columns_[spec.col]->storage.type().Is(), spec.direction == SortDirection::kDescending}); } - uint16_t total_row_stride = CalculateRowLayoutStride(row_layout_params); - auto buffer_reg = CopyToRowLayout(total_row_stride, indices, string_rank_map, - row_layout_params); + RowLayout layout = MakeRowLayout(row_layout_params); + auto total_row_stride = static_cast(layout.stride()); + auto buffer_reg = + CopyToRowLayout(layout, indices, string_rank_map, row_layout_params); { using B = i::SortRowLayout; auto& op = AddOpcode(); @@ -992,25 +995,26 @@ void BytecodeLowering::MaybeReleaseScratchSpanRegister() { } } -uint16_t BytecodeLowering::CalculateRowLayoutStride( +RowLayout BytecodeLowering::MakeRowLayout( const std::vector& row_layout_params) { PERFETTO_CHECK(!row_layout_params.empty()); - uint16_t calculated_total_row_stride = 0; + std::vector columns; + columns.reserve(row_layout_params.size()); for (const auto& param : row_layout_params) { const Column& col = GetColumn(param.column); - bool is_non_null = col.null_storage.nullability().Is(); - calculated_total_row_stride += - (is_non_null ? 0u : 1u) + GetDataSize(col.storage.type()); + columns.push_back({GetRowLayoutType(col.storage.type()), + !col.null_storage.nullability().Is(), + param.invert_copied_bits}); } - return calculated_total_row_stride; + return RowLayout(columns); } i::RwHandle> BytecodeLowering::CopyToRowLayout( - uint16_t row_stride, + const RowLayout& layout, i::RwHandle> indices, i::ReadHandle rank_map, const std::vector& row_layout_params) { - uint32_t buffer_size = plan_.params.max_row_count * row_stride; + uint32_t buffer_size = plan_.params.max_row_count * layout.stride(); i::RwHandle> new_buffer_reg = builder_.AllocateRegister>(); { @@ -1019,8 +1023,8 @@ i::RwHandle> BytecodeLowering::CopyToRowLayout( op.arg() = buffer_size; op.arg() = new_buffer_reg; } - uint16_t current_offset = 0; - for (const auto& param : row_layout_params) { + for (uint32_t i = 0; i < row_layout_params.size(); ++i) { + const RowLayoutParams& param = row_layout_params[i]; const Column& col = GetColumn(param.column); const auto& nullability = col.null_storage.nullability(); auto null_bv_reg = EnsurePrefixPopcountFor(param.column); @@ -1036,14 +1040,12 @@ i::RwHandle> BytecodeLowering::CopyToRowLayout( op.arg() = indices; op.arg() = new_buffer_reg; op.arg() = rank_map; - op.arg() = current_offset; - op.arg() = row_stride; + op.arg() = + static_cast(layout.slot(i).offset); + op.arg() = static_cast(layout.stride()); op.arg() = param.invert_copied_bits; } - current_offset += - (nullability.Is() ? 0u : 1u) + GetDataSize(col.storage.type()); } - PERFETTO_CHECK(current_offset == row_stride); return new_buffer_reg; } diff --git a/src/trace_processor/core/dataframe/bytecode_lowering.h b/src/trace_processor/core/dataframe/bytecode_lowering.h index 1340f6c0c5b..c6d8957e6ca 100644 --- a/src/trace_processor/core/dataframe/bytecode_lowering.h +++ b/src/trace_processor/core/dataframe/bytecode_lowering.h @@ -25,6 +25,7 @@ #include "perfetto/ext/base/flat_hash_map.h" #include "perfetto/public/compiler.h" +#include "src/trace_processor/core/common/row_layout.h" #include "src/trace_processor/core/dataframe/dataframe_register_cache.h" #include "src/trace_processor/core/dataframe/logical_plan.h" #include "src/trace_processor/core/dataframe/query_plan.h" @@ -209,11 +210,11 @@ class BytecodeLowering { void MaybeReleaseScratchSpanRegister(); - uint16_t CalculateRowLayoutStride( + RowLayout MakeRowLayout( const std::vector& row_layout_params); interpreter::RwHandle> CopyToRowLayout( - uint16_t row_stride, + const RowLayout& layout, interpreter::RwHandle> indices, interpreter::ReadHandle rank_map, const std::vector& row_layout_params); diff --git a/src/trace_processor/core/interpreter/bytecode_interpreter_impl.cc b/src/trace_processor/core/interpreter/bytecode_interpreter_impl.cc index 9276bc6c57d..50696c6aeb7 100644 --- a/src/trace_processor/core/interpreter/bytecode_interpreter_impl.cc +++ b/src/trace_processor/core/interpreter/bytecode_interpreter_impl.cc @@ -32,7 +32,6 @@ #include "src/trace_processor/core/util/bit_vector.h" #include "src/trace_processor/core/util/ops.h" #include "src/trace_processor/core/util/slab.h" -#include "src/trace_processor/core/util/sort.h" #include "src/trace_processor/core/util/span.h" #include "src/trace_processor/util/glob.h" @@ -40,11 +39,6 @@ namespace perfetto::trace_processor::core::interpreter::ops { namespace { -struct StringSortToken { - std::string_view str_view; - StringPool::Id id; -}; - struct GlobComparator { bool operator()(StringPool::Id lhs, const util::GlobMatcher& m) const { return m.Matches(pool->Get(lhs)); @@ -65,44 +59,8 @@ struct RegexComparator { const StringPool* pool; }; -struct StringSortKey { - std::string_view operator()(const StringSortToken& token) const { - return token.str_view; - } -}; - } // namespace -void FinalizeRanksInMapImpl( - const StringPool* string_pool, - std::unique_ptr>& - rank_map_ptr) { - PERFETTO_DCHECK(rank_map_ptr && rank_map_ptr.get()); - auto& rank_map = *rank_map_ptr; - - // Initially do *not* default initialize the array for performance. - std::unique_ptr ids_to_sort( - new StringSortToken[rank_map.size()]); - std::unique_ptr scratch( - new StringSortToken[rank_map.size()]); - uint32_t i = 0; - for (auto it = rank_map.GetIterator(); it; ++it) { - base::StringView str_view = string_pool->Get(it.key()); - ids_to_sort[i++] = StringSortToken{ - std::string_view(str_view.data(), str_view.size()), - it.key(), - }; - } - auto* sorted = - core::MsdRadixSort(ids_to_sort.get(), ids_to_sort.get() + rank_map.size(), - scratch.get(), StringSortKey{}); - for (uint32_t rank = 0; rank < rank_map.size(); ++rank) { - auto* it = rank_map.Find(sorted[rank].id); - PERFETTO_DCHECK(it); - *it = rank; - } -} - uint32_t* StringFilterGlobImpl(const StringPool* string_pool, const StringPool::Id* data, const char* pattern, @@ -208,9 +166,8 @@ void InitRankMap(InterpreterState& state, const struct InitRankMap& bytecode) { if (rank_map) { rank_map->get()->Clear(); } else { - state.WriteToRegister( - bytecode.arg(), - std::make_unique>()); + state.WriteToRegister(bytecode.arg(), + std::make_unique()); } } @@ -219,7 +176,8 @@ void FinalizeRanksInMap(InterpreterState& state, using B = struct FinalizeRanksInMap; StringIdToRankMap& rank_map_ptr = state.ReadFromRegister(bytecode.arg()); - FinalizeRanksInMapImpl(state.string_pool, rank_map_ptr); + PERFETTO_DCHECK(rank_map_ptr); + rank_map_ptr->Rank(*state.string_pool); } void Distinct(InterpreterState& state, const struct Distinct& bytecode) { @@ -295,7 +253,7 @@ void CollectIdIntoRankMap(InterpreterState& state, const auto& source = state.ReadFromRegister(bytecode.arg()); for (const uint32_t* it = source.b; it != source.e; ++it) { - rank_map.Insert(data[*it], 0); + rank_map.Add(data[*it]); } } diff --git a/src/trace_processor/core/interpreter/bytecode_interpreter_impl.h b/src/trace_processor/core/interpreter/bytecode_interpreter_impl.h index 2734b5e7eae..f05d5561ac7 100644 --- a/src/trace_processor/core/interpreter/bytecode_interpreter_impl.h +++ b/src/trace_processor/core/interpreter/bytecode_interpreter_impl.h @@ -40,6 +40,7 @@ #include "src/trace_processor/containers/string_pool.h" #include "src/trace_processor/core/common/null_types.h" #include "src/trace_processor/core/common/op_types.h" +#include "src/trace_processor/core/common/row_layout.h" #include "src/trace_processor/core/common/storage_types.h" #include "src/trace_processor/core/interpreter/bytecode_instructions.h" #include "src/trace_processor/core/interpreter/bytecode_interpreter.h" @@ -104,12 +105,6 @@ struct StringLessInvert { namespace ops { -// Outlined implementation of FinalizeRanksInMap bytecode. -// Sorts string IDs and assigns ranks in the map. -void FinalizeRanksInMapImpl( - const StringPool* string_pool, - std::unique_ptr>& rank_map_ptr); - // Outlined implementation of glob filtering for strings. // Returns pointer past last written output index. uint32_t* StringFilterGlobImpl(const StringPool* string_pool, @@ -230,42 +225,6 @@ template return o_write; } -template -inline PERFETTO_ALWAYS_INLINE auto GetComparableRowLayoutReprInteger(T x) { - // The inspiration behind this function comes from: - // https://arrow.apache.org/blog/2022/11/07/multi-column-sorts-in-arrow-rust-part-2/ - if constexpr (std::is_same_v) { - return base::HostToBE32(x); - } else if constexpr (std::is_same_v) { - return base::HostToBE32( - static_cast(x ^ static_cast(0x80000000))); - } else if constexpr (std::is_same_v) { - return base::HostToBE64( - static_cast(x ^ static_cast(0x8000000000000000))); - } else { - static_assert(std::is_same_v, - "Unsupported type for row layout representation"); - } -} - -template -inline PERFETTO_ALWAYS_INLINE auto GetComparableRowLayoutRepr(T x) { - // The inspiration behind this function comes from: - // https://arrow.apache.org/blog/2022/11/07/multi-column-sorts-in-arrow-rust-part-2/ - if constexpr (std::is_same_v || std::is_same_v || - std::is_same_v) { - return GetComparableRowLayoutReprInteger(x); - } else if constexpr (std::is_same_v) { - int64_t bits; - memcpy(&bits, &x, sizeof(double)); - bits ^= static_cast(static_cast(bits >> 63) >> 1); - return GetComparableRowLayoutReprInteger(bits); - } else { - static_assert(std::is_same_v, - "Unsupported type for row layout representation"); - } -} - inline PERFETTO_ALWAYS_INLINE void InitRange(InterpreterState& state, const struct InitRange& init) { using B = struct InitRange; @@ -1588,8 +1547,6 @@ inline PERFETTO_ALWAYS_INLINE void CopyToRowLayout( auto& dest_buffer = state.ReadFromRegister(bytecode.arg()); - uint8_t* dest = dest_buffer.data() + bytecode.arg(); - uint32_t stride = bytecode.arg(); const auto* data = state.ReadStorageFromRegister(bytecode.arg()); @@ -1599,69 +1556,67 @@ inline PERFETTO_ALWAYS_INLINE void CopyToRowLayout( state.MaybeReadFromRegister(bytecode.arg()); [[maybe_unused]] const NullBitvector* nbv = state.MaybeReadFromRegister(bytecode.arg()); - for (uint32_t* ptr = source.b; ptr != source.e; ++ptr) { - uint32_t table_index = *ptr; - uint32_t storage_index; - bool is_non_null; - uint32_t offset; + + // False if the row is null. + auto storage_index = [&](uint32_t i, uint32_t* out) { + uint32_t table_index = source.b[i]; if constexpr (std::is_same_v) { - is_non_null = true; - storage_index = table_index; - offset = 0; + *out = table_index; + return true; } else if constexpr (std::is_same_v) { PERFETTO_DCHECK(nbv && nbv->popcount.size() > 0); - is_non_null = nbv->bv->is_set(table_index); - storage_index = is_non_null - ? static_cast( - nbv->popcount[*ptr / 64] + - nbv->bv->count_set_bits_until_in_word(*ptr)) - : std::numeric_limits::max(); - uint8_t res = is_non_null ? 0xFF : 0; - *dest = invert ? static_cast(~res) : res; - offset = 1; + if (!nbv->bv->is_set(table_index)) { + return false; + } + *out = static_cast( + nbv->popcount[table_index / 64] + + nbv->bv->count_set_bits_until_in_word(table_index)); + return true; } else if constexpr (std::is_same_v) { - is_non_null = nbv->bv->is_set(table_index); - storage_index = table_index; - uint8_t res = is_non_null ? 0xFF : 0; - *dest = invert ? static_cast(~res) : res; - offset = 1; + *out = table_index; + return nbv->bv->is_set(table_index); } else { static_assert(std::is_same_v, "Unsupported Nullability type"); } - if constexpr (std::is_same_v) { - if (is_non_null) { - uint32_t res = GetComparableRowLayoutRepr(storage_index); - res = invert ? ~res : res; - memcpy(dest + offset, &res, sizeof(uint32_t)); - } else { - memset(dest + offset, 0, sizeof(uint32_t)); - } - } else if constexpr (std::is_same_v) { - if (is_non_null) { - uint32_t res; - if (rank_map_ptr) { - auto* rank = (*rank_map_ptr)->Find(data[storage_index]); - PERFETTO_DCHECK(rank); - res = GetComparableRowLayoutRepr(*rank); - } else { - res = GetComparableRowLayoutRepr(data[storage_index].raw_id()); - } - res = invert ? ~res : res; - memcpy(dest + offset, &res, sizeof(uint32_t)); - } else { - memset(dest + offset, 0, sizeof(uint32_t)); - } - } else { - if (is_non_null) { - auto res = GetComparableRowLayoutRepr(data[storage_index]); - res = invert ? ~res : res; - memcpy(dest + offset, &res, sizeof(res)); - } else { - memset(dest + offset, 0, sizeof(decltype(*data))); - } - } - dest += stride; + }; + RowLayout::Slot slot{bytecode.arg(), + bytecode.arg(), + !std::is_same_v, invert}; + auto count = static_cast(source.size()); + if constexpr (std::is_same_v) { + RowLayout::Write(slot, count, storage_index, dest_buffer.data()); + } else if constexpr (std::is_same_v) { + RowLayout::Write( + slot, count, + [&](uint32_t i, uint32_t* out) { + uint32_t index; + if (!storage_index(i, &index)) { + return false; + } + if (rank_map_ptr) { + const uint32_t* rank = (*rank_map_ptr)->Find(data[index]); + PERFETTO_DCHECK(rank); + *out = *rank; + } else { + *out = data[index].raw_id(); + } + return true; + }, + dest_buffer.data()); + } else { + using V = std::remove_cv_t>; + RowLayout::Write( + slot, count, + [&](uint32_t i, V* out) { + uint32_t index; + if (!storage_index(i, &index)) { + return false; + } + *out = data[index]; + return true; + }, + dest_buffer.data()); } } diff --git a/src/trace_processor/core/interpreter/bytecode_interpreter_unittest.cc b/src/trace_processor/core/interpreter/bytecode_interpreter_unittest.cc index ece79d28c09..822dc79eb03 100644 --- a/src/trace_processor/core/interpreter/bytecode_interpreter_unittest.cc +++ b/src/trace_processor/core/interpreter/bytecode_interpreter_unittest.cc @@ -2280,10 +2280,10 @@ TEST_F(BytecodeInterpreterTest, FinalizeRanksInMap_Simple) { StringPool::Id banana_id = spool_.InternString("banana"); StringPool::Id cherry_id = spool_.InternString("cherry"); - auto map = std::make_unique>(); - map->Insert(banana_id, 0); - map->Insert(cherry_id, 0); - map->Insert(apple_id, 0); + auto map = std::make_unique(); + map->Add(banana_id); + map->Add(cherry_id); + map->Add(apple_id); std::string bytecode_str = "FinalizeRanksInMap: [update_register=Register(0)]"; diff --git a/src/trace_processor/core/interpreter/bytecode_registers.h b/src/trace_processor/core/interpreter/bytecode_registers.h index 847ac730f25..a045283eba6 100644 --- a/src/trace_processor/core/interpreter/bytecode_registers.h +++ b/src/trace_processor/core/interpreter/bytecode_registers.h @@ -23,9 +23,9 @@ #include #include "perfetto/base/build_config.h" -#include "perfetto/ext/base/flat_hash_map.h" #include "src/trace_processor/containers/string_pool.h" #include "src/trace_processor/core/common/storage_types.h" +#include "src/trace_processor/core/common/string_ranks.h" #include "src/trace_processor/core/interpreter/interpreter_types.h" #include "src/trace_processor/core/util/bit_vector.h" #include "src/trace_processor/core/util/range.h" @@ -94,8 +94,7 @@ struct WriteHandle : HandleBase { // Empty placeholder type for register values. struct Empty {}; -using StringIdToRankMap = - std::unique_ptr>; +using StringIdToRankMap = std::unique_ptr; // Pointer to storage data along with its type. struct StoragePtr { diff --git a/src/trace_processor/perfetto_sql/engine/perfetto_sql_connection_unittest.cc b/src/trace_processor/perfetto_sql/engine/perfetto_sql_connection_unittest.cc index 536309a3c42..ee06d4d49ad 100644 --- a/src/trace_processor/perfetto_sql/engine/perfetto_sql_connection_unittest.cc +++ b/src/trace_processor/perfetto_sql/engine/perfetto_sql_connection_unittest.cc @@ -903,6 +903,19 @@ TEST_F(PerfettoSqlConnectionPipelineTest, StartsFromAnySql) { "2,0,60,60", "3,1,80,80")); } +TEST_F(PerfettoSqlConnectionPipelineTest, StartsFromSqlWithCtes) { + auto rows = Rows(R"( + FROM ( + WITH nodes AS (SELECT id, parent_id, self FROM tree) + SELECT * FROM nodes + ) + |> TREE ACCUMULATE UP SUM(self) AS total + )"); + ASSERT_TRUE(rows.ok()) << rows.status().c_message(); + EXPECT_THAT(*rows, testing::ElementsAre("0,NULL,10,100", "1,0,20,60", + "2,0,30,30", "3,1,40,40")); +} + // Perfetto tables are dataframes and are scanned directly. TEST_F(PerfettoSqlConnectionPipelineTest, StartsFromAPerfettoTable) { auto rows = Rows(R"( diff --git a/src/trace_processor/perfetto_sql/parser/perfetto_sql_parser.cc b/src/trace_processor/perfetto_sql/parser/perfetto_sql_parser.cc index dac2c12d178..94a1367c624 100644 --- a/src/trace_processor/perfetto_sql/parser/perfetto_sql_parser.cc +++ b/src/trace_processor/perfetto_sql/parser/perfetto_sql_parser.cc @@ -114,28 +114,21 @@ base::StatusOr BuildReturnType( } // --------------------------------------------------------------------------- -// Macro rewrite tree -> SqlSource +// Rewrite tree -> SqlSource // --------------------------------------------------------------------------- -// Walks the flat list of macro rewrites produced by syntaqlite and, for a -// given AST node, builds a SqlSource whose `Rewriter` structure mirrors the -// nesting of macro calls. This lets SQLite-side error tracebacks resolve -// through macro expansions back to the authored call site. -// -// The flat list has O(N) entries reported in insertion order (outer macros -// before their nested calls). Rather than re-scanning the list for each -// rewrite we materialize a parent -> children adjacency once in the -// constructor, so every subsequent lookup descends the tree in O(subtree). -class MacroRewriteBuilder { +// The macro rewrites syntaqlite recorded for a statement, as a tree, taken +// when it is constructed. Gives the SqlSource of any AST node with them +// applied; the SqlSources nest like the rewrites, so SQLite errors trace back +// through every expansion to where the user wrote the text. +class RewriteTree { public: - // `stmt_doc_offset` is the byte offset of the current statement within - // `stmt`. Syntaqlite v0.5 reports every layer-0 offset (node extents, - // spans, macro call offsets) statement-relative, so call sites that - // slice `stmt` add this offset to translate into document coordinates. - MacroRewriteBuilder(SyntaqliteParser* p, - const SqlSource& stmt, - uint32_t stmt_doc_offset, - const base::FlatHashMap& macros) + // `stmt_doc_offset` is where the statement starts in `stmt`; syntaqlite + // reports offsets relative to the statement. + RewriteTree(SyntaqliteParser* p, + const SqlSource& stmt, + uint32_t stmt_doc_offset, + const base::FlatHashMap& macros) : p_(p), stmt_(stmt), stmt_doc_offset_(stmt_doc_offset), macros_(macros) { uint32_t total = syntaqlite_result_rewrite_count(p_); no_macros_ = (total == 0); @@ -335,7 +328,7 @@ class MacroRewriteBuilder { std::vector source_rooted_; }; -SqlSource NodeSource(const MacroRewriteBuilder& rb, uint32_t node_id) { +SqlSource NodeSource(const RewriteTree& rb, uint32_t node_id) { auto s = rb.NodeSource(node_id); PERFETTO_CHECK(s.has_value()); return std::move(*s); @@ -358,7 +351,7 @@ uint32_t CurrentStatementDocOffset(SyntaqliteParser* p) { base::StatusOr CompilePipeline( SyntaqliteParser* p, - const MacroRewriteBuilder& rb, + const RewriteTree& rb, const pipeline::Catalog* catalog, uint32_t pipeline_id) { if (!catalog) { @@ -373,7 +366,7 @@ base::StatusOr CompilePipeline( base::StatusOr ParseCreateTableBody( SyntaqliteParser* p, - const MacroRewriteBuilder& rb, + const RewriteTree& rb, const pipeline::Catalog* catalog, const SyntaqliteCreatePerfettoTableStmt& n) { using Body = PerfettoSqlParser::CreateTable::Body; @@ -386,7 +379,7 @@ base::StatusOr ParseCreateTableBody( base::StatusOr ParseCreateTable( SyntaqliteParser* p, - const MacroRewriteBuilder& rb, + const RewriteTree& rb, const pipeline::Catalog* catalog, const SyntaqliteCreatePerfettoTableStmt& n) { if (syntaqlite_node_is_present(n.table_impl)) { @@ -409,7 +402,7 @@ base::StatusOr ParseCreateTable( base::StatusOr ParseCreateView( SyntaqliteParser* p, - const MacroRewriteBuilder& rb, + const RewriteTree& rb, const SyntaqliteCreatePerfettoViewStmt& n) { ASSIGN_OR_RETURN(auto schema, BuildArgDefs(p, n.schema)); std::string name = SpanText(p, n.view_name); @@ -427,7 +420,7 @@ base::StatusOr ParseCreateView( base::StatusOr ParseCreateFunction( SyntaqliteParser* p, - const MacroRewriteBuilder& rb, + const RewriteTree& rb, const SyntaqliteCreatePerfettoFunctionStmt& n) { ASSIGN_OR_RETURN(auto args, BuildArgDefs(p, n.args)); for (const auto& arg : args) { @@ -547,7 +540,7 @@ Statement ParseCreateMacro(SyntaqliteParser* p, } base::StatusOr ParseStatement(SyntaqliteParser* p, - const MacroRewriteBuilder& rb, + const RewriteTree& rb, const SqlSource& stmt, uint32_t stmt_doc_offset, const pipeline::Catalog* catalog, @@ -734,7 +727,7 @@ bool PerfettoSqlParser::Impl::Next( // source; callers that slice `stmt` add it back on. uint32_t stmt_doc_offset = CurrentStatementDocOffset(synq); - MacroRewriteBuilder rb(synq, stmt, stmt_doc_offset, macros); + RewriteTree rb(synq, stmt, stmt_doc_offset, macros); auto root_src = rb.NodeSource(root); out_statement_sql = root_src.has_value() ? *std::move(root_src) : stmt; diff --git a/src/trace_processor/perfetto_sql/pipeline/compiler.cc b/src/trace_processor/perfetto_sql/pipeline/compiler.cc index 9b7fced40ce..942b2a32cc7 100644 --- a/src/trace_processor/perfetto_sql/pipeline/compiler.cc +++ b/src/trace_processor/perfetto_sql/pipeline/compiler.cc @@ -82,9 +82,8 @@ class Compiler { Compiler(SyntaqliteParser* p, const NodeSourceFn& source, const Catalog& c) : p_(p), source_(source), catalog_(c) {} - base::Status CompileSource(uint32_t from); - base::Status CompileIntersection(uint32_t node); - base::Status CompileStage(uint32_t stage); + // Compiles the pipeline at `pipeline`: its source and each of its stages. + base::Status CompilePipeline(uint32_t pipeline); LogicalPlan Finish(); private: @@ -104,7 +103,23 @@ class Compiler { std::string name; std::vector columns; }; + // What names mean while a pipeline is compiled. + struct Scope { + // The operator being compiled, which prefixes every error. + const char* op = "FROM"; + // The row as of the stage being compiled, and the table aliases in scope. + std::vector row; + std::vector aliases; + }; + // A relation a source reads: the plan node producing it and its columns. + struct Relation { + PlanNodeId node = 0; + std::vector columns; + }; + base::Status CompileSource(uint32_t from); + base::Status CompileIntersection(uint32_t node); + base::Status CompileStage(uint32_t stage); base::Status CompileSelect(uint32_t stage); base::Status CompileExtend(uint32_t stage); base::Status CompileDrop(uint32_t stage); @@ -118,7 +133,7 @@ class Compiler { // Appends a column to the row. void Append(NamedColumn column, uint32_t node, bool qualified_only = false) { - row_.push_back({std::move(column), op_, node, qualified_only}); + scope_.row.push_back({std::move(column), scope_.op, node, qualified_only}); } const Alias* FindAlias(const std::string& name) const; // Forgets the table alias `name`, if there is one. @@ -137,10 +152,13 @@ class Compiler { // The name a source's columns can be qualified with, if it has one. std::optional SourceQualifier( const SyntaqlitePerfettoPipeSource&) const; + // Compiles what a source reads: a dataframe or SQL. + base::StatusOr CompileRelation(uint32_t source); // Fails unless every column of a source has a name a pipeline can use. // Crossing from SQL into a pipeline needs proper names, as creating a // PERFETTO TABLE does. - base::Status CheckSourceNames(const op::Scan&, uint32_t at) const; + base::Status CheckSourceNames(const std::vector&, + uint32_t at) const; op::Scan CompileDataframeSource(const dataframe::Dataframe&, std::string name); base::StatusOr CompileSqlSource(uint32_t from); @@ -185,17 +203,13 @@ class Compiler { const NodeSourceFn& source_; const Catalog& catalog_; LogicalPlan plan_; - // The operator being compiled, which prefixes every error. - const char* op_ = "FROM"; - // The row as of the stage being compiled, and the table aliases in scope. - std::vector row_; - std::vector aliases_; + Scope scope_; }; -// The column of `scan` named `name`, or nothing when it has none. -std::optional FindScanColumn(const op::Scan& scan, - const std::string& name) { - for (const NamedColumn& column : scan.columns) { +// The column of a relation named `name`, or nothing when it has none. +std::optional FindColumn(const std::vector& columns, + const std::string& name) { + for (const NamedColumn& column : columns) { if (base::CaseInsensitiveEqual(column.name, name)) { return column.id; } @@ -204,7 +218,7 @@ std::optional FindScanColumn(const op::Scan& scan, } base::Status Compiler::CompileIntersection(uint32_t node) { - op_ = "INTERVAL INTERSECTION"; + scope_.op = "INTERVAL INTERSECTION"; const auto* n = Node(p_, node); const auto* list = Node(p_, n->operands); uint32_t count = syntaqlite_list_count(list); @@ -237,16 +251,9 @@ base::Status Compiler::CompileIntersection(uint32_t node) { if (FindAlias(*alias)) { return Expected(source_id, "a different alias for each relation"); } - op::Scan scan; - if (const dataframe::Dataframe* dataframe = FindDirectDataframe(*source)) { - scan = - CompileDataframeSource(*dataframe, SpanText(p_, source->table_name)); - } else { - ASSIGN_OR_RETURN(scan, CompileSqlSource(source_id)); - } - RETURN_IF_ERROR(CheckSourceNames(scan, source_id)); - std::optional ts = FindScanColumn(scan, "ts"); - std::optional dur = FindScanColumn(scan, "dur"); + ASSIGN_OR_RETURN(Relation relation, CompileRelation(source_id)); + std::optional ts = FindColumn(relation.columns, "ts"); + std::optional dur = FindColumn(relation.columns, "dur"); if (!ts || !dur) { return Expected(source_id, *alias + " to have a " + (ts ? "dur" : "ts") + " column"); @@ -258,7 +265,7 @@ base::Status Compiler::CompileIntersection(uint32_t node) { uint32_t col_id = syntaqlite_list_child_id(per, k); const auto* col = Node(p_, col_id); std::string name = SpanText(p_, col->name); - std::optional key = FindScanColumn(scan, name); + std::optional key = FindColumn(relation.columns, name); if (!key) { return Expected(col_id, *alias + " to have a " + name + " column"); } @@ -268,15 +275,15 @@ base::Status Compiler::CompileIntersection(uint32_t node) { // A PER column holds the same value in every operand, so the first // operand's is also the one a bare name finds. Alias operand_alias{*alias, {}}; - for (const NamedColumn& column : scan.columns) { + for (const NamedColumn& column : relation.columns) { bool is_key = std::find(operand.keys.begin(), operand.keys.end(), column.id) != operand.keys.end(); Append(column, source_id, /*qualified_only=*/!(is_key && i == 0)); - operand_alias.columns.push_back(row_.back()); + operand_alias.columns.push_back(scope_.row.back()); operand.carried.push_back(column.id); } - aliases_.push_back(std::move(operand_alias)); - children.push_back(plan_.AddNode(std::move(scan))); + scope_.aliases.push_back(std::move(operand_alias)); + children.push_back(relation.node); isect.operands.push_back(std::move(operand)); } plan_.AddNode(std::move(isect), std::move(children)); @@ -285,21 +292,29 @@ base::Status Compiler::CompileIntersection(uint32_t node) { base::Status Compiler::CompileSource(uint32_t from) { const auto* n = Node(p_, from); + ASSIGN_OR_RETURN(Relation relation, CompileRelation(from)); + for (NamedColumn& column : relation.columns) { + Append(std::move(column), from); + } + if (std::optional qualifier = SourceQualifier(*n)) { + scope_.aliases.push_back({std::move(*qualifier), scope_.row}); + } + return base::OkStatus(); +} + +base::StatusOr Compiler::CompileRelation(uint32_t source) { + const auto* n = Node(p_, source); op::Scan scan; if (const dataframe::Dataframe* dataframe = FindDirectDataframe(*n)) { scan = CompileDataframeSource(*dataframe, SpanText(p_, n->table_name)); } else { - ASSIGN_OR_RETURN(scan, CompileSqlSource(from)); - } - RETURN_IF_ERROR(CheckSourceNames(scan, from)); - for (const NamedColumn& column : scan.columns) { - Append(column, from); + ASSIGN_OR_RETURN(scan, CompileSqlSource(source)); } - if (std::optional qualifier = SourceQualifier(*n)) { - aliases_.push_back({std::move(*qualifier), row_}); - } - plan_.AddNode(std::move(scan)); - return base::OkStatus(); + RETURN_IF_ERROR(CheckSourceNames(scan.columns, source)); + Relation relation; + relation.columns = scan.columns; + relation.node = plan_.AddNode(std::move(scan)); + return relation; } const dataframe::Dataframe* Compiler::FindDirectDataframe( @@ -330,19 +345,18 @@ std::optional Compiler::SourceQualifier( return std::nullopt; } -base::Status Compiler::CheckSourceNames(const op::Scan& scan, +base::Status Compiler::CheckSourceNames(const std::vector& columns, uint32_t at) const { - for (size_t i = 0; i < scan.columns.size(); ++i) { + for (size_t i = 0; i < columns.size(); ++i) { for (size_t j = 0; j < i; ++j) { - if (base::CaseInsensitiveEqual(scan.columns[j].name, - scan.columns[i].name)) { + if (base::CaseInsensitiveEqual(columns[j].name, columns[i].name)) { return Expected(at, "distinct column names, but there are two named '" + - scan.columns[j].name + "'"); + columns[j].name + "'"); } } } - for (size_t i = 0; i < scan.columns.size(); ++i) { - const std::string& name = scan.columns[i].name; + for (size_t i = 0; i < columns.size(); ++i) { + const std::string& name = columns[i].name; // An expression is only named by its alias. if (name.empty()) { return Expected(at, "every column to have a name, but column " + @@ -390,7 +404,7 @@ base::StatusOr Compiler::CompileSqlSource(uint32_t from) { base::StatusOr lineage = syntaqlite_node_is_present(n->select) ? analyzer.AnalyzeQuery({p_, n->select}) - : analyzer.AnalyzeRelation(SpanText(p_, n->table_name)); + : analyzer.AnalyzeRelation({p_, from}, SpanText(p_, n->table_name)); if (!lineage.ok()) { return Err(from, Error::kUnsupported, "reading a relation whose columns cannot be worked out", @@ -457,7 +471,7 @@ base::Status Compiler::Err(uint32_t at, {"no such table alias: '", "'"}, }; const Shape& shape = kShapes[static_cast(error)]; - return base::ErrStatus("%s%s: %s%.*s%s%s", Traceback(at).c_str(), op_, + return base::ErrStatus("%s%s: %s%.*s%s%s", Traceback(at).c_str(), scope_.op, shape.prefix, static_cast(what.size()), what.data(), shape.suffix, detail.c_str()); } @@ -465,7 +479,7 @@ base::Status Compiler::Err(uint32_t at, std::string Compiler::Origin(const RowColumn& column) const { // A qualified name is something the user can write to pick this candidate. const std::string& name = column.column.name; - for (const Alias& alias : aliases_) { + for (const Alias& alias : scope_.aliases) { for (const RowColumn& aliased : alias.columns) { if (aliased.column.id == column.column.id && base::CaseInsensitiveEqual(aliased.column.name, name)) { @@ -493,7 +507,7 @@ std::string Compiler::AmbiguousCandidates( } const Compiler::Alias* Compiler::FindAlias(const std::string& name) const { - for (const Alias& alias : aliases_) { + for (const Alias& alias : scope_.aliases) { if (base::CaseInsensitiveEqual(alias.name, name)) { return &alias; } @@ -502,12 +516,12 @@ const Compiler::Alias* Compiler::FindAlias(const std::string& name) const { } void Compiler::RemoveAlias(const std::string& name) { - aliases_.erase(std::remove_if(aliases_.begin(), aliases_.end(), - [&](const Alias& alias) { - return base::CaseInsensitiveEqual(alias.name, - name); - }), - aliases_.end()); + scope_.aliases.erase( + std::remove_if(scope_.aliases.begin(), scope_.aliases.end(), + [&](const Alias& alias) { + return base::CaseInsensitiveEqual(alias.name, name); + }), + scope_.aliases.end()); } base::Status Compiler::NoSuchColumn(const std::string& name, @@ -522,10 +536,10 @@ base::StatusOr Compiler::FindInRow(const std::string& name, uint32_t at) const { std::vector matches; size_t found = 0; - for (size_t i = 0; i < row_.size(); ++i) { - if (!row_[i].qualified_only && - base::CaseInsensitiveEqual(row_[i].column.name, name)) { - matches.push_back(&row_[i]); + for (size_t i = 0; i < scope_.row.size(); ++i) { + if (!scope_.row[i].qualified_only && + base::CaseInsensitiveEqual(scope_.row[i].column.name, name)) { + matches.push_back(&scope_.row[i]); found = i; } } @@ -555,7 +569,7 @@ base::StatusOr Compiler::Resolve(const std::string& qualifier, uint32_t at) const { if (qualifier.empty()) { ASSIGN_OR_RETURN(size_t i, FindInRow(name, at)); - return row_[i].column.id; + return scope_.row[i].column.id; } std::string full_name = qualifier + "." + name; const Alias* alias = FindAlias(qualifier); @@ -628,7 +642,7 @@ base::StatusOr Compiler::CompileSum(uint32_t agg_id, uint32_t expr) { } base::Status Compiler::CompileTreeAccumulate(uint32_t stage) { - op_ = "TREE ACCUMULATE"; + scope_.op = "TREE ACCUMULATE"; const auto* n = Node(p_, stage); op::TreeAccumulate acc; @@ -677,10 +691,10 @@ base::StatusOr> Compiler::ExpandStar( columns = alias->columns; } else { // EXTEND only takes `alias.*`: a bare star would repeat the whole row. - if (std::string_view(op_) == "EXTEND") { + if (std::string_view(scope_.op) == "EXTEND") { return Expected(star_id, "a table alias before the star, as in `t.*`"); } - columns = row_; + columns = scope_.row; } if (syntaqlite_node_is_present(star->except)) { @@ -732,7 +746,7 @@ base::StatusOr> Compiler::ExpandStar( IsPresent(item->qualifier) ? SpanText(p_, item->qualifier) : ""; ASSIGN_OR_RETURN(ColumnId value, Resolve(qualifier, SpanText(p_, item->name), item_id)); - *matches.front() = {{std::move(target), value}, op_, item_id}; + *matches.front() = {{std::move(target), value}, scope_.op, item_id}; } } @@ -763,34 +777,34 @@ base::StatusOr> Compiler::CompileItems( if (IsPresent(item->alias)) { name = SpanText(p_, item->alias); } - columns.push_back({{std::move(name), id}, op_, item_id}); + columns.push_back({{std::move(name), id}, scope_.op, item_id}); } return columns; } // Replaces the row. What it leaves is a new table: no alias reaches into it. base::Status Compiler::CompileSelect(uint32_t stage) { - op_ = "SELECT"; + scope_.op = "SELECT"; const auto* n = Node(p_, stage); - ASSIGN_OR_RETURN(row_, CompileItems(n->columns)); - aliases_.clear(); + ASSIGN_OR_RETURN(scope_.row, CompileItems(n->columns)); + scope_.aliases.clear(); return base::OkStatus(); } // Adds columns to the row. Items see only the row before the stage, not each // other. base::Status Compiler::CompileExtend(uint32_t stage) { - op_ = "EXTEND"; + scope_.op = "EXTEND"; const auto* n = Node(p_, stage); ASSIGN_OR_RETURN(std::vector columns, CompileItems(n->columns)); - row_.insert(row_.end(), columns.begin(), columns.end()); + scope_.row.insert(scope_.row.end(), columns.begin(), columns.end()); return base::OkStatus(); } // Removes every column of each name. Aliases still reach the dropped columns, // except an alias of the same name, which the name now hides. base::Status Compiler::CompileDrop(uint32_t stage) { - op_ = "DROP"; + scope_.op = "DROP"; const auto* n = Node(p_, stage); const auto* list = Node(p_, n->columns); std::vector names; @@ -799,25 +813,26 @@ base::Status Compiler::CompileDrop(uint32_t stage) { std::string name = SpanText(p_, Node(p_, item_id)->name); RETURN_IF_ERROR(CheckListedOnce(names, name, item_id)); - bool found = std::any_of(row_.begin(), row_.end(), [&](const auto& c) { - return !c.qualified_only && - base::CaseInsensitiveEqual(c.column.name, name); - }); + bool found = + std::any_of(scope_.row.begin(), scope_.row.end(), [&](const auto& c) { + return !c.qualified_only && + base::CaseInsensitiveEqual(c.column.name, name); + }); if (!found) { return NoSuchColumn(name, item_id); } } for (const std::string& name : names) { - row_.erase(std::remove_if(row_.begin(), row_.end(), - [&](const RowColumn& c) { - return !c.qualified_only && - base::CaseInsensitiveEqual(c.column.name, - name); - }), - row_.end()); + scope_.row.erase(std::remove_if(scope_.row.begin(), scope_.row.end(), + [&](const RowColumn& c) { + return !c.qualified_only && + base::CaseInsensitiveEqual( + c.column.name, name); + }), + scope_.row.end()); RemoveAlias(name); } - if (row_.empty()) { + if (scope_.row.empty()) { return Expected(stage, "a column to be left after DROP"); } return base::OkStatus(); @@ -827,7 +842,7 @@ base::Status Compiler::CompileDrop(uint32_t stage) { // renames happen at once, so two columns can swap names. Aliases still reach // the columns under their old names. base::Status Compiler::CompileRename(uint32_t stage) { - op_ = "RENAME"; + scope_.op = "RENAME"; const auto* n = Node(p_, stage); const auto* list = Node(p_, n->columns); std::vector seen; @@ -842,9 +857,9 @@ base::Status Compiler::CompileRename(uint32_t stage) { } for (const auto& [at, item_id] : renames) { const auto* item = Node(p_, item_id); - row_[at].column.name = SpanText(p_, item->alias); - row_[at].op = op_; - row_[at].node = item_id; + scope_.row[at].column.name = SpanText(p_, item->alias); + scope_.row[at].op = scope_.op; + scope_.row[at].node = item_id; } return base::OkStatus(); } @@ -854,7 +869,7 @@ base::Status Compiler::CompileRename(uint32_t stage) { // still reach the old values, except an alias of the same name, which the // name now hides. base::Status Compiler::CompileSet(uint32_t stage) { - op_ = "SET"; + scope_.op = "SET"; const auto* n = Node(p_, stage); const auto* list = Node(p_, n->items); std::vector seen; @@ -878,9 +893,9 @@ base::Status Compiler::CompileSet(uint32_t stage) { assignments.push_back({at, id, item_id}); } for (const Assignment& assignment : assignments) { - RowColumn& column = row_[assignment.at]; + RowColumn& column = scope_.row[assignment.at]; column.column.id = assignment.value; - column.op = op_; + column.op = scope_.op; column.node = assignment.node; RemoveAlias(column.column.name); } @@ -889,16 +904,34 @@ base::Status Compiler::CompileSet(uint32_t stage) { // Replaces every alias with one covering the whole row as it is now. base::Status Compiler::CompileAs(uint32_t stage) { - op_ = "AS"; + scope_.op = "AS"; const auto* n = Node(p_, stage); - aliases_.clear(); - aliases_.push_back({SpanText(p_, n->alias), row_}); + scope_.aliases.clear(); + scope_.aliases.push_back({SpanText(p_, n->alias), scope_.row}); + return base::OkStatus(); +} + +base::Status Compiler::CompilePipeline(uint32_t pipeline) { + const auto& n = Node(p_, pipeline)->perfetto_pipeline; + if (syntaqlite_node_is_present(n.intersection)) { + RETURN_IF_ERROR(CompileIntersection(n.intersection)); + } else { + RETURN_IF_ERROR(CompileSource(n.from)); + } + if (!syntaqlite_node_is_present(n.stages)) { + return base::OkStatus(); + } + const auto* stages = Node(p_, n.stages); + uint32_t count = syntaqlite_list_count(stages); + for (uint32_t i = 0; i < count; i++) { + RETURN_IF_ERROR(CompileStage(syntaqlite_list_child_id(stages, i))); + } return base::OkStatus(); } LogicalPlan Compiler::Finish() { plan_.output.clear(); - for (const RowColumn& column : row_) { + for (const RowColumn& column : scope_.row) { plan_.output.push_back(column.column); } return std::move(plan_); @@ -910,21 +943,8 @@ base::StatusOr Compile(SyntaqliteParser* p, uint32_t pipeline, const NodeSourceFn& source, const Catalog& catalog) { - const auto& n = Node(p, pipeline)->perfetto_pipeline; Compiler compiler(p, source, catalog); - if (syntaqlite_node_is_present(n.intersection)) { - RETURN_IF_ERROR(compiler.CompileIntersection(n.intersection)); - } else { - RETURN_IF_ERROR(compiler.CompileSource(n.from)); - } - if (syntaqlite_node_is_present(n.stages)) { - const auto* stages = Node(p, n.stages); - uint32_t count = syntaqlite_list_count(stages); - for (uint32_t i = 0; i < count; i++) { - RETURN_IF_ERROR( - compiler.CompileStage(syntaqlite_list_child_id(stages, i))); - } - } + RETURN_IF_ERROR(compiler.CompilePipeline(pipeline)); LogicalPlan plan = compiler.Finish(); PruneColumns(plan); return plan;