sequel 3.4.0 → 3.5.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 +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
|