SqlStatement 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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:
|