nose-cli 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.
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NoSE
4
+ module CLI
5
+ # Add a command to run a proxy server capable of executing queries
6
+ class NoSECLI < Thor
7
+ desc 'proxy PLAN_FILE', 'start a proxy with the given PLAN_FILE'
8
+
9
+ long_desc <<-LONGDESC
10
+ `nose proxy` loads the schema and execution plans from the given file.
11
+ It then starts the configured proxy server which is able to execute
12
+ the given set of plans. Available proxy servers can be seen in the
13
+ `lib/nose/proxy` subdirectory.
14
+ LONGDESC
15
+
16
+ def proxy(plan_file)
17
+ result = load_results plan_file
18
+ backend = get_backend(options, result)
19
+
20
+ # Create a new instance of the proxy class
21
+ proxy_class = get_class 'proxy', options
22
+ proxy = proxy_class.new options[:proxy], result, backend
23
+
24
+ # Start the proxy server
25
+ trap 'INT' do
26
+ proxy.stop
27
+ end
28
+ proxy.start
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NoSE
4
+ module CLI
5
+ # Add a command to generate a graphic of the schema from a workload
6
+ class NoSECLI < Thor
7
+ desc 'random-plans WORKLOAD TAG',
8
+ 'output random plans for the statement with TAG in WORKLOAD'
9
+
10
+ long_desc <<-LONGDESC
11
+ `nose random-plans` produces a set of randomly chosen plans for a given
12
+ workload. This is useful when evaluating the cost model for a given
13
+ system as this should give a good range of different execution plans.
14
+ LONGDESC
15
+
16
+ shared_option :format
17
+ shared_option :output
18
+
19
+ option :count, type: :numeric, default: 5, aliases: '-n',
20
+ desc: 'the number of random plans to produce'
21
+ option :all, type: :boolean, default: false, aliases: '-a',
22
+ desc: 'whether to include all possible plans, if given ' \
23
+ 'then --count is ignored'
24
+
25
+ def random_plans(workload_name, tag)
26
+ # Find the statement with the given tag
27
+ workload = Workload.load workload_name
28
+ statement = workload.find_with_tag tag
29
+
30
+ # Generate a random set of plans
31
+ indexes = IndexEnumerator.new(workload).indexes_for_workload
32
+ cost_model = get_class('cost', options[:cost_model][:name]) \
33
+ .new(**options[:cost_model])
34
+ plans = find_random_plans statement, workload, indexes, cost_model,
35
+ options
36
+ results = random_plan_results workload, indexes, plans, cost_model
37
+ output_random_plans results, options[:output], options[:format]
38
+ end
39
+
40
+ private
41
+
42
+ # Generate random plans for a givne statement
43
+ # @return [Array]
44
+ def find_random_plans(statement, workload, indexes, cost_model, options)
45
+ planner = Plans::QueryPlanner.new workload, indexes, cost_model
46
+ plans = planner.find_plans_for_query(statement).to_a
47
+ plans = plans.sample options[:count] unless options[:all]
48
+
49
+ plans
50
+ end
51
+
52
+ # Build a results structure for these plans
53
+ # @return [OpenStruct]
54
+ def random_plan_results(workload, indexes, plans, cost_model)
55
+ results = OpenStruct.new
56
+ results.workload = workload
57
+ results.model = workload.model
58
+ results.enumerated_indexes = indexes
59
+ results.indexes = plans.map(&:indexes).flatten(1).to_set
60
+ results.plans = plans
61
+ results.cost_model = cost_model
62
+
63
+ results
64
+ end
65
+
66
+ # Output the results in the specified format
67
+ # @return [void]
68
+ def output_random_plans(results, output, format)
69
+ if output.nil?
70
+ file = $stdout
71
+ else
72
+ File.open(output, 'w')
73
+ end
74
+ begin
75
+ send(('output_' + format).to_sym, results, file, true)
76
+ ensure
77
+ file.close unless output.nil?
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NoSE
4
+ module CLI
5
+ # Add a command to recalculate costs with a new model
6
+ class NoSECLI < Thor
7
+ desc 'recost PLAN_FILE COST_MODEL',
8
+ 'recost the workload in PLAN_FILE with COST_MODEL'
9
+
10
+ long_desc <<-LONGDESC
11
+ `nose recost` takes a given plan file as input and then produces a new
12
+ JSON file with new values for the expected query cost from the named
13
+ cost model. This only works for cost models which don't require any
14
+ parameters such as `Cost::RequestCountCost`.
15
+ LONGDESC
16
+
17
+ def recost(plan_file, cost_model)
18
+ result = load_results plan_file
19
+
20
+ # Get the new cost model and recost all the queries
21
+ new_cost_model = get_class('cost', cost_model).new
22
+ result.plans.each { |plan| plan.cost_model = new_cost_model }
23
+
24
+ # Update the cost values
25
+ result.cost_model = new_cost_model
26
+ result.total_cost = total_cost result.workload, result.plans
27
+
28
+ output_json result
29
+ end
30
+
31
+ private
32
+
33
+ # Get the total cost for a set of plans in a given workload
34
+ # @return [Fixnum]
35
+ def total_cost(workload, plans)
36
+ plan_hash = Hash[plans.map { |plan| [plan.query, plan] }]
37
+
38
+ workload.statement_weights.map do |statement, weight|
39
+ next 0 unless statement.is_a? Query
40
+ weight * plan_hash[statement].cost
41
+ end.inject(0, &:+)
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NoSE
4
+ module CLI
5
+ # Add a command to reformat a plan file
6
+ class NoSECLI < Thor
7
+ desc 'reformat PLAN_FILE',
8
+ 'reformat the data in PLAN_FILE'
9
+
10
+ shared_option :format
11
+ shared_option :mix
12
+
13
+ def reformat(plan_file)
14
+ result = load_results plan_file, options[:mix]
15
+ result.recalculate_cost
16
+
17
+ # Output the results in the specified format
18
+ send(('output_' + options[:format]).to_sym, result)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'formatador'
4
+
5
+ begin
6
+ require 'readline'
7
+ rescue LoadError
8
+ nil
9
+ end
10
+
11
+ module NoSE
12
+ module CLI
13
+ # Add a command to run a REPL which evaluates queries
14
+ class NoSECLI < Thor
15
+ desc 'repl PLAN_FILE', 'start the REPL with the given PLAN_FILE'
16
+
17
+ long_desc <<-LONGDESC
18
+ `nose repl` starts a terminal interface which loads execution plans
19
+ from the given file. It then loops continuously and accepts statements
20
+ from the workload and executes plans against the database. The result
21
+ of any query will be printed to the terminal.
22
+ LONGDESC
23
+
24
+ def repl(plan_file)
25
+ result = load_results plan_file
26
+ backend = get_backend(options, result)
27
+
28
+ load_history
29
+ loop do
30
+ begin
31
+ line = read_line
32
+ rescue Interrupt
33
+ line = nil
34
+ end
35
+ break if line.nil?
36
+
37
+ next if line.empty?
38
+
39
+ execute_statement line, result, backend
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ # Load the history file
46
+ # @return [void]
47
+ def load_history
48
+ return unless Object.const_defined? 'Readline'
49
+
50
+ history_file = File.expand_path '~/.nose_history'
51
+ begin
52
+ File.foreach(history_file) do |line|
53
+ Readline::HISTORY.push line.chomp
54
+ end
55
+ rescue Errno::ENOENT
56
+ nil
57
+ end
58
+ end
59
+
60
+ # Save the history file
61
+ # @return [void]
62
+ def save_history
63
+ return unless Object.const_defined? 'Readline'
64
+
65
+ history_file = File.expand_path '~/.nose_history'
66
+ File.open(history_file, 'w') do |f|
67
+ Readline::HISTORY.each do |line|
68
+ f.puts line
69
+ end
70
+ end
71
+ end
72
+
73
+ # Get the next inputted line in the REPL
74
+ # @return [String]
75
+ def read_line
76
+ prefix = '>> '
77
+
78
+ if Object.const_defined? 'Readline'
79
+ line = Readline.readline prefix
80
+ return if line.nil?
81
+
82
+ Readline::HISTORY.push line unless line.chomp.empty?
83
+ else
84
+ print prefix
85
+ line = gets
86
+ end
87
+
88
+ line.chomp
89
+ end
90
+
91
+ # Parse a statement from a given string of text
92
+ # @return [Statement]
93
+ def parse_statement(text, workload)
94
+ begin
95
+ statement = Statement.parse text, workload.model
96
+ rescue ParseFailed => e
97
+ puts '! ' + e.message
98
+ statement = nil
99
+ end
100
+
101
+ statement
102
+ end
103
+
104
+ # Try to execute a statement read from the REPL
105
+ # @return [void]
106
+ def execute_statement(line, result, backend)
107
+ # Parse the statement
108
+ statement = parse_statement line, result.workload
109
+ return if statement.nil?
110
+
111
+ begin
112
+ results, elapsed = backend_execute_statement backend, statement
113
+ rescue NotImplementedError, Backend::PlanNotFound => e
114
+ puts '! ' + e.message
115
+ else
116
+ display_statement_result results, elapsed
117
+ end
118
+ end
119
+
120
+ # Execute the statement on the provided backend and measure the runtime
121
+ def backend_execute_statement(backend, statement)
122
+ start_time = Time.now.utc
123
+
124
+ if statement.is_a? Query
125
+ results = backend.query statement
126
+ else
127
+ backend.update statement
128
+ results = []
129
+ end
130
+
131
+ elapsed = Time.now.utc - start_time
132
+
133
+ [results, elapsed]
134
+ end
135
+
136
+ # Show the results of executing a statement
137
+ # @return [void]
138
+ def display_statement_result(results, elapsed)
139
+ Formatador.display_compact_table results unless results.empty?
140
+ puts format('(%d rows in %.2fs)', results.length, elapsed)
141
+ end
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'formatador'
4
+ require 'ostruct'
5
+ require 'json'
6
+
7
+ module NoSE
8
+ module CLI
9
+ # Add a command to run the advisor for a given workload
10
+ class NoSECLI < Thor
11
+ desc 'search NAME', 'run the workload NAME'
12
+
13
+ long_desc <<-LONGDESC
14
+ `nose search` is the primary command to run the NoSE advisor. It will
15
+ load the given workload, enumerate indexes for each statement, and
16
+ construct and solve an ILP to produce statement execution plans.
17
+ LONGDESC
18
+
19
+ shared_option :mix
20
+ shared_option :format
21
+ shared_option :output
22
+
23
+ option :max_space, type: :numeric, default: Float::INFINITY,
24
+ aliases: '-s',
25
+ desc: 'maximum space allocated to indexes'
26
+ option :enumerated, type: :boolean, default: false, aliases: '-e',
27
+ desc: 'whether enumerated indexes should be output'
28
+ option :read_only, type: :boolean, default: false,
29
+ desc: 'whether to ignore update statements'
30
+ option :objective, type: :string, default: 'cost',
31
+ enum: %w(cost space indexes),
32
+ desc: 'the objective function to use in the ILP'
33
+ option :by_id_graph, type: :boolean, default: false,
34
+ desc: 'whether to group generated indexes in' \
35
+ 'graphs by ID',
36
+ aliases: '-i'
37
+
38
+ def search(name)
39
+ # Get the workload and cost model
40
+ workload = Workload.load name
41
+ workload.mix = options[:mix].to_sym \
42
+ unless options[:mix] == 'default' && workload.mix != :default
43
+ workload.remove_updates if options[:read_only]
44
+ cost_model = get_class_from_config options, 'cost', :cost_model
45
+
46
+ # Execute the advisor
47
+ objective = Search::Objective.const_get options[:objective].upcase
48
+ result = search_result workload, cost_model, options[:max_space],
49
+ objective, options[:by_id_graph]
50
+ output_search_result result, options unless result.nil?
51
+ end
52
+
53
+ private
54
+
55
+ # Output results from the search procedure
56
+ # @return [void]
57
+ def output_search_result(result, options)
58
+ # Output the results in the specified format
59
+ file = if options[:output].nil?
60
+ $stdout
61
+ else
62
+ File.open(options[:output], 'w')
63
+ end
64
+
65
+ begin
66
+ backend = get_backend options, result
67
+ send(('output_' + options[:format]).to_sym,
68
+ result, file, options[:enumerated], backend)
69
+ rescue
70
+ nil
71
+ ensure
72
+ file.close unless options[:output].nil?
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+
5
+ module NoSE
6
+ module CLI
7
+ # Add a command to reformat a plan file
8
+ class NoSECLI < Thor
9
+ desc 'search-all NAME DIRECTORY',
10
+ 'output all possible schemas for the workload NAME under ' \
11
+ 'different storage constraints to DIRECTORY'
12
+
13
+ long_desc <<-LONGDESC
14
+ `nose search-all` is a convenience for executing `nose search` with a
15
+ variety of different storage constraints. It will start with the
16
+ smallest and largest possible schemas and then perform a binary search
17
+ throughout the search space to discover schemas of various sizes.
18
+ LONGDESC
19
+
20
+ shared_option :format
21
+ shared_option :mix
22
+
23
+ option :enumerated, type: :boolean, aliases: '-e',
24
+ desc: 'whether enumerated indexes should be output'
25
+ option :read_only, type: :boolean, default: false,
26
+ desc: 'whether to ignore update statements'
27
+ option :max_results, type: :numeric, default: Float::INFINITY,
28
+ aliases: '-n',
29
+ desc: 'the maximum number of results to produce'
30
+ def search_all(name, directory)
31
+ # Load the workload and cost model and create the output directory
32
+ workload = Workload.load name
33
+ workload.mix = options[:mix].to_sym \
34
+ unless options[:mix] == 'default' && workload.mix != :default
35
+ workload.remove_updates if options[:read_only]
36
+ cost_model = get_class_from_config options, 'cost', :cost_model
37
+ FileUtils.mkdir_p(directory) unless Dir.exist?(directory)
38
+
39
+ # Run the search and output the results
40
+ results = search_results workload, cost_model, options[:max_results]
41
+ output_results results, directory, options
42
+ end
43
+
44
+ private
45
+
46
+ # Get a list of all possible search results
47
+ # @return [Array<Search::Results>]
48
+ def search_results(workload, cost_model, max_results)
49
+ # Start with the maximum possible size and divide in two
50
+ max_result = search_result workload, cost_model
51
+ max_size = max_result.total_size
52
+ min_result = search_result workload, cost_model,
53
+ Float::INFINITY, Search::Objective::SPACE
54
+ min_size = min_result.total_size
55
+ min_result = search_result workload, cost_model, min_size
56
+
57
+ # If we only have one result, return
58
+ return [max_result] if max_size == min_size
59
+
60
+ results = [max_result, min_result]
61
+ num_results = 2
62
+ sizes = Set.new [min_size, max_size]
63
+ size_queue = [(max_size - min_size) / 2.0 + min_size]
64
+ until size_queue.empty?
65
+ # Stop if we found the appropriate number of results
66
+ return results if num_results >= max_results
67
+
68
+ # Find a new size to examine
69
+ size = size_queue.pop
70
+
71
+ # Continue dividing the range of examined sizes
72
+ next_size = sizes.sort.detect { |n| n > size }
73
+ next_size = (next_size - size) / 2.0 + size
74
+
75
+ prev_size = sizes.sort.reverse_each.detect { |n| n < size }
76
+ prev_size = (size - prev_size) / 2.0 + prev_size
77
+
78
+ begin
79
+ @logger.info "Running search with size #{size}"
80
+
81
+ result = search_result workload, cost_model, size
82
+ next if sizes.include?(result.total_size) || result.nil?
83
+ rescue Search::NoSolutionException, Plans::NoPlanException
84
+ # No result was found, so only explore the larger side
85
+ @logger.info "No solution for size #{size}"
86
+ else
87
+ # Add the smaller size to the queue and save the result
88
+ size_queue.push prev_size unless sizes.include? prev_size
89
+ results.push result
90
+ num_results += 1
91
+ end
92
+
93
+ # Add the larger size to the queue
94
+ size_queue.push(next_size) unless sizes.include? next_size
95
+
96
+ # Note that we visited this size (and the result size)
97
+ sizes.add size
98
+ sizes.add result.total_size unless result.nil?
99
+ end
100
+
101
+ results
102
+ end
103
+
104
+ # Output all results to file
105
+ # @return [void]
106
+ def output_results(results, directory, options)
107
+ results.sort_by!(&:total_size)
108
+ results.each_with_index do |result, i|
109
+ file = File.open File.join(directory, "#{i}.#{options[:format]}"), 'w'
110
+ begin
111
+ send(('output_' + options[:format]).to_sym,
112
+ result, file, options[:enumerated])
113
+ ensure
114
+ file.close
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end