amalgalite 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. data/HISTORY +4 -0
  2. data/LICENSE +31 -0
  3. data/README +28 -0
  4. data/ext/amalgalite3.c +191 -0
  5. data/ext/amalgalite3.h +97 -0
  6. data/ext/amalgalite3_constants.c +179 -0
  7. data/ext/amalgalite3_database.c +458 -0
  8. data/ext/amalgalite3_statement.c +546 -0
  9. data/ext/gen_constants.rb +114 -0
  10. data/ext/mkrf_conf.rb +6 -0
  11. data/ext/sqlite3.c +87003 -0
  12. data/ext/sqlite3.h +5638 -0
  13. data/ext/sqlite3_options.h +4 -0
  14. data/ext/sqlite3ext.h +362 -0
  15. data/gemspec.rb +50 -0
  16. data/lib/amalgalite.rb +28 -0
  17. data/lib/amalgalite/blob.rb +14 -0
  18. data/lib/amalgalite/boolean.rb +42 -0
  19. data/lib/amalgalite/column.rb +83 -0
  20. data/lib/amalgalite/database.rb +505 -0
  21. data/lib/amalgalite/index.rb +27 -0
  22. data/lib/amalgalite/paths.rb +70 -0
  23. data/lib/amalgalite/profile_tap.rb +130 -0
  24. data/lib/amalgalite/schema.rb +90 -0
  25. data/lib/amalgalite/sqlite3.rb +4 -0
  26. data/lib/amalgalite/sqlite3/constants.rb +48 -0
  27. data/lib/amalgalite/sqlite3/version.rb +38 -0
  28. data/lib/amalgalite/statement.rb +307 -0
  29. data/lib/amalgalite/table.rb +34 -0
  30. data/lib/amalgalite/taps/console.rb +27 -0
  31. data/lib/amalgalite/taps/io.rb +71 -0
  32. data/lib/amalgalite/trace_tap.rb +35 -0
  33. data/lib/amalgalite/type_map.rb +60 -0
  34. data/lib/amalgalite/type_maps/default_map.rb +153 -0
  35. data/lib/amalgalite/type_maps/storage_map.rb +41 -0
  36. data/lib/amalgalite/type_maps/text_map.rb +23 -0
  37. data/lib/amalgalite/version.rb +32 -0
  38. data/lib/amalgalite/view.rb +24 -0
  39. data/spec/amalgalite_spec.rb +4 -0
  40. data/spec/boolean_spec.rb +26 -0
  41. data/spec/database_spec.rb +222 -0
  42. data/spec/default_map_spec.rb +85 -0
  43. data/spec/integeration_spec.rb +111 -0
  44. data/spec/paths_spec.rb +28 -0
  45. data/spec/schema_spec.rb +46 -0
  46. data/spec/spec_helper.rb +25 -0
  47. data/spec/sqlite3/constants_spec.rb +25 -0
  48. data/spec/sqlite3/version_spec.rb +14 -0
  49. data/spec/sqlite3_spec.rb +34 -0
  50. data/spec/statement_spec.rb +116 -0
  51. data/spec/storage_map_spec.rb +41 -0
  52. data/spec/tap_spec.rb +59 -0
  53. data/spec/text_map_spec.rb +23 -0
  54. data/spec/type_map_spec.rb +17 -0
  55. data/spec/version_spec.rb +9 -0
  56. data/tasks/announce.rake +38 -0
  57. data/tasks/config.rb +108 -0
  58. data/tasks/distribution.rake +38 -0
  59. data/tasks/documentation.rake +31 -0
  60. data/tasks/extension.rake +45 -0
  61. data/tasks/rspec.rake +32 -0
  62. data/tasks/rubyforge.rake +48 -0
  63. data/tasks/utils.rb +80 -0
  64. metadata +165 -0
@@ -0,0 +1,42 @@
1
+ #--
2
+ # Copyright (c) 2008 Jeremy Hinegardner
3
+ # All rights reserved. See LICENSE and/or COPYING for details.
4
+ #++
5
+ module Amalgalite
6
+ ##
7
+ # Do type conversion on values that could be boolen values into
8
+ # real 'true' or 'false'
9
+ #
10
+ # This is pulled from the possible boolean values from PostgreSQL
11
+ #
12
+ class Boolean
13
+ class << self
14
+ #
15
+ # list of downcased strings are potential true values
16
+ #
17
+ def true_values
18
+ @true_values ||= %w[ true t yes y 1 ]
19
+ end
20
+
21
+ #
22
+ # list of downcased strings are potential false values
23
+ #
24
+ def false_values
25
+ @false_values ||= %w[ false f no n 0 ]
26
+ end
27
+
28
+ #
29
+ # Convert +val+ to a string and attempt to convert it to +true+ or +false+
30
+ #
31
+ def to_bool( val )
32
+ return false if val.nil?
33
+ unless @to_bool
34
+ @to_bool = {}
35
+ true_values.each { |t| @to_bool[t] = true }
36
+ false_values.each { |f| @to_bool[f] = false }
37
+ end
38
+ return @to_bool[val.to_s.downcase]
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,83 @@
1
+ #--
2
+ # Copyright (c) 2008 Jeremy Hinegardner
3
+ # All rights reserved. See LICENSE and/or COPYING for details.
4
+ #++
5
+
6
+ require 'amalgalite/boolean'
7
+ require 'amalgalite/blob'
8
+
9
+ module Amalgalite
10
+ ##
11
+ # a class representing the meta information about an SQLite column, this class
12
+ # serves both for general Schema level information, and for result set
13
+ # information from a SELECT query.
14
+ #
15
+ class Column
16
+ # the database this column belongs to
17
+ attr_accessor :db
18
+
19
+ # the column name
20
+ attr_accessor :name
21
+
22
+ # the table to which this column belongs
23
+ attr_accessor :table
24
+
25
+ # the default value of the column. This may not have a value and that
26
+ # either means that there is no default value, or one could not be
27
+ # determined.
28
+ #
29
+ attr_accessor :default_value
30
+
31
+ # the declared data type of the column in the original sql that created the
32
+ # column
33
+ attr_accessor :declared_data_type
34
+
35
+ # the collation sequence name of the column
36
+ attr_accessor :collation_sequence_name
37
+
38
+ # true if the column has a NOT NULL constraint, false otherwise
39
+ attr_accessor :not_null_constraint
40
+
41
+ # true if the column is part of a primary key, false otherwise
42
+ attr_accessor :primary_key
43
+
44
+ # true if the column is AUTO INCREMENT, false otherwise
45
+ attr_accessor :auto_increment
46
+
47
+ ##
48
+ # Create a column with its name and associated table
49
+ #
50
+ def initialize( db, name, table )
51
+ @db = db
52
+ @name = name
53
+ @table = table
54
+ @declared_data_type = nil
55
+ @default_value = nil
56
+ end
57
+
58
+ # true if the column has a default value
59
+ def has_default_value?
60
+ not default_value.nil?
61
+ end
62
+
63
+ # true if the column may have a NULL value
64
+ def nullable?
65
+ not_null_constraint == false
66
+ end
67
+
68
+ # true if the column as a NOT NULL constraint
69
+ def not_null_constraint?
70
+ not_null_constraint
71
+ end
72
+
73
+ # true if the column is a primary key column
74
+ def primary_key?
75
+ primary_key
76
+ end
77
+
78
+ # true if the column is auto increment
79
+ def auto_increment?
80
+ auto_increment
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,505 @@
1
+ #--
2
+ # Copyright (c) 2008 Jeremy Hinegardner
3
+ # All rights reserved. See LICENSE and/or COPYING for details.
4
+ #++
5
+ require 'amalgalite3'
6
+ require 'amalgalite/statement'
7
+ require 'amalgalite/trace_tap'
8
+ require 'amalgalite/profile_tap'
9
+ require 'amalgalite/type_maps/default_map'
10
+
11
+ module Amalgalite
12
+ #
13
+ # The encapsulation of a connection to an SQLite3 database.
14
+ #
15
+ # Example opening and possibly creating a new daabase
16
+ #
17
+ # db = Amalgalite::Database.new( "mydb.db" )
18
+ # db.execute( "SELECT * FROM table" ) do |row|
19
+ # puts row
20
+ # end
21
+ #
22
+ # db.close
23
+ #
24
+ # Open a database read only:
25
+ #
26
+ # db = Amalgalite::Database.new( "mydb.db", "r" )
27
+ #
28
+ #
29
+ class Database
30
+
31
+ # Error thrown if a database is opened with an invalid mode
32
+ class InvalidModeError < ::Amalgalite::Error; end
33
+
34
+ ##
35
+ # container class for holding transaction behavior constants. These are the
36
+ # SQLite values passed to a START TRANSACTION SQL statement.
37
+ #
38
+ class TransactionBehavior
39
+ # no read or write locks are created until the first statement is executed
40
+ # that requries a read or a write
41
+ DEFERRED = "DEFERRED"
42
+
43
+ # a readlock is obtained immediately so that no other process can write to
44
+ # the database
45
+ IMMEDIATE = "IMMEDIATE"
46
+
47
+ # a read+write lock is obtained, no other proces can read or write to the
48
+ # database
49
+ EXCLUSIVE = "EXCLUSIVE"
50
+
51
+ # list of valid transaction behavior constants
52
+ VALID = [ DEFERRED, IMMEDIATE, EXCLUSIVE ]
53
+
54
+ #
55
+ # is the given mode a valid transaction mode
56
+ #
57
+ def self.valid?( mode )
58
+ VALID.include? mode
59
+ end
60
+ end
61
+
62
+ include Amalgalite::SQLite3::Constants
63
+
64
+ # list of valid modes for opening an Amalgalite::Database
65
+ VALID_MODES = {
66
+ "r" => Open::READONLY,
67
+ "r+" => Open::READWRITE,
68
+ "w+" => Open::READWRITE | Open::CREATE,
69
+ }
70
+
71
+ # the low level Amalgalite::SQLite3::Database
72
+ attr_reader :api
73
+
74
+ # An object that follows the TraceTap protocol, or nil. By default this is nil
75
+ attr_reader :trace_tap
76
+
77
+ # An object that follows the ProfileTap protocol, or nil. By default this is nil
78
+ attr_reader :profile_tap
79
+
80
+ # An object that follows the TypeMap protocol, or nil.
81
+ # By default this is an instances of TypeMaps::DefaultMap
82
+ attr_reader :type_map
83
+
84
+ ##
85
+ # Create a new Amalgalite database
86
+ #
87
+ # :call-seq:
88
+ # Amalgalite::Database.new( filename, "w+", opts = {}) -> Database
89
+ #
90
+ # The first parameter is the filename of the sqlite database.
91
+ # The second parameter is the standard file modes of how to open a file.
92
+ #
93
+ # The modes are:
94
+ #
95
+ # * r - Read-only
96
+ # * r+ - Read/write, an error is thrown if the database does not already exist
97
+ # * w+ - Read/write, create a new database if it doesn't exist
98
+ #
99
+ # <tt>w+</tt> is the default as this is how most databases will want to be utilized.
100
+ #
101
+ # opts is a hash of available options for the database:
102
+ #
103
+ # * :utf16 option to set the database to a utf16 encoding if creating a database.
104
+ #
105
+ # By default, databases are created with an encoding of utf8. Setting this to
106
+ # true and opening an already existing database has no effect.
107
+ #
108
+ # *NOTE* Currently :utf16 is not supported by Amalgalite, it is planned
109
+ # for a later release
110
+ #
111
+ #
112
+ def initialize( filename, mode = "w+", opts = {})
113
+ @open = false
114
+ @profile_tap = nil
115
+ @trace_tap = nil
116
+ @type_map = ::Amalgalite::TypeMaps::DefaultMap.new
117
+
118
+ unless VALID_MODES.keys.include?( mode )
119
+ raise InvalidModeError, "#{mode} is invalid, must be one of #{VALID_MODES.keys.join(', ')}"
120
+ end
121
+
122
+ if not File.exist?( filename ) and opts[:utf16] then
123
+ raise NotImplementedError, "Currently Amalgalite has not implemented utf16 support"
124
+ else
125
+ @api = Amalgalite::SQLite3::Database.open( filename, VALID_MODES[mode] )
126
+ end
127
+ @open = true
128
+ end
129
+
130
+ ##
131
+ # Is the database open or not
132
+ #
133
+ def open?
134
+ @open
135
+ end
136
+
137
+ ##
138
+ # Close the database
139
+ #
140
+ def close
141
+ if open? then
142
+ @api.close
143
+ end
144
+ end
145
+
146
+ ##
147
+ # Is the database in autocommit mode or not
148
+ #
149
+ def autocommit?
150
+ @api.autocommit?
151
+ end
152
+
153
+ ##
154
+ # Return the rowid of the last inserted row
155
+ #
156
+ def last_insert_rowid
157
+ @api.last_insert_rowid
158
+ end
159
+
160
+ ##
161
+ # Is the database utf16 or not? A database is utf16 if the encoding is not
162
+ # UTF-8. Database can only be UTF-8 or UTF-16, and the default is UTF-8
163
+ #
164
+ def utf16?
165
+ unless @utf16.nil?
166
+ @utf16 = (encoding != "UTF-8")
167
+ end
168
+ return @utf16
169
+ end
170
+
171
+ ##
172
+ # return the encoding of the database
173
+ #
174
+ def encoding
175
+ unless @encoding
176
+ @encoding = pragma( "encoding" ).first['encoding']
177
+ end
178
+ return @encoding
179
+ end
180
+
181
+ ##
182
+ # return whether or not the database is currently in a transaction or not
183
+ #
184
+ def in_transaction?
185
+ not @api.autocommit?
186
+ end
187
+
188
+ ##
189
+ # return how many rows changed in the last insert, update or delete statement.
190
+ #
191
+ def row_changes
192
+ @api.row_changes
193
+ end
194
+
195
+ ##
196
+ # return how many rows have changed since this connection to the database was
197
+ # opened.
198
+ #
199
+ def total_changes
200
+ @api.total_changes
201
+ end
202
+
203
+ ##
204
+ # Prepare a statement for execution
205
+ #
206
+ # If called with a block, the statement is yielded to the block and the
207
+ # statement is closed when the block is done.
208
+ #
209
+ # db.prepare( "SELECT * FROM table WHERE c = ?" ) do |stmt|
210
+ # list_of_c_values.each do |c|
211
+ # stmt.execute( c ) do |row|
212
+ # puts "when c = #{c} : #{row.inspect}"
213
+ # end
214
+ # end
215
+ # end
216
+ #
217
+ # Or without a block:
218
+ #
219
+ # stmt = db.prepare( "INSERT INTO t1(x, y, z) VALUES ( :
220
+ #
221
+ def prepare( sql )
222
+ stmt = Amalgalite::Statement.new( self, sql )
223
+ if block_given? then
224
+ begin
225
+ yield stmt
226
+ ensure
227
+ stmt.close
228
+ stmt = nil
229
+ end
230
+ end
231
+ return stmt
232
+ end
233
+
234
+ ##
235
+ # Execute a single SQL statement.
236
+ #
237
+ # If called with a block and there are result rows, then they are iteratively
238
+ # yielded to the block.
239
+ #
240
+ # If no block passed and there are results, then a ResultSet is returned.
241
+ # Otherwise nil is returned. On an error an exception is thrown.
242
+ #
243
+ # This is just a wrapper around the preparation of an Amalgalite Statement and
244
+ # iterating over the results.
245
+ #
246
+ def execute( sql, *bind_params )
247
+ stmt = prepare( sql )
248
+ stmt.bind( *bind_params )
249
+ if block_given? then
250
+ stmt.each { |row| yield row }
251
+ else
252
+ return stmt.all_rows
253
+ end
254
+ ensure
255
+ stmt.close if stmt
256
+ end
257
+
258
+ ##
259
+ # Execute a batch of statements, this will execute all the sql in the given
260
+ # string until no more sql can be found in the string. It will bind the
261
+ # same parameters to each statement. All data that would be returned from
262
+ # all of the statements is thrown away.
263
+ #
264
+ # All statements to be executed in the batch must be terminated with a ';'
265
+ # Returns the number of statements executed
266
+ #
267
+ #
268
+ def execute_batch( sql, *bind_params)
269
+ count = 0
270
+ while sql
271
+ prepare( sql ) do |stmt|
272
+ stmt.execute( *bind_params )
273
+ sql = stmt.remaining_sql
274
+ sql = nil unless (sql.index(";") and Amalgalite::SQLite3.complete?( sql ))
275
+ end
276
+ count += 1
277
+ end
278
+ return count
279
+ end
280
+
281
+ ##
282
+ # clear all the current taps
283
+ #
284
+ def clear_taps!
285
+ self.trace_tap = nil
286
+ self.profile_tap = nil
287
+ end
288
+
289
+ ##
290
+ # call-seq:
291
+ # db.trace_tap = obj
292
+ #
293
+ # Register a trace tap.
294
+ #
295
+ # Registering a trace tap measn that the +obj+ registered will have its
296
+ # +trace+ method called with a string parameter at various times.
297
+ # If the object doesn't respond to the +trace+ method then +write+
298
+ # will be called.
299
+ #
300
+ # For instance:
301
+ #
302
+ # db.trace_tap = Amalgalite::TraceTap.new( logger, 'debug' )
303
+ #
304
+ # This will register an instance of TraceTap, which wraps an logger object.
305
+ # On each +trace+ event the TraceTap#trace method will be called, which in
306
+ # turn will call the <tt>logger.debug</tt> method
307
+ #
308
+ # db.trace_tap = $stderr
309
+ #
310
+ # This will register the <tt>$stderr</tt> io stream as a trace tap. Every time a
311
+ # +trace+ event happens then <tt>$stderr.write( msg )</tt> will be called.
312
+ #
313
+ # db.trace_tap = nil
314
+ #
315
+ # This will unregistere the trace tap
316
+ #
317
+ #
318
+ def trace_tap=( tap_obj )
319
+
320
+ # unregister any previous trace tap
321
+ #
322
+ unless @trace_tap.nil?
323
+ @trace_tap.trace( 'unregistered as trace tap' )
324
+ @trace_tap = nil
325
+ end
326
+ return @trace_tap if tap_obj.nil?
327
+
328
+
329
+ # wrap the tap if we need to
330
+ #
331
+ if tap_obj.respond_to?( 'trace' ) then
332
+ @trace_tap = tap_obj
333
+ elsif tap_obj.respond_to?( 'write' ) then
334
+ @trace_tap = Amalgalite::TraceTap.new( tap_obj, 'write' )
335
+ else
336
+ raise Amalgalite::Error, "#{tap_obj.class.name} cannot be used to tap. It has no 'write' or 'trace' method. Look at wrapping it in a Tap instances."
337
+ end
338
+
339
+ # and do the low level registration
340
+ #
341
+ @api.register_trace_tap( @trace_tap )
342
+
343
+ @trace_tap.trace( 'registered as trace tap' )
344
+ end
345
+
346
+
347
+ ##
348
+ # call-seq:
349
+ # db.profile_tap = obj
350
+ #
351
+ # Register a profile tap.
352
+ #
353
+ # Registering a profile tap means that the +obj+ registered will have its
354
+ # +profile+ method called with an Integer and a String parameter every time
355
+ # a profile event happens. The Integer is the number of nanoseconds it took
356
+ # for the String (SQL) to execute in wall-clock time.
357
+ #
358
+ # That is, every time a profile event happens in SQLite the following is
359
+ # invoked:
360
+ #
361
+ # obj.profile( str, int )
362
+ #
363
+ # For instance:
364
+ #
365
+ # db.profile_tap = Amalgalite::ProfileTap.new( logger, 'debug' )
366
+ #
367
+ # This will register an instance of ProfileTap, which wraps an logger object.
368
+ # On each +profile+ event the ProfileTap#profile method will be called
369
+ # which in turn will call <tt>logger.debug<tt> with a formatted string containing
370
+ # the String and Integer from the profile event.
371
+ #
372
+ # db.profile_tap = nil
373
+ #
374
+ # This will unregister the profile tap
375
+ #
376
+ #
377
+ def profile_tap=( tap_obj )
378
+
379
+ # unregister any previous profile tap
380
+ unless @profile_tap.nil?
381
+ @profile_tap.profile( 'unregistered as profile tap', 0.0 )
382
+ @profile_tap = nil
383
+ end
384
+ return @profile_tap if tap_obj.nil?
385
+
386
+ if tap_obj.respond_to?( 'profile' ) then
387
+ @profile_tap = tap_obj
388
+ else
389
+ raise Amalgalite::Error, "#{tap_obj.class.name} cannot be used to tap. It has no 'profile' method"
390
+ end
391
+ @api.register_profile_tap( @profile_tap )
392
+ @profile_tap.profile( 'registered as profile tap', 0.0 )
393
+ end
394
+
395
+ ##
396
+ # call-seq:
397
+ # db.type_map = DefaultMap.new
398
+ #
399
+ # Assign your own TypeMap instance to do type conversions. The value
400
+ # assigned here must respond to +bind_type_of+ and +result_value_of+
401
+ # methods. See the TypeMap class for more details.
402
+ #
403
+ #
404
+ def type_map=( type_map_obj )
405
+ %w[ bind_type_of result_value_of ].each do |method|
406
+ unless type_map_obj.respond_to?( method )
407
+ raise Amalgalite::Error, "#{type_map_obj.class.name} cannot be used to do type mapping. It does not respond to '#{method}'"
408
+ end
409
+ end
410
+ @type_map = type_map_obj
411
+ end
412
+
413
+ ##
414
+ # :call-seq:
415
+ # db.schema( dbname = "main" ) -> Schema
416
+ #
417
+ # Returns a Schema object containing the table and column structure of the
418
+ # database.
419
+ #
420
+ def schema( dbname = "main" )
421
+ @schema ||= ::Amalgalite::Schema.new( self, dbname )
422
+ end
423
+
424
+ ##
425
+ # :call-seq:
426
+ # db.reload_schema! -> Schema
427
+ #
428
+ # By default once the schema is obtained, it is cached. This is here to
429
+ # force the schema to be reloaded.
430
+ #
431
+ def reload_schema!( dbname = "main" )
432
+ @schema = nil
433
+ schema( dbname )
434
+ end
435
+
436
+ ##
437
+ # Run a pragma command against the database
438
+ #
439
+ # Returns the result set of the pragma
440
+ def pragma( cmd )
441
+ execute("PRAGMA #{cmd}")
442
+ end
443
+
444
+ ##
445
+ # Begin a transaction. The valid transaction types are:
446
+ #
447
+ # DEFERRED:: no read or write locks are created until the first
448
+ # statement is executed that requries a read or a write
449
+ # IMMEDIATE:: a readlock is obtained immediately so that no other process
450
+ # can write to the database
451
+ # EXCLUSIVE:: a read+write lock is obtained, no other proces can read or
452
+ # write to the database
453
+ #
454
+ # As a convenience, these are constants available in the
455
+ # Database::TransactionBehavior class.
456
+ #
457
+ # Amalgalite Transactions are database level transactions, just as SQLite's
458
+ # are.
459
+ #
460
+ # If a block is passed in, then when the block exits, it is guaranteed that
461
+ # either 'COMMIT' or 'ROLLBACK' has been executed.
462
+ #
463
+ # If any exception happens during the transaction that is caught by Amalgalite,
464
+ # then a 'ROLLBACK' is issued when the block closes.
465
+ #
466
+ # If no exception happens during the transaction then a 'COMMIT' is
467
+ # issued upon leaving the block.
468
+ #
469
+ # If no block is passed in then you are on your own.
470
+ #
471
+ def transaction( mode = TransactionBehavior::DEFERRED )
472
+ raise Amalgalite::Error, "Invalid transaction behavior mode #{mode}" unless TransactionBehavior.valid?( mode )
473
+ raise Amalgalite::Error, "Nested Transactions are not supported" if in_transaction?
474
+ execute( "BEGIN #{mode} TRANSACTION" )
475
+ if block_given? then
476
+ begin
477
+ yield self
478
+ ensure
479
+ if $! then
480
+ rollback
481
+ raise $!
482
+ else
483
+ commit
484
+ end
485
+ end
486
+ end
487
+ return in_transaction?
488
+ end
489
+
490
+ ##
491
+ # Commit a transaction
492
+ #
493
+ def commit
494
+ execute( "COMMIT" )
495
+ end
496
+
497
+ ##
498
+ # Rollback a transaction
499
+ #
500
+ def rollback
501
+ execute( "ROLLBACK" )
502
+ end
503
+ end
504
+ end
505
+