sbf-dm-sql-finders 0.0.4

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
+ SHA256:
3
+ metadata.gz: 665a67daa8f15e23b7aab76eba4254701a2cfc1a48a7a713eec4416802f7e22e
4
+ data.tar.gz: d25d3a27c1887c90a0c619d9e97c856de90ca41477406af55ee1de7a118baf9a
5
+ SHA512:
6
+ metadata.gz: 8209b337ad35dd5f9a5cfc827cc48873488da5b2946b229e20c925e2485c099150f86424820618e41b83f87230f4c4efd0e9832b981563da23e76bef3278c0a2
7
+ data.tar.gz: 0efb3a89f66debd66f878807e227c4ed894d011d0ffb2654f486205f59a15ae000040e9eb7ab56a3ebb2fe50cc7569f4978a082d85279f5647f46eda5c17f834
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --colour
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in dm-sql-finders.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Chris Corbyn
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,271 @@
1
+ # DataMapper SQL Finders
2
+
3
+ DataMapper SQL Finders adds `#by_sql` to your models, as a compliment to the standard pure-ruby
4
+ query system used by DataMapper. The SQL you write will be executed directly against the adapter,
5
+ but you do not need to lose the benefit of the field and table name abstraction offered by DM.
6
+ When you invoke `#by_sql`, one or more table representations are yielded into a block, which you
7
+ provide. These objects are interpolated into your SQL, such that you use DataMapper properties in
8
+ the SQL and make no direct reference to the real field names in the database schema.
9
+
10
+ I wrote this gem because my original reason for using DataMapper was that I needed to work with a
11
+ large, existing database schema belonging to an old PHP application, subsequently ported to Rails.
12
+ We need the naming abstraction offered by DataMapper, but we also prefer its design. That said, we
13
+ run some queries that are less than trivial, mixing LEFT JOINs and inline derived tables…
14
+ something which DataMapper does not currently handle at all. `#by_sql` allows us to drop down to
15
+ SQL in places where we need it, but where we don't want to by-pass DataMapper entirely.
16
+
17
+ The `#by_sql` method returns a DataMapper `Collection` wrapping a `Query`, in just the same way
18
+ `#all` does. Just like using `#all`, no SQL is executed until a kicker triggers its execution
19
+ (e.g. beginning to iterate over a Collection).
20
+
21
+ Because the implementation is based around a real DataMapper Query object, you can chain other
22
+ DataMapper methods, such as `#all`, or methods or relationships defined on your Model.
23
+
24
+ It looks like this:
25
+
26
+ ``` ruby
27
+ class Post
28
+ include DataMapper::Resource
29
+
30
+ property :id, Serial
31
+ property :title, String
32
+
33
+ belongs_to :user
34
+ end
35
+
36
+ class User
37
+ include DataMapper::Resource
38
+
39
+ property :id, Serial
40
+ property :username, String
41
+
42
+ has n, :posts
43
+
44
+
45
+ def self.never_posted
46
+ by_sql(Post) { |u, p| "SELECT #{u.*} FROM #{u} LEFT JOIN #{p} ON #{p.user_id} = #{u.id} WHERE #{p.id} IS NULL" }
47
+ end
48
+ end
49
+
50
+ User.never_posted.each do |user|
51
+ puts "#{user.username} has never created a Post"
52
+ end
53
+ ```
54
+
55
+ The first block argument is always the current Model. You can optionally pass additional models to `#by_sql` and have
56
+ them yielded into the block if you need to join.
57
+
58
+ You may chain regular DataMapper finders onto the result (the original SQL is modified with the additions):
59
+
60
+ ``` ruby
61
+ User.never_posted.all(:username.like => "%bob%")
62
+ ```
63
+
64
+ ## A note about DataMapper 2.0
65
+
66
+ The DataMapper guys are hard at work creating DataMapper 2.0, which involves a lot of under-the-surface changes, most
67
+ notably building DM's query interface atop [Veritas](https://github.com/dkubb/veritas), with the adapter layer generating
68
+ SQL by walking a Veritas relation (an AST - abstract syntax tree). Because of the way DM 1 handles queries, it is not
69
+ trivial to support SQL provided by the user (except for the trival case of it being in the WHERE clause). With any hope,
70
+ gems like this will either not be needed in DM 2.0, or at least will be easy to implement cleanly, since SQL and Veritas
71
+ play nicely with each other.
72
+
73
+ ## Installation
74
+
75
+ Via rubygems:
76
+
77
+ gem install dm-sql-finders
78
+
79
+ ## Detailed Usage
80
+
81
+ Note that in the following examples, you are not forced to use the table representations yielded into the block, but you
82
+ are encouraged to. They respond to the following methods:
83
+
84
+ - `tbl.*`: expands the splat to only the known fields defined in your model. Other fields in the database are excluded.
85
+ - `tbl.to_s`: represents the name of the table in the database. `#to_s` is invoked implcitly in String context. Note
86
+ that if you join to the same table multiple times, DataMapper SQL Finders will alias them accordingly.
87
+ - `tbl.property_name`: represents the field name in the database mapping to `property_name` in your model.
88
+
89
+ Writing the field/table names directly, while it will work, is not advised, since it will significantly hamper any future
90
+ efforts to chain onto the query (and it reads just like SQL anyway, right?).
91
+
92
+ ### Basic SELECT statements
93
+
94
+ Returning a String from the block executes the SQL when a kicker is invoked (e.g. iterating the Collection).
95
+
96
+ ``` ruby
97
+ def self.basically_everything
98
+ by_sql { |m| "SELECT #{m.*} FROM #{m}" }
99
+ end
100
+ ```
101
+
102
+ ### Passing in variables
103
+
104
+ The block may return an Array, with the first element as the SQL and the following elements as the bind values.
105
+
106
+ ``` ruby
107
+ def self.created_after(time)
108
+ by_sql { |m| ["SELECT #{m.*} FROM #{m} WHERE #{m.created_at} > ?", time] }
109
+ end
110
+ ```
111
+
112
+ ### Selecting less than all fields
113
+
114
+ Just specify individual fields in the SQL. The regular DM semantics apply (i.e. the rest will be lazy loaded, and omitting the
115
+ primary key means your records are immutable).
116
+
117
+ ``` ruby
118
+ def self.usernames_only
119
+ by_sql { |u| "SELECT #{u.username} FROM #{u}" }
120
+ end
121
+ ```
122
+
123
+ ### Selecting *more* than all fields (experimental)
124
+
125
+ This allows you to pre-load things like aggregate calculations you may otherwise add denormalizations for:
126
+
127
+ ``` ruby
128
+ def self.with_post_counts
129
+ by_sql(Post) { |u, p| "SELECT #{u.*}, COUNT(#{p.id}) AS post_count FROM #{u} INNER JOIN #{p} ON #{p.user_id} = #{u.id} GROUP BY #{u.id}" }
130
+ end
131
+ ```
132
+
133
+ A `@post_count` instnace variable is set on all resources. Currently this is always a String. You will need to typecast manually.
134
+
135
+ See the section on "Joins" for details on the join syntax.
136
+
137
+ You should consider this feature experimental. It takes advantage of the fact DM Property instances can be created and thrown-away
138
+ on-the-fly.
139
+
140
+ ### Ordering
141
+
142
+ DataMapper always adds an ORDER BY to your queries if you don't specify one. DataMapper SQL Finders behaves no differently.
143
+ The default ordering is always ascending by primary key. You can override it in the SQL:
144
+
145
+ ``` ruby
146
+ def self.backwards
147
+ by_sql { |m| "SELECT #{m.*} FROM #{m} ORDER BY #{m.id} DESC" }
148
+ end
149
+ ```
150
+
151
+ Or you can provide it as a regular option to `#by_sql`, just like you can with `#all`:
152
+
153
+ ``` ruby
154
+ def self.backwards
155
+ by_sql(:order => [:id.desc]) { |m| "SELECT #{m.*} FROM #{m}" }
156
+ end
157
+ ```
158
+
159
+ Note that the `:order` option take precendence over anything specified in the SQL. This allows method chains to override it.
160
+
161
+ ### Joins
162
+
163
+ The additional models are passed to `#by_sql`, then you use them to construct the join.
164
+
165
+ ``` ruby
166
+ class User
167
+ ... snip ...
168
+
169
+ def self.posted_today
170
+ by_sql(Post) { |u, p| ["SELECT #{u.*} FROM #{u} INNER JOIN #{p} ON #{p.user_id} = #{u.id} WHERE #{p.created_at} > ?", Date.today - 1] }
171
+ end
172
+ end
173
+ ```
174
+
175
+ The `:links` option will also be interpreted and added to the `FROM` clause in the SQL. This is useful if you need to override the SQL.
176
+
177
+ ### Limits and offsets
178
+
179
+ These can be specified in the SQL:
180
+
181
+ ``` ruby
182
+ def self.penultimate_five
183
+ by_sql { |m| "SELECT #{m.*} FROM #{m} ORDER BY #{m.id} DESC LIMIT 5 OFFSET 5" }
184
+ end
185
+ ```
186
+
187
+ Or they can be provided as options to `#by_sql`:
188
+
189
+ ``` ruby
190
+ def self.penultimate_five
191
+ by_sql(:limit => 5, :offset => 5) { |m| "SELECT #{m.*} FROM #{m}" }
192
+ end
193
+ ```
194
+
195
+ If `:limit` and/or `:offset` are passed to `#by_sql`, they take precedence over anything written in the SQL itself.
196
+
197
+ ### Method chaining
198
+
199
+ Method chaining with `#by_sql`, for the most part, works just like with `#all`. There are some current limitations,
200
+ such as reversing the order of a query that used `ORDER BY` in the SQL, rather than via an `:order` option.
201
+
202
+ Also note the you may not currently chain `#by_sql` calls together. `#by_sql` must, logically, always be the first
203
+ call in the chain.
204
+
205
+ ``` ruby
206
+ User.by_sql{ |u| ["SELECT #{u.*} FROM #{u} WHERE #{u.role} = ?", "Manager"] }.all(:username.like => "%bob%", :order => [:username.desc])
207
+ ```
208
+
209
+ ### Unions, Intersections and Differences
210
+
211
+ Unfortunately this is not currently supported, and will likely only be added after the other limitations are worked out.
212
+
213
+ Specifically, queries like this:
214
+
215
+ ``` ruby
216
+ User.by_sql { |u| ["SELECT #{u.*} FROM #{u} WHERE #{u.created_at} < ?", Date.today - 365] } | User.all(:admin => true)
217
+ ```
218
+
219
+ Should really produce SQL of the nature:
220
+
221
+ ``` sql
222
+ SELECT "users"."id", "users"."username", "users"."admin" FROM "users" WHERE ("created_at" < ?) OR (admin = TRUE)
223
+ ```
224
+
225
+ I have no idea what will happen if it is attempted, but it almost certainly will not work ;)
226
+
227
+ ## Will it interfere with DataMapper?
228
+
229
+ `#select_statement` on the adapter is overridden such that, when you use `#by_sql` query, code in the gem is executed, and
230
+ when you execute a regular query, the original code pathways are followed. I'd prefer some sort of extension API in
231
+ DataObjectsAdapter to allow hooks into its SQL generation logic, but for now, this is how it works.
232
+
233
+ DataMapper 2.0 *should* fix this.
234
+
235
+ ## Contributors
236
+
237
+ DataMapper SQL Finders is currently written by [Chris Corbyn](https://github.com/d11wtq)
238
+
239
+ Contributions are more than gladly accepted. The primary goal is to support SQL in a way that does not break gems like dm-aggregates
240
+ and dm-pager. The more the SQL can be interpreted and turned into a native Query, the better.
241
+
242
+ ## TODO
243
+
244
+ There are some known limitations, that are mostly edge-cases. You will only run into them if you try to get too crazy combining regular
245
+ DM queries with SQL (e.g. adding `:links` to a hand-written SQL query works, unless you have used bind values somewhere other than the
246
+ WHERE clause *and if*, and *only if* DataMapper needs to use a bind value in the join, such as for special join conditions). Real
247
+ edge-cases.
248
+
249
+ - Support overriding `:fields` in a `#by_sql` query (complex if the query depends on RDBMS native functions in both the WHERE and the SELECT)
250
+ - Reverse the order when invoking `#reverse` in a `#by_sql` query that used `ORDER BY` in the SQL (note this will work just fine if
251
+ you use the `:order` option)
252
+ - Better support for `?` replacements in places other than the `WHERE` clause
253
+ - Support set operations (union, intersection, difference)
254
+ - Possibly (?) support crazy complex mass-updates (seems a little DB-specific though):
255
+ `User.by_sql { ... something with join conditions ... }.update!(:banned => true)` (MySQL, for one, can do `UPDATE ... INNER JOIN ...`)
256
+
257
+ ## Future Plans
258
+
259
+ Before I started writing this, I wanted to implement something similar to Doctrine's (PHP) DQL, probably called DMQL.
260
+ This is basically a strict superset of SQL that is pre-processed with DataMapper, having knowledge of your schema,
261
+ therefore allowing you to simplify the query and let DMQL hande things like JOIN logic. Say, for example:
262
+
263
+ ``` ruby
264
+ Post.by_dmql("JOIN User u WHERE u.username = ?", "Bob")
265
+ ```
266
+
267
+ Which would INNER JOIN posts with users and map `u.username` with the real field name of the `User#username` property.
268
+
269
+ This gem would be a pre-requisite for that.
270
+
271
+ Copyright (c) 2011 Chris Corbyn.
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,35 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "data_mapper/sql_finders/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "sbf-dm-sql-finders"
7
+ s.version = DataMapper::SQLFinders::VERSION
8
+ s.authors = ['opensource_firespring']
9
+ s.email = ['opensource@firespring.com']
10
+ s.homepage = "https://github.com/d11wtq/dm-sql-finders"
11
+ s.summary = %q{Query DataMapper models using raw SQL, without sacrificing property and table name abstraction}
12
+ s.description = %q{dm-sql-finders add #by_sql to your DataMapper models and provides a clean mechanism for using
13
+ the names of the properties in your model, instead of the actual fields in the database. Any SQL
14
+ is supported and actual DataMapper Query objects wrap the SQL, thus delaying its execution until
15
+ a kicker method materializes the records for the query. You can also chain standard DataMapper
16
+ query methods onto the #by_sql call to refine the query.}
17
+ s.license = 'Nonstandard'
18
+
19
+ s.rubyforge_project = "dm-sql-finders"
20
+
21
+ s.files = `git ls-files`.split("\n")
22
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
23
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
24
+ s.require_paths = ["lib"]
25
+
26
+ DM_VERSION ||= "~> 1.3"
27
+
28
+ s.add_runtime_dependency "sbf-dm-core", DM_VERSION
29
+ s.add_runtime_dependency "sbf-dm-do-adapter", DM_VERSION
30
+
31
+ s.add_development_dependency "rspec", "~> 3.13"
32
+ s.add_development_dependency "sbf-dm-migrations", DM_VERSION
33
+ s.add_development_dependency "sbf-dm-aggregates", DM_VERSION
34
+ s.add_development_dependency "sbf-dm-sqlite-adapter", DM_VERSION
35
+ end
@@ -0,0 +1,10 @@
1
+ module DataMapper
2
+ class Adapters::DataObjectsAdapter
3
+ def select_statement_with_query(query)
4
+ SQLFinders::SQLBuilder.new(self, query).select_statement
5
+ end
6
+
7
+ alias_method :select_statement_without_query, :select_statement
8
+ alias_method :select_statement, :select_statement_with_query
9
+ end
10
+ end
@@ -0,0 +1,74 @@
1
+ require "forwardable"
2
+
3
+ module DataMapper
4
+ module SQLFinders
5
+ class Query < DataMapper::Query
6
+ def sql=(parts, bind_values)
7
+ @sql_parts = parts
8
+ @sql_values = bind_values
9
+ end
10
+
11
+ def sql
12
+ @sql_parts ||= {}
13
+ @sql_values ||= []
14
+ return @sql_parts, @sql_values
15
+ end
16
+
17
+ def fields
18
+ return super if super.any? { |f| f.kind_of?(Operator) }
19
+ return super unless @sql_parts && @sql_parts.has_key?(:fields)
20
+
21
+ @sql_parts[:fields].map do |field|
22
+ if property = model.properties.detect { |p| p.field == field }
23
+ property
24
+ else
25
+ DataMapper::Property::String.new(model, field)
26
+ end
27
+ end
28
+ end
29
+
30
+ class DefaultDirection < Direction
31
+ extend Forwardable
32
+
33
+ def_delegators :@delegate, :target, :operator, :reverse!, :get
34
+
35
+ def initialize(delegate)
36
+ @delegate = delegate
37
+ end
38
+
39
+ def dup
40
+ self.class.new(@delegate.dup)
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ class Query
47
+ def normalize_order # temporary (will be removed in DM 1.3)
48
+ return if @order.nil?
49
+
50
+ @order = Array(@order)
51
+ @order = @order.map do |order|
52
+ case order
53
+ when Direction
54
+ order.dup
55
+
56
+ when Operator
57
+ target = order.target
58
+ property = target.kind_of?(Property) ? target : @properties[target]
59
+
60
+ Direction.new(property, order.operator)
61
+
62
+ when Symbol, String
63
+ Direction.new(@properties[order])
64
+
65
+ when Property
66
+ Direction.new(order)
67
+
68
+ when Path
69
+ Direction.new(order.property)
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,86 @@
1
+ module DataMapper
2
+ module SQLFinders
3
+ class SQLBuilder
4
+ def initialize(adapter, query)
5
+ @adapter = adapter
6
+ @query = query
7
+ @model = @query.model
8
+ @parts, @sql_values = @query.respond_to?(:sql) ? @query.sql : [{}, []]
9
+ @fields = @query.fields
10
+ @conditions = @query.conditions
11
+ @qualify = @query.links.any? || !@parts[:from].nil?
12
+ @conditions_stmt, @qry_values = @adapter.send(:conditions_statement, @conditions, @qualify)
13
+ @order_by = @query.order
14
+ @limit = @query.limit
15
+ @offset = @query.offset
16
+ @bind_values = @sql_values + @qry_values
17
+ @group_by = if @query.unique?
18
+ @fields.select { |property| property.kind_of?(Property) && @model.properties[property.name] }
19
+ end
20
+ end
21
+
22
+ def select_statement
23
+ return @adapter.send(:select_statement_without_query, @query) unless @query.kind_of?(SQLFinders::Query)
24
+
25
+ statement = [
26
+ columns_fragment,
27
+ from_fragment,
28
+ join_fragment,
29
+ where_fragment,
30
+ group_fragment,
31
+ order_fragment
32
+ ].compact.join(" ")
33
+
34
+ @adapter.send(:add_limit_offset!, statement, @limit, @offset, @bind_values)
35
+
36
+ return statement, @bind_values
37
+ end
38
+
39
+ private
40
+
41
+ def columns_fragment
42
+ if @parts[:select] && @fields.none? { |f| f.kind_of?(DataMapper::Query::Operator) }
43
+ @parts[:select].strip
44
+ else
45
+ "SELECT #{@adapter.send(:columns_statement, @fields, @qualify)}"
46
+ end
47
+ end
48
+
49
+ def from_fragment
50
+ if @parts[:from]
51
+ @parts[:from].strip
52
+ else
53
+ "FROM #{@adapter.send(:quote_name, @model.storage_name(@adapter.name))}"
54
+ end
55
+ end
56
+
57
+ def join_fragment
58
+ @adapter.send(:join_statement, @query, @bind_values, @qualify) if @query.links.any?
59
+ end
60
+
61
+ def where_fragment
62
+ if @parts[:where]
63
+ [@parts[:where].strip, @conditions_stmt].reject{ |c| DataMapper::Ext.blank?(c) }.join(" AND ")
64
+ else
65
+ "WHERE #{@conditions_stmt}" unless DataMapper::Ext.blank?(@conditions_stmt)
66
+ end
67
+ end
68
+
69
+ def group_fragment
70
+ if @parts[:group_by]
71
+ @parts[:group_by].strip
72
+ else
73
+ "GROUP BY #{@adapter.send(:columns_statement, @group_by, @qualify)}" if @group_by && @group_by.any?
74
+ end
75
+ end
76
+
77
+ def order_fragment
78
+ if @parts[:order_by] && (@order_by.nil? || @order_by.all? { |o| o.kind_of?(Query::DefaultDirection) })
79
+ @parts[:order_by].strip
80
+ else
81
+ "ORDER BY #{@adapter.send(:order_statement, @order_by, @qualify)}" if @order_by && @order_by.any?
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,110 @@
1
+ module DataMapper
2
+ module SQLFinders
3
+ class SQLParser
4
+ def initialize(sql)
5
+ @sql = sql.dup.to_s
6
+ end
7
+
8
+ def parse
9
+ tokens = {
10
+ :select => "SELECT",
11
+ :from => "FROM",
12
+ :where => "WHERE",
13
+ :group_by => "GROUP BY",
14
+ :having => "HAVING",
15
+ :order_by => "ORDER BY",
16
+ :limit_offset => "LIMIT"
17
+ }
18
+
19
+ parts = {}
20
+
21
+ tokens.each_with_index do |(key, initial), index|
22
+ parts[key] = scan_chunk(initial, tokens.values[(index + 1)..-1])
23
+ end
24
+
25
+ parse_fields!(parts)
26
+ parse_limit_offset!(parts)
27
+
28
+ parts
29
+ end
30
+
31
+ private
32
+
33
+ def scan_chunk(start_token, end_tokens)
34
+ scan_until(@sql, end_tokens) if @sql =~ /^\s*#{start_token}\b/i
35
+ end
36
+
37
+ def scan_until(str, tokens, include_delimiters = true)
38
+ delimiters = include_delimiters ? { "[" => "]", "(" => ")", "`" => "`", "'" => "'", '"' => '"', "--" => "\n", "/*" => "*/" } : { }
39
+ alternates = ["\\"]
40
+ alternates += delimiters.keys
41
+ alternates += tokens.dup
42
+ regex_body = alternates.map{ |v| v.kind_of?(Regexp) ? v.to_s : Regexp.escape(v) }.join("|")
43
+ pattern = /^(?:#{regex_body}|.)/i
44
+
45
+ chunk = ""
46
+
47
+ while result = pattern.match(str)
48
+ token = result.to_s
49
+ case token
50
+ when "\\"
51
+ chunk << str.slice!(0, 2) # escape consumes following character, always
52
+ when *delimiters.keys
53
+ chunk << str.slice!(0, token.length) << scan_until(str, [delimiters[token]], false)
54
+ when *tokens
55
+ if include_delimiters
56
+ return chunk
57
+ else
58
+ return chunk << str.slice!(0, token.length)
59
+ end
60
+ else
61
+ chunk << str.slice!(0, token.length)
62
+ end
63
+ end
64
+
65
+ chunk
66
+ end
67
+
68
+ def parse_fields!(parts)
69
+ return unless fragment = parts[:select]
70
+
71
+ if m = /^\s*SELECT(?:\s+DISTINCT)?\s+(.*)/is.match(fragment)
72
+ full_fields_str = m[1].dup
73
+ fields_with_aliases = []
74
+ while full_fields_str.length > 0
75
+ fields_with_aliases << scan_until(full_fields_str, [","]).strip
76
+ full_fields_str.slice!(0, 1) if full_fields_str.length > 0
77
+ end
78
+ parts[:fields] = fields_with_aliases.collect { |f| extract_field_name(f) }
79
+ end
80
+ end
81
+
82
+ def extract_field_name(field)
83
+ # simple hack: the last token in a SELECT expression is always (conveniently) the alias, regardless of whether an AS is used, or an alias even given at all
84
+ full_str_rtl = field.dup.reverse
85
+ qualified_alias_str_rtl = scan_until(full_str_rtl, [/\s+/])
86
+ alias_str = scan_until(qualified_alias_str_rtl, ["."]).reverse
87
+ case alias_str[0]
88
+ when '"', '"', "`"
89
+ alias_str[1...-1].gsub(/\\(.)/, "\\1")
90
+ else
91
+ alias_str
92
+ end
93
+ end
94
+
95
+ def parse_limit_offset!(parts)
96
+ return unless fragment = parts[:limit_offset]
97
+
98
+ if m = /^\s*LIMIT\s+(\d+)\s*,\s*(\d+)/i.match(fragment)
99
+ parts[:limit] = m[2].to_i
100
+ parts[:offset] = m[1].to_i
101
+ elsif m = /^\s*LIMIT\s+(\d+)\s+OFFSET\s+(\d+)/i.match(fragment)
102
+ parts[:limit] = m[1].to_i
103
+ parts[:offset] = m[2].to_i
104
+ elsif m = /^\s*LIMIT\s+(\d+)/i.match(fragment)
105
+ parts[:limit] = m[1].to_i
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,49 @@
1
+ module DataMapper
2
+ module SQLFinders
3
+ class TableRepresentation
4
+ class << self
5
+ def from_models(*models)
6
+ seen = {}
7
+ models.map do |model|
8
+ seen[model] ||= -1
9
+ new(model, seen[model] += 1)
10
+ end
11
+ end
12
+ end
13
+
14
+ def initialize(model, idx = 0)
15
+ @model = model
16
+ @idx = idx
17
+ end
18
+
19
+ def to_s
20
+ @model.repository.adapter.send(:quote_name, @model.storage_name).dup.tap do |tbl|
21
+ tbl << " AS #{storage_alias}" unless tbl == storage_alias
22
+ end
23
+ end
24
+
25
+ def storage_alias
26
+ name = @model.storage_name.dup.tap do |name|
27
+ name << "_#{@idx}" if @idx > 0
28
+ end
29
+ @model.repository.adapter.send(:quote_name, name)
30
+ end
31
+
32
+ def *
33
+ @model.properties.map { |p| "#{storage_alias}.#{@model.repository.adapter.send(:quote_name, p.field)}" }.join(", ")
34
+ end
35
+
36
+ def method_missing(name, *args, &block)
37
+ return super unless args.size == 0 && !block_given?
38
+
39
+ if property = @model.properties[name]
40
+ "#{storage_alias}.#{@model.repository.adapter.send(:quote_name, property.field)}"
41
+ elsif @model.method_defined?(name)
42
+ @model.repository.adapter.send(:quote_name, name)
43
+ else
44
+ super
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,5 @@
1
+ module DataMapper
2
+ module SQLFinders
3
+ VERSION = "0.0.4"
4
+ end
5
+ end
@@ -0,0 +1,28 @@
1
+ module DataMapper
2
+ module SQLFinders
3
+ def sql(*models)
4
+ raise ArgumentError, "Block required" unless block_given?
5
+ yield *TableRepresentation.from_models(*models)
6
+ end
7
+
8
+ def by_sql(*additional_models, &block)
9
+ options = if additional_models.last.kind_of?(Hash)
10
+ additional_models.pop
11
+ else
12
+ {}
13
+ end
14
+
15
+ sql, *bind_values = sql(self, *additional_models, &block)
16
+ parts = SQLParser.new(sql).parse
17
+
18
+ options[:limit] ||= parts[:limit] if parts[:limit]
19
+ options[:offset] ||= parts[:offset] if parts[:offset]
20
+
21
+ Collection.new(Query.new(repository, self, options).tap { |q| q.send(:sql=, parts, bind_values) })
22
+ end
23
+
24
+ def default_order(repository_name = default_repository_name)
25
+ Array(super).map { |d| Query::DefaultDirection.new(d) }.freeze
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,11 @@
1
+ require "dm-core"
2
+ require "dm-do-adapter"
3
+ require "data_mapper/sql_finders/sql_builder"
4
+ require "data_mapper/sql_finders/sql_parser"
5
+ require "data_mapper/sql_finders/table_representation"
6
+ require "data_mapper/sql_finders/adapter"
7
+ require "data_mapper/sql_finders/query"
8
+ require "data_mapper/sql_finders/version"
9
+ require "data_mapper/sql_finders"
10
+
11
+ DataMapper::Model.append_extensions(DataMapper::SQLFinders)
@@ -0,0 +1,331 @@
1
+ require "spec_helper"
2
+
3
+ describe DataMapper::Adapters::DataObjectsAdapter do
4
+ before(:each) do
5
+ @bob = User.create(:username => "Bob", :role => "Manager")
6
+ @fred = User.create(:username => "Fred", :role => "Tea Boy")
7
+ end
8
+
9
+ context "query without SQL" do
10
+ before(:each) do
11
+ @users = User.all(:username => "Bob")
12
+ @sql, @bind_values = User.repository.adapter.send(:select_statement, @users.query)
13
+ end
14
+
15
+ it "behaves unchanged" do
16
+ @users.to_a.count.should == 1
17
+ @users.to_a.first.should == @bob
18
+ @sql.should == %{SELECT "id", "username", "role" FROM "users" WHERE "username" = ? ORDER BY "id"}
19
+ @bind_values.should == ["Bob"]
20
+ end
21
+ end
22
+
23
+ context "querying by SQL" do
24
+ context "with a basic SELECT statement" do
25
+ before(:each) do
26
+ @users = User.by_sql { |u| ["SELECT #{u.*} FROM #{u} WHERE #{u.role} = ?", "Manager"] }
27
+ @sql, @bind_values = User.repository.adapter.send(:select_statement, @users.query)
28
+ end
29
+
30
+ it "executes the original query" do
31
+ @sql.should == %q{SELECT "users"."id", "users"."username", "users"."role" FROM "users" WHERE "users"."role" = ? ORDER BY "users"."id"}
32
+ @bind_values.should == ["Manager"]
33
+ end
34
+
35
+ it "finds the matching resources" do
36
+ @users.should include(@bob)
37
+ end
38
+
39
+ it "does not find incorrect resources" do
40
+ @users.should_not include(@fred)
41
+ end
42
+
43
+ describe "chaining" do
44
+ describe "to #all" do
45
+ before(:each) do
46
+ @jim = User.create(:username => "Jim", :role => "Manager")
47
+ @users = @users.all(:username => "Jim")
48
+ @sql, @bind_values = User.repository.adapter.send(:select_statement, @users.query)
49
+ end
50
+
51
+ it "merges the conditions with the original SQL" do
52
+ @sql.should == %q{SELECT "users"."id", "users"."username", "users"."role" FROM "users" WHERE "users"."role" = ? AND "users"."username" = ? ORDER BY "users"."id"}
53
+ @bind_values.should == ["Manager", "Jim"]
54
+ end
55
+
56
+ it "finds the matching resources" do
57
+ @users.should include(@jim)
58
+ end
59
+
60
+ it "does not find incorrect resources" do
61
+ @users.should_not include(@bob)
62
+ end
63
+ end
64
+ end
65
+ end
66
+
67
+ describe "ordering" do
68
+ context "with an ORDER BY clause" do
69
+ before(:each) do
70
+ @users = User.by_sql { |u| "SELECT #{u.*} FROM #{u} ORDER BY #{u.username} DESC" }
71
+ @sql, @bind_values = User.repository.adapter.send(:select_statement, @users.query)
72
+ end
73
+
74
+ it "uses the order from the SQL" do
75
+ @sql.should == %q{SELECT "users"."id", "users"."username", "users"."role" FROM "users" ORDER BY "users"."username" DESC}
76
+ end
77
+
78
+ it "loads the resources in the correct order" do
79
+ @users.to_a.first.should == @fred
80
+ @users.to_a.last.should == @bob
81
+ end
82
+ end
83
+
84
+ context "with :order specified on the query" do
85
+ before(:each) do
86
+ @users = User.by_sql(:order => [:role.asc]) { |u| "SELECT #{u.*} FROM #{u}" }
87
+ @sql, @bind_values = User.repository.adapter.send(:select_statement, @users.query)
88
+ end
89
+
90
+ it "uses the order from the options" do
91
+ @sql.should == %q{SELECT "users"."id", "users"."username", "users"."role" FROM "users" ORDER BY "users"."role"}
92
+ end
93
+
94
+ it "loads the resources in the correct order" do
95
+ @users.to_a.first.should == @bob
96
+ @users.to_a.last.should == @fred
97
+ end
98
+ end
99
+
100
+ context "with both :order and an ORDER BY clause" do
101
+ before(:each) do
102
+ @users = User.by_sql(:order => [:role.desc]) { |u| "SELECT #{u.*} FROM #{u} ORDER BY #{u.username} ASC" }
103
+ @sql, @bind_values = User.repository.adapter.send(:select_statement, @users.query)
104
+ end
105
+
106
+ it "gives the :order option precendence" do
107
+ @sql.should == %q{SELECT "users"."id", "users"."username", "users"."role" FROM "users" ORDER BY "users"."role" DESC}
108
+ end
109
+ end
110
+
111
+ describe "chaining" do
112
+ describe "overriding a previous :order option" do
113
+ before(:each) do
114
+ @users = User.by_sql(:order => [:role.desc]) { |u| "SELECT #{u.*} FROM #{u}" }.all(:order => [:id.asc])
115
+ @sql, @bind_values = User.repository.adapter.send(:select_statement, @users.query)
116
+ end
117
+
118
+ specify "the last :order specified is used" do
119
+ @sql.should == %q{SELECT "users"."id", "users"."username", "users"."role" FROM "users" ORDER BY "users"."id"}
120
+ end
121
+ end
122
+
123
+ describe "overriding the order specified in the SQL" do
124
+ before(:each) do
125
+ @users = User.by_sql { |u| "SELECT #{u.*} FROM #{u} ORDER BY #{u.role} DESC" }.all(:order => [:id.asc])
126
+ @sql, @bind_values = User.repository.adapter.send(:select_statement, @users.query)
127
+ end
128
+
129
+ specify "the last :order specified is used" do
130
+ @sql.should == %q{SELECT "users"."id", "users"."username", "users"."role" FROM "users" ORDER BY "users"."id"}
131
+ end
132
+ end
133
+ end
134
+ end
135
+
136
+ describe "limits" do
137
+ context "with a limit specified by the SQL" do
138
+ before(:each) do
139
+ @users = User.by_sql { |u| "SELECT #{u.*} FROM #{u} LIMIT 1" }
140
+ @sql, @bind_values = User.repository.adapter.send(:select_statement, @users.query)
141
+ end
142
+
143
+ it "uses the limit from the SQL" do
144
+ @sql.should == %q{SELECT "users"."id", "users"."username", "users"."role" FROM "users" ORDER BY "users"."id" LIMIT ?}
145
+ @bind_values.should == [1]
146
+ end
147
+
148
+ it "finds the matching resources" do
149
+ expect(@users.to_a).not_to be_empty
150
+ @users.to_a.first.should == @bob
151
+ end
152
+ end
153
+
154
+ context "with a :limit option to #by_sql" do
155
+ before(:each) do
156
+ @users = User.by_sql(:limit => 1) { |u| "SELECT #{u.*} FROM #{u}" }
157
+ @sql, @bind_values = User.repository.adapter.send(:select_statement, @users.query)
158
+ end
159
+
160
+ it "uses the :limit option" do
161
+ @sql.should == %q{SELECT "users"."id", "users"."username", "users"."role" FROM "users" ORDER BY "users"."id" LIMIT ?}
162
+ @bind_values.should == [1]
163
+ end
164
+
165
+ it "finds the matching resources" do
166
+ expect(@users.to_a).not_to be_empty
167
+ @users.to_a.first.should == @bob
168
+ end
169
+ end
170
+
171
+ context "with both a :limit option and a LIMIT in the SQL" do
172
+ before(:each) do
173
+ @users = User.by_sql(:limit => 1) { |u| "SELECT #{u.*} FROM #{u} LIMIT 2" }
174
+ @sql, @bind_values = User.repository.adapter.send(:select_statement, @users.query)
175
+ end
176
+
177
+ it "the :limit option takes precedence" do
178
+ @sql.should == %q{SELECT "users"."id", "users"."username", "users"."role" FROM "users" ORDER BY "users"."id" LIMIT ?}
179
+ @bind_values.should == [1]
180
+ end
181
+ end
182
+
183
+ context "with an OFFSET in the SQL" do
184
+ before(:each) do
185
+ @users = User.by_sql { |u| "SELECT #{u.*} FROM #{u} LIMIT 1 OFFSET 1" }
186
+ @sql, @bind_values = User.repository.adapter.send(:select_statement, @users.query)
187
+ end
188
+
189
+ it "uses the offset from the SQL" do
190
+ @sql.should == %q{SELECT "users"."id", "users"."username", "users"."role" FROM "users" ORDER BY "users"."id" LIMIT ? OFFSET ?}
191
+ @bind_values.should == [1, 1]
192
+ end
193
+
194
+ it "finds the matching resources" do
195
+ expect(@users.to_a).not_to be_empty
196
+ @users.to_a.first.should == @fred
197
+ end
198
+ end
199
+
200
+ context "with an argument to LIMIT in the SQL" do
201
+ before(:each) do
202
+ @users = User.by_sql { |u| "SELECT #{u.*} FROM #{u} LIMIT 1, 2" }
203
+ @sql, @bind_values = User.repository.adapter.send(:select_statement, @users.query)
204
+ end
205
+
206
+ it "interprets the offset in the SQL" do
207
+ @sql.should == %q{SELECT "users"."id", "users"."username", "users"."role" FROM "users" ORDER BY "users"."id" LIMIT ? OFFSET ?}
208
+ @bind_values.should == [2, 1]
209
+ end
210
+ end
211
+
212
+ context "with an :offset option to #by_sql" do
213
+ before(:each) do
214
+ @users = User.by_sql(:offset => 1) { |u| "SELECT #{u.*} FROM #{u} LIMIT 1" }
215
+ @sql, @bind_values = User.repository.adapter.send(:select_statement, @users.query)
216
+ end
217
+
218
+ it "uses the offset from the options hash" do
219
+ @sql.should == %q{SELECT "users"."id", "users"."username", "users"."role" FROM "users" ORDER BY "users"."id" LIMIT ? OFFSET ?}
220
+ @bind_values.should == [1, 1]
221
+ end
222
+
223
+ it "finds the matching resources" do
224
+ expect(@users.to_a).not_to be_empty
225
+ @users.to_a.first.should == @fred
226
+ end
227
+ end
228
+
229
+ context "with both an OFFSET in the SQL and an :offset option" do
230
+ before(:each) do
231
+ @users = User.by_sql(:offset => 1) { |u| "SELECT #{u.*} FROM #{u} LIMIT 1 OFFSET 0" }
232
+ @sql, @bind_values = User.repository.adapter.send(:select_statement, @users.query)
233
+ end
234
+
235
+ it "the :offset in the options takes precendence" do
236
+ @sql.should == %q{SELECT "users"."id", "users"."username", "users"."role" FROM "users" ORDER BY "users"."id" LIMIT ? OFFSET ?}
237
+ @bind_values.should == [1, 1]
238
+ end
239
+ end
240
+
241
+ describe "chaining" do
242
+ describe "to override a previous :limit option" do
243
+ before(:each) do
244
+ @users = User.by_sql(:limit => 2) { |u| "SELECT #{u.*} FROM #{u}" }.all(:limit => 1)
245
+ @sql, @bind_values = User.repository.adapter.send(:select_statement, @users.query)
246
+ end
247
+
248
+ it "the last :limit option takes precedence" do
249
+ @sql.should == %q{SELECT "users"."id", "users"."username", "users"."role" FROM "users" ORDER BY "users"."id" LIMIT ?}
250
+ @bind_values.should == [1]
251
+ end
252
+ end
253
+
254
+ describe "to override a limit applied in SQL" do
255
+ before(:each) do
256
+ @users = User.by_sql { |u| "SELECT #{u.*} FROM #{u} LIMIT 1" }.all(:limit => 2)
257
+ @sql, @bind_values = User.repository.adapter.send(:select_statement, @users.query)
258
+ end
259
+
260
+ it "the last :limit option takes precedence" do
261
+ @sql.should == %q{SELECT "users"."id", "users"."username", "users"."role" FROM "users" ORDER BY "users"."id" LIMIT ?}
262
+ @bind_values.should == [2]
263
+ end
264
+ end
265
+ end
266
+ end
267
+
268
+ context "with an INNER JOIN" do
269
+ before(:each) do
270
+ @bobs_post = @bob.posts.create(:title => "Bob can write posts")
271
+ @freds_post = @fred.posts.create(:title => "Fred likes to write too")
272
+
273
+ @posts = Post.by_sql(User) { |p, u| ["SELECT #{p.*} FROM #{p} INNER JOIN #{u} ON #{p.user_id} = #{u.id} WHERE #{u.id} = ?", @bob.id] }
274
+ @sql, @bind_values = Post.repository.adapter.send(:select_statement, @posts.query)
275
+ end
276
+
277
+ it "executes the original query" do
278
+ @sql.should == %q{SELECT "posts"."id", "posts"."title", "posts"."user_id" FROM "posts" INNER JOIN "users" ON "posts"."user_id" = "users"."id" WHERE "users"."id" = ? ORDER BY "posts"."id"}
279
+ @bind_values.should == [@bob.id]
280
+ end
281
+
282
+ it "finds the matching resources" do
283
+ @posts.should include(@bobs_post)
284
+ end
285
+
286
+ it "does not find incorrect resources" do
287
+ @posts.should_not include(@freds_post)
288
+ end
289
+
290
+ context "to the same table" do
291
+ before(:each) do
292
+ @posts = Post.by_sql(Post) { |p1, p2| "SELECT #{p1.*} FROM #{p1} INNER JOIN #{p2} ON #{p1.id} = #{p2.id}" }
293
+ @sql, @bind_values = Post.repository.adapter.send(:select_statement, @posts.query)
294
+ end
295
+
296
+ it "creates alises for the subsequent tables" do
297
+ @sql.should == %q{SELECT "posts"."id", "posts"."title", "posts"."user_id" FROM "posts" INNER JOIN "posts" AS "posts_1" ON "posts"."id" = "posts_1"."id" ORDER BY "posts"."id"}
298
+ end
299
+ end
300
+ end
301
+
302
+ describe "aggregate queries" do
303
+ it "supports calculating counts" do
304
+ User.by_sql { |u| "SELECT #{u.*} FROM #{u}" }.count(:id).should == 2
305
+ end
306
+
307
+ it "supports multiple aggregates" do
308
+ User.by_sql { |u| "SELECT #{u.*} FROM #{u}" }.aggregate(:id.count, :id.min, :username.max).should == [2, 1, "Fred"]
309
+ end
310
+ end
311
+
312
+ describe "with virtual attributes" do
313
+ before(:each) do
314
+ @bob.posts.create(:title => "Test")
315
+ @users = User.by_sql(Post) { |u, p| "SELECT #{u.*}, COUNT(#{p.id}) AS #{u.post_count} FROM #{u} INNER JOIN #{p} ON #{p.user_id} = #{u.id}" }
316
+ end
317
+
318
+ it "loads the virtual attributes" do
319
+ @users.to_a.first.post_count.should == 1
320
+ end
321
+ end
322
+
323
+ # fixed an obscure bug with state leakage here
324
+ describe "#reverse!" do
325
+ it "is consistent between invocations" do
326
+ User.all.query.reverse.order.first.operator.should == :desc
327
+ User.all.query.reverse.order.first.operator.should == :desc
328
+ end
329
+ end
330
+ end
331
+ end
@@ -0,0 +1,22 @@
1
+ require "dm-migrations"
2
+ require "dm-aggregates"
3
+ require "dm-sql-finders"
4
+
5
+ DataMapper.setup(:default, "sqlite::memory:")
6
+
7
+ Dir[File.join(File.dirname(__FILE__), "support/**/*.rb")].each { |file| require file }
8
+
9
+ RSpec.configure do |config|
10
+ config.mock_with :rspec
11
+
12
+ config.before(:suite) do
13
+ DataMapper.finalize
14
+ end
15
+
16
+ config.before(:each) do
17
+ DataMapper.auto_migrate!
18
+ end
19
+
20
+ config.after(:each) do
21
+ end
22
+ end
@@ -0,0 +1,8 @@
1
+ class Post
2
+ include DataMapper::Resource
3
+
4
+ property :id, Serial
5
+ property :title, String
6
+
7
+ belongs_to :user
8
+ end
@@ -0,0 +1,13 @@
1
+ class User
2
+ include DataMapper::Resource
3
+
4
+ property :id, Serial
5
+ property :username, String
6
+ property :role, String
7
+
8
+ has n, :posts
9
+
10
+ def post_count
11
+ @post_count.to_i
12
+ end
13
+ end
metadata ADDED
@@ -0,0 +1,156 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sbf-dm-sql-finders
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.4
5
+ platform: ruby
6
+ authors:
7
+ - opensource_firespring
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2025-01-07 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: sbf-dm-core
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.3'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: sbf-dm-do-adapter
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.3'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.3'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.13'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.13'
55
+ - !ruby/object:Gem::Dependency
56
+ name: sbf-dm-migrations
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.3'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.3'
69
+ - !ruby/object:Gem::Dependency
70
+ name: sbf-dm-aggregates
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.3'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.3'
83
+ - !ruby/object:Gem::Dependency
84
+ name: sbf-dm-sqlite-adapter
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.3'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.3'
97
+ description: |-
98
+ dm-sql-finders add #by_sql to your DataMapper models and provides a clean mechanism for using
99
+ the names of the properties in your model, instead of the actual fields in the database. Any SQL
100
+ is supported and actual DataMapper Query objects wrap the SQL, thus delaying its execution until
101
+ a kicker method materializes the records for the query. You can also chain standard DataMapper
102
+ query methods onto the #by_sql call to refine the query.
103
+ email:
104
+ - opensource@firespring.com
105
+ executables: []
106
+ extensions: []
107
+ extra_rdoc_files: []
108
+ files:
109
+ - ".gitignore"
110
+ - ".rspec"
111
+ - Gemfile
112
+ - LICENSE
113
+ - README.md
114
+ - Rakefile
115
+ - dm-sql-finders.gemspec
116
+ - lib/data_mapper/sql_finders.rb
117
+ - lib/data_mapper/sql_finders/adapter.rb
118
+ - lib/data_mapper/sql_finders/query.rb
119
+ - lib/data_mapper/sql_finders/sql_builder.rb
120
+ - lib/data_mapper/sql_finders/sql_parser.rb
121
+ - lib/data_mapper/sql_finders/table_representation.rb
122
+ - lib/data_mapper/sql_finders/version.rb
123
+ - lib/dm-sql-finders.rb
124
+ - spec/public/adapter_spec.rb
125
+ - spec/spec_helper.rb
126
+ - spec/support/fixtures/post.rb
127
+ - spec/support/fixtures/user.rb
128
+ homepage: https://github.com/d11wtq/dm-sql-finders
129
+ licenses:
130
+ - Nonstandard
131
+ metadata: {}
132
+ post_install_message:
133
+ rdoc_options: []
134
+ require_paths:
135
+ - lib
136
+ required_ruby_version: !ruby/object:Gem::Requirement
137
+ requirements:
138
+ - - ">="
139
+ - !ruby/object:Gem::Version
140
+ version: '0'
141
+ required_rubygems_version: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ requirements: []
147
+ rubygems_version: 3.4.10
148
+ signing_key:
149
+ specification_version: 4
150
+ summary: Query DataMapper models using raw SQL, without sacrificing property and table
151
+ name abstraction
152
+ test_files:
153
+ - spec/public/adapter_spec.rb
154
+ - spec/spec_helper.rb
155
+ - spec/support/fixtures/post.rb
156
+ - spec/support/fixtures/user.rb