Skip to content

feat: add internal chq ClickHouse query builder, migrate telemetry from squirrel#1876

Draft
tgmendes wants to merge 1 commit intomainfrom
feat/chq-clickhouse-query-builder
Draft

feat: add internal chq ClickHouse query builder, migrate telemetry from squirrel#1876
tgmendes wants to merge 1 commit intomainfrom
feat/chq-clickhouse-query-builder

Conversation

@tgmendes
Copy link
Copy Markdown
Contributor

@tgmendes tgmendes commented Mar 13, 2026

Summary

  • Adds server/internal/chq/ — a ClickHouse-specific type-safe query builder (expressions, predicates, functions, SELECT/INSERT builders) vendored from the standalone chq repo.
  • Migrates server/internal/telemetry/repo/pagination.go and queries.sql.go from squirrel to chq, using ClickHouse-idiomatic constructs (PREWHERE, toStartOfHour, fromUnixTimestamp64Nano, WITH TOTALS, etc.).
  • Removes the squirrel dependency from go.mod/go.sum.

Why

squirrel is a generic SQL builder with no awareness of ClickHouse syntax. chq provides first-class support for ClickHouse-specific features used heavily in our telemetry queries.

Testing

  • All tests in server/internal/telemetry/... pass (go test ./server/internal/telemetry/... -count=1)
  • mise lint:server passes clean

Notes

  • chq.Gte[chq.Sqlizer] / chq.Lt[chq.Sqlizer] do not work correctly when T = Sqlizer (the generic implementation passes the struct as a raw arg rather than expanding its SQL). Affected time_bucket filters use chq.Expr(...) directly instead.
  • The server/cmd/chqdebug/ directory is an untracked debug helper (not committed).

Open with Devin

@tgmendes tgmendes requested a review from a team as a code owner March 13, 2026 11:53
@vercel
Copy link
Copy Markdown

vercel Bot commented Mar 13, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
gram-docs-redirect Ready Ready Preview, Comment Mar 13, 2026 2:06pm

Request Review

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Mar 13, 2026

⚠️ No Changeset found

Latest commit: 3469ee6

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 13, 2026

atlas migrate lint on server/migrations

Status Step Result
No migration files detected  
ERD and visual diff generated View Visualization
No issues found View Report
Read the full linting report on Atlas Cloud

Comment on lines +2 to +14
CREATE TABLE "principal_grants" (
"id" uuid NOT NULL DEFAULT generate_uuidv7(),
"organization_id" text NOT NULL,
"principal_urn" text NOT NULL,
"principal_type" text NOT NULL GENERATED ALWAYS AS (split_part(principal_urn, ':'::text, 1)) STORED,
"scope" text NOT NULL,
"resource" text NOT NULL DEFAULT '*',
"created_at" timestamptz NOT NULL DEFAULT clock_timestamp(),
"updated_at" timestamptz NOT NULL DEFAULT clock_timestamp(),
PRIMARY KEY ("id"),
CONSTRAINT "principal_grants_organization_id_principal_urn_scope_resource_k" UNIQUE ("organization_id", "principal_urn", "scope", "resource"),
CONSTRAINT "principal_grants_organization_id_fkey" FOREIGN KEY ("organization_id") REFERENCES "organization_metadata" ("id") ON UPDATE NO ACTION ON DELETE CASCADE
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Warning

non-optimal columns alignment
Table "principal_grants" has 4 redundant bytes of padding per row. To reduce disk space, the optimal order of the columns is as follows: "created_at", "updated_at", "organization_id", "principal_urn", "principal_type", "scope", "resource", "id" PG110

Suggested change
CREATE TABLE "principal_grants" (
"id" uuid NOT NULL DEFAULT generate_uuidv7(),
"organization_id" text NOT NULL,
"principal_urn" text NOT NULL,
"principal_type" text NOT NULL GENERATED ALWAYS AS (split_part(principal_urn, ':'::text, 1)) STORED,
"scope" text NOT NULL,
"resource" text NOT NULL DEFAULT '*',
"created_at" timestamptz NOT NULL DEFAULT clock_timestamp(),
"updated_at" timestamptz NOT NULL DEFAULT clock_timestamp(),
PRIMARY KEY ("id"),
CONSTRAINT "principal_grants_organization_id_principal_urn_scope_resource_k" UNIQUE ("organization_id", "principal_urn", "scope", "resource"),
CONSTRAINT "principal_grants_organization_id_fkey" FOREIGN KEY ("organization_id") REFERENCES "organization_metadata" ("id") ON UPDATE NO ACTION ON DELETE CASCADE
);
CREATE TABLE "principal_grants" (
"created_at" timestamptz NOT NULL DEFAULT clock_timestamp(),
"updated_at" timestamptz NOT NULL DEFAULT clock_timestamp(),
"organization_id" text NOT NULL,
"principal_urn" text NOT NULL,
"principal_type" text NOT NULL GENERATED ALWAYS AS (split_part(principal_urn, ':'::text, 1)) STORED,
"scope" text NOT NULL,
"resource" text NOT NULL DEFAULT '*',
"id" uuid NOT NULL DEFAULT generate_uuidv7(),
PRIMARY KEY ("id"),
CONSTRAINT "principal_grants_organization_id_principal_urn_scope_resource_k" UNIQUE ("organization_id", "principal_urn", "scope", "resource"),
CONSTRAINT "principal_grants_organization_id_fkey" FOREIGN KEY ("organization_id") REFERENCES "organization_metadata" ("id") ON UPDATE NO ACTION ON DELETE CASCADE
);

Ensure to run atlas migrate hash --dir "file://server/migrations" after applying the suggested changes.

@github-actions
Copy link
Copy Markdown
Contributor

atlas migrate lint on server/clickhouse/migrations

Status Step Result
No migration files detected  
ERD and visual diff generated View Visualization
No issues found View Report
Read the full linting report on Atlas Cloud

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 2 potential issues.

View 6 additional findings in Devin Review.

Open in Devin Review

Comment on lines +626 to +640
type PrincipalGrant struct {
ID uuid.UUID
// The organization this grant belongs to. Grants are always org-scoped.
OrganizationID string
// Discriminator: 'user' for a direct user grant, 'role' for a WorkOS role grant.
PrincipalType string
// The identifier of the principal: a WorkOS user ID when principal_type='user', or a WorkOS role slug when principal_type='role'.
PrincipalID string
// The scope being granted, e.g. "build:read". Validated in application code, not via FK.
ScopeSlug string
// '*' = unrestricted (scope applies to all resources in the org). Any other value = a specific resource ID this scope is granted on.
Resource string
CreatedAt pgtype.Timestamptz
UpdatedAt pgtype.Timestamptz
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔴 PrincipalGrant sqlc model has stale field names that don't match the database schema

The PrincipalGrant struct in the sqlc-generated models.go has field names that don't match the actual database columns in schema.sql. The DB column principal_urn is represented as PrincipalID (should be PrincipalUrn), the column scope is represented as ScopeSlug (should be Scope), and the principal_urn column is entirely missing from the struct. This happened because sqlc was regenerated (fabfcff2) before the schema rename commit (c0ea4479 — "rename scope_slug to scope"), so the model reflects an older schema version. Any code that reads from or writes to principal_grants using this model will fail at runtime because field names don't map to the actual columns.

Suggested change
type PrincipalGrant struct {
ID uuid.UUID
// The organization this grant belongs to. Grants are always org-scoped.
OrganizationID string
// Discriminator: 'user' for a direct user grant, 'role' for a WorkOS role grant.
PrincipalType string
// The identifier of the principal: a WorkOS user ID when principal_type='user', or a WorkOS role slug when principal_type='role'.
PrincipalID string
// The scope being granted, e.g. "build:read". Validated in application code, not via FK.
ScopeSlug string
// '*' = unrestricted (scope applies to all resources in the org). Any other value = a specific resource ID this scope is granted on.
Resource string
CreatedAt pgtype.Timestamptz
UpdatedAt pgtype.Timestamptz
}
type PrincipalGrant struct {
ID uuid.UUID
// The organization this grant belongs to. Grants are always org-scoped.
OrganizationID string
// URN identifying the principal, e.g. "user:user_abc", "role:admin". Format is type:id.
PrincipalUrn string
// Derived from principal_urn. The type prefix, e.g. "user", "role".
PrincipalType string
// The scope being granted, e.g. "build:read". Validated in application code, not via FK.
Scope string
// '*' = unrestricted (scope applies to all resources in the org). Any other value = a specific resource ID this scope is granted on.
Resource string
CreatedAt pgtype.Timestamptz
UpdatedAt pgtype.Timestamptz
}
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread server/internal/chq/fn.go
Comment on lines +139 to +141
func Extract(part string, expr Sqlizer) Sqlizer {
return Expr(fmt.Sprintf("extract(%s, ?)", part), mustSql(expr))
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔴 Extract() binds inner expression's SQL text as a literal parameter instead of embedding it in the query

Extract uses mustSql(expr) to get the SQL string of the inner expression and passes it as the positional argument for the ? placeholder. This means column references and parameterized expressions are bound as literal string values instead of being embedded in the SQL. For example, Extract("YEAR", Col("ts")) produces extract(YEAR, ?) with args ["ts"], causing ClickHouse to receive extract(YEAR, 'ts') — a string literal rather than a column reference. The correct approach (used by Quantile at server/internal/chq/fn.go:193) is to embed the SQL via mustSqlNoArgs and forward args via mustSqlArgs.

Related: mustSql has dead code suggesting unfinished implementation

mustSql at server/internal/chq/fn.go:320-329 has an if len(args) > 0 branch that returns the same value as the else branch — both return sql. This looks like an incomplete implementation where the args-present case was supposed to do something different.

Suggested change
func Extract(part string, expr Sqlizer) Sqlizer {
return Expr(fmt.Sprintf("extract(%s, ?)", part), mustSql(expr))
}
func Extract(part string, expr Sqlizer) Sqlizer {
return Expr(fmt.Sprintf("extract(%s, %s)", part, mustSqlNoArgs(expr)), mustSqlArgs(expr)...)
}
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@tgmendes tgmendes marked this pull request as draft March 13, 2026 14:03
… queries from squirrel

- Copy chq package into server/internal/chq/ (expr, pred, fn, select, insert builders)
- Rewrite server/internal/telemetry/repo/pagination.go to use chq.SelectBuilder
- Rewrite server/internal/telemetry/repo/queries.sql.go to use chq instead of squirrel
- Remove squirrel from go.mod/go.sum (no longer a dependency)
- All telemetry tests pass; lint clean
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