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/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
  #