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 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
@@ -0,0 +1,2 @@
1
+ require 'sql/statement'
2
+ require 'sql/expression'
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: