blob: d3eb55e43896ed8d95b3e2f9a434241e3f63e103 [file]
# 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.Integration.LogTableTest do
use Fluss.Test.IntegrationCase, async: false
@database "fluss"
describe "append and scan" do
test "append rows and scan with log scanner", %{conn: conn, admin: admin} do
table_name = "ex_test_append_and_scan_#{:rand.uniform(100_000)}"
cleanup_table(admin, table_name)
schema =
Fluss.Schema.new()
|> Fluss.Schema.column("c1", :int)
|> Fluss.Schema.column("c2", :string)
descriptor = Fluss.TableDescriptor.new!(schema)
:ok = Fluss.Admin.create_table(admin, @database, table_name, descriptor, false)
table = Fluss.Table.get!(conn, @database, table_name)
writer = Fluss.AppendWriter.new!(table)
# Append 6 rows
for {c1, c2} <- [{1, "a1"}, {2, "a2"}, {3, "a3"}, {4, "a4"}, {5, "a5"}, {6, "a6"}] do
{:ok, _} = Fluss.AppendWriter.append(writer, [c1, c2])
end
:ok = Fluss.AppendWriter.flush(writer)
# Scan all records
scanner = Fluss.LogScanner.new!(table)
:ok = Fluss.LogScanner.subscribe(scanner, 0, Fluss.earliest_offset())
records = poll_records(scanner, 6)
assert length(records) == 6
sorted = Enum.sort_by(records, fn r -> r[:row][:c1] end)
for {record, i} <- Enum.with_index(sorted, 1) do
assert record[:row][:c1] == i
assert record[:row][:c2] == "a#{i}"
assert record[:change_type] == :append_only
end
# Unsubscribe should not error
:ok = Fluss.LogScanner.unsubscribe(scanner, 0)
cleanup_table(admin, table_name)
end
test "append with nil values", %{conn: conn, admin: admin} do
table_name = "ex_test_append_nil_#{:rand.uniform(100_000)}"
cleanup_table(admin, table_name)
schema =
Fluss.Schema.new()
|> Fluss.Schema.column("id", :int)
|> Fluss.Schema.column("name", :string)
descriptor = Fluss.TableDescriptor.new!(schema)
:ok = Fluss.Admin.create_table(admin, @database, table_name, descriptor, false)
table = Fluss.Table.get!(conn, @database, table_name)
writer = Fluss.AppendWriter.new!(table)
{:ok, _} = Fluss.AppendWriter.append(writer, [1, nil])
{:ok, _} = Fluss.AppendWriter.append(writer, [2, "present"])
:ok = Fluss.AppendWriter.flush(writer)
scanner = Fluss.LogScanner.new!(table)
:ok = Fluss.LogScanner.subscribe(scanner, 0, Fluss.earliest_offset())
records = poll_records(scanner, 2)
assert length(records) == 2
sorted = Enum.sort_by(records, fn r -> r[:row][:id] end)
assert Enum.at(sorted, 0)[:row][:name] == nil
assert Enum.at(sorted, 1)[:row][:name] == "present"
cleanup_table(admin, table_name)
end
end
describe "multiple data types" do
test "tinyint, smallint, int, bigint, float, double, string, boolean", %{
conn: conn,
admin: admin
} do
table_name = "ex_test_data_types_#{:rand.uniform(100_000)}"
cleanup_table(admin, table_name)
schema =
Fluss.Schema.new()
|> Fluss.Schema.column("a_tinyint", :tinyint)
|> Fluss.Schema.column("b_smallint", :smallint)
|> Fluss.Schema.column("c_int", :int)
|> Fluss.Schema.column("d_bigint", :bigint)
|> Fluss.Schema.column("e_float", :float)
|> Fluss.Schema.column("f_double", :double)
|> Fluss.Schema.column("g_string", :string)
|> Fluss.Schema.column("h_bool", :boolean)
descriptor = Fluss.TableDescriptor.new!(schema)
:ok = Fluss.Admin.create_table(admin, @database, table_name, descriptor, false)
table = Fluss.Table.get!(conn, @database, table_name)
writer = Fluss.AppendWriter.new!(table)
{:ok, _} =
Fluss.AppendWriter.append(writer, [
127,
32_000,
42,
1_000_000_000_000,
3.14,
2.718281828,
"hello",
true
])
{:ok, _} =
Fluss.AppendWriter.append(writer, [-128, -32_000, -1, -999, 0.0, -1.5, "", false])
:ok = Fluss.AppendWriter.flush(writer)
scanner = Fluss.LogScanner.new!(table)
:ok = Fluss.LogScanner.subscribe(scanner, 0, Fluss.earliest_offset())
records = poll_records(scanner, 2)
assert length(records) == 2
sorted = Enum.sort_by(records, fn r -> r[:row][:c_int] end)
row1 = Enum.at(sorted, 0)[:row]
row2 = Enum.at(sorted, 1)[:row]
assert row1[:a_tinyint] == -128
assert row1[:b_smallint] == -32_000
assert row1[:c_int] == -1
assert row1[:d_bigint] == -999
assert row1[:g_string] == ""
assert row1[:h_bool] == false
assert row2[:a_tinyint] == 127
assert row2[:b_smallint] == 32_000
assert row2[:c_int] == 42
assert row2[:d_bigint] == 1_000_000_000_000
assert row2[:g_string] == "hello"
assert row2[:h_bool] == true
cleanup_table(admin, table_name)
end
end
describe "subscribe_buckets" do
test "subscribe to multiple buckets at once", %{conn: conn, admin: admin} do
table_name = "ex_test_subscribe_buckets_#{:rand.uniform(100_000)}"
cleanup_table(admin, table_name)
schema =
Fluss.Schema.new()
|> Fluss.Schema.column("id", :int)
|> Fluss.Schema.column("val", :string)
descriptor = Fluss.TableDescriptor.new!(schema, bucket_count: 3)
:ok = Fluss.Admin.create_table(admin, @database, table_name, descriptor, false)
table = Fluss.Table.get!(conn, @database, table_name)
writer = Fluss.AppendWriter.new!(table)
for i <- 1..9 do
{:ok, _} = Fluss.AppendWriter.append(writer, [i, "v#{i}"])
end
:ok = Fluss.AppendWriter.flush(writer)
scanner = Fluss.LogScanner.new!(table)
earliest = Fluss.earliest_offset()
:ok =
Fluss.LogScanner.subscribe_buckets(scanner, [
{0, earliest},
{1, earliest},
{2, earliest}
])
records = poll_records(scanner, 9)
assert length(records) == 9
ids = records |> Enum.map(fn r -> r[:row][:id] end) |> Enum.sort()
assert ids == Enum.to_list(1..9)
cleanup_table(admin, table_name)
end
end
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)
{:ok, databases} = Fluss.Admin.list_databases(admin)
assert db_name in databases
:ok = Fluss.Admin.drop_database(admin, db_name, true)
end
test "list tables", %{admin: admin} do
table_name = "ex_test_list_tables_#{:rand.uniform(100_000)}"
cleanup_table(admin, table_name)
schema =
Fluss.Schema.new()
|> Fluss.Schema.column("id", :int)
descriptor = Fluss.TableDescriptor.new!(schema)
:ok = Fluss.Admin.create_table(admin, @database, table_name, descriptor, false)
{:ok, tables} = Fluss.Admin.list_tables(admin, @database)
assert table_name in tables
cleanup_table(admin, table_name)
end
test "table metadata", %{conn: conn, admin: admin} do
table_name = "ex_test_table_meta_#{:rand.uniform(100_000)}"
cleanup_table(admin, table_name)
schema =
Fluss.Schema.new()
|> Fluss.Schema.column("id", :int)
|> Fluss.Schema.column("name", :string)
descriptor = Fluss.TableDescriptor.new!(schema)
:ok = Fluss.Admin.create_table(admin, @database, table_name, descriptor, false)
table = Fluss.Table.get!(conn, @database, table_name)
assert Fluss.Table.has_primary_key?(table) == false
assert Fluss.Table.column_names(table) == ["id", "name"]
cleanup_table(admin, table_name)
end
end
describe "scan from offset" do
test "subscribe from specific offset skips earlier records", %{conn: conn, admin: admin} do
table_name = "ex_test_scan_offset_#{:rand.uniform(100_000)}"
cleanup_table(admin, table_name)
schema =
Fluss.Schema.new()
|> Fluss.Schema.column("id", :int)
descriptor = Fluss.TableDescriptor.new!(schema)
:ok = Fluss.Admin.create_table(admin, @database, table_name, descriptor, false)
table = Fluss.Table.get!(conn, @database, table_name)
writer = Fluss.AppendWriter.new!(table)
for i <- 1..5 do
{:ok, _} = Fluss.AppendWriter.append(writer, [i])
end
:ok = Fluss.AppendWriter.flush(writer)
# Subscribe from offset 3, should skip first 3 records
scanner = Fluss.LogScanner.new!(table)
:ok = Fluss.LogScanner.subscribe(scanner, 0, 3)
records = poll_records(scanner, 2)
assert length(records) == 2
ids = records |> Enum.map(fn r -> r[:row][:id] end) |> Enum.sort()
assert ids == [4, 5]
cleanup_table(admin, table_name)
end
end
describe "multiple flushes" do
test "append, flush, append more, flush, scan all", %{conn: conn, admin: admin} do
table_name = "ex_test_multi_flush_#{:rand.uniform(100_000)}"
cleanup_table(admin, table_name)
schema =
Fluss.Schema.new()
|> Fluss.Schema.column("id", :int)
|> Fluss.Schema.column("batch", :string)
descriptor = Fluss.TableDescriptor.new!(schema)
:ok = Fluss.Admin.create_table(admin, @database, table_name, descriptor, false)
table = Fluss.Table.get!(conn, @database, table_name)
writer = Fluss.AppendWriter.new!(table)
# First batch
{:ok, _} = Fluss.AppendWriter.append(writer, [1, "first"])
{:ok, _} = Fluss.AppendWriter.append(writer, [2, "first"])
:ok = Fluss.AppendWriter.flush(writer)
# Second batch
{:ok, _} = Fluss.AppendWriter.append(writer, [3, "second"])
{:ok, _} = Fluss.AppendWriter.append(writer, [4, "second"])
:ok = Fluss.AppendWriter.flush(writer)
scanner = Fluss.LogScanner.new!(table)
:ok = Fluss.LogScanner.subscribe(scanner, 0, Fluss.earliest_offset())
records = poll_records(scanner, 4)
assert length(records) == 4
sorted = Enum.sort_by(records, fn r -> r[:row][:id] end)
assert Enum.at(sorted, 0)[:row][:batch] == "first"
assert Enum.at(sorted, 1)[:row][:batch] == "first"
assert Enum.at(sorted, 2)[:row][:batch] == "second"
assert Enum.at(sorted, 3)[:row][:batch] == "second"
cleanup_table(admin, table_name)
end
end
defp poll_records(scanner, expected_count, timeout_ms \\ 10_000) do
deadline = System.monotonic_time(:millisecond) + timeout_ms
do_poll(scanner, expected_count, deadline, [])
end
defp do_poll(_scanner, expected_count, _deadline, acc) when length(acc) >= expected_count do
acc
end
defp do_poll(scanner, expected_count, deadline, acc) do
remaining = deadline - System.monotonic_time(:millisecond)
if remaining <= 0 do
acc
else
:ok = Fluss.LogScanner.poll(scanner, min(5_000, remaining))
receive do
{:fluss_records, records} ->
do_poll(scanner, expected_count, deadline, acc ++ records)
{:fluss_poll_error, reason} ->
IO.warn("poll error during test: #{inspect(reason)}")
do_poll(scanner, expected_count, deadline, acc)
after
min(6_000, remaining) ->
acc
end
end
end
defp cleanup_table(admin, table_name) do
Fluss.Admin.drop_table(admin, @database, table_name, true)
end
end