@@ -175,6 +175,9 @@ class GeneratorImpl {
175175 base::StatusOr<std::string> Union (
176176 const StructuredQuery::ExperimentalUnion::Decoder&);
177177
178+ base::StatusOr<std::string> AddColumns (
179+ const StructuredQuery::ExperimentalAddColumns::Decoder&);
180+
178181 // Filtering.
179182 static base::StatusOr<std::string> Filters (RepeatedProto filters);
180183
@@ -263,6 +266,11 @@ base::StatusOr<std::string> GeneratorImpl::GenerateImpl() {
263266 StructuredQuery::ExperimentalUnion::Decoder union_decoder (
264267 q.experimental_union ());
265268 ASSIGN_OR_RETURN (source, Union (union_decoder));
269+
270+ } else if (q.has_experimental_add_columns ()) {
271+ StructuredQuery::ExperimentalAddColumns::Decoder add_columns_decoder (
272+ q.experimental_add_columns ());
273+ ASSIGN_OR_RETURN (source, AddColumns (add_columns_decoder));
266274 } else if (q.has_sql ()) {
267275 StructuredQuery::Sql::Decoder sql_source (q.sql ());
268276 ASSIGN_OR_RETURN (source, SqlSource (sql_source));
@@ -575,6 +583,103 @@ base::StatusOr<std::string> GeneratorImpl::Union(
575583 return sql;
576584}
577585
586+ base::StatusOr<std::string> GeneratorImpl::AddColumns (
587+ const StructuredQuery::ExperimentalAddColumns::Decoder& add_columns) {
588+ // Validate required fields
589+ if (!add_columns.has_core_query ()) {
590+ return base::ErrStatus (" AddColumns must specify a core query" );
591+ }
592+ if (!add_columns.has_input_query ()) {
593+ return base::ErrStatus (" AddColumns must specify an input query" );
594+ }
595+ if (!add_columns.has_equality_columns () &&
596+ !add_columns.has_freeform_condition ()) {
597+ return base::ErrStatus (
598+ " AddColumns must specify either equality_columns or "
599+ " freeform_condition" );
600+ }
601+
602+ // Validate input_columns
603+ auto input_columns = add_columns.input_columns ();
604+ if (!input_columns) {
605+ return base::ErrStatus (" AddColumns must specify at least one input column" );
606+ }
607+ size_t column_count = 0 ;
608+ for (auto it = input_columns; it; ++it) {
609+ column_count++;
610+ }
611+ if (column_count == 0 ) {
612+ return base::ErrStatus (" AddColumns must specify at least one input column" );
613+ }
614+
615+ // Generate nested sources
616+ std::string core_table = NestedSource (add_columns.core_query ());
617+ std::string input_table = NestedSource (add_columns.input_query ());
618+
619+ // Build the SELECT clause with all core columns plus input columns
620+ std::string select_clause = " core.*" ;
621+ for (auto it = add_columns.input_columns (); it; ++it) {
622+ protozero::ConstChars col_name (*it);
623+ if (col_name.size == 0 ) {
624+ return base::ErrStatus (" Input column name cannot be empty" );
625+ }
626+ select_clause += " , input." + col_name.ToStdString ();
627+ }
628+
629+ // Build the join condition
630+ std::string condition;
631+ if (add_columns.has_equality_columns ()) {
632+ StructuredQuery::ExperimentalJoin::EqualityColumns::Decoder eq_cols (
633+ add_columns.equality_columns ());
634+ if (!eq_cols.has_left_column ()) {
635+ return base::ErrStatus (" EqualityColumns must specify a left column" );
636+ }
637+ if (!eq_cols.has_right_column ()) {
638+ return base::ErrStatus (" EqualityColumns must specify a right column" );
639+ }
640+ condition = " core." + eq_cols.left_column ().ToStdString () + " = input." +
641+ eq_cols.right_column ().ToStdString ();
642+ } else {
643+ StructuredQuery::ExperimentalJoin::FreeformCondition::Decoder free_cond (
644+ add_columns.freeform_condition ());
645+ if (!free_cond.has_left_query_alias ()) {
646+ return base::ErrStatus (
647+ " FreeformCondition must specify a left query alias" );
648+ }
649+ if (!free_cond.has_right_query_alias ()) {
650+ return base::ErrStatus (
651+ " FreeformCondition must specify a right query alias" );
652+ }
653+ if (!free_cond.has_sql_expression ()) {
654+ return base::ErrStatus (" FreeformCondition must specify a sql expression" );
655+ }
656+
657+ std::string left_alias = free_cond.left_query_alias ().ToStdString ();
658+ std::string right_alias = free_cond.right_query_alias ().ToStdString ();
659+
660+ // Validate that aliases match "core" and "input"
661+ if (left_alias != " core" ) {
662+ return base::ErrStatus (
663+ " FreeformCondition left_query_alias must be 'core', got '%s'" ,
664+ left_alias.c_str ());
665+ }
666+ if (right_alias != " input" ) {
667+ return base::ErrStatus (
668+ " FreeformCondition right_query_alias must be 'input', got '%s'" ,
669+ right_alias.c_str ());
670+ }
671+
672+ condition = free_cond.sql_expression ().ToStdString ();
673+ }
674+
675+ // Generate the final SQL using LEFT JOIN to keep all core rows
676+ std::string sql = " (SELECT " + select_clause + " FROM " + core_table +
677+ " AS core LEFT JOIN " + input_table + " AS input ON " +
678+ condition + " )" ;
679+
680+ return sql;
681+ }
682+
578683base::StatusOr<std::string> GeneratorImpl::ReferencedSharedQuery (
579684 protozero::ConstChars raw_id) {
580685 std::string id = raw_id.ToStdString ();
0 commit comments