sql_munger 0.0.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,16 @@
1
+ # The list of files that should be ignored by Mr Bones.
2
+ # Lines that start with '#' are comments.
3
+ #
4
+ # A .gitignore file can be used instead by setting it as the ignore
5
+ # file in your Rakefile:
6
+ #
7
+ # PROJ.ignore_file = '.gitignore'
8
+ #
9
+ # For a project with a C extension, the following would be a good set of
10
+ # exclude patterns (uncomment them if you want to use them):
11
+ # *.[oa]
12
+ # *~
13
+ announcement.txt
14
+ coverage
15
+ doc
16
+ pkg
@@ -0,0 +1,3 @@
1
+ doc
2
+ pkg
3
+
@@ -0,0 +1,3 @@
1
+ == 0.0.6 / 29-May-2011
2
+
3
+ * First public release.
@@ -0,0 +1,57 @@
1
+ by John Anderson http://www.semiosix.com
2
+
3
+ == DESCRIPTION:
4
+
5
+ When you're good at SQL, sometimes you want to build SQL statement
6
+ without an ORM or a db api, or a connection to a database.
7
+ SqlMunger will simplify some of the fiddly parts of doing that.
8
+
9
+ SqlMunger will not give you a SQL equivalent algebra - Sequel and AREL do that very well.
10
+
11
+ See SqlIzer for an example.
12
+
13
+ == FEATURES/PROBLEMS:
14
+
15
+ SqlFieldSet lets you quote, escape, join and generate comparison operators
16
+ for a set of field names and a related set of values. See SqlMunger::FieldSet
17
+
18
+ SqlMunger::SqlParser knows how to parse a create table statement, giving a relatively
19
+ easy way to create migrations or other representations of a table definition.
20
+
21
+
22
+ == SYNOPSIS:
23
+
24
+ require 'sql_munger'
25
+
26
+ == REQUIREMENTS:
27
+
28
+ treetop
29
+
30
+ == INSTALL:
31
+
32
+ gem install sql_munger
33
+
34
+ == LICENSE:
35
+
36
+ (The MIT License)
37
+
38
+ Copyright (c) 2009,2011 John Anderson
39
+
40
+ Permission is hereby granted, free of charge, to any person obtaining
41
+ a copy of this software and associated documentation files (the
42
+ 'Software'), to deal in the Software without restriction, including
43
+ without limitation the rights to use, copy, modify, merge, publish,
44
+ distribute, sublicense, and/or sell copies of the Software, and to
45
+ permit persons to whom the Software is furnished to do so, subject to
46
+ the following conditions:
47
+
48
+ The above copyright notice and this permission notice shall be
49
+ included in all copies or substantial portions of the Software.
50
+
51
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
52
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
53
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
54
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
55
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
56
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
57
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,27 @@
1
+ begin
2
+ require 'bones'
3
+ rescue LoadError
4
+ abort '### Please install the "bones" gem ###'
5
+ end
6
+
7
+ task :default => 'test:run'
8
+ task 'gem:release' => 'test:run'
9
+
10
+ ensure_in_path 'lib'
11
+ require 'sql_munger'
12
+
13
+ Bones {
14
+ name 'sql_munger'
15
+ authors 'John Anderson'
16
+ email 'john@semiosix.com'
17
+ url 'http://www.semiosix.com'
18
+
19
+ version SqlMunger::VERSION
20
+ description "SQL manipulation without a db connection"
21
+
22
+ rdoc.include %w{README.txt ^lib/sql_munger/.*\.rb$ examples/sql_izer.rb History.txt}
23
+
24
+ gem.need_tar false
25
+
26
+ depend_on 'treetop', '~>1.4.8'
27
+ }
@@ -0,0 +1,92 @@
1
+ require 'sql_munger/generation'
2
+ require 'mysql'
3
+
4
+ =begin rdoc
5
+ Example for how to use SqlMunger
6
+
7
+ :include:sql_izer.rb
8
+ =end
9
+
10
+ class SqlIzer
11
+
12
+ # If the SQL you want to generate uses something
13
+ # other than " for identifiers, and ' for string values
14
+ # you need to define quoting.
15
+ #
16
+ # Also, escaping of special characters inside value strings is quite
17
+ # unpleasant, so for now we depend on external libraries. Feel
18
+ # free to implement this and send a patch.
19
+ #
20
+ # Set this as the default quoter. Note that default_quoter wants an instance, not a class.
21
+ SqlMunger::FieldSet.default_quoter = Class.new do
22
+ include SqlMunger::ValueQuoter
23
+ include SqlMunger::IdentifierQuoter
24
+ def escape( st )
25
+ Mysql.escape_string( st )
26
+ end
27
+ end.new
28
+
29
+ # the name of the table. Note that the namespace is optional
30
+ def table
31
+ @table ||= SqlMunger::TableName.new( 'namespace.things' )
32
+ end
33
+
34
+ # this is the set of fields used in the where
35
+ # clause for update statements
36
+ def unique_key_fields
37
+ @unique_key_fields ||= SqlMunger::FieldSet.new( %w{name} )
38
+ end
39
+
40
+ def field_names
41
+ values.keys
42
+ end
43
+
44
+ # sample values for the example
45
+ def values
46
+ @values ||= {
47
+ 'name' => 'Mr Mark',
48
+ 'address' => "'The Pub', Dublin",
49
+ 'phone' => '(31)415328',
50
+ 'flavour' => 'bitter',
51
+ 'colour' => 'dark',
52
+ 'spin' => 'widdershins',
53
+ 'manyness' => 16.5
54
+ }
55
+ end
56
+
57
+ def field_set
58
+ @field_set ||= SqlMunger::FieldSet.new( field_names )
59
+ end
60
+
61
+ # generate an insert statement
62
+ def insert_sql
63
+ <<-EOF
64
+ insert into #{table.quote}
65
+ ( #{field_set.list} )
66
+ values
67
+ (
68
+ \t#{field_set.quoted_values( values ).join(",\n\t")}
69
+ );
70
+ EOF
71
+ end
72
+
73
+ def update_sql
74
+ # We want to update the values, but not the field
75
+ # used in the where clause
76
+ fields = field_set - unique_key_fields
77
+
78
+ <<-EOF
79
+ update #{table.quote}
80
+ set
81
+ #{fields.update( values ) }
82
+ where
83
+ #{unique_key_fields.comparison( values )}
84
+ ;
85
+ EOF
86
+ end
87
+
88
+ end
89
+
90
+ sqlizer = SqlIzer.new
91
+ puts sqlizer.insert_sql
92
+ puts sqlizer.update_sql
@@ -0,0 +1,49 @@
1
+
2
+ module SqlMunger
3
+
4
+ # :stopdoc:
5
+ VERSION = '0.0.7'
6
+ LIBPATH = ::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR
7
+ PATH = ::File.dirname(LIBPATH) + ::File::SEPARATOR
8
+ # :startdoc:
9
+
10
+ # Returns the version string for the library.
11
+ #
12
+ def self.version
13
+ VERSION
14
+ end
15
+
16
+ # Returns the library path for the module. If any arguments are given,
17
+ # they will be joined to the end of the libray path using
18
+ # <tt>File.join</tt>.
19
+ #
20
+ def self.libpath( *args )
21
+ args.empty? ? LIBPATH : ::File.join(LIBPATH, args.flatten)
22
+ end
23
+
24
+ # Returns the lpath for the module. If any arguments are given,
25
+ # they will be joined to the end of the path using
26
+ # <tt>File.join</tt>.
27
+ #
28
+ def self.path( *args )
29
+ args.empty? ? PATH : ::File.join(PATH, args.flatten)
30
+ end
31
+
32
+ # Utility method used to require all files ending in .rb that lie in the
33
+ # directory below this file that has the same name as the filename passed
34
+ # in. Optionally, a specific _directory_ name can be passed in such that
35
+ # the _filename_ does not have to be equivalent to the directory.
36
+ #
37
+ def self.require_all_libs_relative_to( fname, dir = nil )
38
+ dir ||= ::File.basename(fname, '.*')
39
+ search_me = ::File.expand_path(
40
+ ::File.join(::File.dirname(fname), dir, '**', '*.rb'))
41
+
42
+ Dir.glob(search_me).sort.each {|rb| require rb}
43
+ end
44
+
45
+ end # module SqlMunger
46
+
47
+ SqlMunger.require_all_libs_relative_to SqlMunger.libpath( 'sql_munger' )
48
+
49
+ # EOF
@@ -0,0 +1,355 @@
1
+ require 'sql_munger/quoter.rb'
2
+
3
+ module SqlMunger
4
+
5
+ =begin rdoc
6
+ This is a class encapsulating a set of fields
7
+ and providing various methods for producing SQL strings
8
+ from them.
9
+
10
+ You can also call operators on fieldset (+ - | & ), or
11
+ with an array as the right-hand parameter.
12
+
13
+ Fields can be either plain strings, or objects responding
14
+ to name, sql_type, null, default. If they're plain strings, they're
15
+ converted to FieldSet::Field objects, which have a name attribute.
16
+ Mixing different classes in the same FieldSet won't work.
17
+
18
+ =end
19
+
20
+ class FieldSet
21
+ include Quoter
22
+
23
+ class Field
24
+ def initialize( name )
25
+ @name = name.to_sym
26
+ end
27
+ attr_accessor :name
28
+
29
+ def ==( other )
30
+ name == other.name
31
+ end
32
+ end
33
+
34
+ # collection_or_enum is a collection of field names.
35
+ #
36
+ # If block is supplied, collection_or_enum is a collection
37
+ # of objects that when map( &block ) is applied returns
38
+ # a collection of field names.
39
+ #
40
+ # options is intended to easily pass a :qualifier and a :quoter.
41
+ def initialize( collection_or_enum, options = {}, &block )
42
+ @fields =
43
+ if collection_or_enum.all?{|x| x.is_a?( String ) || x.is_a?( Symbol ) }
44
+ collection_or_enum.to_a.map{|x| Field.new(x.to_s)}
45
+ else
46
+ first = collection_or_enum.first
47
+ first.name rescue raise( "Can't use default method :name on #{first}" )
48
+ collection_or_enum.to_a.dup
49
+ end
50
+
51
+ @field_name_block = block || lambda {|x| x.name}
52
+
53
+ # set options
54
+ options.each do |key,value|
55
+ send( "#{key}=", value )
56
+ end
57
+ end
58
+
59
+ # raise an exception with msg appended if any element of ary
60
+ # is not a String. Otherwise return true
61
+ def self.check_strings( ary, msg = nil )
62
+ ary.each do |elt|
63
+ unless elt.is_a?( String ) || elt.is_a?( Symbol )
64
+ raise [ "#{elt.inspect} is neither String nor Symbol", msg ].compact.join(' ')
65
+ end
66
+ end
67
+ true
68
+ end
69
+
70
+ # Create from either a Array of field names (NOTE *not* field objects)
71
+ # or from another FieldSet
72
+ def self.[]( fieldset_or_array )
73
+ case fieldset_or_array
74
+ when Array
75
+ check_strings( fieldset_or_array, "Use #{self.class.name}.new" )
76
+ FieldSet.new( fieldset_or_array )
77
+
78
+ when FieldSet
79
+ field_set = fieldset_or_array
80
+ field_set.dup
81
+
82
+ when NilClass
83
+ nil
84
+
85
+ else
86
+ raise "dunno what to do with #{fieldset_or_array.inspect}"
87
+ end
88
+ end
89
+
90
+ # the field objects, which may be objects or Strings. See field_names
91
+ # if you definitely always want either Strings or Symbols
92
+ attr_reader :fields
93
+
94
+ # Set the fields. Assume that they're the same kind of
95
+ # fields as the previous set of fields
96
+ def fields=( other_fields )
97
+ @field_names = nil
98
+ @fields = other_fields
99
+ end
100
+
101
+ # Return a new instance of FieldSet. The result
102
+ # of fields is passed to block, and the result of that
103
+ # is passed to FieldSet.new. Useful for doing set operations
104
+ # that aren't covered by + - & |
105
+ def reconstruct( &block )
106
+ FieldSet.new( block.call( fields ) )
107
+ end
108
+
109
+ # the names of the fields
110
+ def field_names
111
+ @field_names ||= sanity_check_fields
112
+ end
113
+
114
+ # enumerate through field_names
115
+ def each( &block )
116
+ field_names.each( &block )
117
+ end
118
+ include Enumerable
119
+
120
+ # return the field object for name, or nil if it doesn't exist
121
+ def find_field( name )
122
+ fields.find{|x| name == @field_name_block.call(x) }
123
+ end
124
+
125
+ # + - & | (append, difference, intersection union) same as array.
126
+ # other can be another FieldSet, or an Array
127
+ # Called from method_missing
128
+ def operate( operation, other )
129
+ case other
130
+ when FieldSet
131
+ # fetch new fields using the operated set of field_names
132
+ new_field_objects = field_names.send( operation, other.field_names ).map do |name|
133
+ # first try self's fields, then try other's fields
134
+ find_field( name ) || other.find_field( name )
135
+ end
136
+
137
+ # create the new FieldSet
138
+ FieldSet.new( new_field_objects, :quoter => quoter, :qualifier => qualifier )
139
+
140
+ when Array
141
+ case other.first
142
+ when String
143
+ operate( operation, FieldSet.new( other ) )
144
+
145
+ when Symbol
146
+ operate( operation, FieldSet.new( other ) )
147
+
148
+ else
149
+ # assume it's a field object
150
+ FieldSet.new( fields.send( operation, other ), :quoter => quoter, :qualifier => qualifier )
151
+ end
152
+
153
+ else
154
+ raise "don't know what to do with #{other.inspect}"
155
+ end
156
+ end
157
+
158
+ # passes - + & | to operate, otherwise calls method_missing
159
+ def method_missing( meth, *args, &block )
160
+ @operations ||= %w{ - + & | }.map{|x| x.to_sym}
161
+ if @operations.include?( meth )
162
+ operate( meth, args.first )
163
+ else
164
+ super
165
+ end
166
+ end
167
+
168
+ # whatever is in front of the fields, usually a table name
169
+ # It's stored as an array of parts, to be quoted and joined with
170
+ # '.' when generating SQL
171
+ attr_reader :qualifier
172
+
173
+ # can take a dotted string, an Array of strings, a TableName instance or nil
174
+ def qualifier=( other )
175
+ case other
176
+ when String
177
+ @qualifier = other.split( '.' )
178
+
179
+ when Array
180
+ @qualifier = other
181
+
182
+ when TableName
183
+ @qualifier = other.parts
184
+
185
+ when NilClass
186
+ @qualifier = nil
187
+
188
+ else
189
+ raise "Don't understand #{other.inspect}"
190
+ end
191
+ end
192
+
193
+ # return the set of field names, separated by ', '
194
+ def list( qualifier = nil )
195
+ field_names.map do |field|
196
+ qualify( qualifier, field )
197
+ end.join(', ')
198
+ end
199
+
200
+ # prepend the qualifier to each field name
201
+ def qualified_list
202
+ list( qualifier )
203
+ end
204
+
205
+ alias_method :qlist, :qualified_list
206
+
207
+ # one arg is a field, qualified by qualifier
208
+ # many args is qualifier[s], field
209
+ def qualify( *args )
210
+ case args.flatten.size
211
+ when 1
212
+ [ qualifier, args.first ]
213
+
214
+ else
215
+ args
216
+
217
+ end.flatten.compact.map{|x| identifier_quoter.quote_ident(x)}.join('.')
218
+ end
219
+
220
+ # return a hash of field names to their corresponding transformed values
221
+ # from hash_values. block will be used to transform each value.
222
+ def hash_values( hash_values, &block )
223
+ inject({}) do |hash,field|
224
+ hash[field] =
225
+ if block.nil?
226
+ hash_values[field]
227
+ else
228
+ block.call( hash_values[field] )
229
+ end
230
+ hash
231
+ end
232
+ end
233
+
234
+ # return a comma-separated list of the quoted values for this
235
+ # fieldset
236
+ def quoted_values( hash_values )
237
+ map do |field|
238
+ value_quoter.quote( hash_values[field] )
239
+ end
240
+ end
241
+
242
+ # extract the values for these fields from hash_values
243
+ # block will be applied to each value
244
+ def values( hash_values, &block )
245
+ map do |field|
246
+ if block.nil?
247
+ hash_values[field]
248
+ else
249
+ block.call( hash_values[field] )
250
+ end
251
+ end
252
+ end
253
+
254
+ # return a string of the fields with some operator and value
255
+ # can be used for where clauses and update statements.
256
+ # arg is either a String, which is treated passed to TableName.new
257
+ # or a TableName, which qualifies the rhs
258
+ # or an Array (actually something with a zip method) which is used as a set of values
259
+ # or a Hash (or something with to_hash) from which the values corresponding
260
+ # to the fields in this object are extracted, and used to compare with
261
+ def express( arg, level = 0, options = { :operator => '=', :joiner => ',' } )
262
+ expressions =
263
+ case
264
+ when arg.is_a?( String )
265
+ express( TableName.new( arg ), level+1, options )
266
+
267
+ when arg.is_a?( TableName )
268
+ other_table = arg
269
+ field_names.map do |field|
270
+ "#{qualify( field )} #{options[:operator]} #{qualify( other_table, field )}"
271
+ end
272
+
273
+ when arg.is_a?( Hash )
274
+ field_names.map do |field|
275
+ "#{qualify( field )} #{options[:operator]} #{value_quoter.quote( arg[field] )}"
276
+ end
277
+
278
+ when arg.respond_to?( :to_hash )
279
+ express( arg.to_hash, level+1, options )
280
+
281
+ when arg.respond_to?( :zip )
282
+ raise "#{arg.inspect} doesn't match number of fields in #{field_names}" unless size == arg.size
283
+ field_names.zip( [ *arg ] ).map do |field,value|
284
+ "#{qualify( field )} #{options[:operator]} #{value_quoter.quote( value )}"
285
+ end
286
+
287
+ else
288
+ raise "Don't know what to do with #{arg.inspect}"
289
+ end
290
+
291
+ if level == 0
292
+ if options[:joiner].nil?
293
+ expressions
294
+ else
295
+ expressions.join( options[:joiner] )
296
+ end
297
+ else
298
+ expressions
299
+ end
300
+ end
301
+
302
+ # return an SQL set of comparison expressions, joined with 'and' suitable for
303
+ # a where clause
304
+ def comparison( arg, operator = '=' )
305
+ if arg.is_a?( String ) && qualifier.nil?
306
+ raise "no local qualifier"
307
+ end
308
+
309
+ st = express( arg, 0, :operator => operator, :joiner => ' and ' )
310
+ "( #{st} )"
311
+ end
312
+
313
+ # return something suitable for an update statement
314
+ def update( arg )
315
+ save_qualifier = qualifier
316
+ begin
317
+ self.qualifier = nil
318
+ express( arg, 0, :operator => '=', :joiner => ', ' )
319
+ ensure
320
+ self.qualifier = save_qualifier
321
+ end
322
+ end
323
+
324
+ # probably only works with column definitions from ActiveRecord
325
+ def definitions( options = {:joiner => ', '} )
326
+ fields.map do |field|
327
+ raise "sql_type has no value" if field.sql_type.nil? || field.sql_type == ''
328
+ "#{field.name} #{field.sql_type}"
329
+ end.join( options[:joiner] )
330
+ end
331
+
332
+ def size
333
+ fields.size
334
+ end
335
+
336
+ def empty?
337
+ fields.empty?
338
+ end
339
+
340
+ def ==( other )
341
+ return false unless other.is_a?( self.class )
342
+ qualifier == other.qualifier and
343
+ fields == other.fields and
344
+ @field_name_block == other.instance_variable_get( '@field_name_block' )
345
+ end
346
+
347
+ protected
348
+
349
+ # generate field names from field objects
350
+ def sanity_check_fields
351
+ fields.map( &@field_name_block )
352
+ end
353
+ end
354
+
355
+ end # module SqlMunger