lib/selecto/schema/join.ex

defmodule Selecto.Schema.Join do
  # selecto meta join can edit, add, alter this join!

  # import Selecto.Types - removed to avoid circular dependency

  @moduledoc """
  # Join Configuration and Patterns

  This module handles join configuration for various database join patterns,
  providing simplified configuration helpers for common scenarios.

  ## Available Join Types

  ### :dimension (Existing)
  Basic dimension table join with ID filtering and name display.
  - `dimension`: Field to use for display (required)
  - `name`: Display name for the join (optional)

  ### :tagging (New)
  Many-to-many relationship through a join table (e.g., posts <-> tags).
  - `tag_field`: Field to display from tag table (default: :name)
  - `name`: Display name for the join (optional)
  Creates custom columns for tag aggregation and faceted filtering.

  ### :hierarchical (New)
  Self-referencing hierarchical relationships with multiple implementation patterns:
  - `hierarchy_type`: :adjacency_list, :materialized_path, or :closure_table
  - `depth_limit`: Maximum recursion depth for adjacency lists (default: 5)
  - `path_field`: Field containing path for materialized path pattern (default: :path)
  - `path_separator`: Separator used in path field (default: "/")
  - `closure_table`: Name of closure table for closure table pattern
  - `ancestor_field`, `descendant_field`, `depth_field`: Closure table field names

  ### :star_dimension (New)
  Optimized for OLAP star schema dimensions.
  - `display_field`: Field to use for dimension display (default: :name)
  - `name`: Display name for the dimension (optional)
  Creates faceted filters optimized for aggregation queries.

  ### :snowflake_dimension (New)
  Normalized dimension tables requiring additional joins.
  - `display_field`: Field to use for dimension display (default: :name)
  - `normalization_joins`: List of additional joins needed for full context
  - `name`: Display name for the dimension (optional)

  ## Standard Join Types (Default behavior)
  - one_to_one - Standard lookup with all columns available
  - one_to_many - Treated like one-to-one
  - belongs_to - Treated like one-to-one

  ## Configuration Examples

  ```elixir
  joins: %{
    # Many-to-many tagging
    tags: %{type: :tagging, tag_field: :name},

    # Hierarchical adjacency list
    manager: %{type: :hierarchical, hierarchy_type: :adjacency_list, depth_limit: 5},

    # Star schema dimension
    customer: %{type: :star_dimension, display_field: :full_name},

    # Snowflake dimension with normalization
    category: %{
      type: :snowflake_dimension,
      display_field: :name,
      normalization_joins: [%{table: "category_groups", alias: "cg"}]
    }
  }
  ```
  """

  ### source - a schema name such as SelectoTest.Store.Film
  ### joins - the joins map from this join structure

  # we consume the join tree (atom/list) to a flat map of joins then into a map
  @spec recurse_joins(Selecto.Types.source(), Selecto.Types.domain()) :: %{
          atom() => Selecto.Types.processed_join()
        }
  def recurse_joins(source, domain) do
    joins = Map.get(domain, :joins, %{})

    normalize_joins(source, joins, :selecto_root, domain)
    |> List.flatten()
    |> Enum.reduce(%{}, fn j, acc -> Map.put(acc, j.id, j) end)
  end

  ### source, the schema module name
  ### joins, the joins map from parent,
  ### parent, the 'parent' join

  defp normalize_joins(source, joins, parent, domain) do
    Enum.reduce(joins, [], fn
      # Non-association (custom) joins - explicitly configured without an Ecto association
      {id, %{non_assoc: true} = config}, acc ->
        acc = acc ++ [configure_non_assoc(id, config, parent, source, domain)]

        case Map.get(config, :joins) do
          nil ->
            acc

          nested_joins ->
            # For non-assoc joins, we need to get the target schema from config
            target_schema = get_target_schema_for_non_assoc(config, domain)

            if target_schema do
              acc ++ normalize_joins(target_schema, nested_joins, id, domain)
            else
              acc
            end
        end

      # Standard association-based joins
      {id, config}, acc ->
        association = source.associations[id]

        case association do
          # Handle through associations by expanding the intermediate joins
          %{through: through_path} ->
            expand_through_joins(id, through_path, config, parent, source, domain, acc)

          # Handle many-to-many associations (join_through table)
          %{join_through: _join_through} = assoc ->
            queryable = domain.schemas[assoc.queryable]

            acc =
              acc ++
                [configure_many_to_many(id, assoc, config, parent, source, queryable, domain)]

            case Map.get(config, :joins) do
              nil -> acc
              nested_joins -> acc ++ normalize_joins(queryable, nested_joins, id, domain)
            end

          # Standard association
          _ ->
            queryable = domain.schemas[association.queryable]
            acc = acc ++ [configure(id, association, config, parent, source, queryable)]

            case Map.get(config, :joins) do
              nil -> acc
              nested_joins -> acc ++ normalize_joins(queryable, nested_joins, id, domain)
            end
        end
    end)
  end

  # Get target schema for non-assoc joins from config
  defp get_target_schema_for_non_assoc(config, domain) do
    case config do
      %{target_schema: schema_key} ->
        domain.schemas[schema_key]

      %{source: source_table} ->
        # Try to find schema by source table name
        Enum.find_value(domain.schemas, fn {_key, schema} ->
          if schema.source_table == source_table, do: schema, else: nil
        end)

      _ ->
        nil
    end
  end

  # Expand has_many :through associations into intermediate joins
  defp expand_through_joins(id, through_path, config, parent, source, domain, acc) do
    # through_path is a list like [:posts, :tags] meaning:
    # source -> posts -> tags
    # We need to ensure intermediate joins exist and configure the final join

    case through_path do
      [intermediate_assoc, final_assoc] ->
        # Get the intermediate association
        intermediate = source.associations[intermediate_assoc]

        if intermediate do
          intermediate_queryable = domain.schemas[intermediate.queryable]

          # Check if intermediate join is already being added
          # If not, we need to add it implicitly
          intermediate_join =
            configure(
              intermediate_assoc,
              intermediate,
              # Mark as implicitly added for through
              %{implicit: true},
              parent,
              source,
              intermediate_queryable
            )

          # Now get the final association from the intermediate schema
          # We need to look it up from the actual intermediate queryable associations
          final_assoc_data = intermediate_queryable.associations[final_assoc]

          if final_assoc_data do
            final_queryable = domain.schemas[final_assoc_data.queryable]

            # Configure the final join with reference to intermediate
            final_join =
              configure(
                # Use the original through association name
                id,
                final_assoc_data,
                config,
                # Parent is the intermediate join
                intermediate_assoc,
                intermediate_queryable,
                final_queryable
              )

            # Mark the final join as a through join for special handling
            final_join = Map.put(final_join, :is_through, true)
            final_join = Map.put(final_join, :through_path, through_path)

            # Add both joins
            acc = acc ++ [intermediate_join, final_join]

            # Handle nested joins if any
            case Map.get(config, :joins) do
              nil -> acc
              nested_joins -> acc ++ normalize_joins(final_queryable, nested_joins, id, domain)
            end
          else
            # Final association not found, skip
            acc
          end
        else
          # Intermediate association not found, skip
          acc
        end

      # Longer through paths (rare but possible)
      [first_assoc | rest] when length(rest) >= 2 ->
        # Recursively expand
        first = source.associations[first_assoc]

        if first do
          first_queryable = domain.schemas[first.queryable]

          first_join =
            configure(first_assoc, first, %{implicit: true}, parent, source, first_queryable)

          # Recursively expand the remaining path
          acc = acc ++ [first_join]
          expand_through_joins(id, rest, config, first_assoc, first_queryable, domain, acc)
        else
          acc
        end

      _ ->
        # Invalid through path, skip
        acc
    end
  end

  # Configure non-association (custom) joins
  defp configure_non_assoc(id, config, parent, _from_source, _domain) do
    name = Map.get(config, :name, id)
    source_table = Map.get(config, :source, to_string(id))
    owner_key = Map.get(config, :owner_key, :id)
    related_key = Map.get(config, :related_key, :id)

    %{
      config: config,
      # No Ecto source for custom joins
      from_source: nil,
      owner_key: owner_key,
      my_key: related_key,
      source: source_table,
      id: id,
      name: name,
      requires_join: parent,
      join_type: :custom,
      is_custom: true,
      filters: Map.get(config, :filters, %{}),
      fields: Map.get(config, :fields, %{})
    }
  end

  # Configure many-to-many joins (with join_through table)
  defp configure_many_to_many(id, association, config, parent, from_source, queryable, _domain) do
    name = Map.get(config, :name, id)
    join_through = association.join_through

    # For many-to-many, we need to track the join table
    join_through_table =
      case join_through do
        table when is_binary(table) ->
          table

        module when is_atom(module) ->
          if function_exported?(module, :__schema__, 1) do
            module.__schema__(:source)
          else
            to_string(module) |> String.split(".") |> List.last() |> Macro.underscore()
          end

        _ ->
          "#{id}_join"
      end

    join_keys = Map.get(association, :join_keys, [])

    {derived_main_foreign_key, derived_tag_foreign_key} =
      case join_keys do
        [{main_fk, _owner_key}, {tag_fk, _related_key} | _] ->
          {to_string(main_fk), to_string(tag_fk)}

        _ ->
          {
            foreign_key_from_table(Map.get(from_source, :source_table, to_string(id))),
            foreign_key_from_table(Map.get(queryable, :source_table, to_string(id)))
          }
      end

    final_join_table =
      case Map.get(config, :join_table) do
        nil -> join_through_table
        value when is_atom(value) -> Atom.to_string(value)
        value -> value
      end

    final_main_foreign_key =
      config
      |> Map.get(:main_foreign_key, derived_main_foreign_key)
      |> to_string()

    final_tag_foreign_key =
      config
      |> Map.get(:tag_foreign_key, derived_tag_foreign_key)
      |> to_string()

    %{
      config: config,
      from_source: from_source,
      owner_key: association.owner_key,
      my_key: association.related_key,
      source: queryable.source_table,
      id: id,
      name: name,
      requires_join: parent,
      join_type: :many_to_many,
      join_table: final_join_table,
      main_foreign_key: final_main_foreign_key,
      tag_foreign_key: final_tag_foreign_key,
      join_through: join_through_table,
      join_keys: join_keys,
      filters: Map.get(config, :filters, %{}),
      fields:
        Selecto.Schema.Column.configure_columns(
          association.field,
          queryable.fields -- Map.get(queryable, :redact_fields, []),
          queryable,
          config
        )
    }
    |> parameterize()
  end

  defp foreign_key_from_table(table) when is_atom(table),
    do: table |> Atom.to_string() |> foreign_key_from_table()

  defp foreign_key_from_table(table) when is_binary(table) do
    table
    |> String.split(".")
    |> List.last()
    |> String.trim_trailing("s")
    |> Kernel.<>("_id")
  end

  ### id, the id of this join in the joins map on parent
  ### association, the struct form ecto.schema that has instructions on how to join
  ### config, the map that contains config details for this join
  ### parent - atom that references the parent
  ### from_source - the association from the parent or the domain if parent is root

  ### Dimension table join
  defp configure(id, association, %{type: :dimension} = config, parent, from_source, queryable) do
    # dimension table, has one 'name-ish' value to display, and then the Local reference would provide ID filtering.
    # So create a field for group-by that displays NAME and filters by ID

    name = Map.get(config, :name, id)

    from_field =
      case parent do
        :selecto_root -> "#{association.owner_key}"
        _ -> "#{parent}.#{association.owner_key}"
      end

    config =
      Map.put(
        config,
        :custom_columns,
        Map.get(config, :custom_columns, %{})
        |> Map.put(
          ## we will use the nane of the join's association!
          "#{id}",
          %{
            name: name,
            ### concat_ws?
            select: "#{association.field}.#{config.dimension}",
            ### we will always get a tuple of select + group_by_filter_select here
            group_by_format: fn {a, _id}, _def -> a end,
            group_by_filter: from_field,
            group_by_filter_select: ["#{association.field}.#{config.dimension}", from_field]
          }
        )
      )

    ### Add custom filter that has a select (or set of checkboxes or radios... filter_form_type: :checks eg) list of names, IF not existing by name of the remote ID filter and:
    ### - the join provides a list of tuples {filterable, viewable}
    ### - the join provides a function returning same
    ### - the join has 'facet: true' in which case we will determine given all other filters, what are the available items & how many matches for those items

    %{
      config: config,
      from_source: from_source,
      owner_key: association.owner_key,
      my_key: association.related_key,
      source: queryable.source_table,
      id: id,
      name: name,
      ## probably don't need 'where'
      requires_join: parent,
      filters: Map.get(config, :filters, %{}),
      fields:
        Selecto.Schema.Column.configure_columns(
          association.field,
          [config.dimension],
          queryable,
          config
        )
    }
    |> parameterize()
  end

  ### Many-to-many tagging join (through join table)
  defp configure(id, association, %{type: :tagging} = config, parent, from_source, queryable) do
    # For tagging relationships, we need to handle the intermediate join table
    # This assumes the association is a has_many :through relationship
    # Example: Post has_many :tags, through: :post_tags

    name = Map.get(config, :name, id)
    # Field to display from tag table
    tag_field = Map.get(config, :tag_field, :name)

    # Configure custom columns for tag aggregation
    config =
      Map.put(
        config,
        :custom_columns,
        Map.get(config, :custom_columns, %{})
        |> Map.put(
          "#{id}_list",
          %{
            name: "#{name} List",
            select: "string_agg(#{association.field}.#{tag_field}, ', ')",
            group_by_format: fn {a, _id}, _def -> a end,
            # Aggregate fields typically aren't filterable
            filterable: false
          }
        )
      )

    # Add faceted filter for individual tags
    config =
      Map.put(
        config,
        :custom_filters,
        Map.get(config, :custom_filters, %{})
        |> Map.put(
          "#{id}_filter",
          %{
            name: "#{name}",
            filter_type: :multi_select,
            # Enable faceted filtering
            facet: true,
            source_table: queryable.source_table,
            source_field: tag_field
          }
        )
      )

    %{
      config: config,
      from_source: from_source,
      owner_key: association.owner_key,
      my_key: association.related_key,
      source: queryable.source_table,
      id: id,
      name: name,
      requires_join: parent,
      # Special marker for SQL builder
      join_type: :many_to_many,
      filters: Map.get(config, :filters, %{}),
      fields:
        Selecto.Schema.Column.configure_columns(
          association.field,
          queryable.fields -- queryable.redact_fields,
          queryable,
          config
        )
    }
    |> parameterize()
  end

  ### Hierarchical self-join (adjacency list pattern)
  defp configure(id, association, %{type: :hierarchical} = config, parent, from_source, queryable) do
    # Self-referencing table with parent_id pointing to same table
    # Supports adjacency list hierarchical pattern

    name = Map.get(config, :name, id)
    hierarchy_type = Map.get(config, :hierarchy_type, :adjacency_list)
    # Prevent infinite recursion
    depth_limit = Map.get(config, :depth_limit, 5)

    case hierarchy_type do
      :adjacency_list ->
        configure_adjacency_list(
          id,
          association,
          config,
          parent,
          from_source,
          queryable,
          name,
          depth_limit
        )

      :materialized_path ->
        configure_materialized_path(id, association, config, parent, from_source, queryable, name)

      :closure_table ->
        configure_closure_table(id, association, config, parent, from_source, queryable, name)
    end
  end

  ### Star schema dimension join (optimized for OLAP)
  defp configure(
         id,
         association,
         %{type: :star_dimension} = config,
         parent,
         from_source,
         queryable
       ) do
    # Star schema dimensions are optimized for aggregation and analysis
    # They typically contain descriptive attributes and are joined to fact tables

    name = Map.get(config, :name, id)
    display_field = Map.get(config, :display_field, :name)

    # Configure dimension-specific features
    config =
      Map.put(
        config,
        :custom_columns,
        Map.get(config, :custom_columns, %{})
        |> Map.put(
          "#{id}_display",
          %{
            name: "#{name}",
            select: "#{association.field}.#{display_field}",
            group_by_format: fn {a, _id}, _def -> a end,
            filterable: true,
            # Mark as dimension for special handling
            is_dimension: true
          }
        )
      )

    # Add aggregation-friendly filters
    config =
      Map.put(
        config,
        :custom_filters,
        Map.get(config, :custom_filters, %{})
        |> Map.put(
          "#{id}_facet",
          %{
            name: "#{name} Filter",
            filter_type: :select_facet,
            # Enable faceted filtering for dimensions
            facet: true,
            source_table: queryable.source_table,
            source_field: display_field,
            is_dimension: true
          }
        )
      )

    %{
      config: config,
      from_source: from_source,
      owner_key: association.owner_key,
      my_key: association.related_key,
      source: queryable.source_table,
      id: id,
      name: name,
      requires_join: parent,
      # Optimized for OLAP queries
      join_type: :star_dimension,
      display_field: display_field,
      filters: Map.get(config, :filters, %{}),
      fields:
        Selecto.Schema.Column.configure_columns(
          association.field,
          queryable.fields -- queryable.redact_fields,
          queryable,
          config
        )
    }
    |> parameterize()
  end

  ### Snowflake schema normalized dimension join
  defp configure(
         id,
         association,
         %{type: :snowflake_dimension} = config,
         parent,
         from_source,
         queryable
       ) do
    # Snowflake dimensions are normalized and may require multiple joins to get full context
    # They maintain referential integrity but require more complex queries

    name = Map.get(config, :name, id)
    display_field = Map.get(config, :display_field, :name)
    # Additional joins needed
    normalization_joins = Map.get(config, :normalization_joins, [])

    # Configure for normalized dimension access
    config =
      Map.put(
        config,
        :custom_columns,
        Map.get(config, :custom_columns, %{})
        |> Map.put(
          "#{id}_normalized",
          %{
            name: "#{name}",
            select: build_snowflake_select(association.field, display_field, normalization_joins),
            group_by_format: fn {a, _id}, _def -> a end,
            filterable: true,
            requires_normalization_joins: normalization_joins
          }
        )
      )

    %{
      config: config,
      from_source: from_source,
      owner_key: association.owner_key,
      my_key: association.related_key,
      source: queryable.source_table,
      id: id,
      name: name,
      requires_join: parent,
      join_type: :snowflake_dimension,
      display_field: display_field,
      # Track additional joins needed
      normalization_joins: normalization_joins,
      filters: Map.get(config, :filters, %{}),
      fields:
        Selecto.Schema.Column.configure_columns(
          association.field,
          queryable.fields -- queryable.redact_fields,
          queryable,
          config
        )
    }
    |> parameterize()
  end

  ### Enhanced joins (new join types)
  defp configure(id, association, %{type: join_type} = config, parent, from_source, queryable)
       when join_type in [
              :self_join,
              :lateral_join,
              :cross_join,
              :full_outer_join,
              :conditional_join
            ] do
    Selecto.EnhancedJoins.configure_enhanced_join(
      id,
      association,
      config,
      parent,
      from_source,
      queryable
    )
  end

  ### Regular (catch-all clause)
  defp configure(id, association, config, parent, from_source, queryable) do
    std_config(id, association, config, parent, from_source, queryable)
  end

  defp configure_adjacency_list(
         id,
         association,
         config,
         parent,
         from_source,
         queryable,
         name,
         _depth_limit
       ) do
    # Add custom columns for hierarchy navigation
    # These reference fields from the JOIN alias used in the main query.
    join_alias = to_string(id)

    config =
      Map.put(
        config,
        :custom_columns,
        Map.get(config, :custom_columns, %{})
        |> Map.put(
          "#{id}_path",
          %{
            name: "#{name} Path",
            select: "#{join_alias}.path",
            group_by_format: fn {a, _id}, _def -> a end,
            filterable: false
          }
        )
        |> Map.put(
          "#{id}_level",
          %{
            name: "#{name} Level",
            select: "#{join_alias}.level",
            group_by_format: fn {a, _id}, _def -> a end,
            filterable: true
          }
        )
        |> Map.put(
          "#{id}_path_array",
          %{
            name: "#{name} Path Array",
            select: "#{join_alias}.path_array",
            group_by_format: fn {a, _id}, _def -> a end,
            filterable: false
          }
        )
      )

    # Get the depth limit from config for the join struct
    depth_limit = Map.get(config, :depth_limit, 5)
    id_field = Map.get(config, :id_field, association.related_key || :id)
    parent_field = Map.get(config, :parent_field, association.owner_key || :parent_id)
    name_field = Map.get(config, :name_field, :name)

    %{
      config: config,
      from_source: from_source,
      owner_key: association.owner_key,
      my_key: association.related_key,
      source: queryable.source_table,
      id: id,
      name: name,
      requires_join: parent,
      join_type: :hierarchical_adjacency,
      hierarchy_depth: depth_limit,
      id_field: id_field,
      parent_field: parent_field,
      name_field: name_field,
      filters: Map.get(config, :filters, %{}),
      fields:
        Selecto.Schema.Column.configure_columns(
          association.field,
          queryable.fields -- queryable.redact_fields,
          queryable,
          config
        )
    }
    |> parameterize()
  end

  defp configure_materialized_path(id, association, config, parent, from_source, queryable, name) do
    path_field = Map.get(config, :path_field, :path)
    path_separator = Map.get(config, :path_separator, "/")
    root_path = Map.get(config, :root_path, "")

    # Add custom columns for path-based operations
    # These reference fields from the JOIN alias used in the main query.
    join_alias = to_string(id)

    config =
      Map.put(
        config,
        :custom_columns,
        Map.get(config, :custom_columns, %{})
        |> Map.put(
          "#{id}_depth",
          %{
            name: "#{name} Depth",
            select: "#{join_alias}.depth",
            group_by_format: fn {a, _id}, _def -> a end,
            filterable: true
          }
        )
        |> Map.put(
          "#{id}_path_array",
          %{
            name: "#{name} Path Array",
            select: "#{join_alias}.path_array",
            group_by_format: fn {a, _id}, _def -> a end,
            filterable: false
          }
        )
      )

    %{
      config: config,
      from_source: from_source,
      owner_key: association.owner_key,
      my_key: association.related_key,
      source: queryable.source_table,
      id: id,
      name: name,
      requires_join: parent,
      join_type: :hierarchical_materialized_path,
      path_field: path_field,
      path_separator: path_separator,
      root_path: root_path,
      filters: Map.get(config, :filters, %{}),
      fields:
        Selecto.Schema.Column.configure_columns(
          association.field,
          queryable.fields -- queryable.redact_fields,
          queryable,
          config
        )
    }
    |> parameterize()
  end

  defp configure_closure_table(id, association, config, parent, from_source, queryable, name) do
    # Assumes a separate closure table exists for storing ancestor-descendant relationships
    closure_table = Map.get(config, :closure_table, "#{queryable.source_table}_closure")
    ancestor_field = Map.get(config, :ancestor_field, :ancestor_id)
    descendant_field = Map.get(config, :descendant_field, :descendant_id)
    depth_field = Map.get(config, :depth_field, :depth)

    # Add custom columns leveraging closure table CTE
    # These reference fields from the JOIN alias used in the main query.
    join_alias = to_string(id)

    config =
      Map.put(
        config,
        :custom_columns,
        Map.get(config, :custom_columns, %{})
        |> Map.put(
          "#{id}_depth",
          %{
            name: "#{name} Depth",
            select: "#{join_alias}.depth",
            group_by_format: fn {a, _id}, _def -> a end,
            filterable: true
          }
        )
        |> Map.put(
          "#{id}_descendant_count",
          %{
            name: "#{name} Descendant Count",
            select: "#{join_alias}.descendant_count",
            group_by_format: fn {a, _id}, _def -> a end,
            filterable: true
          }
        )
      )

    %{
      config: config,
      from_source: from_source,
      owner_key: association.owner_key,
      my_key: association.related_key,
      source: queryable.source_table,
      id: id,
      name: name,
      requires_join: parent,
      join_type: :hierarchical_closure_table,
      closure_table: closure_table,
      ancestor_field: ancestor_field,
      descendant_field: descendant_field,
      depth_field: depth_field,
      filters: Map.get(config, :filters, %{}),
      fields:
        Selecto.Schema.Column.configure_columns(
          association.field,
          queryable.fields -- queryable.redact_fields,
          queryable,
          config
        )
    }
    |> parameterize()
  end

  # Helper functions for building hierarchy SQL
  # Note: Previously broken CTE functions have been removed and replaced with
  # proper CTE integration in the SQL builder. Custom columns now reference
  # CTE fields directly instead of embedding invalid subqueries.

  defp build_snowflake_select(field, display_field, normalization_joins) do
    # Build select clause that may require additional joins for normalized data
    # This is a simplified version - real implementation would coordinate with SQL builder
    case normalization_joins do
      [] -> "#{field}.#{display_field}"
      # Use the normalized table's field
      [%{alias: alias_name} | _] -> "#{alias_name}.#{display_field}"
      [%{table: table} | _] -> "#{table}.#{display_field}"
      # Fallback for string joins
      [join | _] when is_binary(join) -> "#{join}.#{display_field}"
    end
  end

  defp std_config(id, association, config, parent, from_source, queryable) do
    %{
      config: config,
      from_source: from_source,
      owner_key: association.owner_key,
      my_key: association.related_key,
      source: queryable.source_table,
      id: id,
      name: Map.get(config, :name, id),
      ## probably don't need 'where'
      requires_join: parent,
      filters: make_filters(config),
      fields:
        Selecto.Schema.Column.configure_columns(
          association.field,
          queryable.fields -- queryable.redact_fields,
          queryable,
          config
        )
    }
    |> parameterize()
  end

  defp parameterize(join) do
    join
  end

  defp make_filters(config) do
    Map.get(config, :filters, %{})
  end
end