diff --git a/test/pg_large_objects/ecto_query_test.exs b/test/pg_large_objects/ecto_query_test.exs new file mode 100644 index 0000000..e270278 --- /dev/null +++ b/test/pg_large_objects/ecto_query_test.exs @@ -0,0 +1,29 @@ +defmodule PgLargeObjects.EctoQueryTest do + use PgLargeObjects.DataCase, async: true + + import Ecto.Query + import PgLargeObjects.EctoQuery + + describe "lo_create" do + test "creates a large object via Ecto query" do + query = from(x in fragment("SELECT 1 AS n"), select: lo_create()) + assert [oid] = TestRepo.all(query) + assert is_integer(oid) and oid > 0 + end + end + + describe "lo_unlink" do + test "deletes a large object via Ecto query" do + oid = put_large_object!("test data") + + values = [%{oid: oid}] + types = %{oid: :integer} + + query = from(x in values(values, types), select: lo_unlink(x.oid)) + TestRepo.all(query) + + # Verify it's gone + assert {:error, :not_found} = PgLargeObjects.LargeObject.open(TestRepo, oid) + end + end +end diff --git a/test/pg_large_objects/large_object_test.exs b/test/pg_large_objects/large_object_test.exs index 8cbae16..92de593 100644 --- a/test/pg_large_objects/large_object_test.exs +++ b/test/pg_large_objects/large_object_test.exs @@ -8,6 +8,22 @@ defmodule PgLargeObjects.LargeObjectTest do assert {:ok, %LargeObject{oid: oid}} = LargeObject.create(TestRepo) assert %{rows: [[^oid]]} = TestRepo.query!("SELECT oid FROM pg_largeobject_metadata") end + + test "creates an object with custom bufsize" do + assert {:ok, %LargeObject{bufsize: 512}} = LargeObject.create(TestRepo, bufsize: 512) + end + + test "creates an object opened for writing" do + {:ok, lob} = LargeObject.create(TestRepo, mode: :write) + assert :ok == LargeObject.write(lob, "test") + end + + test "creates an object opened for read_write by default" do + {:ok, lob} = LargeObject.create(TestRepo) + assert :ok == LargeObject.write(lob, "test") + assert {:ok, 0} = LargeObject.seek(lob, 0) + assert {:ok, "test"} == LargeObject.read(lob, 10) + end end describe "open/3" do @@ -20,6 +36,27 @@ defmodule PgLargeObjects.LargeObjectTest do test "fails given invalid object ID" do assert {:error, :not_found} == LargeObject.open(TestRepo, 12_345) end + + test "opens for read_write mode allowing both read and write" do + oid = put_large_object!("ABCDEFG") + + TestRepo.transaction(fn -> + {:ok, lob} = LargeObject.open(TestRepo, oid, mode: :read_write) + assert :ok == LargeObject.write(lob, "XYZ") + assert {:ok, 0} = LargeObject.seek(lob, 0) + assert {:ok, "XYZ"} == LargeObject.read(lob, 3) + end) + end + + test "raises ArgumentError given invalid mode" do + oid = put_large_object!("test") + + assert_raise ArgumentError, ~r/invalid mode/, fn -> + TestRepo.transaction(fn -> + LargeObject.open(TestRepo, oid, mode: :invalid) + end) + end + end end describe "remove/2" do @@ -64,6 +101,16 @@ defmodule PgLargeObjects.LargeObjectTest do assert {:error, :not_found} == LargeObject.size(lob) end) end + + test "preserves read position" do + with_object("ABCDEFGHIJ", fn lob -> + {:ok, "ABC"} = LargeObject.read(lob, 3) + assert {:ok, 3} = LargeObject.tell(lob) + assert {:ok, 10} = LargeObject.size(lob) + assert {:ok, 3} = LargeObject.tell(lob) + assert {:ok, "DEF"} = LargeObject.read(lob, 3) + end) + end end describe "write/2" do @@ -310,7 +357,22 @@ defmodule PgLargeObjects.LargeObjectTest do end describe "Collectable implementation" do - test "raises when writing to a read-only object" do + test "Stream.into writes data to object" do + {:ok, oid} = + TestRepo.transaction(fn -> + {:ok, lob} = LargeObject.create(TestRepo, mode: :write) + + ["Hello", ", ", "World!"] + |> Stream.into(lob) + |> Stream.run() + + lob.oid + end) + + assert get_large_object!(oid) == "Hello, World!" + end + + test "raises on write error when streaming into read-only object" do oid = put_large_object!("hello") TestRepo.transaction(fn -> @@ -344,7 +406,31 @@ defmodule PgLargeObjects.LargeObjectTest do end describe "Enumerable implementation" do - test "raises on read error" do + test "Enum.to_list returns chunks of data" do + with_object("ABCDEFGHIJ", [bufsize: 4], fn lob -> + assert Enum.to_list(lob) == ["ABCD", "EFGH", "IJ"] + end) + end + + test "Enum.at returns the nth chunk" do + with_object("ABCDEFGHIJ", [bufsize: 4], fn lob -> + assert Enum.at(lob, 1) == "EFGH" + end) + end + + test "Enum.count returns the number of chunks" do + with_object("ABCDEFGHIJ", [bufsize: 4], fn lob -> + assert Enum.count(lob) == 3 + end) + end + + test "Enum.slice returns a subset of chunks" do + with_object("ABCDEFGHIJKL", [bufsize: 4], fn lob -> + assert Enum.slice(lob, 1..2) == ["EFGH", "IJKL"] + end) + end + + test "raises on read error when enumerating a closed object" do oid = put_large_object!("hello") TestRepo.transaction(fn -> diff --git a/test/pg_large_objects/repo_test.exs b/test/pg_large_objects/repo_test.exs index 436a0a4..12e6400 100644 --- a/test/pg_large_objects/repo_test.exs +++ b/test/pg_large_objects/repo_test.exs @@ -61,4 +61,40 @@ defmodule PgLargeObjects.RepoTest do assert {"", ^data} = StringIO.contents(pid) end end + + describe "create_large_object/1" do + test "creates and opens a large object" do + TestRepo.transaction(fn -> + assert {:ok, %PgLargeObjects.LargeObject{}} = TestRepo.create_large_object() + end) + end + end + + describe "open_large_object/2" do + test "opens an existing large object" do + oid = put_large_object!("test data") + + TestRepo.transaction(fn -> + assert {:ok, %PgLargeObjects.LargeObject{oid: ^oid}} = + TestRepo.open_large_object(oid) + end) + end + + test "returns error for invalid oid" do + TestRepo.transaction(fn -> + assert {:error, :not_found} = TestRepo.open_large_object(12_345) + end) + end + end + + describe "remove_large_object/1" do + test "removes an existing large object" do + oid = put_large_object!("test data") + assert :ok == TestRepo.remove_large_object(oid) + end + + test "returns error for invalid oid" do + assert {:error, :not_found} = TestRepo.remove_large_object(12_345) + end + end end diff --git a/test/pg_large_objects_test.exs b/test/pg_large_objects_test.exs index 2de9951..8af1912 100644 --- a/test/pg_large_objects_test.exs +++ b/test/pg_large_objects_test.exs @@ -17,6 +17,31 @@ defmodule PgLargeObjectsTest do assert data == get_large_object!(oid) end + test "can import binary with custom bufsize" do + data = :crypto.strong_rand_bytes(256) + + {:ok, oid} = + TestRepo.transaction(fn -> + {:ok, oid} = PgLargeObjects.import(TestRepo, data, bufsize: 64) + oid + end) + + assert data == get_large_object!(oid) + end + + test "can import large binary spanning multiple chunks" do + # 200KB with 64KB bufsize = 4 chunks (tests multi-chunk path) + data = :crypto.strong_rand_bytes(200_000) + + {:ok, oid} = + TestRepo.transaction(fn -> + {:ok, oid} = PgLargeObjects.import(TestRepo, data, bufsize: 65_536) + oid + end) + + assert data == get_large_object!(oid) + end + test "can import from enumerable" do data = :crypto.strong_rand_bytes(Enum.random(0..1024)) @@ -67,5 +92,15 @@ defmodule PgLargeObjectsTest do assert {:error, :not_found} = PgLargeObjects.export(TestRepo, 12_345) end) end + + test "handles invalid object IDs with collectable" do + {:ok, pid} = StringIO.open("", encoding: :latin1) + stream = IO.binstream(pid, 3) + + TestRepo.transaction(fn -> + assert {:error, :not_found} = + PgLargeObjects.export(TestRepo, 12_345, into: stream) + end) + end end end