viking-sequel 3.10.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.
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