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/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
@@ -20,6 +20,7 @@ end
20
20
  sqlite3
21
21
  statement
22
22
  table
23
+ taps
23
24
  trace_tap
24
25
  type_map
25
26
  version
@@ -4,11 +4,176 @@
4
4
  #++
5
5
  module Amalgalite
6
6
  ##
7
- # Eventually this will be amalgalites interface to the SQLite Blob API to
8
- # allow for streaming of data to and from Blobs in an SQLite database.
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
- # Right now it is just a place holder.
12
+ # For instance during an insert:
11
13
  #
12
- class Blob
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
@@ -13,7 +13,10 @@ module Amalgalite
13
13
  # information from a SELECT query.
14
14
  #
15
15
  class Column
16
- # the database this column belongs to
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, name, table )
53
+ def initialize( db, table, name)
51
54
  @db = db
52
55
  @name = name
53
56
  @table = table
@@ -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'], table )
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
@@ -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 = @db.utf16? ? :prepare16 : :prepare
32
+ prepare_method = @db.utf16? ? :prepare16 : :prepare
26
33
  @param_positions = {}
27
- @stmt_api = @db.api.send( prepare_method, sql )
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
- if block_given? then
59
- while row = next_row
60
- yield row
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
- else
63
- all_rows
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
- if position > 0 then
114
- bind_parameter_to( position, value )
115
- else
116
- raise Amalgalite::Error, "Unable to find parameter '#{param}' in SQL statement [#{sql}]"
117
- end
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
- raise NotImplemented, "Blob binding is not implemented yet"
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
- raise NotImplemented, "returning a blob is not supported yet"
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
- "Received unexepcted result code #{rc} : #{Amalgalite::SQLite3::Constants::ResultCode.from_int( rc )}"
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
  #