Skip to content

🚨 Lighthouse: [Performance] Consolidate multiple COUNT queries using FILTER#223

Open
ldsgroups225 wants to merge 1 commit intomasterfrom
lighthouse-performance-analytics-filter-3327829088487857982
Open

🚨 Lighthouse: [Performance] Consolidate multiple COUNT queries using FILTER#223
ldsgroups225 wants to merge 1 commit intomasterfrom
lighthouse-performance-analytics-filter-3327829088487857982

Conversation

@ldsgroups225
Copy link
Copy Markdown
Owner

@ldsgroups225 ldsgroups225 commented Mar 31, 2026

Impact on performance

  • Reduced DB Round Trips: Consolidated 13 concurrent COUNT queries running across four main analytical dashboards down to just 4 aggregated queries.
  • Lower Resource Load: Minimized query execution overhead and memory allocation, preventing connection pool exhaustion and reducing time-to-first-byte (TTFB).
  • Reduced Network Latency: Drastically lowered the amount of DB chatter and data serialization on the wire for the backend API endpoints.

Technical Rationale

Previously, analytics endpoints calculating performance breakdowns—like counting active, inactive, and suspended schools, or finding current versus previous period growths—were running completely separate db.select({ count: count() }) statements using Promise.all. This forced PostgreSQL to scan the tables multiple times, once for each criteria.

By refactoring these Promise.all structures to use PostgreSQL's native FILTER (WHERE ...) clause (via Drizzle's sql<number> tag), we combine these independent aggregations. Postgres can now scan the table once and correctly bucket the sums in a single pass. We also added fallback defaults (Number() with || 0) to ensure type safety matching the old outputs.


PR created automatically by Jules for task 3327829088487857982 started by @ldsgroups225

Summary by CodeRabbit

  • Refactor
    • Refactored analytics queries to use consolidated database operations for improved efficiency.

@google-labs-jules
Copy link
Copy Markdown

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 31, 2026

📝 Walkthrough

Walkthrough

Multiple analytics query functions have been refactored to consolidate separate count queries into single aggregated SELECT statements with filtered count expressions, reducing database calls across getAnalyticsOverview, getSchoolsPerformance, getEnrollmentStats, and getEnrollmentGrowth.

Changes

Cohort / File(s) Summary
Analytics Query Consolidation
packages/data-ops/src/queries/analytics.ts
Four functions refactored: getAnalyticsOverview consolidates three period-specific counts into one query; getSchoolsPerformance merges three status-specific counts (active/inactive/suspended); getEnrollmentStats collapses five enrollment status counts with adjusted computed fields; getEnrollmentGrowth combines two period-specific counts. All use filtered count expressions with Number() type conversions.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • PR #87: Modifies the same analytics functions (getAnalyticsOverview, getEnrollmentStats, getEnrollmentGrowth) with alternative query consolidation approach using parallelization instead of single aggregated statements.
  • PR #126: Also restructures getAnalyticsOverview and getSchoolsPerformance database queries, merging separate calls and replacing per-school engagement calls with batched query patterns.

Poem

🐰 Many queries bundled tight,
Into single SELECTs bright,
Counting faster, dancing lean,
Cleanest analytics we've seen! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title includes an emoji and reference tags that don't convey essential information, making it harder to scan PR history. While it does relate to the main change (consolidating COUNT queries), the phrasing is verbose and the emoji adds noise. Consider revising to a clearer format like 'Consolidate multiple COUNT queries in analytics using PostgreSQL FILTER' to improve readability in commit history.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch lighthouse-performance-analytics-filter-3327829088487857982

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/data-ops/src/queries/analytics.ts`:
- Around line 351-357: The current db.select on students builds baseConditions
but leaves out a required tenant filter, allowing cross-tenant aggregation via
the students fallback; update the query that builds the select (the
db.select(...) that uses total/active/graduated/transferred/withdrawn and
baseConditions) to always include eq(students.schoolId, schoolId) in the WHERE
clause (i.e., push sql/and(eq(students.schoolId, schoolId)) into baseConditions
or combine it with and(...baseConditions)) rather than conditionally omitting it
when schoolId is undefined; if platform-wide (admin) analytics are required,
extract that into a separate explicit admin-only query instead of making
schoolId optional in this function.
- Around line 47-51: The query functions in analytics.ts (e.g., the
db.select(...) that computes total, currentPeriod, previousPeriod) must stop
returning raw Promise results and instead return ResultAsync<T, DatabaseError>
from `@praha/byethrow`; wrap the db call with ResultAsync.fromPromise(...) (or
ResultAsync) and append .mapErr(tapLogErr(logger, 'analytics.<functionName>'))
so errors are converted to DatabaseError and logged consistently; follow the
same pattern used in academic-intentions.ts/discounts.ts/payments.ts and apply
this change to the other query blocks mentioned (lines ~124–128, ~351–357,
~422–425), referencing tapLogErr, ResultAsync, and DatabaseError to locate the
utilities to import and use.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 15ed7888-1631-4f92-80cc-345a1fcdd71b

📥 Commits

Reviewing files that changed from the base of the PR and between 7e37ef8 and 146157d.

📒 Files selected for processing (1)
  • packages/data-ops/src/queries/analytics.ts

Comment on lines +47 to +51
db.select({
total: count(),
currentPeriod: sql<number>`count(*) filter (where ${schools.createdAt} >= ${startDate})`,
previousPeriod: sql<number>`count(*) filter (where ${schools.createdAt} >= ${previousStartDate} and ${schools.createdAt} < ${startDate})`,
}).from(schools),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Promise-returning exports in analytics.ts =="
rg -n --type=ts 'export async function (getAnalyticsOverview|getSchoolsPerformance|getEnrollmentStats|getEnrollmentGrowth)\b|Promise<' packages/data-ops/src/queries/analytics.ts

echo
echo "== ResultAsync / tapLogErr usage elsewhere in data-ops =="
rg -n -C2 --type=ts 'ResultAsync|tapLogErr' packages/data-ops/src -g '!packages/data-ops/src/queries/analytics.ts'

Repository: ldsgroups225/Yeko

Length of output: 50374


Wrap analytics.ts query functions in the ResultAsync<T, DatabaseError> pattern with tapLogErr error logging.

All query functions in this file currently return bare Promise<...> types and use raw await db.select(...) chains. Per the package contract, every query function must return ResultAsync<T, DatabaseError> from @praha/byethrow and attach .mapErr(tapLogErr(logger, context)) for consistent error handling and auditability. See academic-intentions.ts, discounts.ts, payments.ts, and other files in packages/data-ops/src/queries/ for the required pattern.

Also applies to: 124–128, 351–357, 422–425

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/data-ops/src/queries/analytics.ts` around lines 47 - 51, The query
functions in analytics.ts (e.g., the db.select(...) that computes total,
currentPeriod, previousPeriod) must stop returning raw Promise results and
instead return ResultAsync<T, DatabaseError> from `@praha/byethrow`; wrap the db
call with ResultAsync.fromPromise(...) (or ResultAsync) and append
.mapErr(tapLogErr(logger, 'analytics.<functionName>')) so errors are converted
to DatabaseError and logged consistently; follow the same pattern used in
academic-intentions.ts/discounts.ts/payments.ts and apply this change to the
other query blocks mentioned (lines ~124–128, ~351–357, ~422–425), referencing
tapLogErr, ResultAsync, and DatabaseError to locate the utilities to import and
use.

Comment on lines +351 to +357
db.select({
total: count(),
active: sql<number>`count(*) filter (where ${students.status} = 'active')`,
graduated: sql<number>`count(*) filter (where ${students.status} = 'graduated')`,
transferred: sql<number>`count(*) filter (where ${students.status} = 'transferred')`,
withdrawn: sql<number>`count(*) filter (where ${students.status} = 'withdrawn')`,
}).from(students).where(baseConditions.length > 0 ? and(...baseConditions) : undefined),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Don’t keep an unscoped students fallback here.

Line 357 and Line 425 still omit the students.schoolId predicate when schoolId is undefined, which preserves a cross-tenant aggregation path on a school-scoped table. If platform-wide analytics is intentional, please move that into an explicit admin-only query instead of hiding the tenant bypass behind an optional parameter.

Based on learnings, "Every query on school-scoped tables MUST include where(eq(table.schoolId, schoolId)) for multi-tenant isolation."

Also applies to: 422-425

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/data-ops/src/queries/analytics.ts` around lines 351 - 357, The
current db.select on students builds baseConditions but leaves out a required
tenant filter, allowing cross-tenant aggregation via the students fallback;
update the query that builds the select (the db.select(...) that uses
total/active/graduated/transferred/withdrawn and baseConditions) to always
include eq(students.schoolId, schoolId) in the WHERE clause (i.e., push
sql/and(eq(students.schoolId, schoolId)) into baseConditions or combine it with
and(...baseConditions)) rather than conditionally omitting it when schoolId is
undefined; if platform-wide (admin) analytics are required, extract that into a
separate explicit admin-only query instead of making schoolId optional in this
function.

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