nose 0.1.0pre
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/nose/backend/cassandra.rb +390 -0
- data/lib/nose/backend/file.rb +185 -0
- data/lib/nose/backend/mongo.rb +242 -0
- data/lib/nose/backend.rb +557 -0
- data/lib/nose/cost/cassandra.rb +33 -0
- data/lib/nose/cost/entity_count.rb +27 -0
- data/lib/nose/cost/field_size.rb +31 -0
- data/lib/nose/cost/request_count.rb +32 -0
- data/lib/nose/cost.rb +68 -0
- data/lib/nose/debug.rb +45 -0
- data/lib/nose/enumerator.rb +199 -0
- data/lib/nose/indexes.rb +239 -0
- data/lib/nose/loader/csv.rb +99 -0
- data/lib/nose/loader/mysql.rb +199 -0
- data/lib/nose/loader/random.rb +48 -0
- data/lib/nose/loader/sql.rb +105 -0
- data/lib/nose/loader.rb +38 -0
- data/lib/nose/model/entity.rb +136 -0
- data/lib/nose/model/fields.rb +293 -0
- data/lib/nose/model.rb +113 -0
- data/lib/nose/parser.rb +202 -0
- data/lib/nose/plans/execution_plan.rb +282 -0
- data/lib/nose/plans/filter.rb +99 -0
- data/lib/nose/plans/index_lookup.rb +302 -0
- data/lib/nose/plans/limit.rb +42 -0
- data/lib/nose/plans/query_planner.rb +361 -0
- data/lib/nose/plans/sort.rb +49 -0
- data/lib/nose/plans/update.rb +60 -0
- data/lib/nose/plans/update_planner.rb +270 -0
- data/lib/nose/plans.rb +135 -0
- data/lib/nose/proxy/mysql.rb +275 -0
- data/lib/nose/proxy.rb +102 -0
- data/lib/nose/query_graph.rb +481 -0
- data/lib/nose/random/barbasi_albert.rb +48 -0
- data/lib/nose/random/watts_strogatz.rb +50 -0
- data/lib/nose/random.rb +391 -0
- data/lib/nose/schema.rb +89 -0
- data/lib/nose/search/constraints.rb +143 -0
- data/lib/nose/search/problem.rb +328 -0
- data/lib/nose/search/results.rb +200 -0
- data/lib/nose/search.rb +266 -0
- data/lib/nose/serialize.rb +747 -0
- data/lib/nose/statements/connection.rb +160 -0
- data/lib/nose/statements/delete.rb +83 -0
- data/lib/nose/statements/insert.rb +146 -0
- data/lib/nose/statements/query.rb +161 -0
- data/lib/nose/statements/update.rb +101 -0
- data/lib/nose/statements.rb +645 -0
- data/lib/nose/timing.rb +79 -0
- data/lib/nose/util.rb +305 -0
- data/lib/nose/workload.rb +244 -0
- data/lib/nose.rb +37 -0
- data/templates/workload.erb +42 -0
- metadata +700 -0
@@ -0,0 +1,160 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NoSE
|
4
|
+
# Superclass for connect and disconnect statements
|
5
|
+
class Connection < Statement
|
6
|
+
include StatementSupportQuery
|
7
|
+
|
8
|
+
attr_reader :source_pk, :target, :target_pk, :conditions
|
9
|
+
alias source entity
|
10
|
+
|
11
|
+
def initialize(params, text, group: nil, label: nil)
|
12
|
+
super params, text, group: group, label: label
|
13
|
+
fail InvalidStatementException, 'Incorrect connection initialization' \
|
14
|
+
unless text.split.first == self.class.name.split('::').last.upcase
|
15
|
+
|
16
|
+
populate_conditions params
|
17
|
+
end
|
18
|
+
|
19
|
+
# Build a new disconnect from a provided parse tree
|
20
|
+
# @return [Connection]
|
21
|
+
def self.parse(tree, params, text, group: nil, label: nil)
|
22
|
+
keys_from_tree tree, params
|
23
|
+
|
24
|
+
new params, text, group: group, label: label
|
25
|
+
end
|
26
|
+
|
27
|
+
# @return[void]
|
28
|
+
def self.keys_from_tree(tree, params)
|
29
|
+
params[:source_pk] = tree[:source_pk]
|
30
|
+
params[:target] = params[:entity].foreign_keys[tree[:target].to_s]
|
31
|
+
params[:target_pk] = tree[:target_pk]
|
32
|
+
end
|
33
|
+
|
34
|
+
# Produce the SQL text corresponding to this connection
|
35
|
+
# @return [String]
|
36
|
+
def unparse
|
37
|
+
"CONNECT #{source.name}(\"#{source_pk}\") TO " \
|
38
|
+
"#{target.name}(\"#{target_pk}\")"
|
39
|
+
end
|
40
|
+
|
41
|
+
def ==(other)
|
42
|
+
self.class == other.class &&
|
43
|
+
@graph == other.graph &&
|
44
|
+
@source == other.source &&
|
45
|
+
@target == other.target &&
|
46
|
+
@conditions == other.conditions
|
47
|
+
end
|
48
|
+
alias eql? ==
|
49
|
+
|
50
|
+
def hash
|
51
|
+
@hash ||= [@graph, @source, @target, @conditions].hash
|
52
|
+
end
|
53
|
+
|
54
|
+
# A connection modifies an index if the relationship is in the path
|
55
|
+
def modifies_index?(index)
|
56
|
+
index.path.include?(@target) || index.path.include?(@target.reverse)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Get the support queries for updating an index
|
60
|
+
def support_queries(index)
|
61
|
+
return [] unless modifies_index?(index)
|
62
|
+
|
63
|
+
select = index.all_fields - @conditions.each_value.map(&:field).to_set
|
64
|
+
return [] if select.empty?
|
65
|
+
|
66
|
+
index.graph.split(entity).map do |graph|
|
67
|
+
support_fields = select.select do |field|
|
68
|
+
graph.entities.include? field.parent
|
69
|
+
end.to_set
|
70
|
+
conditions = @conditions.select do |_, c|
|
71
|
+
graph.entities.include? c.field.parent
|
72
|
+
end
|
73
|
+
|
74
|
+
split_entity = split_entity graph, index.graph, entity
|
75
|
+
build_support_query split_entity, index, graph, support_fields,
|
76
|
+
conditions
|
77
|
+
end.compact
|
78
|
+
end
|
79
|
+
|
80
|
+
protected
|
81
|
+
|
82
|
+
# The two key fields are provided with the connection
|
83
|
+
def given_fields
|
84
|
+
[@target.parent.id_field, @target.entity.id_field]
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
# Validate the types of the primary keys
|
90
|
+
# @return [void]
|
91
|
+
def validate_keys
|
92
|
+
# XXX Only works for non-composite PKs
|
93
|
+
source_type = source.id_field.class.const_get 'TYPE'
|
94
|
+
fail TypeError unless source_type.nil? || source_pk.is_a?(type)
|
95
|
+
|
96
|
+
target_type = @target.class.const_get 'TYPE'
|
97
|
+
fail TypeError unless target_type.nil? || target_pk.is_a?(type)
|
98
|
+
end
|
99
|
+
|
100
|
+
# Populate the list of condition objects
|
101
|
+
# @return [void]
|
102
|
+
def populate_conditions(params)
|
103
|
+
@source_pk = params[:source_pk]
|
104
|
+
@target = params[:target]
|
105
|
+
@target_pk = params[:target_pk]
|
106
|
+
|
107
|
+
validate_keys
|
108
|
+
|
109
|
+
# This is needed later when planning updates
|
110
|
+
@eq_fields = [@target.parent.id_field,
|
111
|
+
@target.entity.id_field]
|
112
|
+
|
113
|
+
source_id = source.id_field
|
114
|
+
target_id = @target.entity.id_field
|
115
|
+
@conditions = {
|
116
|
+
source_id.id => Condition.new(source_id, :'=', @source_pk),
|
117
|
+
target_id.id => Condition.new(target_id, :'=', @target_pk)
|
118
|
+
}
|
119
|
+
end
|
120
|
+
|
121
|
+
# Get the where clause for a support query over the given path
|
122
|
+
# @return [String]
|
123
|
+
def support_query_condition_for_path(path, reversed)
|
124
|
+
key = (reversed ? target.entity : target.parent).id_field
|
125
|
+
path = path.reverse if path.entities.last != key.entity
|
126
|
+
eq_key = path.entries[-1]
|
127
|
+
if eq_key.is_a? Fields::ForeignKeyField
|
128
|
+
where = "WHERE #{eq_key.name}.#{eq_key.entity.id_field.name} = ?"
|
129
|
+
else
|
130
|
+
where = "WHERE #{eq_key.parent.name}." \
|
131
|
+
"#{eq_key.parent.id_field.name} = ?"
|
132
|
+
end
|
133
|
+
|
134
|
+
where
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# A representation of a connect in the workload
|
139
|
+
class Connect < Connection
|
140
|
+
# Specifies that connections require insertion
|
141
|
+
def requires_insert?(_index)
|
142
|
+
true
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# A representation of a disconnect in the workload
|
147
|
+
class Disconnect < Connection
|
148
|
+
# Produce the SQL text corresponding to this disconnection
|
149
|
+
# @return [String]
|
150
|
+
def unparse
|
151
|
+
"DISCONNECT #{source.name}(\"#{source_pk}\") FROM " \
|
152
|
+
"#{target.name}(\"#{target_pk}\")"
|
153
|
+
end
|
154
|
+
|
155
|
+
# Specifies that disconnections require deletion
|
156
|
+
def requires_delete?(_index)
|
157
|
+
true
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NoSE
|
4
|
+
# A representation of a delete in the workload
|
5
|
+
class Delete < Statement
|
6
|
+
include StatementConditions
|
7
|
+
include StatementSupportQuery
|
8
|
+
|
9
|
+
def initialize(params, text, group: nil, label: nil)
|
10
|
+
super params, text, group: group, label: label
|
11
|
+
|
12
|
+
populate_conditions params
|
13
|
+
end
|
14
|
+
|
15
|
+
# Build a new delete from a provided parse tree
|
16
|
+
# @return [Delete]
|
17
|
+
def self.parse(tree, params, text, group: nil, label: nil)
|
18
|
+
conditions_from_tree tree, params
|
19
|
+
|
20
|
+
Delete.new params, text, group: group, label: label
|
21
|
+
end
|
22
|
+
|
23
|
+
# Produce the SQL text corresponding to this delete
|
24
|
+
# @return [String]
|
25
|
+
def unparse
|
26
|
+
delete = "DELETE #{entity.name} "
|
27
|
+
delete += "FROM #{from_path @key_path}"
|
28
|
+
delete << where_clause
|
29
|
+
|
30
|
+
delete
|
31
|
+
end
|
32
|
+
|
33
|
+
def ==(other)
|
34
|
+
other.is_a?(Delete) &&
|
35
|
+
@graph == other.graph &&
|
36
|
+
entity == other.entity &&
|
37
|
+
@conditions == other.conditions
|
38
|
+
end
|
39
|
+
alias eql? ==
|
40
|
+
|
41
|
+
def hash
|
42
|
+
@hash ||= [@graph, entity, @conditions].hash
|
43
|
+
end
|
44
|
+
|
45
|
+
# Index contains the entity to be deleted
|
46
|
+
def modifies_index?(index)
|
47
|
+
index.graph.entities.include? entity
|
48
|
+
end
|
49
|
+
|
50
|
+
# Specifies that deletes require deletion
|
51
|
+
def requires_delete?(_index)
|
52
|
+
true
|
53
|
+
end
|
54
|
+
|
55
|
+
# Get the support queries for deleting from an index
|
56
|
+
def support_queries(index)
|
57
|
+
return [] unless modifies_index? index
|
58
|
+
select = (index.hash_fields + index.order_fields.to_set) -
|
59
|
+
@conditions.each_value.map(&:field).to_set
|
60
|
+
return [] if select.empty?
|
61
|
+
|
62
|
+
support_queries = []
|
63
|
+
|
64
|
+
# Build a support query which gets the IDs of the entities being deleted
|
65
|
+
graph = @graph.dup
|
66
|
+
support_fields = select.select do |field|
|
67
|
+
field.parent == entity
|
68
|
+
end.to_set
|
69
|
+
support_fields << entity.id_field \
|
70
|
+
unless @conditions.each_value.map(&:field).include? entity.id_field
|
71
|
+
conditions = Hash[@conditions.map { |k, v| [k.dup, v.dup] }]
|
72
|
+
|
73
|
+
support_queries << build_support_query(entity, index, graph,
|
74
|
+
support_fields, conditions)
|
75
|
+
support_queries.compact + support_queries_for_entity(index, select)
|
76
|
+
end
|
77
|
+
|
78
|
+
# The condition fields are provided with the deletion
|
79
|
+
def given_fields
|
80
|
+
@conditions.each_value.map(&:field)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NoSE
|
4
|
+
# A representation of an insert in the workload
|
5
|
+
class Insert < Statement
|
6
|
+
include StatementConditions
|
7
|
+
include StatementSettings
|
8
|
+
include StatementSupportQuery
|
9
|
+
|
10
|
+
def initialize(params, text, group: nil, label: nil)
|
11
|
+
super params, text, group: group, label: label
|
12
|
+
|
13
|
+
@settings = params[:settings]
|
14
|
+
fail InvalidStatementException, 'Must insert primary key' \
|
15
|
+
unless @settings.map(&:field).include?(entity.id_field)
|
16
|
+
|
17
|
+
populate_conditions params
|
18
|
+
end
|
19
|
+
|
20
|
+
# Build a new insert from a provided parse tree
|
21
|
+
# @return [Insert]
|
22
|
+
def self.parse(tree, params, text, group: nil, label: nil)
|
23
|
+
settings_from_tree tree, params
|
24
|
+
conditions_from_tree tree, params
|
25
|
+
|
26
|
+
Insert.new params, text, group: group, label: label
|
27
|
+
end
|
28
|
+
|
29
|
+
# Extract conditions from a parse tree
|
30
|
+
# @return [Hash]
|
31
|
+
def self.conditions_from_tree(tree, params)
|
32
|
+
connections = tree[:connections] || []
|
33
|
+
connections = connections.map do |connection|
|
34
|
+
field = params[:entity][connection[:target].to_s]
|
35
|
+
value = connection[:target_pk]
|
36
|
+
|
37
|
+
type = field.class.const_get 'TYPE'
|
38
|
+
value = field.class.value_from_string(value.to_s) \
|
39
|
+
unless type.nil? || value.nil?
|
40
|
+
|
41
|
+
connection.delete :value
|
42
|
+
Condition.new field, :'=', value
|
43
|
+
end
|
44
|
+
|
45
|
+
params[:conditions] = Hash[connections.map do |connection|
|
46
|
+
[connection.field.id, connection]
|
47
|
+
end]
|
48
|
+
end
|
49
|
+
private_class_method :conditions_from_tree
|
50
|
+
|
51
|
+
# Produce the SQL text corresponding to this insert
|
52
|
+
# @return [String]
|
53
|
+
def unparse
|
54
|
+
insert = "INSERT INTO #{entity.name} "
|
55
|
+
insert += settings_clause
|
56
|
+
|
57
|
+
insert << ' AND CONNECT TO ' << @conditions.values.map do |condition|
|
58
|
+
value = maybe_quote condition.value, condition.field
|
59
|
+
"#{condition.field.name}(#{value})"
|
60
|
+
end.join(', ') unless @conditions.empty?
|
61
|
+
|
62
|
+
insert
|
63
|
+
end
|
64
|
+
|
65
|
+
def ==(other)
|
66
|
+
other.is_a?(Insert) &&
|
67
|
+
@graph == other.graph &&
|
68
|
+
entity == other.entity &&
|
69
|
+
@settings == other.settings &&
|
70
|
+
@conditions == other.conditions
|
71
|
+
end
|
72
|
+
alias eql? ==
|
73
|
+
|
74
|
+
def hash
|
75
|
+
@hash ||= [@graph, entity, @settings, @conditions].hash
|
76
|
+
end
|
77
|
+
|
78
|
+
# Determine if this insert modifies an index
|
79
|
+
def modifies_index?(index)
|
80
|
+
return true if modifies_single_entity_index?(index)
|
81
|
+
return false if index.graph.size == 1
|
82
|
+
return false unless index.graph.entities.include? entity
|
83
|
+
|
84
|
+
# Check if the index crosses all of the connection keys
|
85
|
+
keys = @conditions.each_value.map(&:field)
|
86
|
+
index.graph.keys_from_entity(entity).all? { |k| keys.include? k }
|
87
|
+
end
|
88
|
+
|
89
|
+
# Specifies that inserts require insertion
|
90
|
+
def requires_insert?(_index)
|
91
|
+
true
|
92
|
+
end
|
93
|
+
|
94
|
+
# Support queries are required for index insertion with connection
|
95
|
+
# to select attributes of the other related entities
|
96
|
+
# @return [Array<SupportQuery>]
|
97
|
+
def support_queries(index)
|
98
|
+
return [] unless modifies_index?(index) &&
|
99
|
+
!modifies_single_entity_index?(index)
|
100
|
+
|
101
|
+
# Get all fields which need to be selected by support queries
|
102
|
+
select = index.all_fields -
|
103
|
+
@settings.map(&:field).to_set -
|
104
|
+
@conditions.each_value.map do |condition|
|
105
|
+
condition.field.entity.id_field
|
106
|
+
end.to_set
|
107
|
+
return [] if select.empty?
|
108
|
+
|
109
|
+
index.graph.split(entity).map do |graph|
|
110
|
+
support_fields = select.select do |field|
|
111
|
+
graph.entities.include? field.parent
|
112
|
+
end.to_set
|
113
|
+
|
114
|
+
# Build conditions by traversing the foreign keys
|
115
|
+
conditions = @conditions.each_value.map do |c|
|
116
|
+
next unless graph.entities.include? c.field.entity
|
117
|
+
|
118
|
+
Condition.new c.field.entity.id_field, c.operator, c.value
|
119
|
+
end.compact
|
120
|
+
conditions = Hash[conditions.map do |condition|
|
121
|
+
[condition.field.id, condition]
|
122
|
+
end]
|
123
|
+
|
124
|
+
split_entity = split_entity graph, index.graph, entity
|
125
|
+
build_support_query split_entity, index, graph, support_fields,
|
126
|
+
conditions
|
127
|
+
end.compact
|
128
|
+
end
|
129
|
+
|
130
|
+
# The settings fields are provided with the insertion
|
131
|
+
def given_fields
|
132
|
+
@settings.map(&:field) + @conditions.each_value.map do |condition|
|
133
|
+
condition.field.entity.id_field
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
private
|
138
|
+
|
139
|
+
# Check if the insert modifies a single entity index
|
140
|
+
# @return [Boolean]
|
141
|
+
def modifies_single_entity_index?(index)
|
142
|
+
!(@settings.map(&:field).to_set & index.all_fields).empty? &&
|
143
|
+
index.graph.size == 1 && index.graph.entities.first == entity
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NoSE
|
4
|
+
# A representation of a query in the workload
|
5
|
+
class Query < Statement
|
6
|
+
include StatementConditions
|
7
|
+
|
8
|
+
attr_reader :select, :order, :limit
|
9
|
+
|
10
|
+
def initialize(params, text, group: nil, label: nil)
|
11
|
+
super params, text, group: group, label: label
|
12
|
+
|
13
|
+
populate_conditions params
|
14
|
+
@select = params[:select]
|
15
|
+
@order = params[:order] || []
|
16
|
+
|
17
|
+
fail InvalidStatementException, 'can\'t order by IDs' \
|
18
|
+
if @order.any? { |f| f.is_a? Fields::IDField }
|
19
|
+
|
20
|
+
if join_order.first != @key_path.entities.first
|
21
|
+
@key_path = @key_path.reverse
|
22
|
+
end
|
23
|
+
|
24
|
+
fail InvalidStatementException, 'must have an equality predicate' \
|
25
|
+
if @conditions.empty? || @conditions.values.all?(&:is_range)
|
26
|
+
|
27
|
+
@limit = params[:limit]
|
28
|
+
end
|
29
|
+
|
30
|
+
# Build a new query from a provided parse tree
|
31
|
+
# @return [Query]
|
32
|
+
def self.parse(tree, params, text, group: nil, label: nil)
|
33
|
+
conditions_from_tree tree, params
|
34
|
+
fields_from_tree tree, params
|
35
|
+
order_from_tree tree, params
|
36
|
+
params[:limit] = tree[:limit].to_i if tree[:limit]
|
37
|
+
|
38
|
+
new params, text, group: group, label: label
|
39
|
+
end
|
40
|
+
|
41
|
+
# Produce the SQL text corresponding to this query
|
42
|
+
# @return [String]
|
43
|
+
def unparse
|
44
|
+
field_namer = -> (f) { field_path f }
|
45
|
+
|
46
|
+
query = 'SELECT ' + @select.map(&field_namer).join(', ')
|
47
|
+
query << " FROM #{from_path @graph.longest_path}"
|
48
|
+
query << where_clause(field_namer)
|
49
|
+
|
50
|
+
query << ' ORDER BY ' << @order.map(&field_namer).join(', ') \
|
51
|
+
unless @order.empty?
|
52
|
+
query << " LIMIT #{@limit}" unless @limit.nil?
|
53
|
+
query << " -- #{@comment}" unless @comment.nil?
|
54
|
+
|
55
|
+
query
|
56
|
+
end
|
57
|
+
|
58
|
+
def ==(other)
|
59
|
+
other.is_a?(Query) &&
|
60
|
+
@graph == other.graph &&
|
61
|
+
@select == other.select &&
|
62
|
+
@conditions == other.conditions &&
|
63
|
+
@order == other.order &&
|
64
|
+
@limit == other.limit &&
|
65
|
+
@comment == other.comment
|
66
|
+
end
|
67
|
+
alias eql? ==
|
68
|
+
|
69
|
+
def hash
|
70
|
+
@hash ||= [@graph, @select, @conditions, @order, @limit, @comment].hash
|
71
|
+
end
|
72
|
+
|
73
|
+
# The order entities should be joined according to the query graph
|
74
|
+
# @return [Array<Entity>]
|
75
|
+
def join_order
|
76
|
+
@graph.join_order(@eq_fields)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Specifies that queries don't modify data
|
80
|
+
def read_only?
|
81
|
+
true
|
82
|
+
end
|
83
|
+
|
84
|
+
# All fields referenced anywhere in the query
|
85
|
+
# @return [Set<Fields::Field>]
|
86
|
+
def all_fields
|
87
|
+
(@select + @conditions.each_value.map(&:field) + @order).to_set
|
88
|
+
end
|
89
|
+
|
90
|
+
# Extract fields to be selected from a parse tree
|
91
|
+
# @return [Set<Field>]
|
92
|
+
def self.fields_from_tree(tree, params)
|
93
|
+
params[:select] = tree[:select].flat_map do |field|
|
94
|
+
if field.last == '*'
|
95
|
+
# Find the entity along the path
|
96
|
+
entity = params[:key_path].entities[tree[:path].index(field.first)]
|
97
|
+
entity.fields.values
|
98
|
+
else
|
99
|
+
field = add_field_with_prefix tree[:path], field, params
|
100
|
+
|
101
|
+
fail InvalidStatementException, 'Foreign keys cannot be selected' \
|
102
|
+
if field.is_a? Fields::ForeignKeyField
|
103
|
+
|
104
|
+
field
|
105
|
+
end
|
106
|
+
end.to_set
|
107
|
+
end
|
108
|
+
private_class_method :fields_from_tree
|
109
|
+
|
110
|
+
# Extract ordering fields from a parse tree
|
111
|
+
# @return [Array<Field>]
|
112
|
+
def self.order_from_tree(tree, params)
|
113
|
+
return params[:order] = [] if tree[:order].nil?
|
114
|
+
|
115
|
+
params[:order] = tree[:order][:fields].each_slice(2).map do |field|
|
116
|
+
field = field.first if field.first.is_a?(Array)
|
117
|
+
add_field_with_prefix tree[:path], field, params
|
118
|
+
end
|
119
|
+
end
|
120
|
+
private_class_method :order_from_tree
|
121
|
+
|
122
|
+
private
|
123
|
+
|
124
|
+
def field_path(field)
|
125
|
+
path = @graph.path_between @graph.longest_path.entities.first,
|
126
|
+
field.parent
|
127
|
+
path = path.drop_while { |k| @graph.longest_path.include? k } << path[-1]
|
128
|
+
path = KeyPath.new(path) unless path.is_a?(KeyPath)
|
129
|
+
|
130
|
+
from_path path, @graph.longest_path, field
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# A query required to support an update
|
135
|
+
class SupportQuery < Query
|
136
|
+
attr_reader :statement, :index, :entity
|
137
|
+
|
138
|
+
def initialize(entity, params, text, group: nil, label: nil)
|
139
|
+
super params, text, group: group, label: label
|
140
|
+
|
141
|
+
@entity = entity
|
142
|
+
end
|
143
|
+
|
144
|
+
# Support queries must also have their statement and index checked
|
145
|
+
def ==(other)
|
146
|
+
other.is_a?(SupportQuery) && @statement == other.statement &&
|
147
|
+
@index == other.index && @comment == other.comment
|
148
|
+
end
|
149
|
+
alias eql? ==
|
150
|
+
|
151
|
+
def hash
|
152
|
+
@hash ||= Zlib.crc32_combine super, @index.hash, @index.hash_str.length
|
153
|
+
end
|
154
|
+
|
155
|
+
# :nocov:
|
156
|
+
def to_color
|
157
|
+
super.to_color + ' for [magenta]' + @index.key + '[/]'
|
158
|
+
end
|
159
|
+
# :nocov:
|
160
|
+
end
|
161
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NoSE
|
4
|
+
# A representation of an update in the workload
|
5
|
+
class Update < Statement
|
6
|
+
include StatementConditions
|
7
|
+
include StatementSettings
|
8
|
+
include StatementSupportQuery
|
9
|
+
|
10
|
+
def initialize(params, text, group: nil, label: nil)
|
11
|
+
super params, text, group: group, label: label
|
12
|
+
|
13
|
+
populate_conditions params
|
14
|
+
@settings = params[:settings]
|
15
|
+
end
|
16
|
+
|
17
|
+
# Build a new update from a provided parse tree
|
18
|
+
# @return [Update]
|
19
|
+
def self.parse(tree, params, text, group: nil, label: nil)
|
20
|
+
conditions_from_tree tree, params
|
21
|
+
settings_from_tree tree, params
|
22
|
+
|
23
|
+
Update.new params, text, group: group, label: label
|
24
|
+
end
|
25
|
+
|
26
|
+
# Produce the SQL text corresponding to this update
|
27
|
+
# @return [String]
|
28
|
+
def unparse
|
29
|
+
update = "UPDATE #{entity.name} "
|
30
|
+
update += "FROM #{from_path @key_path} "
|
31
|
+
update << settings_clause
|
32
|
+
update << where_clause
|
33
|
+
|
34
|
+
update
|
35
|
+
end
|
36
|
+
|
37
|
+
def ==(other)
|
38
|
+
other.is_a?(Update) &&
|
39
|
+
@graph == other.graph &&
|
40
|
+
entity == other.entity &&
|
41
|
+
@settings == other.settings &&
|
42
|
+
@conditions == other.conditions
|
43
|
+
end
|
44
|
+
alias eql? ==
|
45
|
+
|
46
|
+
def hash
|
47
|
+
@hash ||= [@graph, entity, @settings, @conditions].hash
|
48
|
+
end
|
49
|
+
|
50
|
+
# Specifies that updates require insertion
|
51
|
+
def requires_insert?(_index)
|
52
|
+
true
|
53
|
+
end
|
54
|
+
|
55
|
+
# Specifies that updates require deletion
|
56
|
+
def requires_delete?(index)
|
57
|
+
!(settings.map(&:field).to_set &
|
58
|
+
(index.hash_fields + index.order_fields.to_set)).empty?
|
59
|
+
end
|
60
|
+
|
61
|
+
# Get the support queries for updating an index
|
62
|
+
# @return [Array<SupportQuery>]
|
63
|
+
def support_queries(index)
|
64
|
+
return [] unless modifies_index? index
|
65
|
+
|
66
|
+
# Get the updated fields and check if an update is necessary
|
67
|
+
set_fields = settings.map(&:field).to_set
|
68
|
+
|
69
|
+
# We only need to fetch all the fields if we're updating a key
|
70
|
+
updated_key = !(set_fields &
|
71
|
+
(index.hash_fields + index.order_fields)).empty?
|
72
|
+
|
73
|
+
select = if updated_key
|
74
|
+
index.all_fields
|
75
|
+
else
|
76
|
+
index.hash_fields + index.order_fields
|
77
|
+
end - set_fields - @conditions.each_value.map(&:field)
|
78
|
+
return [] if select.empty?
|
79
|
+
|
80
|
+
support_queries = []
|
81
|
+
|
82
|
+
graph = @graph.dup
|
83
|
+
support_fields = select.select do |field|
|
84
|
+
field.parent == entity
|
85
|
+
end.to_set
|
86
|
+
support_fields << entity.id_field \
|
87
|
+
unless @conditions.each_value.map(&:field).include? entity.id_field
|
88
|
+
|
89
|
+
support_queries << build_support_query(entity, index, graph,
|
90
|
+
support_fields, conditions)
|
91
|
+
support_queries.compact + support_queries_for_entity(index, select)
|
92
|
+
end
|
93
|
+
|
94
|
+
# The condition fields are provided with the update
|
95
|
+
# Note that we don't include the settings here because we
|
96
|
+
# care about the previously existing values in the database
|
97
|
+
def given_fields
|
98
|
+
@conditions.each_value.map(&:field)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|