Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
87 commits
Select commit Hold shift + click to select a range
5378a82
chore(hogql): characterization corpus + golden harness (PR0a)
aspicer Jun 5, 2026
9970a59
chore(hogql): reachability oracle for printer rearchitecture (PR0b)
aspicer Jun 5, 2026
dc3c437
chore(hogql): execution + characterization net (PR0c)
aspicer Jun 5, 2026
742123a
feat(hogql): logical property lowering for warehouse dialects (gated)
aspicer Jun 5, 2026
caf0ea4
feat(hogql): add FieldType.unqualified for within_non_hogql bare columns
aspicer Jun 5, 2026
65070cd
feat(hogql): render JSONFieldAccess as HogQL property chain in the Ho…
aspicer Jun 6, 2026
7af62d6
feat(hogql): clickhouse materialized-column physical pass (dormant)
aspicer Jun 5, 2026
56e0551
feat(hogql): flip ClickHouse to logical lowering + physical passes (g…
aspicer Jun 5, 2026
fa2708e
fix(web-analytics): collect lowered JSONFieldAccess in the events pre…
aspicer Jun 6, 2026
ca36c97
test(hogql): normalize lowered JSONFieldAccess in person-where-clause…
aspicer Jun 6, 2026
36853dc
test(hogql): update postgres-table predicate placeholder numbers for …
aspicer Jun 6, 2026
7ad0c24
feat(hogql): differential shadow-compare harness (test-mode)
aspicer Jun 5, 2026
95b05ce
chore(hogql): remove reachability oracle, superseded by the differential
aspicer Jun 5, 2026
1f96633
chore(hogql): gitignore the regenerated shadow-differential sweep report
aspicer Jun 5, 2026
c886f58
feat(hogql): keep within_non_hogql_query on the printer path
aspicer Jun 5, 2026
45ac602
feat(hogql): add Constant.inline; emit fixed mat scrub sentinels inline
aspicer Jun 5, 2026
ee52411
feat(hogql): complete the ClickHouse physical pass + carry Constant.i…
aspicer Jun 5, 2026
e621fc3
feat(hogql): extend the differential to the query-execution boundary
aspicer Jun 5, 2026
90c0cd1
feat(hogql): make the differential always-on in test, comparing results
aspicer Jun 5, 2026
cee3870
feat(hogql): add propertyLowering org feature flag to serve the new path
aspicer Jun 5, 2026
22e8ccb
feat(hogql): cut over to the lowering path; remove the differential s…
aspicer Jun 5, 2026
81d7259
chore(hogql): remove the propertyLowering flag and the lower_property…
aspicer Jun 5, 2026
f625554
test(hogql): drop the removed-gate reference in the DELETE-path test
aspicer Jun 5, 2026
468e32e
feat(hogql): route within_non_hogql through lowering
aspicer Jun 5, 2026
ed39c40
feat(hogql): delete the printer property-to-column code
aspicer Jun 5, 2026
49cd2ca
chore(hogql): post-cutover cleanup from code review
aspicer Jun 6, 2026
2fa69e9
chore: update OpenAPI generated types
tests-posthog[bot] Jun 6, 2026
0e9575b
chore(hogql): regenerate snapshots for the lowering cutover
aspicer Jun 6, 2026
2cbb2ea
chore(hogql): make property-lowering comments self-contained and clearer
aspicer Jun 8, 2026
d66e237
refactor(hogql): drop the FieldType.unqualified printer flag
aspicer Jun 9, 2026
742e85b
Merge remote-tracking branch 'origin/master' into aspicer/hogql-rearc…
aspicer Jun 9, 2026
28f8387
chore(hogql): assert pushed person clause in lowered form, drop the u…
aspicer Jun 9, 2026
d8df6cb
refactor(hogql): single source for the $ai bloom-filter property list
aspicer Jun 9, 2026
5f9a964
refactor(hogql): trust lowering invariants, clarify transform comments
aspicer Jun 9, 2026
ecc6553
fix(hogql): read materialized columns through column-aliased tables
aspicer Jun 9, 2026
f67edbf
chore(hogql): drop PR-context framing from transform comments
aspicer Jun 9, 2026
77fe536
chore(hogql): clarify comparison-rewrite section comment
aspicer Jun 9, 2026
30876cd
chore(hogql): drop two dead helpers, clarify comparison-rewrite parity
aspicer Jun 9, 2026
99d3519
chore(hogql): drop migration framing from comparison-rewrite comment
aspicer Jun 9, 2026
124b76b
chore(hogql): drop remaining point-in-time framing from comments
aspicer Jun 9, 2026
688141e
refactor(hogql): fold comparison-column builders into _OptimizablePro…
aspicer Jun 9, 2026
56d7ce1
refactor(hogql): rename Constant.inline to inline_sentinel, gate to a…
aspicer Jun 9, 2026
4e45c39
fix(hogql): decline materialized columns for access-restricted proper…
aspicer Jun 9, 2026
32e3533
perf(hogql): read access-restricted properties as a constant NULL
aspicer Jun 9, 2026
0748331
refactor(hogql): make restricted-property keys a single source of truth
aspicer Jun 9, 2026
e89e18f
chore(hogql): apply ruff format to clickhouse_physical_passes
aspicer Jun 9, 2026
2952aee
refactor(hogql): rename physical passes to property resolution
aspicer Jun 9, 2026
d9ae02e
refactor(hogql): deduplicate property-resolution helpers
aspicer Jun 9, 2026
8bf7624
fix(hogql): keep synthetic materialized columns in events prefilter s…
aspicer Jun 10, 2026
3b9e9b7
refactor(hogql): share the table-type unwrapping loop in property res…
aspicer Jun 10, 2026
96603f9
refactor(hogql): redesign events predicate pushdown on the lowering a…
aspicer Jun 9, 2026
d5c8ece
test(backend): update query snapshots
tests-posthog[bot] Jun 8, 2026
3694560
chore(hogql): tidy events-predicate-pushdown comments
aspicer Jun 9, 2026
8c2aae7
fix(hogql): qualify hoisted property names by source blob in pushdown
aspicer Jun 9, 2026
600989a
test(backend): update query snapshots
tests-posthog[bot] Jun 9, 2026
e23ca51
fix(hogql): narrow field type before table_type access in pushdown test
aspicer Jun 9, 2026
491acab
refactor(hogql): project only source columns in events pushdown
aspicer Jun 11, 2026
daf80a5
perf(hogql): hoist maximal events-only subexpressions in predicate pu…
aspicer Jun 11, 2026
0dc17eb
refactor(hogql): keep conservative ifNull wrapping for subquery columns
aspicer Jun 11, 2026
a32a206
refactor(hogql): keep scrubbed-blob reads for access-restricted prope…
aspicer Jun 11, 2026
999ab96
fix(hogql): resolve aliased property comparisons from the resolver's …
aspicer Jun 11, 2026
51a5917
perf(hogql): read access-restricted properties as a constant NULL
aspicer Jun 11, 2026
211bc18
test(backend): update query snapshots
tests-posthog[bot] Jun 11, 2026
9a72469
fix(hogql): close pushdown gating gaps found in pre-merge review
aspicer Jun 11, 2026
0751e05
Merge remote-tracking branch 'origin/master' into aspicer/hogql-rearc…
aspicer Jun 11, 2026
f7efc92
fix(hogql): resolve aliased property comparisons from the resolver's …
aspicer Jun 11, 2026
d624946
chore(hogql): warn when restricted-property table resolution fails open
aspicer Jun 11, 2026
059e2ba
Merge remote-tracking branch 'origin/master' into aspicer/hogql-rearc…
aspicer Jun 11, 2026
a87b69c
feat(hogql): port typed materialized-column range rewrite to property…
aspicer Jun 11, 2026
418914c
test(hogql): feed property-planner tests their pipeline-position input
aspicer Jun 11, 2026
df893d8
Merge branch 'aspicer/hogql-rearch-15-clean-cutover' into aspicer/hog…
aspicer Jun 11, 2026
bfef960
fix(hogql): narrow JSONFieldAccess type to ConstantType in semantic-t…
aspicer Jun 11, 2026
b83dfa5
Merge branch 'aspicer/hogql-rearch-15-clean-cutover' into aspicer/hog…
aspicer Jun 11, 2026
b689eea
test(backend): update query snapshots
tests-posthog[bot] Jun 11, 2026
380d2d6
Merge remote-tracking branch 'origin/master' into aspicer/hogql-rearc…
aspicer Jun 11, 2026
1d530df
Merge branch 'aspicer/hogql-rearch-15-clean-cutover' into aspicer/hog…
aspicer Jun 11, 2026
3ccc117
refactor(hogql): rename JSONFieldAccess to PropertyAccess
aspicer Jun 11, 2026
209d822
Merge branch 'aspicer/hogql-rearch-15-clean-cutover' into aspicer/hog…
aspicer Jun 11, 2026
8da411e
refactor(hogql): rename JSONFieldAccess to PropertyAccess in pushdown
aspicer Jun 11, 2026
eba13a4
test(backend): update query snapshots
tests-posthog[bot] Jun 11, 2026
7b6ed40
Merge commit 'daf80a59557' into aspicer/hogql-rearch-16-pushdown-rede…
aspicer Jun 11, 2026
964c737
perf(hogql): trust resolved types for subquery column nullability
aspicer Jun 11, 2026
6fe5d82
test(backend): update query snapshots
tests-posthog[bot] Jun 11, 2026
31d095e
test(hogql): expect bare comparisons over non-nullable view columns
aspicer Jun 11, 2026
244f3b5
test(backend): update query snapshots
tests-posthog[bot] Jun 11, 2026
c2953d1
Merge branch 'master' into aspicer/hogql-rearch-16-pushdown-redesign
aspicer Jun 11, 2026
01ba67b
test(hogql): pin comparison wrapping to subquery column nullability
aspicer Jun 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions ee/clickhouse/models/test/__snapshots__/test_cohort.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@
cohortpeople.team_id AS team_id
FROM cohortpeople
WHERE and(equals(cohortpeople.team_id, 99999), in(tuple(cohortpeople.cohort_id, cohortpeople.version), [(99999, 0)]))) AS cohort_people
WHERE and(ifNull(equals(cohort_people.cohort_id, 99999), 0), ifNull(equals(cohort_people.team_id, 99999), 0))
WHERE and(equals(cohort_people.cohort_id, 99999), equals(cohort_people.team_id, 99999))
LIMIT 1000000000))) as person SETTINGS optimize_aggregation_in_order = 1,
join_algorithm = 'auto'
'''
Expand Down Expand Up @@ -217,7 +217,7 @@
cohortpeople.team_id AS team_id
FROM cohortpeople
WHERE and(equals(cohortpeople.team_id, 99999), in(tuple(cohortpeople.cohort_id, cohortpeople.version), [(99999, 0)]))) AS cohort_people
WHERE and(ifNull(equals(cohort_people.cohort_id, 99999), 0), ifNull(equals(cohort_people.team_id, 99999), 0))
WHERE and(equals(cohort_people.cohort_id, 99999), equals(cohort_people.team_id, 99999))
LIMIT 1000000000))) as person SETTINGS optimize_aggregation_in_order = 1,
join_algorithm = 'auto'
'''
Expand Down Expand Up @@ -272,7 +272,7 @@
cohortpeople.team_id AS team_id
FROM cohortpeople
WHERE 0) AS cohort_people
WHERE and(ifNull(equals(cohort_people.cohort_id, 99999), 0), ifNull(equals(cohort_people.team_id, 99999), 0))))))
WHERE and(equals(cohort_people.cohort_id, 99999), equals(cohort_people.team_id, 99999))))))
LIMIT 1000000000)))
'''
# ---
Expand Down Expand Up @@ -399,7 +399,7 @@
cohortpeople.team_id AS team_id
FROM cohortpeople
WHERE and(equals(cohortpeople.team_id, 99999), in(tuple(cohortpeople.cohort_id, cohortpeople.version), [(99999, 0)]))) AS cohort_people
WHERE and(ifNull(equals(cohort_people.cohort_id, 99999), 0), ifNull(equals(cohort_people.team_id, 99999), 0))
WHERE and(equals(cohort_people.cohort_id, 99999), equals(cohort_people.team_id, 99999))
LIMIT 1000000000))) as person SETTINGS optimize_aggregation_in_order = 1,
join_algorithm = 'auto'
'''
Expand Down Expand Up @@ -447,7 +447,7 @@
cohortpeople.team_id AS team_id
FROM cohortpeople
WHERE and(equals(cohortpeople.team_id, 99999), in(tuple(cohortpeople.cohort_id, cohortpeople.version), [(99999, 0)]))) AS cohort_people
WHERE and(ifNull(equals(cohort_people.cohort_id, 99999), 0), ifNull(equals(cohort_people.team_id, 99999), 0))
WHERE and(equals(cohort_people.cohort_id, 99999), equals(cohort_people.team_id, 99999))
LIMIT 1000000000))) as person SETTINGS optimize_aggregation_in_order = 1,
join_algorithm = 'auto'
'''
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@
/* user_id:0 request:_snapshot_ */
SELECT groupArray(1)(date)[1] AS date,
arrayFold((acc, x) -> arrayMap(i -> plus(acc[i], x[i]), range(1, plus(length(date), 1))), groupArray(ifNull(total, 0)), arrayWithConstant(length(date), reinterpretAsFloat64(0))) AS total,
if(ifNull(ifNull(greaterOrEquals(row_number, 25), 0), 0), '$$_posthog_breakdown_other_$$', breakdown_value) AS breakdown_value
if(ifNull(greaterOrEquals(row_number, 25), 0), '$$_posthog_breakdown_other_$$', breakdown_value) AS breakdown_value
FROM
(SELECT arrayMap(number -> plus(toStartOfInterval(assumeNotNull(toDateTime('2012-01-01 00:00:00', 'UTC')), toIntervalDay(1)), toIntervalDay(number)), range(0, plus(coalesce(dateDiff('day', toStartOfInterval(assumeNotNull(toDateTime('2012-01-01 00:00:00', 'UTC')), toIntervalDay(1)), toStartOfInterval(assumeNotNull(toDateTime('2012-01-15 23:59:59', 'UTC')), toIntervalDay(1)))), 1))) AS date,
arrayFill(x -> ifNull(greater(x, 0), 0), arrayMap(_match_date -> arraySum(arraySlice(groupArray(ifNull(count, 0)), indexOf(groupArray(day_start) AS _days_for_count, _match_date) AS _index, plus(minus(arrayLastIndex(x -> ifNull(equals(x, _match_date), isNull(x)
Expand Down Expand Up @@ -198,7 +198,7 @@
/* user_id:0 request:_snapshot_ */
SELECT groupArray(1)(date)[1] AS date,
arrayFold((acc, x) -> arrayMap(i -> plus(acc[i], x[i]), range(1, plus(length(date), 1))), groupArray(ifNull(total, 0)), arrayWithConstant(length(date), reinterpretAsFloat64(0))) AS total,
if(ifNull(ifNull(greaterOrEquals(row_number, 25), 0), 0), '$$_posthog_breakdown_other_$$', breakdown_value) AS breakdown_value
if(ifNull(greaterOrEquals(row_number, 25), 0), '$$_posthog_breakdown_other_$$', breakdown_value) AS breakdown_value
FROM
(SELECT arrayMap(number -> plus(toStartOfInterval(assumeNotNull(toDateTime('2012-01-01 00:00:00', 'UTC')), toIntervalDay(1)), toIntervalDay(number)), range(0, plus(coalesce(dateDiff('day', toStartOfInterval(assumeNotNull(toDateTime('2012-01-01 00:00:00', 'UTC')), toIntervalDay(1)), toStartOfInterval(assumeNotNull(toDateTime('2012-01-15 23:59:59', 'UTC')), toIntervalDay(1)))), 1))) AS date,
arrayFill(x -> ifNull(greater(x, 0), 0), arrayMap(_match_date -> arraySum(arraySlice(groupArray(ifNull(count, 0)), indexOf(groupArray(day_start) AS _days_for_count, _match_date) AS _index, plus(minus(arrayLastIndex(x -> ifNull(equals(x, _match_date), isNull(x)
Expand Down Expand Up @@ -250,11 +250,11 @@
CROSS JOIN
(SELECT minus(toStartOfInterval(assumeNotNull(toDateTime('2012-01-15 23:59:59', 'UTC')), toIntervalDay(1)), toIntervalDay(numbers.number)) AS timestamp
FROM numbers(dateDiff('day', minus(toStartOfInterval(assumeNotNull(toDateTime('2012-01-01 00:00:00', 'UTC')), toIntervalDay(1)), toIntervalDay(7)), assumeNotNull(toDateTime('2012-01-15 23:59:59', 'UTC')))) AS numbers) AS d
WHERE and(ifNull(lessOrEquals(e.timestamp, d.timestamp), 0), ifNull(greater(e.timestamp, minus(d.timestamp, toIntervalDay(7))), 0))
WHERE and(lessOrEquals(e.timestamp, d.timestamp), greater(e.timestamp, minus(d.timestamp, toIntervalDay(7))))
GROUP BY d.timestamp,
breakdown_value
ORDER BY d.timestamp ASC)
WHERE and(ifNull(greaterOrEquals(timestamp, toStartOfInterval(assumeNotNull(toDateTime('2012-01-01 00:00:00', 'UTC')), toIntervalDay(1))), 0), ifNull(lessOrEquals(timestamp, assumeNotNull(toDateTime('2012-01-15 23:59:59', 'UTC'))), 0)))
WHERE and(greaterOrEquals(timestamp, toStartOfInterval(assumeNotNull(toDateTime('2012-01-01 00:00:00', 'UTC')), toIntervalDay(1))), lessOrEquals(timestamp, assumeNotNull(toDateTime('2012-01-15 23:59:59', 'UTC')))))
GROUP BY day_start,
breakdown_value
ORDER BY day_start ASC,
Expand Down Expand Up @@ -284,7 +284,7 @@
/* user_id:0 request:_snapshot_ */
SELECT groupArray(1)(date)[1] AS date,
arrayFold((acc, x) -> arrayMap(i -> plus(acc[i], x[i]), range(1, plus(length(date), 1))), groupArray(ifNull(total, 0)), arrayWithConstant(length(date), reinterpretAsFloat64(0))) AS total,
if(ifNull(ifNull(greaterOrEquals(row_number, 25), 0), 0), '$$_posthog_breakdown_other_$$', breakdown_value) AS breakdown_value
if(ifNull(greaterOrEquals(row_number, 25), 0), '$$_posthog_breakdown_other_$$', breakdown_value) AS breakdown_value
FROM
(SELECT arrayMap(number -> plus(toStartOfInterval(assumeNotNull(toDateTime('2012-01-01 00:00:00', 'UTC')), toIntervalDay(1)), toIntervalDay(number)), range(0, plus(coalesce(dateDiff('day', toStartOfInterval(assumeNotNull(toDateTime('2012-01-01 00:00:00', 'UTC')), toIntervalDay(1)), toStartOfInterval(assumeNotNull(toDateTime('2012-01-15 23:59:59', 'UTC')), toIntervalDay(1)))), 1))) AS date,
arrayFill(x -> ifNull(greater(x, 0), 0), arrayMap(_match_date -> arraySum(arraySlice(groupArray(ifNull(count, 0)), indexOf(groupArray(day_start) AS _days_for_count, _match_date) AS _index, plus(minus(arrayLastIndex(x -> ifNull(equals(x, _match_date), isNull(x)
Expand Down
37 changes: 37 additions & 0 deletions posthog/hogql/ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -983,6 +983,36 @@ class Tuple(Expr):
exprs: list[Expr]


@dataclass(kw_only=True, slots=True)
class PropertyAccess(Expr):
"""The lowered form of a `properties.X` read: "read this key path from this properties field."

The properties field is treated as an abstract key-value document — how the value is physically served (raw JSON
extract, materialized column, property-group map) is decided by later passes, never encoded in this node.

This is what a blob property read becomes after logical lowering. The contrast that matters is with
`PropertyType`, the read's *type* before lowering. A `PropertyType` means "this is an unresolved property — nobody has
turned it into anything concrete yet." A `PropertyAccess` is that property **lowered to its concrete pieces**: `expr`
is the blob column (e.g. `events.properties`), `keys` is the path into it, and the node's own type is the value type of
the read — a nullable String. It has dropped the "I'm an unresolved property" meaning (it carries *no* `PropertyType`),
so nothing downstream mistakes it for one and re-runs property logic on it.

It does **not** decide where the value ultimately comes from. Lowering produces a `PropertyAccess` for *every* blob
property — a materialized one and a raw one look identical at this stage. On ClickHouse, the next pass (the
materialized-column / property-group / skip-index physical passes) may rewrite this node to a concrete column read — a
faster source of the *same* value — when a backing column exists. Raw JSON is only the **default rendering if nothing
rewrites it**: each printer renders a surviving node mechanically in its own JSON syntax via `visit_property_access`
(ClickHouse `JSONExtractRaw` + null/quote scrub; Postgres/DuckDB `->`/`->>`) and makes no physical-column decision —
that is the whole point of this split. For the warehouse dialects there is no second pass, so the node always renders
as the extract. `keys` mirrors a `PropertyType.chain` —
string object keys and integer array indices, passed through untyped so each dialect's `_json_property_args` handles
them just like the legacy `visit_property_type` blob fallback did.
"""

expr: Expr
keys: list[str | int]


@dataclass(kw_only=True, slots=True)
class Lambda(Expr):
args: list[str]
Expand All @@ -992,6 +1022,13 @@ class Lambda(Expr):
@dataclass(kw_only=True, slots=True)
class Constant(Expr):
value: Any
# Internal ClickHouse-printer hint, not part of the logical AST. Set True only by the physical pass, only for its
# fixed scrubbing sentinels (the nullIf ''/'null' literals, the quote-trim regex, the 'true'/'false' the property
# group stores booleans as), to render them inline instead of as a bound parameter — matching the inline string the
# `json_extract_trim_quotes` helper emits. The printer enforces a fixed allowlist (`INLINE_SENTINEL_LITERALS`), so it
# can never inline arbitrary text. Unset (None) by default, like `type`, so the "skip None" AST serializers (Hog
# compilers, `pretty_dataclasses` snapshots) leave it out.
inline_sentinel: bool | None = None


# Allowlist for `Keyword.name`; the SQL printer interpolates it verbatim (CH returns `name` directly, Postgres uppercases). Restricted to the Postgres-family time pseudo-functions from `resolver.POSTGRES_KEYWORD_TYPES` — a broader `str.isidentifier()` check would still admit arbitrary Python identifiers and let them emit as unquoted ClickHouse tokens.
Expand Down
Loading
Loading