viking-sequel 3.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (237) hide show
  1. data/CHANGELOG +3134 -0
  2. data/COPYING +19 -0
  3. data/README.rdoc +723 -0
  4. data/Rakefile +193 -0
  5. data/bin/sequel +196 -0
  6. data/doc/advanced_associations.rdoc +644 -0
  7. data/doc/cheat_sheet.rdoc +218 -0
  8. data/doc/dataset_basics.rdoc +106 -0
  9. data/doc/dataset_filtering.rdoc +158 -0
  10. data/doc/opening_databases.rdoc +296 -0
  11. data/doc/prepared_statements.rdoc +104 -0
  12. data/doc/reflection.rdoc +84 -0
  13. data/doc/release_notes/1.0.txt +38 -0
  14. data/doc/release_notes/1.1.txt +143 -0
  15. data/doc/release_notes/1.3.txt +101 -0
  16. data/doc/release_notes/1.4.0.txt +53 -0
  17. data/doc/release_notes/1.5.0.txt +155 -0
  18. data/doc/release_notes/2.0.0.txt +298 -0
  19. data/doc/release_notes/2.1.0.txt +271 -0
  20. data/doc/release_notes/2.10.0.txt +328 -0
  21. data/doc/release_notes/2.11.0.txt +215 -0
  22. data/doc/release_notes/2.12.0.txt +534 -0
  23. data/doc/release_notes/2.2.0.txt +253 -0
  24. data/doc/release_notes/2.3.0.txt +88 -0
  25. data/doc/release_notes/2.4.0.txt +106 -0
  26. data/doc/release_notes/2.5.0.txt +137 -0
  27. data/doc/release_notes/2.6.0.txt +157 -0
  28. data/doc/release_notes/2.7.0.txt +166 -0
  29. data/doc/release_notes/2.8.0.txt +171 -0
  30. data/doc/release_notes/2.9.0.txt +97 -0
  31. data/doc/release_notes/3.0.0.txt +221 -0
  32. data/doc/release_notes/3.1.0.txt +406 -0
  33. data/doc/release_notes/3.10.0.txt +286 -0
  34. data/doc/release_notes/3.2.0.txt +268 -0
  35. data/doc/release_notes/3.3.0.txt +192 -0
  36. data/doc/release_notes/3.4.0.txt +325 -0
  37. data/doc/release_notes/3.5.0.txt +510 -0
  38. data/doc/release_notes/3.6.0.txt +366 -0
  39. data/doc/release_notes/3.7.0.txt +179 -0
  40. data/doc/release_notes/3.8.0.txt +151 -0
  41. data/doc/release_notes/3.9.0.txt +233 -0
  42. data/doc/schema.rdoc +36 -0
  43. data/doc/sharding.rdoc +113 -0
  44. data/doc/virtual_rows.rdoc +205 -0
  45. data/lib/sequel.rb +1 -0
  46. data/lib/sequel/adapters/ado.rb +90 -0
  47. data/lib/sequel/adapters/ado/mssql.rb +30 -0
  48. data/lib/sequel/adapters/amalgalite.rb +176 -0
  49. data/lib/sequel/adapters/db2.rb +139 -0
  50. data/lib/sequel/adapters/dbi.rb +113 -0
  51. data/lib/sequel/adapters/do.rb +188 -0
  52. data/lib/sequel/adapters/do/mysql.rb +49 -0
  53. data/lib/sequel/adapters/do/postgres.rb +91 -0
  54. data/lib/sequel/adapters/do/sqlite.rb +40 -0
  55. data/lib/sequel/adapters/firebird.rb +283 -0
  56. data/lib/sequel/adapters/informix.rb +77 -0
  57. data/lib/sequel/adapters/jdbc.rb +587 -0
  58. data/lib/sequel/adapters/jdbc/as400.rb +58 -0
  59. data/lib/sequel/adapters/jdbc/h2.rb +133 -0
  60. data/lib/sequel/adapters/jdbc/mssql.rb +57 -0
  61. data/lib/sequel/adapters/jdbc/mysql.rb +78 -0
  62. data/lib/sequel/adapters/jdbc/oracle.rb +50 -0
  63. data/lib/sequel/adapters/jdbc/postgresql.rb +108 -0
  64. data/lib/sequel/adapters/jdbc/sqlite.rb +55 -0
  65. data/lib/sequel/adapters/mysql.rb +421 -0
  66. data/lib/sequel/adapters/odbc.rb +143 -0
  67. data/lib/sequel/adapters/odbc/mssql.rb +42 -0
  68. data/lib/sequel/adapters/openbase.rb +64 -0
  69. data/lib/sequel/adapters/oracle.rb +131 -0
  70. data/lib/sequel/adapters/postgres.rb +504 -0
  71. data/lib/sequel/adapters/shared/mssql.rb +490 -0
  72. data/lib/sequel/adapters/shared/mysql.rb +498 -0
  73. data/lib/sequel/adapters/shared/oracle.rb +195 -0
  74. data/lib/sequel/adapters/shared/postgres.rb +830 -0
  75. data/lib/sequel/adapters/shared/progress.rb +44 -0
  76. data/lib/sequel/adapters/shared/sqlite.rb +389 -0
  77. data/lib/sequel/adapters/sqlite.rb +224 -0
  78. data/lib/sequel/adapters/utils/stored_procedures.rb +84 -0
  79. data/lib/sequel/connection_pool.rb +99 -0
  80. data/lib/sequel/connection_pool/sharded_single.rb +84 -0
  81. data/lib/sequel/connection_pool/sharded_threaded.rb +211 -0
  82. data/lib/sequel/connection_pool/single.rb +29 -0
  83. data/lib/sequel/connection_pool/threaded.rb +150 -0
  84. data/lib/sequel/core.rb +293 -0
  85. data/lib/sequel/core_sql.rb +241 -0
  86. data/lib/sequel/database.rb +1079 -0
  87. data/lib/sequel/database/schema_generator.rb +327 -0
  88. data/lib/sequel/database/schema_methods.rb +203 -0
  89. data/lib/sequel/database/schema_sql.rb +320 -0
  90. data/lib/sequel/dataset.rb +32 -0
  91. data/lib/sequel/dataset/actions.rb +441 -0
  92. data/lib/sequel/dataset/features.rb +86 -0
  93. data/lib/sequel/dataset/graph.rb +254 -0
  94. data/lib/sequel/dataset/misc.rb +119 -0
  95. data/lib/sequel/dataset/mutation.rb +64 -0
  96. data/lib/sequel/dataset/prepared_statements.rb +227 -0
  97. data/lib/sequel/dataset/query.rb +709 -0
  98. data/lib/sequel/dataset/sql.rb +996 -0
  99. data/lib/sequel/exceptions.rb +51 -0
  100. data/lib/sequel/extensions/blank.rb +43 -0
  101. data/lib/sequel/extensions/inflector.rb +242 -0
  102. data/lib/sequel/extensions/looser_typecasting.rb +21 -0
  103. data/lib/sequel/extensions/migration.rb +239 -0
  104. data/lib/sequel/extensions/named_timezones.rb +61 -0
  105. data/lib/sequel/extensions/pagination.rb +100 -0
  106. data/lib/sequel/extensions/pretty_table.rb +82 -0
  107. data/lib/sequel/extensions/query.rb +52 -0
  108. data/lib/sequel/extensions/schema_dumper.rb +271 -0
  109. data/lib/sequel/extensions/sql_expr.rb +122 -0
  110. data/lib/sequel/extensions/string_date_time.rb +46 -0
  111. data/lib/sequel/extensions/thread_local_timezones.rb +48 -0
  112. data/lib/sequel/metaprogramming.rb +9 -0
  113. data/lib/sequel/model.rb +120 -0
  114. data/lib/sequel/model/associations.rb +1514 -0
  115. data/lib/sequel/model/base.rb +1069 -0
  116. data/lib/sequel/model/default_inflections.rb +45 -0
  117. data/lib/sequel/model/errors.rb +39 -0
  118. data/lib/sequel/model/exceptions.rb +21 -0
  119. data/lib/sequel/model/inflections.rb +162 -0
  120. data/lib/sequel/model/plugins.rb +70 -0
  121. data/lib/sequel/plugins/active_model.rb +59 -0
  122. data/lib/sequel/plugins/association_dependencies.rb +103 -0
  123. data/lib/sequel/plugins/association_proxies.rb +41 -0
  124. data/lib/sequel/plugins/boolean_readers.rb +53 -0
  125. data/lib/sequel/plugins/caching.rb +141 -0
  126. data/lib/sequel/plugins/class_table_inheritance.rb +214 -0
  127. data/lib/sequel/plugins/composition.rb +138 -0
  128. data/lib/sequel/plugins/force_encoding.rb +72 -0
  129. data/lib/sequel/plugins/hook_class_methods.rb +126 -0
  130. data/lib/sequel/plugins/identity_map.rb +116 -0
  131. data/lib/sequel/plugins/instance_filters.rb +98 -0
  132. data/lib/sequel/plugins/instance_hooks.rb +57 -0
  133. data/lib/sequel/plugins/lazy_attributes.rb +77 -0
  134. data/lib/sequel/plugins/many_through_many.rb +208 -0
  135. data/lib/sequel/plugins/nested_attributes.rb +206 -0
  136. data/lib/sequel/plugins/optimistic_locking.rb +81 -0
  137. data/lib/sequel/plugins/rcte_tree.rb +281 -0
  138. data/lib/sequel/plugins/schema.rb +66 -0
  139. data/lib/sequel/plugins/serialization.rb +166 -0
  140. data/lib/sequel/plugins/single_table_inheritance.rb +74 -0
  141. data/lib/sequel/plugins/subclasses.rb +45 -0
  142. data/lib/sequel/plugins/tactical_eager_loading.rb +61 -0
  143. data/lib/sequel/plugins/timestamps.rb +87 -0
  144. data/lib/sequel/plugins/touch.rb +118 -0
  145. data/lib/sequel/plugins/typecast_on_load.rb +72 -0
  146. data/lib/sequel/plugins/validation_class_methods.rb +405 -0
  147. data/lib/sequel/plugins/validation_helpers.rb +223 -0
  148. data/lib/sequel/sql.rb +1020 -0
  149. data/lib/sequel/timezones.rb +161 -0
  150. data/lib/sequel/version.rb +12 -0
  151. data/lib/sequel_core.rb +1 -0
  152. data/lib/sequel_model.rb +1 -0
  153. data/spec/adapters/firebird_spec.rb +407 -0
  154. data/spec/adapters/informix_spec.rb +97 -0
  155. data/spec/adapters/mssql_spec.rb +403 -0
  156. data/spec/adapters/mysql_spec.rb +1019 -0
  157. data/spec/adapters/oracle_spec.rb +286 -0
  158. data/spec/adapters/postgres_spec.rb +969 -0
  159. data/spec/adapters/spec_helper.rb +51 -0
  160. data/spec/adapters/sqlite_spec.rb +432 -0
  161. data/spec/core/connection_pool_spec.rb +808 -0
  162. data/spec/core/core_sql_spec.rb +417 -0
  163. data/spec/core/database_spec.rb +1662 -0
  164. data/spec/core/dataset_spec.rb +3827 -0
  165. data/spec/core/expression_filters_spec.rb +595 -0
  166. data/spec/core/object_graph_spec.rb +296 -0
  167. data/spec/core/schema_generator_spec.rb +159 -0
  168. data/spec/core/schema_spec.rb +830 -0
  169. data/spec/core/spec_helper.rb +56 -0
  170. data/spec/core/version_spec.rb +7 -0
  171. data/spec/extensions/active_model_spec.rb +76 -0
  172. data/spec/extensions/association_dependencies_spec.rb +127 -0
  173. data/spec/extensions/association_proxies_spec.rb +50 -0
  174. data/spec/extensions/blank_spec.rb +67 -0
  175. data/spec/extensions/boolean_readers_spec.rb +92 -0
  176. data/spec/extensions/caching_spec.rb +250 -0
  177. data/spec/extensions/class_table_inheritance_spec.rb +252 -0
  178. data/spec/extensions/composition_spec.rb +194 -0
  179. data/spec/extensions/force_encoding_spec.rb +117 -0
  180. data/spec/extensions/hook_class_methods_spec.rb +470 -0
  181. data/spec/extensions/identity_map_spec.rb +202 -0
  182. data/spec/extensions/inflector_spec.rb +181 -0
  183. data/spec/extensions/instance_filters_spec.rb +55 -0
  184. data/spec/extensions/instance_hooks_spec.rb +133 -0
  185. data/spec/extensions/lazy_attributes_spec.rb +153 -0
  186. data/spec/extensions/looser_typecasting_spec.rb +39 -0
  187. data/spec/extensions/many_through_many_spec.rb +884 -0
  188. data/spec/extensions/migration_spec.rb +332 -0
  189. data/spec/extensions/named_timezones_spec.rb +72 -0
  190. data/spec/extensions/nested_attributes_spec.rb +396 -0
  191. data/spec/extensions/optimistic_locking_spec.rb +100 -0
  192. data/spec/extensions/pagination_spec.rb +99 -0
  193. data/spec/extensions/pretty_table_spec.rb +91 -0
  194. data/spec/extensions/query_spec.rb +85 -0
  195. data/spec/extensions/rcte_tree_spec.rb +205 -0
  196. data/spec/extensions/schema_dumper_spec.rb +357 -0
  197. data/spec/extensions/schema_spec.rb +127 -0
  198. data/spec/extensions/serialization_spec.rb +209 -0
  199. data/spec/extensions/single_table_inheritance_spec.rb +96 -0
  200. data/spec/extensions/spec_helper.rb +91 -0
  201. data/spec/extensions/sql_expr_spec.rb +89 -0
  202. data/spec/extensions/string_date_time_spec.rb +93 -0
  203. data/spec/extensions/subclasses_spec.rb +52 -0
  204. data/spec/extensions/tactical_eager_loading_spec.rb +65 -0
  205. data/spec/extensions/thread_local_timezones_spec.rb +45 -0
  206. data/spec/extensions/timestamps_spec.rb +150 -0
  207. data/spec/extensions/touch_spec.rb +155 -0
  208. data/spec/extensions/typecast_on_load_spec.rb +69 -0
  209. data/spec/extensions/validation_class_methods_spec.rb +984 -0
  210. data/spec/extensions/validation_helpers_spec.rb +438 -0
  211. data/spec/integration/associations_test.rb +281 -0
  212. data/spec/integration/database_test.rb +26 -0
  213. data/spec/integration/dataset_test.rb +963 -0
  214. data/spec/integration/eager_loader_test.rb +734 -0
  215. data/spec/integration/model_test.rb +130 -0
  216. data/spec/integration/plugin_test.rb +814 -0
  217. data/spec/integration/prepared_statement_test.rb +213 -0
  218. data/spec/integration/schema_test.rb +361 -0
  219. data/spec/integration/spec_helper.rb +73 -0
  220. data/spec/integration/timezone_test.rb +55 -0
  221. data/spec/integration/transaction_test.rb +122 -0
  222. data/spec/integration/type_test.rb +96 -0
  223. data/spec/model/association_reflection_spec.rb +175 -0
  224. data/spec/model/associations_spec.rb +2633 -0
  225. data/spec/model/base_spec.rb +418 -0
  226. data/spec/model/dataset_methods_spec.rb +78 -0
  227. data/spec/model/eager_loading_spec.rb +1391 -0
  228. data/spec/model/hooks_spec.rb +240 -0
  229. data/spec/model/inflector_spec.rb +26 -0
  230. data/spec/model/model_spec.rb +593 -0
  231. data/spec/model/plugins_spec.rb +236 -0
  232. data/spec/model/record_spec.rb +1500 -0
  233. data/spec/model/spec_helper.rb +97 -0
  234. data/spec/model/validations_spec.rb +153 -0
  235. data/spec/rcov.opts +6 -0
  236. data/spec/spec_config.rb.example +10 -0
  237. metadata +346 -0
@@ -0,0 +1,98 @@
1
+ module Sequel
2
+ module Plugins
3
+ # This plugin allows you to add filters on a per object basis that
4
+ # restrict updating or deleting the object. It's designed for cases
5
+ # where you would normally have to drop down to the dataset level
6
+ # to get the necessary control, because you only want to delete or
7
+ # update the rows in certain cases based on the current status of
8
+ # the row in the database.
9
+ #
10
+ # class Item < Sequel::Model
11
+ # plugin :instance_filters
12
+ # end
13
+ #
14
+ # # These are two separate objects that represent the same
15
+ # # database row.
16
+ # i1 = Item.first(:id=>1, :delete_allowed=>false)
17
+ # i2 = Item.first(:id=>1, :delete_allowed=>false)
18
+ #
19
+ # # Add an instance filter to the object. This filter is in effect
20
+ # # until the object is successfully updated or deleted.
21
+ # i1.instance_filter(:delete_allowed=>true)
22
+ #
23
+ # # Attempting to delete the object where the filter doesn't
24
+ # # match any rows raises an error.
25
+ # i1.delete # raises Sequel::Error
26
+ #
27
+ # # The other object that represents the same row has no
28
+ # # instance filters, and can be updated normally.
29
+ # i2.update(:delete_allowed=>true)
30
+ #
31
+ # # Even though the filter is now still in effect, since the
32
+ # # database row has been updated to allow deleting,
33
+ # # delete now works.
34
+ # i1.delete
35
+ #
36
+ # This plugin sets the require_modification flag on the model,
37
+ # so if the model's dataset doesn't provide an accurate number
38
+ # of matched rows, this could result in invalid exceptions being raised.
39
+ module InstanceFilters
40
+ # Exception class raised when updating or deleting an object does
41
+ # not affect exactly one row.
42
+ Error = Sequel::NoExistingObject
43
+
44
+ # Set the require_modification flag to true for the model.
45
+ def self.configure(model)
46
+ model.require_modification = true
47
+ end
48
+
49
+ module InstanceMethods
50
+ # Clear the instance filters after successfully destroying the object.
51
+ def after_destroy
52
+ super
53
+ clear_instance_filters
54
+ end
55
+
56
+ # Clear the instance filters after successfully updating the object.
57
+ def after_update
58
+ super
59
+ clear_instance_filters
60
+ end
61
+
62
+ # Add an instance filter to the array of instance filters
63
+ # Both the arguments given and the block are passed to the
64
+ # dataset's filter method.
65
+ def instance_filter(*args, &block)
66
+ instance_filters << [args, block]
67
+ end
68
+
69
+ private
70
+
71
+ # Lazily initialize the instance filter array.
72
+ def instance_filters
73
+ @instance_filters ||= []
74
+ end
75
+
76
+ # Apply the instance filters to the given dataset
77
+ def apply_instance_filters(ds)
78
+ instance_filters.inject(ds){|ds, i| ds.filter(*i[0], &i[1])}
79
+ end
80
+
81
+ # Clear the instance filters.
82
+ def clear_instance_filters
83
+ instance_filters.clear
84
+ end
85
+
86
+ # Apply the instance filters to the dataset returned by super.
87
+ def _delete_dataset
88
+ apply_instance_filters(super)
89
+ end
90
+
91
+ # Apply the instance filters to the dataset returned by super.
92
+ def _update_dataset
93
+ apply_instance_filters(super)
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,57 @@
1
+ module Sequel
2
+ module Plugins
3
+ # The instance_hooks plugin allows you to add hooks to specific instances,
4
+ # by passing a block to a _hook method (e.g. before_save_hook{do_something}).
5
+ # The block executed when the hook is called (e.g. before_save).
6
+ #
7
+ # All of the standard hooks are supported, except for after_initialize.
8
+ # Instance level before hooks are executed in reverse order of addition before
9
+ # calling super. Instance level after hooks are executed in order of addition
10
+ # after calling super. If any of the instance level before hook blocks return
11
+ # false, no more instance level before hooks are called and false is returned.
12
+ #
13
+ # Instance level hooks are cleared when the object is saved successfully.
14
+ module InstanceHooks
15
+ module InstanceMethods
16
+ HOOKS = Sequel::Model::HOOKS - [:after_initialize]
17
+ HOOKS.each{|h| class_eval("def #{h}_hook(&block); add_instance_hook(:#{h}, &block) end", __FILE__, __LINE__)}
18
+
19
+ BEFORE_HOOKS, AFTER_HOOKS = HOOKS.partition{|hook| hook.to_s =~ /\Abefore_/}
20
+ BEFORE_HOOKS.each{|h| class_eval("def #{h}; run_instance_hooks(:#{h}) == false ? false : super end", __FILE__, __LINE__)}
21
+ AFTER_HOOKS.each{|h| class_eval("def #{h}; super; run_instance_hooks(:#{h}) end", __FILE__, __LINE__)}
22
+
23
+ # Clear the instance level hooks after saving the object.
24
+ def after_save
25
+ super
26
+ run_instance_hooks(:after_save)
27
+ @instance_hooks.clear if @instance_hooks
28
+ end
29
+
30
+ private
31
+
32
+ # Add the block as an instance level hook. For before hooks, add it to
33
+ # the beginning of the instance hook's array. For after hooks, add it
34
+ # to the end.
35
+ def add_instance_hook(hook, &block)
36
+ instance_hooks(hook).send(BEFORE_HOOKS.include?(hook) ? :unshift : :push, block)
37
+ end
38
+
39
+ # An array of instance level hook blocks for the given hook type.
40
+ def instance_hooks(hook)
41
+ @instance_hooks ||= {}
42
+ @instance_hooks[hook] ||= []
43
+ end
44
+
45
+ # Run all hook blocks of the given hook type. If a before hook,
46
+ # immediately return false if any hook block call returns false.
47
+ def run_instance_hooks(hook)
48
+ if BEFORE_HOOKS.include?(hook)
49
+ instance_hooks(hook).each{|b| return false if b.call == false}
50
+ else
51
+ instance_hooks(hook).each{|b| b.call}
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,77 @@
1
+ module Sequel
2
+ module Plugins
3
+ # The lazy_attributes plugin allows users to easily set that some attributes
4
+ # should not be loaded by default when loading model objects. If the attribute
5
+ # is needed after the instance has been retrieved, a database query is made to
6
+ # retreive the value of the attribute.
7
+ #
8
+ # This plugin depends on the identity_map and tactical_eager_loading plugin, and allows you to
9
+ # eagerly load lazy attributes for all objects retrieved with the current object.
10
+ # So the following code should issue one query to get the albums and one query to
11
+ # get the reviews for all of those albums:
12
+ #
13
+ # Album.plugin :lazy_attributes, :review
14
+ # Sequel::Model.with_identity_map do
15
+ # Album.filter{id<100}.all do |a|
16
+ # a.review
17
+ # end
18
+ # end
19
+ module LazyAttributes
20
+ # Lazy attributes requires the identity map and tactical eager loading plugins
21
+ def self.apply(model, *attrs)
22
+ model.plugin :identity_map
23
+ model.plugin :tactical_eager_loading
24
+ end
25
+
26
+ # Set the attributes given as lazy attributes
27
+ def self.configure(model, *attrs)
28
+ model.lazy_attributes(*attrs) unless attrs.empty?
29
+ end
30
+
31
+ module ClassMethods
32
+ # Module to store the lazy attribute getter methods, so they can
33
+ # be overridden and call super to get the lazy attribute behavior
34
+ attr_accessor :lazy_attributes_module
35
+
36
+ # Remove the given attributes from the list of columns selected by default.
37
+ # For each attribute given, create an accessor method that allows a lazy
38
+ # lookup of the attribute. Each attribute should be given as a symbol.
39
+ def lazy_attributes(*attrs)
40
+ set_dataset(dataset.select(*(columns - attrs)))
41
+ attrs.each{|a| define_lazy_attribute_getter(a)}
42
+ end
43
+
44
+ private
45
+
46
+ # Add a lazy attribute getter method to the lazy_attributes_module
47
+ def define_lazy_attribute_getter(a)
48
+ include(self.lazy_attributes_module ||= Module.new) unless lazy_attributes_module
49
+ lazy_attributes_module.class_eval do
50
+ define_method(a) do
51
+ if !values.include?(a) && !new?
52
+ lazy_attribute_lookup(a)
53
+ else
54
+ super()
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ module InstanceMethods
62
+ private
63
+
64
+ # If the model was selected with other model objects, eagerly load the
65
+ # attribute for all of those objects. If not, query the database for
66
+ # the attribute for just the current object. Return the value of
67
+ # the attribute for the current object.
68
+ def lazy_attribute_lookup(a)
69
+ primary_key = model.primary_key
70
+ model.select(*(Array(primary_key) + [a])).filter(primary_key=>retrieved_with.map{|o| o.pk}.sql_array).all if model.identity_map && retrieved_with
71
+ values[a] = this.select(a).first[a] unless values.include?(a)
72
+ values[a]
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,208 @@
1
+ module Sequel
2
+ module Plugins
3
+ # The many_through_many plugin allow you to create a association to multiple objects using multiple join tables.
4
+ # For example, assume the following associations:
5
+ #
6
+ # Artist.many_to_many :albums
7
+ # Album.many_to_many :tags
8
+ #
9
+ # The many_through_many plugin would allow this:
10
+ #
11
+ # Artist.many_through_many :tags, [[:albums_artists, :artist_id, :album_id], [:albums, :id, :id], [:albums_tags, :album_id, :tag_id]]
12
+ #
13
+ # Which will give you the tags for all of the artist's albums.
14
+ #
15
+ # Here are some more examples:
16
+ #
17
+ # # Same as Artist.many_to_many :albums
18
+ # Artist.many_through_many :albums, [[:albums_artists, :artist_id, :album_id]]
19
+ #
20
+ # # All artists that are associated to any album that this artist is associated to
21
+ # Artist.many_through_many :artists, [[:albums_artists, :artist_id, :album_id], [:albums, :id, :id], [:albums_artists, :album_id, :artist_id]]
22
+ #
23
+ # # All albums by artists that are associated to any album that this artist is associated to
24
+ # Artist.many_through_many :artist_albums, [[:albums_artists, :artist_id, :album_id], [:albums, :id, :id], \
25
+ # [:albums_artists, :album_id, :artist_id], [:artists, :id, :id], [:albums_artists, :artist_id, :album_id]], \
26
+ # :class=>:Album
27
+ #
28
+ # # All tracks on albums by this artist
29
+ # Artist.many_through_many :tracks, [[:albums_artists, :artist_id, :album_id], [:albums, :id, :id]], \
30
+ # :right_primary_key=>:album_id
31
+ module ManyThroughMany
32
+ # The AssociationReflection subclass for many_through_many associations.
33
+ class ManyThroughManyAssociationReflection < Sequel::Model::Associations::ManyToManyAssociationReflection
34
+ Sequel::Model::Associations::ASSOCIATION_TYPES[:many_through_many] = self
35
+
36
+ # The table containing the column to use for the associated key when eagerly loading
37
+ def associated_key_table
38
+ self[:associated_key_table] = self[:final_reverse_edge][:alias]
39
+ end
40
+
41
+ # The default associated key alias(es) to use when eager loading
42
+ # associations via eager.
43
+ def default_associated_key_alias
44
+ self[:uses_left_composite_keys] ? (0...self[:through].first[:left].length).map{|i| :"x_foreign_key_#{i}_x"} : :x_foreign_key_x
45
+ end
46
+
47
+ # The list of joins to use when eager graphing
48
+ def edges
49
+ self[:edges] || calculate_edges || self[:edges]
50
+ end
51
+
52
+ # Many through many associations don't have a reciprocal
53
+ def reciprocal
54
+ nil
55
+ end
56
+
57
+ # The list of joins to use when lazy loading or eager loading
58
+ def reverse_edges
59
+ self[:reverse_edges] || calculate_edges || self[:reverse_edges]
60
+ end
61
+
62
+ private
63
+
64
+ # Make sure to use unique table aliases when lazy loading or eager loading
65
+ def calculate_reverse_edge_aliases(reverse_edges)
66
+ aliases = [associated_class.table_name]
67
+ reverse_edges.each do |e|
68
+ table_alias = e[:table]
69
+ if aliases.include?(table_alias)
70
+ i = 0
71
+ table_alias = loop do
72
+ ta = :"#{table_alias}_#{i}"
73
+ break ta unless aliases.include?(ta)
74
+ i += 1
75
+ end
76
+ end
77
+ aliases.push(e[:alias] = table_alias)
78
+ end
79
+ end
80
+
81
+ # Transform the :through option into a list of edges and reverse edges to use to join tables when loading the association.
82
+ def calculate_edges
83
+ es = [{:left_table=>self[:model].table_name, :left_key=>self[:left_primary_key]}]
84
+ self[:through].each do |t|
85
+ es.last.merge!(:right_key=>t[:left], :right_table=>t[:table], :join_type=>t[:join_type]||self[:graph_join_type], :conditions=>(t[:conditions]||[]).to_a, :block=>t[:block])
86
+ es.last[:only_conditions] = t[:only_conditions] if t.include?(:only_conditions)
87
+ es << {:left_table=>t[:table], :left_key=>t[:right]}
88
+ end
89
+ es.last.merge!(:right_key=>right_primary_key, :right_table=>associated_class.table_name)
90
+ edges = es.map do |e|
91
+ h = {:table=>e[:right_table], :left=>e[:left_key], :right=>e[:right_key], :conditions=>e[:conditions], :join_type=>e[:join_type], :block=>e[:block]}
92
+ h[:only_conditions] = e[:only_conditions] if e.include?(:only_conditions)
93
+ h
94
+ end
95
+ reverse_edges = es.reverse.map{|e| {:table=>e[:left_table], :left=>e[:left_key], :right=>e[:right_key]}}
96
+ reverse_edges.pop
97
+ calculate_reverse_edge_aliases(reverse_edges)
98
+ self[:final_edge] = edges.pop
99
+ self[:final_reverse_edge] = reverse_edges.pop
100
+ self[:edges] = edges
101
+ self[:reverse_edges] = reverse_edges
102
+ nil
103
+ end
104
+ end
105
+ module ClassMethods
106
+ # Create a many_through_many association. Arguments:
107
+ # * name - Same as associate, the name of the association.
108
+ # * through - The tables and keys to join between the current table and the associated table.
109
+ # Must be an array, with elements that are either 3 element arrays, or hashes with keys :table, :left, and :right.
110
+ # The required entries in the array/hash are:
111
+ # * :table (first array element) - The name of the table to join.
112
+ # * :left (middle array element) - The key joining the table to the previous table. Can use an
113
+ # array of symbols for a composite key association.
114
+ # * :right (last array element) - The key joining the table to the next table. Can use an
115
+ # array of symbols for a composite key association.
116
+ # If a hash is provided, the following keys are respected when using eager_graph:
117
+ # * :block - A proc to use as the block argument to join.
118
+ # * :conditions - Extra conditions to add to the JOIN ON clause. Must be a hash or array of two pairs.
119
+ # * :join_type - The join type to use for the join, defaults to :left_outer.
120
+ # * :only_conditions - Conditions to use for the join instead of the ones specified by the keys.
121
+ # * opts - The options for the associaion. Takes the same options as associate, and supports these additional options:
122
+ # * :left_primary_key - column in current table that the first :left option in
123
+ # through points to, as a symbol. Defaults to primary key of current table. Can use an
124
+ # array of symbols for a composite key association.
125
+ # * :right_primary_key - column in associated table that the final :right option in
126
+ # through points to, as a symbol. Defaults to primary key of the associated table. Can use an
127
+ # array of symbols for a composite key association.
128
+ # * :uniq - Adds a after_load callback that makes the array of objects unique.
129
+ def many_through_many(name, through, opts={}, &block)
130
+ associate(:many_through_many, name, opts.merge(:through=>through), &block)
131
+ end
132
+
133
+ private
134
+
135
+ # Create the association methods and :eager_loader and :eager_grapher procs.
136
+ def def_many_through_many(opts)
137
+ name = opts[:name]
138
+ model = self
139
+ opts[:read_only] = true
140
+ opts[:after_load].unshift(:array_uniq!) if opts[:uniq]
141
+ opts[:cartesian_product_number] ||= 2
142
+ opts[:through] = opts[:through].map do |e|
143
+ case e
144
+ when Array
145
+ raise(Error, "array elements of the through option/argument for many_through_many associations must have at least three elements") unless e.length == 3
146
+ {:table=>e[0], :left=>e[1], :right=>e[2]}
147
+ when Hash
148
+ raise(Error, "hash elements of the through option/argument for many_through_many associations must contain :table, :left, and :right keys") unless e[:table] && e[:left] && e[:right]
149
+ e
150
+ else
151
+ raise(Error, "the through option/argument for many_through_many associations must be an enumerable of arrays or hashes")
152
+ end
153
+ end
154
+
155
+ left_key = opts[:left_key] = opts[:through].first[:left]
156
+ uses_lcks = opts[:uses_left_composite_keys] = left_key.is_a?(Array)
157
+ left_keys = Array(left_key)
158
+ left_pk = (opts[:left_primary_key] ||= self.primary_key)
159
+ left_pks = opts[:left_primary_keys] = Array(left_pk)
160
+ opts[:dataset] ||= lambda do
161
+ ds = opts.associated_class
162
+ opts.reverse_edges.each{|t| ds = ds.join(t[:table], Array(t[:left]).zip(Array(t[:right])), :table_alias=>t[:alias])}
163
+ ft = opts[:final_reverse_edge]
164
+ ds.join(ft[:table], Array(ft[:left]).zip(Array(ft[:right])) + left_keys.zip(left_pks.map{|k| send(k)}), :table_alias=>ft[:alias])
165
+ end
166
+
167
+ left_key_alias = opts[:left_key_alias] ||= opts.default_associated_key_alias
168
+ opts[:eager_loader] ||= lambda do |key_hash, records, associations|
169
+ h = key_hash[left_pk]
170
+ records.each{|object| object.associations[name] = []}
171
+ ds = opts.associated_class
172
+ opts.reverse_edges.each{|t| ds = ds.join(t[:table], Array(t[:left]).zip(Array(t[:right])), :table_alias=>t[:alias])}
173
+ ft = opts[:final_reverse_edge]
174
+ conds = uses_lcks ? [[left_keys.map{|k| SQL::QualifiedIdentifier.new(ft[:table], k)}, SQL::SQLArray.new(h.keys)]] : [[left_key, h.keys]]
175
+ ds = ds.join(ft[:table], Array(ft[:left]).zip(Array(ft[:right])) + conds, :table_alias=>ft[:alias])
176
+ model.eager_loading_dataset(opts, ds, Array(opts.select), associations).all do |assoc_record|
177
+ hash_key = if uses_lcks
178
+ left_key_alias.map{|k| assoc_record.values.delete(k)}
179
+ else
180
+ assoc_record.values.delete(left_key_alias)
181
+ end
182
+ next unless objects = h[hash_key]
183
+ objects.each{|object| object.associations[name].push(assoc_record)}
184
+ end
185
+ end
186
+
187
+ join_type = opts[:graph_join_type]
188
+ select = opts[:graph_select]
189
+ graph_block = opts[:graph_block]
190
+ only_conditions = opts[:graph_only_conditions]
191
+ use_only_conditions = opts.include?(:graph_only_conditions)
192
+ conditions = opts[:graph_conditions]
193
+ opts[:eager_grapher] ||= proc do |ds, assoc_alias, table_alias|
194
+ iq = table_alias
195
+ opts.edges.each do |t|
196
+ ds = ds.graph(t[:table], t.fetch(:only_conditions, (Array(t[:right]).zip(Array(t[:left])) + t[:conditions])), :select=>false, :table_alias=>ds.unused_table_alias(t[:table]), :join_type=>t[:join_type], :implicit_qualifier=>iq, &t[:block])
197
+ iq = nil
198
+ end
199
+ fe = opts[:final_edge]
200
+ ds.graph(opts.associated_class, use_only_conditions ? only_conditions : (Array(opts.right_primary_key).zip(Array(fe[:left])) + conditions), :select=>select, :table_alias=>assoc_alias, :join_type=>join_type, &graph_block)
201
+ end
202
+
203
+ def_association_dataset_methods(opts)
204
+ end
205
+ end
206
+ end
207
+ end
208
+ end
@@ -0,0 +1,206 @@
1
+ module Sequel
2
+ module Plugins
3
+ # The nested_attributes plugin allows you to update attributes for associated
4
+ # objects directly through the parent object, similar to ActiveRecord's
5
+ # Nested Attributes feature.
6
+ #
7
+ # Nested attributes are created using the nested_attributes method:
8
+ #
9
+ # Artist.one_to_many :albums
10
+ # Artist.nested_attributes :albums
11
+ # a = Artist.new(:name=>'YJM',
12
+ # :albums_attributes=>[{:name=>'RF'}, {:name=>'MO'}])
13
+ # # No database activity yet
14
+ #
15
+ # a.save # Saves artist and both albums
16
+ # a.albums.map{|x| x.name} # ['RF', 'MO']
17
+ module NestedAttributes
18
+ # Depend on the instance_hooks plugin.
19
+ def self.apply(model)
20
+ model.plugin(:instance_hooks)
21
+ end
22
+
23
+ module ClassMethods
24
+ # Module to store the nested_attributes setter methods, so they can
25
+ # call be overridden and call super to get the default behavior
26
+ attr_accessor :nested_attributes_module
27
+
28
+ # Allow nested attributes to be set for the given associations. Options:
29
+ # * :destroy - Allow destruction of nested records.
30
+ # * :fields - If provided, should be an Array. Restricts the fields allowed to be
31
+ # modified through the association_attributes= method to the specific fields given.
32
+ # * :limit - For *_to_many associations, a limit on the number of records
33
+ # that will be processed, to prevent denial of service attacks.
34
+ # * :remove - Allow disassociation of nested records (can remove the associated
35
+ # object from the parent object, but not destroy the associated object).
36
+ # * :strict - Set to false to not raise an error message if a primary key
37
+ # is provided in a record, but it doesn't match an existing associated
38
+ # object.
39
+ #
40
+ # If a block is provided, it is passed each nested attribute hash. If
41
+ # the hash should be ignored, the block should return anything except false or nil.
42
+ def nested_attributes(*associations, &block)
43
+ include(self.nested_attributes_module ||= Module.new) unless nested_attributes_module
44
+ opts = associations.last.is_a?(Hash) ? associations.pop : {}
45
+ reflections = associations.map{|a| association_reflection(a) || raise(Error, "no association named #{a} for #{self}")}
46
+ reflections.each do |r|
47
+ r[:nested_attributes] = opts
48
+ r[:nested_attributes][:reject_if] ||= block
49
+ def_nested_attribute_method(r)
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ # Add a nested attribute setter method to a module included in the
56
+ # class.
57
+ def def_nested_attribute_method(reflection)
58
+ nested_attributes_module.class_eval do
59
+ if reflection.returns_array?
60
+ define_method("#{reflection[:name]}_attributes=") do |array|
61
+ nested_attributes_list_setter(reflection, array)
62
+ end
63
+ else
64
+ define_method("#{reflection[:name]}_attributes=") do |h|
65
+ nested_attributes_setter(reflection, h)
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+
72
+ module InstanceMethods
73
+ private
74
+
75
+ # Check that the keys related to the association are not modified inside the block. Does
76
+ # not use an ensure block, so callers should be careful.
77
+ def nested_attributes_check_key_modifications(reflection, obj)
78
+ keys = reflection.associated_object_keys.map{|x| obj.send(x)}
79
+ yield
80
+ raise(Error, "Modifying association dependent key(s) when updating associated objects is not allowed") unless keys == reflection.associated_object_keys.map{|x| obj.send(x)}
81
+ end
82
+
83
+ # Create a new associated object with the given attributes, validate
84
+ # it when the parent is validated, and save it when the object is saved.
85
+ # Returns the object created.
86
+ def nested_attributes_create(reflection, attributes)
87
+ obj = reflection.associated_class.new
88
+ nested_attributes_set_attributes(reflection, obj, attributes)
89
+ after_validation_hook{validate_associated_object(reflection, obj)}
90
+ if reflection.returns_array?
91
+ send(reflection[:name]) << obj
92
+ after_save_hook{send(reflection.add_method, obj)}
93
+ else
94
+ associations[reflection[:name]] = obj
95
+ # Don't need to validate the object twice if :validate association option is not false
96
+ # and don't want to validate it at all if it is false.
97
+ send(reflection[:type] == :many_to_one ? :before_save_hook : :after_save_hook){send(reflection.setter_method, obj.save(:validate=>false))}
98
+ end
99
+ obj
100
+ end
101
+
102
+ # Find an associated object with the matching pk. If a matching option
103
+ # is not found and the :strict option is not false, raise an Error.
104
+ def nested_attributes_find(reflection, pk)
105
+ pk = pk.to_s
106
+ unless obj = Array(associated_objects = send(reflection[:name])).find{|x| x.pk.to_s == pk}
107
+ raise(Error, "no matching associated object with given primary key (association: #{reflection[:name]}, pk: #{pk})") unless reflection[:nested_attributes][:strict] == false
108
+ end
109
+ obj
110
+ end
111
+
112
+ # Take an array or hash of attribute hashes and set each one individually.
113
+ # If a hash is provided it, sort it by key and then use the values.
114
+ # If there is a limit on the nested attributes for this association,
115
+ # make sure the length of the attributes_list is not greater than the limit.
116
+ def nested_attributes_list_setter(reflection, attributes_list)
117
+ attributes_list = attributes_list.sort_by{|x| x.to_s}.map{|k,v| v} if attributes_list.is_a?(Hash)
118
+ if (limit = reflection[:nested_attributes][:limit]) && attributes_list.length > limit
119
+ raise(Error, "number of nested attributes (#{attributes_list.length}) exceeds the limit (#{limit})")
120
+ end
121
+ attributes_list.each{|a| nested_attributes_setter(reflection, a)}
122
+ end
123
+
124
+ # Remove the matching associated object from the current object.
125
+ # If the :destroy option is given, destroy the object after disassociating it.
126
+ # Returns the object removed, if it exists.
127
+ def nested_attributes_remove(reflection, pk, opts={})
128
+ if obj = nested_attributes_find(reflection, pk)
129
+ before_save_hook do
130
+ if reflection.returns_array?
131
+ send(reflection.remove_method, obj)
132
+ else
133
+ send(reflection.setter_method, nil)
134
+ end
135
+ end
136
+ after_save_hook{obj.destroy} if opts[:destroy]
137
+ obj
138
+ end
139
+ end
140
+
141
+ # Set the fields in the obj based on the association, only allowing
142
+ # specific :fields if configured.
143
+ def nested_attributes_set_attributes(reflection, obj, attributes)
144
+ if fields = reflection[:nested_attributes][:fields]
145
+ obj.set_only(attributes, fields)
146
+ else
147
+ obj.set(attributes)
148
+ end
149
+ end
150
+
151
+ # Modify the associated object based on the contents of the attribtues hash:
152
+ # * If a block was given to nested_attributes, call it with the attributes and return immediately if the block returns true.
153
+ # * If no primary key exists in the attributes hash, create a new object.
154
+ # * If _delete is a key in the hash and the :destroy option is used, destroy the matching associated object.
155
+ # * If _remove is a key in the hash and the :remove option is used, disassociated the matching associated object.
156
+ # * Otherwise, update the matching associated object with the contents of the hash.
157
+ def nested_attributes_setter(reflection, attributes)
158
+ return if (b = reflection[:nested_attributes][:reject_if]) && b.call(attributes)
159
+ modified!
160
+ klass = reflection.associated_class
161
+ if pk = attributes.delete(klass.primary_key) || attributes.delete(klass.primary_key.to_s)
162
+ if klass.db.send(:typecast_value_boolean, attributes[:_delete] || attributes['_delete']) && reflection[:nested_attributes][:destroy]
163
+ nested_attributes_remove(reflection, pk, :destroy=>true)
164
+ elsif klass.db.send(:typecast_value_boolean, attributes[:_remove] || attributes['_remove']) && reflection[:nested_attributes][:remove]
165
+ nested_attributes_remove(reflection, pk)
166
+ else
167
+ nested_attributes_update(reflection, pk, attributes)
168
+ end
169
+ else
170
+ nested_attributes_create(reflection, attributes)
171
+ end
172
+ end
173
+
174
+ # Update the matching associated object with the attributes,
175
+ # validating it when the parent object is validated and saving it
176
+ # when the parent is saved.
177
+ # Returns the object updated, if it exists.
178
+ def nested_attributes_update(reflection, pk, attributes)
179
+ if obj = nested_attributes_find(reflection, pk)
180
+ nested_attributes_update_attributes(reflection, obj, attributes)
181
+ after_validation_hook{validate_associated_object(reflection, obj)}
182
+ # Don't need to validate the object twice if :validate association option is not false
183
+ # and don't want to validate it at all if it is false.
184
+ after_save_hook{obj.save_changes(:validate=>false)}
185
+ obj
186
+ end
187
+ end
188
+
189
+ # Update the attributes for the given object related to the current object through the association.
190
+ def nested_attributes_update_attributes(reflection, obj, attributes)
191
+ nested_attributes_check_key_modifications(reflection, obj) do
192
+ nested_attributes_set_attributes(reflection, obj, attributes)
193
+ end
194
+ end
195
+
196
+ # Validate the given associated object, adding any validation error messages from the
197
+ # given object to the parent object.
198
+ def validate_associated_object(reflection, obj)
199
+ return if reflection[:validate] == false
200
+ association = reflection[:name]
201
+ obj.errors.full_messages.each{|m| errors.add(association, m)} unless obj.valid?
202
+ end
203
+ end
204
+ end
205
+ end
206
+ end