Skip to content

SPOC-489: Add bidirectional large object migration between native and lolor storage#34

Merged
mason-sharp merged 3 commits into
mainfrom
spoc-489
Jun 2, 2026
Merged

SPOC-489: Add bidirectional large object migration between native and lolor storage#34
mason-sharp merged 3 commits into
mainfrom
spoc-489

Conversation

@danolivo

@danolivo danolivo commented Apr 2, 2026

Copy link
Copy Markdown
Contributor

Adds lolor.migrate_from_native() and lolor.migrate_to_native() SQL functions
that move large objects between PostgreSQL's native pg_catalog storage and
lolor's replication-compatible tables, preserving OIDs, owners, ACLs, and data.

Design decisions

  • Native → lolor migration is manual: the user calls migrate_from_native() explicitly after CREATE EXTENSION lolor. This requires superuser and should be done during a maintenance window.
  • Lolor → native migration is automatic: DROP EXTENSION lolor triggers migrate_to_native() via the event trigger, ensuring no large objects are lost on extension removal. If migration fails (e.g. OID conflict), the DROP is rejected so the user can resolve the issue and retry.
  • Migration is safe only in master-replica configurations for now. In multi-master setups, migrated OIDs lack node-encoding and may conflict with lolor-generated OIDs on other nodes.

Implementation details

  • Forward migration uses bulk INSERT for performance; reverse migration uses the native LO API (lo_create_orig/lo_lseek64_orig/lowrite_orig) since direct INSERT into pg_catalog.pg_largeobject is not allowed from SQL.
  • LOBLKSIZE is derived at runtime from current_setting('block_size') to support non-default block sizes.
  • Uses lo_lseek64_orig with bigint offsets to handle large objects exceeding 2 GB.
  • The event trigger guards migrate_to_native() with a pg_proc existence check for backward compatibility with lolor versions < 1.2.3.
  • Row count verification after each bulk copy detects silent data loss.
  • Materializes the OID list before lo_unlink_orig to avoid scanning the catalog while modifying it.
  • Emits NOTICE during installation if active streaming replicas are detected.

Tests

  • Forward migration: two native LOs migrated, data integrity verified through lolor API.
  • Reverse migration via DROP EXTENSION: all objects (including ones created directly in lolor) restored to native storage with data verification.
  • Manual migrate_to_native() without dropping the extension.
  • OID conflict detection for both migrate_from_native and migrate_to_native.
  • DROP EXTENSION rejected on OID conflict, user resolves and retries successfully.
  • Edge cases: empty native storage, empty lolor storage, extension upgrade path 1.2.2 → 1.2.3.
  • TAP test for standby promotion with lolor installed.

…rage

Add migrate_from_native() and migrate_to_native() SQL functions that move
large objects between PostgreSQL's native pg_catalog storage and lolor's
replication-compatible tables, preserving OIDs, owners, ACLs, and data.

The DROP EXTENSION event trigger now calls migrate_to_native() automatically
before removing the extension, preventing data loss that previously occurred
when lolor tables were dropped with the schema.

Key details:
- Forward migration uses bulk INSERT for performance
- Reverse migration uses the native LO API (lo_create_orig/lowrite_orig)
  since direct INSERT into pg_catalog.pg_largeobject is not allowed
- LOBLKSIZE derived at runtime from block_size GUC for non-default builds
- Uses lo_lseek64_orig for large objects exceeding 2 GB
- Both functions require superuser and verify lolor is enabled
- Event trigger guards migrate_to_native() with pg_proc existence check
  for backward compatibility with versions < 1.2.3
- Emits NOTICE during installation if streaming replicas are detected
- Migration is safe only in master-replica configurations for now

Add regression tests for forward migration, reverse migration via DROP
EXTENSION, manual migrate_to_native, empty-database edge cases, and the
1.2.2 to 1.2.3 upgrade path.
@danolivo danolivo self-assigned this Apr 2, 2026
@danolivo danolivo added the enhancement New feature or request label Apr 2, 2026
@danolivo
danolivo requested review from mason-sharp and rasifr April 2, 2026 09:45
@coderabbitai

coderabbitai Bot commented Apr 2, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@mason-sharp, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 6 minutes and 50 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bab3eb83-5951-476a-a12a-26c844d19be9

📥 Commits

Reviewing files that changed from the base of the PR and between e409a70 and 6bdab98.

📒 Files selected for processing (2)
  • .github/workflows/workflow.yml
  • docker/entrypoint.sh
📝 Walkthrough

Walkthrough

This PR adds v1.3 of the lolor extension with bidirectional migration between native PostgreSQL large objects and lolor storage. The feature includes two migration functions, automatic reverse migration on DROP EXTENSION, comprehensive test coverage including OID conflict scenarios, and documentation.

Changes

Large Object Migration v1.3

Layer / File(s) Summary
Version and build setup
lolor.control, Makefile
default_version bumped to 1.3; upgrade script lolor--1.2.2--1.3.sql added to DATA list.
Migration function implementations
lolor--1.2.2--1.3.sql
Upgrade script prologue checks for active streaming replicas; migrate_from_native() copies all native pg_largeobject_metadata and data pages into lolor tables with OID/owner/ACL preservation, precondition validation, and conflict detection; migrate_to_native() recreates large objects from lolor storage using native _orig APIs, restoring metadata and handling sparse/hole semantics.
Extension drop integration in C
src/lolor.c
lolor_on_drop_extension() enhanced to check system catalogs for lolor.migrate_to_native() presence and execute it via SPI before disabling the extension, with explicit error handling for migration failures.
Migration test coverage
sql/lolor.sql
Post-upgrade validation, enable/disable/drop state verification, forward-and-reverse migration suite with data integrity checks, manual reverse migration without extension drop, OID conflict detection for both directions, and conflict-at-drop error scenarios.

🐰 With whiskers twitching in glee,
We hop from native to lolor with ease,
OIDs preserved, ACLs too—
Reverse migration, bidirectional brew!
The extension drops, the objects stay true.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly and specifically describes the main change: adding bidirectional large object migration between native and lolor storage, directly matching the changeset content.
Description check ✅ Passed The PR description provides comprehensive details about the new migration functions, design decisions, implementation specifics, and test coverage, all directly related to the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch spoc-489

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codacy-production

codacy-production Bot commented Apr 2, 2026

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 26 high

Results:
26 new issues

Category Results
Compatibility 26 high (26 false positives)

View in Codacy

🟢 Metrics 0 duplication

Metric Results
Duplication 0

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

Comment thread lolor--1.2.2--1.3.sql
@mason-sharp
mason-sharp requested review from mason-sharp and removed request for mason-sharp May 15, 2026 19:21

@mason-sharp mason-sharp left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important feature. I changed the version number to be 1.3.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
sql/lolor.sql (1)

109-145: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add owner/ACL assertions to the migration tests.

These cases validate OID and payload round-tripping, but they never assert that lomowner and lomacl survive either migration direction. Since migrate_to_native() manually patches pg_largeobject_metadata, that part of the feature contract can regress without failing this suite. Please add a role/grant setup and verify owner/ACL preservation before and after both paths.

Also applies to: 156-174

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sql/lolor.sql` around lines 109 - 145, Add assertions and setup to verify
lomowner and lomacl survive both directions of migration: create a test role,
set ownership and specific ACLs on the native large objects (referencing
native_oid1, native_oid2) and on the lolor-created object (lolor_direct_oid)
before calling lolor.migrate_from_native() and before DROP EXTENSION, then after
each migration step query pg_catalog.pg_largeobject_metadata and
lolor.pg_largeobject_metadata for lomowner and lomacl and assert they match the
original role and ACL entries; ensure these checks run both immediately after
migrate_from_native() (confirm native objects removed and lolor rows have
correct lomowner/lomacl) and after DROP EXTENSION/reverse migration (confirm
pg_largeobject_metadata rows restored with the same lomowner/lomacl).
♻️ Duplicate comments (1)
lolor--1.2.2--1.3.sql (1)

156-181: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Replace the per-page LO API loop with bulk catalog inserts.

This is the same unresolved issue from the earlier review: the function already requires superuser, so the comment about not being able to INSERT into pg_catalog.pg_largeobject does not hold here. The row-by-row lo_create_orig/lo_open_orig/lowrite_orig path is much slower than migrate_from_native(), can expand sparse objects, and leaves the descriptor open on mid-write failures. A bulk INSERT ... SELECT into the native large-object catalogs would be both faster and safer.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lolor--1.2.2--1.3.sql` around lines 156 - 181, Replace the slow per-page
native-LO write loop (the FOR r_meta ... PERFORM
pg_catalog.lo_create_orig(r_meta.oid); fd := pg_catalog.lo_open_orig(...); FOR
r_data ... PERFORM pg_catalog.lowrite_orig(...); PERFORM
pg_catalog.lo_close_orig(fd);) with a single bulk catalog insertion: create or
ensure the pg_largeobject_metadata row for r_meta (lomowner/lomacl preserved)
and then perform an INSERT INTO pg_catalog.pg_largeobject ... SELECT pageno,
data FROM lolor.pg_largeobject WHERE loid = r_meta.oid to copy all pages in one
statement; remove use of lo_open_orig/lo_lseek64_orig/lowrite_orig/lo_close_orig
and the inner FOR r_data loop, run under the function's superuser rights, and
ensure transactional safety so partial failures do not leave an open descriptor
or half-migrated object.
🧹 Nitpick comments (1)
lolor--1.2.2--1.3.sql (1)

88-92: ⚡ Quick win

Make the unlink input explicitly buffered for determinism (not because it’s guaranteed to “skip” OIDs)

lolor--1.2.2--1.3.sql:88-92 wraps the scan as FROM (SELECT ...), which doesn’t strictly guarantee planner-level buffering; however, PostgreSQL MVCC/self-visibility generally avoids “missing” tuples purely due to later self-modifications. Still, using an explicitly materialized intermediate set (e.g., temp table / WITH ... AS MATERIALIZED) would make the unlink input evaluation order deterministic and remove any execution-order surprises.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lolor--1.2.2--1.3.sql` around lines 88 - 92, The current PERFORM
pg_catalog.lo_unlink_orig(oid) FROM (SELECT oid FROM
pg_catalog.pg_largeobject_metadata) relies on an unmaterialized subquery; make
the unlink input explicitly buffered for deterministic evaluation by first
materializing the OID set (e.g., create a temporary table or use a WITH ... AS
MATERIALIZED subquery) and then iterate (or SELECT FROM) that materialized set
when calling lo_unlink_orig; ensure you reference the same
pg_largeobject_metadata OIDs and keep the call to lo_unlink_orig(oid) but change
its input source to the explicitly materialized relation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@sql/lolor.sql`:
- Around line 109-145: Add assertions and setup to verify lomowner and lomacl
survive both directions of migration: create a test role, set ownership and
specific ACLs on the native large objects (referencing native_oid1, native_oid2)
and on the lolor-created object (lolor_direct_oid) before calling
lolor.migrate_from_native() and before DROP EXTENSION, then after each migration
step query pg_catalog.pg_largeobject_metadata and lolor.pg_largeobject_metadata
for lomowner and lomacl and assert they match the original role and ACL entries;
ensure these checks run both immediately after migrate_from_native() (confirm
native objects removed and lolor rows have correct lomowner/lomacl) and after
DROP EXTENSION/reverse migration (confirm pg_largeobject_metadata rows restored
with the same lomowner/lomacl).

---

Duplicate comments:
In `@lolor--1.2.2--1.3.sql`:
- Around line 156-181: Replace the slow per-page native-LO write loop (the FOR
r_meta ... PERFORM pg_catalog.lo_create_orig(r_meta.oid); fd :=
pg_catalog.lo_open_orig(...); FOR r_data ... PERFORM
pg_catalog.lowrite_orig(...); PERFORM pg_catalog.lo_close_orig(fd);) with a
single bulk catalog insertion: create or ensure the pg_largeobject_metadata row
for r_meta (lomowner/lomacl preserved) and then perform an INSERT INTO
pg_catalog.pg_largeobject ... SELECT pageno, data FROM lolor.pg_largeobject
WHERE loid = r_meta.oid to copy all pages in one statement; remove use of
lo_open_orig/lo_lseek64_orig/lowrite_orig/lo_close_orig and the inner FOR r_data
loop, run under the function's superuser rights, and ensure transactional safety
so partial failures do not leave an open descriptor or half-migrated object.

---

Nitpick comments:
In `@lolor--1.2.2--1.3.sql`:
- Around line 88-92: The current PERFORM pg_catalog.lo_unlink_orig(oid) FROM
(SELECT oid FROM pg_catalog.pg_largeobject_metadata) relies on an unmaterialized
subquery; make the unlink input explicitly buffered for deterministic evaluation
by first materializing the OID set (e.g., create a temporary table or use a WITH
... AS MATERIALIZED subquery) and then iterate (or SELECT FROM) that
materialized set when calling lo_unlink_orig; ensure you reference the same
pg_largeobject_metadata OIDs and keep the call to lo_unlink_orig(oid) but change
its input source to the explicitly materialized relation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f82a1d95-2fd2-4452-9075-80522d01a78c

📥 Commits

Reviewing files that changed from the base of the PR and between dceb007 and e409a70.

⛔ Files ignored due to path filters (1)
  • expected/lolor.out is excluded by !**/*.out
📒 Files selected for processing (5)
  • Makefile
  • lolor--1.2.2--1.3.sql
  • lolor.control
  • sql/lolor.sql
  • src/lolor.c
✅ Files skipped from review due to trivial changes (2)
  • lolor.control
  • Makefile
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/lolor.c

The platform's pg16 is built --with-llvm expecting LLVM 20, but the
rockylinux:9 base image now ships LLVM 21, so the JIT bitcode step
failed (llvm-lto not found) and the node containers exited. Build with
with_llvm=no and out of the bind-mounted tree, and dump container logs
on CI failure.
@mason-sharp
mason-sharp merged commit 88f3125 into main Jun 2, 2026
4 checks passed
@mason-sharp
mason-sharp deleted the spoc-489 branch June 2, 2026 22:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants