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 c6d04a32 [elixir] feat: Allow comment and custom_properties on 
create_database (#597)
c6d04a32 is described below

commit c6d04a32d1d5d8d3e6a1b79453bd9d93ee8806b3
Author: Nicoleta Lazar <[email protected]>
AuthorDate: Thu Jun 11 00:45:44 2026 +0300

    [elixir] feat: Allow comment and custom_properties on create_database (#597)
    
    * [elixir] feat: Allow comment and custom_properties on create_database
    
    Thread an optional DatabaseDescriptor through Fluss.Admin.create_database
    so callers can set a database comment and custom properties at creation
    time.
    
    * Add missing admin test for create_database/4
---
 bindings/elixir/lib/fluss/admin.ex                 |  13 +-
 bindings/elixir/lib/fluss/database_descriptor.ex   |  37 ++++-
 bindings/elixir/lib/fluss/native.ex                |   2 +-
 bindings/elixir/native/fluss_nif/src/admin.rs      |  14 +-
 bindings/elixir/test/database_descriptor_test.exs  | 169 +++++++++++++++++++++
 bindings/elixir/test/integration/admin_test.exs    |  92 ++++++++++-
 .../elixir/test/integration/log_table_test.exs     |   2 +-
 7 files changed, 318 insertions(+), 11 deletions(-)

diff --git a/bindings/elixir/lib/fluss/admin.ex 
b/bindings/elixir/lib/fluss/admin.ex
index 8a8ae9c4..b9d26f70 100644
--- a/bindings/elixir/lib/fluss/admin.ex
+++ b/bindings/elixir/lib/fluss/admin.ex
@@ -24,6 +24,12 @@ defmodule Fluss.Admin do
       admin = Fluss.Admin.new!(conn)
       :ok = Fluss.Admin.create_database(admin, "my_db")
 
+      database_descriptor =
+        Fluss.DatabaseDescriptor.new()
+        |> Fluss.DatabaseDescriptor.comment("App data")
+
+      :ok = Fluss.Admin.create_database(admin, "another_db", 
database_descriptor)
+
       schema = Fluss.Schema.new() |> Fluss.Schema.column("ts", :bigint)
       descriptor = Fluss.TableDescriptor.new!(schema)
       :ok = Fluss.Admin.create_table(admin, "my_db", "events", descriptor)
@@ -65,10 +71,11 @@ defmodule Fluss.Admin do
     end
   end
 
-  @spec create_database(t(), String.t(), boolean()) :: :ok | {:error, 
Fluss.Error.t()}
-  def create_database(admin, name, ignore_if_exists \\ true) do
+  @spec create_database(t(), String.t(), Fluss.DatabaseDescriptor.t() | nil, 
boolean()) ::
+          :ok | {:error, Fluss.Error.t()}
+  def create_database(admin, name, descriptor \\ nil, ignore_if_exists \\ 
true) do
     admin
-    |> Native.admin_create_database(name, ignore_if_exists)
+    |> Native.admin_create_database(name, descriptor, ignore_if_exists)
     |> Native.await_nif()
   end
 
diff --git a/bindings/elixir/lib/fluss/database_descriptor.ex 
b/bindings/elixir/lib/fluss/database_descriptor.ex
index 71fdc963..43857e46 100644
--- a/bindings/elixir/lib/fluss/database_descriptor.ex
+++ b/bindings/elixir/lib/fluss/database_descriptor.ex
@@ -20,8 +20,19 @@ defmodule Fluss.DatabaseDescriptor do
   User-supplied configuration of a Fluss database — its comment and any custom
   properties.
 
-  Embedded as the `:descriptor` field of `Fluss.DatabaseInfo`; produced
-  indirectly via `Fluss.Admin.get_database_info/2`.
+  Constructed via the builder helpers (or as a struct directly) and passed to
+  `Fluss.Admin.create_database/3`. Also returned as the `:descriptor` field of
+  `Fluss.DatabaseInfo` from `Fluss.Admin.get_database_info/2`.
+
+  ## Examples
+
+      descriptor =
+        Fluss.DatabaseDescriptor.new()
+        |> Fluss.DatabaseDescriptor.comment("My database")
+        |> Fluss.DatabaseDescriptor.put_custom_property("owner", "team-x")
+
+      :ok = Fluss.Admin.create_database(admin, "my_db", descriptor)
+
   """
 
   @enforce_keys [:comment, :custom_properties]
@@ -31,4 +42,26 @@ defmodule Fluss.DatabaseDescriptor do
           comment: String.t() | nil,
           custom_properties: %{optional(String.t()) => String.t()}
         }
+
+  @spec new() :: t()
+  def new, do: %__MODULE__{comment: nil, custom_properties: %{}}
+
+  @spec comment(t(), String.t()) :: t()
+  def comment(%__MODULE__{} = desc, c) when is_binary(c), do: %{desc | 
comment: c}
+
+  @spec put_custom_property(t(), String.t(), String.t()) :: t()
+  def put_custom_property(%__MODULE__{} = desc, k, v) when is_binary(k) and 
is_binary(v),
+    do: %{desc | custom_properties: Map.put(desc.custom_properties, k, v)}
+
+  @spec put_custom_properties(t(), %{optional(String.t()) => String.t()}) :: 
t()
+  def put_custom_properties(%__MODULE__{} = desc, props) when is_map(props) do
+    Enum.each(props, fn {k, v} ->
+      unless is_binary(k) and is_binary(v) do
+        raise ArgumentError,
+              "put_custom_properties/2 requires String keys and values, got 
#{inspect({k, v})}"
+      end
+    end)
+
+    %{desc | custom_properties: Map.merge(desc.custom_properties, props)}
+  end
 end
diff --git a/bindings/elixir/lib/fluss/native.ex 
b/bindings/elixir/lib/fluss/native.ex
index a3b1b489..91b4fbf1 100644
--- a/bindings/elixir/lib/fluss/native.ex
+++ b/bindings/elixir/lib/fluss/native.ex
@@ -31,7 +31,7 @@ defmodule Fluss.Native do
 
   def admin_get_server_nodes(_admin), do: :erlang.nif_error(:nif_not_loaded)
 
-  def admin_create_database(_admin, _name, _ignore_if_exists),
+  def admin_create_database(_admin, _name, _descriptor, _ignore_if_exists),
     do: :erlang.nif_error(:nif_not_loaded)
 
   def admin_get_database_info(_admin, _database_name), do: 
:erlang.nif_error(:nif_not_loaded)
diff --git a/bindings/elixir/native/fluss_nif/src/admin.rs 
b/bindings/elixir/native/fluss_nif/src/admin.rs
index b765725a..7a206a5d 100644
--- a/bindings/elixir/native/fluss_nif/src/admin.rs
+++ b/bindings/elixir/native/fluss_nif/src/admin.rs
@@ -71,6 +71,16 @@ impl NifDatabaseDescriptor {
             custom_properties: desc.custom_properties().clone(),
         }
     }
+
+    pub fn to_core(&self) -> DatabaseDescriptor {
+        let mut desc = DatabaseDescriptor::builder();
+        if let Some(comment) = &self.comment {
+            desc = desc.comment(comment);
+        }
+
+        desc.custom_properties(self.custom_properties.clone())
+            .build()
+    }
 }
 
 #[derive(NifStruct)]
@@ -124,12 +134,14 @@ fn admin_create_database<'a>(
     env: Env<'a>,
     admin: ResourceArc<AdminResource>,
     database_name: String,
+    descriptor: Option<NifDatabaseDescriptor>,
     ignore_if_exists: bool,
 ) -> Term<'a> {
     async_nif::spawn_task(env, async move {
+        let core_descriptor = 
descriptor.as_ref().map(NifDatabaseDescriptor::to_core);
         admin
             .inner
-            .create_database(&database_name, None, ignore_if_exists)
+            .create_database(&database_name, core_descriptor.as_ref(), 
ignore_if_exists)
             .await
     })
 }
diff --git a/bindings/elixir/test/database_descriptor_test.exs 
b/bindings/elixir/test/database_descriptor_test.exs
new file mode 100644
index 00000000..aa860195
--- /dev/null
+++ b/bindings/elixir/test/database_descriptor_test.exs
@@ -0,0 +1,169 @@
+# 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.DatabaseDescriptorTest do
+  use ExUnit.Case, async: true
+
+  alias Fluss.DatabaseDescriptor
+
+  describe "new/0" do
+    test "returns a descriptor with no comment and no custom properties" do
+      assert DatabaseDescriptor.new() ==
+               %DatabaseDescriptor{comment: nil, custom_properties: %{}}
+    end
+  end
+
+  describe "comment/2" do
+    test "sets the comment field" do
+      desc = DatabaseDescriptor.new() |> DatabaseDescriptor.comment("hello")
+      assert desc.comment == "hello"
+    end
+
+    test "overwrites a previous comment" do
+      desc =
+        DatabaseDescriptor.new()
+        |> DatabaseDescriptor.comment("first")
+        |> DatabaseDescriptor.comment("second")
+
+      assert desc.comment == "second"
+    end
+
+    test "preserves existing custom_properties" do
+      desc =
+        DatabaseDescriptor.new()
+        |> DatabaseDescriptor.put_custom_property("k", "v")
+        |> DatabaseDescriptor.comment("c")
+
+      assert desc.comment == "c"
+      assert desc.custom_properties == %{"k" => "v"}
+    end
+
+    test "raises on non-binary input" do
+      assert_raise FunctionClauseError, fn ->
+        DatabaseDescriptor.comment(DatabaseDescriptor.new(), 42)
+      end
+    end
+  end
+
+  describe "put_custom_property/3" do
+    test "adds a property" do
+      desc = DatabaseDescriptor.new() |> 
DatabaseDescriptor.put_custom_property("k", "v")
+      assert desc.custom_properties == %{"k" => "v"}
+    end
+
+    test "accumulates multiple properties" do
+      desc =
+        DatabaseDescriptor.new()
+        |> DatabaseDescriptor.put_custom_property("k1", "v1")
+        |> DatabaseDescriptor.put_custom_property("k2", "v2")
+
+      assert desc.custom_properties == %{"k1" => "v1", "k2" => "v2"}
+    end
+
+    test "overwrites a previous value for the same key" do
+      desc =
+        DatabaseDescriptor.new()
+        |> DatabaseDescriptor.put_custom_property("k", "v1")
+        |> DatabaseDescriptor.put_custom_property("k", "v2")
+
+      assert desc.custom_properties == %{"k" => "v2"}
+    end
+
+    test "preserves existing comment" do
+      desc =
+        DatabaseDescriptor.new()
+        |> DatabaseDescriptor.comment("c")
+        |> DatabaseDescriptor.put_custom_property("k", "v")
+
+      assert desc.comment == "c"
+      assert desc.custom_properties == %{"k" => "v"}
+    end
+
+    test "raises on non-binary key" do
+      assert_raise FunctionClauseError, fn ->
+        DatabaseDescriptor.put_custom_property(DatabaseDescriptor.new(), 
:atom_key, "v")
+      end
+    end
+
+    test "raises on non-binary value" do
+      assert_raise FunctionClauseError, fn ->
+        DatabaseDescriptor.put_custom_property(DatabaseDescriptor.new(), "k", 
42)
+      end
+    end
+  end
+
+  describe "put_custom_properties/2" do
+    test "adds entries from a map" do
+      desc =
+        DatabaseDescriptor.new()
+        |> DatabaseDescriptor.put_custom_properties(%{"k1" => "v1", "k2" => 
"v2"})
+
+      assert desc.custom_properties == %{"k1" => "v1", "k2" => "v2"}
+    end
+
+    test "merges with existing entries" do
+      desc =
+        DatabaseDescriptor.new()
+        |> DatabaseDescriptor.put_custom_property("k0", "v0")
+        |> DatabaseDescriptor.put_custom_properties(%{"k1" => "v1"})
+
+      assert desc.custom_properties == %{"k0" => "v0", "k1" => "v1"}
+    end
+
+    test "overwrites on key collision" do
+      desc =
+        DatabaseDescriptor.new()
+        |> DatabaseDescriptor.put_custom_property("k", "v1")
+        |> DatabaseDescriptor.put_custom_properties(%{"k" => "v2"})
+
+      assert desc.custom_properties == %{"k" => "v2"}
+    end
+
+    test "accepts an empty map" do
+      desc = DatabaseDescriptor.new() |> 
DatabaseDescriptor.put_custom_properties(%{})
+      assert desc.custom_properties == %{}
+    end
+
+    test "preserves existing comment" do
+      desc =
+        DatabaseDescriptor.new()
+        |> DatabaseDescriptor.comment("c")
+        |> DatabaseDescriptor.put_custom_properties(%{"k" => "v"})
+
+      assert desc.comment == "c"
+      assert desc.custom_properties == %{"k" => "v"}
+    end
+
+    test "raises FunctionClauseError on non-map input" do
+      assert_raise FunctionClauseError, fn ->
+        DatabaseDescriptor.put_custom_properties(DatabaseDescriptor.new(), [])
+      end
+    end
+
+    test "raises ArgumentError on non-binary key in the map" do
+      assert_raise ArgumentError, fn ->
+        DatabaseDescriptor.put_custom_properties(DatabaseDescriptor.new(), 
%{:atom_key => "v"})
+      end
+    end
+
+    test "raises ArgumentError on non-binary value in the map" do
+      assert_raise ArgumentError, fn ->
+        DatabaseDescriptor.put_custom_properties(DatabaseDescriptor.new(), 
%{"k" => 42})
+      end
+    end
+  end
+end
diff --git a/bindings/elixir/test/integration/admin_test.exs 
b/bindings/elixir/test/integration/admin_test.exs
index 7942c5e3..e6a3ff73 100644
--- a/bindings/elixir/test/integration/admin_test.exs
+++ b/bindings/elixir/test/integration/admin_test.exs
@@ -53,10 +53,96 @@ defmodule Fluss.Integration.AdminTest do
     end
   end
 
+  describe "create_database/3 with a descriptor" do
+    test "accepts a comment-only descriptor", %{admin: admin} do
+      db = "fluss_data_sources_#{:rand.uniform(100_000)}"
+      descriptor = Fluss.DatabaseDescriptor.new() |> 
Fluss.DatabaseDescriptor.comment("hello")
+      :ok = Fluss.Admin.create_database(admin, db, descriptor)
+      on_exit(fn -> Fluss.Admin.drop_database(admin, db, true) end)
+
+      assert {:ok, info} = Fluss.Admin.get_database_info(admin, db)
+      assert info.descriptor.comment == "hello"
+      assert info.descriptor.custom_properties == %{}
+    end
+
+    test "accepts a properties-only descriptor", %{admin: admin} do
+      db = "fluss_data_sources_#{:rand.uniform(100_000)}"
+
+      descriptor =
+        Fluss.DatabaseDescriptor.new()
+        |> Fluss.DatabaseDescriptor.put_custom_property("region", "eu-west-1")
+
+      :ok = Fluss.Admin.create_database(admin, db, descriptor)
+      on_exit(fn -> Fluss.Admin.drop_database(admin, db, true) end)
+
+      assert {:ok, info} = Fluss.Admin.get_database_info(admin, db)
+      assert info.descriptor.custom_properties == %{"region" => "eu-west-1"}
+    end
+
+    test "accepts a struct constructed directly without builders", %{admin: 
admin} do
+      db = "fluss_data_sources_#{:rand.uniform(100_000)}"
+      descriptor = %Fluss.DatabaseDescriptor{comment: "direct", 
custom_properties: %{"k" => "v"}}
+      :ok = Fluss.Admin.create_database(admin, db, descriptor)
+      on_exit(fn -> Fluss.Admin.drop_database(admin, db, true) end)
+
+      assert {:ok, info} = Fluss.Admin.get_database_info(admin, db)
+      assert info.descriptor.comment == "direct"
+      assert info.descriptor.custom_properties == %{"k" => "v"}
+    end
+  end
+
+  describe "create_database/4 with a descriptor" do
+    test "accepts a descriptor and returns :ok if the database exists when 
ignore_if_exists is true",
+         %{admin: admin} do
+      db = "fluss_data_sources_#{:rand.uniform(100_000)}"
+      descriptor = %Fluss.DatabaseDescriptor{comment: "direct", 
custom_properties: %{"k" => "v"}}
+      :ok = Fluss.Admin.create_database(admin, db, descriptor, true)
+      on_exit(fn -> Fluss.Admin.drop_database(admin, db, true) end)
+
+      descriptor = %Fluss.DatabaseDescriptor{comment: "updated", 
custom_properties: %{}}
+
+      :ok = Fluss.Admin.create_database(admin, db, descriptor)
+      assert {:ok, info} = Fluss.Admin.get_database_info(admin, db)
+
+      # the 2nd create was skipped due to `ignore_if_exists: true`
+      assert info.descriptor.comment == "direct"
+      assert info.descriptor.custom_properties == %{"k" => "v"}
+    end
+
+    test "fails if the database exists and ignore_if_exists is false", 
%{admin: admin} do
+      db = "fluss_data_sources_#{:rand.uniform(100_000)}"
+      descriptor = %Fluss.DatabaseDescriptor{comment: "direct", 
custom_properties: %{"k" => "v"}}
+      :ok = Fluss.Admin.create_database(admin, db, descriptor, true)
+      on_exit(fn -> Fluss.Admin.drop_database(admin, db, true) end)
+
+      assert {:error, %Fluss.Error{}} = Fluss.Admin.create_database(admin, db, 
descriptor, false)
+    end
+  end
+
   describe "get_database_info/2" do
-    test "returns DatabaseInfo for an existing database", %{admin: admin} do
+    test "returns DatabaseInfo with the descriptor used at creation", %{admin: 
admin} do
+      db = "fluss_data_sources_#{:rand.uniform(100_000)}"
+
+      descriptor =
+        Fluss.DatabaseDescriptor.new()
+        |> Fluss.DatabaseDescriptor.comment("Integration test database")
+        |> Fluss.DatabaseDescriptor.put_custom_property("owner", "ci")
+        |> Fluss.DatabaseDescriptor.put_custom_property("env", "test")
+
+      :ok = Fluss.Admin.create_database(admin, db, descriptor)
+      on_exit(fn -> Fluss.Admin.drop_database(admin, db, true) end)
+
+      assert {:ok, %Fluss.DatabaseInfo{} = info} = 
Fluss.Admin.get_database_info(admin, db)
+      assert info.database_name == db
+      assert info.descriptor.comment == "Integration test database"
+      assert info.descriptor.custom_properties == %{"owner" => "ci", "env" => 
"test"}
+      assert is_integer(info.created_time)
+      assert is_integer(info.modified_time)
+    end
+
+    test "returns DatabaseInfo for a database created with no descriptor", 
%{admin: admin} do
       db = "fluss_data_sources_#{:rand.uniform(100_000)}"
-      :ok = Fluss.Admin.create_database(admin, db, true)
+      :ok = Fluss.Admin.create_database(admin, db)
       on_exit(fn -> Fluss.Admin.drop_database(admin, db, true) end)
 
       assert {:ok, %Fluss.DatabaseInfo{} = info} = 
Fluss.Admin.get_database_info(admin, db)
@@ -76,7 +162,7 @@ defmodule Fluss.Integration.AdminTest do
     test "returns {:ok, true} for an existing database", %{admin: admin} do
       db = "fluss_data_sources_#{:rand.uniform(100_000)}"
 
-      :ok = Fluss.Admin.create_database(admin, db, true)
+      :ok = Fluss.Admin.create_database(admin, db)
       on_exit(fn -> Fluss.Admin.drop_database(admin, db, true) end)
 
       assert {:ok, true} = Fluss.Admin.database_exists(admin, db)
diff --git a/bindings/elixir/test/integration/log_table_test.exs 
b/bindings/elixir/test/integration/log_table_test.exs
index 16a670fe..d3eb55e4 100644
--- a/bindings/elixir/test/integration/log_table_test.exs
+++ b/bindings/elixir/test/integration/log_table_test.exs
@@ -213,7 +213,7 @@ defmodule Fluss.Integration.LogTableTest do
   describe "admin operations" do
     test "create and drop database", %{admin: admin} do
       db_name = "ex_test_db_#{:rand.uniform(100_000)}"
-      :ok = Fluss.Admin.create_database(admin, db_name, true)
+      :ok = Fluss.Admin.create_database(admin, db_name)
 
       {:ok, databases} = Fluss.Admin.list_databases(admin)
       assert db_name in databases

Reply via email to