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
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pod4
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.6
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Jones
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-09-06 00:00:00.000000000 Z
11
+ date: 2019-05-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: devnull
@@ -47,12 +47,11 @@ email:
47
47
  executables: []
48
48
  extensions: []
49
49
  extra_rdoc_files:
50
- - md/fixme.md
51
- - md/roadmap.md
52
- - md/typecasting.md
50
+ - md/breaking_changes.md
53
51
  files:
54
52
  - ".bugs/bugs"
55
53
  - ".bugs/details/3979ce1679bc4f8c4aef8436e344e10e3773480d.txt"
54
+ - ".bugs/details/b5368c7ef19065fc597b5692314da71772660963.txt"
56
55
  - ".hgignore"
57
56
  - ".hgtags"
58
57
  - ".rspec"
@@ -63,6 +62,8 @@ files:
63
62
  - lib/pod4.rb
64
63
  - lib/pod4/alert.rb
65
64
  - lib/pod4/basic_model.rb
65
+ - lib/pod4/connection.rb
66
+ - lib/pod4/connection_pool.rb
66
67
  - lib/pod4/encrypting.rb
67
68
  - lib/pod4/errors.rb
68
69
  - lib/pod4/interface.rb
@@ -75,17 +76,20 @@ files:
75
76
  - lib/pod4/sequel_interface.rb
76
77
  - lib/pod4/sql_helper.rb
77
78
  - lib/pod4/tds_interface.rb
79
+ - lib/pod4/tweaking.rb
78
80
  - lib/pod4/typecasting.rb
79
81
  - lib/pod4/version.rb
80
- - md/fixme.md
81
- - md/roadmap.md
82
- - md/typecasting.md
82
+ - md/breaking_changes.md
83
83
  - pod4.gemspec
84
84
  - spec/README.md
85
85
  - spec/common/alert_spec.rb
86
86
  - spec/common/basic_model_spec.rb
87
- - spec/common/model_new_validate_spec.rb
87
+ - spec/common/connection_pool_parallelism_spec.rb
88
+ - spec/common/connection_pool_spec.rb
89
+ - spec/common/connection_spec.rb
90
+ - spec/common/model_ai_missing_id_spec.rb
88
91
  - spec/common/model_plus_encrypting_spec.rb
92
+ - spec/common/model_plus_tweaking_spec.rb
89
93
  - spec/common/model_plus_typecasting_spec.rb
90
94
  - spec/common/model_spec.rb
91
95
  - spec/common/nebulous_interface_spec.rb
@@ -129,7 +133,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
129
133
  version: '0'
130
134
  requirements: []
131
135
  rubyforge_project:
132
- rubygems_version: 2.5.1
136
+ rubygems_version: 2.7.6
133
137
  signing_key:
134
138
  specification_version: 4
135
139
  summary: Totally not an ORM
@@ -137,8 +141,12 @@ test_files:
137
141
  - spec/README.md
138
142
  - spec/common/alert_spec.rb
139
143
  - spec/common/basic_model_spec.rb
140
- - spec/common/model_new_validate_spec.rb
144
+ - spec/common/connection_pool_parallelism_spec.rb
145
+ - spec/common/connection_pool_spec.rb
146
+ - spec/common/connection_spec.rb
147
+ - spec/common/model_ai_missing_id_spec.rb
141
148
  - spec/common/model_plus_encrypting_spec.rb
149
+ - spec/common/model_plus_tweaking_spec.rb
142
150
  - spec/common/model_plus_typecasting_spec.rb
143
151
  - spec/common/model_spec.rb
144
152
  - spec/common/nebulous_interface_spec.rb
@@ -1,3 +0,0 @@
1
- Things I Wish I Could Do
2
- ========================
3
-
@@ -1,125 +0,0 @@
1
- Transactions
2
- ============
3
-
4
- I'd like to support basic transactions because without it we can't really claim to do optimistic
5
- locking properly. And it would be nice to claim that.
6
-
7
- Thought I had a solid idea of how to do it without any faffing around, but, it had some holes. Will
8
- have to think again.
9
-
10
- But, this is top of my wish list.
11
-
12
- For the record, my current thinking:
13
-
14
- customer.new(4).read.or_die
15
- customer.transaction do |c|
16
- c.update(foo: 'bar')
17
- c.orders.update(foo: 'bar')
18
- end.or_die
19
-
20
- * a method supports_transactions() will control whether the interface does that. sql_helper will
21
- define it to return true.
22
-
23
- * sql_helper will define a sql_transaction method which wraps SQL as a transaction.
24
-
25
- * interface methods create() delete() and update now accept an extra parameter, a boolean; if true,
26
- they will return sql (and values), rather than doing anything. This is defined in Pod4::Interface.
27
-
28
- * BasicModel defines @in_transaction = false; @tx_sql = ""; @tx_vals = [].
29
-
30
- * When a Model is @in_transaction, the create, delete and update methods pass the extra parameter
31
- to the corresponding interface methods. The results are accumulated in @tx_sql and @tx_vals.
32
-
33
- * the method BasicModel.transaction will:
34
-
35
- * set interface.in_transaction = true, or raise an error if the interface doesn't support them
36
- * yield a block passing the model instance so that the caller can run methods inside it
37
- * set in_transaction back to false.
38
- * call interface.executep( interface._sql_transaction( @tx_sql ), @tx_vals )
39
-
40
- Notes:
41
-
42
- * We will either have to standardize the execute method or check for it each time?
43
- * Ditto with executep. Ditto with whether an interface supports parameterisation.
44
- * trying to do a transaction across databases will fall over, but, really, no expectation there.
45
- * You can't have a transaction that uses the result of the first half to do the last half.
46
- * You can no longer call select() in a create, as we currently do for some interfaces? This is the
47
- real problem I am wrestling with -- how to allow a transaction that returns a value from create().
48
-
49
-
50
- Migrations
51
- ==========
52
-
53
- This will almost certainly be something crude -- since we don't really control the database
54
- connection in the same way as, say, ActiveRecord -- but I honestly think it's a worthwhile feature.
55
- Just having something that you can version control and run to update a data model is enough,
56
- really.
57
-
58
- I'm not yet sure of the least useless way to implement it. Again, I favour SQL as the DSL.
59
-
60
- We will clearly need transactions first, though.
61
-
62
- My Current thoughts:
63
-
64
- * a migration against a database is on a par with a model but very different. You subclass
65
- migration and give it an interface, pointing to the table that stores the current migration
66
- state.
67
-
68
- * The methods in a module are exectute() up() and down() -- the last two call the first one.
69
-
70
- * Each instance of a model is stored in a file and contains up and down SQL somehow. Each instance
71
- has a version number.
72
-
73
- * You run a migration by running up or down on your migration class, passing a version?
74
-
75
-
76
- Connection Object
77
- =================
78
-
79
- PgInterface and TdsInterface both take a connection Hash, which is all very well, but it means that
80
- we are running one database connection per model. Presumably this is a bad idea. :-)
81
-
82
- This actually hasn't come up in my own use of Pod4 -- for complex reasons I'm either using
83
- SequelInterface or running transient jobs which start up a couple of models, do some work, and then
84
- stop entirely.
85
-
86
- Connection is baked into those interfaces, and interface dependant. So I'm thinking in terms of a
87
- memoising object that stores the connection hash and then gets passed to the interface. When the
88
- interface wants a connection, then it asks the connection object. If the connection object doesn't
89
- have one, then the interface connects, and gives the connection to the connection object.
90
-
91
- It's looking as if we don't need this right now. One connection per model might not be as daft as
92
- it seems.
93
-
94
-
95
- JDBC-SQL interface
96
- ==================
97
-
98
- For the jdbc-msssqlserver gem. Doable ... I *think*.
99
-
100
- driver = Java::com.microsoft.sqlserver.jdbc.SQLServerDriver.new
101
- props = java.util.Properties.new
102
- props.setProperty("user", "username")
103
- props.setProperty("password", "password")
104
- url = 'jdbc:sqlserver://servername;instanceName=instance;databaseName=DbName;'
105
-
106
- conn = driver.connect(url, props)
107
- #or maybe conn = driver.get_connection(url, "username", "password")
108
-
109
- stmt = conn.create_statement
110
- sql = %Q|blah;|
111
-
112
- rsS = stmt.execute_query(sql)
113
-
114
- while (rsS.next) do
115
- veg = Hash.new
116
- veg["vegName"] = rsS.getObject("name")
117
- # etc
118
- end
119
-
120
- stmt.close
121
- conn.close
122
-
123
- see https://github.com/jruby/jruby/wiki/JDBC
124
-
125
-
@@ -1,80 +0,0 @@
1
- TypeCasting
2
- ===========
3
-
4
- Example
5
- -------
6
-
7
- ```
8
- require 'pod4'
9
- require 'pod4/someinterface'
10
- require 'pod4/typecasting'
11
-
12
- class Foo < Pod4::Model
13
- include Pod4::TypeCasting
14
-
15
- class Interface < Pod4::SomeInterface
16
- # blah blah blah
17
- end
18
-
19
- set_interface Interface.new($stuff)
20
-
21
- attr_columns :name, :issue, :created, :due, :last_update, :completed, :thing
22
-
23
- # Now the meat
24
- typecast :issue, as: Integer
25
- typecast :created, :due, as: Date
26
- typecast :last_update, as: Time
27
- typecast :completed, as: BigDecimal, ot_as: Float
28
- typecast :thing, use: mymethod
29
- end
30
- ```
31
-
32
- What You Get
33
- ------------
34
-
35
- ### Every attribute named in a typecast gets:
36
-
37
- * An accessor. (Probably it already has one, if it is named in attr_columns, but it doesn't have to
38
- be. Note, though, that we don't add the attribute to the column list and it does not get output
39
- in to_ot by default.)
40
-
41
- * An attempt to force the value to that data type on set(). If the value cannot be coerced, it is
42
- *untouched*.
43
-
44
- * A second attempt to cast on to_interface(). This time, if the value cannot be coerced, it is set
45
- to nil.
46
-
47
- * if the optional `ot_as` type is set, then we cast a third time in the `to_ot()` method;
48
- additionally we guard the OT with the base type using Octothorpe.guard. Note that this only
49
- effects to_ot().
50
-
51
- ### Additionally the user can call these methods:
52
-
53
- * `typecast?(:columnname, value)` returns true if the value can be cast; value defaults to the
54
- column value if not given.
55
-
56
- * `typecast(type, value, strict)` returns a typecast value, or either the original value, or nil if
57
- strict is `:strict'.
58
-
59
- * `guard(octothorpe)` will set guard conditions for nil values on the given octothorpe, based on
60
- the attributes typecast knows about.
61
-
62
- ### The following types will be supported:
63
-
64
- * Integer
65
- * BigDecimal
66
- * Float
67
- * Date
68
- * Time
69
- * :boolean
70
-
71
- Also: custom typecasting (`use: mymethod`, above). This must accept two parameters: the value, and
72
- an option hash.
73
-
74
-
75
- What You Don't Get
76
- ------------------
77
-
78
- Validation. It's entirely up to you to decide how to validate and we won't second guess that. But
79
- we do provide the `typecast?` method to help.
80
-
@@ -1,204 +0,0 @@
1
- require 'octothorpe'
2
-
3
- require 'pod4/model'
4
- require 'pod4/null_interface'
5
-
6
-
7
- ##
8
- # This is purely here to test that model works when you have a validate that accepts the new
9
- # vmode parameter
10
- #
11
- describe 'Customer Model with new validate' do
12
-
13
- let(:customer_model_class) do
14
- Class.new Pod4::Model do
15
- attr_columns :id, :name, :groups
16
- attr_columns :price # specifically testing multiple calls to attr_columns
17
- set_interface NullInterface.new(:id, :name, :price, :groups, [])
18
-
19
- def map_to_model(ot)
20
- super
21
- @groups = @groups ? @groups.split(',') : []
22
- self
23
- end
24
-
25
- def map_to_interface
26
- x = super
27
- g = (x.>>.groups || []).join(',')
28
- x.merge(groups: g)
29
- end
30
-
31
- def fake_an_alert(*args)
32
- add_alert(*args) #private method
33
- end
34
-
35
- def validate(vmode)
36
- add_alert(:error, "falling over for mode #{vmode}") if name == "fall over"
37
- end
38
-
39
- def reset_alerts; @alerts = []; end
40
- end
41
- end
42
-
43
- let(:records) do
44
- [ {id: 10, name: 'Gomez', price: 1.23, groups: 'trains' },
45
- {id: 20, name: 'Morticia', price: 2.34, groups: 'spanish' },
46
- {id: 30, name: 'Wednesday', price: 3.45, groups: 'school' },
47
- {id: 40, name: 'Pugsley', price: 4.56, groups: 'trains,school'} ]
48
-
49
- end
50
-
51
- let(:records_as_ot) { records.map{|r| Octothorpe.new(r) } }
52
-
53
- # model is just a plain newly created object that you can call read on.
54
- # model2 and model3 are in an identical state - they have been filled with a
55
- # read(). We have two so that we can RSpec 'allow' on one and not the other.
56
-
57
- let(:model) { customer_model_class.new(20) }
58
-
59
- let(:model2) do
60
- m = customer_model_class.new(30)
61
-
62
- allow( m.interface ).to receive(:read).and_return( Octothorpe.new(records[2]) )
63
- m.read.or_die
64
- end
65
-
66
- let(:model3) do
67
- m = customer_model_class.new(40)
68
-
69
- allow( m.interface ).to receive(:read).and_return( Octothorpe.new(records[3]) )
70
- m.read.or_die
71
- end
72
-
73
- ##
74
-
75
-
76
- describe '#create' do
77
-
78
- let (:new_model) { customer_model_class.new }
79
-
80
- it 'calls validate and passes the parameter' do
81
- # validation tests arity of the validate method; rspec freaks out. So we can't
82
- # `expect( new_model ).to receive(:validate)`
83
-
84
- m = customer_model_class.new
85
- m.name = "fall over"
86
- m.create
87
- expect( m.model_status ).to eq :error
88
- expect( m.alerts.map(&:message) ).to include( include "create" )
89
- end
90
-
91
- it 'calls create on the interface if the record is good' do
92
- expect( customer_model_class.interface ).to receive(:create)
93
- customer_model_class.new.create
94
-
95
- new_model.fake_an_alert(:warning, :name, 'foo')
96
- expect( new_model.interface ).to receive(:create)
97
- new_model.create
98
- end
99
-
100
- it 'doesnt call create on the interface if the record is bad' do
101
- new_model.fake_an_alert(:error, :name, 'foo')
102
- expect( new_model.interface ).not_to receive(:create)
103
- new_model.create
104
- end
105
-
106
- end
107
- ##
108
-
109
-
110
- describe '#read' do
111
-
112
- it 'calls validate and passes the parameter' do
113
- # again, because rspec is a bit stupid, we can't just `expect(model).to receive(:validate)`
114
-
115
- allow( model.interface ).
116
- to receive(:read).
117
- and_return( records_as_ot.first.merge(name: "fall over") )
118
-
119
- model.read
120
- expect( model.model_status ).to eq :error
121
- expect( model.alerts.map(&:message) ).to include( include "mode read" )
122
- end
123
-
124
- end
125
- ##
126
-
127
-
128
- describe '#update' do
129
-
130
- before do
131
- allow( model2.interface ).
132
- to receive(:update).
133
- and_return( model2.interface )
134
-
135
- end
136
-
137
- it 'calls validate and passes the parameter' do
138
- # again, we can't `expect(model2).to receive(:validate)` because we're testing arity there
139
- model2.name = "fall over"
140
- model2.update
141
- expect( model2.model_status ).to eq :error
142
- expect( model2.alerts.map(&:message) ).to include( include "mode update" )
143
- end
144
-
145
- it 'calls update on the interface if the validation passes' do
146
- expect( model3.interface ).
147
- to receive(:update).
148
- and_return( model3.interface )
149
-
150
- model3.update
151
- end
152
-
153
- it 'doesn\'t call update on the interface if the validation fails' do
154
- expect( model3.interface ).not_to receive(:update)
155
-
156
- model3.name = "fall over" # triggers validation
157
- model3.update
158
- end
159
-
160
- end
161
- ##
162
-
163
-
164
- describe '#delete' do
165
-
166
- before do
167
- allow( model2.interface ).
168
- to receive(:delete).
169
- and_return( model2.interface )
170
-
171
- end
172
-
173
- it 'calls validate and passes the parameter' do
174
- # again, because rspec can't cope with us testing arity in Pod4::Model, we can't say
175
- # `expect(model2).to receive(:validate)`. But for delete we are only running validation as a
176
- # courtesy -- a validation fail does not stop the delete, it just sets alerts. So the model
177
- # status should be :deleted and not :error
178
- model2.name = "fall over"
179
- model2.delete
180
- expect( model2.alerts.map(&:message) ).to include(include "mode delete")
181
- end
182
-
183
- it 'calls delete on the interface if the model status is good' do
184
- expect( model3.interface ).
185
- to receive(:delete).
186
- and_return( model3.interface )
187
-
188
- model3.delete
189
- end
190
-
191
- it 'calls delete on the interface if the model status is bad' do
192
- expect( model3.interface ).
193
- to receive(:delete).
194
- and_return( model3.interface )
195
-
196
- model3.fake_an_alert(:error, :price, 'qar')
197
- model3.delete
198
- end
199
-
200
- end
201
- ##
202
-
203
- end
204
-