amalgalite 0.10.1-x86-mingw32

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 (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
+