pakyow-data 1.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +0 -0
  3. data/LICENSE +4 -0
  4. data/README.md +29 -0
  5. data/lib/pakyow/data/adapters/abstract.rb +58 -0
  6. data/lib/pakyow/data/adapters/sql/commands.rb +58 -0
  7. data/lib/pakyow/data/adapters/sql/dataset_methods.rb +29 -0
  8. data/lib/pakyow/data/adapters/sql/differ.rb +76 -0
  9. data/lib/pakyow/data/adapters/sql/migrator/adapter_methods.rb +95 -0
  10. data/lib/pakyow/data/adapters/sql/migrator.rb +181 -0
  11. data/lib/pakyow/data/adapters/sql/migrators/automator.rb +49 -0
  12. data/lib/pakyow/data/adapters/sql/migrators/finalizer.rb +96 -0
  13. data/lib/pakyow/data/adapters/sql/runner.rb +49 -0
  14. data/lib/pakyow/data/adapters/sql/source_extension.rb +31 -0
  15. data/lib/pakyow/data/adapters/sql/types.rb +50 -0
  16. data/lib/pakyow/data/adapters/sql.rb +247 -0
  17. data/lib/pakyow/data/behavior/config.rb +28 -0
  18. data/lib/pakyow/data/behavior/lookup.rb +75 -0
  19. data/lib/pakyow/data/behavior/serialization.rb +40 -0
  20. data/lib/pakyow/data/connection.rb +103 -0
  21. data/lib/pakyow/data/container.rb +273 -0
  22. data/lib/pakyow/data/errors.rb +169 -0
  23. data/lib/pakyow/data/framework.rb +42 -0
  24. data/lib/pakyow/data/helpers.rb +11 -0
  25. data/lib/pakyow/data/lookup.rb +85 -0
  26. data/lib/pakyow/data/migrator.rb +182 -0
  27. data/lib/pakyow/data/object.rb +98 -0
  28. data/lib/pakyow/data/proxy.rb +262 -0
  29. data/lib/pakyow/data/result.rb +53 -0
  30. data/lib/pakyow/data/sources/abstract.rb +82 -0
  31. data/lib/pakyow/data/sources/ephemeral.rb +72 -0
  32. data/lib/pakyow/data/sources/relational/association.rb +43 -0
  33. data/lib/pakyow/data/sources/relational/associations/belongs_to.rb +47 -0
  34. data/lib/pakyow/data/sources/relational/associations/has_many.rb +54 -0
  35. data/lib/pakyow/data/sources/relational/associations/has_one.rb +54 -0
  36. data/lib/pakyow/data/sources/relational/associations/through.rb +67 -0
  37. data/lib/pakyow/data/sources/relational/command.rb +531 -0
  38. data/lib/pakyow/data/sources/relational/migrator.rb +101 -0
  39. data/lib/pakyow/data/sources/relational.rb +587 -0
  40. data/lib/pakyow/data/subscribers/adapters/memory.rb +153 -0
  41. data/lib/pakyow/data/subscribers/adapters/redis/pipeliner.rb +45 -0
  42. data/lib/pakyow/data/subscribers/adapters/redis/scripts/_shared.lua +73 -0
  43. data/lib/pakyow/data/subscribers/adapters/redis/scripts/expire.lua +16 -0
  44. data/lib/pakyow/data/subscribers/adapters/redis/scripts/persist.lua +15 -0
  45. data/lib/pakyow/data/subscribers/adapters/redis/scripts/register.lua +37 -0
  46. data/lib/pakyow/data/subscribers/adapters/redis.rb +209 -0
  47. data/lib/pakyow/data/subscribers.rb +148 -0
  48. data/lib/pakyow/data/tasks/bootstrap.rake +18 -0
  49. data/lib/pakyow/data/tasks/create.rake +22 -0
  50. data/lib/pakyow/data/tasks/drop.rake +32 -0
  51. data/lib/pakyow/data/tasks/finalize.rake +56 -0
  52. data/lib/pakyow/data/tasks/migrate.rake +24 -0
  53. data/lib/pakyow/data/tasks/reset.rake +18 -0
  54. data/lib/pakyow/data/types.rb +37 -0
  55. data/lib/pakyow/data.rb +27 -0
  56. data/lib/pakyow/environment/data/auto_migrate.rb +31 -0
  57. data/lib/pakyow/environment/data/config.rb +54 -0
  58. data/lib/pakyow/environment/data/connections.rb +76 -0
  59. data/lib/pakyow/environment/data/memory_db.rb +23 -0
  60. data/lib/pakyow/validations/unique.rb +26 -0
  61. metadata +186 -0
@@ -0,0 +1,182 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+ require "sequel"
5
+
6
+ require "pakyow/support/inflector"
7
+
8
+ module Pakyow
9
+ module Data
10
+ class Migrator
11
+ def initialize(connection)
12
+ @connection = connection
13
+ end
14
+
15
+ IVARS_TO_DISCONNECT = %i(@runner @migrator).freeze
16
+
17
+ def disconnect!
18
+ IVARS_TO_DISCONNECT.each do |ivar|
19
+ if instance_variable_defined?(ivar) && value = instance_variable_get(ivar)
20
+ value.disconnect!
21
+ end
22
+ end
23
+
24
+ @connection.disconnect
25
+ end
26
+
27
+ def create!
28
+ migrator.create!
29
+ disconnect!
30
+
31
+ # Recreate the connection, since we just created the database it's supposed to connect to.
32
+ #
33
+ @connection = Connection.new(
34
+ opts: @connection.opts,
35
+ type: @connection.type,
36
+ name: @connection.name
37
+ )
38
+ end
39
+
40
+ def drop!
41
+ migrator.drop!
42
+ end
43
+
44
+ def migrate!
45
+ if migrations_to_run?
46
+ runner.run!
47
+ end
48
+ end
49
+
50
+ def auto_migrate!
51
+ migrator.auto_migrate!
52
+ end
53
+
54
+ def finalize!
55
+ migrator.finalize!.each do |filename, content|
56
+ FileUtils.mkdir_p(migration_path)
57
+
58
+ File.open(File.join(migration_path, filename), "w+") do |file|
59
+ file.write <<~CONTENT
60
+ Pakyow.migration do
61
+ #{content.to_s.split("\n").map { |line| " #{line}" }.join("\n")}
62
+ end
63
+ CONTENT
64
+ end
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ def migrator
71
+ @migrator = self.class.migrator_for_adapter(Support.inflector.classify(@connection.type), :Migrator).new(
72
+ @connection, sources: sources
73
+ )
74
+ end
75
+
76
+ def runner
77
+ @runner = self.class.migrator_for_adapter(Support.inflector.classify(@connection.type), :Runner).new(
78
+ @connection, migration_path
79
+ )
80
+ end
81
+
82
+ def sources
83
+ Pakyow.apps.reject(&:rescued?).flat_map { |app|
84
+ app.data.containers.flat_map(&:sources).concat(
85
+ app.plugs.flat_map { |plug|
86
+ plug.data.containers.flat_map(&:sources)
87
+ }
88
+ )
89
+ }.select { |source|
90
+ source.connection == @connection.name && source.adapter == @connection.type
91
+ }
92
+ end
93
+
94
+ def migrations
95
+ Dir.glob(File.join(migration_path, "*.rb"))
96
+ end
97
+
98
+ def migrations_to_run?
99
+ migrations.count > 0
100
+ end
101
+
102
+ def migration_path
103
+ File.join(Pakyow.config.data.migration_path, "#{@connection.type}/#{@connection.name}")
104
+ end
105
+
106
+ def track_exported_migrations
107
+ initial_migrations = migrations
108
+ yield
109
+ migrations - initial_migrations
110
+ end
111
+
112
+ class << self
113
+ def migrator_for_adapter(adapter, type = :Migrator)
114
+ Adapters.const_get(Support.inflector.camelize(adapter)).const_get(type)
115
+ end
116
+
117
+ def connect(adapter:, connection:, connection_overrides: {})
118
+ adapter = if adapter
119
+ adapter.to_sym
120
+ else
121
+ Pakyow.config.data.default_adapter
122
+ end
123
+
124
+ connection = if connection
125
+ connection.to_sym
126
+ else
127
+ Pakyow.config.data.default_connection
128
+ end
129
+
130
+ connection_opts = Connection.parse_connection_string(
131
+ Pakyow.config.data.connections.send(adapter)[connection]
132
+ )
133
+
134
+ merge_connection_overrides!(connection_opts, connection_overrides)
135
+ new(Connection.new(opts: connection_opts, type: adapter, name: connection))
136
+ end
137
+
138
+ def connect_global(adapter:, connection:, connection_overrides: {})
139
+ adapter = if adapter
140
+ adapter.to_sym
141
+ else
142
+ Pakyow.config.data.default_adapter
143
+ end
144
+
145
+ connection = if connection
146
+ connection.to_sym
147
+ else
148
+ Pakyow.config.data.default_connection
149
+ end
150
+
151
+ connection_opts = Connection.parse_connection_string(
152
+ Pakyow.config.data.connections.send(adapter)[connection]
153
+ )
154
+
155
+ merge_connection_overrides!(connection_opts, connection_overrides)
156
+ globalize_connection_opts!(adapter, connection_opts)
157
+
158
+ connect(
159
+ adapter: adapter,
160
+ connection: connection,
161
+ connection_overrides: connection_opts
162
+ )
163
+ end
164
+
165
+ def globalize_connection_opts!(adapter, connection_opts)
166
+ migrator_for_adapter(adapter).globalize_connection_opts!(connection_opts)
167
+ end
168
+
169
+ def merge_connection_overrides!(connection_opts, connection_overrides)
170
+ connection_overrides.each do |key, value|
171
+ key = key.to_sym
172
+ connection_opts[key] = if value.is_a?(Proc)
173
+ value.call(connection_opts[key])
174
+ else
175
+ value
176
+ end
177
+ end
178
+ end
179
+ end
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pakyow/support/bindable"
4
+ require "pakyow/support/class_state"
5
+ require "pakyow/support/makeable"
6
+ require "pakyow/support/indifferentize"
7
+
8
+ module Pakyow
9
+ module Data
10
+ # Wraps values for a data object returned by a source.
11
+ #
12
+ class Object
13
+ include Support::Bindable
14
+
15
+ attr_reader :values
16
+
17
+ # @api private
18
+ attr_accessor :originating_source
19
+
20
+ extend Support::ClassState
21
+ class_state :__serialized_methods, default: [], inheritable: true
22
+
23
+ extend Support::Makeable
24
+
25
+ def initialize(values)
26
+ @values = Support::IndifferentHash.new(values).freeze
27
+ end
28
+
29
+ def source
30
+ @originating_source.__object_name.name
31
+ end
32
+
33
+ def include?(key)
34
+ respond_to?(key)
35
+ end
36
+ alias key? include?
37
+
38
+ def [](key)
39
+ key = key.to_s.to_sym
40
+ if respond_to?(key)
41
+ public_send(key)
42
+ else
43
+ @values[key]
44
+ end
45
+ end
46
+
47
+ def method_missing(name, *_args)
48
+ if include?(name)
49
+ @values[name]
50
+ else
51
+ super
52
+ end
53
+ end
54
+
55
+ def respond_to_missing?(name, *)
56
+ @values.include?(name) || super
57
+ end
58
+
59
+ def to_h
60
+ hash = @values.dup
61
+ self.class.__serialized_methods.each do |method|
62
+ hash[method] = public_send(method)
63
+ end
64
+
65
+ hash
66
+ end
67
+
68
+ def to_json(*)
69
+ @values.to_json
70
+ end
71
+
72
+ def ==(other)
73
+ comparator = case other
74
+ when self.class
75
+ other
76
+ when Result
77
+ other.__getobj__
78
+ else
79
+ nil
80
+ end
81
+
82
+ comparator && comparator.class == self.class && comparator.values == @values
83
+ end
84
+
85
+ class << self
86
+ attr_reader :name
87
+
88
+ def make(name, state: nil, parent: nil, **kwargs, &block)
89
+ super(name, state: state, parent: parent, **kwargs, &block)
90
+ end
91
+
92
+ def serialize(*methods)
93
+ @__serialized_methods.concat(methods.map(&:to_sym)).uniq!
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,262 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pakyow/support/core_refinements/array/ensurable"
4
+ require "pakyow/support/deep_dup"
5
+ require "pakyow/support/inspectable"
6
+
7
+ require "pakyow/data/result"
8
+
9
+ module Pakyow
10
+ module Data
11
+ # @api private
12
+ class Proxy
13
+ include Support::Inspectable
14
+ inspectable :@source
15
+
16
+ using Support::Refinements::Array::Ensurable
17
+
18
+ using Support::DeepDup
19
+
20
+ attr_reader :source, :proxied_calls, :nested_proxies, :app
21
+
22
+ def initialize(source, subscribers, app)
23
+ @source, @subscribers, @app = source, subscribers, app
24
+
25
+ @proxied_calls = []
26
+ @subscribable = true
27
+ @nested_proxies = []
28
+ end
29
+
30
+ IVARS_TO_DUP = %i(@proxied_calls @nested_proxies).freeze
31
+
32
+ def deep_dup
33
+ super.tap do |duped|
34
+ IVARS_TO_DUP.each do |ivar|
35
+ duped.instance_variable_set(ivar, duped.instance_variable_get(ivar).deep_dup)
36
+ end
37
+ end
38
+ end
39
+
40
+ def method_missing(method_name, *args, &block)
41
+ if @source.command?(method_name)
42
+ dup.tap { |duped_proxy|
43
+ result = @source.command(method_name).call(*args) { |yielded_result|
44
+ duped_proxy.instance_variable_set(:@source, yielded_result)
45
+ yield duped_proxy if block_given?
46
+ }
47
+
48
+ if @source.respond_to?(:transaction) && @source.transaction?
49
+ @source.on_commit do
50
+ @subscribers.did_mutate(
51
+ @source.source_name, args[0], result
52
+ )
53
+ end
54
+ else
55
+ @subscribers.did_mutate(
56
+ @source.source_name, args[0], result
57
+ )
58
+ end
59
+ }
60
+ elsif @source.query?(method_name) || @source.modifier?(method_name)
61
+ dup.tap { |duped_proxy|
62
+ nested_calls = []
63
+
64
+ new_source = if block_given? && @source.block_for_nested_source?(method_name)
65
+ # In this case a block has been passed that would, without intervention,
66
+ # be called in context of a source instance. We don't want that, since
67
+ # it would provide full access to the underlying dataset. Instead the
68
+ # exposed object should simply be another proxy.
69
+
70
+ local_subscribers, local_app = @subscribers, @app
71
+ @source.source_from_self.public_send(method_name, *args) {
72
+ nested_proxy = Proxy.new(self, local_subscribers, local_app)
73
+ nested_proxy.instance_variable_set(:@proxied_calls, nested_calls)
74
+ nested_proxy.instance_exec(&block).source.tap do |nested_proxy_source|
75
+ duped_proxy.nested_proxies << nested_proxy.dup.tap do |finalized_nested_proxy|
76
+ finalized_nested_proxy.instance_variable_set(:@source, nested_proxy_source)
77
+ end
78
+ end
79
+ }
80
+ else
81
+ @source.source_from_self.public_send(method_name, *args).tap do |working_source|
82
+ working_source.included.each do |_, included_source|
83
+ nested_proxy = Proxy.new(included_source, @subscribers, @app)
84
+ duped_proxy.nested_proxies << nested_proxy
85
+ end
86
+ end
87
+ end
88
+
89
+ duped_proxy.instance_variable_set(:@source, new_source)
90
+ duped_proxy.instance_variable_get(:@proxied_calls) << [
91
+ method_name, args, nested_calls
92
+ ]
93
+ }
94
+ else
95
+ if Array.instance_methods.include?(method_name) && !@source.class.instance_methods.include?(method_name)
96
+ build_result(
97
+ @source.to_a.public_send(method_name, *args, &block),
98
+ method_name, args
99
+ )
100
+ elsif @source.class.instance_methods.include?(method_name)
101
+ build_result(
102
+ @source.public_send(method_name, *args, &block),
103
+ method_name, args
104
+ )
105
+ else
106
+ super
107
+ end
108
+ end
109
+ end
110
+
111
+ def respond_to_missing?(method_name, include_private = false)
112
+ return false if method_name == :marshal_dump || method_name == :marshal_load
113
+ @source.command?(method_name) || @source.query?(method_name) || @source.modifier?(method_name) || @source.respond_to?(method_name, include_private)
114
+ end
115
+
116
+ def to_ary
117
+ to_a
118
+ end
119
+
120
+ def to_json(*)
121
+ @source.to_json
122
+ end
123
+
124
+ def subscribe(subscriber, handler:, payload: nil, &block)
125
+ subscriptions = []
126
+
127
+ if subscribable?
128
+ subscriptions << {
129
+ source: @source.source_name,
130
+ ephemeral: @source.is_a?(Sources::Ephemeral),
131
+ handler: handler,
132
+ payload: payload,
133
+ qualifications: qualifications,
134
+ proxy: self
135
+ }
136
+
137
+ @nested_proxies.each do |related_proxy|
138
+ subscriptions.concat(
139
+ related_proxy.subscribe_related(
140
+ parent_source: @source,
141
+ serialized_proxy: self,
142
+ handler: handler,
143
+ payload: payload
144
+ )
145
+ )
146
+ end
147
+ end
148
+
149
+ @subscribers.register_subscriptions(subscriptions, subscriber: subscriber, &block)
150
+ end
151
+
152
+ def subscribe_related(parent_source:, serialized_proxy:, handler:, payload: nil)
153
+ subscriptions = []
154
+
155
+ if association = parent_source.class.find_association_to_source(@source)
156
+ parent_source.each do |parent_result|
157
+ subscriptions << {
158
+ source: @source.source_name,
159
+ handler: handler,
160
+ payload: payload,
161
+ qualifications: qualifications.merge(
162
+ association.associated_query_field => parent_result[association.query_field]
163
+ ),
164
+ proxy: serialized_proxy
165
+ }
166
+ end
167
+ else
168
+ Pakyow.logger.error "tried to subscribe a related source, but we don't know how it's related"
169
+ end
170
+
171
+ @nested_proxies.each do |related_proxy|
172
+ subscriptions.concat(
173
+ related_proxy.subscribe_related(
174
+ parent_source: @source,
175
+ serialized_proxy: serialized_proxy,
176
+ handler: handler,
177
+ payload: payload
178
+ )
179
+ )
180
+ end
181
+
182
+ subscriptions
183
+ end
184
+
185
+ def unsubscribe
186
+ subscribable(false)
187
+ end
188
+
189
+ def subscribable(boolean)
190
+ tap do
191
+ @subscribable = boolean
192
+ end
193
+ end
194
+
195
+ def subscribable?
196
+ @subscribable == true
197
+ end
198
+
199
+ def _dump(_)
200
+ Marshal.dump(
201
+ {
202
+ app: @app,
203
+ source: @source.is_a?(Sources::Ephemeral) ? @source : @source.source_name,
204
+ proxied_calls: @proxied_calls
205
+ }
206
+ )
207
+ end
208
+
209
+ def self._load(state)
210
+ state = Marshal.load(state)
211
+
212
+ case state[:source]
213
+ when Sources::Ephemeral
214
+ ephemeral = state[:app].data.ephemeral(state[:source].source_name)
215
+ ephemeral.instance_variable_set(:@source, state[:source])
216
+ ephemeral
217
+ else
218
+ state[:app].data.public_send(state[:source]).apply(state[:proxied_calls])
219
+ end
220
+ end
221
+
222
+ def qualifications
223
+ @proxied_calls.inject(@source.qualifications) { |qualifications, proxied_call|
224
+ qualifications_for_proxied_call = @source.class.qualifications(proxied_call[0]).dup
225
+
226
+ # Populate argument qualifications with argument values.
227
+ #
228
+ qualifications_for_proxied_call.each do |qualification_key, qualification_value|
229
+ next unless qualification_value.to_s.start_with?("__arg")
230
+ arg_number = qualification_value.to_s.gsub(/[^0-9]/, "").to_i
231
+ qualifications_for_proxied_call[qualification_key] = @source.class.attributes[qualification_key][proxied_call[1][arg_number]]
232
+ end
233
+
234
+ qualifications.merge(qualifications_for_proxied_call)
235
+ }
236
+ end
237
+
238
+ # @api private
239
+ def apply(proxied_calls)
240
+ proxied_calls.inject(self) { |proxy, proxied_call|
241
+ if proxied_call[2].any?
242
+ proxy.public_send(proxied_call[0], *proxied_call[1]) do
243
+ apply(proxied_call[2])
244
+ end
245
+ else
246
+ proxy.public_send(proxied_call[0], *proxied_call[1])
247
+ end
248
+ }
249
+ end
250
+
251
+ private
252
+
253
+ def build_result(value, method_name, args)
254
+ if method_name.to_s.end_with?("?")
255
+ value
256
+ else
257
+ Result.new(value, self, originating_method: method_name, originating_args: args)
258
+ end
259
+ end
260
+ end
261
+ end
262
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "delegate"
4
+
5
+ module Pakyow
6
+ module Data
7
+ class Result < SimpleDelegator
8
+ def initialize(result, proxy, originating_method: nil, originating_args: [])
9
+ @__proxy = proxy
10
+ @originating_method = originating_method
11
+ @originating_args = originating_args
12
+ __setobj__(result)
13
+ end
14
+
15
+ def nil?
16
+ __getobj__.nil?
17
+ end
18
+
19
+ def marshal_dump
20
+ {
21
+ proxy: {
22
+ app: @__proxy.app,
23
+ source: @__proxy.source.source_name,
24
+ proxied_calls: @__proxy.proxied_calls
25
+ },
26
+
27
+ originating_method: @originating_method,
28
+ originating_args: @originating_args
29
+ }
30
+ end
31
+
32
+ def marshal_load(state)
33
+ result = state[:proxy][:app].data.public_send(
34
+ state[:proxy][:source]
35
+ ).apply(
36
+ state[:proxy][:proxied_calls]
37
+ )
38
+
39
+ if state[:originating_method]
40
+ result = result.public_send(state[:originating_method], *state[:originating_args])
41
+ end
42
+
43
+ __setobj__(result)
44
+ end
45
+
46
+ # Fixes an issue using pp inside a delegator.
47
+ #
48
+ def pp(*args)
49
+ Kernel.pp(*args)
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "delegate"
4
+
5
+ require "pakyow/support/class_state"
6
+
7
+ module Pakyow
8
+ module Data
9
+ module Sources
10
+ class Abstract < SimpleDelegator
11
+ extend Support::ClassState
12
+ class_state :__finalized, default: false, inheritable: true
13
+
14
+ def initialize(dataset)
15
+ __setobj__(dataset)
16
+ end
17
+
18
+ # Fixes an issue using pp inside a delegator.
19
+ #
20
+ def pp(*args)
21
+ Kernel.pp(*args)
22
+ end
23
+
24
+ # @api private
25
+ attr_reader :original_results
26
+
27
+ # @api private
28
+ def qualifications
29
+ {}
30
+ end
31
+
32
+ # @api private
33
+ def command?(_maybe_command_name)
34
+ false
35
+ end
36
+
37
+ # @api private
38
+ def query?(_maybe_query_name)
39
+ false
40
+ end
41
+
42
+ # @api private
43
+ def modifier?(_maybe_modifier_name)
44
+ false
45
+ end
46
+
47
+ # @api private
48
+ def source_from_self(dataset = __getobj__)
49
+ self.class.source_from_source(self, dataset)
50
+ end
51
+
52
+ class << self
53
+ attr_reader :container
54
+
55
+ def instance
56
+ container.source(plural_name)
57
+ end
58
+
59
+ # @api private
60
+ attr_writer :container
61
+
62
+ # @api private
63
+ def plural_name
64
+ Support.inflector.pluralize(__object_name.name).to_sym
65
+ end
66
+
67
+ # @api private
68
+ def singular_name
69
+ Support.inflector.singularize(__object_name.name).to_sym
70
+ end
71
+
72
+ # @api private
73
+ def source_from_source(source, dataset)
74
+ source.dup.tap do |duped_source|
75
+ duped_source.__setobj__(dataset)
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end