amalgalite 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
#
|