Class: Epoxy

Inherits:
Object
  • Object
show all
Defined in:
lib/epoxy.rb

Overview

Epoxy - bind data to queries for any query language.

Let me hit ya with some science!

# numbered binds
ep = Epoxy.new("select * from foo where bar=?")
binds = %W[foo]
bound_query = ep.quote { |x| "'" + binds[x] + "'" }
"select * from foo where bar='foo'"

# named binds
binds = { :name => 'Lee', :age => 132 }
ep = Epoxy.new("select * from people where name=?name and age=?age")
bound_query = ep.quote(binds) { |x| "'#{binds[x]}'" }
"select * from people where name='Lee' and age='132'"

# mix them!
binds = { 0 => "Age", :name => 'Lee' }
ep = Epoxy.new("select * from people where name=?name and age=?")
bound_query = ep.quote(binds) { |x| "'#{binds[x]}'" }
"select * from people where name='Lee' and age='Age'"

Epoxy handles:

  • ?<name> for named binds

  • ? for numbered binds

  • ?? for a real question mark

  • ‘?’ for a real question mark

  • comments, weird quoting styles (look at the “holy shit” test for examples)

  • not telling you how to quote your data. This solution works for any query language and any database.

Constant Summary collapse

/[a-zA-Z]+/

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(query, comment_chars = %r{--|//}) ⇒ Epoxy

Takes a query as a string and an optional regexp defining beginning-of-line comments. The binding rules are as follows:

  • ?<name> for named binds

  • ? for numbered binds

  • ?? for a real question mark

  • ‘?’ for a real question mark

  • comments, weird quoting styles are unaffected.



79
80
81
82
83
# File 'lib/epoxy.rb', line 79

def initialize(query, comment_chars=%r{--|//})
  @comment_chars = comment_chars
  @query  = query
  @tokens = self.class.parse_tokens(query, @comment_chars)
end

Instance Attribute Details

#comment_charsObject (readonly)

leader comment characters - defaults to SQL “–”



67
68
69
# File 'lib/epoxy.rb', line 67

def comment_chars
  @comment_chars
end

#queryObject (readonly)

the original query, before quoting.



65
66
67
# File 'lib/epoxy.rb', line 65

def query
  @query
end

#tokensObject (readonly)

tokens generated by Epoxy.parse_tokens. Just use Epoxy#quote for now.



63
64
65
# File 'lib/epoxy.rb', line 63

def tokens
  @tokens
end

Class Method Details

.parse_tokens(query, comment_chars) ⇒ Object

Token parser, isolates components of the query into parts to where they can be managed indepdently.

Probably not the easiest thing to deal with by itself. Use the standard methods plox.



42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# File 'lib/epoxy.rb', line 42

def self.parse_tokens(query, comment_chars)
  query.scan(%r{
    (
      #{comment_chars}.*                  (?# matches "--" style comments to the end of line or string )
      |
      ' ( [^'\\]  |  ''  |  \\. )* '      (?# match strings surrounded by apostophes )
      |
      " ( [^"\\]  |  ""  |  \\. )* "      (?# match strings surrounded by " )
      |
      ['"]                                (?# match a loose quote ) 
      |         
      \?#{LEGAL_NAMED_BIND}               (?# match a named bind )
      |
      \?\??                               (?# match one or two question marks )
      |
      [^'"?]+                          (?# match all characters except ' " ? - : and / )
  )
  }x).collect(&:first)
end

Instance Method Details

#indexed_bindsObject

Returns a hash of position => name (as Symbol), if any, which correspond to binds located in the query. nil is provided as a name if it is an indexed bind already. This is useful for sanitizing features Epoxy has before sending them to the SQL engine.

Ex:

ep = Epoxy.new("select * from foo where bar=?bar and quux=? and foomatic=?foo")
ep.indexed_binds

# yields...
[ :bar, nil, :foo]

NOTE: all syntax lookalikes are considered in this method; in the actual quote() routine, only named binds with a corresponding map are considered.



143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/epoxy.rb', line 143

def indexed_binds
  ary = []

  tokens.each do |toke|
    case toke 
    when '?'
      ary.push(toke)
    when /\?(#{LEGAL_NAMED_BIND})/
      ary.push($1.to_sym)
    end
  end

  ary.map! { |x| x == '?' ? nil : x }

  return ary
end

#quote(binds = {}, &block) ⇒ Object

Processes your query for quoting. Provide a block that emulates how your data should be quoted. This method accepts a Hash to process named bindings, which when provided will yield each successive Hash key which has a match in the named binds. Keys are coerced to symbols before being yielded.

Without a Hash it will yield on each successive bound element with the index of that element passed.

You are responsible for quoting your data properly. Epoxy just makes it easier to get the places you need to quote out of the query.



97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/epoxy.rb', line 97

def quote(binds = {}, &block)
  result = ""
  bind_pos = 0

  binds = binds.keys.inject({}) { |x,y| x.merge({ y.kind_of?(String) ? y.to_sym : y => binds[y] }) }

  tokens.each do |part|
    case part
    when '?'
      result << block.call(bind_pos)
      bind_pos += 1
    when '??'
      result << "?"
    when /^\?(#{LEGAL_NAMED_BIND})$/
      key = $1.to_sym
      if binds.has_key?(key)
        result << block.call(key)
        bind_pos += 1
      else
        result << part
      end
    else
      result << part
    end
  end

  return result
end