terrestrial 0.3.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. checksums.yaml +5 -5
  2. data/.ruby-version +1 -1
  3. data/Gemfile.lock +44 -53
  4. data/README.md +3 -6
  5. data/bin/test +1 -1
  6. data/features/env.rb +12 -2
  7. data/features/example.feature +23 -26
  8. data/lib/terrestrial.rb +31 -0
  9. data/lib/terrestrial/adapters/abstract_adapter.rb +6 -0
  10. data/lib/terrestrial/adapters/memory_adapter.rb +82 -6
  11. data/lib/terrestrial/adapters/sequel_postgres_adapter.rb +191 -0
  12. data/lib/terrestrial/configurations/conventional_association_configuration.rb +65 -35
  13. data/lib/terrestrial/configurations/conventional_configuration.rb +280 -124
  14. data/lib/terrestrial/configurations/mapping_config_options_proxy.rb +97 -0
  15. data/lib/terrestrial/deleted_record.rb +12 -8
  16. data/lib/terrestrial/dirty_map.rb +17 -9
  17. data/lib/terrestrial/functional_pipeline.rb +64 -0
  18. data/lib/terrestrial/inspection_string.rb +6 -1
  19. data/lib/terrestrial/lazy_object_proxy.rb +1 -0
  20. data/lib/terrestrial/many_to_many_association.rb +34 -20
  21. data/lib/terrestrial/many_to_one_association.rb +11 -3
  22. data/lib/terrestrial/one_to_many_association.rb +9 -0
  23. data/lib/terrestrial/public_conveniencies.rb +65 -82
  24. data/lib/terrestrial/record.rb +106 -0
  25. data/lib/terrestrial/relation_mapping.rb +43 -12
  26. data/lib/terrestrial/relational_store.rb +33 -11
  27. data/lib/terrestrial/upsert_record.rb +54 -0
  28. data/lib/terrestrial/version.rb +1 -1
  29. data/spec/automatic_timestamps_spec.rb +339 -0
  30. data/spec/changes_api_spec.rb +81 -0
  31. data/spec/config_override_spec.rb +28 -19
  32. data/spec/custom_serializers_spec.rb +3 -2
  33. data/spec/database_default_fields_spec.rb +213 -0
  34. data/spec/database_generated_id_spec.rb +291 -0
  35. data/spec/database_owned_fields_and_timestamps_spec.rb +200 -0
  36. data/spec/deletion_spec.rb +1 -1
  37. data/spec/error_handling/factory_error_handling_spec.rb +1 -4
  38. data/spec/error_handling/serialization_error_spec.rb +1 -4
  39. data/spec/error_handling/upsert_error_spec.rb +7 -11
  40. data/spec/graph_persistence_spec.rb +52 -18
  41. data/spec/ordered_association_spec.rb +10 -12
  42. data/spec/predefined_queries_spec.rb +14 -12
  43. data/spec/readme_examples_spec.rb +1 -1
  44. data/spec/sequel_query_efficiency_spec.rb +19 -16
  45. data/spec/spec_helper.rb +6 -1
  46. data/spec/support/blog_schema.rb +7 -3
  47. data/spec/support/object_graph_setup.rb +30 -39
  48. data/spec/support/object_store_setup.rb +16 -196
  49. data/spec/support/seed_data_setup.rb +15 -149
  50. data/spec/support/seed_records.rb +141 -0
  51. data/spec/support/sequel_test_support.rb +46 -13
  52. data/spec/terrestrial/abstract_record_spec.rb +138 -106
  53. data/spec/terrestrial/adapters/sequel_postgres_adapter_spec.rb +138 -0
  54. data/spec/terrestrial/deleted_record_spec.rb +0 -27
  55. data/spec/terrestrial/dirty_map_spec.rb +52 -77
  56. data/spec/terrestrial/functional_pipeline_spec.rb +153 -0
  57. data/spec/terrestrial/inspection_string_spec.rb +61 -0
  58. data/spec/terrestrial/upsert_record_spec.rb +29 -0
  59. data/terrestrial.gemspec +7 -8
  60. metadata +43 -40
  61. data/MissingFeatures.md +0 -64
  62. data/lib/terrestrial/abstract_record.rb +0 -99
  63. data/lib/terrestrial/association_loaders.rb +0 -52
  64. data/lib/terrestrial/upserted_record.rb +0 -15
  65. data/spec/terrestrial/public_conveniencies_spec.rb +0 -63
  66. data/spec/terrestrial/upserted_record_spec.rb +0 -59
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: c61b139216aa29d6468f16cabdbb135d7c68cbc1
4
- data.tar.gz: ab6960d54b3c7546f8d95a6e337efddaef166a7f
2
+ SHA256:
3
+ metadata.gz: 67854cbc017e8771ba0794d62cfc07b7cf55548c49327cd9a8c82c3167076bc1
4
+ data.tar.gz: e0fb7f57b53e059485238e36aa00699f15664b721c353e98ec4050d1283dfa6b
5
5
  SHA512:
6
- metadata.gz: b9a11c083365c86dce49908720970407824645d40802fb5adbfd9d9b7adb8fd22f4a2b539e35edd4d630be0881413f3b0ee5063b3a20011707b33edc0687d72d
7
- data.tar.gz: 7e3a194ff7a687b28fdf62ba993af5b83d5e4294c439d14cce8489f8f2439eadc8684842cfc557a642574076b831e6a5e6eb94347e5ee53079b41793c3349348
6
+ metadata.gz: a41417eaba6d60cdebab5586f61d6c6534bccffb19d736157c7ae43c3651b973b878e1c05ad012d5f8cbb9247af2126d054154c3ec6c506db3158525174e48aa
7
+ data.tar.gz: aed7b695f3457e7c25ce9c4285137ee04106bbd9692f2d18b11cf680bb002c4b75d96c3f2b066009e6f697b230896442f13aa8173451f4edccdc9fb1eee665b4
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 2.3.1
1
+ 2.7.1
data/Gemfile.lock CHANGED
@@ -1,78 +1,69 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- terrestrial (0.3.0)
5
- activesupport (~> 4.0)
4
+ terrestrial (0.5.0)
6
5
  fetchable (~> 1.0)
7
- sequel (~> 4.16)
6
+ sequel (~> 5.0)
8
7
 
9
8
  GEM
10
9
  remote: https://rubygems.org/
11
10
  specs:
12
- activesupport (4.2.7.1)
13
- i18n (~> 0.7)
14
- json (~> 1.7, >= 1.7.7)
15
- minitest (~> 5.1)
16
- thread_safe (~> 0.3, >= 0.3.4)
17
- tzinfo (~> 1.1)
18
- builder (3.2.2)
19
- coderay (1.1.1)
20
- cucumber (2.4.0)
11
+ backports (3.20.2)
12
+ builder (3.2.4)
13
+ coderay (1.1.3)
14
+ cucumber (3.2.0)
21
15
  builder (>= 2.1.2)
22
- cucumber-core (~> 1.5.0)
16
+ cucumber-core (~> 3.2.0)
17
+ cucumber-expressions (~> 6.0.1)
23
18
  cucumber-wire (~> 0.0.1)
24
- diff-lcs (>= 1.1.3)
25
- gherkin (~> 4.0)
19
+ diff-lcs (~> 1.3)
20
+ gherkin (~> 5.1.0)
26
21
  multi_json (>= 1.7.5, < 2.0)
27
22
  multi_test (>= 0.1.2)
28
- cucumber-core (1.5.0)
29
- gherkin (~> 4.0)
23
+ cucumber-core (3.2.1)
24
+ backports (>= 3.8.0)
25
+ cucumber-tag_expressions (~> 1.1.0)
26
+ gherkin (~> 5.0)
27
+ cucumber-expressions (6.0.1)
28
+ cucumber-tag_expressions (1.1.1)
30
29
  cucumber-wire (0.0.1)
31
- diff-lcs (1.2.5)
30
+ diff-lcs (1.4.4)
32
31
  fetchable (1.0.0)
33
- gherkin (4.0.0)
34
- i18n (0.7.0)
35
- json (1.8.3)
36
- method_source (0.8.2)
37
- minitest (5.9.1)
38
- multi_json (1.12.1)
32
+ gherkin (5.1.0)
33
+ method_source (1.0.0)
34
+ multi_json (1.15.0)
39
35
  multi_test (0.1.2)
40
- pg (0.17.1)
41
- pry (0.10.4)
42
- coderay (~> 1.1.0)
43
- method_source (~> 0.8.1)
44
- slop (~> 3.4)
45
- rake (10.5.0)
46
- rspec (3.5.0)
47
- rspec-core (~> 3.5.0)
48
- rspec-expectations (~> 3.5.0)
49
- rspec-mocks (~> 3.5.0)
50
- rspec-core (3.5.3)
51
- rspec-support (~> 3.5.0)
52
- rspec-expectations (3.5.0)
36
+ pg (1.2.3)
37
+ pry (0.14.0)
38
+ coderay (~> 1.1)
39
+ method_source (~> 1.0)
40
+ rake (13.0.3)
41
+ rspec (3.10.0)
42
+ rspec-core (~> 3.10.0)
43
+ rspec-expectations (~> 3.10.0)
44
+ rspec-mocks (~> 3.10.0)
45
+ rspec-core (3.10.1)
46
+ rspec-support (~> 3.10.0)
47
+ rspec-expectations (3.10.1)
53
48
  diff-lcs (>= 1.2.0, < 2.0)
54
- rspec-support (~> 3.5.0)
55
- rspec-mocks (3.5.0)
49
+ rspec-support (~> 3.10.0)
50
+ rspec-mocks (3.10.2)
56
51
  diff-lcs (>= 1.2.0, < 2.0)
57
- rspec-support (~> 3.5.0)
58
- rspec-support (3.5.0)
59
- sequel (4.39.0)
60
- slop (3.6.0)
61
- thread_safe (0.3.5)
62
- tzinfo (1.2.2)
63
- thread_safe (~> 0.1)
52
+ rspec-support (~> 3.10.0)
53
+ rspec-support (3.10.2)
54
+ sequel (5.42.0)
64
55
 
65
56
  PLATFORMS
66
57
  ruby
67
58
 
68
59
  DEPENDENCIES
69
- bundler (~> 1.7)
70
- cucumber
71
- pg (~> 0.17.1)
72
- pry (~> 0.10.1)
73
- rake (~> 10.0)
74
- rspec (~> 3.1)
60
+ bundler (~> 1)
61
+ cucumber (~> 3.1)
62
+ pg (~> 1.0)
63
+ pry (~> 0.13)
64
+ rake (~> 13.0)
65
+ rspec (~> 3.9)
75
66
  terrestrial!
76
67
 
77
68
  BUNDLED WITH
78
- 1.12.5
69
+ 1.17.1
data/README.md CHANGED
@@ -74,7 +74,7 @@ code of conduct first.
74
74
  ## This is kept separate from your domain models as knowledge of the schema
75
75
  ## is required to wire them up.
76
76
 
77
- MAPPINGS = Terrestrial.config(DB)
77
+ CONFIG = Terrestrial.config(DB)
78
78
  .setup_mapping(:users) { |users|
79
79
  users.class(User) # Specify a class and the constructor will be used
80
80
  users.has_many(:posts, foreign_key: :author_id)
@@ -92,12 +92,9 @@ code of conduct first.
92
92
 
93
93
  # 4. Create an object store by combining a connection and a configuration
94
94
 
95
- OBJECT_STORE = Terrestrial.object_store(
96
- datastore: DB,
97
- mappings: MAPPINGS,
98
- )
95
+ OBJECT_STORE = Terrestrial.object_store(config: CONFIG)
99
96
 
100
- ## You are not limted to one object store configuration or one database
97
+ ## You are not limited to one object store configuration or one database
101
98
  ## connection. To handle complex situations you may create several segregated
102
99
  ## mappings and object stores for your separate aggregate roots, potentially
103
100
  ## utilising multiple databases and different domain object
data/bin/test CHANGED
@@ -3,7 +3,7 @@
3
3
  require "bundler"
4
4
  Bundler.setup
5
5
 
6
- ADAPTERS = ["memory", "sequel"]
6
+ ADAPTERS = ["sequel"]
7
7
 
8
8
  module TerrestrialTesting
9
9
  module_function def run_rspec_with_adapter(adapter)
data/features/env.rb CHANGED
@@ -28,18 +28,28 @@ module ExampleRunnerSupport
28
28
  .strip
29
29
  .gsub(/[\n\s]+/, " ")
30
30
  .gsub(/ \>/, ">")
31
- .gsub(/\:0x[0-9a-f]{14}/, ":<<object id removed>>")
31
+ .gsub(/\:0x[0-9a-f]{14,}/, ":<<object id removed>>")
32
32
  end
33
33
 
34
34
  def parse_schema_table(string)
35
35
  string.each_line.drop(2).map { |line|
36
- name, type = line.split("|").map(&:strip)
36
+ name, type, options = line.split("|").map(&:strip)
37
+
37
38
  {
38
39
  name: name,
39
40
  type: Object.const_get(type),
41
+ options: string_to_schema_options(options.to_s),
40
42
  }
41
43
  }
42
44
  end
45
+
46
+ def string_to_schema_options(string)
47
+ Hash[
48
+ string.split(",").map(&:strip).reject(&:empty?).map { |s|
49
+ [s.downcase.gsub(" ", "_").to_sym, true]
50
+ }
51
+ ]
52
+ end
43
53
  end
44
54
 
45
55
  module DatabaseSupport
@@ -9,36 +9,36 @@ Feature: Basic setup
9
9
  """
10
10
  And a conventionally similar database schema for table "users"
11
11
  """
12
- Column | Type
13
- ------------ +---------
14
- id | String
15
- first_name | String
16
- last_name | String
17
- email | String
12
+ Column | Type | Options
13
+ -------------+----------+-------------
14
+ id | String | Primary key
15
+ first_name | String |
16
+ last_name | String |
17
+ email | String |
18
18
  """
19
19
  And a conventionally similar database schema for table "posts"
20
20
  """
21
- Column | Type
22
- -------------+---------
23
- id | String
24
- author_id | String
25
- subject | String
26
- body | String
27
- created_at | DateTime
21
+ Column | Type | Options
22
+ -------------┼----------+-------------
23
+ id | String | Primary key
24
+ author_id | String |
25
+ subject | String |
26
+ body | String |
27
+ created_at | DateTime |
28
28
  """
29
29
  And a conventionally similar database schema for table "categories"
30
30
  """
31
- Column | Type
32
- -------------+---------
33
- id | String
34
- name | String
31
+ Column | Type | Options
32
+ -------------+---------+-------------
33
+ id | String | Primary key
34
+ name | String |
35
35
  """
36
36
  And a conventionally similar database schema for table "categories_to_posts"
37
37
  """
38
- Column | Type
39
- -------------+---------
40
- post_id | String
41
- category_id | String
38
+ Column | Type | Options
39
+ -------------+---------+-------------
40
+ post_id | String | Primary key
41
+ category_id | String |
42
42
  """
43
43
  And a database connection is established
44
44
  """
@@ -50,7 +50,7 @@ Feature: Basic setup
50
50
  """
51
51
  And the associations are defined in the configuration
52
52
  """
53
- MAPPINGS = Terrestrial.config(DB)
53
+ CONFIG = Terrestrial.config(DB)
54
54
  .setup_mapping(:users) { |users|
55
55
  users.class(User)
56
56
  users.has_many(:posts, foreign_key: :author_id)
@@ -67,10 +67,7 @@ Feature: Basic setup
67
67
  """
68
68
  And a object store is instantiated
69
69
  """
70
- OBJECT_STORE = Terrestrial.object_store(
71
- datastore: DB,
72
- mappings: MAPPINGS,
73
- )
70
+ OBJECT_STORE = Terrestrial.object_store(config: CONFIG)
74
71
  """
75
72
  When a new graph of objects are created
76
73
  """
data/lib/terrestrial.rb CHANGED
@@ -2,7 +2,38 @@ require "logger"
2
2
  require "terrestrial/public_conveniencies"
3
3
 
4
4
  module Terrestrial
5
+ # TODO: whoa! wtf is this? why did i?
5
6
  extend PublicConveniencies
6
7
 
7
8
  LOGGER = Logger.new(STDERR)
9
+
10
+ class DatabaseID
11
+ def initialize(val = nil)
12
+ @value = val
13
+ end
14
+
15
+ def sql_literal(_dataset)
16
+ @value.nil? ? "NULL" : @value.to_s
17
+ end
18
+
19
+ def nil?
20
+ @value.nil?
21
+ end
22
+
23
+ def value=(v)
24
+ @value = v
25
+ end
26
+
27
+ def to_s
28
+ inspect
29
+ end
30
+
31
+ def inspect
32
+ "#<%{class_name}>:0x%{hex_object_id} @value=%{value}>" % {
33
+ class_name: self.class.name,
34
+ hex_object_id: object_id.<<(1).to_s(16),
35
+ value: @value,
36
+ }
37
+ end
38
+ end
8
39
  end
@@ -0,0 +1,6 @@
1
+ module Terrestrial
2
+ module Adapters
3
+ module AbstractAdapter
4
+ end
5
+ end
6
+ end
@@ -1,9 +1,9 @@
1
- module Terrestrial
2
- module Adapters
3
- end
4
- end
1
+ require "terrestrial/adapters/abstract_adapter"
2
+ require "terrestrial/error"
5
3
 
6
4
  class Terrestrial::Adapters::MemoryAdapter
5
+ include Terrestrial::Adapters::AbstractAdapter
6
+
7
7
  def self.build_from_schema(schema, raw_storage)
8
8
  schema.each { |name, _| raw_storage[name] = [] }
9
9
 
@@ -55,6 +55,18 @@ class Terrestrial::Adapters::MemoryAdapter
55
55
  @relations.fetch(table_name)
56
56
  end
57
57
 
58
+ def upsert(record)
59
+ existing = self[record.namespace].where(record.identity)
60
+
61
+ if existing.any?
62
+ existing.update(record.updatable_attributes)
63
+ else
64
+ self[record.namespace].insert(record.to_h)
65
+ end
66
+ rescue Object => e
67
+ raise Terrestrial::UpsertError.new(record.namespace, record.to_h, e)
68
+ end
69
+
58
70
  private
59
71
 
60
72
  def rollback(relations)
@@ -148,10 +160,19 @@ class Terrestrial::Adapters::MemoryAdapter
148
160
  end
149
161
  end
150
162
 
163
+ def insert_conflict(target:, update: {})
164
+ Upsert.new(self, target: target, update: update)
165
+ end
166
+
151
167
  def insert(new_row)
152
- new_row_with_empty_fields = empty_row.merge(clone(new_row))
168
+ new_row_with_all_fields = empty_row.merge(clone(new_row))
169
+ row_id = extract_row_id(new_row_with_all_fields)
153
170
 
154
- all_rows.push(new_row_with_empty_fields)
171
+ if row_id.any? && where(row_id).any?
172
+ raise DuplicateKeyError.new(row_id)
173
+ else
174
+ all_rows.push(new_row_with_all_fields)
175
+ end
155
176
  end
156
177
 
157
178
  def update(attrs)
@@ -237,5 +258,60 @@ class Terrestrial::Adapters::MemoryAdapter
237
258
  def clone(object)
238
259
  Marshal.load(Marshal.dump(object))
239
260
  end
261
+
262
+ def extract_row_id(row)
263
+ row.select { |k, _v| primary_key.include?(k) }
264
+ end
265
+
266
+ def primary_key
267
+ @primary_key ||= schema
268
+ .select { |col| col.fetch(:options, {}).fetch(:primary_key, nil) }
269
+ .map { |col| col.fetch(:name) }
270
+ end
271
+ end
272
+
273
+ # Small amount of code necessary to simulate upserts with Sequel's API
274
+ class Upsert
275
+ def initialize(dataset, target:, update:)
276
+ @dataset = dataset
277
+ @target = target
278
+ @update_attributes = update
279
+ end
280
+
281
+ attr_reader :dataset, :target, :update_attributes
282
+ private :dataset, :target, :update_attributes
283
+
284
+ def insert(row)
285
+ dataset.insert(row)
286
+ rescue DuplicateKeyError => e
287
+ if target_matches?(e.key)
288
+ attempt_update(e.row_id)
289
+ end
290
+ end
291
+
292
+ def attempt_update(row_id)
293
+ dataset.where(row_id).update(update_attributes)
294
+ end
295
+
296
+ def target_matches?(key)
297
+ key.sort == Array(target).sort
298
+ end
299
+ end
300
+
301
+
302
+ class DuplicateKeyError < RuntimeError
303
+ def initialize(row_id)
304
+ @row_id = row_id
305
+ end
306
+
307
+ attr_reader :row_id
308
+
309
+ def key
310
+ row_id.keys
311
+ end
312
+
313
+ def message
314
+ "Insert conflict. Row with `#{row_id}` already exists"
315
+ end
240
316
  end
241
317
  end