dbt-labs / dbt-labs/metricflow

[Bug] mf query --start-time/--end-time truncates the bound to a date, so the end bound is not inclusive

Open
#2,129 1 comment 0 reactions 0 assignees View on GitHub
bug triage
Dominant language
Python
Stars
1.8k
Forks
202
Avg merge
1d 8h
Merged PRs (30d)
14

Description

### Is this a new bug in metricflow?

- [x] I believe this is a new bug in metricflow
- [x] I have searched the existing issues, and I could not find an existing issue for this bug

### Current Behavior

`mf query --start-time` and `--end-time` accept an ISO 8601 timestamp, but the time of day is dropped when the time-range constraint is rendered into SQL. The bounds are truncated to a date.

Because the truncated end bound is then interpreted by the warehouse as midnight of that day, every row later than `00:00:00` on the final day is silently excluded. No warning or error is raised, so the result simply undercounts.

This happens even when the time dimension is declared at `second` granularity. The column expression itself does respect the declared granularity (`DATE_TRUNC('second', ordered_at)`); only the constraint literals are truncated.

Given the following command:

```
mf query --metrics order_count --start-time 2026-02-15T00:00:00 --end-time 2026-02-18T23:59:59 --explain
```

the generated SQL is:

```sql
SELECT
SUM(1) AS order_count
FROM .fct_orders fct_orders_src_10000
WHERE DATE_TRUNC('second', ordered_at) BETWEEN '2026-02-15' AND '2026-02-18'
```

The end bound `'2026-02-18'` resolves to `2026-02-18 00:00:00`, so rows on 2026-02-18 are dropped.

A second, related problem: passing a timezone offset raises an unhandled exception.

```
mf query --metrics order_count --start-time 2026-02-15T00:00:00 --end-time 2026-02-18T23:59:59Z
```

```
TypeError: can't compare offset-naive and offset-aware datetimes
```

The CLI help text describes the value as an "iso8601 timestamp" without noting that a timezone offset is not accepted.

### Expected Behavior

The documentation describes `--end-time` as:

> Optional iso8601 timestamp to constraint the end time of the data (inclusive)

Based on that, `--end-time 2026-02-18T23:59:59` should include rows up to and including `2026-02-18 23:59:59`. The time of day supplied on the bound should be preserved when the time-range constraint is rendered, at least when the time dimension is declared at a sub-daily granularity.

For the timezone case, one of the following would be reasonable:

- accept the offset and normalize it, or
- reject it with a clear validation error naming the accepted format, rather than raising a `TypeError` from inside MetricFlow.

### Steps To Reproduce

1. Define a model with a time dimension at `second` granularity and a simple metric.

`models/fct/fct_orders.sql`:

```sql
select
id as order_id,
customer as customer_id,
ordered_at as ordered_at,
order_total as order_total
from {{ source('jaffle_shop', 'orders') }}
```

`models/fct/schema.yml`:

```yaml
models:
- name: fct_orders
semantic_model:
enabled: true
agg_time_dimension: ordered_at
columns:
- name: order_id
entity:
type: primary
name: order

- name: ordered_at
granularity: second
dimension:
type: time

metrics:
- name: order_count
label: Order count
type: simple
agg: sum
expr: 1
```

2. Add a daily time spine (required by the semantic layer) and build both models.

```
dbt run --select time_spine_daily fct_orders
dbt parse
```

3. Make sure the source data contains rows with a time of day other than `00:00:00` on the day you use as the end bound.

4. Run a query whose end bound is the last second of a day, and inspect the generated SQL.

```
mf query --metrics order_count --start-time 2026-02-15T00:00:00 --end-time 2026-02-18T23:59:59 --explain
```

The `WHERE` clause shows `BETWEEN '2026-02-15' AND '2026-02-18'`, with the time of day removed from both bounds.

5. Compare the result against a bound one day later, which shifts the truncated midnight boundary past the final day.

```
mf query --metrics order_count --start-time 2026-02-15T00:00:00 --end-time 2026-02-18T23:59:59
mf query --metrics order_count --start-time 2026-02-15T00:00:00 --end-time 2026-02-19T00:00:00
```

The second command returns a larger count. The difference is the set of rows on 2026-02-18 that should have been included by the first command.

6. For the timezone problem, run the same query with a `Z` suffix on the bound.

```
mf query --metrics order_count --start-time 2026-02-15T00:00:00 --end-time 2026-02-18T23:59:59Z
```

### Relevant log output

```shell
Generated SQL from `--explain`:

SELECT
SUM(1) AS order_count
FROM .fct_orders fct_orders_src_10000
WHERE DATE_TRUNC('second', ordered_at) BETWEEN '2026-02-15' AND '2026-02-18'

Verifying the same predicate directly against the warehouse confirms that the final day is excluded. Selecting `max(ordered_at)` alongside the count returns `2026-02-17 23:59:49`, that is, the last row before the truncated boundary, even though the end bound passed on the command line was on 2026-02-18.

Error raised when a timezone offset is passed:

TypeError: can't compare offset-naive and offset-aware datetimes
```

### Environment

```markdown
- OS: macOS
- Python: 3.11
- dbt: 1.12.3
- metricflow: 0.212.0
- dbt-metricflow: 0.14.0
```

### Which database are you using?

snowflake

### Additional Context

Using `--where` with the `TimeDimension()` template instead of `--start-time` / `--end-time` preserves the full timestamp and returns the expected rows, so this appears specific to the time-range constraint path rather than to dimension granularity resolution.

The impact is that results are silently wrong rather than obviously broken: a query that looks like it covers a full day range quietly omits the final day.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.