activerecord-amalgalite-adapter 0.8.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 ADDED
@@ -0,0 +1,4 @@
1
+ = Changelog
2
+ == Version 0.8.0
3
+
4
+ * Initial public release
data/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ Copyright (c) 2009, Jeremy Hinegadner
2
+
3
+ Permission to use, copy, modify, and/or distribute this software for any
4
+ purpose with or without fee is hereby granted, provided that the above
5
+ copyright notice and this permission notice appear in all copies.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
8
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
9
+ FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
10
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
11
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
12
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
13
+ PERFORMANCE OF THIS SOFTWARE.
data/README ADDED
@@ -0,0 +1,51 @@
1
+ == ActiveRecord Amalgalite Adapter
2
+
3
+ * Homepage[http://copiousfreetime.rubyforge.org/activerecord-amalgalite-adapter]
4
+ * {Rubyforge Project}[http://rubyforge.org/projects/copiousfreetime/]
5
+ * email jeremy at copiousfreetime dot org
6
+ * git clone url git://github.com/copiousfreetime/activerecord-amalgalite-adapter
7
+
8
+ == DESCRIPTION
9
+
10
+ This is that ActiveRecord adapter for the Amalgalite Database gem. The
11
+ Amalgalite gem embeds the SQLite database inside a ruby extensions.
12
+
13
+ == INSTALL
14
+
15
+ * gem install activerecord-amalgalite-adapter
16
+
17
+ == SYNOPSIS
18
+
19
+ You will need to have the 'activerecord-amalgalite-adapter' gem installed.
20
+
21
+ After that you can use the the 'amalgalite' adapter in your config/database.yml
22
+
23
+ For example config/database.yml:
24
+
25
+ development:
26
+ adapter: amalgalite
27
+ database: db/production.db
28
+
29
+ test:
30
+ adapter: amalgalite
31
+ database: db/test.db
32
+
33
+ development:
34
+ adapter: amalgalite
35
+ database: db/development.db
36
+
37
+ == LICENSE
38
+
39
+ Copyright (c) 2009, Jeremy Hinegardner
40
+
41
+ Permission to use, copy, modify, and/or distribute this software for any
42
+ purpose with or without fee is hereby granted, provided that the above
43
+ copyright notice and this permission notice appear in all copies.
44
+
45
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
46
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
47
+ FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
48
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
49
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
50
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
51
+ PERFORMANCE OF THIS SOFTWARE.
@@ -0,0 +1,52 @@
1
+ require 'rubygems'
2
+ require 'activerecord-amalgalite-adapter'
3
+ require 'tasks/config'
4
+
5
+ class ActiveRecord::ConnectionAdapters::AmalgaliteAdapter
6
+
7
+ GEM_SPEC = Gem::Specification.new do |spec|
8
+ proj = Configuration.for('project')
9
+ spec.name = proj.name
10
+ spec.version = VERSION
11
+
12
+ spec.author = proj.author
13
+ spec.email = proj.email
14
+ spec.homepage = proj.homepage
15
+ spec.summary = proj.summary
16
+ spec.description = proj.description
17
+ spec.platform = ::Gem::Platform::RUBY
18
+
19
+
20
+ pkg = ::Configuration.for('packaging')
21
+ spec.files = pkg.files.all
22
+ spec.executables = pkg.files.bin.collect { |b| File.basename(b) }
23
+
24
+ # add dependencies here
25
+ spec.add_dependency("configuration", ">= 0.0.5")
26
+ spec.add_dependency("amalgalite", "~> 0.8.0")
27
+ spec.add_dependency("activerecord", "= 2.3.2")
28
+
29
+ if ext_conf = ::Configuration.for_if_exist?("extension") then
30
+ spec.extensions << ext_conf.configs
31
+ spec.extensions.flatten!
32
+ spec.require_paths << "ext"
33
+ end
34
+
35
+ if rdoc = ::Configuration.for_if_exist?('rdoc') then
36
+ spec.has_rdoc = true
37
+ spec.extra_rdoc_files = pkg.files.rdoc
38
+ spec.rdoc_options = rdoc.options + [ "--main" , rdoc.main_page ]
39
+ else
40
+ spec.has_rdoc = false
41
+ end
42
+
43
+ if test = ::Configuration.for_if_exist?('testing') then
44
+ spec.test_files = test.files
45
+ end
46
+
47
+ if rf = ::Configuration.for_if_exist?('rubyforge') then
48
+ spec.rubyforge_project = rf.project
49
+ end
50
+ end
51
+
52
+ end
@@ -0,0 +1,442 @@
1
+ #--
2
+ # Copyright (c) 2009 Jeremy Hinegadner
3
+ # All rights reserved. See LICENSE and/or COPYING for details
4
+ #
5
+ # much of this behavior ported from the standard active record sqlite
6
+ # and sqlite3 # adapters
7
+ #++
8
+ require 'active_record'
9
+ require 'active_record/connection_adapters/abstract_adapter'
10
+ require 'amalgalite'
11
+ require 'stringio'
12
+
13
+ module ActiveRecord
14
+ class Base
15
+ class << self
16
+ def amalgalite_connection( config ) # :nodoc:
17
+ config[:database] ||= config[:dbfile]
18
+ raise ArgumentError, "No database file specified. Missing argument: database" unless config[:database]
19
+
20
+ # Allow database path relative to RAILS_ROOT, but only if the database
21
+ # is not the in memory database
22
+ if Object.const_defined?( :RAILS_ROOT ) and (":memory:" != config[:database] ) then
23
+ config[:database] = File.expand_path( config[:database], RAILS_ROOT )
24
+ end
25
+
26
+ db = ::Amalgalite::Database.new( config[:database] )
27
+ ConnectionAdapters::AmalgaliteAdapter.new( db, logger )
28
+ end
29
+ end
30
+ end
31
+
32
+
33
+ module ConnectionAdapters
34
+ class AmalgaliteColumn < Column
35
+ def self.from_amalgalite( am_col )
36
+ new( am_col.name,
37
+ am_col.default_value,
38
+ am_col.declared_data_type,
39
+ am_col.nullable? )
40
+ end
41
+
42
+ # unfortunately, not able to use the Blob interface as that requires
43
+ # knowing what column the blob is going to be stored in. Use the approach
44
+ # in the sqlite3 driver.
45
+ def self.string_to_binary( value )
46
+ value.gsub(/\0|\%/n) do |b|
47
+ case b
48
+ when "\0" then "%00"
49
+ when "%" then "%25"
50
+ end
51
+ end
52
+ end
53
+
54
+ # since the type is blog, the amalgalite drive extracts it as a blob and we need to
55
+ # convert back into a string and do the substitution
56
+ def self.binary_to_string(value)
57
+ value.to_s.gsub(/%00|%25/n) do |b|
58
+ case b
59
+ when "%00" then "\0"
60
+ when "%25" then "%"
61
+ end
62
+ end
63
+ end
64
+
65
+ # AR asks to convert a datetime column to a time and then passes in a
66
+ # string... WTF ?
67
+ def self.datetime_to_time( dt )
68
+ case dt
69
+ when String
70
+ return nil if dt.empty?
71
+ when DateTime
72
+ return dt.to_time
73
+ when Time
74
+ return dt.to_time
75
+ end
76
+ end
77
+
78
+ # active record assumes that type casting is from a string to a value, and
79
+ # it might not be. It might be something appropriate for the field in
80
+ # question, like say a DateTime for a :datetime field?.
81
+ def type_cast_code( var_name )
82
+ case type
83
+ when :datetime then "#{self.class.name}.datetime_to_time(#{var_name})"
84
+ else
85
+ super
86
+ end
87
+ end
88
+
89
+ end
90
+
91
+ class AmalgaliteAdapter < AbstractAdapter
92
+ class Version
93
+ MAJOR = 0
94
+ MINOR = 8
95
+ BUILD = 0
96
+
97
+ def self.to_a() [MAJOR, MINOR, BUILD]; end
98
+ def self.to_s() to_a.join("."); end
99
+ def to_a() Version.to_a; end
100
+ def to_s() Version.to_s; end
101
+
102
+ STRING = Version.to_s
103
+ end
104
+
105
+ VERSION = Version.to_s
106
+
107
+ def adapter_name
108
+ "Amalgalite"
109
+ end
110
+
111
+ def supports_ddl_transactions?
112
+ true
113
+ end
114
+
115
+ def supports_migrations?
116
+ true
117
+ end
118
+
119
+ def requires_reloading?
120
+ true
121
+ end
122
+
123
+ def supports_add_column?
124
+ true
125
+ end
126
+
127
+ def supports_count_distinct?
128
+ true
129
+ end
130
+
131
+ def supports_autoincrement?
132
+ true
133
+ end
134
+
135
+
136
+ def native_database_types #:nodoc:
137
+ {
138
+ :primary_key => default_primary_key_type,
139
+ :string => { :name => "varchar", :limit => 255 },
140
+ :text => { :name => "text" },
141
+ :integer => { :name => "integer" },
142
+ :float => { :name => "float" },
143
+ :decimal => { :name => "decimal" },
144
+ :datetime => { :name => "datetime" },
145
+ :timestamp => { :name => "datetime" },
146
+ :time => { :name => "time" },
147
+ :date => { :name => "date" },
148
+ :binary => { :name => "blob" },
149
+ :boolean => { :name => "boolean" }
150
+ }
151
+ end
152
+
153
+ # QUOTING ==================================================
154
+
155
+ # this is really escaping
156
+ def quote_string( s ) #:nodoc:
157
+ @connection.escape( s )
158
+ end
159
+
160
+ def quote_column_name( name ) #:nodoc:
161
+ return "\"#{name}\""
162
+ end
163
+
164
+ # DATABASE STATEMENTS ======================================
165
+ def execute( sql, name = nil )
166
+ log( sql, name) { @connection.execute( sql ) }
167
+ end
168
+
169
+ def update_sql( sql, name = nil )
170
+ super
171
+ @connection.row_changes
172
+ end
173
+
174
+ def delete_sql(sql, name = nil )
175
+ sql += "WHERE 1=1" unless sql =~ /WHERE/i
176
+ super sql, name
177
+ end
178
+
179
+ def insert_sql( sql, name = nil, pk = nil, id_value = nil, sequence_name = nil )
180
+ super || @connection.last_insert_rowid
181
+ end
182
+
183
+ def select_rows( sql, name = nil )
184
+ execute( sql, name )
185
+ end
186
+
187
+ def select( sql, name = nil )
188
+ execute( sql, name ).map do |row|
189
+ row.to_hash
190
+ end
191
+ end
192
+
193
+
194
+ def begin_db_transaction() @connection.transaction; end
195
+ def commit_db_transaction() @connection.commit; end
196
+ def rollback_db_transaction() @connection.rollback; end
197
+
198
+ # there is no select for update in sqlite
199
+ def add_lock!( sql, options )
200
+ sql
201
+ end
202
+
203
+
204
+ # SCHEMA STATEMENTS ========================================
205
+
206
+ def default_primary_key_type
207
+ 'INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL'.freeze
208
+ end
209
+
210
+ def tables( name = nil )
211
+ sql = "SELECT name FROM sqlite_master WHERE type = 'table' AND NOT name = 'sqlite_sequence'"
212
+ raw_list = execute( sql, nil ).map { |row| row['name'] }
213
+ if raw_list.sort != @connection.schema.tables.keys.sort then
214
+ @connection.schema.load_schema!
215
+ if raw_list.sort != @connection.schema.tables.keys.sort then
216
+ raise "raw_list - tables : #{raw_list - @connection.schema.tables.keys} :: tables - raw_list #{@connection.schema.tables.keys - raw_list}"
217
+ end
218
+ end
219
+ ActiveRecord::Base.logger.info "schema_migrations in tables? : #{raw_list.include?( "schema_migrations" )}"
220
+ ActiveRecord::Base.logger.info "schema_migrations(2) in tables? : #{@connection.schema.tables.keys.include?( "schema_migrations" )}"
221
+ @connection.schema.tables.keys
222
+ end
223
+
224
+ def columns( table_name, name = nil )
225
+ t = @connection.schema.tables[table_name.to_s]
226
+ raise "Invalid table #{table_name}" unless t
227
+ t.columns_in_order.map do |c|
228
+ AmalgaliteColumn.from_amalgalite( c )
229
+ end
230
+ end
231
+
232
+ def indexes( table_name, name = nil )
233
+ table = @connection.schema.tables[table_name.to_s]
234
+ indexes = []
235
+ if table then
236
+ indexes = table.indexes.map do |key, idx|
237
+ index = IndexDefinition.new( table_name, idx.name )
238
+ index.unique = idx.unique?
239
+ index.columns = idx.columns.map { |col| col.name }
240
+ index
241
+ end
242
+ end
243
+ return indexes
244
+ end
245
+
246
+ def primary_key( table_name )
247
+ pk_list = @connection.schema.tables[table_name.to_s].primary_key
248
+ if pk_list.empty? then
249
+ return nil
250
+ else
251
+ return pk_list.first.name
252
+ end
253
+ end
254
+
255
+ def remove_index( table_name, options = {} )
256
+ execute "DROP INDEX #{quote_column_name(index_name( table_name.to_s, options ) ) }"
257
+ @connection.schema.dirty!
258
+ end
259
+
260
+ def rename_table( name, new_name )
261
+ execute "ALTER TABLE #{name} RENAME TO #{new_name}"
262
+ @connection.schema.dirty!
263
+ end
264
+
265
+ # See: http://www.sqlite.org/lang_altertable.html
266
+ # SQLite has an additional restriction on the ALTER TABLE statement
267
+ def valid_alter_table_options( type, options )
268
+ type.to_sym != :primary_key
269
+ end
270
+
271
+ ##
272
+ # Wrap the create table so we can mark the schema as dirty
273
+ #
274
+ def create_table( table_name, options = {}, &block )
275
+ super( table_name, options, &block )
276
+ @connection.schema.load_table( table_name.to_s )
277
+ end
278
+
279
+ def change_table( table_name, &block )
280
+ super( table_name, &block )
281
+ @connection.schema.load_table( table_name.to_s )
282
+
283
+ end
284
+
285
+ def drop_table( table_name, options = {} )
286
+ super( table_name, options )
287
+ @connection.schema.tables.delete( table_name.to_s )
288
+ puts "dropped table #{table_name} : #{@connection.schema.tables.include?( table_name )}" if table_name == "delete_me"
289
+ end
290
+
291
+ def add_column(table_name, column_name, type, options = {})
292
+ rc = nil
293
+ if valid_alter_table_options( type, options ) then
294
+ rc = super( table_name, column_name, type, options )
295
+ else
296
+ table_name = table_name.to_s
297
+ rc = alter_table( table_name ) do |definition|
298
+ definition.column( column_name, type, options )
299
+ end
300
+ end
301
+ @connection.schema.load_table( table_name.to_s )
302
+ return rc
303
+ end
304
+
305
+ def add_index( table_name, column_name, options = {} )
306
+ super
307
+ @connection.schema.load_table( table_name.to_s )
308
+ end
309
+
310
+ def remove_column( table_name, *column_names )
311
+ column_names.flatten.each do |column_name|
312
+ alter_table( table_name ) do |definition|
313
+ definition.columns.delete( definition[column_name] )
314
+ end
315
+ end
316
+ end
317
+ alias :remove_columns :remove_column
318
+
319
+ def change_column_default(table_name, column_name, default) #:nodoc:
320
+ alter_table(table_name) do |definition|
321
+ definition[column_name].default = default
322
+ end
323
+ end
324
+
325
+ def change_column_null(table_name, column_name, null, default = nil)
326
+ unless null || default.nil?
327
+ execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
328
+ end
329
+ alter_table(table_name) do |definition|
330
+ definition[column_name].null = null
331
+ end
332
+ end
333
+
334
+ def change_column(table_name, column_name, type, options = {}) #:nodoc:
335
+ alter_table(table_name) do |definition|
336
+ include_default = options_include_default?(options)
337
+ definition[column_name].instance_eval do
338
+ self.type = type
339
+ self.limit = options[:limit] if options.include?(:limit)
340
+ self.default = options[:default] if include_default
341
+ self.null = options[:null] if options.include?(:null)
342
+ end
343
+ end
344
+ end
345
+
346
+ def rename_column(table_name, column_name, new_column_name) #:nodoc:
347
+ unless columns(table_name).detect{|c| c.name == column_name.to_s }
348
+ raise ActiveRecord::ActiveRecordError, "Missing column #{table_name}.#{column_name}"
349
+ end
350
+ alter_table(table_name, :rename => {column_name.to_s => new_column_name.to_s})
351
+ end
352
+
353
+ def empty_insert_statement(table_name)
354
+ "INSERT INTO #{table_name} VALUES(NULL)"
355
+ end
356
+
357
+ #################
358
+ protected
359
+ #################
360
+
361
+ def alter_table(table_name, options = {}) #:nodoc:
362
+ altered_table_name = "altered_#{table_name}"
363
+ caller = lambda {|definition| yield definition if block_given?}
364
+
365
+ transaction do
366
+ move_table(table_name, altered_table_name,
367
+ options.merge(:temporary => true))
368
+ move_table(altered_table_name, table_name, &caller)
369
+ end
370
+ end
371
+
372
+ def move_table(from, to, options = {}, &block) #:nodoc:
373
+ copy_table(from, to, options, &block)
374
+ drop_table(from)
375
+ end
376
+
377
+ def copy_table(from, to, options = {}) #:nodoc:
378
+ options = options.merge( :id => (!columns(from).detect{|c| c.name == 'id'}.nil? && 'id' == primary_key(from).to_s))
379
+ options = options.merge( :primary_key => primary_key(from).to_s )
380
+ create_table(to, options) do |definition|
381
+ @definition = definition
382
+ columns(from).each do |column|
383
+ column_name = options[:rename] ?
384
+ (options[:rename][column.name] ||
385
+ options[:rename][column.name.to_sym] ||
386
+ column.name) : column.name
387
+
388
+ @definition.column(column_name, column.type,
389
+ :limit => column.limit, :default => column.default,
390
+ :null => column.null)
391
+ end
392
+ @definition.primary_key(primary_key(from)) if primary_key(from)
393
+ yield @definition if block_given?
394
+ end
395
+
396
+ copy_table_indexes(from, to, options[:rename] || {})
397
+ copy_table_contents(from, to,
398
+ @definition.columns.map {|column| column.name},
399
+ options[:rename] || {})
400
+ end
401
+
402
+ def copy_table_indexes(from, to, rename = {}) #:nodoc:
403
+ indexes(from).each do |index|
404
+ name = index.name
405
+ if to == "altered_#{from}"
406
+ name = "temp_#{name}"
407
+ elsif from == "altered_#{to}"
408
+ name = name[5..-1]
409
+ end
410
+
411
+ to_column_names = columns(to).map(&:name)
412
+ columns = index.columns.map {|c| rename[c] || c }.select do |column|
413
+ to_column_names.include?(column)
414
+ end
415
+
416
+ unless columns.empty?
417
+ # index name can't be the same
418
+ opts = { :name => name.gsub(/_(#{from})_/, "_#{to}_") }
419
+ opts[:unique] = true if index.unique
420
+ add_index(to, columns, opts)
421
+ end
422
+ end
423
+ end
424
+
425
+ def copy_table_contents(from, to, columns, rename = {}) #:nodoc:
426
+ column_mappings = Hash[*columns.map {|name| [name, name]}.flatten]
427
+ rename.inject(column_mappings) {|map, a| map[a.last] = a.first; map}
428
+ from_columns = columns(from).collect {|col| col.name}
429
+ columns = columns.find_all{|col| from_columns.include?(column_mappings[col])}
430
+ quoted_columns = columns.map { |col| quote_column_name(col) } * ','
431
+
432
+ quoted_to = quote_table_name(to)
433
+ @connection.execute "SELECT * FROM #{quote_table_name(from)}" do |row|
434
+ sql = "INSERT INTO #{quoted_to} (#{quoted_columns}) VALUES ("
435
+ sql << columns.map {|col| quote row[column_mappings[col]]} * ', '
436
+ sql << ')'
437
+ @connection.execute sql
438
+ end
439
+ end
440
+ end
441
+ end
442
+ end