-
Notifications
You must be signed in to change notification settings - Fork 407
Enable logical replication slots (wal_level=logical + pgoutput fixes) #1047
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
riderx
wants to merge
2
commits into
electric-sql:main
Choose a base branch
from
riderx:feat/logical-replication-slots
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| '@electric-sql/pglite': patch | ||
| --- | ||
|
|
||
| Support logical replication slots: start Postgres with `wal_level=logical` by default, export the symbols the `pgoutput` output plugin needs from the WASM main module, and surface unexpected WASM crashes as errors instead of silently corrupting the session. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,134 @@ | ||
| import { describe, it, expect, beforeEach, afterEach } from 'vitest' | ||
| import { PGlite } from '../dist/index.js' | ||
|
|
||
| describe('replication slots', () => { | ||
| let db: PGlite | ||
|
|
||
| beforeEach(async () => { | ||
| db = await PGlite.create() | ||
| }) | ||
|
|
||
| afterEach(async () => { | ||
| if (!db.closed) { | ||
| await db.close() | ||
| } | ||
| }) | ||
|
|
||
| it('wal_level defaults to logical', async () => { | ||
| const res = await db.query<{ wal_level: string }>(`SHOW wal_level`) | ||
| expect(res.rows[0].wal_level).toBe('logical') | ||
| }) | ||
|
|
||
| it('can create, list and drop a logical replication slot', async () => { | ||
| const created = await db.query<{ slot_name: string; lsn: string }>( | ||
| `SELECT * FROM pg_create_logical_replication_slot('test_slot', 'pgoutput')`, | ||
| ) | ||
| expect(created.rows[0].slot_name).toBe('test_slot') | ||
| expect(created.rows[0].lsn).toBeTruthy() | ||
|
|
||
| const slots = await db.query<{ slot_name: string; plugin: string }>( | ||
| `SELECT slot_name, plugin FROM pg_replication_slots`, | ||
| ) | ||
| expect(slots.rows).toEqual([{ slot_name: 'test_slot', plugin: 'pgoutput' }]) | ||
|
|
||
| await db.query(`SELECT pg_drop_replication_slot('test_slot')`) | ||
| const after = await db.query(`SELECT slot_name FROM pg_replication_slots`) | ||
| expect(after.rows).toEqual([]) | ||
| }) | ||
|
|
||
| it('decodes changes via pgoutput', async () => { | ||
| await db.exec(` | ||
| CREATE TABLE rep_test (id int PRIMARY KEY, value text); | ||
| CREATE PUBLICATION rep_pub FOR ALL TABLES; | ||
| `) | ||
| // slot creation is not allowed in a transaction that has performed | ||
| // writes, so it can't be part of the exec batch above | ||
| await db.query( | ||
| `SELECT pg_create_logical_replication_slot('rep_slot', 'pgoutput')`, | ||
| ) | ||
|
|
||
| await db.exec(` | ||
| INSERT INTO rep_test VALUES (1, 'one'), (2, 'two'); | ||
| UPDATE rep_test SET value = 'uno' WHERE id = 1; | ||
| DELETE FROM rep_test WHERE id = 2; | ||
| `) | ||
|
|
||
| // peek first - changes must remain available afterwards | ||
| const peeked = await db.query<{ data: Uint8Array }>( | ||
| `SELECT data FROM pg_logical_slot_peek_binary_changes( | ||
| 'rep_slot', NULL, NULL, | ||
| 'proto_version', '1', 'publication_names', 'rep_pub')`, | ||
| ) | ||
| expect(peeked.rows.length).toBeGreaterThan(0) | ||
|
|
||
| // messages start with a tag byte; expect insert (I), update (U) and delete (D) | ||
| const tags = peeked.rows.map((r) => String.fromCharCode(r.data[0])) | ||
| expect(tags).toContain('I') | ||
| expect(tags).toContain('U') | ||
| expect(tags).toContain('D') | ||
|
|
||
| // consuming advances the slot | ||
| const consumed = await db.query( | ||
| `SELECT lsn FROM pg_logical_slot_get_binary_changes( | ||
| 'rep_slot', NULL, NULL, | ||
| 'proto_version', '1', 'publication_names', 'rep_pub')`, | ||
| ) | ||
| expect(consumed.rows.length).toBe(peeked.rows.length) | ||
|
|
||
| const empty = await db.query( | ||
| `SELECT lsn FROM pg_logical_slot_get_binary_changes( | ||
| 'rep_slot', NULL, NULL, | ||
| 'proto_version', '1', 'publication_names', 'rep_pub')`, | ||
| ) | ||
| expect(empty.rows).toEqual([]) | ||
| }) | ||
|
|
||
| it('recovers cleanly from a decoding error', async () => { | ||
| await db.query( | ||
| `SELECT pg_create_logical_replication_slot('err_slot', 'pgoutput')`, | ||
| ) | ||
|
|
||
| // pgoutput requires publication_names; this must fail with a proper error | ||
| await expect( | ||
| db.query( | ||
| `SELECT * FROM pg_logical_slot_get_binary_changes('err_slot', NULL, NULL, 'proto_version', '1')`, | ||
| ), | ||
| ).rejects.toThrow(/publication_names/) | ||
|
|
||
| // and the session must still be usable afterwards | ||
| const res = await db.query<{ one: number }>(`SELECT 1 AS one`) | ||
| expect(res.rows[0].one).toBe(1) | ||
|
|
||
| const slots = await db.query<{ slot_name: string; active: boolean }>( | ||
| `SELECT slot_name, active FROM pg_replication_slots`, | ||
| ) | ||
| expect(slots.rows).toEqual([{ slot_name: 'err_slot', active: false }]) | ||
| }) | ||
|
|
||
| it('slots survive a dump/load cycle', async () => { | ||
| await db.exec(` | ||
| CREATE TABLE rep_persist (id int PRIMARY KEY); | ||
| CREATE PUBLICATION persist_pub FOR ALL TABLES; | ||
| `) | ||
| await db.query( | ||
| `SELECT pg_create_logical_replication_slot('persist_slot', 'pgoutput')`, | ||
| ) | ||
| const dump = await db.dumpDataDir('none') | ||
| await db.close() | ||
|
|
||
| db = await PGlite.create({ loadDataDir: dump }) | ||
| const slots = await db.query<{ slot_name: string }>( | ||
| `SELECT slot_name FROM pg_replication_slots`, | ||
| ) | ||
| expect(slots.rows).toEqual([{ slot_name: 'persist_slot' }]) | ||
|
|
||
| // the restored slot must still be usable | ||
| await db.exec(`INSERT INTO rep_persist VALUES (1)`) | ||
| const changes = await db.query( | ||
| `SELECT lsn FROM pg_logical_slot_get_binary_changes( | ||
| 'persist_slot', NULL, NULL, | ||
| 'proto_version', '1', 'publication_names', 'persist_pub')`, | ||
| ) | ||
| expect(changes.rows.length).toBeGreaterThan(0) | ||
| }) | ||
| }) |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This enables
wal_level=logicaland documents logical slot support, but the commit does not include the required wasm-side symbol export fix:git ls-treeshows both this commit and its parent still pinpostgres-pgliteto7b4ee5086055dc5e54ae1e13e487888249438e68, and the rootwasm:buildscript builds the release wasm from that submodule. In a clean build of this tree,pgoutputstill lacks the exported core symbols described in the commit message, so calls likepg_logical_slot_get_binary_changes(..., 'pgoutput')will still hit the missing-symbol crash instead of delivering changes; please include the submodule bump before advertising/enabling this feature.Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch — addressed in 6d55f6c: the submodule now points at the head of electric-sql/postgres-pglite#79 (
04f1bea), which contains the pgoutput symbol-export fix. That commit is served by the upstream repo viarefs/pull/79/head, sogit submodule updatecan fetch it by SHA (verified with agit fetch --depth 1 <upstream> 04f1beafrom a fresh clone). A cleanwasm:buildof this tree now produces a main module that exports all 46 symbols pgoutput.so imports, and the new replication-slot tests pass against it. Once #79 merges, this pointer should be bumped to the merge commit.