defmodule Aerospike.Scan do @moduledoc """ Composable scan description (pure data, no I/O). Build a scan with `new/1` or `new/2`, then chain `select/2`, `filter/2`, `max_records/2`, and related functions. Execution is performed by the client facade in a later phase. Expression filters (`filter/2`) are combined with **AND** semantics at execution time. """ alias Aerospike.Exp alias Aerospike.PartitionFilter @enforce_keys [:namespace] defstruct [ :namespace, :set, bin_names: [], filters: [], max_records: nil, records_per_second: 0, partition_filter: nil, no_bins: false ] @type t :: %__MODULE__{ namespace: String.t(), set: String.t() | nil, bin_names: [String.t()], filters: [Exp.t()], max_records: pos_integer() | nil, records_per_second: non_neg_integer(), partition_filter: PartitionFilter.t() | nil, no_bins: boolean() } @doc """ Starts a namespace-wide scan (no set filter). """ @spec new(String.t()) :: t() def new(namespace) when is_binary(namespace) do validate_namespace!(namespace) %__MODULE__{namespace: namespace, set: nil} end @doc """ Starts a scan limited to a single set. """ @spec new(String.t(), String.t()) :: t() def new(namespace, set) when is_binary(namespace) and is_binary(set) do validate_namespace!(namespace) %__MODULE__{namespace: namespace, set: set} end @doc """ Restricts returned bins to the given names (strings). """ @spec select(t(), [String.t()]) :: t() def select(%__MODULE__{} = scan, bin_names) when is_list(bin_names) do validate_bin_names!(bin_names) %{scan | bin_names: bin_names} end @doc """ Appends a server-side expression filter (AND-ed with existing filters at execution). """ @spec filter(t(), Exp.t()) :: t() def filter(%__MODULE__{} = scan, %Exp{} = exp) do %{scan | filters: scan.filters ++ [exp]} end @doc """ Sets the maximum number of records to return. Must be a positive integer. """ @spec max_records(t(), pos_integer()) :: t() def max_records(%__MODULE__{} = scan, n) when is_integer(n) and n > 0 do %{scan | max_records: n} end @doc """ Sets the server-side records-per-second throttle. Use `0` for no throttle. """ @spec records_per_second(t(), non_neg_integer()) :: t() def records_per_second(%__MODULE__{} = scan, n) when is_integer(n) and n >= 0 do %{scan | records_per_second: n} end @doc """ Attaches a partition filter for partial scans or resume. """ @spec partition_filter(t(), PartitionFilter.t()) :: t() def partition_filter(%__MODULE__{} = scan, %PartitionFilter{} = pf) do %{scan | partition_filter: pf} end @doc """ When `true`, the server omits bin payloads (useful for count-style scans). """ @spec no_bins(t(), boolean()) :: t() def no_bins(%__MODULE__{} = scan, flag) when is_boolean(flag) do %{scan | no_bins: flag} end defp validate_namespace!(namespace) do if namespace == "" do raise ArgumentError, "namespace must be a non-empty string" end end defp validate_bin_names!(bin_names) do Enum.each(bin_names, fn name -> unless is_binary(name) and name != "" do raise ArgumentError, "bin names must be non-empty strings, got: #{inspect(name)}" end end) end end