├── .gitignore ├── .rspec ├── .rubocop.yml ├── .ruby-gemset ├── .travis.yml ├── .yardopts ├── AST_FORMAT.md ├── CONTRIBUTING.md ├── Gemfile ├── Gemfile.devtools ├── Guardfile ├── LICENSE ├── README.md ├── Rakefile ├── TODO ├── config ├── devtools.yml ├── flay.yml ├── flog.yml ├── mutant.yml ├── reek.yml ├── rubocop.yml └── yardstick.yml ├── lib ├── sql.rb └── sql │ ├── ast │ └── node.rb │ ├── constants.rb │ ├── fuzzer.rb │ ├── generator.rb │ ├── generator │ ├── emitter.rb │ ├── emitter │ │ ├── binary_connective_operation.rb │ │ ├── binary_infix_operation.rb │ │ ├── conditional_parenthesis.rb │ │ ├── delete.rb │ │ ├── delimited.rb │ │ ├── dsl.rb │ │ ├── fields.rb │ │ ├── identifier.rb │ │ ├── insert.rb │ │ ├── join.rb │ │ ├── literal.rb │ │ ├── literal │ │ │ ├── date.rb │ │ │ ├── datetime.rb │ │ │ ├── decimal.rb │ │ │ ├── float.rb │ │ │ ├── integer.rb │ │ │ ├── singleton.rb │ │ │ ├── string.rb │ │ │ └── time.rb │ │ ├── predicate.rb │ │ ├── root.rb │ │ ├── select.rb │ │ ├── set.rb │ │ ├── sort_operation.rb │ │ ├── tuple.rb │ │ ├── unary_function_operation.rb │ │ ├── unary_prefix_operation.rb │ │ ├── update.rb │ │ └── update_set.rb │ ├── registry.rb │ └── stream.rb │ ├── node_helper.rb │ ├── parser.rb │ └── version.rb ├── spec ├── shared │ └── emitter_context.rb ├── spec_helper.rb ├── support │ └── emitter_helper.rb └── unit │ └── sql │ ├── ast │ └── node │ │ └── to_sql_spec.rb │ ├── fuzzer │ └── each_spec.rb │ └── generator │ ├── class_methods │ └── generate_spec.rb │ ├── emitter │ └── class_methods │ │ └── visit_spec.rb │ └── stream │ ├── append_spec.rb │ ├── indent_spec.rb │ ├── indented │ └── unindent_spec.rb │ ├── nl_spec.rb │ └── output_spec.rb └── sql.gemspec /.gitignore: -------------------------------------------------------------------------------- 1 | ## MAC OS 2 | .DS_Store 3 | 4 | ## TEXTMATE 5 | *.tmproj 6 | tmtags 7 | 8 | ## EMACS 9 | *~ 10 | \#* 11 | .\#* 12 | 13 | ## VIM 14 | *.sw[op] 15 | 16 | ## Rubinius 17 | *.rbc 18 | .rbx 19 | 20 | ## PROJECT::GENERAL 21 | *.gem 22 | coverage 23 | profiling 24 | turbulence 25 | rdoc 26 | pkg 27 | tmp 28 | doc 29 | log 30 | .yardoc 31 | measurements 32 | 33 | ## BUNDLER 34 | .bundle 35 | Gemfile.lock 36 | 37 | ## PROJECT::SPECIFIC 38 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format progress 3 | --profile 4 | --order random 5 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | Includes: 3 | - 'Gemfile' 4 | Excludes: 5 | - 'Gemfile.devtools' 6 | - 'vendor/**' 7 | -------------------------------------------------------------------------------- /.ruby-gemset: -------------------------------------------------------------------------------- 1 | sql 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | bundler_args: --without yard guard benchmarks 3 | script: "bundle exec rake ci:metrics" 4 | rvm: 5 | - 1.9.3 6 | - 2.0.0 7 | - 2.1.0 8 | - ruby-head 9 | - rbx 10 | matrix: 11 | include: 12 | - rvm: jruby-19mode 13 | env: JRUBY_OPTS="$JRUBY_OPTS --debug" # for simplecov 14 | - rvm: jruby-20mode 15 | env: JRUBY_OPTS="$JRUBY_OPTS --debug" # for simplecov 16 | - rvm: jruby-21mode 17 | env: JRUBY_OPTS="$JRUBY_OPTS --debug" # for simplecov 18 | - rvm: jruby-head 19 | env: JRUBY_OPTS="$JRUBY_OPTS --debug" # for simplecov 20 | allow_failures: 21 | - rvm: jruby-19mode # inconsistent travis-ci failures 22 | - rvm: jruby-20mode # inconsistent travis-ci failures 23 | - rvm: jruby-21mode # inconsistent travis-ci failures 24 | - rvm: jruby-head # inconsistent travis-ci failures 25 | fast_finish: true 26 | notifications: 27 | irc: 28 | channels: 29 | - irc.freenode.org#rom-rb 30 | on_success: never 31 | on_failure: change 32 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --quiet 2 | README.md 3 | lib/**/*.rb 4 | LICENSE 5 | -------------------------------------------------------------------------------- /AST_FORMAT.md: -------------------------------------------------------------------------------- 1 | AST Format 2 | ========== 3 | 4 | ## Literals 5 | 6 | ### Singletons 7 | 8 | Format: 9 | 10 | ~~~ 11 | (true) 12 | "TRUE" 13 | 14 | (false) 15 | "FALSE" 16 | 17 | (null) 18 | "NULL" 19 | ~~~ 20 | 21 | ### String 22 | 23 | ~~~ 24 | (string "foo") 25 | "'foo'" 26 | 27 | # Quoted delimiter 28 | (string "fo'o") 29 | "'fo''o'" 30 | ~~~ 31 | 32 | ### Integer 33 | 34 | ~~~ 35 | (int 1) 36 | "1" 37 | ~~~ 38 | 39 | ## Unary scalar operators 40 | 41 | ~~~ 42 | (uplus (int 1)) 43 | "+1" 44 | 45 | (uminus (int 1)) 46 | "-1" 47 | 48 | (not true) 49 | "NOT TRUE" 50 | ~~~ 51 | 52 | ## Binary scalar operators 53 | 54 | ~~~ 55 | (add (int 1) (int 1)) 56 | "1 + 1" 57 | ~~~ 58 | 59 | ~~~ 60 | (sub (int 1) (int 1)) 61 | "1 - 1" 62 | ~~~ 63 | 64 | ~~~ 65 | (div (int 1) (int 1)) 66 | "1 / 1" 67 | ~~~ 68 | 69 | ~~~ 70 | (mul (int 1) (int 1)) 71 | "1 * 1" 72 | ~~~ 73 | 74 | ## Binary boolean operators 75 | 76 | ~~~ 77 | (and left right) 78 | "left AND right" 79 | ~~~ 80 | 81 | ~~~ 82 | (or left right) 83 | "left OR right" 84 | ~~~ 85 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ------------ 3 | 4 | * If you want your code merged into the mainline, please discuss the proposed changes with me before doing any work on it. This library is still in early development, and the direction it is going may not always be clear. Some features may not be appropriate yet, may need to be deferred until later when the foundation for them is laid, or may be more applicable in a plugin. 5 | * Fork the project. 6 | * Make your feature addition or bug fix. 7 | * Follow this [style guide](https://github.com/dkubb/styleguide). 8 | * Add specs for it. This is important so I don't break it in a future version unintentionally. Tests must cover all branches within the code, and code must be fully covered. 9 | * Commit, do not mess with Rakefile, version, or history. (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull) 10 | * Run "rake ci". This must pass and not show any regressions in the metrics for the code to be merged. 11 | * Send me a pull request. Bonus points for topic branches. 12 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | source 'https://rubygems.org' 4 | 5 | gemspec 6 | 7 | platform :rbx do 8 | gem 'rubysl-bigdecimal', '~> 2.0.2' 9 | end 10 | 11 | group :development, :test do 12 | gem 'devtools', git: 'https://github.com/rom-rb/devtools.git' 13 | end 14 | 15 | eval_gemfile 'Gemfile.devtools' 16 | -------------------------------------------------------------------------------- /Gemfile.devtools: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | group :development do 4 | gem 'rake', '~> 10.1.0' 5 | gem 'rspec', '~> 2.14.1' 6 | gem 'yard', '~> 0.8.7' 7 | 8 | platform :rbx do 9 | gem 'rubysl-singleton', '~> 2.0.0' 10 | end 11 | end 12 | 13 | group :yard do 14 | gem 'kramdown', '~> 1.3.0' 15 | end 16 | 17 | group :guard do 18 | gem 'guard', '~> 2.2.4' 19 | gem 'guard-bundler', '~> 2.0.0' 20 | gem 'guard-rspec', '~> 4.2.0' 21 | gem 'guard-rubocop', '~> 1.0.0' 22 | 23 | # file system change event handling 24 | gem 'listen', '~> 2.4.0' 25 | gem 'rb-fchange', '~> 0.0.6', require: false 26 | gem 'rb-fsevent', '~> 0.9.3', require: false 27 | gem 'rb-inotify', '~> 0.9.0', require: false 28 | 29 | # notification handling 30 | gem 'libnotify', '~> 0.8.0', require: false 31 | gem 'rb-notifu', '~> 0.0.4', require: false 32 | gem 'terminal-notifier-guard', '~> 1.5.3', require: false 33 | end 34 | 35 | group :metrics do 36 | gem 'coveralls', '~> 0.7.0' 37 | gem 'flay', '~> 2.4.0' 38 | gem 'flog', '~> 4.2.0' 39 | gem 'reek', '~> 1.3.2' 40 | gem 'rubocop', '~> 0.16.0' 41 | gem 'simplecov', '~> 0.8.2' 42 | gem 'yardstick', '~> 0.9.7', git: 'https://github.com/dkubb/yardstick.git' 43 | 44 | platforms :mri do 45 | gem 'mutant', '~> 0.3.4' 46 | end 47 | 48 | platforms :ruby_19, :ruby_20 do 49 | gem 'yard-spellcheck', '~> 0.1.5' 50 | end 51 | 52 | platform :rbx do 53 | gem 'json', '~> 1.8.1' 54 | gem 'racc', '~> 1.4.10' 55 | gem 'rubysl-logger', '~> 2.0.0' 56 | gem 'rubysl-open-uri', '~> 2.0.0' 57 | gem 'rubysl-prettyprint', '~> 2.0.2' 58 | end 59 | end 60 | 61 | group :benchmarks do 62 | gem 'rbench', '~> 0.2.3' 63 | end 64 | 65 | platform :jruby do 66 | group :jruby do 67 | gem 'jruby-openssl', '~> 0.8.5' 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | guard :bundler do 4 | watch('Gemfile') 5 | watch('Gemfile.lock') 6 | watch(%w{.+.gemspec\z}) 7 | end 8 | 9 | guard :rspec, cli: File.read('.rspec').split.push('--fail-fast').join(' '), keep_failed: false do 10 | # Run all specs if configuration is modified 11 | watch('.rspec') { 'spec' } 12 | watch('Guardfile') { 'spec' } 13 | watch('Gemfile.lock') { 'spec' } 14 | watch('spec/spec_helper.rb') { 'spec' } 15 | 16 | # Run all specs if supporting files files are modified 17 | watch(%r{\Aspec/(?:fixtures|lib|support|shared)/.+\.rb\z}) { 'spec' } 18 | 19 | # Run unit specs if associated lib code is modified 20 | watch(%r{\Alib/(.+)\.rb\z}) { |m| Dir["spec/unit/#{m[1]}*"] } 21 | watch(%r{\Alib/(.+)/support/(.+)\.rb\z}) { |m| Dir["spec/unit/#{m[1]}/#{m[2]}*"] } 22 | watch("lib/#{File.basename(File.expand_path('../', __FILE__))}.rb") { 'spec' } 23 | 24 | # Run a spec if it is modified 25 | watch(%r{\Aspec/(?:unit|integration)/.+_spec\.rb\z}) 26 | end 27 | 28 | guard :rubocop, cli: %w[--config config/rubocop.yml] do 29 | watch(%r{.+\.(?:rb|rake)\z}) 30 | watch(%r{\Aconfig/rubocop\.yml\z}) { |m| File.dirname(m[0]) } 31 | watch(%r{(?:.+/)?\.rubocop\.yml\z}) { |m| File.dirname(m[0]) } 32 | end 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Dan Kubb 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sql.rb 2 | 3 | SQL Parser and Generator for Ruby 4 | 5 | [![Gem Version](https://badge.fury.io/rb/sql.png)][gem] 6 | [![Build Status](https://secure.travis-ci.org/dkubb/sql.png?branch=master)][travis] 7 | [![Dependency Status](https://gemnasium.com/dkubb/sql.png)][gemnasium] 8 | [![Code Climate](https://codeclimate.com/github/dkubb/sql.png)][codeclimate] 9 | [![Coverage Status](https://coveralls.io/repos/dkubb/sql/badge.png?branch=master)][coveralls] 10 | 11 | [gem]: https://rubygems.org/gems/sql 12 | [travis]: https://travis-ci.org/dkubb/sql 13 | [gemnasium]: https://gemnasium.com/dkubb/sql 14 | [codeclimate]: https://codeclimate.com/github/dkubb/sql 15 | [coveralls]: https://coveralls.io/r/dkubb/sql 16 | 17 | ## Contributing 18 | 19 | See [CONTRIBUTING.md](CONTRIBUTING.md) for details. 20 | 21 | ## Copyright 22 | 23 | Copyright © 2013 Dan Kubb. See LICENSE for details. 24 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'devtools' 4 | 5 | Devtools.init_rake_tasks 6 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkubb/sql/c690ace4afac1aa177f0989cabfdbff64ed461a6/TODO -------------------------------------------------------------------------------- /config/devtools.yml: -------------------------------------------------------------------------------- 1 | --- 2 | unit_test_timeout: 1.0 3 | -------------------------------------------------------------------------------- /config/flay.yml: -------------------------------------------------------------------------------- 1 | --- 2 | threshold: 28 3 | total_score: 179 4 | -------------------------------------------------------------------------------- /config/flog.yml: -------------------------------------------------------------------------------- 1 | --- 2 | threshold: 13.7 3 | -------------------------------------------------------------------------------- /config/mutant.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: sql 3 | namespace: SQL 4 | -------------------------------------------------------------------------------- /config/reek.yml: -------------------------------------------------------------------------------- 1 | --- 2 | Attribute: 3 | enabled: true 4 | exclude: 5 | - SQL::Generator::Emitter 6 | - SQL::Generator::Stream 7 | BooleanParameter: 8 | enabled: true 9 | exclude: [] 10 | ClassVariable: 11 | enabled: true 12 | exclude: 13 | - SQL::Generator::Emitter 14 | ControlParameter: 15 | enabled: true 16 | exclude: [] 17 | DataClump: 18 | enabled: true 19 | exclude: [] 20 | max_copies: 2 21 | min_clump_size: 2 22 | DuplicateMethodCall: 23 | enabled: true 24 | exclude: 25 | - SQL::Generator::Emitter#indented 26 | - SQL::Generator::Emitter::BinaryInfixOperation#dispatch 27 | - SQL::Generator::Emitter::Delete#dispatch 28 | - SQL::Generator::Emitter::Select#dispatch 29 | - SQL::Generator::Emitter::Update#dispatch 30 | - SQL::Generator::Stream::Indented#<< 31 | max_calls: 1 32 | allow_calls: [] 33 | FeatureEnvy: 34 | enabled: true 35 | exclude: 36 | - SQL::UnknownTypeError#initialize 37 | IrresponsibleModule: 38 | enabled: true 39 | exclude: [] 40 | LongParameterList: 41 | enabled: true 42 | exclude: 43 | - SQL::Generator::Emitter#write_node 44 | - SQL::Generator::Emitter::DSL#visit 45 | max_params: 2 46 | overrides: 47 | initialize: 48 | max_params: 3 49 | LongYieldList: 50 | enabled: true 51 | exclude: [] 52 | max_params: 2 53 | NestedIterators: 54 | enabled: true 55 | exclude: 56 | - SQL::Generator::Emitter#self.children # metaprogramming 57 | - SQL::Generator::Emitter::Set#dispatch # parenthesis inside a loop 58 | max_allowed_nesting: 1 59 | ignore_iterators: [] 60 | NilCheck: 61 | enabled: true 62 | exclude: [] 63 | RepeatedConditional: 64 | enabled: true 65 | exclude: [] 66 | max_ifs: 1 67 | TooManyInstanceVariables: 68 | enabled: true 69 | exclude: 70 | - SQL::Generator::Emitter # incorrectly counts class ivar 71 | max_instance_variables: 3 72 | TooManyMethods: 73 | enabled: true 74 | exclude: [] 75 | max_methods: 10 76 | TooManyStatements: 77 | enabled: true 78 | max_statements: 7 79 | UncommunicativeMethodName: 80 | enabled: true 81 | exclude: 82 | - SQL::NodeHelper#s 83 | reject: 84 | - !ruby/regexp /^[a-z]$/ 85 | - !ruby/regexp /[0-9]$/ 86 | - !ruby/regexp /[A-Z]/ 87 | accept: [] 88 | UncommunicativeModuleName: 89 | enabled: true 90 | exclude: [] 91 | reject: 92 | - !ruby/regexp /^.$/ 93 | - !ruby/regexp /[0-9]$/ 94 | accept: [] 95 | UncommunicativeParameterName: 96 | enabled: true 97 | exclude: [] 98 | reject: 99 | - !ruby/regexp /^.$/ 100 | - !ruby/regexp /[0-9]$/ 101 | - !ruby/regexp /[A-Z]/ 102 | accept: [] 103 | UncommunicativeVariableName: 104 | enabled: true 105 | exclude: [] 106 | reject: 107 | - !ruby/regexp /^.$/ 108 | - !ruby/regexp /[0-9]$/ 109 | - !ruby/regexp /[A-Z]/ 110 | accept: [] 111 | UnusedParameters: 112 | enabled: true 113 | exclude: [] 114 | UtilityFunction: 115 | enabled: true 116 | exclude: 117 | - SQL::NodeHelper#s 118 | - SQL::UnknownTypeError#initialize 119 | max_helper_calls: 0 120 | -------------------------------------------------------------------------------- /config/rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: ../.rubocop.yml 2 | 3 | # Avoid parameter lists longer than five parameters. 4 | ParameterLists: 5 | Max: 3 6 | CountKeywordArgs: true 7 | 8 | # Avoid more than `Max` levels of nesting. 9 | BlockNesting: 10 | Max: 3 11 | 12 | # Align with the style guide. 13 | CollectionMethods: 14 | PreferredMethods: 15 | collect: 'map' 16 | inject: 'reduce' 17 | find: 'detect' 18 | find_all: 'select' 19 | 20 | # Do not force public/protected/private keyword to be indented at the same 21 | # level as the def keyword. My personal preference is to outdent these keywords 22 | # because I think when scanning code it makes it easier to identify the 23 | # sections of code and visually separate them. When the keyword is at the same 24 | # level I think it sort of blends in with the def keywords and makes it harder 25 | # to scan the code and see where the sections are. 26 | AccessModifierIndentation: 27 | Enabled: false 28 | 29 | # Limit line length 30 | LineLength: 31 | Max: 79 32 | 33 | # Disable documentation checking until a class needs to be documented once 34 | Documentation: 35 | Enabled: false 36 | 37 | # Do not always use &&/|| instead of and/or. 38 | AndOr: 39 | Enabled: false 40 | 41 | # Do not favor modifier if/unless usage when you have a single-line body 42 | IfUnlessModifier: 43 | Enabled: false 44 | 45 | # Allow case equality operator (in limited use within the specs) 46 | CaseEquality: 47 | Enabled: false 48 | 49 | # Constants do not always have to use SCREAMING_SNAKE_CASE 50 | ConstantName: 51 | Enabled: false 52 | 53 | # Not all trivial readers/writers can be defined with attr_* methods 54 | TrivialAccessors: 55 | Enabled: false 56 | 57 | # Allow empty lines around body 58 | EmptyLinesAroundBody: 59 | Enabled: false 60 | -------------------------------------------------------------------------------- /config/yardstick.yml: -------------------------------------------------------------------------------- 1 | --- 2 | threshold: 100 3 | -------------------------------------------------------------------------------- /lib/sql.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'singleton' 4 | 5 | require 'ast' 6 | require 'ice_nine' 7 | require 'adamantium' 8 | require 'abstract_type' 9 | 10 | # Library namespace 11 | module SQL 12 | 13 | # Raised when a node type is unknown 14 | class UnknownTypeError < ArgumentError 15 | 16 | # Initialize the unknown type error exception 17 | # 18 | # @param [Symbol] type 19 | # 20 | # @return [undefined] 21 | # 22 | # @api private 23 | def initialize(type) 24 | super("No emitter for node: #{type.inspect}") 25 | end 26 | 27 | end # UnknownTypeError 28 | end # SQL 29 | 30 | require 'sql/ast/node' 31 | require 'sql/fuzzer' 32 | 33 | require 'sql/constants' 34 | require 'sql/generator' 35 | 36 | require 'sql/generator/stream' 37 | require 'sql/generator/registry' 38 | 39 | require 'sql/generator/emitter/dsl' 40 | require 'sql/generator/emitter/conditional_parenthesis' 41 | 42 | require 'sql/generator/emitter' 43 | require 'sql/generator/emitter/root' 44 | 45 | require 'sql/generator/emitter/literal' 46 | require 'sql/generator/emitter/literal/date' 47 | require 'sql/generator/emitter/literal/datetime' 48 | require 'sql/generator/emitter/literal/decimal' 49 | require 'sql/generator/emitter/literal/float' 50 | require 'sql/generator/emitter/literal/integer' 51 | require 'sql/generator/emitter/literal/singleton' 52 | require 'sql/generator/emitter/literal/string' 53 | require 'sql/generator/emitter/literal/time' 54 | 55 | require 'sql/generator/emitter/binary_connective_operation' 56 | require 'sql/generator/emitter/binary_infix_operation' 57 | require 'sql/generator/emitter/unary_function_operation' 58 | require 'sql/generator/emitter/unary_prefix_operation' 59 | 60 | require 'sql/generator/emitter/identifier' 61 | require 'sql/generator/emitter/delimited' 62 | require 'sql/generator/emitter/fields' 63 | require 'sql/generator/emitter/predicate' 64 | require 'sql/generator/emitter/sort_operation' 65 | require 'sql/generator/emitter/tuple' 66 | require 'sql/generator/emitter/update_set' 67 | 68 | require 'sql/generator/emitter/insert' 69 | require 'sql/generator/emitter/delete' 70 | require 'sql/generator/emitter/update' 71 | require 'sql/generator/emitter/select' 72 | 73 | require 'sql/generator/emitter/set' 74 | require 'sql/generator/emitter/join' 75 | 76 | require 'sql/parser' 77 | require 'sql/version' 78 | require 'sql/node_helper' 79 | 80 | # Finalize the emitter dispatch table 81 | SQL::Generator::Emitter.finalize 82 | -------------------------------------------------------------------------------- /lib/sql/ast/node.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module SQL 4 | module AST 5 | 6 | # An SQL abstract syntax tree node 7 | class Node < ::AST::Node 8 | 9 | # The AST node in SQL form 10 | # 11 | # @example 12 | # node.to_sql # => 'SELECT * FROM users' 13 | # 14 | # @return [String] 15 | # 16 | # @api public 17 | def to_sql 18 | Generator.generate(self) 19 | end 20 | 21 | end # Node 22 | end # AST 23 | end # SQL 24 | -------------------------------------------------------------------------------- /lib/sql/constants.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module SQL 4 | 5 | # SQL string constants 6 | # 7 | # @private 8 | module Constants 9 | 10 | # Keywords 11 | K_TRUE = 'TRUE'.freeze 12 | K_FALSE = 'FALSE'.freeze 13 | K_NULL = 'NULL'.freeze 14 | K_SELECT = 'SELECT'.freeze 15 | K_WHERE = 'WHERE'.freeze 16 | K_FROM = 'FROM'.freeze 17 | K_GROUP_BY = 'GROUP BY'.freeze 18 | K_HAVING = 'HAVING'.freeze 19 | K_ORDER_BY = 'ORDER BY'.freeze 20 | K_UPDATE = 'UPDATE'.freeze 21 | K_SET = 'SET'.freeze 22 | K_INSERT = 'INSERT INTO'.freeze 23 | K_VALUES = 'VALUES'.freeze 24 | K_DELETE = 'DELETE FROM'.freeze 25 | 26 | # Unary Prefix Operators 27 | O_NEGATION = 'NOT'.freeze 28 | O_ON = 'ON'.freeze 29 | O_USING = 'USING'.freeze 30 | 31 | # Unary Function Operators 32 | O_COUNT = 'COUNT'.freeze 33 | O_SUM = 'SUM'.freeze 34 | O_MIN = 'MIN'.freeze 35 | O_MAX = 'MAX'.freeze 36 | O_AVG = 'AVG'.freeze 37 | O_VAR = 'VAR_POP'.freeze 38 | O_STDDEV = 'STDDEV_POP'.freeze 39 | O_SQRT = 'SQRT'.freeze 40 | O_ABS = 'ABS'.freeze 41 | O_LENGTH = 'LENGTH'.freeze 42 | 43 | # Binary Infix Operators 44 | O_AND = 'AND'.freeze 45 | O_OR = 'OR'.freeze 46 | O_IN = 'IN'.freeze 47 | O_BETWEEN = 'BETWEEN'.freeze 48 | O_PLUS = '+'.freeze 49 | O_MINUS = '-'.freeze 50 | O_MULTIPLY = '*'.freeze 51 | O_DIVIDE = '/'.freeze 52 | O_MOD = '%'.freeze 53 | O_POW = '^'.freeze 54 | O_IS = 'IS'.freeze 55 | O_EQ = '='.freeze 56 | O_NE = '<>'.freeze 57 | O_GT = '>'.freeze 58 | O_GTE = '>='.freeze 59 | O_LT = '<'.freeze 60 | O_LTE = '<='.freeze 61 | O_CONCAT = '||'.freeze 62 | O_AS = 'AS'.freeze 63 | 64 | # Set Operators 65 | O_EXCEPT = 'EXCEPT'.freeze 66 | O_INTERSECT = 'INTERSECT'.freeze 67 | O_UNION = 'UNION'.freeze 68 | 69 | # Join Operators 70 | O_JOIN = 'JOIN'.freeze 71 | O_LEFT_JOIN = 'LEFT JOIN'.freeze 72 | O_RIGHT_JOIN = 'RIGHT JOIN'.freeze 73 | O_FULL_JOIN = 'FULL JOIN'.freeze 74 | O_NATURAL_JOIN = 'NATURAL JOIN'.freeze 75 | O_CROSS_JOIN = 'CROSS JOIN'.freeze 76 | 77 | # Sort Operators 78 | O_ASC = 'ASC'.freeze 79 | O_DESC = 'DESC'.freeze 80 | 81 | # Delimiters 82 | D_QUOTE = %q['].freeze 83 | D_ESCAPED_QUOTE = %q[''].freeze 84 | D_DBL_QUOTE = '"'.freeze 85 | D_ESCAPED_DBL_QUOTE = '""'.freeze 86 | D_PERIOD = '.'.freeze 87 | D_COMMA = ','.freeze 88 | 89 | BRACES_L = '{'.freeze 90 | BRACES_R = '}'.freeze 91 | PARENTHESIS_L = '('.freeze 92 | PARENTHESIS_R = ')'.freeze 93 | 94 | WS = ' '.freeze 95 | NL = "\n".freeze 96 | 97 | EMPTY_STRING = ''.freeze 98 | 99 | end # Constants 100 | end # SQL 101 | -------------------------------------------------------------------------------- /lib/sql/fuzzer.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module SQL 4 | 5 | # An SQL fuzzer 6 | class Fuzzer 7 | include Enumerable 8 | 9 | # Initialize a Fuzzer 10 | # 11 | # @param [#to_ast] ast 12 | # 13 | # @return [undefined] 14 | # 15 | # @api public 16 | def initialize(ast) 17 | @ast = ast.to_ast 18 | end 19 | 20 | # Iterate over each ast in the set 21 | # 22 | # @example 23 | # fuzzer = SQL::Fuzzer.new(ast) 24 | # fuzzer.each { |ast| ... } 25 | # 26 | # @yield [ast] 27 | # 28 | # @yieldparam [SQL::AST::Node] ast 29 | # each ast in the set 30 | # 31 | # @return [self] 32 | # 33 | # @api public 34 | def each 35 | return to_enum unless block_given? 36 | # TODO: yield 0 or more mutated AST objects 37 | yield @ast 38 | self 39 | end 40 | 41 | end # Fuzzer 42 | end # SQL 43 | -------------------------------------------------------------------------------- /lib/sql/generator.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module SQL 4 | 5 | # Namepsace for SQL generator 6 | module Generator 7 | 8 | # Return generated sql statement for node 9 | # 10 | # @param [AST::Node] node 11 | # 12 | # @return [String] 13 | # 14 | # @api private 15 | def self.generate(node) 16 | stream = Stream.new 17 | Emitter.visit(node, stream) 18 | stream.output 19 | end 20 | 21 | end # Generator 22 | end # SQL 23 | -------------------------------------------------------------------------------- /lib/sql/generator/emitter.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module SQL 4 | module Generator 5 | 6 | # Emitter base class 7 | class Emitter 8 | extend DSL 9 | include Adamantium::Flat, AbstractType, Constants 10 | 11 | # Default delimiter 12 | DEFAULT_DELIMITER = D_COMMA + WS 13 | 14 | # Regitry of Emitter subclasses by node type 15 | @registry = Registry.new 16 | 17 | protected 18 | 19 | # The node type 20 | # 21 | # @return [Symbol] 22 | # 23 | # @api private 24 | def node_type 25 | node.type 26 | end 27 | 28 | private 29 | 30 | # Return node 31 | # 32 | # @return [Parser::AST::Node] 33 | # 34 | # @api private 35 | attr_reader :node 36 | 37 | # Return stream 38 | # 39 | # @return [Stream] 40 | # 41 | # @api private 42 | attr_reader :stream 43 | 44 | # Parent node 45 | # 46 | # @return [Emitter] 47 | # 48 | # @api private 49 | attr_reader :parent 50 | 51 | # Initialize object 52 | # 53 | # @param [Parser::AST::Node] node 54 | # @param [Stream] stream 55 | # @param [Emitter] parent 56 | # 57 | # @return [undefined] 58 | # 59 | # @api private 60 | def initialize(node, stream, parent = Root.instance) 61 | @node, @stream, @parent = node, stream, parent 62 | dispatch 63 | end 64 | 65 | # Emit contents of block within parenthesis 66 | # 67 | # @return [undefined] 68 | # 69 | # @api private 70 | def parenthesis 71 | write(PARENTHESIS_L) 72 | yield 73 | write(PARENTHESIS_R) 74 | end 75 | 76 | # Dispatch helper 77 | # 78 | # @param [Parser::AST::Node] node 79 | # 80 | # @return [undefined] 81 | # 82 | # @api private 83 | def visit(node) 84 | self.class.visit(node, stream, self) 85 | end 86 | 87 | # Emit delimited body 88 | # 89 | # @param [Enumerable] nodes 90 | # @param [String] delimiter 91 | # 92 | # @return [undefined] 93 | # 94 | # @api private 95 | def delimited(nodes, delimiter = DEFAULT_DELIMITER) 96 | head, *tail = nodes 97 | visit(head) 98 | tail.each do |node| 99 | write(delimiter) 100 | visit(node) 101 | end 102 | end 103 | 104 | # Return children of node 105 | # 106 | # @return [Array] 107 | # 108 | # @api private 109 | def children 110 | node.children 111 | end 112 | 113 | # Write strings into stream 114 | # 115 | # @return [undefined] 116 | # 117 | # @api private 118 | def write(*strings) 119 | strings.each(&stream.method(:<<)) 120 | end 121 | 122 | # Write the command 123 | # 124 | # @return [undefined] 125 | # 126 | # @api private 127 | def write_command(node, keyword) 128 | write_node(node, keyword, EMPTY_STRING) 129 | end 130 | 131 | # Write the node if it exists 132 | # 133 | # @return [undefined] 134 | # 135 | # @api private 136 | def write_node(node, keyword, prefix = WS) 137 | write(prefix, keyword, WS) 138 | visit(node) 139 | end 140 | 141 | end # Emitter 142 | end # Generator 143 | end # SQL 144 | -------------------------------------------------------------------------------- /lib/sql/generator/emitter/binary_connective_operation.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module SQL 4 | module Generator 5 | class Emitter 6 | 7 | # Connective operation emitter base class 8 | class ConnectiveOperation < self 9 | include ConditionalParenthesis 10 | 11 | TYPES = IceNine.deep_freeze( 12 | and: O_AND, 13 | or: O_OR, 14 | ) 15 | 16 | handle(*TYPES.keys) 17 | 18 | children :left, :right 19 | 20 | private 21 | 22 | # Perform dispatch 23 | # 24 | # @return [undefined] 25 | # 26 | # @api private 27 | def dispatch 28 | parenthesis do 29 | visit(left) 30 | write(WS, TYPES.fetch(node_type), WS) 31 | visit(right) 32 | end 33 | end 34 | 35 | # Test if the connective needs to be parenthesized 36 | # 37 | # @return [Boolean] 38 | # 39 | # @api private 40 | def parenthesize? 41 | kind_of?(parent.class) && parent.node_type != node_type 42 | end 43 | 44 | end # ConnectiveOperation 45 | end # Emitter 46 | end # Generator 47 | end # SQL 48 | -------------------------------------------------------------------------------- /lib/sql/generator/emitter/binary_infix_operation.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module SQL 4 | module Generator 5 | class Emitter 6 | 7 | # Binary infix operation emitter base class 8 | class BinaryInfixOperation < self 9 | 10 | TYPES = IceNine.deep_freeze( 11 | in: O_IN, 12 | between: O_BETWEEN, 13 | add: O_PLUS, 14 | sub: O_MINUS, 15 | mul: O_MULTIPLY, 16 | div: O_DIVIDE, 17 | mod: O_MOD, 18 | pow: O_POW, 19 | is: O_IS, 20 | eq: O_EQ, 21 | ne: O_NE, 22 | gt: O_GT, 23 | gte: O_GTE, 24 | lt: O_LT, 25 | lte: O_LTE, 26 | concat: O_CONCAT, 27 | as: O_AS 28 | ) 29 | 30 | handle(*TYPES.keys) 31 | 32 | children :left, :right 33 | 34 | private 35 | 36 | # Perform dispatch 37 | # 38 | # @return [undefined] 39 | # 40 | # @api private 41 | def dispatch 42 | visit(left) 43 | write(WS, TYPES.fetch(node_type), WS) 44 | visit(right) 45 | end 46 | 47 | end # BinaryInfixOperation 48 | end # Emitter 49 | end # Generator 50 | end # SQL 51 | -------------------------------------------------------------------------------- /lib/sql/generator/emitter/conditional_parenthesis.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module SQL 4 | module Generator 5 | class Emitter 6 | 7 | # Add conditional parenthesization to an emitter 8 | module ConditionalParenthesis 9 | 10 | # Emit contents of the block within parenthesis when necessary 11 | # 12 | # @return [Boolean] 13 | # 14 | # @api private 15 | def parenthesis 16 | parenthesize? ? super : yield 17 | end 18 | 19 | # Test if the expression needs to be parenthesized 20 | # 21 | # @return [Boolean] 22 | # 23 | # @api private 24 | def parenthesize? 25 | fail NotImplementedError, "#{self}##{__method__} is not implemented" 26 | end 27 | 28 | end # ConditionalParenthesis 29 | end # Emitter 30 | end # Generator 31 | end # SQL 32 | -------------------------------------------------------------------------------- /lib/sql/generator/emitter/delete.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module SQL 4 | module Generator 5 | class Emitter 6 | 7 | # Delete statement emitter 8 | class Delete < self 9 | handle :delete 10 | 11 | children :from, :where 12 | 13 | private 14 | 15 | # @see Emitter#dispatch 16 | # 17 | # @return [undefined] 18 | # 19 | # @api private 20 | def dispatch 21 | write_command(from, K_DELETE) 22 | visit(where) if where 23 | end 24 | 25 | end # Delete 26 | end # Emitter 27 | end # Generator 28 | end # SQL 29 | -------------------------------------------------------------------------------- /lib/sql/generator/emitter/delimited.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module SQL 4 | module Generator 5 | class Emitter 6 | 7 | # Delimited names emitter 8 | class Delimited < self 9 | 10 | TYPES = IceNine.deep_freeze( 11 | group_by: K_GROUP_BY, 12 | order_by: K_ORDER_BY 13 | ) 14 | 15 | handle(*TYPES.keys) 16 | 17 | private 18 | 19 | # @see Emitter#dispatch 20 | # 21 | # @return [undefined] 22 | # 23 | # @api private 24 | def dispatch 25 | write(WS, TYPES.fetch(node_type), WS) 26 | delimited(children) 27 | end 28 | 29 | end # Delimited 30 | end # Emitter 31 | end # Generator 32 | end # SQL 33 | -------------------------------------------------------------------------------- /lib/sql/generator/emitter/dsl.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module SQL 4 | module Generator 5 | class Emitter 6 | 7 | # Emitter DSL Methods 8 | module DSL 9 | 10 | # Hook called when another module is extended by this module 11 | # 12 | # @param [Module] descendant 13 | # the module being extended with Emitter 14 | # 15 | # @return [undefined] 16 | # 17 | # @api private 18 | def self.extended(descendant) 19 | descendant.class_eval { private_class_method :new } 20 | end 21 | 22 | # Visit node 23 | # 24 | # @param [Parser::AST::Node] node 25 | # @param [Stream] stream 26 | # @param [Emitter] parent optional 27 | # 28 | # @return [Class] 29 | # 30 | # @api private 31 | def visit(node, stream, parent = Emitter::Root.instance) 32 | @registry[node.type].emit(node, stream, parent) 33 | self 34 | end 35 | 36 | # Finalize the emitter registry 37 | # 38 | # @return [Class] 39 | # 40 | # @api private 41 | def finalize 42 | @registry.finalize 43 | self 44 | end 45 | 46 | protected 47 | 48 | # Emit node into stream 49 | # 50 | # @return [Class] 51 | # 52 | # @api private 53 | def emit(*arguments) 54 | new(*arguments) 55 | self 56 | end 57 | 58 | private 59 | 60 | # Hook called when class is inherited 61 | # 62 | # @param [Class] descendant 63 | # the class inheriting Emitter 64 | # 65 | # @return [undefined] 66 | # 67 | # @api private 68 | def inherited(descendant) 69 | descendant.instance_variable_set(:@registry, @registry) 70 | end 71 | 72 | # Register emitter for type 73 | # 74 | # @param [Symbol] type 75 | # 76 | # @return [undefined] 77 | # 78 | # @api private 79 | def handle(*types) 80 | types.each { |type| @registry[type] = self } 81 | end 82 | 83 | # Create name helpers 84 | # 85 | # @return [undefined] 86 | # 87 | # @api private 88 | def children(*names) 89 | names.each_with_index(&method(:define_named_child)) 90 | define_remaining_children(names.length) 91 | end 92 | 93 | # Define named child 94 | # 95 | # @param [Symbol] name 96 | # @param [Fixnum] index 97 | # 98 | # @return [undefined] 99 | # 100 | # @api private 101 | def define_named_child(name, index) 102 | define_method(name) { children.at(index) } 103 | end 104 | 105 | # Define remaining children 106 | # 107 | # @param [Fixnum] from_index 108 | # 109 | # @return [undefined] 110 | # 111 | # @api private 112 | def define_remaining_children(from_index) 113 | define_method(:remaining_children) { children.drop(from_index) } 114 | end 115 | 116 | end # DSL 117 | end # Emitter 118 | end # Generator 119 | end # SQL 120 | -------------------------------------------------------------------------------- /lib/sql/generator/emitter/fields.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module SQL 4 | module Generator 5 | class Emitter 6 | 7 | # Field names emitter 8 | class Fields < self 9 | handle :fields 10 | 11 | private 12 | 13 | # @see Emitter#dispatch 14 | # 15 | # @return [undefined] 16 | # 17 | # @api private 18 | def dispatch 19 | delimited(children) 20 | end 21 | 22 | end # Fields 23 | end # Emitter 24 | end # Generator 25 | end # SQL 26 | -------------------------------------------------------------------------------- /lib/sql/generator/emitter/identifier.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module SQL 4 | module Generator 5 | class Emitter 6 | 7 | # Identifier emitter class 8 | class Identifier < self 9 | 10 | handle(:id) 11 | 12 | private 13 | 14 | # Perform dispatch 15 | # 16 | # @return [undefined] 17 | # 18 | # @api private 19 | def dispatch 20 | head, *tail = children.map do |value| 21 | value.gsub(D_DBL_QUOTE, D_ESCAPED_DBL_QUOTE) 22 | end 23 | write(D_DBL_QUOTE, head, D_DBL_QUOTE) 24 | tail.each do |value| 25 | write(D_PERIOD, D_DBL_QUOTE, value, D_DBL_QUOTE) 26 | end 27 | end 28 | 29 | end # Identifier 30 | end # Emitter 31 | end # Generator 32 | end # SQL 33 | -------------------------------------------------------------------------------- /lib/sql/generator/emitter/insert.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module SQL 4 | module Generator 5 | class Emitter 6 | 7 | # Insert statement emitter 8 | class Insert < self 9 | 10 | handle :insert 11 | 12 | children :from, :tuple 13 | 14 | private 15 | 16 | # Perform dispatch 17 | # 18 | # @return [undefined] 19 | # 20 | # @api private 21 | def dispatch 22 | write_command(from, K_INSERT) 23 | write_node(tuple, K_VALUES) 24 | end 25 | 26 | end # Insert 27 | end # Emitter 28 | end # Generator 29 | end # SQL 30 | -------------------------------------------------------------------------------- /lib/sql/generator/emitter/join.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module SQL 4 | module Generator 5 | class Emitter 6 | 7 | # Join statement emitter 8 | class Join < self 9 | TYPES = IceNine.deep_freeze( 10 | join: O_JOIN, 11 | left_join: O_LEFT_JOIN, 12 | right_join: O_RIGHT_JOIN, 13 | full_join: O_FULL_JOIN, 14 | natural_join: O_NATURAL_JOIN, 15 | cross_join: O_CROSS_JOIN 16 | ) 17 | 18 | handle(*TYPES.keys) 19 | 20 | children :left, :right, :predicate 21 | 22 | private 23 | 24 | # Perform dispatch 25 | # 26 | # @return [undefined] 27 | # 28 | # @api private 29 | def dispatch 30 | visit(left) 31 | write(WS, TYPES.fetch(node_type), WS) 32 | visit(right) 33 | if predicate 34 | write(WS) 35 | visit(predicate) 36 | end 37 | end 38 | 39 | end # Join 40 | end # Emitter 41 | end # Generator 42 | end # SQL 43 | -------------------------------------------------------------------------------- /lib/sql/generator/emitter/literal.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module SQL 4 | module Generator 5 | class Emitter 6 | 7 | # Namespace for literal emitters 8 | class Literal < self 9 | 10 | children :value 11 | 12 | # Returns an unfrozen object 13 | # 14 | # Some objects, like Date, DateTime and Time memoize values 15 | # when serialized to a String, so when they are frozen this will 16 | # dup them and then return the unfrozen copy. 17 | # 18 | # @param [Object] object 19 | # 20 | # @return [Object] 21 | # non-frozen object 22 | # 23 | # @api private 24 | def self.unfrozen(object) 25 | object.frozen? ? object.dup : object 26 | end 27 | 28 | end # Literal 29 | end # Emitter 30 | end # Generator 31 | end # SQL 32 | -------------------------------------------------------------------------------- /lib/sql/generator/emitter/literal/date.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module SQL 4 | module Generator 5 | class Emitter 6 | class Literal 7 | 8 | # Literal date emitter 9 | class Date < self 10 | 11 | handle :date 12 | 13 | private 14 | 15 | # Perform dispatch 16 | # 17 | # @return [undefined] 18 | # 19 | # @api private 20 | def dispatch 21 | write(D_QUOTE, value.iso8601, D_QUOTE) 22 | end 23 | 24 | end # Date 25 | end # Literal 26 | end # Emitter 27 | end # Generator 28 | end # SQL 29 | -------------------------------------------------------------------------------- /lib/sql/generator/emitter/literal/datetime.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module SQL 4 | module Generator 5 | class Emitter 6 | class Literal 7 | 8 | # Literal datetime emitter 9 | class Datetime < self 10 | 11 | # Display the time with nanoseconds 12 | TIME_SCALE = 9 13 | 14 | handle :datetime 15 | 16 | private 17 | 18 | # Perform dispatch 19 | # 20 | # @return [undefined] 21 | # 22 | # @api private 23 | def dispatch 24 | write(D_QUOTE, value.new_offset.iso8601(TIME_SCALE), D_QUOTE) 25 | end 26 | 27 | end # Datetime 28 | end # Literal 29 | end # Emitter 30 | end # Generator 31 | end # SQL 32 | -------------------------------------------------------------------------------- /lib/sql/generator/emitter/literal/decimal.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module SQL 4 | module Generator 5 | class Emitter 6 | class Literal 7 | 8 | # Literal decimal emitter 9 | class Decimal < self 10 | 11 | handle :decimal 12 | 13 | private 14 | 15 | # Perform dispatch 16 | # 17 | # @return [undefined] 18 | # 19 | # @api private 20 | def dispatch 21 | write(value.to_s('F')) 22 | end 23 | 24 | end # Decimal 25 | end # Literal 26 | end # Emitter 27 | end # Generator 28 | end # SQL 29 | -------------------------------------------------------------------------------- /lib/sql/generator/emitter/literal/float.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module SQL 4 | module Generator 5 | class Emitter 6 | class Literal 7 | 8 | # Literal float emitter 9 | class Float < self 10 | 11 | handle :float 12 | 13 | private 14 | 15 | # Perform dispatch 16 | # 17 | # @return [undefined] 18 | # 19 | # @api private 20 | def dispatch 21 | write(value.to_s) 22 | end 23 | 24 | end # Float 25 | end # Literal 26 | end # Emitter 27 | end # Generator 28 | end # SQL 29 | -------------------------------------------------------------------------------- /lib/sql/generator/emitter/literal/integer.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module SQL 4 | module Generator 5 | class Emitter 6 | class Literal 7 | 8 | # Literal integer emitter 9 | class Integer < self 10 | 11 | handle :integer 12 | 13 | private 14 | 15 | # Perform dispatch 16 | # 17 | # @return [undefined] 18 | # 19 | # @api private 20 | def dispatch 21 | write(value.to_s) 22 | end 23 | 24 | end # Integer 25 | end # Literal 26 | end # Emitter 27 | end # Generator 28 | end # SQL 29 | -------------------------------------------------------------------------------- /lib/sql/generator/emitter/literal/singleton.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module SQL 4 | module Generator 5 | class Emitter 6 | class Literal 7 | 8 | # Literal Singleton emitter base class 9 | class Singleton < self 10 | 11 | TYPES = IceNine.deep_freeze( 12 | true: K_TRUE, 13 | false: K_FALSE, 14 | null: K_NULL 15 | ) 16 | 17 | handle(*TYPES.keys) 18 | 19 | private 20 | 21 | # Perform dispatch 22 | # 23 | # @return [undefined] 24 | # 25 | # @api private 26 | def dispatch 27 | write(TYPES.fetch(node_type)) 28 | end 29 | 30 | end # Singleton 31 | end # Literal 32 | end # Emitter 33 | end # Generator 34 | end # SQL 35 | -------------------------------------------------------------------------------- /lib/sql/generator/emitter/literal/string.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module SQL 4 | module Generator 5 | class Emitter 6 | class Literal 7 | 8 | # Literal string emitter 9 | class String < self 10 | 11 | handle :string 12 | 13 | private 14 | 15 | # Perform dispatch 16 | # 17 | # @return [undefined] 18 | # 19 | # @api private 20 | def dispatch 21 | write(D_QUOTE, value.gsub(D_QUOTE, D_ESCAPED_QUOTE), D_QUOTE) 22 | end 23 | 24 | end # String 25 | end # Literal 26 | end # Emitter 27 | end # Generator 28 | end # SQL 29 | -------------------------------------------------------------------------------- /lib/sql/generator/emitter/literal/time.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module SQL 4 | module Generator 5 | class Emitter 6 | class Literal 7 | 8 | # Literal time emitter 9 | class Time < self 10 | 11 | # Display the time with nanoseconds 12 | TIME_SCALE = 9 13 | 14 | handle :time 15 | 16 | private 17 | 18 | # Perform dispatch 19 | # 20 | # @return [undefined] 21 | # 22 | # @api private 23 | def dispatch 24 | unfrozen = self.class.unfrozen(value) 25 | write(D_QUOTE, unfrozen.utc.iso8601(TIME_SCALE), D_QUOTE) 26 | end 27 | 28 | end # Time 29 | end # Literal 30 | end # Emitter 31 | end # Generator 32 | end # SQL 33 | -------------------------------------------------------------------------------- /lib/sql/generator/emitter/predicate.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module SQL 4 | module Generator 5 | class Emitter 6 | 7 | # Predicate emitter 8 | class Predicate < self 9 | 10 | TYPES = IceNine.deep_freeze( 11 | where: K_WHERE, 12 | having: K_HAVING 13 | ) 14 | 15 | handle(*TYPES.keys) 16 | 17 | children :predicate 18 | 19 | private 20 | 21 | # @see Emitter#dispatch 22 | # 23 | # @return [undefined] 24 | # 25 | # @api private 26 | def dispatch 27 | write_node(predicate, TYPES.fetch(node_type)) 28 | end 29 | 30 | end # Predicate 31 | end # Emitter 32 | end # Generator 33 | end # SQL 34 | -------------------------------------------------------------------------------- /lib/sql/generator/emitter/root.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module SQL 4 | module Generator 5 | class Emitter 6 | 7 | # Root emitter 8 | class Root < self 9 | include Singleton 10 | 11 | # Initialize a root emitter object 12 | # 13 | # @return [undefined] 14 | # 15 | # @api private 16 | def initialize 17 | end 18 | 19 | protected 20 | 21 | # The node type 22 | # 23 | # @return [nil] 24 | # 25 | # @api private 26 | def node_type 27 | end 28 | 29 | end # Root 30 | end # Emitter 31 | end # Generator 32 | end # SQL 33 | -------------------------------------------------------------------------------- /lib/sql/generator/emitter/select.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module SQL 4 | module Generator 5 | class Emitter 6 | 7 | # Select statement emitter 8 | class Select < self 9 | include ConditionalParenthesis 10 | 11 | handle :select 12 | 13 | children :fields, :from 14 | 15 | private 16 | 17 | # @see Emitter#dispatch 18 | # 19 | # @return [undefined] 20 | # 21 | # @api private 22 | def dispatch 23 | parenthesis do 24 | write_command(fields, K_SELECT) 25 | write_node(from, K_FROM) 26 | remaining_children.each(&method(:visit)) 27 | end 28 | end 29 | 30 | # Test if the statement needs to be parenthesized 31 | # 32 | # @return [Boolean] 33 | # 34 | # @api private 35 | def parenthesize? 36 | !parent.equal?(Root.instance) 37 | end 38 | 39 | end # Select 40 | end # Emitter 41 | end # Generator 42 | end # SQL 43 | -------------------------------------------------------------------------------- /lib/sql/generator/emitter/set.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module SQL 4 | module Generator 5 | class Emitter 6 | 7 | # Set statement emitter 8 | class Set < self 9 | TYPES = IceNine.deep_freeze( 10 | difference: WS + O_EXCEPT + WS, 11 | intersection: WS + O_INTERSECT + WS, 12 | union: WS + O_UNION + WS, 13 | ) 14 | 15 | handle(*TYPES.keys) 16 | 17 | private 18 | 19 | # Perform dispatch 20 | # 21 | # @return [undefined] 22 | # 23 | # @api private 24 | def dispatch 25 | delimited(children, TYPES.fetch(node_type)) 26 | end 27 | 28 | end # Set 29 | end # Emitter 30 | end # Generator 31 | end # SQL 32 | -------------------------------------------------------------------------------- /lib/sql/generator/emitter/sort_operation.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module SQL 4 | module Generator 5 | class Emitter 6 | 7 | # Emitter class for sort operations 8 | class SortOperation < self 9 | 10 | TYPES = IceNine.deep_freeze( 11 | asc: O_ASC, 12 | desc: O_DESC 13 | ) 14 | 15 | handle(*TYPES.keys) 16 | 17 | children :field 18 | 19 | private 20 | 21 | # Perform dispatch 22 | # 23 | # @return [undefined] 24 | # 25 | # @api private 26 | def dispatch 27 | visit(field) 28 | write(WS, TYPES.fetch(node_type)) 29 | end 30 | 31 | end # SortOperation 32 | end # Emitter 33 | end # Generator 34 | end # SQL 35 | -------------------------------------------------------------------------------- /lib/sql/generator/emitter/tuple.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module SQL 4 | module Generator 5 | class Emitter 6 | 7 | # Tuple emitter 8 | class Tuple < self 9 | 10 | handle :tuple 11 | 12 | private 13 | 14 | # Perform dispatch 15 | # 16 | # @return [undefined] 17 | # 18 | # @api private 19 | def dispatch 20 | parenthesis { delimited(children) } 21 | end 22 | 23 | end # Tuple 24 | end # Emitter 25 | end # Generator 26 | end # SQL 27 | -------------------------------------------------------------------------------- /lib/sql/generator/emitter/unary_function_operation.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module SQL 4 | module Generator 5 | class Emitter 6 | 7 | # Emitter class for unary operations using functional notation 8 | class UnaryFunctionOperation < self 9 | 10 | TYPES = IceNine.deep_freeze( 11 | count: O_COUNT, 12 | sum: O_SUM, 13 | min: O_MIN, 14 | max: O_MAX, 15 | avg: O_AVG, 16 | var: O_VAR, 17 | stddev: O_STDDEV, 18 | sqrt: O_SQRT, 19 | abs: O_ABS, 20 | length: O_LENGTH 21 | ) 22 | 23 | handle(*TYPES.keys) 24 | 25 | children :operand 26 | 27 | private 28 | 29 | # Perform dispatch 30 | # 31 | # @return [undefined] 32 | # 33 | # @api private 34 | def dispatch 35 | write(TYPES.fetch(node_type), WS) 36 | parenthesis { visit(operand) } 37 | end 38 | 39 | end # UnaryFunctionOperation 40 | end # Emitter 41 | end # Generator 42 | end # SQL 43 | -------------------------------------------------------------------------------- /lib/sql/generator/emitter/unary_prefix_operation.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module SQL 4 | module Generator 5 | class Emitter 6 | 7 | # Emitter class for unary operations using prefix notation 8 | class UnaryPrefixOperation < self 9 | 10 | TYPES = IceNine.deep_freeze( 11 | uplus: O_PLUS, 12 | uminus: O_MINUS, 13 | not: O_NEGATION + WS, 14 | on: O_ON + WS, 15 | using: O_USING + WS 16 | ) 17 | 18 | handle(*TYPES.keys) 19 | 20 | children :value 21 | 22 | private 23 | 24 | # Perform dispatch 25 | # 26 | # @return [undefined] 27 | # 28 | # @api private 29 | def dispatch 30 | write(TYPES.fetch(node_type)) 31 | visit(value) 32 | end 33 | 34 | end # UnaryPrefixOperation 35 | end # Emitter 36 | end # Generator 37 | end # SQL 38 | -------------------------------------------------------------------------------- /lib/sql/generator/emitter/update.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module SQL 4 | module Generator 5 | class Emitter 6 | 7 | # Update statement emitter 8 | class Update < self 9 | 10 | handle :update 11 | 12 | children :from, :assignment, :where 13 | 14 | private 15 | 16 | # @see Emitter#dispatch 17 | # 18 | # @return [undefined] 19 | # 20 | # @api private 21 | def dispatch 22 | write_command(from, K_UPDATE) 23 | visit(assignment) 24 | visit(where) if where 25 | end 26 | 27 | end # Update 28 | end # Emitter 29 | end # Generator 30 | end # SQL 31 | -------------------------------------------------------------------------------- /lib/sql/generator/emitter/update_set.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module SQL 4 | module Generator 5 | class Emitter 6 | 7 | # Update set emitter 8 | class UpdateSet < self 9 | handle :set 10 | 11 | private 12 | 13 | # @see Emitter#dispatch 14 | # 15 | # @return [undefined] 16 | # 17 | # @api private 18 | def dispatch 19 | write(WS, K_SET, WS) 20 | delimited(children) 21 | end 22 | 23 | end # UpdateSet 24 | end # Emitter 25 | end # Generator 26 | end # SQL 27 | -------------------------------------------------------------------------------- /lib/sql/generator/registry.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module SQL 4 | module Generator 5 | 6 | # Registry for emitters 7 | # 8 | # @private 9 | class Registry 10 | 11 | # Initialize registry 12 | # 13 | # @return [undefined] 14 | # 15 | # @api private 16 | def initialize 17 | @emitters = Hash.new { |_emitters, type| fail UnknownTypeError, type } 18 | end 19 | 20 | # Return emitter based on type 21 | # 22 | # @param [Symbol] type 23 | # 24 | # @return [Emitter] 25 | # 26 | # @api private 27 | def [](type) 28 | @emitters[type] 29 | end 30 | 31 | # Register an emitter 32 | # 33 | # @param [Symbol] type 34 | # @param [Emitter] emitter 35 | # 36 | # @return [undefined] 37 | # 38 | # @api private 39 | def []=(type, emitter) 40 | @emitters[type] = emitter 41 | end 42 | 43 | # Finalize the registry 44 | # 45 | # @return [self] 46 | # 47 | # @api private 48 | def finalize 49 | IceNine.deep_freeze(self) 50 | end 51 | 52 | end # Registry 53 | end # Generator 54 | end # SQL 55 | -------------------------------------------------------------------------------- /lib/sql/generator/stream.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module SQL 4 | module Generator 5 | 6 | # Stream used to emit into 7 | class Stream 8 | 9 | # Return current output 10 | # 11 | # @return [#<<] 12 | # 13 | # @api private 14 | attr_reader :output 15 | 16 | # Initialize output stream 17 | # 18 | # @param [#<<] output 19 | # 20 | # @return [undefined] 21 | # 22 | # @api private 23 | def initialize(output = '') 24 | @output = output 25 | @start_of_line = true 26 | end 27 | 28 | # Append string 29 | # 30 | # @param [String] string 31 | # 32 | # @return [self] 33 | # 34 | # @api private 35 | def <<(string) 36 | output << string 37 | @start_of_line = string.end_with?(Constants::NL) 38 | self 39 | end 40 | 41 | # Increase indent 42 | # 43 | # @return [self] 44 | # 45 | # @api private 46 | def indent 47 | Indented.new(self).nl 48 | end 49 | 50 | # Write newline 51 | # 52 | # @return [self] 53 | # 54 | # @api private 55 | def nl 56 | output << Constants::NL 57 | self 58 | end 59 | 60 | protected 61 | 62 | # Test if the stream is at the start of a line 63 | # 64 | # @return [Boolean] 65 | # 66 | # @api private 67 | def start_of_line? 68 | @start_of_line 69 | end 70 | 71 | # Indented Stream used to emit into 72 | class Indented < self 73 | DEFAULT_INDENT = ' '.freeze 74 | 75 | # Append string with indentation 76 | # 77 | # @param [String] _string 78 | # 79 | # @return [self] 80 | # 81 | # @api private 82 | def <<(_string) 83 | output << DEFAULT_INDENT if output.start_of_line? 84 | super 85 | end 86 | 87 | # Decrease indent 88 | # 89 | # @return [self] 90 | # 91 | # @api private 92 | def unindent 93 | output.nl 94 | end 95 | 96 | end # Indented 97 | end # Stream 98 | end # Generator 99 | end # SQL 100 | -------------------------------------------------------------------------------- /lib/sql/node_helper.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module SQL 4 | 5 | # A mixin for node helpers 6 | module NodeHelper 7 | 8 | # Return new node 9 | # 10 | # @param [Symbol] type 11 | # 12 | # @return [AST::Node] 13 | # 14 | # @api private 15 | def s(type, *children) 16 | AST::Node.new(type, children) 17 | end 18 | private :s 19 | 20 | end # NodeHelper 21 | end # SQL 22 | -------------------------------------------------------------------------------- /lib/sql/parser.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module SQL 4 | 5 | # An SQL parser 6 | module Parser 7 | end # Parser 8 | end # SQL 9 | -------------------------------------------------------------------------------- /lib/sql/version.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module SQL 4 | 5 | # Gem version 6 | VERSION = '0.0.1'.freeze 7 | 8 | end # SQL 9 | -------------------------------------------------------------------------------- /spec/shared/emitter_context.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | shared_context 'emitter' do 4 | let(:stream) { SQL::Generator::Stream.new } 5 | end 6 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | if ENV['COVERAGE'] == 'true' 4 | require 'simplecov' 5 | require 'coveralls' 6 | 7 | SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[ 8 | SimpleCov::Formatter::HTMLFormatter, 9 | Coveralls::SimpleCov::Formatter 10 | ] 11 | 12 | SimpleCov.start do 13 | command_name 'spec:unit' 14 | 15 | add_filter 'config' 16 | add_filter 'spec' 17 | add_filter 'vendor' 18 | 19 | minimum_coverage 99.81 20 | end 21 | end 22 | 23 | require 'devtools/spec_helper' 24 | require 'sql' 25 | require 'bigdecimal' 26 | 27 | RSpec.configure do |config| 28 | config.include(SQL::NodeHelper) 29 | config.extend(SQL::NodeHelper) 30 | config.extend(EmitterSpecHelper) 31 | 32 | config.expect_with :rspec do |expect_with| 33 | expect_with.syntax = :expect 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/support/emitter_helper.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module EmitterSpecHelper 4 | 5 | # Create an example that the expected SQL is generated 6 | # 7 | # @param [SQL::AST::Node] node 8 | # @param [String] expectation 9 | # 10 | # @return [undefined] 11 | # 12 | # @api public 13 | def assert_generates(node, expectation) 14 | it "generates #{node.type} correctly" do 15 | SQL::Generator::Emitter.visit(node, stream) 16 | expect(stream.output).to eql(expectation) 17 | end 18 | end 19 | 20 | end # EmitterSpecHelper 21 | -------------------------------------------------------------------------------- /spec/unit/sql/ast/node/to_sql_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'spec_helper' 4 | 5 | # This spec is trivial, we test more complex generations in 6 | # Via SQL::Generator::Emitter.visit 7 | describe SQL::AST::Node, '#to_sql' do 8 | let(:object) { described_class.new(type, children) } 9 | 10 | subject { object.to_sql } 11 | 12 | let(:type) { :true } 13 | let(:children) { [] } 14 | 15 | it 'returns sql representation' do 16 | should eql('TRUE') 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/unit/sql/fuzzer/each_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'spec_helper' 4 | 5 | describe SQL::Fuzzer, '#each' do 6 | let(:object) { described_class.new(ast) } 7 | let(:ast) { double('ast').as_null_object } 8 | 9 | before do 10 | ast.should_receive(:to_ast) 11 | end 12 | 13 | context 'with a block' do 14 | subject { object.each {} } 15 | 16 | it_behaves_like 'a command method' 17 | 18 | it 'yields the ast' do 19 | expect { |block| object.each(&block) }.to yield_with_args(ast) 20 | end 21 | end 22 | 23 | context 'without a block' do 24 | subject { object.each } 25 | 26 | it 'returns an enumerator' do 27 | expect(subject).to be_instance_of(to_enum.class) 28 | end 29 | 30 | it 'yields the ast in #each' do 31 | expect { |block| subject.each(&block) }.to yield_with_args(ast) 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/unit/sql/generator/class_methods/generate_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'spec_helper' 4 | 5 | describe SQL::Generator do 6 | subject { object.generate(node) } 7 | 8 | let(:object) { described_class } 9 | let(:node) { s(:true) } 10 | 11 | it 'generates the expected SQL' do 12 | expect(subject).to eql('TRUE') 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/unit/sql/generator/emitter/class_methods/visit_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'spec_helper' 4 | 5 | # FIXME: split this spec into smaller files 6 | describe SQL::Generator::Emitter, '.visit' do 7 | include_context 'emitter' 8 | 9 | context 'with literal singletons' do 10 | assert_generates s(:true), 'TRUE' 11 | assert_generates s(:false), 'FALSE' 12 | assert_generates s(:null), 'NULL' 13 | end 14 | 15 | context 'with strings' do 16 | assert_generates s(:string, %q[echo 'Hello']), %q['echo ''Hello'''] 17 | end 18 | 19 | context 'with integers' do 20 | assert_generates s(:integer, 1), '1' 21 | end 22 | 23 | context 'with floats' do 24 | assert_generates s(:float, 1.0), '1.0' 25 | end 26 | 27 | context 'with decimals' do 28 | assert_generates s(:decimal, BigDecimal('1.0')), '1.0' 29 | end 30 | 31 | context 'with dates' do 32 | assert_generates s(:date, Date.new(2013, 1, 1)), %q['2013-01-01'] 33 | end 34 | 35 | context 'with datetimes' do 36 | nsec_in_seconds = Rational(1, 10**9) 37 | offset = Rational(-8, 24) 38 | 39 | # A DateTime not in the UTC timezone 40 | datetime = DateTime.new(2013, 12, 31, 15, 59, 59 + nsec_in_seconds, offset) 41 | 42 | assert_generates( 43 | s(:datetime, datetime), 44 | %q['2013-12-31T23:59:59.000000001+00:00'] # converts to UTC 45 | ) 46 | end 47 | 48 | context 'with times' do 49 | # A Time not in the UTC timezone 50 | time = begin 51 | original, ENV['TZ'] = ENV['TZ'], 'America/Vancouver' 52 | Time.local(2010, 12, 31, 15, 59, 59, 1).freeze 53 | ensure 54 | ENV['TZ'] = original 55 | end 56 | 57 | assert_generates( 58 | s(:time, time), 59 | %q['2010-12-31T23:59:59.000001000Z'] # converts to UTC 60 | ) 61 | end 62 | 63 | context 'identifiers' do 64 | assert_generates s(:id, 'echo "oh hai"'), '"echo ""oh hai"""' 65 | end 66 | 67 | context 'unary prefix operations' do 68 | context 'with plus' do 69 | assert_generates s(:uplus, s(:integer, 1)), '+1' 70 | end 71 | 72 | context 'with minus' do 73 | assert_generates s(:uminus, s(:integer, 1)), '-1' 74 | end 75 | 76 | context 'with negation' do 77 | assert_generates s(:not, s(:true)), 'NOT TRUE' 78 | end 79 | end 80 | 81 | context 'unary function operations' do 82 | { 83 | count: 'COUNT', 84 | sum: 'SUM', 85 | min: 'MIN', 86 | max: 'MAX', 87 | avg: 'AVG', 88 | var: 'VAR_POP', 89 | stddev: 'STDDEV_POP', 90 | sqrt: 'SQRT', 91 | abs: 'ABS', 92 | length: 'LENGTH', 93 | }.each do |type, operator| 94 | context type.inspect do 95 | assert_generates( 96 | s(type, s(:id, 'foo')), 97 | %Q[#{operator} ("foo")] 98 | ) 99 | end 100 | end 101 | end 102 | 103 | context 'binary infix operations' do 104 | { 105 | add: '+', 106 | sub: '-', 107 | mul: '*', 108 | div: '/', 109 | mod: '%', 110 | pow: '^', 111 | eq: '=', 112 | ne: '<>', 113 | gt: '>', 114 | gte: '>=', 115 | lt: '<', 116 | lte: '<=', 117 | concat: '||', 118 | as: 'AS', 119 | }.each do |type, operator| 120 | context type.inspect do 121 | assert_generates( 122 | s(type, s(:id, 'foo'), s(:id, 'bar')), 123 | %Q["foo" #{operator} "bar"] 124 | ) 125 | end 126 | end 127 | 128 | context ':is' do 129 | assert_generates( 130 | s(:is, s(:id, 'foo'), s(:null)), 131 | '"foo" IS NULL' 132 | ) 133 | 134 | assert_generates( 135 | s(:is, s(:id, 'foo'), s(:not, s(:null))), 136 | '"foo" IS NOT NULL' 137 | ) 138 | end 139 | 140 | context ':in' do 141 | assert_generates( 142 | s(:in, s(:id, 'foo'), s(:tuple, s(:integer, 1), s(:integer, 2))), 143 | '"foo" IN (1, 2)' 144 | ) 145 | end 146 | 147 | context ':between' do 148 | assert_generates( 149 | s(:between, s(:id, 'foo'), s(:and, s(:integer, 1), s(:integer, 2))), 150 | '"foo" BETWEEN 1 AND 2' 151 | ) 152 | end 153 | end 154 | 155 | context 'binary connective operations' do 156 | context ':and' do 157 | assert_generates( 158 | s(:and, s(:true), s(:true)), 159 | 'TRUE AND TRUE' 160 | ) 161 | 162 | assert_generates( 163 | s(:and, s(:true), s(:and, s(:true), s(:true))), 164 | 'TRUE AND TRUE AND TRUE' 165 | ) 166 | 167 | assert_generates( 168 | s(:and, s(:true), s(:or, s(:false), s(:true))), 169 | 'TRUE AND (FALSE OR TRUE)' 170 | ) 171 | end 172 | 173 | context ':or' do 174 | assert_generates( 175 | s(:or, s(:false), s(:true)), 176 | 'FALSE OR TRUE' 177 | ) 178 | 179 | assert_generates( 180 | s(:or, s(:false), s(:or, s(:false), s(:true))), 181 | 'FALSE OR FALSE OR TRUE' 182 | ) 183 | 184 | assert_generates( 185 | s(:or, s(:false), s(:and, s(:true), s(:true))), 186 | 'FALSE OR (TRUE AND TRUE)' 187 | ) 188 | end 189 | end 190 | 191 | context 'tuples' do 192 | assert_generates( 193 | s(:tuple, s(:integer, 1), s(:string, 'foo')), 194 | "(1, 'foo')" 195 | ) 196 | end 197 | 198 | context 'insert' do 199 | assert_generates( 200 | s(:insert, 201 | s(:id, 'users'), 202 | s(:tuple, s(:integer, 1), s(:string, 'foo')) 203 | ), 204 | %q[INSERT INTO "users" VALUES (1, 'foo')] 205 | ) 206 | end 207 | 208 | context 'delete' do 209 | context 'without where clause' do 210 | assert_generates s(:delete, s(:id, 'users')), %q[DELETE FROM "users"] 211 | end 212 | 213 | context 'with where clause' do 214 | assert_generates( 215 | s(:delete, 216 | s(:id, 'users'), 217 | s(:where, 218 | s(:eq, s(:id, 'name'), s(:string, 'foo')) 219 | ) 220 | ), 221 | %q[DELETE FROM "users" WHERE "name" = 'foo'] 222 | ) 223 | end 224 | end 225 | 226 | context 'update' do 227 | context 'without where clause' do 228 | assert_generates( 229 | s(:update, 230 | s(:id, 'users'), 231 | s(:set, 232 | s(:eq, s(:id, 'name'), s(:string, 'foo')), 233 | s(:eq, s(:id, 'age'), s(:integer, 1)) 234 | ) 235 | ), 236 | %q[UPDATE "users" SET "name" = 'foo', "age" = 1] 237 | ) 238 | end 239 | 240 | context 'with where clause' do 241 | assert_generates( 242 | s(:update, 243 | s(:id, 'users'), 244 | s(:set, 245 | s(:eq, s(:id, 'name'), s(:string, 'foo')), 246 | s(:eq, s(:id, 'age'), s(:integer, 1)) 247 | ), 248 | s(:where, 249 | s(:eq, s(:id, 'age'), s(:integer, 2)) 250 | ) 251 | ), 252 | <<-SQL.gsub(/\s+/, ' ').strip 253 | UPDATE "users" 254 | SET "name" = 'foo', "age" = 1 255 | WHERE "age" = 2 256 | SQL 257 | ) 258 | end 259 | end 260 | 261 | context 'select' do 262 | context 'without where clause' do 263 | assert_generates( 264 | s(:select, 265 | s(:fields, s(:id, 'name'), s(:id, 'age')), s(:id, 'users') 266 | ), 267 | %q[SELECT "name", "age" FROM "users"] 268 | ) 269 | end 270 | 271 | context 'with where clause' do 272 | assert_generates( 273 | s(:select, 274 | s(:fields, 275 | s(:id, 'name'), s(:id, 'age') 276 | ), 277 | s(:id, 'users'), 278 | s(:where, 279 | s(:eq, s(:id, 'id'), s(:integer, 1)) 280 | ) 281 | ), 282 | %q[SELECT "name", "age" FROM "users" WHERE "id" = 1] 283 | ) 284 | end 285 | 286 | context 'with group by' do 287 | assert_generates( 288 | s(:select, 289 | s(:fields, s(:id, 'name'), s(:id, 'age')), 290 | s(:id, 'users'), 291 | s(:group_by, s(:id, 'name'), s(:id, 'age')) 292 | ), 293 | <<-SQL.gsub(/\s+/, ' ').strip 294 | SELECT "name", "age" 295 | FROM "users" 296 | GROUP BY "name", "age" 297 | SQL 298 | ) 299 | end 300 | 301 | context 'with order by' do 302 | assert_generates( 303 | s(:select, 304 | s(:fields, s(:id, 'name'), s(:id, 'age')), 305 | s(:id, 'users'), 306 | s(:order_by, s(:asc, s(:id, 'name')), s(:desc, s(:id, 'age'))) 307 | ), 308 | <<-SQL.gsub(/\s+/, ' ').strip 309 | SELECT "name", "age" 310 | FROM "users" 311 | ORDER BY "name" ASC, "age" DESC 312 | SQL 313 | ) 314 | end 315 | 316 | context 'with having' do 317 | assert_generates( 318 | s(:select, 319 | s(:fields, s(:id, 'name'), s(:id, 'age')), 320 | s(:id, 'users'), 321 | s(:group_by, s(:id, 'name'), s(:id, 'age')), 322 | s(:having, s(:eq, s(:id, 'id'), s(:integer, 1))) 323 | ), 324 | <<-SQL.gsub(/\s+/, ' ').strip 325 | SELECT "name", "age" 326 | FROM "users" 327 | GROUP BY "name", "age" 328 | HAVING "id" = 1 329 | SQL 330 | ) 331 | end 332 | end 333 | 334 | context 'set operations' do 335 | { 336 | difference: 'EXCEPT', 337 | intersection: 'INTERSECT', 338 | union: 'UNION', 339 | }.each do |type, operator| 340 | context type.inspect do 341 | assert_generates( 342 | s(type, 343 | s(:select, s(:fields, s(:id, 'name')), s(:id, 'users')), 344 | s(:select, s(:fields, s(:id, 'name')), s(:id, 'customers')), 345 | s(:select, s(:fields, s(:id, 'name')), s(:id, 'employees')), 346 | ), 347 | <<-SQL.gsub(/\s+/, ' ').strip 348 | (SELECT "name" FROM "users") 349 | #{operator} 350 | (SELECT "name" FROM "customers") 351 | #{operator} 352 | (SELECT "name" FROM "employees") 353 | SQL 354 | ) 355 | end 356 | end 357 | end 358 | 359 | context 'join operations' do 360 | { 361 | join: 'JOIN', 362 | left_join: 'LEFT JOIN', 363 | right_join: 'RIGHT JOIN', 364 | full_join: 'FULL JOIN', 365 | }.each do |type, operator| 366 | context type.inspect do 367 | assert_generates( 368 | s(type, 369 | s(:id, 'foo'), 370 | s(:id, 'bar'), 371 | s(:on, s(:eq, s(:id, 'foo', 'name'), s(:id, 'bar', 'name'))) 372 | ), 373 | %Q["foo" #{operator} "bar" ON "foo"."name" = "bar"."name"] 374 | ) 375 | 376 | assert_generates( 377 | s(type, 378 | s(:id, 'foo'), 379 | s(:id, 'bar'), 380 | s(:using, s(:tuple, s(:id, 'name'))) 381 | ), 382 | %Q["foo" #{operator} "bar" USING ("name")] 383 | ) 384 | end 385 | end 386 | 387 | { 388 | natural_join: 'NATURAL JOIN', 389 | cross_join: 'CROSS JOIN', 390 | }.each do |type, operator| 391 | context type.inspect do 392 | assert_generates( 393 | s(type, s(:id, 'foo'), s(:id, 'bar')), 394 | %Q["foo" #{operator} "bar"] 395 | ) 396 | end 397 | end 398 | end 399 | 400 | context 'when emitter is missing' do 401 | it 'raises argument error' do 402 | expect { described_class.visit(s(:not_supported, []), stream) } 403 | .to raise_error( 404 | SQL::UnknownTypeError, 405 | 'No emitter for node: :not_supported' 406 | ) 407 | end 408 | end 409 | end 410 | -------------------------------------------------------------------------------- /spec/unit/sql/generator/stream/append_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'spec_helper' 4 | 5 | describe SQL::Generator::Stream, '#<<' do 6 | subject { object << string } 7 | 8 | let(:object) { described_class.new } 9 | let(:string) { 'foo' } 10 | 11 | specify do 12 | expect { subject }.to change { object.output }.from('').to('foo') 13 | end 14 | 15 | # Yeah duplicate, mutant will be improved ;) 16 | it 'prefixes with indentation' do 17 | (object << 'foo').indent << 'bar' << 'baz' 18 | expect(object.output).to eql("foo\n barbaz") 19 | end 20 | 21 | it_behaves_like 'a command method' 22 | end 23 | -------------------------------------------------------------------------------- /spec/unit/sql/generator/stream/indent_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'spec_helper' 4 | 5 | describe SQL::Generator::Stream, '#indent' do 6 | subject { object.indent } 7 | 8 | let(:object) { described_class.new } 9 | 10 | it 'indents with two chars' do 11 | ((object << 'foo').indent << 'bar').indent << 'baz' 12 | expect(object.output).to eql("foo\n bar\n baz") 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/unit/sql/generator/stream/indented/unindent_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'spec_helper' 4 | 5 | describe SQL::Generator::Stream::Indented, '#unindent' do 6 | subject { object.unindent } 7 | 8 | let(:object) { described_class.new(stream) } 9 | let(:stream) { SQL::Generator::Stream.new } 10 | 11 | it 'unindents two chars' do 12 | ((object << 'foo').indent << 'bar').unindent << 'baz' 13 | expect(stream.output).to eql(" foo\n bar\n baz") 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/unit/sql/generator/stream/nl_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'spec_helper' 4 | 5 | describe SQL::Generator::Stream, '#nl' do 6 | let(:object) { described_class.new } 7 | 8 | subject { object.nl } 9 | 10 | it 'writes a newline' do 11 | object << 'foo' 12 | subject 13 | object << 'bar' 14 | expect(object.output).to eql("foo\nbar") 15 | end 16 | 17 | it_behaves_like 'a command method' 18 | end 19 | -------------------------------------------------------------------------------- /spec/unit/sql/generator/stream/output_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'spec_helper' 4 | 5 | describe SQL::Generator::Stream, '#output' do 6 | subject { object.output } 7 | 8 | let(:object) { described_class.new } 9 | 10 | shared_examples_for 'stream output' do 11 | it 'contains expected output' do 12 | should eql(expected_output) 13 | end 14 | end 15 | 16 | context 'with empty string' do 17 | let(:expected_output) { '' } 18 | 19 | it_behaves_like 'stream output' 20 | end 21 | 22 | context 'with filled stream' do 23 | before do 24 | object << 'foo' 25 | end 26 | 27 | let(:expected_output) { 'foo' } 28 | 29 | it_behaves_like 'stream output' 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /sql.gemspec: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require File.expand_path('../lib/sql/version', __FILE__) 4 | 5 | Gem::Specification.new do |gem| 6 | gem.name = 'sql' 7 | gem.version = SQL::VERSION.dup 8 | gem.authors = ['Dan Kubb'] 9 | gem.email = 'dan.kubb@gmail.com' 10 | gem.description = 'SQL Parser and Generator' 11 | gem.summary = gem.description 12 | gem.homepage = 'https://github.com/dkubb/sql' 13 | gem.licenses = %w[MIT] 14 | 15 | gem.require_paths = %w[lib] 16 | gem.files = `git ls-files`.split($/) 17 | gem.test_files = `git ls-files -- spec/{unit,integration}`.split($/) 18 | gem.extra_rdoc_files = %w[LICENSE README.md CONTRIBUTING.md TODO] 19 | 20 | gem.add_runtime_dependency('abstract_type', '~> 0.0.7') 21 | gem.add_runtime_dependency('adamantium', '~> 0.1.0') 22 | gem.add_runtime_dependency('ast', '~> 1.1.0') 23 | gem.add_runtime_dependency('ice_nine', '~> 0.11.0') 24 | 25 | gem.add_development_dependency('bundler', '~> 1.3', '>= 1.3.5') 26 | end 27 | --------------------------------------------------------------------------------