query_set 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 7c4ccc0d00a6a52280fe9dec2cb138ad071a4811
4
+ data.tar.gz: acfb0146a4b850ced79ed9f782799307a1fe3025
5
+ SHA512:
6
+ metadata.gz: 24b0b9beb0cf0c05816345cfbbf733b87442e2c36740ac7d2a871d9dd935242cb495b22cc8b7b8200751f36896641da6dc8d8f213ad26ca4dde07efea9d18201
7
+ data.tar.gz: 5e6ecc3af71bd77d44b8774d0b057122a992c1f0d69586b05c3af5c5ad0b3770455323e84502537ee7519b72e883392ae98db501d238aefc894ac2bfcb73bbea
data/.gems ADDED
@@ -0,0 +1,2 @@
1
+ cutest -v 1.2.3
2
+ pg -v 0.21.0
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ .gs/
2
+ .DS_Store
3
+ .env
4
+ .gem
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2017 Steven Weiss
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,111 @@
1
+ # QuerySet
2
+
3
+ QuerySet is a small wrapper around the Ruby pg gem for safely executing
4
+ sql that is saved in files.
5
+
6
+ ## Installation
7
+
8
+ `$ gem install query-set`
9
+
10
+ ## API
11
+
12
+ ### QuerySet
13
+
14
+ `initialize` Requires a connection and a formattable directory path.
15
+ Optional arguments include `query:` for setting the query class, and `store:`
16
+ for setting the caching object.
17
+
18
+ `[]` Fetches a query object from the cache (store). If it does not exist, loads
19
+ the corresponding template from disk and compiles it.
20
+
21
+ `[]=` Manually sets an object in the cache.
22
+
23
+ `execute` Delegates execution to the appropriate query object, passing it the configured
24
+ connection and any provided arguments.
25
+
26
+ ### Query
27
+
28
+ `initialize` Requires a string to be compiled into a sql string with ordered
29
+ placeholders.
30
+
31
+ `execute` Sends sql to the connection for the given arguments.
32
+
33
+ ## Usage
34
+
35
+ To get an understanding of QuerySet (qs), we're going to start from the bottom
36
+ and build our way up. The foundation of qs is the class QuerySet::Query.
37
+
38
+ QuerySet::Query is responsible for parsing a template string,
39
+ and converting it into a sql string with position based placeholders that
40
+ Postgres understands. This buys us two things. First, we "upgrade" our coupling
41
+ from one that is based on position to one that is based on name
42
+ (which is generally a good thing, see [connascence][Connascence]). The second
43
+ thing we get is security, since we no longer have to be escaping sql strings
44
+ manually we get to avoid all sorts of vulnerabilities. If you ever find yourself
45
+ using QuerySet::Query manually, remember to always escape any untrusted values
46
+ via `PG::Connection.escape_string`.
47
+
48
+ ```ruby
49
+ ## Initial a new query object.
50
+ query = QuerySet::Query.new("SELECT * FROM users WHERE id = {{ id }} LIMIT 1;")
51
+
52
+ ## During initialization, the query object
53
+ ## 'compiled' the template string into a sql string.
54
+ query.sql ## SELECT * FROM users WHERE id = $1 LIMIT 1;
55
+
56
+ ## It also remembered the params (as symbols).
57
+ query.params ## [:id]
58
+ ```
59
+
60
+ Now that we have converted the template string into sql that our database
61
+ understands, we can execute it. To do so we send our query object
62
+ `##execute(conn, args)`.
63
+
64
+ The first argument, `conn`, is expected to be a `PG::Connection`.
65
+ The second argument is typically a `Hash`,
66
+ though you can pass it custom objects that implement `##values_at` (quick quick).
67
+
68
+ This means that `##execute` knows how to put our args in order
69
+ and then delegate the execution to the connection. The return values are the
70
+ same as using the `pg` gem directly: `PG::Result` on success, and
71
+ on a failure it raises `PG::Error`.
72
+
73
+ You're probably thinking this is a little inconvenient, having to pass the conn
74
+ in each time. But don't worry, this is handled for you by the `QuerySet` class.
75
+
76
+ The QuerySet class is the top level of the library, and is basically a
77
+ factory for your query objects your query objects. It is responsible for:
78
+
79
+ * Holding a reference to your database connection.
80
+ * Dealing with the file system.
81
+ * Caching query objects (not the results, just the compiled templates).
82
+ * Delegating execute to the the appropriate query object.
83
+
84
+ ```ruby
85
+ conn = PG.connection.open(ENV['pg'])
86
+
87
+ ## Construct a QuerySet by giving it a reference to your connection
88
+ ## and also a path where it can find your query templates.
89
+ ## Notice the `%s` in the path, it's super important.
90
+ ## When we call methods on our query set, we will send it the file name
91
+ ## of our query. This file name serves as both the cache key for a query
92
+ ## an it's location on disk.
93
+ query_set = QuerySet.new(conn, './path/to/queries/%s.sql')
94
+
95
+ ## To execute a query located in the directory we configured, we send it
96
+ ## '##execute'.
97
+ query_set.execute('users/by_id', id: id)
98
+
99
+ ## This is the meat of the entire library. The first thing the execute method
100
+ ## does is check the cache to see if a query object exists for the file
101
+ ## name we gave it. If it does not exist, it creates one by giving QuerySet::Query
102
+ ## the template string it finds on disc. Once the template is compiled, we
103
+ ## get to hop back on the branch as if it was a cache hit. On a cache hit,
104
+ ## we send our query object the `##execute` method, passing it the conn we have
105
+ ## a reference to and also the supplied arguments.
106
+ ```
107
+
108
+ Check out the examples directory and tests to get an even better understand
109
+ on how to use QuerySet.
110
+
111
+ [connascence][https://www.youtube.com/watch?v=HQXVKHoUQxY]
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ desc 'open irb in gs context'
2
+ task :console do
3
+ sh 'gs irb'
4
+ end
5
+
6
+ desc 'installs gems'
7
+ task :install do
8
+ sh 'mkdir -p .gs & gs dep install'
9
+ end
10
+
11
+ desc 'tests the given [test].rb'
12
+ task :test, :name do |t, args|
13
+ name = args[:name] || '*'
14
+
15
+ sh "gs cutest -r ./lib/query_set ./test/#{name}_test.rb"
16
+ end
data/bin/rk ADDED
@@ -0,0 +1,8 @@
1
+ #!/bin/sh
2
+
3
+ if [ -f .env ]; then
4
+ env `cat .env` \
5
+ rake $*
6
+ else
7
+ rake $*
8
+ fi
@@ -0,0 +1,34 @@
1
+ # Below is an example on how to implement a poor man's ORM with the
2
+ # active record pattern using QuerySet. If something like this interests you,
3
+ # check out the 'MiniModel' gem at https://github.com/sirscriptalot/mini_model.
4
+
5
+ require 'pg'
6
+ require 'query_set'
7
+
8
+ class Model
9
+ class << self
10
+ attr_accessor :query_set
11
+
12
+ def [](id)
13
+ result = query_set.execute('by_id', id: id)
14
+
15
+ build(result)
16
+ rescue PG::Error => e
17
+ puts e
18
+
19
+ nil
20
+ end
21
+
22
+ alias_method :by_id, :[]
23
+
24
+ def build(result)
25
+ # ...
26
+ end
27
+ end
28
+ end
29
+
30
+ conn = PG::Connection.open(ENV['db1'])
31
+
32
+ User.query_set = QuerySet.new(conn, './sql/users/%s.sql')
33
+
34
+ User[0]
@@ -0,0 +1,20 @@
1
+ # Custom delimiters can be set for compiling templates by subclassing
2
+ # QuerySet::Query.
3
+
4
+ require 'pg'
5
+ require 'query_set'
6
+
7
+ class Query < QuerySet::Query
8
+ # Override the left and right delimiter methods
9
+ # to build a custom regexp for compiling templates.
10
+ def left
11
+ '<%='
12
+ end)
13
+
14
+ def right
15
+ '%>'
16
+ end
17
+ end
18
+
19
+ # Inject your subclass when you initializing the QuerySet.
20
+ QuerySet.new(conn, '', query: Query)
@@ -0,0 +1,15 @@
1
+ # You can "preprocess" your sql templates via string interpolation
2
+ # when working directly with QuerySet::Query.
3
+
4
+ # Unnecessary for trusted data, but always remember to escape user input.
5
+ # Be careful though, escaping user input is never guarenteed to be 100% safe.
6
+ column = conn.escape_string('column')
7
+
8
+ # Initialize a query with an interpolated string with escaped values.
9
+ by_column = QuerySet::Query.new("SELECT * FROM users WHERE #{column} = {{ column }};")
10
+
11
+ # Executing a query directly requires passing it your db connection.
12
+ by_column.execute(conn, column: 'value')
13
+
14
+ # You can also assign a query to an existing query set.
15
+ query_set['by_column'] = by_column
@@ -0,0 +1,10 @@
1
+ require 'pg'
2
+ require 'query_set'
3
+
4
+ # Connect to multiple databases.
5
+ db1 = PG::Connection.open(ENV['db1'])
6
+ db2 = PG::Connection.open(ENV['db2'])
7
+
8
+ # Initialize QuerySet with the appropriate connection.
9
+ users = QuerySet.new(db1, './sql/users/%s.sql')
10
+ posts = QuerySet.new(db2, './sql/posts/%s.sql')
@@ -0,0 +1,3 @@
1
+ SELECT *
2
+ FROM posts
3
+ WHERE title = {{ title }};
@@ -0,0 +1,3 @@
1
+ SELECT id, email
2
+ FROM users
3
+ WHERE id = {{ id }};
data/lib/query_set.rb ADDED
@@ -0,0 +1,84 @@
1
+ class QuerySet
2
+ VERSION = '0.0.0'
3
+
4
+ attr_accessor :conn, :path, :query, :store
5
+
6
+ def initialize(conn, path, query: Query, store: {})
7
+ @conn = conn
8
+ @path = path
9
+ @query = query
10
+ @store = store
11
+ end
12
+
13
+ def [](file_name)
14
+ store.fetch(file_name) do
15
+ store[file_name] = query.new(File.read(path % file_name))
16
+ end
17
+ end
18
+
19
+ def []=(key, value)
20
+ store[key] = value
21
+ end
22
+
23
+ def execute(file_name, *args)
24
+ self.[](file_name).execute(conn, *args)
25
+ end
26
+
27
+ class Query
28
+ LEFT = '{{'
29
+
30
+ RIGHT = '}}'
31
+
32
+ attr_reader :sql, :params
33
+
34
+ def initialize(str)
35
+ @sql = ''
36
+ @params = []
37
+
38
+ compile(str)
39
+
40
+ @sql.freeze
41
+ @params.freeze
42
+ end
43
+
44
+ def execute(conn, args = {})
45
+ conn.exec_params(sql, args.values_at(*params))
46
+ end
47
+
48
+ private
49
+
50
+ def compile(str)
51
+ terms = str.split(regexp)
52
+
53
+ while (term = terms.shift)
54
+ case term
55
+ when left
56
+ param = terms.shift.to_sym
57
+
58
+ # Capture the param for execute position.
59
+ params << param
60
+
61
+ # Append a 1-based placeholder for the param to the sql string.
62
+ sql << "$#{params.index(param).succ}"
63
+ else
64
+ sql << term
65
+ end
66
+ end
67
+
68
+ # Only remember the unique parameters.
69
+ params.uniq!
70
+ end
71
+
72
+ def left
73
+ LEFT
74
+ end
75
+
76
+ def right
77
+ RIGHT
78
+ end
79
+
80
+ def regexp
81
+ /(#{left})\s*(.*?)\s*#{right}/
82
+ end
83
+ end
84
+ end
data/query_set.gemspec ADDED
@@ -0,0 +1,14 @@
1
+ require_relative "./lib/query_set"
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = "query_set"
5
+ s.summary = "QuerySet"
6
+ s.version = QuerySet::VERSION
7
+ s.authors = ["Steve Weiss"]
8
+ s.email = ["weissst@mail.gvsu.edu"]
9
+ s.homepage = "https://github.com/sirscriptalot/query_set"
10
+ s.license = "MIT"
11
+ s.files = `git ls-files`.split("\n")
12
+
13
+ s.add_development_dependency "cutest", "~> 1.2"
14
+ end
@@ -0,0 +1,47 @@
1
+ setup do
2
+ query_set = QuerySet.new(nil, __dir__ + "/sql/%s.sql")
3
+ end
4
+
5
+ test '#[] initializes query objects for path/file_name' do |query_set|
6
+ query = query_set['example']
7
+
8
+ assert query.is_a?(query_set.query)
9
+ end
10
+
11
+ test '#[] memoizes query objects' do |query_set|
12
+ a = query_set['example']
13
+ b = query_set['example']
14
+
15
+ assert_equal a.object_id, b.object_id
16
+ end
17
+
18
+ test '#[]= sets query objects' do |query_set|
19
+ query_set['foo'] = 'bar'
20
+
21
+ assert_equal query_set['foo'], 'bar'
22
+ end
23
+
24
+ class FakeQuery
25
+ attr_reader :conn, :args
26
+
27
+ def initialize(*args); end
28
+
29
+ def execute(conn, args)
30
+ @conn, @args = conn, args
31
+ end
32
+ end
33
+
34
+ test '#execute sends conn and args to query object for file name' do
35
+ conn = :conn
36
+
37
+ args = { foo: :bar }
38
+
39
+ query_set = QuerySet.new(conn, __dir__ + "/sql/%s.sql", query: FakeQuery)
40
+
41
+ query_set.execute('example', args)
42
+
43
+ query = query_set['example']
44
+
45
+ assert_equal query.conn, conn
46
+ assert_equal query.args, args
47
+ end
@@ -0,0 +1,39 @@
1
+ test '#compile replaces identifiers with ordered placeholders' do
2
+ query = QuerySet::Query.new('{{ first }} {{ second }} {{ first }}')
3
+
4
+ assert_equal query.sql, '$1 $2 $1'
5
+ end
6
+
7
+ test '#compile captures unique params' do
8
+ query = QuerySet::Query.new('{{ first }} {{ second }} {{ first }}')
9
+
10
+ assert_equal query.params, [:first, :second]
11
+ end
12
+
13
+ test '#compile ignores whitespace in identifiers' do
14
+ a = QuerySet::Query.new('{{first}} {{second}} {{first}}')
15
+ b = QuerySet::Query.new('{{ first }} {{ second }} {{ first }}')
16
+
17
+ assert_equal a.sql, b.sql
18
+ end
19
+
20
+ class FakeConn
21
+ attr_reader :sql, :ary
22
+
23
+ def exec_params(sql, ary)
24
+ @sql, @ary = sql, ary
25
+ end
26
+ end
27
+
28
+ test '#execute delegates to conn with correct parameters order' do
29
+ conn = FakeConn.new
30
+
31
+ query = QuerySet::Query.new('{{ first }} {{ second }} {{ first }}')
32
+
33
+ args = { second: 2, first: 1}
34
+
35
+ query.execute(conn, args)
36
+
37
+ assert_equal conn.sql, query.sql
38
+ assert_equal conn.ary, [1, 2]
39
+ end
@@ -0,0 +1 @@
1
+ SELECT * FROM table WHERE id = {{ id }};
metadata ADDED
@@ -0,0 +1,75 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: query_set
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Steve Weiss
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-10-18 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: cutest
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.2'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.2'
27
+ description:
28
+ email:
29
+ - weissst@mail.gvsu.edu
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - ".gems"
35
+ - ".gitignore"
36
+ - LICENSE
37
+ - README.md
38
+ - Rakefile
39
+ - bin/rk
40
+ - examples/active_record_pattern.rb
41
+ - examples/custom_query_delimiters.rb
42
+ - examples/interpolated_queries.rb
43
+ - examples/multiple_databases.rb
44
+ - examples/sql/posts/by_title.sql
45
+ - examples/sql/users/by_id.sql
46
+ - lib/query_set.rb
47
+ - query_set.gemspec
48
+ - test/query_set_test.rb
49
+ - test/query_test.rb
50
+ - test/sql/example.sql
51
+ homepage: https://github.com/sirscriptalot/query_set
52
+ licenses:
53
+ - MIT
54
+ metadata: {}
55
+ post_install_message:
56
+ rdoc_options: []
57
+ require_paths:
58
+ - lib
59
+ required_ruby_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: '0'
64
+ required_rubygems_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ requirements: []
70
+ rubyforge_project:
71
+ rubygems_version: 2.6.11
72
+ signing_key:
73
+ specification_version: 4
74
+ summary: QuerySet
75
+ test_files: []