amalgalite 1.8.0-x64-mingw-ucrt
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CONTRIBUTING.md +60 -0
- data/HISTORY.md +386 -0
- data/LICENSE +31 -0
- data/Manifest.txt +105 -0
- data/README.md +62 -0
- data/Rakefile +27 -0
- data/TODO.md +57 -0
- data/bin/amalgalite-pack +147 -0
- data/examples/a.rb +9 -0
- data/examples/blob.rb +88 -0
- data/examples/bootstrap.rb +36 -0
- data/examples/define_aggregate.rb +75 -0
- data/examples/define_function.rb +104 -0
- data/examples/fts5.rb +152 -0
- data/examples/gem-db.rb +94 -0
- data/examples/require_me.rb +11 -0
- data/examples/requires.rb +42 -0
- data/examples/schema-info.rb +34 -0
- data/ext/amalgalite/c/amalgalite.c +355 -0
- data/ext/amalgalite/c/amalgalite.h +151 -0
- data/ext/amalgalite/c/amalgalite_blob.c +240 -0
- data/ext/amalgalite/c/amalgalite_constants.c +1432 -0
- data/ext/amalgalite/c/amalgalite_database.c +1188 -0
- data/ext/amalgalite/c/amalgalite_requires_bootstrap.c +282 -0
- data/ext/amalgalite/c/amalgalite_statement.c +649 -0
- data/ext/amalgalite/c/extconf.rb +71 -0
- data/ext/amalgalite/c/gen_constants.rb +353 -0
- data/ext/amalgalite/c/notes.txt +134 -0
- data/ext/amalgalite/c/sqlite3.c +243616 -0
- data/ext/amalgalite/c/sqlite3.h +12894 -0
- data/ext/amalgalite/c/sqlite3_options.h +4 -0
- data/ext/amalgalite/c/sqlite3ext.h +705 -0
- data/lib/amalgalite/3.1/amalgalite.so +0 -0
- data/lib/amalgalite/aggregate.rb +73 -0
- data/lib/amalgalite/blob.rb +186 -0
- data/lib/amalgalite/boolean.rb +42 -0
- data/lib/amalgalite/busy_timeout.rb +47 -0
- data/lib/amalgalite/column.rb +99 -0
- data/lib/amalgalite/core_ext/kernel/require.rb +21 -0
- data/lib/amalgalite/csv_table_importer.rb +75 -0
- data/lib/amalgalite/database.rb +933 -0
- data/lib/amalgalite/function.rb +61 -0
- data/lib/amalgalite/index.rb +43 -0
- data/lib/amalgalite/memory_database.rb +15 -0
- data/lib/amalgalite/packer.rb +231 -0
- data/lib/amalgalite/paths.rb +80 -0
- data/lib/amalgalite/profile_tap.rb +131 -0
- data/lib/amalgalite/progress_handler.rb +21 -0
- data/lib/amalgalite/requires.rb +151 -0
- data/lib/amalgalite/schema.rb +225 -0
- data/lib/amalgalite/sqlite3/constants.rb +95 -0
- data/lib/amalgalite/sqlite3/database/function.rb +48 -0
- data/lib/amalgalite/sqlite3/database/status.rb +68 -0
- data/lib/amalgalite/sqlite3/status.rb +60 -0
- data/lib/amalgalite/sqlite3/version.rb +55 -0
- data/lib/amalgalite/sqlite3.rb +6 -0
- data/lib/amalgalite/statement.rb +421 -0
- data/lib/amalgalite/table.rb +91 -0
- data/lib/amalgalite/taps/console.rb +27 -0
- data/lib/amalgalite/taps/io.rb +74 -0
- data/lib/amalgalite/taps.rb +2 -0
- data/lib/amalgalite/trace_tap.rb +35 -0
- data/lib/amalgalite/type_map.rb +63 -0
- data/lib/amalgalite/type_maps/default_map.rb +166 -0
- data/lib/amalgalite/type_maps/storage_map.rb +38 -0
- data/lib/amalgalite/type_maps/text_map.rb +21 -0
- data/lib/amalgalite/version.rb +8 -0
- data/lib/amalgalite/view.rb +26 -0
- data/lib/amalgalite.rb +51 -0
- data/spec/aggregate_spec.rb +158 -0
- data/spec/amalgalite_spec.rb +4 -0
- data/spec/blob_spec.rb +78 -0
- data/spec/boolean_spec.rb +24 -0
- data/spec/busy_handler.rb +157 -0
- data/spec/data/iso-3166-country.txt +242 -0
- data/spec/data/iso-3166-schema.sql +22 -0
- data/spec/data/iso-3166-subcountry.txt +3995 -0
- data/spec/data/make-iso-db.sh +12 -0
- data/spec/database_spec.rb +505 -0
- data/spec/default_map_spec.rb +92 -0
- data/spec/function_spec.rb +78 -0
- data/spec/integeration_spec.rb +97 -0
- data/spec/iso_3166_database.rb +58 -0
- data/spec/json_spec.rb +24 -0
- data/spec/packer_spec.rb +60 -0
- data/spec/paths_spec.rb +28 -0
- data/spec/progress_handler_spec.rb +91 -0
- data/spec/requires_spec.rb +54 -0
- data/spec/rtree_spec.rb +66 -0
- data/spec/schema_spec.rb +131 -0
- data/spec/spec_helper.rb +48 -0
- data/spec/sqlite3/constants_spec.rb +108 -0
- data/spec/sqlite3/database_status_spec.rb +36 -0
- data/spec/sqlite3/status_spec.rb +22 -0
- data/spec/sqlite3/version_spec.rb +28 -0
- data/spec/sqlite3_spec.rb +53 -0
- data/spec/statement_spec.rb +168 -0
- data/spec/storage_map_spec.rb +38 -0
- data/spec/tap_spec.rb +57 -0
- data/spec/text_map_spec.rb +20 -0
- data/spec/type_map_spec.rb +14 -0
- data/spec/version_spec.rb +8 -0
- data/tasks/custom.rake +101 -0
- data/tasks/default.rake +244 -0
- data/tasks/extension.rake +28 -0
- data/tasks/this.rb +208 -0
- metadata +325 -0
@@ -0,0 +1,933 @@
|
|
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
|
+
end
|
343
|
+
|
344
|
+
##
|
345
|
+
# Execute a sql statment, and only return the first row of results. This
|
346
|
+
# is a shorthand method when you only want a single row of results from a
|
347
|
+
# query. If there is no result, then return an empty array
|
348
|
+
#
|
349
|
+
# It is in all other was, exactly like #execute()
|
350
|
+
#
|
351
|
+
def first_row_from( sql, *bind_params )
|
352
|
+
stmt = prepare( sql )
|
353
|
+
stmt.bind( *bind_params)
|
354
|
+
row = stmt.next_row || []
|
355
|
+
stmt.close
|
356
|
+
return row
|
357
|
+
end
|
358
|
+
|
359
|
+
##
|
360
|
+
# Execute an sql statement, and return only the first column of the first
|
361
|
+
# row. If there is no result, return nil.
|
362
|
+
#
|
363
|
+
# It is in all other ways, exactly like #first_row_from()
|
364
|
+
#
|
365
|
+
def first_value_from( sql, *bind_params )
|
366
|
+
return first_row_from( sql, *bind_params).first
|
367
|
+
end
|
368
|
+
|
369
|
+
##
|
370
|
+
# call-seq:
|
371
|
+
# db.trace_tap = obj
|
372
|
+
#
|
373
|
+
# Register a trace tap.
|
374
|
+
#
|
375
|
+
# Registering a trace tap measn that the +obj+ registered will have its
|
376
|
+
# +trace+ method called with a string parameter at various times.
|
377
|
+
# If the object doesn't respond to the +trace+ method then +write+
|
378
|
+
# will be called.
|
379
|
+
#
|
380
|
+
# For instance:
|
381
|
+
#
|
382
|
+
# db.trace_tap = Amalgalite::TraceTap.new( logger, 'debug' )
|
383
|
+
#
|
384
|
+
# This will register an instance of TraceTap, which wraps an logger object.
|
385
|
+
# On each +trace+ event the TraceTap#trace method will be called, which in
|
386
|
+
# turn will call the <tt>logger.debug</tt> method
|
387
|
+
#
|
388
|
+
# db.trace_tap = $stderr
|
389
|
+
#
|
390
|
+
# This will register the <tt>$stderr</tt> io stream as a trace tap. Every time a
|
391
|
+
# +trace+ event happens then <tt>$stderr.write( msg )</tt> will be called.
|
392
|
+
#
|
393
|
+
# db.trace_tap = nil
|
394
|
+
#
|
395
|
+
# This will unregister the trace tap
|
396
|
+
#
|
397
|
+
#
|
398
|
+
def trace_tap=( tap_obj )
|
399
|
+
|
400
|
+
# unregister any previous trace tap
|
401
|
+
#
|
402
|
+
if !@trace_tap.nil? then
|
403
|
+
@trace_tap.trace( 'unregistered as trace tap' )
|
404
|
+
@trace_tap = nil
|
405
|
+
end
|
406
|
+
return @trace_tap if tap_obj.nil?
|
407
|
+
|
408
|
+
# wrap the tap if we need to
|
409
|
+
#
|
410
|
+
if tap_obj.respond_to?( 'trace' ) then
|
411
|
+
@trace_tap = tap_obj
|
412
|
+
elsif tap_obj.respond_to?( 'write' ) then
|
413
|
+
@trace_tap = Amalgalite::TraceTap.new( tap_obj, 'write' )
|
414
|
+
else
|
415
|
+
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."
|
416
|
+
end
|
417
|
+
|
418
|
+
# and do the low level registration
|
419
|
+
#
|
420
|
+
@api.register_trace_tap( @trace_tap )
|
421
|
+
|
422
|
+
@trace_tap.trace( 'registered as trace tap' )
|
423
|
+
end
|
424
|
+
|
425
|
+
##
|
426
|
+
# call-seq:
|
427
|
+
# db.type_map = DefaultMap.new
|
428
|
+
#
|
429
|
+
# Assign your own TypeMap instance to do type conversions. The value
|
430
|
+
# assigned here must respond to +bind_type_of+ and +result_value_of+
|
431
|
+
# methods. See the TypeMap class for more details.
|
432
|
+
#
|
433
|
+
#
|
434
|
+
def type_map=( type_map_obj )
|
435
|
+
%w[ bind_type_of result_value_of ].each do |method|
|
436
|
+
unless type_map_obj.respond_to?( method )
|
437
|
+
raise Amalgalite::Error, "#{type_map_obj.class.name} cannot be used to do type mapping. It does not respond to '#{method}'"
|
438
|
+
end
|
439
|
+
end
|
440
|
+
@type_map = type_map_obj
|
441
|
+
end
|
442
|
+
|
443
|
+
##
|
444
|
+
# :call-seq:
|
445
|
+
# db.schema( dbname = "main" ) -> Schema
|
446
|
+
#
|
447
|
+
# Returns a Schema object containing the table and column structure of the
|
448
|
+
# database.
|
449
|
+
#
|
450
|
+
def schema( dbname = "main" )
|
451
|
+
@schema ||= ::Amalgalite::Schema.new( self, dbname )
|
452
|
+
if @schema and @schema.dirty?
|
453
|
+
reload_schema!( dbname )
|
454
|
+
end
|
455
|
+
return @schema
|
456
|
+
end
|
457
|
+
|
458
|
+
##
|
459
|
+
# :call-seq:
|
460
|
+
# db.reload_schema! -> Schema
|
461
|
+
#
|
462
|
+
# By default once the schema is obtained, it is cached. This is here to
|
463
|
+
# force the schema to be reloaded.
|
464
|
+
#
|
465
|
+
def reload_schema!( dbname = "main" )
|
466
|
+
@schema = nil
|
467
|
+
schema( dbname )
|
468
|
+
end
|
469
|
+
|
470
|
+
##
|
471
|
+
# Run a pragma command against the database
|
472
|
+
#
|
473
|
+
# Returns the result set of the pragma
|
474
|
+
def pragma( cmd, &block )
|
475
|
+
execute("PRAGMA #{cmd}", &block)
|
476
|
+
end
|
477
|
+
|
478
|
+
##
|
479
|
+
# Begin a transaction. The valid transaction types are:
|
480
|
+
#
|
481
|
+
# DEFERRED:: no read or write locks are created until the first
|
482
|
+
# statement is executed that requries a read or a write
|
483
|
+
# IMMEDIATE:: a readlock is obtained immediately so that no other process
|
484
|
+
# can write to the database
|
485
|
+
# EXCLUSIVE:: a read+write lock is obtained, no other proces can read or
|
486
|
+
# write to the database
|
487
|
+
#
|
488
|
+
# As a convenience, these are constants available in the
|
489
|
+
# Database::TransactionBehavior class.
|
490
|
+
#
|
491
|
+
# Amalgalite Transactions are database level transactions, just as SQLite's
|
492
|
+
# are.
|
493
|
+
#
|
494
|
+
# If a block is passed in, then when the block exits, it is guaranteed that
|
495
|
+
# either 'COMMIT' or 'ROLLBACK' has been executed.
|
496
|
+
#
|
497
|
+
# If any exception happens during the transaction that is caught by Amalgalite,
|
498
|
+
# then a 'ROLLBACK' is issued when the block closes.
|
499
|
+
#
|
500
|
+
# If no exception happens during the transaction then a 'COMMIT' is
|
501
|
+
# issued upon leaving the block.
|
502
|
+
#
|
503
|
+
# If no block is passed in then you are on your own.
|
504
|
+
#
|
505
|
+
# Nesting a transaaction via the _transaction_ method are no-ops.
|
506
|
+
# If you call transaction within a transaction, no new transaction is
|
507
|
+
# started, the current one is just continued.
|
508
|
+
#
|
509
|
+
# True nexted transactions are available through the _savepoint_ method.
|
510
|
+
#
|
511
|
+
def transaction( mode = TransactionBehavior::DEFERRED, &block )
|
512
|
+
raise Amalgalite::Error, "Invalid transaction behavior mode #{mode}" unless TransactionBehavior.valid?( mode )
|
513
|
+
|
514
|
+
# if already in a transaction, no need to start a new one.
|
515
|
+
if not in_transaction? then
|
516
|
+
execute( "BEGIN #{mode} TRANSACTION" )
|
517
|
+
end
|
518
|
+
|
519
|
+
if block_given? then
|
520
|
+
begin
|
521
|
+
previous_exception = $!
|
522
|
+
return ( yield self )
|
523
|
+
ensure
|
524
|
+
if $! and ($! != previous_exception) then
|
525
|
+
rollback
|
526
|
+
raise $!
|
527
|
+
else
|
528
|
+
commit
|
529
|
+
end
|
530
|
+
end
|
531
|
+
else
|
532
|
+
return in_transaction?
|
533
|
+
end
|
534
|
+
end
|
535
|
+
alias :deferred_transaction :transaction
|
536
|
+
|
537
|
+
# helper for an immediate transaction
|
538
|
+
def immediate_transaction( &block )
|
539
|
+
transaction( TransactionBehavior::IMMEDIATE, &block )
|
540
|
+
end
|
541
|
+
|
542
|
+
# helper for an exclusive transaction
|
543
|
+
def exclusive_transaction( &block )
|
544
|
+
transaction( TransactionBehavior::EXCLUSIVE, &block )
|
545
|
+
end
|
546
|
+
|
547
|
+
##
|
548
|
+
# call-seq:
|
549
|
+
# db.savepoint( 'mypoint' ) -> db
|
550
|
+
# db.savepoint( 'mypoint' ) do |db_in_savepoint|
|
551
|
+
# ...
|
552
|
+
# end
|
553
|
+
#
|
554
|
+
# Much of the following documentation is para-phrased from
|
555
|
+
# http://sqlite.org/lang_savepoint.html
|
556
|
+
#
|
557
|
+
# Savepoints are a method of creating transactions, similar to _transaction_
|
558
|
+
# except that they may be nested.
|
559
|
+
#
|
560
|
+
# * Every savepoint must have a name, +to_s+ is called on the method
|
561
|
+
# argument
|
562
|
+
# * A savepoint does not need to be initialized inside a _transaction_. If
|
563
|
+
# it is not inside a _transaction_ it behaves exactly as if a DEFERRED
|
564
|
+
# transaction had been started.
|
565
|
+
# * If a block is passed to _saveponit_ then when the block exists, it is
|
566
|
+
# guaranteed that either a 'RELEASE' or 'ROLLBACK TO name' has been executed.
|
567
|
+
# * If any exception happens during the savepoint transaction, then a
|
568
|
+
# 'ROLLOBACK TO' is issued when the block closes.
|
569
|
+
# * If no exception happens during the transaction then a 'RELEASE name' is
|
570
|
+
# issued upon leaving the block
|
571
|
+
#
|
572
|
+
# If no block is passed in then you are on your own.
|
573
|
+
#
|
574
|
+
def savepoint( name )
|
575
|
+
point_name = name.to_s.strip
|
576
|
+
raise Amalgalite::Error, "Invalid savepoint name '#{name}'" unless point_name and point_name.length > 1
|
577
|
+
execute( "SAVEPOINT #{point_name};")
|
578
|
+
if block_given? then
|
579
|
+
begin
|
580
|
+
return ( yield self )
|
581
|
+
ensure
|
582
|
+
if $! then
|
583
|
+
rollback_to( point_name )
|
584
|
+
raise $!
|
585
|
+
else
|
586
|
+
release( point_name )
|
587
|
+
end
|
588
|
+
end
|
589
|
+
else
|
590
|
+
return in_transaction?
|
591
|
+
end
|
592
|
+
end
|
593
|
+
|
594
|
+
##
|
595
|
+
# call-seq:
|
596
|
+
# db.release( 'mypoint' )
|
597
|
+
#
|
598
|
+
# Release a savepoint. This is similar to a _commit_ but only for
|
599
|
+
# savepoints. All savepoints up the savepoint stack and include the name
|
600
|
+
# savepoint being released are 'committed' to the transaction. There are
|
601
|
+
# several ways of thinking about release and they are all detailed in the
|
602
|
+
# sqlite documentation: http://sqlite.org/lang_savepoint.html
|
603
|
+
#
|
604
|
+
def release( point_name )
|
605
|
+
execute( "RELEASE SAVEPOINT #{point_name}" ) if in_transaction?
|
606
|
+
end
|
607
|
+
|
608
|
+
##
|
609
|
+
# call-seq:
|
610
|
+
# db.rollback_to( point_name )
|
611
|
+
#
|
612
|
+
# Rollback to a savepoint. The transaction is not cancelled, the
|
613
|
+
# transaction is restarted.
|
614
|
+
def rollback_to( point_name )
|
615
|
+
execute( "ROLLBACK TO SAVEPOINT #{point_name}" )
|
616
|
+
end
|
617
|
+
|
618
|
+
##
|
619
|
+
# Commit a transaction
|
620
|
+
#
|
621
|
+
def commit
|
622
|
+
execute( "COMMIT TRANSACTION" ) if in_transaction?
|
623
|
+
end
|
624
|
+
|
625
|
+
##
|
626
|
+
# Rollback a transaction
|
627
|
+
#
|
628
|
+
def rollback
|
629
|
+
execute( "ROLLBACK TRANSACTION" ) if in_transaction?
|
630
|
+
end
|
631
|
+
|
632
|
+
##
|
633
|
+
# call-seq:
|
634
|
+
# db.define_function( "name", MyDBFunction.new )
|
635
|
+
# db.define_function( "my_func", callable )
|
636
|
+
# db.define_function( "my_func" ) do |x,y|
|
637
|
+
# ....
|
638
|
+
# return result
|
639
|
+
# end
|
640
|
+
#
|
641
|
+
# register a callback to be exposed as an SQL function. There are multiple
|
642
|
+
# ways to register this function:
|
643
|
+
#
|
644
|
+
# 1. db.define_function( "name" ) { |a| ... }
|
645
|
+
# * pass +define_function+ a _name_ and a block.
|
646
|
+
# * The SQL function _name_ taking _arity_ parameters will be registered,
|
647
|
+
# where _arity_ is the _arity_ of the block.
|
648
|
+
# * The return value of the block is the return value of the registred
|
649
|
+
# SQL function
|
650
|
+
# 2. db.define_function( "name", callable )
|
651
|
+
# * pass +function+ a _name_ and something that <tt>responds_to?( :to_proc )</tt>
|
652
|
+
# * The SQL function _name_ is registered taking _arity_ parameters is
|
653
|
+
# registered where _arity_ is the _arity_ of +callable.to_proc.call+
|
654
|
+
# * The return value of the +callable.to_proc.call+ is the return value
|
655
|
+
# of the SQL function
|
656
|
+
#
|
657
|
+
# See also ::Amalgalite::Function
|
658
|
+
#
|
659
|
+
def define_function( name, callable = nil, &block )
|
660
|
+
p = ( callable || block ).to_proc
|
661
|
+
raise FunctionError, "Use only mandatory or arbitrary parameters in an SQL Function, not both" if p.arity < -1
|
662
|
+
db_function = ::Amalgalite::SQLite3::Database::Function.new( name, p )
|
663
|
+
@api.define_function( db_function.name, db_function )
|
664
|
+
@functions[db_function.signature] = db_function
|
665
|
+
nil
|
666
|
+
end
|
667
|
+
alias :function :define_function
|
668
|
+
|
669
|
+
##
|
670
|
+
# call-seq:
|
671
|
+
# db.remove_function( 'name', MyScalerFunctor.new )
|
672
|
+
# db.remove_function( 'name', callable )
|
673
|
+
# db.remove_function( 'name', arity )
|
674
|
+
# db.remove_function( 'name' )
|
675
|
+
#
|
676
|
+
# Remove a function from use in the database. Since the same function may
|
677
|
+
# be registered more than once with different arity, you may specify the
|
678
|
+
# arity, or the function object, or nil. If nil is used for the arity, then
|
679
|
+
# Amalgalite does its best to remove all functions of given name.
|
680
|
+
#
|
681
|
+
def remove_function( name, callable_or_arity = nil )
|
682
|
+
arity = nil
|
683
|
+
if callable_or_arity.respond_to?( :to_proc ) then
|
684
|
+
arity = callable_or_arity.to_proc.arity
|
685
|
+
elsif callable_or_arity.respond_to?( :to_int ) then
|
686
|
+
arity = callable_or_arity.to_int
|
687
|
+
end
|
688
|
+
to_remove = []
|
689
|
+
|
690
|
+
if arity then
|
691
|
+
signature = ::Amalgalite::SQLite3::Database::Function.signature( name, arity )
|
692
|
+
db_function = @functions[ signature ]
|
693
|
+
raise FunctionError, "db function '#{name}' with arity #{arity} does not appear to be defined" unless db_function
|
694
|
+
to_remove << db_function
|
695
|
+
else
|
696
|
+
possibles = @functions.values.select { |f| f.name == name }
|
697
|
+
raise FunctionError, "no db function '#{name}' appears to be defined" if possibles.empty?
|
698
|
+
to_remove = possibles
|
699
|
+
end
|
700
|
+
|
701
|
+
to_remove.each do |db_func|
|
702
|
+
@api.remove_function( db_func.name, db_func )
|
703
|
+
@functions.delete( db_func.signature )
|
704
|
+
end
|
705
|
+
end
|
706
|
+
|
707
|
+
##
|
708
|
+
# call-seq:
|
709
|
+
# db.define_aggregate( 'name', MyAggregateClass )
|
710
|
+
#
|
711
|
+
# Define an SQL aggregate function, these are functions like max(), min(),
|
712
|
+
# avg(), etc. SQL functions that would be used when a GROUP BY clause is in
|
713
|
+
# effect. See also ::Amalgalite::Aggregate.
|
714
|
+
#
|
715
|
+
# A new instance of MyAggregateClass is created for each instance that the
|
716
|
+
# SQL aggregate is mentioned in SQL.
|
717
|
+
#
|
718
|
+
def define_aggregate( name, klass )
|
719
|
+
db_aggregate = klass
|
720
|
+
a = klass.new
|
721
|
+
raise AggregateError, "Use only mandatory or arbitrary parameters in an SQL Aggregate, not both" if a.arity < -1
|
722
|
+
raise AggregateError, "Aggregate implementation name '#{a.name}' does not match defined name '#{name}'" if a.name != name
|
723
|
+
@api.define_aggregate( name, a.arity, klass )
|
724
|
+
@aggregates[a.signature] = db_aggregate
|
725
|
+
nil
|
726
|
+
end
|
727
|
+
alias :aggregate :define_aggregate
|
728
|
+
|
729
|
+
##
|
730
|
+
# call-seq:
|
731
|
+
# db.remove_aggregate( 'name', MyAggregateClass )
|
732
|
+
# db.remove_aggregate( 'name' )
|
733
|
+
#
|
734
|
+
# Remove an aggregate from use in the database. Since the same aggregate
|
735
|
+
# may be refistered more than once with different arity, you may specify the
|
736
|
+
# arity, or the aggregate class, or nil. If nil is used for the arity then
|
737
|
+
# Amalgalite does its best to remove all aggregates of the given name
|
738
|
+
#
|
739
|
+
def remove_aggregate( name, klass_or_arity = nil )
|
740
|
+
klass = nil
|
741
|
+
case klass_or_arity
|
742
|
+
when Integer
|
743
|
+
arity = klass_or_arity
|
744
|
+
when NilClass
|
745
|
+
arity = nil
|
746
|
+
else
|
747
|
+
klass = klass_or_arity
|
748
|
+
arity = klass.new.arity
|
749
|
+
end
|
750
|
+
to_remove = []
|
751
|
+
if arity then
|
752
|
+
signature = ::Amalgalite::SQLite3::Database::Function.signature( name, arity )
|
753
|
+
db_aggregate = @aggregates[ signature ]
|
754
|
+
raise AggregateError, "db aggregate '#{name}' with arity #{arity} does not appear to be defined" unless db_aggregate
|
755
|
+
to_remove << db_aggregate
|
756
|
+
else
|
757
|
+
possibles = @aggregates.values.select { |a| a.new.name == name }
|
758
|
+
raise AggregateError, "no db aggregate '#{name}' appears to be defined" if possibles.empty?
|
759
|
+
to_remove = possibles
|
760
|
+
end
|
761
|
+
|
762
|
+
to_remove.each do |db_agg|
|
763
|
+
i = db_agg.new
|
764
|
+
@api.remove_aggregate( i.name, i.arity, db_agg)
|
765
|
+
@aggregates.delete( i.signature )
|
766
|
+
end
|
767
|
+
end
|
768
|
+
|
769
|
+
##
|
770
|
+
# call-seq:
|
771
|
+
# db.busy_handler( callable )
|
772
|
+
# db.define_busy_handler do |count|
|
773
|
+
# end
|
774
|
+
# db.busy_handler( Amalgalite::BusyTimeout.new( 30 ) )
|
775
|
+
#
|
776
|
+
# Register a busy handler for this database connection, the handler MUST
|
777
|
+
# follow the +to_proc+ protocol indicating that is will
|
778
|
+
# +respond_to?(:call)+. This is intrinsic to lambdas and blocks so
|
779
|
+
# those will work automatically.
|
780
|
+
#
|
781
|
+
# This exposes the sqlite busy handler api to ruby.
|
782
|
+
#
|
783
|
+
# * http://sqlite.org/c3ref/busy_handler.html
|
784
|
+
#
|
785
|
+
# The busy handler's _call(N)_ method may be invoked whenever an attempt is
|
786
|
+
# made to open a database table that another thread or process has locked.
|
787
|
+
# +N+ will be the number of times the _call(N)_ method has been invoked
|
788
|
+
# during this locking event.
|
789
|
+
#
|
790
|
+
# The handler may or maynot be called based upon what SQLite determins.
|
791
|
+
#
|
792
|
+
# If the handler returns _nil_ or _false_ then no more busy handler calls will
|
793
|
+
# be made in this lock event and you are probably going to see an
|
794
|
+
# SQLite::Error in your immediately future in another process or in another
|
795
|
+
# piece of code.
|
796
|
+
#
|
797
|
+
# If the handler returns non-nil or non-false then another attempt will be
|
798
|
+
# made to obtain the lock, lather, rinse, repeat.
|
799
|
+
#
|
800
|
+
# If an Exception happens in a busy handler, it will be the same as if the
|
801
|
+
# busy handler had returned _nil_ or _false_. The exception itself will not
|
802
|
+
# be propogated further.
|
803
|
+
#
|
804
|
+
def define_busy_handler( callable = nil, &block )
|
805
|
+
handler = ( callable || block ).to_proc
|
806
|
+
a = handler.arity
|
807
|
+
raise BusyHandlerError, "A busy handler expects 1 and only 1 argument, not #{a}" if a != 1
|
808
|
+
@api.busy_handler( handler )
|
809
|
+
end
|
810
|
+
alias :busy_handler :define_busy_handler
|
811
|
+
|
812
|
+
##
|
813
|
+
# call-seq:
|
814
|
+
# db.remove_busy_handler
|
815
|
+
#
|
816
|
+
# Remove the busy handler for this database connection.
|
817
|
+
def remove_busy_handler
|
818
|
+
@api.busy_handler( nil )
|
819
|
+
end
|
820
|
+
|
821
|
+
##
|
822
|
+
# call-seq:
|
823
|
+
# db.interrupt!
|
824
|
+
#
|
825
|
+
# Cause another thread with a handle on this database to be interrupted and
|
826
|
+
# return at the earliest opportunity as interrupted. It is not safe to call
|
827
|
+
# this method if the database might be closed before interrupt! returns.
|
828
|
+
#
|
829
|
+
def interrupt!
|
830
|
+
@api.interrupt!
|
831
|
+
end
|
832
|
+
|
833
|
+
##
|
834
|
+
# call-seq:
|
835
|
+
# db.progress_handler( 50, MyProgressHandler.new )
|
836
|
+
# db.progress_handler( 25 , callable )
|
837
|
+
# db.progress_handler do
|
838
|
+
# ....
|
839
|
+
# return result
|
840
|
+
# end
|
841
|
+
#
|
842
|
+
# Register a progress handler for this database connection, the handler MUST
|
843
|
+
# follow the +to_proc+ protocol indicating that is will
|
844
|
+
# +respond_to?(:call)+. This is intrinsic to lambdas and blocks so
|
845
|
+
# those will work automatically.
|
846
|
+
#
|
847
|
+
# This exposes the sqlite progress handler api to ruby.
|
848
|
+
#
|
849
|
+
# * http://sqlite.org/c3ref/progress_handler.html
|
850
|
+
#
|
851
|
+
# The progress handler's _call()_ method may be invoked ever N SQLite op
|
852
|
+
# codes. If the progress handler returns anything that can evaluate to
|
853
|
+
# +true+ then current running sqlite statement is terminated at the earliest
|
854
|
+
# oppportunity.
|
855
|
+
#
|
856
|
+
# You can use this to be notified that a thread is still processingn a
|
857
|
+
# request.
|
858
|
+
#
|
859
|
+
def define_progress_handler( op_code_count = 25, callable = nil, &block )
|
860
|
+
handler = ( callable || block ).to_proc
|
861
|
+
a = handler.arity
|
862
|
+
raise ProgressHandlerError, "A progress handler expects 0 arguments, not #{a}" if a != 0
|
863
|
+
@api.progress_handler( op_code_count, handler )
|
864
|
+
end
|
865
|
+
alias :progress_handler :define_progress_handler
|
866
|
+
|
867
|
+
##
|
868
|
+
# call-seq:
|
869
|
+
# db.remove_progress_handler
|
870
|
+
#
|
871
|
+
# Remove the progress handler for this database connection.
|
872
|
+
def remove_progress_handler
|
873
|
+
@api.progress_handler( nil, nil )
|
874
|
+
end
|
875
|
+
|
876
|
+
##
|
877
|
+
# call-seq:
|
878
|
+
# db.replicate_to( ":memory:" ) -> new_db
|
879
|
+
# db.replicate_to( "/some/location/my.db" ) -> new_db
|
880
|
+
# db.replicate_to( Amalgalite::Database.new( "/my/backup.db" ) ) -> new_db
|
881
|
+
#
|
882
|
+
# replicate_to() takes a single argument, either a String or an
|
883
|
+
# Amalgalite::Database. It returns the replicated database object. If
|
884
|
+
# given a String, it will truncate that database if it already exists.
|
885
|
+
#
|
886
|
+
# Replicate the current database to another location, this can be used for a
|
887
|
+
# number of purposes:
|
888
|
+
#
|
889
|
+
# * load an sqlite database from disk into memory
|
890
|
+
# * snaphost an in memory db and save it to disk
|
891
|
+
# * backup on sqlite database to another location
|
892
|
+
#
|
893
|
+
def replicate_to( location )
|
894
|
+
to_db = nil
|
895
|
+
case location
|
896
|
+
when String
|
897
|
+
to_db = Amalgalite::Database.new( location )
|
898
|
+
when Amalgalite::Database
|
899
|
+
to_db = location
|
900
|
+
else
|
901
|
+
raise ArgumentError, "replicate_to( #{location} ) must be a String or a Database"
|
902
|
+
end
|
903
|
+
|
904
|
+
@api.replicate_to( to_db.api )
|
905
|
+
return to_db
|
906
|
+
end
|
907
|
+
|
908
|
+
##
|
909
|
+
# call-seq:
|
910
|
+
# db.import_csv_to_table( "/some/location/data.csv", "my_table" )
|
911
|
+
# db.import_csv_to_table( "countries.csv", "countries", :col_sep => "|", :headers => %w[ name two_letter id ] )
|
912
|
+
#
|
913
|
+
#
|
914
|
+
# import_csv_to_table() takes 2 required arguments, and a hash of options. The
|
915
|
+
# first argument is the path to a CSV, the second is the table in which
|
916
|
+
# to load the data. The options has is a subset of those used by CSV
|
917
|
+
#
|
918
|
+
# * :col_sep - the string placed between each field. Default is ","
|
919
|
+
# * :row_sep - the String appended to the end of each row. Default is :auto
|
920
|
+
# * :quote_char - The character used to quote fields. Default '"'
|
921
|
+
# * :headers - set to true or :first_row if there are headers in this CSV. Default is false.
|
922
|
+
# This may also be an Array. If that is the case then the
|
923
|
+
# array is used as the fields in the CSV and the fields in the
|
924
|
+
# table in which to insert. If this is set to an Array, it is
|
925
|
+
# assumed that all rows in the csv will be inserted.
|
926
|
+
#
|
927
|
+
def import_csv_to_table( csv_path, table_name, options = {} )
|
928
|
+
importer = CSVTableImporter.new( csv_path, self, table_name, options )
|
929
|
+
importer.run
|
930
|
+
end
|
931
|
+
end
|
932
|
+
end
|
933
|
+
|