sbf-dm-sql-finders 0.0.4

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
+ 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