amalgalite 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/HISTORY +18 -1
- data/README +12 -4
- data/ext/amalgalite3.c +1 -0
- data/ext/amalgalite3.h +25 -1
- data/ext/amalgalite3_blob.c +238 -0
- data/ext/amalgalite3_database.c +39 -0
- data/ext/amalgalite3_statement.c +85 -2
- data/gemspec.rb +1 -0
- data/lib/amalgalite.rb +1 -0
- data/lib/amalgalite/blob.rb +169 -4
- data/lib/amalgalite/column.rb +5 -2
- data/lib/amalgalite/schema.rb +5 -1
- data/lib/amalgalite/statement.rb +107 -20
- data/lib/amalgalite/table.rb +3 -1
- data/lib/amalgalite/taps.rb +2 -0
- data/lib/amalgalite/type_maps/default_map.rb +15 -1
- data/lib/amalgalite/version.rb +1 -1
- data/lib/amalgalite/view.rb +2 -0
- data/spec/blob_spec.rb +81 -0
- data/spec/default_map_spec.rb +3 -2
- data/spec/statement_spec.rb +18 -0
- data/spec/storage_map_spec.rb +1 -1
- metadata +17 -3
data/gemspec.rb
CHANGED
@@ -23,6 +23,7 @@ Amalgalite::GEM_SPEC = Gem::Specification.new do |spec|
|
|
23
23
|
# spec.add_dependency("rake", ">= 0.8.1")
|
24
24
|
spec.add_dependency("configuration", ">= 0.0.5")
|
25
25
|
spec.add_dependency("arrayfields", ">= 4.5.0")
|
26
|
+
spec.add_dependency("mkrf", ">= 0.2.3")
|
26
27
|
|
27
28
|
|
28
29
|
if ext_conf = Configuration.for_if_exist?("extension") then
|
data/lib/amalgalite.rb
CHANGED
data/lib/amalgalite/blob.rb
CHANGED
@@ -4,11 +4,176 @@
|
|
4
4
|
#++
|
5
5
|
module Amalgalite
|
6
6
|
##
|
7
|
-
#
|
8
|
-
#
|
7
|
+
# This is the interface to allow Blob objects to be written to and read from
|
8
|
+
# the SQLite database. When using statements, use a Blob object as
|
9
|
+
# the wrapper around the source to be written to the row, and a Blob object is
|
10
|
+
# returned if the the type mapping warrents during select queries.
|
9
11
|
#
|
10
|
-
#
|
12
|
+
# For instance during an insert:
|
11
13
|
#
|
12
|
-
|
14
|
+
# blob_column = db.schema.tables['blobs'].columsn['data']
|
15
|
+
# db.execute("INSERT INTO blobs(name, data) VALUES ( $name, $blob )",
|
16
|
+
# { "$name" => "/path/to/file",
|
17
|
+
# "$blob" => Amalgalite::Blob.new( :file => '/path/to/file',
|
18
|
+
# :column => blob_column) } )
|
19
|
+
#
|
20
|
+
# db.execute("INSERT INTO blobs(id, data) VALUES ($id, $blob )",
|
21
|
+
# { "$name" => 'blobname',
|
22
|
+
# "$blob" => Amalgalite::Blob.new( :io => "something with .read and .length methods",
|
23
|
+
# :column => blob_column) } )
|
24
|
+
#
|
25
|
+
# On select the blob data needs to be read into an IO object
|
26
|
+
#
|
27
|
+
# all_rows = db.execute("SELECT name, blob FROM blobs WHERE name = '/path/to/file' ")
|
28
|
+
# blob_row = all_rows.first
|
29
|
+
# blob_row['blob'].write_to_file( blob_row['name'] )
|
30
|
+
#
|
31
|
+
# Or write to an IO object
|
32
|
+
#
|
33
|
+
# blob_results = {}
|
34
|
+
# db.execute("SELECT name, blob FROM blobs") do |row|
|
35
|
+
# io = StringIO.new
|
36
|
+
# row['blob'].write_to_io( io )
|
37
|
+
# blob_results[row['name']] = io
|
38
|
+
# # or use a shortcut
|
39
|
+
# # blob_results[row['name']] = row['blob'].to_string_io
|
40
|
+
# end
|
41
|
+
#
|
42
|
+
# If using a Blob as a conditional, for instance in a WHERE clause then the
|
43
|
+
# Blob must resolvable to a String.
|
44
|
+
#
|
45
|
+
# db.execute("SELECT FROM blobs(name, data) WHERE data = $blob",
|
46
|
+
# { "$blob' => Amalgalite::Blob.new( :string => "A string of data" ) })
|
47
|
+
#
|
48
|
+
class Blob
|
49
|
+
class Error < ::Amalgalite::Error; end
|
50
|
+
class << self
|
51
|
+
def valid_source_params
|
52
|
+
@valid_source_params ||= [ :file, :io, :string, :db_blob ]
|
53
|
+
end
|
54
|
+
|
55
|
+
def default_block_size
|
56
|
+
@default_block_size ||= 8192
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# the object representing the source of the blob
|
61
|
+
attr_reader :source
|
62
|
+
|
63
|
+
# the size in bytes of the of the blob
|
64
|
+
attr_reader :length
|
65
|
+
|
66
|
+
# the size in bytes of the blocks of data to move from the source
|
67
|
+
attr_reader :block_size
|
68
|
+
|
69
|
+
# the column the blob is associated with
|
70
|
+
attr_reader :column
|
71
|
+
|
72
|
+
##
|
73
|
+
# Initialize a new blob, it takes a single parameter, a hash which describes
|
74
|
+
# the source of the blob. The keys of the hash are one of:
|
75
|
+
#
|
76
|
+
# :file : the value is the path to a file on the file system
|
77
|
+
# :io : the value is an object that responds to the the methods +read+
|
78
|
+
# and +length+. +read+ should have the behavior of IO#read
|
79
|
+
# :db_blob : not normally used by an end user, used to initialize a blob
|
80
|
+
# object that is returned from an SQL query.
|
81
|
+
# :string : used when a Blob is part of a WHERE clause
|
82
|
+
#
|
83
|
+
# And additional key of :block_size may be used to indicate the maximum size
|
84
|
+
# of a single block of data to move from the source to the destination, this
|
85
|
+
# defaults ot 8192.
|
86
|
+
#
|
87
|
+
def initialize( params )
|
88
|
+
if (Blob.valid_source_params & params.keys).size > 1 then
|
89
|
+
raise Blob::Error, "Only a one of #{Blob.valid_source_params.join(', ')} is allowed to initialize a Blob. #{params.keys.join(', ')} were sent"
|
90
|
+
end
|
91
|
+
|
92
|
+
@source = nil
|
93
|
+
@source_length = 0
|
94
|
+
@close_source_after_read = false
|
95
|
+
@incremental = true
|
96
|
+
@block_size = params[:block_size] || Blob.default_block_size
|
97
|
+
@column = params[:column]
|
98
|
+
|
99
|
+
raise Blob::Error, "A :column parameter is required for a Blob" unless @column
|
100
|
+
|
101
|
+
if params.has_key?( :file ) then
|
102
|
+
@source = File.open( params[:file], "r" )
|
103
|
+
@length = File.size( params[:file] )
|
104
|
+
@close_source_after_read = true
|
105
|
+
elsif params.has_key?( :io ) then
|
106
|
+
@source = params[:io]
|
107
|
+
@length = @source.length
|
108
|
+
elsif params.has_key?( :db_blob ) then
|
109
|
+
@source = params[:db_blob]
|
110
|
+
@length = @source.length
|
111
|
+
@close_source_after_read = true
|
112
|
+
elsif params.has_key?( :string ) then
|
113
|
+
@source = params[:string]
|
114
|
+
@length = @source.length
|
115
|
+
@incremental = false
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
##
|
120
|
+
# close the source when done reading from it
|
121
|
+
#
|
122
|
+
def close_source_after_read?
|
123
|
+
@close_source_after_read
|
124
|
+
end
|
125
|
+
|
126
|
+
##
|
127
|
+
# is this an incremental Blob or not
|
128
|
+
#
|
129
|
+
def incremental?
|
130
|
+
@incremental
|
131
|
+
end
|
132
|
+
|
133
|
+
##
|
134
|
+
# Write the Blob to an IO object
|
135
|
+
#
|
136
|
+
def write_to_io( io )
|
137
|
+
if source.respond_to?( :read ) then
|
138
|
+
while buf = source.read( block_size ) do
|
139
|
+
io.write( buf )
|
140
|
+
end
|
141
|
+
else
|
142
|
+
io.write( source.to_s )
|
143
|
+
end
|
144
|
+
|
145
|
+
if close_source_after_read? then
|
146
|
+
source.close
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
##
|
151
|
+
# write the Blob contents to a StringIO
|
152
|
+
#
|
153
|
+
def to_string_io
|
154
|
+
sio = StringIO.new
|
155
|
+
write_to_io( sio )
|
156
|
+
return sio
|
157
|
+
end
|
158
|
+
|
159
|
+
##
|
160
|
+
# Write the Blob contents to a File.
|
161
|
+
#
|
162
|
+
def write_to_file( filename, modestring="w" )
|
163
|
+
File.open(filename, modestring) do |f|
|
164
|
+
write_to_io( f )
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
##
|
169
|
+
# Write the Blob contents to the column. This assumes that the row_id to
|
170
|
+
# insert into is the last row that was inserted into the db
|
171
|
+
#
|
172
|
+
def write_to_column!
|
173
|
+
last_rowid = column.schema.db.last_insert_rowid
|
174
|
+
SQLite3::Blob.new( column.schema.db.api, column.db, column.table, column.name, last_rowid, "w" ) do |sqlite_blob|
|
175
|
+
write_to_io( sqlite_blob )
|
176
|
+
end
|
177
|
+
end
|
13
178
|
end
|
14
179
|
end
|
data/lib/amalgalite/column.rb
CHANGED
@@ -13,7 +13,10 @@ module Amalgalite
|
|
13
13
|
# information from a SELECT query.
|
14
14
|
#
|
15
15
|
class Column
|
16
|
-
# the
|
16
|
+
# the schema object this column is associated with
|
17
|
+
attr_accessor :schema
|
18
|
+
|
19
|
+
# the database name this column belongs to
|
17
20
|
attr_accessor :db
|
18
21
|
|
19
22
|
# the column name
|
@@ -47,7 +50,7 @@ module Amalgalite
|
|
47
50
|
##
|
48
51
|
# Create a column with its name and associated table
|
49
52
|
#
|
50
|
-
def initialize( db,
|
53
|
+
def initialize( db, table, name)
|
51
54
|
@db = db
|
52
55
|
@name = name
|
53
56
|
@table = table
|
data/lib/amalgalite/schema.rb
CHANGED
@@ -19,6 +19,7 @@ module Amalgalite
|
|
19
19
|
attr_reader :schema
|
20
20
|
attr_reader :tables
|
21
21
|
attr_reader :views
|
22
|
+
attr_reader :db
|
22
23
|
|
23
24
|
#
|
24
25
|
# Create a new instance of Schema
|
@@ -46,6 +47,7 @@ module Amalgalite
|
|
46
47
|
@db.execute("SELECT tbl_name, sql FROM sqlite_master WHERE type = 'table'") do |table_info|
|
47
48
|
table = Amalgalite::Table.new( table_info['tbl_name'], table_info['sql'] )
|
48
49
|
table.columns = load_columns( table )
|
50
|
+
table.schema = self
|
49
51
|
|
50
52
|
@db.prepare("SELECT name, sql FROM sqlite_master WHERE type ='index' and tbl_name = @name") do |idx_stmt|
|
51
53
|
idx_stmt.execute( "@name" => table.name) do |idx_info|
|
@@ -64,12 +66,13 @@ module Amalgalite
|
|
64
66
|
def load_columns( table )
|
65
67
|
cols = {}
|
66
68
|
@db.execute("PRAGMA table_info(#{table.name})") do |row|
|
67
|
-
col = Amalgalite::Column.new( "main", row['name']
|
69
|
+
col = Amalgalite::Column.new( "main", table.name, row['name'] )
|
68
70
|
|
69
71
|
col.default_value = row['dflt_value']
|
70
72
|
@db.api.table_column_metadata( "main", table.name, col.name ).each_pair do |key, value|
|
71
73
|
col.send("#{key}=", value)
|
72
74
|
end
|
75
|
+
col.schema = self
|
73
76
|
cols[col.name] = col
|
74
77
|
end
|
75
78
|
cols
|
@@ -82,6 +85,7 @@ module Amalgalite
|
|
82
85
|
@views = {}
|
83
86
|
@db.execute("SELECT name, sql FROM sqlite_master WHERE type = 'view'") do |view_info|
|
84
87
|
view = Amalgalite::View.new( view_info['name'], view_info['sql'] )
|
88
|
+
view.schema = self
|
85
89
|
@views[view.name] = view
|
86
90
|
end
|
87
91
|
@views
|
data/lib/amalgalite/statement.rb
CHANGED
@@ -17,14 +17,30 @@ module Amalgalite
|
|
17
17
|
attr_reader :sql
|
18
18
|
attr_reader :api
|
19
19
|
|
20
|
+
class << self
|
21
|
+
# special column names that indicate that indicate the column is a rowid
|
22
|
+
def rowid_column_names
|
23
|
+
@rowid_column_names ||= %w[ ROWID OID _ROWID_ ]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
20
27
|
##
|
21
28
|
# Initialize a new statement on the database.
|
22
29
|
#
|
23
30
|
def initialize( db, sql )
|
24
31
|
@db = db
|
25
|
-
prepare_method
|
32
|
+
prepare_method = @db.utf16? ? :prepare16 : :prepare
|
26
33
|
@param_positions = {}
|
27
|
-
@stmt_api
|
34
|
+
@stmt_api = @db.api.send( prepare_method, sql )
|
35
|
+
@blobs_to_write = []
|
36
|
+
@rowid_index = nil
|
37
|
+
end
|
38
|
+
|
39
|
+
##
|
40
|
+
# Is the special column "ROWID", "OID", or "_ROWID_" used?
|
41
|
+
#
|
42
|
+
def using_rowid_column?
|
43
|
+
not @rowid_index.nil?
|
28
44
|
end
|
29
45
|
|
30
46
|
##
|
@@ -34,8 +50,9 @@ module Amalgalite
|
|
34
50
|
#
|
35
51
|
def reset!
|
36
52
|
@stmt_api.reset!
|
37
|
-
@column_names = nil
|
38
53
|
@param_positions = {}
|
54
|
+
@blobs_to_write.clear
|
55
|
+
@rowid_index = nil
|
39
56
|
end
|
40
57
|
|
41
58
|
##
|
@@ -47,20 +64,39 @@ module Amalgalite
|
|
47
64
|
@stmt_api.clear_bindings!
|
48
65
|
end
|
49
66
|
|
67
|
+
##
|
68
|
+
# reset the statment in preparation for executing it again
|
69
|
+
#
|
70
|
+
def reset_for_next_execute!
|
71
|
+
@stmt_api.reset!
|
72
|
+
@stmt_api.clear_bindings!
|
73
|
+
@blobs_to_write.clear
|
74
|
+
end
|
75
|
+
|
50
76
|
##
|
51
77
|
# Execute the statement with the given parameters
|
52
78
|
#
|
53
79
|
# If a block is given, then yield each returned row to the block. If no
|
54
|
-
# block is given then return all rows from the result
|
80
|
+
# block is given then return all rows from the result. No matter what the
|
81
|
+
# prepared statement should be reset before returning the final time.
|
55
82
|
#
|
56
83
|
def execute( *params )
|
57
84
|
bind( *params )
|
58
|
-
|
59
|
-
|
60
|
-
|
85
|
+
begin
|
86
|
+
if block_given? then
|
87
|
+
while row = next_row
|
88
|
+
yield row
|
89
|
+
end
|
90
|
+
else
|
91
|
+
all_rows
|
61
92
|
end
|
62
|
-
|
63
|
-
|
93
|
+
ensure
|
94
|
+
s = $!
|
95
|
+
begin
|
96
|
+
reset_for_next_execute!
|
97
|
+
rescue => e
|
98
|
+
end
|
99
|
+
raise s if s
|
64
100
|
end
|
65
101
|
end
|
66
102
|
|
@@ -110,11 +146,11 @@ module Amalgalite
|
|
110
146
|
check_parameter_count!( params.size )
|
111
147
|
params.each_pair do | param, value |
|
112
148
|
position = param_position_of( param )
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
149
|
+
if position > 0 then
|
150
|
+
bind_parameter_to( position, value )
|
151
|
+
else
|
152
|
+
raise Amalgalite::Error, "Unable to find parameter '#{param}' in SQL statement [#{sql}]"
|
153
|
+
end
|
118
154
|
end
|
119
155
|
end
|
120
156
|
|
@@ -144,12 +180,17 @@ module Amalgalite
|
|
144
180
|
when DataType::TEXT
|
145
181
|
@stmt_api.bind_text( position, value.to_s )
|
146
182
|
when DataType::BLOB
|
147
|
-
|
183
|
+
if value.incremental? then
|
184
|
+
@stmt_api.bind_zeroblob( position, value.length )
|
185
|
+
@blobs_to_write << value
|
186
|
+
else
|
187
|
+
@stmt_api.bind_blob( position, value.source )
|
188
|
+
end
|
148
189
|
else
|
149
190
|
raise ::Amalgalite::Error, "Unknown binding type of #{bind_type} from #{db.type_map.class.name}.bind_type_of"
|
150
191
|
end
|
151
192
|
end
|
152
|
-
|
193
|
+
|
153
194
|
|
154
195
|
##
|
155
196
|
# Find and cache the binding parameter indexes
|
@@ -174,6 +215,17 @@ module Amalgalite
|
|
174
215
|
return expected
|
175
216
|
end
|
176
217
|
|
218
|
+
##
|
219
|
+
# Write any blobs that have been bound to parameters to the database. This
|
220
|
+
# assumes that the blobs go into the last inserted row
|
221
|
+
#
|
222
|
+
def write_blobs
|
223
|
+
unless @blobs_to_write.empty?
|
224
|
+
@blobs_to_write.each do |blob|
|
225
|
+
blob.write_to_column!
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
177
229
|
|
178
230
|
##
|
179
231
|
# Iterate over the results of the statement returning each row of results
|
@@ -208,7 +260,19 @@ module Amalgalite
|
|
208
260
|
when DataType::NULL
|
209
261
|
value = nil
|
210
262
|
when DataType::BLOB
|
211
|
-
|
263
|
+
# if the rowid column is encountered, then we can use an incremental
|
264
|
+
# blob api, otherwise we have to use the all at once version.
|
265
|
+
if using_rowid_column? then
|
266
|
+
value = Amalgalite::Blob.new( :db_blob => SQLite3::Blob.new( db.api,
|
267
|
+
col.schema.db,
|
268
|
+
col.schema.table,
|
269
|
+
col.schema.name,
|
270
|
+
@stmt_api.column_int64( @rowid_index ),
|
271
|
+
"r"),
|
272
|
+
:column => col.schema)
|
273
|
+
else
|
274
|
+
value = Amalgalite::Blob.new( :string => @stmt_api.column_blob( idx ), :column => col.schema )
|
275
|
+
end
|
212
276
|
else
|
213
277
|
raise ::Amalgalite::Error, "BUG! : Unknown SQLite column type of #{column_type}"
|
214
278
|
end
|
@@ -218,9 +282,10 @@ module Amalgalite
|
|
218
282
|
row.fields = result_fields
|
219
283
|
when ResultCode::DONE
|
220
284
|
row = nil
|
285
|
+
write_blobs
|
221
286
|
else
|
222
287
|
raise Amalgalite::SQLite3::Error,
|
223
|
-
"
|
288
|
+
"SQLITE ERROR #{rc} (#{Amalgalite::SQLite3::Constants::ResultCode.from_int( rc )}) : #{@db.api.last_error_message}"
|
224
289
|
end
|
225
290
|
return row
|
226
291
|
end
|
@@ -245,13 +310,17 @@ module Amalgalite
|
|
245
310
|
# The full meta information from teh origin column is also obtained for help
|
246
311
|
# in doing type conversion.
|
247
312
|
#
|
313
|
+
# As iteration over the row meta informatio happens, record if the special
|
314
|
+
# "ROWID", "OID", or "_ROWID_" column is encountered. If that column is
|
315
|
+
# encountered then we make note of it.
|
316
|
+
#
|
248
317
|
def result_meta
|
249
318
|
unless @result_meta
|
250
319
|
meta = []
|
251
320
|
column_count.times do |idx|
|
252
321
|
column_meta = ::OpenStruct.new
|
253
322
|
column_meta.name = @stmt_api.column_name( idx )
|
254
|
-
|
323
|
+
|
255
324
|
db_name = @stmt_api.column_database_name( idx )
|
256
325
|
tbl_name = @stmt_api.column_table_name( idx )
|
257
326
|
col_name = @stmt_api.column_origin_name( idx )
|
@@ -259,13 +328,31 @@ module Amalgalite
|
|
259
328
|
column_meta.schema = ::Amalgalite::Column.new( db_name, tbl_name, col_name )
|
260
329
|
column_meta.schema.declared_data_type = @stmt_api.column_declared_type( idx )
|
261
330
|
|
331
|
+
# only check for rowid if we have a table name and it is not the
|
332
|
+
# sqlite_master table. We could get recursion in those cases.
|
333
|
+
if not using_rowid_column? and tbl_name and tbl_name != 'sqlite_master' and is_column_rowid?( tbl_name, col_name ) then
|
334
|
+
@rowid_index = idx
|
335
|
+
end
|
336
|
+
|
262
337
|
meta << column_meta
|
263
338
|
end
|
339
|
+
|
264
340
|
@result_meta = meta
|
265
341
|
end
|
266
342
|
return @result_meta
|
267
343
|
end
|
268
344
|
|
345
|
+
##
|
346
|
+
# is the column indicated by the Column a 'rowid' column
|
347
|
+
#
|
348
|
+
def is_column_rowid?( table_name, column_name )
|
349
|
+
column_schema = @db.schema.tables[table_name].columns[column_name]
|
350
|
+
if column_schema.primary_key? and column_schema.declared_data_type.upcase == "INTEGER" then
|
351
|
+
return true
|
352
|
+
end
|
353
|
+
return false
|
354
|
+
end
|
355
|
+
|
269
356
|
##
|
270
357
|
# Return the array of field names for the result set, the field names are
|
271
358
|
# all strings
|
@@ -288,7 +375,7 @@ module Amalgalite
|
|
288
375
|
def column_count
|
289
376
|
@stmt_api.column_count
|
290
377
|
end
|
291
|
-
|
378
|
+
|
292
379
|
##
|
293
380
|
# return the raw sql that was originally used to prepare the statement
|
294
381
|
#
|