Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ DATA = lolor--1.0.sql \
lolor--1.2.2--1.3.0.sql
PGFILEDESC = "lolor - drop in large objects replacement for logical replication"

OBJS = src/lolor.o src/lolor_fsstubs.o src/lolor_inv_api.o src/lolor_largeobject.o
OBJS = src/lolor.o src/lolor_fsstubs.o src/lolor_inv_api.o src/lolor_largeobject.o \
src/lolor_migrate.o

REGRESS = lolor
TAP_TESTS = 1
Expand Down
24 changes: 21 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,23 @@ SELECT lolor.migrate_to_native(); -- manual
DROP EXTENSION lolor; -- automatic
```

Both directions preserve original OIDs, owners, ACLs, and data.
Both directions preserve original OIDs, owners, ACLs, comments and data, and
copy pages verbatim so sparse large objects stay sparse. Moving an object to
native storage also reinstates its `pg_shdepend` entries, so `DROP ROLE`,
`REASSIGN OWNED` and `DROP OWNED` continue to see it.

`migrate_to_native()` does not require lolor to be enabled; it operates on the
catalogs directly.

Security labels on large objects cannot be represented in lolor storage and
cannot be reinstated without their label provider, so `migrate_from_native()`
refuses rather than discarding them. Remove them first if you hit this.

A helper supports verifying a migration across nodes:

```sql
SELECT * FROM lolor.digest(); -- per-object checksum, compare across nodes
```

When the spock extension is installed, both migration functions run under
`spock.repair_mode()`, so the row-shuffling migration DML is **not**
Expand All @@ -101,6 +117,7 @@ Even with spock, migration is refused if a non-spock logical replication slot
own output plugin and those consumers would still decode the migration DML:
`migrate_to_native()` raises an `ERROR`, while `migrate_from_native()` warns and
returns -1 without doing anything. Drop the offending slots before migrating.
The check identifies spock's slots by their `spock_output` plugin.

Without spock, the migration DML cannot be excluded from logical decoding, so
both functions refuse to migrate while logical replication slots exist in the
Expand All @@ -123,5 +140,6 @@ database user. Upgrading to 1.3.0 revokes the privilege; see the release notes.
### Limitations

- Native large object functionality cannot be used while you are using the lolor extension.
- lolor does not support the following statements: `ALTER LARGE OBJECT`, `GRANT ON LARGE OBJECT`, `COMMENT ON LARGE OBJECT`, and `REVOKE ON LARGE OBJECT`.
- Large object migration is node-local. Native large objects live in `pg_catalog.pg_largeobject`, which is never replicated, so each node holds an independent set and `migrate_from_native()` migrates only the local node's objects; with spock installed, the migration DML runs in repair mode and is not replicated. Run the migration on every node that holds native large objects — for example with `spock.replicate_ddl('SELECT lolor.migrate_from_native()')`, which queues the command so that each node executes it locally. Migrated objects keep their original native OIDs, which are not node-encoded: if different nodes hold different objects under the same OID, the nodes' lolor contents will diverge and later replicated changes to those objects can conflict. Newly created large objects are collision-free, since new OIDs are node-encoded via `lolor.node` and checked against existing rows.
- lolor does not support the following statements against objects held in lolor storage: `ALTER LARGE OBJECT`, `GRANT ON LARGE OBJECT`, `COMMENT ON LARGE OBJECT`, and `REVOKE ON LARGE OBJECT`. Owners, ACLs and comments set while an object was in native storage are preserved across migration in both directions.
- Large object migration is node-local. Native large objects live in `pg_catalog.pg_largeobject`, which is never replicated, so each node holds an independent set and `migrate_from_native()` migrates only the local node's objects; with spock installed, the migration DML runs in repair mode and is not replicated. Run the migration on every node that holds native large objects, connecting to each node directly. It refuses to run inside an apply worker, so sending it through DDL replication fails on every subscriber. Migrated objects keep their original native OIDs, which are not node-encoded: if different nodes hold different objects under the same OID, the nodes' lolor contents will diverge and later replicated changes to those objects can conflict. Newly created large objects are collision-free, since new OIDs are node-encoded via `lolor.node` and checked against existing rows. To make that hazard an error rather than silent divergence, collect the other nodes' OIDs with `lolor.native_lo_oids()` and pass them in: `SELECT lolor.migrate_from_native(peer_oids => ARRAY[...])` refuses when any of them collide. After migrating every node, compare `lolor.digest()` across nodes to confirm they converged.
- `lolor.migrate_from_native()` and `lolor.migrate_to_native()` refuse while any other session is connected to the database, and refuse to run from anything but a client session, so replicated DDL cannot execute them inside an apply worker.
14 changes: 11 additions & 3 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,14 @@ installed:
SELECT lolor.migrate_to_native();
```

Both paths preserve OIDs, owners, ACLs, and data.
Both paths preserve OIDs, owners, ACLs, comments and data, and copy pages
verbatim so sparse large objects stay sparse.

A helper supports verifying a migration across nodes:

```sql
SELECT * FROM lolor.digest(); -- per-object checksum, compare across nodes
```

When the spock extension is installed, both migration functions run under
`spock.repair_mode()`, so the row-shuffling migration DML is **not** replicated
Expand All @@ -82,5 +89,6 @@ retains the changes and delivers them when replication resumes.
## Limitations

- Native Postgres large object functionality cannot be used while you are using the lolor extension.
- lolor does not support the following statements: `ALTER LARGE OBJECT`, `GRANT ON LARGE OBJECT`, `COMMENT ON LARGE OBJECT`, and `REVOKE ON LARGE OBJECT`.
- Large object migration is node-local. Native large objects live in `pg_catalog.pg_largeobject`, which is never replicated, so each node holds an independent set and `migrate_from_native()` migrates only the local node's objects; with spock installed, the migration DML runs in repair mode and is not replicated. Run the migration on every node that holds native large objects — for example with `spock.replicate_ddl('SELECT lolor.migrate_from_native()')`, which queues the command so that each node executes it locally. Migrated objects keep their original native OIDs, which are not node-encoded: if different nodes hold different objects under the same OID, the nodes' lolor contents will diverge and later replicated changes to those objects can conflict. Newly created large objects are collision-free, since new OIDs are node-encoded via `lolor.node` and checked against existing rows.
- lolor does not support the following statements against objects held in lolor storage: `ALTER LARGE OBJECT`, `GRANT ON LARGE OBJECT`, `COMMENT ON LARGE OBJECT`, and `REVOKE ON LARGE OBJECT`. Owners, ACLs and comments set while an object was in native storage are preserved across migration in both directions.
- Large object migration is node-local. Native large objects live in `pg_catalog.pg_largeobject`, which is never replicated, so each node holds an independent set and `migrate_from_native()` migrates only the local node's objects; with spock installed, the migration DML runs in repair mode and is not replicated. Run the migration on every node that holds native large objects, connecting to each node directly. It refuses to run inside an apply worker, so sending it through DDL replication fails on every subscriber. Migrated objects keep their original native OIDs, which are not node-encoded: if different nodes hold different objects under the same OID, the nodes' lolor contents will diverge and later replicated changes to those objects can conflict. Newly created large objects are collision-free, since new OIDs are node-encoded via `lolor.node` and checked against existing rows. To make that hazard an error rather than silent divergence, collect the other nodes' OIDs with `lolor.native_lo_oids()` and pass them in: `SELECT lolor.migrate_from_native(peer_oids => ARRAY[...])` refuses when any of them collide. After migrating every node, compare `lolor.digest()` across nodes to confirm they converged.
- `lolor.migrate_from_native()` and `lolor.migrate_to_native()` refuse while any other session is connected to the database, and refuse to run from anything but a client session, so replicated DDL cannot execute them inside an apply worker.
19 changes: 12 additions & 7 deletions docs/lolor_release_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,21 @@

## lolor 1.3.0

* Add bidirectional large object migration between native PostgreSQL and lolor storage:
* `lolor.migrate_from_native()` migrates existing native large objects into lolor storage. This is a manual step (run after `CREATE EXTENSION lolor`) and requires superuser privileges.
* `lolor.migrate_to_native()` migrates lolor large objects back to native storage.
* Reverse migration runs automatically on `DROP EXTENSION lolor`, so large objects are never lost when the extension is removed.
* With the spock extension installed, both migration functions run under `spock.repair_mode()` so migration is replication-safe. If a logical replication slot that spock cannot suppress is present — a non-spock slot, or any logical slot when spock is absent — the functions refuse to migrate rather than risk losing objects.
* Migration is node-local: native large objects are never replicated, so each node holds an independent set and `migrate_from_native()` migrates only the local node's objects. Run it on every node that holds native large objects, for example via `spock.replicate_ddl('SELECT lolor.migrate_from_native()')`. Migrated objects keep their original native OIDs, which are not node-encoded and can collide across nodes if different nodes hold different objects under the same OID; newly created large objects are collision-free, since new OIDs are node-encoded via `lolor.node` and checked against existing rows.
* Add bidirectional large object migration between native PostgreSQL and lolor storage, implemented as a direct relation-to-relation copy in C (`lolor.migrate_storage()`) rather than a loop through the large object API:
* `lolor.migrate_from_native(peer_oids oid[] DEFAULT NULL)` moves native large objects into lolor storage. It is a manual step after `CREATE EXTENSION lolor`, requires superuser, and returns the number of objects moved or -1 if the migration was refused.
* `lolor.migrate_to_native()` moves them back, whether or not lolor is enabled. It also still runs automatically on `DROP EXTENSION lolor`.
* Both directions preserve OIDs, owners, ACLs, comments and the exact page layout, so sparse objects stay sparse. Ownership and ACLs are recorded in `pg_shdepend` on the way back, so `DROP ROLE`, `REASSIGN OWNED` and `DROP OWNED` see the objects again. Comments are parked in `lolor.pg_largeobject_description` while an object is in lolor storage. Native objects are removed through `performMultipleDeletions()`, the path `DROP` uses, so their dependencies, comments and labels are cleaned up.
* Storage layouts are verified against the running server at migration time. Security labels, which lolor storage cannot represent, cause `migrate_from_native()` to refuse. OID conflicts are reported with the conflicting OIDs.
* Migration holds `ShareRowExclusiveLock` on both stores until commit. Ordinary large object access takes `RowExclusiveLock`, which does not conflict with itself, so a concurrent `lo_write()` could otherwise be lost silently. Opposing migrations take their locks in a fixed order and cannot deadlock.
* Both migration functions refuse while any other session is connected to the database, and refuse to run from anything but a client session.
* With spock installed, both functions run under `spock.repair_mode()`. If a logical slot spock cannot suppress exists, a non-spock slot or any slot when spock is absent, they refuse rather than let subscribers decode the migration as row changes.
* Migration is node-local: native large objects are never replicated, so run the migration on each node by connecting to it directly. Do not send it through DDL replication, which would run it inside every subscriber's apply worker. Native OIDs are not node-encoded, so pass the other nodes' OIDs (from `lolor.native_lo_oids()`) as `peer_oids` to turn a cross-node collision into a refusal, and compare `lolor.digest()` across nodes afterwards.
* `migrate_to_native()` refuses with a clear error while any object refers to a dropped role, instead of failing part-way with "role N was concurrently dropped"; remove those objects with `lo_unlink()` and retry.
* New helpers `lolor.digest()` and `lolor.native_lo_oids()` for cross-node checks after a node-local migration.
* **Security fix: `lo_import()` and `lo_export()` were executable by any database user.** These read and write files on the server as the account PostgreSQL runs under, so core revokes `EXECUTE` on them from `PUBLIC`. lolor replaces them by renaming the originals to `*_orig`; an ACL belongs to a function rather than a name, so the restriction stayed on the parked original while each replacement got the default `EXECUTE TO PUBLIC`. Any user could read an arbitrary server file with `lo_import()` or overwrite one with `lo_export()`. The replacements are now locked down at install time, and upgrading revokes the privilege on existing installations in either state. Versions 1.0 through 1.2.2 are affected.
* The extension is no longer marked `trusted`. Installing lolor renames functions in `pg_catalog` for the whole database, which a non-superuser should not be able to do.
* Fixed the `lolor.node` upper bound. The GUC accepted 0..16 while a generated OID reserves four bits for the node id, so node 16 did not fit: the node field of every OID it generated read back as 0. The bound is now derived from the encoding (`LOLOR_MAX_NODE_ID`), giving 0..15. **If you have `lolor.node = 16` configured**, the server still starts but logs `16 is outside the valid range for parameter "lolor.node" (0 .. 15)` and falls back to 0, which is the node id it was effectively using already. Set it to a value in 0..15, and check for OID collisions against whichever node is genuinely 0.
* Expanded test coverage: TAP tests for dump/restore, streaming and logical replication, and standby promotion; regression tests for `lo_lseek`, `lo_tell`, and `lo_truncate`.
* Expanded test coverage: TAP tests for dump/restore, streaming and logical replication, standby promotion, migration fidelity and locking, and cross-node OID collisions; regression tests for `lo_lseek`, `lo_tell`, `lo_truncate`, sparse and multi-page objects, comment round trips and the migration helpers.
* Security hardening: addressed Codacy/Flawfinder warnings.

## lolor 1.2.2
Expand Down
Loading
Loading