babik 0.1.0

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.
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)