activerecord-amalgalite-adapter 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
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