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,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>