defmodule QuackDB.Secret do @moduledoc """ SQL builders for DuckDB secrets. DuckDB uses secrets to configure access to HTTP endpoints, object stores, and cloud filesystems. These helpers build `CREATE SECRET` statements with QuackDB's SQL literal formatting so credentials and scopes are escaped consistently. alias QuackDB.Secret Secret.create(:s3, provider: :credential_chain, scope: "s3://bucket/prefix/") Secret.create(:http, name: :api, bearer_token: token) Atom values are emitted as DuckDB identifiers, which is useful for options such as `PROVIDER credential_chain`. String values are emitted as SQL string literals. """ alias QuackDB.Error alias QuackDB.SQL @type secret_type :: :http | :s3 | :r2 | :gcs | :azure | :huggingface | String.t() @type option_value :: SQL.parameter() | atom() | map() @doc "Builds a DuckDB `CREATE SECRET` statement." @spec create(secret_type(), keyword(option_value())) :: iodata() def create(type, options \\ []) when is_list(options) do {name, options} = Keyword.pop(options, :name) {replace?, options} = Keyword.pop(options, :replace, true) {temporary?, options} = Keyword.pop(options, :temporary, false) [ "CREATE ", if(replace?, do: "OR REPLACE ", else: []), if(temporary?, do: "TEMPORARY ", else: []), "SECRET ", secret_name(name), "(", secret_options(Keyword.put_new(options, :type, type)), ");" ] end defp secret_name(nil), do: [] defp secret_name(name), do: [identifier!(name, :secret), " "] defp secret_options(options) do options |> Enum.map(fn {name, value} -> [option_name(name), " ", option_value(value)] end) |> Enum.intersperse(", ") end defp option_name(:type), do: "TYPE" defp option_name(name) when is_atom(name), do: name |> Atom.to_string() |> option_name() defp option_name(name) when is_binary(name) do name |> String.upcase() |> identifier!(:option) end defp option_name(name), do: invalid_identifier!(name, :option) defp option_value(value) when is_atom(value) and not is_boolean(value) and not is_nil(value) do identifier!(value, :value) end defp option_value(value) when is_map(value) do entries = value |> Enum.map(fn {key, entry_value} -> [literal_key(key), ": ", option_value(entry_value)] end) |> Enum.intersperse(", ") ["MAP {", entries, "}"] end defp option_value(value), do: literal!(value) defp literal_key(key) when is_atom(key), do: key |> Atom.to_string() |> literal_key() defp literal_key(key) when is_binary(key), do: ["'", String.replace(key, "'", "''"), "'"] defp literal!(value) do case SQL.literal(value) do {:ok, literal} -> literal {:error, %Error{} = error} -> raise error end end defp identifier!(value, kind) do if QuackDB.Identifier.valid?(value) do to_string(value) else invalid_identifier!(value, kind) end end defp invalid_identifier!(value, kind) do raise ArgumentError, "invalid DuckDB secret #{kind} identifier: #{inspect(value)}" end end