PhoenixKitSync.PullCursor (PhoenixKitSync v0.2.5)

Copy Markdown View Source

The keyset cursor of a bulk pull: where a page starts and where the next one continues.

A pull answers at most max_records_per_request rows in primary-key order. When rows are left, the sender names the last row's key values ("next_after"), and the receiver asks for the page after them with "after". Both are a map of every primary-key column to the value's text, e.g. %{"uuid" => "0190…"} or %{"order_id" => "7", "line" => "3"}.

The page after a key is a row-value comparison in the columns' own types, ("a", "b") > ($1::text::bigint, $2::text::uuid), so a composite key and a non-integer key continue exactly where ORDER BY "a", "b" left off. Values are bound, never interpolated; the only text that reaches the SQL is a quoted column name and a type name from the list below.

A key type has a cursor when a value's text reads back as exactly that value and ORDER BY and the row comparison order it alike. That holds for the types below. A float goes as its shortest text that reads back to the same float (NaN sorts last and equals itself in both), bytea as hex, character compares without its padding on both sides.

Keys of other types have no cursor: such a table answers its first page and "truncated", as a sender without cursors does, and unsupported_types/1 names the type for the admin. Left out, and why: time with time zone (Postgrex returns the time without its zone, so two keys can share one text), interval (no canonical text, and equal intervals are spelled differently), money (its text depends on the locale), enum, citext and other extension types (the schema names them all USER-DEFINED, with no type name to cast to, and an enum orders by declaration), network types, arrays and json.

Summary

Types

A primary key as {column, information_schema data_type} pairs, in key order.

Functions

Reads the "after" field of a request: {:ok, nil} when there is none, {:ok, map} for a map of strings and integers (integers are read as their text), {:error, :invalid_cursor} for anything else.

The most bytes the text of one key value may be in a cursor.

The cursor that continues after row, a map of column name to the value as Postgrex returned it: {:ok, map}, :error when the key has no cursor or the row lacks a key value, or {:too_long, type} when a key value's text is past max_value_bytes/0. Every position answered here is one from_params/1 and where/4 accept.

Whether every column of key has a type a cursor can continue from.

What a cut answer names as the reason it has no position to continue from, for {:too_long, type}: read where a key type without a cursor is.

The types in key that no cursor continues from, each once, in key order; [] for a key that has a cursor. A column whose type is not known is named "unknown".

The condition for the rows strictly after values in the order of key, with its bind values numbered from $(offset + 1). quote_ident quotes a column name.

Types

key()

@type key() :: [{String.t(), String.t() | nil}]

A primary key as {column, information_schema data_type} pairs, in key order.

Functions

from_params(params)

@spec from_params(map()) ::
  {:ok, %{required(String.t()) => String.t()} | nil} | {:error, :invalid_cursor}

Reads the "after" field of a request: {:ok, nil} when there is none, {:ok, map} for a map of strings and integers (integers are read as their text), {:error, :invalid_cursor} for anything else.

max_value_bytes()

@spec max_value_bytes() :: pos_integer()

The most bytes the text of one key value may be in a cursor.

next_after(key, row)

@spec next_after(key(), map()) ::
  {:ok, %{required(String.t()) => String.t()}}
  | :error
  | {:too_long, String.t()}

The cursor that continues after row, a map of column name to the value as Postgrex returned it: {:ok, map}, :error when the key has no cursor or the row lacks a key value, or {:too_long, type} when a key value's text is past max_value_bytes/0. Every position answered here is one from_params/1 and where/4 accept.

supported?(key)

@spec supported?(key()) :: boolean()

Whether every column of key has a type a cursor can continue from.

too_long_reason(type)

@spec too_long_reason(String.t()) :: String.t()

What a cut answer names as the reason it has no position to continue from, for {:too_long, type}: read where a key type without a cursor is.

unsupported_types(key)

@spec unsupported_types(key()) :: [String.t()]

The types in key that no cursor continues from, each once, in key order; [] for a key that has a cursor. A column whose type is not known is named "unknown".

where(key, values, offset, quote_ident)

@spec where(
  key(),
  %{required(String.t()) => String.t()},
  non_neg_integer(),
  (String.t() -> String.t())
) ::
  {:ok, String.t(), [String.t()]}
  | {:error, :invalid_cursor | :unsupported_key_type}

The condition for the rows strictly after values in the order of key, with its bind values numbered from $(offset + 1). quote_ident quotes a column name.

{:error, :invalid_cursor} when values does not name exactly the key's columns or a value does not read as its column's type; {:error, :unsupported_key_type} for a key no cursor continues from.