sql_munger 0.0.7

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.
@@ -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