terrestrial 0.3.0 → 0.5.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 (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