amalgalite 0.1.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.
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
+