{"id":51082980,"url":"https://github.com/kanutocd/pgoutput-decoder","last_synced_at":"2026-06-23T20:00:52.811Z","repository":{"id":361587995,"uuid":"1255009444","full_name":"kanutocd/pgoutput-decoder","owner":"kanutocd","description":"Decodes pgoutput-parser protocol messages into immutable Ruby row-change events.","archived":false,"fork":false,"pushed_at":"2026-06-01T04:36:54.000Z","size":59,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-13T08:20:12.094Z","etag":null,"topics":["cdc","change-data-capture","decoder","jsonb","logical-decoding","logical-replication","oid","pgoutput","postgres","postgres-typed","postgres-types","postgresql","postgresql-types","ruby","type-decoder"],"latest_commit_sha":null,"homepage":"https://kanutocd.github.io/pgoutput-decoder/","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/kanutocd.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-31T09:30:35.000Z","updated_at":"2026-06-03T04:55:19.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/kanutocd/pgoutput-decoder","commit_stats":null,"previous_names":["kanutocd/pgoutput-decoder"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/kanutocd/pgoutput-decoder","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kanutocd%2Fpgoutput-decoder","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kanutocd%2Fpgoutput-decoder/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kanutocd%2Fpgoutput-decoder/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kanutocd%2Fpgoutput-decoder/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/kanutocd","download_url":"https://codeload.github.com/kanutocd/pgoutput-decoder/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kanutocd%2Fpgoutput-decoder/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34704748,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-23T02:00:07.161Z","response_time":65,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cdc","change-data-capture","decoder","jsonb","logical-decoding","logical-replication","oid","pgoutput","postgres","postgres-typed","postgres-types","postgresql","postgresql-types","ruby","type-decoder"],"created_at":"2026-06-23T20:00:51.566Z","updated_at":"2026-06-23T20:00:52.805Z","avatar_url":"https://github.com/kanutocd.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# pgoutput-decoder\n\n[![Gem Version](https://badge.fury.io/rb/pgoutput-decoder.svg)](https://badge.fury.io/rb/pgoutput-decoder)\n[![CI](https://github.com/kanutocd/pgoutput-decoder/workflows/CI/badge.svg)](https://github.com/kanutocd/pgoutput-decoder/actions)\n[![Ruby Version](https://img.shields.io/badge/ruby-%3E%3D%203.4-ruby.svg)](https://www.ruby-lang.org/en/)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n\nA high-level PostgreSQL `pgoutput` logical replication value decoder for Ruby.\n\n`pgoutput-decoder` is the companion layer to [`pgoutput-parser`](https://rubygems.org/gems/pgoutput-parser). It accepts immutable protocol messages produced by `pgoutput-parser` and turns tuple payloads into application-friendly Ruby row-change events.\n\nIt does **not** parse PostgreSQL wire bytes and it does **not** open replication connections. Those concerns belong to lower-level parser and future client layers.\n\n---\n\n## Requirements\n\n- Ruby 3.4+\n- `pgoutput-parser` `~\u003e 0.1`\n\n---\n\n## Architecture\n\n```text\npgoutput-parser\n      │\n      ▼\nProtocol messages\n      │\n      ▼\npgoutput-decoder\n      │\n      ▼\nDecoded row-change events\n```\n\n---\n\n## What This Gem Does\n\n- Decodes PostgreSQL OID-backed tuple values\n- Builds Ruby hashes from relation columns and tuple values\n- Tracks relation metadata from `Relation` messages\n- Tracks active transaction context from `Begin` / `Commit` messages\n- Attaches `transaction_id` to DML events\n- Returns immutable, Ractor-shareable event objects\n- Supports custom OID decoders\n\n---\n\n## What This Gem Does Not Do\n\nThis gem intentionally does not:\n\n- Parse PostgreSQL `CopyData` bytes\n- Manage replication slots\n- Open replication connections\n- Maintain WAL acknowledgements\n- Reconnect to PostgreSQL\n- Publish events to queues\n- Integrate with ActiveRecord\n\n---\n\n## Installation\n\n```ruby\ngem \"pgoutput-decoder\"\n```\n\nThen:\n\n```bash\nbundle install\n```\n\nRequire it with:\n\n```ruby\nrequire \"pgoutput/decoder\"\n```\n\n---\n\n## Quick Start\n\n```ruby\nrequire \"pgoutput\"\nrequire \"pgoutput/decoder\"\n\nstream = Pgoutput::RelationTracker.new\ndecoder = Pgoutput::Decoder.new\n\nprotocol_message = stream.process(payload)\nevent = decoder.decode(protocol_message)\n```\n\nA `Relation` message updates decoder metadata and returns `nil`:\n\n```ruby\ndecoder.decode(relation_message)\n# =\u003e nil\n```\n\nAn insert message returns a decoded event:\n\n```ruby\nevent = decoder.decode(insert_message)\n\nevent.transaction_id\n# =\u003e 789\n\nevent.schema\n# =\u003e \"public\"\n\nevent.table\n# =\u003e \"users\"\n\nevent.values\n# =\u003e { \"id\" =\u003e 7, \"name\" =\u003e \"Alice\", \"active\" =\u003e true }\n```\n\n---\n\n## Transaction Context\n\nPostgreSQL `pgoutput` carries the transaction ID in the `Begin` (`B`) message, not on every row-change message.\n\nThe decoder remembers the active transaction and attaches it to decoded DML events:\n\n```ruby\ndecoder.decode(begin_message)\ndecoder.decode(relation_message)\ninsert = decoder.decode(insert_message)\n\ninsert.transaction_id\n# =\u003e 789\n```\n\nThe transaction ID is useful for grouping changes, debugging, and CDC processing. It should not be treated as a globally permanent identifier because PostgreSQL transaction IDs can wrap around.\n\n---\n\n## Supported Events\n\n```ruby\nPgoutput::Decoder::Events::Begin\nPgoutput::Decoder::Events::Commit\nPgoutput::Decoder::Events::Insert\nPgoutput::Decoder::Events::Update\nPgoutput::Decoder::Events::Delete\n```\n\n---\n\n## Default Type Support\n\nThe default registry supports common scalar PostgreSQL OIDs:\n\n| OID  | Type             |\n| ---- | ---------------- |\n| 16   | boolean          |\n| 20   | bigint           |\n| 21   | smallint         |\n| 23   | integer          |\n| 25   | text             |\n| 114  | json             |\n| 700  | real             |\n| 701  | double precision |\n| 1043 | varchar          |\n| 1082 | date             |\n| 1114 | timestamp        |\n| 1184 | timestamptz      |\n| 1700 | numeric          |\n| 2950 | uuid             |\n| 3802 | jsonb            |\n\nUnsupported OIDs are returned as frozen raw strings.\n\n---\n\n## Binary Values\n\nBinary decoding is intentionally conservative.\n\nThe decoder handles safe fixed-width binary scalar types such as:\n\n- boolean\n- int2\n- int4\n- int8\n- float4\n- float8\n\nUnsupported binary values are preserved as frozen raw bytes.\n\n---\n\n## Custom OID Decoders\n\n```ruby\nregistry =\n  Pgoutput::Decoder::TypeRegistry.default.with_decoder(999_999) do |raw, format|\n    format == :text ? \"custom:#{raw}\" : raw\n  end\n\ndecoder = Pgoutput::Decoder.new(type_registry: registry)\n```\n\n---\n\n## Update Events\n\n```ruby\nupdate = decoder.decode(update_message)\n\nupdate.old_key\n# =\u003e { \"id\" =\u003e 7 } or nil\n\nupdate.old_values\n# =\u003e { ... } or nil\n\nupdate.new_values\n# =\u003e { \"id\" =\u003e 7, \"name\" =\u003e \"Bob\" }\n```\n\n---\n\n## Delete Events\n\n```ruby\ndelete = decoder.decode(delete_message)\n\ndelete.old_key\n# =\u003e { \"id\" =\u003e 7 } or nil\n\ndelete.old_values\n# =\u003e { ... } or nil\n```\n\n---\n\n## Ractor Safety\n\nDecoded events are deeply shareable:\n\n```ruby\nevent = decoder.decode(update_message)\n\nRactor.shareable?(event)\n# =\u003e true\n```\n\nThe decoder instance itself is stateful and should not be shared across Ractors.\n\n---\n\n## Testing\n\n```bash\nbundle exec rake test\n```\n\nWith coverage:\n\n```bash\nCOVERAGE=true bundle exec rake test\n```\n\n---\n\n## Type Checking\n\n```bash\nbundle exec steep check\n```\n\n---\n\n## License\n\n[MIT](LICENSE.txt).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkanutocd%2Fpgoutput-decoder","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkanutocd%2Fpgoutput-decoder","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkanutocd%2Fpgoutput-decoder/lists"}