content: Base query building#1462
Conversation
|
There was a problem hiding this comment.
Pull request overview
This PR implements the shared base content-query builder for Stage 3/4 query generation in @itwin/presentation-content, centralizing common query fragments (FROM / JOIN / WHERE) and extracting reusable helpers for target filters, value filters, and SQLite query-limit handling. It also updates the public value-filter API shape and adds extensive Vitest coverage for the new query-building behavior.
Changes:
- Added
buildBaseQueryto assemble shared FROM/JOIN/WHERE fragments, handle query-filterer injection, and split related joins to respect SQLite JOIN-table limits. - Extracted shared helpers for target filtering (
buildTargetFilter), value filtering (buildValueFilterClauses), and query-limit utilities (QueryLimits). - Updated
ContentValueFilterpublic typing (includingis-not-in) and refreshed API report + tests.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/content/src/test/query/ValueFilters.test.ts | Adds unit tests for value-filter clause generation and binding typing. |
| packages/content/src/test/query/TargetFilter.test.ts | Adds unit tests for shared target-filter join/where/bindings behavior. |
| packages/content/src/test/query/QueryLimits.test.ts | Adds unit tests for join-budget partitioning and compound-select chunking helpers. |
| packages/content/src/test/query/BaseQuery.test.ts | Adds comprehensive tests for base-query assembly, aliasing, filters, and split-group behavior. |
| packages/content/src/test/model/Utils.test.ts | Extends tests for relationship-path serialization (including instance filters). |
| packages/content/src/test/Content.test.ts | Updates expectations to use the extracted TARGET_FILTER_JOIN_ALIAS. |
| packages/content/src/content/ResolveContentSources.ts | Switches to shared buildTargetFilter and standardizes primary alias usage via PRIMARY_CLASS_ALIAS. |
| packages/content/src/content/query/ValueFilters.ts | Introduces helper for translating ContentValueFilter[] into WHERE + bindings. |
| packages/content/src/content/query/TargetFilter.ts | Introduces shared helper for instanceIds/instanceFilter target filtering. |
| packages/content/src/content/query/QueryLimits.ts | Introduces SQLite join/compound-query limit helpers used by base-query splitting. |
| packages/content/src/content/query/BaseQuery.ts | Introduces shared base-query builder with deterministic aliasing, join splitting, and filter integration. |
| packages/content/src/content/model/Utils.ts | Enhances relationship-path serialization to optionally include step instance filters. |
| packages/content/src/content/InternalUtils.ts | Adds PRIMARY_CLASS_ALIAS and alias-substitution helper used across query builders. |
| packages/content/src/content/extensions/ecexpressions/Emitter.ts | Aligns default primary alias handling with PRIMARY_CLASS_ALIAS. |
| packages/content/src/content/Content.ts | Refines/extends public ContentValueFilter typing (incl. is-not-in) and value constraints. |
| packages/content/api/presentation-content.api.md | Updates extracted public API report to match the new exported type shapes. |
| if (includeInstanceFilters && step.instanceFilter) { | ||
| result += `{${JSON.stringify(step.instanceFilter)}}`; | ||
| } |
| if (target.instanceFilter.bindings) { | ||
| Object.assign(bindings, target.instanceFilter.bindings); | ||
| } |
| props.filters.forEach((filter, filterIndex) => { | ||
| const resolved = props.resolveSelector(filter.field, filter.member); | ||
| // Navigation properties are structs whose target instance id lives in a `.Id` member, so filter | ||
| // against that member rather than the raw navigation column. | ||
| const selector = | ||
| filter.field.type.kind === "navigation" ? { ...resolved, selector: `${resolved.selector}.[Id]` } : resolved; | ||
| const clause = buildFilterClause({ filter, selector, filterIndex, bindings }); | ||
| clauses.push(clause); | ||
| }); |
| // Shared-prefix steps contribute identical bindings; keep the first occurrence of each key. | ||
| for (const [name, binding] of Object.entries(info.bindings ?? {})) { | ||
| if (!(name in bindings)) { | ||
| bindings[name] = binding; | ||
| } | ||
| } |
| /** @public */ | ||
| type ScalarValueFilterOperator = Exclude<ValueFilterOperator, "is-null" | "is-not-null" | "is-in" | "is-not-in">; |
| export type ContentValueFilter = | ||
| | (ContentValueFilterTarget & { | ||
| /** The filter operator. */ | ||
| operator: ScalarValueFilterOperator; | ||
| /** The scalar value to compare against. Points are filtered per-coordinate via `member`, so a whole-point value is not accepted. */ |
Closes #1409.