Full Code of cookpad/arproxy for AI

main 93c3988736cf cached
59 files
60.2 KB
18.1k tokens
105 symbols
1 requests
Download .txt
Repository: cookpad/arproxy
Branch: main
Commit: 93c3988736cf
Files: 59
Total size: 60.2 KB

Directory structure:
gitextract_pt1acgj_/

├── .github/
│   └── workflows/
│       ├── integration_test.yml
│       ├── integration_tests.yml
│       ├── rubocop.yml
│       └── unit_tests.yml
├── .gitignore
├── .rspec
├── .rubocop.yml
├── Appraisals
├── ChangeLog.md
├── Dockerfile
├── Gemfile
├── LICENSE.txt
├── README.md
├── Rakefile
├── UPGRADING.md
├── arproxy.gemspec
├── compose-ci.yaml
├── compose.yaml
├── db/
│   ├── mysql/
│   │   └── my.cnf
│   └── sqlserver/
│       └── init.sql
├── gemfiles/
│   ├── ar_6.1.gemfile
│   ├── ar_7.0.gemfile
│   ├── ar_7.1.gemfile
│   ├── ar_7.2.gemfile
│   └── ar_8.0.gemfile
├── integration_test/
│   ├── Gemfile
│   ├── docker-compose.yml
│   ├── gemfiles/
│   │   ├── ar_6.1.gemfile
│   │   ├── ar_7.0.gemfile
│   │   └── ar_7.1.gemfile
│   └── spec/
│       ├── mysql2_spec.rb
│       ├── postgresql_spec.rb
│       └── spec_helper.rb
├── lib/
│   ├── arproxy/
│   │   ├── base.rb
│   │   ├── config.rb
│   │   ├── connection_adapter_patch.rb
│   │   ├── error.rb
│   │   ├── plugin.rb
│   │   ├── proxy.rb
│   │   ├── proxy_chain.rb
│   │   ├── proxy_chain_tail.rb
│   │   ├── query_context.rb
│   │   └── version.rb
│   └── arproxy.rb
└── spec/
    ├── integration/
    │   ├── mysql2_spec.rb
    │   ├── postgresql_spec.rb
    │   ├── shared_examples/
    │   │   ├── active_record_functions.rb
    │   │   └── custom_proxies.rb
    │   ├── spec_helper.rb
    │   ├── sqlite3_spec.rb
    │   ├── sqlserver_spec.rb
    │   └── trilogy_spec.rb
    ├── lib/
    │   └── arproxy/
    │       └── plugin/
    │           ├── legacy_plugin.rb
    │           ├── query_logger.rb
    │           └── test_plugin.rb
    └── unit/
        ├── arproxy_spec.rb
        ├── config_spec.rb
        ├── proxy_spec.rb
        └── spec_helper.rb

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/integration_test.yml
================================================
name: Integration Test

on:
  push:
    branches:
      - master
  pull_request:
    branches:
      - master

env:
  RUBY_VERSION: 3.3

jobs:
  mysql:
    continue-on-error: true
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: ./integration_test
    env:
      BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}.gemfile
      MYSQL_HOST: 127.0.0.1
    strategy:
      matrix:
        gemfile:
          - ar_6.1
          - ar_7.0
          - ar_7.1
    steps:
      - uses: actions/checkout@v4
      - name: Start DB
        run: docker compose up -d mysql
      - name: Set up Ruby
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: ${{ env.RUBY_VERSION }}
      - name: Run bundle install
        run: bundle install
      - name: Run integration test
        run: bundle exec rspec spec/mysql2_spec.rb

  postgresql:
    continue-on-error: true
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: ./integration_test
    env:
      BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}.gemfile
      POSTGRES_HOST: 127.0.0.1
    strategy:
      matrix:
        gemfile:
          - ar_6.1
          - ar_7.0
          - ar_7.1
    steps:
      - uses: actions/checkout@v4
      - name: Start DB
        run: docker compose up -d postgres
      - name: Set up Ruby
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: ${{ env.RUBY_VERSION }}
      - name: Run bundle install
        run: bundle install
      - name: Run integration test
        run: bundle exec rspec spec/postgresql_spec.rb


================================================
FILE: .github/workflows/integration_tests.yml
================================================
name: Integration tests

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  integration_test:
    runs-on: ubuntu-latest
    timeout-minutes: 5

    strategy:
      matrix:
        appraisal:
          - ar-6.1
          - ar-7.0
          - ar-7.1
          - ar-7.2
          - ar-8.0
    steps:
      - uses: actions/checkout@v4
      - name: docker compose up
        run: docker compose -f compose-ci.yaml up -d
      - name: Run integration test
        run: docker compose -f compose-ci.yaml exec ruby bundle exec appraisal ${{ matrix.appraisal }} rspec spec/integration/*_spec.rb


================================================
FILE: .github/workflows/rubocop.yml
================================================
name: RuboCop

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

env:
  RUBY_VERSION: 3.3

jobs:
  rubocop:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Ruby
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: ${{ env.RUBY_VERSION }}
          bundler-cache: true

      - name: Run RuboCop
        run: bundle exec rubocop


================================================
FILE: .github/workflows/unit_tests.yml
================================================
name: Unit tests

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        ruby-version:
          - 3.0
          - 3.1
          - 3.2
          - 3.3

    steps:
    - uses: actions/checkout@v4
    - name: Set up Ruby
      uses: ruby/setup-ruby@v1
      with:
        ruby-version: ${{ matrix.ruby-version }}
        bundler-cache: true
    - name: Run unit tests on ruby-${{ matrix.ruby-version }}
      run: bundle exec rspec spec/unit/*_spec.rb


================================================
FILE: .gitignore
================================================
*.swp
*.gem
Gemfile.lock
*.gemfile.lock
.bundle/
tmp/
db/mysql/data/*


================================================
FILE: .rspec
================================================
--color
-Ispec/lib


================================================
FILE: .rubocop.yml
================================================
---
# Referenced from https://github.com/rails/rails/blob/main/.rubocop.yml

require:
  - rubocop-md

AllCops:
  TargetRubyVersion: 3.3
  DisabledByDefault: true
  SuggestExtensions: false
  Exclude:
    - '**/tmp/**/*'
    - '**/*.gemfile'
    - 'vendor/**/*'

# Prefer &&/|| over and/or.
Style/AndOr:
  Enabled: true

Layout/ClosingHeredocIndentation:
  Enabled: true

Layout/ClosingParenthesisIndentation:
  Enabled: true

# Align comments with method definitions.
Layout/CommentIndentation:
  Enabled: true

Layout/DefEndAlignment:
  Enabled: true

Layout/ElseAlignment:
  Enabled: true

# Align `end` with the matching keyword or starting expression except for
# assignments, where it should be aligned with the LHS.
Layout/EndAlignment:
  Enabled: true
  EnforcedStyleAlignWith: variable
  AutoCorrect: true

Layout/EndOfLine:
  Enabled: true

Layout/EmptyLineAfterMagicComment:
  Enabled: true

Layout/EmptyLinesAroundAccessModifier:
  Enabled: true
  EnforcedStyle: around

Layout/EmptyLinesAroundBlockBody:
  Enabled: true

# In a regular class definition, no empty lines around the body.
Layout/EmptyLinesAroundClassBody:
  Enabled: true

# In a regular method definition, no empty lines around the body.
Layout/EmptyLinesAroundMethodBody:
  Enabled: true

# In a regular module definition, no empty lines around the body.
Layout/EmptyLinesAroundModuleBody:
  Enabled: true

# Use Ruby >= 1.9 syntax for hashes. Prefer { a: :b } over { :a => :b }.
Style/HashSyntax:
  Enabled: true
  EnforcedShorthandSyntax: either

# Method definitions after `private` or `protected` isolated calls need one
# extra level of indentation.
Layout/IndentationConsistency:
  Enabled: true
  EnforcedStyle: indented_internal_methods
  Exclude:
    - '**/*.md'

# Two spaces, no tabs (for indentation).
Layout/IndentationWidth:
  Enabled: true

Layout/LeadingCommentSpace:
  Enabled: true

Layout/SpaceAfterColon:
  Enabled: true

Layout/SpaceAfterComma:
  Enabled: true

Layout/SpaceAfterSemicolon:
  Enabled: true

Layout/SpaceAroundEqualsInParameterDefault:
  Enabled: false

Layout/SpaceAroundKeyword:
  Enabled: true

Layout/SpaceAroundOperators:
  Enabled: true

Layout/SpaceBeforeComma:
  Enabled: true

Layout/SpaceBeforeComment:
  Enabled: true

Layout/SpaceBeforeFirstArg:
  Enabled: true

Style/DefWithParentheses:
  Enabled: true

# Defining a method with parameters needs parentheses.
Style/MethodDefParentheses:
  Enabled: true

Style/ExplicitBlockArgument:
  Enabled: true

Style/MapToHash:
  Enabled: true

Style/RedundantFreeze:
  Enabled: true

# Use `foo {}` not `foo{}`.
Layout/SpaceBeforeBlockBraces:
  Enabled: true

# Use `foo { bar }` not `foo {bar}`.
Layout/SpaceInsideBlockBraces:
  Enabled: true
  EnforcedStyleForEmptyBraces: space

# Use `{ a: 1 }` not `{a:1}`.
Layout/SpaceInsideHashLiteralBraces:
  Enabled: true

Layout/SpaceInsideParens:
  Enabled: true

# Check quotes usage according to lint rule below.
Style/StringLiterals:
  Enabled: true

# Detect hard tabs, no hard tabs.
Layout/IndentationStyle:
  Enabled: true

# Empty lines should not have any spaces.
Layout/TrailingEmptyLines:
  Enabled: true

# No trailing whitespace.
Layout/TrailingWhitespace:
  Enabled: true
# Use quotes for string literals when they are enough.
Style/RedundantPercentQ:
  Enabled: true

Lint/AmbiguousOperator:
  Enabled: true

Lint/AmbiguousRegexpLiteral:
  Enabled: true

Lint/Debugger:
  Enabled: true
  DebuggerRequires:
    - debug

Lint/DuplicateRequire:
  Enabled: true

Lint/DuplicateMagicComment:
  Enabled: true

Lint/DuplicateMethods:
  Enabled: true

Lint/ErbNewArguments:
  Enabled: true

Lint/EnsureReturn:
  Enabled: true

Lint/MissingCopEnableDirective:
  Enabled: true

# Use my_method(my_arg) not my_method( my_arg ) or my_method my_arg.
Lint/RequireParentheses:
  Enabled: true

Lint/RedundantCopDisableDirective:
  Enabled: true

Lint/RedundantCopEnableDirective:
  Enabled: true

Lint/RedundantRequireStatement:
  Enabled: true

Lint/RedundantStringCoercion:
  Enabled: true

Lint/RedundantSafeNavigation:
  Enabled: true

Lint/UriEscapeUnescape:
  Enabled: true

Lint/UselessAssignment:
  Enabled: true

Lint/DeprecatedClassMethods:
  Enabled: true

Lint/InterpolationCheck:
  Enabled: true

Lint/SafeNavigationChain:
  Enabled: true

Style/EvalWithLocation:
  Enabled: false

Style/ParenthesesAroundCondition:
  Enabled: true

Style/HashTransformKeys:
  Enabled: true

Style/HashTransformValues:
  Enabled: true

Style/RedundantBegin:
  Enabled: true

Style/RedundantReturn:
  Enabled: true
  AllowMultipleReturnValues: true

Style/RedundantRegexpEscape:
  Enabled: true

Style/Semicolon:
  Enabled: true
  AllowAsExpressionSeparator: true

# Prefer Foo.method over Foo::method
Style/ColonMethodCall:
  Enabled: true

Style/TrivialAccessors:
  Enabled: true

# Prefer a = b || c over a = b ? b : c
Style/RedundantCondition:
  Enabled: true

Style/RedundantDoubleSplatHashBraces:
  Enabled: true

Style/OpenStructUse:
  Enabled: true

Style/ArrayIntersect:
  Enabled: true

Markdown:
  # Whether to run RuboCop against non-valid snippets
  WarnInvalid: true
  # Whether to lint codeblocks without code attributes
  Autodetect: false


================================================
FILE: Appraisals
================================================
appraise 'ar-6.1' do
  gem 'activerecord', '~> 6.1.0'
  gem 'mysql2'
  gem 'pg'
  gem 'activerecord-sqlserver-adapter'
  gem 'trilogy'
  gem 'sqlite3', '~> 1.4'

  # required to suppress warnings
  gem 'bigdecimal'
  gem 'base64'
  gem 'mutex_m'
end

appraise 'ar-7.0' do
  gem 'activerecord', '~> 7.0.0'
  gem 'mysql2'
  gem 'pg'
  gem 'activerecord-sqlserver-adapter'
  gem 'trilogy'
  gem 'sqlite3', '~> 1.4'

  # required to suppress warnings
  gem 'bigdecimal'
  gem 'base64'
  gem 'mutex_m'
end

appraise 'ar-7.1' do
  gem 'activerecord', '~> 7.1.0'
  gem 'mysql2'
  gem 'pg'
  gem 'activerecord-sqlserver-adapter'
  gem 'trilogy'
  gem 'sqlite3', '~> 2.0'
end

appraise 'ar-7.2' do
  gem 'activerecord', '~> 7.2.0'
  gem 'mysql2'
  gem 'pg'
  gem 'activerecord-sqlserver-adapter'
  gem 'trilogy'
  gem 'sqlite3', '~> 2.0'
end

appraise 'ar-8.0' do
  gem 'activerecord', '~> 8.0.0'
  gem 'mysql2'
  gem 'pg'
  gem 'activerecord-sqlserver-adapter'
  gem 'trilogy'
  gem 'sqlite3', '~> 2.1'
end


================================================
FILE: ChangeLog.md
================================================
# Change Log
## 1.0.0
* Added support for ActiveRecord 7.1.
* Redesigned the proxy chain to accommodate internal structure changes in ActiveRecord 7.1.
* Introduced integration tests using real databases, allowing for more robust testing of functionality with MySQL, PostgreSQL, SQLite, and SQLServer.
  See: https://github.com/cookpad/arproxy/issues/30

## 0.2.9
* Support ActiveRecord 7.0 (#21)
  Thanks to @r7kamura

## 0.2.8
* Support postgresql adapter (#19)
  Thanks to @jhnvz

## 0.2.7
* Support sqlserver adapter (#16)
  Note that it supports `AR::B.connection.execute` but not `exec_query` yet.
  See: https://github.com/cookpad/arproxy/pull/16
  Thanks to @takanamito

## 0.2.6
* Support sqlite3 adapter (#15)
  Thanks to @hakatashi

## 0.2.5
* Fix against warnings around `::` in void context (#12)

## 0.2.4
* Fix against warnings around uninitialized instance variables (#12)
  Thanks to @amatsuda

## 0.2.3
* Set Arproxy::Config#adapter from database.yml automatically (#11)
  Thanks to @k0kubun

## 0.2.2
* Start supporting activerecord-5.0 and stop 3.2-4.1

## 0.2.1
* Make ProxyChain thread-safe (#7)
  Thanks to @saidie

## 0.2.0
* Arproxy plugin: an easy way to make reusable proxies as gems (#6)
  Thanks to @winebarrel

## 0.1.3
* Silence some deprecation warnings (#1)
  Thanks to @amatsuda

* Implement Arproxy.#enable? and Arproxy.#reenable!

## 0.1.2
* Bug fix: An error occoured when call disable! after disable!

* config.adapter accepts not only String but also Class

## 0.1.1
* First Release


================================================
FILE: Dockerfile
================================================
FROM ruby:3.3

WORKDIR /app

COPY lib lib
COPY spec spec
COPY gemfiles gemfiles

COPY arproxy.gemspec arproxy.gemspec
COPY Gemfile Gemfile
COPY Appraisals Appraisals
COPY .env .env
COPY .rspec .rspec

RUN apt update
RUN apt install --no-install-recommends -y build-essential freetds-dev

RUN bundle install
RUN bundle exec appraisal install

RUN mkdir -p /app/db/mysql
RUN ln -s /var/lib/mysql /app/db/mysql/data

# dummy command to keep the container running
CMD ["sleep", "infinity"]


================================================
FILE: Gemfile
================================================
source 'https://rubygems.org'

gemspec

gem 'rspec'
gem 'appraisal'
gem 'dotenv', require: 'dotenv/load'
gem 'rubocop'
gem 'rubocop-md'


================================================
FILE: LICENSE.txt
================================================
The MIT License (MIT)

Copyright (c) 2016 Issei Naruta

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.


================================================
FILE: README.md
================================================
[![Integration tests](https://github.com/cookpad/arproxy/actions/workflows/integration_tests.yml/badge.svg)](https://github.com/cookpad/arproxy/actions/workflows/integration_tests.yml)
[![Unit tests](https://github.com/cookpad/arproxy/actions/workflows/unit_tests.yml/badge.svg)](https://github.com/cookpad/arproxy/actions/workflows/unit_tests.yml)
[![Rubocop](https://github.com/cookpad/arproxy/actions/workflows/rubocop.yml/badge.svg)](https://github.com/cookpad/arproxy/actions/workflows/rubocop.yml)

# Arproxy
Arproxy is a library that can intercept SQL queries executed by ActiveRecord to log them or modify the queries themselves.

# Getting Started
Create your custom proxy and add its configuration in your Rails' `config/initializers/` directory:

```ruby
class QueryTracer < Arproxy::Proxy
  def execute(sql, context)
    Rails.logger.debug sql
    Rails.logger.debug caller(1).join("\n")
    super(sql, context)
  end
end

Arproxy.configure do |config|
  config.adapter = 'mysql2' # A DB Adapter name which is used in your database.yml
  config.use QueryTracer
end
Arproxy.enable!
```

Then you can see the backtrace of SQLs in the Rails' log.

```ruby
# In your Rails code
MyTable.where(id: id).limit(1) # => The SQL and the backtrace appear in the log
```

## What the `context` argument is

`context` is an instance of `Arproxy::QueryContext` and contains values that are passed from Arproxy to the Database Adapter.
`context` is a set of values used when calling Database Adapter methods, and you don't need to use the `context` values directly.
However, you must always pass `context` to `super` like `super(sql, context)`.

For example, let's look at the Mysql2Adapter implementation. When executing a query in Mysql2Adapter, the `Mysql2Adapter#internal_exec_query` method is called internally.

```
# https://github.com/rails/rails/blob/v7.1.0/activerecord/lib/active_record/connection_adapters/mysql2/database_statements.rb#L21
def internal_exec_query(sql, name = "SQL", binds = [], prepare: false, async: false) # :nodoc:
  # ...
end
```

In 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`:

| member           | example value                      |
|------------------|------------------------------------|
| `context.name`   | `"SQL"`                            |
| `context.binds`  | `[]`                               |
| `context.kwargs` | `{ prepare: false, async: false }` |

You can modify the values of `context` in the proxy, but do so after understanding the implementation of the Database Adapter.

### `context.name`

In the Rails' log you may see queries like this:

```
User Load (22.6ms)  SELECT `users`.* FROM `users` WHERE `users`.`name` = 'Issei Naruta'
```

Then `"User Load"` is the `context.name`.

# Architecture
Without Arproxy:

```
+-------------------------+        +------------------+
| ActiveRecord::Base#find |--SQL-->| Database Adapter |
+-------------------------+        +------------------+
```

With Arproxy:

```ruby
Arproxy.configure do |config|
  config.adapter = 'mysql2'
  config.use MyProxy1
  config.use MyProxy2
end
```

```
+-------------------------+        +----------+   +----------+   +------------------+
| ActiveRecord::Base#find |--SQL-->| MyProxy1 |-->| MyProxy2 |-->| Database Adapter |
+-------------------------+        +----------+   +----------+   +------------------+
```

# Supported Environments

Arproxy supports the following databases and adapters:

- MySQL
  - `mysql2`, `trilogy`
- PostgreSQL
  - `pg`
- SQLite
  - `sqlite3`
- SQLServer
  - `activerecord-sqlserver-adapter`

We have tested with the following versions of Ruby, ActiveRecord, and databases:

- Ruby
  - `3.0`, `3.1`, `3.2`, `3.3`
- ActiveRecord
  - `6.1`, `7.0`, `7.1`, `7.2`, `8.0`
- MySQL
  - `9.0`
- PostgreSQL
  - `17`
- SQLite
  - `3.x` (not specified)
- SQLServer
  - `2022`

# Examples

## Adding Comments to SQLs

```ruby
class CommentAdder < Arproxy::Proxy
  def execute(sql, context)
    sql += ' /*this_is_comment*/'
    super(sql, context)
  end
end
```

## Slow Query Logger

```ruby
class SlowQueryLogger < Arproxy::Proxy
  def initialize(slow_ms)
    @slow_ms = slow_ms
  end

  def execute(sql, context)
    result = nil
    ms = Benchmark.ms { result = super(sql, context) }
    if ms >= @slow_ms
      Rails.logger.info "Slow(#{ms.to_i}ms): #{sql}"
    end
    result
  end
end

Arproxy.configure do |config|
  config.use SlowQueryLogger, 1000
end
```

## Readonly Access

If you don't call `super` in the proxy, you can block the query execution.

```ruby
class Readonly < Arproxy::Proxy
  def execute(sql, context)
    if sql =~ /^(SELECT|SET|SHOW|DESCRIBE)\b/
      super(sql, context)
    else
      Rails.logger.warn "#{context.name} (BLOCKED) #{sql}"
      nil
    end
  end
end
```

# Use plug-in

```ruby
# any_gem/lib/arproxy/plugin/my_plugin
module Arproxy::Plugin
  class MyPlugin < Arproxy::Proxy
    Arproxy::Plugin.register(:my_plugin, self)

    def execute(sql, context)
      # Any processing
      # ...
      super(sql, context)
    end
  end
end
```

```ruby
Arproxy.configure do |config|
  config.plugin :my_plugin
end
```

# Upgrading guide from v0.x to v1

See [UPGRADING.md](UPGRADING.md)

# Development

## Setup

```
$ git clone https://github.com/cookpad/arproxy.git
$ cd arproxy
$ bundle install
$ bundle exec appraisal install
```

## Run test

To run all tests with all supported versions of ActiveRecord:

```
$ docker compose up -d
$ bundle exec appraisal rspec
```

To run tests for a specific version of ActiveRecord:

```
$ bundle exec appraisal ar_7.1 rspec
or
$ BUNDLE_GEMFILE=gemfiles/ar_7.1.gemfile bundle exec rspec
```

# License
Arproxy is released under the MIT license:
* www.opensource.org/licenses/MIT


================================================
FILE: Rakefile
================================================
require 'bundler/gem_tasks'
require 'rspec/core/rake_task'
require 'rubocop/rake_task'

RSpec::Core::RakeTask.new(:spec)

RuboCop::RakeTask.new(:rubocop)

task default: [:spec, :rubocop]


================================================
FILE: UPGRADING.md
================================================
# Upgrading guide from v0.x to v1

The proxy specification has changed from v0.x to v1 and is not backward compatible.
The base class for proxies has changed from `Arproxy::Base` to `Arproxy::Proxy`.
Also, the arguments to `#execute` have changed from `sql, name=nil, **kwargs` to `sql, context`.

```ruby
# ~> v0.2.9
class MyProxy < Arproxy::Base
  def execute(sql, name=nil, **kwargs)
    super
  end
end

# >= v1.0.0
class MyProxy < Arproxy::Proxy
  def execute(sql, context)
    super
  end
end
```

There are no other backward incompatible changes besides the above changes in proxy base class and arguments.


================================================
FILE: arproxy.gemspec
================================================
$:.push File.expand_path('../lib', __FILE__)
require 'arproxy/version'

Gem::Specification.new do |spec|
  spec.name              = 'arproxy'
  spec.version           = Arproxy::VERSION
  spec.summary           = 'A proxy layer between ActiveRecord and database adapters'
  spec.description       = 'Arproxy is a proxy layer that allows hooking into ActiveRecord query execution and injecting custom processing'
  spec.files             = Dir.glob('lib/**/*.rb')
  spec.author            = 'Issei Naruta'
  spec.email             = 'mimitako@gmail.com'
  spec.homepage          = 'https://github.com/cookpad/arproxy'
  spec.license           = 'MIT'
  spec.require_paths     = ['lib']

  spec.add_dependency 'activerecord', '>= 6.1'
end


================================================
FILE: compose-ci.yaml
================================================
services:
  ruby:
    build:
      context: .
      dockerfile: Dockerfile
    environment:
      MYSQL_HOST: mysql
      MYSQL_PORT: 3306
      POSTGRES_HOST: postgres
      POSTGRES_PORT: 5432
      MSSQL_HOST: sqlserver
      MSSQL_PORT: 1433
    volumes:
      - mysql-data:/var/lib/mysql

  mysql:
    image: mysql:9.0
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: rootpassword
      MYSQL_DATABASE: ${ARPROXY_DB_DATABASE}
      MYSQL_USER: ${ARPROXY_DB_USER}
      MYSQL_PASSWORD: ${ARPROXY_DB_PASSWORD}
    volumes:
      - ./db/mysql/my.cnf:/etc/mysql/conf.d/my.cnf
      - mysql-data:/var/lib/mysql
    ports:
      - "23306:3306"

  postgres:
    image: postgres:17
    restart: always
    environment:
      POSTGRES_DB: ${ARPROXY_DB_DATABASE}
      POSTGRES_USER: ${ARPROXY_DB_USER}
      POSTGRES_PASSWORD: ${ARPROXY_DB_PASSWORD}
    ports:
      - "25432:5432"

  sqlserver:
    image: mcr.microsoft.com/mssql/server:2022-latest
    restart: always
    environment:
      ACCEPT_EULA: Y
      MSSQL_SA_PASSWORD: R00tPassword12!
    ports:
      - "21433:1433"
    healthcheck:
      test: ["CMD-SHELL", "/opt/mssql-tools18/bin/sqlcmd -C -S localhost -U sa -P R00tPassword12! -Q 'SELECT 1' || exit 1"]
      interval: 5s
      retries: 10
      start_period: 10s

  sqlserver-init:
    image: mcr.microsoft.com/mssql/server:2022-latest
    volumes:
      - ./db/sqlserver/init.sql:/init.sql
    command: /opt/mssql-tools18/bin/sqlcmd -C -S sqlserver -U sa -P R00tPassword12! -d master -i /init.sql
    depends_on:
      sqlserver:
        condition: service_healthy

volumes:
  mysql-data:


================================================
FILE: compose.yaml
================================================
services:
  mysql:
    image: mysql:9.0
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: rootpassword
      MYSQL_DATABASE: ${ARPROXY_DB_DATABASE}
      MYSQL_USER: ${ARPROXY_DB_USER}
      MYSQL_PASSWORD: ${ARPROXY_DB_PASSWORD}
    volumes:
      - ./db/mysql/my.cnf:/etc/mysql/conf.d/my.cnf
      - ./db/mysql/data:/var/lib/mysql
    ports:
      - "23306:3306"

  postgres:
    image: postgres:17
    restart: always
    environment:
      POSTGRES_DB: ${ARPROXY_DB_DATABASE}
      POSTGRES_USER: ${ARPROXY_DB_USER}
      POSTGRES_PASSWORD: ${ARPROXY_DB_PASSWORD}
    ports:
      - "25432:5432"

  sqlserver:
    image: mcr.microsoft.com/mssql/server:2022-latest
    restart: always
    environment:
      ACCEPT_EULA: Y
      MSSQL_SA_PASSWORD: R00tPassword12!
    ports:
      - "21433:1433"
    healthcheck:
      test: ["CMD-SHELL", "/opt/mssql-tools18/bin/sqlcmd -C -S localhost -U sa -P R00tPassword12! -Q 'SELECT 1' || exit 1"]
      interval: 5s
      retries: 10
      start_period: 10s

  sqlserver-init:
    image: mcr.microsoft.com/mssql/server:2022-latest
    volumes:
      - ./db/sqlserver/init.sql:/init.sql
    command: /opt/mssql-tools18/bin/sqlcmd -C -S sqlserver -U sa -P R00tPassword12! -d master -i /init.sql
    depends_on:
      sqlserver:
        condition: service_healthy


================================================
FILE: db/mysql/my.cnf
================================================
[mysqld]
tls-version=TLSv1.2,TLSv1.3
auto_generate_certs = ON


================================================
FILE: db/sqlserver/init.sql
================================================
CREATE DATABASE arproxy_test;
GO
USE arproxy_test;
GO
CREATE LOGIN arproxy WITH PASSWORD = '4rpr0*y#2024';
GO
CREATE USER arproxy FOR LOGIN arproxy;
GO
ALTER ROLE db_owner ADD MEMBER arproxy;
GO


================================================
FILE: gemfiles/ar_6.1.gemfile
================================================
# This file was generated by Appraisal

source "https://rubygems.org"

gem "rspec"
gem "appraisal"
gem "dotenv", require: "dotenv/load"
gem "rubocop"
gem "rubocop-md"
gem "activerecord", "~> 6.1.0"
gem "mysql2"
gem "pg"
gem "activerecord-sqlserver-adapter"
gem "trilogy"
gem "sqlite3", "~> 1.4"
gem "bigdecimal"
gem "base64"
gem "mutex_m"

gemspec path: "../"


================================================
FILE: gemfiles/ar_7.0.gemfile
================================================
# This file was generated by Appraisal

source "https://rubygems.org"

gem "rspec"
gem "appraisal"
gem "dotenv", require: "dotenv/load"
gem "rubocop"
gem "rubocop-md"
gem "activerecord", "~> 7.0.0"
gem "mysql2"
gem "pg"
gem "activerecord-sqlserver-adapter"
gem "trilogy"
gem "sqlite3", "~> 1.4"
gem "bigdecimal"
gem "base64"
gem "mutex_m"

gemspec path: "../"


================================================
FILE: gemfiles/ar_7.1.gemfile
================================================
# This file was generated by Appraisal

source "https://rubygems.org"

gem "rspec"
gem "appraisal"
gem "dotenv", require: "dotenv/load"
gem "rubocop"
gem "rubocop-md"
gem "activerecord", "~> 7.1.0"
gem "mysql2"
gem "pg"
gem "activerecord-sqlserver-adapter"
gem "trilogy"
gem "sqlite3", "~> 2.0"

gemspec path: "../"


================================================
FILE: gemfiles/ar_7.2.gemfile
================================================
# This file was generated by Appraisal

source "https://rubygems.org"

gem "rspec"
gem "appraisal"
gem "dotenv", require: "dotenv/load"
gem "rubocop"
gem "rubocop-md"
gem "activerecord", "~> 7.2.0"
gem "mysql2"
gem "pg"
gem "activerecord-sqlserver-adapter"
gem "trilogy"
gem "sqlite3", "~> 2.0"

gemspec path: "../"


================================================
FILE: gemfiles/ar_8.0.gemfile
================================================
# This file was generated by Appraisal

source "https://rubygems.org"

gem "rspec"
gem "appraisal"
gem "dotenv", require: "dotenv/load"
gem "rubocop"
gem "rubocop-md"
gem "activerecord", "~> 8.0.0"
gem "mysql2"
gem "pg"
gem "activerecord-sqlserver-adapter"
gem "trilogy"
gem "sqlite3", "~> 2.1"

gemspec path: "../"


================================================
FILE: integration_test/Gemfile
================================================
source 'https://rubygems.org'

gem 'arproxy', path: '..'
gem 'rspec'
gem 'appraisal'
gem 'mysql2'
gem 'pg'


================================================
FILE: integration_test/docker-compose.yml
================================================
version: '3'

services:
  mysql:
    image: mysql:9.0
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: rootpassword
      MYSQL_DATABASE: arproxy_test
      MYSQL_USER: arproxy
      MYSQL_PASSWORD: password
    ports:
      - "23306:3306"

  postgres:
    image: postgres:16
    restart: always
    environment:
      POSTGRES_DB: arproxy_test
      POSTGRES_USER: arproxy
      POSTGRES_PASSWORD: password
    ports:
      - "25432:5432"


================================================
FILE: integration_test/gemfiles/ar_6.1.gemfile
================================================
# This file was generated by Appraisal

source "https://rubygems.org"

gem "arproxy", path: "../.."
gem "rspec"
gem "appraisal"
gem "mysql2"
gem "pg"
gem "activerecord", "~> 6.1.0"


================================================
FILE: integration_test/gemfiles/ar_7.0.gemfile
================================================
# This file was generated by Appraisal

source "https://rubygems.org"

gem "arproxy", path: "../.."
gem "rspec"
gem "appraisal"
gem "mysql2"
gem "pg"
gem "activerecord", "~> 7.0.0"


================================================
FILE: integration_test/gemfiles/ar_7.1.gemfile
================================================
# This file was generated by Appraisal

source "https://rubygems.org"

gem "arproxy", path: "../.."
gem "rspec"
gem "appraisal"
gem "mysql2"
gem "pg"
gem "activerecord", "~> 7.1.0"


================================================
FILE: integration_test/spec/mysql2_spec.rb
================================================
require_relative 'spec_helper'
require 'mysql2'

context 'MySQL' do
  before(:all) do
    ActiveRecord::Base.establish_connection(
      adapter: 'mysql2',
      host: ENV.fetch('MYSQL_HOST', '127.0.0.1'),
      port: ENV.fetch('MYSQL_PORT', '23306').to_i,
      database: 'arproxy_test',
      username: 'arproxy',
      password: 'password'
    )

    Arproxy.configure do |config|
      config.adapter = 'mysql2'
      config.use HelloProxy
      config.use QueryLogger
    end
    Arproxy.enable!

    ActiveRecord::Base.connection.create_table :products, force: true do |t|
      t.string :name
      t.integer :price
    end

    Product.create(name: 'apple', price: 100)
    Product.create(name: 'banana', price: 200)
    Product.create(name: 'orange', price: 300)
  end

  after(:all) do
    ActiveRecord::Base.connection.drop_table :products
    ActiveRecord::Base.connection.close
    Arproxy.disable!
  end

  before(:each) do
    QueryLogger.reset!
  end

  it do
    expect(QueryLogger.log.size).to eq(0)

    expect(Product.count).to eq(3)
    expect(Product.first.name).to eq('apple')

    expect(QueryLogger.log.size).to eq(2)
    expect(QueryLogger.log[0]).to eq('SELECT COUNT(*) FROM `products` -- Hello Arproxy!')
    expect(QueryLogger.log[1]).to eq('SELECT `products`.* FROM `products` ORDER BY `products`.`id` ASC LIMIT 1 -- Hello Arproxy!')
  end
end


================================================
FILE: integration_test/spec/postgresql_spec.rb
================================================
require_relative 'spec_helper'
require 'pg'

context 'PostgreSQL' do
  before(:all) do
    ActiveRecord::Base.establish_connection(
      adapter: 'postgresql',
      host: ENV.fetch('POSTGRES_HOST', '127.0.0.1'),
      port: ENV.fetch('POSTGRES_PORT', '25432').to_i,
      database: 'arproxy_test',
      username: 'arproxy',
      password: 'password'
    )

    Arproxy.configure do |config|
      config.adapter = 'postgresql'
      config.use HelloProxy
      config.use QueryLogger
    end
    Arproxy.enable!

    ActiveRecord::Base.connection.create_table :products, force: true do |t|
      t.string :name
      t.integer :price
    end

    Product.create(name: 'apple', price: 100)
    Product.create(name: 'banana', price: 200)
    Product.create(name: 'orange', price: 300)
  end

  after(:all) do
    ActiveRecord::Base.connection.drop_table :products
    ActiveRecord::Base.connection.close
    Arproxy.disable!
  end

  before(:each) do
    QueryLogger.reset!
  end

  it do
    expect(QueryLogger.log.size).to eq(0)

    expect(Product.count).to eq(3)
    expect(Product.first.name).to eq('apple')

    expect(QueryLogger.log.size).to eq(2)
    expect(QueryLogger.log[0]).to eq('SELECT COUNT(*) FROM `products` -- Hello Arproxy!')
    expect(QueryLogger.log[1]).to eq('SELECT `products`.* FROM `products` ORDER BY `products`.`id` ASC LIMIT 1 -- Hello Arproxy!')
  end
end


================================================
FILE: integration_test/spec/spec_helper.rb
================================================
require 'arproxy'
require 'active_record'

class Product < ActiveRecord::Base
end

class QueryLogger < Arproxy::Base
  def execute(sql, name = nil)
    @@log ||= []
    @@log << sql
    puts "QueryLogger: #{sql}"
    super
  end

  def self.log
    @@log
  end

  def self.reset!
    @@log = []
  end
end

class HelloProxy < Arproxy::Base
  def execute(sql, name = nil)
    super("#{sql} -- Hello Arproxy!", name)
  end
end


================================================
FILE: lib/arproxy/base.rb
================================================
module Arproxy
  # This class is no longer used since Arproxy v1.
  class Base
  end
end


================================================
FILE: lib/arproxy/config.rb
================================================
require 'active_record'
require 'active_record/base'
require 'arproxy/base'
require 'arproxy/error'

module Arproxy
  class Config
    attr_accessor :adapter, :logger
    attr_reader :proxies

    def initialize
      @proxies = []
      if defined?(Rails)
        @adapter = Rails.application.config_for(:database)['adapter']
      end
    end

    def use(proxy_class, *options)
      if proxy_class.is_a?(Class) && proxy_class.ancestors.include?(Arproxy::Base)
        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"
      end

      ::Arproxy.logger.debug("Arproxy: Mounting #{proxy_class.inspect} (#{options.inspect})")
      @proxies << [proxy_class, options]
    end

    def plugin(name, *options)
      plugin_class = Plugin.get(name)

      if plugin_class.is_a?(Class) && plugin_class.ancestors.include?(Arproxy::Base)
        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"
      end

      use(plugin_class, *options)
    end

    def adapter_class
      raise Arproxy::Error, 'config.adapter must be set' unless @adapter
      case @adapter
      when String, Symbol
        eval "::ActiveRecord::ConnectionAdapters::#{camelized_adapter_name}Adapter"
      when Class
        @adapter
      else
        raise Arproxy::Error, "unexpected config.adapter: #{@adapter}"
      end
    end

    private

      def camelized_adapter_name
        adapter_name = @adapter.to_s.split('_').map(&:capitalize).join

        case adapter_name
        when 'Sqlite3'
          'SQLite3'
        when 'Sqlserver'
          'SQLServer'
        when 'Postgresql'
          'PostgreSQL'
        else
          adapter_name
        end
      end
  end
end


================================================
FILE: lib/arproxy/connection_adapter_patch.rb
================================================
module Arproxy
  class ConnectionAdapterPatch
    attr_reader :adapter_class

    def initialize(adapter_class)
      @adapter_class = adapter_class
      @applied_patches = Set.new
    end

    def self.register_patches(adapter_name, patches: [], binds_patches: [])
      @@patches ||= {}
      @@patches[adapter_name] = {
        patches: patches,
        binds_patches: binds_patches
      }
    end

    if ActiveRecord.version >= Gem::Version.new('8.0')
      register_patches('Mysql2', patches: [], binds_patches: [:raw_execute])
      register_patches('Trilogy', patches: [], binds_patches: [:raw_execute])
    elsif ActiveRecord.version >= Gem::Version.new('7.0')
      register_patches('Mysql2', patches: [:raw_execute], binds_patches: [])
      register_patches('Trilogy', patches: [:raw_execute], binds_patches: [])
    else
      register_patches('Mysql2', patches: [:execute], binds_patches: [])
      register_patches('Trilogy', patches: [:raw_execute], binds_patches: [])
    end

    if ActiveRecord.version >= Gem::Version.new('8.0')
      register_patches('PostgreSQL', patches: [], binds_patches: [:raw_execute])
      register_patches('SQLServer', patches: [], binds_patches: [:raw_execute])
      register_patches('SQLite', patches: [], binds_patches: [:raw_execute])
    elsif ActiveRecord.version >= Gem::Version.new('7.1')
      register_patches('PostgreSQL', patches: [:raw_execute], binds_patches: [:exec_no_cache, :exec_cache])
      register_patches('SQLServer', patches: [:raw_execute], binds_patches: [:internal_exec_query])
      register_patches('SQLite', patches: [:raw_execute], binds_patches: [:internal_exec_query])
    else
      register_patches('PostgreSQL', patches: [:execute], binds_patches: [:exec_no_cache, :exec_cache])
      register_patches('SQLServer', patches: [:execute], binds_patches: [:exec_query])
      register_patches('SQLite', patches: [:execute], binds_patches: [:exec_query])
    end

    def enable!
      patches = @@patches[adapter_class::ADAPTER_NAME]
      if patches
        patches[:patches]&.each do |patch|
          apply_patch patch
        end
        patches[:binds_patches]&.each do |binds_patch|
          apply_patch_binds binds_patch
        end
      else
        raise Arproxy::Error, "Unexpected connection adapter: patches not registered for #{adapter_class&.name}"
      end
      ::Arproxy.logger.debug("Arproxy: Enabled (#{adapter_class::ADAPTER_NAME})")
    end

    def disable!
      @applied_patches.dup.each do |target_method|
        adapter_class.class_eval do
          if instance_methods.include?(:"#{target_method}_with_arproxy")
            alias_method target_method, :"#{target_method}_without_arproxy"
            remove_method :"#{target_method}_with_arproxy"
          end
        end
        @applied_patches.delete(target_method)
      end
      ::Arproxy.logger.debug("Arproxy: Disabled (#{adapter_class::ADAPTER_NAME})")
    end

    private

      def apply_patch(target_method)
        return if @applied_patches.include?(target_method)
        adapter_class.class_eval do
          raw_execute_method_name = :"#{target_method}_without_arproxy"
          patched_execute_method_name = :"#{target_method}_with_arproxy"
          break if instance_methods.include?(patched_execute_method_name)
          define_method(patched_execute_method_name) do |sql, name=nil, **kwargs|
            context = QueryContext.new(
              raw_connection: self,
              execute_method_name: raw_execute_method_name,
              with_binds: false,
              name: name,
              kwargs: kwargs,
            )
            ::Arproxy.proxy_chain.head.execute(sql, context)
          end
          alias_method raw_execute_method_name, target_method
          alias_method target_method, patched_execute_method_name
        end
        @applied_patches << target_method
      end

      def apply_patch_binds(target_method)
        return if @applied_patches.include?(target_method)
        adapter_class.class_eval do
          raw_execute_method_name = :"#{target_method}_without_arproxy"
          patched_execute_method_name = :"#{target_method}_with_arproxy"
          break if instance_methods.include?(patched_execute_method_name)
          define_method(patched_execute_method_name) do |sql, name=nil, binds=[], **kwargs|
            context = QueryContext.new(
              raw_connection: self,
              execute_method_name: raw_execute_method_name,
              with_binds: true,
              name: name,
              binds: binds,
              kwargs: kwargs,
            )
            ::Arproxy.proxy_chain.head.execute(sql, context)
          end
          alias_method raw_execute_method_name, target_method
          alias_method target_method, patched_execute_method_name
        end
        @applied_patches << target_method
      end
  end
end


================================================
FILE: lib/arproxy/error.rb
================================================
module Arproxy
  class Error < Exception
  end
end


================================================
FILE: lib/arproxy/plugin.rb
================================================
module Arproxy
  module Plugin
    class << self
      def register(name, klass)
        name = name.to_s
        @plugins ||= {}

        if @plugins.has_key?(name)
          raise Arproxy::Error, "Plugin has already been registered: #{name}"
        end

        @plugins[name] = klass
      end

      def get(name)
        name = name.to_s
        require "arproxy/plugin/#{name}"
        plugin = @plugins[name]

        unless plugin
          raise Arproxy::Error, "Plugin is not found: #{name}"
        end

        plugin
      end
    end
  end
end


================================================
FILE: lib/arproxy/proxy.rb
================================================
require 'arproxy/query_context'

module Arproxy
  class Proxy
    attr_accessor :context, :next_proxy

    def execute(sql, context)
      unless context.instance_of?(QueryContext)
        raise Arproxy::Error, "`context` is expected a `Arproxy::QueryContext` but got `#{context.class}`"
      end

      next_proxy.execute(sql, context)
    end
  end
end


================================================
FILE: lib/arproxy/proxy_chain.rb
================================================
require 'arproxy/proxy_chain_tail'
require 'arproxy/connection_adapter_patch'

module Arproxy
  class ProxyChain
    attr_reader :head, :tail, :patch

    def initialize(config, patch)
      @config = config
      @patch = patch
      setup
    end

    def setup
      @tail = ProxyChainTail.new
      @head = @config.proxies.reverse.inject(@tail) do |next_proxy, proxy_config|
        cls, options = proxy_config
        proxy = cls.new(*options)
        proxy.next_proxy = next_proxy
        proxy
      end
    end
    private :setup

    def reenable!
      disable!
      setup
      enable!
    end

    def enable!
      @patch.enable!
    end

    def disable!
      @patch.disable!
    end
  end
end


================================================
FILE: lib/arproxy/proxy_chain_tail.rb
================================================
require 'arproxy/proxy'
require 'arproxy/query_context'

module Arproxy
  class ProxyChainTail < Proxy
    def execute(sql, context)
      unless context.instance_of?(QueryContext)
        raise Arproxy::Error, "`context` is expected a `Arproxy::QueryContext` but got `#{context.class}`"
      end

      if context.with_binds?
        context.raw_connection.send(context.execute_method_name, sql, context.name, context.binds, **context.kwargs)
      else
        context.raw_connection.send(context.execute_method_name, sql, context.name, **context.kwargs)
      end
    end
  end
end


================================================
FILE: lib/arproxy/query_context.rb
================================================
module Arproxy
  class QueryContext
    attr_accessor :raw_connection, :execute_method_name, :with_binds, :name, :binds, :kwargs

    def initialize(raw_connection:, execute_method_name:, with_binds:, name: nil, binds: [], kwargs: {})
      @raw_connection = raw_connection
      @execute_method_name = execute_method_name
      @with_binds = with_binds
      @name = name
      @binds = binds
      @kwargs = kwargs
    end

    def with_binds?
      !!@with_binds
    end
  end
end


================================================
FILE: lib/arproxy/version.rb
================================================
module Arproxy
  VERSION = '1.0.0'
end


================================================
FILE: lib/arproxy.rb
================================================
require 'logger'
require 'arproxy/base'
require 'arproxy/config'
require 'arproxy/connection_adapter_patch'
require 'arproxy/proxy_chain'
require 'arproxy/error'
require 'arproxy/plugin'

module Arproxy
  @config = nil
  @enabled = nil
  @patch = nil

  module_function

    def clear_configuration
      @config = nil
    end

    def configure
      @config ||= Config.new
      yield @config
    end

    def enable!
      if enable?
        Arproxy.logger.warn 'Arproxy has already been enabled'
        return
      end

      unless @config
        raise Arproxy::Error, 'Arproxy has not been configured'
      end

      @patch = ConnectionAdapterPatch.new(@config.adapter_class)
      @proxy_chain = ProxyChain.new(@config, @patch)
      @proxy_chain.enable!

      @enabled = true
    end

    def disable!
      unless enable?
        Arproxy.logger.warn 'Arproxy is not enabled yet'
        return
      end

      if @proxy_chain
        @proxy_chain.disable!
        @proxy_chain = nil
      end

      @enabled = false
    end

    def enable?
      !!@enabled
    end

    def reenable!
      if enable?
        @proxy_chain.reenable!
      else
        enable!
      end
    end

    def logger
      @logger ||= @config && @config.logger ||
                      defined?(::Rails) && ::Rails.logger ||
                      ::Logger.new(STDOUT)
    end

    def proxy_chain
      @proxy_chain
    end

    def connection_adapter_patch
      @patch
    end
end


================================================
FILE: spec/integration/mysql2_spec.rb
================================================
require_relative './spec_helper'

context "MySQL (AR#{ar_version})" do
  before(:all) do
    host = ENV.fetch('MYSQL_HOST', '127.0.0.1')
    port = ENV.fetch('MYSQL_PORT', '23306').to_i
    wait_for_db(host, port)

    ActiveRecord::Base.establish_connection(
      adapter: 'mysql2',
      host: host,
      port: port,
      database: 'arproxy_test',
      username: 'arproxy',
      password: ENV.fetch('ARPROXY_DB_PASSWORD')
    )

    Arproxy.configure do |config|
      config.adapter = 'mysql2'
      config.use HelloProxy
      config.plugin :query_logger
    end
    Arproxy.enable!
  end

  after(:all) do
    cleanup_activerecord
    Arproxy.disable!
    Arproxy.clear_configuration
  end

  it_behaves_like 'Arproxy does not break the original ActiveRecord functionality'
  it_behaves_like 'Custom proxies work expectedly'
end


================================================
FILE: spec/integration/postgresql_spec.rb
================================================
require_relative './spec_helper'

context "PostgreSQL (AR#{ar_version})" do
  before(:all) do
    host = ENV.fetch('POSTGRES_HOST', '127.0.0.1')
    port = ENV.fetch('POSTGRES_PORT', '25432').to_i
    wait_for_db(host, port)

    ActiveRecord::Base.establish_connection(
      adapter: 'postgresql',
      host: host,
      port: port,
      database: 'arproxy_test',
      username: 'arproxy',
      password: ENV.fetch('ARPROXY_DB_PASSWORD')
    )

    Arproxy.configure do |config|
      config.adapter = 'postgresql'
      config.use HelloProxy
      config.plugin :query_logger
    end
    Arproxy.enable!
  end

  after(:all) do
    cleanup_activerecord
    Arproxy.disable!
    Arproxy.clear_configuration
  end

  it_behaves_like 'Arproxy does not break the original ActiveRecord functionality'
  it_behaves_like 'Custom proxies work expectedly'
end


================================================
FILE: spec/integration/shared_examples/active_record_functions.rb
================================================
RSpec.shared_examples 'Arproxy does not break the original ActiveRecord functionality' do
  before do
    # CREATE
    ActiveRecord::Base.connection.create_table :products, force: true do |t|
      t.string :name
      t.integer :price
    end
    # INSERT
    Product.create!(name: 'apple', price: 100)
    Product.create!(name: 'banana', price: 200)
    Product.create!(name: 'orange', price: 300)
  end

  after(:all) do
    ActiveRecord::Base.connection.drop_table :products
  end

  context 'SELECT' do
    it { expect(Product.where(name: ['apple', 'orange']).sum(:price)).to eq(400) }
  end

  context 'UPDATE' do
    it do
      expect {
        Product.where(name: 'banana').update_all(price: 1000)
      }.to change {
        Product.find_by!(name: 'banana').price
      }.from(200).to(1000)
    end
  end

  context 'DELETE' do
    it do
      expect {
        Product.where(name: 'banana').delete_all
      }.to change {
        Product.where(name: 'banana').exists?
      }.from(true).to(false)
    end
  end
end


================================================
FILE: spec/integration/shared_examples/custom_proxies.rb
================================================
class Product < ActiveRecord::Base
end

class HelloLegacyProxy < Arproxy::Base
  def execute(sql, name = nil)
    super("#{sql} -- Hello Legacy Arproxy!", name)
  end
end

class HelloProxy < Arproxy::Proxy
  def execute(sql, context)
    super("#{sql} -- Hello Arproxy!", context)
  end
end

RSpec::Matchers.define :add_query_log do |log_line_regex|
  supports_block_expectations

  match do |block|
    idx = QueryLogger.log.size
    block.call
    QueryLogger.log.size > idx && QueryLogger.log[idx..-1].any? { |log| log.match(log_line_regex) }
  end

  failure_message do |block|
    "expected to add query log matching #{log_line_regex.inspect}, but got #{QueryLogger.log.inspect}"
  end

  failure_message_when_negated do |block|
    "expected not to add query log matching #{log_line_regex.inspect}, but added"
  end

  def supports_block_expectations?
    true
  end
end

RSpec.shared_examples 'Custom proxies work expectedly' do
  before do
    ActiveRecord::Base.connection.create_table :products, force: true do |t|
      t.string :name
      t.integer :price
    end
    Product.create(name: 'apple', price: 100)
    Product.create(name: 'banana', price: 200)
    Product.create(name: 'orange', price: 300)
    QueryLogger.reset!
  end

  after(:all) do
    ActiveRecord::Base.connection.drop_table :products
  end

  around do |example|
    ActiveRecord::Base.uncached do
      example.run
    end
  end

  context 'CREATE TABLE' do
    it do
      expect {
        ActiveRecord::Base.connection.create_table :products, force: true do |t|
          t.string :name
          t.integer :price
        end
      }.to add_query_log(/^CREATE TABLE.*products.* -- Hello Arproxy!$/)
    end
  end

  context 'SELECT' do
    it do
      expect {
        Product.where(name: ['apple', 'orange']).sum(:price)
      }.to add_query_log(/^SELECT.*products.* -- Hello Arproxy!$/)
    end
  end

  context 'INSERT' do
    it do
      expect {
        Product.create(name: 'grape', price: 400)
      }.to add_query_log(/^INSERT INTO.*products.* -- Hello Arproxy!$/)
    end
  end

  context 'UPDATE' do
    it do
      expect {
        Product.where(name: 'banana').update_all(price: 1000)
      }.to add_query_log(/^UPDATE.*products.* -- Hello Arproxy!$/)
    end
  end

  context 'DELETE' do
    it do
      expect {
        Product.where(name: 'banana').delete_all
      }.to add_query_log(/^DELETE.*products.* -- Hello Arproxy!$/)
    end
  end
end


================================================
FILE: spec/integration/spec_helper.rb
================================================
require 'arproxy'
require 'active_record'
require 'dotenv/load'
require_relative './shared_examples/custom_proxies'
require_relative './shared_examples/active_record_functions'

Arproxy.logger.level = Logger::WARN unless ENV['DEBUG']

def ar_version
  "#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}"
end

def cleanup_activerecord
  ActiveRecord::Base.connection.close
  ActiveRecord::Base.connection.clear_cache!
  ActiveRecord::Base.descendants.each(&:reset_column_information)
  ActiveRecord::Base.connection.schema_cache.clear!
end

def wait_for_db(host, port, interval = 0.2, timeout = 10)
  print "\nWaiting for DB on #{host}:#{port}..." if ENV['DEBUG']
  Timeout.timeout(timeout) do
    loop do
      TCPSocket.new(host, port).close
      puts 'ok' if ENV['DEBUG']
      break
    rescue Errno::ECONNREFUSED
      print '.' if ENV['DEBUG']
      sleep interval
    end
  end
rescue Timeout::Error
  raise "Timeout waiting for DB on #{host}:#{port}"
end


================================================
FILE: spec/integration/sqlite3_spec.rb
================================================
require_relative './spec_helper'

context "SQLite3 (AR#{ar_version})" do
  before(:all) do
    ActiveRecord::Base.establish_connection(
      adapter: 'sqlite3',
      database: ':memory:'
    )

    Arproxy.configure do |config|
      config.adapter = 'sqlite3'
      config.use HelloProxy
      config.plugin :query_logger
    end
    Arproxy.enable!
  end

  after(:all) do
    cleanup_activerecord
    Arproxy.disable!
    Arproxy.clear_configuration
  end

  it_behaves_like 'Arproxy does not break the original ActiveRecord functionality'
  it_behaves_like 'Custom proxies work expectedly'
end


================================================
FILE: spec/integration/sqlserver_spec.rb
================================================
require_relative './spec_helper'

context "SQLServer (AR#{ar_version})" do
  before(:all) do
    if ActiveRecord.version >= Gem::Version.new('7.2')
      ActiveRecord::ConnectionAdapters.register(
        'sqlserver',
        'ActiveRecord::ConnectionAdapters::SQLServerAdapter',
        'active_record/connection_adapters/sqlserver_adapter'
      )
    end

    host = ENV.fetch('MSSQL_HOST', '127.0.0.1')
    port = ENV.fetch('MSSQL_PORT', '21433').to_i
    wait_for_db(host, port)

    ActiveRecord::Base.establish_connection(
      adapter: 'sqlserver',
      host: host,
      port: port,
      database: 'arproxy_test',
      username: 'arproxy',
      password: ENV.fetch('ARPROXY_DB_PASSWORD')
    )

    Arproxy.configure do |config|
      config.adapter = 'sqlserver'
      config.use HelloProxy
      config.plugin :query_logger
    end
    Arproxy.enable!
  end

  after(:all) do
    cleanup_activerecord
    Arproxy.disable!
    Arproxy.clear_configuration
  end

  it_behaves_like 'Arproxy does not break the original ActiveRecord functionality'
  it_behaves_like 'Custom proxies work expectedly'
end


================================================
FILE: spec/integration/trilogy_spec.rb
================================================
require_relative './spec_helper'
require 'trilogy'

context "Trilogy (AR#{ar_version})", if: ActiveRecord.version >= '7.1' do
  before(:all) do
    host = ENV.fetch('MYSQL_HOST', '127.0.0.1')
    port = ENV.fetch('MYSQL_PORT', '23306').to_i
    wait_for_db(host, port)

    mysql_data_dir = File.expand_path('../../db/mysql/data', __dir__)
    ActiveRecord::Base.establish_connection(
      adapter: 'trilogy',
      host: host,
      port: port,
      ssl: true,
      ssl_mode: Trilogy::SSL_VERIFY_CA,
      tls_min_version: Trilogy::TLS_VERSION_12,
      ssl_ca: File.join(mysql_data_dir, 'ca.pem'),
      ssl_cert: File.join(mysql_data_dir, 'client-cert.pem'),
      ssl_key: File.join(mysql_data_dir, 'client-key.pem'),
      database: 'arproxy_test',
      username: 'arproxy',
      password: ENV.fetch('ARPROXY_DB_PASSWORD')
    )

    Arproxy.configure do |config|
      config.adapter = 'trilogy'
      config.use HelloProxy
      config.plugin :query_logger
    end
    Arproxy.enable!
  end

  after(:all) do
    cleanup_activerecord
    Arproxy.disable!
    Arproxy.clear_configuration
  end

  it_behaves_like 'Arproxy does not break the original ActiveRecord functionality'
  it_behaves_like 'Custom proxies work expectedly'
end


================================================
FILE: spec/lib/arproxy/plugin/legacy_plugin.rb
================================================
module Arproxy::Plugin
  class LegacyPlugin < Arproxy::Base
    Arproxy::Plugin.register(:legacy_plugin, self)

    def execute(sql, name)
      super("#{sql} /* legacy_plugin */", name)
    end
  end
end


================================================
FILE: spec/lib/arproxy/plugin/query_logger.rb
================================================
require 'arproxy/plugin'

class QueryLogger < Arproxy::Proxy
  Arproxy::Plugin.register(:query_logger, self)

  def execute(sql, context)
    @@log ||= []
    @@log << sql
    if ENV['DEBUG']
      puts "QueryLogger: [#{context.name}] #{sql}"
    end
    super
  end

  def self.log
    @@log
  end

  def self.reset!
    @@log = []
  end
end


================================================
FILE: spec/lib/arproxy/plugin/test_plugin.rb
================================================
module Arproxy::Plugin
  class TestPlugin < Arproxy::Proxy
    Arproxy::Plugin.register(:test_plugin, self)

    def initialize(*options)
      @options = options
    end

    def execute(sql, context)
      context.name = "#{context.name}_PLUGIN"
      super("#{sql} /* options: #{@options.inspect} */", context)
    end
  end
end


================================================
FILE: spec/unit/arproxy_spec.rb
================================================
require_relative './spec_helper'

describe Arproxy do
  before do
    allow(Arproxy).to receive(:logger) { Logger.new('/dev/null') }
  end

  class LegacyProxyA < Arproxy::Base
    def execute(sql, name)
      super "#{sql}_A", "#{name}_A"
    end
  end

  class LegacyProxyB < Arproxy::Base
    def initialize(opt=nil)
      @opt = opt
    end

    def execute(sql, name)
      super "#{sql}_B#{@opt}", "#{name}_B#{@opt}"
    end
  end

  class ProxyA < Arproxy::Proxy
    def execute(sql, context)
      context.name = "#{context.name}_A"
      super "#{sql}_A", context
    end
  end

  class ProxyB < Arproxy::Proxy
    def initialize(opt=nil)
      @opt = opt
    end

    def execute(sql, context)
      context.name = "#{context.name}_B#{@opt}"
      super "#{sql}_B#{@opt}", context
    end
  end

  module ::ActiveRecord
    module ConnectionAdapters
      class DummyAdapter
        ADAPTER_NAME = 'Dummy'

        def execute1(sql, name = nil, **kwargs)
          { sql: sql, name: name, kwargs: kwargs }
        end

        def execute2(sql, name = nil, binds = [], **kwargs)
          { sql: sql, name: name, binds: binds, kwargs: kwargs }
        end
      end
      Arproxy::ConnectionAdapterPatch.register_patches('Dummy', patches: [:execute1], binds_patches: [:execute2])
    end
  end

  let(:connection) { ::ActiveRecord::ConnectionAdapters::DummyAdapter.new }
  after(:each) do
    Arproxy.disable!
  end

  context 'with a proxy' do
    before do
      Arproxy.clear_configuration
      Arproxy.configure do |config|
        config.adapter = 'dummy'
        config.use ProxyA
      end
      Arproxy.enable!
    end

    it { expect(connection.execute1('SQL', 'NAME')).to eq({ sql: 'SQL_A', name: 'NAME_A', kwargs: {} }) }
    it { expect(connection.execute1('SQL', 'NAME', a: 1, b: 2)).to eq({ sql: 'SQL_A', name: 'NAME_A', kwargs: { a: 1, b: 2 } }) }

    it { expect(connection.execute2('SQL', 'NAME')).to eq({ sql: 'SQL_A', name: 'NAME_A', binds: [], kwargs: {} }) }

    it do
      expect(
        connection.execute2('SQL', 'NAME', [:x, :y], a: 1, b: 2)
      ).to eq(
        { sql: 'SQL_A', name: 'NAME_A', binds: [:x, :y], kwargs: { a: 1, b: 2 } }
      )
    end
  end

  context 'with 2 proxies' do
    before do
      Arproxy.clear_configuration
      Arproxy.configure do |config|
        config.adapter = 'dummy'
        config.use ProxyA
        config.use ProxyB
      end
      Arproxy.enable!
    end

    it { expect(connection.execute1('SQL', 'NAME')).to eq({ sql: 'SQL_A_B', name: 'NAME_A_B', kwargs: {} }) }
  end

  context 'with 2 proxies which have an option' do
    before do
      Arproxy.clear_configuration
      Arproxy.configure do |config|
        config.adapter = 'dummy'
        config.use ProxyA
        config.use ProxyB, 1
      end
      Arproxy.enable!
    end

    it { expect(connection.execute1('SQL', 'NAME')).to eq({ sql: 'SQL_A_B1', name: 'NAME_A_B1', kwargs: {} }) }
  end

  context 'with a proxy that returns nil' do
    class ReadonlyAccess < Arproxy::Proxy
      def execute(sql, context)
        if sql =~ /^(SELECT)\b/
          super sql, context
        else
          nil
        end
      end
    end

    before do
      Arproxy.clear_configuration
      Arproxy.configure do |config|
        config.adapter = 'dummy'
        config.use ReadonlyAccess
      end
      Arproxy.enable!
    end

    it { expect(connection.execute1('SELECT 1', 'NAME')).to eq({ sql: 'SELECT 1', name: 'NAME', kwargs: {} }) }
    it { expect(connection.execute1('UPDATE foo SET bar = 1', 'NAME')).to eq(nil) }
  end

  context 'with a legacy proxy' do
    class LegacyProxy < Arproxy::Base
      def execute(sql, name)
        super("#{sql} /* legacy_proxy */", name)
      end
    end

    before do
      Arproxy.clear_configuration
    end

    it 'raises an error' do
      expect {
        Arproxy.configure do |config|
          config.adapter = 'dummy'
          config.use LegacyProxy
        end
      }.to raise_error(Arproxy::Error, /Use `Arproxy::Proxy` instead/)
    end
  end

  context 'calls #execute with an String argument instead of `context`' do
    class WrongProxy < Arproxy::Proxy
      def execute(sql, context)
        super("#{sql} /* my_proxy */", "name=#{context.name}")
      end
    end

    before do
      Arproxy.clear_configuration
      Arproxy.configure do |config|
        config.adapter = 'dummy'
        config.use WrongProxy
      end
      Arproxy.enable!
    end

    it do
      expect {
        connection.execute1('SQL', 'NAME')
      }.to raise_error(Arproxy::Error, /expected a `Arproxy::QueryContext`/)
    end
  end

  context do
    before do
      Arproxy.clear_configuration
      Arproxy.configure do |config|
        config.adapter = 'dummy'
        config.use ProxyA
      end
    end

    context 'enable -> disable' do
      before do
        Arproxy.enable!
        Arproxy.disable!
      end
      it { expect(connection.execute1('SQL', 'NAME')).to eq({ sql: 'SQL', name: 'NAME', kwargs: {} }) }
    end

    context 'enable -> enable' do
      before do
        Arproxy.enable!
        Arproxy.enable!
      end
      it { expect(connection.execute1('SQL', 'NAME')).to eq({ sql: 'SQL_A', name: 'NAME_A', kwargs: {} }) }
    end

    context 'enable -> disable -> disable' do
      before do
        Arproxy.enable!
        Arproxy.disable!
        Arproxy.disable!
      end
      it { expect(connection.execute1('SQL', 'NAME')).to eq({ sql: 'SQL', name: 'NAME', kwargs: {} }) }
    end

    context 'clear_configuration -> enable' do
      before do
        Arproxy.clear_configuration
      end
      it do
        expect {
          Arproxy.enable!
        }.to raise_error(Arproxy::Error, /Arproxy has not been configured/)
      end
    end


    context 'enable -> disable -> enable' do
      before do
        Arproxy.enable!
        Arproxy.disable!
        Arproxy.enable!
      end
      it { expect(connection.execute1('SQL', 'NAME')).to eq({ sql: 'SQL_A', name: 'NAME_A', kwargs: {} }) }
    end

    context 're-configure' do
      before do
        Arproxy.configure do |config|
          config.adapter = 'dummy'
          config.use ProxyB
        end
        Arproxy.enable!
      end
      it { expect(connection.execute1('SQL', 'NAME')).to eq({ sql: 'SQL_A_B', name: 'NAME_A_B', kwargs: {} }) }
    end
  end

  context 'use a plug-in' do
    before do
      Arproxy.clear_configuration
      Arproxy.configure do |config|
        config.adapter = 'dummy'
        config.plugin :test_plugin, :option_a, :option_b
      end
      Arproxy.enable!
    end

    it do
      expect(
        connection.execute1('SQL', 'NAME')
      ).to eq(
        { sql: 'SQL /* options: [:option_a, :option_b] */', name: 'NAME_PLUGIN', kwargs: {} }
      )
    end
  end

  context 'use a legacy plugin' do
    before do
      Arproxy.clear_configuration
    end

    it 'raises an error' do
      expect {
        Arproxy.configure do |config|
          config.adapter = 'dummy'
          config.plugin :legacy_plugin
        end
      }.to raise_error(Arproxy::Error, /Use `Arproxy::Proxy` instead/)
    end
  end
end


================================================
FILE: spec/unit/config_spec.rb
================================================
require_relative './spec_helper'

describe Arproxy::Config do
  describe '#adapter default value' do
    subject { Arproxy::Config.new.adapter }

    context 'when Rails is defined' do
      let(:rails) { Module.new }

      around do |example|
        Object.const_set('Rails', rails)
        example.run
        Object.send(:remove_const, 'Rails')
      end

      before do
        allow(rails).to receive_message_chain('application.config_for') { database_config }
      end

      context 'when adapter is configured in database.yml' do
        let(:database_config) { { 'adapter' => 'mysql2' } }

        it { should == 'mysql2' }
      end

      context "when adapter isn't configured in database.yml" do
        let(:database_config) { {} }

        it { should == nil }
      end
    end

    context "when Rails isn't defined" do
      it { should == nil }
    end
  end

  describe '#adapter_class' do
    subject { config.adapter_class }
    let(:config) { Arproxy::Config.new }

    before do
      config.adapter = adapter
    end

    context "when adapter is configured as 'mysql2'" do
      let(:adapter) { 'mysql2' }
      let(:mysql2_class) { Class.new }

      before do
        stub_const('ActiveRecord::ConnectionAdapters::Mysql2Adapter', mysql2_class)
      end

      it { should == mysql2_class }
    end

    context "when adapter is configured as 'sqlite3'" do
      let(:adapter) { 'sqlite3' }
      let(:sqlite3_class) { Class.new }

      before do
        stub_const('ActiveRecord::ConnectionAdapters::SQLite3Adapter', sqlite3_class)
      end

      it { should == sqlite3_class }
    end

    context "when adapter is configured as 'sqlserver'" do
      let(:adapter) { 'sqlserver' }
      let(:sqlserver_class) { Class.new }

      before do
        stub_const('ActiveRecord::ConnectionAdapters::SQLServerAdapter', sqlserver_class)
      end

      it { should == sqlserver_class }
    end

    context "when adapter is configured as 'postgresql'" do
      let(:adapter) { 'postgresql' }
      let(:postgresql_class) { Class.new }

      before do
        stub_const('ActiveRecord::ConnectionAdapters::PostgreSQLAdapter', postgresql_class)
      end

      it { should == postgresql_class }
    end
  end
end


================================================
FILE: spec/unit/proxy_spec.rb
================================================
require_relative './spec_helper'
require 'arproxy/proxy_chain_tail'
require 'arproxy/proxy'
require 'arproxy/query_context'

describe Arproxy::Proxy do
  before(:all) do
    class DummyConnectionAdapter
      def execute(sql, name = nil, binds = [], **kwargs)
        "#{sql}"
      end
    end

    class Proxy1 < Arproxy::Proxy
      def execute(sql, context)
        super("#{sql} /* Proxy1 */", context)
      end
    end

    class Proxy2 < Arproxy::Proxy
      def execute(sql, context)
        super("#{sql} /* Proxy2 */", context)
      end
    end

    tail = Arproxy::ProxyChainTail.new
    p2 = Proxy2.new
    p2.next_proxy = tail
    p1 = Proxy1.new
    p1.next_proxy = p2
    @head = p1

    @conn = DummyConnectionAdapter.new
  end

  context 'with binds' do
    let(:context) { Arproxy::QueryContext.new(raw_connection: @conn, execute_method_name: 'execute', with_binds: true, name: 'test', binds: [1]) }
    describe '#execute' do
      it do
        expect(@head.execute('SELECT 1', context)).to eq('SELECT 1 /* Proxy1 */ /* Proxy2 */')
      end
    end
  end

  context 'without binds' do
    let(:context) { Arproxy::QueryContext.new(raw_connection: @conn, execute_method_name: 'execute', with_binds: false, name: 'test') }
    describe '#execute' do
      it do
        expect(@head.execute('SELECT 1', context)).to eq('SELECT 1 /* Proxy1 */ /* Proxy2 */')
      end
    end
  end
end


================================================
FILE: spec/unit/spec_helper.rb
================================================
require 'arproxy'

Arproxy.logger.level = Logger::WARN unless ENV['DEBUG']
Download .txt
gitextract_pt1acgj_/

├── .github/
│   └── workflows/
│       ├── integration_test.yml
│       ├── integration_tests.yml
│       ├── rubocop.yml
│       └── unit_tests.yml
├── .gitignore
├── .rspec
├── .rubocop.yml
├── Appraisals
├── ChangeLog.md
├── Dockerfile
├── Gemfile
├── LICENSE.txt
├── README.md
├── Rakefile
├── UPGRADING.md
├── arproxy.gemspec
├── compose-ci.yaml
├── compose.yaml
├── db/
│   ├── mysql/
│   │   └── my.cnf
│   └── sqlserver/
│       └── init.sql
├── gemfiles/
│   ├── ar_6.1.gemfile
│   ├── ar_7.0.gemfile
│   ├── ar_7.1.gemfile
│   ├── ar_7.2.gemfile
│   └── ar_8.0.gemfile
├── integration_test/
│   ├── Gemfile
│   ├── docker-compose.yml
│   ├── gemfiles/
│   │   ├── ar_6.1.gemfile
│   │   ├── ar_7.0.gemfile
│   │   └── ar_7.1.gemfile
│   └── spec/
│       ├── mysql2_spec.rb
│       ├── postgresql_spec.rb
│       └── spec_helper.rb
├── lib/
│   ├── arproxy/
│   │   ├── base.rb
│   │   ├── config.rb
│   │   ├── connection_adapter_patch.rb
│   │   ├── error.rb
│   │   ├── plugin.rb
│   │   ├── proxy.rb
│   │   ├── proxy_chain.rb
│   │   ├── proxy_chain_tail.rb
│   │   ├── query_context.rb
│   │   └── version.rb
│   └── arproxy.rb
└── spec/
    ├── integration/
    │   ├── mysql2_spec.rb
    │   ├── postgresql_spec.rb
    │   ├── shared_examples/
    │   │   ├── active_record_functions.rb
    │   │   └── custom_proxies.rb
    │   ├── spec_helper.rb
    │   ├── sqlite3_spec.rb
    │   ├── sqlserver_spec.rb
    │   └── trilogy_spec.rb
    ├── lib/
    │   └── arproxy/
    │       └── plugin/
    │           ├── legacy_plugin.rb
    │           ├── query_logger.rb
    │           └── test_plugin.rb
    └── unit/
        ├── arproxy_spec.rb
        ├── config_spec.rb
        ├── proxy_spec.rb
        └── spec_helper.rb
Download .txt
SYMBOL INDEX (105 symbols across 19 files)

FILE: integration_test/spec/spec_helper.rb
  class Product (line 4) | class Product < ActiveRecord::Base
  class QueryLogger (line 7) | class QueryLogger < Arproxy::Base
    method execute (line 8) | def execute(sql, name = nil)
    method log (line 15) | def self.log
    method reset! (line 19) | def self.reset!
  class HelloProxy (line 24) | class HelloProxy < Arproxy::Base
    method execute (line 25) | def execute(sql, name = nil)

FILE: lib/arproxy.rb
  type Arproxy (line 9) | module Arproxy
    function clear_configuration (line 16) | def clear_configuration
    function configure (line 20) | def configure
    function enable! (line 25) | def enable!
    function disable! (line 42) | def disable!
    function enable? (line 56) | def enable?
    function reenable! (line 60) | def reenable!
    function logger (line 68) | def logger
    function proxy_chain (line 74) | def proxy_chain
    function connection_adapter_patch (line 78) | def connection_adapter_patch

FILE: lib/arproxy/base.rb
  type Arproxy (line 1) | module Arproxy
    class Base (line 3) | class Base

FILE: lib/arproxy/config.rb
  type Arproxy (line 6) | module Arproxy
    class Config (line 7) | class Config
      method initialize (line 11) | def initialize
      method use (line 18) | def use(proxy_class, *options)
      method plugin (line 27) | def plugin(name, *options)
      method adapter_class (line 37) | def adapter_class
      method camelized_adapter_name (line 51) | def camelized_adapter_name

FILE: lib/arproxy/connection_adapter_patch.rb
  type Arproxy (line 1) | module Arproxy
    class ConnectionAdapterPatch (line 2) | class ConnectionAdapterPatch
      method initialize (line 5) | def initialize(adapter_class)
      method register_patches (line 10) | def self.register_patches(adapter_name, patches: [], binds_patches: [])
      method enable! (line 43) | def enable!
      method disable! (line 58) | def disable!
      method apply_patch (line 73) | def apply_patch(target_method)
      method apply_patch_binds (line 95) | def apply_patch_binds(target_method)

FILE: lib/arproxy/error.rb
  type Arproxy (line 1) | module Arproxy
    class Error (line 2) | class Error < Exception

FILE: lib/arproxy/plugin.rb
  type Arproxy (line 1) | module Arproxy
    type Plugin (line 2) | module Plugin
      function register (line 4) | def register(name, klass)
      function get (line 15) | def get(name)

FILE: lib/arproxy/proxy.rb
  type Arproxy (line 3) | module Arproxy
    class Proxy (line 4) | class Proxy
      method execute (line 7) | def execute(sql, context)

FILE: lib/arproxy/proxy_chain.rb
  type Arproxy (line 4) | module Arproxy
    class ProxyChain (line 5) | class ProxyChain
      method initialize (line 8) | def initialize(config, patch)
      method setup (line 14) | def setup
      method reenable! (line 25) | def reenable!
      method enable! (line 31) | def enable!
      method disable! (line 35) | def disable!

FILE: lib/arproxy/proxy_chain_tail.rb
  type Arproxy (line 4) | module Arproxy
    class ProxyChainTail (line 5) | class ProxyChainTail < Proxy
      method execute (line 6) | def execute(sql, context)

FILE: lib/arproxy/query_context.rb
  type Arproxy (line 1) | module Arproxy
    class QueryContext (line 2) | class QueryContext
      method initialize (line 5) | def initialize(raw_connection:, execute_method_name:, with_binds:, n...
      method with_binds? (line 14) | def with_binds?

FILE: lib/arproxy/version.rb
  type Arproxy (line 1) | module Arproxy

FILE: spec/integration/shared_examples/custom_proxies.rb
  class Product (line 1) | class Product < ActiveRecord::Base
  class HelloLegacyProxy (line 4) | class HelloLegacyProxy < Arproxy::Base
    method execute (line 5) | def execute(sql, name = nil)
  class HelloProxy (line 10) | class HelloProxy < Arproxy::Proxy
    method execute (line 11) | def execute(sql, context)
  function supports_block_expectations? (line 33) | def supports_block_expectations?

FILE: spec/integration/spec_helper.rb
  function ar_version (line 9) | def ar_version
  function cleanup_activerecord (line 13) | def cleanup_activerecord
  function wait_for_db (line 20) | def wait_for_db(host, port, interval = 0.2, timeout = 10)

FILE: spec/lib/arproxy/plugin/legacy_plugin.rb
  type Arproxy::Plugin (line 1) | module Arproxy::Plugin
    class LegacyPlugin (line 2) | class LegacyPlugin < Arproxy::Base
      method execute (line 5) | def execute(sql, name)

FILE: spec/lib/arproxy/plugin/query_logger.rb
  class QueryLogger (line 3) | class QueryLogger < Arproxy::Proxy
    method execute (line 6) | def execute(sql, context)
    method log (line 15) | def self.log
    method reset! (line 19) | def self.reset!

FILE: spec/lib/arproxy/plugin/test_plugin.rb
  type Arproxy::Plugin (line 1) | module Arproxy::Plugin
    class TestPlugin (line 2) | class TestPlugin < Arproxy::Proxy
      method initialize (line 5) | def initialize(*options)
      method execute (line 9) | def execute(sql, context)

FILE: spec/unit/arproxy_spec.rb
  class LegacyProxyA (line 8) | class LegacyProxyA < Arproxy::Base
    method execute (line 9) | def execute(sql, name)
  class LegacyProxyB (line 14) | class LegacyProxyB < Arproxy::Base
    method initialize (line 15) | def initialize(opt=nil)
    method execute (line 19) | def execute(sql, name)
  class ProxyA (line 24) | class ProxyA < Arproxy::Proxy
    method execute (line 25) | def execute(sql, context)
  class ProxyB (line 31) | class ProxyB < Arproxy::Proxy
    method initialize (line 32) | def initialize(opt=nil)
    method execute (line 36) | def execute(sql, context)
  type ::ActiveRecord (line 42) | module ::ActiveRecord
    type ConnectionAdapters (line 43) | module ConnectionAdapters
      class DummyAdapter (line 44) | class DummyAdapter
        method execute1 (line 47) | def execute1(sql, name = nil, **kwargs)
        method execute2 (line 51) | def execute2(sql, name = nil, binds = [], **kwargs)
  class ReadonlyAccess (line 117) | class ReadonlyAccess < Arproxy::Proxy
    method execute (line 118) | def execute(sql, context)
  class LegacyProxy (line 141) | class LegacyProxy < Arproxy::Base
    method execute (line 142) | def execute(sql, name)
  class WrongProxy (line 162) | class WrongProxy < Arproxy::Proxy
    method execute (line 163) | def execute(sql, context)

FILE: spec/unit/proxy_spec.rb
  class DummyConnectionAdapter (line 8) | class DummyConnectionAdapter
    method execute (line 9) | def execute(sql, name = nil, binds = [], **kwargs)
  class Proxy1 (line 14) | class Proxy1 < Arproxy::Proxy
    method execute (line 15) | def execute(sql, context)
  class Proxy2 (line 20) | class Proxy2 < Arproxy::Proxy
    method execute (line 21) | def execute(sql, context)
Condensed preview — 59 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (68K chars).
[
  {
    "path": ".github/workflows/integration_test.yml",
    "chars": 1581,
    "preview": "name: Integration Test\n\non:\n  push:\n    branches:\n      - master\n  pull_request:\n    branches:\n      - master\n\nenv:\n  RU"
  },
  {
    "path": ".github/workflows/integration_tests.yml",
    "chars": 719,
    "preview": "name: Integration tests\n\non:\n  push:\n    branches:\n      - main\n  pull_request:\n    branches:\n      - main\n\nconcurrency:"
  },
  {
    "path": ".github/workflows/rubocop.yml",
    "chars": 545,
    "preview": "name: RuboCop\n\non:\n  push:\n    branches:\n      - main\n  pull_request:\n    branches:\n      - main\n\nconcurrency:\n  group: "
  },
  {
    "path": ".github/workflows/unit_tests.yml",
    "chars": 646,
    "preview": "name: Unit tests\n\non:\n  push:\n    branches:\n      - main\n  pull_request:\n    branches:\n      - main\n\nconcurrency:\n  grou"
  },
  {
    "path": ".gitignore",
    "chars": 70,
    "preview": "*.swp\n*.gem\nGemfile.lock\n*.gemfile.lock\n.bundle/\ntmp/\ndb/mysql/data/*\n"
  },
  {
    "path": ".rspec",
    "chars": 19,
    "preview": "--color\n-Ispec/lib\n"
  },
  {
    "path": ".rubocop.yml",
    "chars": 5163,
    "preview": "---\n# Referenced from https://github.com/rails/rails/blob/main/.rubocop.yml\n\nrequire:\n  - rubocop-md\n\nAllCops:\n  TargetR"
  },
  {
    "path": "Appraisals",
    "chars": 999,
    "preview": "appraise 'ar-6.1' do\n  gem 'activerecord', '~> 6.1.0'\n  gem 'mysql2'\n  gem 'pg'\n  gem 'activerecord-sqlserver-adapter'\n "
  },
  {
    "path": "ChangeLog.md",
    "chars": 1522,
    "preview": "# Change Log\n## 1.0.0\n* Added support for ActiveRecord 7.1.\n* Redesigned the proxy chain to accommodate internal structu"
  },
  {
    "path": "Dockerfile",
    "chars": 486,
    "preview": "FROM ruby:3.3\n\nWORKDIR /app\n\nCOPY lib lib\nCOPY spec spec\nCOPY gemfiles gemfiles\n\nCOPY arproxy.gemspec arproxy.gemspec\nCO"
  },
  {
    "path": "Gemfile",
    "chars": 136,
    "preview": "source 'https://rubygems.org'\n\ngemspec\n\ngem 'rspec'\ngem 'appraisal'\ngem 'dotenv', require: 'dotenv/load'\ngem 'rubocop'\ng"
  },
  {
    "path": "LICENSE.txt",
    "chars": 1079,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2016 Issei Naruta\n\nPermission is hereby granted, free of charge, to any person obta"
  },
  {
    "path": "README.md",
    "chars": 5847,
    "preview": "[![Integration tests](https://github.com/cookpad/arproxy/actions/workflows/integration_tests.yml/badge.svg)](https://git"
  },
  {
    "path": "Rakefile",
    "chars": 187,
    "preview": "require 'bundler/gem_tasks'\nrequire 'rspec/core/rake_task'\nrequire 'rubocop/rake_task'\n\nRSpec::Core::RakeTask.new(:spec)"
  },
  {
    "path": "UPGRADING.md",
    "chars": 614,
    "preview": "# Upgrading guide from v0.x to v1\n\nThe proxy specification has changed from v0.x to v1 and is not backward compatible.\nT"
  },
  {
    "path": "arproxy.gemspec",
    "chars": 737,
    "preview": "$:.push File.expand_path('../lib', __FILE__)\nrequire 'arproxy/version'\n\nGem::Specification.new do |spec|\n  spec.name    "
  },
  {
    "path": "compose-ci.yaml",
    "chars": 1621,
    "preview": "services:\n  ruby:\n    build:\n      context: .\n      dockerfile: Dockerfile\n    environment:\n      MYSQL_HOST: mysql\n    "
  },
  {
    "path": "compose.yaml",
    "chars": 1318,
    "preview": "services:\n  mysql:\n    image: mysql:9.0\n    restart: always\n    environment:\n      MYSQL_ROOT_PASSWORD: rootpassword\n   "
  },
  {
    "path": "db/mysql/my.cnf",
    "chars": 62,
    "preview": "[mysqld]\ntls-version=TLSv1.2,TLSv1.3\nauto_generate_certs = ON\n"
  },
  {
    "path": "db/sqlserver/init.sql",
    "chars": 195,
    "preview": "CREATE DATABASE arproxy_test;\nGO\nUSE arproxy_test;\nGO\nCREATE LOGIN arproxy WITH PASSWORD = '4rpr0*y#2024';\nGO\nCREATE USE"
  },
  {
    "path": "gemfiles/ar_6.1.gemfile",
    "chars": 360,
    "preview": "# This file was generated by Appraisal\n\nsource \"https://rubygems.org\"\n\ngem \"rspec\"\ngem \"appraisal\"\ngem \"dotenv\", require"
  },
  {
    "path": "gemfiles/ar_7.0.gemfile",
    "chars": 360,
    "preview": "# This file was generated by Appraisal\n\nsource \"https://rubygems.org\"\n\ngem \"rspec\"\ngem \"appraisal\"\ngem \"dotenv\", require"
  },
  {
    "path": "gemfiles/ar_7.1.gemfile",
    "chars": 316,
    "preview": "# This file was generated by Appraisal\n\nsource \"https://rubygems.org\"\n\ngem \"rspec\"\ngem \"appraisal\"\ngem \"dotenv\", require"
  },
  {
    "path": "gemfiles/ar_7.2.gemfile",
    "chars": 316,
    "preview": "# This file was generated by Appraisal\n\nsource \"https://rubygems.org\"\n\ngem \"rspec\"\ngem \"appraisal\"\ngem \"dotenv\", require"
  },
  {
    "path": "gemfiles/ar_8.0.gemfile",
    "chars": 316,
    "preview": "# This file was generated by Appraisal\n\nsource \"https://rubygems.org\"\n\ngem \"rspec\"\ngem \"appraisal\"\ngem \"dotenv\", require"
  },
  {
    "path": "integration_test/Gemfile",
    "chars": 107,
    "preview": "source 'https://rubygems.org'\n\ngem 'arproxy', path: '..'\ngem 'rspec'\ngem 'appraisal'\ngem 'mysql2'\ngem 'pg'\n"
  },
  {
    "path": "integration_test/docker-compose.yml",
    "chars": 455,
    "preview": "version: '3'\n\nservices:\n  mysql:\n    image: mysql:9.0\n    restart: always\n    environment:\n      MYSQL_ROOT_PASSWORD: ro"
  },
  {
    "path": "integration_test/gemfiles/ar_6.1.gemfile",
    "chars": 181,
    "preview": "# This file was generated by Appraisal\n\nsource \"https://rubygems.org\"\n\ngem \"arproxy\", path: \"../..\"\ngem \"rspec\"\ngem \"app"
  },
  {
    "path": "integration_test/gemfiles/ar_7.0.gemfile",
    "chars": 181,
    "preview": "# This file was generated by Appraisal\n\nsource \"https://rubygems.org\"\n\ngem \"arproxy\", path: \"../..\"\ngem \"rspec\"\ngem \"app"
  },
  {
    "path": "integration_test/gemfiles/ar_7.1.gemfile",
    "chars": 181,
    "preview": "# This file was generated by Appraisal\n\nsource \"https://rubygems.org\"\n\ngem \"arproxy\", path: \"../..\"\ngem \"rspec\"\ngem \"app"
  },
  {
    "path": "integration_test/spec/mysql2_spec.rb",
    "chars": 1374,
    "preview": "require_relative 'spec_helper'\nrequire 'mysql2'\n\ncontext 'MySQL' do\n  before(:all) do\n    ActiveRecord::Base.establish_c"
  },
  {
    "path": "integration_test/spec/postgresql_spec.rb",
    "chars": 1389,
    "preview": "require_relative 'spec_helper'\nrequire 'pg'\n\ncontext 'PostgreSQL' do\n  before(:all) do\n    ActiveRecord::Base.establish_"
  },
  {
    "path": "integration_test/spec/spec_helper.rb",
    "chars": 424,
    "preview": "require 'arproxy'\nrequire 'active_record'\n\nclass Product < ActiveRecord::Base\nend\n\nclass QueryLogger < Arproxy::Base\n  d"
  },
  {
    "path": "lib/arproxy/base.rb",
    "chars": 89,
    "preview": "module Arproxy\n  # This class is no longer used since Arproxy v1.\n  class Base\n  end\nend\n"
  },
  {
    "path": "lib/arproxy/config.rb",
    "chars": 2037,
    "preview": "require 'active_record'\nrequire 'active_record/base'\nrequire 'arproxy/base'\nrequire 'arproxy/error'\n\nmodule Arproxy\n  cl"
  },
  {
    "path": "lib/arproxy/connection_adapter_patch.rb",
    "chars": 4880,
    "preview": "module Arproxy\n  class ConnectionAdapterPatch\n    attr_reader :adapter_class\n\n    def initialize(adapter_class)\n      @a"
  },
  {
    "path": "lib/arproxy/error.rb",
    "chars": 51,
    "preview": "module Arproxy\n  class Error < Exception\n  end\nend\n"
  },
  {
    "path": "lib/arproxy/plugin.rb",
    "chars": 559,
    "preview": "module Arproxy\n  module Plugin\n    class << self\n      def register(name, klass)\n        name = name.to_s\n        @plugi"
  },
  {
    "path": "lib/arproxy/proxy.rb",
    "chars": 356,
    "preview": "require 'arproxy/query_context'\n\nmodule Arproxy\n  class Proxy\n    attr_accessor :context, :next_proxy\n\n    def execute(s"
  },
  {
    "path": "lib/arproxy/proxy_chain.rb",
    "chars": 710,
    "preview": "require 'arproxy/proxy_chain_tail'\nrequire 'arproxy/connection_adapter_patch'\n\nmodule Arproxy\n  class ProxyChain\n    att"
  },
  {
    "path": "lib/arproxy/proxy_chain_tail.rb",
    "chars": 586,
    "preview": "require 'arproxy/proxy'\nrequire 'arproxy/query_context'\n\nmodule Arproxy\n  class ProxyChainTail < Proxy\n    def execute(s"
  },
  {
    "path": "lib/arproxy/query_context.rb",
    "chars": 484,
    "preview": "module Arproxy\n  class QueryContext\n    attr_accessor :raw_connection, :execute_method_name, :with_binds, :name, :binds,"
  },
  {
    "path": "lib/arproxy/version.rb",
    "chars": 39,
    "preview": "module Arproxy\n  VERSION = '1.0.0'\nend\n"
  },
  {
    "path": "lib/arproxy.rb",
    "chars": 1477,
    "preview": "require 'logger'\nrequire 'arproxy/base'\nrequire 'arproxy/config'\nrequire 'arproxy/connection_adapter_patch'\nrequire 'arp"
  },
  {
    "path": "spec/integration/mysql2_spec.rb",
    "chars": 839,
    "preview": "require_relative './spec_helper'\n\ncontext \"MySQL (AR#{ar_version})\" do\n  before(:all) do\n    host = ENV.fetch('MYSQL_HOS"
  },
  {
    "path": "spec/integration/postgresql_spec.rb",
    "chars": 858,
    "preview": "require_relative './spec_helper'\n\ncontext \"PostgreSQL (AR#{ar_version})\" do\n  before(:all) do\n    host = ENV.fetch('POST"
  },
  {
    "path": "spec/integration/shared_examples/active_record_functions.rb",
    "chars": 1025,
    "preview": "RSpec.shared_examples 'Arproxy does not break the original ActiveRecord functionality' do\n  before do\n    # CREATE\n    A"
  },
  {
    "path": "spec/integration/shared_examples/custom_proxies.rb",
    "chars": 2448,
    "preview": "class Product < ActiveRecord::Base\nend\n\nclass HelloLegacyProxy < Arproxy::Base\n  def execute(sql, name = nil)\n    super("
  },
  {
    "path": "spec/integration/spec_helper.rb",
    "chars": 977,
    "preview": "require 'arproxy'\nrequire 'active_record'\nrequire 'dotenv/load'\nrequire_relative './shared_examples/custom_proxies'\nrequ"
  },
  {
    "path": "spec/integration/sqlite3_spec.rb",
    "chars": 600,
    "preview": "require_relative './spec_helper'\n\ncontext \"SQLite3 (AR#{ar_version})\" do\n  before(:all) do\n    ActiveRecord::Base.establ"
  },
  {
    "path": "spec/integration/sqlserver_spec.rb",
    "chars": 1115,
    "preview": "require_relative './spec_helper'\n\ncontext \"SQLServer (AR#{ar_version})\" do\n  before(:all) do\n    if ActiveRecord.version"
  },
  {
    "path": "spec/integration/trilogy_spec.rb",
    "chars": 1244,
    "preview": "require_relative './spec_helper'\nrequire 'trilogy'\n\ncontext \"Trilogy (AR#{ar_version})\", if: ActiveRecord.version >= '7."
  },
  {
    "path": "spec/lib/arproxy/plugin/legacy_plugin.rb",
    "chars": 205,
    "preview": "module Arproxy::Plugin\n  class LegacyPlugin < Arproxy::Base\n    Arproxy::Plugin.register(:legacy_plugin, self)\n\n    def "
  },
  {
    "path": "spec/lib/arproxy/plugin/query_logger.rb",
    "chars": 343,
    "preview": "require 'arproxy/plugin'\n\nclass QueryLogger < Arproxy::Proxy\n  Arproxy::Plugin.register(:query_logger, self)\n\n  def exec"
  },
  {
    "path": "spec/lib/arproxy/plugin/test_plugin.rb",
    "chars": 332,
    "preview": "module Arproxy::Plugin\n  class TestPlugin < Arproxy::Proxy\n    Arproxy::Plugin.register(:test_plugin, self)\n\n    def ini"
  },
  {
    "path": "spec/unit/arproxy_spec.rb",
    "chars": 7139,
    "preview": "require_relative './spec_helper'\n\ndescribe Arproxy do\n  before do\n    allow(Arproxy).to receive(:logger) { Logger.new('/"
  },
  {
    "path": "spec/unit/config_spec.rb",
    "chars": 2244,
    "preview": "require_relative './spec_helper'\n\ndescribe Arproxy::Config do\n  describe '#adapter default value' do\n    subject { Arpro"
  },
  {
    "path": "spec/unit/proxy_spec.rb",
    "chars": 1406,
    "preview": "require_relative './spec_helper'\nrequire 'arproxy/proxy_chain_tail'\nrequire 'arproxy/proxy'\nrequire 'arproxy/query_conte"
  },
  {
    "path": "spec/unit/spec_helper.rb",
    "chars": 75,
    "preview": "require 'arproxy'\n\nArproxy.logger.level = Logger::WARN unless ENV['DEBUG']\n"
  }
]

About this extraction

This page contains the full source code of the cookpad/arproxy GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 59 files (60.2 KB), approximately 18.1k tokens, and a symbol index with 105 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!