amalgalite 1.6.0-x64-mingw32

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