sqltorial 0.0.1
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/.gitignore +10 -0
- data/.travis.yml +3 -0
- data/CHANGELOG.md +30 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +71 -0
- data/Rakefile +1 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/examples/example_input.sql +15 -0
- data/examples/example_output.md +47 -0
- data/exe/sqltorial +25 -0
- data/lib/sqltorial.rb +8 -0
- data/lib/sqltorial/assemble_command.rb +40 -0
- data/lib/sqltorial/directives/all_directive.rb +26 -0
- data/lib/sqltorial/directives/directive.rb +22 -0
- data/lib/sqltorial/directives/valid_column_directive.rb +22 -0
- data/lib/sqltorial/metadata.rb +6 -0
- data/lib/sqltorial/query_cache.rb +35 -0
- data/lib/sqltorial/query_to_md.rb +142 -0
- data/lib/sqltorial/regexp_directive.rb +31 -0
- data/lib/sqltorial/sql_to_example.rb +138 -0
- data/sqltorial.gemspec +36 -0
- metadata +155 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: bc641ec92204199a0c354f413f381b544a4fc0f1
|
4
|
+
data.tar.gz: c42caec90a3cefc4a6c6273f99699fae636ed492
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 61f3145d3f3cbf346b9f88370bda5ab9fef7ad083340f562d4fde4ec16a2eda5cb64373f391a6162a253e8947da9bc91dd3fd173dc84ac160ba94e3364fc19cc
|
7
|
+
data.tar.gz: 01d990718cf2c72b49cec7ac9f308c8e0722beeb71125818e2f1f4abba06bc1d37f5f713a071bdfa8893de34209aec7e81ab0332a356a8cfae2094a3c0a69b0b
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/CHANGELOG.md
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# Changelog
|
2
|
+
All notable changes to this project will be documented in this file.
|
3
|
+
|
4
|
+
## Unreleased
|
5
|
+
|
6
|
+
### Added
|
7
|
+
- Nothing.
|
8
|
+
|
9
|
+
### Deprecated
|
10
|
+
- Nothing.
|
11
|
+
|
12
|
+
### Removed
|
13
|
+
- Nothing.
|
14
|
+
|
15
|
+
### Fixed
|
16
|
+
- Nothing.
|
17
|
+
|
18
|
+
## 0.0.1 - 2015-08-21
|
19
|
+
|
20
|
+
### Added
|
21
|
+
- The project itself
|
22
|
+
|
23
|
+
### Deprecated
|
24
|
+
- Nothing.
|
25
|
+
|
26
|
+
### Removed
|
27
|
+
- Nothing.
|
28
|
+
|
29
|
+
### Fixed
|
30
|
+
- Nothing.
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Outcomes Insights, Inc.
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
# SQLtorial
|
2
|
+
|
3
|
+
Create your own SQL Tutorials with SQLtorial.
|
4
|
+
|
5
|
+
|
6
|
+
## Motivation
|
7
|
+
|
8
|
+
SQLtorial is a gem I cooked up because I was frequently demonstrating how to write certain queries or how to explore data in certain databases. Generally, I wanted each example SQL statement to have a bit of explanation about how it works or what I’m looking for, followed by the query itself, followed by the results that SQL statement generated.
|
9
|
+
|
10
|
+
So I decided to write SQLtorial, a command that will process all files ending in \*.sql and generate a Markdown document with all the examples concatenated together.
|
11
|
+
|
12
|
+
The gem will process each .sql statement in the following manner:
|
13
|
+
- The first line is considered the title for the entire example
|
14
|
+
- Comments placed above a SQL query will be run through a Markdown formatter and placed as formatted text before the SQL query
|
15
|
+
- SQL queries must end with a ;
|
16
|
+
- SQL queries are run through [pgFormatter](https://github.com/darold/pgFormatter) to create a consistent presentation for queries
|
17
|
+
- Results from the query are shown in a table after the query. Only the first ten results are shown.
|
18
|
+
|
19
|
+
See the examples directory.
|
20
|
+
|
21
|
+
## Installation
|
22
|
+
|
23
|
+
Add this line to your application's Gemfile:
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
gem 'sqltorial'
|
27
|
+
```
|
28
|
+
|
29
|
+
And then execute:
|
30
|
+
|
31
|
+
$ bundle
|
32
|
+
|
33
|
+
Or install it yourself as:
|
34
|
+
|
35
|
+
$ gem install sqltorial
|
36
|
+
|
37
|
+
## Usage
|
38
|
+
|
39
|
+
Create a directory for your SQL examples. Create at least one file ending with `.sql` and add comments and example queries as you see fit.
|
40
|
+
|
41
|
+
When you are ready to execute your examples against a database and compile the examples into a markdown file, create a configuration file following the instructions in the [sequelizer](https://github.com/outcomesinsights/sequelizer) README.
|
42
|
+
|
43
|
+
Once your sequelizer configuration is set up, run
|
44
|
+
|
45
|
+
$ bundle exec sqltorial
|
46
|
+
|
47
|
+
The `sqltorial` command will convert each `.sql` file into a markdown and concatenate the files into a single markdown file called `output.md`
|
48
|
+
|
49
|
+
## Development
|
50
|
+
|
51
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/console` for an interactive prompt that will allow you to experiment.
|
52
|
+
|
53
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release` to create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
54
|
+
|
55
|
+
## Contributing
|
56
|
+
|
57
|
+
1. Fork it ( https://github.com/outcomesinsights/sqltorial/fork )
|
58
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
59
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
60
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
61
|
+
5. Create a new Pull Request
|
62
|
+
|
63
|
+
## Thanks
|
64
|
+
|
65
|
+
- [Outcomes Insights, Inc.](http://outins.com)
|
66
|
+
- Many thanks for allowing me to release a portion of my work as Open Source Software!
|
67
|
+
- [knitr](http://yihui.name/knitr/)
|
68
|
+
- Thanks for the inspiration!
|
69
|
+
|
70
|
+
## License
|
71
|
+
Released under the MIT license, Copyright (c) 2015 Outcomes Insights, Inc.
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "sqltorial"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
-- This is the title of the example
|
2
|
+
--
|
3
|
+
-- And here is the first query
|
4
|
+
SELECT 1 as column_header;
|
5
|
+
|
6
|
+
-- You can use [Markdown](https://daringfireball.net/projects/markdown/) to:
|
7
|
+
--
|
8
|
+
-- - Format your comments
|
9
|
+
--
|
10
|
+
-- - Highlight things in **bold**
|
11
|
+
--
|
12
|
+
-- - And make lists like this one, though the formatting is a bit odd. I'm looking into it.
|
13
|
+
--
|
14
|
+
-- And they'll appear above your next query like this query:
|
15
|
+
SELECT 'hey' as some_name_for_column;
|
@@ -0,0 +1,47 @@
|
|
1
|
+
## Example 1: This is the title of the example
|
2
|
+
|
3
|
+
|
4
|
+
|
5
|
+
And here is the first query
|
6
|
+
|
7
|
+
**Query 1.1**
|
8
|
+
|
9
|
+
```sql
|
10
|
+
SELECT
|
11
|
+
1 AS column_header
|
12
|
+
;
|
13
|
+
```
|
14
|
+
|
15
|
+
Found 1 results.
|
16
|
+
|
17
|
+
| column_header |
|
18
|
+
| ------------------: |
|
19
|
+
| 1 |
|
20
|
+
|
21
|
+
|
22
|
+
You can use [Markdown](https://daringfireball.net/projects/markdown/) to:
|
23
|
+
|
24
|
+
- Format your comments
|
25
|
+
|
26
|
+
- Highlight things in **bold**
|
27
|
+
|
28
|
+
- And make lists like this one, though the formatting is a bit odd. I'm looking into it.
|
29
|
+
|
30
|
+
And they'll appear above your next query like this query:
|
31
|
+
|
32
|
+
**Query 1.2**
|
33
|
+
|
34
|
+
```sql
|
35
|
+
SELECT
|
36
|
+
'hey' AS some_name_for_column
|
37
|
+
;
|
38
|
+
```
|
39
|
+
|
40
|
+
Found 1 results.
|
41
|
+
|
42
|
+
| some_name_for_column |
|
43
|
+
| :----------------------------- |
|
44
|
+
| hey |
|
45
|
+
|
46
|
+
|
47
|
+
|
data/exe/sqltorial
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "escort"
|
4
|
+
require "bundler/setup"
|
5
|
+
require "sqltorial"
|
6
|
+
|
7
|
+
Escort::App.create do |app|
|
8
|
+
app.version SQLtorial::VERSION
|
9
|
+
app.summary SQLtorial::SUMMARY
|
10
|
+
app.description SQLtorial::DESCRIPTION
|
11
|
+
app.options do |opts|
|
12
|
+
opts.opt :no_results, "Don't Include Results", short: '-n', long: '--no-results', type: :boolean, default: false
|
13
|
+
opts.opt :output, "Output File", short: '-o', long: '--output', type: :string, default: 'output.md'
|
14
|
+
opts.opt :preface, "Preface File", short: '-p', long: '--preface', type: :string, default: 'preface.md'
|
15
|
+
end
|
16
|
+
app.action do |options, arguments|
|
17
|
+
begin
|
18
|
+
SQLtorial::AssembleCommand.new(options, arguments).execute
|
19
|
+
rescue
|
20
|
+
puts $!.message
|
21
|
+
puts $!.backtrace.join("\n")
|
22
|
+
raise
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/sqltorial.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
require_relative 'sql_to_example'
|
2
|
+
require 'sequelizer'
|
3
|
+
require 'facets/pathname/chdir'
|
4
|
+
|
5
|
+
module SQLtorial
|
6
|
+
class AssembleCommand < ::Escort::ActionCommand::Base
|
7
|
+
include Sequelizer
|
8
|
+
def execute
|
9
|
+
process_dir.chdir do
|
10
|
+
preface = Pathname.new(global_options[:preface]) if global_options[:preface]
|
11
|
+
File.open(global_options[:output], 'w') do |f|
|
12
|
+
f.puts preface.read if preface && preface.exist?
|
13
|
+
examples = files.map.with_index do |file, index|
|
14
|
+
Escort::Logger.output.puts "Examplizing #{file.to_s}"
|
15
|
+
SqlToExample.new(file, db, index + 1).to_str(!global_options[:no_results])
|
16
|
+
end
|
17
|
+
f.puts(examples.join("\n\n"))
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def process_dir
|
23
|
+
@process_dir = path.directory? ? path : Pathname.pwd
|
24
|
+
end
|
25
|
+
|
26
|
+
def path
|
27
|
+
@path ||= Pathname.new(arguments.first || ".")
|
28
|
+
end
|
29
|
+
|
30
|
+
def files
|
31
|
+
path.directory? ? Pathname.glob('*.sql') : files_from_file
|
32
|
+
end
|
33
|
+
|
34
|
+
def files_from_file
|
35
|
+
path.readlines.map(&:chomp!).select { |l| l !~ /^\s*#/ && !l.empty? }.map do |file_name|
|
36
|
+
Pathname.new(file_name)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require_relative "directive"
|
2
|
+
|
3
|
+
module SQLtorial
|
4
|
+
class AllDirective
|
5
|
+
REGEXP = /^ DIRECTIVE:\s*ALL/
|
6
|
+
class << self
|
7
|
+
def regexp
|
8
|
+
REGEXP
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(line)
|
13
|
+
end
|
14
|
+
|
15
|
+
def alter(query_to_md)
|
16
|
+
query_to_md.row_limit = nil
|
17
|
+
end
|
18
|
+
|
19
|
+
def inspect
|
20
|
+
"ALL"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
Directive.register(AllDirective)
|
24
|
+
end
|
25
|
+
|
26
|
+
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module SQLtorial
|
2
|
+
class Directive
|
3
|
+
class << self
|
4
|
+
def register(directive_klass)
|
5
|
+
(@@directives ||= []) << directive_klass
|
6
|
+
end
|
7
|
+
|
8
|
+
def match(line)
|
9
|
+
@@directives.any? do |directive_klass|
|
10
|
+
directive_klass.regexp.match(line)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def from_line(line)
|
15
|
+
@@directives.each do |directive_klass|
|
16
|
+
return directive_klass.new(line) if directive_klass.regexp.match(line)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module SQLtorial
|
2
|
+
class ValidColumnDirective
|
3
|
+
REGEXP = /^ DIRECTIVE:\s*(\S+)\s+(\S+)\s+(.+)/
|
4
|
+
attr :column, :op, :matcher
|
5
|
+
def initialize(line)
|
6
|
+
_, column, op, matcher = REGEXP.match(line).to_a
|
7
|
+
@column = column.to_sym
|
8
|
+
@op = op
|
9
|
+
@matcher = Regexp.new(matcher)
|
10
|
+
end
|
11
|
+
|
12
|
+
def validate(result)
|
13
|
+
md = matcher.match(result[column])
|
14
|
+
op == '=' ? !md.nil? : md.nil?
|
15
|
+
end
|
16
|
+
|
17
|
+
def inspect
|
18
|
+
[column, op, matcher].join(" ")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
Directive.register(AllDirective)
|
22
|
+
end
|
@@ -0,0 +1,6 @@
|
|
1
|
+
module SQLtorial
|
2
|
+
VERSION = "0.0.1"
|
3
|
+
SUMMARY = %q{Knitr, but for SQL files, sorta}
|
4
|
+
DESCRIPTION = %q{Ingests a set of commented SQL statements, executes them, and dumps the comments, queries, and results into a markdown file}
|
5
|
+
HOMEPAGE = "http://github.com/outcomesinsights/sqltorial"
|
6
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module SQLtorial
|
2
|
+
class QueryCache
|
3
|
+
attr_reader :query_to_md
|
4
|
+
|
5
|
+
def initialize(query_to_md)
|
6
|
+
@query_to_md = query_to_md
|
7
|
+
end
|
8
|
+
|
9
|
+
def to_md
|
10
|
+
unless cache_file.exist?
|
11
|
+
make_cache_file
|
12
|
+
end
|
13
|
+
cache_file.read
|
14
|
+
end
|
15
|
+
|
16
|
+
def make_cache_file
|
17
|
+
cache_file.dirname.mkpath
|
18
|
+
cache_file.write(query_to_md.get_md)
|
19
|
+
end
|
20
|
+
|
21
|
+
def cache_file
|
22
|
+
@cache_file ||= Pathname.pwd + '.cache' + cache_file_name
|
23
|
+
end
|
24
|
+
|
25
|
+
def cache_file_name
|
26
|
+
@cache_file_name ||= Digest::SHA256.hexdigest("#{input_str}") + ".md"
|
27
|
+
end
|
28
|
+
|
29
|
+
def input_str
|
30
|
+
@input_str ||= %w(query row_limit validation_directives other_directives).inject("") do |s, meth|
|
31
|
+
s + query_to_md.send(meth).inspect
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
require_relative 'query_cache'
|
2
|
+
|
3
|
+
module SQLtorial
|
4
|
+
class QueryToMD
|
5
|
+
attr :validation_directives, :other_directives
|
6
|
+
attr_accessor :query, :row_limit
|
7
|
+
def initialize(query, directives, row_limit = 10)
|
8
|
+
@query = query
|
9
|
+
@validation_directives, @other_directives = directives.partition { |d| d.respond_to?(:validate) }
|
10
|
+
@row_limit = row_limit
|
11
|
+
@other_directives.each do |directive|
|
12
|
+
directive.alter(self)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def row_limit
|
17
|
+
@row_limit ||= count
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_md
|
21
|
+
cache.to_md
|
22
|
+
end
|
23
|
+
|
24
|
+
def get_md
|
25
|
+
return "**No results found.**" if all.empty?
|
26
|
+
output = []
|
27
|
+
output << "Found #{count} results."
|
28
|
+
if count > row_limit
|
29
|
+
output.last << " Displaying first #{row_limit}."
|
30
|
+
end
|
31
|
+
output << ""
|
32
|
+
output << tableize(all.first.keys + additional_headers)
|
33
|
+
output << tableize(orientations_for(all))
|
34
|
+
output_rows.each do |row|
|
35
|
+
output << tableize(process(row.values))
|
36
|
+
end
|
37
|
+
output.join("\n") + "\n\n"
|
38
|
+
end
|
39
|
+
|
40
|
+
def count
|
41
|
+
@count ||= query.from_self.count
|
42
|
+
end
|
43
|
+
|
44
|
+
def all
|
45
|
+
@all ||= begin
|
46
|
+
q = query.from_self
|
47
|
+
q = q.limit(row_limit)
|
48
|
+
q.all
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def additional_headers
|
53
|
+
validation_directives.empty? ? [] : [:valid_row]
|
54
|
+
end
|
55
|
+
|
56
|
+
def output_rows
|
57
|
+
rows = all[0...row_limit]
|
58
|
+
return rows if validation_directives.empty?
|
59
|
+
|
60
|
+
rows.map do |row|
|
61
|
+
row[:valid_row] = validation_directives.all? { |d| d.validate(row) } ? 'Y' : 'N'
|
62
|
+
row
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def tableize(columns)
|
67
|
+
"| #{columns.join(" | ")} |"
|
68
|
+
end
|
69
|
+
|
70
|
+
def processors
|
71
|
+
@processors ||= make_processors
|
72
|
+
end
|
73
|
+
|
74
|
+
def process(columns)
|
75
|
+
processors.map.with_index do |processor, index|
|
76
|
+
value = columns[index]
|
77
|
+
if processor
|
78
|
+
processor.call(value)
|
79
|
+
else
|
80
|
+
value
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def make_processors
|
86
|
+
output_rows.first.map do |name, column|
|
87
|
+
if name.to_s.end_with?('_id')
|
88
|
+
Proc.new do |column|
|
89
|
+
column.to_s.chomp
|
90
|
+
end
|
91
|
+
else
|
92
|
+
case column
|
93
|
+
when Float, BigDecimal
|
94
|
+
Proc.new do |column|
|
95
|
+
sprintf("%.02f", column)
|
96
|
+
end
|
97
|
+
when Numeric, Fixnum
|
98
|
+
Proc.new do |column|
|
99
|
+
commatize(column.to_s)
|
100
|
+
end
|
101
|
+
else
|
102
|
+
Proc.new do |column|
|
103
|
+
column.to_s.chomp
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def orientations_for(dataset)
|
111
|
+
widths = widths_for(dataset)
|
112
|
+
dataset.first.map.with_index do |(_, value), index|
|
113
|
+
case value
|
114
|
+
when Numeric, Fixnum, Float
|
115
|
+
widths[index][-1] = ":"
|
116
|
+
else
|
117
|
+
widths[index][0] = ":"
|
118
|
+
end
|
119
|
+
widths[index]
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def widths_for(dataset)
|
124
|
+
widths = [0] * dataset.first.length
|
125
|
+
dataset.each do |row|
|
126
|
+
widths = row.map.with_index do |value, index|
|
127
|
+
[value.to_s.length, widths[index]].max
|
128
|
+
end
|
129
|
+
end
|
130
|
+
widths.map { |width| '-' * width }
|
131
|
+
end
|
132
|
+
|
133
|
+
def commatize(str)
|
134
|
+
return str unless str =~ /^\d+$/
|
135
|
+
str.reverse.chars.each_slice(3).map(&:join).join(',').reverse
|
136
|
+
end
|
137
|
+
|
138
|
+
def cache
|
139
|
+
@cache ||= QueryCache.new(self)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require_relative "directive"
|
2
|
+
|
3
|
+
module SQLtorial
|
4
|
+
class RegexpDirective
|
5
|
+
REGEXP = /^ DIRECTIVE:\s*(\S+)\s+(\S+)\s+(.+)/
|
6
|
+
class << self
|
7
|
+
def regexp
|
8
|
+
REGEXP
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
attr :column, :op, :matcher
|
13
|
+
def initialize(line)
|
14
|
+
_, column, op, matcher = REGEXP.match(line).to_a
|
15
|
+
@column = column.to_sym
|
16
|
+
@op = op
|
17
|
+
@matcher = Regexp.new(matcher)
|
18
|
+
end
|
19
|
+
|
20
|
+
def validate(result)
|
21
|
+
md = matcher.match(result[column])
|
22
|
+
op == '=' ? !md.nil? : md.nil?
|
23
|
+
end
|
24
|
+
|
25
|
+
def inspect
|
26
|
+
[column, op, matcher].join(" ")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
Directive.register(RegexpDirective)
|
31
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
require 'sqltorial'
|
2
|
+
require_relative 'query_to_md'
|
3
|
+
require_relative 'formatter'
|
4
|
+
|
5
|
+
module SQLtorial
|
6
|
+
class SqlToExample
|
7
|
+
attr :file, :db
|
8
|
+
def initialize(file, db, number)
|
9
|
+
@file = file
|
10
|
+
@db = db
|
11
|
+
@number = number
|
12
|
+
end
|
13
|
+
|
14
|
+
def formatted
|
15
|
+
@formatted ||= `fsqlf -i #{file}`
|
16
|
+
#@formatted ||= `pg_format #{file}`
|
17
|
+
#@formatted ||= `cat #{file} | anbt-sql-formatter`
|
18
|
+
#@formatted ||= `cat #{file} | py_format`
|
19
|
+
#@formatted ||= formatter.format(file.read)
|
20
|
+
#file.read
|
21
|
+
end
|
22
|
+
|
23
|
+
def formatter
|
24
|
+
@formatter ||= Formatter.new
|
25
|
+
end
|
26
|
+
|
27
|
+
def formatted_lines
|
28
|
+
if @formatted_lines.nil?
|
29
|
+
@title_line, @formatted_lines = get_title_and_formatted_lines
|
30
|
+
end
|
31
|
+
@formatted_lines
|
32
|
+
end
|
33
|
+
|
34
|
+
def queries
|
35
|
+
@queries ||= formatted_lines.slice_after { |l| l =~ /;$/ }
|
36
|
+
end
|
37
|
+
|
38
|
+
def title_line
|
39
|
+
if @title_line.nil?
|
40
|
+
@title_line, @formatted_lines = get_title_and_formatted_lines
|
41
|
+
end
|
42
|
+
@title_line
|
43
|
+
end
|
44
|
+
|
45
|
+
def title
|
46
|
+
@title ||= title_line.gsub(/^\s*-+\s*/, '')
|
47
|
+
end
|
48
|
+
|
49
|
+
def make_prose_directives_and_query(query)
|
50
|
+
lines = query.dup
|
51
|
+
prose_lines = []
|
52
|
+
lines.shift while lines.first.strip.empty?
|
53
|
+
prose_lines << lines.shift.sub(/^\s*-+\s*/, ' ').chomp.sub(/^ $/, "\n\n") while lines.first =~ /^\s*(-+|$)/
|
54
|
+
directives, prose_lines = prose_lines.partition { |line| Directive.match(line) }
|
55
|
+
[prose_lines.join(''), process_directives(directives), lines.join("\n")]
|
56
|
+
end
|
57
|
+
|
58
|
+
def number
|
59
|
+
@number ||= file.basename.to_s.to_i
|
60
|
+
end
|
61
|
+
|
62
|
+
def to_str(include_results = true)
|
63
|
+
hash = {}
|
64
|
+
queries.each_with_index do |query, index|
|
65
|
+
prose, directives, sql = make_prose_directives_and_query(query)
|
66
|
+
|
67
|
+
begin
|
68
|
+
if is_create(sql)
|
69
|
+
execute(sql, include_results)
|
70
|
+
hash[sql] = [prose, create_to_md(include_results, sql, directives)];
|
71
|
+
next
|
72
|
+
elsif is_drop(sql)
|
73
|
+
execute(sql, include_results)
|
74
|
+
hash[sql] = [prose, nil];
|
75
|
+
next
|
76
|
+
end
|
77
|
+
hash[sql] = [prose, query_to_md(include_results, sql, directives)]
|
78
|
+
rescue
|
79
|
+
puts sql
|
80
|
+
puts $!.message
|
81
|
+
puts $!.backtrace.join("\n")
|
82
|
+
$stdin.gets
|
83
|
+
end
|
84
|
+
end
|
85
|
+
parts = []
|
86
|
+
parts << "## Example #{number}: #{title}\n"
|
87
|
+
part_num = 0
|
88
|
+
parts += hash.map do |key, value|
|
89
|
+
arr = [value.first]
|
90
|
+
if key && !key.empty?
|
91
|
+
part_num += 1
|
92
|
+
arr << "**Query #{number}.#{part_num}**"
|
93
|
+
arr << "```sql\n#{key}\n```"
|
94
|
+
end
|
95
|
+
arr << value.last
|
96
|
+
arr.join("\n\n")
|
97
|
+
end
|
98
|
+
parts.join("\n") + "\n\n"
|
99
|
+
end
|
100
|
+
|
101
|
+
private
|
102
|
+
def process_directives(directives)
|
103
|
+
directives.map do |line|
|
104
|
+
Directive.from_line(line)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def get_title_and_formatted_lines
|
109
|
+
all_lines = formatted.split("\n")
|
110
|
+
title_line = all_lines.shift
|
111
|
+
[title_line, all_lines]
|
112
|
+
end
|
113
|
+
|
114
|
+
def is_create(sql)
|
115
|
+
sql =~ /^\s*create/i
|
116
|
+
end
|
117
|
+
|
118
|
+
def is_drop(sql)
|
119
|
+
sql =~ /^\s*drop/i
|
120
|
+
end
|
121
|
+
|
122
|
+
def execute(sql, include_results)
|
123
|
+
db.execute(sql) if include_results
|
124
|
+
end
|
125
|
+
|
126
|
+
def create_to_md(include_results, sql, directives)
|
127
|
+
return nil unless include_results
|
128
|
+
table_name = /create\s*(?:temp)?\s*(?:table|view)\s*(\S+)/i.match(sql)[1].gsub('.', '__')
|
129
|
+
QueryToMD.new(db[table_name.to_sym], directives).to_md
|
130
|
+
end
|
131
|
+
|
132
|
+
def query_to_md(include_results, sql, directives)
|
133
|
+
return nil unless include_results
|
134
|
+
return nil if sql.empty?
|
135
|
+
QueryToMD.new(db[sql.sub(';', '')], directives).to_md
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
data/sqltorial.gemspec
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'sqltorial/metadata'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "sqltorial"
|
8
|
+
spec.version = SQLtorial::VERSION
|
9
|
+
spec.authors = ["Ryan Duryea"]
|
10
|
+
spec.email = ["aguynamedryan@gmail.com"]
|
11
|
+
|
12
|
+
spec.summary = SQLtorial::SUMMARY
|
13
|
+
spec.description = SQLtorial::DESCRIPTION
|
14
|
+
spec.homepage = SQLtorial::HOMEPAGE
|
15
|
+
spec.license = 'MIT'
|
16
|
+
|
17
|
+
# Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or
|
18
|
+
# delete this section to allow pushing this gem to any host.
|
19
|
+
if spec.respond_to?(:metadata)
|
20
|
+
spec.metadata['allowed_push_host'] = "https://rubygems.org"
|
21
|
+
else
|
22
|
+
raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
|
23
|
+
end
|
24
|
+
|
25
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
26
|
+
spec.bindir = "exe"
|
27
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
28
|
+
spec.require_paths = ["lib"]
|
29
|
+
|
30
|
+
spec.add_development_dependency "bundler", "~> 1.9"
|
31
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
32
|
+
spec.add_dependency "sequelizer", "~> 0.0.6"
|
33
|
+
spec.add_dependency "anbt-sql-formatter", "~> 0.0.3"
|
34
|
+
spec.add_dependency "facets", "~> 3.0"
|
35
|
+
spec.add_dependency "escort", "~> 0.4.0"
|
36
|
+
end
|
metadata
ADDED
@@ -0,0 +1,155 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sqltorial
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ryan Duryea
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-08-21 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.9'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.9'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: sequelizer
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 0.0.6
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.0.6
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: anbt-sql-formatter
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 0.0.3
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 0.0.3
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: facets
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '3.0'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '3.0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: escort
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 0.4.0
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 0.4.0
|
97
|
+
description: Ingests a set of commented SQL statements, executes them, and dumps the
|
98
|
+
comments, queries, and results into a markdown file
|
99
|
+
email:
|
100
|
+
- aguynamedryan@gmail.com
|
101
|
+
executables:
|
102
|
+
- sqltorial
|
103
|
+
extensions: []
|
104
|
+
extra_rdoc_files: []
|
105
|
+
files:
|
106
|
+
- ".gitignore"
|
107
|
+
- ".travis.yml"
|
108
|
+
- CHANGELOG.md
|
109
|
+
- Gemfile
|
110
|
+
- LICENSE.txt
|
111
|
+
- README.md
|
112
|
+
- Rakefile
|
113
|
+
- bin/console
|
114
|
+
- bin/setup
|
115
|
+
- examples/example_input.sql
|
116
|
+
- examples/example_output.md
|
117
|
+
- exe/sqltorial
|
118
|
+
- lib/sqltorial.rb
|
119
|
+
- lib/sqltorial/assemble_command.rb
|
120
|
+
- lib/sqltorial/directives/all_directive.rb
|
121
|
+
- lib/sqltorial/directives/directive.rb
|
122
|
+
- lib/sqltorial/directives/valid_column_directive.rb
|
123
|
+
- lib/sqltorial/metadata.rb
|
124
|
+
- lib/sqltorial/query_cache.rb
|
125
|
+
- lib/sqltorial/query_to_md.rb
|
126
|
+
- lib/sqltorial/regexp_directive.rb
|
127
|
+
- lib/sqltorial/sql_to_example.rb
|
128
|
+
- sqltorial.gemspec
|
129
|
+
homepage: http://github.com/outcomesinsights/sqltorial
|
130
|
+
licenses:
|
131
|
+
- MIT
|
132
|
+
metadata:
|
133
|
+
allowed_push_host: https://rubygems.org
|
134
|
+
post_install_message:
|
135
|
+
rdoc_options: []
|
136
|
+
require_paths:
|
137
|
+
- lib
|
138
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
139
|
+
requirements:
|
140
|
+
- - ">="
|
141
|
+
- !ruby/object:Gem::Version
|
142
|
+
version: '0'
|
143
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
144
|
+
requirements:
|
145
|
+
- - ">="
|
146
|
+
- !ruby/object:Gem::Version
|
147
|
+
version: '0'
|
148
|
+
requirements: []
|
149
|
+
rubyforge_project:
|
150
|
+
rubygems_version: 2.4.6
|
151
|
+
signing_key:
|
152
|
+
specification_version: 4
|
153
|
+
summary: Knitr, but for SQL files, sorta
|
154
|
+
test_files: []
|
155
|
+
has_rdoc:
|