Spaces:
Running
Running
Download source/teaql-ts/src/core/facet.ts from teaql/browser-sqlite-playground: direct link, hf CLI and curl.
- Browser
- Download file 9.89 kB
-
https://huggingface.co/spaces/teaql/browser-sqlite-playground/resolve/main/source/teaql-ts/src/core/facet.ts
- Command line
-
hf download hf://spaces/teaql/browser-sqlite-playground/source/teaql-ts/src/core/facet.ts
-
curl -L -o facet.ts https://huggingface.co/spaces/teaql/browser-sqlite-playground/resolve/main/source/teaql-ts/src/core/facet.ts
9.89 kB
| import { FacetRequest, SelectQuery } from './ast'; | |
| import { SmartList, SmartListRecord } from './smart-list'; | |
| import { QueryRequest } from './request-intent'; | |
| import { retainQueryDiagnosticOrigin } from './query-snapshot'; | |
| export interface FacetAggregate { | |
| function: string; | |
| field: string; | |
| alias: string; | |
| } | |
| export interface FacetQueryService { | |
| executeQuery(query: SelectQuery): Promise<SmartListRecord[]>; | |
| executeFacetAggregates?( | |
| outerQuery: SelectQuery, | |
| relationName: string, | |
| aggregates: readonly FacetAggregate[], | |
| ): Promise<Map<string, SmartListRecord>>; | |
| executeFacetMembership?( | |
| outerQuery: SelectQuery, relationName: string, countAliases?: readonly string[], | |
| ): Promise<Map<string, number>>; | |
| } | |
| function snakeCase(value: string): string { | |
| return value.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase(); | |
| } | |
| function scalarId(value: unknown): unknown { | |
| if (value && typeof value === 'object') { | |
| const record = value as Record<string, unknown>; | |
| return record.id ?? record.Id; | |
| } | |
| return value; | |
| } | |
| function relationId(row: SmartListRecord, relationName: string): unknown { | |
| const snake = snakeCase(relationName); | |
| for (const key of [relationName, `${relationName}Id`, snake, `${snake}_id`]) { | |
| const value = scalarId(row[key]); | |
| if (value !== undefined && value !== null) return value; | |
| } | |
| return undefined; | |
| } | |
| function emptyAggregateValue(aggregate: FacetAggregate): unknown { | |
| const fn = aggregate.function.toLowerCase(); | |
| return fn === 'count' || fn === 'sum' ? 0 : null; | |
| } | |
| function compareAggregateValues(left: unknown, right: unknown): number { | |
| if (typeof left === 'number' && typeof right === 'number') return left - right; | |
| if (left instanceof Date && right instanceof Date) return left.getTime() - right.getTime(); | |
| return String(left).localeCompare(String(right)); | |
| } | |
| async function executeFacetAggregatesFallback( | |
| service: FacetQueryService, | |
| prepareQuery: (query: SelectQuery) => SelectQuery, | |
| request: QueryRequest, | |
| outerQuery: SelectQuery, | |
| relationName: string, | |
| aggregates: readonly FacetAggregate[], | |
| ): Promise<Map<string, SmartListRecord>> { | |
| const membershipQuery = outerQuery.clone(); | |
| membershipQuery.facets = []; | |
| membershipQuery.relations = []; | |
| membershipQuery.relationAggregates = []; | |
| membershipQuery.orderItems = []; | |
| membershipQuery.aggregateItems = []; | |
| membershipQuery.groupByItems = []; | |
| membershipQuery.offsetValue = 0; | |
| membershipQuery.limitValue = 0; | |
| membershipQuery.selectItems = [...new Set([ | |
| relationName, | |
| ...aggregates.map(aggregate => aggregate.field), | |
| ])]; | |
| membershipQuery.comment(`aggregate ${relationName}`); | |
| retainQueryDiagnosticOrigin(outerQuery, membershipQuery); | |
| const memberships = await service.executeQuery( | |
| request.deriveHelper(prepareQuery(membershipQuery), `aggregate ${relationName}`).query, | |
| ); | |
| const result = new Map<string, SmartListRecord>(); | |
| const averageCounts = new Map<string, Record<string, number>>(); | |
| for (const row of memberships) { | |
| const id = relationId(row, relationName); | |
| if (id === undefined || id === null) continue; | |
| const key = String(id); | |
| const values = result.get(key) ?? {}; | |
| const counts = averageCounts.get(key) ?? {}; | |
| for (const aggregate of aggregates) { | |
| const fn = aggregate.function.toLowerCase(); | |
| const raw = row[aggregate.field]; | |
| if (fn === 'count') { | |
| if (raw !== undefined && raw !== null) { | |
| values[aggregate.alias] = Number(values[aggregate.alias] ?? 0) + 1; | |
| } | |
| } else if (fn === 'sum' || fn === 'avg') { | |
| if (raw !== undefined && raw !== null) { | |
| const numeric = Number(raw); | |
| if (!Number.isFinite(numeric)) { | |
| throw new Error(`Facet aggregate ${aggregate.alias} requires numeric values`); | |
| } | |
| values[aggregate.alias] = Number(values[aggregate.alias] ?? 0) + numeric; | |
| if (fn === 'avg') counts[aggregate.alias] = (counts[aggregate.alias] ?? 0) + 1; | |
| } | |
| } else if (fn === 'min' || fn === 'max') { | |
| if (raw !== undefined && raw !== null) { | |
| const current = values[aggregate.alias]; | |
| const comparison = current === undefined || current === null | |
| ? 0 | |
| : compareAggregateValues(raw, current); | |
| if (current === undefined || current === null | |
| || (fn === 'min' && comparison < 0) | |
| || (fn === 'max' && comparison > 0)) { | |
| values[aggregate.alias] = raw; | |
| } | |
| } | |
| } else { | |
| throw new Error(`Unsupported facet aggregate: ${aggregate.function}`); | |
| } | |
| } | |
| result.set(key, values); | |
| averageCounts.set(key, counts); | |
| } | |
| for (const [key, values] of result) { | |
| const counts = averageCounts.get(key) ?? {}; | |
| for (const aggregate of aggregates) { | |
| if (aggregate.function.toLowerCase() !== 'avg') continue; | |
| const count = counts[aggregate.alias] ?? 0; | |
| values[aggregate.alias] = count ? Number(values[aggregate.alias] ?? 0) / count : null; | |
| } | |
| } | |
| return result; | |
| } | |
| /** | |
| * Execute relation facets without crossing entity boundaries with the outer | |
| * filter. The outer query determines relation membership; the nested query | |
| * determines which facet entities and fields are returned. | |
| */ | |
| export async function executeRelationFacets( | |
| service: FacetQueryService, | |
| prepareQuery: (query: SelectQuery) => SelectQuery, | |
| outerQuery: SelectQuery, | |
| facets: readonly FacetRequest[], | |
| ): Promise<Record<string, SmartList<SmartListRecord>>> { | |
| const request = new QueryRequest(outerQuery); | |
| outerQuery = request.query; | |
| const result: Record<string, SmartList<SmartListRecord>> = {}; | |
| for (const facet of facets) { | |
| const operation = request.deriveOperation(outerQuery, facet.operation ?? { | |
| kind: 'facet', relation: facet.relationName, alias: facet.facetName, | |
| }); | |
| const nestedRequest = operation.deriveChild(facet.query.clone(), facet.relationName); | |
| const nestedQuery = nestedRequest.query; | |
| const nestedFacets = nestedQuery.facets; | |
| nestedQuery.facets = []; | |
| const requestedAggregates: FacetAggregate[] = nestedQuery.aggregateItems.map(item => ({ | |
| function: String(item.function), | |
| field: String(item.field), | |
| alias: String(item.alias), | |
| })); | |
| const executedAggregates = requestedAggregates.length | |
| ? requestedAggregates | |
| : [{ function: 'Count', field: 'id', alias: '__teaql_facet_membership_count' }]; | |
| let aggregateValues: Map<string, SmartListRecord>; | |
| if (service.executeFacetAggregates) { | |
| aggregateValues = await service.executeFacetAggregates( | |
| operation.deriveHelper( | |
| prepareQuery(outerQuery.clone()), `aggregate ${facet.relationName}`, | |
| ).query, | |
| facet.relationName, | |
| executedAggregates, | |
| ); | |
| } else if (service.executeFacetMembership | |
| && executedAggregates.every(item => item.function.toLowerCase() === 'count')) { | |
| const counts = await service.executeFacetMembership( | |
| operation.deriveHelper( | |
| prepareQuery(outerQuery.clone()), `aggregate ${facet.relationName}`, | |
| ).query, | |
| facet.relationName, | |
| ); | |
| aggregateValues = new Map([...counts].map(([key, count]) => [ | |
| key, | |
| Object.fromEntries(executedAggregates.map(item => [item.alias, count])), | |
| ])); | |
| } else { | |
| aggregateValues = await executeFacetAggregatesFallback( | |
| service, prepareQuery, operation, outerQuery, facet.relationName, executedAggregates, | |
| ); | |
| } | |
| nestedQuery.aggregateItems = []; | |
| nestedQuery.groupByItems = []; | |
| for (const aggregate of nestedQuery.relationAggregates) { | |
| if (aggregate.query.entity !== outerQuery.entity) continue; | |
| // Reverse facet sums share the complete source membership, not the | |
| // returned page or all source rows pointing at the same facet value. | |
| const membership = outerQuery.clone(); | |
| membership.selectItems = ['id']; | |
| membership.properties = []; | |
| membership.relations = []; | |
| membership.relationAggregates = []; | |
| membership.facets = []; | |
| membership.aggregateItems = []; | |
| membership.groupByItems = []; | |
| membership.orderItems = []; | |
| membership.offsetValue = 0; | |
| membership.limitValue = 0; | |
| const filter = { id: { $inSubquery: { query: membership, field: 'id' } } }; | |
| aggregate.query.filterCondition = aggregate.query.filterCondition | |
| ? { $and: [aggregate.query.filterCondition, filter] } : filter; | |
| retainQueryDiagnosticOrigin(outerQuery, aggregate.query); | |
| } | |
| // Restrict before pagination and before evaluating child facets. Filtering | |
| // only the returned rows makes child counts include unrelated entities. | |
| if (!facet.includeAllFacets) { | |
| const membership = { id: { $in: [...aggregateValues.keys()] } }; | |
| nestedQuery.filterCondition = nestedQuery.filterCondition | |
| ? { $and: [nestedQuery.filterCondition, membership] } : membership; | |
| } | |
| retainQueryDiagnosticOrigin(outerQuery, nestedQuery); | |
| const rows = await service.executeQuery(nestedRequest.withQuery(prepareQuery(nestedQuery)).query); | |
| const decorated = rows | |
| .map(row => { | |
| const key = String(scalarId(row.id ?? row.Id)); | |
| const values = aggregateValues.get(key); | |
| const copy = { ...row }; | |
| for (const aggregate of requestedAggregates) { | |
| copy[aggregate.alias] = values?.[aggregate.alias] ?? emptyAggregateValue(aggregate); | |
| } | |
| return copy; | |
| }) | |
| .filter(row => facet.includeAllFacets | |
| || aggregateValues.has(String(scalarId(row.id ?? row.Id)))); | |
| const list = new SmartList(decorated); | |
| if (nestedFacets.length) { | |
| list.facets = await executeRelationFacets(service, prepareQuery, | |
| nestedRequest.withQuery(nestedQuery).query, nestedFacets); | |
| } | |
| result[facet.facetName] = list; | |
| } | |
| return result; | |
| } | |