├── .gitignore ├── ChangeLog.md ├── LICENSE ├── README.md ├── erb_sql_templates.gemspec ├── lib └── erb_sql_templates.rb └── test ├── connection_stub.rb ├── erb_sql_templates_test.rb └── templates ├── _crazy_manipulation.erb.sql ├── complex_update.erb.sql └── deprecated.sql.erb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | *~ 4 | /.config 5 | /coverage/ 6 | /InstalledFiles 7 | /pkg/ 8 | /spec/reports/ 9 | /test/tmp/ 10 | /test/version_tmp/ 11 | /tmp/ 12 | 13 | ## Specific to RubyMotion: 14 | .dat* 15 | .repl_history 16 | build/ 17 | 18 | ## Documentation cache and generated files: 19 | /.yardoc/ 20 | /_yardoc/ 21 | /doc/ 22 | /rdoc/ 23 | 24 | ## Environment normalisation: 25 | /.bundle/ 26 | /lib/bundler/man/ 27 | 28 | # for a library or gem, you might want to ignore these files since the code is 29 | # intended to run in multiple environments; otherwise, check them in: 30 | # Gemfile.lock 31 | # .ruby-version 32 | # .ruby-gemset 33 | 34 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 35 | .rvmrc 36 | -------------------------------------------------------------------------------- /ChangeLog.md: -------------------------------------------------------------------------------- 1 | 2 | Version 0.2 3 | ================== 4 | * Added partials. 5 | * Changed file extension from `.sql.erb` to `.erb.sql` so text editors will highlight SQL without too much fuss. 6 | 7 | Version 0.1 8 | =================== 9 | First release. 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Tyler Roberts 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ERB SQL Templates for Ruby 2 | 3 | Use `erb_sql_templates` when you have large complex SQL queries that you do not want 4 | embedded inside your Ruby as strings. The gem allows for SQL to be stored in their own 5 | directory and files and the queries can be customized with ERB. 6 | 7 | 8 | ### Example 9 | 10 | Example SQL template file, `some_query.erb.sql`. Templates must have `.erb.sql` extension. 11 | ```sql 12 | -- FILE: find_user.erb.sql 13 | SELECT 14 | *, 15 | <%= render '_say_hello' :name => 'Bob' %> AS hello_message 16 | FROM <%= table_name %> 17 | WHERE 18 | email = <%= h(email) %> 19 | ``` 20 | 21 | ```sql 22 | -- FILE: _say_hello.erb.sql 23 | CONCAT('Hello ', <%= h name %>) 24 | ``` 25 | 26 | 27 | To use the template in Ruby, specify the template directory, 28 | a database connection, and call `#execute` or `#render` with the template name. 29 | Names arguments will be available inside the SQL template. 30 | 31 | ```ruby 32 | template_dir = Rails.root.to_s + '/db/sql/templates' 33 | sql_builder = ErbSqlTemplates.new(template_dir, ActiveRecord::Base.connection) 34 | 35 | # Execute SQL directly 36 | sql_builder.execute(:find_user, :email => 'bob@example.com', :table_name => 'users') 37 | 38 | # Or have it return the SQL 39 | sql = sql_builder.render(:find_user, :email => 'bob@example.com', :table_name => 'users') 40 | ``` 41 | 42 | ### SQL Sanitizing 43 | The library expects that the connection has a `#quote` method on it. 44 | Inside your SQL template you can use the `h` helper to escape variables. 45 | Example: `UPDATE users SET name = <%= h dirty_input %>` 46 | 47 | 48 | ### Running tess 49 | Try `ruby test/*_test.rb`. 50 | 51 | -------------------------------------------------------------------------------- /erb_sql_templates.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = 'erb_sql_templates' 3 | s.version = '0.2' 4 | s.date = '2015-04-01' 5 | s.summary = "Customize large SQL queries with ERB." 6 | s.description = "Utility to render and execute `.erb.sql` templates that have been customized with ERB." 7 | s.authors = ["Tyler Roberts"] 8 | s.email = 'code@polar-concepts.com' 9 | s.files = ["lib/erb_sql_templates.rb"] 10 | s.homepage = 'https://github.com/bdevel/erb_sql_templates' 11 | s.license = 'MIT' 12 | end 13 | -------------------------------------------------------------------------------- /lib/erb_sql_templates.rb: -------------------------------------------------------------------------------- 1 | require 'erb' 2 | require 'ostruct' 3 | 4 | # Name template files as `some_query.sql.erb` -- Use <%= h my_var %> - h helper will sanitize 5 | # est = ErbSqlTemplates.new(Rails.root.to_s + '/db/sql', ActiveRecord::Base.connection) 6 | # est.execute(:some_sql, :my_var => 'Foo!') 7 | # sql = est.render(:some_sql, :my_var => 'Foo!') 8 | 9 | class ErbSqlTemplates 10 | attr_reader :connection 11 | 12 | def initialize(dir, connection) 13 | @directory = dir 14 | @connection = connection 15 | end 16 | 17 | def execute(name, **data) 18 | sql = render(name, data) 19 | @connection.execute(sql) 20 | end 21 | 22 | # Returns built sql 23 | def render(template_name, **data) 24 | scope = TemplateScope.new(self, data) 25 | erb_string = load_template(template_name) 26 | renderer = ERB.new(erb_string, 0, '>') # thread level zero, and '>' means no new lines for <%= %> 27 | return renderer.result(scope.get_binding) 28 | end 29 | 30 | # Returns string of the filename 31 | def locate_template(name) 32 | results = Dir.glob(@directory + "/#{name}.{erb.sql,sql.erb}") 33 | if results.length > 1 34 | raise Exception.new("Too many templates have the name '#{name}'. ") 35 | elsif results.length == 0 36 | raise Exception.new("Cannot find template '#{name}.erb.sql' in the directory '#{@directory}'.") 37 | else 38 | # Check if they are using old file extensions. Only do it once though. 39 | if @@did_send_deprecation_notice != true && results.first.match(/\.sql\.erb$/) 40 | @@did_send_deprecation_notice = true 41 | puts "Deprecation Notice: .sql.erb extensions for ERB SQL templates has been deprecated in favor of .erb.sql extensions." 42 | end 43 | 44 | return results.first 45 | end 46 | end 47 | 48 | # returns string of the template 49 | def load_template(name) 50 | filename = locate_template(name) 51 | File.read(filename) 52 | end 53 | 54 | 55 | class TemplateScope < OpenStruct 56 | def initialize(builder, hash) 57 | @builder = builder 58 | super hash 59 | end 60 | 61 | def render(template_name, **args) 62 | return @builder.render(template_name, **args) 63 | end 64 | 65 | # helper for sanitizing sql inputs 66 | def h(value) 67 | return @builder.connection.quote(value) 68 | end 69 | 70 | # Expose private binding() method. 71 | def get_binding 72 | binding() 73 | end 74 | 75 | # Raise exeption if you try to use a property that does not exist. 76 | def method_missing(prop, *args) 77 | if prop.to_s.include?('=') || (@table != nil && (@table.has_key?(prop) || self.respond_to?(prop)) ) 78 | super 79 | else 80 | raise NoMethodError, "No property `#{prop}` set for this SQL.", caller(1) 81 | end 82 | end 83 | 84 | end 85 | 86 | @@did_send_deprecation_notice=false 87 | end 88 | 89 | 90 | -------------------------------------------------------------------------------- /test/connection_stub.rb: -------------------------------------------------------------------------------- 1 | 2 | class ConnectionStub 3 | attr_reader :executions 4 | 5 | def initialize 6 | @executions = [] 7 | end 8 | 9 | def execute(sql) 10 | @executions << sql 11 | end 12 | 13 | def quote(value) 14 | return value if value.is_a?(Numeric) 15 | return "'" + value.gsub("'", "''") + "'" 16 | end 17 | 18 | end 19 | 20 | -------------------------------------------------------------------------------- /test/erb_sql_templates_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../lib/erb_sql_templates' 2 | require_relative 'connection_stub' 3 | require "minitest/autorun" 4 | 5 | describe ErbSqlTemplates do 6 | 7 | before :each do 8 | @connection = ConnectionStub.new 9 | @builder = ErbSqlTemplates.new('test/templates', @connection) 10 | end 11 | 12 | it "should build sql" do 13 | assert_match /UPDATE my_table/, rendered_sql 14 | assert_match /SET\s+my_column = 'bad''value'/, rendered_sql 15 | end 16 | 17 | it "should render partials" do 18 | partial_sql = 'crazyName = CONCAT(name, "Foo!", lastName)' 19 | assert rendered_sql.include?(partial_sql), "Did not render partial: " + partial_sql 20 | end 21 | 22 | it "should execute sql" do 23 | @builder.execute(:complex_update, 24 | table_name: 'my_table', 25 | assignment_column: 'my_column', 26 | value: "bad'value") 27 | assert_equal 1, @connection.executions.size 28 | end 29 | 30 | it "should give warning about .sql.erb extensions" do 31 | # Stub the puts method. 32 | def @builder.puts(s) 33 | @puts_log ||= [] 34 | @puts_log << s 35 | end 36 | def @builder.puts_log 37 | @puts_log 38 | end 39 | 40 | sql = @builder.render(:deprecated) 41 | sql = @builder.render(:deprecated) 42 | 43 | assert @builder.puts_log.length != 0, "Did not warn user of deprecation." 44 | assert @builder.puts_log.length < 2, "Warned user more than once." 45 | assert_match /\.sql\.erb/i, @builder.puts_log.first 46 | end 47 | 48 | #---------------------------------------------------------------------------- 49 | 50 | def rendered_sql 51 | @builder.render(:complex_update, 52 | table_name: 'my_table', 53 | assignment_column: 'my_column', 54 | value: "bad'value") 55 | end 56 | 57 | end 58 | -------------------------------------------------------------------------------- /test/templates/_crazy_manipulation.erb.sql: -------------------------------------------------------------------------------- 1 | CONCAT(<%= column %>, "Foo!", <%= other_column %>) 2 | -------------------------------------------------------------------------------- /test/templates/complex_update.erb.sql: -------------------------------------------------------------------------------- 1 | 2 | -- Some comments can go here. 3 | 4 | UPDATE <%= table_name %> 5 | SET 6 | <%= assignment_column %> = <%= h value %>, 7 | crazyName = <%= render '_crazy_manipulation', column: 'name', other_column: 'lastName' %>; 8 | -------------------------------------------------------------------------------- /test/templates/deprecated.sql.erb: -------------------------------------------------------------------------------- 1 | SELECT 1; 2 | --------------------------------------------------------------------------------