pod4 0.10.6 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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