ackbar 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +39 -0
- data/README +88 -0
- data/Rakefile +75 -0
- data/TODO +14 -0
- data/kirbybase_adapter.rb +1275 -0
- data/test/001_schema_migration_test.rb +32 -0
- data/test/ar_base_tests_runner.rb +415 -0
- data/test/ar_model_adaptation.rb +98 -0
- data/test/connection.rb +27 -0
- data/test/create_dbs_for_ar_tests.rb +171 -0
- data/test/fixtures/authors.yml +6 -0
- data/test/fixtures/authors_books.yml +8 -0
- data/test/fixtures/books.yml +4 -0
- data/test/fixtures/pages.yml +98 -0
- data/test/fixtures/publishers.yml +7 -0
- data/test/kb_associations_test.rb +107 -0
- data/test/kb_basics_test.rb +204 -0
- data/test/kb_schema_test.rb +169 -0
- data/test/kb_sql_to_code_test.rb +79 -0
- data/test/kb_stdlib_extensions_test.rb +38 -0
- data/test/model.rb +27 -0
- data/test/schema.rb +41 -0
- data/test/test_helper.rb +49 -0
- metadata +89 -0
data/CHANGELOG
ADDED
@@ -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
|
+
|
data/Rakefile
ADDED
@@ -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
|
+
|