F3 (F3 v0.1.0)

Copy Markdown

Experimental Elixir API for the F3 columnar file format.

Upstream F3 is a research prototype. open/2 and close/1 acquire and release a byte source. schema/1, metadata/1, and inspect/2 decode the postscript and the FlatBuffers footer. read/2 returns columnar batches. stream/2 returns a lazy stream and does not pread until the first pull. F3.Writer writes a local file when F3_BUILD_NATIVE=1 and returns :not_implemented otherwise. Wasm bytes are not executed.

max_metadata_bytes on the metadata calls caps the postscript size fields. The default is F3.Postscript.max_metadata_bytes/0.

These functions return {:error, %F3.Error{}} for a missing file, a closed handle, a bad option, or a footer this build will not decode. They do not raise for those cases. stream/2 returns that error before a stream when the native decoder is not loaded. A later pull that cannot decode throws {:f3, %F3.Error{}}.

A memory source is open(path, source: {:memory, binary}) or open({:memory, binary}). When the :source option is a memory binary, path is not opened. An HTTP source is open(path, source: {:http, url}) or open(path, source: {:http, url, opts}). path is not opened. Remote reads do not follow redirects and do not fetch lib_urls.

Summary

Functions

Releases the source. Closing a closed handle returns {:ok, file}. A value that is not an F3.File is :invalid_argument. This does not raise.

Textual inspection of a file.

Returns footer and column metadata.

Opens a local path, an in-memory binary, or an HTTP url.

Reads row data into columnar batches.

Returns the logical schema from the footer.

Lazy columnar batches.

Functions

close(file)

@spec close(F3.File.t()) :: {:ok, F3.File.t()} | {:error, F3.Error.t()}

Releases the source. Closing a closed handle returns {:ok, file}. A value that is not an F3.File is :invalid_argument. This does not raise.

Examples

iex> {:ok, file} = F3.open("test/fixtures/small.f3")
iex> {:ok, closed} = F3.close(file)
iex> closed.closed
true
iex> {:ok, again} = F3.close(closed)
iex> again.closed
true
iex> {:error, error} = F3.close(:nope)
iex> error.message
"expected an F3.File"

inspect(file, opts \\ [])

@spec inspect(F3.File.t(), keyword()) :: {:ok, String.t()} | {:error, F3.Error.t()}

Textual inspection of a file.

This name is the public inspection API. It is unrelated to Kernel.inspect/2. The default is a compact summary. detail: true adds row groups, encoding units, and optional sections. format: :json returns the golden metadata map encoded as JSON. Neither form includes column payloads.

max_metadata_bytes overrides the allocation cap for this call. detail must be a boolean. format is :text or :json.

Examples

iex> {:ok, file} = F3.open("test/fixtures/small.f3")
iex> {:ok, text} = F3.inspect(file, detail: false, format: :text)
iex> text =~ "schema: trade_id int64, symbol utf8, price float64"
true

metadata(file, opts \\ [])

@spec metadata(F3.File.t(), keyword()) ::
  {:ok, F3.Metadata.t()} | {:error, F3.Error.t()}

Returns footer and column metadata.

Checksums are reported and not verified. Wasm sections are offsets only. max_metadata_bytes overrides the allocation cap for this call.

Examples

iex> {:ok, file} = F3.open("test/fixtures/small.f3")
iex> {:ok, meta} = F3.metadata(file)
iex> {meta.major_version, meta.minor_version, length(meta.row_groups)}
{0, 1, 1}

open(source, opts \\ [])

@spec open(Path.t() | {:memory, binary()}, keyword()) ::
  {:ok, F3.File.t()} | {:error, F3.Error.t()}

Opens a local path, an in-memory binary, or an HTTP url.

The returned handle is not validated as F3. Missing local paths and missing HTTP objects return {:error, %F3.Error{category: :not_found}}. HTTP options live inside {:http, url, opts}.

opts for a path may contain :source. A memory tuple accepts no options. :source is :file (the default), {:memory, binary}, {:http, url}, or {:http, url, http_opts}. HTTP options are headers:, timeout_ms: (default 10_000), sign:, and the six SigV4 keys. See F3.IO.HTTP.

Examples

iex> {:ok, file} = F3.open("test/fixtures/small.f3")
iex> file.origin
"test/fixtures/small.f3"

iex> {:ok, memory} = F3.open("unused", source: {:memory, <<1, 2>>})
iex> memory.origin
:memory

iex> {:error, missing} = F3.open("missing-file.f3")
iex> missing.category
:not_found

iex> {:error, bad} = F3.open("unused", source: {:http, "ftp://example.test/a"})
iex> bad.message
"http url must be http or https"

read(file, opts \\ [])

@spec read(F3.File.t(), keyword()) :: {:ok, [F3.Batch.t()]} | {:error, F3.Error.t()}

Reads row data into columnar batches.

Options:

  • :columns — logical field names. Omitted means every top-level field. An unknown name is :invalid_projection.
  • :row_groups — zero-based indexes. An index past the footer is :invalid_argument. Omitted means every row group.
  • :batch_size — a positive integer. The default is 8192.
  • :wasm — :off (default) or :sandbox. :off returns :unsupported_encoding for CUSTOM_WASM and does not read the module. :sandbox runs that module inside the fuel and memory caps.
  • :extensions — pid from F3.Registry.start_link/1. A matching Elixir extension runs instead of Wasm. Registries are not shared by default.
  • :wasm_module_bytes — module size cap. Default 1 MiB. Hard ceiling 16 MiB.

One native call decodes a whole encoding unit. Elixir slices it to batch_size. These fixtures are smaller than one unit. Without the native decoder this returns :native_unavailable and does not read the payload. A bad option is :invalid_argument and does not raise.

Examples

iex> {:ok, file} = F3.open("test/fixtures/small.f3")
iex> {:error, error} = F3.read(file, columns: ["trade_id"], batch_size: 2)
iex> error.category
:native_unavailable

schema(file, opts \\ [])

@spec schema(F3.File.t(), keyword()) :: {:ok, F3.Schema.t()} | {:error, F3.Error.t()}

Returns the logical schema from the footer.

max_metadata_bytes overrides the allocation cap for this call. It must be a non-negative integer. The default is 16_777_216. A closed file is :closed. A short file is :truncated.

Examples

iex> {:ok, file} = F3.open("test/fixtures/small.f3")
iex> {:ok, schema} = F3.schema(file, max_metadata_bytes: 1_048_576)
iex> Enum.map(schema.fields, & &1.name)
["trade_id", "symbol", "price"]

stream(file, opts \\ [])

@spec stream(F3.File.t(), keyword()) :: {:ok, Enumerable.t()} | {:error, F3.Error.t()}

Lazy columnar batches.

The stream does not pread until the first pull. Enum.take/2 stops further reads. The caller still owns close/1. Options match read/2. A pull that cannot decode throws {:f3, %F3.Error{}}. Without the native decoder this returns {:error, %F3.Error{}} and does not return a stream.

Examples

iex> {:ok, file} = F3.open("test/fixtures/small.f3")
iex> {:error, error} = F3.stream(file, wasm: :off, row_groups: [0])
iex> error.message
"native decoder is not loaded"