ackbar 0.1.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.
@@ -0,0 +1,39 @@
1
+ Version 0.1.0 - Initial Release
2
+
3
+ * Override methods in AR::Base (class and instance) to support KB CRUDs
4
+
5
+ * Override Associations to support KB
6
+
7
+ * Simple test suite for boostrapping
8
+
9
+ * Tests use modified versrion of AR::Fixtures
10
+
11
+ * Added runner for the AR test suite
12
+ Importing test classes one by one and removing inapplicable tests
13
+ Forcing non-transactional fixtures
14
+
15
+ * Added an SQL fragment translator (mostly around conditions)
16
+
17
+ * #serialize'd attributes will cause the corresponding column to be changed to YAML
18
+
19
+ * The adapter #indexes method will return a default index name. Index names are
20
+ not used as in SQL, but the name is generated each time from the table name
21
+ and the relevant columns.
22
+
23
+ * All blocks passed to :finder_sql and :counter_sql might be called with
24
+ multiple parameters:
25
+ * has_one and belongs_to: remote record
26
+ * has_many: remote record and this record
27
+ * has_and_belongs_to_many: join-table record and this record
28
+ Additionally HasAndBelongsToManyAssociation :delete_sql will be called with
29
+ three parameters: join record, this record and remote record
30
+ Make sure that all blocks passed adhere to this convention.
31
+ See ar_base_tests_runner & ar_model_adaptation for examples.
32
+
33
+ * There are some minor extensions to the stdlib, with asosciated tests.
34
+ See the bottom of kirbybase_adapter.rb, but the short list is:
35
+ * Array#sort_by - takes a symbol or a block
36
+ * Array#sort_by! - inplace
37
+ * Array#stable_sort and Array#stable_sort_by - based on Matz's post to ruby-talk
38
+ * Object#in(ary) - equal to ary.include?(o)
39
+
data/README ADDED
@@ -0,0 +1,88 @@
1
+ = About Ackbar
2
+
3
+ Ackbar is an adapter for ActiveRecord (the Rails ORM layer) to the KirbyBase
4
+ pure-ruby plain-text DBMS. Because KirbyBase does not support SQL, joins or
5
+ transactions, this is not a 100% fit. There are some changes to the ActiveRecord
6
+ interface (see below), but it may still be useful in some cases.
7
+
8
+ = URIs
9
+
10
+ Ackbar: http://ackbar.rubyforge.org
11
+
12
+ KirbyBase: http://www.netpromi.com/kirbybase_ruby.html
13
+
14
+ Rails: http://www.rubyonrails.com
15
+
16
+ Pimki: http://pimki.rubyforge.org
17
+
18
+ = Goals
19
+
20
+ Ackbar's project goals, in order of importance, are:
21
+ 1. Support Pimki with a pure-ruby, cross-platform hassle-less install DBMS
22
+ 2. An exercise for me to learn ActiveRecord inside out
23
+ 3. Support other "shrink-wrapped" Rails projects with similar needs
24
+
25
+ As can be seen, the main reason I need Ackbar is so I distribute Pimki across
26
+ multiple platforms without requiring non-Ruby 3rd party libraries. KirbyBase will
27
+ work wherever Ruby works, and so will Pimki. That alleviates the need to repackage
28
+ other bits, end users will not have to install extra software, I have full control
29
+ on the storage, the storage is in plain text. Just what I need to "shrink wrap"
30
+ a Rails project for end-user distribution.
31
+
32
+ = What's Covered
33
+
34
+ Ackbar currently passes through a small bootstrap test suite, and through about
35
+ 80% of the ActiveRecord test suite. I will never pass 100% of the tests because
36
+ KirbyBase does not support all required functionality.
37
+
38
+ Ackbar includes a SQL fragment translator, so that simple cross-database code
39
+ should be maintainable. For example the following will work as expected,
40
+ Book.find :all, :conditions => "name = 'Pickaxe'"
41
+ Book.find :all, :conditions => ["name = ?", 'Pickaxe']
42
+ Additionally, you can also provide blocks:
43
+ Book.find :all, :conditions => lambda{|rec| rec.name == 'Pickaxe'}
44
+ or even:
45
+ Book.find(:all) {|rec| rec.name == 'Pickaxe'}
46
+
47
+ Most of these changes are around the #find method, bit some apply to #update and
48
+ associations. Basic SQL translation should work the same, but you can always
49
+ provide custom code to be used. See the CHANGELOG and the tests for examples.
50
+
51
+
52
+ = What's Not Covered
53
+
54
+ * Transactions
55
+ * Joins, and therefore Eager Associations
56
+ * Mixins
57
+ * Other plugins
58
+
59
+ On the todo list is support for mixins. It might even be possible to rig something
60
+ to simulate joins and eager associations, but that is for a later stage. Transactions
61
+ will obviously only be supported once they are supported by KirbyBase.
62
+
63
+ Additionally, there are numerous little changes to the standard behaviour. See
64
+ the CHANGELOG and the tests for more details. These may cause little heart attacks
65
+ if you expect a standard SQL database.
66
+
67
+ It is also worth noting that other plugins that write SQL will not work. You will
68
+ need to get a copy of them to your /vendors dir and modify the relevant parts.
69
+
70
+ = Installation
71
+
72
+ Simply:
73
+ gem install ackbar
74
+ or download the zip file from http://rubyforge.org/projects/ackbar and just stick
75
+ kirbybase_adapter.rb in the Rails lib dir.
76
+
77
+ You will then need to add
78
+ require 'kirbybase_adapter'
79
+ in the config/environment.rb file of your project.
80
+
81
+ If you plan on multi-database development / deployment, you must require the adapter
82
+ only if necessary:
83
+ require 'kirbybase_adapter' if ActiveRecord::Base.configurations[RAILS_ENV]['adapter'] == 'kirbybase'
84
+
85
+ This is because Ackbar overrides certain methods in ActiveRecord::Base and others.
86
+ These methods translate the standard SQL generation to method calls on KirbyBase,
87
+ and obviously should not be overridden for regular DBs.
88
+
@@ -0,0 +1,75 @@
1
+ require 'rake'
2
+ require 'rake/clean'
3
+ require 'rake/testtask'
4
+ require 'rake/gempackagetask'
5
+
6
+ CLEAN << 'pkg' << 'doc' << 'test/db' << '*.log' << '*.orig'
7
+
8
+ desc "Run all tests by default"
9
+ task :default => [:basic_tests, :ar_tests]
10
+
11
+ desc 'Run the unit tests in test directory'
12
+ Rake::TestTask.new('basic_tests') do |t|
13
+ t.libs << 'test'
14
+ t.pattern = 'test/**/*_test.rb'
15
+ t.verbose = true
16
+ end
17
+
18
+ desc 'Run the ActiveRecords tests with Ackbar'
19
+ Rake::TestTask.new('ar_tests') do |t|
20
+ t.libs << 'test'
21
+ t.pattern = 'ar_base_tests_runner.rb'
22
+ t.verbose = true
23
+ end
24
+
25
+
26
+ ackbar_spec = Gem::Specification.new do |s|
27
+ s.platform = Gem::Platform::RUBY
28
+ s.name = 'ackbar'
29
+ s.version = "0.1.0"
30
+ s.summary = "ActiveRecord KirbyBase Adapter"
31
+ s.description = %q{An adapter for Rails::ActiveRecord ORM to the KirbyBase pure-ruby DBMS}
32
+
33
+ s.author = "Assaph Mehr"
34
+ s.email = "assaph@gmail.com"
35
+ s.rubyforge_project = 'ackbar'
36
+ s.homepage = 'http://ackbar.rubyforge.org'
37
+
38
+ s.has_rdoc = true
39
+ s.extra_rdoc_files = %W{README CHANGELOG TODO}
40
+ s.rdoc_options << '--title' << 'Ackbar -- ActiveRecord Adapter for KirbyBase' <<
41
+ '--main' << 'README' <<
42
+ '--exclude' << 'test' <<
43
+ '--line-numbers'
44
+
45
+ s.add_dependency('KirbyBase', '= 2.5.2')
46
+ s.add_dependency('activerecord', '= 1.13.2')
47
+
48
+ s.require_path = '.'
49
+
50
+ s.files = FileList.new %W[
51
+ kirbybase_adapter.rb
52
+ Rakefile
53
+ CHANGELOG
54
+ README
55
+ TODO
56
+ test/00*.rb
57
+ test/ar_base_tests_runner.rb
58
+ test/ar_model_adaptation.rb
59
+ test/connection.rb
60
+ test/create_dbs_for_ar_tests.rb
61
+ test/kb_*_test.rb
62
+ test/model.rb
63
+ test/schema.rb
64
+ test/test_helper.rb
65
+ test/fixtures/*.yml
66
+ ]
67
+ end
68
+
69
+ desc 'Package as gem & zip'
70
+ Rake::GemPackageTask.new(ackbar_spec) do |p|
71
+ p.gem_spec = ackbar_spec
72
+ p.need_tar = true
73
+ p.need_zip = true
74
+ end
75
+
data/TODO ADDED
@@ -0,0 +1,14 @@
1
+ Short Term:
2
+ * Try and run Typo or similar over ackbar
3
+ * See if I can add a :finder_block & :counter_block similar
4
+ to the *_sql variety. Will need to Submit patch to Rails to allow
5
+ overriding of acceptable keys.
6
+ * Test if handling binary data through KBBlobs is better
7
+
8
+ Mid Term:
9
+ * Get the mixins working
10
+
11
+ Long Term:
12
+ * Use KB indexes if they exist
13
+ * Integration with KB's Lookup and Link_many
14
+ * Find a solution to joins (and thus to eager associations)
@@ -0,0 +1,1275 @@
1
+ require 'kirbybase'
2
+ require_gem 'rails'
3
+ require 'active_record'
4
+ require 'active_record/connection_adapters/abstract_adapter'
5
+
6
+ module ActiveRecord
7
+ ##############################################################################
8
+ # Define the KirbyBase connection establishment method
9
+
10
+ class Base
11
+ # Establishes a connection to the database that's used by all Active Record objects.
12
+ def self.kirbybase_connection(config) # :nodoc:
13
+ # Load the KirbyBase DBMS
14
+ unless self.class.const_defined?(:KirbyBase)
15
+ begin
16
+ require 'kirbybase'
17
+ rescue LoadError
18
+ raise "Unable to load KirbyBase"
19
+ end
20
+ end
21
+
22
+ config = config.symbolize_keys
23
+ connection_type = config[:connection_type] || config[:conn_type]
24
+ connection_type = if connection_type.nil? or connection_type.empty?
25
+ :local
26
+ else
27
+ connection_type.to_sym
28
+ end
29
+ host = config[:host]
30
+ port = config[:port]
31
+ path = config[:dbpath] || config[:database] || File.join(RAILS_ROOT, 'db/data')
32
+
33
+ # ActiveRecord::Base.allow_concurrency = false if connection_type == :local
34
+ ConnectionAdapters::KirbyBaseAdapter.new(connection_type, host, port, path)
35
+ end
36
+ end
37
+
38
+ ##############################################################################
39
+ # Define the KirbyBase adapter and column classes
40
+ module ConnectionAdapters
41
+ class KirbyBaseColumn < Column
42
+ def initialize(name, default, sql_type = nil, null = true)
43
+ super
44
+ @name = (name == 'recno' ? 'id' : @name)
45
+ @text = [:string, :text, 'yaml'].include? @type
46
+ end
47
+
48
+ def simplified_type(field_type)
49
+ case field_type
50
+ when /int/i
51
+ :integer
52
+ when /float|double|decimal|numeric/i
53
+ :float
54
+ when /datetime/i
55
+ :datetime
56
+ when /timestamp/i
57
+ :timestamp
58
+ when /time/i
59
+ :time
60
+ when /date/i
61
+ :date
62
+ when /clob/i, /text/i
63
+ :text
64
+ when /blob/i, /binary/i
65
+ :binary
66
+ when /char/i, /string/i
67
+ :string
68
+ when /boolean/i
69
+ :boolean
70
+ else
71
+ field_type
72
+ end
73
+ end
74
+ end
75
+
76
+ # The KirbyBase adapter does not need a "db driver", as KirbyBase is a
77
+ # pure-ruby DBMS. This adapter defines all the required functionality by
78
+ # executing direct method calls on a KirbyBase DB object.
79
+ #
80
+ # Options (for database.yml):
81
+ #
82
+ # * <tt>:connection_type</tt> -- type of connection (local or client). Defaults to :local
83
+ # * <tt>:host</tt> -- If using KirbyBase in a client/server mode
84
+ # * <tt>:port</tt> -- If using KirbyBase in a client/server mode
85
+ # * <tt>:path</tt> -- Path to DB storage area. Defaults to /db/data
86
+ #
87
+ # *Note* that Ackbar/KirbyBase support migrations/schema but not transactions.
88
+ class KirbyBaseAdapter < AbstractAdapter
89
+
90
+ # Ackbar's own version - i.e. the adapter version, not KirbyBase or Rails.
91
+ VERSION = '0.1.0'
92
+
93
+ attr_accessor :db
94
+
95
+ def initialize(connect_type, host, port, path)
96
+ if connect_type == :local
97
+ FileUtils.mkdir_p(path) unless File.exists?(path)
98
+ end
99
+ @db = KirbyBase.new(connect_type, host, port, path)
100
+ end
101
+
102
+ def adapter_name
103
+ 'KirbyBase'
104
+ end
105
+
106
+ def supports_migrations?
107
+ true
108
+ end
109
+
110
+ PRIMARY_KEY_TYPE = { :Calculated => 'recno', :DataType => :Integer }
111
+ def PRIMARY_KEY_TYPE.to_sym() :integer end
112
+
113
+ # Translates all the ActiveRecord simplified SQL types to KirbyBase (Ruby)
114
+ # Types. Also allows KB specific types like :YAML.
115
+ def native_database_types #:nodoc
116
+ {
117
+ :primary_key => PRIMARY_KEY_TYPE,
118
+ :string => { :DataType => :String },
119
+ :text => { :DataType => :String }, # are KBMemos better?
120
+ :integer => { :DataType => :Integer },
121
+ :float => { :DataType => :Float },
122
+ :datetime => { :DataType => :Time },
123
+ :timestamp => { :DataType => :Time },
124
+ :time => { :DataType => :Time },
125
+ :date => { :DataType => :Date },
126
+ :binary => { :DataType => :String }, # are KBBlobs better?
127
+ :boolean => { :DataType => :Boolean },
128
+ :YAML => { :DataType => :YAML }
129
+ }
130
+ end
131
+
132
+ # NOT SUPPORTED !!!
133
+ def execute(*params)
134
+ raise ArgumentError, "SQL not supported! (#{params.inspect})" unless block_given?
135
+ yield db
136
+ end
137
+
138
+ # NOT SUPPORTED !!!
139
+ def update(*params)
140
+ raise ArgumentError, "SQL not supported! (#{params.inspect})" unless block_given?
141
+ yield db
142
+ end
143
+
144
+ # Returns a handle on a KBTable object
145
+ def get_table(table_name)
146
+ db.get_table(table_name.to_sym)
147
+ end
148
+
149
+
150
+
151
+ def create_table(name, options = {})
152
+ table_definition = TableDefinition.new(self)
153
+ table_definition.primary_key(options[:primary_key] || "id") unless options[:id] == false
154
+
155
+ yield table_definition
156
+
157
+ if options[:force]
158
+ drop_table(name) rescue nil
159
+ end
160
+
161
+ # Todo: Handle temporary tables (options[:temporary]), creation options (options[:options])
162
+ defns = table_definition.columns.inject([]) do |defns, col|
163
+ if col.type == PRIMARY_KEY_TYPE
164
+ defns
165
+ else
166
+ kb_col_options = native_database_types[col.type]
167
+ kb_col_options = kb_col_options.merge({ :Required => true }) if not col.null.nil? and not col.null
168
+ kb_col_options = kb_col_options.merge({ :Default => col.default }) unless col.default.nil?
169
+ kb_col_options[:Default] = true if kb_col_options[:DataType] == :Boolean && kb_col_options[:Default]
170
+ # the :limit option is ignored - meaningless considering the ruby types and KB storage
171
+ defns << [col.name.to_sym, kb_col_options]
172
+ end
173
+ end
174
+ begin
175
+ db.create_table(name.to_sym, *defns.flatten)
176
+ rescue => detail
177
+ raise "Create table '#{name}' failed: #{detail}"
178
+ end
179
+ end
180
+
181
+ def drop_table(table_name)
182
+ db.drop_table(table_name.to_sym)
183
+ end
184
+
185
+ def initialize_schema_information
186
+ begin
187
+ schema_info_table = create_table(ActiveRecord::Migrator.schema_info_table_name.to_sym) do |t|
188
+ t.column :version, :integer
189
+ end
190
+ schema_info_table.insert(0)
191
+ rescue ActiveRecord::StatementInvalid, RuntimeError
192
+ # RuntimeError is raised by KB if the table already exists
193
+ # Schema has been intialized
194
+ end
195
+ end
196
+
197
+ def tables(name = nil)
198
+ db.tables.map {|t| t.to_s}
199
+ end
200
+
201
+ def columns(table_name, name=nil)
202
+ tbl = db.get_table(table_name.to_sym)
203
+ tbl.field_names.zip(tbl.field_defaults, tbl.field_types, tbl.field_requireds).map do |fname, fdefault, ftype, frequired|
204
+ KirbyBaseColumn.new(fname.to_s, fdefault, ftype.to_s.downcase, !frequired)
205
+ end
206
+ end
207
+
208
+ def indexes(table_name, name = nil)
209
+ table = db.get_table(table_name.to_sym)
210
+ indices = table.field_names.zip(table.field_indexes)
211
+ indices_to_columns = indices.inject(Hash.new{|h,k| h[k] = Array.new}) {|hsh, (fn, ind)| hsh[ind] << fn.to_s unless ind.nil?; hsh}
212
+ indices_to_columns.map do |ind, cols|
213
+ # we're not keeping the names anywhere (KB doesn't store them), so we
214
+ # just give the default name
215
+ IndexDefinition.new(table_name, "#{table_name}_#{cols[0]}_index", false, cols)
216
+ end
217
+ end
218
+
219
+ def primary_key(table_name)
220
+ raise ArgumentError, "#primary_key called"
221
+ column = table_structure(table_name).find {|field| field['pk'].to_i == 1}
222
+ column ? column['name'] : nil
223
+ end
224
+
225
+ def add_index(table_name, column_name, options = {})
226
+ db.get_table(table_name.to_sym).add_index( *Array(column_name).map{|c| c.to_sym} )
227
+ end
228
+
229
+ def remove_index(table_name, options={})
230
+ db.get_table(table_name.to_sym).drop_index(options) rescue nil
231
+ end
232
+
233
+ def rename_table(name, new_name)
234
+ db.rename_table(name.to_sym, new_name.to_sym)
235
+ end
236
+
237
+ def add_column(table_name, column_name, type, options = {})
238
+ type = type.is_a?(Hash)? type : native_database_types[type]
239
+ type.merge!({:Required => true}) if options[:null] == false
240
+ type.merge!({:Default => options[:default]}) if options.has_key?(:default)
241
+ if type[:DataType] == :Boolean && type.has_key?(:Default)
242
+ type[:Default] = case type[:Default]
243
+ when true, false, nil then type[:Default]
244
+ when String then type[:Default] == 't' ? true : false
245
+ when Integer then type[:Default] == 1 ? true : false
246
+ end
247
+ end
248
+ db.get_table(table_name.to_sym).add_column(column_name.to_sym, type)
249
+ end
250
+
251
+ def remove_column(table_name, column_name)
252
+ db.get_table(table_name.to_sym).drop_column(column_name.to_sym)
253
+ end
254
+
255
+ def change_column_default(table_name, column_name, default)
256
+ column_name = column_name.to_sym
257
+ tbl = db.get_table(table_name.to_sym)
258
+ if columns(table_name.to_sym).detect{|col| col.name.to_sym == column_name}.type == :boolean
259
+ default = case default
260
+ when true, false, nil then default
261
+ when String then default == 't' ? true : false
262
+ when Integer then default == 1 ? true : false
263
+ end
264
+ end
265
+ tbl.change_column_default_value(column_name.to_sym, default)
266
+ end
267
+
268
+ def change_column(table_name, column_name, type, options = {})
269
+ column_name = column_name.to_sym
270
+ tbl = db.get_table(table_name.to_sym)
271
+ tbl.change_column_type(column_name, native_database_types[type][:DataType])
272
+ tbl.change_column_required(column_name, options[:null] == false)
273
+ if options.has_key?(:default)
274
+ change_column_default(table_name, column_name, options[:default])
275
+ end
276
+ end
277
+
278
+ def rename_column(table_name, column_name, new_column_name)
279
+ db.get_table(table_name.to_sym).rename_column(column_name.to_sym, new_column_name.to_sym)
280
+ end
281
+ end
282
+ end
283
+
284
+ ##############################################################################
285
+ # CLASS METHODS: Override SQL based methods in ActiveRecord::Base
286
+ # Class methods: everything invoked from records classes, e.g. Book.find(:all)
287
+
288
+ class Base
289
+ # Utilities ################################################################
290
+
291
+ # The KirbyBase object
292
+ def self.db
293
+ #db ||= connection.db
294
+ connection.db
295
+ end
296
+
297
+ # The KBTable object for this AR model object
298
+ def self.table
299
+ begin
300
+ db.get_table(table_name.to_sym)
301
+ rescue RuntimeError => detail
302
+ raise StatementInvalid, detail.message
303
+ end
304
+ end
305
+
306
+ # NOT SUPPORTED !!!
307
+ def self.select_all(sql, name = nil)
308
+ raise StatementInvalid, "select_all(#{sql}, #{name}"
309
+ execute(sql, name).map do |row|
310
+ record = {}
311
+ row.each_key do |key|
312
+ if key.is_a?(String)
313
+ record[key.sub(/^\w+\./, '')] = row[key]
314
+ end
315
+ end
316
+ record
317
+ end
318
+ end
319
+
320
+ # NOT SUPPORTED !!!
321
+ def self.select_one(sql, name = nil)
322
+ raise StatementInvalid, "select_one(#{sql}, #{name}"
323
+ result = select_all(sql, name)
324
+ result.nil? ? nil : result.first
325
+ end
326
+
327
+ # NOT SUPPORTED !!!
328
+ def self.find_by_sql(*args)
329
+ raise StatementInvalid, "SQL not Supported"
330
+ end
331
+
332
+ # NOT SUPPORTED !!!
333
+ def self.count_by_sql(*args)
334
+ raise StatementInvalid, "SQL not Supported"
335
+ end
336
+
337
+ # Deletes the selected rows from the DB.
338
+ def self.delete(ids)
339
+ ids = [ids].flatten
340
+ table.delete {|r| ids.include? r.recno }
341
+ end
342
+
343
+ # Deletes the matching rows from the table. If no conditions are specified,
344
+ # will clear the whole table.
345
+ def self.delete_all(conditions = nil)
346
+ if conditions.nil? and !block_given?
347
+ table.delete_all
348
+ else
349
+ table.delete &build_conditions_from_options(:conditions => conditions)
350
+ end
351
+ end
352
+
353
+ # Updates the matching rows from the table. If no conditions are specified,
354
+ # will update all rows in the table.
355
+ def self.update_all(updates, conditions = nil)
356
+ finder = build_conditions_from_options :conditions => conditions
357
+ updater = case updates
358
+ when Proc then updates
359
+ when Hash then updates
360
+ when Array then parse_updates_from_sql_array(updates)
361
+ when String then parse_updates_from_sql_string(updates)
362
+ else raise ArgumentError, "Don't know how to process updates: #{updates.inspect}"
363
+ end
364
+ updater.is_a?(Proc) ?
365
+ table.update(&finder).set(&updater) :
366
+ table.update(&finder).set(updater)
367
+ end
368
+
369
+ # Attempt to parse parameters in the format of ['name = ?', some_name] for updates
370
+ def self.parse_updates_from_sql_array sql_parameters_array
371
+ updates_string = sql_parameters_array[0]
372
+ args = sql_parameters_array[1..-1]
373
+
374
+ update_code = table.field_names.inject(updates_string) {|updates, fld| fld == :id ? updates.gsub(/\bid\b/, 'rec.recno') : updates.gsub(/\b(#{fld})\b/, 'rec.\1') }
375
+ update_code = update_code.split(',').zip(args).map {|i,v| [i.gsub('?', ''), v.inspect]}.to_s.gsub(/\bNULL\b/i, 'nil')
376
+ eval "lambda{ |rec| #{update_code} }"
377
+ end
378
+
379
+ # Attempt to parse parameters in the format of 'name = "Some Name"' for updates
380
+ def self.parse_updates_from_sql_string sql_string
381
+ update_code = table.field_names.inject(sql_string) {|updates, fld| fld == :id ? updates.gsub(/\bid\b/, 'rec.recno') : updates.gsub(/\b(#{fld})\b/, 'rec.\1') }.gsub(/\bNULL\b/i, 'nil')
382
+ eval "lambda{ |rec| #{update_code} }"
383
+ end
384
+
385
+ # Attempt to parse parameters in the format of ['name = ? AND value = ?', some_name, 1]
386
+ # in the :conditions clause
387
+ def self.parse_conditions_from_sql_array(sql_parameters_array)
388
+ query = sql_parameters_array[0]
389
+ args = sql_parameters_array[1..-1].map{|arg| arg.is_a?(Hash) ? (raise PreparedStatementInvalid if arg.size > 1; arg.values[0]) : arg }
390
+
391
+ query = translate_sql_to_code query
392
+ raise PreparedStatementInvalid if query.count('?') != args.size
393
+ query_components = query.split('?').zip(args.map{ |a|
394
+ case a
395
+ when String, Array then a.inspect
396
+ when nil then 'nil'
397
+ else a
398
+ end
399
+ })
400
+ block_string = query_components.to_s
401
+ begin
402
+ eval "lambda{ |rec| #{block_string} }"
403
+ rescue Exception => detail
404
+ raise PreparedStatementInvalid, detail.to_s
405
+ end
406
+ end
407
+
408
+ # Override of AR::Base SQL construction to build a conditions block. Used only
409
+ # by AR::Base#method_missing to support dynamic finders (e.g. find_by_name).
410
+ def self.construct_conditions_from_arguments(attribute_names, arguments)
411
+ conditions = []
412
+ attribute_names.each_with_index { |name, idx| conditions << "#{name} #{attribute_condition(arguments[idx])} " }
413
+ build_conditions_from_options :conditions => [ conditions.join(" and ").strip, *arguments[0...attribute_names.length] ]
414
+ end
415
+
416
+ # Override of AR::Base that was using raw SQL
417
+ def self.increment_counter(counter_name, ids)
418
+ [ids].flatten.each do |id|
419
+ table.update{|rec| rec.recno == id }.set{ |rec| rec.send "#{counter_name}=", (rec.send(counter_name)+1) }
420
+ end
421
+ end
422
+
423
+ # Override of AR::Base that was using raw SQL
424
+ def self.decrement_counter(counter_name, ids)
425
+ [ids].flatten.each do |id|
426
+ table.update{|rec| rec.recno == id }.set{ |rec| rec.send "#{counter_name}=", (rec.send(counter_name)-1) }
427
+ end
428
+ end
429
+
430
+ # This methods differs in the API from ActiveRecord::Base#find!
431
+ # The changed options are:
432
+ # * <tt>:conditions</tt> this should be a block for selecting the records
433
+ # * <tt>:order</tt> this should be the symbol of the field name
434
+ # * <tt>:include</tt>: Names associations that should be loaded alongside using KirbyBase Lookup fields
435
+ # The following work as before:
436
+ # * <tt>:offset</tt>: An integer determining the offset from where the rows should be fetched. So at 5, it would skip the first 4 rows.
437
+ # * <tt>:readonly</tt>: Mark the returned records read-only so they cannot be saved or updated.
438
+ # * <tt>:limit</tt>: Max numer of records returned
439
+ # * <tt>:select</tt>: Field names from the table. Not as useful, as joins are irrelevant
440
+ # The following are not supported (silently ignored);
441
+ # * <tt>:joins</tt>: An SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id".
442
+ #
443
+ # As a more Kirby-ish way, you can also pass a block to #find that will be
444
+ # used to select the matching records. It's a shortcut to :conditions.
445
+ def self.find(*args)
446
+ options = extract_options_from_args!(args)
447
+ conditions = Proc.new if block_given?
448
+ raise ArgumentError, "Please specify EITHER :conditions OR a block!" if conditions and options[:conditions]
449
+ options[:conditions] ||= conditions
450
+ options[:conditions] = build_conditions_from_options(options)
451
+ filter = options[:select] ? [:recno, options[:select]].flatten.map{|s| s.to_sym} : nil
452
+
453
+ # Inherit :readonly from finder scope if set. Otherwise,
454
+ # if :joins is not blank then :readonly defaults to true.
455
+ unless options.has_key?(:readonly)
456
+ if scoped?(:find, :readonly)
457
+ options[:readonly] = scope(:find, :readonly)
458
+ elsif !options[:joins].blank?
459
+ options[:readonly] = true
460
+ end
461
+ end
462
+
463
+ case args.first
464
+ when :first
465
+ return find(:all, options.merge(options[:include] ? { } : { :limit => 1 })).first
466
+ when :all
467
+ records = options[:include] ?
468
+ find_with_associations(options) :
469
+ filter ? table.select( *filter, &options[:conditions] ) : table.select( &options[:conditions] )
470
+ records = apply_options_to_result_set records, options
471
+ records = instantiate_records(records, :filter => filter, :readonly => options[:readonly])
472
+ records
473
+ else
474
+ return args.first if args.first.kind_of?(Array) && args.first.empty?
475
+ raise RecordNotFound, "Expecting a list of IDs!" unless args.flatten.all?{|i| i.is_a? Numeric}
476
+
477
+ expects_array = ( args.is_a?(Array) and args.first.kind_of?(Array) )
478
+ ids = args.flatten.compact.uniq
479
+
480
+ records = filter ?
481
+ table.select_by_recno_index(*filter) { |r| ids.include?(r.recno) } :
482
+ table.select_by_recno_index { |r| ids.include?(r.recno) }
483
+ records = apply_options_to_result_set(records, options) rescue records
484
+
485
+ conditions_message = options[:conditions] ? " and conditions: #{options[:conditions].inspect}" : ''
486
+ case ids.size
487
+ when 0
488
+ raise RecordNotFound, "Couldn't find #{name} without an ID#{conditions_message}"
489
+ when 1
490
+ if records.nil? or records.empty?
491
+ raise RecordNotFound, "Couldn't find #{name} with ID=#{ids.first}#{conditions_message}"
492
+ end
493
+ records = instantiate_records(records, :filter => filter, :readonly => options[:readonly])
494
+ expects_array ? records : records.first
495
+ else
496
+ if records.size == ids.size
497
+ return instantiate_records(records, :filter => filter, :readonly => options[:readonly])
498
+ else
499
+ raise RecordNotFound, "Couldn't find all #{name.pluralize} with IDs (#{ids.join(', ')})#{conditions_message}"
500
+ end
501
+ end
502
+ end
503
+ end
504
+
505
+ # Instantiates the model record-objects from the KirbyBase structs.
506
+ # Will also apply the limit/offset/readonly/order and other options.
507
+ def self.instantiate_records rec_array, options = {}
508
+ field_names = ['id', table.field_names[1..-1]].flatten.map { |f| f.to_s }
509
+ field_names &= ['id', options[:filter]].flatten.map{|f| f.to_s} if options[:filter]
510
+ records = [rec_array].flatten.compact.map { |rec| instantiate( field_names.zip(rec.values).inject({}){|h, (k,v)| h[k] = v; h} ) }
511
+ records.each { |record| record.readonly! } if options[:readonly]
512
+ records
513
+ end
514
+
515
+
516
+ # Applies the limit/offset/readonly/order and other options to the result set.
517
+ # Will also reapply the conditions.
518
+ def self.apply_options_to_result_set records, options
519
+ records = [records].flatten.compact
520
+ records = records.select( &options[:conditions] ) if options[:conditions]
521
+ if options[:order]
522
+ options[:order].split(',').reverse.each do |order_field|
523
+ # this algorithm is probably incorrect for complex sorts, like
524
+ # col_a, col_b DESC, col_C
525
+ reverse = order_field =~ /\bDESC\b/i
526
+ order_field = order_field.strip.split[0] # clear any DESC, ASC
527
+ records = records.stable_sort_by(order_field.to_sym == :id ? :recno : order_field.to_sym)
528
+ records.reverse! if reverse
529
+ end
530
+ end
531
+ offset = options[:offset] || scope(:find, :offset)
532
+ records = records.slice!(offset..-1) if offset
533
+ limit = options[:limit] || scope(:find, :limit)
534
+ records = records.slice!(0, limit) if limit
535
+ records
536
+ end
537
+
538
+ private_class_method :instantiate_records, :apply_options_to_result_set
539
+
540
+ # One of the main methods: Assembles the :conditions block from the
541
+ # options argument (See build_conditions_block for actual translation). Then
542
+ # adds the scope and inheritance-type conditions (if present).
543
+ def self.build_conditions_from_options options
544
+ basic_selector_block = case options
545
+ when Array
546
+ if options[0].is_a? Proc
547
+ options[0]
548
+ elsif options.flatten.length == 1
549
+ translate_sql_to_code options.flatten[0]
550
+ else
551
+ parse_conditions_from_sql_array options.flatten
552
+ end
553
+
554
+ when Hash
555
+ build_conditions_block options[:conditions]
556
+
557
+ when Proc
558
+ options
559
+
560
+ else
561
+ raise ArgumentError, "Don't know how to process (#{options.inspect})"
562
+ end
563
+
564
+ selector_with_scope = if scope(:find, :conditions)
565
+ scope_conditions_block = build_conditions_block(scope(:find, :conditions))
566
+ lambda{|rec| basic_selector_block[rec] && scope_conditions_block[rec]}
567
+ else
568
+ basic_selector_block
569
+ end
570
+
571
+ conditions_block = if descends_from_active_record?
572
+ selector_with_scope
573
+ else
574
+ untyped_conditions_block = selector_with_scope
575
+ type_condition_block = type_condition(options.is_a?(Hash) ? options[:class_name] : nil)
576
+ lambda{|rec| type_condition_block[rec] && untyped_conditions_block[rec]}
577
+ end
578
+
579
+ conditions_block
580
+ end
581
+
582
+ # For handling the table inheritance column.
583
+ def self.type_condition class_name = nil
584
+ type_condition = if class_name
585
+ "rec.#{inheritance_column} == '#{class_name}'"
586
+ else
587
+ subclasses.inject("rec.#{inheritance_column}.to_s == '#{name.demodulize}' ") do |condition, subclass|
588
+ condition << "or rec.#{inheritance_column}.to_s == '#{subclass.name.demodulize}' "
589
+ end
590
+ end
591
+
592
+ eval "lambda{ |rec| #{type_condition} }"
593
+ end
594
+
595
+ # Builds the :conditions block from various forms of input.
596
+ # * Procs are passed as is
597
+ # * Arrays are assumed to be in the format of ['name = ?', 'Assaph']
598
+ # * Fragment String are translated to code
599
+ # Full SQL statements will raise an error
600
+ # * No parameters will assume a true for all records
601
+ def self.build_conditions_block conditions
602
+ case conditions
603
+ when Proc then conditions
604
+ when Array then parse_conditions_from_sql_array(conditions)
605
+ when String
606
+ if conditions.match(/^(SELECT|INSERT|DELETE|UPDATE)/i)
607
+ raise ArgumentError, "KirbyBase does not support SQL for :conditions! '#{conditions.inspect}''"
608
+ else
609
+ conditions_string = translate_sql_to_code(conditions)
610
+ lambda{|rec| eval conditions_string }
611
+ end
612
+
613
+ when nil
614
+ if block_given?
615
+ Proc.new
616
+ else
617
+ lambda{|r| true}
618
+ end
619
+ end # case conditions
620
+ end
621
+
622
+ # TODO: handle LIKE
623
+ SQL_FRAGMENT_TRANSLATIONS = [
624
+ [/1\s*=\s*1/, 'true'],
625
+ ['rec.', ''],
626
+ ['==', '='],
627
+ [/(\w+)\s*=\s*/, 'rec.\1 == '],
628
+ [/(\w+)\s*<>\s*?/, 'rec.\1 !='],
629
+ [/(\w+)\s*<\s*?/, 'rec.\1 <'],
630
+ [/(\w+)\s*>\s*?/, 'rec.\1 >'],
631
+ [/(\w+)\s*IS\s+NOT\s*?/, 'rec.\1 !='],
632
+ [/(\w+)\s*IS\s*?/, 'rec.\1 =='],
633
+ [/(\w+)\s+IN\s+/, 'rec.\1.in'],
634
+ [/\.id(\W)/i, '.recno\1'],
635
+ ['<>', '!='],
636
+ ['NULL', 'nil'],
637
+ ['AND', 'and'],
638
+ ['OR', 'or'],
639
+ ["'%s'", '?'],
640
+ ['%d', '?'],
641
+ [/:\w+/, '?'],
642
+ [/\bid\b/i, 'rec.recno'],
643
+ ]
644
+ # Translates SQL fragments to a code string. This code string can then be
645
+ # used to construct a code block for KirbyBase. Relies on the SQL_FRAGMENT_TRANSLATIONS
646
+ # series of transformations. Will also remove table names (e.g. people.name)
647
+ # so not safe to use for joins.
648
+ def self.translate_sql_to_code sql_string
649
+ block_string = SQL_FRAGMENT_TRANSLATIONS.inject(sql_string) {|str, (from, to)| str.gsub(from, to)}
650
+ block_string.gsub(/#{table_name}\./, '')
651
+ end
652
+
653
+ # May also be called with a block, e.g.:
654
+ # Book.count {|rec| rec.author_id == @author.id}
655
+ def self.count(*args)
656
+ if args.empty?
657
+ if block_given?
658
+ find(:all, :conditions => Proc.new).size
659
+ else
660
+ self.find(:all).size
661
+ end
662
+ else
663
+ self.find(:all, :conditions => build_conditions_from_options(args)).size
664
+ end
665
+ end
666
+
667
+ # NOT SUPPORTED!!!
668
+ def self.begin_db_transaction
669
+ raise ArgumentError, "#begin_db_transaction called"
670
+ # connection.transaction
671
+ end
672
+
673
+ # NOT SUPPORTED!!!
674
+ def self.commit_db_transaction
675
+ raise ArgumentError, "#commit_db_transaction"
676
+ # connection.commit
677
+ end
678
+
679
+ # NOT SUPPORTED!!!
680
+ def self.rollback_db_transaction
681
+ raise ArgumentError, "#rollback_db_transaction"
682
+ # connection.rollback
683
+ end
684
+
685
+ class << self
686
+ alias_method :__before_ackbar_serialize, :serialize
687
+
688
+ # Serializing a column will cause it to change the column type to :YAML
689
+ # in the database.
690
+ def serialize(attr_name, class_name = Object)
691
+ __before_ackbar_serialize(attr_name, class_name)
692
+ connection.change_column(table_name, attr_name, :YAML)
693
+ end
694
+ end
695
+ end
696
+
697
+ ##############################################################################
698
+ # INSTANCE METHODS: Override SQL based methods in ActiveRecord::Base
699
+ # Instance methods: everything invoked from records instances,
700
+ # e.g. book = Book.find(:first); book.destroy
701
+
702
+ class Base
703
+ # KirbyBase DB Object
704
+ def db
705
+ self.class.db
706
+ end
707
+
708
+ # Table for the AR Model class for this record
709
+ def table
710
+ self.class.table
711
+ end
712
+
713
+ # DATABASE STATEMENTS ######################################################
714
+
715
+ # Updates the associated record with values matching those of the instance attributes.
716
+ def update_without_lock
717
+ table.update{ |rec| rec.recno == id }.set(attributes_to_input_rec)
718
+ end
719
+
720
+ # Updates the associated record with values matching those of the instance
721
+ # attributes. Will also check for a lock (See ActiveRecord::Locking.
722
+ def update_with_lock
723
+ if locking_enabled?
724
+ previous_value = self.lock_version
725
+ self.lock_version = previous_value + 1
726
+
727
+ pk = self.class.primary_key == 'id' ? :recno : :id
728
+ affected_rows = table.update(attributes_to_input_rec){|rec| rec.send(pk) == id and rec.lock_version == previous_value}
729
+
730
+ unless affected_rows == 1
731
+ raise ActiveRecord::StaleObjectError, "Attempted to update a stale object"
732
+ end
733
+ else
734
+ update_without_lock
735
+ end
736
+ end
737
+ alias_method :update_without_callbacks, :update_with_lock
738
+
739
+ # Creates a new record with values matching those of the instance attributes.
740
+ def create_without_callbacks
741
+ input_rec = attributes_to_input_rec
742
+ (input_rec.keys - table.field_names + [:id]).each {|unknown_attribute| input_rec.delete(unknown_attribute)}
743
+ self.id = table.insert(input_rec)
744
+ @new_record = false
745
+ end
746
+
747
+ # Deletes the matching row for this object
748
+ def destroy_without_callbacks
749
+ unless new_record?
750
+ table.delete{ |rec| rec.recno == id }
751
+ end
752
+ freeze
753
+ end
754
+
755
+ # translates the Active-Record instance attributes to a input hash for
756
+ # KirbyBase to be used in #insert or #update
757
+ def attributes_to_input_rec
758
+ field_types = Hash[ *table.field_names.zip(table.field_types).flatten ]
759
+ attributes.inject({}) do |irec, (key, val)|
760
+ irec[key.to_sym] = case field_types[key.to_sym]
761
+ when :Integer
762
+ case val
763
+ when false then 0
764
+ when true then 1
765
+ else val
766
+ end
767
+
768
+ when :Boolean
769
+ case val
770
+ when 0 then false
771
+ when 1 then true
772
+ else val
773
+ end
774
+
775
+ when :Date
776
+ val.is_a?(Time) ? val.to_date : val
777
+
778
+ else val
779
+ end
780
+ irec
781
+ end
782
+ end
783
+ end
784
+
785
+ ##############################################################################
786
+ # Associations adaptation to KirbyBase
787
+ #
788
+ # CHANGES FORM ActiveRecord:
789
+ # All blocks passed to :finder_sql and :counter_sql might be called with
790
+ # multiple parameters:
791
+ # has_one and belongs_to: remote record
792
+ # has_many: remote record and this record
793
+ # has_and_belongs_to_many: join-table record and this record
794
+ # Additionally HasAndBelongsToManyAssociation :delete_sql will be called with
795
+ # three parameters: join record, this record and remote record
796
+ # Make sure that all blocks passed adhere to this convention.
797
+ # See ar_base_tests_runner & ar_model_adaptation for examples.
798
+ module Associations
799
+ class HasOneAssociation
800
+ def find_target
801
+ @association_class.find(:first, :conditions => lambda{|rec| rec.send(@association_class_primary_key_name) == @owner.id}, :order => @options[:order], :include => @options[:include])
802
+ end
803
+ end
804
+
805
+ class HasManyAssociation
806
+ def find(*args)
807
+ options = Base.send(:extract_options_from_args!, args)
808
+
809
+ # If using a custom finder_sql, scan the entire collection.
810
+ if @options[:finder_sql]
811
+ expects_array = args.first.kind_of?(Array)
812
+ ids = args.flatten.compact.uniq
813
+
814
+ if ids.size == 1
815
+ id = ids.first
816
+ record = load_target.detect { |record| id == record.id }
817
+ expects_array? ? [record] : record
818
+ else
819
+ load_target.select { |record| ids.include?(record.id) }
820
+ end
821
+ else
822
+ options[:conditions] = if options[:conditions]
823
+ selector = @association_class.build_conditions_from_options(options)
824
+ if @finder_sql
825
+ lambda{|rec| selector[rec] && @finder_sql[rec]}
826
+ else
827
+ selector
828
+ end
829
+ elsif @finder_sql
830
+ @finder_sql
831
+ end
832
+
833
+
834
+ if options[:order] && @options[:order]
835
+ options[:order] = "#{options[:order]}, #{@options[:order]}"
836
+ elsif @options[:order]
837
+ options[:order] = @options[:order]
838
+ end
839
+
840
+ # Pass through args exactly as we received them.
841
+ args << options
842
+ @association_class.find(*args)
843
+ end
844
+ end
845
+
846
+ def construct_sql
847
+ if @options[:finder_sql]
848
+ raise ArgumentError, "KirbyBase does not support SQL! #{@options[:finder_sql].inspect}" unless @options[:finder_sql].is_a? Proc
849
+ @finder_sql = lambda{|rec| @options[:finder_sql][rec, @owner] }
850
+ else
851
+ extra_conditions = @options[:conditions] ? @association_class.build_conditions_from_options(@options) : nil
852
+ @finder_sql = if extra_conditions
853
+ lambda{ |rec| rec.send(@association_class_primary_key_name) == @owner.id and extra_conditions[rec] }
854
+ else
855
+ lambda{ |rec| rec.send(@association_class_primary_key_name) == @owner.id }
856
+ end
857
+ end
858
+
859
+ if @options[:counter_sql]
860
+ raise ArgumentError, "KirbyBase does not support SQL! #{@options[:counter_sql].inspect}" unless @options[:counter_sql].is_a? Proc
861
+ @counter_sql = lambda{|rec| @options[:counter_sql][rec, @owner] }
862
+ elsif @options[:finder_sql] && @options[:finder_sql].is_a?(Proc)
863
+ @counter_sql = @finder_sql
864
+ else
865
+ extra_conditions = @options[:conditions] ? @association_class.build_conditions_from_options(@options) : nil
866
+ @counter_sql = if @options[:conditions]
867
+ lambda{|rec| rec.send(@association_class_primary_key_name) == @owner.id and extra_conditions[rec]}
868
+ else
869
+ lambda{|rec| rec.send(@association_class_primary_key_name) == @owner.id}
870
+ end
871
+ end
872
+ end
873
+
874
+ def delete_records(records)
875
+ case @options[:dependent]
876
+ when true
877
+ records.each { |r| r.destroy }
878
+
879
+ # when :delete_all
880
+ # ids = records.map{|rec| rec.id}
881
+ # @association_class.table.delete do |rec|
882
+ # rec.send(@association_class_primary_key_name) == @owner.id && ids.include?(rec.recno)
883
+ # end
884
+
885
+ else
886
+ ids = records.map{|rec| rec.id}
887
+ @association_class.table.update do |rec|
888
+ rec.send(@association_class_primary_key_name) == @owner.id && ids.include?(rec.recno)
889
+ end.set { |rec| rec.send "#@association_class_primary_key_name=", nil}
890
+ end
891
+ end
892
+
893
+ def find_target
894
+ @association_class.find(:all,
895
+ :conditions => @finder_sql,
896
+ :order => @options[:order],
897
+ :limit => @options[:limit],
898
+ :joins => @options[:joins],
899
+ :include => @options[:include],
900
+ :group => @options[:group]
901
+ )
902
+ end
903
+
904
+ # DEPRECATED, but still covered by the AR tests
905
+ def find_all(runtime_conditions = nil, orderings = nil, limit = nil, joins = nil)
906
+ if @options[:finder_sql]
907
+ @association_class.find(@finder_sql)
908
+ else
909
+ selector = if runtime_conditions
910
+ runtime_conditions_block = @association_class.build_conditions_from_options(:conditions => runtime_conditions)
911
+ lambda{|rec| runtime_conditions_block[rec] && @finder_sql[rec] }
912
+ else
913
+ @finder_sql
914
+ end
915
+ orderings ||= @options[:order]
916
+ @association_class.find_all(selector, orderings, limit, joins)
917
+ end
918
+ end
919
+
920
+ # Count the number of associated records. All arguments are optional.
921
+ def count(runtime_conditions = nil)
922
+ if @options[:counter_sql]
923
+ @association_class.count(@counter_sql)
924
+ elsif @options[:finder_sql]
925
+ @association_class.count(@finder_sql)
926
+ else
927
+ sql = if runtime_conditions
928
+ runtime_conditions = @association_class.build_conditions_from_options(:conditions => runtime_conditions)
929
+ lambda{|rec| runtime_conditions[rec] && @finder_sql[rec, @owner] }
930
+ else
931
+ @finder_sql
932
+ end
933
+ @association_class.count(sql)
934
+ end
935
+ end
936
+
937
+ def count_records
938
+ count = if has_cached_counter?
939
+ @owner.send(:read_attribute, cached_counter_attribute_name)
940
+ else
941
+ @association_class.count(@counter_sql)
942
+ end
943
+
944
+ @target = [] and loaded if count == 0
945
+
946
+ return count
947
+ end
948
+ end
949
+
950
+ class BelongsToAssociation
951
+ def find_target
952
+ return nil if @owner[@association_class_primary_key_name].nil?
953
+ if @options[:conditions]
954
+ @association_class.find(
955
+ @owner[@association_class_primary_key_name],
956
+ :conditions => @options[:conditions],
957
+ :include => @options[:include]
958
+ )
959
+ else
960
+ @association_class.find(@owner[@association_class_primary_key_name], :include => @options[:include])
961
+ end
962
+ end
963
+ end
964
+
965
+ class HasAndBelongsToManyAssociation
966
+ def find_target
967
+ if @custom_finder_sql
968
+ join_records = @owner.connection.db.get_table(@join_table.to_sym).select do |join_record|
969
+ @options[:finder_sql][join_record, @owner]
970
+ end
971
+ else
972
+ join_records = @owner.connection.db.get_table(@join_table.to_sym).select(&@join_sql)
973
+ end
974
+ association_ids = join_records.map { |rec| rec.send @association_foreign_key }
975
+
976
+ records = if @finder_sql
977
+ @association_class.find :all, :conditions => lambda{|rec| association_ids.include?(rec.recno) && @finder_sql[rec]}
978
+ else
979
+ @association_class.find :all, :conditions => lambda{|rec| association_ids.include?(rec.recno) }
980
+ end
981
+
982
+ # add association properties
983
+ if @owner.connection.db.get_table(@join_table.to_sym).field_names.size > 3
984
+ join_records = join_records.inject({}){|hsh, rec| hsh[rec.send(@association_foreign_key)] = rec; hsh}
985
+ table = @owner.connection.db.get_table(@join_table.to_sym)
986
+ extras = table.field_names - [:recno, @association_foreign_key.to_sym, @association_class_primary_key_name.to_sym]
987
+ records.each do |rec|
988
+ extras.each do |field|
989
+ rec.send :write_attribute, field.to_s, join_records[rec.id].send(field)
990
+ end
991
+ end
992
+ end
993
+
994
+ @options[:uniq] ? uniq(records) : records
995
+ end
996
+
997
+ def method_missing(method, *args, &block)
998
+ if @target.respond_to?(method) || (!@association_class.respond_to?(method) && Class.respond_to?(method))
999
+ super
1000
+ else
1001
+ if method.to_s =~ /^find/
1002
+ records = @association_class.send(method, *args, &block)
1003
+ (records.is_a?(Array) ? records : [records]) & find_target
1004
+ else
1005
+ @association_class.send(method, *args, &block)
1006
+ end
1007
+ end
1008
+ end
1009
+
1010
+ def find(*args)
1011
+ options = ActiveRecord::Base.send(:extract_options_from_args!, args)
1012
+
1013
+ # If using a custom finder_sql, scan the entire collection.
1014
+ if @options[:finder_sql]
1015
+ expects_array = args.first.kind_of?(Array)
1016
+ ids = args.flatten.compact.uniq
1017
+
1018
+ if ids.size == 1
1019
+ id = ids.first.to_i
1020
+ record = load_target.detect { |record| id == record.id }
1021
+ if expects_array
1022
+ [record].compact
1023
+ elsif record.nil?
1024
+ raise RecordNotFound
1025
+ else
1026
+ record
1027
+ end
1028
+ else
1029
+ load_target.select { |record| ids.include?(record.id) }
1030
+ end
1031
+ else
1032
+ options[:conditions] = if options[:conditions]
1033
+ selector = @association_class.build_conditions_from_options(options)
1034
+ @finder_sql ? lambda{|rec| selector[rec] && @finder_sql[rec]} : selector
1035
+ elsif @finder_sql
1036
+ @finder_sql
1037
+ end
1038
+
1039
+ options[:readonly] ||= false
1040
+ options[:order] ||= @options[:order]
1041
+
1042
+ join_records = @owner.connection.db.get_table(@join_table.to_sym).select(&@join_sql)
1043
+ association_ids = join_records.map { |rec| rec.send @association_foreign_key }
1044
+ association_ids &= args if args.all? {|a| Integer === a }
1045
+ records = @association_class.find(:all, options).select{|rec| association_ids.include?(rec.id)}
1046
+ if args.first.kind_of?(Array)
1047
+ records.compact
1048
+ elsif records.first.nil?
1049
+ raise RecordNotFound
1050
+ else
1051
+ records.first
1052
+ end
1053
+ end
1054
+ end
1055
+
1056
+ def insert_record(record)
1057
+ if record.new_record?
1058
+ return false unless record.save
1059
+ end
1060
+
1061
+ if @options[:insert_sql]
1062
+ raise ArgumentError, "SQL not supported by KirbyBase! #{@options[:insert_sql]}"
1063
+ @owner.connection.execute(interpolate_sql(@options[:insert_sql], record))
1064
+ else
1065
+ columns = @owner.connection.columns(@join_table, "#{@join_table} Columns")
1066
+
1067
+ attributes = columns.inject({}) do |attributes, column|
1068
+ case column.name
1069
+ when @association_class_primary_key_name
1070
+ attributes[column.name] = @owner.id
1071
+ when @association_foreign_key
1072
+ attributes[column.name] = record.id
1073
+ else
1074
+ if record.attributes.has_key?(column.name)
1075
+ attributes[column.name] = record[column.name]
1076
+ end
1077
+ end
1078
+ attributes
1079
+ end
1080
+
1081
+ input_rec = Hash[*@owner.send(:quoted_column_names, attributes).zip(attributes.values).flatten].symbolize_keys
1082
+ @owner.connection.db.get_table(@join_table.to_sym).insert(input_rec)
1083
+ end
1084
+
1085
+ return true
1086
+ end
1087
+
1088
+ def delete_records(records)
1089
+ if sql = @options[:delete_sql]
1090
+ delete_conditions = if sql.is_a?(Proc)
1091
+ sql
1092
+ else
1093
+ association_selector = @association_class.build_conditions_from_options(:conditions => sql)
1094
+ lambda do |join_rec, owner, record|
1095
+ rec.send(@association_foreign_key) == @owner.id &&
1096
+ record = @associtation_class.find(rec.send(@association_class_primary_key_name)) &&
1097
+ association_selector[record]
1098
+ end
1099
+ end
1100
+ records.each do |record|
1101
+ delete_selector = lambda{|join_rec| delete_conditions[join_rec, @owner, record]}
1102
+ @owner.connection.db.get_table(@join_table.to_sym).delete(&delete_selector)
1103
+ end
1104
+ else
1105
+ ids = records.map { |rec| rec.id }
1106
+ @owner.connection.db.get_table(@join_table.to_sym).delete do |rec|
1107
+ rec.send(@association_class_primary_key_name) == @owner.id && ids.include?(rec.send(@association_foreign_key))
1108
+ end
1109
+ end
1110
+ end
1111
+
1112
+ def construct_sql
1113
+ if @options[:finder_sql]
1114
+ @custom_finder_sql = lambda{|rec| @options[:finder_sql][rec, @owner, @association_class.find(rec.send(@association_class_primary_key_name))] }
1115
+ else
1116
+ # Need to run @join_sql as well - see #find above
1117
+ @finder_sql = @association_class.build_conditions_from_options(@options)
1118
+ end
1119
+
1120
+ # this should be run on the join_table
1121
+ # "LEFT JOIN #{@join_table} ON #{@association_class.table_name}.#{@association_class.primary_key} = #{@join_table}.#{@association_foreign_key}"
1122
+ @join_sql = lambda{|rec| rec.send(@association_class_primary_key_name) == @owner.id}
1123
+ end
1124
+
1125
+ end
1126
+ end
1127
+
1128
+ ##############################################################################
1129
+ # A few methods using raw SQL need to be adapted
1130
+ class Migrator
1131
+ def self.current_version
1132
+ Base.connection.get_table(schema_info_table_name.to_sym).select[0].version.to_i rescue 0
1133
+ end
1134
+
1135
+ def set_schema_version(version)
1136
+ Base.connection.get_table(self.class.schema_info_table_name.to_sym).update_all(:version => (down? ? version.to_i - 1 : version.to_i))
1137
+ end
1138
+ end
1139
+
1140
+ ##############################################################################
1141
+ ### WARNING: The following two changes should go in the ar_test_runner as well!!
1142
+
1143
+ # Needed to override #define as it was using SQL to update the schema version
1144
+ # information.
1145
+ class Schema
1146
+ def self.define(info={}, &block)
1147
+ instance_eval(&block)
1148
+
1149
+ unless info.empty?
1150
+ initialize_schema_information
1151
+ ActiveRecord::Base.connection.get_table(ActiveRecord::Migrator.schema_info_table_name.to_sym).update_all(info)
1152
+ end
1153
+ end
1154
+ end
1155
+
1156
+ # Override SQL to retrieve the schema info version number.
1157
+ class SchemaDumper
1158
+ def initialize(connection)
1159
+ @connection = connection
1160
+ @types = @connection.native_database_types
1161
+ @info = @connection.get_table(:schema_info).select[0] rescue nil
1162
+ end
1163
+ end
1164
+
1165
+ ### WARNING: The above two changes should go in the ar_test_runner as well!!
1166
+ ##############################################################################
1167
+ end
1168
+
1169
+ ###############################################################################
1170
+ # Fixtures adaptation to KirbyRecord
1171
+ require 'active_record/fixtures'
1172
+
1173
+ # Override raw SQL for ActiveRecord insert/delete Fixtures
1174
+ class Fixtures
1175
+ # Override raw SQL
1176
+ def delete_existing_fixtures
1177
+ begin
1178
+ tbl = @connection.db.get_table(@table_name.to_sym)
1179
+ tbl.clear
1180
+ @connection.db.engine.reset_recno_ctr(tbl)
1181
+ rescue => detail
1182
+ STDERR.puts detail, @table_name
1183
+ end
1184
+ end
1185
+
1186
+ # Override raw SQL
1187
+ def insert_fixtures
1188
+ tbl = @connection.db.get_table(@table_name.to_sym)
1189
+ column_types = Hash[*tbl.field_names.zip(tbl.field_types).flatten]
1190
+ items = begin
1191
+ values.sort_by { |fix| fix['id'] }
1192
+ rescue
1193
+ values
1194
+ end
1195
+ items.each do |fixture|
1196
+ insert_data = fixture.to_hash.symbolize_keys.inject({}) do |data, (col, val)|
1197
+ data[col] = case column_types[col]
1198
+ when :String then val.to_s
1199
+ when :Integer then val.to_i rescue (val ? 1 : 0)
1200
+ when :Float then val.to_f
1201
+ when :Time then Time.parse val.to_s
1202
+ when :Date then Date.parse val.to_s
1203
+ when :DateTime then DateTime.parse(val.asctime)
1204
+ else val # ignore Memo, Blob and YAML for the moment
1205
+ end
1206
+ data
1207
+ end
1208
+ insert_data.delete(:id)
1209
+ recno = tbl.insert(insert_data)
1210
+ fixture.recno = recno
1211
+ end
1212
+ end
1213
+ end
1214
+
1215
+ # Override raw finder SQL for ActiveRecord Fixtures
1216
+ class Fixture
1217
+ attr_accessor :recno
1218
+ def find
1219
+ if Object.const_defined?(@class_name)
1220
+ klass = Object.const_get(@class_name)
1221
+ klass.find(:first) { |rec| rec.recno == recno }
1222
+ end
1223
+ end
1224
+ end
1225
+
1226
+ ################################################################################
1227
+ # Stdlib extensions
1228
+ class Array
1229
+ # Modifies the receiver - sorts in place by the given attribute / block
1230
+ def sort_by!(*args, &bl)
1231
+ self.replace self.sort_by(*args, &bl)
1232
+ end
1233
+
1234
+ # Will now accept a symbol or a block. Block behaves as before, symbol will
1235
+ # be used as the property on which value to sort elements
1236
+ def sort_by(*args, &bl)
1237
+ if not bl.nil?
1238
+ super &bl
1239
+ else
1240
+ super &lambda{ |item| item.send(args.first) }
1241
+ end
1242
+ end
1243
+
1244
+ # A stable sort - preserves the order in which elements were encountred. Used
1245
+ # in multi-field sorts, where the second sort should preserve the order form the
1246
+ # first sort.
1247
+ def stable_sort
1248
+ n = 0
1249
+ sort_by {|x| n+= 1; [x, n]}
1250
+ end
1251
+
1252
+ # Stable sort by a particular attribute.
1253
+ def stable_sort_by(*args, &bl)
1254
+ n = 0
1255
+ if not bl.nil?
1256
+ super &bl
1257
+ sort_by { |item| n+=1; [bl[item], n] }
1258
+ else
1259
+ sort_by { |item| n+=1; [item.send(args.first), n] }
1260
+ end
1261
+ end
1262
+ end
1263
+
1264
+ # Stdlib extensions
1265
+ class Object
1266
+ # The inverse to ary.include?(self)
1267
+ def in *ary
1268
+ if ary.size == 1 and ary[0].is_a?(Array)
1269
+ ary[0].include?(self)
1270
+ else
1271
+ ary.include?(self)
1272
+ end
1273
+ end
1274
+ end
1275
+