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,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