LCOV - code coverage report
Current view: top level - engine/duckdb/transpiler - transpiler_emit_attribution_composition_test.cc (source / functions) Coverage Total Hit
Test: _coverage_report.dat Lines: 84.4 % 173 146
Test Date: 2026-08-08 08:38:15 Functions: 100.0 % 5 5

            Line data    Source code
       1              : #include "backend/engine/duckdb/transpiler/transpiler_test_fixture.h"
       2              : 
       3              : // R17: multi-CTE attribution SELECT / INSERT...SELECT / CTAS with
       4              : // COALESCE(SUM) + ROW_NUMBER over joins must bind in DuckDB (no stale
       5              : // `__bq_j_<id>` across CTE / LEFT JOIN / analytic boundaries).
       6              : 
       7              : namespace bigquery_emulator {
       8              : namespace backend {
       9              : namespace engine {
      10              : namespace duckdb {
      11              : namespace transpiler {
      12              : 
      13              : class TranspilerAttributionCompositionTest : public TranspilerBindFixture {
      14              :  protected:
      15            4 :   void SetUp() override {
      16            4 :     TranspilerBindFixture::SetUp();
      17              : 
      18              :     // R17 attribution shape (bench/cases/attribution_insert_10k.yaml).
      19            4 :     auto activity_events = std::make_unique<::googlesql::SimpleTable>(
      20            4 :         "activity_events",
      21            4 :         std::vector<::googlesql::SimpleTable::NameAndType>{
      22            4 :             {"tenant_id", type_factory_->get_string()},
      23            4 :             {"activity_id", type_factory_->get_string()},
      24            4 :             {"user_public_id", type_factory_->get_string()},
      25            4 :             {"action_type", type_factory_->get_string()},
      26            4 :             {"channel", type_factory_->get_string()},
      27            4 :             {"occurred_at", type_factory_->get_timestamp()},
      28            4 :             {"spend", type_factory_->get_double()},
      29            4 :         });
      30            4 :     catalog_->AddOwnedTable(std::move(activity_events));
      31              : 
      32            4 :     auto attr_users = std::make_unique<::googlesql::SimpleTable>(
      33            4 :         "attr_users",
      34            4 :         std::vector<::googlesql::SimpleTable::NameAndType>{
      35            4 :             {"id", type_factory_->get_int64()},
      36            4 :             {"public_id", type_factory_->get_string()},
      37            4 :         });
      38            4 :     catalog_->AddOwnedTable(std::move(attr_users));
      39              : 
      40            4 :     auto attr_transactions = std::make_unique<::googlesql::SimpleTable>(
      41            4 :         "attr_transactions",
      42            4 :         std::vector<::googlesql::SimpleTable::NameAndType>{
      43            4 :             {"id", type_factory_->get_int64()},
      44            4 :             {"user_id", type_factory_->get_int64()},
      45            4 :             {"total_amount", type_factory_->get_double()},
      46            4 :             {"total_reversed", type_factory_->get_double()},
      47            4 :             {"is_deleted", type_factory_->get_bool()},
      48            4 :             {"status", type_factory_->get_string()},
      49            4 :             {"source_created_at", type_factory_->get_timestamp()},
      50            4 :         });
      51            4 :     catalog_->AddOwnedTable(std::move(attr_transactions));
      52              : 
      53            4 :     auto attr_summary = std::make_unique<::googlesql::SimpleTable>(
      54            4 :         "attr_summary",
      55            4 :         std::vector<::googlesql::SimpleTable::NameAndType>{
      56            4 :             {"activity_id", type_factory_->get_string()},
      57            4 :             {"total_transactions", type_factory_->get_int64()},
      58            4 :             {"gross_value", type_factory_->get_double()},
      59            4 :         });
      60            4 :     catalog_->AddOwnedTable(std::move(attr_summary));
      61              : 
      62            4 :     ExecDdl(
      63            4 :         "CREATE TABLE activity_events (tenant_id VARCHAR, activity_id "
      64            4 :         "VARCHAR, user_public_id VARCHAR, action_type VARCHAR, channel "
      65            4 :         "VARCHAR, occurred_at TIMESTAMPTZ, spend DOUBLE)");
      66            4 :     ExecDdl("CREATE TABLE attr_users (id BIGINT, public_id VARCHAR)");
      67            4 :     ExecDdl(
      68            4 :         "CREATE TABLE attr_transactions (id BIGINT, user_id BIGINT, "
      69            4 :         "total_amount DOUBLE, total_reversed DOUBLE, is_deleted BOOLEAN, "
      70            4 :         "status VARCHAR, source_created_at TIMESTAMPTZ)");
      71            4 :     ExecDdl(
      72            4 :         "CREATE TABLE attr_summary (activity_id VARCHAR, "
      73            4 :         "total_transactions BIGINT, gross_value DOUBLE)");
      74            4 :   }
      75              : 
      76              :   // Compact R17 attribution body: multi-CTE + COALESCE(SUM) + JOIN +
      77              :   // ROW_NUMBER + LEFT JOIN + final COALESCE(SUM). Matches the bench
      78              :   // case shape that previously stayed on semantic_executor.
      79              :   static constexpr const char kAttributionSelectBody[] = R"sql(
      80              : WITH ActivityLogs AS (
      81              :   SELECT tenant_id, activity_id, user_public_id, action_type, channel,
      82              :          occurred_at, spend
      83              :   FROM activity_events
      84              : ),
      85              : MetricStats AS (
      86              :   SELECT
      87              :     tenant_id,
      88              :     activity_id,
      89              :     COUNT(DISTINCT IF(action_type = 'action_1', user_public_id, NULL))
      90              :       AS metric_a,
      91              :     COUNTIF(action_type = 'action_3') AS metric_c_total,
      92              :     COALESCE(SUM(spend), 0) AS total_spend
      93              :   FROM ActivityLogs
      94              :   GROUP BY tenant_id, activity_id
      95              : ),
      96              : ValidActivityDispatches AS (
      97              :   SELECT user_public_id, activity_id, channel, occurred_at AS dispatched_at
      98              :   FROM ActivityLogs
      99              :   WHERE action_type = 'action_3'
     100              : ),
     101              : LatestTransactions AS (
     102              :   SELECT * FROM attr_transactions
     103              : ),
     104              : AttributedTransactionsRaw AS (
     105              :   SELECT
     106              :     logs.activity_id,
     107              :     logs.channel,
     108              :     txn.id AS txn_id,
     109              :     txn.total_amount,
     110              :     COALESCE(txn.total_reversed, 0) AS reversed_amount,
     111              :     ROW_NUMBER() OVER (
     112              :       PARTITION BY txn.id ORDER BY logs.dispatched_at DESC
     113              :     ) AS attribution_rank
     114              :   FROM ValidActivityDispatches logs
     115              :   JOIN attr_users users ON logs.user_public_id = users.public_id
     116              :   JOIN LatestTransactions txn ON users.id = txn.user_id
     117              :   WHERE txn.source_created_at > logs.dispatched_at
     118              :     AND txn.source_created_at <= TIMESTAMP_ADD(
     119              :       logs.dispatched_at, INTERVAL 7 DAY
     120              :     )
     121              :     AND COALESCE(txn.is_deleted, FALSE) = FALSE
     122              :     AND txn.status NOT IN ('status_x', 'status_y')
     123              : ),
     124              : ValueStats AS (
     125              :   SELECT
     126              :     activity_id,
     127              :     COUNT(txn_id) AS total_transactions,
     128              :     COALESCE(SUM(total_amount), 0) AS gross_value
     129              :   FROM AttributedTransactionsRaw
     130              :   WHERE attribution_rank = 1
     131              :   GROUP BY activity_id
     132              : ),
     133              : Attribution AS (
     134              :   SELECT
     135              :     p.activity_id AS id,
     136              :     COALESCE(r.total_transactions, 0) AS total_transactions,
     137              :     COALESCE(r.gross_value, 0) AS gross_value
     138              :   FROM MetricStats p
     139              :   LEFT JOIN ValueStats r ON p.activity_id = r.activity_id
     140              : )
     141              : )sql";
     142              : };
     143              : 
     144              : // R17 follow-up: full multi-CTE attribution SELECT with COALESCE(SUM)
     145              : // must bind (no stale `__bq_j_<id>` across CTE / LEFT JOIN boundaries).
     146            1 : TEST_F(TranspilerAttributionCompositionTest, AttributionMultiCteSelectBinds) {
     147            1 :   const std::string sql =
     148            1 :       absl::StrCat(kAttributionSelectBody,
     149            1 :                    "SELECT COUNT(*) AS activity_rows, "
     150            1 :                    "COALESCE(SUM(total_transactions), 0) AS txn_sum, "
     151            1 :                    "COALESCE(SUM(gross_value), 0) AS gross_sum "
     152            1 :                    "FROM Attribution");
     153            1 :   AssertSqlTranspileBinds(sql);
     154            1 : }
     155              : 
     156              : // R17 follow-up: compact INSERT...SELECT (single CTE + ROW_NUMBER over
     157              : // joins + COALESCE(SUM)) must bind — this is the conformance fixture
     158              : // shape that previously stayed on semantic_executor.
     159              : TEST_F(TranspilerAttributionCompositionTest,
     160            1 :        AttributionCompactInsertSelectBinds) {
     161            1 :   static constexpr const char kCompactSql[] = R"sql(
     162            1 : INSERT INTO attr_summary (activity_id, total_transactions, gross_value)
     163            1 : WITH Attributed AS (
     164            1 :   SELECT
     165            1 :     logs.activity_id,
     166            1 :     txn.id AS txn_id,
     167            1 :     txn.total_amount,
     168            1 :     ROW_NUMBER() OVER (
     169            1 :       PARTITION BY txn.id ORDER BY logs.occurred_at DESC
     170            1 :     ) AS attribution_rank
     171            1 :   FROM activity_events logs
     172            1 :   JOIN attr_users users ON logs.user_public_id = users.public_id
     173            1 :   JOIN attr_transactions txn ON users.id = txn.user_id
     174            1 :   WHERE txn.source_created_at > logs.occurred_at
     175            1 :     AND txn.source_created_at <= TIMESTAMP_ADD(
     176            1 :       logs.occurred_at, INTERVAL 7 DAY
     177            1 :     )
     178            1 :     AND txn.status NOT IN ('status_x', 'status_y')
     179            1 : )
     180            1 : SELECT
     181            1 :   activity_id,
     182            1 :   COUNT(txn_id) AS total_transactions,
     183            1 :   COALESCE(SUM(total_amount), 0) AS gross_value
     184            1 : FROM Attributed
     185            1 : WHERE attribution_rank = 1
     186            1 : GROUP BY activity_id
     187            1 : )sql";
     188            1 :   const ::googlesql::ResolvedStatement* stmt = Analyze(kCompactSql);
     189            1 :   ASSERT_NE(stmt, nullptr);
     190            1 :   ASSERT_EQ(stmt->node_kind(), ::googlesql::RESOLVED_INSERT_STMT);
     191            1 :   TestTranspiler t;
     192            1 :   const std::string emitted =
     193            1 :       t.EmitInsertSelect(stmt->GetAs<::googlesql::ResolvedInsertStmt>());
     194            2 :   ASSERT_FALSE(emitted.empty()) << "EmitInsertSelect returned empty for:\n"
     195            2 :                                 << kCompactSql;
     196            1 :   SCOPED_TRACE(emitted);
     197            1 :   ::duckdb_result result{};
     198            1 :   const auto rc = ::duckdb_query(conn_, emitted.c_str(), &result);
     199            1 :   if (rc != ::DuckDBSuccess) {
     200            0 :     const char* err = ::duckdb_result_error(&result);
     201            0 :     FAIL() << "DuckDB rejected EmitInsertSelect SQL\n"
     202            0 :            << "source_sql:\n"
     203            0 :            << kCompactSql << "\n"
     204            0 :            << "emitted_sql:\n"
     205            0 :            << emitted << "\n"
     206            0 :            << "duckdb_error:\n"
     207            0 :            << (err == nullptr ? "(null)" : err);
     208            0 :   }
     209            1 :   ::duckdb_destroy_result(&result);
     210            1 : }
     211              : 
     212              : // R17 follow-up: INSERT...SELECT materialization of the attribution
     213              : // body must bind (EmitInsertSelect clears join-alias flags).
     214            1 : TEST_F(TranspilerAttributionCompositionTest, AttributionInsertSelectBinds) {
     215            1 :   const std::string sql = absl::StrCat(
     216            1 :       "INSERT INTO attr_summary "
     217            1 :       "(activity_id, total_transactions, gross_value) ",
     218            1 :       kAttributionSelectBody,
     219            1 :       "SELECT id, total_transactions, gross_value FROM Attribution");
     220            1 :   const ::googlesql::ResolvedStatement* stmt = Analyze(sql);
     221            1 :   ASSERT_NE(stmt, nullptr);
     222            1 :   ASSERT_EQ(stmt->node_kind(), ::googlesql::RESOLVED_INSERT_STMT);
     223            1 :   TestTranspiler t;
     224            1 :   const std::string emitted =
     225            1 :       t.EmitInsertSelect(stmt->GetAs<::googlesql::ResolvedInsertStmt>());
     226            2 :   ASSERT_FALSE(emitted.empty()) << "EmitInsertSelect returned empty for:\n"
     227            2 :                                 << sql;
     228            1 :   SCOPED_TRACE(emitted);
     229            1 :   ::duckdb_result result{};
     230            1 :   const auto rc = ::duckdb_query(conn_, emitted.c_str(), &result);
     231            1 :   if (rc != ::DuckDBSuccess) {
     232            0 :     const char* err = ::duckdb_result_error(&result);
     233            0 :     FAIL() << "DuckDB rejected EmitInsertSelect SQL\n"
     234            0 :            << "source_sql:\n"
     235            0 :            << sql << "\n"
     236            0 :            << "emitted_sql:\n"
     237            0 :            << emitted << "\n"
     238            0 :            << "duckdb_error:\n"
     239            0 :            << (err == nullptr ? "(null)" : err);
     240            0 :   }
     241            1 :   ::duckdb_destroy_result(&result);
     242            1 : }
     243              : 
     244              : // R17 follow-up: CTAS materialization of the attribution body must bind.
     245            1 : TEST_F(TranspilerAttributionCompositionTest, AttributionCtasSelectBinds) {
     246            1 :   const std::string sql =
     247            1 :       absl::StrCat("CREATE TABLE attr_summary_ctas AS ",
     248            1 :                    kAttributionSelectBody,
     249            1 :                    "SELECT id AS activity_id, total_transactions, gross_value "
     250            1 :                    "FROM Attribution");
     251            1 :   const ::googlesql::ResolvedStatement* stmt = Analyze(sql);
     252            1 :   ASSERT_NE(stmt, nullptr);
     253            1 :   ASSERT_EQ(stmt->node_kind(),
     254            1 :             ::googlesql::RESOLVED_CREATE_TABLE_AS_SELECT_STMT);
     255            1 :   TestTranspiler t;
     256            1 :   const std::string emitted = t.EmitCtasSelect(
     257            1 :       stmt->GetAs<::googlesql::ResolvedCreateTableAsSelectStmt>());
     258            2 :   ASSERT_FALSE(emitted.empty()) << "EmitCtasSelect returned empty for:\n"
     259            2 :                                 << sql;
     260            1 :   SCOPED_TRACE(emitted);
     261            1 :   ::duckdb_result result{};
     262            1 :   const auto rc = ::duckdb_query(conn_, emitted.c_str(), &result);
     263            1 :   if (rc != ::DuckDBSuccess) {
     264            0 :     const char* err = ::duckdb_result_error(&result);
     265            0 :     FAIL() << "DuckDB rejected EmitCtasSelect SQL\n"
     266            0 :            << "source_sql:\n"
     267            0 :            << sql << "\n"
     268            0 :            << "emitted_sql:\n"
     269            0 :            << emitted << "\n"
     270            0 :            << "duckdb_error:\n"
     271            0 :            << (err == nullptr ? "(null)" : err);
     272            0 :   }
     273            1 :   ::duckdb_destroy_result(&result);
     274            1 : }
     275              : 
     276              : }  // namespace transpiler
     277              : }  // namespace duckdb
     278              : }  // namespace engine
     279              : }  // namespace backend
     280              : }  // namespace bigquery_emulator
        

Generated by: LCOV version 2.0-1