Merge "tp: migrate RuntimeTableFunction away from SqliteTable" into main
diff --git a/src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.cc b/src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.cc
index 266245c..4d514ab 100644
--- a/src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.cc
+++ b/src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.cc
@@ -180,9 +180,10 @@
     PERFETTO_FATAL("Failed to initialize perfetto_tables: %s", errmsg_raw);
   }
 
-  engine_->RegisterVirtualTableModule<RuntimeTableFunction>(
-      "runtime_table_function", this,
-      SqliteTableLegacy::TableType::kExplicitCreate, false);
+  auto ctx = std::make_unique<RuntimeTableFunctionModule::Context>();
+  runtime_table_fn_context_ = ctx.get();
+  engine_->RegisterVirtualTableModule<RuntimeTableFunctionModule>(
+      "runtime_table_function", std::move(ctx));
   auto context = std::make_unique<DbSqliteTable::Context>(
       [this](const std::string& name) {
         auto* table = runtime_tables_.Find(name);
@@ -202,7 +203,6 @@
   // Destroying the sqlite engine should also destroy all the created table
   // functions.
   engine_.reset();
-  PERFETTO_CHECK(runtime_table_fn_states_.size() == 0);
   PERFETTO_CHECK(runtime_tables_.size() == 0);
 }
 
@@ -620,8 +620,9 @@
                                    cf.sql);
   }
 
-  std::unique_ptr<RuntimeTableFunction::State> state(
-      new RuntimeTableFunction::State{cf.sql, cf.prototype, {}, std::nullopt});
+  auto state = std::make_unique<RuntimeTableFunctionModule::State>(
+      RuntimeTableFunctionModule::State{
+          this, cf.sql, cf.prototype, {}, std::nullopt});
 
   // Parse the return type into a enum format.
   {
@@ -699,36 +700,44 @@
           state->return_values[i].name().c_str());
     }
   }
-  state->reusable_stmt = std::move(stmt);
+  state->temporary_create_stmt = std::move(stmt);
 
   // TODO(lalitm): this suffers the same non-atomic DROP/CREATE problem as
   // CREATE PERFETTO TABLE implementation above: see the comment there for
   // more info on this.
-  std::string fn_name = state->prototype.function_name;
-  std::string lower_name = base::ToLower(state->prototype.function_name);
-  if (runtime_table_fn_states_.Find(lower_name)) {
-    if (!cf.replace) {
-      return base::ErrStatus("Table function named %s already exists",
-                             state->prototype.function_name.c_str());
-    }
-    // This will cause |OnTableFunctionDestroyed| below to be executed.
-    base::StackString<1024> drop("DROP TABLE %s",
+  if (cf.replace) {
+    base::StackString<1024> drop("DROP TABLE IF EXISTS %s",
                                  state->prototype.function_name.c_str());
     auto res = Execute(
         SqlSource::FromTraceProcessorImplementation(drop.ToStdString()));
     RETURN_IF_ERROR(res.status());
   }
 
-  auto it_and_inserted =
-      runtime_table_fn_states_.Insert(lower_name, std::move(state));
-  PERFETTO_CHECK(it_and_inserted.second);
-
   base::StackString<1024> create(
-      "CREATE VIRTUAL TABLE %s USING runtime_table_function", fn_name.c_str());
-  return Execute(cf.sql.RewriteAllIgnoreExisting(
-                     SqlSource::FromTraceProcessorImplementation(
-                         create.ToStdString())))
-      .status();
+      "CREATE VIRTUAL TABLE %s USING runtime_table_function",
+      state->prototype.function_name.c_str());
+
+  // Make sure we didn't accidentally leak a state from a previous function
+  // creation.
+  PERFETTO_CHECK(!runtime_table_fn_context_->temporary_create_state);
+
+  // Move the state into the context so that it will be picked up in xCreate
+  // of RuntimeTableFunctionModule.
+  runtime_table_fn_context_->temporary_create_state = std::move(state);
+  auto status = Execute(cf.sql.RewriteAllIgnoreExisting(
+                            SqlSource::FromTraceProcessorImplementation(
+                                create.ToStdString())))
+                    .status();
+
+  // If an error happened, it's possible that the state was not picked up.
+  // Therefore, always reset the state just in case. OTOH if the creation
+  // succeeded, the state should always have been captured.
+  if (status.ok()) {
+    PERFETTO_CHECK(!runtime_table_fn_context_->temporary_create_state);
+  } else {
+    runtime_table_fn_context_->temporary_create_state.reset();
+  }
+  return status;
 }
 
 base::Status PerfettoSqlEngine::ExecuteCreateMacro(
@@ -779,18 +788,6 @@
   return base::OkStatus();
 }
 
-RuntimeTableFunction::State* PerfettoSqlEngine::GetRuntimeTableFunctionState(
-    const std::string& name) const {
-  auto* it = runtime_table_fn_states_.Find(base::ToLower(name));
-  PERFETTO_CHECK(it);
-  return it->get();
-}
-
-void PerfettoSqlEngine::OnRuntimeTableFunctionDestroyed(
-    const std::string& name) {
-  PERFETTO_CHECK(runtime_table_fn_states_.Erase(base::ToLower(name)));
-}
-
 base::StatusOr<std::vector<std::string>>
 PerfettoSqlEngine::GetColumnNamesFromSelectStatement(
     const SqliteEngine::PreparedStatement& stmt,
diff --git a/src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.h b/src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.h
index ef5358d..cb7930b 100644
--- a/src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.h
+++ b/src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.h
@@ -172,13 +172,6 @@
   // Registers a trace processor C++ table function with SQLite.
   void RegisterStaticTableFunction(std::unique_ptr<StaticTableFunction> fn);
 
-  // Returns the state for the given table function.
-  RuntimeTableFunction::State* GetRuntimeTableFunctionState(
-      const std::string&) const;
-
-  // Should be called when a table function is destroyed.
-  void OnRuntimeTableFunctionDestroyed(const std::string&);
-
   SqliteEngine* sqlite_engine() { return engine_.get(); }
 
   // Makes new SQL module available to import.
@@ -276,8 +269,7 @@
   uint64_t static_window_function_count_ = 0;
   uint64_t runtime_function_count_ = 0;
 
-  base::FlatHashMap<std::string, std::unique_ptr<RuntimeTableFunction::State>>
-      runtime_table_fn_states_;
+  RuntimeTableFunctionModule::Context* runtime_table_fn_context_ = nullptr;
   base::FlatHashMap<std::string, const Table*> static_tables_;
   base::FlatHashMap<std::string, std::unique_ptr<RuntimeTable>> runtime_tables_;
   base::FlatHashMap<std::string, sql_modules::RegisteredModule> modules_;
diff --git a/src/trace_processor/perfetto_sql/engine/runtime_table_function.cc b/src/trace_processor/perfetto_sql/engine/runtime_table_function.cc
index 3d0af93..12faa70 100644
--- a/src/trace_processor/perfetto_sql/engine/runtime_table_function.cc
+++ b/src/trace_processor/perfetto_sql/engine/runtime_table_function.cc
@@ -16,15 +16,28 @@
 
 #include "src/trace_processor/perfetto_sql/engine/runtime_table_function.h"
 
+#include <sqlite3.h>
+#include <cstddef>
+#include <cstdint>
+#include <memory>
 #include <optional>
+#include <string>
 #include <utility>
+#include <vector>
 
+#include "perfetto/base/logging.h"
+#include "perfetto/base/status.h"
+#include "perfetto/ext/base/string_utils.h"
+#include "perfetto/public/compiler.h"
+#include "src/trace_processor/perfetto_sql/engine/function_util.h"
 #include "src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.h"
 #include "src/trace_processor/sqlite/bindings/sqlite_result.h"
-#include "src/trace_processor/util/status_macros.h"
+#include "src/trace_processor/sqlite/module_lifecycle_manager.h"
+#include "src/trace_processor/sqlite/sqlite_utils.h"
+#include "src/trace_processor/tp_metatrace.h"
+#include "src/trace_processor/util/sql_argument.h"
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 
 namespace {
 
@@ -33,217 +46,226 @@
   sqlite3_clear_bindings(stmt);
 }
 
-}  // namespace
-
-RuntimeTableFunction::RuntimeTableFunction(sqlite3*, PerfettoSqlEngine* engine)
-    : engine_(engine) {}
-
-RuntimeTableFunction::~RuntimeTableFunction() {
-  engine_->OnRuntimeTableFunctionDestroyed(name());
-}
-
-base::Status RuntimeTableFunction::Init(int,
-                                        const char* const*,
-                                        Schema* schema) {
-  state_ = engine_->GetRuntimeTableFunctionState(name());
-
-  // Now we've parsed prototype and return values, create the schema.
-  *schema = CreateSchema();
-  return base::OkStatus();
-}
-
-SqliteTableLegacy::Schema RuntimeTableFunction::CreateSchema() {
-  std::vector<Column> columns;
-  for (size_t i = 0; i < state_->return_values.size(); ++i) {
-    const auto& ret = state_->return_values[i];
-    columns.push_back(Column(columns.size(), ret.name().ToStdString(),
+auto CreateTableStrFromState(RuntimeTableFunctionModule::State* state) {
+  std::vector<std::string> columns;
+  columns.reserve(state->return_values.size());
+  for (const auto& ret : state->return_values) {
+    columns.emplace_back(ret.name().ToStdString() + " " +
+                         sqlite::utils::SqlValueTypeToString(
                              sql_argument::TypeToSqlValueType(ret.type())));
   }
-  for (size_t i = 0; i < state_->prototype.arguments.size(); ++i) {
-    const auto& arg = state_->prototype.arguments[i];
-
+  for (const auto& arg : state->prototype.arguments) {
     // Add the "in_" prefix to every argument param to avoid clashes between the
     // output and input parameters.
-    columns.push_back(Column(columns.size(), "in_" + arg.name().ToStdString(),
-                             sql_argument::TypeToSqlValueType(arg.type()),
-                             true));
+    columns.emplace_back("in_" + arg.name().ToStdString() + " " +
+                         sqlite::utils::SqlValueTypeToString(
+                             sql_argument::TypeToSqlValueType(arg.type())) +
+                         " HIDDEN");
   }
+  columns.emplace_back("_primary_key BIGINT HIDDEN");
 
-  std::vector<size_t> primary_keys;
-
-  // Add the "primary key" column. SQLite requires that we provide a column
-  // which is non-null and unique. Unfortunately, we have no restrictions on
-  // the subqueries so we cannot rely on this constraint being held there.
-  // Therefore, we create a "primary key" column which exists purely for SQLite
-  // primary key purposes and is equal to the row number.
-  columns.push_back(
-      Column(columns.size(), "_primary_key", SqlValue::kLong, true));
-  primary_keys.emplace_back(columns.size() - 1);
-
-  return SqliteTableLegacy::Schema(std::move(columns), std::move(primary_keys));
+  std::string cols = base::Join(columns, ",");
+  return base::StackString<1024>(
+      R"(CREATE TABLE x(%s, PRIMARY KEY(_primary_key)) WITHOUT ROWID)",
+      cols.c_str());
 }
 
-std::unique_ptr<SqliteTableLegacy::BaseCursor>
-RuntimeTableFunction::CreateCursor() {
-  return std::unique_ptr<Cursor>(new Cursor(this, state_));
+}  // namespace
+
+int RuntimeTableFunctionModule::Create(sqlite3* db,
+                                       void* ctx,
+                                       int,
+                                       const char* const* argv,
+                                       sqlite3_vtab** vtab,
+                                       char**) {
+  auto* context = GetContext(ctx);
+  auto state = std::move(context->temporary_create_state);
+
+  auto create_table_str = CreateTableStrFromState(state.get());
+  if (int ret = sqlite3_declare_vtab(db, create_table_str.c_str());
+      ret != SQLITE_OK) {
+    return ret;
+  }
+  std::unique_ptr<Vtab> res = std::make_unique<Vtab>();
+  res->reusable_stmt = std::move(state->temporary_create_stmt);
+  state->temporary_create_stmt = std::nullopt;
+  res->state = context->manager.OnCreate(argv, std::move(state));
+  *vtab = res.release();
+  return SQLITE_OK;
 }
 
-int RuntimeTableFunction::BestIndex(const QueryConstraints& qc,
-                                    BestIndexInfo* info) {
-  // Only accept constraint sets where every input parameter has a value.
-  size_t seen_argument_constraints = 0;
-  for (size_t i = 0; i < qc.constraints().size(); ++i) {
-    const auto& cs = qc.constraints()[i];
-    seen_argument_constraints +=
-        state_->IsArgumentColumn(static_cast<size_t>(cs.column));
+int RuntimeTableFunctionModule::Destroy(sqlite3_vtab* vtab) {
+  std::unique_ptr<Vtab> tab(GetVtab(vtab));
+  sqlite::ModuleStateManager<RuntimeTableFunctionModule>::OnDestroy(tab->state);
+  return SQLITE_OK;
+}
+
+int RuntimeTableFunctionModule::Connect(sqlite3* db,
+                                        void* ctx,
+                                        int,
+                                        const char* const*,
+                                        sqlite3_vtab** vtab,
+                                        char** argv) {
+  auto* context = GetContext(ctx);
+
+  std::unique_ptr<Vtab> res = std::make_unique<Vtab>();
+  res->state = context->manager.OnConnect(argv);
+
+  auto create_table_str = CreateTableStrFromState(
+      sqlite::ModuleStateManager<RuntimeTableFunctionModule>::GetState(
+          res->state));
+  if (int ret = sqlite3_declare_vtab(db, create_table_str.c_str());
+      ret != SQLITE_OK) {
+    // If the registration happens to fail, make sure to disconnect the state
+    // again.
+    sqlite::ModuleStateManager<RuntimeTableFunctionModule>::OnDisconnect(
+        res->state);
+    return ret;
   }
-  if (seen_argument_constraints < state_->prototype.arguments.size())
+  *vtab = res.release();
+  return SQLITE_OK;
+}
+
+int RuntimeTableFunctionModule::Disconnect(sqlite3_vtab* vtab) {
+  std::unique_ptr<Vtab> tab(GetVtab(vtab));
+  sqlite::ModuleStateManager<RuntimeTableFunctionModule>::OnDisconnect(
+      tab->state);
+  return SQLITE_OK;
+}
+
+int RuntimeTableFunctionModule::BestIndex(sqlite3_vtab* tab,
+                                          sqlite3_index_info* info) {
+  auto* t = GetVtab(tab);
+  auto* s = sqlite::ModuleStateManager<RuntimeTableFunctionModule>::GetState(
+      t->state);
+
+  // Don't deal with any constraints on the output parameters for simplicty.
+  // TODO(lalitm): reconsider this decision to allow more efficient queries:
+  // we would need to wrap the query in a SELECT * FROM (...) WHERE constraint
+  // like we do for SPAN JOIN.
+  base::Status status = sqlite::utils::ValidateFunctionArguments(
+      info, s->prototype.arguments.size(),
+      [s](size_t c) { return s->IsArgumentColumn(c); });
+  if (!status.ok()) {
     return SQLITE_CONSTRAINT;
-
-  for (size_t i = 0; i < info->sqlite_omit_constraint.size(); ++i) {
-    size_t col = static_cast<size_t>(qc.constraints()[i].column);
-    if (state_->IsArgumentColumn(col)) {
-      info->sqlite_omit_constraint[i] = true;
-    }
   }
   return SQLITE_OK;
 }
 
-RuntimeTableFunction::Cursor::Cursor(RuntimeTableFunction* table, State* state)
-    : SqliteTableLegacy::BaseCursor(table), table_(table), state_(state) {
-  if (state->reusable_stmt) {
-    stmt_ = std::move(state->reusable_stmt);
-    state->reusable_stmt = std::nullopt;
-    return_stmt_to_state_ = true;
+int RuntimeTableFunctionModule::Open(sqlite3_vtab* tab,
+                                     sqlite3_vtab_cursor** cursor) {
+  auto* t = GetVtab(tab);
+  std::unique_ptr<Cursor> c = std::make_unique<Cursor>();
+  if (t->reusable_stmt) {
+    c->stmt = std::move(t->reusable_stmt);
+    t->reusable_stmt = std::nullopt;
   }
+  *cursor = c.release();
+  return SQLITE_OK;
 }
 
-RuntimeTableFunction::Cursor::~Cursor() {
-  if (return_stmt_to_state_) {
-    ResetStatement(stmt_->sqlite_stmt());
-    state_->reusable_stmt = std::move(stmt_);
+int RuntimeTableFunctionModule::Close(sqlite3_vtab_cursor* cursor) {
+  std::unique_ptr<Cursor> c(GetCursor(cursor));
+  auto* t = GetVtab(c->pVtab);
+  if (!t->reusable_stmt && c->stmt) {
+    ResetStatement(c->stmt->sqlite_stmt());
+    t->reusable_stmt = std::move(c->stmt);
   }
+  return SQLITE_OK;
 }
 
-base::Status RuntimeTableFunction::Cursor::Filter(const QueryConstraints& qc,
-                                                  sqlite3_value** argv,
-                                                  FilterHistory) {
+int RuntimeTableFunctionModule::Filter(sqlite3_vtab_cursor* cur,
+                                       int,
+                                       const char*,
+                                       int argc,
+                                       sqlite3_value** argv) {
+  auto* c = GetCursor(cur);
+  auto* t = GetVtab(cur->pVtab);
+  auto* s = sqlite::ModuleStateManager<RuntimeTableFunctionModule>::GetState(
+      t->state);
+
+  PERFETTO_CHECK(static_cast<size_t>(argc) == s->prototype.arguments.size());
   PERFETTO_TP_TRACE(metatrace::Category::FUNCTION_CALL, "TABLE_FUNCTION_CALL",
-                    [this](metatrace::Record* r) {
-                      r->AddArg("Function",
-                                state_->prototype.function_name.c_str());
+                    [s](metatrace::Record* r) {
+                      r->AddArg("Function", s->prototype.function_name.c_str());
                     });
 
-  auto col_to_arg_idx = [this](int col) {
-    return static_cast<uint32_t>(col) -
-           static_cast<uint32_t>(state_->return_values.size());
-  };
-
-  size_t seen_argument_constraints = 0;
-  for (size_t i = 0; i < qc.constraints().size(); ++i) {
-    const auto& cs = qc.constraints()[i];
-
-    // Only consider argument columns (i.e. input parameters) as we're
-    // delegating the rest to SQLite.
-    if (!state_->IsArgumentColumn(static_cast<size_t>(cs.column)))
-      continue;
-
-    // We only support equality constraints as we're expecting "input arguments"
-    // to our "function".
-    if (!sqlite::utils::IsOpEq(cs.op)) {
-      return base::ErrStatus("%s: non-equality constraint passed",
-                             state_->prototype.function_name.c_str());
-    }
-
-    const auto& arg = state_->prototype.arguments[col_to_arg_idx(cs.column)];
-    base::Status status = sqlite::utils::TypeCheckSqliteValue(
-        argv[i], sql_argument::TypeToSqlValueType(arg.type()),
-        sql_argument::TypeToHumanFriendlyString(arg.type()));
-    if (!status.ok()) {
-      return base::ErrStatus("%s: argument %s (index %zu) %s",
-                             state_->prototype.function_name.c_str(),
-                             arg.name().c_str(), i, status.c_message());
-    }
-
-    seen_argument_constraints++;
-  }
-
-  // Verify that we saw one valid constraint for every input argument.
-  if (seen_argument_constraints < state_->prototype.arguments.size()) {
-    return base::ErrStatus(
-        "%s: missing value for input argument. Saw %zu arguments but expected "
-        "%zu",
-        state_->prototype.function_name.c_str(), seen_argument_constraints,
-        state_->prototype.arguments.size());
-  }
-
   // Prepare the SQL definition as a statement using SQLite.
   // TODO(lalitm): measure and implement whether it would be a good idea to
   // forward constraints here when we build the nested query.
-  if (stmt_) {
+  if (c->stmt) {
     // Filter can be called multiple times for the same cursor, so if we
     // already have a statement, reset and reuse it. Otherwise, create a
     // new one.
-    ResetStatement(stmt_->sqlite_stmt());
+    ResetStatement(c->stmt->sqlite_stmt());
   } else {
-    auto stmt = table_->engine_->sqlite_engine()->PrepareStatement(
-        state_->sql_defn_str);
-    RETURN_IF_ERROR(stmt.status());
-    stmt_ = std::move(stmt);
+    auto stmt = s->engine->sqlite_engine()->PrepareStatement(s->sql_defn_str);
+    c->stmt = std::move(stmt);
+    if (const auto& status = c->stmt->status(); !status.ok()) {
+      return sqlite::utils::SetError(t, status.c_message());
+    }
   }
 
   // Bind all the arguments to the appropriate places in the function.
-  for (size_t i = 0; i < qc.constraints().size(); ++i) {
-    const auto& cs = qc.constraints()[i];
-
-    // Don't deal with any constraints on the output parameters for simplicty.
-    // TODO(lalitm): reconsider this decision to allow more efficient queries:
-    // we would need to wrap the query in a SELECT * FROM (...) WHERE constraint
-    // like we do for SPAN JOIN.
-    if (!state_->IsArgumentColumn(static_cast<size_t>(cs.column)))
-      continue;
-
-    uint32_t index = col_to_arg_idx(cs.column);
-    PERFETTO_DCHECK(index < state_->prototype.arguments.size());
-
-    const auto& arg = state_->prototype.arguments[index];
-    auto status = MaybeBindArgument(
-        stmt_->sqlite_stmt(), state_->prototype.function_name, arg, argv[i]);
-    RETURN_IF_ERROR(status);
+  for (uint32_t i = 0; i < static_cast<uint32_t>(argc); ++i) {
+    const auto& arg = s->prototype.arguments[i];
+    base::Status status = MaybeBindArgument(
+        c->stmt->sqlite_stmt(), s->prototype.function_name, arg, argv[i]);
+    if (!status.ok()) {
+      return sqlite::utils::SetError(t, status.c_message());
+    }
   }
 
   // Reset the next call count - this is necessary because the same cursor
   // can be used for multiple filter operations.
-  next_call_count_ = 0;
-  return Next();
+  c->next_call_count = 0;
+  return Next(cur);
 }
 
-base::Status RuntimeTableFunction::Cursor::Next() {
-  is_eof_ = !stmt_->Step();
-  next_call_count_++;
-  return stmt_->status();
+int RuntimeTableFunctionModule::Next(sqlite3_vtab_cursor* cur) {
+  auto* c = GetCursor(cur);
+  c->is_eof = !c->stmt->Step();
+  c->next_call_count++;
+  if (const auto& status = c->stmt->status(); !status.ok()) {
+    return sqlite::utils::SetError(cur->pVtab, status.c_message());
+  }
+  return SQLITE_OK;
 }
 
-bool RuntimeTableFunction::Cursor::Eof() {
-  return is_eof_;
+int RuntimeTableFunctionModule::Eof(sqlite3_vtab_cursor* cur) {
+  return GetCursor(cur)->is_eof;
 }
 
-base::Status RuntimeTableFunction::Cursor::Column(sqlite3_context* ctx, int i) {
-  size_t idx = static_cast<size_t>(i);
-  if (state_->IsReturnValueColumn(idx)) {
-    sqlite::result::Value(ctx, sqlite3_column_value(stmt_->sqlite_stmt(), i));
-  } else if (state_->IsArgumentColumn(idx)) {
+int RuntimeTableFunctionModule::Column(sqlite3_vtab_cursor* cur,
+                                       sqlite3_context* ctx,
+                                       int N) {
+  auto* c = GetCursor(cur);
+  auto* t = GetVtab(cur->pVtab);
+  auto* s = sqlite::ModuleStateManager<RuntimeTableFunctionModule>::GetState(
+      t->state);
+
+  auto idx = static_cast<size_t>(N);
+  if (PERFETTO_LIKELY(s->IsReturnValueColumn(idx))) {
+    sqlite::result::Value(ctx, sqlite3_column_value(c->stmt->sqlite_stmt(), N));
+    return SQLITE_OK;
+  }
+
+  if (PERFETTO_LIKELY(s->IsArgumentColumn(idx))) {
     // TODO(lalitm): it may be more appropriate to keep a note of the arguments
     // which we passed in and return them here. Not doing this to because it
     // doesn't seem necessary for any useful thing but something which may need
     // to be changed in the future.
     sqlite::result::Null(ctx);
-  } else {
-    PERFETTO_DCHECK(state_->IsPrimaryKeyColumn(idx));
-    sqlite::result::Long(ctx, next_call_count_);
+    return SQLITE_OK;
   }
-  return base::OkStatus();
+
+  PERFETTO_DCHECK(s->IsPrimaryKeyColumn(idx));
+  sqlite::result::Long(ctx, c->next_call_count);
+  return SQLITE_OK;
 }
 
-}  // namespace trace_processor
-}  // namespace perfetto
+int RuntimeTableFunctionModule::Rowid(sqlite3_vtab_cursor*, sqlite_int64*) {
+  return SQLITE_ERROR;
+}
+
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/perfetto_sql/engine/runtime_table_function.h b/src/trace_processor/perfetto_sql/engine/runtime_table_function.h
index 80edf72..a027e02 100644
--- a/src/trace_processor/perfetto_sql/engine/runtime_table_function.h
+++ b/src/trace_processor/perfetto_sql/engine/runtime_table_function.h
@@ -17,31 +17,37 @@
 #ifndef SRC_TRACE_PROCESSOR_PERFETTO_SQL_ENGINE_RUNTIME_TABLE_FUNCTION_H_
 #define SRC_TRACE_PROCESSOR_PERFETTO_SQL_ENGINE_RUNTIME_TABLE_FUNCTION_H_
 
+#include <cstddef>
+#include <cstdint>
+#include <memory>
 #include <optional>
+#include <string>
+#include <vector>
 
+#include "perfetto/base/logging.h"
 #include "src/trace_processor/perfetto_sql/engine/function_util.h"
+#include "src/trace_processor/sqlite/bindings/sqlite_module.h"
+#include "src/trace_processor/sqlite/module_lifecycle_manager.h"
+#include "src/trace_processor/sqlite/sql_source.h"
 #include "src/trace_processor/sqlite/sqlite_engine.h"
+#include "src/trace_processor/util/sql_argument.h"
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 
 class PerfettoSqlEngine;
 
 // The implementation of the SqliteTableLegacy interface for table functions
 // defined at runtime using SQL.
-class RuntimeTableFunction final
-    : public TypedSqliteTable<RuntimeTableFunction, PerfettoSqlEngine*> {
- public:
-  // The state of this function. This is separated from |RuntimeTableFunction|
-  // because |RuntimeTableFunction| is owned by Sqlite while |State| is owned by
-  // PerfettoSqlEngine.
+struct RuntimeTableFunctionModule
+    : public sqlite::Module<RuntimeTableFunctionModule> {
   struct State {
+    PerfettoSqlEngine* engine;
     SqlSource sql_defn_str;
 
     FunctionPrototype prototype;
     std::vector<sql_argument::ArgumentDefinition> return_values;
 
-    std::optional<SqliteEngine::PreparedStatement> reusable_stmt;
+    std::optional<SqliteEngine::PreparedStatement> temporary_create_stmt;
 
     bool IsReturnValueColumn(size_t i) const {
       PERFETTO_DCHECK(i < TotalColumnCount());
@@ -65,44 +71,55 @@
              kPrimaryKeyColumns;
     }
   };
-  class Cursor final : public SqliteTableLegacy::BaseCursor {
-   public:
-    explicit Cursor(RuntimeTableFunction* table, State* state);
-    ~Cursor() final;
-
-    base::Status Filter(const QueryConstraints& qc,
-                        sqlite3_value**,
-                        FilterHistory);
-    base::Status Next();
-    bool Eof();
-    base::Status Column(sqlite3_context* context, int N);
-
-   private:
-    RuntimeTableFunction* table_ = nullptr;
-    State* state_ = nullptr;
-
-    std::optional<SqliteEngine::PreparedStatement> stmt_;
-    bool return_stmt_to_state_ = false;
-
-    bool is_eof_ = false;
-    int next_call_count_ = 0;
+  struct Context {
+    std::unique_ptr<State> temporary_create_state;
+    sqlite::ModuleStateManager<RuntimeTableFunctionModule> manager;
+  };
+  struct Vtab : sqlite::Module<RuntimeTableFunctionModule>::Vtab {
+    sqlite::ModuleStateManager<RuntimeTableFunctionModule>::PerVtabState* state;
+    std::optional<SqliteEngine::PreparedStatement> reusable_stmt;
+  };
+  struct Cursor : sqlite::Module<RuntimeTableFunctionModule>::Cursor {
+    std::optional<SqliteEngine::PreparedStatement> stmt;
+    bool is_eof = false;
+    int next_call_count = 0;
   };
 
-  RuntimeTableFunction(sqlite3*, PerfettoSqlEngine*);
-  ~RuntimeTableFunction() final;
+  static constexpr bool kSupportsWrites = false;
+  static constexpr bool kDoesOverloadFunctions = false;
 
-  base::Status Init(int argc, const char* const* argv, Schema*) final;
-  std::unique_ptr<SqliteTableLegacy::BaseCursor> CreateCursor() final;
-  int BestIndex(const QueryConstraints& qc, BestIndexInfo* info) final;
+  static int Create(sqlite3*,
+                    void*,
+                    int,
+                    const char* const*,
+                    sqlite3_vtab**,
+                    char**);
+  static int Destroy(sqlite3_vtab*);
 
- private:
-  Schema CreateSchema();
+  static int Connect(sqlite3*,
+                     void*,
+                     int,
+                     const char* const*,
+                     sqlite3_vtab**,
+                     char**);
+  static int Disconnect(sqlite3_vtab*);
 
-  PerfettoSqlEngine* engine_ = nullptr;
-  State* state_ = nullptr;
+  static int BestIndex(sqlite3_vtab*, sqlite3_index_info*);
+
+  static int Open(sqlite3_vtab*, sqlite3_vtab_cursor**);
+  static int Close(sqlite3_vtab_cursor*);
+
+  static int Filter(sqlite3_vtab_cursor*,
+                    int,
+                    const char*,
+                    int,
+                    sqlite3_value**);
+  static int Next(sqlite3_vtab_cursor*);
+  static int Eof(sqlite3_vtab_cursor*);
+  static int Column(sqlite3_vtab_cursor*, sqlite3_context*, int);
+  static int Rowid(sqlite3_vtab_cursor*, sqlite_int64*);
 };
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
 
 #endif  // SRC_TRACE_PROCESSOR_PERFETTO_SQL_ENGINE_RUNTIME_TABLE_FUNCTION_H_
diff --git a/src/trace_processor/sqlite/sqlite_utils.h b/src/trace_processor/sqlite/sqlite_utils.h
index ba1eed6..bb95e2e 100644
--- a/src/trace_processor/sqlite/sqlite_utils.h
+++ b/src/trace_processor/sqlite/sqlite_utils.h
@@ -18,10 +18,13 @@
 #define SRC_TRACE_PROCESSOR_SQLITE_SQLITE_UTILS_H_
 
 #include <sqlite3.h>
+#include <algorithm>
 #include <bitset>
 #include <cstddef>
 #include <cstdint>
 #include <cstring>
+#include <functional>
+#include <initializer_list>
 #include <optional>
 #include <string>
 #include <utility>
@@ -156,6 +159,54 @@
                                 status.c_message()));
 }
 
+// For a given |sqlite3_index_info| struct received in a BestIndex call, returns
+// whether all |arg_count| arguments (with |is_arg_column| indicating whether a
+// given column is a function argument) have exactly one equaltiy constraint
+// associated with them.
+//
+// If so, the associated constraint is omitted and the argvIndex is mapped to
+// the corresponding argument's index.
+inline base::Status ValidateFunctionArguments(
+    sqlite3_index_info* info,
+    size_t arg_count,
+    const std::function<bool(size_t)>& is_arg_column) {
+  std::vector<bool> present;
+  size_t present_count = 0;
+  for (int i = 0; i < info->nConstraint; ++i) {
+    const auto& in = info->aConstraint[i];
+    if (!in.usable) {
+      continue;
+    }
+    auto cs_col = static_cast<size_t>(in.iColumn);
+    if (!is_arg_column(cs_col)) {
+      continue;
+    }
+    if (!IsOpEq(in.op)) {
+      return base::ErrStatus(
+          "Unexpected non equality constraints for column %zu", cs_col);
+    }
+    if (cs_col >= present.size()) {
+      present.resize(cs_col + 1);
+    }
+    if (present[cs_col]) {
+      return base::ErrStatus("Unexpected multiple constraints for column %zu",
+                             cs_col);
+    }
+    present[cs_col] = true;
+    present_count++;
+
+    auto& out = info->aConstraintUsage[i];
+    out.argvIndex = static_cast<int>(present_count);
+    out.omit = true;
+  }
+  if (present_count != arg_count) {
+    return base::ErrStatus(
+        "Unexpected missing argument: expected %zu, actual %zu", arg_count,
+        present_count);
+  }
+  return base::OkStatus();
+}
+
 // Converts the given SqlValue type to the type string SQLite understands.
 inline std::string SqlValueTypeToString(SqlValue::Type type) {
   switch (type) {