ackbar 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+
|