This is an automated email from the ASF dual-hosted git repository.
fresh-borzoni pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fluss-rust.git
The following commit(s) were added to refs/heads/main by this push:
new 5ac69f38 [elixir] feat: Support full table descriptor options in
create_table (#647)
5ac69f38 is described below
commit 5ac69f38fe0500d68d603167fce67acde5e3f261
Author: Nicoleta Lazar <[email protected]>
AuthorDate: Fri Jul 3 15:56:02 2026 +0300
[elixir] feat: Support full table descriptor options in create_table (#647)
This commit brings the Elixir's create_table descriptor to full parity
with the native builder by also supporting :bucket_keys,
:partition_keys, :custom_properties & comment.
The optional fields are bundled into a Fluss.TableDescriptor.Options struct
decoded across the NIF boundary as a NifStruct (NifTableOptions), rather
than
growing table_descriptor_new to seven positional arguments. This mirrors the
existing NifDatabaseDescriptor pattern and the Python binding, which builds
the
core descriptor with unconditional .properties / .custom_properties /
.partitioned_by / .distributed_by calls.
:properties is now a map (was a list of {k, v} tuples), fixing a write/read
asymmetry — get_table_info already reports properties as a map — and
matching
:custom_properties.
---
bindings/elixir/lib/fluss/native.ex | 2 +-
bindings/elixir/lib/fluss/table_descriptor.ex | 63 ++++++++++++++++--
bindings/elixir/native/fluss_nif/src/schema.rs | 31 ++++++---
bindings/elixir/test/integration/admin_test.exs | 65 +++++++++++++++++++
bindings/elixir/test/table_descriptor_test.exs | 86 +++++++++++++++++++++++++
5 files changed, 234 insertions(+), 13 deletions(-)
diff --git a/bindings/elixir/lib/fluss/native.ex
b/bindings/elixir/lib/fluss/native.ex
index 6de93683..3713e720 100644
--- a/bindings/elixir/lib/fluss/native.ex
+++ b/bindings/elixir/lib/fluss/native.ex
@@ -61,7 +61,7 @@ defmodule Fluss.Native do
do: :erlang.nif_error(:nif_not_loaded)
# Schema / TableDescriptor
- def table_descriptor_new(_schema, _bucket_count, _properties),
+ def table_descriptor_new(_schema, _options),
do: :erlang.nif_error(:nif_not_loaded)
# Table
diff --git a/bindings/elixir/lib/fluss/table_descriptor.ex
b/bindings/elixir/lib/fluss/table_descriptor.ex
index b95b5a50..9bd8485f 100644
--- a/bindings/elixir/lib/fluss/table_descriptor.ex
+++ b/bindings/elixir/lib/fluss/table_descriptor.ex
@@ -19,25 +19,80 @@ defmodule Fluss.TableDescriptor do
@moduledoc """
Descriptor for creating a Fluss table.
- Options: `:bucket_count`, `:properties` (list of `{key, value}` string
tuples).
+ Bundles a `Fluss.Schema` with optional table settings; pass the result to
+ `Fluss.Admin.create_table/5`.
+
+ ## Options
+
+ `new!/2` takes a keyword list:
+
+ * `:bucket_count` - number of buckets to distribute the table into
+ (non-negative integer). Omitted means the server decides.
+ * `:bucket_keys` - column names to hash for bucketing. For primary-key
+ tables these must be a subset of the primary key (excluding partition
+ keys), defaulting to the primary key columns when omitted. For log
+ tables they are unconstrained.
+ * `:partition_keys` - column names to partition the table by.
+ * `:properties` - map of recognized Fluss table properties that the engine
+ interprets, e.g. `%{"table.log.format" => "ARROW"}`.
+ * `:custom_properties` - map of arbitrary string metadata (e.g. ownership,
+ team). Stored verbatim and never interpreted by Fluss.
+ * `:comment` - table comment.
## Examples
Fluss.TableDescriptor.new!(schema)
Fluss.TableDescriptor.new!(schema, bucket_count: 3)
+ Fluss.TableDescriptor.new!(schema,
+ bucket_count: 4,
+ bucket_keys: ["id"],
+ partition_keys: ["dt"],
+ custom_properties: %{"owner" => "data-platform"},
+ comment: "events table"
+ )
+
"""
+ defmodule Options do
+ @moduledoc false
+ # Internal boundary struct decoded by the `table_descriptor_new` NIF.
+ # Users supply these as a keyword list to `Fluss.TableDescriptor.new!/2`;
+ # `new!/2` normalizes that into this struct before crossing into Rust.
+
+ @type t :: %__MODULE__{
+ bucket_count: non_neg_integer() | nil,
+ bucket_keys: list(),
+ partition_keys: list(),
+ properties: map(),
+ custom_properties: map(),
+ comment: String.t() | nil
+ }
+
+ defstruct bucket_count: nil,
+ bucket_keys: [],
+ partition_keys: [],
+ properties: %{},
+ custom_properties: %{},
+ comment: nil
+ end
+
alias Fluss.Native
@type t :: reference()
+ @doc """
+ Builds a table descriptor from a `Fluss.Schema` and a keyword list of
options.
+
+ See the module documentation for the available options. Raises `Fluss.Error`
+ if the descriptor cannot be built (e.g. an invalid bucket-key/partition-key
+ combination), or `KeyError` if an unknown option key is given.
+ """
@spec new!(Fluss.Schema.t(), keyword()) :: t()
def new!(%Fluss.Schema{} = schema, opts \\ []) do
- bucket_count = Keyword.get(opts, :bucket_count)
- properties = Keyword.get(opts, :properties, [])
+ opts_struct = struct!(Fluss.TableDescriptor.Options, opts)
- case Native.table_descriptor_new(schema, bucket_count, properties) do
+ case Native.table_descriptor_new(schema, opts_struct) do
{:error, %Fluss.Error{} = err} -> raise err
ref -> ref
end
diff --git a/bindings/elixir/native/fluss_nif/src/schema.rs
b/bindings/elixir/native/fluss_nif/src/schema.rs
index 410334fc..95b31bf2 100644
--- a/bindings/elixir/native/fluss_nif/src/schema.rs
+++ b/bindings/elixir/native/fluss_nif/src/schema.rs
@@ -19,6 +19,7 @@ use crate::atoms::to_nif_err;
use fluss::error::Error;
use fluss::metadata::{self, DataTypes, Schema, TableDescriptor};
use rustler::{NifStruct, NifTaggedEnum, ResourceArc};
+use std::collections::HashMap;
pub struct TableDescriptorResource {
pub inner: TableDescriptor,
@@ -129,11 +130,21 @@ impl NifSchema {
}
}
+#[derive(NifStruct)]
+#[module = "Fluss.TableDescriptor.Options"]
+pub struct NifTableOptions {
+ pub bucket_count: Option<i32>,
+ pub bucket_keys: Vec<String>,
+ pub partition_keys: Vec<String>,
+ pub properties: HashMap<String, String>,
+ pub custom_properties: HashMap<String, String>,
+ pub comment: Option<String>,
+}
+
#[rustler::nif]
fn table_descriptor_new(
schema: NifSchema,
- bucket_count: Option<i32>,
- properties: Vec<(String, String)>,
+ opts: NifTableOptions,
) -> Result<ResourceArc<TableDescriptorResource>, rustler::Error> {
let mut schema_builder = Schema::builder();
for (name, dt) in &schema.columns {
@@ -142,14 +153,18 @@ fn table_descriptor_new(
if !schema.primary_key.is_empty() {
schema_builder = schema_builder.primary_key(schema.primary_key);
}
+
let built_schema = schema_builder.build().map_err(to_nif_err)?;
- let mut builder = TableDescriptor::builder().schema(built_schema);
- if let Some(count) = bucket_count {
- builder = builder.distributed_by(Some(count), vec![]);
- }
- for (key, value) in properties {
- builder = builder.property(&key, &value);
+ let mut builder = TableDescriptor::builder()
+ .schema(built_schema)
+ .properties(opts.properties)
+ .custom_properties(opts.custom_properties)
+ .partitioned_by(opts.partition_keys)
+ .distributed_by(opts.bucket_count, opts.bucket_keys);
+
+ if let Some(comment) = opts.comment {
+ builder = builder.comment(comment);
}
let descriptor = builder.build().map_err(to_nif_err)?;
Ok(ResourceArc::new(TableDescriptorResource {
diff --git a/bindings/elixir/test/integration/admin_test.exs
b/bindings/elixir/test/integration/admin_test.exs
index 8c34187a..1de7a1e7 100644
--- a/bindings/elixir/test/integration/admin_test.exs
+++ b/bindings/elixir/test/integration/admin_test.exs
@@ -400,4 +400,69 @@ defmodule Fluss.Integration.AdminTest do
Fluss.Admin.get_table_schema(admin, @database, table)
end
end
+
+ describe "create_table/5 with descriptor options" do
+ test "round-trips comment and custom_properties", %{admin: admin} do
+ table = "fluss_table_#{:rand.uniform(100_000)}"
+
+ schema =
+ Fluss.Schema.new()
+ |> Fluss.Schema.column("id", :int)
+ |> Fluss.Schema.column("name", :string)
+
+ descriptor =
+ Fluss.TableDescriptor.new!(schema,
+ comment: "events table",
+ custom_properties: %{"owner" => "data-platform"}
+ )
+
+ :ok = Fluss.Admin.create_table(admin, @database, table, descriptor, true)
+ on_exit(fn -> Fluss.Admin.drop_table(admin, @database, table, true) end)
+
+ assert {:ok, info} = Fluss.Admin.get_table_info(admin, @database, table)
+ assert info.comment == "events table"
+ assert info.custom_properties == %{"owner" => "data-platform"}
+ end
+
+ test "round-trips explicit bucket_keys on a primary-key table", %{admin:
admin} do
+ table = "fluss_table_#{:rand.uniform(100_000)}"
+
+ schema =
+ Fluss.Schema.new()
+ |> Fluss.Schema.column("id", :int)
+ |> Fluss.Schema.column("region", :string)
+ |> Fluss.Schema.primary_key(["id", "region"])
+
+ descriptor = Fluss.TableDescriptor.new!(schema, bucket_count: 3,
bucket_keys: ["id"])
+
+ :ok = Fluss.Admin.create_table(admin, @database, table, descriptor, true)
+ on_exit(fn -> Fluss.Admin.drop_table(admin, @database, table, true) end)
+
+ assert {:ok, info} = Fluss.Admin.get_table_info(admin, @database, table)
+ assert info.num_buckets == 3
+ assert info.bucket_keys == ["id"]
+ # An explicit, non-default bucket key (the default would be the full PK).
+ assert info.is_default_bucket_key == false
+ end
+
+ test "round-trips partition_keys and reports the table as partitioned",
%{admin: admin} do
+ table = "fluss_table_#{:rand.uniform(100_000)}"
+
+ # Partition keys must be a subset of the primary key for PK tables.
+ schema =
+ Fluss.Schema.new()
+ |> Fluss.Schema.column("id", :int)
+ |> Fluss.Schema.column("dt", :string)
+ |> Fluss.Schema.primary_key(["id", "dt"])
+
+ descriptor = Fluss.TableDescriptor.new!(schema, partition_keys: ["dt"],
bucket_count: 2)
+
+ :ok = Fluss.Admin.create_table(admin, @database, table, descriptor, true)
+ on_exit(fn -> Fluss.Admin.drop_table(admin, @database, table, true) end)
+
+ assert {:ok, info} = Fluss.Admin.get_table_info(admin, @database, table)
+ assert info.is_partitioned == true
+ assert info.partition_keys == ["dt"]
+ end
+ end
end
diff --git a/bindings/elixir/test/table_descriptor_test.exs
b/bindings/elixir/test/table_descriptor_test.exs
new file mode 100644
index 00000000..3c391319
--- /dev/null
+++ b/bindings/elixir/test/table_descriptor_test.exs
@@ -0,0 +1,86 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+defmodule Fluss.TableDescriptorTest do
+ use ExUnit.Case, async: true
+
+ alias Fluss.Schema
+ alias Fluss.TableDescriptor
+
+ describe "new!/2" do
+ test "returns a descriptor reference for a minimal schema" do
+ schema =
+ Schema.new()
+ |> Schema.column("id", :int)
+ |> Schema.column("name", :string)
+
+ assert is_reference(TableDescriptor.new!(schema))
+ end
+
+ test "returns a descriptor reference with all options set" do
+ schema =
+ Schema.new()
+ |> Schema.column("id", :int)
+ |> Schema.column("dt", :string)
+ |> Schema.primary_key(["id", "dt"])
+
+ descriptor =
+ TableDescriptor.new!(schema,
+ bucket_count: 3,
+ bucket_keys: ["id"],
+ partition_keys: ["dt"],
+ properties: %{"table.replication.factor" => "1"},
+ custom_properties: %{"owner" => "data-platform"},
+ comment: "events table"
+ )
+
+ assert is_reference(descriptor)
+ end
+
+ test "raises Fluss.Error when bucket keys are not a subset of the primary
key" do
+ schema =
+ Schema.new()
+ |> Schema.column("id", :int)
+ |> Schema.column("region", :string)
+ |> Schema.primary_key(["id"])
+
+ assert_raise Fluss.Error, fn ->
+ TableDescriptor.new!(schema, bucket_keys: ["region"])
+ end
+ end
+
+ test "raises Fluss.Error when bucket keys overlap partition keys" do
+ schema =
+ Schema.new()
+ |> Schema.column("id", :int)
+ |> Schema.column("dt", :string)
+
+ assert_raise Fluss.Error, fn ->
+ TableDescriptor.new!(schema, partition_keys: ["dt"], bucket_keys:
["dt"])
+ end
+ end
+
+ # `new!/2` rejects unknown keys rather than silently dropping them.
+ test "raises KeyError on an unknown option" do
+ schema = Schema.new() |> Schema.column("id", :int)
+
+ assert_raise KeyError, fn ->
+ TableDescriptor.new!(schema, bogus: 1)
+ end
+ end
+ end
+end