Latu.DataFrame (latu v0.1.1)

Copy Markdown View Source

A lazy DataFrame: a session and an inert plan.

Building one does no IO and touches no process. Nothing reaches the server until an action, such as Latu.show/2 or Latu.collect/2.

Summary

Functions

Aggregate the whole frame, with no grouping.

Name the DataFrame, so its columns can be qualified as name.column.

Like cache/1, raising on failure. Returns the DataFrame, so it pipes.

Like checkpoint/2, raising on failure.

Fewer partitions without a shuffle. repartition/2 with shuffle: false.

A reference to one of this DataFrame's columns, tagged with its identity.

All the rows, as maps.

Like collect/2, raising on failure.

collect/2, and what the run reported besides the rows.

Like collect_with_metrics/2, raising on failure and returning {rows, info}.

Like columns/1, raising on failure.

Like corr/4, raising on failure.

How many rows, counted by the server.

Like count/2, raising on failure.

count/2, and what the run reported besides the result. See Latu.ExecutionInfo.

Like count_with_metrics/2, raising on failure and returning {count, info}.

Like cov/4, raising on failure.

Group by every combination of these columns.

Drop duplicate rows.

Remove columns.

Like dtypes/1, raising on failure.

Rows in the first and not the second.

A predicate that holds when this DataFrame has any rows at all.

Like explain/2, raising on failure.

Keep the rows the condition holds for.

The first row, or nil when there are none. Options are collect/2's.

Like first/2, raising on failure.

Like glimpse/2, raising on failure.

Group rows, giving a Latu.GroupedData that agg/2 turns back into a DataFrame.

first/2 under PySpark's other name: one row or nil, not a list. With a count it is take/3: a list, even for one row. Both shapes are PySpark's. Options are collect/2's.

Like input_files/1, raising on failure.

insert_into/3, and what the run reported besides the result. See Latu.ExecutionInfo.

Rows in both. Distinct unless all: true, which is PySpark's intersectAll.

Like is_empty/1, raising on failure.

Like is_local/1, raising on failure.

Like is_streaming/1, raising on failure.

Join two DataFrames.

Keep at most count rows.

Like merge/2, raising on failure.

merge/2, and the metrics an observe/3 in the source plan asked for.

Skip the first count rows.

sort/2, spelled Spark's other way.

Like persist/2, raising on failure. Returns the DataFrame, so it pipes.

Like print_schema/2, raising on failure.

Like release/1, raising on failure.

Rename columns.

Shuffle into count partitions, or partition by these columns, or both.

Group by every prefix of these columns, plus the grand total.

Like same_semantics/2, raising on failure.

A random fraction of the rows.

save_as_table/3, and what the run reported besides the result. See Latu.ExecutionInfo.

This DataFrame as a scalar subquery: a single value, usable wherever a value belongs.

Like schema/1, raising on failure.

Keep these columns, in this order.

Like semantic_hash/1, raising on failure.

Print the table Spark renders, and return :ok.

Like show/2, raising on failure.

Sort rows.

Sort within each partition, leaving the partitions unordered.

Like sql/3, raising on failure.

Like storage_level/1, raising on failure.

The result as a lazy stream of Explorer.DataFrames, one per Arrow batch.

Like tail/3, raising on failure.

The first count rows, as maps — limit/2 then collect/2, as in PySpark. Options are collect/2's.

Like take/3, raising on failure.

The raw Arrow IPC streaming-format binaries, one per batch, for doing your own thing.

Like to_arrow/2, raising on failure.

Like to_explorer/2, raising on failure.

to_explorer/2, and what the run reported besides the result. See Latu.ExecutionInfo.

Like to_explorer_with_metrics/2, raising on failure and returning {frame, info}.

Like to_html/2, raising on failure.

Like tree_string/2, raising on failure.

All the rows of both, duplicates kept.

Like unpersist/2, raising on failure. Returns the DataFrame, so it pipes.

filter/2, spelled Spark's other way.

Add or replace columns, keeping the rest.

Like write/2, raising on failure.

Like write_v2/3, raising on failure.

write_v2/3, and what the run reported besides the result. See Latu.ExecutionInfo.

write/2, and what the run reported besides the result. See Latu.ExecutionInfo.

Types

t()

@type t() :: %Latu.DataFrame{plan: Latu.Plan.relation(), session: Latu.Session.t()}

Functions

agg(df, aggregates)

@spec agg(t(), term()) :: t()

Aggregate the whole frame, with no grouping.

Latu.agg(df, total: F.sum(:price))

The same Aggregate relation group_by/2 builds, with no grouping expressions — which is what PySpark's df.agg(...) sends too.

approx_quantile(df, cols, probabilities, relative_error, opts \\ [])

@spec approx_quantile(
  t(),
  [String.t() | atom()] | String.t() | atom(),
  [number()],
  number(),
  keyword()
) :: {:ok, [float()] | [[float()]]} | {:error, Latu.Error.t()}

See Latu.approx_quantile/5.

approx_quantile!(df, cols, probabilities, relative_error, opts \\ [])

@spec approx_quantile!(
  t(),
  [String.t() | atom()] | String.t() | atom(),
  [number()],
  number(),
  keyword()
) :: [float()] | [[float()]]

Like approx_quantile/5, raising on failure.

as(df, name)

@spec as(t(), String.t() | atom()) :: t()

Name the DataFrame, so its columns can be qualified as name.column.

Spark calls this alias, which Elixir cannot use as a function name. See Latu.Plan.as/2.

cache(df)

@spec cache(t()) :: {:ok, t()} | {:error, Latu.Error.t()}

See Latu.cache/1.

cache!(df)

@spec cache!(t()) :: t()

Like cache/1, raising on failure. Returns the DataFrame, so it pipes.

checkpoint(df, opts \\ [])

@spec checkpoint(
  t(),
  keyword()
) :: {:ok, t()} | {:error, Latu.Error.t()}

See Latu.checkpoint/2.

checkpoint!(df, opts \\ [])

@spec checkpoint!(
  t(),
  keyword()
) :: t()

Like checkpoint/2, raising on failure.

coalesce(df, count)

@spec coalesce(t(), pos_integer()) :: t()

Fewer partitions without a shuffle. repartition/2 with shuffle: false.

col(df, name)

@spec col(t(), String.t() | atom()) :: Latu.Plan.expression()

A reference to one of this DataFrame's columns, tagged with its identity.

Latu.col(orders, :id)

Needed only when two DataFrames in one pipeline share a column name, and it has to be a DataFrame the plan already contains: a self-join is fine — both branches are the relation — but selecting one frame's column from another is refused by Spark (CANNOT_RESOLVE_DATAFRAME_COLUMN), hoisted or not. Measured; docs/decisions.md (M9.1). For a value from another frame, use a subquery — scalar/1, exists/1, or Latu.Column.isin/2 over a DataFrame.

col_regex(df, pattern)

@spec col_regex(t(), String.t()) :: Latu.Plan.expression()

See Latu.col_regex/2.

collect(df, opts \\ [])

@spec collect(
  t(),
  keyword()
) :: {:ok, [map()]} | {:error, Latu.Error.t()}

All the rows, as maps.

Latu.collect(df)
#=> {:ok, [%{id: 0}, %{id: 1}]}

Atom keys by default: they pattern-match, and the atom table only grows by the set of column names ever selected. keys: :strings when names come out of dynamic SQL.

The whole result is held at once; stream/2 is the lazy escape for results that do not fit.

collect!(df, opts \\ [])

@spec collect!(
  t(),
  keyword()
) :: [map()]

Like collect/2, raising on failure.

collect_with_metrics(df, opts \\ [])

@spec collect_with_metrics(
  t(),
  keyword()
) :: {:ok, [map()], Latu.ExecutionInfo.t()} | {:error, Latu.Error.t()}

collect/2, and what the run reported besides the rows.

df = Latu.observe(df, :checks, total: F.count(:id))
{:ok, rows, info} = Latu.collect_with_metrics(df)

info.observed  #=> %{checks: %{total: 4}}
info.metrics   #=> Spark's own per-node SQL metrics

See Latu.ExecutionInfo. Options are collect/2's.

collect_with_metrics!(df, opts \\ [])

@spec collect_with_metrics!(
  t(),
  keyword()
) :: {[map()], Latu.ExecutionInfo.t()}

Like collect_with_metrics/2, raising on failure and returning {rows, info}.

columns(df)

@spec columns(t()) :: {:ok, [String.t()]} | {:error, Latu.Error.t()}

See Latu.columns/1.

columns!(df)

@spec columns!(t()) :: [String.t()]

Like columns/1, raising on failure.

corr(df, col1, col2, opts \\ [])

@spec corr(t(), String.t() | atom(), String.t() | atom(), keyword()) ::
  {:ok, float()} | {:error, Latu.Error.t()}

See Latu.corr/4.

corr!(df, col1, col2, opts \\ [])

@spec corr!(t(), String.t() | atom(), String.t() | atom(), keyword()) :: float()

Like corr/4, raising on failure.

count(df, opts \\ [])

@spec count(
  t(),
  keyword()
) :: {:ok, non_neg_integer()} | {:error, Latu.Error.t()}

How many rows, counted by the server.

Latu.count(df)  #=> {:ok, 10}

PySpark's own composition — agg(count(lit(1))), unaliased and all — pinned by the count_action fixture.

count!(df, opts \\ [])

@spec count!(
  t(),
  keyword()
) :: non_neg_integer()

Like count/2, raising on failure.

count_with_metrics(df, opts \\ [])

@spec count_with_metrics(
  t(),
  keyword()
) :: {:ok, non_neg_integer(), Latu.ExecutionInfo.t()} | {:error, Latu.Error.t()}

count/2, and what the run reported besides the result. See Latu.ExecutionInfo.

count_with_metrics!(df, opts \\ [])

@spec count_with_metrics!(
  t(),
  keyword()
) :: {non_neg_integer(), Latu.ExecutionInfo.t()}

Like count_with_metrics/2, raising on failure and returning {count, info}.

cov(df, col1, col2, opts \\ [])

@spec cov(t(), String.t() | atom(), String.t() | atom(), keyword()) ::
  {:ok, float()} | {:error, Latu.Error.t()}

See Latu.cov/4.

cov!(df, col1, col2, opts \\ [])

@spec cov!(t(), String.t() | atom(), String.t() | atom(), keyword()) :: float()

Like cov/4, raising on failure.

create_dataframe(session, data, opts \\ [])

@spec create_dataframe(Latu.Session.t(), term(), keyword()) ::
  {:ok, t()} | {:error, Latu.Error.t()}

See Latu.create_dataframe/3.

create_dataframe!(session, data, opts \\ [])

@spec create_dataframe!(Latu.Session.t(), term(), keyword()) :: t()

Like create_dataframe/3, raising on failure.

create_temp_view(df, name, opts \\ [])

@spec create_temp_view(t(), String.t() | atom(), keyword()) ::
  :ok | {:error, Latu.Error.t()}

See Latu.create_temp_view/3.

create_temp_view!(df, name, opts \\ [])

@spec create_temp_view!(t(), String.t() | atom(), keyword()) :: :ok

Like create_temp_view/3, raising on failure.

cross_join(df, other)

@spec cross_join(t(), t()) :: t()

See Latu.cross_join/2.

crosstab(df, col1, col2)

@spec crosstab(t(), String.t() | atom(), String.t() | atom()) :: t()

See Latu.crosstab/3.

cube(df, columns)

@spec cube(t(), term()) :: Latu.GroupedData.t()

Group by every combination of these columns.

describe(df, cols \\ [])

@spec describe(t(), [String.t() | atom()] | String.t() | atom()) :: t()

See Latu.describe/2.

distinct(df, columns \\ [])

@spec distinct(t(), term()) :: t()

Drop duplicate rows.

Latu.distinct(df)              # every column is a key
Latu.distinct(df, [:suburb])   # these columns are

drop(df, columns)

@spec drop(t(), term()) :: t()

Remove columns.

Latu.drop(df, :x)
Latu.drop(df, [:x, "y"])

drop_na(df, opts \\ [])

@spec drop_na(
  t(),
  keyword()
) :: t()

See Latu.drop_na/2.

dtypes(df)

@spec dtypes(t()) :: {:ok, [{String.t(), String.t()}]} | {:error, Latu.Error.t()}

See Latu.dtypes/1.

dtypes!(df)

@spec dtypes!(t()) :: [{String.t(), String.t()}]

Like dtypes/1, raising on failure.

except(df, other, opts \\ [])

@spec except(t(), t(), keyword()) :: t()

Rows in the first and not the second.

Distinct unless all: true. PySpark spells these subtract and exceptAll; except is Spark's own Scala name and SQL's.

exists(df)

@spec exists(t()) :: Latu.Plan.expression()

A predicate that holds when this DataFrame has any rows at all.

Latu.filter(orders, Latu.exists(Latu.filter(alerts, :open)))

Hoisted like scalar/1, including its note about sessions. Not to be confused with Latu.Functions.exists/2, which is Spark's higher-order function over an array — Spark named both.

explain(df, opts \\ [])

@spec explain(
  t(),
  keyword()
) :: :ok | {:error, Latu.Error.t()}

See Latu.explain/2.

explain!(df, opts \\ [])

@spec explain!(
  t(),
  keyword()
) :: :ok

Like explain/2, raising on failure.

explain_string(df, opts \\ [])

@spec explain_string(
  t(),
  keyword()
) :: {:ok, String.t()} | {:error, Latu.Error.t()}

See Latu.explain_string/2.

explain_string!(df, opts \\ [])

@spec explain_string!(
  t(),
  keyword()
) :: String.t()

Like explain_string/2, raising on failure.

fill_na(df, value, opts \\ [])

@spec fill_na(t(), term(), keyword()) :: t()

See Latu.fill_na/3.

filter(df, condition)

@spec filter(t(), term()) :: t()

Keep the rows the condition holds for.

A string is SQL, parsed by the server — filter(df, "id > 3") is filter(df, expr("id > 3")). This is the only position where a string means SQL: in select/2 it is a column name, and inside an expression it is a literal. PySpark reads all three the same way.

Latu.filter(df, greater(:id, 3))
Latu.filter(df, "id > 3")

first(df, opts \\ [])

@spec first(
  t(),
  keyword()
) :: {:ok, map() | nil} | {:error, Latu.Error.t()}

The first row, or nil when there are none. Options are collect/2's.

first!(df, opts \\ [])

@spec first!(
  t(),
  keyword()
) :: map() | nil

Like first/2, raising on failure.

freq_items(df, cols, opts \\ [])

@spec freq_items(t(), [String.t() | atom()] | String.t() | atom(), keyword()) :: t()

See Latu.freq_items/3.

glimpse(df, opts \\ [])

@spec glimpse(
  t(),
  keyword()
) :: :ok | {:error, Latu.Error.t()}

See Latu.glimpse/2.

glimpse!(df, opts \\ [])

@spec glimpse!(
  t(),
  keyword()
) :: :ok

Like glimpse/2, raising on failure.

group_by(df, columns)

@spec group_by(t(), term()) :: Latu.GroupedData.t()

Group rows, giving a Latu.GroupedData that agg/2 turns back into a DataFrame.

df |> Latu.group_by(:suburb) |> Latu.agg(total: F.sum(:price))

Spark has no group_by relation, so nothing is built until agg/2.

grouping_sets(df, sets, columns \\ [])

@spec grouping_sets(t(), [[term()]], term()) :: Latu.GroupedData.t()

See Latu.grouping_sets/3.

head(df, count_or_opts \\ [], opts \\ [])

@spec head(t(), non_neg_integer() | keyword(), keyword()) ::
  {:ok, map() | nil} | {:ok, [map()]} | {:error, Latu.Error.t()}

first/2 under PySpark's other name: one row or nil, not a list. With a count it is take/3: a list, even for one row. Both shapes are PySpark's. Options are collect/2's.

head!(df, count_or_opts \\ [], opts \\ [])

@spec head!(t(), non_neg_integer() | keyword(), keyword()) :: map() | nil | [map()]

Like head/3, raising on failure.

hint(df, name, parameters \\ [])

@spec hint(t(), String.t() | atom(), [term()]) :: t()

See Latu.hint/3.

input_files(df)

@spec input_files(t()) :: {:ok, [String.t()]} | {:error, Latu.Error.t()}

See Latu.input_files/1.

input_files!(df)

@spec input_files!(t()) :: [String.t()]

Like input_files/1, raising on failure.

insert_into(df, name, opts \\ [])

@spec insert_into(t(), String.t() | atom(), keyword()) ::
  :ok | {:error, Latu.Error.t()}

See Latu.insert_into/3.

insert_into!(df, name, opts \\ [])

@spec insert_into!(t(), String.t() | atom(), keyword()) :: :ok

Like insert_into/3, raising on failure.

insert_into_with_metrics(df, name, opts \\ [])

@spec insert_into_with_metrics(t(), String.t() | atom(), keyword()) ::
  {:ok, Latu.ExecutionInfo.t()} | {:error, Latu.Error.t()}

insert_into/3, and what the run reported besides the result. See Latu.ExecutionInfo.

insert_into_with_metrics!(df, name, opts \\ [])

@spec insert_into_with_metrics!(t(), String.t() | atom(), keyword()) ::
  Latu.ExecutionInfo.t()

Like insert_into_with_metrics/3, raising on failure.

intersect(df, other, opts \\ [])

@spec intersect(t(), t(), keyword()) :: t()

Rows in both. Distinct unless all: true, which is PySpark's intersectAll.

is_empty(df)

@spec is_empty(t()) :: {:ok, boolean()} | {:error, Latu.Error.t()}

See Latu.is_empty/1.

is_empty!(df)

@spec is_empty!(t()) :: boolean()

Like is_empty/1, raising on failure.

is_local(df)

@spec is_local(t()) :: {:ok, boolean()} | {:error, Latu.Error.t()}

See Latu.is_local/1.

is_local!(df)

@spec is_local!(t()) :: boolean()

Like is_local/1, raising on failure.

is_streaming(df)

@spec is_streaming(t()) :: {:ok, boolean()} | {:error, Latu.Error.t()}

See Latu.is_streaming/1.

is_streaming!(df)

@spec is_streaming!(t()) :: boolean()

Like is_streaming/1, raising on failure.

join(df, other, opts \\ [])

@spec join(t(), t(), keyword()) :: t()

Join two DataFrames.

Latu.join(orders, customers, on: :customer_id)
Latu.join(orders, customers, on: expr("o.id = c.id"), how: :left)

join_as_of(df, other, opts)

@spec join_as_of(t(), t(), keyword()) :: t()

See Latu.join_as_of/3.

lateral_join(df, other, opts \\ [])

@spec lateral_join(t(), t(), keyword()) :: t()

See Latu.lateral_join/3.

limit(df, count)

@spec limit(t(), non_neg_integer()) :: t()

Keep at most count rows.

merge(merge, opts \\ [])

@spec merge(
  Latu.MergeInto.t(),
  keyword()
) :: :ok | {:error, Latu.Error.t()}

See Latu.merge/2.

merge!(merge, opts \\ [])

@spec merge!(
  Latu.MergeInto.t(),
  keyword()
) :: :ok

Like merge/2, raising on failure.

merge_into(source, table, condition, opts \\ [])

@spec merge_into(t(), String.t() | atom(), term(), keyword()) :: Latu.MergeInto.t()

See Latu.merge_into/4.

merge_with_metrics(merge, opts \\ [])

@spec merge_with_metrics(
  Latu.MergeInto.t(),
  keyword()
) :: {:ok, Latu.ExecutionInfo.t()} | {:error, Latu.Error.t()}

merge/2, and the metrics an observe/3 in the source plan asked for.

merge_with_metrics!(merge, opts \\ [])

@spec merge_with_metrics!(
  Latu.MergeInto.t(),
  keyword()
) :: Latu.ExecutionInfo.t()

Like merge_with_metrics/2, raising on failure.

metadata_column(df, name)

@spec metadata_column(t(), String.t() | atom()) :: Latu.Plan.expression()

See Latu.metadata_column/2.

nearest_by_join(df, other, ranking, opts)

@spec nearest_by_join(t(), t(), term(), keyword()) :: t()

See Latu.nearest_by_join/4.

observe(df, name, metrics)

@spec observe(t(), String.t() | atom(), keyword() | [Latu.Plan.expression()]) :: t()

See Latu.observe/3.

offset(df, count)

@spec offset(t(), non_neg_integer()) :: t()

Skip the first count rows.

order_by(df, columns)

@spec order_by(t(), term()) :: t()

sort/2, spelled Spark's other way.

parse(df, opts)

@spec parse(
  t(),
  keyword()
) :: t()

See Latu.parse/2.

persist(df, opts \\ [])

@spec persist(
  t(),
  keyword()
) :: {:ok, t()} | {:error, Latu.Error.t()}

See Latu.persist/2.

persist!(df, opts \\ [])

@spec persist!(
  t(),
  keyword()
) :: t()

Like persist/2, raising on failure. Returns the DataFrame, so it pipes.

random_split(df, weights, opts \\ [])

@spec random_split(t(), [number()], keyword()) :: [t()]

See Latu.random_split/3.

range(session, start, stop, step, opts \\ [])

@spec range(Latu.Session.t(), integer(), integer(), integer(), keyword()) :: t()

See Latu.range/2.

read(session, opts)

@spec read(
  Latu.Session.t(),
  keyword()
) :: t()

See Latu.read/2.

release(df)

@spec release(t()) :: :ok | {:error, Latu.Error.t()}

See Latu.release/1.

release!(df)

@spec release!(t()) :: :ok

Like release/1, raising on failure.

rename(df, names)

@spec rename(t(), keyword() | map() | [String.t() | atom()]) :: t()

Rename columns.

Latu.rename(df, id: :n)         # by mapping, leaving the rest
Latu.rename(df, [:renamed])     # positionally, one name per column

Two Spark relations behind one verb: WithColumnsRenamed for pairs, ToDF for a plain list. Explorer.DataFrame.rename/2 reads both shapes the same way.

repartition(df, count)

@spec repartition(t(), pos_integer() | term()) :: t()

Shuffle into count partitions, or partition by these columns, or both.

Latu.repartition(df, 4)
Latu.repartition(df, [:suburb])
Latu.repartition(df, 4, [:suburb])

Two Spark relations: Repartition for a count alone, RepartitionByExpression once columns are named.

repartition(df, count, columns)

@spec repartition(t(), pos_integer(), term()) :: t()

See repartition/2.

repartition_by_range(df, columns, opts \\ [])

@spec repartition_by_range(t(), term(), keyword()) :: t()

See Latu.repartition_by_range/3.

replace(df, replacements, opts \\ [])

@spec replace(t(), [{term(), term()}], keyword()) :: t()

See Latu.replace/3.

rollup(df, columns)

@spec rollup(t(), term()) :: Latu.GroupedData.t()

Group by every prefix of these columns, plus the grand total.

same_semantics(df, other)

@spec same_semantics(t(), t()) :: {:ok, boolean()} | {:error, Latu.Error.t()}

See Latu.same_semantics/2.

same_semantics!(df, other)

@spec same_semantics!(t(), t()) :: boolean()

Like same_semantics/2, raising on failure.

sample(df, fraction, opts \\ [])

@spec sample(t(), number(), keyword()) :: t()

A random fraction of the rows.

Latu.sample(df, 0.1)
Latu.sample(df, 0.1, seed: 42, with_replacement: true)

Without a :seed a random one is drawn, as in PySpark, so the plan differs between runs.

sample_by(df, col, fractions, opts \\ [])

@spec sample_by(t(), term(), [{term(), number()}] | map(), keyword()) :: t()

See Latu.sample_by/4.

save_as_table(df, name, opts \\ [])

@spec save_as_table(t(), String.t() | atom(), keyword()) ::
  :ok | {:error, Latu.Error.t()}

See Latu.save_as_table/3.

save_as_table!(df, name, opts \\ [])

@spec save_as_table!(t(), String.t() | atom(), keyword()) :: :ok

Like save_as_table/3, raising on failure.

save_as_table_with_metrics(df, name, opts \\ [])

@spec save_as_table_with_metrics(t(), String.t() | atom(), keyword()) ::
  {:ok, Latu.ExecutionInfo.t()} | {:error, Latu.Error.t()}

save_as_table/3, and what the run reported besides the result. See Latu.ExecutionInfo.

save_as_table_with_metrics!(df, name, opts \\ [])

@spec save_as_table_with_metrics!(t(), String.t() | atom(), keyword()) ::
  Latu.ExecutionInfo.t()

Like save_as_table_with_metrics/3, raising on failure.

scalar(df)

@spec scalar(t()) :: Latu.Plan.expression()

This DataFrame as a scalar subquery: a single value, usable wherever a value belongs.

totals = Latu.agg(orders, total: F.sum(:amount))
Latu.filter(orders, greater(:amount, Latu.scalar(totals)))

The frame is hoisted into the plan that uses it, so the whole thing is one query and the two frames need no relationship beyond sharing a session. Spark refuses it at analysis if the subquery yields more than one row or column.

This is the reference Spark resolves; a bare col/2 pointing outside its own tree is not.

Unlike join/3 and the set operations, this does not check that both frames come from one session: the referenced plan travels inline, so a cross-session subquery still executes. What does not travel is session-scoped state — a temp view the other frame reads, an artifact behind its local data, a conf set on that session — and Spark names whatever is missing. docs/decisions.md (M9.3).

schema(df)

@spec schema(t()) :: {:ok, [Latu.Result.field()]} | {:error, Latu.Error.t()}

See Latu.schema/1.

schema!(df)

@spec schema!(t()) :: [Latu.Result.field()]

Like schema/1, raising on failure.

select(df, columns)

@spec select(t(), term()) :: t()

Keep these columns, in this order.

A string or an atom is a column name; anything else is an expression. Trailing keywords name what they hold:

Latu.select(df, [:id, doubled: multiply(:id, 2)])

A single column needs no list.

select_expr(df, expressions)

@spec select_expr(t(), [String.t()] | String.t()) :: t()

See Latu.select_expr/2.

semantic_hash(df)

@spec semantic_hash(t()) :: {:ok, integer()} | {:error, Latu.Error.t()}

See Latu.semantic_hash/1.

semantic_hash!(df)

@spec semantic_hash!(t()) :: integer()

Like semantic_hash/1, raising on failure.

show(df, opts \\ [])

@spec show(
  t(),
  keyword()
) :: :ok | {:error, Latu.Error.t()}

Print the table Spark renders, and return :ok.

Byte for byte what PySpark's df.show() prints: Spark formats it server-side, Latu decodes one string cell. Options, all PySpark's:

  • :num_rows — how many rows, default 20
  • :truncate — cell width, default 20; true means 20 and false means no truncation
  • :vertical — one row per block, default false

Pipelines want Kernel.tap/2:

df |> tap(&Latu.show!/1) |> Latu.filter(...)

show!(df, opts \\ [])

@spec show!(
  t(),
  keyword()
) :: :ok

Like show/2, raising on failure.

sort(df, columns)

@spec sort(t(), term()) :: t()

Sort rows.

Latu.sort(df, :id)                    # ascending, nulls first
Latu.sort(df, [desc(:price), :id])
Latu.order_by(df, "id")

A bare name sorts ascending with nulls first, as PySpark's orderBy("id") does. Latu.Column.asc/1, Latu.Column.desc/1 and the four explicit *_nulls_* spellings are the alternatives.

sort_within_partitions(df, columns)

@spec sort_within_partitions(t(), term()) :: t()

Sort within each partition, leaving the partitions unordered.

The same relation as sort/2 with is_global: false, and cheaper: no shuffle.

sql(session, query, bindings \\ [])

@spec sql(Latu.Session.t(), String.t(), [term()] | map() | keyword()) ::
  {:ok, t()} | {:error, Latu.Error.t()}

See Latu.sql/3.

sql!(session, query, bindings \\ [])

@spec sql!(Latu.Session.t(), String.t(), [term()] | map() | keyword()) :: t()

Like sql/3, raising on failure.

storage_level(df)

@spec storage_level(t()) :: {:ok, map()} | {:error, Latu.Error.t()}

See Latu.storage_level/1.

storage_level!(df)

@spec storage_level!(t()) :: map()

Like storage_level/1, raising on failure.

stream(df, opts \\ [])

@spec stream(
  t(),
  keyword()
) :: Enumerable.t()

The result as a lazy stream of Explorer.DataFrames, one per Arrow batch.

Backpressure for results too large to hold: each batch decodes as it arrives, and stopping early releases the execution. Raises Latu.Error on failure, since an enumeration has no way to return one. The schema guard runs on the DataType the server sends ahead of the first batch.

df |> Latu.stream() |> Stream.map(&Explorer.DataFrame.n_rows/1) |> Enum.sum()

summary(df, statistics \\ [])

@spec summary(t(), [String.t() | atom()] | String.t() | atom()) :: t()

See Latu.summary/2.

table(session, name, options \\ [])

@spec table(Latu.Session.t(), String.t() | atom(), keyword() | map()) :: t()

See Latu.table/2.

table_changes(session, table, opts \\ [])

@spec table_changes(Latu.Session.t(), String.t() | atom(), keyword()) :: t()

See Latu.table_changes/3.

table_function(session, name, arguments \\ [])

@spec table_function(Latu.Session.t(), String.t() | atom(), [term()]) :: t()

See Latu.table_function/3.

tail(df, count, opts \\ [])

@spec tail(t(), non_neg_integer(), keyword()) ::
  {:ok, [map()]} | {:error, Latu.Error.t()}

See Latu.tail/3.

tail!(df, count, opts \\ [])

@spec tail!(t(), non_neg_integer(), keyword()) :: [map()]

Like tail/3, raising on failure.

take(df, count, opts \\ [])

@spec take(t(), non_neg_integer(), keyword()) ::
  {:ok, [map()]} | {:error, Latu.Error.t()}

The first count rows, as maps — limit/2 then collect/2, as in PySpark. Options are collect/2's.

take!(df, count, opts \\ [])

@spec take!(t(), non_neg_integer(), keyword()) :: [map()]

Like take/3, raising on failure.

to(df, schema)

@spec to(t(), Latu.Plan.data_type()) :: t()

See Latu.to/2.

to_arrow(df, opts \\ [])

@spec to_arrow(
  t(),
  keyword()
) :: {:ok, [binary()]} | {:error, Latu.Error.t()}

The raw Arrow IPC streaming-format binaries, one per batch, for doing your own thing.

Bypasses the decoder AND the schema guard on purpose: these bytes are headed for some other Arrow reader, whose capabilities are its own business. Each binary is a complete IPC stream — schema, record batch, end marker — and they must never be byte-concatenated.

to_arrow!(df, opts \\ [])

@spec to_arrow!(
  t(),
  keyword()
) :: [binary()]

Like to_arrow/2, raising on failure.

to_explorer(df, opts \\ [])

@spec to_explorer(
  t(),
  keyword()
) :: {:ok, Explorer.DataFrame.t()} | {:error, Latu.Error.t()}

The result as one Explorer.DataFrame.

Unbounded, like collect/2, to_arrow/2 and Spark's own collect: the whole result comes back. To take part of it, bound the plan — which is how Spark does it, and what limit/2 is for. A result too large to hold at all is what stream/2 is for.

{:ok, frame} = Latu.to_explorer(df)
{:ok, frame} = df |> Latu.limit(10_000) |> Latu.to_explorer()

An empty result is a 0-row frame with the right columns and dtypes — the server sends the Arrow schema even when there are no rows.

to_explorer!(df, opts \\ [])

@spec to_explorer!(
  t(),
  keyword()
) :: Explorer.DataFrame.t()

Like to_explorer/2, raising on failure.

to_explorer_with_metrics(df, opts \\ [])

@spec to_explorer_with_metrics(
  t(),
  keyword()
) ::
  {:ok, Explorer.DataFrame.t(), Latu.ExecutionInfo.t()}
  | {:error, Latu.Error.t()}

to_explorer/2, and what the run reported besides the result. See Latu.ExecutionInfo.

to_explorer_with_metrics!(df, opts \\ [])

@spec to_explorer_with_metrics!(
  t(),
  keyword()
) :: {Explorer.DataFrame.t(), Latu.ExecutionInfo.t()}

Like to_explorer_with_metrics/2, raising on failure and returning {frame, info}.

to_html(df, opts \\ [])

@spec to_html(
  t(),
  keyword()
) :: {:ok, String.t()} | {:error, Latu.Error.t()}

See Latu.to_html/2.

to_html!(df, opts \\ [])

@spec to_html!(
  t(),
  keyword()
) :: String.t()

Like to_html/2, raising on failure.

transpose(df, index_column \\ nil)

@spec transpose(t(), term() | nil) :: t()

See Latu.transpose/2.

tree_string(df, opts \\ [])

@spec tree_string(
  t(),
  keyword()
) :: {:ok, String.t()} | {:error, Latu.Error.t()}

See Latu.tree_string/2.

tree_string!(df, opts \\ [])

@spec tree_string!(
  t(),
  keyword()
) :: String.t()

Like tree_string/2, raising on failure.

union(df, other, opts \\ [])

@spec union(t(), t(), keyword()) :: t()

All the rows of both, duplicates kept.

  • :all — default true, as Spark's union is UNION ALL rather than SQL's UNION

  • :by_name — match columns by name rather than position

  • :allow_missing_columns — fill a missing column with null; needs :by_name

    Latu.union(df, other) Latu.union(df, other, by_name: true)

unpersist(df, opts \\ [])

@spec unpersist(
  t(),
  keyword()
) :: {:ok, t()} | {:error, Latu.Error.t()}

See Latu.unpersist/2.

unpersist!(df, opts \\ [])

@spec unpersist!(
  t(),
  keyword()
) :: t()

Like unpersist/2, raising on failure. Returns the DataFrame, so it pipes.

unpivot(df, ids, opts)

@spec unpivot(t(), term(), keyword()) :: t()

See Latu.unpivot/3.

where(df, condition)

@spec where(t(), term()) :: t()

filter/2, spelled Spark's other way.

with_checkpoint(df, opts, fun)

@spec with_checkpoint(t(), keyword(), (t() -> result)) ::
  {:ok, result} | {:error, Latu.Error.t()}
when result: term()

See Latu.with_checkpoint/3.

with_checkpoint!(df, opts, fun)

@spec with_checkpoint!(t(), keyword(), (t() -> result)) :: result when result: term()

Like with_checkpoint/3, raising on failure.

with_columns(df, columns)

@spec with_columns(
  t(),
  keyword()
) :: t()

Add or replace columns, keeping the rest.

Latu.with_columns(df, doubled: multiply(:id, 2))
Latu.with_columns(df, a: add(:id, 1), b: subtract(:id, 1))

A keyword list, because it is ordered and a map is not. There is no with_column: Spark has no singular relation, and the keyword form is already short.

with_metadata(df, name, metadata)

@spec with_metadata(t(), String.t() | atom(), map()) :: t()

See Latu.with_metadata/3.

write(df, opts)

@spec write(
  t(),
  keyword()
) :: :ok | {:error, Latu.Error.t()}

See Latu.write/2.

write!(df, opts)

@spec write!(
  t(),
  keyword()
) :: :ok

Like write/2, raising on failure.

write_v2(df, table, opts)

@spec write_v2(t(), String.t() | atom(), keyword()) :: :ok | {:error, Latu.Error.t()}

See Latu.write_v2/3.

write_v2!(df, table, opts)

@spec write_v2!(t(), String.t() | atom(), keyword()) :: :ok

Like write_v2/3, raising on failure.

write_v2_with_metrics(df, table, opts)

@spec write_v2_with_metrics(t(), String.t() | atom(), keyword()) ::
  {:ok, Latu.ExecutionInfo.t()} | {:error, Latu.Error.t()}

write_v2/3, and what the run reported besides the result. See Latu.ExecutionInfo.

write_v2_with_metrics!(df, table, opts)

@spec write_v2_with_metrics!(t(), String.t() | atom(), keyword()) ::
  Latu.ExecutionInfo.t()

Like write_v2_with_metrics/3, raising on failure.

write_with_metrics(df, opts)

@spec write_with_metrics(
  t(),
  keyword()
) :: {:ok, Latu.ExecutionInfo.t()} | {:error, Latu.Error.t()}

write/2, and what the run reported besides the result. See Latu.ExecutionInfo.

This is the shape observe was built for: a data-quality aggregate attached on the way in, the write done, the counts read back. There are no rows to return, so the metrics are the whole result.

df = Latu.observe(df, :quality, rows: F.count(:id), worst: F.min(:price))
{:ok, info} = Latu.write_with_metrics(df, path: "/out")

info.observed  #=> %{quality: %{rows: 1000, worst: -2.5}}

write_with_metrics!(df, opts)

@spec write_with_metrics!(
  t(),
  keyword()
) :: Latu.ExecutionInfo.t()

Like write_with_metrics/2, raising on failure.

zip_with_index(df, name \\ :index)

@spec zip_with_index(t(), String.t() | atom()) :: t()

See Latu.zip_with_index/2.