praxis-mapper 3.4.0 → 4.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.
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