sequel 3.4.0 → 3.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +84 -0
- data/Rakefile +1 -1
- data/doc/cheat_sheet.rdoc +5 -2
- data/doc/opening_databases.rdoc +2 -0
- data/doc/release_notes/3.5.0.txt +510 -0
- data/lib/sequel/adapters/ado.rb +3 -1
- data/lib/sequel/adapters/ado/mssql.rb +2 -2
- data/lib/sequel/adapters/do.rb +2 -11
- data/lib/sequel/adapters/do/mysql.rb +7 -0
- data/lib/sequel/adapters/do/postgres.rb +2 -2
- data/lib/sequel/adapters/firebird.rb +3 -3
- data/lib/sequel/adapters/informix.rb +3 -3
- data/lib/sequel/adapters/jdbc/h2.rb +3 -3
- data/lib/sequel/adapters/jdbc/mssql.rb +7 -0
- data/lib/sequel/adapters/mysql.rb +60 -21
- data/lib/sequel/adapters/odbc.rb +1 -1
- data/lib/sequel/adapters/openbase.rb +3 -3
- data/lib/sequel/adapters/oracle.rb +1 -5
- data/lib/sequel/adapters/postgres.rb +3 -3
- data/lib/sequel/adapters/shared/mssql.rb +142 -33
- data/lib/sequel/adapters/shared/mysql.rb +54 -31
- data/lib/sequel/adapters/shared/oracle.rb +17 -6
- data/lib/sequel/adapters/shared/postgres.rb +7 -7
- data/lib/sequel/adapters/shared/progress.rb +3 -3
- data/lib/sequel/adapters/shared/sqlite.rb +3 -17
- data/lib/sequel/connection_pool.rb +4 -6
- data/lib/sequel/core.rb +29 -113
- data/lib/sequel/database.rb +14 -12
- data/lib/sequel/dataset.rb +8 -21
- data/lib/sequel/dataset/convenience.rb +1 -1
- data/lib/sequel/dataset/graph.rb +9 -2
- data/lib/sequel/dataset/sql.rb +170 -104
- data/lib/sequel/exceptions.rb +3 -0
- data/lib/sequel/extensions/looser_typecasting.rb +21 -0
- data/lib/sequel/extensions/named_timezones.rb +61 -0
- data/lib/sequel/extensions/schema_dumper.rb +7 -1
- data/lib/sequel/extensions/sql_expr.rb +122 -0
- data/lib/sequel/extensions/string_date_time.rb +4 -4
- data/lib/sequel/extensions/thread_local_timezones.rb +48 -0
- data/lib/sequel/model/associations.rb +105 -45
- data/lib/sequel/model/base.rb +37 -28
- data/lib/sequel/plugins/active_model.rb +35 -0
- data/lib/sequel/plugins/association_dependencies.rb +96 -0
- data/lib/sequel/plugins/class_table_inheritance.rb +214 -0
- data/lib/sequel/plugins/force_encoding.rb +61 -0
- data/lib/sequel/plugins/many_through_many.rb +32 -11
- data/lib/sequel/plugins/nested_attributes.rb +7 -2
- data/lib/sequel/plugins/subclasses.rb +45 -0
- data/lib/sequel/plugins/touch.rb +118 -0
- data/lib/sequel/plugins/typecast_on_load.rb +61 -0
- data/lib/sequel/sql.rb +31 -30
- data/lib/sequel/timezones.rb +161 -0
- data/lib/sequel/version.rb +1 -1
- data/spec/adapters/mssql_spec.rb +262 -0
- data/spec/adapters/mysql_spec.rb +46 -8
- data/spec/adapters/postgres_spec.rb +6 -3
- data/spec/adapters/spec_helper.rb +21 -0
- data/spec/adapters/sqlite_spec.rb +1 -1
- data/spec/core/connection_pool_spec.rb +1 -1
- data/spec/core/database_spec.rb +27 -1
- data/spec/core/dataset_spec.rb +63 -1
- data/spec/core/object_graph_spec.rb +1 -1
- data/spec/core/schema_spec.rb +1 -0
- data/spec/extensions/active_model_spec.rb +47 -0
- data/spec/extensions/association_dependencies_spec.rb +108 -0
- data/spec/extensions/class_table_inheritance_spec.rb +252 -0
- data/spec/extensions/force_encoding_spec.rb +75 -0
- data/spec/extensions/looser_typecasting_spec.rb +39 -0
- data/spec/extensions/many_through_many_spec.rb +60 -2
- data/spec/extensions/named_timezones_spec.rb +72 -0
- data/spec/extensions/nested_attributes_spec.rb +29 -1
- data/spec/extensions/schema_dumper_spec.rb +10 -0
- data/spec/extensions/spec_helper.rb +1 -1
- data/spec/extensions/sql_expr_spec.rb +89 -0
- data/spec/extensions/subclasses_spec.rb +52 -0
- data/spec/extensions/thread_local_timezones_spec.rb +45 -0
- data/spec/extensions/touch_spec.rb +155 -0
- data/spec/extensions/typecast_on_load_spec.rb +60 -0
- data/spec/integration/database_test.rb +8 -0
- data/spec/integration/dataset_test.rb +9 -9
- data/spec/integration/plugin_test.rb +139 -0
- data/spec/integration/schema_test.rb +7 -7
- data/spec/integration/spec_helper.rb +32 -1
- data/spec/integration/timezone_test.rb +3 -3
- data/spec/integration/transaction_test.rb +1 -1
- data/spec/integration/type_test.rb +6 -6
- data/spec/model/association_reflection_spec.rb +18 -0
- data/spec/model/associations_spec.rb +169 -9
- data/spec/model/base_spec.rb +2 -0
- data/spec/model/eager_loading_spec.rb +82 -2
- data/spec/model/model_spec.rb +8 -1
- data/spec/model/record_spec.rb +52 -9
- metadata +33 -23
data/lib/sequel/model/base.rb
CHANGED
@@ -317,8 +317,9 @@ module Sequel
|
|
317
317
|
# You can set it to nil to not have a primary key, but that
|
318
318
|
# cause certain things not to work, see no_primary_key.
|
319
319
|
def set_primary_key(*key)
|
320
|
+
key = key.flatten
|
320
321
|
@simple_pk = key.length == 1 ? db.literal(key.first) : nil
|
321
|
-
@primary_key = (key.length == 1) ? key[0] : key
|
322
|
+
@primary_key = (key.length == 1) ? key[0] : key
|
322
323
|
end
|
323
324
|
|
324
325
|
# Set the columns to restrict in new/set/update. Using this means that
|
@@ -489,7 +490,7 @@ module Sequel
|
|
489
490
|
# same name, caching the result in an instance variable. Define
|
490
491
|
# standard attr_writer method for modifying that instance variable
|
491
492
|
def self.class_attr_overridable(*meths) # :nodoc:
|
492
|
-
meths.each{|meth| class_eval("def #{meth}; !defined?(@#{meth}) ? (@#{meth} = self.class.#{meth}) : @#{meth} end")}
|
493
|
+
meths.each{|meth| class_eval("def #{meth}; !defined?(@#{meth}) ? (@#{meth} = self.class.#{meth}) : @#{meth} end", __FILE__, __LINE__)}
|
493
494
|
attr_writer(*meths)
|
494
495
|
end
|
495
496
|
|
@@ -498,7 +499,7 @@ module Sequel
|
|
498
499
|
#
|
499
500
|
# define_method(meth){self.class.send(meth)}
|
500
501
|
def self.class_attr_reader(*meths) # :nodoc:
|
501
|
-
meths.each{|meth| class_eval("def #{meth}; model.#{meth} end")}
|
502
|
+
meths.each{|meth| class_eval("def #{meth}; model.#{meth} end", __FILE__, __LINE__)}
|
502
503
|
end
|
503
504
|
|
504
505
|
private_class_method :class_attr_overridable, :class_attr_reader
|
@@ -539,21 +540,19 @@ module Sequel
|
|
539
540
|
@values[column]
|
540
541
|
end
|
541
542
|
|
542
|
-
# Sets value
|
543
|
-
#
|
544
|
-
#
|
545
|
-
#
|
546
|
-
# value that is different from the column's current value but is the
|
547
|
-
# same after typecasting will also cause changed_columns to include the
|
548
|
-
# column.
|
543
|
+
# Sets the value for the given column. If typecasting is enabled for
|
544
|
+
# this object, typecast the value based on the column's type.
|
545
|
+
# If this a a new record or the typecasted value isn't the same
|
546
|
+
# as the current value for the column, mark the column as changed.
|
549
547
|
def []=(column, value)
|
550
548
|
# If it is new, it doesn't have a value yet, so we should
|
551
549
|
# definitely set the new value.
|
552
550
|
# If the column isn't in @values, we can't assume it is
|
553
551
|
# NULL in the database, so assume it has changed.
|
554
|
-
|
552
|
+
v = typecast_value(column, value)
|
553
|
+
if new? || !@values.include?(column) || v != @values[column]
|
555
554
|
changed_columns << column unless changed_columns.include?(column)
|
556
|
-
@values[column] =
|
555
|
+
@values[column] = v
|
557
556
|
end
|
558
557
|
end
|
559
558
|
|
@@ -655,9 +654,10 @@ module Sequel
|
|
655
654
|
end
|
656
655
|
|
657
656
|
# Whether this object has been modified since last saved, used by
|
658
|
-
# save_changes to determine whether changes should be saved
|
657
|
+
# save_changes to determine whether changes should be saved. New
|
658
|
+
# values are always considered modified.
|
659
659
|
def modified?
|
660
|
-
!changed_columns.empty?
|
660
|
+
new? || !changed_columns.empty?
|
661
661
|
end
|
662
662
|
|
663
663
|
# Returns true if the current instance represents a new record.
|
@@ -802,6 +802,22 @@ module Sequel
|
|
802
802
|
self
|
803
803
|
end
|
804
804
|
|
805
|
+
def _insert
|
806
|
+
ds = model.dataset
|
807
|
+
if ds.respond_to?(:insert_select) and h = ds.insert_select(@values)
|
808
|
+
@values = h
|
809
|
+
nil
|
810
|
+
else
|
811
|
+
iid = ds.insert(@values)
|
812
|
+
# if we have a regular primary key and it's not set in @values,
|
813
|
+
# we assume it's the last inserted id
|
814
|
+
if (pk = autoincrementing_primary_key) && pk.is_a?(Symbol) && !@values[pk]
|
815
|
+
@values[pk] = iid
|
816
|
+
end
|
817
|
+
pk
|
818
|
+
end
|
819
|
+
end
|
820
|
+
|
805
821
|
# Refresh using a particular dataset, used inside save to make sure the same server
|
806
822
|
# is used for reading newly inserted values from the database
|
807
823
|
def _refresh(dataset)
|
@@ -817,19 +833,8 @@ module Sequel
|
|
817
833
|
return save_failure(:save) if before_save == false
|
818
834
|
if new?
|
819
835
|
return save_failure(:create) if before_create == false
|
820
|
-
|
821
|
-
|
822
|
-
@values = h
|
823
|
-
@this = nil
|
824
|
-
else
|
825
|
-
iid = ds.insert(@values)
|
826
|
-
# if we have a regular primary key and it's not set in @values,
|
827
|
-
# we assume it's the last inserted id
|
828
|
-
if (pk = autoincrementing_primary_key) && pk.is_a?(Symbol) && !@values[pk]
|
829
|
-
@values[pk] = iid
|
830
|
-
end
|
831
|
-
@this = nil if pk
|
832
|
-
end
|
836
|
+
pk = _insert
|
837
|
+
@this = nil if pk
|
833
838
|
@new = false
|
834
839
|
@was_new = true
|
835
840
|
after_create
|
@@ -850,7 +855,7 @@ module Sequel
|
|
850
855
|
changed_columns.reject!{|c| columns.include?(c)}
|
851
856
|
end
|
852
857
|
Array(primary_key).each{|x| @columns_updated.delete(x)}
|
853
|
-
|
858
|
+
_update(@columns_updated) unless @columns_updated.empty?
|
854
859
|
after_update
|
855
860
|
after_save
|
856
861
|
@columns_updated = nil
|
@@ -858,6 +863,10 @@ module Sequel
|
|
858
863
|
self
|
859
864
|
end
|
860
865
|
|
866
|
+
def _update(columns)
|
867
|
+
this.update(columns)
|
868
|
+
end
|
869
|
+
|
861
870
|
# Default inspection output for the values hash, overwrite to change what #inspect displays.
|
862
871
|
def inspect_values
|
863
872
|
@values.inspect
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Sequel
|
2
|
+
module Plugins
|
3
|
+
# The ActiveModel plugin makes Sequel::Model objects the
|
4
|
+
# pass the ActiveModel::Lint tests, which should
|
5
|
+
# hopefully mean full ActiveModel compliance. This should
|
6
|
+
# allow the full support of Sequel::Model objects in Rails 3.
|
7
|
+
module ActiveModel
|
8
|
+
module InstanceMethods
|
9
|
+
# Record that an object was destroyed, for later use by
|
10
|
+
# destroyed?
|
11
|
+
def after_destroy
|
12
|
+
super
|
13
|
+
@destroyed = true
|
14
|
+
end
|
15
|
+
|
16
|
+
# Whether the object was destroyed by destroy. Not true
|
17
|
+
# for objects that were deleted.
|
18
|
+
def destroyed?
|
19
|
+
@destroyed == true
|
20
|
+
end
|
21
|
+
|
22
|
+
# An alias for new?
|
23
|
+
def new_record?
|
24
|
+
new?
|
25
|
+
end
|
26
|
+
|
27
|
+
# With the ActiveModel plugin, Sequel model objects are already
|
28
|
+
# compliant, so this returns self.
|
29
|
+
def to_model
|
30
|
+
self
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
module Sequel
|
2
|
+
module Plugins
|
3
|
+
# The AssociationDependencies plugin allows you do easily set up before and/or after destroy hooks
|
4
|
+
# for destroying, deleting, or nullifying associated model objects. The following
|
5
|
+
# association types support the following dependency actions:
|
6
|
+
#
|
7
|
+
# * :many_to_many - :nullify (removes all related entries in join table)
|
8
|
+
# * :many_to_one - :delete, :destroy
|
9
|
+
# * :one_to_many - :delete, :destroy, :nullify (sets foreign key to NULL for all associated objects)
|
10
|
+
#
|
11
|
+
# This plugin works directly with the association datasets and does not use any cached association values.
|
12
|
+
# The :delete action will delete all associated objects from the database in a single SQL call.
|
13
|
+
# The :destroy action will load each associated object from the database and call the destroy method on it.
|
14
|
+
#
|
15
|
+
# To set up an association dependency, you must provide a hash with association name symbols
|
16
|
+
# and dependency action values. You can provide the hash to the plugin call itself or
|
17
|
+
# to the add_association_dependencies method:
|
18
|
+
#
|
19
|
+
# Business.plugin :association_dependencies, :address=>delete
|
20
|
+
# # or:
|
21
|
+
# Artist.plugin :association_dependencies
|
22
|
+
# Artist.add_association_dependencies :albums=>:destroy, :reviews=>:delete, :tags=>:nullify
|
23
|
+
module AssociationDependencies
|
24
|
+
# Mapping of association types to when the dependency calls should be made (either
|
25
|
+
# :before for in before_destroy or :after for in after_destroy)
|
26
|
+
ASSOCIATION_MAPPING = {:one_to_many=>:before, :many_to_one=>:after, :many_to_many=>:before}
|
27
|
+
|
28
|
+
# The valid dependence actions
|
29
|
+
DEPENDENCE_ACTIONS = [:delete, :destroy, :nullify]
|
30
|
+
|
31
|
+
# Initialize the association_dependencies hash for this model.
|
32
|
+
def self.apply(model, hash={})
|
33
|
+
model.instance_eval{@association_dependencies = {:before_delete=>[], :before_destroy=>[], :before_nullify=>[], :after_delete=>[], :after_destroy=>[]}}
|
34
|
+
end
|
35
|
+
|
36
|
+
# Call add_association_dependencies with any dependencies given in the plugin call.
|
37
|
+
def self.configure(model, hash={})
|
38
|
+
model.add_association_dependencies(hash) unless hash.empty?
|
39
|
+
end
|
40
|
+
|
41
|
+
module ClassMethods
|
42
|
+
# A hash specifying the association dependencies for each model. The keys
|
43
|
+
# are symbols indicating the type of action and when it should be executed
|
44
|
+
# (e.g. :before_delete). Values are an array of method symbols.
|
45
|
+
# For before_nullify, the symbols are remove_all_association methods. For other
|
46
|
+
# types, the symbols are association_dataset methods, on which delete or
|
47
|
+
# destroy is called.
|
48
|
+
attr_reader :association_dependencies
|
49
|
+
|
50
|
+
# Add association dependencies to this model. The hash should have association name
|
51
|
+
# symbol keys and dependency action symbol values (e.g. :albums=>:destroy).
|
52
|
+
def add_association_dependencies(hash)
|
53
|
+
hash.each do |association, action|
|
54
|
+
raise(Error, "Nonexistent association: #{association}") unless r = association_reflection(association)
|
55
|
+
raise(Error, "Invalid dependence action type: association: #{association}, dependence action: #{action}") unless DEPENDENCE_ACTIONS.include?(action)
|
56
|
+
raise(Error, "Invalid association type: association: #{association}, type: #{r[:type]}") unless time = ASSOCIATION_MAPPING[r[:type]]
|
57
|
+
association_dependencies[:"#{time}_#{action}"] << if action == :nullify
|
58
|
+
raise(Error, "Can't nullify many_to_one associated objects: association: #{association}") if r[:type] == :many_to_one
|
59
|
+
r.remove_all_method
|
60
|
+
else
|
61
|
+
raise(Error, "Can only nullify many_to_many associations: association: #{association}") if r[:type] == :many_to_many
|
62
|
+
r.dataset_method
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Copy the current model object's association_dependencies into the subclass.
|
68
|
+
def inherited(subclass)
|
69
|
+
super
|
70
|
+
ad = association_dependencies.dup
|
71
|
+
ad.keys.each{|k| ad[k] = ad[k].dup}
|
72
|
+
subclass.instance_variable_set(:@association_dependencies, ad)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
module InstanceMethods
|
77
|
+
# Run the delete and destroy association dependency actions for
|
78
|
+
# many_to_one associations.
|
79
|
+
def after_destroy
|
80
|
+
super
|
81
|
+
model.association_dependencies[:after_delete].each{|m| send(m).delete}
|
82
|
+
model.association_dependencies[:after_destroy].each{|m| send(m).destroy}
|
83
|
+
end
|
84
|
+
|
85
|
+
# Run the delete, destroy, and nullify association dependency actions for
|
86
|
+
# *_to_many associations.
|
87
|
+
def before_destroy
|
88
|
+
model.association_dependencies[:before_delete].each{|m| send(m).delete}
|
89
|
+
model.association_dependencies[:before_destroy].each{|m| send(m).destroy}
|
90
|
+
model.association_dependencies[:before_nullify].each{|m| send(m)}
|
91
|
+
super
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,214 @@
|
|
1
|
+
module Sequel
|
2
|
+
module Plugins
|
3
|
+
# The class_table_inheritance plugin allows you to model inheritance in the
|
4
|
+
# database using a table per model class in the hierarchy, with only columns
|
5
|
+
# unique to that model class (or subclass hierarchy) being stored in the related
|
6
|
+
# table. For example, with this hierarchy:
|
7
|
+
#
|
8
|
+
# Employee
|
9
|
+
# / \
|
10
|
+
# Staff Manager
|
11
|
+
# |
|
12
|
+
# Executive
|
13
|
+
#
|
14
|
+
# the following database schema may be used (table - columns):
|
15
|
+
#
|
16
|
+
# * employees - id, name, kind
|
17
|
+
# * staff - id, manager_id
|
18
|
+
# * managers - id, num_staff
|
19
|
+
# * executives - id, num_managers
|
20
|
+
#
|
21
|
+
# The class_table_inheritance plugin assumes that the main table
|
22
|
+
# (e.g. employees) has a primary key field (usually autoincrementing),
|
23
|
+
# and all other tables have a foreign key of the same name that points
|
24
|
+
# to the same key in their superclass's table. For example:
|
25
|
+
#
|
26
|
+
# * employees.id - primary key, autoincrementing
|
27
|
+
# * staff.id - foreign key referencing employees(id)
|
28
|
+
# * managers.id - foreign key referencing employees(id)
|
29
|
+
# * executives.id - foreign key referencing managers(id)
|
30
|
+
#
|
31
|
+
# When using the class_table_inheritance plugin, subclasses use joined
|
32
|
+
# datasets:
|
33
|
+
#
|
34
|
+
# Employee.dataset.sql # SELECT * FROM employees
|
35
|
+
# Manager.dataset.sql # SELECT * FROM employees
|
36
|
+
# # INNER JOIN managers USING (id)
|
37
|
+
# Executive.dataset.sql # SELECT * FROM employees
|
38
|
+
# # INNER JOIN managers USING (id)
|
39
|
+
# # INNER JOIN executives USING (id)
|
40
|
+
#
|
41
|
+
# This allows Executive.all to return instances with all attributes
|
42
|
+
# loaded. The plugin overrides the deleting, inserting, and updating
|
43
|
+
# in the model to work with multiple tables, by handling each table
|
44
|
+
# individually.
|
45
|
+
#
|
46
|
+
# This plugin allows the use of a :key option when loading to mark
|
47
|
+
# a column holding a class name. This allows methods on the
|
48
|
+
# superclass to return instances of specific subclasses.
|
49
|
+
# This plugin also requires the lazy_attributes plugin and uses it to
|
50
|
+
# return subclass specific attributes that would not be loaded
|
51
|
+
# when calling superclass methods (since those wouldn't join
|
52
|
+
# to the subclass tables). For example:
|
53
|
+
#
|
54
|
+
# a = Employee.all # [<#Staff>, <#Manager>, <#Executive>]
|
55
|
+
# a.first.values # {:id=>1, name=>'S', :kind=>'Staff'}
|
56
|
+
# a.first.manager_id # Loads the manager_id attribute from the database
|
57
|
+
module ClassTableInheritance
|
58
|
+
# The class_table_inheritance plugin requires the lazy_attributes plugin
|
59
|
+
# to handle lazily-loaded attributes for subclass instances returned
|
60
|
+
# by superclass methods.
|
61
|
+
def self.apply(model, opts={}, &block)
|
62
|
+
model.plugin :lazy_attributes
|
63
|
+
end
|
64
|
+
|
65
|
+
# Initialize the per-model data structures and set the dataset's row_proc
|
66
|
+
# to check for the :key option column for the type of class when loading objects.
|
67
|
+
# Options:
|
68
|
+
# * :key - The column symbol holding the name of the model class this
|
69
|
+
# is an instance of. Necessary if you want to call model methods
|
70
|
+
# using the superclass, but have them return subclass instances.
|
71
|
+
# * :table_map - Hash with class name symbol keys and table name symbol
|
72
|
+
# values. Necessary if the implicit table name for the model class
|
73
|
+
# does not match the database table name
|
74
|
+
# Example:
|
75
|
+
# class Employee < Sequel::Model
|
76
|
+
# plugin :class_table_inheritance, :key=>:kind, :table_map=>{:Staff=>:staff}
|
77
|
+
# end
|
78
|
+
def self.configure(model, opts={}, &block)
|
79
|
+
model.instance_eval do
|
80
|
+
m = method(:constantize)
|
81
|
+
@cti_base_model = self
|
82
|
+
@cti_key = key = opts[:key]
|
83
|
+
@cti_tables = [table_name]
|
84
|
+
@cti_columns = {table_name=>columns}
|
85
|
+
@cti_table_map = opts[:table_map] || {}
|
86
|
+
dataset.row_proc = lambda{|r| (m.call(r[key]) rescue model).load(r)}
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
module ClassMethods
|
91
|
+
# The parent/root/base model for this class table inheritance hierarchy.
|
92
|
+
# This is the only model in the hierarchy that load the
|
93
|
+
# class_table_inheritance plugin.
|
94
|
+
attr_reader :cti_base_model
|
95
|
+
|
96
|
+
# Hash with table name symbol keys and arrays of column symbol values,
|
97
|
+
# giving the columns to update in each backing database table.
|
98
|
+
attr_reader :cti_columns
|
99
|
+
|
100
|
+
# The column containing the class name as a string. Used to
|
101
|
+
# return instances of subclasses when calling the superclass's
|
102
|
+
# load method.
|
103
|
+
attr_reader :cti_key
|
104
|
+
|
105
|
+
# An array of table symbols that back this model. The first is
|
106
|
+
# cti_base_model table symbol, and the last is the current model
|
107
|
+
# table symbol.
|
108
|
+
attr_reader :cti_tables
|
109
|
+
|
110
|
+
# A hash with class name symbol keys and table name symbol values.
|
111
|
+
# Specified with the :table_map option to the plugin, and used if
|
112
|
+
# the implicit naming is incorrect.
|
113
|
+
attr_reader :cti_table_map
|
114
|
+
|
115
|
+
# Add the appropriate data structures to the subclass. Does not
|
116
|
+
# allow anonymous subclasses to be created, since they would not
|
117
|
+
# be mappable to a table.
|
118
|
+
def inherited(subclass)
|
119
|
+
cc = cti_columns
|
120
|
+
ck = cti_key
|
121
|
+
ct = cti_tables.dup
|
122
|
+
ctm = cti_table_map.dup
|
123
|
+
cbm = cti_base_model
|
124
|
+
pk = primary_key
|
125
|
+
ds = dataset
|
126
|
+
subclass.instance_eval do
|
127
|
+
raise(Error, "cannot create anonymous subclass for model class using class_table_inheritance") if !(n = name) || n.empty?
|
128
|
+
table = ctm[n.to_sym] || implicit_table_name
|
129
|
+
columns = db.from(table).columns
|
130
|
+
@cti_key = ck
|
131
|
+
@cti_tables = ct + [table]
|
132
|
+
@cti_columns = cc.merge(table=>columns)
|
133
|
+
@cti_table_map = ctm
|
134
|
+
@cti_base_model = cbm
|
135
|
+
# Need to set dataset and columns before calling super so that
|
136
|
+
# the main column accessor module is included in the class before any
|
137
|
+
# plugin accessor modules (such as the lazy attributes accessor module).
|
138
|
+
set_dataset(ds.join(table, [pk]))
|
139
|
+
set_columns(self.columns)
|
140
|
+
end
|
141
|
+
super
|
142
|
+
subclass.instance_eval do
|
143
|
+
m = method(:constantize)
|
144
|
+
dataset.row_proc = lambda{|r| (m.call(r[ck]) rescue subclass).load(r)}
|
145
|
+
(columns - [cbm.primary_key]).each{|a| define_lazy_attribute_getter(a)}
|
146
|
+
cti_tables.reverse.each do |table|
|
147
|
+
db.schema(table).each{|k,v| db_schema[k] = v}
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# The primary key in the parent/base/root model, which should have a
|
153
|
+
# foreign key with the same name referencing it in each model subclass.
|
154
|
+
def primary_key
|
155
|
+
return super if self == cti_base_model
|
156
|
+
cti_base_model.primary_key
|
157
|
+
end
|
158
|
+
|
159
|
+
# The table name for the current model class's main table (not used
|
160
|
+
# by any superclasses).
|
161
|
+
def table_name
|
162
|
+
self == cti_base_model ? super : cti_tables.last
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
module InstanceMethods
|
167
|
+
# Set the cti_key column to the name of the model.
|
168
|
+
def before_create
|
169
|
+
return false if super == false
|
170
|
+
send("#{model.cti_key}=", model.name.to_s) if model.cti_key
|
171
|
+
end
|
172
|
+
|
173
|
+
# Delete the row from all backing tables, starting from the
|
174
|
+
# most recent table and going through all superclasses.
|
175
|
+
def delete
|
176
|
+
m = model
|
177
|
+
m.cti_tables.reverse.each do |table|
|
178
|
+
m.db.from(table).filter(m.primary_key=>pk).delete
|
179
|
+
end
|
180
|
+
self
|
181
|
+
end
|
182
|
+
|
183
|
+
private
|
184
|
+
|
185
|
+
# Insert rows into all backing tables, using the columns
|
186
|
+
# in each table.
|
187
|
+
def _insert
|
188
|
+
return super if model == model.cti_base_model
|
189
|
+
iid = nil
|
190
|
+
m = model
|
191
|
+
m.cti_tables.each do |table|
|
192
|
+
h = {}
|
193
|
+
h[m.primary_key] = iid if iid
|
194
|
+
m.cti_columns[table].each{|c| h[c] = @values[c] if @values.include?(c)}
|
195
|
+
nid = m.db.from(table).insert(h)
|
196
|
+
iid ||= nid
|
197
|
+
end
|
198
|
+
@values[primary_key] = iid
|
199
|
+
end
|
200
|
+
|
201
|
+
# Update rows in all backing tables, using the columns in each table.
|
202
|
+
def _update(columns)
|
203
|
+
pkh = pk_hash
|
204
|
+
m = model
|
205
|
+
m.cti_tables.each do |table|
|
206
|
+
h = {}
|
207
|
+
m.cti_columns[table].each{|c| h[c] = columns[c] if columns.include?(c)}
|
208
|
+
m.db.from(table).filter(pkh).update(h) unless h.empty?
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|