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
================================================
[](https://github.com/cookpad/arproxy/actions/workflows/integration_tests.yml)
[](https://github.com/cookpad/arproxy/actions/workflows/unit_tests.yml)
[](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']
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
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": "[](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.