praxis-mapper 3.4.0 → 4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e7b9f2ce4f066cc2efacc2305788cb72c6e7b263
4
- data.tar.gz: c1c76b006d57dac6e868e43109f37db79931b9b4
3
+ metadata.gz: 04a36d68c2385426a0b0aed2295c41299b0c3943
4
+ data.tar.gz: 5eca3e994f89a94f67a7c1f0d9ec3ed64c274793
5
5
  SHA512:
6
- metadata.gz: 5f4e0bc3f8e16c1b73a6268c5f73b4c57a692aefaa600f5113280b44f37c7543535be23717b90ca5e79c99e9fa7ade59e4205a829c1035ffd9f0b023409df882
7
- data.tar.gz: ca517a3d90bbf750aacc028bd6a1b6f0ee22bb82c632a1777933ca68e2714a73f7e64311d337f975a7d4b02c1fe35fbca02a6eccc1bf182d8d716203027c6a79
6
+ metadata.gz: 9604920ffc4719206ee8dd41cb2e3a642bb87f8efd64fa44c310d1e6db6f4a702a94c9965724bd98d27984336d745d06b131f766752b989ccfbeb127b754bf9e
7
+ data.tar.gz: 88a2a5c4c87f99701c955e4f1063e8d5169363f6250cf06b992b53f978581b6cc35cbd645c4d2c34229146e8ada75534ad5b338598038c870a2ace98f214b895
@@ -1,4 +1,5 @@
1
1
  language: ruby
2
2
  rvm:
3
- - "2.1.2"
3
+ - 2.1.2
4
+ - 2.2.2
4
5
  script: bundle exec rspec spec
@@ -1,6 +1,25 @@
1
1
  # praxis-mapper changelog
2
2
 
3
- ## next
3
+ ## 4.0
4
+
5
+ * Optimization on handling repeated ids (especially noticeable when using subloads)
6
+ * Refactored `ConnectionManager` repository handling to improve integration with other connection-pooling (specifically Sequel's at present).
7
+ * Added two types of connection factory under `Praxis::Mapper::ConnectionFactories::`:
8
+ * `Simple`: Takes a `connection:` option to specify the raw object to return for all `checkout`. Also, preserves current behavior with proc-based uses when `ConnectionManager.repository` is given a block. This is the default factory type if one is not specified for the repository.
9
+ * `Sequel`: Takes `connection:` option to specify a `Sequel::Database`, or hash of options to pass to `Sequel.connect`.
10
+ * `IdentityMap#finalize!` now calls `ConnectionManager#release` to ensure any connections are returned to their respective pools, if applicable.
11
+ * Added `SequelCompat` module, which provides support for using `Sequel::Model` objects with an `IdentityMap` when included in model class.
12
+ * This overrides the association accessors on instances associated with an `IdentityMap` (see below for more on this) to query the map instead of database.
13
+ * See (spec/support/spec_sequel_models.rb) for example definition.
14
+ * Added prototype for write-path support to `IdentityMap` (for `Sequel::Model` models):
15
+ * `IdentityMap#attach(record)`: adds the record to the identity map, saving it to the database first if it does not have a value for its primary key (or other identities).
16
+ * `IdentityMap#detatch(record)`: removes the record from the identity map
17
+ * `IdentityMap#flush!(record_or_class=nil)`: depending on the value of `record_or_class` it:
18
+ * with no argument, or nil, given it saves all modified records in the identity map.
19
+ * with an instance of a model, it saves just that record.
20
+ * with a model class, it saves all modified records for that class in the identity map.
21
+ * `IdentityMap#remove`: calls `detatch` with the record, and then calls`record.delete` to delete it.
22
+
4
23
 
5
24
  ## 3.4.0
6
25
 
@@ -45,12 +45,19 @@ end
45
45
  require 'praxis-mapper/finalizable'
46
46
  require 'praxis-mapper/logging'
47
47
 
48
+ require 'praxis-mapper/identity_map_extensions/persistence'
48
49
  require 'praxis-mapper/identity_map'
49
50
 
50
51
  require 'praxis-mapper/model'
51
52
  require 'praxis-mapper/query_statistics'
53
+
54
+ require 'praxis-mapper/sequel_compat'
55
+
52
56
  require 'praxis-mapper/connection_manager'
53
57
 
58
+ require 'praxis-mapper/connection_factories/simple'
59
+ require 'praxis-mapper/connection_factories/sequel'
60
+
54
61
  require 'praxis-mapper/resource'
55
62
 
56
63
  require 'praxis-mapper/query/base'
@@ -0,0 +1,66 @@
1
+ module Praxis::Mapper
2
+ module ConnectionFactories
3
+
4
+ class Sequel
5
+
6
+ def initialize(connection:nil, **opts)
7
+ raise ArgumentError, 'May not provide both a connection and opts' if connection && !opts.empty?
8
+
9
+ if connection
10
+ @connection = connection
11
+ else
12
+ @connection = ::Sequel.connect(**opts)
13
+ end
14
+
15
+ # steal timeout values so we can replicate the same timeout behavior
16
+ @timeout = @connection.pool.instance_variable_get(:@timeout)
17
+ @sleep_time = @connection.pool.instance_variable_get(:@sleep_time)
18
+
19
+ # connections that we created explicitly
20
+ @owned_connections = Hash.new
21
+ end
22
+
23
+ def checkout(connection_manager)
24
+ # copied from Sequel's ThreadedConnectionPool#hold
25
+ # to ensure consistent behavior
26
+ unless acquire(connection_manager.thread)
27
+ time = Time.now
28
+ timeout = time + @timeout
29
+ sleep_time = @sleep_time
30
+ sleep sleep_time
31
+ until acquire(connection_manager.thread)
32
+ raise(::Sequel::PoolTimeout) if Time.now > timeout
33
+ sleep sleep_time
34
+ end
35
+ end
36
+
37
+ @connection
38
+ end
39
+
40
+ def release(connection_manager, connection)
41
+ # ensure we only release connections we own, in case
42
+ # we've acquired a connection from Sequel that
43
+ # is likely still in use.
44
+ if (@owned_connections.delete(connection_manager.thread))
45
+ @connection.pool.send(:sync) do
46
+ @connection.pool.send(:release,connection_manager.thread)
47
+ end
48
+ end
49
+ end
50
+
51
+ def acquire(thread)
52
+ # check connection's pool to see if it already has a connection
53
+ # if so, re-use it. otherwise, acquire a new one and mark that we
54
+ # "own" it for future releasing.
55
+ if (owned = @connection.pool.send(:owned_connection, thread))
56
+ return true
57
+ else
58
+ conn = @connection.pool.send(:acquire, thread)
59
+ @owned_connections[thread] = conn
60
+ true
61
+ end
62
+ end
63
+
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,27 @@
1
+ module Praxis::Mapper
2
+ module ConnectionFactories
3
+ class Simple
4
+ def initialize(connection: nil, &block)
5
+ @connection = connection if connection
6
+ if block
7
+ @checkout = block
8
+ end
9
+
10
+ if @connection && @checkout
11
+ raise ArgumentError, 'May not provide both a connection and block'
12
+ end
13
+ end
14
+
15
+ def checkout(connection_manager)
16
+ return @connection if @connection
17
+
18
+ @checkout.call
19
+ end
20
+
21
+ def release(connection_manager, connection)
22
+ true
23
+ end
24
+
25
+ end
26
+ end
27
+ end
@@ -1,61 +1,39 @@
1
1
  module Praxis::Mapper
2
2
  class ConnectionManager
3
3
 
4
- # Configures a data store.
5
- def self.setup(config_data={}, &block)
6
- config_data.each do |repository_name, data|
7
- klass_name = data.delete(:connection_factory)
8
- connection_factory_class = Object.const_get(klass_name)
9
- repositories[repository_name][:connection_factory] = connection_factory_class.new(data[:connection_opts])
4
+ @repositories = {}
5
+ class << self
6
+ attr_accessor :repositories
7
+ end
10
8
 
11
- if (query_klass_name = data.delete(:query))
12
- query_klass = Object.const_get(query_klass_name)
13
- repositories[repository_name][:query] = query_klass
14
- end
15
- end
9
+ def self.setup(&block)
16
10
  if block_given?
17
11
  self.instance_eval(&block)
18
12
  end
19
13
  end
20
14
 
21
- def self.repository(repository_name, data=nil,&block)
22
- return repositories[repository_name] if data.nil? && !block_given?
15
+ def self.repository(repository_name, **data, &block)
16
+ return repositories[repository_name] if data.empty? && !block_given?
23
17
 
24
- if data && data[:query]
25
- query_klass = case data[:query]
26
- when String
27
- query_klass_name = data[:query]
28
- Object.const_get(query_klass_name) #FIXME: won't really work consistently
29
- when Class
30
- data[:query]
31
- when Symbol
32
- raise "symbol support is not implemented yet"
33
- else
34
- raise "unknown type for query: #{data[:query].inspect} has type #{data[:query].class}"
35
- end
36
- repositories[repository_name][:query] = query_klass
37
- end
18
+ query = data[:query] || Praxis::Mapper::Query::Sql
19
+ factory_class = data[:factory] || ConnectionFactories::Simple
38
20
 
39
- if block_given?
40
- # TODO: ? complain if data.has_key?(:connection_factory)
41
- repositories[repository_name][:connection_factory] = block
42
- elsif data
43
- klass_name = data.delete(:connection_factory)
44
- connection_factory_class = Object.const_get(klass_name) #FIXME: won't really work consistently
45
- repositories[repository_name][:connection_factory] = connection_factory_class.new(data[:connection_opts])
21
+ opts = data[:opts] || {}
22
+ if query.kind_of? String
23
+ query = query.constantize
46
24
  end
47
- end
48
-
49
25
 
50
- def self.repositories
51
- @repositories ||= Hash.new do |hash,key|
52
- hash[key] = {
53
- :connection_factory => nil,
54
- :query => Praxis::Mapper::Query::Sql
55
- }
26
+ if factory_class.kind_of? String
27
+ factory_class = factory_class.constantize
56
28
  end
29
+
30
+ repositories[repository_name] = {
31
+ query: query,
32
+ factory: factory_class.new(**opts, &block)
33
+ }
57
34
  end
58
35
 
36
+
59
37
  def repositories
60
38
  self.class.repositories
61
39
  end
@@ -66,26 +44,27 @@ module Praxis::Mapper
66
44
 
67
45
  def initialize
68
46
  @connections = {}
47
+ @thread = Thread.current
48
+ end
49
+
50
+ def thread
51
+ return @thread if @thread == Thread.current
52
+ raise 'threading violation in ConnectionManager. Calling Thread is different from Thread that owns this instance.'
69
53
  end
70
54
 
71
55
  def checkout(name)
72
56
  connection = @connections[name]
73
57
  return connection if connection
74
58
 
75
- factory = repositories[name][:connection_factory]
76
- connection = if factory.kind_of?(Proc)
77
- factory.call
78
- else
79
- factory.checkout
80
- end
59
+ factory = repositories[name][:factory]
60
+ connection = factory.checkout(self)
81
61
 
82
62
  @connections[name] = connection
83
63
  end
84
64
 
85
65
  def release_one(name)
86
66
  if (connection = @connections.delete(name))
87
- return true if repositories[name][:connection_factory].kind_of? Proc
88
- repositories[name][:connection_factory].release(connection)
67
+ repositories[name][:factory].release(self, connection)
89
68
  end
90
69
  end
91
70
 
@@ -4,7 +4,8 @@
4
4
  # The scope can be thought of as a set of named filters.
5
5
  module Praxis::Mapper
6
6
  class IdentityMap
7
-
7
+ include IdentityMapExtensions::Persistence
8
+
8
9
  class UnloadedRecordException < StandardError; end;
9
10
  class UnsupportedModel < StandardError; end;
10
11
  class UnknownIdentity < StandardError; end;
@@ -69,6 +70,11 @@ module Praxis::Mapper
69
70
  @connection_manager = ConnectionManager.new
70
71
  @scope = scope
71
72
  clear!
73
+
74
+ # Ensure we clean up open connections
75
+ ObjectSpace.define_finalizer(self) do
76
+ @connection_manager.release
77
+ end
72
78
  end
73
79
 
74
80
  def clear!
@@ -104,7 +110,9 @@ module Praxis::Mapper
104
110
  end
105
111
 
106
112
  # TODO: rework this so it's a hash with default values and simplify #index
107
- @secondary_indexes = Hash.new
113
+ @secondary_indexes = Hash.new do |hash, model|
114
+ hash[model] = Hash.new
115
+ end
108
116
  end
109
117
 
110
118
 
@@ -184,9 +192,16 @@ module Praxis::Mapper
184
192
  finalize_model!(model).any?
185
193
  end
186
194
 
187
- finalize! if did_something
195
+ if did_something
196
+ finalize!
197
+ else
198
+ release
199
+ end
188
200
  end
189
201
 
202
+ def release
203
+ @connection_manager.release
204
+ end
190
205
 
191
206
  # don't doc. never ever use yourself!
192
207
  # FIXME: make private and fix specs that break?
@@ -293,8 +308,6 @@ module Praxis::Mapper
293
308
 
294
309
 
295
310
  def index(model, key, value)
296
- @secondary_indexes[model] ||= Hash.new
297
-
298
311
  unless @secondary_indexes[model].has_key? key
299
312
  @secondary_indexes[model][key] ||= Hash.new
300
313
  reindex!(model, key)
@@ -311,6 +324,7 @@ module Praxis::Mapper
311
324
  else
312
325
  row.send(key)
313
326
  end
327
+ # FIXME: make this a set? or handle duplicates better
314
328
  index(model, key, val) << row
315
329
  end
316
330
  end
@@ -325,7 +339,8 @@ module Praxis::Mapper
325
339
  if values.size == 1
326
340
  value = values[0]
327
341
  if @row_keys[model].has_key?(key)
328
- res = row_by_key(model, key, value)
342
+ res = @row_keys[model][key][value]
343
+
329
344
  if res
330
345
  [res]
331
346
  else
@@ -337,7 +352,7 @@ module Praxis::Mapper
337
352
  else
338
353
  if @row_keys[model].has_key?(key)
339
354
  values.collect do |value|
340
- row_by_key(model, key, value)
355
+ @row_keys[model][key][value]
341
356
  end.compact
342
357
  else
343
358
  values.each_with_object(Array.new) do |value, results|
@@ -375,20 +390,19 @@ module Praxis::Mapper
375
390
  end
376
391
  end
377
392
 
378
-
379
393
  def connection(name)
380
394
  @connection_manager.checkout(name)
381
395
  end
382
396
 
383
397
  def extract_keys(field, records)
384
- row_keys = []
398
+ row_keys = Set.new
385
399
  if field.kind_of?(Array) # composite identities
386
400
  records.each do |record|
387
401
  row_key = field.collect { |col| record.send(col) }
388
402
  row_keys << row_key unless row_key.include?(nil)
389
403
  end
390
404
  else
391
- row_keys.push *records.collect(&field).compact
405
+ row_keys.merge records.collect(&field).compact
392
406
  end
393
407
  row_keys
394
408
  end
@@ -418,9 +432,9 @@ module Praxis::Mapper
418
432
  key = tracked_association[:key]
419
433
  primary_key = tracked_association[:primary_key] || :id
420
434
 
421
- row_keys = []
435
+ row_keys = Set.new
422
436
  records.collect(&key).each do |keys|
423
- row_keys.push *keys
437
+ row_keys.merge keys
424
438
  end
425
439
 
426
440
  row_keys.reject! do |row_key|
@@ -438,7 +452,7 @@ module Praxis::Mapper
438
452
 
439
453
 
440
454
  def add_records(records)
441
- return [] if records.empty?
455
+ return [] if records.empty?
442
456
 
443
457
  to_stage = Hash.new do |hash,staged_model|
444
458
  hash[staged_model] = Hash.new do |identities, identity_name|
@@ -497,6 +511,16 @@ module Praxis::Mapper
497
511
  @row_keys[model][identity][key] = record
498
512
  end
499
513
 
514
+ @secondary_indexes[model].each do |key, indexed_values|
515
+ val = if key.kind_of? Array
516
+ key.collect { |k| record.send(k) }
517
+ else
518
+ record.send(key)
519
+ end
520
+
521
+ indexed_values[val] << record
522
+ end
523
+
500
524
  record.identity_map = self
501
525
  @rows[model] << record
502
526
  record
@@ -0,0 +1,83 @@
1
+ module Praxis::Mapper
2
+ module IdentityMapExtensions
3
+ module Persistence
4
+
5
+ def deindex(record)
6
+ model = record.class
7
+
8
+ # delete from full set of rows
9
+ rows_for(model).delete record
10
+
11
+ # remove record from identity indexes
12
+ @row_keys[model].each do |identity, index|
13
+ index.delete_if {|k,v| v == record }
14
+ end
15
+
16
+ # remove any secondary indexes
17
+ @secondary_indexes[model].each do |key, index|
18
+ index.each do |index_key, indexed_values|
19
+ indexed_values.delete record
20
+ end
21
+ end
22
+ end
23
+
24
+ def reindex(record)
25
+ # fully remove the record from any indexes it may be part of
26
+ deindex(record)
27
+
28
+ # hack to update any indexes as applicable
29
+ add_record(record)
30
+ end
31
+
32
+ # attach record to the identity map.
33
+ # save the record if, and only if, we need to
34
+ def attach(record)
35
+ # save unless it has all identities populated
36
+ unless record.identities.all? { |identity, value| value }
37
+ record.save
38
+ end
39
+
40
+ # raise if still don't have full identities
41
+ unless record.identities.all? { |identity, value| value }
42
+ raise "can not attach #{record.inspect} without a full set of identities."
43
+ end
44
+
45
+ add_record(record)
46
+
47
+ # TODO: what to do with related records?
48
+ end
49
+
50
+ def flush!(object=nil)
51
+ if object.nil?
52
+ return @rows.keys.each { |klass| self.flush!(klass) }
53
+ end
54
+
55
+ case object
56
+ when Class
57
+ @rows[object].select(&:modified?).each do |record|
58
+ record.save
59
+ reindex(record)
60
+ end
61
+ when Sequel::Model
62
+ if object.modified?
63
+ object.save
64
+ reindex(object)
65
+ end
66
+ end
67
+ end
68
+
69
+ def remove(record)
70
+ detach(record)
71
+
72
+ record.delete
73
+ end
74
+
75
+ def detach(record)
76
+ record.identity_map = nil
77
+ deindex(record)
78
+ end
79
+ end
80
+
81
+
82
+ end
83
+ end