Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2,646 changes: 1,336 additions & 1,310 deletions proto/gen/rill/runtime/v1/resources.pb.go

Large diffs are not rendered by default.

31 changes: 31 additions & 0 deletions proto/gen/rill/runtime/v1/resources.pb.validate.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions proto/gen/rill/runtime/v1/runtime.swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6485,6 +6485,14 @@ definitions:
type: object
$ref: '#/definitions/v1SecurityRule'
title: Security for the metrics view
whereSql:
type: string
description: |-
Raw SQL filter that is AND-combined into the WHERE clause of every query against this metrics view.
Behaves like `where_sql` on MetricsViewAggregationRequest: applied automatically by the executor on top of any caller-supplied `where`/`where_sql` and security row filters.
whereExpression:
$ref: '#/definitions/v1Expression'
description: Parsed form of `where_sql`. Only populated in `state.valid_spec` by the reconciler so the UI can render per-condition read-only filter chips without needing a SQL parser. Mirrors the dual `sql`/`expression` pattern on SecurityRuleRowFilter.
firstDayOfWeek:
type: integer
format: int64
Expand Down
5 changes: 5 additions & 0 deletions proto/rill/runtime/v1/resources.proto
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,11 @@ message MetricsViewSpec {
repeated Annotation annotations = 29;
// Security for the metrics view
repeated SecurityRule security_rules = 23;
// Raw SQL filter that is AND-combined into the WHERE clause of every query against this metrics view.
// Behaves like `where_sql` on MetricsViewAggregationRequest: applied automatically by the executor on top of any caller-supplied `where`/`where_sql` and security row filters.
string where_sql = 36;
// Parsed form of `where_sql`. Only populated in `state.valid_spec` by the reconciler so the UI can render per-condition read-only filter chips without needing a SQL parser. Mirrors the dual `sql`/`expression` pattern on SecurityRuleRowFilter.
Expression where_expression = 37;
// ISO 8601 weekday number to use as the base for time aggregations by week. Defaults to 1 (Monday).
uint32 first_day_of_week = 12;
// Month number to use as the base for time aggregations by year. Defaults to 1 (January).
Expand Down
6 changes: 6 additions & 0 deletions runtime/metricsview/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -1043,6 +1043,12 @@ func (a *AST) buildWhereForUnderlyingTable(where *Expression) (*ExprNode, error)
res = res.And(rf, nil)
}

// Inject the metrics view's spec-level `where_sql` so every query on this metrics view is scoped by it.
// Caller-supplied where/where_sql and security row filters are already combined above; this AND's on top.
if ws := a.MetricsView.WhereSql; ws != "" {
res = res.And(ws, nil)
}

return res, nil
}

Expand Down
13 changes: 13 additions & 0 deletions runtime/metricsview/executor/executor_validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/rilldata/rill/runtime"
"github.com/rilldata/rill/runtime/drivers"
"github.com/rilldata/rill/runtime/metricsview"
"github.com/rilldata/rill/runtime/metricsview/metricssql"
"github.com/rilldata/rill/runtime/pkg/fieldselectorpb"
"golang.org/x/sync/errgroup"
)
Expand Down Expand Up @@ -130,6 +131,18 @@ func (e *Executor) ValidateAndNormalizeMetricsView(ctx context.Context) (*Valida
}
}

// Parse the spec-level where_sql so the frontend can render it as filter chips and so we surface syntax errors at reconcile time.
// The executor injects the raw string into queries; the parsed form is only stored for UI consumption.
mv.WhereExpression = nil
if mv.WhereSql != "" {
expr, err := metricssql.ParseFilter(mv.WhereSql)
if err != nil {
res.OtherErrs = append(res.OtherErrs, fmt.Errorf("invalid 'where_sql': %w", err))
} else {
mv.WhereExpression = metricsview.ExpressionToProto(expr)
}
}

// ClickHouse specifically does not support using a column name as a dimension or measure name if the dimension or measure has an expression.
// This is due to ClickHouse's aggressive substitution of aliases: https://github.com/ClickHouse/ClickHouse/issues/9715.
if e.olap.Dialect().String() == drivers.DialectNameClickHouse {
Expand Down
2 changes: 2 additions & 0 deletions runtime/parser/parse_metrics_view.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ type MetricsViewYAML struct {
Measures *FieldSelectorYAML `yaml:"measures"`
} `yaml:"rollups"`
Security *SecurityPolicyYAML
WhereSQL string `yaml:"where_sql"` // Baseline SQL filter applied to every query against this metrics view
QueryAttributes map[string]string `yaml:"query_attributes"`
Cache struct {
Enabled *bool `yaml:"enabled"`
Expand Down Expand Up @@ -919,6 +920,7 @@ func (p *Parser) parseMetricsView(node *Node) error {
}

spec.SecurityRules = securityRules
spec.WhereSql = tmp.WhereSQL
spec.CacheEnabled = tmp.Cache.Enabled
spec.CacheKeySql = tmp.Cache.KeySQL
spec.CacheKeyTtlSeconds = int64(cacheTTLDuration.Seconds())
Expand Down
14 changes: 12 additions & 2 deletions web-common/src/features/canvas/LocalFiltersHeader.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,22 @@
MetricsViewSpecMeasure,
V1TimeRange,
} from "@rilldata/web-common/runtime-client";
import { readable, type Readable } from "svelte/store";
import { derived, readable, type Readable } from "svelte/store";

export let component: BaseCanvasComponent;

let measures: Readable<MetricsViewSpecMeasure[]> = readable([]);
let dimensions: Readable<MetricsViewSpecDimension[]> = readable([]);
let metricsViewWhereSql: Readable<string | undefined> = readable(undefined);

$: ({
specStore,
parent: {
metricsView: { getDimensionsForMetricView, getMeasuresForMetricView },
metricsView: {
getDimensionsForMetricView,
getMeasuresForMetricView,
getMetricsViewFromName,
},
},
timeAndFilterStore,
localFilters,
Expand All @@ -30,6 +35,10 @@
$: if (metricsViewName) {
measures = getMeasuresForMetricView(metricsViewName);
dimensions = getDimensionsForMetricView(metricsViewName);
metricsViewWhereSql = derived(
getMetricsViewFromName(metricsViewName),
($mv) => $mv.metricsView?.whereSql,
);
}

$: ({
Expand Down Expand Up @@ -101,6 +110,7 @@
{dimensionThresholdFilters}
dimensionsWithInlistFilter={dimensionsWithInListFilter}
filters={dimensionFilter}
metricsViewWhereSql={$metricsViewWhereSql}
{displayComparisonTimeRange}
displayTimeRange={hasTimeFilters ? displayTimeRange : undefined}
queryTimeStart={selectedTimeRange?.start?.toISOString()}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@
$spec.metrics_view,
);

$: metricsViewWhereSql = derived(
metricsViewSelectors.getMetricsViewFromName($spec.metrics_view),
($mv) => $mv.metricsView?.whereSql,
);

$: chartDataQuery = chartProvider.createChartDataQuery(
client,
timeAndFilterStore,
Expand Down Expand Up @@ -145,6 +150,7 @@
{dimensionThresholdFilters}
dimensionsWithInlistFilter={[]}
filters={whereFilter}
metricsViewWhereSql={$metricsViewWhereSql}
displayTimeRange={$timeAndFilterStore.timeRange}
displayComparisonTimeRange={$timeAndFilterStore.showTimeComparison
? $timeAndFilterStore.comparisonTimeRange
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@
runtimeClient,
metricsViewNamesStore,
);
$: ({ measures, dimensions } = $combinedMeasuresAndDimensions);
$: ({
measures,
dimensions,
whereSql: metricsViewWhereSql,
} = $combinedMeasuresAndDimensions);
</script>

<FilterChipsReadOnly
Expand All @@ -44,4 +48,5 @@
{displayTimeRange}
{queryTimeStart}
{queryTimeEnd}
{metricsViewWhereSql}
/>
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ The main feature-set component for dashboard filters
import { getMeasureFilters } from "../state-managers/selectors/measure-filters";
import DimensionFilterReadOnlyChip from "./dimension-filters/DimensionFilterReadOnlyChip.svelte";
import MeasureFilterReadOnlyChip from "./measure-filters/MeasureFilterReadOnlyChip.svelte";
import MetricsViewWhereChip from "./MetricsViewWhereChip.svelte";

export let metricsViewNames: string[];
export let dimensions: MetricsViewSpecDimension[];
Expand All @@ -31,6 +32,8 @@ The main feature-set component for dashboard filters
export let queryTimeEnd: string | undefined = undefined;
export let hasBoldTimeRange: boolean = true;
export let chipLayout: "wrap" | "scroll" = "wrap";
// Raw `where_sql` from the metrics view spec, rendered as a single non-clickable chip so users see the locked baseline filter the backend injects into every query.
export let metricsViewWhereSql: string | undefined = undefined;

let scrollContainer: HTMLDivElement;

Expand Down Expand Up @@ -77,6 +80,9 @@ The main feature-set component for dashboard filters
{hasBoldTimeRange}
/>
{/if}
{#if metricsViewWhereSql}
<MetricsViewWhereChip whereSql={metricsViewWhereSql} />
{/if}
{#if dimensionFilters.length > 0}
{#each dimensionFilters as filterData (filterData.name)}
{@const dimension = filterData.dimensions.get(metricsViewNames[0])}
Expand Down
6 changes: 5 additions & 1 deletion web-common/src/features/dashboards/filters/Filters.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import { useTimeControlStore } from "../time-controls/time-control-store";
import FilterButton from "./FilterButton.svelte";
import DimensionFilter from "./dimension-filters/DimensionFilter.svelte";
import MetricsViewWhereChip from "./MetricsViewWhereChip.svelte";
import { featureFlags } from "../../feature-flags";
import Timestamp from "@rilldata/web-common/features/dashboards/time-controls/super-pill/components/Timestamp.svelte";
import { getDefaultTimeGrain } from "@rilldata/web-common/lib/time/grains";
Expand Down Expand Up @@ -509,9 +510,12 @@
<Filter size="16px" className="text-fg-secondary flex-none mt-[5px]" />
{/if}
<div class="relative flex flex-row flex-wrap gap-x-2 gap-y-2">
{#if metricsViewSpec.whereSql}
<MetricsViewWhereChip whereSql={metricsViewSpec.whereSql} />
{/if}
{#if isComplexFilter}
<AdvancedFilter advancedFilter={whereFilter} />
{:else if !allDimensionFilters.length && !allMeasureFilters.length}
{:else if !allDimensionFilters.length && !allMeasureFilters.length && !metricsViewSpec.whereSql}
<div
in:fly={{ duration: 200, x: 8 }}
class="text-fg-muted grid ml-1 items-center"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<!-- @component
A non-clickable chip displaying the metrics view's spec-level `where_sql`.
The backend injects this filter into every query automatically; this chip is purely informational.
-->
<script lang="ts">
import { Chip } from "@rilldata/web-common/components/chip";
import * as Tooltip from "@rilldata/web-common/components/tooltip-v2";

export let whereSql: string;
</script>

<Tooltip.Root>
<Tooltip.Trigger>
<Chip type="dimension" theme readOnly gray label="Dashboard filter">
<span slot="body" class="flex items-center gap-x-1">
<span class="font-bold">Filter</span>
<span class="truncate max-w-[280px]">{whereSql}</span>
</span>
</Chip>
</Tooltip.Trigger>
<Tooltip.Content sideOffset={8}>
<div class="max-w-[480px]">
<div class="font-bold mb-1">Dashboard filter</div>
<div class="whitespace-pre-wrap break-words">{whereSql}</div>
<div class="mt-1 text-fg-muted text-xs">
Applied automatically to every query in this dashboard.
</div>
</div>
</Tooltip.Content>
</Tooltip.Root>
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export function getCombinedMeasuresAndDimensionsForMetricsViews(
const measures: MetricsViewSpecMeasure[] = [];
const seenDimensionNames = new Set<string>();
const dimensions: MetricsViewSpecDimension[] = [];
const whereSqls: string[] = [];

metricsViewQueryResponses.forEach((metricsViewQueryResponse) => {
const spec =
Expand All @@ -53,11 +54,25 @@ export function getCombinedMeasuresAndDimensionsForMetricsViews(
return true;
}),
);

if (spec.whereSql) {
whereSqls.push(spec.whereSql);
}
});

// For multi-metricsview views (e.g. embedded), join the locked filters with AND so the chip
// reflects the conjunction the backend actually applies across all of them.
const whereSql =
whereSqls.length === 0
? undefined
: whereSqls.length === 1
? whereSqls[0]
: whereSqls.map((s) => `(${s})`).join(" AND ");

return {
measures,
dimensions,
whereSql,
};
},
});
Expand Down
17 changes: 17 additions & 0 deletions web-common/src/proto/gen/rill/runtime/v1/resources_pb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1491,6 +1491,21 @@ export class MetricsViewSpec extends Message<MetricsViewSpec> {
*/
securityRules: SecurityRule[] = [];

/**
* Raw SQL filter that is AND-combined into the WHERE clause of every query against this metrics view.
* Behaves like `where_sql` on MetricsViewAggregationRequest: applied automatically by the executor on top of any caller-supplied `where`/`where_sql` and security row filters.
*
* @generated from field: string where_sql = 36;
*/
whereSql = "";

/**
* Parsed form of `where_sql`. Only populated in `state.valid_spec` by the reconciler so the UI can render per-condition read-only filter chips without needing a SQL parser. Mirrors the dual `sql`/`expression` pattern on SecurityRuleRowFilter.
*
* @generated from field: rill.runtime.v1.Expression where_expression = 37;
*/
whereExpression?: Expression;

/**
* ISO 8601 weekday number to use as the base for time aggregations by week. Defaults to 1 (Monday).
*
Expand Down Expand Up @@ -1574,6 +1589,8 @@ export class MetricsViewSpec extends Message<MetricsViewSpec> {
{ no: 32, name: "parent_measures", kind: "message", T: FieldSelector },
{ no: 29, name: "annotations", kind: "message", T: MetricsViewSpec_Annotation, repeated: true },
{ no: 23, name: "security_rules", kind: "message", T: SecurityRule, repeated: true },
{ no: 36, name: "where_sql", kind: "scalar", T: 9 /* ScalarType.STRING */ },
{ no: 37, name: "where_expression", kind: "message", T: Expression },
{ no: 12, name: "first_day_of_week", kind: "scalar", T: 13 /* ScalarType.UINT32 */ },
{ no: 13, name: "first_month_of_year", kind: "scalar", T: 13 /* ScalarType.UINT32 */ },
{ no: 25, name: "cache_enabled", kind: "scalar", T: 8 /* ScalarType.BOOL */, opt: true },
Expand Down
5 changes: 5 additions & 0 deletions web-common/src/runtime-client/gen/index.schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1692,6 +1692,11 @@ export interface V1MetricsViewSpec {
parentMeasures?: V1FieldSelector;
annotations?: V1MetricsViewSpecAnnotation[];
securityRules?: V1SecurityRule[];
/** Raw SQL filter that is AND-combined into the WHERE clause of every query against this metrics view.
Behaves like `where_sql` on MetricsViewAggregationRequest: applied automatically by the executor on top of any caller-supplied `where`/`where_sql` and security row filters. */
whereSql?: string;
/** Parsed form of `where_sql`. Only populated in `state.valid_spec` by the reconciler so the UI can render per-condition read-only filter chips without needing a SQL parser. Mirrors the dual `sql`/`expression` pattern on SecurityRuleRowFilter. */
whereExpression?: V1Expression;
/** ISO 8601 weekday number to use as the base for time aggregations by week. Defaults to 1 (Monday). */
firstDayOfWeek?: number;
/** Month number to use as the base for time aggregations by year. Defaults to 1 (January). */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,16 @@ async function dispatchWrite(

case ResourceKind.MetricsView:
void invalidateMetricsViewData(queryClient, event.name!.name!, failed);
// The Explore resource embeds the parent metrics view spec (via GetExplore), so
// changes to spec-level fields like `where_sql` need to refetch the explore query
// too — otherwise the dashboard reads a stale validSpec until reload.
void queryClient.refetchQueries({
predicate: (query) =>
Array.isArray(query.queryKey) &&
query.queryKey[0] === "RuntimeService" &&
query.queryKey[1] === "getExplore" &&
query.queryKey[2] === instanceId,
});
return;

case ResourceKind.Explore: {
Expand Down
Loading