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 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: