babik 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (109) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +16 -0
  3. data/README.md +718 -0
  4. data/Rakefile +18 -0
  5. data/lib/babik.rb +122 -0
  6. data/lib/babik/database.rb +16 -0
  7. data/lib/babik/queryset.rb +154 -0
  8. data/lib/babik/queryset/components/aggregation.rb +172 -0
  9. data/lib/babik/queryset/components/limit.rb +22 -0
  10. data/lib/babik/queryset/components/order.rb +161 -0
  11. data/lib/babik/queryset/components/projection.rb +118 -0
  12. data/lib/babik/queryset/components/select_related.rb +78 -0
  13. data/lib/babik/queryset/components/sql_renderer.rb +99 -0
  14. data/lib/babik/queryset/components/where.rb +43 -0
  15. data/lib/babik/queryset/lib/association/foreign_association_chain.rb +97 -0
  16. data/lib/babik/queryset/lib/association/select_related_association_chain.rb +32 -0
  17. data/lib/babik/queryset/lib/condition.rb +103 -0
  18. data/lib/babik/queryset/lib/field.rb +34 -0
  19. data/lib/babik/queryset/lib/join/association_joiner.rb +39 -0
  20. data/lib/babik/queryset/lib/join/join.rb +86 -0
  21. data/lib/babik/queryset/lib/selection/config.rb +19 -0
  22. data/lib/babik/queryset/lib/selection/foreign_selection.rb +39 -0
  23. data/lib/babik/queryset/lib/selection/local_selection.rb +40 -0
  24. data/lib/babik/queryset/lib/selection/operation/base.rb +126 -0
  25. data/lib/babik/queryset/lib/selection/operation/date.rb +178 -0
  26. data/lib/babik/queryset/lib/selection/operation/operations.rb +201 -0
  27. data/lib/babik/queryset/lib/selection/operation/regex.rb +58 -0
  28. data/lib/babik/queryset/lib/selection/path/foreign_path.rb +50 -0
  29. data/lib/babik/queryset/lib/selection/path/local_path.rb +44 -0
  30. data/lib/babik/queryset/lib/selection/path/path.rb +23 -0
  31. data/lib/babik/queryset/lib/selection/select_related_selection.rb +38 -0
  32. data/lib/babik/queryset/lib/selection/selection.rb +19 -0
  33. data/lib/babik/queryset/lib/update/assignment.rb +108 -0
  34. data/lib/babik/queryset/mixins/aggregatable.rb +17 -0
  35. data/lib/babik/queryset/mixins/bounded.rb +38 -0
  36. data/lib/babik/queryset/mixins/clonable.rb +52 -0
  37. data/lib/babik/queryset/mixins/countable.rb +44 -0
  38. data/lib/babik/queryset/mixins/deletable.rb +13 -0
  39. data/lib/babik/queryset/mixins/distinguishable.rb +27 -0
  40. data/lib/babik/queryset/mixins/filterable.rb +51 -0
  41. data/lib/babik/queryset/mixins/limitable.rb +88 -0
  42. data/lib/babik/queryset/mixins/lockable.rb +31 -0
  43. data/lib/babik/queryset/mixins/none.rb +16 -0
  44. data/lib/babik/queryset/mixins/projectable.rb +34 -0
  45. data/lib/babik/queryset/mixins/related_selector.rb +28 -0
  46. data/lib/babik/queryset/mixins/set_operations.rb +32 -0
  47. data/lib/babik/queryset/mixins/sortable.rb +49 -0
  48. data/lib/babik/queryset/mixins/sql_renderizable.rb +17 -0
  49. data/lib/babik/queryset/mixins/updatable.rb +14 -0
  50. data/lib/babik/queryset/templates/default/delete/main.sql.erb +14 -0
  51. data/lib/babik/queryset/templates/default/select/components/aggregation.sql.erb +5 -0
  52. data/lib/babik/queryset/templates/default/select/components/from.sql.erb +16 -0
  53. data/lib/babik/queryset/templates/default/select/components/from_set.sql.erb +3 -0
  54. data/lib/babik/queryset/templates/default/select/components/from_table.sql.erb +2 -0
  55. data/lib/babik/queryset/templates/default/select/components/limit.sql.erb +10 -0
  56. data/lib/babik/queryset/templates/default/select/components/order_by.sql.erb +9 -0
  57. data/lib/babik/queryset/templates/default/select/components/projection.sql.erb +7 -0
  58. data/lib/babik/queryset/templates/default/select/components/select_related.sql.erb +26 -0
  59. data/lib/babik/queryset/templates/default/select/components/where.sql.erb +39 -0
  60. data/lib/babik/queryset/templates/default/select/main.sql.erb +42 -0
  61. data/lib/babik/queryset/templates/default/update/main.sql.erb +15 -0
  62. data/lib/babik/queryset/templates/mssql/select/components/limit.sql.erb +8 -0
  63. data/lib/babik/queryset/templates/mssql/select/components/order_by.sql.erb +21 -0
  64. data/lib/babik/queryset/templates/mysql2/delete/main.sql.erb +15 -0
  65. data/lib/babik/queryset/templates/mysql2/update/main.sql.erb +18 -0
  66. data/lib/babik/queryset/templates/sqlite3/select/components/from_set.sql.erb +5 -0
  67. data/test/config/db/schema.rb +83 -0
  68. data/test/config/models/bad_post.rb +5 -0
  69. data/test/config/models/bad_tag.rb +5 -0
  70. data/test/config/models/category.rb +4 -0
  71. data/test/config/models/geozone.rb +6 -0
  72. data/test/config/models/group.rb +5 -0
  73. data/test/config/models/group_user.rb +5 -0
  74. data/test/config/models/post.rb +24 -0
  75. data/test/config/models/post_tag.rb +5 -0
  76. data/test/config/models/tag.rb +5 -0
  77. data/test/config/models/user.rb +6 -0
  78. data/test/delete/delete_test.rb +60 -0
  79. data/test/delete/foreign_conditions_delete_test.rb +57 -0
  80. data/test/delete/local_conditions_delete_test.rb +20 -0
  81. data/test/enable_coverage.rb +17 -0
  82. data/test/lib/selection/operation/log/test-queries.log +1 -0
  83. data/test/lib/selection/operation/test_date.rb +131 -0
  84. data/test/lib/selection/operation/test_regex.rb +55 -0
  85. data/test/other/clone_test.rb +129 -0
  86. data/test/other/escape_test.rb +21 -0
  87. data/test/other/inverse_of_required_test.rb +33 -0
  88. data/test/select/aggregate_test.rb +151 -0
  89. data/test/select/bounds_test.rb +46 -0
  90. data/test/select/count_test.rb +147 -0
  91. data/test/select/distinct_test.rb +38 -0
  92. data/test/select/exclude_test.rb +72 -0
  93. data/test/select/filter_from_object_test.rb +125 -0
  94. data/test/select/filter_test.rb +207 -0
  95. data/test/select/for_update_test.rb +19 -0
  96. data/test/select/foreign_selection_test.rb +60 -0
  97. data/test/select/get_test.rb +40 -0
  98. data/test/select/limit_test.rb +109 -0
  99. data/test/select/local_selection_test.rb +24 -0
  100. data/test/select/lookup_test.rb +208 -0
  101. data/test/select/none_test.rb +40 -0
  102. data/test/select/order_test.rb +165 -0
  103. data/test/select/project_test.rb +107 -0
  104. data/test/select/select_related_test.rb +124 -0
  105. data/test/select/subquery_test.rb +50 -0
  106. data/test/set_operations/basic_usage_test.rb +121 -0
  107. data/test/test_helper.rb +55 -0
  108. data/test/update/update_test.rb +93 -0
  109. metadata +278 -0
@@ -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
@@ -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)