Skip to main content

lib/quack_db/protocol/fsst.ex

if Code.ensure_loaded?(FSST.Table) do
  defmodule QuackDB.Protocol.FSST do
    @moduledoc """
    Internal bridge to the optional `:fsst` package.

    DuckDB currently flattens FSST vectors before Quack serialization in the
    versions QuackDB targets. This module keeps the package boundary ready for
    future Quack payloads that expose serialized FSST symbols and compressed
    string payloads.
    """

    alias QuackDB.Error

    @spec decompress_values([binary()], [binary()]) :: {:ok, [binary()]} | {:error, Error.t()}
    def decompress_values(symbols, payloads) when is_list(symbols) and is_list(payloads) do
      with {:ok, table} <- table_from_symbols(symbols) do
        decompress_payloads(table, payloads, [])
      end
    end

    def decompress_values(_symbols, _payloads) do
      error(:invalid_fsst_payload, "expected FSST symbols and payloads to be lists")
    end

    defp table_from_symbols(symbols) do
      case FSST.Table.from_symbols(symbols) do
        {:ok, table} ->
          {:ok, table}

        {:error, reason} ->
          error(:invalid_fsst_symbols, "invalid FSST symbol table: #{inspect(reason)}")
      end
    end

    defp decompress_payloads(_table, [], acc), do: {:ok, Enum.reverse(acc)}

    defp decompress_payloads(table, [payload | payloads], acc) when is_binary(payload) do
      case FSST.decompress(table, payload) do
        {:ok, value} ->
          decompress_payloads(table, payloads, [value | acc])

        {:error, reason} ->
          error(:invalid_fsst_payload, "invalid FSST payload: #{inspect(reason)}")
      end
    end

    defp decompress_payloads(_table, [_payload | _payloads], _acc) do
      error(:invalid_fsst_payload, "expected compressed FSST payloads to be binaries")
    end

    defp error(code, message) do
      {:error, Error.new(code, message, source: :protocol)}
    end
  end
end