babik 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +16 -0
- data/README.md +718 -0
- data/Rakefile +18 -0
- data/lib/babik.rb +122 -0
- data/lib/babik/database.rb +16 -0
- data/lib/babik/queryset.rb +154 -0
- data/lib/babik/queryset/components/aggregation.rb +172 -0
- data/lib/babik/queryset/components/limit.rb +22 -0
- data/lib/babik/queryset/components/order.rb +161 -0
- data/lib/babik/queryset/components/projection.rb +118 -0
- data/lib/babik/queryset/components/select_related.rb +78 -0
- data/lib/babik/queryset/components/sql_renderer.rb +99 -0
- data/lib/babik/queryset/components/where.rb +43 -0
- data/lib/babik/queryset/lib/association/foreign_association_chain.rb +97 -0
- data/lib/babik/queryset/lib/association/select_related_association_chain.rb +32 -0
- data/lib/babik/queryset/lib/condition.rb +103 -0
- data/lib/babik/queryset/lib/field.rb +34 -0
- data/lib/babik/queryset/lib/join/association_joiner.rb +39 -0
- data/lib/babik/queryset/lib/join/join.rb +86 -0
- data/lib/babik/queryset/lib/selection/config.rb +19 -0
- data/lib/babik/queryset/lib/selection/foreign_selection.rb +39 -0
- data/lib/babik/queryset/lib/selection/local_selection.rb +40 -0
- data/lib/babik/queryset/lib/selection/operation/base.rb +126 -0
- data/lib/babik/queryset/lib/selection/operation/date.rb +178 -0
- data/lib/babik/queryset/lib/selection/operation/operations.rb +201 -0
- data/lib/babik/queryset/lib/selection/operation/regex.rb +58 -0
- data/lib/babik/queryset/lib/selection/path/foreign_path.rb +50 -0
- data/lib/babik/queryset/lib/selection/path/local_path.rb +44 -0
- data/lib/babik/queryset/lib/selection/path/path.rb +23 -0
- data/lib/babik/queryset/lib/selection/select_related_selection.rb +38 -0
- data/lib/babik/queryset/lib/selection/selection.rb +19 -0
- data/lib/babik/queryset/lib/update/assignment.rb +108 -0
- data/lib/babik/queryset/mixins/aggregatable.rb +17 -0
- data/lib/babik/queryset/mixins/bounded.rb +38 -0
- data/lib/babik/queryset/mixins/clonable.rb +52 -0
- data/lib/babik/queryset/mixins/countable.rb +44 -0
- data/lib/babik/queryset/mixins/deletable.rb +13 -0
- data/lib/babik/queryset/mixins/distinguishable.rb +27 -0
- data/lib/babik/queryset/mixins/filterable.rb +51 -0
- data/lib/babik/queryset/mixins/limitable.rb +88 -0
- data/lib/babik/queryset/mixins/lockable.rb +31 -0
- data/lib/babik/queryset/mixins/none.rb +16 -0
- data/lib/babik/queryset/mixins/projectable.rb +34 -0
- data/lib/babik/queryset/mixins/related_selector.rb +28 -0
- data/lib/babik/queryset/mixins/set_operations.rb +32 -0
- data/lib/babik/queryset/mixins/sortable.rb +49 -0
- data/lib/babik/queryset/mixins/sql_renderizable.rb +17 -0
- data/lib/babik/queryset/mixins/updatable.rb +14 -0
- data/lib/babik/queryset/templates/default/delete/main.sql.erb +14 -0
- data/lib/babik/queryset/templates/default/select/components/aggregation.sql.erb +5 -0
- data/lib/babik/queryset/templates/default/select/components/from.sql.erb +16 -0
- data/lib/babik/queryset/templates/default/select/components/from_set.sql.erb +3 -0
- data/lib/babik/queryset/templates/default/select/components/from_table.sql.erb +2 -0
- data/lib/babik/queryset/templates/default/select/components/limit.sql.erb +10 -0
- data/lib/babik/queryset/templates/default/select/components/order_by.sql.erb +9 -0
- data/lib/babik/queryset/templates/default/select/components/projection.sql.erb +7 -0
- data/lib/babik/queryset/templates/default/select/components/select_related.sql.erb +26 -0
- data/lib/babik/queryset/templates/default/select/components/where.sql.erb +39 -0
- data/lib/babik/queryset/templates/default/select/main.sql.erb +42 -0
- data/lib/babik/queryset/templates/default/update/main.sql.erb +15 -0
- data/lib/babik/queryset/templates/mssql/select/components/limit.sql.erb +8 -0
- data/lib/babik/queryset/templates/mssql/select/components/order_by.sql.erb +21 -0
- data/lib/babik/queryset/templates/mysql2/delete/main.sql.erb +15 -0
- data/lib/babik/queryset/templates/mysql2/update/main.sql.erb +18 -0
- data/lib/babik/queryset/templates/sqlite3/select/components/from_set.sql.erb +5 -0
- data/test/config/db/schema.rb +83 -0
- data/test/config/models/bad_post.rb +5 -0
- data/test/config/models/bad_tag.rb +5 -0
- data/test/config/models/category.rb +4 -0
- data/test/config/models/geozone.rb +6 -0
- data/test/config/models/group.rb +5 -0
- data/test/config/models/group_user.rb +5 -0
- data/test/config/models/post.rb +24 -0
- data/test/config/models/post_tag.rb +5 -0
- data/test/config/models/tag.rb +5 -0
- data/test/config/models/user.rb +6 -0
- data/test/delete/delete_test.rb +60 -0
- data/test/delete/foreign_conditions_delete_test.rb +57 -0
- data/test/delete/local_conditions_delete_test.rb +20 -0
- data/test/enable_coverage.rb +17 -0
- data/test/lib/selection/operation/log/test-queries.log +1 -0
- data/test/lib/selection/operation/test_date.rb +131 -0
- data/test/lib/selection/operation/test_regex.rb +55 -0
- data/test/other/clone_test.rb +129 -0
- data/test/other/escape_test.rb +21 -0
- data/test/other/inverse_of_required_test.rb +33 -0
- data/test/select/aggregate_test.rb +151 -0
- data/test/select/bounds_test.rb +46 -0
- data/test/select/count_test.rb +147 -0
- data/test/select/distinct_test.rb +38 -0
- data/test/select/exclude_test.rb +72 -0
- data/test/select/filter_from_object_test.rb +125 -0
- data/test/select/filter_test.rb +207 -0
- data/test/select/for_update_test.rb +19 -0
- data/test/select/foreign_selection_test.rb +60 -0
- data/test/select/get_test.rb +40 -0
- data/test/select/limit_test.rb +109 -0
- data/test/select/local_selection_test.rb +24 -0
- data/test/select/lookup_test.rb +208 -0
- data/test/select/none_test.rb +40 -0
- data/test/select/order_test.rb +165 -0
- data/test/select/project_test.rb +107 -0
- data/test/select/select_related_test.rb +124 -0
- data/test/select/subquery_test.rb +50 -0
- data/test/set_operations/basic_usage_test.rb +121 -0
- data/test/test_helper.rb +55 -0
- data/test/update/update_test.rb +93 -0
- metadata +278 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 646ba03c537cfc500c7ac40ef4e366ee294d1eacf4edeae1519e348b29ae8b79
|
4
|
+
data.tar.gz: 0eae3285ec2d703c0c8ee8de80bb88cf043b054e36ad743ecd1aeee73b66a823
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ae7e7799bae16ea2b9039f3af265a859ae917dee90af93162bc97b6fe1b0e5d834bb256ce3f910538f1544289e24c4980428528b73a31ba959257a245ff87b64
|
7
|
+
data.tar.gz: dc0f8a9a867b9f53bd9af83a720b5c969eeb6a8a6a5f0d998c2dcb9e0fca6ed0f63788b2182877f1524f161c9518fb3f41c8cf313ff5a0dc7aa7c361791e55af
|
data/Gemfile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
gem 'activerecord'
|
4
|
+
gem 'rails'
|
5
|
+
gem 'ruby_deep_clone'
|
6
|
+
|
7
|
+
group :development, :test do
|
8
|
+
gem 'mysql2'
|
9
|
+
gem 'overcommit'
|
10
|
+
gem 'pg'
|
11
|
+
gem 'reek'
|
12
|
+
gem 'rubocop'
|
13
|
+
gem 'simplecov'
|
14
|
+
gem 'simplecov-console'
|
15
|
+
gem 'sqlite3'
|
16
|
+
end
|
data/README.md
ADDED
@@ -0,0 +1,718 @@
|
|
1
|
+
# Babik
|
2
|
+
|
3
|
+
[![Build Status](https://travis-ci.com/diegojromerolopez/babik.svg?branch=master)](https://travis-ci.com/diegojromerolopez/babik)
|
4
|
+
[![Maintainability](https://api.codeclimate.com/v1/badges/8a64e9a43c77d31a0df1/maintainability)](https://codeclimate.com/github/diegojromerolopez/babik/maintainability)
|
5
|
+
[![Test Coverage](https://api.codeclimate.com/v1/badges/8a64e9a43c77d31a0df1/test_coverage)](https://codeclimate.com/github/diegojromerolopez/babik/test_coverage)
|
6
|
+
|
7
|
+
A Django [queryset-like](https://docs.djangoproject.com/en/2.0/ref/models/querysets/) API for [Ruby on Rails](https://rubyonrails.org/).
|
8
|
+
|
9
|
+
**This project is in beta phase. Use it with caution.**
|
10
|
+
|
11
|
+
See [Roadmap](#roadmap) to check what is keeping it from being stable.
|
12
|
+
|
13
|
+
See the [QuerySet API](/doc/api/queryset.md) if you know this library and want to
|
14
|
+
see the documentation.
|
15
|
+
|
16
|
+
Contact [me](mailto:diegojromerolopez@gmail.com) if you are interested in
|
17
|
+
helping me developing it or make a PR with some feature or fix.
|
18
|
+
|
19
|
+
## What's this?
|
20
|
+
|
21
|
+
This is a library to help you to make queries based on associations without having
|
22
|
+
to worry about doing joins or writing the exact name of the related table as a prefix
|
23
|
+
of the foreign field conditions.
|
24
|
+
|
25
|
+
### Example: Blog platform in Rails
|
26
|
+
|
27
|
+
Suppose you are developing a blog platform with the following [schema](/test/config/db/schema.rb).
|
28
|
+
Compare these two queries and check what is more easier to write:
|
29
|
+
|
30
|
+
Returning all users with last name equals to 'Fabia' that are from Rome:
|
31
|
+
```ruby
|
32
|
+
User.joins(:zones).where('last_name': 'Fabia').where('geo_zones.name': 'Rome')
|
33
|
+
# vs.
|
34
|
+
User.objects.filter(last_name: 'Fabia', 'zone::name': 'Rome')
|
35
|
+
```
|
36
|
+
|
37
|
+
Returning all users with posts tagged with 'gallic' that are from Rome:
|
38
|
+
```ruby
|
39
|
+
User.joins(:zones).joins(posts: :tags)
|
40
|
+
.where('last_name': 'Fabia')
|
41
|
+
.where('geo_zones.name': 'Rome')
|
42
|
+
.where('tags.name': 'gallic')
|
43
|
+
# vs.
|
44
|
+
User.objects.filter(
|
45
|
+
last_name: 'Fabia',
|
46
|
+
'zone::name': 'Rome',
|
47
|
+
'posts::tags::name': 'gallic'
|
48
|
+
)
|
49
|
+
```
|
50
|
+
|
51
|
+
The second alternative is done by using the powerful [Babik querysets](/doc/api/queryset.md).
|
52
|
+
|
53
|
+
[See Usage for more examples](#usage).
|
54
|
+
|
55
|
+
## Install
|
56
|
+
|
57
|
+
Add to Gemfile:
|
58
|
+
|
59
|
+
```
|
60
|
+
gem install babik, git: 'git://github.com/diegojromerolopez/babik.git'
|
61
|
+
```
|
62
|
+
|
63
|
+
No rubygem version for the moment.
|
64
|
+
|
65
|
+
## Requirements
|
66
|
+
|
67
|
+
Ruby Version >= 2.5
|
68
|
+
|
69
|
+
Include all [inverse relationships](http://guides.rubyonrails.org/association_basics.html#bi-directional-associations)
|
70
|
+
in your models. **It is required to compute the object selection from instance**.
|
71
|
+
|
72
|
+
All your many-to-many relationships must have a through attribute.
|
73
|
+
Per Rubocop guidelines, [using has_and_belongs_to_many is discouraged](https://github.com/rubocop-hq/rails-style-guide#has-many-through).
|
74
|
+
|
75
|
+
## Configuration
|
76
|
+
|
77
|
+
No configuration is needed, Babik automatically includes two methods for your models:
|
78
|
+
- **objects** class method to make queries for a model.
|
79
|
+
- **objects** instance method to make queries from an instance.
|
80
|
+
|
81
|
+
## Database support
|
82
|
+
|
83
|
+
PostgreSQL, MySQL and Sqlite are fully supported.
|
84
|
+
|
85
|
+
MariaDB and MSSQL should work as well (happy to solve any reported issues).
|
86
|
+
|
87
|
+
Accepting contributors to port this library to Oracle.
|
88
|
+
|
89
|
+
## Documentation
|
90
|
+
|
91
|
+
See the [QuerySet API documentation](/doc/api/queryset.md).
|
92
|
+
|
93
|
+
## Main differences with Django QuerySet system
|
94
|
+
- Django does not make any distinct against relationships, local fields or lookups when selecting by
|
95
|
+
calling **filter**, **exclude** or **get**. Babik uses **::** for foreign fields.
|
96
|
+
- Django has a [Q objects](https://docs.djangoproject.com/en/2.0/topics/db/queries/#complex-lookups-with-q-objects)
|
97
|
+
that allows the construction of complex queries. Babik allows passing an array to selection methods so
|
98
|
+
there is no need of this artifact.
|
99
|
+
- Django [select_related](https://docs.djangoproject.com/en/2.0/ref/models/querysets/#select-related)
|
100
|
+
method cache the objects in the returned object.
|
101
|
+
We return a pair of objects and a hash with the associated objects. [See doc here](/doc/api/queryset/methods/dont_return_queryset.md#select-related).
|
102
|
+
|
103
|
+
## Known issues
|
104
|
+
|
105
|
+
### Clone in each non-modifying method call
|
106
|
+
|
107
|
+
This library uses [ruby_deep_clone](https://github.com/gmodarelli/ruby-deepclone/) to create a new QuerySet each time
|
108
|
+
a non-modifying method is called:
|
109
|
+
|
110
|
+
```ruby
|
111
|
+
julius = User.objects.filter(first_name: 'Julius')
|
112
|
+
julius_caesar = julius.filter(last_name: 'Caesar')
|
113
|
+
|
114
|
+
puts julius_caesar == julius
|
115
|
+
# Will print false
|
116
|
+
```
|
117
|
+
|
118
|
+
This library is somewhat unstable or not as stable as I would like.
|
119
|
+
|
120
|
+
## Usage
|
121
|
+
|
122
|
+
For a complete reference and full examples of methods, see [documentation](/doc/README.md).
|
123
|
+
|
124
|
+
See [schema](/test/config/db/schema.rb) for information about this example's schema.
|
125
|
+
|
126
|
+
### objects method
|
127
|
+
|
128
|
+
A new **objects** method will be injected in your ActiveRecord classes and instances.
|
129
|
+
|
130
|
+
#### Classes
|
131
|
+
|
132
|
+
When called from a class, it will return a QuerySet of objects of this class.
|
133
|
+
|
134
|
+
```ruby
|
135
|
+
User.objects.filter(last_name: 'Fabia')
|
136
|
+
# Returning all users with last name equals to 'Fabia'
|
137
|
+
|
138
|
+
User.objects.filter(last_name: 'Fabia', 'zone::name': 'Rome')
|
139
|
+
# Returning all users with last name equals to 'Fabia' that are from Rome
|
140
|
+
```
|
141
|
+
|
142
|
+
#### Instances
|
143
|
+
|
144
|
+
When called from an instance, it will return the foreign related instances:
|
145
|
+
|
146
|
+
```ruby
|
147
|
+
julius = User.objects.get(first_name: 'Julius')
|
148
|
+
julius.objects('posts').filter(stars__gte: 3)
|
149
|
+
# Will return the posts written by Julius with 3 or more stars
|
150
|
+
|
151
|
+
julius.objects('posts::tags').filter(name__in: ['war', 'battle', 'victory'])
|
152
|
+
# Will return the tags of posts written by Julius with the names 'war', 'battle' and 'victory'
|
153
|
+
```
|
154
|
+
|
155
|
+
|
156
|
+
### Examples
|
157
|
+
|
158
|
+
#### Selection
|
159
|
+
|
160
|
+
[See the main docs](/doc/api/queryset/methods/return_queryset.md#filter).
|
161
|
+
|
162
|
+
Basic selection is made by passing a hash to filter function:
|
163
|
+
|
164
|
+
```ruby
|
165
|
+
User.objects.filter(first_name: 'Flavius', last_name: 'Josephus')
|
166
|
+
# SELECT users.* FROM users WHERE first_name = 'Flavius' AND last_name = 'Josephus'
|
167
|
+
```
|
168
|
+
|
169
|
+
To make an OR condition, pass an array of hashes:
|
170
|
+
|
171
|
+
```ruby
|
172
|
+
User.objects.filter([{first_name: 'Flavius', last_name: 'Josephus'}, {last_name: 'Iosephus'}])
|
173
|
+
# SELECT users.*
|
174
|
+
# FROM users
|
175
|
+
# WHERE (first_name = 'Flavius' AND last_name = 'Josephus') OR last_name = 'Iosephus'
|
176
|
+
```
|
177
|
+
|
178
|
+
#### Selection by exclusion
|
179
|
+
|
180
|
+
You can make negative conditions easily by using **exclude** function:
|
181
|
+
|
182
|
+
```ruby
|
183
|
+
User.objects.exclude(first_name: 'Flavius', last_name: 'Josephus')
|
184
|
+
# SELECT users.* FROM users WHERE NOT(first_name = 'Flavius' AND last_name = 'Josephus')
|
185
|
+
```
|
186
|
+
|
187
|
+
You can combine **filter** and **exclude** to create complex queries:
|
188
|
+
|
189
|
+
```ruby
|
190
|
+
User.objects.filter([{first_name: 'Marcus'}, {first_name: 'Julius'}]).exclude(last_name: 'Servilia')
|
191
|
+
# SELECT users.*
|
192
|
+
# FROM users
|
193
|
+
# WHERE (first_name = 'Marcus' OR first_name = 'Julius') AND NOT(last_name = 'Servilia')
|
194
|
+
```
|
195
|
+
|
196
|
+
#### Selecting one object
|
197
|
+
|
198
|
+
```ruby
|
199
|
+
# Returns an exception if more than one object matches the selection
|
200
|
+
User.objects.get(id: 258)
|
201
|
+
|
202
|
+
# Returns the first object that matches the selection
|
203
|
+
User.objects.filter(id: 258).first
|
204
|
+
```
|
205
|
+
|
206
|
+
#### Selecting from an ActiveRecord
|
207
|
+
|
208
|
+
You can filter from an actual ActiveRecord object:
|
209
|
+
|
210
|
+
```ruby
|
211
|
+
user = User.objects.get(id: 258)
|
212
|
+
user.objects('posts::tags').filter(name__in: %w[battle history]).order_by(name: :ASC)
|
213
|
+
# SELECT users.*
|
214
|
+
# FROM users
|
215
|
+
# LEFT JOIN posts posts_0 ON users.id = posts_0.author_id
|
216
|
+
# LEFT JOIN post_tag post_tags_0 ON posts_0.id = post_tags_0.post_id
|
217
|
+
# WHERE post_tags_0.name IN ['battle', 'history']
|
218
|
+
# ORDER BY post_tags_0.name ASC
|
219
|
+
```
|
220
|
+
|
221
|
+
```ruby
|
222
|
+
julius = User.objects.get(first_name: 'Julius', last_name: 'Caesar')
|
223
|
+
|
224
|
+
# Will return a QuerySet with only the Julius Caesar user (useful for aggregations)
|
225
|
+
julius.objects
|
226
|
+
|
227
|
+
# Will return a QuerySet with all tags of posts of Julius Caesar
|
228
|
+
julius.objects('posts::tags')
|
229
|
+
|
230
|
+
# Will return a QuerySet with the GeoZone of Julius Caesar
|
231
|
+
julius.objects('zone')
|
232
|
+
|
233
|
+
```
|
234
|
+
|
235
|
+
|
236
|
+
##### Lookups
|
237
|
+
|
238
|
+
[See the main docs](/doc/api/queryset/methods/return_queryset.md#field-lookups).
|
239
|
+
|
240
|
+
There are other operators than equal to, these are implemented by using lookups:
|
241
|
+
|
242
|
+
###### equal
|
243
|
+
|
244
|
+
```ruby
|
245
|
+
User.objects.filter(first_name: 'Julius')
|
246
|
+
User.objects.filter(first_name__equal: 'Julius')
|
247
|
+
# SELECT users.*
|
248
|
+
# FROM users
|
249
|
+
# WHERE first_name = 'Julius'
|
250
|
+
```
|
251
|
+
|
252
|
+
###### exact/iexact
|
253
|
+
|
254
|
+
```ruby
|
255
|
+
User.objects.filter(last_name__exact: nil)
|
256
|
+
# SELECT users.*
|
257
|
+
# FROM users
|
258
|
+
# WHERE last_name IS NULL
|
259
|
+
```
|
260
|
+
|
261
|
+
```ruby
|
262
|
+
User.objects.filter(last_name__exact: 'Postumia')
|
263
|
+
# SELECT users.*
|
264
|
+
# FROM users
|
265
|
+
# WHERE last_name LIKE 'Postumia'
|
266
|
+
```
|
267
|
+
|
268
|
+
i preceding a comparison operator means case-insensitive version:
|
269
|
+
|
270
|
+
```ruby
|
271
|
+
User.objects.filter(last_name__iexact: 'Postumia')
|
272
|
+
# SELECT users.*
|
273
|
+
# FROM users
|
274
|
+
# WHERE last_name ILIKE 'Postumia'
|
275
|
+
```
|
276
|
+
|
277
|
+
###### contains/icontains
|
278
|
+
|
279
|
+
```ruby
|
280
|
+
User.objects.filter(first_name__contains: 'iu')
|
281
|
+
# SELECT users.*
|
282
|
+
# FROM users
|
283
|
+
# WHERE last_name LIKE '%iu%'
|
284
|
+
```
|
285
|
+
|
286
|
+
```ruby
|
287
|
+
User.objects.filter(first_name__icontains: 'iu')
|
288
|
+
# SELECT users.*
|
289
|
+
# FROM users
|
290
|
+
# WHERE last_name ILIKE '%iu%'
|
291
|
+
```
|
292
|
+
|
293
|
+
###### endswith/iendswith
|
294
|
+
|
295
|
+
```ruby
|
296
|
+
User.objects.filter(first_name__endswith: 'us')
|
297
|
+
# SELECT users.*
|
298
|
+
# FROM users
|
299
|
+
# WHERE last_name LIKE '%us'
|
300
|
+
```
|
301
|
+
|
302
|
+
```ruby
|
303
|
+
User.objects.filter(first_name__iendswith: 'us')
|
304
|
+
# SELECT users.*
|
305
|
+
# FROM users
|
306
|
+
# WHERE last_name ILIKE '%us'
|
307
|
+
```
|
308
|
+
|
309
|
+
###### startswith/istartswith
|
310
|
+
|
311
|
+
```ruby
|
312
|
+
User.objects.filter(first_name__startswith: 'Mark')
|
313
|
+
# SELECT users.*
|
314
|
+
# FROM users
|
315
|
+
# WHERE first_name LIKE 'Mark%'
|
316
|
+
```
|
317
|
+
|
318
|
+
```ruby
|
319
|
+
User.objects.filter(first_name__istartswith: 'Mark')
|
320
|
+
# SELECT users.*
|
321
|
+
# FROM users
|
322
|
+
# WHERE first_name ILIKE 'Mark%'
|
323
|
+
```
|
324
|
+
|
325
|
+
###### in
|
326
|
+
|
327
|
+
```ruby
|
328
|
+
User.objects.filter(first_name__in: ['Marcus', 'Julius', 'Crasus'])
|
329
|
+
# SELECT users.*
|
330
|
+
# FROM users
|
331
|
+
# WHERE first_name IN ('Marcus', 'Julius', 'Crasus')
|
332
|
+
```
|
333
|
+
|
334
|
+
There is also the possibility to use a subquery instead of a list of elements:
|
335
|
+
|
336
|
+
```ruby
|
337
|
+
Post.objects.filter(id__in: @seneca_sr.objects(:posts).project(:id))
|
338
|
+
# SELECT posts.*
|
339
|
+
# FROM posts
|
340
|
+
# WHERE id IN (SELECT posts.id FROM posts WHERE author_id = 2)
|
341
|
+
```
|
342
|
+
|
343
|
+
|
344
|
+
###### Comparison operators: gt, gte, lt, lte
|
345
|
+
|
346
|
+
```ruby
|
347
|
+
Posts.objects.filter(score__gt: 4)
|
348
|
+
# SELECT posts.*
|
349
|
+
# FROM posts
|
350
|
+
# WHERE score > 4
|
351
|
+
```
|
352
|
+
|
353
|
+
```ruby
|
354
|
+
Posts.objects.filter(score__lt: 4)
|
355
|
+
# SELECT posts.*
|
356
|
+
# FROM posts
|
357
|
+
# WHERE score < 4
|
358
|
+
```
|
359
|
+
|
360
|
+
```ruby
|
361
|
+
Posts.objects.filter(score__gte: 4)
|
362
|
+
# SELECT posts.*
|
363
|
+
# FROM posts
|
364
|
+
# WHERE score >= 4
|
365
|
+
```
|
366
|
+
|
367
|
+
```ruby
|
368
|
+
Posts.objects.filter(score__lte: 4)
|
369
|
+
# SELECT posts.*
|
370
|
+
# FROM posts
|
371
|
+
# WHERE score <= 4
|
372
|
+
```
|
373
|
+
|
374
|
+
|
375
|
+
###### Other lookups
|
376
|
+
|
377
|
+
See more [here](/doc/api/queryset/lookups.md).
|
378
|
+
|
379
|
+
#### Selection by foreign model field
|
380
|
+
|
381
|
+
The main feature of Babik is filtering by foreign keys.
|
382
|
+
|
383
|
+
Remember:
|
384
|
+
|
385
|
+
- **Your associations must have always an inverse (by making use of inverse_of)**.
|
386
|
+
|
387
|
+
- **Many-to-many** relationships are only supported when based on **has_many through**.
|
388
|
+
[Reason](https://github.com/rubocop-hq/rails-style-guide#has-many-through).
|
389
|
+
|
390
|
+
##### Belongs to relationships
|
391
|
+
|
392
|
+
```ruby
|
393
|
+
User.objects.filter('zone::name': 'Roman Empire')
|
394
|
+
# SELECT users.*
|
395
|
+
# FOR users
|
396
|
+
# LEFT JOIN geo_zones users_zone_0 ON users.zone_id = parent_zones_0.id
|
397
|
+
# WHERE users_zone_0 = 'Roman Empire'
|
398
|
+
```
|
399
|
+
|
400
|
+
All depth levels are accepted:
|
401
|
+
|
402
|
+
```ruby
|
403
|
+
User.objects.filter('zone::parent_zone::parent_zone::name': 'Roman Empire')
|
404
|
+
# SELECT users.*
|
405
|
+
# FOR users
|
406
|
+
# LEFT JOIN geo_zones users_zone_0 ON users.zone_id = parent_zones_0.id
|
407
|
+
# LEFT JOIN geo_zones parent_zones_0 ON users_zone_0.parent_id = parent_zones_0.id
|
408
|
+
# LEFT JOIN geo_zones parent_zones_1 ON parent_zones_0.parent_id = parent_zones_1.id
|
409
|
+
# WHERE parent_zones_1 = 'Roman Empire'
|
410
|
+
```
|
411
|
+
|
412
|
+
##### Has many relationships
|
413
|
+
|
414
|
+
```ruby
|
415
|
+
User.objects.distinct.filter('posts::tag::name': 'history')
|
416
|
+
# SELECT DISTINCT users.*
|
417
|
+
# FOR users
|
418
|
+
# LEFT JOIN posts posts_0 ON users.id = posts_0.author_id
|
419
|
+
# LEFT JOIN post_tag post_tags_0 ON posts_0.id = post_tags_0.post_id
|
420
|
+
# LEFT JOIN tags tags_0 ON post_tags_0.tag_id = tags_0.id
|
421
|
+
# WHERE post_tag_tags_0 = 'history'
|
422
|
+
```
|
423
|
+
|
424
|
+
Note by using [distinct](/doc/api/queryset/methods/return_queryset.md#distinct)
|
425
|
+
we have avoided duplicated users (in case the same user has more than one post
|
426
|
+
with tagged as 'history').
|
427
|
+
|
428
|
+
#### Projections
|
429
|
+
|
430
|
+
[See the main docs](/doc/api/queryset/methods/dont_return_queryset.md#project).
|
431
|
+
|
432
|
+
Return
|
433
|
+
an [ActiveRecord Result](http://api.rubyonrails.org/classes/ActiveRecord/Result.html)
|
434
|
+
with only the fields you are interested
|
435
|
+
by using a [projection](/doc/api/queryset/methods/dont_return_queryset.md#project):
|
436
|
+
|
437
|
+
```ruby
|
438
|
+
p User.objects.filter('zone::name': 'Castilla').order_by('first_name').project('first_name', 'email')
|
439
|
+
|
440
|
+
# Query:
|
441
|
+
# SELECT users.first_name, users.email
|
442
|
+
# FROM users
|
443
|
+
# LEFT JOIN geo_zones users_zone_0 ON users.zone_id = parent_zones_0.id
|
444
|
+
# WHERE users_zone_0.name = 'Castilla'
|
445
|
+
# ORDER BY users.first_name ASC
|
446
|
+
|
447
|
+
# Result:
|
448
|
+
# [
|
449
|
+
# { first_name: 'Isabel I', email: 'isabeli@example.com' },
|
450
|
+
# { first_name: 'Juan II', email: 'juanii@example.com' },
|
451
|
+
# { first_name: 'Juana I', email: 'juanai@example.com' }
|
452
|
+
# ]
|
453
|
+
```
|
454
|
+
|
455
|
+
#### Select related
|
456
|
+
|
457
|
+
[See the main docs](/doc/api/queryset/methods/dont_return_queryset.md#select-related).
|
458
|
+
|
459
|
+
**select_related** method allows fetching an object and its related ones at once.
|
460
|
+
|
461
|
+
```ruby
|
462
|
+
User.filter(first_name: 'Julius').select_related(:zone)
|
463
|
+
# Will return in each iteration a list with two elements, the first one
|
464
|
+
# will be the User instance, and the other one a hash where the keys are
|
465
|
+
# each one of the association names and the value the associated object
|
466
|
+
```
|
467
|
+
|
468
|
+
##### Order
|
469
|
+
|
470
|
+
[See the main docs](/doc/api/queryset/methods/return_queryset.md#order-by).
|
471
|
+
|
472
|
+
Ordering by one field (ASC)
|
473
|
+
|
474
|
+
```ruby
|
475
|
+
User.objects.order_by(:last_name)
|
476
|
+
# SELECT users.*
|
477
|
+
# FOR users
|
478
|
+
# ORDER BY users.last_name ASC
|
479
|
+
```
|
480
|
+
|
481
|
+
Ordering by one field (DESC)
|
482
|
+
|
483
|
+
```ruby
|
484
|
+
User.objects.order_by(%i[last_name, DESC])
|
485
|
+
# SELECT users.*
|
486
|
+
# FOR users
|
487
|
+
# ORDER BY users.last_name DESC
|
488
|
+
```
|
489
|
+
|
490
|
+
Ordering by several fields
|
491
|
+
|
492
|
+
```ruby
|
493
|
+
User.objects.order_by(%i[last_name, ASC], %i[first_name, ASC])
|
494
|
+
# SELECT users.*
|
495
|
+
# FOR users
|
496
|
+
# ORDER BY users.last_name ASC, users.first_name ASC
|
497
|
+
```
|
498
|
+
|
499
|
+
Ordering by foreign fields
|
500
|
+
|
501
|
+
```ruby
|
502
|
+
User.objects
|
503
|
+
.filter('zone::name': 'Roman Empire')
|
504
|
+
.order_by(%i[zone::name, ASC], %i[created_at, DESC])
|
505
|
+
# SELECT users.*
|
506
|
+
# FOR users
|
507
|
+
# LEFT JOIN geo_zones users_zone_0 ON users.zone_id = parent_zones_0.id
|
508
|
+
# WHERE users_zone_0 = 'Roman Empire'
|
509
|
+
# ORDER BY parent_zones_0.name ASC, users.created_at DESC
|
510
|
+
```
|
511
|
+
|
512
|
+
Inverting the order
|
513
|
+
|
514
|
+
```ruby
|
515
|
+
|
516
|
+
User.objects
|
517
|
+
.filter('zone::name': 'Roman Empire')
|
518
|
+
.order_by(%i[zone::name, ASC], %i[created_at, DESC]).reverse
|
519
|
+
# SELECT users.*
|
520
|
+
# FOR users
|
521
|
+
# LEFT JOIN geo_zones users_zone_0 ON users.zone_id = parent_zones_0.id
|
522
|
+
# WHERE users_zone_0 = 'Roman Empire'
|
523
|
+
# ORDER BY parent_zones_0.name DES, users.created_at ASC
|
524
|
+
```
|
525
|
+
|
526
|
+
#### Delete
|
527
|
+
|
528
|
+
[See the main docs](/doc/api/queryset/methods/dont_return_queryset.md#delete).
|
529
|
+
|
530
|
+
There is no standard DELETE from foreign field SQL statement, so for now
|
531
|
+
the default implementation makes use of DELETE WHERE id IN SELECT subqueries.
|
532
|
+
|
533
|
+
Future implementations will use joins.
|
534
|
+
|
535
|
+
##### Delete by local field
|
536
|
+
|
537
|
+
```ruby
|
538
|
+
User.objects.filter('first_name': 'Julius', 'last_name': 'Caesar').delete
|
539
|
+
# DELETE
|
540
|
+
# FROM users
|
541
|
+
# WHERE id IN (
|
542
|
+
# SELECT users.*
|
543
|
+
# FOR users
|
544
|
+
# WHERE users.first_name = 'Julius' AND users.last_name = 'Caesar'
|
545
|
+
# )
|
546
|
+
```
|
547
|
+
|
548
|
+
##### Delete by foreign field
|
549
|
+
|
550
|
+
```ruby
|
551
|
+
GeoZone.get('name': 'Roman Empire').objects('users').delete
|
552
|
+
User.objects.filter('zone::name': 'Roman Empire').delete
|
553
|
+
# Both statements are equal:
|
554
|
+
# DELETE
|
555
|
+
# FROM users
|
556
|
+
# WHERE id IN (
|
557
|
+
# SELECT users.*
|
558
|
+
# FOR users
|
559
|
+
# LEFT JOIN geo_zones users_zone_0 ON users.zone_id = parent_zones_0.id
|
560
|
+
# WHERE users_zone_0 = 'Roman Empire'
|
561
|
+
# )
|
562
|
+
```
|
563
|
+
|
564
|
+
## Update
|
565
|
+
|
566
|
+
[See the main docs](/doc/api/queryset/methods/dont_return_queryset.md#update).
|
567
|
+
|
568
|
+
Similar to what happens in when running SQL-delete statements, there is no
|
569
|
+
standard UPDATE from foreign field SQL statement, so for now
|
570
|
+
the default implementation makes use of UPDATE SET ... WHERE id IN SELECT subqueries.
|
571
|
+
|
572
|
+
Future implementations will use joins.
|
573
|
+
|
574
|
+
##### Update by local field
|
575
|
+
|
576
|
+
```ruby
|
577
|
+
User.objects.filter('first_name': 'Julius', 'last_name': 'Caesar').update(first_name: 'Iulius')
|
578
|
+
# UPDATE SET first_name = 'Iulius'
|
579
|
+
# FROM users
|
580
|
+
# WHERE id IN (
|
581
|
+
# SELECT users.*
|
582
|
+
# FOR users
|
583
|
+
# WHERE users.first_name = 'Julius' AND users.last_name = 'Caesar'
|
584
|
+
# )
|
585
|
+
```
|
586
|
+
|
587
|
+
##### Update by foreign field
|
588
|
+
|
589
|
+
```ruby
|
590
|
+
GeoZone.get(name: 'Roman Empire').objects('users').filter(last_name__isnull: true).update(last_name: 'Romanum')
|
591
|
+
User.objects.filter('zone::name': 'Roman Empire', last_name__isnull: true).update(last_name: 'Romanum')
|
592
|
+
# Both statements are equal:
|
593
|
+
# UPDATE SET last_name = 'Romanum'
|
594
|
+
# FROM users
|
595
|
+
# WHERE id IN (
|
596
|
+
# SELECT users.*
|
597
|
+
# FOR users
|
598
|
+
# LEFT JOIN geo_zones users_zone_0 ON users.zone_id = parent_zones_0.id
|
599
|
+
# WHERE users_zone_0 = 'Roman Empire' AND users.last_name IS NULL
|
600
|
+
# )
|
601
|
+
```
|
602
|
+
|
603
|
+
##### Update field by using an actual value of the record
|
604
|
+
|
605
|
+
```ruby
|
606
|
+
Post.objects.filter(stars__gte: 1, stars__lte: 4)
|
607
|
+
.update(stars: Babik::QuerySet::Update::Increment.new('stars'))
|
608
|
+
# UPDATE SET stars = stars + 1
|
609
|
+
# FROM posts
|
610
|
+
# WHERE id IN (
|
611
|
+
# SELECT posts.*
|
612
|
+
# FOR posts
|
613
|
+
# WHERE posts.stars >= 1 AND posts.stars <= 4
|
614
|
+
# )
|
615
|
+
```
|
616
|
+
|
617
|
+
## Documentation
|
618
|
+
|
619
|
+
See the [documentation](doc/README.md) for more information
|
620
|
+
about the [API](doc/README.md#queryset-api) and the
|
621
|
+
internals of this library.
|
622
|
+
|
623
|
+
|
624
|
+
|
625
|
+
## Unimplemented API
|
626
|
+
|
627
|
+
### Methods that return a QuerySet
|
628
|
+
|
629
|
+
#### Will be implemented
|
630
|
+
|
631
|
+
- [prefetch_related](https://docs.djangoproject.com/en/2.0/ref/models/querysets/#prefetch_related)
|
632
|
+
|
633
|
+
#### Will not be implemented
|
634
|
+
|
635
|
+
- [dates](https://docs.djangoproject.com/en/2.0/ref/models/querysets/#dates): [project](/doc/api/queryset/methods/dont_return_queryset.md#project) allow transformer functions that can be used to get dates in the desired format.
|
636
|
+
- [datetimes](https://docs.djangoproject.com/en/2.0/ref/models/querysets/#datetimes): [project](/doc/api/queryset/methods/dont_return_queryset.md#project) allow transformer functions that can be used to get datetimes in the desired format.
|
637
|
+
- [extra](https://docs.djangoproject.com/en/2.0/ref/models/querysets/#extra): better use the ActiveRecord API or for raw SQL use [find_by_sql](https://apidock.com/rails/ActiveRecord/Querying/find_by_sql).
|
638
|
+
- [values](https://docs.djangoproject.com/en/2.0/ref/models/querysets/#values): can be computed using [project](/doc/api/queryset/methods/dont_return_queryset.md#project).
|
639
|
+
- [values_list](https://docs.djangoproject.com/en/2.0/ref/models/querysets/#values_list): can be computed using [project](/doc/api/queryset/methods/dont_return_queryset.md#project).
|
640
|
+
- [raw](https://docs.djangoproject.com/en/2.0/ref/models/querysets/#raw): use ActiveRecord [find_by_sql](https://apidock.com/rails/ActiveRecord/Querying/find_by_sql). Babik is not
|
641
|
+
for doing raw queries, is for having an additional query system to the ActiveRecord one.
|
642
|
+
- [using](https://docs.djangoproject.com/en/2.0/ref/models/querysets/#using): to change the database a model
|
643
|
+
is better to use something like [this](https://stackoverflow.com/questions/180349/how-can-i-dynamically-change-the-active-record-database-for-all-models-in-ruby-o).
|
644
|
+
|
645
|
+
#### Under consideration
|
646
|
+
|
647
|
+
I am not sure it is a good idea to allow deferred loading or fields. I think is a poor solution for tables with too many
|
648
|
+
fields. Should I have to take the trouble to implement this two methods?:
|
649
|
+
|
650
|
+
- [defer](https://docs.djangoproject.com/en/2.0/ref/models/querysets/#defer)
|
651
|
+
- [only](https://docs.djangoproject.com/en/2.0/ref/models/querysets/#only)
|
652
|
+
|
653
|
+
|
654
|
+
|
655
|
+
### Methods that don't return a QuerySet
|
656
|
+
|
657
|
+
#### Will not be implemented
|
658
|
+
|
659
|
+
The aim of this library is to help make complex queries, not re-implementing the
|
660
|
+
well-defined and working API of Rails. All of this methods have equivalents in Rails,
|
661
|
+
but if you are interested, I'm accepting pull-requests.
|
662
|
+
|
663
|
+
- [create](https://docs.djangoproject.com/en/2.0/ref/models/querysets/#create)
|
664
|
+
- [get_or_create](https://docs.djangoproject.com/en/2.0/ref/models/querysets/#get_or_create)
|
665
|
+
- [update_or_create](https://docs.djangoproject.com/en/2.0/ref/models/querysets/#update_or_create)
|
666
|
+
- [bulk_create](https://docs.djangoproject.com/en/2.0/ref/models/querysets/#bulk_create)
|
667
|
+
- [in_bulk](https://docs.djangoproject.com/en/2.0/ref/models/querysets/#in_bulk)
|
668
|
+
- [iterator](https://docs.djangoproject.com/en/2.0/ref/models/querysets/#iterator)
|
669
|
+
- [as_manager](https://docs.djangoproject.com/en/2.0/ref/models/querysets/#as_manager)
|
670
|
+
|
671
|
+
|
672
|
+
### Aggregation functions
|
673
|
+
|
674
|
+
#### Will be not implemented
|
675
|
+
|
676
|
+
- [expression](https://docs.djangoproject.com/en/2.0/ref/models/querysets/#expression):
|
677
|
+
there are no [Query Expressions](https://docs.djangoproject.com/en/2.0/ref/models/expressions/)
|
678
|
+
in Babik, will be possible with the custom aggregations.
|
679
|
+
- [output_field](https://docs.djangoproject.com/en/2.0/ref/models/querysets/#output_field): already possible passing a hash where the key is the output field.
|
680
|
+
- [filter](https://docs.djangoproject.com/en/2.0/ref/models/querysets/#id6): there are no Q objects in Babik.
|
681
|
+
- [**extra](https://docs.djangoproject.com/en/2.0/ref/models/querysets/#id7): no way to include
|
682
|
+
extra keyword arguments in the aggregates for now.
|
683
|
+
|
684
|
+
|
685
|
+
## Roadmap
|
686
|
+
|
687
|
+
### Increase code quality
|
688
|
+
|
689
|
+
This project must follow Rubocop directives and pass Reek checks.
|
690
|
+
|
691
|
+
### Make a babik-test project
|
692
|
+
|
693
|
+
Make a repository with the test schema to check the library is really working.
|
694
|
+
|
695
|
+
### Deploy in rubygems
|
696
|
+
|
697
|
+
Deploy gem in rubygems.
|
698
|
+
|
699
|
+
### Prefect
|
700
|
+
|
701
|
+
[Object prefetching](https://docs.djangoproject.com/en/2.0/ref/models/querysets/#prefetch-objects)
|
702
|
+
is not implemented yet.
|
703
|
+
|
704
|
+
### Annotations
|
705
|
+
|
706
|
+
[Annotations](https://docs.djangoproject.com/en/2.0/topics/db/aggregation/#aggregation)
|
707
|
+
are not implemented yet.
|
708
|
+
|
709
|
+
### Support other DBMS
|
710
|
+
|
711
|
+
Oracle is not supported at the moment because of they lack LIMIT clause
|
712
|
+
in SELECT queries.
|
713
|
+
|
714
|
+
MSSQL is supported in some operations.
|
715
|
+
|
716
|
+
## License
|
717
|
+
|
718
|
+
[MIT](LICENSE)
|