sql_capsule 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 7e4724c21b2c31b68991236b530a899fbb70152f
4
+ data.tar.gz: 485b598ea069ffb03c1d013a3cf80354a8dd1a27
5
+ SHA512:
6
+ metadata.gz: c5226e6d66ef2cf79a1979581c6d60388921b90a246cd30b7aee6ac1940e6d86f1a270047f909a36518c780e06d2fa66dc12eceb207c9c4463ed6b1acbba8576
7
+ data.tar.gz: c742e47a417cb9d5c5b55474c57b9708f559d0f3ab667a005d77ff1e5fce0588d152eea3444382d361d90e3037068f8c0f0030582278ad268e339e84fe9fb046
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2.2
4
+ before_install: gem install bundler -v 1.10.5
@@ -0,0 +1,13 @@
1
+ # Contributor Code of Conduct
2
+
3
+ As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
4
+
5
+ We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion.
6
+
7
+ Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
8
+
9
+ Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team.
10
+
11
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.
12
+
13
+ This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/)
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in sql_capsule.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 TODO: Write your name
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.
data/README.md ADDED
@@ -0,0 +1,133 @@
1
+ # SQLCapsule
2
+
3
+ SQLCapsule is the culmination of many of my thoughts and concerns surrounding ORMs and how
4
+ we use Ruby to interact with databases. The goal is to be a small and easy to understand
5
+ tool to help you talk to your database without the baggage of a using a full fledged ORM.
6
+ SQLCapsule is reminiscent of the repository pattern (though you may use it however you like)
7
+ and works by registering and naming SQL queries for later use.
8
+
9
+ SQLCapsule aims to provide helpful errors, and to assist you along the way to building
10
+ an application specific PostgreSQL interaction layer.
11
+
12
+ ## Installation
13
+
14
+ Add this line to your application's Gemfile:
15
+
16
+ ```ruby
17
+ gem 'sql_capsule'
18
+ ```
19
+
20
+ And then execute:
21
+
22
+ $ bundle
23
+
24
+ Or install it yourself as:
25
+
26
+ $ gem install sql_capsule
27
+
28
+ ## Usage
29
+
30
+ Initialize a wrapper using a PG connection object:
31
+ ```ruby
32
+ user_database = SQLCapsule.wrap(pg_connection)
33
+ ```
34
+
35
+ Once you have a wrapper you can register bits of SQL. The method
36
+ signature is: `query_name, raw_sql, *arguments`
37
+
38
+ ```ruby
39
+ query = "SELECT * FROM users_table WHERE id = $1;"
40
+ user_database.register(:find_user, query, :id)
41
+ ```
42
+
43
+ If you try and register a SQL statement using `$1` without defining an
44
+ argument name it will raise an error.
45
+
46
+ ```ruby
47
+ user_database.register :find_user, "SELECT * FROM users WHERE id = $1;"
48
+ # SQLCapsule::Query::ArgumentCountMismatchError: Argument count mismatch
49
+ # 0 arguments provided for
50
+ # SQL: SELECT * FROM users WHERE id = $1;
51
+ # Args:[]
52
+ ```
53
+
54
+ Likewise, if you try and register an argument without defining
55
+ its use within the SQL it will also raise an error.
56
+
57
+ ```ruby
58
+ user_database.register :find_user, "SELECT * FROM users;", :id
59
+ # SQLCapsule::Query::ArgumentCountMismatchError: Argument count mismatch
60
+ # 1 arguments provided for
61
+ # SQL: SELECT * FROM users;
62
+ # Args:[:id]
63
+ ```
64
+
65
+ Arguments are used in order, so if you register `:id, :name` then `$1` will
66
+ correspond with `:id` and `$2` will correspond with `:name`. SQLCapsule does
67
+ not attempt to verify your numbering. :-)
68
+
69
+ It is also possible to register a query with a block to handle the resulting rows:
70
+
71
+ ```ruby
72
+ query = "SELECT * FROM users_table WHERE id = $1;", :id
73
+ user_database.register(:find_user, query, :id) { |row| row.merge('preprocessed' => true) }
74
+ user_database.run(:find_user, id: 1) => [ { 'name' => 'John', 'age' => 20, 'id' => 3, 'preprocessed' => true} ]
75
+ ```
76
+
77
+ Any registered query can be called like:
78
+ ```ruby
79
+ user_database.run :find_user, id: 3 # => [ { 'name' => 'John', 'age' => 20, 'id' => 3 } ]
80
+ ```
81
+
82
+ Or with a block:
83
+ ```ruby
84
+ user_database.run(:find_user, id: 3) { |user| user.merge('loaded' => true) }
85
+ # => [ { 'name' => 'John', 'age' => 20, 'id' => 3, 'loaded' => true } ]
86
+ ```
87
+
88
+ Run checks for required keywords when called and will throw an error if missing one:
89
+ ```ruby
90
+ user_database.run :find_user
91
+ # => SQLCapsule::QueryGroup::MissingKeywordArgumentError: Missing query argument: id
92
+ ```
93
+
94
+ The result of a query will return in the form of an array of hashes, where each item in
95
+ the array corresponds to result row, and key/value pairs in the hash correspond with
96
+ column/value pairs in a resulting row.
97
+
98
+ ```ruby
99
+ user_database.register :find_adult_users, "SELECT * FROM users WHERE age >= 18;"
100
+ user_database.run :find_adult_users # => [ { 'name' => 'John', 'age' => 20 }, { 'name' => 'Anne', 'age' => 23 } ]
101
+ ```
102
+
103
+ ### Complex Queries
104
+
105
+ One thing about SQL and relational databases is that returning tables with identical
106
+ column names is a perfectly normal and sane thing to do. SQLCapsule enforces the use
107
+ of `AS` to alias column names and will raise an error when duplicate column names result
108
+ from a query (like a join)
109
+
110
+ ```ruby
111
+ query = 'SELECT * FROM widgets LEFT JOIN orders on widgets.id=orders.widget_id;'
112
+ widget_database.register :join_widgets, query
113
+ widget_database.run :join_widgets
114
+ # SQLCapsule::Wrapper::DuplicateColumnNamesError: Error duplicate column names in resulting table: ["name", "price", "id", "widget_id", "amount", "id"]
115
+ # This usually happens when using a `JOIN` with a `SELECT *`
116
+ # You may need use `AS` to name your columns.
117
+ # QUERY: SELECT * FROM widgets LEFT JOIN orders on widgets.id=orders.widget_id;
118
+ ```
119
+
120
+ ## Development
121
+
122
+ 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.
123
+
124
+ 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).
125
+
126
+ ## Contributing
127
+
128
+ Bug reports and pull requests are welcome on GitHub at https://github.com/piisalie/sql_capsule. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](contributor-covenant.org) code of conduct.
129
+
130
+
131
+ ## License
132
+
133
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,32 @@
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
+ end
9
+
10
+ task :default => :test
11
+
12
+ namespace :db do
13
+
14
+ desc "Create testing database."
15
+ task :create do
16
+ system("createdb --echo sql_capsule_test")
17
+ end
18
+
19
+ desc "Drop testing database."
20
+ task :drop do
21
+ system("dropdb --echo sql_capsule_test")
22
+ end
23
+
24
+ desc "Migrate testing database."
25
+ task :migrate do
26
+ require 'pg'
27
+ db = PG.connect(dbname: 'sql_capsule_test')
28
+ db.exec('CREATE TABLE widgets (name VARCHAR NOT NULL, price INTEGER NOT NULL, id INTEGER NOT NULL UNIQUE);')
29
+ db.exec('CREATE TABLE orders (widget_id INTEGER NOT NULL, amount INTEGER NOT NULL, id INTEGER NOT NULL UNIQUE);')
30
+ end
31
+
32
+ end
data/bin/console ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "sql_capsule"
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
+ @db = PG.connect(dbname: 'sql_capsule_test')
15
+ @wrapper = SQLCapsule.wrap(@db)
16
+ @wrapper.register(:all_widgets, "SELECT * FROM widgets;") { |widget| widget.merge!("inblock" => true) }
17
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+ bundle exec rake db:drop
7
+ bundle exec rake db:create
8
+ bundle exec rake db:migrate
@@ -0,0 +1,44 @@
1
+ module SQLCapsule
2
+ class Query
3
+ attr_reader :sql, :args, :pre_processor
4
+ private :sql
5
+
6
+ def initialize(sql, *args, &pre_processor)
7
+ @sql = sql
8
+ @args = args
9
+ @pre_processor = pre_processor || Proc.new { |row| row }
10
+ verify_arguments
11
+ end
12
+
13
+ def to_sql
14
+ sql.end_with?(";") ? sql : sql + ";"
15
+ end
16
+
17
+ def filter_args(given_args)
18
+ given_args.select { |key, value| args.include?(key) }.values
19
+ end
20
+
21
+ def add_post_processor(block)
22
+ block ||= Proc.new { |row| row }
23
+ Proc.new { |row| block.call(pre_processor.call(row)) }
24
+ end
25
+
26
+ private
27
+
28
+ def verify_arguments
29
+ raise ArgumentCountMismatchError.new(sql,args) unless args_count_matches_sql_args_count?
30
+ end
31
+
32
+ def args_count_matches_sql_args_count?
33
+ sql.scan(/\$\d+/).count == args.count
34
+ end
35
+
36
+ class ArgumentCountMismatchError < RuntimeError
37
+ def initialize(sql, args)
38
+ message = "Argument count mismatch\n#{args.count} arguments provided for\nSQL: #{sql}\nArgs:#{args}"
39
+ super(message)
40
+ end
41
+ end
42
+
43
+ end
44
+ end
@@ -0,0 +1,46 @@
1
+ require_relative 'query'
2
+
3
+ module SQLCapsule
4
+ class QueryGroup
5
+ attr_reader :queries, :wrapper, :query_object
6
+ private :queries, :wrapper, :query_object
7
+
8
+ def initialize(wrapper, query_object = Query)
9
+ @wrapper = wrapper
10
+ @queries = { }
11
+ @query_object = query_object
12
+ end
13
+
14
+ def register(name, query, *args, &block)
15
+ queries[name] = query_object.new(query, *args, &block)
16
+ end
17
+
18
+ def registered_queries
19
+ queries.keys
20
+ end
21
+
22
+ def run name, args = { }, &handler
23
+ query = find_query name
24
+ check_args query.args, args.keys
25
+
26
+ block = query.add_post_processor handler
27
+ wrapper.run(query.to_sql, query.filter_args(args), &block)
28
+ end
29
+
30
+ private
31
+
32
+ def find_query name
33
+ queries.fetch(name) { fail MissingQueryError.new "Query #{name} not registered" }
34
+ end
35
+
36
+ def check_args required_args, args
37
+ required_args.each do |keyword|
38
+ fail MissingKeywordArgumentError.new "Missing query argument: #{keyword}" unless args.include? keyword
39
+ end
40
+ end
41
+
42
+ class MissingQueryError < RuntimeError; end
43
+ class MissingKeywordArgumentError < RuntimeError; end
44
+
45
+ end
46
+ end
@@ -0,0 +1,3 @@
1
+ module SQLCapsule
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,39 @@
1
+ require 'pg'
2
+
3
+ module SQLCapsule
4
+ class Wrapper
5
+ attr_reader :db
6
+ private :db
7
+
8
+ def initialize(db)
9
+ @db = db
10
+ end
11
+
12
+ def run(query, arguments = [ ])
13
+ result = db.exec(query, arguments)
14
+ raise DuplicateColumnNamesError.new(result.fields, query) if duplicate_result_columns?(result.fields)
15
+
16
+ if block_given?
17
+ result.map do |row|
18
+ yield row
19
+ end
20
+ else
21
+ result.to_a
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def duplicate_result_columns?(fields)
28
+ fields.uniq.count != fields.count
29
+ end
30
+
31
+ class DuplicateColumnNamesError < RuntimeError
32
+ def initialize(fields, query)
33
+ message = "Error duplicate column names in resulting table: #{fields}\nThis usually happens when using a `JOIN` with a `SELECT *`\nYou may need use `AS` to name your columns.\nQUERY: #{query}"
34
+ super(message)
35
+ end
36
+ end
37
+
38
+ end
39
+ end
@@ -0,0 +1,9 @@
1
+ require "sql_capsule/version"
2
+ require "sql_capsule/wrapper"
3
+ require "sql_capsule/query_group"
4
+
5
+ module SQLCapsule
6
+ def self.wrap(connection)
7
+ QueryGroup.new(Wrapper.new(connection))
8
+ end
9
+ end
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'sql_capsule/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "sql_capsule"
8
+ spec.version = SQLCapsule::VERSION
9
+ spec.authors = ["Paul Dawson"]
10
+ spec.email = ["paul@into.computer"]
11
+
12
+ spec.summary = %q{ Less ORM, more SQL }
13
+ spec.description = %q{ SQLCapsule is the culmination of many of my thoughts and concerns surrounding ORMs and how we use Ruby to interact with databases. The goal is to be a small and easy to understand tool to help you talk to your database without the baggage of a using a full fledged ORM. SQLCapsule is reminiscent of the repository pattern (though you may use it however you like) and works by registering and naming SQL queries for later use. SQLCapsule aims to provide helpful errors, and to assist you along the way to building an application specific PostgreSQL interaction layer. }
14
+
15
+ spec.homepage = "https://github.com/piisalie/sql_capsule"
16
+ spec.license = "MIT"
17
+
18
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
19
+ spec.bindir = "exe"
20
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
+ spec.require_paths = ["lib"]
22
+ spec.required_ruby_version = '~> 2.0'
23
+
24
+ spec.add_development_dependency "bundler", "~> 1.10"
25
+ spec.add_development_dependency "rake", "~> 10.0"
26
+ spec.add_development_dependency "minitest"
27
+
28
+ spec.add_dependency "pg", "~> 0.18.1"
29
+ end
metadata ADDED
@@ -0,0 +1,121 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sql_capsule
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Paul Dawson
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2015-11-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.10'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.10'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: minitest
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: pg
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 0.18.1
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 0.18.1
69
+ description: " SQLCapsule is the culmination of many of my thoughts and concerns surrounding
70
+ ORMs and how we use Ruby to interact with databases. The goal is to be a small and
71
+ easy to understand tool to help you talk to your database without the baggage of
72
+ a using a full fledged ORM. SQLCapsule is reminiscent of the repository pattern
73
+ (though you may use it however you like) and works by registering and naming SQL
74
+ queries for later use. SQLCapsule aims to provide helpful errors, and to assist
75
+ you along the way to building an application specific PostgreSQL interaction layer. "
76
+ email:
77
+ - paul@into.computer
78
+ executables: []
79
+ extensions: []
80
+ extra_rdoc_files: []
81
+ files:
82
+ - ".gitignore"
83
+ - ".travis.yml"
84
+ - CODE_OF_CONDUCT.md
85
+ - Gemfile
86
+ - LICENSE.txt
87
+ - README.md
88
+ - Rakefile
89
+ - bin/console
90
+ - bin/setup
91
+ - lib/sql_capsule.rb
92
+ - lib/sql_capsule/query.rb
93
+ - lib/sql_capsule/query_group.rb
94
+ - lib/sql_capsule/version.rb
95
+ - lib/sql_capsule/wrapper.rb
96
+ - sql_capsule.gemspec
97
+ homepage: https://github.com/piisalie/sql_capsule
98
+ licenses:
99
+ - MIT
100
+ metadata: {}
101
+ post_install_message:
102
+ rdoc_options: []
103
+ require_paths:
104
+ - lib
105
+ required_ruby_version: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '2.0'
110
+ required_rubygems_version: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - ">="
113
+ - !ruby/object:Gem::Version
114
+ version: '0'
115
+ requirements: []
116
+ rubyforge_project:
117
+ rubygems_version: 2.4.5
118
+ signing_key:
119
+ specification_version: 4
120
+ summary: Less ORM, more SQL
121
+ test_files: []