From 9381498bee0b87b17f0977700f42c2846f552677 Mon Sep 17 00:00:00 2001 From: Frerich Raabe Date: Mon, 4 May 2026 19:05:51 +0200 Subject: [PATCH 01/10] Add tests for Enumerable protocol (Enum.to_list, Enum.at, Enum.count, Enum.slice) --- test/pg_large_objects/large_object_test.exs | 26 ++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/test/pg_large_objects/large_object_test.exs b/test/pg_large_objects/large_object_test.exs index 8cbae16..6c59061 100644 --- a/test/pg_large_objects/large_object_test.exs +++ b/test/pg_large_objects/large_object_test.exs @@ -344,7 +344,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 -> From cf0308941e896ab15c16d24d701c283b8948cc68 Mon Sep 17 00:00:00 2001 From: Frerich Raabe Date: Mon, 4 May 2026 19:06:16 +0200 Subject: [PATCH 02/10] Add test for Collectable protocol (Stream.into for writing) --- test/pg_large_objects/large_object_test.exs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/test/pg_large_objects/large_object_test.exs b/test/pg_large_objects/large_object_test.exs index 6c59061..ab846b5 100644 --- a/test/pg_large_objects/large_object_test.exs +++ b/test/pg_large_objects/large_object_test.exs @@ -310,7 +310,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 -> From 2c1ba31ae5f96cfe9d31759dc9c6663771260305 Mon Sep 17 00:00:00 2001 From: Frerich Raabe Date: Mon, 4 May 2026 19:07:05 +0200 Subject: [PATCH 03/10] Add tests for EctoQuery macros (lo_create, lo_unlink) --- test/pg_large_objects/ecto_query_test.exs | 29 +++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 test/pg_large_objects/ecto_query_test.exs 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 From 096e97fd03d4b0037d924d80b0bfaadd19c1a9a9 Mon Sep 17 00:00:00 2001 From: Frerich Raabe Date: Mon, 4 May 2026 19:07:27 +0200 Subject: [PATCH 04/10] Add tests for LargeObject.create/2 with options (bufsize, mode) --- test/pg_large_objects/large_object_test.exs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/pg_large_objects/large_object_test.exs b/test/pg_large_objects/large_object_test.exs index ab846b5..012415b 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 From b5552b07159fcaf32f75c382a9e8b0c2a763eab4 Mon Sep 17 00:00:00 2001 From: Frerich Raabe Date: Mon, 4 May 2026 19:07:48 +0200 Subject: [PATCH 05/10] Add tests for open/3 with :read_write mode and invalid mode --- test/pg_large_objects/large_object_test.exs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/test/pg_large_objects/large_object_test.exs b/test/pg_large_objects/large_object_test.exs index 012415b..bd92915 100644 --- a/test/pg_large_objects/large_object_test.exs +++ b/test/pg_large_objects/large_object_test.exs @@ -36,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 From 41877c9549a9276a6a141da2abcf52931ff97eab Mon Sep 17 00:00:00 2001 From: Frerich Raabe Date: Mon, 4 May 2026 19:10:07 +0200 Subject: [PATCH 06/10] Add test for import/3 with custom bufsize option --- test/pg_large_objects_test.exs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/pg_large_objects_test.exs b/test/pg_large_objects_test.exs index 2de9951..c45f721 100644 --- a/test/pg_large_objects_test.exs +++ b/test/pg_large_objects_test.exs @@ -17,6 +17,18 @@ 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 from enumerable" do data = :crypto.strong_rand_bytes(Enum.random(0..1024)) From 37185ddfde1eac3246e16f0c53f4c4109f8301f2 Mon Sep 17 00:00:00 2001 From: Frerich Raabe Date: Mon, 4 May 2026 19:17:33 +0200 Subject: [PATCH 07/10] Add test for export/3 not_found via collectable path --- test/pg_large_objects_test.exs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/pg_large_objects_test.exs b/test/pg_large_objects_test.exs index c45f721..61191aa 100644 --- a/test/pg_large_objects_test.exs +++ b/test/pg_large_objects_test.exs @@ -79,5 +79,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 From 193b5cc4e5d616cd7c5423ec6fa65c404f30a75b Mon Sep 17 00:00:00 2001 From: Frerich Raabe Date: Mon, 4 May 2026 19:22:55 +0200 Subject: [PATCH 08/10] Add tests for Repo convenience functions (create/open/remove_large_object) --- test/pg_large_objects/repo_test.exs | 36 +++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) 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 From 732e1651fe0c84ca81636b653083e7e2ead07b96 Mon Sep 17 00:00:00 2001 From: Frerich Raabe Date: Mon, 4 May 2026 19:33:17 +0200 Subject: [PATCH 09/10] Add test for importing large data spanning multiple chunks --- test/pg_large_objects_test.exs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/pg_large_objects_test.exs b/test/pg_large_objects_test.exs index 61191aa..8af1912 100644 --- a/test/pg_large_objects_test.exs +++ b/test/pg_large_objects_test.exs @@ -29,6 +29,19 @@ defmodule PgLargeObjectsTest do 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)) From 3a5635eaf3b81757571d3a56828948a367395942 Mon Sep 17 00:00:00 2001 From: Frerich Raabe Date: Mon, 4 May 2026 19:40:43 +0200 Subject: [PATCH 10/10] Add test verifying size/1 preserves read position --- test/pg_large_objects/large_object_test.exs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/pg_large_objects/large_object_test.exs b/test/pg_large_objects/large_object_test.exs index bd92915..92de593 100644 --- a/test/pg_large_objects/large_object_test.exs +++ b/test/pg_large_objects/large_object_test.exs @@ -101,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