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,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Babik
4
+ module QuerySet
5
+ # Functionality related to the size of the QuerySet
6
+ module Countable
7
+
8
+ # Return the number of elements that match the condition defined by successive calls of filter and exclude.
9
+ # @return [Integer] Number of elements that match the condition defined in this QuerySet.
10
+ def count
11
+ self.all.count
12
+ end
13
+
14
+ # Inform if the QuerySet has no elements that match the condition.
15
+ # @return [Boolean] True if no records match the filter, false otherwise.
16
+ def empty?
17
+ self.count.zero?
18
+ end
19
+
20
+ # Inform if the QuerySet has at least one element that match the condition.
21
+ # @return [Boolean] True if one or more records match the filter, false otherwise.
22
+ def exists?
23
+ self.count.positive?
24
+ end
25
+
26
+ # Return the number of elements that match the condition defined by successive calls of filter and exclude.
27
+ # Alias of count.
28
+ # @see Babik::QuerySet::Countable#count
29
+ # @return [Integer] Number of elements that match the condition defined in this QuerySet.
30
+ def length
31
+ self.count
32
+ end
33
+
34
+ # Return the number of elements that match the condition defined by successive calls of filter and exclude.
35
+ # Alias of count.
36
+ # @see Babik::QuerySet::Countable#count
37
+ # @return [Integer] Number of elements that match the condition defined in this QuerySet.
38
+ def size
39
+ self.count
40
+ end
41
+
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Babik
4
+ module QuerySet
5
+ # Functionality related to the DELETE operation
6
+ module Deletable
7
+ # Delete the selected records
8
+ def delete
9
+ result = @model.connection.execute(sql.delete)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Babik
4
+ module QuerySet
5
+ # Distinguishable functionality for QuerySet
6
+ module Distinguishable
7
+
8
+ # Mark this QuerySet as distinguishable.
9
+ # Modify this object
10
+ # (i.e. DISTINCT keyword will be applied to the final SQL query).
11
+ # @return [QuerySet] Reference to this QuerySet.
12
+ def distinct!
13
+ @_distinct = true
14
+ self
15
+ end
16
+
17
+ # Mark this QuerySet as not distinguishable
18
+ # (i.e. DISTINCT keyword will NOT be applied to query).
19
+ # @return [QuerySet] Reference to this QuerySet.
20
+ def undistinct!
21
+ @_distinct = false
22
+ self
23
+ end
24
+
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Babik
4
+ module QuerySet
5
+ # Functionality related to the DELETE operation
6
+ module Filterable
7
+ # Exclude objects according to some criteria.
8
+ # @return [QuerySet] Reference to self.
9
+ def exclude!(filter)
10
+ _filter(filter, 'exclusion')
11
+ self
12
+ end
13
+
14
+ # Select objects according to some criteria.
15
+ # @param filter [Array, Hash] if array, it is considered an disjunction (OR clause),
16
+ # if a hash, it is considered a conjunction (AND clause).
17
+ # @return [QuerySet] Reference to self.
18
+ def filter!(filter)
19
+ _filter(filter, 'inclusion')
20
+ self
21
+ end
22
+
23
+ # Get an single element
24
+ # @param filter [Array, Hash] if array, it is considered an disjunction (OR clause),
25
+ # if a hash, it is considered a conjunction (AND clause).
26
+ # @raise [RuntimeError] Exception:
27
+ # 'Multiple objects returned' if more than one object matches the condition.
28
+ # 'Does not exist' if no object match the conditions.
29
+ # @return [ActiveRecord::Base] object that matches the filter.
30
+ def get(filter)
31
+ result_ = self.filter(filter).all
32
+ result_count = result_.count
33
+ raise 'Does not exist' if result_count.zero?
34
+ raise 'Multiple objects returned' if result_count > 1
35
+ result_.first
36
+ end
37
+
38
+ private
39
+
40
+ # Select the objects according to some criteria.
41
+ # @param filter [Array, Hash] if array, it is considered an disjunction (OR clause),
42
+ # if a hash, it is considered a conjunction (AND clause).
43
+ # @param filter_type [String] Filter type. Must be 'inclusion' or 'exclusion'.
44
+ # @raise [NoMethodError] if filter_type is not 'inclusion' nor 'exclusion'.
45
+ def _filter(filter, filter_type)
46
+ @_where.send("add_#{filter_type}_filter", filter)
47
+ end
48
+
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Babik
4
+ module QuerySet
5
+ # Limit functionality of QuerySet
6
+ module Limitable
7
+
8
+ # Configure a limit this QuerySet
9
+ # @param param [Range, Integer]
10
+ # If it is a range, first_element..last_element will be selected.
11
+ # If it is an integer, the element in that position will be returned. No negative number is allowed.
12
+ # @raise RuntimeError 'Invalid limit passed to query: <VALUE>' If param is not a Range or Integer.
13
+ # @return [QuerySet, ActiveRecord::Base] QuerySet if a slice was passe as parameter,
14
+ # otherwise an ActiveRecord model.
15
+ def [](param)
16
+ raise "Invalid limit passed to query: #{param}" unless [Range, Integer].include?(param.class)
17
+ self.clone.send("limit_#{param.class.to_s.downcase}!", param)
18
+ end
19
+
20
+ # Inform if at least one record is matched by this QuerySet
21
+ # @return [Boolean] True if at least one record matches the conditions of the QuerySet, false otherwise.
22
+ def exists?
23
+ element = self.fetch(0, false)
24
+ return true if element
25
+ false
26
+ end
27
+
28
+ # Return an element at an index, otherwise:
29
+ # - Return a default value if it has been passed as second argument.
30
+ # - Raise an IndexError exception
31
+ # @param index [Integer] Position of the element want to return. No negative number is allowed.
32
+ # @param default_value [Object] Anything that will be returned if no record is found at the index position.
33
+ # By default it takes a nil value (in that case, it will raise the IndexError exception).
34
+ # @raise [IndexError] When there is no default value
35
+ def fetch(index, default_value = nil)
36
+ element = self.[](index)
37
+ return element if element
38
+ return default_value unless default_value.nil?
39
+ raise IndexError, "Index #{index} outside of QuerySet bounds"
40
+ end
41
+
42
+ # Configure a limit this QuerySet
43
+ # @param size [Integer] Number of elements to be selected.
44
+ # @param offset [Integer] Position where the selection will start. By default is 0. No negative number is allowed.
45
+ # @return [QuerySet] Reference to this QuerySet.
46
+ def limit!(size, offset = 0)
47
+ @_limit = Babik::QuerySet::Limit.new(size, offset)
48
+ self
49
+ end
50
+
51
+ # Inform if this QuerySet is limited
52
+ # @return [Boolean] true if this QuerySet has a limit, false otherwise.
53
+ def limit?
54
+ @_limit && true
55
+ end
56
+
57
+ # Destroy the current limit of this QuerySet
58
+ # @return [QuerySet] Reference to this QuerySet.
59
+ def unlimit!
60
+ @_limit = nil
61
+ self
62
+ end
63
+
64
+ private
65
+
66
+ # Get one element at a determined position
67
+ # @param position [Integer] Position of the element to be returned.
68
+ # @api private
69
+ # @return [ActiveRecord::Base, nil] ActiveRecord::Base if exists a record in that position, nil otherwise.
70
+ def limit_integer!(position)
71
+ @_limit = Babik::QuerySet::Limit.new(1, position)
72
+ self.first
73
+ end
74
+
75
+ # Get a QuerySet with a slice of the original QuerySet
76
+ # @param param [Range] first_element..last_element will be selected.
77
+ # @api private
78
+ # @return [QuerySet] QuerySet with a slice of the caller QuerySet.
79
+ def limit_range!(param)
80
+ offset = param.min
81
+ size = param.max.to_i - param.min.to_i
82
+ @_limit = Babik::QuerySet::Limit.new(size, offset)
83
+ self
84
+ end
85
+
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Babik
4
+ module QuerySet
5
+ # Lock functionality of QuerySet
6
+ module Lockable
7
+
8
+ # Lock the table for writes
9
+ # This must be inside a transaction
10
+ def for_update!
11
+ @_lock_type = 'FOR UPDATE'
12
+ self
13
+ end
14
+
15
+ # Lock the table for writes
16
+ # This must be inside a transaction
17
+ # @see #for_update Alias of for_update method
18
+ def lock!
19
+ self.for_update!
20
+ end
21
+
22
+ # Check if there is a lock
23
+ # @return [Boolean] True if there is a lock, false otherwise.
24
+ def lock?
25
+ return true if @_lock_type
26
+ false
27
+ end
28
+
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Babik
4
+ module QuerySet
5
+ # None functionality for QuerySet
6
+ module NoneQuerySet
7
+
8
+ # Return an empty ActiveRecord ResultSet
9
+ # @return [ResultSet] Empty result set.
10
+ def none
11
+ @model.find_by_sql("SELECT * FROM #{@model.table_name} WHERE 1 = 0")
12
+ end
13
+
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Babik
4
+ module QuerySet
5
+ # Project functionality of QuerySet
6
+ module Projectable
7
+
8
+ # Prepares a projection of only some attributes
9
+ # @param *attributes [Array] Attributes that will be projected.
10
+ # Each one of these can be a local field, or a foreign entity field.
11
+ # Babik will take care of joins.
12
+ # @return [QuerySet] Reference to this QuerySet.
13
+ def project!(*attributes)
14
+ @_projection = Babik::QuerySet::Projection.new(@model, attributes)
15
+ self
16
+ end
17
+
18
+ # Removes the projection.
19
+ # @return [QuerySet] Reference to this QuerySet.
20
+ def unproject!
21
+ @_projection = nil
22
+ self
23
+ end
24
+
25
+ # Inform if there is the QuerySet is configured with a projection
26
+ # @return [Boolean] True if there is a projection configured, false otherwise.
27
+ def projection?
28
+ return true if @_projection
29
+ false
30
+ end
31
+
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Babik
4
+ module QuerySet
5
+
6
+ # select_related functionality of QuerySet
7
+ module RelatedSelector
8
+
9
+ # Load the related objects of each model object specified by the association_paths
10
+ #
11
+ # e.g.
12
+ # - User.objects.filter(first_name: 'Julius').select_related(:group)
13
+ # - User.objects.filter(first_name: 'Cassius').select_related([:group, :zone])
14
+ # - Post.objects.select_related(:author)
15
+ #
16
+ # @param association_paths [Array<Symbol>, Symbol] Array of association paths
17
+ # of belongs_to and has_one related objects.
18
+ # A passed symbol will be considered as an array of one symbol.
19
+ # That is, select_related(:group) is equal to select_related([:group])
20
+ def select_related!(association_paths)
21
+ @_select_related = Babik::QuerySet::SelectRelated.new(@model, association_paths)
22
+ self
23
+ end
24
+
25
+ end
26
+
27
+ end
28
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Babik
4
+ module QuerySet
5
+ # Set operations over QuerySets
6
+ module SetOperations
7
+
8
+ # Difference (minus) operation
9
+ # @param other_queryset [QuerySet] Other QuerySet
10
+ # @return [Babik::QuerySet::Except] Except set operation between this queryset and the other queryset.
11
+ def difference(other_queryset)
12
+ Babik::QuerySet::Except.new(self.model, self, other_queryset)
13
+ end
14
+
15
+ # Intersection (except) operation
16
+ # @param other_queryset [QuerySet] Other QuerySet
17
+ # @return [Babik::QuerySet::Intersect] Intersection set operation between this queryset and the other queryset.
18
+ def intersection(other_queryset)
19
+ Babik::QuerySet::Intersect.new(self.model, self, other_queryset)
20
+ end
21
+
22
+ # Union operation
23
+ # @param other_queryset [QuerySet] Other QuerySet
24
+ # @return [Babik::QuerySet::Union] Union set operation between this queryset and the other queryset.
25
+ def union(other_queryset)
26
+ Babik::QuerySet::Union.new(self.model, self, other_queryset)
27
+ end
28
+
29
+ end
30
+ end
31
+ end
32
+
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Babik
4
+ module QuerySet
5
+ # Sort functionality of QuerySet
6
+ module Sortable
7
+
8
+ # Sort QuerySet according to an order
9
+ # @param order [Array, String, Hash] ordering that will be applied to the QuerySet.
10
+ # See {Babik::QuerySet::Order#order_by}.
11
+ # @return [QuerySet] reference to this QuerySet.
12
+ def order_by!(*order)
13
+ @_order = Babik::QuerySet::Order.new(@model, *order)
14
+ self
15
+ end
16
+
17
+ # Remove the order on this QuerySet according to an order
18
+ # @return [QuerySet] reference to this QuerySet.
19
+ def disorder!
20
+ @_order = nil
21
+ self
22
+ end
23
+
24
+ # Alias for order_by
25
+ # @see #order_by
26
+ # @param order [Array, String, Hash] ordering that will be applied to the QuerySet.
27
+ # @return [QuerySet] reference to this QuerySet.
28
+ def order!(*order)
29
+ order_by!(*order)
30
+ end
31
+
32
+ # Invert the order
33
+ # e.g.
34
+ # first_name ASC, last_name ASC, created_at DESC => invert => first_name DESC, last_name DESC, created_at ASC
35
+ # @return [QuerySet] reference to this QuerySet.
36
+ def invert_order!
37
+ @_order.invert!
38
+ self
39
+ end
40
+
41
+ # Inform if there is an order for this QuerySet
42
+ # @return [Boolean] True if this QuerySet is ordered, false otherwise.
43
+ def ordered?
44
+ return true if @_order
45
+ false
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Babik
4
+ module QuerySet
5
+ # Enumerable functionality for QuerySet
6
+ module SQLRenderizable
7
+
8
+ # Get the SQL renderer for this QuerySet.
9
+ # @return [QuerySet] SQL Renderer for this QuerySet.
10
+ def sql
11
+ renderer = SQLRenderer.new(self)
12
+ renderer
13
+ end
14
+
15
+ end
16
+ end
17
+ end