# Queries And Scans

Scans walk records by namespace, set, or partition. Queries add a
secondary-index predicate through `Aerospike.Filter`. Both surfaces use pure
builder structs and run through explicit `Aerospike` facade calls.

The current stream helpers are lazy at the outer `Enumerable` boundary, but
the runtime buffers each node's response before yielding that node's records.
There is no public cancellation API.

## Scan Streams

Use `Aerospike.Scan` when no secondary index predicate is needed.

```elixir
scan =
  Aerospike.Scan.new("test", "events")
  |> Aerospike.Scan.select(["type", "created_at"])
  |> Aerospike.Scan.filter(
    Aerospike.Exp.gte(
      Aerospike.Exp.int_bin("created_at"),
      Aerospike.Exp.int(1_700_000_000)
    )
  )
  |> Aerospike.Scan.records_per_second(1_000)

{:ok, stream} = Aerospike.scan_stream(:aerospike, scan)

Enum.each(stream, fn record ->
  record.bins["type"]
end)
```

`Scan.filter/2` appends server-side expression filters. Use
`Aerospike.Exp` there, not `Aerospike.Filter`.

## Query Predicates

Use `Aerospike.Query.where/2` for one secondary-index predicate.

```elixir
{:ok, task} =
  Aerospike.create_index(:aerospike, "test", "users",
    bin: "age",
    name: "users_age_idx",
    type: :numeric
  )

:ok = Aerospike.IndexTask.wait(task)

query =
  Aerospike.Query.new("test", "users")
  |> Aerospike.Query.where(Aerospike.Filter.range("age", 18, 65))
  |> Aerospike.Query.filter(
    Aerospike.Exp.eq(Aerospike.Exp.str_bin("status"), Aerospike.Exp.str("active"))
  )
  |> Aerospike.Query.select(["name", "age"])

{:ok, stream} = Aerospike.query_stream(:aerospike, query)
names = stream |> Enum.map(& &1.bins["name"])
```

Secondary-index filters and expression filters are separate lanes:
`Query.where/2` carries the index predicate and `Query.filter/2` carries
server-side expression filters. They can be combined where the server command
supports both.

## Collection And Paging

`query_all/3`, `scan_page/3`, and `query_page/3` require an explicit
`max_records` budget. That budget bounds each page walk; it is not a stable
snapshot size guarantee.

```elixir
paged_query =
  Aerospike.Query.new("test", "users")
  |> Aerospike.Query.where(Aerospike.Filter.equal("status", "active"))
  |> Aerospike.Query.max_records(100)

{:ok, first_page} = Aerospike.query_page(:aerospike, paged_query)

next_cursor =
  if first_page.cursor do
    Aerospike.Cursor.encode(first_page.cursor)
  end

if next_cursor do
  {:ok, next_page} =
    Aerospike.query_page(:aerospike, paged_query, cursor: next_cursor)

  next_page.records
end
```

Cursors resume partition progress. They are suitable for continuing a query
walk, but they are not snapshot tokens.

Scan and query execution options are runtime controls, not record write
policies. Common options include `timeout:`, `socket_timeout:`,
`task_timeout:`, `pool_checkout_timeout:`, `max_concurrent_nodes:`,
`max_retries:`, `sleep_between_retries_ms:`, `replica_policy:`,
`records_per_second:`, `include_bin_data:`, `task_id:`, and `cursor:` where
the helper supports paging. Query helpers also accept `expected_duration:`
with `:long`, `:short`, or `:long_relax_ap`. Use builder functions such as
`Scan.filter/2`, `Query.filter/2`, `Query.where/2`, and `max_records/2` for
server-visible query shape.

Scan pages use the same cursor shape:

```elixir
paged_scan =
  Aerospike.Scan.new("test", "events")
  |> Aerospike.Scan.max_records(100)

{:ok, first_page} = Aerospike.scan_page(:aerospike, paged_scan)
```

## Partition And Node Targeting

Use `Aerospike.PartitionFilter` for advanced partial scans or queries.

```elixir
scan =
  Aerospike.Scan.new("test", "events")
  |> Aerospike.Scan.partition_filter(Aerospike.PartitionFilter.by_range(0, 128))
  |> Aerospike.Scan.max_records(1_000)

{:ok, count} = Aerospike.scan_count(:aerospike, scan)
```

Helpers that support node targeting take `node: node_name` in `opts`.
Discover names with `Aerospike.node_names/1` or `Aerospike.nodes/1`.

```elixir
{:ok, [node_name | _]} = Aerospike.node_names(:aerospike)
{:ok, stream} = Aerospike.query_stream(:aerospike, query, node: node_name)
```

Node targeting is available for scan/query streams, counts, collected pages,
and background query jobs. Finalized aggregate queries do not support
`node: node_name` because they must consume all server partials needed for one
local result.
