ackbar 0.1.0

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