Packages

Start with everything. Keep what performs. Pre-aggregate what matters. | Inline Cubes with Ecto.Schema

Current section

Files

Jump to
power_of_3 lib power_of_three dimension_ref.ex
Raw

lib/power_of_three/dimension_ref.ex

defmodule PowerOfThree.DimensionRef do
@moduledoc """
Represents a reference to a cube dimension.
Used in dot-accessible collections like `Customer.dimensions.email`
## Fields
* `:name` - The dimension name (atom or string)
* `:module` - The module where this dimension is defined
* `:type` - The dimension type (`:string`, `:number`, `:time`, `:boolean`)
* `:sql` - The SQL expression or column reference
* `:meta` - Metadata map containing ecto field information
* `:description` - Optional description of the dimension
* `:primary_key` - Whether this dimension is part of the primary key
* `:format` - Optional format specification
## Examples
# Reference created by generated accessor
dimension_ref = Customer.dimensions.email()
%DimensionRef{
name: :email,
module: Customer,
type: :string,
sql: "email",
...
}
# Convert to SQL column expression
DimensionRef.to_sql_column(dimension_ref)
# => "customer.email"
"""
@enforce_keys [:name, :module, :type]
defstruct [
:name,
:module,
:type,
:sql,
:meta,
:description,
:primary_key,
:format,
:propagate_filters_to_sub_query,
:public
]
@type dimension_type :: :string | :number | :time | :boolean | :geo
@type t :: %__MODULE__{
name: atom() | String.t(),
module: module(),
type: dimension_type(),
sql: String.t(),
meta: map() | nil,
description: String.t() | nil,
primary_key: boolean(),
format: atom() | nil,
propagate_filters_to_sub_query: boolean() | nil,
public: boolean() | nil
}
@doc """
Converts a dimension reference to a SQL column expression.
Returns the qualified column name used by Cube SQL.
## Examples
iex> dimension = %DimensionRef{name: :email, module: Customer, type: :string, sql: "email"}
iex> DimensionRef.to_sql_column(dimension)
"customer.email"
iex> dimension = %DimensionRef{name: "brand_code", module: Customer, type: :string, sql: "brand_code"}
iex> DimensionRef.to_sql_column(dimension)
"customer.brand_code"
"""
@spec to_sql_column(t()) :: String.t()
def to_sql_column(%__MODULE__{module: module, name: name}) do
cube_name = extract_cube_name(module)
dimension_name = to_string(name)
"#{cube_name}.#{dimension_name}"
end
@doc """
Returns the cube name from the module.
Uses the Ecto schema source as the cube name.
## Examples
iex> DimensionRef.extract_cube_name(Customer)
"customer"
"""
@spec extract_cube_name(module()) :: String.t()
def extract_cube_name(module) do
module.__schema__(:source)
end
@doc """
Returns the dimension name as a string.
## Examples
iex> dimension = %DimensionRef{name: :email, module: Customer, type: :string, sql: "email"}
iex> DimensionRef.name_string(dimension)
"email"
"""
@spec name_string(t()) :: String.t()
def name_string(%__MODULE__{name: name}) when is_atom(name), do: Atom.to_string(name)
def name_string(%__MODULE__{name: name}) when is_binary(name), do: name
@doc """
Returns a human-readable description of the dimension.
## Examples
iex> dimension = %DimensionRef{
...> name: :email,
...> module: Customer,
...> type: :string,
...> sql: "email",
...> description: "Customer email address"
...> }
iex> DimensionRef.describe(dimension)
"email (string): Customer email address"
"""
@spec describe(t()) :: String.t()
def describe(%__MODULE__{name: name, type: type, description: nil}) do
"#{name} (#{type})"
end
def describe(%__MODULE__{name: name, type: type, description: description}) do
"#{name} (#{type}): #{description}"
end
@doc """
Validates that the dimension reference is well-formed.
Returns `:ok` if valid, or `{:error, reason}` if invalid.
## Examples
iex> dimension = %DimensionRef{name: :email, module: Customer, type: :string, sql: "email"}
iex> DimensionRef.validate(dimension)
:ok
iex> dimension = %DimensionRef{name: nil, module: Customer, type: :string, sql: "email"}
iex> DimensionRef.validate(dimension)
{:error, "name cannot be nil"}
"""
@spec validate(t()) :: :ok | {:error, String.t()}
def validate(%__MODULE__{name: nil}), do: {:error, "name cannot be nil"}
def validate(%__MODULE__{module: nil}), do: {:error, "module cannot be nil"}
def validate(%__MODULE__{type: nil}), do: {:error, "type cannot be nil"}
def validate(%__MODULE__{sql: nil}), do: {:error, "sql cannot be nil"}
def validate(%__MODULE__{type: type}) do
valid_types = [:string, :number, :time, :boolean, :geo]
if type in valid_types do
:ok
else
{:error, "invalid dimension type: #{inspect(type)}"}
end
end
@doc """
Checks if this dimension is a primary key.
## Examples
iex> dimension = %DimensionRef{
...> name: :id,
...> module: Customer,
...> type: :number,
...> sql: "id",
...> primary_key: true
...> }
iex> DimensionRef.primary_key?(dimension)
true
"""
@spec primary_key?(t()) :: boolean()
def primary_key?(%__MODULE__{primary_key: true}), do: true
def primary_key?(%__MODULE__{}), do: false
@doc """
Returns the SQL expression for this dimension.
Handles both simple column names and complex SQL expressions.
## Examples
iex> dimension = %DimensionRef{
...> name: :email,
...> module: Customer,
...> type: :string,
...> sql: "email"
...> }
iex> DimensionRef.sql_expression(dimension)
"email"
iex> dimension = %DimensionRef{
...> name: :full_name,
...> module: Customer,
...> type: :string,
...> sql: "first_name || ' ' || last_name"
...> }
iex> DimensionRef.sql_expression(dimension)
"first_name || ' ' || last_name"
"""
@spec sql_expression(t()) :: String.t()
def sql_expression(%__MODULE__{sql: sql}) when is_binary(sql), do: sql
def sql_expression(%__MODULE__{sql: sql}) when is_atom(sql), do: Atom.to_string(sql)
end