diff --git a/lib/model/common/src/drivers/pframe/query/query_common.ts b/lib/model/common/src/drivers/pframe/query/query_common.ts index 89289ae17a..3351eb5c3c 100644 --- a/lib/model/common/src/drivers/pframe/query/query_common.ts +++ b/lib/model/common/src/drivers/pframe/query/query_common.ts @@ -477,6 +477,56 @@ export interface ExprIsIn { set: T[]; } +// ============ Ranking Expression ============ + +/** + * Ranking function kind. + * + * - `rank` - Standard rank: 1,2,2,4 (gaps after ties) + * - `denseRank` - Dense rank: 1,2,2,3 (no gaps) + * - `rowNumber` - Row number: 1,2,3,4 (no ties) + */ +export type RankingKind = "rank" | "denseRank" | "rowNumber"; + +/** + * Ranking expression (window function). + * + * Assigns ranks to records within partitions based on explicit ordering. + * Each unique combination of axis values is a record. + * Records are grouped by `partitionBy` values, then ordered by `orderBy` expression + * within each partition. Ties are broken deterministically by axis values. + * + * **Output**: Numeric rank value. + * + * @template I - The expression type (for recursion) + * @template A - Axis selector type + * @template C - Column selector type + * + * @example + * // Rank rows by score descending + * { type: 'ranking', kind: 'rank', orderBy: scoreRef, ascending: false } + * + * // Dense rank within partitions + * { + * type: 'ranking', + * kind: 'denseRank', + * orderBy: valueRef, + * ascending: true, + * partitionBy: [{ type: 'axis', id: 'sample' }] + * } + */ +export interface ExprRanking { + type: "ranking"; + /** Ranking function kind */ + kind: RankingKind; + /** Expression to order by */ + orderBy: I; + /** Ascending or descending order */ + ascending?: boolean; + /** Partition specification — ranking is computed independently within each partition */ + partitionBy?: (QueryAxisSelector | QueryColumnSelector)[]; +} + // ============ Reference Expression Types ============ /** diff --git a/lib/model/common/src/drivers/pframe/query/query_data.ts b/lib/model/common/src/drivers/pframe/query/query_data.ts index be65246194..4f9b366322 100644 --- a/lib/model/common/src/drivers/pframe/query/query_data.ts +++ b/lib/model/common/src/drivers/pframe/query/query_data.ts @@ -15,6 +15,7 @@ import type { ExprStringEquals, ExprStringRegex, ExprNumericUnary, + ExprRanking, QueryAxisSelector, QueryColumn, QuerySparseToDenseColumn, @@ -118,6 +119,7 @@ export type DataQueryExpression = | ExprLogicalUnary | ExprLogicalVariadic | ExprIsIn - | ExprIsIn; + | ExprIsIn + | ExprRanking; export type DataQueryBooleanExpression = InferBooleanExpressionUnion; diff --git a/lib/model/common/src/drivers/pframe/query/query_spec.ts b/lib/model/common/src/drivers/pframe/query/query_spec.ts index 2d33cdba32..41af936c42 100644 --- a/lib/model/common/src/drivers/pframe/query/query_spec.ts +++ b/lib/model/common/src/drivers/pframe/query/query_spec.ts @@ -15,6 +15,7 @@ import type { ExprStringEquals, ExprStringRegex, ExprNumericUnary, + ExprRanking, QueryAxisSelector, QueryColumn, QuerySparseToDenseColumn, @@ -137,6 +138,7 @@ export type SpecQueryExpression = | ExprLogicalUnary | ExprLogicalVariadic | ExprIsIn - | ExprIsIn; + | ExprIsIn + | ExprRanking; export type SpecQueryBooleanExpression = InferBooleanExpressionUnion; diff --git a/sdk/model/src/filters/converters/filterToQuery.ts b/sdk/model/src/filters/converters/filterToQuery.ts index 619250ec80..6d9d8bd92d 100644 --- a/sdk/model/src/filters/converters/filterToQuery.ts +++ b/sdk/model/src/filters/converters/filterToQuery.ts @@ -254,8 +254,29 @@ function leafToSpecQueryExpr>( }; case "topN": + return { + type: "numericComparison", + operand: "le", + left: { + type: "ranking", + kind: "rank", + orderBy: resolveColumnRef(filter.column), + ascending: false, + }, + right: { type: "constant", value: filter.n }, + }; case "bottomN": - throw new Error(`Filter type "${filter.type}" is not supported in query expressions`); + return { + type: "numericComparison", + operand: "le", + left: { + type: "ranking", + kind: "rank", + orderBy: resolveColumnRef(filter.column), + ascending: true, + }, + right: { type: "constant", value: filter.n }, + }; case undefined: throw new Error("Filter type is undefined"); diff --git a/sdk/ui-vue/src/components/PlTableFilters/PlTableFiltersV2.vue b/sdk/ui-vue/src/components/PlTableFilters/PlTableFiltersV2.vue index 76344c7105..44f1045713 100644 --- a/sdk/ui-vue/src/components/PlTableFilters/PlTableFiltersV2.vue +++ b/sdk/ui-vue/src/components/PlTableFilters/PlTableFiltersV2.vue @@ -27,6 +27,7 @@ import { import type { PlAdvancedFilterColumnId } from "../PlAdvancedFilter/types"; import type { Nil } from "@milaboratories/helpers"; import { isNil } from "es-toolkit"; +import { useFeatureFlags } from "../../defineApp"; const model = defineModel({ required: true }); const props = defineProps<{ @@ -89,6 +90,10 @@ const supportedFilters = [ "notEqual", ] as (typeof PlAdvancedFilterSupportedFilters)[number][]; +if (useFeatureFlags().supportsPframeQueryRanking) { + supportedFilters.push("topN", "bottomN"); +} + // getSuggestOptions - provide discrete values from column annotations function handleSuggestOptions(params: { columnId: PlAdvancedFilterColumnId;