pod4 0.10.6 → 1.0.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 (48) hide show
  1. checksums.yaml +5 -5
  2. data/.bugs/bugs +2 -1
  3. data/.bugs/details/b5368c7ef19065fc597b5692314da71772660963.txt +53 -0
  4. data/.hgtags +1 -0
  5. data/Gemfile +5 -5
  6. data/README.md +157 -46
  7. data/lib/pod4/basic_model.rb +9 -22
  8. data/lib/pod4/connection.rb +67 -0
  9. data/lib/pod4/connection_pool.rb +154 -0
  10. data/lib/pod4/errors.rb +20 -0
  11. data/lib/pod4/interface.rb +34 -12
  12. data/lib/pod4/model.rb +32 -27
  13. data/lib/pod4/nebulous_interface.rb +25 -30
  14. data/lib/pod4/null_interface.rb +22 -16
  15. data/lib/pod4/pg_interface.rb +84 -104
  16. data/lib/pod4/sequel_interface.rb +138 -82
  17. data/lib/pod4/tds_interface.rb +83 -70
  18. data/lib/pod4/tweaking.rb +105 -0
  19. data/lib/pod4/version.rb +1 -1
  20. data/md/breaking_changes.md +80 -0
  21. data/spec/common/basic_model_spec.rb +67 -70
  22. data/spec/common/connection_pool_parallelism_spec.rb +154 -0
  23. data/spec/common/connection_pool_spec.rb +246 -0
  24. data/spec/common/connection_spec.rb +129 -0
  25. data/spec/common/model_ai_missing_id_spec.rb +256 -0
  26. data/spec/common/model_plus_encrypting_spec.rb +16 -4
  27. data/spec/common/model_plus_tweaking_spec.rb +128 -0
  28. data/spec/common/model_plus_typecasting_spec.rb +10 -4
  29. data/spec/common/model_spec.rb +283 -363
  30. data/spec/common/nebulous_interface_spec.rb +159 -108
  31. data/spec/common/null_interface_spec.rb +88 -65
  32. data/spec/common/sequel_interface_pg_spec.rb +217 -161
  33. data/spec/common/shared_examples_for_interface.rb +50 -50
  34. data/spec/jruby/sequel_encrypting_jdbc_pg_spec.rb +1 -1
  35. data/spec/jruby/sequel_interface_jdbc_ms_spec.rb +3 -3
  36. data/spec/jruby/sequel_interface_jdbc_pg_spec.rb +3 -23
  37. data/spec/mri/pg_encrypting_spec.rb +1 -1
  38. data/spec/mri/pg_interface_spec.rb +311 -223
  39. data/spec/mri/sequel_encrypting_spec.rb +1 -1
  40. data/spec/mri/sequel_interface_spec.rb +177 -180
  41. data/spec/mri/tds_encrypting_spec.rb +1 -1
  42. data/spec/mri/tds_interface_spec.rb +296 -212
  43. data/tags +340 -174
  44. metadata +19 -11
  45. data/md/fixme.md +0 -3
  46. data/md/roadmap.md +0 -125
  47. data/md/typecasting.md +0 -80
  48. data/spec/common/model_new_validate_spec.rb +0 -204
@@ -1,8 +1,8 @@
1
- require 'octothorpe'
1
+ require "octothorpe"
2
2
 
3
- require_relative 'metaxing'
4
- require_relative 'errors'
5
- require_relative 'alert'
3
+ require_relative "metaxing"
4
+ require_relative "errors"
5
+ require_relative "alert"
6
6
 
7
7
 
8
8
  module Pod4
@@ -20,17 +20,15 @@ module Pod4
20
20
  class BasicModel
21
21
  extend Metaxing
22
22
 
23
-
24
23
  # The value of the ID field on the record
25
24
  attr_reader :model_id
26
25
 
27
26
  # one of Model::STATII
28
27
  attr_reader :model_status
29
28
 
30
- # Valid values for @model_status: :error :warning :okay :deleted or :empty
29
+ # Valid values for @model_status: :error :warning :okay :deleted or :unknown
31
30
  STATII = %i|error warning okay deleted empty|
32
31
 
33
-
34
32
  class << self
35
33
 
36
34
  ##
@@ -44,27 +42,23 @@ module Pod4
44
42
  raise NotImplemented, "no call to set_interface in the model"
45
43
  end
46
44
 
47
- end
48
- ##
49
-
45
+ end # of class << self
50
46
 
51
47
  ##
52
48
  # Initialize a model by passing it a unique id value.
53
49
  # Override this to set initial values for your column attributes.
54
50
  #
55
51
  def initialize(id=nil)
56
- @model_status = :empty
52
+ @model_status = :unknown
57
53
  @model_id = id
58
54
  @alerts = []
59
55
  end
60
56
 
61
-
62
57
  ##
63
58
  # Syntactic sugar; same as self.class.interface, which returns the interface instance.
64
59
  #
65
60
  def interface; self.class.interface; end
66
61
 
67
-
68
62
  ##
69
63
  # Return the list of alerts.
70
64
  #
@@ -72,11 +66,10 @@ module Pod4
72
66
  #
73
67
  def alerts; @alerts.dup; end
74
68
 
75
-
76
69
  ##
77
70
  # Clear down the alerts.
78
71
  #
79
- # Note that set model_status to :okay. Theoretically it might need to be :empty or :deleted,
72
+ # Note that we set model_status to :okay. Theoretically it might need to be :unknown or :deleted,
80
73
  # but if you are calling clear_alerts before a call to `read` or after a call to `delete`, then
81
74
  # you have more problems than I can solve.
82
75
  #
@@ -85,7 +78,6 @@ module Pod4
85
78
  @model_status = :okay
86
79
  end
87
80
 
88
-
89
81
  ##
90
82
  # Raise a Pod4 exception for the model if any alerts are status :error; otherwise do
91
83
  # nothing.
@@ -100,13 +92,10 @@ module Pod4
100
92
  raise ValidationError.from_alert(al) if al && al.type == :error
101
93
  self
102
94
  end
103
-
104
95
  alias :or_die :raise_exceptions
105
96
 
106
-
107
97
  private
108
98
 
109
-
110
99
  ##
111
100
  # Add a Pod4::Alert to the model instance @alerts attribute
112
101
  #
@@ -123,10 +112,8 @@ module Pod4
123
112
  st = @alerts.sort.first.type
124
113
  @model_status = st if %i|error warning|.include?(st)
125
114
  end
126
-
127
115
 
128
- end
129
- ##
116
+ end # of BasicModel
130
117
 
131
118
 
132
119
  end
@@ -0,0 +1,67 @@
1
+ require_relative 'interface'
2
+
3
+
4
+ module Pod4
5
+
6
+
7
+ class Connection
8
+
9
+ attr_reader :interface_class
10
+ attr_accessor :data_layer_options
11
+
12
+ ##
13
+ # Intitialise a Connection. You must pass a Pod4::Interface class. The connection object will
14
+ # only accept calls from instances of this class.
15
+ #
16
+ # `conn = Pod4::Connection.new(interface: MyInterface)`
17
+ #
18
+ def initialize(args)
19
+ raise ArgumentError, "Connection#new needs a Hash" unless args.is_a? Hash
20
+ raise ArgumentError, "You must pass a Pod4::Interface" \
21
+ unless args[:interface] \
22
+ && args[:interface].is_a?(Class) \
23
+ && args[:interface].ancestors.include?(Interface)
24
+
25
+ @interface_class = args[:interface]
26
+ @data_layer_options = nil
27
+ @client = nil
28
+ @options = nil
29
+ end
30
+
31
+ ##
32
+ # When an interface wants a connection, it calls connection.client. If the connection does
33
+ # not have one, it asks the interface for one....
34
+ #
35
+ # Interface is an instance of whatever class you passed to Connection when you initialised
36
+ # it. That is: when an interface wants a connection, it passes `self`.
37
+ #
38
+ def client(interface)
39
+ fail_bad_interfaces(interface)
40
+ @client ||= interface.new_connection(@data_layer_options)
41
+ @client
42
+ end
43
+
44
+ ##
45
+ # Close the connection.
46
+ # In the case of a single connection, this is probably not going to get used much. But.
47
+ #
48
+ def close(interface)
49
+ fail_bad_interfaces(interface)
50
+ interface.close_connection
51
+ @client = nil
52
+ return self
53
+ end
54
+
55
+ private
56
+
57
+ def fail_bad_interfaces(f)
58
+ raise ArgumentError, "That's not a #@interface_class", caller \
59
+ unless f.kind_of?(@interface_class)
60
+
61
+ end
62
+
63
+ end # of Connection
64
+
65
+
66
+ end
67
+
@@ -0,0 +1,154 @@
1
+ require "time"
2
+ require "thread"
3
+
4
+ require_relative "interface"
5
+ require_relative "connection"
6
+ require_relative "errors"
7
+
8
+
9
+ module Pod4
10
+
11
+
12
+ class ConnectionPool < Connection
13
+
14
+ PoolItem = Struct.new(:client, :thread_id)
15
+
16
+ class Pool
17
+ def initialize
18
+ @items = []
19
+ @mutex = Mutex.new
20
+ end
21
+
22
+ def <<(cl)
23
+ @mutex.synchronize do
24
+ @items << PoolItem.new(cl, Thread.current.object_id)
25
+ end
26
+ end
27
+
28
+ def get_current
29
+ @items.find{|x| x.thread_id == Thread.current.object_id }
30
+ end
31
+
32
+ def get_free
33
+ @mutex.synchronize do
34
+ pi = @items.find{|x| x.thread_id.nil? }
35
+ pi.thread_id = Thread.current.object_id if pi
36
+ pi
37
+ end
38
+ end
39
+
40
+ def release
41
+ pi = get_current
42
+ pi.thread_id = nil if pi
43
+ end
44
+
45
+ def drop
46
+ @items.delete_if{|x| x.thread_id == Thread.current.object_id }
47
+ end
48
+
49
+ def size
50
+ @items.size
51
+ end
52
+
53
+ def _dump
54
+ @mutex.synchronize do
55
+ @items
56
+ end
57
+ end
58
+ end # of Pool
59
+
60
+ attr_reader :max_clients, :max_wait
61
+
62
+ DEFAULT_MAX_CLIENTS = 10
63
+
64
+ ##
65
+ # As Connection, but with some options you can set.
66
+ #
67
+ # * max_clients -- if this many clients are assigned to threads, wait until one is freed.
68
+ # pass nil for no maximum. Tries to default to something sensible.
69
+ #
70
+ # * max_wait -- throw a Pod4::PoolTimeout if you wait more than this time in seconds.
71
+ # Pass nil to wait forever. Default is nil, because you would need to handle that timeout.
72
+ #
73
+ def initialize(args)
74
+ super(args)
75
+
76
+ @max_clients = args[:max_clients] || DEFAULT_MAX_CLIENTS
77
+ @max_wait = args[:max_wait]
78
+ @pool = Pool.new
79
+ end
80
+
81
+ ##
82
+ # Return a client for the interface to use.
83
+ #
84
+ # Return the client we gave this thread before.
85
+ # Failing that, assign a free one from the pool.
86
+ # Failing that, ask the interface to give us a new client.
87
+ #
88
+ # Note: The interface passes itself in case we want to call it back to get a new client; but
89
+ # clients are assigned to a _thread_. Every interface in a given thread gets the same pool
90
+ # item, the same client object.
91
+ #
92
+ def client(interface)
93
+ time = Time.now
94
+ cl = nil
95
+
96
+ # NB: We are constrained to use loop in order for our test to work
97
+ loop do
98
+ if (pi = @pool.get_current)
99
+ cl = pi.client
100
+ break
101
+ end
102
+
103
+ if (pi = @pool.get_free)
104
+ cl = pi.client
105
+ break
106
+ end
107
+
108
+ if @max_clients && @pool.size >= @max_clients
109
+ raise Pod4::PoolTimeout if @max_wait && (Time.now - time > @max_wait)
110
+ sleep 1
111
+ next
112
+ end
113
+
114
+ cl = interface.new_connection(@data_layer_options)
115
+ @pool << cl
116
+ break
117
+ end # of loop
118
+
119
+ cl
120
+ end
121
+
122
+ ##
123
+ # De-assign the client for the current thread from that thread.
124
+ #
125
+ # We never ask the interface to close the connection to the database. There is no advantage in
126
+ # doing that for us.
127
+ #
128
+ # Note: The interface passes itself in case we want to call it back to actually close the
129
+ # client; but clients are assigned to a _thread_.
130
+ #
131
+ def close(interface)
132
+ @pool.release
133
+ end
134
+
135
+ ##
136
+ # Remove the client object entirely -- for example, because the connection to the database has
137
+ # expired.
138
+ #
139
+ def drop(interface)
140
+ @pool.drop
141
+ end
142
+
143
+ ##
144
+ # Dump the internal pool (for test purposes only)
145
+ #
146
+ def _pool
147
+ @pool._dump
148
+ end
149
+
150
+ end # of ConnectionPool
151
+
152
+
153
+ end
154
+
@@ -94,6 +94,26 @@ module Pod4
94
94
  end
95
95
 
96
96
 
97
+ ##
98
+ # Raised if ConnectionPool times out waiting for a client to become free.
99
+ # This can only happen if your pool has a maximum number of clients set, and a max_wait value
100
+ # set; and if all the clients are currently in use.
101
+ #
102
+ class PoolTimeout < Pod4Error
103
+ attr_reader :field
104
+
105
+ def self.from_alert(alert)
106
+ self.new(alert.message, alert.field)
107
+ end
108
+
109
+ def initialize(message=nil, field=nil)
110
+ super(message || $! && $!.message)
111
+ @field = field.to_s.to_sym
112
+ end
113
+
114
+ end
115
+
116
+
97
117
 
98
118
  end
99
119
 
@@ -38,9 +38,15 @@ module Pod4
38
38
  # A field name in the data source, the name of the unique ID field.
39
39
  #
40
40
  def id_fld
41
- raise NotImplemented, "Interface needs to define an 'id_fld' method"
41
+ raise NotImplemented, "Interface has no 'id_fld' method (use `set_id_fld`?)"
42
42
  end
43
43
 
44
+ ##
45
+ # true if id_fld autoincrements
46
+ #
47
+ def id_ai
48
+ raise NotImplemented, "Interface has no 'id_ai' method (use `set_id_fld`?)"
49
+ end
44
50
 
45
51
  ##
46
52
  # Individual implementations are likely to have very different initialize methods, which will
@@ -51,7 +57,6 @@ module Pod4
51
57
  raise NotImplemented, "Interface needs to define an 'initialize' method"
52
58
  end
53
59
 
54
-
55
60
  ##
56
61
  # List accepts a parameter as selection criteria, and returns an array of Octothorpes. Exactly
57
62
  # what the selection criteria look like will vary from interface to interface. So will the
@@ -61,46 +66,63 @@ module Pod4
61
66
  # Note that list should ALWAYS return an array; never nil.
62
67
  #
63
68
  def list(selection=nil)
64
- raise NotImplemented, "Interface needs to define 'list' method"
69
+ raise NotImplemented, "Interface needs to define a 'list' method"
65
70
  end
66
71
 
67
-
68
72
  ##
69
73
  # Create accepts a record parameter (Hash or OT, but again, the format of this will vary)
70
74
  # representing a record, and creates the record. Should return the ID for the new record.
71
75
  #
72
76
  def create(record)
73
- raise NotImplemented, "Interface needs to define 'create' method"
77
+ raise NotImplemented, "Interface needs to define a 'create' method"
74
78
  end
75
79
 
76
-
77
80
  ##
78
81
  # Read accepts an ID, and returns an Octothorpe representing the unique record for that ID. If
79
82
  # there is no record matching the ID then it returns an empty Octothorpe.
80
83
  #
81
84
  def read(id)
82
- raise NotImplemented, "Interface needs to define 'read' method"
85
+ raise NotImplemented, "Interface needs to define a 'read' method"
83
86
  end
84
87
 
85
-
86
88
  ##
87
89
  # Update accepts an ID and a record parameter. It updates the record on the data source that
88
90
  # matches the ID using the record parameter. It returns self.
89
91
  #
90
92
  def update(id, record)
91
- raise NotImplemented, "Interface needs to define 'update' method"
93
+ raise NotImplemented, "Interface needs to define a 'update' method"
92
94
  end
93
95
 
94
-
95
96
  ##
96
97
  # delete removes the record with the given ID. returns self.
97
98
  #
98
99
  def delete(id)
99
- raise NotImplemented, "Interface needs to define 'delete' method"
100
+ raise NotImplemented, "Interface needs to define a 'delete' method"
100
101
  end
101
102
 
103
+ ##
104
+ # Called by a Connection object to start a database connection
105
+ #
106
+ def new_connection(args)
107
+ raise NotImplemented, "Interface needs to define a 'new_connection' method"
108
+ end
109
+
110
+ ##
111
+ # Called by a Connection Object to close the connection.
112
+ #
113
+ def close_connection(conn)
114
+ raise NotImplemented, "Interface needs to define a 'close_connection' method"
115
+ end
116
+
117
+ ##
118
+ # For testing purposes you should expose a _connection method that returns the Connection
119
+ # object the Interface uses
120
+ #
121
+ def _connection
122
+ raise NotImplemented, "Interface needs to define a '_connection' method"
123
+ end
102
124
 
103
- end
125
+ end # of Interface
104
126
 
105
127
 
106
128
  end