amalgalite 0.10.1-x86-mingw32

Sign up to get free protection for your applications and to get access to all the features.
Files changed (100) hide show
  1. data/HISTORY +201 -0
  2. data/LICENSE +29 -0
  3. data/README +51 -0
  4. data/bin/amalgalite-pack +126 -0
  5. data/examples/a.rb +9 -0
  6. data/examples/blob.rb +88 -0
  7. data/examples/bootstrap.rb +36 -0
  8. data/examples/define_aggregate.rb +75 -0
  9. data/examples/define_function.rb +104 -0
  10. data/examples/gem-db.rb +94 -0
  11. data/examples/gems.db +0 -0
  12. data/examples/require_me.rb +11 -0
  13. data/examples/requires.rb +42 -0
  14. data/examples/schema-info.rb +34 -0
  15. data/ext/amalgalite/amalgalite3.c +290 -0
  16. data/ext/amalgalite/amalgalite3.h +151 -0
  17. data/ext/amalgalite/amalgalite3_blob.c +240 -0
  18. data/ext/amalgalite/amalgalite3_constants.c +221 -0
  19. data/ext/amalgalite/amalgalite3_database.c +1148 -0
  20. data/ext/amalgalite/amalgalite3_requires_bootstrap.c +210 -0
  21. data/ext/amalgalite/amalgalite3_statement.c +639 -0
  22. data/ext/amalgalite/extconf.rb +36 -0
  23. data/ext/amalgalite/gen_constants.rb +130 -0
  24. data/ext/amalgalite/sqlite3.c +106729 -0
  25. data/ext/amalgalite/sqlite3.h +5626 -0
  26. data/ext/amalgalite/sqlite3_options.h +4 -0
  27. data/ext/amalgalite/sqlite3ext.h +380 -0
  28. data/gemspec.rb +60 -0
  29. data/lib/amalgalite.rb +43 -0
  30. data/lib/amalgalite/1.8/amalgalite3.so +0 -0
  31. data/lib/amalgalite/1.9/amalgalite3.so +0 -0
  32. data/lib/amalgalite/aggregate.rb +67 -0
  33. data/lib/amalgalite/blob.rb +186 -0
  34. data/lib/amalgalite/boolean.rb +42 -0
  35. data/lib/amalgalite/busy_timeout.rb +47 -0
  36. data/lib/amalgalite/column.rb +97 -0
  37. data/lib/amalgalite/core_ext/kernel/require.rb +21 -0
  38. data/lib/amalgalite/database.rb +947 -0
  39. data/lib/amalgalite/function.rb +61 -0
  40. data/lib/amalgalite/index.rb +43 -0
  41. data/lib/amalgalite/packer.rb +226 -0
  42. data/lib/amalgalite/paths.rb +70 -0
  43. data/lib/amalgalite/profile_tap.rb +131 -0
  44. data/lib/amalgalite/progress_handler.rb +21 -0
  45. data/lib/amalgalite/requires.rb +120 -0
  46. data/lib/amalgalite/schema.rb +191 -0
  47. data/lib/amalgalite/sqlite3.rb +6 -0
  48. data/lib/amalgalite/sqlite3/constants.rb +80 -0
  49. data/lib/amalgalite/sqlite3/database/function.rb +48 -0
  50. data/lib/amalgalite/sqlite3/database/status.rb +68 -0
  51. data/lib/amalgalite/sqlite3/status.rb +60 -0
  52. data/lib/amalgalite/sqlite3/version.rb +37 -0
  53. data/lib/amalgalite/statement.rb +414 -0
  54. data/lib/amalgalite/table.rb +90 -0
  55. data/lib/amalgalite/taps.rb +2 -0
  56. data/lib/amalgalite/taps/console.rb +27 -0
  57. data/lib/amalgalite/taps/io.rb +71 -0
  58. data/lib/amalgalite/trace_tap.rb +35 -0
  59. data/lib/amalgalite/type_map.rb +63 -0
  60. data/lib/amalgalite/type_maps/default_map.rb +167 -0
  61. data/lib/amalgalite/type_maps/storage_map.rb +40 -0
  62. data/lib/amalgalite/type_maps/text_map.rb +22 -0
  63. data/lib/amalgalite/version.rb +37 -0
  64. data/lib/amalgalite/view.rb +26 -0
  65. data/spec/aggregate_spec.rb +169 -0
  66. data/spec/amalgalite_spec.rb +4 -0
  67. data/spec/blob_spec.rb +81 -0
  68. data/spec/boolean_spec.rb +23 -0
  69. data/spec/busy_handler.rb +165 -0
  70. data/spec/database_spec.rb +494 -0
  71. data/spec/default_map_spec.rb +87 -0
  72. data/spec/function_spec.rb +94 -0
  73. data/spec/integeration_spec.rb +111 -0
  74. data/spec/packer_spec.rb +60 -0
  75. data/spec/paths_spec.rb +28 -0
  76. data/spec/progress_handler_spec.rb +105 -0
  77. data/spec/requires_spec.rb +23 -0
  78. data/spec/rtree_spec.rb +71 -0
  79. data/spec/schema_spec.rb +120 -0
  80. data/spec/spec_helper.rb +27 -0
  81. data/spec/sqlite3/constants_spec.rb +65 -0
  82. data/spec/sqlite3/database_status_spec.rb +36 -0
  83. data/spec/sqlite3/status_spec.rb +18 -0
  84. data/spec/sqlite3/version_spec.rb +14 -0
  85. data/spec/sqlite3_spec.rb +53 -0
  86. data/spec/statement_spec.rb +161 -0
  87. data/spec/storage_map_spec.rb +41 -0
  88. data/spec/tap_spec.rb +59 -0
  89. data/spec/text_map_spec.rb +23 -0
  90. data/spec/type_map_spec.rb +17 -0
  91. data/spec/version_spec.rb +15 -0
  92. data/tasks/announce.rake +43 -0
  93. data/tasks/config.rb +107 -0
  94. data/tasks/distribution.rake +77 -0
  95. data/tasks/documentation.rake +32 -0
  96. data/tasks/extension.rake +141 -0
  97. data/tasks/rspec.rake +33 -0
  98. data/tasks/rubyforge.rake +59 -0
  99. data/tasks/utils.rb +80 -0
  100. metadata +237 -0
@@ -0,0 +1,21 @@
1
+ module Kernel
2
+ # alias the original require away to use later
3
+ alias :amalgalite_original_require :require
4
+
5
+ #
6
+ # hook into the system 'require' to allow for required text or blobs from an
7
+ # amalgalite database.
8
+ #
9
+ def require( filename )
10
+ loaded = amalgalite_original_require( filename )
11
+ rescue LoadError => load_error
12
+ if load_error.message =~ /#{Regexp.escape filename}\z/ then
13
+ loaded = Amalgalite::Requires.require( filename )
14
+ else
15
+ raise load_error
16
+ end
17
+ end
18
+
19
+ private :require
20
+ private :amalgalite_original_require
21
+ end
@@ -0,0 +1,947 @@
1
+ #--
2
+ # Copyright (c) 2008 Jeremy Hinegardner
3
+ # All rights reserved. See LICENSE and/or COPYING for details.
4
+ #++
5
+ require 'amalgalite/statement'
6
+ require 'amalgalite/trace_tap'
7
+ require 'amalgalite/profile_tap'
8
+ require 'amalgalite/type_maps/default_map'
9
+ require 'amalgalite/function'
10
+ require 'amalgalite/aggregate'
11
+ require 'amalgalite/busy_timeout'
12
+ require 'amalgalite/progress_handler'
13
+
14
+ module Amalgalite
15
+ #
16
+ # The encapsulation of a connection to an SQLite3 database.
17
+ #
18
+ # Example opening and possibly creating a new database
19
+ #
20
+ # db = Amalgalite::Database.new( "mydb.db" )
21
+ # db.execute( "SELECT * FROM table" ) do |row|
22
+ # puts row
23
+ # end
24
+ #
25
+ # db.close
26
+ #
27
+ # Open a database read only:
28
+ #
29
+ # db = Amalgalite::Database.new( "mydb.db", "r" )
30
+ #
31
+ # Open an in-memory database:
32
+ #
33
+ # db = Amalgalite::Database.new( ":memory:" )
34
+ #
35
+ class Database
36
+
37
+ # Error thrown if a database is opened with an invalid mode
38
+ class InvalidModeError < ::Amalgalite::Error; end
39
+
40
+ # Error thrown if there is a failure in a user defined function
41
+ class FunctionError < ::Amalgalite::Error; end
42
+
43
+ # Error thrown if there is a failure in a user defined aggregate
44
+ class AggregateError < ::Amalgalite::Error; end
45
+
46
+ # Error thrown if there is a failure in defining a busy handler
47
+ class BusyHandlerError < ::Amalgalite::Error; end
48
+
49
+ # Error thrown if there is a failure in defining a progress handler
50
+ class ProgressHandlerError < ::Amalgalite::Error; end
51
+
52
+ ##
53
+ # container class for holding transaction behavior constants. These are the
54
+ # SQLite values passed to a START TRANSACTION SQL statement.
55
+ #
56
+ class TransactionBehavior
57
+ # no read or write locks are created until the first statement is executed
58
+ # that requries a read or a write
59
+ DEFERRED = "DEFERRED"
60
+
61
+ # a readlock is obtained immediately so that no other process can write to
62
+ # the database
63
+ IMMEDIATE = "IMMEDIATE"
64
+
65
+ # a read+write lock is obtained, no other proces can read or write to the
66
+ # database
67
+ EXCLUSIVE = "EXCLUSIVE"
68
+
69
+ # list of valid transaction behavior constants
70
+ VALID = [ DEFERRED, IMMEDIATE, EXCLUSIVE ]
71
+
72
+ #
73
+ # is the given mode a valid transaction mode
74
+ #
75
+ def self.valid?( mode )
76
+ VALID.include? mode
77
+ end
78
+ end
79
+
80
+ include Amalgalite::SQLite3::Constants
81
+
82
+ # list of valid modes for opening an Amalgalite::Database
83
+ VALID_MODES = {
84
+ "r" => Open::READONLY,
85
+ "r+" => Open::READWRITE,
86
+ "w+" => Open::READWRITE | Open::CREATE,
87
+ }
88
+
89
+ # the low level Amalgalite::SQLite3::Database
90
+ attr_reader :api
91
+
92
+ # An object that follows the TraceTap protocol, or nil. By default this is nil
93
+ attr_reader :trace_tap
94
+
95
+ # An object that follows the ProfileTap protocol, or nil. By default this is nil
96
+ attr_reader :profile_tap
97
+
98
+ # An object that follows the TypeMap protocol, or nil.
99
+ # By default this is an instances of TypeMaps::DefaultMap
100
+ attr_reader :type_map
101
+
102
+ # A list of the user defined functions
103
+ attr_reader :functions
104
+
105
+ # A list of the user defined aggregates
106
+ attr_reader :aggregates
107
+
108
+ ##
109
+ # Create a new Amalgalite database
110
+ #
111
+ # :call-seq:
112
+ # Amalgalite::Database.new( filename, "w+", opts = {}) -> Database
113
+ #
114
+ # The first parameter is the filename of the sqlite database. Specifying
115
+ # ":memory:" as the filename creates an in-memory database.
116
+ #
117
+ # The second parameter is the standard file modes of how to open a file.
118
+ #
119
+ # The modes are:
120
+ #
121
+ # * r - Read-only
122
+ # * r+ - Read/write, an error is thrown if the database does not already exist
123
+ # * w+ - Read/write, create a new database if it doesn't exist
124
+ #
125
+ # <tt>w+</tt> is the default as this is how most databases will want to be utilized.
126
+ #
127
+ # opts is a hash of available options for the database:
128
+ #
129
+ # * :utf16 option to set the database to a utf16 encoding if creating a database.
130
+ #
131
+ # By default, databases are created with an encoding of utf8. Setting this to
132
+ # true and opening an already existing database has no effect.
133
+ #
134
+ # *NOTE* Currently :utf16 is not supported by Amalgalite, it is planned
135
+ # for a later release
136
+ #
137
+ #
138
+ def initialize( filename, mode = "w+", opts = {})
139
+ @open = false
140
+ @profile_tap = nil
141
+ @trace_tap = nil
142
+ @type_map = ::Amalgalite::TypeMaps::DefaultMap.new
143
+ @functions = Hash.new
144
+ @aggregates = Hash.new
145
+ @utf16 = false
146
+
147
+ unless VALID_MODES.keys.include?( mode )
148
+ raise InvalidModeError, "#{mode} is invalid, must be one of #{VALID_MODES.keys.join(', ')}"
149
+ end
150
+
151
+ if not File.exist?( filename ) and opts[:utf16] then
152
+ raise NotImplementedError, "Currently Amalgalite has not implemented utf16 support"
153
+ else
154
+ @api = Amalgalite::SQLite3::Database.open( filename, VALID_MODES[mode] )
155
+ end
156
+ @open = true
157
+ end
158
+
159
+ ##
160
+ # Is the database open or not
161
+ #
162
+ def open?
163
+ @open
164
+ end
165
+
166
+ ##
167
+ # Close the database
168
+ #
169
+ def close
170
+ if open? then
171
+ @api.close
172
+ @open = false
173
+ end
174
+ end
175
+
176
+ ##
177
+ # Is the database in autocommit mode or not
178
+ #
179
+ def autocommit?
180
+ @api.autocommit?
181
+ end
182
+
183
+ ##
184
+ # Return the rowid of the last inserted row
185
+ #
186
+ def last_insert_rowid
187
+ @api.last_insert_rowid
188
+ end
189
+
190
+ ##
191
+ # SQL escape the input string
192
+ #
193
+ def escape( s )
194
+ Amalgalite::SQLite3.escape( s )
195
+ end
196
+
197
+ ##
198
+ # Surround the give string with single-quotes and escape any single-quotes
199
+ # in the string
200
+ def quote( s )
201
+ Amalgalite::SQLite3.quote( s )
202
+ end
203
+
204
+ ##
205
+ # Is the database utf16 or not? A database is utf16 if the encoding is not
206
+ # UTF-8. Database can only be UTF-8 or UTF-16, and the default is UTF-8
207
+ #
208
+ def utf16?
209
+ return @utf16
210
+ #if @utf16.nil?
211
+ # @utf16 = (encoding != "UTF-8")
212
+ #end
213
+ #return @utf16
214
+ end
215
+
216
+ ##
217
+ # return the encoding of the database
218
+ #
219
+ def encoding
220
+ @encoding ||= pragma( "encoding" ).first['encoding']
221
+ end
222
+
223
+ ##
224
+ # return whether or not the database is currently in a transaction or not
225
+ #
226
+ def in_transaction?
227
+ not @api.autocommit?
228
+ end
229
+
230
+ ##
231
+ # return how many rows changed in the last insert, update or delete statement.
232
+ #
233
+ def row_changes
234
+ @api.row_changes
235
+ end
236
+
237
+ ##
238
+ # return how many rows have changed since this connection to the database was
239
+ # opened.
240
+ #
241
+ def total_changes
242
+ @api.total_changes
243
+ end
244
+
245
+ ##
246
+ # Prepare a statement for execution
247
+ #
248
+ # If called with a block, the statement is yielded to the block and the
249
+ # statement is closed when the block is done.
250
+ #
251
+ # db.prepare( "SELECT * FROM table WHERE c = ?" ) do |stmt|
252
+ # list_of_c_values.each do |c|
253
+ # stmt.execute( c ) do |row|
254
+ # puts "when c = #{c} : #{row.inspect}"
255
+ # end
256
+ # end
257
+ # end
258
+ #
259
+ # Or without a block:
260
+ #
261
+ # stmt = db.prepare( "INSERT INTO t1(x, y, z) VALUES ( :
262
+ #
263
+ def prepare( sql )
264
+ stmt = Amalgalite::Statement.new( self, sql )
265
+ if block_given? then
266
+ begin
267
+ yield stmt
268
+ ensure
269
+ stmt.close
270
+ stmt = nil
271
+ end
272
+ end
273
+ return stmt
274
+ end
275
+
276
+ ##
277
+ # Execute a single SQL statement.
278
+ #
279
+ # If called with a block and there are result rows, then they are iteratively
280
+ # yielded to the block.
281
+ #
282
+ # If no block is passed, then all the results are returned as an arrayfields
283
+ # instance. This is an array with field name access.
284
+ #
285
+ # If no block is passed, and there are no results, then an empty Array is
286
+ # returned.
287
+ #
288
+ # On an error an exception is thrown
289
+ #
290
+ # This is just a wrapper around the preparation of an Amalgalite Statement and
291
+ # iterating over the results.
292
+ #
293
+ def execute( sql, *bind_params )
294
+ stmt = prepare( sql )
295
+ stmt.bind( *bind_params )
296
+ if block_given? then
297
+ stmt.each { |row| yield row }
298
+ else
299
+ return stmt.all_rows
300
+ end
301
+ ensure
302
+ stmt.close if stmt
303
+ end
304
+
305
+ ##
306
+ # Execute a batch of statements, this will execute all the sql in the given
307
+ # string until no more sql can be found in the string. It will bind the
308
+ # same parameters to each statement. All data that would be returned from
309
+ # all of the statements is thrown away.
310
+ #
311
+ # All statements to be executed in the batch must be terminated with a ';'
312
+ # Returns the number of statements executed
313
+ #
314
+ #
315
+ def execute_batch( sql, *bind_params)
316
+ count = 0
317
+ while sql
318
+ prepare( sql ) do |stmt|
319
+ stmt.execute( *bind_params )
320
+ sql = stmt.remaining_sql
321
+ sql = nil unless (sql.index(";") and Amalgalite::SQLite3.complete?( sql ))
322
+ end
323
+ count += 1
324
+ end
325
+ return count
326
+ end
327
+
328
+ ##
329
+ # clear all the current taps
330
+ #
331
+ def clear_taps!
332
+ self.trace_tap = nil
333
+ self.profile_tap = nil
334
+ end
335
+
336
+ ##
337
+ # Execute a sql statment, and only return the first row of results. This
338
+ # is a shorthand method when you only want a single row of results from a
339
+ # query. If there is no result, then return an empty array
340
+ #
341
+ # It is in all other was, exactly like #execute()
342
+ #
343
+ def first_row_from( sql, *bind_params )
344
+ stmt = prepare( sql )
345
+ stmt.bind( *bind_params)
346
+ row = stmt.next_row || []
347
+ stmt.close
348
+ return row
349
+ end
350
+
351
+ ##
352
+ # Execute an sql statement, and return only the first column of the first
353
+ # row. If there is no result, return nil.
354
+ #
355
+ # It is in all other ways, exactly like #first_row_from()
356
+ #
357
+ def first_value_from( sql, *bind_params )
358
+ return first_row_from( sql, *bind_params).first
359
+ end
360
+
361
+ ##
362
+ # call-seq:
363
+ # db.trace_tap = obj
364
+ #
365
+ # Register a trace tap.
366
+ #
367
+ # Registering a trace tap measn that the +obj+ registered will have its
368
+ # +trace+ method called with a string parameter at various times.
369
+ # If the object doesn't respond to the +trace+ method then +write+
370
+ # will be called.
371
+ #
372
+ # For instance:
373
+ #
374
+ # db.trace_tap = Amalgalite::TraceTap.new( logger, 'debug' )
375
+ #
376
+ # This will register an instance of TraceTap, which wraps an logger object.
377
+ # On each +trace+ event the TraceTap#trace method will be called, which in
378
+ # turn will call the <tt>logger.debug</tt> method
379
+ #
380
+ # db.trace_tap = $stderr
381
+ #
382
+ # This will register the <tt>$stderr</tt> io stream as a trace tap. Every time a
383
+ # +trace+ event happens then <tt>$stderr.write( msg )</tt> will be called.
384
+ #
385
+ # db.trace_tap = nil
386
+ #
387
+ # This will unregistere the trace tap
388
+ #
389
+ #
390
+ def trace_tap=( tap_obj )
391
+
392
+ # unregister any previous trace tap
393
+ #
394
+ unless @trace_tap.nil?
395
+ @trace_tap.trace( 'unregistered as trace tap' )
396
+ @trace_tap = nil
397
+ end
398
+ return @trace_tap if tap_obj.nil?
399
+
400
+
401
+ # wrap the tap if we need to
402
+ #
403
+ if tap_obj.respond_to?( 'trace' ) then
404
+ @trace_tap = tap_obj
405
+ elsif tap_obj.respond_to?( 'write' ) then
406
+ @trace_tap = Amalgalite::TraceTap.new( tap_obj, 'write' )
407
+ else
408
+ 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."
409
+ end
410
+
411
+ # and do the low level registration
412
+ #
413
+ @api.register_trace_tap( @trace_tap )
414
+
415
+ @trace_tap.trace( 'registered as trace tap' )
416
+ end
417
+
418
+
419
+ ##
420
+ # call-seq:
421
+ # db.profile_tap = obj
422
+ #
423
+ # Register a profile tap.
424
+ #
425
+ # Registering a profile tap means that the +obj+ registered will have its
426
+ # +profile+ method called with an Integer and a String parameter every time
427
+ # a profile event happens. The Integer is the number of nanoseconds it took
428
+ # for the String (SQL) to execute in wall-clock time.
429
+ #
430
+ # That is, every time a profile event happens in SQLite the following is
431
+ # invoked:
432
+ #
433
+ # obj.profile( str, int )
434
+ #
435
+ # For instance:
436
+ #
437
+ # db.profile_tap = Amalgalite::ProfileTap.new( logger, 'debug' )
438
+ #
439
+ # This will register an instance of ProfileTap, which wraps an logger object.
440
+ # On each +profile+ event the ProfileTap#profile method will be called
441
+ # which in turn will call <tt>logger.debug<tt> with a formatted string containing
442
+ # the String and Integer from the profile event.
443
+ #
444
+ # db.profile_tap = nil
445
+ #
446
+ # This will unregister the profile tap
447
+ #
448
+ #
449
+ def profile_tap=( tap_obj )
450
+
451
+ # unregister any previous profile tap
452
+ unless @profile_tap.nil?
453
+ @profile_tap.profile( 'unregistered as profile tap', 0.0 )
454
+ @profile_tap = nil
455
+ end
456
+ return @profile_tap if tap_obj.nil?
457
+
458
+ if tap_obj.respond_to?( 'profile' ) then
459
+ @profile_tap = tap_obj
460
+ else
461
+ raise Amalgalite::Error, "#{tap_obj.class.name} cannot be used to tap. It has no 'profile' method"
462
+ end
463
+ @api.register_profile_tap( @profile_tap )
464
+ @profile_tap.profile( 'registered as profile tap', 0.0 )
465
+ end
466
+
467
+ ##
468
+ # call-seq:
469
+ # db.type_map = DefaultMap.new
470
+ #
471
+ # Assign your own TypeMap instance to do type conversions. The value
472
+ # assigned here must respond to +bind_type_of+ and +result_value_of+
473
+ # methods. See the TypeMap class for more details.
474
+ #
475
+ #
476
+ def type_map=( type_map_obj )
477
+ %w[ bind_type_of result_value_of ].each do |method|
478
+ unless type_map_obj.respond_to?( method )
479
+ raise Amalgalite::Error, "#{type_map_obj.class.name} cannot be used to do type mapping. It does not respond to '#{method}'"
480
+ end
481
+ end
482
+ @type_map = type_map_obj
483
+ end
484
+
485
+ ##
486
+ # :call-seq:
487
+ # db.schema( dbname = "main" ) -> Schema
488
+ #
489
+ # Returns a Schema object containing the table and column structure of the
490
+ # database.
491
+ #
492
+ def schema( dbname = "main" )
493
+ @schema ||= ::Amalgalite::Schema.new( self, dbname )
494
+ end
495
+
496
+ ##
497
+ # :call-seq:
498
+ # db.reload_schema! -> Schema
499
+ #
500
+ # By default once the schema is obtained, it is cached. This is here to
501
+ # force the schema to be reloaded.
502
+ #
503
+ def reload_schema!( dbname = "main" )
504
+ @schema = nil
505
+ schema( dbname )
506
+ end
507
+
508
+ ##
509
+ # Run a pragma command against the database
510
+ #
511
+ # Returns the result set of the pragma
512
+ def pragma( cmd, &block )
513
+ execute("PRAGMA #{cmd}", &block)
514
+ end
515
+
516
+ ##
517
+ # Begin a transaction. The valid transaction types are:
518
+ #
519
+ # DEFERRED:: no read or write locks are created until the first
520
+ # statement is executed that requries a read or a write
521
+ # IMMEDIATE:: a readlock is obtained immediately so that no other process
522
+ # can write to the database
523
+ # EXCLUSIVE:: a read+write lock is obtained, no other proces can read or
524
+ # write to the database
525
+ #
526
+ # As a convenience, these are constants available in the
527
+ # Database::TransactionBehavior class.
528
+ #
529
+ # Amalgalite Transactions are database level transactions, just as SQLite's
530
+ # are.
531
+ #
532
+ # If a block is passed in, then when the block exits, it is guaranteed that
533
+ # either 'COMMIT' or 'ROLLBACK' has been executed.
534
+ #
535
+ # If any exception happens during the transaction that is caught by Amalgalite,
536
+ # then a 'ROLLBACK' is issued when the block closes.
537
+ #
538
+ # If no exception happens during the transaction then a 'COMMIT' is
539
+ # issued upon leaving the block.
540
+ #
541
+ # If no block is passed in then you are on your own.
542
+ #
543
+ # Nesting a transaaction via the _transaction_ method are no-ops.
544
+ # If you call transaction within a transaction, no new transaction is
545
+ # started, the current one is just continued.
546
+ #
547
+ # True nexted transactions are available through the _savepoint_ method.
548
+ #
549
+ def transaction( mode = TransactionBehavior::DEFERRED, &block )
550
+ raise Amalgalite::Error, "Invalid transaction behavior mode #{mode}" unless TransactionBehavior.valid?( mode )
551
+
552
+ # if already in a transaction, no need to start a new one.
553
+ if not in_transaction? then
554
+ execute( "BEGIN #{mode} TRANSACTION" )
555
+ end
556
+
557
+ if block_given? then
558
+ begin
559
+ previous_exception = $!
560
+ return ( yield self )
561
+ ensure
562
+ if $! and ($! != previous_exception) then
563
+ rollback
564
+ raise $!
565
+ else
566
+ commit
567
+ end
568
+ end
569
+ else
570
+ return in_transaction?
571
+ end
572
+ end
573
+ alias :deferred_transaction :transaction
574
+
575
+ # helper for an immediate transaction
576
+ def immediate_transaction( &block )
577
+ transaction( TransactionBehavior::IMMEDIATE, &block )
578
+ end
579
+
580
+ # helper for an exclusive transaction
581
+ def exclusive_transaction( &block )
582
+ transaction( TransactionBehavior::EXCLUSIVE, &block )
583
+ end
584
+
585
+ ##
586
+ # call-seq:
587
+ # db.savepoint( 'mypoint' ) -> db
588
+ # db.savepoint( 'mypoint' ) do |db_in_savepoint|
589
+ # ...
590
+ # end
591
+ #
592
+ # Much of the following documentation is para-phrased from
593
+ # http://sqlite.org/lang_savepoint.html
594
+ #
595
+ # Savepoints are a method of creating transactions, similar to _transaction_
596
+ # except that they may be nested.
597
+ #
598
+ # * Every savepoint must have a name, +to_s+ is called on the method
599
+ # argument
600
+ # * A savepoint does not need to be initialized inside a _transaction_. If
601
+ # it is not inside a _transaction_ it behaves exactly as if a DEFERRED
602
+ # transaction had been started.
603
+ # * If a block is passed to _saveponit_ then when the block exists, it is
604
+ # guaranteed that either a 'RELEASE' or 'ROLLBACK TO name' has been executed.
605
+ # * If any exception happens during the savepoint transaction, then a
606
+ # 'ROLLOBACK TO' is issued when the block closes.
607
+ # * If no exception happens during the transaction then a 'RELEASE name' is
608
+ # issued upon leaving the block
609
+ #
610
+ # If no block is passed in then you are on your own.
611
+ #
612
+ def savepoint( name )
613
+ point_name = name.to_s.strip
614
+ raise Amalgalite::Error, "Invalid savepoint name '#{name}'" unless point_name and point_name.length > 1
615
+ execute( "SAVEPOINT #{point_name};")
616
+ if block_given? then
617
+ begin
618
+ return ( yield self )
619
+ ensure
620
+ if $! then
621
+ rollback_to( point_name )
622
+ raise $!
623
+ else
624
+ release( point_name )
625
+ end
626
+ end
627
+ else
628
+ return in_transaction?
629
+ end
630
+ end
631
+
632
+ ##
633
+ # call-seq:
634
+ # db.release( 'mypoint' )
635
+ #
636
+ # Release a savepoint. This is similar to a _commit_ but only for
637
+ # savepoints. All savepoints up the savepoint stack and include the name
638
+ # savepoint being released are 'committed' to the transaction. There are
639
+ # several ways of thinking about release and they are all detailed in the
640
+ # sqlite documentation: http://sqlite.org/lang_savepoint.html
641
+ #
642
+ def release( point_name )
643
+ execute( "RELEASE SAVEPOINT #{point_name}" ) if in_transaction?
644
+ end
645
+
646
+ ##
647
+ # call-seq:
648
+ # db.rollback_to( point_name )
649
+ #
650
+ # Rollback to a savepoint. The transaction is not cancelled, the
651
+ # transaction is restarted.
652
+ def rollback_to( point_name )
653
+ execute( "ROLLBACK TO SAVEPOINT #{point_name}" )
654
+ end
655
+
656
+ ##
657
+ # Commit a transaction
658
+ #
659
+ def commit
660
+ execute( "COMMIT TRANSACTION" ) if in_transaction?
661
+ end
662
+
663
+ ##
664
+ # Rollback a transaction
665
+ #
666
+ def rollback
667
+ execute( "ROLLBACK TRANSACTION" ) if in_transaction?
668
+ end
669
+
670
+ ##
671
+ # call-seq:
672
+ # db.function( "name", MyDBFunction.new )
673
+ # db.function( "my_func", callable )
674
+ # db.function( "my_func" ) do |x,y|
675
+ # ....
676
+ # return result
677
+ # end
678
+ #
679
+ # register a callback to be exposed as an SQL function. There are multiple
680
+ # ways to register this function:
681
+ #
682
+ # 1. db.function( "name" ) { |a| ... }
683
+ # * pass +function+ a _name_ and a block.
684
+ # * The SQL function _name_ taking _arity_ parameters will be registered,
685
+ # where _arity_ is the _arity_ of the block.
686
+ # * The return value of the block is the return value of the registred
687
+ # SQL function
688
+ # 2. db.function( "name", callable )
689
+ # * pass +function+ a _name_ and something that <tt>responds_to?( :to_proc )</tt>
690
+ # * The SQL function _name_ is registered taking _arity_ parameters is
691
+ # registered where _arity_ is the _arity_ of +callable.to_proc.call+
692
+ # * The return value of the +callable.to_proc.call+ is the return value
693
+ # of the SQL function
694
+ #
695
+ # See also ::Amalgalite::Function
696
+ #
697
+ def define_function( name, callable = nil, &block )
698
+ p = ( callable || block ).to_proc
699
+ raise FunctionError, "Use only mandatory or arbitrary parameters in an SQL Function, not both" if p.arity < -1
700
+ db_function = ::Amalgalite::SQLite3::Database::Function.new( name, p )
701
+ @api.define_function( db_function.name, db_function )
702
+ @functions[db_function.signature] = db_function
703
+ nil
704
+ end
705
+ alias :function :define_function
706
+
707
+ ##
708
+ # call-seq:
709
+ # db.remove_function( 'name', MyScalerFunctor.new )
710
+ # db.remove_function( 'name', callable )
711
+ # db.remove_function( 'name', arity )
712
+ # db.remove_function( 'name' )
713
+ #
714
+ # Remove a function from use in the database. Since the same function may
715
+ # be registered more than once with different arity, you may specify the
716
+ # arity, or the function object, or nil. If nil is used for the arity, then
717
+ # Amalgalite does its best to remove all functions of given name.
718
+ #
719
+ def remove_function( name, callable_or_arity = nil )
720
+ arity = nil
721
+ if callable_or_arity.respond_to?( :to_proc ) then
722
+ arity = callable_or_arity.to_proc.arity
723
+ elsif callable_or_arity.respond_to?( :to_int ) then
724
+ arity = callable_or_arity.to_int
725
+ end
726
+ to_remove = []
727
+
728
+ if arity then
729
+ signature = ::Amalgalite::SQLite3::Database::Function.signature( name, arity )
730
+ db_function = @functions[ signature ]
731
+ raise FunctionError, "db function '#{name}' with arity #{arity} does not appear to be defined" unless db_function
732
+ to_remove << db_function
733
+ else
734
+ possibles = @functions.values.select { |f| f.name == name }
735
+ raise FunctionError, "no db function '#{name}' appears to be defined" if possibles.empty?
736
+ to_remove = possibles
737
+ end
738
+
739
+ to_remove.each do |db_function|
740
+ @api.remove_function( db_function.name, db_function)
741
+ @functions.delete( db_function.signature )
742
+ end
743
+ end
744
+
745
+ ##
746
+ # call-seq:
747
+ # db.define_aggregate( 'name', MyAggregateClass )
748
+ #
749
+ # Define an SQL aggregate function, these are functions like max(), min(),
750
+ # avg(), etc. SQL functions that would be used when a GROUP BY clause is in
751
+ # effect. See also ::Amalgalite::Aggregate.
752
+ #
753
+ # A new instance of MyAggregateClass is created for each instance that the
754
+ # SQL aggregate is mentioned in SQL.
755
+ #
756
+ def define_aggregate( name, klass )
757
+ db_aggregate = klass
758
+ a = klass.new
759
+ raise AggregateError, "Use only mandatory or arbitrary parameters in an SQL Aggregate, not both" if a.arity < -1
760
+ raise AggregateError, "Aggregate implementation name '#{a.name}' does not match defined name '#{name}'"if a.name != name
761
+ @api.define_aggregate( name, a.arity, klass )
762
+ @aggregates[a.signature] = db_aggregate
763
+ nil
764
+ end
765
+ alias :aggregate :define_aggregate
766
+
767
+ ##
768
+ # call-seq:
769
+ # db.remove_aggregate( 'name', MyAggregateClass )
770
+ # db.remove_aggregate( 'name' )
771
+ #
772
+ # Remove an aggregate from use in the database. Since the same aggregate
773
+ # may be refistered more than once with different arity, you may specify the
774
+ # arity, or the aggregate class, or nil. If nil is used for the arity then
775
+ # Amalgalite does its best to remove all aggregates of the given name
776
+ #
777
+ def remove_aggregate( name, klass_or_arity = nil )
778
+ klass = nil
779
+ case klass_or_arity
780
+ when Integer
781
+ arity = klass_or_arity
782
+ when NilClass
783
+ arity = nil
784
+ else
785
+ klass = klass_or_arity
786
+ arity = klass.new.arity
787
+ end
788
+ to_remove = []
789
+ if arity then
790
+ signature = ::Amalgalite::SQLite3::Database::Function.signature( name, arity )
791
+ db_aggregate = @aggregates[ signature ]
792
+ raise AggregateError, "db aggregate '#{name}' with arity #{arity} does not appear to be defined" unless db_aggregate
793
+ to_remove << db_aggregate
794
+ else
795
+ possibles = @aggregates.values.select { |a| a.new.name == name }
796
+ raise AggregateError, "no db aggregate '#{name}' appears to be defined" if possibles.empty?
797
+ to_remove = possibles
798
+ end
799
+
800
+ to_remove.each do |db_aggregate|
801
+ i = db_aggregate.new
802
+ @api.remove_aggregate( i.name, i.arity, db_aggregate )
803
+ @aggregates.delete( i.signature )
804
+ end
805
+ end
806
+
807
+ ##
808
+ # call-seq:
809
+ # db.busy_handler( callable )
810
+ # db.define_busy_handler do |count|
811
+ # end
812
+ # db.busy_handler( Amalgalite::BusyTimeout.new( 30 ) )
813
+ #
814
+ # Register a busy handler for this database connection, the handler MUST
815
+ # follow the +to_proc+ protocol indicating that is will
816
+ # +respond_to?(:call)+. This is intrinsic to lambdas and blocks so
817
+ # those will work automatically.
818
+ #
819
+ # This exposes the sqlite busy handler api to ruby.
820
+ #
821
+ # * http://sqlite.org/c3ref/busy_handler.html
822
+ #
823
+ # The busy handler's _call(N)_ method may be invoked whenever an attempt is
824
+ # made to open a database table that another thread or process has locked.
825
+ # +N+ will be the number of times the _call(N)_ method has been invoked
826
+ # during this locking event.
827
+ #
828
+ # The handler may or maynot be called based upon what SQLite determins.
829
+ #
830
+ # If the handler returns _nil_ or _false_ then no more busy handler calls will
831
+ # be made in this lock event and you are probably going to see an
832
+ # SQLite::Error in your immediately future in another process or in another
833
+ # piece of code.
834
+ #
835
+ # If the handler returns non-nil or non-false then another attempt will be
836
+ # made to obtain the lock, lather, rinse, repeat.
837
+ #
838
+ # If an Exception happens in a busy handler, it will be the same as if the
839
+ # busy handler had returned _nil_ or _false_. The exception itself will not
840
+ # be propogated further.
841
+ #
842
+ def define_busy_handler( callable = nil, &block )
843
+ handler = ( callable || block ).to_proc
844
+ a = handler.arity
845
+ raise BusyHandlerError, "A busy handler expects 1 and only 1 argument, not #{a}" if a != 1
846
+ @api.busy_handler( handler )
847
+ end
848
+ alias :busy_handler :define_busy_handler
849
+
850
+ ##
851
+ # call-seq:
852
+ # db.remove_busy_handler
853
+ #
854
+ # Remove the busy handler for this database connection.
855
+ def remove_busy_handler
856
+ @api.busy_handler( nil )
857
+ end
858
+
859
+ ##
860
+ # call-seq:
861
+ # db.interrupt!
862
+ #
863
+ # Cause another thread with a handle on this database to be interrupted and
864
+ # return at the earliest opportunity as interrupted. It is not safe to call
865
+ # this method if the database might be closed before interrupt! returns.
866
+ #
867
+ def interrupt!
868
+ @api.interrupt!
869
+ end
870
+
871
+ ##
872
+ # call-seq:
873
+ # db.progress_handler( 50, MyProgressHandler.new )
874
+ # db.progress_handler( 25 , callable )
875
+ # db.progress_handler do
876
+ # ....
877
+ # return result
878
+ # end
879
+ #
880
+ # Register a progress handler for this database connection, the handler MUST
881
+ # follow the +to_proc+ protocol indicating that is will
882
+ # +respond_to?(:call)+. This is intrinsic to lambdas and blocks so
883
+ # those will work automatically.
884
+ #
885
+ # This exposes the sqlite progress handler api to ruby.
886
+ #
887
+ # * http://sqlite.org/c3ref/progress_handler.html
888
+ #
889
+ # The progress handler's _call()_ method may be invoked ever N SQLite op
890
+ # codes. If the progress handler returns anything that can evaluate to
891
+ # +true+ then current running sqlite statement is terminated at the earliest
892
+ # oppportunity.
893
+ #
894
+ # You can use this to be notified that a thread is still processingn a
895
+ # request.
896
+ #
897
+ def define_progress_handler( op_code_count = 25, callable = nil, &block )
898
+ handler = ( callable || block ).to_proc
899
+ a = handler.arity
900
+ raise ProgressHandlerError, "A progress handler expects 0 arguments, not #{a}" if a != 0
901
+ @api.progress_handler( op_code_count, handler )
902
+ end
903
+ alias :progress_handler :define_progress_handler
904
+
905
+ ##
906
+ # call-seq:
907
+ # db.remove_progress_handler
908
+ #
909
+ # Remove the progress handler for this database connection.
910
+ def remove_progress_handler
911
+ @api.progress_handler( nil, nil )
912
+ end
913
+
914
+ ##
915
+ # call-seq:
916
+ # db.replicate_to( ":memory:" ) -> new_db
917
+ # db.replicate_to( "/some/location/my.db" ) -> new_db
918
+ # db.replicate_to( Amalgalite::Database.new( "/my/backup.db" ) ) -> new_db
919
+ #
920
+ # replicate_to() takes a single argument, either a String or an
921
+ # Amalgalite::Database. It returns the replicated database object. If
922
+ # given a String, it will truncate that database if it already exists.
923
+ #
924
+ # Replicate the current database to another location, this can be used for a
925
+ # number of purposes:
926
+ #
927
+ # * load an sqlite database from disk into memory
928
+ # * snaphost an in memory db and save it to disk
929
+ # * backup on sqlite database to another location
930
+ #
931
+ def replicate_to( location )
932
+ to_db = nil
933
+ case location
934
+ when String
935
+ to_db = Amalgalite::Database.new( location )
936
+ when Amalgalite::Database
937
+ to_db = location
938
+ else
939
+ raise ArgumentError, "replicate_to( #{location} ) must be a String or a Database"
940
+ end
941
+
942
+ @api.replicate_to( to_db.api )
943
+ return to_db
944
+ end
945
+ end
946
+ end
947
+