[
  {
    "path": ".github/workflows/integration_test.yml",
    "content": "name: Integration Test\n\non:\n  push:\n    branches:\n      - master\n  pull_request:\n    branches:\n      - master\n\nenv:\n  RUBY_VERSION: 3.3\n\njobs:\n  mysql:\n    continue-on-error: true\n    runs-on: ubuntu-latest\n    defaults:\n      run:\n        working-directory: ./integration_test\n    env:\n      BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}.gemfile\n      MYSQL_HOST: 127.0.0.1\n    strategy:\n      matrix:\n        gemfile:\n          - ar_6.1\n          - ar_7.0\n          - ar_7.1\n    steps:\n      - uses: actions/checkout@v4\n      - name: Start DB\n        run: docker compose up -d mysql\n      - name: Set up Ruby\n        uses: ruby/setup-ruby@v1\n        with:\n          ruby-version: ${{ env.RUBY_VERSION }}\n      - name: Run bundle install\n        run: bundle install\n      - name: Run integration test\n        run: bundle exec rspec spec/mysql2_spec.rb\n\n  postgresql:\n    continue-on-error: true\n    runs-on: ubuntu-latest\n    defaults:\n      run:\n        working-directory: ./integration_test\n    env:\n      BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}.gemfile\n      POSTGRES_HOST: 127.0.0.1\n    strategy:\n      matrix:\n        gemfile:\n          - ar_6.1\n          - ar_7.0\n          - ar_7.1\n    steps:\n      - uses: actions/checkout@v4\n      - name: Start DB\n        run: docker compose up -d postgres\n      - name: Set up Ruby\n        uses: ruby/setup-ruby@v1\n        with:\n          ruby-version: ${{ env.RUBY_VERSION }}\n      - name: Run bundle install\n        run: bundle install\n      - name: Run integration test\n        run: bundle exec rspec spec/postgresql_spec.rb\n"
  },
  {
    "path": ".github/workflows/integration_tests.yml",
    "content": "name: Integration tests\n\non:\n  push:\n    branches:\n      - main\n  pull_request:\n    branches:\n      - main\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\njobs:\n  integration_test:\n    runs-on: ubuntu-latest\n    timeout-minutes: 5\n\n    strategy:\n      matrix:\n        appraisal:\n          - ar-6.1\n          - ar-7.0\n          - ar-7.1\n          - ar-7.2\n          - ar-8.0\n    steps:\n      - uses: actions/checkout@v4\n      - name: docker compose up\n        run: docker compose -f compose-ci.yaml up -d\n      - name: Run integration test\n        run: docker compose -f compose-ci.yaml exec ruby bundle exec appraisal ${{ matrix.appraisal }} rspec spec/integration/*_spec.rb\n"
  },
  {
    "path": ".github/workflows/rubocop.yml",
    "content": "name: RuboCop\n\non:\n  push:\n    branches:\n      - main\n  pull_request:\n    branches:\n      - main\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\nenv:\n  RUBY_VERSION: 3.3\n\njobs:\n  rubocop:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Set up Ruby\n        uses: ruby/setup-ruby@v1\n        with:\n          ruby-version: ${{ env.RUBY_VERSION }}\n          bundler-cache: true\n\n      - name: Run RuboCop\n        run: bundle exec rubocop\n"
  },
  {
    "path": ".github/workflows/unit_tests.yml",
    "content": "name: Unit tests\n\non:\n  push:\n    branches:\n      - main\n  pull_request:\n    branches:\n      - main\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        ruby-version:\n          - 3.0\n          - 3.1\n          - 3.2\n          - 3.3\n\n    steps:\n    - uses: actions/checkout@v4\n    - name: Set up Ruby\n      uses: ruby/setup-ruby@v1\n      with:\n        ruby-version: ${{ matrix.ruby-version }}\n        bundler-cache: true\n    - name: Run unit tests on ruby-${{ matrix.ruby-version }}\n      run: bundle exec rspec spec/unit/*_spec.rb\n"
  },
  {
    "path": ".gitignore",
    "content": "*.swp\n*.gem\nGemfile.lock\n*.gemfile.lock\n.bundle/\ntmp/\ndb/mysql/data/*\n"
  },
  {
    "path": ".rspec",
    "content": "--color\n-Ispec/lib\n"
  },
  {
    "path": ".rubocop.yml",
    "content": "---\n# Referenced from https://github.com/rails/rails/blob/main/.rubocop.yml\n\nrequire:\n  - rubocop-md\n\nAllCops:\n  TargetRubyVersion: 3.3\n  DisabledByDefault: true\n  SuggestExtensions: false\n  Exclude:\n    - '**/tmp/**/*'\n    - '**/*.gemfile'\n    - 'vendor/**/*'\n\n# Prefer &&/|| over and/or.\nStyle/AndOr:\n  Enabled: true\n\nLayout/ClosingHeredocIndentation:\n  Enabled: true\n\nLayout/ClosingParenthesisIndentation:\n  Enabled: true\n\n# Align comments with method definitions.\nLayout/CommentIndentation:\n  Enabled: true\n\nLayout/DefEndAlignment:\n  Enabled: true\n\nLayout/ElseAlignment:\n  Enabled: true\n\n# Align `end` with the matching keyword or starting expression except for\n# assignments, where it should be aligned with the LHS.\nLayout/EndAlignment:\n  Enabled: true\n  EnforcedStyleAlignWith: variable\n  AutoCorrect: true\n\nLayout/EndOfLine:\n  Enabled: true\n\nLayout/EmptyLineAfterMagicComment:\n  Enabled: true\n\nLayout/EmptyLinesAroundAccessModifier:\n  Enabled: true\n  EnforcedStyle: around\n\nLayout/EmptyLinesAroundBlockBody:\n  Enabled: true\n\n# In a regular class definition, no empty lines around the body.\nLayout/EmptyLinesAroundClassBody:\n  Enabled: true\n\n# In a regular method definition, no empty lines around the body.\nLayout/EmptyLinesAroundMethodBody:\n  Enabled: true\n\n# In a regular module definition, no empty lines around the body.\nLayout/EmptyLinesAroundModuleBody:\n  Enabled: true\n\n# Use Ruby >= 1.9 syntax for hashes. Prefer { a: :b } over { :a => :b }.\nStyle/HashSyntax:\n  Enabled: true\n  EnforcedShorthandSyntax: either\n\n# Method definitions after `private` or `protected` isolated calls need one\n# extra level of indentation.\nLayout/IndentationConsistency:\n  Enabled: true\n  EnforcedStyle: indented_internal_methods\n  Exclude:\n    - '**/*.md'\n\n# Two spaces, no tabs (for indentation).\nLayout/IndentationWidth:\n  Enabled: true\n\nLayout/LeadingCommentSpace:\n  Enabled: true\n\nLayout/SpaceAfterColon:\n  Enabled: true\n\nLayout/SpaceAfterComma:\n  Enabled: true\n\nLayout/SpaceAfterSemicolon:\n  Enabled: true\n\nLayout/SpaceAroundEqualsInParameterDefault:\n  Enabled: false\n\nLayout/SpaceAroundKeyword:\n  Enabled: true\n\nLayout/SpaceAroundOperators:\n  Enabled: true\n\nLayout/SpaceBeforeComma:\n  Enabled: true\n\nLayout/SpaceBeforeComment:\n  Enabled: true\n\nLayout/SpaceBeforeFirstArg:\n  Enabled: true\n\nStyle/DefWithParentheses:\n  Enabled: true\n\n# Defining a method with parameters needs parentheses.\nStyle/MethodDefParentheses:\n  Enabled: true\n\nStyle/ExplicitBlockArgument:\n  Enabled: true\n\nStyle/MapToHash:\n  Enabled: true\n\nStyle/RedundantFreeze:\n  Enabled: true\n\n# Use `foo {}` not `foo{}`.\nLayout/SpaceBeforeBlockBraces:\n  Enabled: true\n\n# Use `foo { bar }` not `foo {bar}`.\nLayout/SpaceInsideBlockBraces:\n  Enabled: true\n  EnforcedStyleForEmptyBraces: space\n\n# Use `{ a: 1 }` not `{a:1}`.\nLayout/SpaceInsideHashLiteralBraces:\n  Enabled: true\n\nLayout/SpaceInsideParens:\n  Enabled: true\n\n# Check quotes usage according to lint rule below.\nStyle/StringLiterals:\n  Enabled: true\n\n# Detect hard tabs, no hard tabs.\nLayout/IndentationStyle:\n  Enabled: true\n\n# Empty lines should not have any spaces.\nLayout/TrailingEmptyLines:\n  Enabled: true\n\n# No trailing whitespace.\nLayout/TrailingWhitespace:\n  Enabled: true\n# Use quotes for string literals when they are enough.\nStyle/RedundantPercentQ:\n  Enabled: true\n\nLint/AmbiguousOperator:\n  Enabled: true\n\nLint/AmbiguousRegexpLiteral:\n  Enabled: true\n\nLint/Debugger:\n  Enabled: true\n  DebuggerRequires:\n    - debug\n\nLint/DuplicateRequire:\n  Enabled: true\n\nLint/DuplicateMagicComment:\n  Enabled: true\n\nLint/DuplicateMethods:\n  Enabled: true\n\nLint/ErbNewArguments:\n  Enabled: true\n\nLint/EnsureReturn:\n  Enabled: true\n\nLint/MissingCopEnableDirective:\n  Enabled: true\n\n# Use my_method(my_arg) not my_method( my_arg ) or my_method my_arg.\nLint/RequireParentheses:\n  Enabled: true\n\nLint/RedundantCopDisableDirective:\n  Enabled: true\n\nLint/RedundantCopEnableDirective:\n  Enabled: true\n\nLint/RedundantRequireStatement:\n  Enabled: true\n\nLint/RedundantStringCoercion:\n  Enabled: true\n\nLint/RedundantSafeNavigation:\n  Enabled: true\n\nLint/UriEscapeUnescape:\n  Enabled: true\n\nLint/UselessAssignment:\n  Enabled: true\n\nLint/DeprecatedClassMethods:\n  Enabled: true\n\nLint/InterpolationCheck:\n  Enabled: true\n\nLint/SafeNavigationChain:\n  Enabled: true\n\nStyle/EvalWithLocation:\n  Enabled: false\n\nStyle/ParenthesesAroundCondition:\n  Enabled: true\n\nStyle/HashTransformKeys:\n  Enabled: true\n\nStyle/HashTransformValues:\n  Enabled: true\n\nStyle/RedundantBegin:\n  Enabled: true\n\nStyle/RedundantReturn:\n  Enabled: true\n  AllowMultipleReturnValues: true\n\nStyle/RedundantRegexpEscape:\n  Enabled: true\n\nStyle/Semicolon:\n  Enabled: true\n  AllowAsExpressionSeparator: true\n\n# Prefer Foo.method over Foo::method\nStyle/ColonMethodCall:\n  Enabled: true\n\nStyle/TrivialAccessors:\n  Enabled: true\n\n# Prefer a = b || c over a = b ? b : c\nStyle/RedundantCondition:\n  Enabled: true\n\nStyle/RedundantDoubleSplatHashBraces:\n  Enabled: true\n\nStyle/OpenStructUse:\n  Enabled: true\n\nStyle/ArrayIntersect:\n  Enabled: true\n\nMarkdown:\n  # Whether to run RuboCop against non-valid snippets\n  WarnInvalid: true\n  # Whether to lint codeblocks without code attributes\n  Autodetect: false\n"
  },
  {
    "path": "Appraisals",
    "content": "appraise 'ar-6.1' do\n  gem 'activerecord', '~> 6.1.0'\n  gem 'mysql2'\n  gem 'pg'\n  gem 'activerecord-sqlserver-adapter'\n  gem 'trilogy'\n  gem 'sqlite3', '~> 1.4'\n\n  # required to suppress warnings\n  gem 'bigdecimal'\n  gem 'base64'\n  gem 'mutex_m'\nend\n\nappraise 'ar-7.0' do\n  gem 'activerecord', '~> 7.0.0'\n  gem 'mysql2'\n  gem 'pg'\n  gem 'activerecord-sqlserver-adapter'\n  gem 'trilogy'\n  gem 'sqlite3', '~> 1.4'\n\n  # required to suppress warnings\n  gem 'bigdecimal'\n  gem 'base64'\n  gem 'mutex_m'\nend\n\nappraise 'ar-7.1' do\n  gem 'activerecord', '~> 7.1.0'\n  gem 'mysql2'\n  gem 'pg'\n  gem 'activerecord-sqlserver-adapter'\n  gem 'trilogy'\n  gem 'sqlite3', '~> 2.0'\nend\n\nappraise 'ar-7.2' do\n  gem 'activerecord', '~> 7.2.0'\n  gem 'mysql2'\n  gem 'pg'\n  gem 'activerecord-sqlserver-adapter'\n  gem 'trilogy'\n  gem 'sqlite3', '~> 2.0'\nend\n\nappraise 'ar-8.0' do\n  gem 'activerecord', '~> 8.0.0'\n  gem 'mysql2'\n  gem 'pg'\n  gem 'activerecord-sqlserver-adapter'\n  gem 'trilogy'\n  gem 'sqlite3', '~> 2.1'\nend\n"
  },
  {
    "path": "ChangeLog.md",
    "content": "# Change Log\n## 1.0.0\n* Added support for ActiveRecord 7.1.\n* Redesigned the proxy chain to accommodate internal structure changes in ActiveRecord 7.1.\n* Introduced integration tests using real databases, allowing for more robust testing of functionality with MySQL, PostgreSQL, SQLite, and SQLServer.\n  See: https://github.com/cookpad/arproxy/issues/30\n\n## 0.2.9\n* Support ActiveRecord 7.0 (#21)\n  Thanks to @r7kamura\n\n## 0.2.8\n* Support postgresql adapter (#19)\n  Thanks to @jhnvz\n\n## 0.2.7\n* Support sqlserver adapter (#16)\n  Note that it supports `AR::B.connection.execute` but not `exec_query` yet.\n  See: https://github.com/cookpad/arproxy/pull/16\n  Thanks to @takanamito\n\n## 0.2.6\n* Support sqlite3 adapter (#15)\n  Thanks to @hakatashi\n\n## 0.2.5\n* Fix against warnings around `::` in void context (#12)\n\n## 0.2.4\n* Fix against warnings around uninitialized instance variables (#12)\n  Thanks to @amatsuda\n\n## 0.2.3\n* Set Arproxy::Config#adapter from database.yml automatically (#11)\n  Thanks to @k0kubun\n\n## 0.2.2\n* Start supporting activerecord-5.0 and stop 3.2-4.1\n\n## 0.2.1\n* Make ProxyChain thread-safe (#7)\n  Thanks to @saidie\n\n## 0.2.0\n* Arproxy plugin: an easy way to make reusable proxies as gems (#6)\n  Thanks to @winebarrel\n\n## 0.1.3\n* Silence some deprecation warnings (#1)\n  Thanks to @amatsuda\n\n* Implement Arproxy.#enable? and Arproxy.#reenable!\n\n## 0.1.2\n* Bug fix: An error occoured when call disable! after disable!\n\n* config.adapter accepts not only String but also Class\n\n## 0.1.1\n* First Release\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM ruby:3.3\n\nWORKDIR /app\n\nCOPY lib lib\nCOPY spec spec\nCOPY gemfiles gemfiles\n\nCOPY arproxy.gemspec arproxy.gemspec\nCOPY Gemfile Gemfile\nCOPY Appraisals Appraisals\nCOPY .env .env\nCOPY .rspec .rspec\n\nRUN apt update\nRUN apt install --no-install-recommends -y build-essential freetds-dev\n\nRUN bundle install\nRUN bundle exec appraisal install\n\nRUN mkdir -p /app/db/mysql\nRUN ln -s /var/lib/mysql /app/db/mysql/data\n\n# dummy command to keep the container running\nCMD [\"sleep\", \"infinity\"]\n"
  },
  {
    "path": "Gemfile",
    "content": "source 'https://rubygems.org'\n\ngemspec\n\ngem 'rspec'\ngem 'appraisal'\ngem 'dotenv', require: 'dotenv/load'\ngem 'rubocop'\ngem 'rubocop-md'\n"
  },
  {
    "path": "LICENSE.txt",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2016 Issei Naruta\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "[![Integration tests](https://github.com/cookpad/arproxy/actions/workflows/integration_tests.yml/badge.svg)](https://github.com/cookpad/arproxy/actions/workflows/integration_tests.yml)\n[![Unit tests](https://github.com/cookpad/arproxy/actions/workflows/unit_tests.yml/badge.svg)](https://github.com/cookpad/arproxy/actions/workflows/unit_tests.yml)\n[![Rubocop](https://github.com/cookpad/arproxy/actions/workflows/rubocop.yml/badge.svg)](https://github.com/cookpad/arproxy/actions/workflows/rubocop.yml)\n\n# Arproxy\nArproxy is a library that can intercept SQL queries executed by ActiveRecord to log them or modify the queries themselves.\n\n# Getting Started\nCreate your custom proxy and add its configuration in your Rails' `config/initializers/` directory:\n\n```ruby\nclass QueryTracer < Arproxy::Proxy\n  def execute(sql, context)\n    Rails.logger.debug sql\n    Rails.logger.debug caller(1).join(\"\\n\")\n    super(sql, context)\n  end\nend\n\nArproxy.configure do |config|\n  config.adapter = 'mysql2' # A DB Adapter name which is used in your database.yml\n  config.use QueryTracer\nend\nArproxy.enable!\n```\n\nThen you can see the backtrace of SQLs in the Rails' log.\n\n```ruby\n# In your Rails code\nMyTable.where(id: id).limit(1) # => The SQL and the backtrace appear in the log\n```\n\n## What the `context` argument is\n\n`context` is an instance of `Arproxy::QueryContext` and contains values that are passed from Arproxy to the Database Adapter.\n`context` is a set of values used when calling Database Adapter methods, and you don't need to use the `context` values directly.\nHowever, you must always pass `context` to `super` like `super(sql, context)`.\n\nFor example, let's look at the Mysql2Adapter implementation. When executing a query in Mysql2Adapter, the `Mysql2Adapter#internal_exec_query` method is called internally.\n\n```\n# https://github.com/rails/rails/blob/v7.1.0/activerecord/lib/active_record/connection_adapters/mysql2/database_statements.rb#L21\ndef internal_exec_query(sql, name = \"SQL\", binds = [], prepare: false, async: false) # :nodoc:\n  # ...\nend\n```\n\nIn Arproxy, this method is called at the end of the `Arproxy::Proxy#execute` method chain, and at this time `context` contains the arguments to be passed to `#internal_exec_query`:\n\n| member           | example value                      |\n|------------------|------------------------------------|\n| `context.name`   | `\"SQL\"`                            |\n| `context.binds`  | `[]`                               |\n| `context.kwargs` | `{ prepare: false, async: false }` |\n\nYou can modify the values of `context` in the proxy, but do so after understanding the implementation of the Database Adapter.\n\n### `context.name`\n\nIn the Rails' log you may see queries like this:\n\n```\nUser Load (22.6ms)  SELECT `users`.* FROM `users` WHERE `users`.`name` = 'Issei Naruta'\n```\n\nThen `\"User Load\"` is the `context.name`.\n\n# Architecture\nWithout Arproxy:\n\n```\n+-------------------------+        +------------------+\n| ActiveRecord::Base#find |--SQL-->| Database Adapter |\n+-------------------------+        +------------------+\n```\n\nWith Arproxy:\n\n```ruby\nArproxy.configure do |config|\n  config.adapter = 'mysql2'\n  config.use MyProxy1\n  config.use MyProxy2\nend\n```\n\n```\n+-------------------------+        +----------+   +----------+   +------------------+\n| ActiveRecord::Base#find |--SQL-->| MyProxy1 |-->| MyProxy2 |-->| Database Adapter |\n+-------------------------+        +----------+   +----------+   +------------------+\n```\n\n# Supported Environments\n\nArproxy supports the following databases and adapters:\n\n- MySQL\n  - `mysql2`, `trilogy`\n- PostgreSQL\n  - `pg`\n- SQLite\n  - `sqlite3`\n- SQLServer\n  - `activerecord-sqlserver-adapter`\n\nWe have tested with the following versions of Ruby, ActiveRecord, and databases:\n\n- Ruby\n  - `3.0`, `3.1`, `3.2`, `3.3`\n- ActiveRecord\n  - `6.1`, `7.0`, `7.1`, `7.2`, `8.0`\n- MySQL\n  - `9.0`\n- PostgreSQL\n  - `17`\n- SQLite\n  - `3.x` (not specified)\n- SQLServer\n  - `2022`\n\n# Examples\n\n## Adding Comments to SQLs\n\n```ruby\nclass CommentAdder < Arproxy::Proxy\n  def execute(sql, context)\n    sql += ' /*this_is_comment*/'\n    super(sql, context)\n  end\nend\n```\n\n## Slow Query Logger\n\n```ruby\nclass SlowQueryLogger < Arproxy::Proxy\n  def initialize(slow_ms)\n    @slow_ms = slow_ms\n  end\n\n  def execute(sql, context)\n    result = nil\n    ms = Benchmark.ms { result = super(sql, context) }\n    if ms >= @slow_ms\n      Rails.logger.info \"Slow(#{ms.to_i}ms): #{sql}\"\n    end\n    result\n  end\nend\n\nArproxy.configure do |config|\n  config.use SlowQueryLogger, 1000\nend\n```\n\n## Readonly Access\n\nIf you don't call `super` in the proxy, you can block the query execution.\n\n```ruby\nclass Readonly < Arproxy::Proxy\n  def execute(sql, context)\n    if sql =~ /^(SELECT|SET|SHOW|DESCRIBE)\\b/\n      super(sql, context)\n    else\n      Rails.logger.warn \"#{context.name} (BLOCKED) #{sql}\"\n      nil\n    end\n  end\nend\n```\n\n# Use plug-in\n\n```ruby\n# any_gem/lib/arproxy/plugin/my_plugin\nmodule Arproxy::Plugin\n  class MyPlugin < Arproxy::Proxy\n    Arproxy::Plugin.register(:my_plugin, self)\n\n    def execute(sql, context)\n      # Any processing\n      # ...\n      super(sql, context)\n    end\n  end\nend\n```\n\n```ruby\nArproxy.configure do |config|\n  config.plugin :my_plugin\nend\n```\n\n# Upgrading guide from v0.x to v1\n\nSee [UPGRADING.md](UPGRADING.md)\n\n# Development\n\n## Setup\n\n```\n$ git clone https://github.com/cookpad/arproxy.git\n$ cd arproxy\n$ bundle install\n$ bundle exec appraisal install\n```\n\n## Run test\n\nTo run all tests with all supported versions of ActiveRecord:\n\n```\n$ docker compose up -d\n$ bundle exec appraisal rspec\n```\n\nTo run tests for a specific version of ActiveRecord:\n\n```\n$ bundle exec appraisal ar_7.1 rspec\nor\n$ BUNDLE_GEMFILE=gemfiles/ar_7.1.gemfile bundle exec rspec\n```\n\n# License\nArproxy is released under the MIT license:\n* www.opensource.org/licenses/MIT\n"
  },
  {
    "path": "Rakefile",
    "content": "require 'bundler/gem_tasks'\nrequire 'rspec/core/rake_task'\nrequire 'rubocop/rake_task'\n\nRSpec::Core::RakeTask.new(:spec)\n\nRuboCop::RakeTask.new(:rubocop)\n\ntask default: [:spec, :rubocop]\n"
  },
  {
    "path": "UPGRADING.md",
    "content": "# Upgrading guide from v0.x to v1\n\nThe proxy specification has changed from v0.x to v1 and is not backward compatible.\nThe base class for proxies has changed from `Arproxy::Base` to `Arproxy::Proxy`.\nAlso, the arguments to `#execute` have changed from `sql, name=nil, **kwargs` to `sql, context`.\n\n```ruby\n# ~> v0.2.9\nclass MyProxy < Arproxy::Base\n  def execute(sql, name=nil, **kwargs)\n    super\n  end\nend\n\n# >= v1.0.0\nclass MyProxy < Arproxy::Proxy\n  def execute(sql, context)\n    super\n  end\nend\n```\n\nThere are no other backward incompatible changes besides the above changes in proxy base class and arguments.\n"
  },
  {
    "path": "arproxy.gemspec",
    "content": "$:.push File.expand_path('../lib', __FILE__)\nrequire 'arproxy/version'\n\nGem::Specification.new do |spec|\n  spec.name              = 'arproxy'\n  spec.version           = Arproxy::VERSION\n  spec.summary           = 'A proxy layer between ActiveRecord and database adapters'\n  spec.description       = 'Arproxy is a proxy layer that allows hooking into ActiveRecord query execution and injecting custom processing'\n  spec.files             = Dir.glob('lib/**/*.rb')\n  spec.author            = 'Issei Naruta'\n  spec.email             = 'mimitako@gmail.com'\n  spec.homepage          = 'https://github.com/cookpad/arproxy'\n  spec.license           = 'MIT'\n  spec.require_paths     = ['lib']\n\n  spec.add_dependency 'activerecord', '>= 6.1'\nend\n"
  },
  {
    "path": "compose-ci.yaml",
    "content": "services:\n  ruby:\n    build:\n      context: .\n      dockerfile: Dockerfile\n    environment:\n      MYSQL_HOST: mysql\n      MYSQL_PORT: 3306\n      POSTGRES_HOST: postgres\n      POSTGRES_PORT: 5432\n      MSSQL_HOST: sqlserver\n      MSSQL_PORT: 1433\n    volumes:\n      - mysql-data:/var/lib/mysql\n\n  mysql:\n    image: mysql:9.0\n    restart: always\n    environment:\n      MYSQL_ROOT_PASSWORD: rootpassword\n      MYSQL_DATABASE: ${ARPROXY_DB_DATABASE}\n      MYSQL_USER: ${ARPROXY_DB_USER}\n      MYSQL_PASSWORD: ${ARPROXY_DB_PASSWORD}\n    volumes:\n      - ./db/mysql/my.cnf:/etc/mysql/conf.d/my.cnf\n      - mysql-data:/var/lib/mysql\n    ports:\n      - \"23306:3306\"\n\n  postgres:\n    image: postgres:17\n    restart: always\n    environment:\n      POSTGRES_DB: ${ARPROXY_DB_DATABASE}\n      POSTGRES_USER: ${ARPROXY_DB_USER}\n      POSTGRES_PASSWORD: ${ARPROXY_DB_PASSWORD}\n    ports:\n      - \"25432:5432\"\n\n  sqlserver:\n    image: mcr.microsoft.com/mssql/server:2022-latest\n    restart: always\n    environment:\n      ACCEPT_EULA: Y\n      MSSQL_SA_PASSWORD: R00tPassword12!\n    ports:\n      - \"21433:1433\"\n    healthcheck:\n      test: [\"CMD-SHELL\", \"/opt/mssql-tools18/bin/sqlcmd -C -S localhost -U sa -P R00tPassword12! -Q 'SELECT 1' || exit 1\"]\n      interval: 5s\n      retries: 10\n      start_period: 10s\n\n  sqlserver-init:\n    image: mcr.microsoft.com/mssql/server:2022-latest\n    volumes:\n      - ./db/sqlserver/init.sql:/init.sql\n    command: /opt/mssql-tools18/bin/sqlcmd -C -S sqlserver -U sa -P R00tPassword12! -d master -i /init.sql\n    depends_on:\n      sqlserver:\n        condition: service_healthy\n\nvolumes:\n  mysql-data:\n"
  },
  {
    "path": "compose.yaml",
    "content": "services:\n  mysql:\n    image: mysql:9.0\n    restart: always\n    environment:\n      MYSQL_ROOT_PASSWORD: rootpassword\n      MYSQL_DATABASE: ${ARPROXY_DB_DATABASE}\n      MYSQL_USER: ${ARPROXY_DB_USER}\n      MYSQL_PASSWORD: ${ARPROXY_DB_PASSWORD}\n    volumes:\n      - ./db/mysql/my.cnf:/etc/mysql/conf.d/my.cnf\n      - ./db/mysql/data:/var/lib/mysql\n    ports:\n      - \"23306:3306\"\n\n  postgres:\n    image: postgres:17\n    restart: always\n    environment:\n      POSTGRES_DB: ${ARPROXY_DB_DATABASE}\n      POSTGRES_USER: ${ARPROXY_DB_USER}\n      POSTGRES_PASSWORD: ${ARPROXY_DB_PASSWORD}\n    ports:\n      - \"25432:5432\"\n\n  sqlserver:\n    image: mcr.microsoft.com/mssql/server:2022-latest\n    restart: always\n    environment:\n      ACCEPT_EULA: Y\n      MSSQL_SA_PASSWORD: R00tPassword12!\n    ports:\n      - \"21433:1433\"\n    healthcheck:\n      test: [\"CMD-SHELL\", \"/opt/mssql-tools18/bin/sqlcmd -C -S localhost -U sa -P R00tPassword12! -Q 'SELECT 1' || exit 1\"]\n      interval: 5s\n      retries: 10\n      start_period: 10s\n\n  sqlserver-init:\n    image: mcr.microsoft.com/mssql/server:2022-latest\n    volumes:\n      - ./db/sqlserver/init.sql:/init.sql\n    command: /opt/mssql-tools18/bin/sqlcmd -C -S sqlserver -U sa -P R00tPassword12! -d master -i /init.sql\n    depends_on:\n      sqlserver:\n        condition: service_healthy\n"
  },
  {
    "path": "db/mysql/my.cnf",
    "content": "[mysqld]\ntls-version=TLSv1.2,TLSv1.3\nauto_generate_certs = ON\n"
  },
  {
    "path": "db/sqlserver/init.sql",
    "content": "CREATE DATABASE arproxy_test;\nGO\nUSE arproxy_test;\nGO\nCREATE LOGIN arproxy WITH PASSWORD = '4rpr0*y#2024';\nGO\nCREATE USER arproxy FOR LOGIN arproxy;\nGO\nALTER ROLE db_owner ADD MEMBER arproxy;\nGO\n"
  },
  {
    "path": "gemfiles/ar_6.1.gemfile",
    "content": "# This file was generated by Appraisal\n\nsource \"https://rubygems.org\"\n\ngem \"rspec\"\ngem \"appraisal\"\ngem \"dotenv\", require: \"dotenv/load\"\ngem \"rubocop\"\ngem \"rubocop-md\"\ngem \"activerecord\", \"~> 6.1.0\"\ngem \"mysql2\"\ngem \"pg\"\ngem \"activerecord-sqlserver-adapter\"\ngem \"trilogy\"\ngem \"sqlite3\", \"~> 1.4\"\ngem \"bigdecimal\"\ngem \"base64\"\ngem \"mutex_m\"\n\ngemspec path: \"../\"\n"
  },
  {
    "path": "gemfiles/ar_7.0.gemfile",
    "content": "# This file was generated by Appraisal\n\nsource \"https://rubygems.org\"\n\ngem \"rspec\"\ngem \"appraisal\"\ngem \"dotenv\", require: \"dotenv/load\"\ngem \"rubocop\"\ngem \"rubocop-md\"\ngem \"activerecord\", \"~> 7.0.0\"\ngem \"mysql2\"\ngem \"pg\"\ngem \"activerecord-sqlserver-adapter\"\ngem \"trilogy\"\ngem \"sqlite3\", \"~> 1.4\"\ngem \"bigdecimal\"\ngem \"base64\"\ngem \"mutex_m\"\n\ngemspec path: \"../\"\n"
  },
  {
    "path": "gemfiles/ar_7.1.gemfile",
    "content": "# This file was generated by Appraisal\n\nsource \"https://rubygems.org\"\n\ngem \"rspec\"\ngem \"appraisal\"\ngem \"dotenv\", require: \"dotenv/load\"\ngem \"rubocop\"\ngem \"rubocop-md\"\ngem \"activerecord\", \"~> 7.1.0\"\ngem \"mysql2\"\ngem \"pg\"\ngem \"activerecord-sqlserver-adapter\"\ngem \"trilogy\"\ngem \"sqlite3\", \"~> 2.0\"\n\ngemspec path: \"../\"\n"
  },
  {
    "path": "gemfiles/ar_7.2.gemfile",
    "content": "# This file was generated by Appraisal\n\nsource \"https://rubygems.org\"\n\ngem \"rspec\"\ngem \"appraisal\"\ngem \"dotenv\", require: \"dotenv/load\"\ngem \"rubocop\"\ngem \"rubocop-md\"\ngem \"activerecord\", \"~> 7.2.0\"\ngem \"mysql2\"\ngem \"pg\"\ngem \"activerecord-sqlserver-adapter\"\ngem \"trilogy\"\ngem \"sqlite3\", \"~> 2.0\"\n\ngemspec path: \"../\"\n"
  },
  {
    "path": "gemfiles/ar_8.0.gemfile",
    "content": "# This file was generated by Appraisal\n\nsource \"https://rubygems.org\"\n\ngem \"rspec\"\ngem \"appraisal\"\ngem \"dotenv\", require: \"dotenv/load\"\ngem \"rubocop\"\ngem \"rubocop-md\"\ngem \"activerecord\", \"~> 8.0.0\"\ngem \"mysql2\"\ngem \"pg\"\ngem \"activerecord-sqlserver-adapter\"\ngem \"trilogy\"\ngem \"sqlite3\", \"~> 2.1\"\n\ngemspec path: \"../\"\n"
  },
  {
    "path": "integration_test/Gemfile",
    "content": "source 'https://rubygems.org'\n\ngem 'arproxy', path: '..'\ngem 'rspec'\ngem 'appraisal'\ngem 'mysql2'\ngem 'pg'\n"
  },
  {
    "path": "integration_test/docker-compose.yml",
    "content": "version: '3'\n\nservices:\n  mysql:\n    image: mysql:9.0\n    restart: always\n    environment:\n      MYSQL_ROOT_PASSWORD: rootpassword\n      MYSQL_DATABASE: arproxy_test\n      MYSQL_USER: arproxy\n      MYSQL_PASSWORD: password\n    ports:\n      - \"23306:3306\"\n\n  postgres:\n    image: postgres:16\n    restart: always\n    environment:\n      POSTGRES_DB: arproxy_test\n      POSTGRES_USER: arproxy\n      POSTGRES_PASSWORD: password\n    ports:\n      - \"25432:5432\"\n"
  },
  {
    "path": "integration_test/gemfiles/ar_6.1.gemfile",
    "content": "# This file was generated by Appraisal\n\nsource \"https://rubygems.org\"\n\ngem \"arproxy\", path: \"../..\"\ngem \"rspec\"\ngem \"appraisal\"\ngem \"mysql2\"\ngem \"pg\"\ngem \"activerecord\", \"~> 6.1.0\"\n"
  },
  {
    "path": "integration_test/gemfiles/ar_7.0.gemfile",
    "content": "# This file was generated by Appraisal\n\nsource \"https://rubygems.org\"\n\ngem \"arproxy\", path: \"../..\"\ngem \"rspec\"\ngem \"appraisal\"\ngem \"mysql2\"\ngem \"pg\"\ngem \"activerecord\", \"~> 7.0.0\"\n"
  },
  {
    "path": "integration_test/gemfiles/ar_7.1.gemfile",
    "content": "# This file was generated by Appraisal\n\nsource \"https://rubygems.org\"\n\ngem \"arproxy\", path: \"../..\"\ngem \"rspec\"\ngem \"appraisal\"\ngem \"mysql2\"\ngem \"pg\"\ngem \"activerecord\", \"~> 7.1.0\"\n"
  },
  {
    "path": "integration_test/spec/mysql2_spec.rb",
    "content": "require_relative 'spec_helper'\nrequire 'mysql2'\n\ncontext 'MySQL' do\n  before(:all) do\n    ActiveRecord::Base.establish_connection(\n      adapter: 'mysql2',\n      host: ENV.fetch('MYSQL_HOST', '127.0.0.1'),\n      port: ENV.fetch('MYSQL_PORT', '23306').to_i,\n      database: 'arproxy_test',\n      username: 'arproxy',\n      password: 'password'\n    )\n\n    Arproxy.configure do |config|\n      config.adapter = 'mysql2'\n      config.use HelloProxy\n      config.use QueryLogger\n    end\n    Arproxy.enable!\n\n    ActiveRecord::Base.connection.create_table :products, force: true do |t|\n      t.string :name\n      t.integer :price\n    end\n\n    Product.create(name: 'apple', price: 100)\n    Product.create(name: 'banana', price: 200)\n    Product.create(name: 'orange', price: 300)\n  end\n\n  after(:all) do\n    ActiveRecord::Base.connection.drop_table :products\n    ActiveRecord::Base.connection.close\n    Arproxy.disable!\n  end\n\n  before(:each) do\n    QueryLogger.reset!\n  end\n\n  it do\n    expect(QueryLogger.log.size).to eq(0)\n\n    expect(Product.count).to eq(3)\n    expect(Product.first.name).to eq('apple')\n\n    expect(QueryLogger.log.size).to eq(2)\n    expect(QueryLogger.log[0]).to eq('SELECT COUNT(*) FROM `products` -- Hello Arproxy!')\n    expect(QueryLogger.log[1]).to eq('SELECT `products`.* FROM `products` ORDER BY `products`.`id` ASC LIMIT 1 -- Hello Arproxy!')\n  end\nend\n"
  },
  {
    "path": "integration_test/spec/postgresql_spec.rb",
    "content": "require_relative 'spec_helper'\nrequire 'pg'\n\ncontext 'PostgreSQL' do\n  before(:all) do\n    ActiveRecord::Base.establish_connection(\n      adapter: 'postgresql',\n      host: ENV.fetch('POSTGRES_HOST', '127.0.0.1'),\n      port: ENV.fetch('POSTGRES_PORT', '25432').to_i,\n      database: 'arproxy_test',\n      username: 'arproxy',\n      password: 'password'\n    )\n\n    Arproxy.configure do |config|\n      config.adapter = 'postgresql'\n      config.use HelloProxy\n      config.use QueryLogger\n    end\n    Arproxy.enable!\n\n    ActiveRecord::Base.connection.create_table :products, force: true do |t|\n      t.string :name\n      t.integer :price\n    end\n\n    Product.create(name: 'apple', price: 100)\n    Product.create(name: 'banana', price: 200)\n    Product.create(name: 'orange', price: 300)\n  end\n\n  after(:all) do\n    ActiveRecord::Base.connection.drop_table :products\n    ActiveRecord::Base.connection.close\n    Arproxy.disable!\n  end\n\n  before(:each) do\n    QueryLogger.reset!\n  end\n\n  it do\n    expect(QueryLogger.log.size).to eq(0)\n\n    expect(Product.count).to eq(3)\n    expect(Product.first.name).to eq('apple')\n\n    expect(QueryLogger.log.size).to eq(2)\n    expect(QueryLogger.log[0]).to eq('SELECT COUNT(*) FROM `products` -- Hello Arproxy!')\n    expect(QueryLogger.log[1]).to eq('SELECT `products`.* FROM `products` ORDER BY `products`.`id` ASC LIMIT 1 -- Hello Arproxy!')\n  end\nend\n"
  },
  {
    "path": "integration_test/spec/spec_helper.rb",
    "content": "require 'arproxy'\nrequire 'active_record'\n\nclass Product < ActiveRecord::Base\nend\n\nclass QueryLogger < Arproxy::Base\n  def execute(sql, name = nil)\n    @@log ||= []\n    @@log << sql\n    puts \"QueryLogger: #{sql}\"\n    super\n  end\n\n  def self.log\n    @@log\n  end\n\n  def self.reset!\n    @@log = []\n  end\nend\n\nclass HelloProxy < Arproxy::Base\n  def execute(sql, name = nil)\n    super(\"#{sql} -- Hello Arproxy!\", name)\n  end\nend\n"
  },
  {
    "path": "lib/arproxy/base.rb",
    "content": "module Arproxy\n  # This class is no longer used since Arproxy v1.\n  class Base\n  end\nend\n"
  },
  {
    "path": "lib/arproxy/config.rb",
    "content": "require 'active_record'\nrequire 'active_record/base'\nrequire 'arproxy/base'\nrequire 'arproxy/error'\n\nmodule Arproxy\n  class Config\n    attr_accessor :adapter, :logger\n    attr_reader :proxies\n\n    def initialize\n      @proxies = []\n      if defined?(Rails)\n        @adapter = Rails.application.config_for(:database)['adapter']\n      end\n    end\n\n    def use(proxy_class, *options)\n      if proxy_class.is_a?(Class) && proxy_class.ancestors.include?(Arproxy::Base)\n        raise Arproxy::Error, \"Error on loading a proxy `#{proxy_class.inspect}`: the superclass `Arproxy::Base` is no longer supported since Arproxy v1. Use `Arproxy::Proxy` instead. See: https://github.com/cookpad/arproxy/blob/main/UPGRADING.md\"\n      end\n\n      ::Arproxy.logger.debug(\"Arproxy: Mounting #{proxy_class.inspect} (#{options.inspect})\")\n      @proxies << [proxy_class, options]\n    end\n\n    def plugin(name, *options)\n      plugin_class = Plugin.get(name)\n\n      if plugin_class.is_a?(Class) && plugin_class.ancestors.include?(Arproxy::Base)\n        raise Arproxy::Error, \"Error on loading a plugin `#{plugin_class.inspect}`: the superclass `Arproxy::Base` is no longer supported since Arproxy v1. Use `Arproxy::Proxy` instead. See: https://github.com/cookpad/arproxy/blob/main/UPGRADING.md\"\n      end\n\n      use(plugin_class, *options)\n    end\n\n    def adapter_class\n      raise Arproxy::Error, 'config.adapter must be set' unless @adapter\n      case @adapter\n      when String, Symbol\n        eval \"::ActiveRecord::ConnectionAdapters::#{camelized_adapter_name}Adapter\"\n      when Class\n        @adapter\n      else\n        raise Arproxy::Error, \"unexpected config.adapter: #{@adapter}\"\n      end\n    end\n\n    private\n\n      def camelized_adapter_name\n        adapter_name = @adapter.to_s.split('_').map(&:capitalize).join\n\n        case adapter_name\n        when 'Sqlite3'\n          'SQLite3'\n        when 'Sqlserver'\n          'SQLServer'\n        when 'Postgresql'\n          'PostgreSQL'\n        else\n          adapter_name\n        end\n      end\n  end\nend\n"
  },
  {
    "path": "lib/arproxy/connection_adapter_patch.rb",
    "content": "module Arproxy\n  class ConnectionAdapterPatch\n    attr_reader :adapter_class\n\n    def initialize(adapter_class)\n      @adapter_class = adapter_class\n      @applied_patches = Set.new\n    end\n\n    def self.register_patches(adapter_name, patches: [], binds_patches: [])\n      @@patches ||= {}\n      @@patches[adapter_name] = {\n        patches: patches,\n        binds_patches: binds_patches\n      }\n    end\n\n    if ActiveRecord.version >= Gem::Version.new('8.0')\n      register_patches('Mysql2', patches: [], binds_patches: [:raw_execute])\n      register_patches('Trilogy', patches: [], binds_patches: [:raw_execute])\n    elsif ActiveRecord.version >= Gem::Version.new('7.0')\n      register_patches('Mysql2', patches: [:raw_execute], binds_patches: [])\n      register_patches('Trilogy', patches: [:raw_execute], binds_patches: [])\n    else\n      register_patches('Mysql2', patches: [:execute], binds_patches: [])\n      register_patches('Trilogy', patches: [:raw_execute], binds_patches: [])\n    end\n\n    if ActiveRecord.version >= Gem::Version.new('8.0')\n      register_patches('PostgreSQL', patches: [], binds_patches: [:raw_execute])\n      register_patches('SQLServer', patches: [], binds_patches: [:raw_execute])\n      register_patches('SQLite', patches: [], binds_patches: [:raw_execute])\n    elsif ActiveRecord.version >= Gem::Version.new('7.1')\n      register_patches('PostgreSQL', patches: [:raw_execute], binds_patches: [:exec_no_cache, :exec_cache])\n      register_patches('SQLServer', patches: [:raw_execute], binds_patches: [:internal_exec_query])\n      register_patches('SQLite', patches: [:raw_execute], binds_patches: [:internal_exec_query])\n    else\n      register_patches('PostgreSQL', patches: [:execute], binds_patches: [:exec_no_cache, :exec_cache])\n      register_patches('SQLServer', patches: [:execute], binds_patches: [:exec_query])\n      register_patches('SQLite', patches: [:execute], binds_patches: [:exec_query])\n    end\n\n    def enable!\n      patches = @@patches[adapter_class::ADAPTER_NAME]\n      if patches\n        patches[:patches]&.each do |patch|\n          apply_patch patch\n        end\n        patches[:binds_patches]&.each do |binds_patch|\n          apply_patch_binds binds_patch\n        end\n      else\n        raise Arproxy::Error, \"Unexpected connection adapter: patches not registered for #{adapter_class&.name}\"\n      end\n      ::Arproxy.logger.debug(\"Arproxy: Enabled (#{adapter_class::ADAPTER_NAME})\")\n    end\n\n    def disable!\n      @applied_patches.dup.each do |target_method|\n        adapter_class.class_eval do\n          if instance_methods.include?(:\"#{target_method}_with_arproxy\")\n            alias_method target_method, :\"#{target_method}_without_arproxy\"\n            remove_method :\"#{target_method}_with_arproxy\"\n          end\n        end\n        @applied_patches.delete(target_method)\n      end\n      ::Arproxy.logger.debug(\"Arproxy: Disabled (#{adapter_class::ADAPTER_NAME})\")\n    end\n\n    private\n\n      def apply_patch(target_method)\n        return if @applied_patches.include?(target_method)\n        adapter_class.class_eval do\n          raw_execute_method_name = :\"#{target_method}_without_arproxy\"\n          patched_execute_method_name = :\"#{target_method}_with_arproxy\"\n          break if instance_methods.include?(patched_execute_method_name)\n          define_method(patched_execute_method_name) do |sql, name=nil, **kwargs|\n            context = QueryContext.new(\n              raw_connection: self,\n              execute_method_name: raw_execute_method_name,\n              with_binds: false,\n              name: name,\n              kwargs: kwargs,\n            )\n            ::Arproxy.proxy_chain.head.execute(sql, context)\n          end\n          alias_method raw_execute_method_name, target_method\n          alias_method target_method, patched_execute_method_name\n        end\n        @applied_patches << target_method\n      end\n\n      def apply_patch_binds(target_method)\n        return if @applied_patches.include?(target_method)\n        adapter_class.class_eval do\n          raw_execute_method_name = :\"#{target_method}_without_arproxy\"\n          patched_execute_method_name = :\"#{target_method}_with_arproxy\"\n          break if instance_methods.include?(patched_execute_method_name)\n          define_method(patched_execute_method_name) do |sql, name=nil, binds=[], **kwargs|\n            context = QueryContext.new(\n              raw_connection: self,\n              execute_method_name: raw_execute_method_name,\n              with_binds: true,\n              name: name,\n              binds: binds,\n              kwargs: kwargs,\n            )\n            ::Arproxy.proxy_chain.head.execute(sql, context)\n          end\n          alias_method raw_execute_method_name, target_method\n          alias_method target_method, patched_execute_method_name\n        end\n        @applied_patches << target_method\n      end\n  end\nend\n"
  },
  {
    "path": "lib/arproxy/error.rb",
    "content": "module Arproxy\n  class Error < Exception\n  end\nend\n"
  },
  {
    "path": "lib/arproxy/plugin.rb",
    "content": "module Arproxy\n  module Plugin\n    class << self\n      def register(name, klass)\n        name = name.to_s\n        @plugins ||= {}\n\n        if @plugins.has_key?(name)\n          raise Arproxy::Error, \"Plugin has already been registered: #{name}\"\n        end\n\n        @plugins[name] = klass\n      end\n\n      def get(name)\n        name = name.to_s\n        require \"arproxy/plugin/#{name}\"\n        plugin = @plugins[name]\n\n        unless plugin\n          raise Arproxy::Error, \"Plugin is not found: #{name}\"\n        end\n\n        plugin\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/arproxy/proxy.rb",
    "content": "require 'arproxy/query_context'\n\nmodule Arproxy\n  class Proxy\n    attr_accessor :context, :next_proxy\n\n    def execute(sql, context)\n      unless context.instance_of?(QueryContext)\n        raise Arproxy::Error, \"`context` is expected a `Arproxy::QueryContext` but got `#{context.class}`\"\n      end\n\n      next_proxy.execute(sql, context)\n    end\n  end\nend\n"
  },
  {
    "path": "lib/arproxy/proxy_chain.rb",
    "content": "require 'arproxy/proxy_chain_tail'\nrequire 'arproxy/connection_adapter_patch'\n\nmodule Arproxy\n  class ProxyChain\n    attr_reader :head, :tail, :patch\n\n    def initialize(config, patch)\n      @config = config\n      @patch = patch\n      setup\n    end\n\n    def setup\n      @tail = ProxyChainTail.new\n      @head = @config.proxies.reverse.inject(@tail) do |next_proxy, proxy_config|\n        cls, options = proxy_config\n        proxy = cls.new(*options)\n        proxy.next_proxy = next_proxy\n        proxy\n      end\n    end\n    private :setup\n\n    def reenable!\n      disable!\n      setup\n      enable!\n    end\n\n    def enable!\n      @patch.enable!\n    end\n\n    def disable!\n      @patch.disable!\n    end\n  end\nend\n"
  },
  {
    "path": "lib/arproxy/proxy_chain_tail.rb",
    "content": "require 'arproxy/proxy'\nrequire 'arproxy/query_context'\n\nmodule Arproxy\n  class ProxyChainTail < Proxy\n    def execute(sql, context)\n      unless context.instance_of?(QueryContext)\n        raise Arproxy::Error, \"`context` is expected a `Arproxy::QueryContext` but got `#{context.class}`\"\n      end\n\n      if context.with_binds?\n        context.raw_connection.send(context.execute_method_name, sql, context.name, context.binds, **context.kwargs)\n      else\n        context.raw_connection.send(context.execute_method_name, sql, context.name, **context.kwargs)\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/arproxy/query_context.rb",
    "content": "module Arproxy\n  class QueryContext\n    attr_accessor :raw_connection, :execute_method_name, :with_binds, :name, :binds, :kwargs\n\n    def initialize(raw_connection:, execute_method_name:, with_binds:, name: nil, binds: [], kwargs: {})\n      @raw_connection = raw_connection\n      @execute_method_name = execute_method_name\n      @with_binds = with_binds\n      @name = name\n      @binds = binds\n      @kwargs = kwargs\n    end\n\n    def with_binds?\n      !!@with_binds\n    end\n  end\nend\n"
  },
  {
    "path": "lib/arproxy/version.rb",
    "content": "module Arproxy\n  VERSION = '1.0.0'\nend\n"
  },
  {
    "path": "lib/arproxy.rb",
    "content": "require 'logger'\nrequire 'arproxy/base'\nrequire 'arproxy/config'\nrequire 'arproxy/connection_adapter_patch'\nrequire 'arproxy/proxy_chain'\nrequire 'arproxy/error'\nrequire 'arproxy/plugin'\n\nmodule Arproxy\n  @config = nil\n  @enabled = nil\n  @patch = nil\n\n  module_function\n\n    def clear_configuration\n      @config = nil\n    end\n\n    def configure\n      @config ||= Config.new\n      yield @config\n    end\n\n    def enable!\n      if enable?\n        Arproxy.logger.warn 'Arproxy has already been enabled'\n        return\n      end\n\n      unless @config\n        raise Arproxy::Error, 'Arproxy has not been configured'\n      end\n\n      @patch = ConnectionAdapterPatch.new(@config.adapter_class)\n      @proxy_chain = ProxyChain.new(@config, @patch)\n      @proxy_chain.enable!\n\n      @enabled = true\n    end\n\n    def disable!\n      unless enable?\n        Arproxy.logger.warn 'Arproxy is not enabled yet'\n        return\n      end\n\n      if @proxy_chain\n        @proxy_chain.disable!\n        @proxy_chain = nil\n      end\n\n      @enabled = false\n    end\n\n    def enable?\n      !!@enabled\n    end\n\n    def reenable!\n      if enable?\n        @proxy_chain.reenable!\n      else\n        enable!\n      end\n    end\n\n    def logger\n      @logger ||= @config && @config.logger ||\n                      defined?(::Rails) && ::Rails.logger ||\n                      ::Logger.new(STDOUT)\n    end\n\n    def proxy_chain\n      @proxy_chain\n    end\n\n    def connection_adapter_patch\n      @patch\n    end\nend\n"
  },
  {
    "path": "spec/integration/mysql2_spec.rb",
    "content": "require_relative './spec_helper'\n\ncontext \"MySQL (AR#{ar_version})\" do\n  before(:all) do\n    host = ENV.fetch('MYSQL_HOST', '127.0.0.1')\n    port = ENV.fetch('MYSQL_PORT', '23306').to_i\n    wait_for_db(host, port)\n\n    ActiveRecord::Base.establish_connection(\n      adapter: 'mysql2',\n      host: host,\n      port: port,\n      database: 'arproxy_test',\n      username: 'arproxy',\n      password: ENV.fetch('ARPROXY_DB_PASSWORD')\n    )\n\n    Arproxy.configure do |config|\n      config.adapter = 'mysql2'\n      config.use HelloProxy\n      config.plugin :query_logger\n    end\n    Arproxy.enable!\n  end\n\n  after(:all) do\n    cleanup_activerecord\n    Arproxy.disable!\n    Arproxy.clear_configuration\n  end\n\n  it_behaves_like 'Arproxy does not break the original ActiveRecord functionality'\n  it_behaves_like 'Custom proxies work expectedly'\nend\n"
  },
  {
    "path": "spec/integration/postgresql_spec.rb",
    "content": "require_relative './spec_helper'\n\ncontext \"PostgreSQL (AR#{ar_version})\" do\n  before(:all) do\n    host = ENV.fetch('POSTGRES_HOST', '127.0.0.1')\n    port = ENV.fetch('POSTGRES_PORT', '25432').to_i\n    wait_for_db(host, port)\n\n    ActiveRecord::Base.establish_connection(\n      adapter: 'postgresql',\n      host: host,\n      port: port,\n      database: 'arproxy_test',\n      username: 'arproxy',\n      password: ENV.fetch('ARPROXY_DB_PASSWORD')\n    )\n\n    Arproxy.configure do |config|\n      config.adapter = 'postgresql'\n      config.use HelloProxy\n      config.plugin :query_logger\n    end\n    Arproxy.enable!\n  end\n\n  after(:all) do\n    cleanup_activerecord\n    Arproxy.disable!\n    Arproxy.clear_configuration\n  end\n\n  it_behaves_like 'Arproxy does not break the original ActiveRecord functionality'\n  it_behaves_like 'Custom proxies work expectedly'\nend\n"
  },
  {
    "path": "spec/integration/shared_examples/active_record_functions.rb",
    "content": "RSpec.shared_examples 'Arproxy does not break the original ActiveRecord functionality' do\n  before do\n    # CREATE\n    ActiveRecord::Base.connection.create_table :products, force: true do |t|\n      t.string :name\n      t.integer :price\n    end\n    # INSERT\n    Product.create!(name: 'apple', price: 100)\n    Product.create!(name: 'banana', price: 200)\n    Product.create!(name: 'orange', price: 300)\n  end\n\n  after(:all) do\n    ActiveRecord::Base.connection.drop_table :products\n  end\n\n  context 'SELECT' do\n    it { expect(Product.where(name: ['apple', 'orange']).sum(:price)).to eq(400) }\n  end\n\n  context 'UPDATE' do\n    it do\n      expect {\n        Product.where(name: 'banana').update_all(price: 1000)\n      }.to change {\n        Product.find_by!(name: 'banana').price\n      }.from(200).to(1000)\n    end\n  end\n\n  context 'DELETE' do\n    it do\n      expect {\n        Product.where(name: 'banana').delete_all\n      }.to change {\n        Product.where(name: 'banana').exists?\n      }.from(true).to(false)\n    end\n  end\nend\n"
  },
  {
    "path": "spec/integration/shared_examples/custom_proxies.rb",
    "content": "class Product < ActiveRecord::Base\nend\n\nclass HelloLegacyProxy < Arproxy::Base\n  def execute(sql, name = nil)\n    super(\"#{sql} -- Hello Legacy Arproxy!\", name)\n  end\nend\n\nclass HelloProxy < Arproxy::Proxy\n  def execute(sql, context)\n    super(\"#{sql} -- Hello Arproxy!\", context)\n  end\nend\n\nRSpec::Matchers.define :add_query_log do |log_line_regex|\n  supports_block_expectations\n\n  match do |block|\n    idx = QueryLogger.log.size\n    block.call\n    QueryLogger.log.size > idx && QueryLogger.log[idx..-1].any? { |log| log.match(log_line_regex) }\n  end\n\n  failure_message do |block|\n    \"expected to add query log matching #{log_line_regex.inspect}, but got #{QueryLogger.log.inspect}\"\n  end\n\n  failure_message_when_negated do |block|\n    \"expected not to add query log matching #{log_line_regex.inspect}, but added\"\n  end\n\n  def supports_block_expectations?\n    true\n  end\nend\n\nRSpec.shared_examples 'Custom proxies work expectedly' do\n  before do\n    ActiveRecord::Base.connection.create_table :products, force: true do |t|\n      t.string :name\n      t.integer :price\n    end\n    Product.create(name: 'apple', price: 100)\n    Product.create(name: 'banana', price: 200)\n    Product.create(name: 'orange', price: 300)\n    QueryLogger.reset!\n  end\n\n  after(:all) do\n    ActiveRecord::Base.connection.drop_table :products\n  end\n\n  around do |example|\n    ActiveRecord::Base.uncached do\n      example.run\n    end\n  end\n\n  context 'CREATE TABLE' do\n    it do\n      expect {\n        ActiveRecord::Base.connection.create_table :products, force: true do |t|\n          t.string :name\n          t.integer :price\n        end\n      }.to add_query_log(/^CREATE TABLE.*products.* -- Hello Arproxy!$/)\n    end\n  end\n\n  context 'SELECT' do\n    it do\n      expect {\n        Product.where(name: ['apple', 'orange']).sum(:price)\n      }.to add_query_log(/^SELECT.*products.* -- Hello Arproxy!$/)\n    end\n  end\n\n  context 'INSERT' do\n    it do\n      expect {\n        Product.create(name: 'grape', price: 400)\n      }.to add_query_log(/^INSERT INTO.*products.* -- Hello Arproxy!$/)\n    end\n  end\n\n  context 'UPDATE' do\n    it do\n      expect {\n        Product.where(name: 'banana').update_all(price: 1000)\n      }.to add_query_log(/^UPDATE.*products.* -- Hello Arproxy!$/)\n    end\n  end\n\n  context 'DELETE' do\n    it do\n      expect {\n        Product.where(name: 'banana').delete_all\n      }.to add_query_log(/^DELETE.*products.* -- Hello Arproxy!$/)\n    end\n  end\nend\n"
  },
  {
    "path": "spec/integration/spec_helper.rb",
    "content": "require 'arproxy'\nrequire 'active_record'\nrequire 'dotenv/load'\nrequire_relative './shared_examples/custom_proxies'\nrequire_relative './shared_examples/active_record_functions'\n\nArproxy.logger.level = Logger::WARN unless ENV['DEBUG']\n\ndef ar_version\n  \"#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}\"\nend\n\ndef cleanup_activerecord\n  ActiveRecord::Base.connection.close\n  ActiveRecord::Base.connection.clear_cache!\n  ActiveRecord::Base.descendants.each(&:reset_column_information)\n  ActiveRecord::Base.connection.schema_cache.clear!\nend\n\ndef wait_for_db(host, port, interval = 0.2, timeout = 10)\n  print \"\\nWaiting for DB on #{host}:#{port}...\" if ENV['DEBUG']\n  Timeout.timeout(timeout) do\n    loop do\n      TCPSocket.new(host, port).close\n      puts 'ok' if ENV['DEBUG']\n      break\n    rescue Errno::ECONNREFUSED\n      print '.' if ENV['DEBUG']\n      sleep interval\n    end\n  end\nrescue Timeout::Error\n  raise \"Timeout waiting for DB on #{host}:#{port}\"\nend\n"
  },
  {
    "path": "spec/integration/sqlite3_spec.rb",
    "content": "require_relative './spec_helper'\n\ncontext \"SQLite3 (AR#{ar_version})\" do\n  before(:all) do\n    ActiveRecord::Base.establish_connection(\n      adapter: 'sqlite3',\n      database: ':memory:'\n    )\n\n    Arproxy.configure do |config|\n      config.adapter = 'sqlite3'\n      config.use HelloProxy\n      config.plugin :query_logger\n    end\n    Arproxy.enable!\n  end\n\n  after(:all) do\n    cleanup_activerecord\n    Arproxy.disable!\n    Arproxy.clear_configuration\n  end\n\n  it_behaves_like 'Arproxy does not break the original ActiveRecord functionality'\n  it_behaves_like 'Custom proxies work expectedly'\nend\n"
  },
  {
    "path": "spec/integration/sqlserver_spec.rb",
    "content": "require_relative './spec_helper'\n\ncontext \"SQLServer (AR#{ar_version})\" do\n  before(:all) do\n    if ActiveRecord.version >= Gem::Version.new('7.2')\n      ActiveRecord::ConnectionAdapters.register(\n        'sqlserver',\n        'ActiveRecord::ConnectionAdapters::SQLServerAdapter',\n        'active_record/connection_adapters/sqlserver_adapter'\n      )\n    end\n\n    host = ENV.fetch('MSSQL_HOST', '127.0.0.1')\n    port = ENV.fetch('MSSQL_PORT', '21433').to_i\n    wait_for_db(host, port)\n\n    ActiveRecord::Base.establish_connection(\n      adapter: 'sqlserver',\n      host: host,\n      port: port,\n      database: 'arproxy_test',\n      username: 'arproxy',\n      password: ENV.fetch('ARPROXY_DB_PASSWORD')\n    )\n\n    Arproxy.configure do |config|\n      config.adapter = 'sqlserver'\n      config.use HelloProxy\n      config.plugin :query_logger\n    end\n    Arproxy.enable!\n  end\n\n  after(:all) do\n    cleanup_activerecord\n    Arproxy.disable!\n    Arproxy.clear_configuration\n  end\n\n  it_behaves_like 'Arproxy does not break the original ActiveRecord functionality'\n  it_behaves_like 'Custom proxies work expectedly'\nend\n"
  },
  {
    "path": "spec/integration/trilogy_spec.rb",
    "content": "require_relative './spec_helper'\nrequire 'trilogy'\n\ncontext \"Trilogy (AR#{ar_version})\", if: ActiveRecord.version >= '7.1' do\n  before(:all) do\n    host = ENV.fetch('MYSQL_HOST', '127.0.0.1')\n    port = ENV.fetch('MYSQL_PORT', '23306').to_i\n    wait_for_db(host, port)\n\n    mysql_data_dir = File.expand_path('../../db/mysql/data', __dir__)\n    ActiveRecord::Base.establish_connection(\n      adapter: 'trilogy',\n      host: host,\n      port: port,\n      ssl: true,\n      ssl_mode: Trilogy::SSL_VERIFY_CA,\n      tls_min_version: Trilogy::TLS_VERSION_12,\n      ssl_ca: File.join(mysql_data_dir, 'ca.pem'),\n      ssl_cert: File.join(mysql_data_dir, 'client-cert.pem'),\n      ssl_key: File.join(mysql_data_dir, 'client-key.pem'),\n      database: 'arproxy_test',\n      username: 'arproxy',\n      password: ENV.fetch('ARPROXY_DB_PASSWORD')\n    )\n\n    Arproxy.configure do |config|\n      config.adapter = 'trilogy'\n      config.use HelloProxy\n      config.plugin :query_logger\n    end\n    Arproxy.enable!\n  end\n\n  after(:all) do\n    cleanup_activerecord\n    Arproxy.disable!\n    Arproxy.clear_configuration\n  end\n\n  it_behaves_like 'Arproxy does not break the original ActiveRecord functionality'\n  it_behaves_like 'Custom proxies work expectedly'\nend\n"
  },
  {
    "path": "spec/lib/arproxy/plugin/legacy_plugin.rb",
    "content": "module Arproxy::Plugin\n  class LegacyPlugin < Arproxy::Base\n    Arproxy::Plugin.register(:legacy_plugin, self)\n\n    def execute(sql, name)\n      super(\"#{sql} /* legacy_plugin */\", name)\n    end\n  end\nend\n"
  },
  {
    "path": "spec/lib/arproxy/plugin/query_logger.rb",
    "content": "require 'arproxy/plugin'\n\nclass QueryLogger < Arproxy::Proxy\n  Arproxy::Plugin.register(:query_logger, self)\n\n  def execute(sql, context)\n    @@log ||= []\n    @@log << sql\n    if ENV['DEBUG']\n      puts \"QueryLogger: [#{context.name}] #{sql}\"\n    end\n    super\n  end\n\n  def self.log\n    @@log\n  end\n\n  def self.reset!\n    @@log = []\n  end\nend\n"
  },
  {
    "path": "spec/lib/arproxy/plugin/test_plugin.rb",
    "content": "module Arproxy::Plugin\n  class TestPlugin < Arproxy::Proxy\n    Arproxy::Plugin.register(:test_plugin, self)\n\n    def initialize(*options)\n      @options = options\n    end\n\n    def execute(sql, context)\n      context.name = \"#{context.name}_PLUGIN\"\n      super(\"#{sql} /* options: #{@options.inspect} */\", context)\n    end\n  end\nend\n"
  },
  {
    "path": "spec/unit/arproxy_spec.rb",
    "content": "require_relative './spec_helper'\n\ndescribe Arproxy do\n  before do\n    allow(Arproxy).to receive(:logger) { Logger.new('/dev/null') }\n  end\n\n  class LegacyProxyA < Arproxy::Base\n    def execute(sql, name)\n      super \"#{sql}_A\", \"#{name}_A\"\n    end\n  end\n\n  class LegacyProxyB < Arproxy::Base\n    def initialize(opt=nil)\n      @opt = opt\n    end\n\n    def execute(sql, name)\n      super \"#{sql}_B#{@opt}\", \"#{name}_B#{@opt}\"\n    end\n  end\n\n  class ProxyA < Arproxy::Proxy\n    def execute(sql, context)\n      context.name = \"#{context.name}_A\"\n      super \"#{sql}_A\", context\n    end\n  end\n\n  class ProxyB < Arproxy::Proxy\n    def initialize(opt=nil)\n      @opt = opt\n    end\n\n    def execute(sql, context)\n      context.name = \"#{context.name}_B#{@opt}\"\n      super \"#{sql}_B#{@opt}\", context\n    end\n  end\n\n  module ::ActiveRecord\n    module ConnectionAdapters\n      class DummyAdapter\n        ADAPTER_NAME = 'Dummy'\n\n        def execute1(sql, name = nil, **kwargs)\n          { sql: sql, name: name, kwargs: kwargs }\n        end\n\n        def execute2(sql, name = nil, binds = [], **kwargs)\n          { sql: sql, name: name, binds: binds, kwargs: kwargs }\n        end\n      end\n      Arproxy::ConnectionAdapterPatch.register_patches('Dummy', patches: [:execute1], binds_patches: [:execute2])\n    end\n  end\n\n  let(:connection) { ::ActiveRecord::ConnectionAdapters::DummyAdapter.new }\n  after(:each) do\n    Arproxy.disable!\n  end\n\n  context 'with a proxy' do\n    before do\n      Arproxy.clear_configuration\n      Arproxy.configure do |config|\n        config.adapter = 'dummy'\n        config.use ProxyA\n      end\n      Arproxy.enable!\n    end\n\n    it { expect(connection.execute1('SQL', 'NAME')).to eq({ sql: 'SQL_A', name: 'NAME_A', kwargs: {} }) }\n    it { expect(connection.execute1('SQL', 'NAME', a: 1, b: 2)).to eq({ sql: 'SQL_A', name: 'NAME_A', kwargs: { a: 1, b: 2 } }) }\n\n    it { expect(connection.execute2('SQL', 'NAME')).to eq({ sql: 'SQL_A', name: 'NAME_A', binds: [], kwargs: {} }) }\n\n    it do\n      expect(\n        connection.execute2('SQL', 'NAME', [:x, :y], a: 1, b: 2)\n      ).to eq(\n        { sql: 'SQL_A', name: 'NAME_A', binds: [:x, :y], kwargs: { a: 1, b: 2 } }\n      )\n    end\n  end\n\n  context 'with 2 proxies' do\n    before do\n      Arproxy.clear_configuration\n      Arproxy.configure do |config|\n        config.adapter = 'dummy'\n        config.use ProxyA\n        config.use ProxyB\n      end\n      Arproxy.enable!\n    end\n\n    it { expect(connection.execute1('SQL', 'NAME')).to eq({ sql: 'SQL_A_B', name: 'NAME_A_B', kwargs: {} }) }\n  end\n\n  context 'with 2 proxies which have an option' do\n    before do\n      Arproxy.clear_configuration\n      Arproxy.configure do |config|\n        config.adapter = 'dummy'\n        config.use ProxyA\n        config.use ProxyB, 1\n      end\n      Arproxy.enable!\n    end\n\n    it { expect(connection.execute1('SQL', 'NAME')).to eq({ sql: 'SQL_A_B1', name: 'NAME_A_B1', kwargs: {} }) }\n  end\n\n  context 'with a proxy that returns nil' do\n    class ReadonlyAccess < Arproxy::Proxy\n      def execute(sql, context)\n        if sql =~ /^(SELECT)\\b/\n          super sql, context\n        else\n          nil\n        end\n      end\n    end\n\n    before do\n      Arproxy.clear_configuration\n      Arproxy.configure do |config|\n        config.adapter = 'dummy'\n        config.use ReadonlyAccess\n      end\n      Arproxy.enable!\n    end\n\n    it { expect(connection.execute1('SELECT 1', 'NAME')).to eq({ sql: 'SELECT 1', name: 'NAME', kwargs: {} }) }\n    it { expect(connection.execute1('UPDATE foo SET bar = 1', 'NAME')).to eq(nil) }\n  end\n\n  context 'with a legacy proxy' do\n    class LegacyProxy < Arproxy::Base\n      def execute(sql, name)\n        super(\"#{sql} /* legacy_proxy */\", name)\n      end\n    end\n\n    before do\n      Arproxy.clear_configuration\n    end\n\n    it 'raises an error' do\n      expect {\n        Arproxy.configure do |config|\n          config.adapter = 'dummy'\n          config.use LegacyProxy\n        end\n      }.to raise_error(Arproxy::Error, /Use `Arproxy::Proxy` instead/)\n    end\n  end\n\n  context 'calls #execute with an String argument instead of `context`' do\n    class WrongProxy < Arproxy::Proxy\n      def execute(sql, context)\n        super(\"#{sql} /* my_proxy */\", \"name=#{context.name}\")\n      end\n    end\n\n    before do\n      Arproxy.clear_configuration\n      Arproxy.configure do |config|\n        config.adapter = 'dummy'\n        config.use WrongProxy\n      end\n      Arproxy.enable!\n    end\n\n    it do\n      expect {\n        connection.execute1('SQL', 'NAME')\n      }.to raise_error(Arproxy::Error, /expected a `Arproxy::QueryContext`/)\n    end\n  end\n\n  context do\n    before do\n      Arproxy.clear_configuration\n      Arproxy.configure do |config|\n        config.adapter = 'dummy'\n        config.use ProxyA\n      end\n    end\n\n    context 'enable -> disable' do\n      before do\n        Arproxy.enable!\n        Arproxy.disable!\n      end\n      it { expect(connection.execute1('SQL', 'NAME')).to eq({ sql: 'SQL', name: 'NAME', kwargs: {} }) }\n    end\n\n    context 'enable -> enable' do\n      before do\n        Arproxy.enable!\n        Arproxy.enable!\n      end\n      it { expect(connection.execute1('SQL', 'NAME')).to eq({ sql: 'SQL_A', name: 'NAME_A', kwargs: {} }) }\n    end\n\n    context 'enable -> disable -> disable' do\n      before do\n        Arproxy.enable!\n        Arproxy.disable!\n        Arproxy.disable!\n      end\n      it { expect(connection.execute1('SQL', 'NAME')).to eq({ sql: 'SQL', name: 'NAME', kwargs: {} }) }\n    end\n\n    context 'clear_configuration -> enable' do\n      before do\n        Arproxy.clear_configuration\n      end\n      it do\n        expect {\n          Arproxy.enable!\n        }.to raise_error(Arproxy::Error, /Arproxy has not been configured/)\n      end\n    end\n\n\n    context 'enable -> disable -> enable' do\n      before do\n        Arproxy.enable!\n        Arproxy.disable!\n        Arproxy.enable!\n      end\n      it { expect(connection.execute1('SQL', 'NAME')).to eq({ sql: 'SQL_A', name: 'NAME_A', kwargs: {} }) }\n    end\n\n    context 're-configure' do\n      before do\n        Arproxy.configure do |config|\n          config.adapter = 'dummy'\n          config.use ProxyB\n        end\n        Arproxy.enable!\n      end\n      it { expect(connection.execute1('SQL', 'NAME')).to eq({ sql: 'SQL_A_B', name: 'NAME_A_B', kwargs: {} }) }\n    end\n  end\n\n  context 'use a plug-in' do\n    before do\n      Arproxy.clear_configuration\n      Arproxy.configure do |config|\n        config.adapter = 'dummy'\n        config.plugin :test_plugin, :option_a, :option_b\n      end\n      Arproxy.enable!\n    end\n\n    it do\n      expect(\n        connection.execute1('SQL', 'NAME')\n      ).to eq(\n        { sql: 'SQL /* options: [:option_a, :option_b] */', name: 'NAME_PLUGIN', kwargs: {} }\n      )\n    end\n  end\n\n  context 'use a legacy plugin' do\n    before do\n      Arproxy.clear_configuration\n    end\n\n    it 'raises an error' do\n      expect {\n        Arproxy.configure do |config|\n          config.adapter = 'dummy'\n          config.plugin :legacy_plugin\n        end\n      }.to raise_error(Arproxy::Error, /Use `Arproxy::Proxy` instead/)\n    end\n  end\nend\n"
  },
  {
    "path": "spec/unit/config_spec.rb",
    "content": "require_relative './spec_helper'\n\ndescribe Arproxy::Config do\n  describe '#adapter default value' do\n    subject { Arproxy::Config.new.adapter }\n\n    context 'when Rails is defined' do\n      let(:rails) { Module.new }\n\n      around do |example|\n        Object.const_set('Rails', rails)\n        example.run\n        Object.send(:remove_const, 'Rails')\n      end\n\n      before do\n        allow(rails).to receive_message_chain('application.config_for') { database_config }\n      end\n\n      context 'when adapter is configured in database.yml' do\n        let(:database_config) { { 'adapter' => 'mysql2' } }\n\n        it { should == 'mysql2' }\n      end\n\n      context \"when adapter isn't configured in database.yml\" do\n        let(:database_config) { {} }\n\n        it { should == nil }\n      end\n    end\n\n    context \"when Rails isn't defined\" do\n      it { should == nil }\n    end\n  end\n\n  describe '#adapter_class' do\n    subject { config.adapter_class }\n    let(:config) { Arproxy::Config.new }\n\n    before do\n      config.adapter = adapter\n    end\n\n    context \"when adapter is configured as 'mysql2'\" do\n      let(:adapter) { 'mysql2' }\n      let(:mysql2_class) { Class.new }\n\n      before do\n        stub_const('ActiveRecord::ConnectionAdapters::Mysql2Adapter', mysql2_class)\n      end\n\n      it { should == mysql2_class }\n    end\n\n    context \"when adapter is configured as 'sqlite3'\" do\n      let(:adapter) { 'sqlite3' }\n      let(:sqlite3_class) { Class.new }\n\n      before do\n        stub_const('ActiveRecord::ConnectionAdapters::SQLite3Adapter', sqlite3_class)\n      end\n\n      it { should == sqlite3_class }\n    end\n\n    context \"when adapter is configured as 'sqlserver'\" do\n      let(:adapter) { 'sqlserver' }\n      let(:sqlserver_class) { Class.new }\n\n      before do\n        stub_const('ActiveRecord::ConnectionAdapters::SQLServerAdapter', sqlserver_class)\n      end\n\n      it { should == sqlserver_class }\n    end\n\n    context \"when adapter is configured as 'postgresql'\" do\n      let(:adapter) { 'postgresql' }\n      let(:postgresql_class) { Class.new }\n\n      before do\n        stub_const('ActiveRecord::ConnectionAdapters::PostgreSQLAdapter', postgresql_class)\n      end\n\n      it { should == postgresql_class }\n    end\n  end\nend\n"
  },
  {
    "path": "spec/unit/proxy_spec.rb",
    "content": "require_relative './spec_helper'\nrequire 'arproxy/proxy_chain_tail'\nrequire 'arproxy/proxy'\nrequire 'arproxy/query_context'\n\ndescribe Arproxy::Proxy do\n  before(:all) do\n    class DummyConnectionAdapter\n      def execute(sql, name = nil, binds = [], **kwargs)\n        \"#{sql}\"\n      end\n    end\n\n    class Proxy1 < Arproxy::Proxy\n      def execute(sql, context)\n        super(\"#{sql} /* Proxy1 */\", context)\n      end\n    end\n\n    class Proxy2 < Arproxy::Proxy\n      def execute(sql, context)\n        super(\"#{sql} /* Proxy2 */\", context)\n      end\n    end\n\n    tail = Arproxy::ProxyChainTail.new\n    p2 = Proxy2.new\n    p2.next_proxy = tail\n    p1 = Proxy1.new\n    p1.next_proxy = p2\n    @head = p1\n\n    @conn = DummyConnectionAdapter.new\n  end\n\n  context 'with binds' do\n    let(:context) { Arproxy::QueryContext.new(raw_connection: @conn, execute_method_name: 'execute', with_binds: true, name: 'test', binds: [1]) }\n    describe '#execute' do\n      it do\n        expect(@head.execute('SELECT 1', context)).to eq('SELECT 1 /* Proxy1 */ /* Proxy2 */')\n      end\n    end\n  end\n\n  context 'without binds' do\n    let(:context) { Arproxy::QueryContext.new(raw_connection: @conn, execute_method_name: 'execute', with_binds: false, name: 'test') }\n    describe '#execute' do\n      it do\n        expect(@head.execute('SELECT 1', context)).to eq('SELECT 1 /* Proxy1 */ /* Proxy2 */')\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "spec/unit/spec_helper.rb",
    "content": "require 'arproxy'\n\nArproxy.logger.level = Logger::WARN unless ENV['DEBUG']\n"
  }
]