SqlStatement 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/example.rb +129 -0
- data/lib/sql/bathon-sxp.rb +265 -0
- data/lib/sql/expression.rb +59 -0
- data/lib/sql/statement.rb +539 -0
- data/lib/sqlstatement.rb +2 -0
- metadata +59 -0
data/bin/example.rb
ADDED
@@ -0,0 +1,129 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require_gem 'SqlStatement'
|
3
|
+
require 'sql/statement'
|
4
|
+
require 'sql/expression'
|
5
|
+
|
6
|
+
# We subclass the standard classes so that we can construct
|
7
|
+
# a query with a complex computed field. The newlists method
|
8
|
+
# makes this easy, giving us a constructor and combination
|
9
|
+
# operators for free.
|
10
|
+
|
11
|
+
class ProbabilityParts < SQLStatement::SelectParts
|
12
|
+
newlists :multiply
|
13
|
+
end
|
14
|
+
class ProbabilityStatement < SQLStatement::SelectCreate
|
15
|
+
newlists :multiply
|
16
|
+
|
17
|
+
# Add our own special fields here, computed
|
18
|
+
# from the list we added to this class.
|
19
|
+
def allfields
|
20
|
+
retval=super
|
21
|
+
retval.merge! :probability =>
|
22
|
+
SQLFunc(@multiply.collect{|x| x.to_sqlpart}.join('*'))
|
23
|
+
retval
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
#In real life, I use a special library for detecting what attributes
|
29
|
+
#are available based on the columns in my tables.
|
30
|
+
#We'll mimic that functionality here.
|
31
|
+
#
|
32
|
+
#The first definition of this module mocks the stuff that's in my more
|
33
|
+
#complicated library
|
34
|
+
module AttributeDetection
|
35
|
+
class Attribute
|
36
|
+
def initialize(domain,name)
|
37
|
+
@domain=domain
|
38
|
+
@name=name
|
39
|
+
end
|
40
|
+
attr_reader :name, :domain
|
41
|
+
end
|
42
|
+
class Hierarchy < Attribute
|
43
|
+
def initialize(leftcolumn)
|
44
|
+
ignored1,m_domain,m_name,ignored2=leftcolumn.split(/_/)
|
45
|
+
super(m_domain,m_name)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
class DomainlessPerGroup < Attribute
|
49
|
+
def initialize (colname)
|
50
|
+
name=@colname=colname
|
51
|
+
domain="!"
|
52
|
+
super(domain,name)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
#I have a few other attribute types, none of which
|
57
|
+
#are used in this code yet.
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
include SQLHelpers
|
62
|
+
|
63
|
+
#The second definition of this module is the code that I actually define
|
64
|
+
#in this particular program.
|
65
|
+
module AttributeDetection
|
66
|
+
class Attribute
|
67
|
+
def condname
|
68
|
+
:"cond_#{name}"
|
69
|
+
end
|
70
|
+
def priorname
|
71
|
+
:"prior_#{name}"
|
72
|
+
end
|
73
|
+
#parts that go in to the CREATE TABLE `probabilities` statement
|
74
|
+
#which relate to the attribute we are disambiguating
|
75
|
+
def probability_disambig
|
76
|
+
parts=ProbabilityParts.new
|
77
|
+
parts.add_fields [priorname[probkey]]
|
78
|
+
parts.add_tables [priorname]
|
79
|
+
parts.multiply << priorname[:pt]
|
80
|
+
parts
|
81
|
+
end
|
82
|
+
#parts that go in to the CREATE TABLE `probabilities` statement
|
83
|
+
#which relate to the attributes we are using as features
|
84
|
+
def probability_features(conditionalon)
|
85
|
+
parts=ProbabilityParts.new
|
86
|
+
parts.tables={priorname=>priorname,condname=>condname}
|
87
|
+
parts.add_fields [priorname[probkey]]
|
88
|
+
parts.conditions << sql_func{ condname[probkey]==priorname[probkey]}
|
89
|
+
parts.conditions << sql_func{
|
90
|
+
condname[conditionalon.probkey]==
|
91
|
+
conditionalon.priorname[conditionalon.probkey]
|
92
|
+
}
|
93
|
+
parts.multiply << sql_func{condname[:pt]/priorname[:pt]}
|
94
|
+
parts
|
95
|
+
end
|
96
|
+
end
|
97
|
+
class Hierarchy
|
98
|
+
def probkey
|
99
|
+
"att_#{domain}_#{name}_left".to_sym
|
100
|
+
end
|
101
|
+
end
|
102
|
+
class DomainlessPerGroup
|
103
|
+
def probkey
|
104
|
+
@colname.to_sym
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
include AttributeDetection
|
110
|
+
include SQLHelpers
|
111
|
+
|
112
|
+
#And this code calls methods to actually build the query.
|
113
|
+
|
114
|
+
#First we define the names of the attributes
|
115
|
+
att_disambig=Hierarchy.new("att_appraisal_attitude_left")
|
116
|
+
att_features=[Hierarchy.new("att_products_appraisedtype_left"),
|
117
|
+
DomainlessPerGroup.new("priority")]
|
118
|
+
|
119
|
+
|
120
|
+
#Then we construct the statement
|
121
|
+
stmt=ProbabilityStatement.new :probabilities,true
|
122
|
+
|
123
|
+
stmt << att_disambig.probability_disambig
|
124
|
+
att_features.each { |x| stmt<< x.probability_features(att_disambig) }
|
125
|
+
|
126
|
+
#Finally, we print it (or execute it against the database)
|
127
|
+
puts stmt.to_s
|
128
|
+
|
129
|
+
|
@@ -0,0 +1,265 @@
|
|
1
|
+
#Copyright 2006 Dominik Bathon, released under the BSD license.
|
2
|
+
#
|
3
|
+
#Written for Ruby Quiz #95[http://www.rubyquiz.com/quiz95.html]. If you
|
4
|
+
#dislike the use of rubynode (or rubynode breaks in some future
|
5
|
+
#version), you can probably substitute in any solution to the quiz,
|
6
|
+
#although you may lose some functionality by doing so. Particularly, the
|
7
|
+
#+or+, +and+, +not+, +||+, +&&+, +!+ and +!=+ operators are difficult or
|
8
|
+
#impossible to make work correctly in a pure ruby s-expression
|
9
|
+
#generator.
|
10
|
+
|
11
|
+
require 'rubygems'
|
12
|
+
require "rubynode"
|
13
|
+
|
14
|
+
class Node2Sexp
|
15
|
+
BINARY_METHODS = [:+, :-, :*, :/, :%, :**, :^, :<, :>, :<=, :>=, :==, :|, :&]
|
16
|
+
def initialize(binding)
|
17
|
+
@binding = binding
|
18
|
+
end
|
19
|
+
|
20
|
+
# (transformed) nodes are arrays, that look like:
|
21
|
+
# [:type, attribute hash or array of nodes]
|
22
|
+
def to_sexp(node)
|
23
|
+
node && send("#{node.first}_to_sexp", node.last)
|
24
|
+
end
|
25
|
+
|
26
|
+
# fixed argument lists are represented as :array nodes, e.g.
|
27
|
+
# [:array, [argnode1, argnode2, ...]]
|
28
|
+
def process_args(args_node)
|
29
|
+
return [] unless args_node
|
30
|
+
if args_node.first == :array
|
31
|
+
args_node.last.map { |node| to_sexp(node) }
|
32
|
+
else
|
33
|
+
raise "variable arguments not allowed"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def is_sql?(arg)
|
38
|
+
return true if arg[0]==:vcall and arg[1][:mid]==:sql
|
39
|
+
return true if [:lvar,:dvar].include?(arg[0]) and arg[1][:vid]==:sql
|
40
|
+
return false
|
41
|
+
end
|
42
|
+
|
43
|
+
#returns the +self+ in which the block was contained
|
44
|
+
def origself
|
45
|
+
eval("self",@binding)
|
46
|
+
end
|
47
|
+
|
48
|
+
# :call nodes: method call with explicit receiver:
|
49
|
+
# nil.foo => [:call, {:args=>false, :mid=>:foo, :recv=>[:nil, {}]}]
|
50
|
+
# nil == nil =>
|
51
|
+
# [:call, {:args=>[:array, [[:nil, {}]]], :mid=>:==, :recv=>[:nil, {}]}]
|
52
|
+
def call_to_sexp(hash)
|
53
|
+
mid=hash[:mid]
|
54
|
+
args=process_args(hash[:args])
|
55
|
+
if is_sql?(hash[:recv])
|
56
|
+
return [mid, *args]
|
57
|
+
end
|
58
|
+
recv=to_sexp(hash[:recv])
|
59
|
+
if BINARY_METHODS.include?(mid)
|
60
|
+
[mid, recv, *args]
|
61
|
+
else
|
62
|
+
recv.send(mid,*args)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# :fcall nodes: function call (no explicit receiver):
|
67
|
+
# foo() => [:fcall, {:args=>false, :mid=>:foo}]
|
68
|
+
# foo(nil) => [:fcall, {:args=>[:array, [[:nil, {}]]], :mid=>:foo]
|
69
|
+
def fcall_to_sexp(hash)
|
70
|
+
if origself.methods.include?(hash[:mid].to_s)
|
71
|
+
origself.send(hash[:mid],*process_args(hash[:args]))
|
72
|
+
else
|
73
|
+
[hash[:mid], *process_args(hash[:args])]
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# :vcall nodes: function call that looks like variable
|
78
|
+
# foo => [:vcall, {:mid=>:foo}]
|
79
|
+
alias vcall_to_sexp fcall_to_sexp
|
80
|
+
|
81
|
+
# :lit nodes: literals
|
82
|
+
# 1 => [:lit, {:lit=>1}]
|
83
|
+
# :abc => [:lit, {:lit=>:abc}]
|
84
|
+
def lit_to_sexp(hash)
|
85
|
+
hash[:lit]
|
86
|
+
end
|
87
|
+
|
88
|
+
# :str nodes: strings without interpolation
|
89
|
+
# "abc" => [:str, {:lit=>"abc"}]
|
90
|
+
alias str_to_sexp lit_to_sexp
|
91
|
+
|
92
|
+
def nil_to_sexp(hash) nil end
|
93
|
+
def false_to_sexp(hash) false end
|
94
|
+
def true_to_sexp(hash) true end
|
95
|
+
|
96
|
+
# :lvar nodes: local variables
|
97
|
+
# var => [:lvar, {:cnt=>3, :vid=>:var}]
|
98
|
+
# cnt is the index in the lvar table
|
99
|
+
def lvar_to_sexp(hash)
|
100
|
+
eval(hash[:vid].to_s, @binding)
|
101
|
+
end
|
102
|
+
# :dvar nodes: block local variables
|
103
|
+
# var => [:dvar, {:vid=>:var}]
|
104
|
+
alias dvar_to_sexp lvar_to_sexp
|
105
|
+
# :ivar nodes: instance variables
|
106
|
+
alias ivar_to_sexp lvar_to_sexp
|
107
|
+
# :cvar nodes: class variables
|
108
|
+
alias cvar_to_sexp lvar_to_sexp
|
109
|
+
alias const_to_sexp lvar_to_sexp
|
110
|
+
|
111
|
+
def self_to_sexp(hash)
|
112
|
+
origself
|
113
|
+
end
|
114
|
+
|
115
|
+
# :not nodes: boolean negation
|
116
|
+
# not :field => [:not, {:body=>[:lit, {:lit=>:field}]}]
|
117
|
+
# !:field => [:not, {:body=>[:lit, {:lit=>:field}]}]
|
118
|
+
def not_to_sexp(hash)
|
119
|
+
body = to_sexp(hash[:body])
|
120
|
+
if Array === body && body[0] == :== && body.size == 3
|
121
|
+
[:"!=", body[1], body[2]]
|
122
|
+
else
|
123
|
+
[:not, body]
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def and_to_sexp(hash)
|
128
|
+
[:& , to_sexp(hash[:first]), to_sexp(hash[:second])]
|
129
|
+
end
|
130
|
+
|
131
|
+
def or_to_sexp(hash)
|
132
|
+
[:| , to_sexp(hash[:first]), to_sexp(hash[:second])]
|
133
|
+
end
|
134
|
+
|
135
|
+
end
|
136
|
+
|
137
|
+
def sxp(&block)
|
138
|
+
body = block.body_node
|
139
|
+
return nil unless body
|
140
|
+
Node2Sexp.new(block).to_sexp(body.transform)
|
141
|
+
end
|
142
|
+
|
143
|
+
if $0 == __FILE__ then
|
144
|
+
require 'test/unit'
|
145
|
+
|
146
|
+
class TestQuiz < Test::Unit::TestCase
|
147
|
+
def test_sxp_nested_calls
|
148
|
+
assert_equal [:max, [:count, :name]], sxp{max(count(:name))}
|
149
|
+
end
|
150
|
+
|
151
|
+
def test_sxp_vcall
|
152
|
+
assert_equal [:abc], sxp{abc}
|
153
|
+
end
|
154
|
+
|
155
|
+
def test_sxp_call_plus_eval
|
156
|
+
assert_equal [:count, [:+, 3, 7]], sxp{count(3+7)}
|
157
|
+
end
|
158
|
+
|
159
|
+
def test_sxp_call_with_multiple_args
|
160
|
+
assert_equal [:count, 3, 7], sxp{count(3,7)}
|
161
|
+
end
|
162
|
+
|
163
|
+
def test_sxp_binarymsg_mixed_1
|
164
|
+
assert_equal [:+, 3, :symbol], sxp{3+:symbol}
|
165
|
+
end
|
166
|
+
|
167
|
+
def test_sxp_binarymsg_mixed_call
|
168
|
+
assert_equal [:+, 3, [:count, :field]], sxp{3+count(:field)}
|
169
|
+
end
|
170
|
+
|
171
|
+
def test_sxp_binarymsg_mixed_2
|
172
|
+
assert_equal [:/, 7, :field], sxp{7/:field}
|
173
|
+
end
|
174
|
+
|
175
|
+
def test_sxp_binarymsg_mixed_3
|
176
|
+
assert_equal [:>, :field, 5], sxp{:field > 5}
|
177
|
+
end
|
178
|
+
|
179
|
+
def test_sxp_lits
|
180
|
+
assert_equal 8, sxp{8}
|
181
|
+
end
|
182
|
+
|
183
|
+
def test_sxp_true_false_nil
|
184
|
+
assert_equal [:+, true, false], sxp{true+false}
|
185
|
+
assert_equal nil, sxp{nil}
|
186
|
+
end
|
187
|
+
|
188
|
+
def test_sxp_empty
|
189
|
+
assert_equal nil, sxp{}
|
190
|
+
end
|
191
|
+
|
192
|
+
def test_sxp_binarymsg_syms
|
193
|
+
assert_equal [:==, :field1, :field2], sxp{:field1 == :field2 }
|
194
|
+
end
|
195
|
+
|
196
|
+
def test_sxp_variables
|
197
|
+
lvar = :field # local variable
|
198
|
+
assert_equal [:count, :field], sxp{ count(lvar) }
|
199
|
+
proc {
|
200
|
+
dvar = :field2 # dynavar (block local variable)
|
201
|
+
assert_equal [:==, :field, :field2], sxp{ lvar == dvar }
|
202
|
+
}.call
|
203
|
+
end
|
204
|
+
|
205
|
+
def test_sxp_not
|
206
|
+
assert_equal [:not, :field], sxp{ not :field }
|
207
|
+
assert_equal [:"!=", :a, :b], sxp{ :a != :b }
|
208
|
+
end
|
209
|
+
|
210
|
+
def test_sxp_from_sander_dot_land_at_gmail_com
|
211
|
+
assert_equal [:==,[:^, 2, 3], [:^, 1, 1]], sxp{ 2^3 == 1^1}
|
212
|
+
assert_equal [:==, [:+, 3.0, 0.1415], 3], sxp{3.0 + 0.1415 == 3}
|
213
|
+
|
214
|
+
assert_equal([:|,
|
215
|
+
[:==, [:+, :hello, :world], :helloworld],
|
216
|
+
[:==, [:+, [:+, "hello", " "], "world"], "hello world"]] ,
|
217
|
+
sxp {
|
218
|
+
(:hello + :world == :helloworld) |
|
219
|
+
('hello' + ' ' + 'world' == 'hello world')
|
220
|
+
})
|
221
|
+
|
222
|
+
# assert_equal [:==, [:+, [:abs, [:factorial, 3]],
|
223
|
+
# [:*, [:factorial, 4], 42]],
|
224
|
+
# [:+, [:+, 4000000, [:**, 2, 32]], [:%, 2.7, 1.1]]],
|
225
|
+
# sxp{ 3.factorial.abs + 4.factorial * 42 == 4_000_000 + 2**32 + 2.7 % 1.1 }
|
226
|
+
end
|
227
|
+
|
228
|
+
def test_ihavenocluewhy
|
229
|
+
assert_equal 11, 5 + 6
|
230
|
+
assert_raise(TypeError) { 7 / :field }
|
231
|
+
assert_raise(NoMethodError) { 7+count(:field) }
|
232
|
+
assert_raise(NoMethodError) { :field > 5 }
|
233
|
+
end
|
234
|
+
|
235
|
+
def test_methodcall
|
236
|
+
#free functions should be turned in to s-expressions
|
237
|
+
#binary operators (which are specially-named methods) should be
|
238
|
+
# turned into s-expressions
|
239
|
+
#(these are tested in other tests)
|
240
|
+
|
241
|
+
#but bound functions should be called, and this is what's tested
|
242
|
+
#here
|
243
|
+
assert_equal [:+,3,5], sxp{ 3 + "hello".length }
|
244
|
+
end
|
245
|
+
|
246
|
+
def test_indexoperator
|
247
|
+
Symbol.class_eval { define_method :[] do |arg| self end }
|
248
|
+
assert_equal :table, sxp{:table[:field]}
|
249
|
+
end
|
250
|
+
|
251
|
+
def test_keywords_and_or
|
252
|
+
assert_equal [:|, [:<, 1, 2], [:<, 3, 4]], sxp{1<2 or 3<4}
|
253
|
+
assert_equal [:|, [:<, 1, 2], [:<, 3, 4]], sxp{1<2 || 3<4}
|
254
|
+
assert_equal [:&, [:<, 1, 2], [:<, 3, 4]], sxp{1<2 and 3<4}
|
255
|
+
assert_equal [:&, [:<, 1, 2], [:<, 3, 4]], sxp{1<2 && 3<4}
|
256
|
+
end
|
257
|
+
|
258
|
+
def test_self
|
259
|
+
assert_equal self, sxp{self}
|
260
|
+
assert_equal [:object_id], sxp{object_id}
|
261
|
+
assert_equal object_id, sxp{self.object_id}
|
262
|
+
end
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'sql/bathon-sxp'
|
2
|
+
|
3
|
+
class Array
|
4
|
+
def rest
|
5
|
+
self[1..-1]
|
6
|
+
end
|
7
|
+
def rest_sqlpart
|
8
|
+
rest.collect {|x| x.to_sqlpart}.join(", ")
|
9
|
+
end
|
10
|
+
def to_sqlpart
|
11
|
+
if first==:count_distinct
|
12
|
+
"count(distinct "+rest_sqlpart+")"
|
13
|
+
elsif first.to_s =~ /[a-zA-Z]/
|
14
|
+
first.to_s+"("+rest_sqlpart+")"
|
15
|
+
elsif x={:& => " and ", :| => " or ", :== => "="}[first]
|
16
|
+
"("+self[1].to_sqlpart+x+self[2].to_sqlpart+")"
|
17
|
+
else
|
18
|
+
"("+self[1].to_sqlpart+first.to_s+self[2].to_sqlpart+")"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
def placeheld
|
22
|
+
rest.collect{|x| x.placeheld}.flatten
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
module SQLHelpers
|
27
|
+
#this function takes a block containing an expression, and converts
|
28
|
+
#it to a proper SQL expression.
|
29
|
+
#
|
30
|
+
#The rules for doing so are quite complex:
|
31
|
+
#* all Ruby binary operaters are converted to their SQL equivalents
|
32
|
+
#* the + operator on strings is never converted to the
|
33
|
+
# <tt>concat()</tt> function. Use the concat() function directly.
|
34
|
+
#* the <tt>count_distinct(...)</tt> function is converted to
|
35
|
+
# <tt>count(distinct ...)</tt>.
|
36
|
+
#* all Ruby variable references are evaluated and their values
|
37
|
+
# substituted, with the exception of the special +sql+ logical
|
38
|
+
# method reciever.
|
39
|
+
#* method calls on ruby objects are evaluated, with the exception of
|
40
|
+
# the logical +sql+ method reciever.
|
41
|
+
#* free functions (for example <tt>count(:field)</tt>) that are
|
42
|
+
# undefined in the surrounding scope are returned as SQL
|
43
|
+
# function calls.
|
44
|
+
#* free functions that are defined in the surrounding scope are
|
45
|
+
# called by Ruby. To override this behavior, specify the function
|
46
|
+
# as a method call on the virtual reciever +sql+ (for example,
|
47
|
+
# <tt>sql.object_id</tt> becomes <tt>object_id()</tt>, because
|
48
|
+
# in ordinary Ruby contexts, object_id is almost always defined,
|
49
|
+
# and will therefore be evaluated.)
|
50
|
+
#* Symbols are interpreted as field names, just like everywhere else
|
51
|
+
# in this library. Array indexes evaluate to tablename.fieldname
|
52
|
+
# references. The special symbol <tt>:*</tt> converts to a literal
|
53
|
+
# SQL <tt>*</tt>
|
54
|
+
#* <tt>#{...}</tt> interpolated strings are not supported (yet).
|
55
|
+
#* Other objects (except Arrays) are treated as themselves.
|
56
|
+
def sql_func(&block)
|
57
|
+
sxp(&block)
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,539 @@
|
|
1
|
+
=begin rdoc
|
2
|
+
= sqlstatement - Generate complex SQL statements programmatically
|
3
|
+
|
4
|
+
The main goal of this library is to be able to construct an SQL statement
|
5
|
+
from "slices" that concern different aspects of the final query (perhaps
|
6
|
+
in different places in your code) and then combine them all together into
|
7
|
+
one statement easily.
|
8
|
+
|
9
|
+
Another important goal of this library is to give some consistent Ruby
|
10
|
+
syntax to three statements (INSERT, SELECT, and UPDATE) that seem to have
|
11
|
+
different enough syntax that one has two write different code to generate
|
12
|
+
each kind of statement.
|
13
|
+
|
14
|
+
I use my SQL database (specifically MySQL) largely as a bulk data
|
15
|
+
processing engine, by doing INSERT...SELECT or CREATE TABLE...SELECT
|
16
|
+
statements. This library is intended to make that kind of coding easier. I
|
17
|
+
expect that Object Relational mappers (such as ActiveRecord) are more
|
18
|
+
useful for most people, who are performing queries and
|
19
|
+
inserting/updating/querying for individual records. I have nevertheless
|
20
|
+
added INSERT...VALUES statements, and will add other statements soon, for
|
21
|
+
consistency.
|
22
|
+
|
23
|
+
This library is inspired by CLSQL[http://clsql.b9.com/] for Common LISP,
|
24
|
+
or SchemeQL[http://schematics.sourceforge.net/schemeql.html] for Scheme,
|
25
|
+
although it is very different from these two libraries. Scheme and
|
26
|
+
LISP's use of s-expressions make it very easy to construct an entire
|
27
|
+
sublanguage for the WHERE clause, simply by list parsing. The
|
28
|
+
Criteria[http://mephle.org/Criteria/] library for Ruby has attempted
|
29
|
+
this, but in a more limited manner than SchemeQL or CLSQL. My library
|
30
|
+
aims to cover much of the functionality in these libraries.
|
31
|
+
|
32
|
+
This library doesn't try to abstract out the limitations of your DBMS, and
|
33
|
+
I think that the SQL it uses should be fairly portable, in large measure
|
34
|
+
because it hasn't attempted to deal with serious CREATE TABLE statements,
|
35
|
+
where a lot of syntax concerning types, keys and sequences is much more
|
36
|
+
variable.
|
37
|
+
|
38
|
+
==License
|
39
|
+
|
40
|
+
Copyright (c) 2006 Ken Bloom
|
41
|
+
All rights reserved.
|
42
|
+
|
43
|
+
Redistribution and use in source and binary forms, with or without
|
44
|
+
modification, are permitted provided that the following conditions
|
45
|
+
are met:
|
46
|
+
1. Redistributions of source code must retain the above copyright
|
47
|
+
notice, this list of conditions and the following disclaimer.
|
48
|
+
2. Redistributions in binary form must reproduce the above copyright
|
49
|
+
notice, this list of conditions and the following disclaimer in the
|
50
|
+
documentation and/or other materials provided with the distribution.
|
51
|
+
3. Neither Ken Bloom's name, nor the name of any of his contributors may
|
52
|
+
may be used to endorse or promote products derived from this software
|
53
|
+
without specific prior written permission.
|
54
|
+
|
55
|
+
THIS SOFTWARE IS PROVIDED BY THE KEN BLOOM AND CONTRIBUTORS ``AS IS'' AND
|
56
|
+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
57
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
58
|
+
ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
|
59
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
60
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
61
|
+
OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
62
|
+
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
63
|
+
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
64
|
+
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
65
|
+
SUCH DAMAGE.
|
66
|
+
=end
|
67
|
+
|
68
|
+
# Everything in Ruby is a descendant of the Object class. Practically
|
69
|
+
# speaking, this means that any object that hasn't otherwise defined
|
70
|
+
# the +placeheld+ and <tt>to_sqlpart</tt> methods will be treated as a piece
|
71
|
+
# of data (rather than as an SQL instruction).
|
72
|
+
class Object
|
73
|
+
|
74
|
+
# Generates a string representation to be included in the SQL statement's string representation.
|
75
|
+
def to_sqlpart
|
76
|
+
"?"
|
77
|
+
end
|
78
|
+
# When the string representation includes unbound variables (represented in sql with a +?+),
|
79
|
+
# then the values to be bound to them are returned here, in the proper order.
|
80
|
+
# This allows SQL statements to be called in a manner similar to
|
81
|
+
# s=Select.new
|
82
|
+
# s << SelectParts.new {...}
|
83
|
+
# dbh.execute(s.to_s, *s.placeheld)
|
84
|
+
# When there are no unbound variables, and empty array is returned.
|
85
|
+
def placeheld; [self]; end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Symbols are used for representing table names and field names, and aliases for these.
|
89
|
+
# They correctly expand into SQL so as not to cause syntax errors, even if the symbol happens
|
90
|
+
# to be the same as a reserved word in SQL.
|
91
|
+
class Symbol
|
92
|
+
|
93
|
+
def to_sqlpart
|
94
|
+
if self==:*
|
95
|
+
to_s
|
96
|
+
else
|
97
|
+
"`"+to_s+"`"
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def placeheld; []; end
|
102
|
+
|
103
|
+
#Create an SQL_Field object referencing the column +column+ in the
|
104
|
+
#table named by this symbol.
|
105
|
+
def [](column)
|
106
|
+
SQLStatement::SQL_Field.new(self,column)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
module SQLStatement
|
111
|
+
|
112
|
+
# This class is used to represent complex SQL expressions, so that
|
113
|
+
# queries can return expressions. This is also meant to be used for
|
114
|
+
# conditions in a WHERE clause. There are no utility classes for
|
115
|
+
# generating these (even though they'll probably include field names
|
116
|
+
# and other kinds of values), but for convenience, they can be generated
|
117
|
+
# with <tt>Kernel#SQLFunc</tt>.
|
118
|
+
class Function < String
|
119
|
+
|
120
|
+
def initialize(val="",placeheld=[])
|
121
|
+
super
|
122
|
+
@placeheld=placeheld
|
123
|
+
end
|
124
|
+
|
125
|
+
#you need to manually keep this list of values for placeholders in
|
126
|
+
#sync with the actual string contained in this expression.
|
127
|
+
attr_accessor :placeheld
|
128
|
+
|
129
|
+
def to_sqlpart
|
130
|
+
self
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
|
135
|
+
# +SQL_Field+ is used for representing field names qualified by table names.
|
136
|
+
# Their text expansion looks like <tt>`tablename`.`fieldname`</tt>.
|
137
|
+
class SQL_Field
|
138
|
+
|
139
|
+
def initialize(table,field)
|
140
|
+
@table,@field=table,field
|
141
|
+
end
|
142
|
+
|
143
|
+
attr_reader :table, :field
|
144
|
+
|
145
|
+
def to_sqlpart
|
146
|
+
@table.to_sqlpart+"."+@field.to_sqlpart
|
147
|
+
end
|
148
|
+
|
149
|
+
#This is used when performing #{...} substitions
|
150
|
+
#although technically, it should be made to work like the Symbol.to_s
|
151
|
+
#(which gives unquoted results), this works just fine giving quoted
|
152
|
+
#results.
|
153
|
+
def to_s
|
154
|
+
to_sqlpart
|
155
|
+
end
|
156
|
+
|
157
|
+
def placeheld; []; end
|
158
|
+
end
|
159
|
+
|
160
|
+
|
161
|
+
# This class is used to represent a (possibly coherent) set of parts to add to a SQL statement.
|
162
|
+
# If we wanted to generate SQL code similar to
|
163
|
+
# SELECT `value1`,`value2`
|
164
|
+
# FROM `jointable`,`dictionary1`,`dictionary2`
|
165
|
+
# WHERE `jointable`.`id1`=`dictionary1`.`id` AND `jointable`.`id2`=`dictionary2`.`id`
|
166
|
+
# then it may make sense for your application to generate
|
167
|
+
# the +dictionary1+ code and the +dictionary2+ code separately,
|
168
|
+
# in different +SelectParts+ objects, then combine them into one +Select+.
|
169
|
+
# stmt=Select.new
|
170
|
+
# stmt.tablesarray << :jointable
|
171
|
+
# (1..2).each do |x|
|
172
|
+
# s=SelectParts.new
|
173
|
+
# s.add_fields [:"value#{x}"]
|
174
|
+
# s.add_tables [:"dictionary#{x}"]
|
175
|
+
# s.conditions << SQLFunc("`jointable`.`id#{x}`=`dictionary#{x}`.`id`")
|
176
|
+
# stmt << s
|
177
|
+
# end
|
178
|
+
# dbh.execute(stmt.to_s,*stmt.placeheld)
|
179
|
+
class SelectParts
|
180
|
+
|
181
|
+
def expectarray (arg,hash)
|
182
|
+
v=hash[arg]
|
183
|
+
if v==nil
|
184
|
+
[]
|
185
|
+
elsif Array===v
|
186
|
+
v
|
187
|
+
else
|
188
|
+
[v]
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
def expecthash (arg,hash,*options)
|
193
|
+
v=hash[arg]
|
194
|
+
if v==nil
|
195
|
+
v={}
|
196
|
+
elsif Array===v and not options.include?(:PreserveArray)
|
197
|
+
v=Hash[*([v,v].transpose.flatten)]
|
198
|
+
elsif Array===v
|
199
|
+
v
|
200
|
+
elsif Hash===v
|
201
|
+
v
|
202
|
+
else
|
203
|
+
[v]
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
protected :expectarray,:expecthash
|
208
|
+
|
209
|
+
#Initializes the various pieces of the SQL statement to values specified in the hash.
|
210
|
+
#Use Symbols corresponding to the attribute names to set an attribute here, otherwise a default
|
211
|
+
#empty array or hash will be used.
|
212
|
+
def initialize(hash={})
|
213
|
+
@fields=expecthash :fields,hash,:PreserveArray
|
214
|
+
@tables=expecthash :tables,hash,:PreserveArray
|
215
|
+
@conditions=expectarray :conditions,hash
|
216
|
+
@groupby=expectarray :groupby,hash
|
217
|
+
@orderby=expectarray :orderby,hash
|
218
|
+
end
|
219
|
+
|
220
|
+
#See the documentation for SelectStatement which describes these.
|
221
|
+
attr_accessor :conditions, :groupby, :orderby, :fields, :tables
|
222
|
+
|
223
|
+
#Adds an array of unaliased fields to this SQL statement. See
|
224
|
+
#documentation at SelectStatement
|
225
|
+
def add_fields (newfields)
|
226
|
+
newfields.each do |x|
|
227
|
+
@fields[x]=x
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
#Adds an array of unaliased tables to this SQL statement. See
|
232
|
+
#documentation for SelectStatement
|
233
|
+
def add_tables (newtables)
|
234
|
+
newtables.each do |x|
|
235
|
+
@tables[x]=x
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
#Merge several SelectParts objects into one.
|
240
|
+
def +(rhs)
|
241
|
+
b=self.dup
|
242
|
+
b.fields.merge!(rhs.fields)
|
243
|
+
b.tables.merge!(rhs.tables)
|
244
|
+
b.conditions+=rhs.conditions
|
245
|
+
b.groupby+=rhs.groupby
|
246
|
+
b.orderby+=rhs.orderby
|
247
|
+
b
|
248
|
+
end
|
249
|
+
|
250
|
+
class << self
|
251
|
+
#Use this to define additional array attributes in descendant classes
|
252
|
+
#with all of the necessary addition, initialization and merging (<<)
|
253
|
+
#semantics. <b>You can only call this function once per class, as
|
254
|
+
#it redefines methods each time it is called.</b>
|
255
|
+
def newlists *lists
|
256
|
+
attr_accessor *lists
|
257
|
+
|
258
|
+
lines=lists.collect do |listname|
|
259
|
+
["@#{listname}=expectarray :#{listname},hash",
|
260
|
+
"b.#{listname}+=rhs.#{listname}"]
|
261
|
+
end.transpose
|
262
|
+
class_eval <<-"end;"
|
263
|
+
def initialize hash={}
|
264
|
+
super
|
265
|
+
#{lines[0].join("\n")}
|
266
|
+
end
|
267
|
+
def + (rhs)
|
268
|
+
b=super
|
269
|
+
#{lines[1].join("\n")}
|
270
|
+
b
|
271
|
+
end
|
272
|
+
end;
|
273
|
+
end
|
274
|
+
protected :newlists
|
275
|
+
end
|
276
|
+
|
277
|
+
end
|
278
|
+
|
279
|
+
# Creates a SELECT statement
|
280
|
+
class Select
|
281
|
+
def initialize
|
282
|
+
@fields={}
|
283
|
+
@tables={}
|
284
|
+
@conditions=[]
|
285
|
+
@groupby=[]
|
286
|
+
@orderby=[]
|
287
|
+
end
|
288
|
+
|
289
|
+
#This is the list of fields or instructions to be retrieved. In a
|
290
|
+
#+SELECT+ statement, this appears immediately after the +SELECT+ keyword.
|
291
|
+
#In an +UPDATE+ statement, this appears after the +SET+ keyword. (And in other
|
292
|
+
#places for other kinds of queries #In an +UPDATE+ statement, this appears after the +SET+ keyword. (And in other
|
293
|
+
#places for other kinds of queries. The SQL inventors were nothing if not consistent.)
|
294
|
+
#
|
295
|
+
#This is a hash of fields to be used in the SQL statmenet. Entries are in the
|
296
|
+
#form <tt>alias => underlying_expression</tt>. If you are not
|
297
|
+
#aliasing a field name, use the form <tt>fieldname => fieldname</tt>.
|
298
|
+
attr_accessor :fields
|
299
|
+
|
300
|
+
#Adds an array of unaliased fields to this SQL statement.
|
301
|
+
def add_fields (newfields)
|
302
|
+
newfields.each do |x|
|
303
|
+
@fields[x]=x
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
#This is the tables to include in the query (i.e. the +FROM+ clause).
|
308
|
+
#
|
309
|
+
#This is a hash of tables to be used in the SQL statmenet. Entries are in the
|
310
|
+
#form <tt>alias => underlying_expression</tt>. If you are not
|
311
|
+
#aliasing a table name, use the form <tt>tablename => tablename</tt>.
|
312
|
+
attr_accessor :tables
|
313
|
+
|
314
|
+
#Adds an array of unaliased tables to this SQL statement.
|
315
|
+
def add_tables (newtables)
|
316
|
+
newtables.each do |x|
|
317
|
+
@tables[x]=x
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
321
|
+
#This is an array of Function objects that specify the contents of
|
322
|
+
#the +WHERE+ clause.
|
323
|
+
attr_accessor :conditions
|
324
|
+
|
325
|
+
#Specifieds fields or expressions to group by, as an array.
|
326
|
+
attr_accessor :groupby
|
327
|
+
|
328
|
+
#Specifieds fields or expressions to sort by, as an array.
|
329
|
+
attr_accessor :orderby
|
330
|
+
|
331
|
+
#If you overriding this class (or any of its subclasses) to constructing other fields
|
332
|
+
#from other lists, override this function, call +super+, construct the additional fields here,
|
333
|
+
#and add them to the result of +super+. Then return the resulting hash.
|
334
|
+
def allfields
|
335
|
+
@fields.dup
|
336
|
+
end
|
337
|
+
|
338
|
+
#Select whether this is a SELECT statement or a SELECT DISTINCT
|
339
|
+
#statement. (non-distinct by default)
|
340
|
+
attr_writer :distinct
|
341
|
+
|
342
|
+
def distinct
|
343
|
+
@distinct ||= false
|
344
|
+
end
|
345
|
+
|
346
|
+
#Merges the various parts of a SelectParts into the correct places in this SQL statement.
|
347
|
+
def << (parts)
|
348
|
+
@fields.merge!(parts.fields)
|
349
|
+
@tables.merge!(parts.tables)
|
350
|
+
@conditions += parts.conditions
|
351
|
+
@groupby += parts.groupby
|
352
|
+
@orderby += parts.orderby
|
353
|
+
self
|
354
|
+
end
|
355
|
+
|
356
|
+
#Returns the SQL string that you would actually want to execute on the database.
|
357
|
+
#it may contain placeholders for actual data. Those actual data can
|
358
|
+
#be retrieved with the placeheld method, and used in a manner similar to
|
359
|
+
# dbh.execute(s.to_s,*s.placeheld)
|
360
|
+
def to_s
|
361
|
+
statement="SELECT #{distinct ? 'DISTINCT' : ''} #{fields_s} FROM #{tables_s}"
|
362
|
+
v=conditions_s; statement << " WHERE "<< v if v
|
363
|
+
v=groupby_s; statement << " GROUP BY "<< v if v
|
364
|
+
v=orderby_s; statement << " ORDER BY "<< v if v
|
365
|
+
return statement
|
366
|
+
end
|
367
|
+
|
368
|
+
def placeheld
|
369
|
+
(allfields.values+@tables.values+conditions+groupby+orderby).collect{|x| x.placeheld}.flatten
|
370
|
+
end
|
371
|
+
|
372
|
+
#This is useful for writing nested queries.
|
373
|
+
def to_sqlpart
|
374
|
+
"("+to_s+")"
|
375
|
+
end
|
376
|
+
protected
|
377
|
+
|
378
|
+
def tables_s
|
379
|
+
@tables.collect{|key,value| value==key ? key.to_sqlpart : "#{value.to_sqlpart} as #{key.to_sqlpart}"}.join(", ")
|
380
|
+
end
|
381
|
+
|
382
|
+
def conditions_s
|
383
|
+
@conditions==[] ? nil : @conditions.collect{|x| x.to_sqlpart}.join(" AND ")
|
384
|
+
end
|
385
|
+
|
386
|
+
def groupby_s
|
387
|
+
@groupby==[] ? nil : @groupby.collect{|x| x.to_sqlpart}.join(", ")
|
388
|
+
end
|
389
|
+
|
390
|
+
def orderby_s
|
391
|
+
@orderby==[] ? nil : @orderby.collect{|x| x.to_sqlpart}.join(", ")
|
392
|
+
end
|
393
|
+
|
394
|
+
def fields_s
|
395
|
+
allfields.collect{|key,value| value==key ? key.to_sqlpart : "#{value.to_sqlpart} as #{key.to_sqlpart}"}.join(", ")
|
396
|
+
end
|
397
|
+
|
398
|
+
class << self
|
399
|
+
#Use this to define additional array attributes in descendant classes
|
400
|
+
#with all of the necessary addition, initialization and merging (<<)
|
401
|
+
#semantics. You still need to appropriately define +allfields+ to make
|
402
|
+
#appropriate use of the lists you have added. <b> You can only call
|
403
|
+
#this method once, as it redefines methods each time it is called</b>
|
404
|
+
def newlists *lists
|
405
|
+
attr_accessor *lists
|
406
|
+
|
407
|
+
lines=lists.collect do |listname|
|
408
|
+
["@#{listname}=[]","@#{listname} += parts.#{listname}"]
|
409
|
+
end.transpose
|
410
|
+
class_eval <<-"end;"
|
411
|
+
def initialize *args
|
412
|
+
super
|
413
|
+
#{lines[0].join("\n")}
|
414
|
+
end
|
415
|
+
def << (parts)
|
416
|
+
super
|
417
|
+
#{lines[1].join("\n")}
|
418
|
+
self
|
419
|
+
end
|
420
|
+
end;
|
421
|
+
end
|
422
|
+
protected :newlists
|
423
|
+
end
|
424
|
+
|
425
|
+
end
|
426
|
+
|
427
|
+
#Creates a statement of the form
|
428
|
+
# CREATE [TEMPORARY] TABLE targettable SELECT ... FROM ... WHERE ...
|
429
|
+
class SelectCreate < Select
|
430
|
+
#The name of the table to create.
|
431
|
+
attr_accessor :targettable
|
432
|
+
|
433
|
+
#Indicates whether a temporary table (which automatically deletes
|
434
|
+
#itself when the connection to the database closes) should be created.
|
435
|
+
#This may not be supported by all databases.
|
436
|
+
attr_accessor :temporary
|
437
|
+
|
438
|
+
def initialize(targettable,temporary=false)
|
439
|
+
super()
|
440
|
+
@targettable=targettable
|
441
|
+
@temporary=temporary
|
442
|
+
end
|
443
|
+
|
444
|
+
def to_s
|
445
|
+
"CREATE #{ @temporary ? 'TEMPORARY':''} TABLE #{@targettable.to_sqlpart} #{super}"
|
446
|
+
end
|
447
|
+
end
|
448
|
+
|
449
|
+
#Creates a statement of the form
|
450
|
+
# INSERT INTO targettable (fields) SELECT values FROM ... WHERE ...
|
451
|
+
#(The generated sql statement may contain aliases in the SELECT clause. This should be harmless.)
|
452
|
+
class SelectInsert < Select
|
453
|
+
#The name of the table to insert into.
|
454
|
+
attr_accessor :targettable
|
455
|
+
|
456
|
+
def initialize(targettable)
|
457
|
+
super()
|
458
|
+
@targettable=targettable
|
459
|
+
end
|
460
|
+
|
461
|
+
def to_s
|
462
|
+
"INSERT INTO #{@targettable.to_sqlpart} #{names_s} #{super}"
|
463
|
+
end
|
464
|
+
|
465
|
+
protected
|
466
|
+
|
467
|
+
def names_s
|
468
|
+
intermediate=allfields.keys.collect do |x|
|
469
|
+
if x.is_a? SQL_Field
|
470
|
+
x.field.to_sqlpart
|
471
|
+
else
|
472
|
+
x.to_sqlpart
|
473
|
+
end
|
474
|
+
end
|
475
|
+
"("+intermediate.join(", ")+")"
|
476
|
+
end
|
477
|
+
end
|
478
|
+
|
479
|
+
#Creates a statement of the form
|
480
|
+
# UPDATE tables SET field=value, field2=value2 WHERE ...
|
481
|
+
#This descends from SelectStatement. Even though the strings are very
|
482
|
+
#different, they have lots of other stuff in common.
|
483
|
+
class Update < Select
|
484
|
+
|
485
|
+
def to_s
|
486
|
+
statement="UPDATE #{tables_s} SET #{updatepart_s}"
|
487
|
+
v=conditions_s; statement << " WHERE "<< v if v
|
488
|
+
statement
|
489
|
+
end
|
490
|
+
|
491
|
+
def placeheld
|
492
|
+
(allfields.values+conditions).collect{|x| x.placeheld}.flatten
|
493
|
+
end
|
494
|
+
|
495
|
+
protected
|
496
|
+
|
497
|
+
def updatepart_s
|
498
|
+
allfields.collect{|key,value| key.to_sqlpart+"="+value.to_sqlpart}.join(", ")
|
499
|
+
end
|
500
|
+
end
|
501
|
+
|
502
|
+
#Insert values directly into a table. This class does not support the
|
503
|
+
#use of SelectParts, rather it behaves like a hash of column names (as
|
504
|
+
#symbols) to values. You can get the "slice" functionality by merging
|
505
|
+
#hashes into the statement using <tt>Hash.merge</tt>.
|
506
|
+
class Insert < Hash
|
507
|
+
def initialize table
|
508
|
+
@targettable=table
|
509
|
+
end
|
510
|
+
#The name of the table to insert into.
|
511
|
+
attr_accessor :targettable
|
512
|
+
#Returns the SQL code to execute. May contain ?'s as placeholders for
|
513
|
+
#values.
|
514
|
+
def to_s
|
515
|
+
"INSERT INTO #{@targettable.to_sqlpart} ("+
|
516
|
+
keys.collect{|x| x.to_sqlpart}.join(", ")+")"+
|
517
|
+
"VALUES ("+values.collect{|x| x.to_sqlpart}.join(", ")+")"
|
518
|
+
end
|
519
|
+
#Returns the objects corresponding to ?'s in the SQL code, in the
|
520
|
+
#proper order. Insert into the database using the statement:
|
521
|
+
# i=Insert.new(:tablename)
|
522
|
+
# #add values
|
523
|
+
# dbh.do(i.to_s,*i.placeheld)
|
524
|
+
def placeheld
|
525
|
+
values.collect{|x| x.placeheld}.flatten
|
526
|
+
end
|
527
|
+
end
|
528
|
+
|
529
|
+
end
|
530
|
+
|
531
|
+
module SQLHelpers
|
532
|
+
|
533
|
+
#This helper method takes a string, and encapsulates it as a proper
|
534
|
+
#SQL expression, so that it won't get passed to the database as
|
535
|
+
#though it were a (potentially hostile) string value.
|
536
|
+
def string_func(str,placeheld=[])
|
537
|
+
SQLStatement::Function.new(str,placeheld)
|
538
|
+
end
|
539
|
+
end
|
data/lib/sqlstatement.rb
ADDED
metadata
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
rubygems_version: 0.9.0
|
3
|
+
specification_version: 1
|
4
|
+
name: SqlStatement
|
5
|
+
version: !ruby/object:Gem::Version
|
6
|
+
version: 1.0.0
|
7
|
+
date: 2006-10-09 00:00:00 -05:00
|
8
|
+
summary: A library for generating arbitrary SQL statements using convenient Ruby objects.
|
9
|
+
require_paths:
|
10
|
+
- lib
|
11
|
+
email: kbloom@gmail.com
|
12
|
+
homepage: http://www.rubyforge.org/sqlstatement/
|
13
|
+
rubyforge_project:
|
14
|
+
description:
|
15
|
+
autorequire: sqlstatement
|
16
|
+
default_executable:
|
17
|
+
bindir: bin
|
18
|
+
has_rdoc: true
|
19
|
+
required_ruby_version: !ruby/object:Gem::Version::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: "1.8"
|
24
|
+
version:
|
25
|
+
platform: ruby
|
26
|
+
signing_key:
|
27
|
+
cert_chain:
|
28
|
+
post_install_message:
|
29
|
+
authors:
|
30
|
+
- Ken Bloom
|
31
|
+
files:
|
32
|
+
- bin/example.rb
|
33
|
+
- lib/sql
|
34
|
+
- lib/sqlstatement.rb
|
35
|
+
- lib/sql/expression.rb
|
36
|
+
- lib/sql/bathon-sxp.rb
|
37
|
+
- lib/sql/statement.rb
|
38
|
+
test_files: []
|
39
|
+
|
40
|
+
rdoc_options: []
|
41
|
+
|
42
|
+
extra_rdoc_files: []
|
43
|
+
|
44
|
+
executables: []
|
45
|
+
|
46
|
+
extensions: []
|
47
|
+
|
48
|
+
requirements: []
|
49
|
+
|
50
|
+
dependencies:
|
51
|
+
- !ruby/object:Gem::Dependency
|
52
|
+
name: rubynode
|
53
|
+
version_requirement:
|
54
|
+
version_requirements: !ruby/object:Gem::Version::Requirement
|
55
|
+
requirements:
|
56
|
+
- - ">"
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: 0.0.0
|
59
|
+
version:
|