sql_runner 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 0fe73046f6047d45f926dcc2a2dfe481698d6c27
4
+ data.tar.gz: fdabc17b0da986f3e0f1676dbc35d1b74ca0510c
5
+ SHA512:
6
+ metadata.gz: 539aa4ac64c12629feb865ffde679507bc00a0951353261c42fbd4fcf547228593d2bc0675cf792c94f065ff908c9b8eb1f8239f673f883ff07d2c4789fa73aa
7
+ data.tar.gz: 014fa997673cd75e241ca74e667d7f3fb31b7b40acb1a044c0f983272cd7543e8b61e31bbdebb092b362ae0771c73109776d491608981a47019bec73c28059ce
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ /examples/**/*.html
@@ -0,0 +1,13 @@
1
+ language: ruby
2
+ cache: bundler
3
+ sudo: false
4
+ rvm:
5
+ - 2.3.1
6
+ before_install:
7
+ - gem install bundler
8
+ - createdb test
9
+ notifications:
10
+ email: false
11
+ env:
12
+ global:
13
+ secure: S5CYGmnmHV6FE/P0W2H7sEJwR3jGcuW97wIzNLBSDkl1Nvmc2NAw6WynoqOGSEVafmauVjX+w+gOkF2UgJImBYwDjwGmSVcKSPj1SWuuT648pq5mJjW4kyS/lKnA8PaiqS1nMVAbGHtZjH3cEG0w0oFPilDN7nsCCzm4kztkRWtZgHhGPc/UU0EYjsGkUVq3gsYFNm09dzMxzmg9U8jdOLUM354mxP2FKqBQ795/HkEXtb5mGSKnSBWB4jdk9enmBe/M6/Vvo9X98drz4GRVdHpvy5LvWPwYSvwKNprErPcIasLxONOUXTEzA45/RSJZBLUuGlrNu7f9tA9JNbrUaVRp+vnhRpue5nNYIowPUVOnjxfE/qgx3X4k5K26pbO6wKfhdDYRK6Qh85rYNuHpd0caiEoTJJlXQp6sTo/wKQI5p9BF4b3FRG1RtCoMbjLFrQin4l0JwILoAyGyTbk50XdQvwDDlFs1lGiGxe0AK0lxX6s5yqdykaXkx5qwTSuWZqFc+9+bAEy6TNzEqBuT+r1mxIfVep8bNxynDLRg26NibGAFHzMHuqgEM4mLeOUkKnBDv1LghRJ0fzhroINJHAj5KBp145r6iJSVR7xRjKJpQptpIZ3asi5Xl37sQb98+p+1Pg4Wwzn9Y8lEyx49lml7oppDvabcbOCTy0LFuuk=
@@ -0,0 +1,49 @@
1
+ # Contributor Code of Conduct
2
+
3
+ As contributors and maintainers of this project, and in the interest of
4
+ fostering an open and welcoming community, we pledge to respect all people who
5
+ contribute through reporting issues, posting feature requests, updating
6
+ documentation, submitting pull requests or patches, and other activities.
7
+
8
+ We are committed to making participation in this project a harassment-free
9
+ experience for everyone, regardless of level of experience, gender, gender
10
+ identity and expression, sexual orientation, disability, personal appearance,
11
+ body size, race, ethnicity, age, religion, or nationality.
12
+
13
+ Examples of unacceptable behavior by participants include:
14
+
15
+ * The use of sexualized language or imagery
16
+ * Personal attacks
17
+ * Trolling or insulting/derogatory comments
18
+ * Public or private harassment
19
+ * Publishing other's private information, such as physical or electronic
20
+ addresses, without explicit permission
21
+ * Other unethical or unprofessional conduct
22
+
23
+ Project maintainers have the right and responsibility to remove, edit, or
24
+ reject comments, commits, code, wiki edits, issues, and other contributions
25
+ that are not aligned to this Code of Conduct, or to ban temporarily or
26
+ permanently any contributor for other behaviors that they deem inappropriate,
27
+ threatening, offensive, or harmful.
28
+
29
+ By adopting this Code of Conduct, project maintainers commit themselves to
30
+ fairly and consistently applying these principles to every aspect of managing
31
+ this project. Project maintainers who do not follow or enforce the Code of
32
+ Conduct may be permanently removed from the project team.
33
+
34
+ This code of conduct applies both within project spaces and in public spaces
35
+ when an individual is representing the project or its community.
36
+
37
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
38
+ reported by contacting a project maintainer at fnando.vieira@gmail.com. All
39
+ complaints will be reviewed and investigated and will result in a response that
40
+ is deemed necessary and appropriate to the circumstances. Maintainers are
41
+ obligated to maintain confidentiality with regard to the reporter of an
42
+ incident.
43
+
44
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
45
+ version 1.3.0, available at
46
+ [http://contributor-covenant.org/version/1/3/0/][version]
47
+
48
+ [homepage]: http://contributor-covenant.org
49
+ [version]: http://contributor-covenant.org/version/1/3/0/
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source "https://rubygems.org"
2
+ gemspec
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Nando Vieira
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,196 @@
1
+ # SQLRunner
2
+
3
+ [![Travis-CI](https://travis-ci.org/fnando/sql_runner.png)](https://travis-ci.org/fnando/sql_runner)
4
+ [![Code Climate](https://codeclimate.com/github/fnando/sql_runner/badges/gpa.svg)](https://codeclimate.com/github/fnando/sql_runner)
5
+ [![Test Coverage](https://codeclimate.com/github/fnando/sql_runner/badges/coverage.svg)](https://codeclimate.com/github/fnando/sql_runner/coverage)
6
+ [![Gem](https://img.shields.io/gem/v/sql_runner.svg)](https://rubygems.org/gems/sql_runner)
7
+ [![Gem](https://img.shields.io/gem/dt/sql_runner.svg)](https://rubygems.org/gems/sql_runner)
8
+
9
+ SQLRunner allows you to load your queries out of SQL files, without using ORMs. Available only for PostgreSQL.
10
+
11
+ ## Installation
12
+
13
+ Add this line to your application's Gemfile:
14
+
15
+ ```ruby
16
+ gem "sql_runner"
17
+ ```
18
+
19
+ And then execute:
20
+
21
+ $ bundle
22
+
23
+ Or install it yourself as:
24
+
25
+ $ gem install sql_runner
26
+
27
+ ## Usage
28
+
29
+ Run raw queries:
30
+
31
+ ```ruby
32
+ SQLRunner.connect("postgresql:///test?application_name=myapp")
33
+
34
+ SQLRunner.execute("SELECT 1")
35
+ #=> <PG:Result:0x007fdf8d3475f0>
36
+
37
+ SQLRunner.execute("SELECT 1").values
38
+ #=> [["1"]]
39
+
40
+ SQLRunner.execute("SELECT :number::integer", number: 1).values
41
+ #=> [["1"]]
42
+ ```
43
+
44
+ Run SQL files:
45
+
46
+ ```ruby
47
+ SQLRunner.root_dir = "#{__dir__}/sql"
48
+
49
+ class GetMembers < SQLRunner::Query
50
+ # by default will use root_dir/get_members.sql
51
+ end
52
+ ```
53
+
54
+ Specify custom SQL:
55
+
56
+ ```ruby
57
+ class GetMembers < SQLRunner::Query
58
+ query "SELECT * FROM members ORDER BY created_at"
59
+ end
60
+ ```
61
+
62
+ Specify custom connection:
63
+
64
+ ```ruby
65
+ class GetMembers < SQLRunner::Query
66
+ connect "postgresql:///another_database"
67
+ end
68
+ ```
69
+
70
+ Specify other options:
71
+
72
+ ```ruby
73
+ class GetMembers < SQLRunner::Query
74
+ query_name "users" #=> will load root_dir/users.sql
75
+ root_dir "/some/path"
76
+ end
77
+ ```
78
+
79
+ ### Plugins
80
+
81
+ #### Load just one record
82
+
83
+ ```ruby
84
+ class FindUserByEmail < SQLRunner::Query
85
+ plugin :one
86
+ query "SELECT * FROM users WHERE email = :email LIMIT 1"
87
+ end
88
+
89
+ FindUserByEmail.call(email: "john@example.com")
90
+ #=> {"id" => 1, "email" => "john@example.com"}
91
+
92
+ FindUserByEmail.call(email: "invalid")
93
+ #=> nil
94
+
95
+ FindUserByEmail.call!(email: "invalid")
96
+ #=> raise exception SQLRunner::RecordNotFound
97
+ ```
98
+
99
+ #### Use a model
100
+
101
+ ```ruby
102
+ require "virtus"
103
+
104
+ class UserModel
105
+ include Virtus.model
106
+
107
+ attribute :id, String
108
+ attribute :email, String
109
+ attribute :name, String
110
+ end
111
+
112
+ class FindUserByEmail < SQLRunner::Query
113
+ plugin :one
114
+ plugin model: UserModel
115
+ query "SELECT * FROM users WHERE email = :email LIMIT 1"
116
+ end
117
+
118
+ FindUserByEmail.call(email: "john@example.com")
119
+ #=> <UserModel:0x007fdf8c2c1280>
120
+ ```
121
+
122
+ #### Avoid calling .to_a in collections
123
+
124
+ ```ruby
125
+ class FindUsers < SQLRunner::Query
126
+ plugin :many
127
+ query "SELECT * FROM users"
128
+ end
129
+
130
+ FindUsers.call
131
+ #=> [{"id" => "1", "email" => "john@example.com"}]
132
+ ```
133
+
134
+ ### Adding new plugins
135
+
136
+ First you have to create a class/module that implements the `.activate(target, options)` class method. The following example overrides the `call(**bind_vars)` method by using `Module.prepend`.
137
+
138
+ ```ruby
139
+ module ReverseRecords
140
+ def self.activate(target, options)
141
+ target.singleton_class.prepend self
142
+ end
143
+
144
+ def call(**bind_vars)
145
+ super(**bind_vars).to_a.reverse
146
+ end
147
+ end
148
+
149
+ # Register the plugin.
150
+ SQLRunner::Query.register_plugin :reverse, ReverseRecords
151
+
152
+ class Users < SQLRunner::Query
153
+ query "SELECT * FROM users ORDER BY created_at ASC"
154
+ plugin :reverse
155
+ end
156
+
157
+ Users.call
158
+ ```
159
+
160
+ If you plugin can receive options, you can call it as `plugin reverse: options`, where `options` can be anything (e.g. `Hash`, `Array`, `Object`, etc).
161
+
162
+ ## Benchmarks
163
+
164
+ You won't gain too much performance by using this gem. These are the results against ActiveRecord using different wrapping libraries like [virtus](https://rubygems.org/gems/virtus) and [dry-types](https://rubygems.org/gems/dry-types).
165
+
166
+ Loading just one record:
167
+
168
+ ```
169
+ sql_runner - find one (raw) : 5518.6 i/s
170
+ sql_runner - find one (dry-types): 5015.4 i/s - same-ish: difference falls within error
171
+ sql_runner - find one (virtus) : 4746.2 i/s - 1.16x slower
172
+ activerecord - find one : 3468.5 i/s - 1.59x slower
173
+ ```
174
+
175
+ Loading several records:
176
+
177
+ ```
178
+ sql_runner - find many (raw) : 6808.2 i/s
179
+ sql_runner - find many (dry-types): 5251.4 i/s - same-ish: difference falls within error
180
+ sql_runner - find many (virtus) : 4145.6 i/s - 1.64x slower
181
+ activerecord - find many : 2731.5 i/s - 2.49x slower
182
+ ```
183
+
184
+ ## Development
185
+
186
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
187
+
188
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
189
+
190
+ ## Contributing
191
+
192
+ Bug reports and pull requests are welcome on GitHub at https://github.com/fnando/sql_runner. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
193
+
194
+ ## License
195
+
196
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
@@ -0,0 +1,11 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList['test/**/*_test.rb']
8
+ t.warning = false
9
+ end
10
+
11
+ task :default => :test
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "sql_runner"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,93 @@
1
+ $LOAD_PATH.push File.expand_path("#{__dir__}/../lib")
2
+ require "sql_runner"
3
+ require "virtus"
4
+ require "benchmark/ips"
5
+ require "active_record"
6
+ require "dry-types"
7
+
8
+ GC.disable
9
+
10
+ connection_string = "postgres:///test?connect_timeout=2&application_name=myapp"
11
+ SQLRunner.connect connection_string
12
+ SQLRunner.pool = 25
13
+ SQLRunner.timeout = 10
14
+ SQLRunner.root_dir = "#{__dir__}/sql"
15
+
16
+ ActiveRecord::Base.establish_connection("#{connection_string}&prepared_statements=false&pool=25")
17
+
18
+ module Types
19
+ include Dry::Types.module
20
+ end
21
+
22
+ class User < ActiveRecord::Base
23
+ end
24
+
25
+ class UserDry < Dry::Types::Struct
26
+ module Builder
27
+ def self.new(attrs)
28
+ attrs = attrs.each_with_object({}) do |(key, value), buffer|
29
+ buffer[key.to_sym] = value
30
+ end
31
+
32
+ UserDry.new(attrs)
33
+ end
34
+ end
35
+
36
+ attribute :id, Types::String
37
+ attribute :name, Types::String
38
+ attribute :email, Types::String
39
+ end
40
+
41
+ class UserVirtus
42
+ include Virtus.model
43
+
44
+ attribute :id, String
45
+ attribute :name, String
46
+ attribute :email, String
47
+ end
48
+
49
+ class FindUserDry < SQLRunner::Query
50
+ query_name "find_user"
51
+ plugin :one
52
+ plugin model: UserDry::Builder
53
+ end
54
+
55
+ class FindUserVirtus < SQLRunner::Query
56
+ query_name "find_user"
57
+ plugin :one
58
+ plugin model: UserVirtus
59
+ end
60
+
61
+ class FindUser < SQLRunner::Query
62
+ plugin :one
63
+ end
64
+
65
+ class UsersVirtus < SQLRunner::Query
66
+ plugin model: UserVirtus
67
+ query_name "users"
68
+ end
69
+
70
+ class UsersDry < SQLRunner::Query
71
+ plugin model: UserDry::Builder
72
+ query_name "users"
73
+ end
74
+
75
+ class Users < SQLRunner::Query
76
+ query_name "users"
77
+ end
78
+
79
+ Benchmark.ips do |x|
80
+ x.report("activerecord - find one ") { User.find_by_email("me@fnando.com") }
81
+ x.report(" sql_runner - find one (dry-types)") { FindUserDry.call(email: "me@fnando.com") }
82
+ x.report(" sql_runner - find one (virtus) ") { FindUserVirtus.call(email: "me@fnando.com") }
83
+ x.report(" sql_runner - find one (raw) ") { FindUser.call(email: "me@fnando.com") }
84
+ x.compare!
85
+ end
86
+
87
+ Benchmark.ips do |x|
88
+ x.report("activerecord - find many ") { User.all.to_a }
89
+ x.report(" sql_runner - find many (virtus) ") { UsersVirtus.call }
90
+ x.report(" sql_runner - find many (dry-types)") { UsersDry.call }
91
+ x.report(" sql_runner - find many (raw) ") { Users.call }
92
+ x.compare!
93
+ end
@@ -0,0 +1,21 @@
1
+ $LOAD_PATH.push File.expand_path("#{__dir__}/../lib")
2
+ require "sql_runner"
3
+ require "ruby-prof"
4
+
5
+ connection_string = "postgres:///test?connect_timeout=2&application_name=myapp"
6
+ SQLRunner.connect connection_string
7
+ SQLRunner.pool = 25
8
+ SQLRunner.timeout = 10
9
+ SQLRunner.root_dir = "#{__dir__}/sql"
10
+
11
+ class FindUser < SQLRunner::Query
12
+ plugin :one
13
+ end
14
+
15
+ result = RubyProf.profile do
16
+ FindUser.call email: "me@fnando.com"
17
+ end
18
+
19
+ File.open("examples/profiling.html", "w") do |io|
20
+ RubyProf::CallStackPrinter.new(result).print(io)
21
+ end
@@ -0,0 +1 @@
1
+ INSERT INTO users (name, email) VALUES (:name, :email) RETURNING *
@@ -0,0 +1 @@
1
+ DELETE FROM users RETURNING *
@@ -0,0 +1 @@
1
+ SELECT * FROM users WHERE email = :email
@@ -0,0 +1 @@
1
+ SELECT * FROM users
@@ -0,0 +1,93 @@
1
+ $LOAD_PATH.push File.expand_path("#{__dir__}/../lib")
2
+ require "sql_runner"
3
+ require "virtus"
4
+
5
+ SQLRunner.connect "postgres:///test?connect_timeout=2&application_name=myapp"
6
+ SQLRunner.pool = 25
7
+ SQLRunner.timeout = 10
8
+ SQLRunner.root_dir = "#{__dir__}/sql"
9
+
10
+ result = SQLRunner.execute "select application_name from pg_stat_activity where pid = pg_backend_pid();"
11
+ p result.to_a
12
+
13
+ result = SQLRunner.execute <<-SQL, name: "john", age: 18
14
+ select
15
+ 'hello'::text as message,
16
+ :name::text as name,
17
+ :age::integer as age,
18
+ :name::text as name2
19
+ SQL
20
+ p result.to_a
21
+
22
+ class Users < SQLRunner::Query
23
+ end
24
+
25
+ module NumericModel
26
+ def self.new(attrs)
27
+ attrs.values.first.to_i
28
+ end
29
+ end
30
+
31
+ class Numbers < SQLRunner::Query
32
+ plugin model: NumericModel
33
+ plugin :many
34
+
35
+ query <<-SQL
36
+ SELECT n FROM generate_series(1, 10) n
37
+ SQL
38
+ end
39
+
40
+ class User
41
+ include Virtus.model
42
+
43
+ attribute :id, String
44
+ attribute :name, String
45
+ attribute :email, Integer
46
+ end
47
+
48
+ class Customer < User
49
+ end
50
+
51
+ class FindUser < SQLRunner::Query
52
+ plugins :one
53
+ plugin model: User
54
+ end
55
+
56
+ class FindAllUsers < SQLRunner::Query
57
+ plugins :many
58
+ plugin model: User
59
+ end
60
+
61
+ class CreateUser < SQLRunner::Query
62
+ plugin :one
63
+ plugin model: User
64
+ end
65
+
66
+ class DeleteAllUsers < SQLRunner::Query
67
+ plugin :many
68
+ plugin model: User
69
+ end
70
+
71
+ class FindCustomer < SQLRunner::Query
72
+ query_name "find_user"
73
+ plugin model: Customer
74
+ plugins :one
75
+ end
76
+
77
+ p DeleteAllUsers.call
78
+ p CreateUser.call(name: "Nando Vieira", email: "me@fnando.com")
79
+ p CreateUser.call(name: "John Doe", email: "john@example.com")
80
+ p Numbers.call
81
+ p Users.call.to_a
82
+ p FindUser.call(email: "me@fnando.com")
83
+ p FindUser.call(email: "' OR 1=1 --me@fnando.com")
84
+ p FindUser.call!(email: "me@fnando.com")
85
+ p FindCustomer.call!(email: "me@fnando.com")
86
+
87
+ begin
88
+ FindUser.call!(email: "me@fnando.coms")
89
+ rescue SQLRunner::RecordNotFound => error
90
+ p error
91
+ end
92
+
93
+ SQLRunner.disconnect
@@ -0,0 +1,27 @@
1
+ require "uri"
2
+ require "connection_pool"
3
+
4
+ module SQLRunner
5
+ require "sql_runner/version"
6
+ require "sql_runner/connection"
7
+ require "sql_runner/adapters"
8
+ require "sql_runner/runner"
9
+ require "sql_runner/query"
10
+ require "sql_runner/query/one"
11
+ require "sql_runner/query/model"
12
+ require "sql_runner/query/many"
13
+ require "sql_runner/configuration"
14
+
15
+ extend Configuration
16
+ extend Runner
17
+
18
+ Adapters.register("postgres", Adapters::PostgreSQL)
19
+ Adapters.register("postgresql", Adapters::PostgreSQL)
20
+
21
+ Query.register_plugin :one, Query::One
22
+ Query.register_plugin :many, Query::Many
23
+ Query.register_plugin :model, Query::Model
24
+
25
+ self.timeout = ENV.fetch("SQL_CONNECTION_TIMEOUT", 5)
26
+ self.pool = ENV.fetch("SQL_CONNECTION_POOL", 5)
27
+ end
@@ -0,0 +1,20 @@
1
+ module SQLRunner
2
+ UnsupportedDatabase = Class.new(StandardError)
3
+ MissingDependency = Class.new(StandardError)
4
+
5
+ module Adapters
6
+ require "sql_runner/adapters/postgresql"
7
+
8
+ ADAPTERS = {}
9
+
10
+ def self.register(name, adapter)
11
+ ADAPTERS[name] = adapter
12
+ end
13
+
14
+ def self.find(name)
15
+ ADAPTERS
16
+ .fetch(name) { fail UnsupportedDatabase, "#{name} is not supported by SQLRunner" }
17
+ .tap {|adapter| adapter.load }
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,94 @@
1
+ module SQLRunner
2
+ module Adapters
3
+ class PostgreSQL
4
+ InvalidPreparedStatement = Class.new(StandardError)
5
+
6
+ def self.load
7
+ require "pg"
8
+ rescue LoadError
9
+ fail MissingDependency, "make sure the pg gem is available"
10
+ end
11
+
12
+ def initialize(connection_string)
13
+ @connection_string = connection_string
14
+ connect
15
+ end
16
+
17
+ def connect(started = Process.clock_gettime(Process::CLOCK_MONOTONIC))
18
+ @connection = PG.connect(@connection_string)
19
+ rescue PG::ConnectionBad
20
+ ended = Process.clock_gettime(Process::CLOCK_MONOTONIC)
21
+
22
+ if ended - started < SQLRunner.timeout
23
+ sleep 0.1
24
+ connect(started)
25
+ else
26
+ raise
27
+ end
28
+ end
29
+
30
+ def disconnect
31
+ @connection && @connection.close && (@connection = nil)
32
+ end
33
+
34
+ def reconnect
35
+ disconnect
36
+ connect
37
+ end
38
+
39
+ def connection
40
+ @connection
41
+ end
42
+
43
+ def execute(query, **bind_vars)
44
+ query, bindings = parse(query)
45
+ args = extract_args(query, bindings, bind_vars)
46
+ @connection.exec_params(query, args)
47
+ rescue PG::ConnectionBad
48
+ reconnect
49
+ execute(query, **bind_vars)
50
+ end
51
+
52
+ def active?
53
+ @connection && @connection.status == PG::Connection::CONNECTION_OK
54
+ rescue PGError
55
+ false
56
+ end
57
+
58
+ def to_s
59
+ %[#<#{self.class.name} #{"0x00%x" % (object_id << 1)}>]
60
+ end
61
+
62
+ def inspect
63
+ to_s
64
+ end
65
+
66
+ def parse(query)
67
+ bindings = {}
68
+ count = 0
69
+
70
+ parsed_query = query.gsub(/(:?):([a-zA-Z]\w*)/) do |match|
71
+ next match if $1 == ":" # skip type casting
72
+
73
+ name = match[1..-1]
74
+ sym_name = name.to_sym
75
+
76
+ if (!index = bindings[sym_name])
77
+ index = (count += 1)
78
+ bindings[sym_name] = index
79
+ end
80
+
81
+ "$#{index}"
82
+ end
83
+
84
+ [parsed_query, bindings]
85
+ end
86
+
87
+ private def extract_args(query, bindings, bind_vars)
88
+ bindings.each_with_object([]) do |(name, position), buffer|
89
+ buffer[position - 1] = bind_vars.fetch(name) { fail InvalidPreparedStatement, "missing value for :#{name} in #{query}" }
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,7 @@
1
+ module SQLRunner
2
+ module Configuration
3
+ attr_accessor :root_dir
4
+ attr_accessor :timeout
5
+ attr_accessor :pool
6
+ end
7
+ end
@@ -0,0 +1,28 @@
1
+ module SQLRunner
2
+ module Connection
3
+ def self.call(connection_string)
4
+ uri = URI.parse(connection_string)
5
+ adapter = Adapters.find(uri.scheme)
6
+
7
+ ConnectionPool.new(timeout: SQLRunner.timeout, size: SQLRunner.pool) do
8
+ adapter.new(connection_string)
9
+ end
10
+ end
11
+
12
+ def with_connection(&block)
13
+ connection_pool.with(&block)
14
+ end
15
+
16
+ def connect(connection_string)
17
+ @connection_pool = Connection.call(connection_string)
18
+ end
19
+
20
+ def disconnect
21
+ connection_pool && connection_pool.shutdown {|conn| conn.disconnect } && (@connection_pool = nil)
22
+ end
23
+
24
+ def connection_pool
25
+ @connection_pool
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,70 @@
1
+ module SQLRunner
2
+ RecordNotFound = Class.new(StandardError)
3
+ PluginNotFound = Class.new(StandardError)
4
+ InvalidPluginOrder = Class.new(StandardError)
5
+
6
+ class Query
7
+ extend Runner
8
+
9
+ PLUGINS = {}
10
+
11
+ def self.query_name(*values)
12
+ @query_name = values.first if values.any?
13
+ @query_name || (@query_name = query_name_from_class)
14
+ end
15
+
16
+ def self.query_name_from_class
17
+ name
18
+ .gsub("::", "/")
19
+ .gsub(/([a-z0-9])([A-Z])/) { "#{$1}_#{$2.downcase}" }
20
+ .downcase
21
+ end
22
+
23
+ def self.query(*value)
24
+ @query = value.first if value.any?
25
+ @query || (@query = File.read(File.join(root_dir, "#{query_name}.sql")))
26
+ end
27
+
28
+ def self.connection_pool
29
+ @connection_pool || SQLRunner.connection_pool
30
+ end
31
+
32
+ def self.root_dir(*value)
33
+ @root_dir = value.first if value.any?
34
+ @root_dir || SQLRunner.root_dir
35
+ end
36
+
37
+ def self.call(**bind_vars)
38
+ execute(query, **bind_vars)
39
+ end
40
+
41
+ def self.register_plugin(name, mod)
42
+ PLUGINS[name] = mod
43
+ end
44
+
45
+ def self.plugin(*names)
46
+ plugins *names
47
+ end
48
+
49
+ def self.plugins(*names)
50
+ names = prepare_plugins_with_options(names)
51
+
52
+ names.each do |name, options|
53
+ plugin = PLUGINS.fetch(name) { fail PluginNotFound, "#{name.inspect} wasn't found" }
54
+ plugin.activate(self, options)
55
+ end
56
+ end
57
+
58
+ def self.prepare_plugins_with_options(plugins)
59
+ return plugins unless plugins.last.kind_of?(Hash)
60
+
61
+ plugins_with_options = plugins.pop
62
+
63
+ plugins_with_options.each do |(name, options)|
64
+ plugins << [name.to_sym, options]
65
+ end
66
+
67
+ plugins
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,13 @@
1
+ module SQLRunner
2
+ class Query
3
+ module Many
4
+ def self.activate(target, _options)
5
+ target.singleton_class.prepend self
6
+ end
7
+
8
+ def call(**bind_vars)
9
+ super(**bind_vars).to_a
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,24 @@
1
+ module SQLRunner
2
+ class Query
3
+ module Model
4
+ def self.activate(target, model)
5
+ target.singleton_class.class_eval do
6
+ attr_accessor :model
7
+ end
8
+
9
+ target.model = model
10
+ target.singleton_class.prepend self
11
+ end
12
+
13
+ def call(**bind_vars)
14
+ result = super(**bind_vars)
15
+ return unless result
16
+ return model.new(result) if result.kind_of?(Hash)
17
+
18
+ result.to_a.map do |attrs|
19
+ model.new(attrs)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,18 @@
1
+ module SQLRunner
2
+ class Query
3
+ module One
4
+ def self.activate(target, _options)
5
+ target.singleton_class.prepend self
6
+ end
7
+
8
+ def call(**bind_vars)
9
+ result = super(**bind_vars)
10
+ result.to_a.first
11
+ end
12
+
13
+ def call!(**bind_vars)
14
+ call(**bind_vars) or fail SQLRunner::RecordNotFound, "#{name}: record was not found with #{bind_vars.inspect} arguments"
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,11 @@
1
+ module SQLRunner
2
+ module Runner
3
+ include Connection
4
+
5
+ def execute(query, **bind_vars)
6
+ with_connection do |conn|
7
+ conn.execute(query, **bind_vars)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,3 @@
1
+ module SQLRunner
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,28 @@
1
+ require "./lib/sql_runner/version"
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "sql_runner"
5
+ spec.version = SQLRunner::VERSION
6
+ spec.authors = ["Nando Vieira"]
7
+ spec.email = ["fnando.vieira@gmail.com"]
8
+
9
+ spec.summary = "SQLRunner allows you to load your queries out of SQL files, without using ORMs. Available only for PostgreSQL."
10
+ spec.description = spec.summary
11
+ spec.homepage = "https://github.com/fnando/sql_runner"
12
+ spec.license = "MIT"
13
+
14
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
15
+ spec.bindir = "exe"
16
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
17
+ spec.require_paths = ["lib"]
18
+
19
+ spec.add_dependency "connection_pool"
20
+
21
+ spec.add_development_dependency "bundler"
22
+ spec.add_development_dependency "rake"
23
+ spec.add_development_dependency "minitest-utils"
24
+ spec.add_development_dependency "pry-meta"
25
+ spec.add_development_dependency "pg"
26
+ spec.add_development_dependency "mocha"
27
+ spec.add_development_dependency "codeclimate-test-reporter"
28
+ end
metadata ADDED
@@ -0,0 +1,186 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sql_runner
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Nando Vieira
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-06-07 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: connection_pool
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: minitest-utils
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: pry-meta
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: pg
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: mocha
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: codeclimate-test-reporter
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ description: SQLRunner allows you to load your queries out of SQL files, without using
126
+ ORMs. Available only for PostgreSQL.
127
+ email:
128
+ - fnando.vieira@gmail.com
129
+ executables: []
130
+ extensions: []
131
+ extra_rdoc_files: []
132
+ files:
133
+ - ".gitignore"
134
+ - ".travis.yml"
135
+ - CODE_OF_CONDUCT.md
136
+ - Gemfile
137
+ - LICENSE.txt
138
+ - README.md
139
+ - Rakefile
140
+ - bin/console
141
+ - bin/setup
142
+ - examples/bench.rb
143
+ - examples/profiling.rb
144
+ - examples/sql/create_user.sql
145
+ - examples/sql/delete_all_users.sql
146
+ - examples/sql/find_user.sql
147
+ - examples/sql/users.sql
148
+ - examples/test.rb
149
+ - lib/sql_runner.rb
150
+ - lib/sql_runner/adapters.rb
151
+ - lib/sql_runner/adapters/postgresql.rb
152
+ - lib/sql_runner/configuration.rb
153
+ - lib/sql_runner/connection.rb
154
+ - lib/sql_runner/query.rb
155
+ - lib/sql_runner/query/many.rb
156
+ - lib/sql_runner/query/model.rb
157
+ - lib/sql_runner/query/one.rb
158
+ - lib/sql_runner/runner.rb
159
+ - lib/sql_runner/version.rb
160
+ - sql_runner.gemspec
161
+ homepage: https://github.com/fnando/sql_runner
162
+ licenses:
163
+ - MIT
164
+ metadata: {}
165
+ post_install_message:
166
+ rdoc_options: []
167
+ require_paths:
168
+ - lib
169
+ required_ruby_version: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ required_rubygems_version: !ruby/object:Gem::Requirement
175
+ requirements:
176
+ - - ">="
177
+ - !ruby/object:Gem::Version
178
+ version: '0'
179
+ requirements: []
180
+ rubyforge_project:
181
+ rubygems_version: 2.5.1
182
+ signing_key:
183
+ specification_version: 4
184
+ summary: SQLRunner allows you to load your queries out of SQL files, without using
185
+ ORMs. Available only for PostgreSQL.
186
+ test_files: []