dm-sql-finders 0.0.1
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.
- data/.gitignore +4 -0
- data/.rspec +1 -0
- data/Gemfile +4 -0
- data/LICENSE +20 -0
- data/README.md +240 -0
- data/Rakefile +1 -0
- data/dm-sql-finders.gemspec +34 -0
- data/lib/data_mapper/sql_finders.rb +28 -0
- data/lib/data_mapper/sql_finders/adapter.rb +7 -0
- data/lib/data_mapper/sql_finders/query.rb +70 -0
- data/lib/data_mapper/sql_finders/sql_builder.rb +84 -0
- data/lib/data_mapper/sql_finders/sql_parser.rb +110 -0
- data/lib/data_mapper/sql_finders/table_representation.rb +49 -0
- data/lib/data_mapper/sql_finders/version.rb +5 -0
- data/lib/dm-sql-finders.rb +11 -0
- data/spec/public/adapter_spec.rb +309 -0
- data/spec/spec_helper.rb +22 -0
- data/spec/support/fixtures/post.rb +8 -0
- data/spec/support/fixtures/user.rb +13 -0
- metadata +138 -0
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--colour
|
data/Gemfile
ADDED
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,240 @@
|
|
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.
|
71
|
+
|
72
|
+
## Installation
|
73
|
+
|
74
|
+
Via rubygems:
|
75
|
+
|
76
|
+
gem install dm-sql-finders
|
77
|
+
|
78
|
+
Note that while the gem is functional, it has several known limitations, which I aim to work about by improving the
|
79
|
+
parsing and generating logic. It is unlikely you will hit the limitations unless you extensively use `#by_sql` in
|
80
|
+
conjunction with options such as `:links`.
|
81
|
+
|
82
|
+
## Detailed Usage
|
83
|
+
|
84
|
+
Note that in the following examples, you are not forced to use the table representations yielded into the block, but you
|
85
|
+
are encouraged to. They respond to the following methods:
|
86
|
+
|
87
|
+
- `tbl.*`: expands the splat to only the known fields defined in your model. Other fields in the database are excluded.
|
88
|
+
- `tbl.to_s`: represents the name of the table in the database. `#to_s` is invoked implcitly in String context. Note
|
89
|
+
that if you join to the same table multiple times, DataMapper SQL Finders will alias them accordingly.
|
90
|
+
- `tbl.property_name`: represents the field name in the database mapping to `property_name` in your model.
|
91
|
+
|
92
|
+
Writing the field/table names directly, while it will work, is not advised, since it will significantly hamper any future
|
93
|
+
efforts to chain onto the query (and it reads just like SQL, right?).
|
94
|
+
|
95
|
+
### Basic SELECT statements
|
96
|
+
|
97
|
+
Returning a String from the block executes the SQL when a kicker is invoked (e.g. iterating the Collection).
|
98
|
+
|
99
|
+
``` ruby
|
100
|
+
def self.basically_everything
|
101
|
+
by_sql { |m| "SELECT #{m.*} FROM #{m}" }
|
102
|
+
end
|
103
|
+
```
|
104
|
+
|
105
|
+
### Passing in variables
|
106
|
+
|
107
|
+
The block may return an Array, with the first element as the SQL and the following elements as the bind values.
|
108
|
+
|
109
|
+
``` ruby
|
110
|
+
def self.created_after(time)
|
111
|
+
by_sql { |m| ["SELECT #{m.*} FROM #{m} WHERE #{m.created_at} > ?", time] }
|
112
|
+
end
|
113
|
+
```
|
114
|
+
|
115
|
+
### Ordering
|
116
|
+
|
117
|
+
DataMapper always adds an ORDER BY to your queries if you don't specify one. DataMapper SQL Finders behaves no differently.
|
118
|
+
The default ordering is always ascending by primary key. You can override it in the SQL:
|
119
|
+
|
120
|
+
``` ruby
|
121
|
+
def self.backwards
|
122
|
+
by_sql { |m| "SELECT #{m.*} FROM #{m} ORDER BY #{m.id} DESC" }
|
123
|
+
end
|
124
|
+
```
|
125
|
+
|
126
|
+
Or you can provide it as a regular option to `#by_sql`, just like you can with `#all`:
|
127
|
+
|
128
|
+
``` ruby
|
129
|
+
def self.backwards
|
130
|
+
by_sql(:order => [:id.desc]) { |m| "SELECT #{m.*} FROM #{m}" }
|
131
|
+
end
|
132
|
+
```
|
133
|
+
|
134
|
+
Note that the `:order` option take precendence over anything specified in the SQL. This allows method chains to override it.
|
135
|
+
|
136
|
+
### Joins
|
137
|
+
|
138
|
+
The additional models are passed to `#by_sql`, then you use them to construct the join.
|
139
|
+
|
140
|
+
``` ruby
|
141
|
+
class User
|
142
|
+
... snip ...
|
143
|
+
|
144
|
+
def self.posted_today
|
145
|
+
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] }
|
146
|
+
end
|
147
|
+
end
|
148
|
+
```
|
149
|
+
|
150
|
+
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.
|
151
|
+
|
152
|
+
### Limits and offsets
|
153
|
+
|
154
|
+
These can be specified in the SQL:
|
155
|
+
|
156
|
+
``` ruby
|
157
|
+
def self.penultimate_five
|
158
|
+
by_sql { |m| "SELECT #{m.*} FROM #{m} ORDER BY #{m.id} DESC LIMIT 5 OFFSET 5" }
|
159
|
+
end
|
160
|
+
```
|
161
|
+
|
162
|
+
Or they can be provided as options to `#by_sql`:
|
163
|
+
|
164
|
+
``` ruby
|
165
|
+
def self.penultimate_five
|
166
|
+
by_sql(:limit => 5, :offset => 5) { |m| "SELECT #{m.*} FROM #{m}" }
|
167
|
+
end
|
168
|
+
```
|
169
|
+
|
170
|
+
If `:limit` and/or `:offset` are passed to `#by_sql`, they take precedence over anything written in the SQL itself.
|
171
|
+
|
172
|
+
### Method chaining
|
173
|
+
|
174
|
+
Method chaining with `#by_sql`, for the most part, works just like with `#all`. There are some current limitations,
|
175
|
+
such as reversing the order of a query that used `ORDER BY` in the SQL, rather than via an `:order` option.
|
176
|
+
|
177
|
+
Also note the you may not currently chain `#by_sql` calls together. `#by_sql` must, logically, always be the first
|
178
|
+
call in the chain.
|
179
|
+
|
180
|
+
``` ruby
|
181
|
+
User.by_sql{ |u| ["SELECT #{u.*} FROM #{u} WHERE #{u.role} = ?", "Manager"] }.all(:username.like => "%bob%", :order => [:username.desc])
|
182
|
+
```
|
183
|
+
|
184
|
+
### Unions, Intersections and Differences
|
185
|
+
|
186
|
+
Unfortunately this is not currently supported, and will likely only be added after the other limitations are worked out.
|
187
|
+
|
188
|
+
Specifically, queries like this:
|
189
|
+
|
190
|
+
``` ruby
|
191
|
+
User.by_sql { |u| ["SELECT #{u.*} FROM #{u} WHERE #{u.created_at} < ?", Date.today - 365] } | User.all(:admin => true)
|
192
|
+
```
|
193
|
+
|
194
|
+
Should really produce SQL of the nature:
|
195
|
+
|
196
|
+
``` sql
|
197
|
+
SELECT "users"."id", "users"."username", "users"."admin" FROM "users" WHERE ("created_at" < ?) OR (admin = TRUE)
|
198
|
+
```
|
199
|
+
|
200
|
+
I have no idea what will happen if it is attempted, but it almost certainly will not work ;)
|
201
|
+
|
202
|
+
## Will it interfere with DataMapper?
|
203
|
+
|
204
|
+
Almost all of the implementation is unintrusive, but unfortunately, because DataMapper's DataObjects Adapter does not provide
|
205
|
+
a great deal of flexibility when it comes to SQL generation, the entire `#select_statement` method has been overridden. For
|
206
|
+
non-`#by_sql` queries everything follows the original code pathways, and during a `#by_sql` query, the SQL is re-built using
|
207
|
+
a combination of the original logic and some custom logic to include your SQL. In short, yes, it does interfere, but I don't
|
208
|
+
believe there are any alternatives without extensive work on DataMapper's Query interface and the DataObjects adapter itself.
|
209
|
+
|
210
|
+
DataMapper 2.0 *should* fix this.
|
211
|
+
|
212
|
+
## Contributors
|
213
|
+
|
214
|
+
DataMapper SQL Finders is currently written by Chris Corbyn, but I'm extremely open to contributors which can make the
|
215
|
+
extension feel as natural and robust as possible. It should be developed such that other DataMapper gems (such as
|
216
|
+
dm-aggregates and dm-pager) still function without caring that raw SQL is being used in the queries.
|
217
|
+
|
218
|
+
## TODO
|
219
|
+
|
220
|
+
- Support overriding `:fields` in a `#by_sql` query (complex if the query depends on RDBMS native functions)
|
221
|
+
- 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
|
222
|
+
you use the `:order` option)
|
223
|
+
- Better support for `?` replacements in places other than the `WHERE` clause
|
224
|
+
- Support set operations (union, intersection, difference)
|
225
|
+
|
226
|
+
## Future Plans
|
227
|
+
|
228
|
+
Before I started writing this, I wanted to implement something similar to Doctrine's (PHP) DQL, probably called DMQL.
|
229
|
+
This is basically a strict superset of SQL that is pre-processed with DataMapper, having knowledge of your schema,
|
230
|
+
therefore allowing you to simplify the query and let DMQL hande things like JOIN logic. Say, for example:
|
231
|
+
|
232
|
+
``` ruby
|
233
|
+
Post.by_dmql("JOIN User u WHERE u.username = ?", "Bob")
|
234
|
+
```
|
235
|
+
|
236
|
+
Which would INNER JOIN posts with users and map u.username with the real field name of the `User#username` property.
|
237
|
+
|
238
|
+
This gem would be a pre-requisite for that.
|
239
|
+
|
240
|
+
Copyright (c) 2011 Chris Corbyn.
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,34 @@
|
|
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 = "dm-sql-finders"
|
7
|
+
s.version = DataMapper::SQLFinders::VERSION
|
8
|
+
s.authors = ["d11wtq"]
|
9
|
+
s.email = ["chris@w3style.co.uk"]
|
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
|
+
|
18
|
+
s.rubyforge_project = "dm-sql-finders"
|
19
|
+
|
20
|
+
s.files = `git ls-files`.split("\n")
|
21
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
22
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
23
|
+
s.require_paths = ["lib"]
|
24
|
+
|
25
|
+
DM_VERSION ||= ">= 1.2.0"
|
26
|
+
|
27
|
+
s.add_runtime_dependency "dm-core", DM_VERSION
|
28
|
+
s.add_runtime_dependency "dm-do-adapter", DM_VERSION
|
29
|
+
|
30
|
+
s.add_development_dependency "rspec", "~> 2.6"
|
31
|
+
s.add_development_dependency "dm-migrations", DM_VERSION
|
32
|
+
s.add_development_dependency "dm-aggregates", DM_VERSION
|
33
|
+
s.add_development_dependency "dm-sqlite-adapter", DM_VERSION
|
34
|
+
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,70 @@
|
|
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
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class Query
|
43
|
+
def normalize_order # temporary (will be removed in DM 1.3)
|
44
|
+
return if @order.nil?
|
45
|
+
|
46
|
+
@order = Array(@order)
|
47
|
+
@order = @order.map do |order|
|
48
|
+
case order
|
49
|
+
when Direction
|
50
|
+
order.dup
|
51
|
+
|
52
|
+
when Operator
|
53
|
+
target = order.target
|
54
|
+
property = target.kind_of?(Property) ? target : @properties[target]
|
55
|
+
|
56
|
+
Direction.new(property, order.operator)
|
57
|
+
|
58
|
+
when Symbol, String
|
59
|
+
Direction.new(@properties[order])
|
60
|
+
|
61
|
+
when Property
|
62
|
+
Direction.new(order)
|
63
|
+
|
64
|
+
when Path
|
65
|
+
Direction.new(order.property)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,84 @@
|
|
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
|
+
statement = [
|
24
|
+
columns_fragment,
|
25
|
+
from_fragment,
|
26
|
+
join_fragment,
|
27
|
+
where_fragment,
|
28
|
+
group_fragment,
|
29
|
+
order_fragment
|
30
|
+
].compact.join(" ")
|
31
|
+
|
32
|
+
@adapter.send(:add_limit_offset!, statement, @limit, @offset, @bind_values)
|
33
|
+
|
34
|
+
return statement, @bind_values
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def columns_fragment
|
40
|
+
if @parts[:select] && @fields.none? { |f| f.kind_of?(DataMapper::Query::Operator) }
|
41
|
+
@parts[:select].strip
|
42
|
+
else
|
43
|
+
"SELECT #{@adapter.send(:columns_statement, @fields, @qualify)}"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def from_fragment
|
48
|
+
if @parts[:from]
|
49
|
+
@parts[:from].strip
|
50
|
+
else
|
51
|
+
"FROM #{@adapter.send(:quote_name, @model.storage_name(@adapter.name))}"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def join_fragment
|
56
|
+
@adapter.send(:join_statement, @query, @bind_values, @qualify) if @query.links.any?
|
57
|
+
end
|
58
|
+
|
59
|
+
def where_fragment
|
60
|
+
if @parts[:where]
|
61
|
+
[@parts[:where].strip, @conditions_stmt].reject{ |c| DataMapper::Ext.blank?(c) }.join(" AND ")
|
62
|
+
else
|
63
|
+
"WHERE #{@conditions_stmt}" unless DataMapper::Ext.blank?(@conditions_stmt)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def group_fragment
|
68
|
+
if @parts[:group_by]
|
69
|
+
@parts[:group_by].strip
|
70
|
+
else
|
71
|
+
"GROUP BY #{@adapter.send(:columns_statement, @group_by, @qualify)}" if @group_by && @group_by.any?
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def order_fragment
|
76
|
+
if @parts[:order_by] && (@order_by.nil? || @order_by.all? { |o| o.kind_of?(Query::DefaultDirection) })
|
77
|
+
@parts[:order_by].strip
|
78
|
+
else
|
79
|
+
"ORDER BY #{@adapter.send(:order_statement, @order_by, @qualify)}" if @order_by && @order_by.any?
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
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,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,309 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe DataMapper::Adapters::DataObjectsAdapter do
|
4
|
+
context "querying by SQL" do
|
5
|
+
before(:each) do
|
6
|
+
@bob = User.create(:username => "Bob", :role => "Manager")
|
7
|
+
@fred = User.create(:username => "Fred", :role => "Tea Boy")
|
8
|
+
end
|
9
|
+
|
10
|
+
context "with a basic SELECT statement" do
|
11
|
+
before(:each) do
|
12
|
+
@users = User.by_sql { |u| ["SELECT #{u.*} FROM #{u} WHERE #{u.role} = ?", "Manager"] }
|
13
|
+
@sql, @bind_values = User.repository.adapter.send(:select_statement, @users.query)
|
14
|
+
end
|
15
|
+
|
16
|
+
it "executes the original query" do
|
17
|
+
@sql.should == %q{SELECT "users"."id", "users"."username", "users"."role" FROM "users" WHERE "users"."role" = ? ORDER BY "users"."id"}
|
18
|
+
@bind_values.should == ["Manager"]
|
19
|
+
end
|
20
|
+
|
21
|
+
it "finds the matching resources" do
|
22
|
+
@users.should include(@bob)
|
23
|
+
end
|
24
|
+
|
25
|
+
it "does not find incorrect resources" do
|
26
|
+
@users.should_not include(@fred)
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "chaining" do
|
30
|
+
describe "to #all" do
|
31
|
+
before(:each) do
|
32
|
+
@jim = User.create(:username => "Jim", :role => "Manager")
|
33
|
+
@users = @users.all(:username => "Jim")
|
34
|
+
@sql, @bind_values = User.repository.adapter.send(:select_statement, @users.query)
|
35
|
+
end
|
36
|
+
|
37
|
+
it "merges the conditions with the original SQL" do
|
38
|
+
@sql.should == %q{SELECT "users"."id", "users"."username", "users"."role" FROM "users" WHERE "users"."role" = ? AND "users"."username" = ? ORDER BY "users"."id"}
|
39
|
+
@bind_values.should == ["Manager", "Jim"]
|
40
|
+
end
|
41
|
+
|
42
|
+
it "finds the matching resources" do
|
43
|
+
@users.should include(@jim)
|
44
|
+
end
|
45
|
+
|
46
|
+
it "does not find incorrect resources" do
|
47
|
+
@users.should_not include(@bob)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
describe "ordering" do
|
54
|
+
context "with an ORDER BY clause" do
|
55
|
+
before(:each) do
|
56
|
+
@users = User.by_sql { |u| "SELECT #{u.*} FROM #{u} ORDER BY #{u.username} DESC" }
|
57
|
+
@sql, @bind_values = User.repository.adapter.send(:select_statement, @users.query)
|
58
|
+
end
|
59
|
+
|
60
|
+
it "uses the order from the SQL" do
|
61
|
+
@sql.should == %q{SELECT "users"."id", "users"."username", "users"."role" FROM "users" ORDER BY "users"."username" DESC}
|
62
|
+
end
|
63
|
+
|
64
|
+
it "loads the resources in the correct order" do
|
65
|
+
@users.to_a.first.should == @fred
|
66
|
+
@users.to_a.last.should == @bob
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
context "with :order specified on the query" do
|
71
|
+
before(:each) do
|
72
|
+
@users = User.by_sql(:order => [:role.asc]) { |u| "SELECT #{u.*} FROM #{u}" }
|
73
|
+
@sql, @bind_values = User.repository.adapter.send(:select_statement, @users.query)
|
74
|
+
end
|
75
|
+
|
76
|
+
it "uses the order from the options" do
|
77
|
+
@sql.should == %q{SELECT "users"."id", "users"."username", "users"."role" FROM "users" ORDER BY "users"."role"}
|
78
|
+
end
|
79
|
+
|
80
|
+
it "loads the resources in the correct order" do
|
81
|
+
@users.to_a.first.should == @bob
|
82
|
+
@users.to_a.last.should == @fred
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
context "with both :order and an ORDER BY clause" do
|
87
|
+
before(:each) do
|
88
|
+
@users = User.by_sql(:order => [:role.desc]) { |u| "SELECT #{u.*} FROM #{u} ORDER BY #{u.username} ASC" }
|
89
|
+
@sql, @bind_values = User.repository.adapter.send(:select_statement, @users.query)
|
90
|
+
end
|
91
|
+
|
92
|
+
it "gives the :order option precendence" do
|
93
|
+
@sql.should == %q{SELECT "users"."id", "users"."username", "users"."role" FROM "users" ORDER BY "users"."role" DESC}
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
describe "chaining" do
|
98
|
+
describe "overriding a previous :order option" do
|
99
|
+
before(:each) do
|
100
|
+
@users = User.by_sql(:order => [:role.desc]) { |u| "SELECT #{u.*} FROM #{u}" }.all(:order => [:id.asc])
|
101
|
+
@sql, @bind_values = User.repository.adapter.send(:select_statement, @users.query)
|
102
|
+
end
|
103
|
+
|
104
|
+
specify "the last :order specified is used" do
|
105
|
+
@sql.should == %q{SELECT "users"."id", "users"."username", "users"."role" FROM "users" ORDER BY "users"."id"}
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
describe "overriding the order specified in the SQL" do
|
110
|
+
before(:each) do
|
111
|
+
@users = User.by_sql { |u| "SELECT #{u.*} FROM #{u} ORDER BY #{u.role} DESC" }.all(:order => [:id.asc])
|
112
|
+
@sql, @bind_values = User.repository.adapter.send(:select_statement, @users.query)
|
113
|
+
end
|
114
|
+
|
115
|
+
specify "the last :order specified is used" do
|
116
|
+
@sql.should == %q{SELECT "users"."id", "users"."username", "users"."role" FROM "users" ORDER BY "users"."id"}
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
describe "limits" do
|
123
|
+
context "with a limit specified by the SQL" do
|
124
|
+
before(:each) do
|
125
|
+
@users = User.by_sql { |u| "SELECT #{u.*} FROM #{u} LIMIT 1" }
|
126
|
+
@sql, @bind_values = User.repository.adapter.send(:select_statement, @users.query)
|
127
|
+
end
|
128
|
+
|
129
|
+
it "uses the limit from the SQL" do
|
130
|
+
@sql.should == %q{SELECT "users"."id", "users"."username", "users"."role" FROM "users" ORDER BY "users"."id" LIMIT ?}
|
131
|
+
@bind_values.should == [1]
|
132
|
+
end
|
133
|
+
|
134
|
+
it "finds the matching resources" do
|
135
|
+
@users.to_a.should have(1).items
|
136
|
+
@users.to_a.first.should == @bob
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
context "with a :limit option to #by_sql" do
|
141
|
+
before(:each) do
|
142
|
+
@users = User.by_sql(:limit => 1) { |u| "SELECT #{u.*} FROM #{u}" }
|
143
|
+
@sql, @bind_values = User.repository.adapter.send(:select_statement, @users.query)
|
144
|
+
end
|
145
|
+
|
146
|
+
it "uses the :limit option" do
|
147
|
+
@sql.should == %q{SELECT "users"."id", "users"."username", "users"."role" FROM "users" ORDER BY "users"."id" LIMIT ?}
|
148
|
+
@bind_values.should == [1]
|
149
|
+
end
|
150
|
+
|
151
|
+
it "finds the matching resources" do
|
152
|
+
@users.to_a.should have(1).items
|
153
|
+
@users.to_a.first.should == @bob
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
context "with both a :limit option and a LIMIT in the SQL" do
|
158
|
+
before(:each) do
|
159
|
+
@users = User.by_sql(:limit => 1) { |u| "SELECT #{u.*} FROM #{u} LIMIT 2" }
|
160
|
+
@sql, @bind_values = User.repository.adapter.send(:select_statement, @users.query)
|
161
|
+
end
|
162
|
+
|
163
|
+
it "the :limit option takes precedence" do
|
164
|
+
@sql.should == %q{SELECT "users"."id", "users"."username", "users"."role" FROM "users" ORDER BY "users"."id" LIMIT ?}
|
165
|
+
@bind_values.should == [1]
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
context "with an OFFSET in the SQL" do
|
170
|
+
before(:each) do
|
171
|
+
@users = User.by_sql { |u| "SELECT #{u.*} FROM #{u} LIMIT 1 OFFSET 1" }
|
172
|
+
@sql, @bind_values = User.repository.adapter.send(:select_statement, @users.query)
|
173
|
+
end
|
174
|
+
|
175
|
+
it "uses the offset from the SQL" do
|
176
|
+
@sql.should == %q{SELECT "users"."id", "users"."username", "users"."role" FROM "users" ORDER BY "users"."id" LIMIT ? OFFSET ?}
|
177
|
+
@bind_values.should == [1, 1]
|
178
|
+
end
|
179
|
+
|
180
|
+
it "finds the matching resources" do
|
181
|
+
@users.to_a.should have(1).items
|
182
|
+
@users.to_a.first.should == @fred
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
context "with an argument to LIMIT in the SQL" do
|
187
|
+
before(:each) do
|
188
|
+
@users = User.by_sql { |u| "SELECT #{u.*} FROM #{u} LIMIT 1, 2" }
|
189
|
+
@sql, @bind_values = User.repository.adapter.send(:select_statement, @users.query)
|
190
|
+
end
|
191
|
+
|
192
|
+
it "interprets the offset in the SQL" do
|
193
|
+
@sql.should == %q{SELECT "users"."id", "users"."username", "users"."role" FROM "users" ORDER BY "users"."id" LIMIT ? OFFSET ?}
|
194
|
+
@bind_values.should == [2, 1]
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
context "with an :offset option to #by_sql" do
|
199
|
+
before(:each) do
|
200
|
+
@users = User.by_sql(:offset => 1) { |u| "SELECT #{u.*} FROM #{u} LIMIT 1" }
|
201
|
+
@sql, @bind_values = User.repository.adapter.send(:select_statement, @users.query)
|
202
|
+
end
|
203
|
+
|
204
|
+
it "uses the offset from the options hash" do
|
205
|
+
@sql.should == %q{SELECT "users"."id", "users"."username", "users"."role" FROM "users" ORDER BY "users"."id" LIMIT ? OFFSET ?}
|
206
|
+
@bind_values.should == [1, 1]
|
207
|
+
end
|
208
|
+
|
209
|
+
it "finds the matching resources" do
|
210
|
+
@users.to_a.should have(1).items
|
211
|
+
@users.to_a.first.should == @fred
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
context "with both an OFFSET in the SQL and an :offset option" do
|
216
|
+
before(:each) do
|
217
|
+
@users = User.by_sql(:offset => 1) { |u| "SELECT #{u.*} FROM #{u} LIMIT 1 OFFSET 0" }
|
218
|
+
@sql, @bind_values = User.repository.adapter.send(:select_statement, @users.query)
|
219
|
+
end
|
220
|
+
|
221
|
+
it "the :offset in the options takes precendence" do
|
222
|
+
@sql.should == %q{SELECT "users"."id", "users"."username", "users"."role" FROM "users" ORDER BY "users"."id" LIMIT ? OFFSET ?}
|
223
|
+
@bind_values.should == [1, 1]
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
describe "chaining" do
|
228
|
+
describe "to override a previous :limit option" do
|
229
|
+
before(:each) do
|
230
|
+
@users = User.by_sql(:limit => 2) { |u| "SELECT #{u.*} FROM #{u}" }.all(:limit => 1)
|
231
|
+
@sql, @bind_values = User.repository.adapter.send(:select_statement, @users.query)
|
232
|
+
end
|
233
|
+
|
234
|
+
it "the last :limit option takes precedence" do
|
235
|
+
@sql.should == %q{SELECT "users"."id", "users"."username", "users"."role" FROM "users" ORDER BY "users"."id" LIMIT ?}
|
236
|
+
@bind_values.should == [1]
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
describe "to override a limit applied in SQL" do
|
241
|
+
before(:each) do
|
242
|
+
@users = User.by_sql { |u| "SELECT #{u.*} FROM #{u} LIMIT 1" }.all(:limit => 2)
|
243
|
+
@sql, @bind_values = User.repository.adapter.send(:select_statement, @users.query)
|
244
|
+
end
|
245
|
+
|
246
|
+
it "the last :limit option takes precedence" do
|
247
|
+
@sql.should == %q{SELECT "users"."id", "users"."username", "users"."role" FROM "users" ORDER BY "users"."id" LIMIT ?}
|
248
|
+
@bind_values.should == [2]
|
249
|
+
end
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
context "with an INNER JOIN" do
|
255
|
+
before(:each) do
|
256
|
+
@bobs_post = @bob.posts.create(:title => "Bob can write posts")
|
257
|
+
@freds_post = @fred.posts.create(:title => "Fred likes to write too")
|
258
|
+
|
259
|
+
@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] }
|
260
|
+
@sql, @bind_values = Post.repository.adapter.send(:select_statement, @posts.query)
|
261
|
+
end
|
262
|
+
|
263
|
+
it "executes the original query" do
|
264
|
+
@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"}
|
265
|
+
@bind_values.should == [@bob.id]
|
266
|
+
end
|
267
|
+
|
268
|
+
it "finds the matching resources" do
|
269
|
+
@posts.should include(@bobs_post)
|
270
|
+
end
|
271
|
+
|
272
|
+
it "does not find incorrect resources" do
|
273
|
+
@posts.should_not include(@freds_post)
|
274
|
+
end
|
275
|
+
|
276
|
+
context "to the same table" do
|
277
|
+
before(:each) do
|
278
|
+
@posts = Post.by_sql(Post) { |p1, p2| "SELECT #{p1.*} FROM #{p1} INNER JOIN #{p2} ON #{p1.id} = #{p2.id}" }
|
279
|
+
@sql, @bind_values = Post.repository.adapter.send(:select_statement, @posts.query)
|
280
|
+
end
|
281
|
+
|
282
|
+
it "creates alises for the subsequent tables" do
|
283
|
+
@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"}
|
284
|
+
end
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
describe "aggregate queries" do
|
289
|
+
it "supports calculating counts" do
|
290
|
+
User.by_sql { |u| "SELECT #{u.*} FROM #{u}" }.count(:id).should == 2
|
291
|
+
end
|
292
|
+
|
293
|
+
it "supports multiple aggregates" do
|
294
|
+
User.by_sql { |u| "SELECT #{u.*} FROM #{u}" }.aggregate(:id.count, :id.min, :username.max).should == [2, 1, "Fred"]
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
describe "with virtual attributes" do
|
299
|
+
before(:each) do
|
300
|
+
@bob.posts.create(:title => "Test")
|
301
|
+
@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}" }
|
302
|
+
end
|
303
|
+
|
304
|
+
it "loads the virtual attributes" do
|
305
|
+
@users.to_a.first.post_count.should == 1
|
306
|
+
end
|
307
|
+
end
|
308
|
+
end
|
309
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -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
|
metadata
ADDED
@@ -0,0 +1,138 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: dm-sql-finders
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- d11wtq
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-10-17 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: dm-core
|
16
|
+
requirement: &6758340 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 1.2.0
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *6758340
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: dm-do-adapter
|
27
|
+
requirement: &6757000 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 1.2.0
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *6757000
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: rspec
|
38
|
+
requirement: &6755440 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ~>
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '2.6'
|
44
|
+
type: :development
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *6755440
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: dm-migrations
|
49
|
+
requirement: &6754500 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 1.2.0
|
55
|
+
type: :development
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *6754500
|
58
|
+
- !ruby/object:Gem::Dependency
|
59
|
+
name: dm-aggregates
|
60
|
+
requirement: &6753640 !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ! '>='
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: 1.2.0
|
66
|
+
type: :development
|
67
|
+
prerelease: false
|
68
|
+
version_requirements: *6753640
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: dm-sqlite-adapter
|
71
|
+
requirement: &6752280 !ruby/object:Gem::Requirement
|
72
|
+
none: false
|
73
|
+
requirements:
|
74
|
+
- - ! '>='
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: 1.2.0
|
77
|
+
type: :development
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: *6752280
|
80
|
+
description: ! "dm-sql-finders add #by_sql to your DataMapper models and provides
|
81
|
+
a clean mechanism for using\n the names of the properties in
|
82
|
+
your model, instead of the actual fields in the database. Any SQL\n is
|
83
|
+
supported and actual DataMapper Query objects wrap the SQL, thus delaying its execution
|
84
|
+
until\n a kicker method materializes the records for the query.
|
85
|
+
\ You can also chain standard DataMapper\n query methods onto
|
86
|
+
the #by_sql call to refine the query."
|
87
|
+
email:
|
88
|
+
- chris@w3style.co.uk
|
89
|
+
executables: []
|
90
|
+
extensions: []
|
91
|
+
extra_rdoc_files: []
|
92
|
+
files:
|
93
|
+
- .gitignore
|
94
|
+
- .rspec
|
95
|
+
- Gemfile
|
96
|
+
- LICENSE
|
97
|
+
- README.md
|
98
|
+
- Rakefile
|
99
|
+
- dm-sql-finders.gemspec
|
100
|
+
- lib/data_mapper/sql_finders.rb
|
101
|
+
- lib/data_mapper/sql_finders/adapter.rb
|
102
|
+
- lib/data_mapper/sql_finders/query.rb
|
103
|
+
- lib/data_mapper/sql_finders/sql_builder.rb
|
104
|
+
- lib/data_mapper/sql_finders/sql_parser.rb
|
105
|
+
- lib/data_mapper/sql_finders/table_representation.rb
|
106
|
+
- lib/data_mapper/sql_finders/version.rb
|
107
|
+
- lib/dm-sql-finders.rb
|
108
|
+
- spec/public/adapter_spec.rb
|
109
|
+
- spec/spec_helper.rb
|
110
|
+
- spec/support/fixtures/post.rb
|
111
|
+
- spec/support/fixtures/user.rb
|
112
|
+
homepage: https://github.com/d11wtq/dm-sql-finders
|
113
|
+
licenses: []
|
114
|
+
post_install_message:
|
115
|
+
rdoc_options: []
|
116
|
+
require_paths:
|
117
|
+
- lib
|
118
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
119
|
+
none: false
|
120
|
+
requirements:
|
121
|
+
- - ! '>='
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: '0'
|
124
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
125
|
+
none: false
|
126
|
+
requirements:
|
127
|
+
- - ! '>='
|
128
|
+
- !ruby/object:Gem::Version
|
129
|
+
version: '0'
|
130
|
+
requirements: []
|
131
|
+
rubyforge_project: dm-sql-finders
|
132
|
+
rubygems_version: 1.8.10
|
133
|
+
signing_key:
|
134
|
+
specification_version: 3
|
135
|
+
summary: Query DataMapper models using raw SQL, without sacrificing property and table
|
136
|
+
name abstraction
|
137
|
+
test_files: []
|
138
|
+
has_rdoc:
|