Skip to content

Add backend derivation support - #428

Open
tomanizer wants to merge 2 commits into
devfrom
backend-v1-derivations
Open

tomanizer wants to merge 2 commits into
devfrom
backend-v1-derivations

Conversation

@tomanizer

Copy link
Copy Markdown
Owner

Fixes #414

Summary

  • add backend derivation registry and derivation-aware tuples/cells/members query building
  • return derived output names using default field__derivation aliases or caller overrides
  • translate current frontend derivation labels to stable API ids in the remote adapter

Validation

  • ./.venv-server/bin/ruff check server/
  • ./.venv-server/bin/pytest server/tests -q
  • ./.venv-server/bin/pytest tests/test_backend_benchmark_runner.py -q
  • npm run test:unit
  • npm run lint:js

Copilot AI review requested due to automatic review settings March 11, 2026 10:13

Copilot AI 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.

Pull request overview

Adds backend derivation support across query endpoints, including stable derivation IDs, default/override aliases, and frontend-to-API derivation mapping for the remote adapter.

Changes:

  • Introduces a backend derivation registry/helpers and updates SQL builders to project derived expressions with stable output names.
  • Updates remote adapter query building to include derivation + alias and map UI labels to API derivation IDs.
  • Expands API tests and documentation to cover derivations, validation, and alias behavior.

Reviewed changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
tests/unit/remote-query-adapter.test.js Adds unit coverage for derivation label→id mapping and default aliases in remote queries.
src/DataSource/remote/RemoteQueryAdapter.js Maps derivation labels to stable ids; includes derivation+alias in tuples/cells/picklist query payloads.
src/DataSet/TupleSet.js Uses derived output names (default field__derivation or alias) when building tuple schemas/field lists.
server/tests/test_validation_errors.py Adds validation tests for unknown/incompatible derivations across tuples/cells/members.
server/tests/test_query_tuples_api.py Adds API tests for tuples derivation default alias and alias override.
server/tests/test_query_picklist_api.py Adds API tests for members derivation default alias and alias override.
server/tests/test_query_cells_api.py Adds API tests for cells row derivation default alias and alias override.
server/tests/test_query_builder.py Adds SQL builder assertions verifying derived expressions + aliasing in tuples/picklist.
server/tests/test_derivations.py Adds direct unit tests for derivation registry helpers and error conditions.
server/routers/query.py Threads derivation/alias through axis window fetching and returns derived output names in responses.
server/query_builder.py Centralizes dimension select building, projects derived expressions, and updates tuples/cells/picklist/export SQL generation.
server/models.py Adds alias + derivation normalization to tuple/axis specs; adds derivation/alias fields to picklist models.
server/main.py Maps derivation validation errors to DERIVATION_NOT_SUPPORTED in validation error handler.
server/errors.py Adds DerivationNotSupportedError with consistent API error code/message.
server/derivations.py Introduces backend derivation registry, output naming, quoting, and type-compatibility checks.
docs/server/api-reference.md Documents new derivation + alias request fields and the new DERIVATION_NOT_SUPPORTED error code.

You can also share your feedback on Copilot code review. Take the survey.

Comment thread server/derivations.py
Comment on lines +16 to +26
"month_name": {"template": "CAST(MONTH({expr}) AS UTINYINT)", "kinds": {"date", "timestamp"}},
"month_shortname": {"template": "CAST(MONTH({expr}) AS UTINYINT)", "kinds": {"date", "timestamp"}},
"week_num": {"template": "CAST(WEEK({expr}) AS UTINYINT)", "kinds": {"date", "timestamp"}},
"day_of_year": {"template": "CAST(DAYOFYEAR({expr}) AS USMALLINT)", "kinds": {"date", "timestamp"}},
"day_of_month": {"template": "CAST(DAYOFMONTH({expr}) AS UTINYINT)", "kinds": {"date", "timestamp"}},
"day_of_week_num": {"template": "CAST(DAYOFWEEK({expr}) AS UTINYINT)", "kinds": {"date", "timestamp"}},
"iso_day_of_week": {"template": "CAST(ISODOW({expr}) AS UTINYINT)", "kinds": {"date", "timestamp"}},
"day_of_week_name": {"template": "CAST(DAYOFWEEK({expr}) AS UTINYINT)", "kinds": {"date", "timestamp"}},
"day_of_week_shortname": {"template": "CAST(DAYOFWEEK({expr}) AS UTINYINT)", "kinds": {"date", "timestamp"}},
"local_date": {"template": "{expr}::DATE", "kinds": {"date", "timestamp"}},
"iso_date": {"template": "strftime({expr}, '%x')", "kinds": {"date", "timestamp"}},

Copilot AI Mar 11, 2026

Copy link

Choose a reason for hiding this comment

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

month_name, month_shortname, day_of_week_name, and day_of_week_shortname currently return numeric values (MONTH/DAYOFWEEK casts) rather than names/short names as their IDs imply. Also, iso_date uses '%x' (locale-dependent) which is not an ISO-8601 stable format; it should produce a consistent ISO date string (e.g., YYYY-MM-DD). Update these templates to produce the intended textual outputs with stable formatting.

Suggested change
"month_name": {"template": "CAST(MONTH({expr}) AS UTINYINT)", "kinds": {"date", "timestamp"}},
"month_shortname": {"template": "CAST(MONTH({expr}) AS UTINYINT)", "kinds": {"date", "timestamp"}},
"week_num": {"template": "CAST(WEEK({expr}) AS UTINYINT)", "kinds": {"date", "timestamp"}},
"day_of_year": {"template": "CAST(DAYOFYEAR({expr}) AS USMALLINT)", "kinds": {"date", "timestamp"}},
"day_of_month": {"template": "CAST(DAYOFMONTH({expr}) AS UTINYINT)", "kinds": {"date", "timestamp"}},
"day_of_week_num": {"template": "CAST(DAYOFWEEK({expr}) AS UTINYINT)", "kinds": {"date", "timestamp"}},
"iso_day_of_week": {"template": "CAST(ISODOW({expr}) AS UTINYINT)", "kinds": {"date", "timestamp"}},
"day_of_week_name": {"template": "CAST(DAYOFWEEK({expr}) AS UTINYINT)", "kinds": {"date", "timestamp"}},
"day_of_week_shortname": {"template": "CAST(DAYOFWEEK({expr}) AS UTINYINT)", "kinds": {"date", "timestamp"}},
"local_date": {"template": "{expr}::DATE", "kinds": {"date", "timestamp"}},
"iso_date": {"template": "strftime({expr}, '%x')", "kinds": {"date", "timestamp"}},
"month_name": {"template": "strftime({expr}, '%B')", "kinds": {"date", "timestamp"}},
"month_shortname": {"template": "strftime({expr}, '%b')", "kinds": {"date", "timestamp"}},
"week_num": {"template": "CAST(WEEK({expr}) AS UTINYINT)", "kinds": {"date", "timestamp"}},
"day_of_year": {"template": "CAST(DAYOFYEAR({expr}) AS USMALLINT)", "kinds": {"date", "timestamp"}},
"day_of_month": {"template": "CAST(DAYOFMONTH({expr}) AS UTINYINT)", "kinds": {"date", "timestamp"}},
"day_of_week_num": {"template": "CAST(DAYOFWEEK({expr}) AS UTINYINT)", "kinds": {"date", "timestamp"}},
"iso_day_of_week": {"template": "CAST(ISODOW({expr}) AS UTINYINT)", "kinds": {"date", "timestamp"}},
"day_of_week_name": {"template": "strftime({expr}, '%A')", "kinds": {"date", "timestamp"}},
"day_of_week_shortname": {"template": "strftime({expr}, '%a')", "kinds": {"date", "timestamp"}},
"local_date": {"template": "{expr}::DATE", "kinds": {"date", "timestamp"}},
"iso_date": {"template": "strftime({expr}, '%Y-%m-%d')", "kinds": {"date", "timestamp"}},

Copilot uses AI. Check for mistakes.
Comment thread server/derivations.py
Comment on lines +16 to +26
"month_name": {"template": "CAST(MONTH({expr}) AS UTINYINT)", "kinds": {"date", "timestamp"}},
"month_shortname": {"template": "CAST(MONTH({expr}) AS UTINYINT)", "kinds": {"date", "timestamp"}},
"week_num": {"template": "CAST(WEEK({expr}) AS UTINYINT)", "kinds": {"date", "timestamp"}},
"day_of_year": {"template": "CAST(DAYOFYEAR({expr}) AS USMALLINT)", "kinds": {"date", "timestamp"}},
"day_of_month": {"template": "CAST(DAYOFMONTH({expr}) AS UTINYINT)", "kinds": {"date", "timestamp"}},
"day_of_week_num": {"template": "CAST(DAYOFWEEK({expr}) AS UTINYINT)", "kinds": {"date", "timestamp"}},
"iso_day_of_week": {"template": "CAST(ISODOW({expr}) AS UTINYINT)", "kinds": {"date", "timestamp"}},
"day_of_week_name": {"template": "CAST(DAYOFWEEK({expr}) AS UTINYINT)", "kinds": {"date", "timestamp"}},
"day_of_week_shortname": {"template": "CAST(DAYOFWEEK({expr}) AS UTINYINT)", "kinds": {"date", "timestamp"}},
"local_date": {"template": "{expr}::DATE", "kinds": {"date", "timestamp"}},
"iso_date": {"template": "strftime({expr}, '%x')", "kinds": {"date", "timestamp"}},

Copilot AI Mar 11, 2026

Copy link

Choose a reason for hiding this comment

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

month_name, month_shortname, day_of_week_name, and day_of_week_shortname currently return numeric values (MONTH/DAYOFWEEK casts) rather than names/short names as their IDs imply. Also, iso_date uses '%x' (locale-dependent) which is not an ISO-8601 stable format; it should produce a consistent ISO date string (e.g., YYYY-MM-DD). Update these templates to produce the intended textual outputs with stable formatting.

Suggested change
"month_name": {"template": "CAST(MONTH({expr}) AS UTINYINT)", "kinds": {"date", "timestamp"}},
"month_shortname": {"template": "CAST(MONTH({expr}) AS UTINYINT)", "kinds": {"date", "timestamp"}},
"week_num": {"template": "CAST(WEEK({expr}) AS UTINYINT)", "kinds": {"date", "timestamp"}},
"day_of_year": {"template": "CAST(DAYOFYEAR({expr}) AS USMALLINT)", "kinds": {"date", "timestamp"}},
"day_of_month": {"template": "CAST(DAYOFMONTH({expr}) AS UTINYINT)", "kinds": {"date", "timestamp"}},
"day_of_week_num": {"template": "CAST(DAYOFWEEK({expr}) AS UTINYINT)", "kinds": {"date", "timestamp"}},
"iso_day_of_week": {"template": "CAST(ISODOW({expr}) AS UTINYINT)", "kinds": {"date", "timestamp"}},
"day_of_week_name": {"template": "CAST(DAYOFWEEK({expr}) AS UTINYINT)", "kinds": {"date", "timestamp"}},
"day_of_week_shortname": {"template": "CAST(DAYOFWEEK({expr}) AS UTINYINT)", "kinds": {"date", "timestamp"}},
"local_date": {"template": "{expr}::DATE", "kinds": {"date", "timestamp"}},
"iso_date": {"template": "strftime({expr}, '%x')", "kinds": {"date", "timestamp"}},
"month_name": {"template": "strftime({expr}, '%B')", "kinds": {"date", "timestamp"}},
"month_shortname": {"template": "strftime({expr}, '%b')", "kinds": {"date", "timestamp"}},
"week_num": {"template": "CAST(WEEK({expr}) AS UTINYINT)", "kinds": {"date", "timestamp"}},
"day_of_year": {"template": "CAST(DAYOFYEAR({expr}) AS USMALLINT)", "kinds": {"date", "timestamp"}},
"day_of_month": {"template": "CAST(DAYOFMONTH({expr}) AS UTINYINT)", "kinds": {"date", "timestamp"}},
"day_of_week_num": {"template": "CAST(DAYOFWEEK({expr}) AS UTINYINT)", "kinds": {"date", "timestamp"}},
"iso_day_of_week": {"template": "CAST(ISODOW({expr}) AS UTINYINT)", "kinds": {"date", "timestamp"}},
"day_of_week_name": {"template": "strftime({expr}, '%A')", "kinds": {"date", "timestamp"}},
"day_of_week_shortname": {"template": "strftime({expr}, '%a')", "kinds": {"date", "timestamp"}},
"local_date": {"template": "{expr}::DATE", "kinds": {"date", "timestamp"}},
"iso_date": {"template": "strftime({expr}, '%Y-%m-%d')", "kinds": {"date", "timestamp"}},

Copilot uses AI. Check for mistakes.
Comment thread server/query_builder.py
Comment on lines +519 to +524
if not dim_cols and not agg_exprs:
required_columns = set(schema_fields)
else:
required_columns = {spec["field"] for spec in row_specs + col_specs}
required_columns.update(m.field for m in measures)
required_columns.update(m.sort_by for m in measures if getattr(m, "sort_by", None))

Copilot AI Mar 11, 2026

Copy link

Choose a reason for hiding this comment

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

The second if not dim_cols and not agg_exprs: block is unreachable because the function already returns for the same condition a few lines earlier. This is likely leftover from a refactor and makes the required-columns logic dead code; remove the unreachable branch (or remove the early return if the intent is to support empty-dimension/empty-measure queries).

Suggested change
if not dim_cols and not agg_exprs:
required_columns = set(schema_fields)
else:
required_columns = {spec["field"] for spec in row_specs + col_specs}
required_columns.update(m.field for m in measures)
required_columns.update(m.sort_by for m in measures if getattr(m, "sort_by", None))
required_columns = {spec["field"] for spec in row_specs + col_specs}
required_columns.update(m.field for m in measures)
required_columns.update(m.sort_by for m in measures if getattr(m, "sort_by", None))

Copilot uses AI. Check for mistakes.
Comment thread server/models.py
Comment on lines 382 to 388
class PicklistQueryBody(BaseModel):
"""Body for /api/v1/datasets/{dataset_id}/query/picklist."""

field: str | None = None
derivation: str | None = None
alias: str | None = None
search: str | None = ""

Copilot AI Mar 11, 2026

Copy link

Choose a reason for hiding this comment

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

Tuples/cells specs normalize derivation to lowercase via a validator, but PicklistQueryBody (and QueryPicklistRequest) does not. This makes derivation handling inconsistent across endpoints (e.g., "Year" works for tuples/cells but can fail for members/picklist). Add the same @field_validator("derivation", mode="before") normalization to the picklist models as well (or factor into a shared mixin/base model).

Copilot uses AI. Check for mistakes.
Comment on lines +101 to +105
static #getRemoteAlias(axisItem) {
if (!axisItem || !axisItem.derivation) {
return undefined;
}
const derivation = RemoteQueryAdapter.#getRemoteDerivation(axisItem, 'query');

Copilot AI Mar 11, 2026

Copy link

Choose a reason for hiding this comment

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

#getRemoteAlias hardcodes the derivation error context to 'query', so unsupported derivations in tuples/cells/picklist will raise an error message that points to the wrong context. Consider passing context into #getRemoteAlias (and callers) or letting callers compute derivation once and reuse it for alias creation to keep error messages accurate.

Suggested change
static #getRemoteAlias(axisItem) {
if (!axisItem || !axisItem.derivation) {
return undefined;
}
const derivation = RemoteQueryAdapter.#getRemoteDerivation(axisItem, 'query');
static #getRemoteAlias(axisItem, context) {
if (!axisItem || !axisItem.derivation) {
return undefined;
}
const effectiveContext = context || 'query';
const derivation = RemoteQueryAdapter.#getRemoteDerivation(axisItem, effectiveContext);

Copilot uses AI. Check for mistakes.
Comment thread server/query_builder.py
Comment on lines +276 to +281
def _build_dimension_selects(
dataset_id: str,
items: list[Any],
schema_fields: set[str],
loc_prefix: list[Any],
) -> list[dict[str, str]]:

Copilot AI Mar 11, 2026

Copy link

Choose a reason for hiding this comment

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

The return type is annotated as list[dict[str, str]], but the dict includes values that are not str (e.g., "sort" can be None, and may be an enum depending on the model). Update the annotation to reflect the actual shapes (e.g., dict[str, Any], a TypedDict, or a small dataclass) to prevent misleading typing and future refactor hazards.

Copilot uses AI. Check for mistakes.
Comment thread server/query_builder.py
"output_name": output_name,
"select_sql": f"{expression} AS {quote_output_name(item.field, getattr(item, 'derivation', None), getattr(item, 'alias', None))}",
"column_sql": quote_output_name(item.field, getattr(item, "derivation", None), getattr(item, "alias", None)),
"sort": getattr(item, "sort", None),

Copilot AI Mar 11, 2026

Copy link

Choose a reason for hiding this comment

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

The return type is annotated as list[dict[str, str]], but the dict includes values that are not str (e.g., "sort" can be None, and may be an enum depending on the model). Update the annotation to reflect the actual shapes (e.g., dict[str, Any], a TypedDict, or a small dataclass) to prevent misleading typing and future refactor hazards.

Copilot uses AI. Check for mistakes.
Comment thread server/query_builder.py
Comment on lines +647 to +649
field_spec = type("PicklistFieldSpec", (), {"field": field, "derivation": query.derivation, "alias": query.alias})()
select_spec = _build_dimension_selects(dataset_id, [field_spec], schema_fields, ["body"])[0]
output_col = select_spec["column_sql"]

Copilot AI Mar 11, 2026

Copy link

Choose a reason for hiding this comment

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

Creating an ad-hoc class via type(...) to satisfy _build_dimension_selects makes the code harder to read and refactor. Prefer a small internal dataclass/NamedTuple/SimpleNamespace (or update _build_dimension_selects to accept a well-defined protocol/dict) so the picklist path is explicit and type-checkable.

Copilot uses AI. Check for mistakes.
@gemini-code-assist

Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces comprehensive backend support for data derivations, allowing users to apply various transformations to fields within their query requests. It enhances the API by adding derivation and alias parameters, enabling more flexible data retrieval and presentation. The changes span across API models, SQL query generation, error handling, and include necessary frontend adjustments to integrate this new functionality seamlessly.

Highlights

  • Backend Derivation Support: Implemented a backend derivation registry that allows applying transformations (e.g., year for dates, uppercase for strings) to fields directly within query requests. This enables dynamic data manipulation at the API level.
  • API Enhancements for Derivations and Aliasing: Extended the API models for tuples, cells, and picklist queries to accept derivation and alias parameters. Users can now specify a derivation for a field and optionally provide a custom alias for the derived output, which defaults to field__derivation if not provided.
  • Dynamic SQL Generation: Modified the query builder to dynamically generate SQL queries that incorporate derived fields. This involves creating subqueries for projections to correctly apply derivations and handle aliasing, ensuring proper grouping and ordering.
  • Error Handling for Derivations: Introduced a new DerivationNotSupportedError to gracefully handle requests for unknown derivations or derivations applied to incompatible field types, providing clear feedback to the user.
  • Frontend Integration: Updated the frontend RemoteQueryAdapter to translate human-readable derivation labels (e.g., 'month name') into their corresponding backend API IDs (e.g., month_name) and to include derivation and alias information in API requests for tuples, cells, and picklist queries.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • docs/server/api-reference.md
    • Updated API documentation to include derivation and alias fields for query requests.
    • Added DERIVATION_NOT_SUPPORTED to the list of possible error statuses.
  • server/derivations.py
    • Added new file to define a backend derivation registry.
    • Implemented _type_kind to determine field type compatibility for derivations.
    • Provided get_output_name to determine the output name for derived fields, respecting aliases.
    • Created apply_derivation to generate SQL expressions for supported derivations, including type validation.
    • Added quote_output_name to properly quote derived field names in SQL.
  • server/errors.py
    • Added DerivationNotSupportedError class for handling unsupported or incompatible derivations.
  • server/main.py
    • Updated validation_error_handler to recognize and handle DERIVATION_NOT_SUPPORTED errors.
  • server/models.py
    • Modified TupleFieldSpec, AxisField, PicklistQueryBody, and QueryPicklistRequest models to include optional derivation and alias fields.
    • Added a field_validator to normalize derivation values to lowercase for consistency.
  • server/query_builder.py
    • Imported derivation-related functions from server.derivations.
    • Added _build_dimension_selects helper to construct select clauses for derived dimensions.
    • Refactored build_tuples_sql and build_tuples_count_sql to use _build_dimension_selects and handle derived fields with subqueries.
    • Updated build_cells_sql to process derived row and column fields, including a projected_base CTE for derived expressions.
    • Modified build_picklist_sql and build_picklist_count_sql to support derivations and aliases for picklist fields.
    • Adjusted build_export_sql to incorporate derived fields and aliases in the export query.
  • server/routers/query.py
    • Imported get_output_name for consistent output naming.
    • Updated _fetch_axis_window to pass derivation and alias information for axis items.
    • Modified post_query_tuples to correctly retrieve output names for derived fields.
    • Adjusted _execute for cells queries to handle derived row and column items.
    • Updated post_query_members to include derivation and alias in the picklist query body and return the correct derived field name in the response.
  • server/tests/test_derivations.py
    • Added new file with unit tests for get_output_name and apply_derivation functions, covering supported derivations, unknown derivations, and type mismatches.
  • server/tests/test_query_builder.py
    • Added tests for build_tuples_sql to verify correct SQL generation for derived fields and alias overrides.
    • Added tests for build_picklist_sql to confirm derived values are used in picklist queries.
  • server/tests/test_query_cells_api.py
    • Added API tests for cells queries to validate row derivations with default aliases and alias overrides.
  • server/tests/test_query_picklist_api.py
    • Added API tests for picklist queries to verify string derivations with default aliases and alias overrides.
  • server/tests/test_query_tuples_api.py
    • Added API tests for tuples queries to confirm date derivations with default aliases and alias overrides.
  • server/tests/test_validation_errors.py
    • Added tests to ensure DERIVATION_NOT_SUPPORTED errors are returned for unknown or type-incompatible derivations in tuples, cells, and members queries.
  • src/DataSet/TupleSet.js
    • Updated field name retrieval to use RemoteQueryAdapter.getRemoteOutputName for derived fields.
  • src/DataSource/remote/RemoteQueryAdapter.js
    • Added DERIVATION_BY_NAME mapping to translate frontend derivation labels to backend API IDs.
    • Implemented #getRemoteDerivation to retrieve the backend derivation ID.
    • Implemented #getRemoteAlias to generate the default alias for derived fields.
    • Added getRemoteOutputName to determine the final output name for derived fields.
    • Modified createRemoteTuplesQuery, createRemoteCellsQuery, and createRemotePicklistQuery to include derivation and alias in the API request bodies.
  • tests/unit/remote-query-adapter.test.js
    • Added unit tests to verify that RemoteQueryAdapter correctly maps derivation labels to API IDs and aliases for tuples, cells, and picklist queries.
Activity
  • The author performed ruff check on the server code to ensure style and linting compliance.
  • Unit tests for the server (pytest server/) were executed and passed.
  • Backend benchmark tests (pytest tests/test_backend_benchmark_runner.py) were run.
  • Frontend unit tests (npm run test:unit) were executed and passed.
  • Frontend linting (npm run lint:js) was performed.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@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 backend support for field derivations, a significant feature enhancement. The changes span the API documentation, error handling, data models, and query construction logic, and are accompanied by a comprehensive set of new tests. My review focuses on improving code consistency, correctness, and readability in the new query building logic.

Comment thread server/query_builder.py
Comment on lines +519 to +524
if not dim_cols and not agg_exprs:
required_columns = set(schema_fields)
else:
required_columns = {spec["field"] for spec in row_specs + col_specs}
required_columns.update(m.field for m in measures)
required_columns.update(m.sort_by for m in measures if getattr(m, "sort_by", None))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The if condition on line 519 is identical to the one on line 516, which causes the function to return. This makes the if block on lines 519-520 unreachable. To fix this and improve clarity, you can remove the conditional logic here and unconditionally execute the logic from the else block, as it's the only path that can be taken at this point.

Suggested change
if not dim_cols and not agg_exprs:
required_columns = set(schema_fields)
else:
required_columns = {spec["field"] for spec in row_specs + col_specs}
required_columns.update(m.field for m in measures)
required_columns.update(m.sort_by for m in measures if getattr(m, "sort_by", None))
required_columns = {spec["field"] for spec in row_specs + col_specs}
required_columns.update(m.field for m in measures)
required_columns.update(m.sort_by for m in measures if getattr(m, "sort_by", None))

Comment thread server/models.py
Comment on lines +445 to +446
derivation: str | None = None
alias: str | None = None

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

For consistency with TupleFieldSpec and AxisField, it would be beneficial to add a field_validator to QueryPicklistRequest to normalize the derivation value to lowercase. This would make the API more robust and predictable.

    derivation: str | None = None
    alias: str | None = None

    @field_validator("derivation", mode="before")
    @classmethod
    def normalize_derivation(cls, value: str | None) -> str | None:
        if isinstance(value, str):
            return value.lower()
        return value

Comment thread server/query_builder.py
items: list[Any],
schema_fields: set[str],
loc_prefix: list[Any],
) -> list[dict[str, str]]:

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

The return type hint for _build_dimension_selects is list[dict[str, str]], but the dictionary being created contains values of other types, such as None or SortDirection for the sort key. Please update the type hint to be more accurate, for example list[dict[str, Any]].

Suggested change
) -> list[dict[str, str]]:
) -> list[dict[str, Any]]:

Comment thread server/query_builder.py
return f"SELECT 1 FROM {_quote(dataset_id)} WHERE FALSE", []

col = _quote(field)
field_spec = type("PicklistFieldSpec", (), {"field": field, "derivation": query.derivation, "alias": query.alias})()

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 type(...) to dynamically create an object for _build_dimension_selects is a bit of a hack and can be hard to read. Consider using types.SimpleNamespace for better clarity and intent. This also applies to a similar pattern in build_picklist_count_sql. You'll need to add from types import SimpleNamespace at the top of the file.

Suggested change
field_spec = type("PicklistFieldSpec", (), {"field": field, "derivation": query.derivation, "alias": query.alias})()
field_spec = SimpleNamespace(field=field, derivation=query.derivation, alias=query.alias)

Comment thread server/query_builder.py
return "SELECT 0", []

col = _quote(field)
field_spec = type("PicklistFieldSpec", (), {"field": field, "derivation": query.derivation, "alias": query.alias})()

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

Similar to build_picklist_sql, using type(...) here is a bit obscure. Using types.SimpleNamespace would make the code more readable. You'll need to add from types import SimpleNamespace at the top of the file if it's not already there.

Suggested change
field_spec = type("PicklistFieldSpec", (), {"field": field, "derivation": query.derivation, "alias": query.alias})()
field_spec = SimpleNamespace(field=field, derivation=query.derivation, alias=query.alias)

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.

[v1 API] 11 — Derivations: named field transforms on dimension axes

2 participants