# Datasets

Incant datasets describe analytical data: dimensions, metrics, default table views, heatmaps, and drilldowns. They are metadata-first. Data-source adapters own SQL generation and execution for QuackDB, Postgres, external APIs, or other analytical backends.

```elixir
defmodule MyApp.Admin.Datasets.CampaignPerformance do
  use Incant.Dataset, source: MyApp.Analytics.QuackDB

  title "Campaign Performance"
  from "campaign_daily"

  dimensions do
    dimension :date, type: :date
    dimension :source
    dimension :campaign
    dimension :ad_group
    dimension :keyword
    dimension :landing_page
  end

  metrics do
    metric :impressions, :sum
    metric :clicks, :sum
    metric :orders, :sum
    metric :cost, :sum, format: :money
    metric :revenue, :sum, format: :money
    metric :ctr, expr: "clicks / nullif(impressions, 0)", format: :percent
    metric :cpa, expr: "cost / nullif(orders, 0)", format: :money
    metric :roas, expr: "revenue / nullif(cost, 0)", format: :number
  end

  filters do
    filter :date, :date_range
    filter :campaign, :select, options: &MyApp.Admin.Options.campaigns/2
  end

  table density: :compact do
    group_by [:campaign]
    columns [:campaign, :clicks, :cost, :orders, :revenue, :cpa, :roas]
    sort :cost, :desc
    heatmap [:cost, :orders, :roas]

    drilldown :campaign, group_by: [:ad_group], columns: [:ad_group, :clicks, :cost, :cpa]
    drilldown :ad_group, group_by: [:keyword], columns: [:keyword, :clicks, :cost, :cpa]
  end
end
```

Register datasets on an admin root:

```elixir
defmodule MyApp.Admin do
  use Incant.Admin

  dataset MyApp.Admin.Datasets.CampaignPerformance
end
```

Build and run normalized dataset queries through the configured source:

```elixir
query =
  Incant.Dataset.query(MyApp.Admin.Datasets.CampaignPerformance,
    filters: %{"range" => "30d"},
    page: 1,
    page_size: 50
  )

{:ok, result} = Incant.Dataset.run(MyApp.Admin.Datasets.CampaignPerformance, filters: query.filters)
```

Data sources implement the `Incant.DataSource` query callback and receive `%Incant.Query{}` with `from`, dimensions, metrics, groupings, columns, filters, sort, pagination, variables, and context. Incant normalizes source rows into `%Incant.Result{}`.

Dataset filters use the same semantic controls as resource filters and are URL-persistent through `filter[...]` query params. The default LiveView adapter renders them in a right-side filter rail beside the dataset table.

Use atoms for closed app-owned enums and strings for external analytical values. UTM values, campaign names, referrers, provider model names, and other imported dimensions should stay strings; do not convert arbitrary external data to atoms.

Static options are fine for closed sets:

```elixir
filter :status, :select, options: [:draft, :active, :archived]
```

Analytical options can be dynamic:

```elixir
filter :campaign, :select, options: &MyApp.Admin.Options.campaigns/2
```

Option callbacks receive `%{source: filter}` and the current context, and should return a list of values or `{label, value}` pairs.

Drilldowns can switch grouping and columns through URL state:

```elixir
table do
  group_by [:campaign]
  columns [:campaign, :clicks, :cost]
  drilldown :campaign, group_by: [:ad_group], columns: [:ad_group, :clicks, :cost]
end
```

The default LiveView adapter renders available drilldowns above the dataset table and passes the selected drilldown into `%Incant.Query{}`.

Chart binding, saved views, and deeper breadcrumb-style drilldown paths are planned follow-up work.
