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.
- checksums.yaml +7 -0
- data/Gemfile +16 -0
- data/README.md +718 -0
- data/Rakefile +18 -0
- data/lib/babik.rb +122 -0
- data/lib/babik/database.rb +16 -0
- data/lib/babik/queryset.rb +154 -0
- data/lib/babik/queryset/components/aggregation.rb +172 -0
- data/lib/babik/queryset/components/limit.rb +22 -0
- data/lib/babik/queryset/components/order.rb +161 -0
- data/lib/babik/queryset/components/projection.rb +118 -0
- data/lib/babik/queryset/components/select_related.rb +78 -0
- data/lib/babik/queryset/components/sql_renderer.rb +99 -0
- data/lib/babik/queryset/components/where.rb +43 -0
- data/lib/babik/queryset/lib/association/foreign_association_chain.rb +97 -0
- data/lib/babik/queryset/lib/association/select_related_association_chain.rb +32 -0
- data/lib/babik/queryset/lib/condition.rb +103 -0
- data/lib/babik/queryset/lib/field.rb +34 -0
- data/lib/babik/queryset/lib/join/association_joiner.rb +39 -0
- data/lib/babik/queryset/lib/join/join.rb +86 -0
- data/lib/babik/queryset/lib/selection/config.rb +19 -0
- data/lib/babik/queryset/lib/selection/foreign_selection.rb +39 -0
- data/lib/babik/queryset/lib/selection/local_selection.rb +40 -0
- data/lib/babik/queryset/lib/selection/operation/base.rb +126 -0
- data/lib/babik/queryset/lib/selection/operation/date.rb +178 -0
- data/lib/babik/queryset/lib/selection/operation/operations.rb +201 -0
- data/lib/babik/queryset/lib/selection/operation/regex.rb +58 -0
- data/lib/babik/queryset/lib/selection/path/foreign_path.rb +50 -0
- data/lib/babik/queryset/lib/selection/path/local_path.rb +44 -0
- data/lib/babik/queryset/lib/selection/path/path.rb +23 -0
- data/lib/babik/queryset/lib/selection/select_related_selection.rb +38 -0
- data/lib/babik/queryset/lib/selection/selection.rb +19 -0
- data/lib/babik/queryset/lib/update/assignment.rb +108 -0
- data/lib/babik/queryset/mixins/aggregatable.rb +17 -0
- data/lib/babik/queryset/mixins/bounded.rb +38 -0
- data/lib/babik/queryset/mixins/clonable.rb +52 -0
- data/lib/babik/queryset/mixins/countable.rb +44 -0
- data/lib/babik/queryset/mixins/deletable.rb +13 -0
- data/lib/babik/queryset/mixins/distinguishable.rb +27 -0
- data/lib/babik/queryset/mixins/filterable.rb +51 -0
- data/lib/babik/queryset/mixins/limitable.rb +88 -0
- data/lib/babik/queryset/mixins/lockable.rb +31 -0
- data/lib/babik/queryset/mixins/none.rb +16 -0
- data/lib/babik/queryset/mixins/projectable.rb +34 -0
- data/lib/babik/queryset/mixins/related_selector.rb +28 -0
- data/lib/babik/queryset/mixins/set_operations.rb +32 -0
- data/lib/babik/queryset/mixins/sortable.rb +49 -0
- data/lib/babik/queryset/mixins/sql_renderizable.rb +17 -0
- data/lib/babik/queryset/mixins/updatable.rb +14 -0
- data/lib/babik/queryset/templates/default/delete/main.sql.erb +14 -0
- data/lib/babik/queryset/templates/default/select/components/aggregation.sql.erb +5 -0
- data/lib/babik/queryset/templates/default/select/components/from.sql.erb +16 -0
- data/lib/babik/queryset/templates/default/select/components/from_set.sql.erb +3 -0
- data/lib/babik/queryset/templates/default/select/components/from_table.sql.erb +2 -0
- data/lib/babik/queryset/templates/default/select/components/limit.sql.erb +10 -0
- data/lib/babik/queryset/templates/default/select/components/order_by.sql.erb +9 -0
- data/lib/babik/queryset/templates/default/select/components/projection.sql.erb +7 -0
- data/lib/babik/queryset/templates/default/select/components/select_related.sql.erb +26 -0
- data/lib/babik/queryset/templates/default/select/components/where.sql.erb +39 -0
- data/lib/babik/queryset/templates/default/select/main.sql.erb +42 -0
- data/lib/babik/queryset/templates/default/update/main.sql.erb +15 -0
- data/lib/babik/queryset/templates/mssql/select/components/limit.sql.erb +8 -0
- data/lib/babik/queryset/templates/mssql/select/components/order_by.sql.erb +21 -0
- data/lib/babik/queryset/templates/mysql2/delete/main.sql.erb +15 -0
- data/lib/babik/queryset/templates/mysql2/update/main.sql.erb +18 -0
- data/lib/babik/queryset/templates/sqlite3/select/components/from_set.sql.erb +5 -0
- data/test/config/db/schema.rb +83 -0
- data/test/config/models/bad_post.rb +5 -0
- data/test/config/models/bad_tag.rb +5 -0
- data/test/config/models/category.rb +4 -0
- data/test/config/models/geozone.rb +6 -0
- data/test/config/models/group.rb +5 -0
- data/test/config/models/group_user.rb +5 -0
- data/test/config/models/post.rb +24 -0
- data/test/config/models/post_tag.rb +5 -0
- data/test/config/models/tag.rb +5 -0
- data/test/config/models/user.rb +6 -0
- data/test/delete/delete_test.rb +60 -0
- data/test/delete/foreign_conditions_delete_test.rb +57 -0
- data/test/delete/local_conditions_delete_test.rb +20 -0
- data/test/enable_coverage.rb +17 -0
- data/test/lib/selection/operation/log/test-queries.log +1 -0
- data/test/lib/selection/operation/test_date.rb +131 -0
- data/test/lib/selection/operation/test_regex.rb +55 -0
- data/test/other/clone_test.rb +129 -0
- data/test/other/escape_test.rb +21 -0
- data/test/other/inverse_of_required_test.rb +33 -0
- data/test/select/aggregate_test.rb +151 -0
- data/test/select/bounds_test.rb +46 -0
- data/test/select/count_test.rb +147 -0
- data/test/select/distinct_test.rb +38 -0
- data/test/select/exclude_test.rb +72 -0
- data/test/select/filter_from_object_test.rb +125 -0
- data/test/select/filter_test.rb +207 -0
- data/test/select/for_update_test.rb +19 -0
- data/test/select/foreign_selection_test.rb +60 -0
- data/test/select/get_test.rb +40 -0
- data/test/select/limit_test.rb +109 -0
- data/test/select/local_selection_test.rb +24 -0
- data/test/select/lookup_test.rb +208 -0
- data/test/select/none_test.rb +40 -0
- data/test/select/order_test.rb +165 -0
- data/test/select/project_test.rb +107 -0
- data/test/select/select_related_test.rb +124 -0
- data/test/select/subquery_test.rb +50 -0
- data/test/set_operations/basic_usage_test.rb +121 -0
- data/test/test_helper.rb +55 -0
- data/test/update/update_test.rb +93 -0
- metadata +278 -0
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Babik
|
4
|
+
# QuerySet module
|
5
|
+
module QuerySet
|
6
|
+
# Manages the limit of the QuerySet
|
7
|
+
class Limit
|
8
|
+
attr_reader :size, :offset
|
9
|
+
|
10
|
+
# Construct a limit for QuerySet
|
11
|
+
# @param size [Integer] Size to be selected
|
12
|
+
# @param offset [Integer] Offset from the selection will begin. By default is 0.
|
13
|
+
def initialize(size, offset = 0)
|
14
|
+
@size = size.to_i
|
15
|
+
@offset = offset.to_i
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
|
@@ -0,0 +1,161 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'babik/queryset/lib/selection/selection'
|
4
|
+
|
5
|
+
module Babik
|
6
|
+
# QuerySet module
|
7
|
+
module QuerySet
|
8
|
+
# Manages the order of the QuerySet
|
9
|
+
class Order
|
10
|
+
|
11
|
+
attr_reader :order_fields
|
12
|
+
|
13
|
+
# Construct the order manager
|
14
|
+
# @param model [ActiveRecord::Base] base model.
|
15
|
+
# @param ordering [Array, String, Hash] ordering that will be applied to the QuerySet.
|
16
|
+
# Each item of the array represents an order:
|
17
|
+
# - field1: field1 ASC
|
18
|
+
# - -field1: field1 DESC
|
19
|
+
# - [field1, :ASC]: field1 ASC
|
20
|
+
# - [field1, :DESC]: field1 DESC
|
21
|
+
# - {field1, :ASC}: field1 ASC
|
22
|
+
# - {field1, :DESC}: field1 DESC
|
23
|
+
# @raise [RuntimeError] Invalid type of order
|
24
|
+
def initialize(model, *ordering)
|
25
|
+
@model = model
|
26
|
+
# Convert the types of each order field
|
27
|
+
order_as_array_or_pairs = ordering.map do |order|
|
28
|
+
if [Hash, String, Symbol].include?(order.class)
|
29
|
+
self.send("_order_from_#{order.class.to_s.downcase}", order)
|
30
|
+
elsif order.class == Array
|
31
|
+
order
|
32
|
+
else
|
33
|
+
raise "Invalid type of order: #{order}"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
_initialize_field_orders(order_as_array_or_pairs)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Get order from string
|
40
|
+
# @param order [String] The string of the form 'field1'
|
41
|
+
# @api private
|
42
|
+
# @return [Array] Conversion of order as string to array.
|
43
|
+
def _order_from_string(order)
|
44
|
+
return [order, :ASC] if order[0] != '-'
|
45
|
+
[order[1..-1], :DESC]
|
46
|
+
end
|
47
|
+
|
48
|
+
# Get order from symbol
|
49
|
+
# @param order [Symbol] The symbol of the form :field1
|
50
|
+
# @api private
|
51
|
+
# @return [Array] Conversion of order as symbol to array.
|
52
|
+
def _order_from_symbol(order)
|
53
|
+
_order_from_string(order.to_s)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Get order from a hash
|
57
|
+
# @param order [Hash] The string of the form <field>: <ORD> (where <ORD> is :ASC or :DESC)
|
58
|
+
# @return [Array] Conversion of order as hash to array.
|
59
|
+
def _order_from_hash(order)
|
60
|
+
raise "More than one key found in order by for class #{self.class}" if order.keys.length > 1
|
61
|
+
order_field = order.keys[0]
|
62
|
+
order_value = order[order_field]
|
63
|
+
[order_field, order_value]
|
64
|
+
end
|
65
|
+
|
66
|
+
# Initialize the order paths
|
67
|
+
# @api private
|
68
|
+
# @return [Array] Conversion of order as hash to array.
|
69
|
+
def _initialize_field_orders(order)
|
70
|
+
# Check
|
71
|
+
@order_fields = []
|
72
|
+
order.each_with_index do |order_field_direction, _order_field_index|
|
73
|
+
order_field_path = order_field_direction[0]
|
74
|
+
order_direction = order_field_direction[1]
|
75
|
+
@order_fields << OrderField.new(@model, order_field_path, order_direction)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Return an direction inversion of this order
|
80
|
+
# e.g.
|
81
|
+
# User, first_name, ASC => invert => User, first_name, DESC
|
82
|
+
# @return [Array<OrderField>] Inverted order.
|
83
|
+
def invert
|
84
|
+
@order_fields.map(&:invert)
|
85
|
+
end
|
86
|
+
|
87
|
+
# Invert actual order direction
|
88
|
+
def invert!
|
89
|
+
@order_fields = self.invert
|
90
|
+
end
|
91
|
+
|
92
|
+
# Return the left joins this order include, grouped by alias
|
93
|
+
# @return [Hash] Hash with the key equal to alias and the value equals to a Join.
|
94
|
+
def left_joins_by_alias
|
95
|
+
left_joins_by_alias = {}
|
96
|
+
@order_fields.each do |order_field|
|
97
|
+
left_joins_by_alias.merge!(order_field.left_joins_by_alias)
|
98
|
+
end
|
99
|
+
left_joins_by_alias
|
100
|
+
end
|
101
|
+
|
102
|
+
# Return sql of the fields to order.
|
103
|
+
# Does not include ORDER BY.
|
104
|
+
# @return [SQL] SQL code for fields to order.
|
105
|
+
def sql
|
106
|
+
@order_fields.map(&:sql).join(', ')
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# Each one of the fields that appear in the order statement
|
111
|
+
class OrderField
|
112
|
+
attr_reader :selection, :direction, :model
|
113
|
+
|
114
|
+
delegate :left_joins_by_alias, to: :selection
|
115
|
+
|
116
|
+
# Construct the OrderField
|
117
|
+
# @param model [ActiveRecord::Base] base model.
|
118
|
+
# @param field_path [String, Symbol, Selection] field path. If local, it will be one of the attributes,
|
119
|
+
# otherwise will be an association path.
|
120
|
+
# @param direction [String, Symbol] :ASC or :DESC (a string will be converted to symbol).
|
121
|
+
def initialize(model, field_path, direction)
|
122
|
+
direction_sym = direction.to_sym
|
123
|
+
unless %i[ASC DESC].include?(direction_sym)
|
124
|
+
raise "Invalid order type #{direction} in #{field_path}: Expecting :ASC or :DESC"
|
125
|
+
end
|
126
|
+
@model = model
|
127
|
+
if [String, Symbol].include?(field_path.class)
|
128
|
+
@selection = Babik::Selection::Path::Factory.build(@model, field_path)
|
129
|
+
elsif [Babik::Selection::Path::LocalPath, Babik::Selection::Path::ForeignPath].include?(field_path.class)
|
130
|
+
@selection = field_path
|
131
|
+
else
|
132
|
+
raise "field_path of class #{field_path.class} not valid. A Symbol/String/Babik::Selection::Base expected"
|
133
|
+
end
|
134
|
+
@direction = direction_sym
|
135
|
+
end
|
136
|
+
|
137
|
+
# Return a new OrderField with the direction inverted
|
138
|
+
# @return [OrderField] Order field with inverted direction.
|
139
|
+
def invert
|
140
|
+
inverted_direction = if @direction.to_sym == :ASC
|
141
|
+
:DESC
|
142
|
+
else
|
143
|
+
:ASC
|
144
|
+
end
|
145
|
+
OrderField.new(@model, @selection, inverted_direction)
|
146
|
+
end
|
147
|
+
|
148
|
+
# Return sql of the field to order.
|
149
|
+
# i.e. something like this:
|
150
|
+
# <table_alias>.<field> ASC
|
151
|
+
# <table_alias>.<field> DESC
|
152
|
+
# e.g.
|
153
|
+
# users_0.first_name ASC
|
154
|
+
# posts_0.title DESC
|
155
|
+
# @return [SQL] SQL code for field to order.
|
156
|
+
def sql
|
157
|
+
"#{@selection.target_alias}.#{@selection.selected_field} #{@direction}"
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'babik/queryset/lib/selection/path/path'
|
4
|
+
|
5
|
+
module Babik
|
6
|
+
# QuerySet module
|
7
|
+
module QuerySet
|
8
|
+
|
9
|
+
# Manages the projection of a SELECT QuerySet
|
10
|
+
class Projection
|
11
|
+
|
12
|
+
# Constructs a projection
|
13
|
+
# @param model [ActiveRecord::Base] model the projection is based on.
|
14
|
+
# @param fields [Array] array of fields that will be projected.
|
15
|
+
def initialize(model, fields)
|
16
|
+
@fields = []
|
17
|
+
@fields_hash = {}
|
18
|
+
fields.each do |field|
|
19
|
+
new_field = ProjectedField.new(model, field)
|
20
|
+
@fields << new_field
|
21
|
+
@fields_hash[new_field.alias.to_sym] = new_field
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def apply_transforms(result_set)
|
26
|
+
result_set.map do |record|
|
27
|
+
record.symbolize_keys!
|
28
|
+
transformed_record = {}
|
29
|
+
record.each do |field, value|
|
30
|
+
transform = @fields_hash[field].transform
|
31
|
+
transformed_record[field] = if transform
|
32
|
+
transform.call(value)
|
33
|
+
else
|
34
|
+
value
|
35
|
+
end
|
36
|
+
end
|
37
|
+
transformed_record
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Return sql of the fields to project.
|
42
|
+
# Does not include SELECT.
|
43
|
+
# @return [SQL] SQL code for fields to select in SELECT.
|
44
|
+
def sql
|
45
|
+
@fields.map(&:sql).join(', ')
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Each one of the fields that will be returned by SELECT clause
|
50
|
+
class ProjectedField
|
51
|
+
attr_reader :model, :alias, :transform, :selection
|
52
|
+
|
53
|
+
# Construct a projected field from a model and its field.
|
54
|
+
# @param model [ActiveRecord::Base] model whose field will be returned in the SELECT query.
|
55
|
+
# @param field [Array, String]
|
56
|
+
# if Array, it must be [field_name, alias, transform] where
|
57
|
+
# - field_name is the name of the field (the column name). It is mandatory and must be the first
|
58
|
+
# item of the array.
|
59
|
+
# - alias if present, it will be used to name the field instead of its name.
|
60
|
+
# - transform, if present, a lambda function with the transformation each value of that column it must suffer.
|
61
|
+
# e.g.:
|
62
|
+
# [:created_at, :birth_date]
|
63
|
+
# [:stars, ->(stars) { [stars, 5].min } ]
|
64
|
+
# Otherwise, a field of the local table or foreign tables.
|
65
|
+
#
|
66
|
+
def initialize(model, field)
|
67
|
+
@model = model
|
68
|
+
method_name = "initialize_from_#{field.class.to_s.downcase}"
|
69
|
+
unless self.respond_to?(method_name)
|
70
|
+
raise "No other parameter type is permitted in #{self.class}.new than Array, String and Symbol."
|
71
|
+
end
|
72
|
+
self.send(method_name, field)
|
73
|
+
@selection = Babik::Selection::Path::Factory.build(model, @name)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Initialize from Array
|
77
|
+
def initialize_from_array(field)
|
78
|
+
@name = field[0]
|
79
|
+
@alias = @name
|
80
|
+
[1, 2].each do |field_index|
|
81
|
+
next unless field[field_index]
|
82
|
+
field_i = field[field_index]
|
83
|
+
if [Symbol, String].include?(field_i.class)
|
84
|
+
@alias = field_i
|
85
|
+
elsif field_i.class == Proc
|
86
|
+
@transform = field_i
|
87
|
+
else
|
88
|
+
raise "#{self.class}.new only accepts String/Symbol or Proc. Passed a #{field_i.class}."
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Initialize from String
|
94
|
+
def initialize_from_string(field)
|
95
|
+
@name = field.to_sym
|
96
|
+
@alias = field.to_sym
|
97
|
+
@transform = nil
|
98
|
+
end
|
99
|
+
|
100
|
+
# Initialize from Symbol
|
101
|
+
def initialize_from_symbol(field)
|
102
|
+
initialize_from_string(field)
|
103
|
+
end
|
104
|
+
|
105
|
+
# Return sql of the field to project.
|
106
|
+
# i.e. something like this:
|
107
|
+
# <table_alias>.<field>
|
108
|
+
# <table_alias>.<field> AS <field_alias>
|
109
|
+
# e.g.
|
110
|
+
# users_0.first_name
|
111
|
+
# posts_0.title AS post_title
|
112
|
+
# @return [SQL] SQL code for field to appear in SELECT.
|
113
|
+
def sql
|
114
|
+
"#{@selection.target_alias}.#{@selection.selected_field} AS #{@alias}"
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'babik/queryset/lib/selection/select_related_selection'
|
4
|
+
|
5
|
+
module Babik
|
6
|
+
# QuerySet module
|
7
|
+
module QuerySet
|
8
|
+
# Delegate object that must deals with all the select_related particularities.
|
9
|
+
class SelectRelated
|
10
|
+
|
11
|
+
attr_reader :model, :associations
|
12
|
+
|
13
|
+
# Creates a new SelectRelated
|
14
|
+
def initialize(model, selection_paths)
|
15
|
+
@model = model
|
16
|
+
@associations = []
|
17
|
+
selection_paths = [selection_paths] if selection_paths.class != Array
|
18
|
+
selection_paths.each do |selection_path|
|
19
|
+
@associations << Babik::Selection::SelectRelatedSelection.new(@model, selection_path)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Return the joins that are needed according to the associated path
|
24
|
+
# @return [Hash{table_alias: String}] Left joins by table alias.
|
25
|
+
def left_joins_by_alias
|
26
|
+
left_joins_by_alias = {}
|
27
|
+
@associations.each do |association|
|
28
|
+
left_joins_by_alias.merge!(association.left_joins_by_alias)
|
29
|
+
end
|
30
|
+
left_joins_by_alias
|
31
|
+
end
|
32
|
+
|
33
|
+
# Return the next object and its related objects
|
34
|
+
# Requires a result set of the query that selects all object attributes and the rest
|
35
|
+
# of attributes of the associated objects.
|
36
|
+
# @param result_set [ResultSet] Result with the query that loads all the required objects
|
37
|
+
# (main and related ones).
|
38
|
+
# @return [ActiveRecord::Base, Hash{selection_path: ActiveRecord::Base}]
|
39
|
+
# Return and object with its associated objects.
|
40
|
+
def all_with_related(result_set)
|
41
|
+
result_set.map do |record|
|
42
|
+
object = instantiate_model_object(record)
|
43
|
+
associated_objects = @associations.map do |association|
|
44
|
+
[association.selection_path, instantiate_associated_object(record, association)]
|
45
|
+
end
|
46
|
+
[object, associated_objects.to_h.symbolize_keys]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
# Construct a model object
|
53
|
+
def instantiate_model_object(record)
|
54
|
+
object = @model.new
|
55
|
+
object.assign_attributes(record.select { |attribute| @model.column_names.include?(attribute) })
|
56
|
+
object
|
57
|
+
end
|
58
|
+
|
59
|
+
# Construct an associated object
|
60
|
+
def instantiate_associated_object(record, association)
|
61
|
+
target_model = association.target_model
|
62
|
+
target_object = target_model.new
|
63
|
+
|
64
|
+
# First, get the attributes that have the desired prefix (the association path)
|
65
|
+
target_attributes_with_prefix = record.select { |attr, value| attr.start_with?("#{association.id}__") }
|
66
|
+
|
67
|
+
# Second, convert it to a hash
|
68
|
+
target_attributes = (target_attributes_with_prefix.map do |attribute, value|
|
69
|
+
[attribute.split('__')[1], value]
|
70
|
+
end).to_h
|
71
|
+
|
72
|
+
# Last, assign it to the associated object
|
73
|
+
target_object.assign_attributes(target_attributes)
|
74
|
+
target_object
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'erb'
|
4
|
+
require 'babik/database'
|
5
|
+
|
6
|
+
module Babik
|
7
|
+
module QuerySet
|
8
|
+
# SQL renderer
|
9
|
+
class SQLRenderer
|
10
|
+
|
11
|
+
attr_reader :queryset
|
12
|
+
|
13
|
+
# Where the SQL templates are
|
14
|
+
TEMPLATE_PATH = "#{__dir__}/../templates"
|
15
|
+
|
16
|
+
# Construct a new SQL rendered for a QuerySet
|
17
|
+
# @param queryset [QuerySet] QuerySet to be rendered.
|
18
|
+
def initialize(queryset)
|
19
|
+
@queryset = queryset
|
20
|
+
end
|
21
|
+
|
22
|
+
# Render the SELECT statement
|
23
|
+
# @return [String] SQL SELECT statement for this QuerySet.
|
24
|
+
def select
|
25
|
+
_render('select/main.sql.erb')
|
26
|
+
end
|
27
|
+
|
28
|
+
# Render the UPDATE statement
|
29
|
+
# @param update_command [Hash{field: value}] Runs the update query.
|
30
|
+
# @return [String] SQL UPDATE statement for this QuerySet.
|
31
|
+
def update(update_command)
|
32
|
+
@queryset.project!(['id'])
|
33
|
+
sql = _render('update/main.sql.erb', {update_command: update_command})
|
34
|
+
@queryset.unproject!
|
35
|
+
sql
|
36
|
+
end
|
37
|
+
|
38
|
+
# Render the DELETE statement
|
39
|
+
# @return [String] SQL DELETE statement for this QuerySet.
|
40
|
+
def delete
|
41
|
+
@queryset.project(['id'])
|
42
|
+
sql = _render('delete/main.sql.erb')
|
43
|
+
@queryset.unproject
|
44
|
+
sql
|
45
|
+
end
|
46
|
+
|
47
|
+
# Return the SQL representation of all joins of the QuerySet
|
48
|
+
# @return [String] A String with all LEFT JOIN statements required for this QuerySet.
|
49
|
+
def left_joins
|
50
|
+
# Join all left joins and return a string with the SQL code
|
51
|
+
@queryset.left_joins_by_alias.values.map(&:sql).join("\n")
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
# Render a file in a path
|
57
|
+
# @api private
|
58
|
+
# @param template_path [String] Relative (to {SQLRenderer::TEMPLATE_PATH}) path of the template file.
|
59
|
+
# @param extra_replacements [Hash] Hash with the replacements. By default is an empty hash.
|
60
|
+
# @return [String] Rendered SQL with QuerySet replacements completed
|
61
|
+
def _render(template_path, extra_replacements = {})
|
62
|
+
render = lambda do |partial_template_path, replacements|
|
63
|
+
replacements[:render] = render
|
64
|
+
_base_render(partial_template_path, **replacements)
|
65
|
+
end
|
66
|
+
replacements = extra_replacements.clone
|
67
|
+
replacements[:queryset] = @queryset
|
68
|
+
replacements[:render] = render
|
69
|
+
_base_render(template_path, **replacements)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Render a file
|
73
|
+
# It first search in the dbms_adapter directory and if the file exists, uses that as template.
|
74
|
+
# Otherwise, load the one placed in the default directory.
|
75
|
+
# @api private
|
76
|
+
# @param template_path [String] Relative (to {SQLRenderer::TEMPLATE_PATH}) path of the template file.
|
77
|
+
# @param replacements [Hash] Hash with the replacements.
|
78
|
+
# @return [String] Rendered SQL with QuerySet replacements completed
|
79
|
+
def _base_render(template_path, replacements)
|
80
|
+
dbms_adapter = _dbms_adapter
|
81
|
+
dbms_adapter_template_path = "#{TEMPLATE_PATH}/#{dbms_adapter}/#{template_path}"
|
82
|
+
template_path = if File.exist?(dbms_adapter_template_path)
|
83
|
+
dbms_adapter_template_path
|
84
|
+
else
|
85
|
+
"#{TEMPLATE_PATH}/default/#{template_path}"
|
86
|
+
end
|
87
|
+
template_content = File.read(template_path)
|
88
|
+
::ERB.new(template_content).result_with_hash(**replacements).gsub(/[ ]+/, ' ').gsub(/[\n ]{2,}/, "\n")
|
89
|
+
end
|
90
|
+
|
91
|
+
# Return the DBMS adapter.
|
92
|
+
# @return [String] DBMS adapter (sqlite3, postgre, mysql, mariadb, oracle or mssql).
|
93
|
+
def _dbms_adapter
|
94
|
+
Babik::Database.config[:adapter]
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|