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
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
-