Skip to content

feat(spp_metric_service): breakdown expansion + SQL column support (re-land from #76)#279

Open
gonzalesedwin1123 wants to merge 2 commits into
reland/cel-domainfrom
reland/metric-service
Open

feat(spp_metric_service): breakdown expansion + SQL column support (re-land from #76)#279
gonzalesedwin1123 wants to merge 2 commits into
reland/cel-domainfrom
reland/metric-service

Conversation

@gonzalesedwin1123

Copy link
Copy Markdown
Member

Re-lands the spp_metric_service portion of reverted PR #76 (revert: #271). Wave 2 — stacked on #275 (spp_cel_domain); merge that first. The base is set to reland/cel-domain so this diff shows only spp_metric_service; GitHub retargets to 19.0 when #275 merges.

Summary

  • Demographic breakdown expansion and SQL column support for metric disaggregation (uses the spp_cel_domain SQL CASE compiler).

Verification

…pport (from #76)

Re-lands the spp_metric_service portion of reverted PR #76. Depends on the
spp_cel_domain SQL CASE compiler (#275) — do not merge before it.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces demographic breakdown expansion and SQL column compilation support for metric disaggregation. Key changes include updating model names, refining the age group dimension to align with UNICEF/WHO buckets, automatically expanding groups to members for individual-level dimensions, and adding a to_sql_column method to compile dimensions into SQL expressions. The reviewer feedback focuses on performance and code quality optimizations, such as using recordset subtraction instead of lambda filtering, batch-fetching membership IDs to prevent N+1 queries, checking _fields on Odoo recordsets instead of using hasattr, guarding against falsy values early, and removing redundant SQL parameter wrapping.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +125 to +126
group_ids = records.filtered("is_group").ids
individual_ids = set(records.filtered(lambda r: not r.is_group).ids)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using filtered with a lambda function can be slow and is less idiomatic in Odoo. We can use recordset subtraction (records - groups) to get the individual records, which is much faster and cleaner.

Suggested change
group_ids = records.filtered("is_group").ids
individual_ids = set(records.filtered(lambda r: not r.is_group).ids)
groups = records.filtered("is_group")
group_ids = groups.ids
individual_ids = set((records - groups).ids)

Comment on lines +140 to +141
for membership in memberships:
individual_ids.add(membership.individual.id)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Looping over memberships to extract individual IDs one by one can trigger N+1 queries and is inefficient. Since individual is a Many2one field, we can fetch all IDs in a single batch using memberships.individual.ids.

        individual_ids.update(memberships.individual.ids)

# Odoo recordset (Many2one). Empty recordset means the field is unset.
if not value:
return self.default_value or "unknown"
if hasattr(value, "code") and value.code:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using hasattr(value, "code") on an Odoo recordset is inefficient and can be unreliable because it triggers dynamic attribute lookup. It is safer and much faster to check if the field exists in the model's _fields dictionary.

Suggested change
if hasattr(value, "code") and value.code:
if "code" in value._fields and value.code:

:param raw_value: The raw dimension value (typically a code string)
:returns: display_name or None if not found
"""
# Only handle simple field paths (one segment, no dotted traversal

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If raw_value is falsy (e.g., None or False), performing a database search comodel.search([("code", "=", raw_value)]) is unnecessary and inefficient. We should guard against falsy values early in the method.

Suggested change
# Only handle simple field paths (one segment, no dotted traversal
if not raw_value:
return None
# Only handle simple field paths (one segment, no dotted traversal

Comment on lines +353 to +363
result = SQLColumnResult(
expression=SQL(
"CASE WHEN %s.%s = FALSE THEN %s ELSE %s END",
SQL.identifier(alias),
SQL.identifier("is_group"),
result.expression,
SQL("%s", default),
),
joins=result.joins,
alias_counter=result.alias_counter,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Wrapping default in SQL("%s", default) is redundant and inconsistent with how parameters are passed elsewhere in this file. You can pass default directly as a parameter to the parent SQL object.

Suggested change
result = SQLColumnResult(
expression=SQL(
"CASE WHEN %s.%s = FALSE THEN %s ELSE %s END",
SQL.identifier(alias),
SQL.identifier("is_group"),
result.expression,
SQL("%s", default),
),
joins=result.joins,
alias_counter=result.alias_counter,
)
result = SQLColumnResult(
expression=SQL(
"CASE WHEN %s.%s = FALSE THEN %s ELSE %s END",
SQL.identifier(alias),
SQL.identifier("is_group"),
result.expression,
default,
),
joins=result.joins,
alias_counter=result.alias_counter,
)

Comment on lines +365 to +375
result = SQLColumnResult(
expression=SQL(
"CASE WHEN %s.%s = TRUE THEN %s ELSE %s END",
SQL.identifier(alias),
SQL.identifier("is_group"),
result.expression,
SQL("%s", default),
),
joins=result.joins,
alias_counter=result.alias_counter,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Wrapping default in SQL("%s", default) is redundant and inconsistent with how parameters are passed elsewhere in this file. You can pass default directly as a parameter to the parent SQL object.

Suggested change
result = SQLColumnResult(
expression=SQL(
"CASE WHEN %s.%s = TRUE THEN %s ELSE %s END",
SQL.identifier(alias),
SQL.identifier("is_group"),
result.expression,
SQL("%s", default),
),
joins=result.joins,
alias_counter=result.alias_counter,
)
result = SQLColumnResult(
expression=SQL(
"CASE WHEN %s.%s = TRUE THEN %s ELSE %s END",
SQL.identifier(alias),
SQL.identifier("is_group"),
result.expression,
default,
),
joins=result.joins,
alias_counter=result.alias_counter,
)

- recordset subtraction instead of lambda filter for individuals
- batch individual ids from memberships (avoids N+1)
- check _fields membership instead of hasattr on recordsets
- early return for falsy raw_value before code search
@gonzalesedwin1123

Copy link
Copy Markdown
Member Author

gemini-code-assist disposition:

Applied (commit 778e037): recordset subtraction for individuals, batched memberships.individual.ids, _fields membership check instead of hasattr, early return for falsy raw_value. Suite: 161/0 on the integration state with #275.

Deferred: the two SQL("%s", default) wrapping comments — both forms parameterize identically with Odoo's SQL wrapper; cosmetic, and I'd rather keep the re-land diff byte-stable. Happy to fold in if reviewers prefer.

@gemini-code-assist

Copy link
Copy Markdown

Thank you for the detailed disposition. The changes you've implemented, particularly the shift to _fields checks and the batching of memberships.individual.ids, are solid improvements for performance and robustness in an Odoo environment. Deferring the cosmetic SQL wrapping is also a sensible decision to maintain diff stability. Everything looks well-aligned with the requirements.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant