nose 0.1.0pre
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/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
|