nose-cli 0.1.0pre

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NoSE
4
+ module CLI
5
+ # Add a command to run the advisor and benchmarks for a given workload
6
+ class NoSECLI < Thor
7
+ desc 'search-bench NAME', 'run the workload NAME and benchmarks'
8
+
9
+ long_desc <<-LONGDESC
10
+ `nose search-bench` is a convenient way to quickly test many different
11
+ schemas. It will run `nose search`, `nose create`, `nose load`, and
12
+ `nose benchmark` to produce final benchmark results. It also accepts
13
+ all of the command-line options for each of those commands.
14
+ LONGDESC
15
+
16
+ def search_bench(name)
17
+ # Open a tempfile which will be used for advisor output
18
+ filename = Tempfile.new('workload').path
19
+
20
+ # Set some default options for various commands
21
+ opts = options.to_h
22
+ opts[:output] = filename
23
+ opts[:format] = 'json'
24
+ opts[:skip_existing] = true
25
+
26
+ o = filter_command_options opts, 'search'
27
+ $stderr.puts "Running advisor #{o}..."
28
+ invoke self.class, :search, [name], o
29
+
30
+ invoke self.class, :reformat, [filename], {}
31
+
32
+ o = filter_command_options opts, 'create'
33
+ $stderr.puts "Creating indexes #{o}..."
34
+ invoke self.class, :create, [filename], o
35
+
36
+ o = filter_command_options opts, 'load'
37
+ $stderr.puts "Loading data #{o}..."
38
+ invoke self.class, :load, [filename], o
39
+
40
+ o = filter_command_options opts, 'benchmark'
41
+ $stderr.puts "Running benchmark #{o}..."
42
+ invoke self.class, :benchmark, [filename], o
43
+ end
44
+
45
+ # Allow this command to accept the options for all commands it calls
46
+ commands['search_bench'].options.merge! commands['create'].options
47
+ commands['search_bench'].options.merge! commands['benchmark'].options
48
+ commands['search_bench'].options.merge! commands['load'].options
49
+ commands['search_bench'].options.merge! commands['search'].options
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NoSE
4
+ module CLI
5
+ # Add the use of shared options
6
+ class NoSECLI < Thor
7
+ # Add a new option to those which can be potentially shared
8
+ def self.share_option(name, options = {})
9
+ @options ||= {}
10
+ @options[name] = options
11
+ end
12
+
13
+ # Use a shared option for the current command
14
+ # @return [void]
15
+ def self.shared_option(name)
16
+ method_option name, @options[name]
17
+ end
18
+
19
+ share_option :mix, type: :string, default: 'default',
20
+ desc: 'the name of the mix for weighting queries'
21
+ share_option :format, type: :string, default: 'txt',
22
+ enum: %w(txt json yml html), aliases: '-f',
23
+ desc: 'the format of the produced plans'
24
+ share_option :output, type: :string, default: nil, aliases: '-o',
25
+ banner: 'FILE',
26
+ desc: 'a file where produced plans ' \
27
+ 'should be stored'
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,141 @@
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 'texify PLAN_FILE',
8
+ 'print the results from PLAN_FILE in LaTeX format'
9
+
10
+ long_desc <<-LONGDESC
11
+ `nose texify` loads the generated schema from the given file and prints
12
+ the generated schema in LaTeX format.
13
+ LONGDESC
14
+
15
+ shared_option :mix
16
+
17
+ def texify(plan_file)
18
+ # Load the indexes from the file
19
+ result, = load_plans plan_file, options
20
+
21
+ # If these are manually generated plans, load them separately
22
+ if result.plans.nil?
23
+ plans = Plans::ExecutionPlans.load(plan_file) \
24
+ .groups.values.flatten(1)
25
+ result.plans = plans.select { |p| p.update_steps.empty? }
26
+ result.update_plans = plans.reject { |p| p.update_steps.empty? }
27
+ end
28
+
29
+ # Print document header
30
+ puts "\\documentclass{article}\n\\begin{document}\n\\begin{flushleft}"
31
+
32
+ # Print the LaTeX for all indexes and plans
33
+ texify_indexes result.indexes
34
+ texify_plans result.plans + result.update_plans
35
+
36
+ # End the document
37
+ puts "\\end{flushleft}\n\\end{document}"
38
+ end
39
+
40
+ private
41
+
42
+ # Escape values for latex output
43
+ # @return [String]
44
+ def tex_escape(str)
45
+ str.gsub '_', '\\_'
46
+ end
47
+
48
+ # Print the LaTeX for all query plans
49
+ # @return [void]
50
+ def texify_plans(plans)
51
+ puts '\\bigskip\\textbf{Plans} \\\\\\bigskip'
52
+
53
+ plans.group_by(&:group).each do |group, grouped_plans|
54
+ group = group.nil? ? '' : tex_escape(group)
55
+ texify_plan_group group, grouped_plans
56
+ end
57
+ end
58
+
59
+ # Print the LaTeX from a group of query plans
60
+ # @return [void]
61
+ def texify_plan_group(group, grouped_plans)
62
+ puts "\\textbf{#{group}} \\\\" unless group.empty?
63
+
64
+ grouped_plans.each do |plan|
65
+ if plan.is_a?(Plans::QueryPlan) ||
66
+ (plan.is_a?(Plans::QueryExecutionPlan) &&
67
+ plan.update_steps.empty?)
68
+ puts texify_plan_steps plan.steps
69
+ else
70
+ puts texify_plan_steps plan.query_plans.flat_map(&:to_a) + \
71
+ plan.update_steps
72
+ end
73
+
74
+ puts ' \\\\'
75
+ end
76
+
77
+ puts '\\medskip'
78
+ end
79
+
80
+ # Print the LaTeX from a set of plan steps
81
+ # @return [void]
82
+ def texify_plan_steps(steps)
83
+ steps.map do |step|
84
+ case step
85
+ when Plans::IndexLookupPlanStep
86
+ "Request \\textbf{#{tex_escape step.index.key}}"
87
+ when Plans::FilterPlanStep
88
+ "Filter by #{texify_fields((step.eq + [step.range]).compact)}"
89
+ when Plans::SortPlanStep
90
+ "Sort by #{texify_fields step.sort_fields}"
91
+ when Plans::LimitPlanStep
92
+ "Limit #{step.limit}"
93
+ when Plans::DeletePlanStep
94
+ "Delete from \\textbf{#{tex_escape step.index.key}}"
95
+ when Plans::InsertPlanStep
96
+ "Insert into \\textbf{#{tex_escape step.index.key}}"
97
+ end
98
+ end.join(', ')
99
+ end
100
+
101
+ # Print all LaTeX for a given index
102
+ # @return [void]
103
+ def texify_indexes(indexes)
104
+ puts '\\bigskip\\textbf{Indexes} \\\\\\bigskip'
105
+
106
+ indexes.each do |index|
107
+ # Print the key of the index
108
+ puts "\\textbf{#{tex_escape index.key}} \\\\"
109
+
110
+ fields = index.hash_fields.map do |field|
111
+ texify_field(field, true)
112
+ end
113
+
114
+ fields += index.order_fields.map do |field|
115
+ texify_field(field, true, true)
116
+ end
117
+
118
+ fields += index.extra.map { |field| texify_field(field) }
119
+
120
+ puts fields.join(', ') + ' \\\\\\medskip'
121
+ end
122
+ end
123
+
124
+ # Produce the LaTex for an array of fields
125
+ # @return [String]
126
+ def texify_fields(fields)
127
+ fields.map { |field| texify_field field }.join ', '
128
+ end
129
+
130
+ # Produce the LaTeX for a given index field
131
+ # @return [String]
132
+ def texify_field(field, underline = false, italic = false)
133
+ tex = tex_escape field.to_s
134
+ tex = "\\textit{#{tex}}" if italic
135
+ tex = "\\underline{#{tex}}" if underline
136
+
137
+ tex
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,70 @@
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 'why PLAN_FILE',
8
+ 'output the reason for including each index in PLAN_FILE'
9
+
10
+ long_desc <<-LONGDESC
11
+ `nose why` is used to better understand why NoSE included a particular
12
+ index in a schema. This is especially helpful when comparing with
13
+ manually-defined execution plans.
14
+ LONGDESC
15
+
16
+ def why(plan_file)
17
+ result = load_results plan_file
18
+ indexes_usage = Hash.new { |h, k| h[k] = [] }
19
+
20
+ # Count the indexes used in queries
21
+ query_count = Set.new
22
+ update_index_usage result.plans, indexes_usage, query_count
23
+
24
+ # Count the indexes used in support queries
25
+ # (ignoring those used in queries)
26
+ support_count = Set.new
27
+ result.update_plans.each do |plan|
28
+ update_index_usage plan.query_plans, indexes_usage,
29
+ support_count, query_count
30
+ end
31
+
32
+ # Produce the final output of index usage
33
+ print_index_usage indexes_usage, query_count, support_count
34
+ end
35
+
36
+ private
37
+
38
+ # Track usage of indexes in the set of query plans updating both
39
+ # a dictionary of statements relevant to each index and a set
40
+ # of unique statements used (optionally ignoring some)
41
+ # @return [void]
42
+ def update_index_usage(plans, indexes_usage, statement_usage,
43
+ ignore = Set.new)
44
+ plans.each do |plan|
45
+ plan.indexes.each do |index|
46
+ indexes_usage[index] << if plan.respond_to?(:statement)
47
+ plan.statement
48
+ else
49
+ plan.query
50
+ end
51
+ statement_usage.add index unless ignore.include? index
52
+ end
53
+ end
54
+ end
55
+
56
+ # Print out the statements each index is used for
57
+ # @return [void]
58
+ def print_index_usage(indexes_usage, query_count, support_count)
59
+ indexes_usage.each do |index, statements|
60
+ p index
61
+ statements.each { |s| p s }
62
+ puts
63
+ end
64
+
65
+ puts " Queries: #{query_count.length}"
66
+ puts "Support queries: #{support_count.length}"
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,56 @@
1
+ _nose_complete() {
2
+ case $1 in
3
+ 1) _nose_commands;;
4
+ 2)
5
+ case "$2" in
6
+ help) _nose_commands;;
7
+ *) _nose_option;;
8
+ esac;;
9
+ *) _nose_option;;
10
+ esac
11
+ }
12
+
13
+ if type compdef 1>/dev/null 2>/dev/null; then
14
+ compdef _nose nose
15
+ _nose() { _nose_complete $((${#words} - 1)) "${words[2]}"; }
16
+ _nose_commands() { list=(<%= commands.map { |name, c| "#{name}:\"#{Shellwords.escape(c.description)}\"" }.join(' ') %>) _describe -t common-commands 'common commands' list; }
17
+ _nose_option() {
18
+ case "${words[2]}" in
19
+ <%= commands.map do |name, c|
20
+ " #{name}) _arguments -s -S " + c.options.values.flat_map do |opt|
21
+ (opt.aliases << opt.switch_name).map do |switch|
22
+ "\"#{switch}[#{Shellwords.escape(opt.description)}]\""
23
+ end
24
+ end.uniq.join(' ') + ' \'*:file:_files\' && return 0;;'
25
+ end.join("\n") %>
26
+ esac
27
+ }
28
+ elif type compctl 1>/dev/null 2>/dev/null; then
29
+ compctl -K _nose nose
30
+ _nose() { read -cA words && _nose_complete $((${#words} - 1)) "${words[2]}"; }
31
+ _nose_commands() { reply=(<%= commands.map { |name, _| "\"#{name}\"" }.join(' ') %>); }
32
+ _nose_option() {
33
+ case "${words[2]}" in
34
+ <%= commands.map do |name, c|
35
+ " #{name}) reply=(" + c.options.values.flat_map do |opt|
36
+ (opt.aliases << opt.switch_name).map { |s| "\"#{s}\"" }
37
+ end.uniq.join(' ') + ');;'
38
+ end.join("\n") %>
39
+ esac
40
+ }
41
+ elif type complete 1>/dev/null 2>/dev/null; then
42
+ complete -F _nose nose
43
+ _nose() { _nose_complete "$COMP_CWORD" "${COMP_WORDS[1]}"; }
44
+ _nose_commands() { COMPREPLY=( $(compgen -W "<%= commands.map(&:first).join(' ') %>" -- "${COMP_WORDS[COMP_CWORD]}") ); }
45
+ _nose_option() {
46
+ local options
47
+ case "${COMP_WORDS[1]}" in
48
+ <%= commands.map do |name, c|
49
+ " #{name}) options=\"" + c.options.values.flat_map do |opt|
50
+ (opt.aliases << opt.switch_name)
51
+ end.uniq.join(' ') + '";;'
52
+ end.join("\n") %>
53
+ esac
54
+ COMPREPLY=( $(compgen -W "$options" -- "${COMP_WORDS[COMP_CWORD]}") )
55
+ }
56
+ fi
data/templates/man.erb ADDED
@@ -0,0 +1,33 @@
1
+ nose(1) -- NoSQL Schema Evaluator
2
+ ========================================
3
+
4
+ ## SYNOPSIS
5
+
6
+ `nose` <%= options.each_value.map do |option|
7
+ '[' + (option.aliases + ["--#{option.name}"]).join('|') + ']'
8
+ end.join ', ' %>
9
+
10
+ ## DESCRIPTION
11
+
12
+ NoSE is a tool for schema design in NoSQL databases.
13
+
14
+ ## OPTIONS
15
+
16
+ <% options.each do |name, option| %>
17
+ * <%= option.usage %>
18
+ <%= option.description %>
19
+
20
+ <% end %>
21
+
22
+ ## SUBCOMMANDS
23
+
24
+ <% commands.each do |name, command| %>
25
+ `nose-<%= name %>`(1)
26
+ <%= command.description %>
27
+
28
+
29
+ <% end %>
30
+
31
+ ## SEE ALSO
32
+
33
+ <%= commands.map { |name, _| "`nose-#{name}`(1)" }.join ', ' %>
@@ -0,0 +1,138 @@
1
+ <% require 'ansi-to-html' %>
2
+
3
+ <!DOCTYPE html>
4
+ <html lang="en">
5
+ <head>
6
+ <meta charset="utf-8">
7
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
8
+ <meta name="viewport" content="width=device-width, initial-scale=1">
9
+ <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
10
+ <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
11
+ <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
12
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.4.0/styles/default.min.css">
13
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.4.0/highlight.min.js"></script>
14
+ <script>hljs.initHighlightingOnLoad();</script>
15
+ <title>NoSE Schema Recommendation</title>
16
+ </head>
17
+ <body style="margin: 1em">
18
+ <h1>NoSE Schema Recommendation</h1>
19
+
20
+ <h2>Model</h2>
21
+ <%= svg %>
22
+
23
+ <% unless workload.model.source_code.nil? %>
24
+ <a class="btn btn-primary" style="display: block" data-toggle="collapse" data-target="#model-code">Model code
25
+ <span class="glyphicon glyphicon-menu-hamburger" style="float:right"></span></a>
26
+ <pre id="model-code" class="collapse"><code class="lang-ruby"><%= workload.model.source_code %></pre></code>
27
+ <% end %>
28
+
29
+ <h2>Queries</h2>
30
+ <% workload.queries.each do |query| %>
31
+ <% unless query.label.nil? %><strong><%= query.label %></strong><% end %>
32
+ <pre><code class="lang-sql"><%= query.text %></code></pre>
33
+ <% end %>
34
+
35
+ <% unless workload.updates.empty? %><h2>Updates</h2><% end %>
36
+ <% workload.updates.each do |update| %>
37
+ <% unless update.label.nil? %><strong><%= update.label %></strong><% end %>
38
+ <pre><code class="lang-sql"><%= update.text %></code></pre>
39
+ <% end %>
40
+
41
+ <% unless workload.source_code.nil? %>
42
+ <a class="btn btn-primary" style="display: block" data-toggle="collapse" data-target="#workload-code">Workload code
43
+ <span class="glyphicon glyphicon-menu-hamburger" style="float:right"></span></a>
44
+ <pre id="workload-code" class="collapse"><code class="lang-ruby"><%= workload.source_code %></pre></code>
45
+ <% end %>
46
+
47
+ <h2>Indexes</h2>
48
+ <% ddl = backend ? backend.indexes_ddl.to_a : nil %>
49
+ <% indexes.each_with_index do |index, i| %>
50
+ <h3><%= index.key %></h3>
51
+ <% if ddl %>
52
+ <p><tt><%= ddl[i] %></tt></p>
53
+ <% end %>
54
+ <p><%= Ansi::To::Html.new(index.inspect).to_html %></p>
55
+ <% end %>
56
+
57
+ <h2>Query plans</h2>
58
+ <% plans.each do |plan| %>
59
+ <% unless plan.query.label.nil? %><strong><%= plan.query.label %></strong><% end %>
60
+ <pre><code class="lang-sql"><%= Ansi::To::Html.new(plan.query.inspect).to_html %></code></pre>
61
+ <strong>Cost: <%= plan.cost %></strong>
62
+
63
+ <% if plan.steps.length > 1 %>
64
+ <ol>
65
+ <% plan.each do |step| %>
66
+ <li><%= Ansi::To::Html.new(step.inspect).to_html %></li>
67
+ <% end %>
68
+ </ol>
69
+ <% else %>
70
+ <p><%= Ansi::To::Html.new(plan.steps.first.inspect).to_html %></p>
71
+ <% end %>
72
+ <hr>
73
+ <% end %>
74
+
75
+ <h2>Update plans</h2>
76
+ <% update_plans.group_by(&:statement).each do |statement, plans| %>
77
+ <% unless statement.label.nil? %><strong><%= statement.label %></strong><% end %>
78
+ <pre><code class="lang-sql"><%= statement.text %></code></pre>
79
+ <strong>Total cost: <%= plans.map(&:cost).inject(0, &:+) %></strong>
80
+
81
+ <% plans.each do |plan| %>
82
+ <h4><%= plan.index.key %></h4>
83
+ <strong>Cost: <%= plan.cost %></strong>
84
+
85
+ <% unless plan.query_plans.empty? %>
86
+ <h5>Support queries</h5>
87
+ <% end %>
88
+
89
+ <% plan.query_plans.each do |query_plan| %>
90
+ <% unless query_plan.query.label.nil? %><strong><%= query_plan.query.label %></strong><% end %>
91
+ <pre><code class="lang-sql"><%= query_plan.query.text %></code></pre>
92
+ <% if query_plan.steps.length > 1 %>
93
+ <ol>
94
+ <% query_plan.each do |step| %>
95
+ <li><%= Ansi::To::Html.new(step.inspect).to_html %></li>
96
+ <% end %>
97
+ </ol>
98
+ <% else %>
99
+ <p><%= Ansi::To::Html.new(query_plan.steps.first.inspect).to_html %></p>
100
+ <% end %>
101
+ <% end %>
102
+
103
+ <h5>Updates</h5>
104
+
105
+ <% if plan.update_steps.length > 1 %>
106
+ <ol>
107
+ <% plan.update_steps.each do |step| %>
108
+ <li><%= Ansi::To::Html.new(step.inspect).to_html %></li>
109
+ <% end %>
110
+ </ol>
111
+ <% else %>
112
+ <p><%= Ansi::To::Html.new(plan.update_steps.first.inspect).to_html %></p>
113
+ <% end %>
114
+ <% end %>
115
+ <hr>
116
+ <% end %>
117
+
118
+ <% if enumerated_indexes %>
119
+ <h2>Enumerated Indexes</h2>
120
+ <% enumerated_indexes.each_with_index do |index, i| %>
121
+ <h3><%= index.key %></h3>
122
+ <% if ddl %>
123
+ <p><tt><%= ddl[i] %></tt></p>
124
+ <% end %>
125
+ <p><%= Ansi::To::Html.new(index.inspect).to_html %></p>
126
+ <% end %>
127
+ <% end %>
128
+
129
+ <h2>Summary</h2>
130
+ <dl>
131
+ <dt>Total size</dt>
132
+ <dd><%= total_size %></dd>
133
+
134
+ <dt>Total cost</dt>
135
+ <dd><%= total_cost %></dd>
136
+ </dl>
137
+ </body>
138
+ </html>