{"id":25177739,"url":"https://github.com/nasdaq/active_record_proxy_adapters","last_synced_at":"2026-01-03T05:14:53.417Z","repository":{"id":265132021,"uuid":"894097493","full_name":"Nasdaq/active_record_proxy_adapters","owner":"Nasdaq","description":"Read replica proxy adapters for ActiveRecord!","archived":false,"fork":false,"pushed_at":"2025-04-04T21:50:45.000Z","size":209,"stargazers_count":52,"open_issues_count":0,"forks_count":5,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-04-09T21:13:00.456Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","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/Nasdaq.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}},"created_at":"2024-11-25T18:43:14.000Z","updated_at":"2025-04-05T13:09:10.000Z","dependencies_parsed_at":null,"dependency_job_id":"7fc7ef7e-7ee0-4d55-83e0-fafc5a5f2293","html_url":"https://github.com/Nasdaq/active_record_proxy_adapters","commit_stats":null,"previous_names":["nasdaq/active_record_proxy_adapters"],"tags_count":21,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Nasdaq%2Factive_record_proxy_adapters","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Nasdaq%2Factive_record_proxy_adapters/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Nasdaq%2Factive_record_proxy_adapters/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Nasdaq%2Factive_record_proxy_adapters/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Nasdaq","download_url":"https://codeload.github.com/Nasdaq/active_record_proxy_adapters/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248111973,"owners_count":21049578,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","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":[],"created_at":"2025-02-09T14:49:26.844Z","updated_at":"2026-01-03T05:14:53.411Z","avatar_url":"https://github.com/Nasdaq.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ActiveRecordProxyAdapters\n\n[![Run Test Suite](https://github.com/Nasdaq/active_record_proxy_adapters/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/Nasdaq/active_record_proxy_adapters/actions/workflows/test.yml)\n\nA set of ActiveRecord adapters that leverage Rails native multiple database setup to allow automatic connection switching from _one_ primary pool to _one_ replica pool at the database statement level.\n\n## Why do I need this?\n\nMaybe you don't. Rails already provides, since version 6.0, a [Rack middleware](https://guides.rubyonrails.org/active_record_multiple_databases.html#activating-automatic-role-switching) that switches between primary and replica automatically based on the HTTP request (`GET` and `HEAD` requests go the replica, everything else goes to the primary).\n\nThe caveat is: you are not allowed do any writes in any `GET` or `HEAD` requests (including controller callbacks).\nWhich means, for example, your `devise` callbacks that save user metadata will now crash.\nSo will your `ahoy-matey` callbacks.\n\nYou will then start wrapping those callbacks in `ApplicationRecord.connected_to(role :reading) {}` blocks as a workaround and, many months later, you have dozens of those (we had nearly 40 when we decided to build this gem).\n\nBy the way, that middleware only works at HTTP request layer (well, duh! it's a Rack middleware).\nSo not good for background jobs, cron jobs or anything that happens outside the scope of an HTTP request. And, if your application needs a replica at this point, for sure you would benefit from automatic connection switching in background jobs too, wouldn't you?\n\nThis gem is heavily inspired by [Makara](https://github.com/instacart/makara), a fantastic gem built by the Instacart folks, which is [no longer maintained](https://github.com/instacart/makara/issues/393), but we took a slightly different, slimmer approach. We don't support load balancing replicas, and that is by design. We believe that should be done outside the scope of the application (using tools like `Pgpool-II`, `pgcat` or RDS Proxy).\n\n## Installation\n\nInstall the gem and add to the application's Gemfile by executing:\n\n    $ bundle add 'active_record_proxy_adapters'\n\nThat will install and load the proxies for all Rails-supported adapters (i.e. `postgresql_proxy`, `mysql2_proxy`, `sqlite3_proxy` and `trilogy_proxy`).\n\nIf you wish to load only one specific adapter for a faster application boot, use:\n\n    $ bundle add 'active_record_proxy_adapters' --require 'active_record_proxy_adapters/railties/\u003cpostgresql|mysql2|sqlite3|trilogy\u003e'\n\nOr, in your Gemfile, use:\n\n```ruby\n  gem \"active_record_proxy_adapters\", require: 'active_record_proxy_adapters/railties/\u003cpostgresql|mysql2|sqlite3|trilogy\u003e'\n```\n\nIf bundler is not being used to manage dependencies, install the gem by executing:\n\n    $ gem install active_record_proxy_adapters\n\n## Usage\n\n### On Rails\n\nIn `config/database.yml`, use `{your_database_adapter}_proxy` as the adapter for the `primary` database, and keep `{your_database_adapter}` for the replica database.\n\nCurrently supported adapters:\n\n- `postgresql`\n- `mysql2`\n- `trilogy`\n- `sqlite3`\n\n\n#### PostgreSQL\n```yaml\n# config/database.yml\ndevelopment:\n  primary:\n    adapter: postgresql_proxy\n    # your primary credentials here\n\n  primary_replica:\n    adapter: postgresql\n    replica: true\n    # your replica credentials here\n```\n\n#### MySQL\n```yaml\n# config/database.yml\ndevelopment:\n  primary:\n    adapter: mysql2_proxy\n    # your primary credentials here\n\n  primary_replica:\n    adapter: mysql2\n    replica: true\n    # your replica credentials here\n```\n\n#### Trilogy\n```yaml\n# config/database.yml\ndevelopment:\n  primary:\n    adapter: trilogy_proxy\n    # your primary credentials here\n\n  primary_replica:\n    adapter: trilogy\n    replica: true\n    # your replica credentials here\n```\n\n#### SQLite\n```yaml\n# config/database.yml\ndevelopment:\n  primary:\n    adapter: sqlite3_proxy\n    # your primary credentials here\n\n  primary_replica:\n    adapter: sqlite3\n    replica: true\n    # your replica credentials here\n```\n\n```ruby\n# app/models/application_record.rb\nclass ApplicationRecord \u003c ActiveRecord::Base\n  self.abstract_class = true\n\n  connects_to database: { writing: :primary, reading: :primary_replica }\nend\n```\n\n### Off Rails\n\n```ruby\n# In your application setup\nrequire \"active_record_proxy_adapters\"\nrequire \"active_record_proxy_adapters/connection_handling\"\n\n# in your base model\nclass ApplicationRecord \u003c\u003c ActiveRecord::Base\n    establish_connection(\n        {\n            adapter: 'postgresql_proxy', # or any of the following: mysql2_proxy, trilogy_proxy, sqlite3_proxy\n            # your primary credentials here\n        },\n        role: :writing\n    )\n\n    establish_connection(\n        {\n            adapter: 'postgresql',  # or any of the following: mysql2, trilogy, sqlite3\n            # your replica credentials here\n        },\n        role: :reading\n    )\nend\n```\n\n## Configuration\n\nThe gem comes preconfigured out of the box. However, if default configuration does not suit your needs, you can modify it by using a `.configure` block:\n\n```ruby\n# config/initializers/active_record_proxy_adapters.rb\nActiveRecordProxyAdapters.configure do |config|\n  # How long proxy should reroute all read requests to primary after a write\n  config.proxy_delay = 5.seconds # defaults to 2.seconds\n\n  # How long proxy should wait for replica to connect.\n  config.checkout_timeout = 5.seconds # defaults to 2.seconds\nend\n```\n\n### Configuring multiple connections\nGeneral settings are automatically applied to the `primary` database by default.\n\nIf your application has multiple databases that need separate proxy settings, you can use the `database` block for individual database settings.\n\n```ruby\nActiveRecordProxyAdapters.configure do |config|\n  config.database :primary do |primary_config|\n    primary_config.proxy_delay = 2.seconds\n  end\n\n  config.database :secondary do |secondary_config|\n     secondary_config.proxy_delay = 5.seconds\n  end\nend\n```\n\nWith those settings, any model that `connects_to database: { writing: :primary }` will have a 2-second delay, and any model that `connects_to database: { writing: :secondary }` will have a 5-second delay.\n\n## Logging\n\n```ruby\n# config/initializers/active_record_proxy_adapters.rb\nrequire \"active_record_proxy_adapters/log_subscriber\"\n\nActiveRecordProxyAdapters.configure do |config|\n  config.log_subscriber_primary_prefix = \"My primary tag\" # defaults to ActiveRecord configuration name\", i.e \"primary\"\n  config.log_subscriber_replica_prefix = \"My replica tag\" # defaults to ActiveRecord configuration name\", i.e \"primary_replica\"\nend\n\n# You may want to remove duplicate logs\nActiveRecord::LogSubscriber.detach_from :active_record\n```\n\n### Example:\n\n```ruby\nirb(main):001\u003e User.count ; User.create(name: 'John Doe', email: 'john.doe@example.com') ; 3.times { User.count ; sleep(1) }\n```\nyields\n\n```\nD, [2024-12-24T17:18:49.151235 #328] DEBUG -- :   [My replica tag] User Count (0.5ms)  SELECT COUNT(*) FROM \"users\"\nD, [2024-12-24T17:18:49.156633 #328] DEBUG -- :   [My primary tag] TRANSACTION (0.1ms)  BEGIN\nD, [2024-12-24T17:18:49.157323 #328] DEBUG -- :   [My primary tag] User Create (0.4ms)  INSERT INTO \"users\" (\"name\", \"email\", \"created_at\", \"updated_at\") VALUES ($1, $2, $3, $4) RETURNING \"id\"  [[\"name\", \"John Doe\"], [\"email\", \"john.doe@example.com\"], [\"created_at\", \"2024-12-24 17:18:49.156063\"], [\"updated_at\", \"2024-12-24 17:18:49.156063\"]]\nD, [2024-12-24T17:18:49.158305 #328] DEBUG -- :   [My primary tag] TRANSACTION (0.7ms)  COMMIT\nD, [2024-12-24T17:18:49.159079 #328] DEBUG -- :   [My primary tag] User Count (0.3ms)  SELECT COUNT(*) FROM \"users\"\nD, [2024-12-24T17:18:50.166105 #328] DEBUG -- :   [My primary tag] User Count (1.9ms)  SELECT COUNT(*) FROM \"users\"\nD, [2024-12-24T17:18:51.169911 #328] DEBUG -- :   [My replica tag] User Count (0.9ms)  SELECT COUNT(*) FROM \"users\"\n=\u003e 3\n```\n\n## How it works\n\nThe proxy will analyze each SQL string, using pattern matching, to decide the appropriate connection for it (i.e. if it should go to the primary or replica).\n\n- All queries inside a transaction go to the primary\n- All `SET` queries go to all connections\n- All `INSERT`, `UPDATE` and `DELETE` queries go to the primary\n- All `SELECT FOR UPDATE` queries go to the primary\n- All `lock` queries (e.g `get_lock`) go the primary\n- All sequence methods (e.g `nextval`) go the primary\n- Everything else goes to the replica\n\n### TL;DR\n\nAll `SELECT` queries go to the _replica_, everything else goes to _primary_.\n\n## Stickiness context\n\nSimilar to Rails' built-in [automatic role switching](https://guides.rubyonrails.org/active_record_multiple_databases.html#activating-automatic-role-switching) Rack middleware, the proxy guarantes read-your-own-writes consistency by keeping a contextual timestamp for each Adapter Instance (a.k.a what you get when you call `Model.connection`).\n\nUntil `config.proxy_delay` time has been reached, all subsequent read requests _only for that connection_ will be rerouted to the primary. Once that has been reached, all following read requests will go the replica.\n\nAlthough the gem comes configured out of the box with `config.proxy_delay = 2.seconds`, it is your responsibility to find the proper number to use here, as that is very particular to each application and may be affected by many different factors (i.e. hardware, workload, availability, fault-tolerance, etc.). **Do not use this gem** if you don't have any replication delay metrics avaiable in your production APM. And make sure you have the proper alerts setup in case there's a spike in replication delay.\n\nOne strategy you can use to quickly disable the proxy is set your adapter using an environment variable:\n\n```yaml\n# config/database.yml\nproduction:\n  primary:\n    adapter: \u003c%= ENV.fetch(\"PRIMARY_DATABASE_ADAPTER\", \"postgresql\") %\u003e\n  primary_replica:\n    adapter: postgresql\n    replica: true\n```\nThen set `PRIMARY_DATABASE_ADAPTER=postgresql_proxy` to enable the proxy.\nThat way you can redeploy your application disabling the proxy completely, without any code change.\n\n### Sticking to a database manually\n\nThe proxy respects ActiveRecord's `#connected_to_stack` and will use it if present.\nYou can use that to force connection to the primary or replica and bypass the proxy entirely.\n\n```ruby\nUser.create(name: 'John Doe', email: 'john.doe@example.com')\nlast_user = User.last # This would normally go to the primary to adhere to read-your-own-writes consistency\nlast_user = ApplicationRecord.connected_to(role: :reading) { User.last } # but I can override it with this block\n```\n\nThis is useful when picking up a background job that could be impacted by replication delay.\n\n```ruby\n# app/models/application_record.rb\nclass ApplicationRecord \u003c ActiveRecord::Base\n  self.abstract_class = true\n\n  connects_to database: { writing: :primary, reading: :primary_replica }\nend\n\n# app/models/user.rb\nclass User \u003c ApplicationRecord\n  validates :name, :email, presence: true\n\n  after_commit :say_hello, on: :create\n\n  private\n\n  def say_hello\n    SayHelloJob.perform_later(id) # new row may not be replicated yet\n  end\nend\n\n# app/jobs/say_hello_job.rb\nclass SayHelloJob \u003c ApplicationJob\n  def perform(user_id)\n    # so we manually reroute it to the primary\n    user = ApplicationRecord.connected_to(role: :writing) { User.find(user_id) }\n\n    UserMailer.welcome(user).deliver_now\n  end\nend\n```\n\n## Caching Configuration\n\nActiveRecordProxyAdapters supports caching of SQL pattern matching results to improve performance for frequently executed queries.\n\n### Enabling Caching\n\nBy default, caching is disabled (using `NullStore`). To enable caching:\n\n```ruby\nActiveRecordProxyAdapters.configure do |config|\n  # Configure the cache store\n  config.cache do |cache|\n    # Use a specific cache implementation\n    # Notice that if using a Memcached or a Redis store, the network latency may outweigh the benefits you would get from caching the pattern matching\n    cache.store = ActiveSupport::Cache::MemoryStore.new(size: 64.megabytes)\n\n    # Optional: Customize the cache key prefix (default: \"arpa_\")\n    cache.key_prefix = \"custom_prefix_\"\n\n    # Optional: Customize the cache key generation (default: SHA2 hexdigest)\n    cache.key_builder = -\u003e(sql) { \"sql_#{Digest::MD5.hexdigest(sql)}\" }\n  end\nend\n```\n\n### How Caching Works\nThe caching system stores the results of SQL pattern matching operations to determine whether a query should be routed to a primary or replica database. This improves performance by avoiding repeated pattern matching on identical SQL strings.\n\n- Cache keys are generated using the configured `key_builder` (SHA2 digest by default).\n- All keys are prefixed with the configured `key_prefix` (\"arpa_\" by default).\n- Cache misses are instrumented with the `active_record_proxy_adapters.cache_miss` notification. They can be monitored by subscribing to that topic:\n  ```ruby\n  ActiveSupport::Notifications.subscribe(\"active_record_proxy_adapters.cache_miss\") do |event|\n    cache_key, sql = event[:payload].values_at(:cache_key, :sql)\n\n    logger.info(\"Cache miss for SQL: #{sql.inspect} with cache key: #{cache_key.inspec}\")\n  end\n  ```\n\n### Busting the cache\n\nIf you ever need to manually clear the cached SQL patterns:\n\n```ruby\n# This will clear all cached entries with the configured prefix\nActiveRecordProxyAdapters.bust_query_cache\n```\n\n### Performance Considerations\nFor applications with a high volume of repetitive queries, enabling caching can significantly reduce CPU overhead from SQL parsing. However, this comes with the tradeoff of increased memory usage in your cache store.\n\nFor optimal results:\n- Consider enabling prepared statements as that will increase cache hit rate, and decrease cache growth rate\n  ```ruby\n  irb(main):001\u003e (1..10).each { |i| User.where(id: i).exists? }\n  ```\n  _Without_ Prepared statements yields\n  ```\n  Cache miss for SQL: \"SELECT 1 AS one FROM \\\"users\\\" WHERE \\\"users\\\".\\\"id\\\" = 1 LIMIT 1\" with cache key: \"arpa_9fa3972e45b27985eef6bfb4aa6269c12d43363c60e7aa67fb290ec317503710\"\n  Cache miss for SQL: \"SELECT 1 AS one FROM \\\"users\\\" WHERE \\\"users\\\".\\\"id\\\" = 2 LIMIT 1\" with cache key: \"arpa_0e51756270138442ad26087dffcfb53c21df4a430961f1ca3b4270183f4b066d\"\n  Cache miss for SQL: \"SELECT 1 AS one FROM \\\"users\\\" WHERE \\\"users\\\".\\\"id\\\" = 3 LIMIT 1\" with cache key: \"arpa_db5b8c323ee2c284ba96adc6e20b7ea1373ca07fa9b09969f5207d467bd895b6\"\n  Cache miss for SQL: \"SELECT 1 AS one FROM \\\"users\\\" WHERE \\\"users\\\".\\\"id\\\" = 4 LIMIT 1\" with cache key: \"arpa_129459a1ba342cad3dbd4458cd8eacda4ed641a94a5d1e6cc23604495e44b565\"\n  Cache miss for SQL: \"SELECT 1 AS one FROM \\\"users\\\" WHERE \\\"users\\\".\\\"id\\\" = 5 LIMIT 1\" with cache key: \"arpa_9817a74a6f162ea110ed14cef79e95aa78830ff19266cdce75668e0c9c5ccef7\"\n  Cache miss for SQL: \"SELECT 1 AS one FROM \\\"users\\\" WHERE \\\"users\\\".\\\"id\\\" = 6 LIMIT 1\" with cache key: \"arpa_610e37a117abc81ec1afebafa0f36b35547f57879536ca7535475075ea08d8ac\"\n  Cache miss for SQL: \"SELECT 1 AS one FROM \\\"users\\\" WHERE \\\"users\\\".\\\"id\\\" = 7 LIMIT 1\" with cache key: \"arpa_79e172d168c59c4e5befbe954861ff9076000f955719dd3cca1423b68fb5f319\"\n  Cache miss for SQL: \"SELECT 1 AS one FROM \\\"users\\\" WHERE \\\"users\\\".\\\"id\\\" = 8 LIMIT 1\" with cache key: \"arpa_fb29367bdae3e2a48d1fa63cca00fd611c0b6dc84c9f5fd985b9222d49f1f7d9\"\n  Cache miss for SQL: \"SELECT 1 AS one FROM \\\"users\\\" WHERE \\\"users\\\".\\\"id\\\" = 9 LIMIT 1\" with cache key: \"arpa_e6e9a73cf9066077893b21dde038e5a616bf25731aeb5a4a9cdb41b7d84d1ece\"\n  Cache miss for SQL: \"SELECT 1 AS one FROM \\\"users\\\" WHERE \\\"users\\\".\\\"id\\\" = 10 LIMIT 1\" with cache key: \"arpa_8c94cdae65b6d529364d6ae8cf68f0e827566d471d2bd1107d6bdca29345759e\"\n  ```\n\n  _With_ prepared statments yields\n  ```\n  Cache miss for SQL: \"SELECT 1 AS one FROM \\\"users\\\" WHERE \\\"users\\\".\\\"id\\\" = $1 LIMIT $2\" with cache key: \"arpa_3c2ef2bb9a5f370adf63eac3bc9994c054554798d31d247818049c8c21cb68be\"\n  ```\n- Use a cache store with an appropriate size limit, and low latency (Memory Store has lower latency than Memcached or Redis)\n- Monitor cache hit/miss rates using the instrumentation events\n- Consider occasionally busting the cache during low-traffic periods to prevent stale entries, or setting a reasonable expiry window for cached values\n\n### Thread safety\n\nSince Rails already leases exactly one connection per thread from the pool and the adapter operates on that premise, it is safe to use it in multi-threaded servers such as Puma.\n\nAs long as you're not writing thread unsafe code that handles connections from the pool directly, or you don't have any other gem depenencies that write thread unsafe pool operations, you're all set.\n\nMulti-threaded queries example:\n```ruby\n# app/models/application_record.rb\nclass ApplicationRecord \u003c ActiveRecord::Base\n  self.abstract_class = true\n\n  connects_to database: { writing: :primary, reading: :primary_replica }\nend\n\n# app/models/portal.rb\nclass Portal \u003c ApplicationRecord\n  validates :name, uniqueness: true\nend\n\n# in rails console -e test\nActiveRecord::Base.logger.formatter = proc do |_severity, _time, _progname, msg|\n  \"[#{Time.current.iso8601} THREAD #{Thread.current[:name]}] #{msg}\\n\"\nend\n\nActiveRecordProxyAdapters.configure do |config|\n  config.proxy_delay = 2.seconds\nend\n\ndef read_your_own_writes\n  proc do\n    Portal.all.count # should go to the replica\n    Portal.create(name: 'Read your own write')\n\n    5.times do\n      Portal.all.count # first one goes the primary, last 4 should go to the replica\n      sleep(3)\n    end\n  end\nend\n\ndef use_replica\n  proc do\n    5.times do\n      Portal.all.count # should always go the replica\n      sleep(1.5)\n    end\n  end\nend\n\ndef executor\n  Rails.application.executor\nend\n\ndef test_multithread_queries\n  ActiveRecordProxyAdapters.configure do |config|\n    config.proxy_delay = 2.seconds\n    config.checkout_timeout = 2.seconds\n  end\n\n  t1 = Thread.new do\n    Thread.current[:name] = \"USE REPLICA\"\n    executor.wrap { ActiveRecord::Base.uncached { use_replica.call } }\n  end\n\n  t2 = Thread.new do\n    Thread.current[:name] = \"READ YOUR OWN WRITES\"\n    executor.wrap { ActiveRecord::Base.uncached { read_your_own_writes.call } }\n  end\n\n  [t1, t2].each(\u0026:join)\nend\n```\n\nYields:\n```bash\nirb(main):051:0\u003e test_multithread_queries\n[2024-12-24T13:52:40-05:00 THREAD USE REPLICA]   [PostgreSQL Replica] Portal Count (1.4ms)  SELECT COUNT(*) FROM \"portals\"\n[2024-12-24T13:52:40-05:00 THREAD READ YOUR OWN WRITES]   [PostgreSQL Replica] Portal Count (0.4ms)  SELECT COUNT(*) FROM \"portals\"\n[2024-12-24T13:52:40-05:00 THREAD READ YOUR OWN WRITES]   [PostgreSQLProxy Primary] TRANSACTION (0.5ms)  BEGIN\n[2024-12-24T13:52:40-05:00 THREAD READ YOUR OWN WRITES]   [PostgreSQLProxy Primary] Portal Exists? (0.4ms)  SELECT 1 AS one FROM \"portals\" WHERE \"portals\".\"name\" = $1 LIMIT $2  [[\"name\", \"Read your own write\"], [\"LIMIT\", 1]]\n[2024-12-24T13:52:40-05:00 THREAD READ YOUR OWN WRITES]   [PostgreSQLProxy Primary] Portal Create (0.8ms)  INSERT INTO \"portals\" (\"name\", \"created_at\", \"updated_at\") VALUES ($1, $2, $3) RETURNING \"id\"  [[\"name\", \"Read your own write\"], [\"created_at\", \"2024-12-24 18:52:40.428383\"], [\"updated_at\", \"2024-12-24 18:52:40.428383\"]]\n[2024-12-24T13:52:40-05:00 THREAD READ YOUR OWN WRITES]   [PostgreSQLProxy Primary] TRANSACTION (0.7ms)  COMMIT\n[2024-12-24T13:52:40-05:00 THREAD READ YOUR OWN WRITES]   [PostgreSQLProxy Primary] Portal Count (0.6ms)  SELECT COUNT(*) FROM \"portals\"\n[2024-12-24T13:52:41-05:00 THREAD USE REPLICA]   [PostgreSQL Replica] Portal Count (4.4ms)  SELECT COUNT(*) FROM \"portals\"\n[2024-12-24T13:52:43-05:00 THREAD USE REPLICA]   [PostgreSQL Replica] Portal Count (3.3ms)  SELECT COUNT(*) FROM \"portals\"\n[2024-12-24T13:52:43-05:00 THREAD READ YOUR OWN WRITES]   [PostgreSQL Replica] Portal Count (2.8ms)  SELECT COUNT(*) FROM \"portals\"\n[2024-12-24T13:52:44-05:00 THREAD USE REPLICA]   [PostgreSQL Replica] Portal Count (18.0ms)  SELECT COUNT(*) FROM \"portals\"\n[2024-12-24T13:52:46-05:00 THREAD USE REPLICA]   [PostgreSQL Replica] Portal Count (0.9ms)  SELECT COUNT(*) FROM \"portals\"\n[2024-12-24T13:52:46-05:00 THREAD READ YOUR OWN WRITES]   [PostgreSQL Replica] Portal Count (2.3ms)  SELECT COUNT(*) FROM \"portals\"\n[2024-12-24T13:52:49-05:00 THREAD READ YOUR OWN WRITES]   [PostgreSQL Replica] Portal Count (7.2ms)  SELECT COUNT(*) FROM \"portals\"\n[2024-12-24T13:52:52-05:00 THREAD READ YOUR OWN WRITES]   [PostgreSQL Replica] Portal Count (3.7ms)  SELECT COUNT(*) FROM \"portals\"\n=\u003e [#\u003cThread:0x00007fffdd6c9348 (irb):38 dead\u003e, #\u003cThread:0x00007fffdd6c9230 (irb):43 dead\u003e]\n```\n\n## Building your own proxy\n\nThese instructions assume an active record adapter `ActiveRecord::ConnectionAdapters::FoobarAdapter` already exists and is properly loaded in your environment.\n\nTo create a proxy adapter for an existing database `FoobarAdapter`, follow these steps under the lib folder of your rails application source code:\n\n1. **Create database tasks for your proxy adapter** to allow Rails tasks like `db:create` and `db:migrate` to work:\n\n   ```ruby\n   # lib/active_record/tasks/foobar_proxy_database_tasks.rb\n\n   require \"active_record_proxy_adapters/database_tasks\"\n\n   module ActiveRecord\n     module Tasks\n       class FoobarProxyDatabaseTasks \u003c FoobarDatabaseTasks\n         include ActiveRecordProxyAdapters::DatabaseTasks\n       end\n     end\n   end\n\n   ActiveRecord::Tasks::DatabaseTasks.register_task(\n     /foobar_proxy/,\n     \"ActiveRecord::Tasks::FoobarProxyDatabaseTasks\"\n   )\n   ```\n\n2. **Create the proxy implementation class** that will handle the routing logic:\n\n   ```ruby\n   # lib/active_record_proxy_adapters/foobar_proxy.rb\n\n   require \"active_record_proxy_adapters/primary_replica_proxy\"\n\n   module ActiveRecordProxyAdapters\n     class FoobarProxy \u003c PrimaryReplicaProxy\n       # Override or hijack extra methods here if you need custom behavior\n       # For most adapters, the default behavior works fine\n     end\n   end\n   ```\n\n3. **Create the proxy adapter class** that inherits from the underlying adapter, including the `Hijackable` concern. You need to require the database tasks source, the original adapter source, and the proxy source:\n\n   ```ruby\n   # lib/active_record/connection_adapters/foobar_proxy_adapter.rb\n\n   require \"active_record/tasks/foobar_proxy_database_tasks\"\n   require \"active_record/connection_adapters/foobar_adapter\"\n   require \"active_record_proxy_adapters/active_record_context\"\n   require \"active_record_proxy_adapters/hijackable\"\n   require \"active_record_proxy_adapters/foobar_proxy\"\n\n   module ActiveRecord\n     module ConnectionAdapters\n       class FoobarProxyAdapter \u003c FoobarAdapter\n         include ActiveRecordProxyAdapters::Hijackable\n\n         ADAPTER_NAME = \"FoobarProxy\" # This is only an ActiveRecord convention and is not required to work\n\n         delegate_to_proxy(*ActiveRecordProxyAdapters::ActiveRecordContext.hijackable_methods)\n\n         def initialize(...)\n           @proxy = ActiveRecordProxyAdapters::FoobarProxy.new(self)\n\n           super\n         end\n\n         private\n\n         attr_reader :proxy\n       end\n     end\n   end\n\n   # This is only required for Rails 7.2 or greater.\n   if ActiveRecordProxyAdapters::ActiveRecordContext.active_record_v7_2_or_greater?\n     ActiveRecord::ConnectionAdapters.register(\n       \"foobar_proxy\",\n       \"ActiveRecord::ConnectionAdapters::FoobarProxyAdapter\",\n       \"active_record/connection_adapters/foobar_proxy_adapter\"\n     )\n   end\n\n   ActiveSupport.run_load_hooks(:active_record_foobarproxyadapter,\n                                ActiveRecord::ConnectionAdapters::FoobarProxyAdapter)\n   ```\n\n4. **Create connection handling module** for ActiveRecord integration:\n\n   ```ruby\n   # lib/active_record_proxy_adapters/connection_handling/foobar.rb\n\n   begin\n     require \"active_record/connection_adapters/foobar_proxy_adapter\"\n   rescue LoadError\n     # foobar not available\n     return\n   end\n\n   # This is only required for Rails 7.1 or earlier.\n   module ActiveRecordProxyAdapters\n     module Foobar\n       module ConnectionHandling\n         def foobar_proxy_adapter_class\n           ActiveRecord::ConnectionAdapters::FoobarProxyAdapter\n         end\n\n         def foobar_proxy_connection(config)\n            # copy and paste the contents of the original method foobar_connection here.\n            # If the contents contain a hardcoded FooBarAdapter.new instance,\n            # replace it with foobar_proxy_adapter_class.new\n         end\n       end\n     end\n   end\n\n   ActiveSupport.on_load(:active_record) do\n     ActiveRecord::Base.extend(ActiveRecordProxyAdapters::Foobar::ConnectionHandling)\n   end\n   ```\n\n5. **In your initializer, load the custom adapter** when the parent adapter is fully loaded:\n\n   ```ruby\n   # config/initializers/active_record_proxy_adapters.rb\n\n   # The parent adapter should have a load hook already. If not, you might need to monkey patch it.\n   ActiveSupport.on_load(:active_record_foobaradapter) do\n     require \"active_record_proxy_adapters/connection_handling/foobar\"\n   end\n   ```\n\n6. **Add a custom Zeitwerk inflection rule** if your adapter file paths do not follow Rails conventions. You can skip this if it does:\n\n   ```ruby\n   # config/initializers/active_record_proxy_adapters.rb\n\n   Rails.autoloaders.each do |autoloader|\n     autoloader.inflector.inflect(\n       \"foobar_proxy_adapter\" =\u003e \"FoobarProxyAdapter\"\n     )\n   end\n   ```\n\n7. **Configure your database.yml** to use your new adapter:\n\n   ```yaml\n   development:\n     primary:\n       adapter: foobar_proxy\n       # primary database configuration\n\n     primary_replica:\n       adapter: foobar\n       replica: true\n       # replica database configuration\n   ```\n\n8. **Set up your model to use both connections**:\n\n   ```ruby\n   class ApplicationRecord \u003c ActiveRecord::Base\n     self.abstract_class = true\n     connects_to database: { writing: :primary, reading: :primary_replica }\n   end\n   ```\n\nFor testing your adapter, follow the examples in the test suite by creating spec files that match the pattern used for the other adapters.\n\n## Development\n\nAfter checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.\n\nTo install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).\n\n## Contributing\n\nBug reports and pull requests are welcome on GitHub at https://github.com/nasdaq/active_record_proxy_adapters. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/nasdaq/active_record_proxy_adapters/blob/main/CODE_OF_CONDUCT.md).\n\n## License\n\nThe gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).\n\n## Code of Conduct\n\nEveryone interacting in the ActiveRecordProxyAdapters project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/nasdaq/active_record_proxy_adapters/blob/main/CODE_OF_CONDUCT.md).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnasdaq%2Factive_record_proxy_adapters","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnasdaq%2Factive_record_proxy_adapters","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnasdaq%2Factive_record_proxy_adapters/lists"}