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,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Common module for Babik library
|
4
|
+
module Babik
|
5
|
+
# QuerySet module
|
6
|
+
module QuerySet
|
7
|
+
# Where conditions
|
8
|
+
class Where
|
9
|
+
|
10
|
+
attr_reader :model, :inclusion_filters, :exclusion_filters
|
11
|
+
|
12
|
+
def initialize(model)
|
13
|
+
@model = model
|
14
|
+
@inclusion_filters = []
|
15
|
+
@exclusion_filters = []
|
16
|
+
end
|
17
|
+
|
18
|
+
def exclusion_filters?
|
19
|
+
@exclusion_filters.length.positive?
|
20
|
+
end
|
21
|
+
|
22
|
+
def inclusion_filters?
|
23
|
+
@inclusion_filters.length.positive?
|
24
|
+
end
|
25
|
+
|
26
|
+
def add_exclusion_filter(filter)
|
27
|
+
@exclusion_filters << Babik::QuerySet::Condition.factory(@model, filter)
|
28
|
+
end
|
29
|
+
|
30
|
+
def add_inclusion_filter(filter)
|
31
|
+
@inclusion_filters << Babik::QuerySet::Condition.factory(@model, filter)
|
32
|
+
end
|
33
|
+
|
34
|
+
def left_joins_by_alias
|
35
|
+
left_joins_by_alias = {}
|
36
|
+
[@inclusion_filters, @exclusion_filters].flatten.each do |filter|
|
37
|
+
left_joins_by_alias.merge!(filter.left_joins_by_alias)
|
38
|
+
end
|
39
|
+
left_joins_by_alias
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Babik
|
4
|
+
module Association
|
5
|
+
|
6
|
+
# Association chain for association paths
|
7
|
+
# An association chain is a chain of associations
|
8
|
+
# where the target model of association i is the origin model of association i + 1
|
9
|
+
# Remember, an association path is of the form: zone::parent_zone, category::posts::tags
|
10
|
+
class ForeignAssociationChain
|
11
|
+
|
12
|
+
attr_reader :model, :associations, :target_model, :selection_path
|
13
|
+
|
14
|
+
# Construct the association chain
|
15
|
+
# @param model [ActiveRecord::Base] origin model
|
16
|
+
# @param association_path [Array] association path as an array.
|
17
|
+
# @param selection_path [String, Symbol] selection path used only to raise errors.
|
18
|
+
def initialize(model, association_path, selection_path)
|
19
|
+
@model = model
|
20
|
+
@association_path = association_path
|
21
|
+
@selection_path = selection_path
|
22
|
+
_init_associations
|
23
|
+
end
|
24
|
+
|
25
|
+
# Init associations
|
26
|
+
def _init_associations
|
27
|
+
@associations = []
|
28
|
+
associated_model_i = @model
|
29
|
+
@association_path.each do |association_i_name|
|
30
|
+
associated_model_i = _init_association(associated_model_i, association_i_name)
|
31
|
+
end
|
32
|
+
@target_model = associated_model_i
|
33
|
+
end
|
34
|
+
|
35
|
+
# Initialize association by name
|
36
|
+
# @param model [ActiveRecord::Base] origin model of the association association_name
|
37
|
+
# @param association_name [String, Symbol] association name.
|
38
|
+
# @return [ActiveRecord::Base] target model of ith association.
|
39
|
+
def _init_association(model, association_name)
|
40
|
+
association = _assert_association(model, association_name)
|
41
|
+
_association_pass(association)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Each one of the asssociation
|
45
|
+
# @param association_i [AssociationReflection] ith association.
|
46
|
+
# @return [ActiveRecord::Base] target model of ith association.
|
47
|
+
def _association_pass(association_i)
|
48
|
+
# To one relationship
|
49
|
+
if association_i.belongs_to? || association_i.has_one?
|
50
|
+
@associations << association_i
|
51
|
+
return association_i.klass
|
52
|
+
end
|
53
|
+
|
54
|
+
# Many-to-many with through relationship
|
55
|
+
# The has-and-belongs-to-many relationships have been detected and filtered in _construct_association
|
56
|
+
|
57
|
+
# Add model-through association (active_record -> klass)
|
58
|
+
if association_i.through_reflection
|
59
|
+
@associations << association_i.through_reflection
|
60
|
+
# Add through-target association (through -> target)
|
61
|
+
target_name = association_i.source_reflection_name
|
62
|
+
through_model = association_i.through_reflection.klass
|
63
|
+
through_target_association = through_model.reflect_on_association(target_name)
|
64
|
+
@associations << through_target_association
|
65
|
+
# The next association comes from target model
|
66
|
+
return through_target_association.klass
|
67
|
+
end
|
68
|
+
|
69
|
+
# Add direct has_many association
|
70
|
+
@associations << association_i
|
71
|
+
Object.const_get(association_i.class_name)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Return an association or raise an exception if is not an allowed association
|
75
|
+
# @return [Association] Association of model
|
76
|
+
def _assert_association(association_model, association_name)
|
77
|
+
association = association_model.reflect_on_association(association_name.to_sym)
|
78
|
+
|
79
|
+
# Check the association exists
|
80
|
+
unless association
|
81
|
+
raise "Bad selection path: #{@selection_path}. #{association_name} not found "\
|
82
|
+
"in model #{association_model} when filtering #{@model} objects"
|
83
|
+
end
|
84
|
+
|
85
|
+
# Check the association is no a has-and belongs-to-many
|
86
|
+
# These associations are discouraged by Rails Community
|
87
|
+
if association.class == ActiveRecord::Reflection::HasAndBelongsToManyReflection
|
88
|
+
raise "Relationship #{association.name} is has_and_belongs_to_many. Convert it to has_many-through"
|
89
|
+
end
|
90
|
+
|
91
|
+
# Valid association
|
92
|
+
association
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'babik/queryset/lib/association/foreign_association_chain'
|
4
|
+
|
5
|
+
module Babik
|
6
|
+
module Association
|
7
|
+
|
8
|
+
# Association chain for association paths
|
9
|
+
# An association chain is a chain of associations
|
10
|
+
# where the target model of association i is the origin model of association i + 1
|
11
|
+
# Remember, an association path is of the form: zone::parent_zone, category::posts::tags
|
12
|
+
class SelectRelatedAssociationChain < ForeignAssociationChain
|
13
|
+
|
14
|
+
# Each one of the association
|
15
|
+
# @param association_i [AssociationReflection] ith association.
|
16
|
+
# @return [ActiveRecord::Base] target model of ith association.
|
17
|
+
def _association_pass(association_i)
|
18
|
+
# To one relationship
|
19
|
+
if association_i.belongs_to? || association_i.has_one?
|
20
|
+
@associations << association_i
|
21
|
+
associated_model_i = association_i.klass
|
22
|
+
@target_model = associated_model_i
|
23
|
+
return @target_model
|
24
|
+
end
|
25
|
+
raise "Bad association path: #{association_i.name} in model #{association_i.klass} " \
|
26
|
+
"is not belongs_to or has_one when constructing select_related for #{@model} objects"
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Babik
|
4
|
+
module QuerySet
|
5
|
+
# Each one of the conditions that can appear in a SQL WHERE.
|
6
|
+
module Condition
|
7
|
+
|
8
|
+
# Return the Disjunction or Conjunction according to what class the filters parameter is.
|
9
|
+
# @param model [ActiveRecord::Base] Model owner of this condition.
|
10
|
+
# @param filter [Array, Hash] if it is an Array, it would be a disjunction.
|
11
|
+
# If a Hash, it would be a conjunction.
|
12
|
+
# @raise [RuntimeError] if the class of filters is not an Array or a Hash.
|
13
|
+
def self.factory(model, filter)
|
14
|
+
if filter.class == Array
|
15
|
+
return Disjunction.new(model, filter.map { |filter_i| Conjunction.new(model, filter_i) })
|
16
|
+
end
|
17
|
+
if filter.class == Hash
|
18
|
+
return Conjunction.new(model, filter)
|
19
|
+
end
|
20
|
+
raise '`filter\' parameter must be an Array for OR-based AND-conditions or a hash for a lone AND-condition'
|
21
|
+
end
|
22
|
+
|
23
|
+
# AND-based condition, also known as conjunction
|
24
|
+
class Conjunction
|
25
|
+
attr_reader :model, :selections
|
26
|
+
|
27
|
+
# Construct a conjunction condition.
|
28
|
+
# @param model [ActiveRecord::Base] Model owner of this condition.
|
29
|
+
# @param filter [Hash] a hash where the key identify field paths and the values the values they must take.
|
30
|
+
def initialize(model, filter)
|
31
|
+
@model = model
|
32
|
+
@selections = []
|
33
|
+
# filter is a Hash composed by :selection_path => value
|
34
|
+
filter.each do |selection_path, value|
|
35
|
+
@selections << Babik::Selection::Base.factory(@model, selection_path, value)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Return a hash with the joins grouped by alias
|
40
|
+
# @return [Hash] alias: SQL::Join object
|
41
|
+
def left_joins_by_alias
|
42
|
+
left_joins_by_alias_ = {}
|
43
|
+
@selections.each do |selection|
|
44
|
+
left_joins_by_alias_.merge!(selection.left_joins_by_alias)
|
45
|
+
end
|
46
|
+
left_joins_by_alias_
|
47
|
+
end
|
48
|
+
|
49
|
+
# Return SQL code for this conjunction.
|
50
|
+
# e.g
|
51
|
+
# (first_name = 'Julius' AND last_name = 'Caesar' AND zone = 'Rome')
|
52
|
+
# @return [String] SQL code that will be used in the WHERE part of SQL SELECT statements.
|
53
|
+
def sql
|
54
|
+
@selections.map(&:sql_where_condition).join(" AND\n")
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
# Disjunction in Disjunctive Normal Form
|
61
|
+
# i.e OR-based condition of AND-based conditions (disjunction of conjunctions)
|
62
|
+
#
|
63
|
+
# See https://en.wikipedia.org/wiki/Disjunctive_normal_form
|
64
|
+
#
|
65
|
+
# e.g.
|
66
|
+
# (users.filter_name = 'Julius' AND posts.title = 'Stabbed to death: My story') OR
|
67
|
+
# (users.filter_name = 'Marcus Antonius' AND posts.title = 'A sword in my belly button')
|
68
|
+
#
|
69
|
+
class Disjunction
|
70
|
+
attr_reader :model, :conjunctions
|
71
|
+
|
72
|
+
# Construct a conjunction condition.
|
73
|
+
# @param model [ActiveRecord::Base] Model owner of this condition.
|
74
|
+
# @param conjunctions [Array] array of conjunctions that will be
|
75
|
+
# joined in a disjunction (hence the name Disjunctive Normal Form).
|
76
|
+
def initialize(model, conjunctions)
|
77
|
+
@model = model
|
78
|
+
@conjunctions = conjunctions
|
79
|
+
end
|
80
|
+
|
81
|
+
# Return a hash with the joins grouped by alias
|
82
|
+
# @return [Hash] alias: SQL::Join object
|
83
|
+
def left_joins_by_alias
|
84
|
+
left_joins_by_alias_ = {}
|
85
|
+
@conjunctions.each do |conjunction|
|
86
|
+
left_joins_by_alias_.merge!(conjunction.left_joins_by_alias)
|
87
|
+
end
|
88
|
+
left_joins_by_alias_
|
89
|
+
end
|
90
|
+
|
91
|
+
# Return SQL code for this disjunction.
|
92
|
+
# e.g
|
93
|
+
# (first_name = 'Julius' AND last_name = 'Caesar') OR (zone.name = 'Rome')
|
94
|
+
# @return [String] SQL code that will be used in the WHERE part of SQL SELECT statements.
|
95
|
+
def sql
|
96
|
+
"(\n#{@conjunctions.map(&:sql).join(" OR\n")}\n)"
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Common module for Babik library
|
4
|
+
module Babik
|
5
|
+
module Table
|
6
|
+
# Field module
|
7
|
+
# abstracts the concept of table field according to some useful conversions
|
8
|
+
class Field
|
9
|
+
|
10
|
+
# Create an actual field for a model.
|
11
|
+
# @param model [ActiveRecord::Base] model this field belongs to.
|
12
|
+
# @param field [String] field model that could need the conversion.
|
13
|
+
def initialize(model, field)
|
14
|
+
@model = model
|
15
|
+
@field = field
|
16
|
+
end
|
17
|
+
|
18
|
+
# Check if the field requires some conversion and if that's the case, return the converted final field
|
19
|
+
# If the field is a name of an association, it will be converted to the foreign entity id
|
20
|
+
# @return [String] Actual name of the field that will be used in the SQL.
|
21
|
+
def real_field
|
22
|
+
# If the selected field is a local attribute return the condition as-is (that's the most usual case)
|
23
|
+
is_local_attribute = @model.column_names.include?(@field.to_s)
|
24
|
+
return @field if is_local_attribute
|
25
|
+
# If the selected field is the name of an association, convert it to be a right condition
|
26
|
+
association = @model.reflect_on_association(@field.to_sym)
|
27
|
+
# Only if the association is belongs to, the other associations will be checked by foreign filter method
|
28
|
+
return association.foreign_key if association && association.belongs_to?
|
29
|
+
# Field that is not present in the model
|
30
|
+
raise "Unrecognized field #{@field} for model #{@model} in filter/exclude"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Babik
|
4
|
+
module QuerySet
|
5
|
+
# Join between two tables
|
6
|
+
module Join
|
7
|
+
# Class whose mission is to catch an association path and construct all the sequence of LEFT JOINS
|
8
|
+
# that lies behind this set of associations.
|
9
|
+
class AssociationJoiner
|
10
|
+
attr_reader :left_joins_by_alias, :target_alias
|
11
|
+
|
12
|
+
# Read an Array of associations an construct a list of joins.
|
13
|
+
# @param associations [Array<ActiveRecord::Associations::Association>] Array of associations.
|
14
|
+
def initialize(associations)
|
15
|
+
@left_joins_by_alias = {}
|
16
|
+
last_table_alias = nil
|
17
|
+
associations.each_with_index do |association, association_path_index|
|
18
|
+
# Important, this variable will take the last joined table to chain the join, in the first loop, will take
|
19
|
+
# the association origin table name.
|
20
|
+
last_table_alias ||= association.active_record.table_name
|
21
|
+
left_join = Babik::QuerySet::Join.new_from_association(association, association_path_index, last_table_alias)
|
22
|
+
|
23
|
+
@left_joins_by_alias[left_join.target_alias] = left_join
|
24
|
+
last_table_alias = left_join.target_alias
|
25
|
+
end
|
26
|
+
@target_alias = last_table_alias
|
27
|
+
end
|
28
|
+
|
29
|
+
# Table alias will be another way of calling target alias,
|
30
|
+
# i.e. the alias of the target table in the join,
|
31
|
+
# i.e. the alias of the last table in the join,
|
32
|
+
# @return [String] Target table alias
|
33
|
+
def table_alias
|
34
|
+
@target_alias
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Babik
|
4
|
+
module QuerySet
|
5
|
+
# Join between two tables
|
6
|
+
module Join
|
7
|
+
|
8
|
+
# Construct a new Join from an association
|
9
|
+
# @param association Association between two ActiveRecord::Base objects.
|
10
|
+
# @param association_position Association position. Used when the relationship is a many-to-many through.
|
11
|
+
# @param origin_table_alias Alias of table that is the origin of the join.
|
12
|
+
# @param join [LeftJoin] Join class.
|
13
|
+
# @return [LeftJoin] object with the join for this association.
|
14
|
+
def self.new_from_association(association, association_position, origin_table_alias, join = LeftJoin)
|
15
|
+
owner_table = association.active_record.table_name
|
16
|
+
target_table_alias = "#{owner_table}__#{association.name}_#{association_position}"
|
17
|
+
join_keys = association.join_keys
|
18
|
+
|
19
|
+
target_table = TargetTable.new(association.table_name, target_table_alias, join_keys.key)
|
20
|
+
origin_table = OriginTable.new(origin_table_alias, join_keys.foreign_key)
|
21
|
+
|
22
|
+
join.new(target_table, origin_table)
|
23
|
+
end
|
24
|
+
|
25
|
+
# A table join
|
26
|
+
class AbstractJoin
|
27
|
+
attr_reader :target_table, :origin_table, :sql
|
28
|
+
|
29
|
+
# Construct a Join
|
30
|
+
# @param target_table [Babik::QuerySet::Join::TargetTable] target table of the join.
|
31
|
+
# @param origin_table [Babik::QuerySet::Join::OriginTable] origin table of the join.
|
32
|
+
def initialize(target_table, origin_table)
|
33
|
+
@target_table = target_table
|
34
|
+
@origin_table = origin_table
|
35
|
+
_init_sql
|
36
|
+
end
|
37
|
+
|
38
|
+
# Initialize SQL of the JOIN
|
39
|
+
def _init_sql
|
40
|
+
# Create the SQL code of the join
|
41
|
+
@sql = %(
|
42
|
+
#{self.class::JOIN_TYPE} JOIN #{@target_table.name} #{@target_table.table_alias}
|
43
|
+
ON #{@target_table.table_alias}.#{@target_table.key} = #{@origin_table.table_alias}.#{@origin_table.key}
|
44
|
+
).delete("\n").gsub(/\s{2,}/, ' ').strip
|
45
|
+
end
|
46
|
+
|
47
|
+
# Return the target table alias.
|
48
|
+
# @return [String] Target table alias.
|
49
|
+
def target_alias
|
50
|
+
@target_table.table_alias
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Left join between tables
|
55
|
+
class LeftJoin < AbstractJoin
|
56
|
+
JOIN_TYPE = 'LEFT'
|
57
|
+
end
|
58
|
+
|
59
|
+
# Target table of the join
|
60
|
+
class TargetTable
|
61
|
+
attr_reader :name, :table_alias, :key
|
62
|
+
# Constructor
|
63
|
+
# @param name [String] target table name
|
64
|
+
# @param table_alias [String] target table alias
|
65
|
+
# @param key [String] field that serves as key in the target table.
|
66
|
+
def initialize(name, table_alias, key)
|
67
|
+
@name = name
|
68
|
+
@table_alias = table_alias
|
69
|
+
@key = key
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Origin table of the join
|
74
|
+
class OriginTable
|
75
|
+
attr_reader :table_alias, :key
|
76
|
+
# Constructor
|
77
|
+
# @param table_alias [String] origin table alias
|
78
|
+
# @param key [String] field that serves as key.
|
79
|
+
def initialize(table_alias, key)
|
80
|
+
@table_alias = table_alias
|
81
|
+
@key = key
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Babik
|
4
|
+
module Selection
|
5
|
+
# Selection configuration
|
6
|
+
class Config
|
7
|
+
# Relationship separator string
|
8
|
+
# e.g.
|
9
|
+
# - author::first_name__iregex where author is the association, first_name is the field and iregex the operator
|
10
|
+
# - posts::tags::name__contains where posts is related with tags and (tag) name is the field
|
11
|
+
# and contains the operator.
|
12
|
+
RELATIONSHIP_SEPARATOR = '::'
|
13
|
+
|
14
|
+
# Operator separator string
|
15
|
+
# e.g. first_name__iregex where first_name is the field an iregex the operator
|
16
|
+
OPERATOR_SEPARATOR = '__'
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|