terrestrial 0.1.0 → 0.1.1

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 (77) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -9
  3. data/.rspec +2 -0
  4. data/.ruby-version +1 -0
  5. data/CODE_OF_CONDUCT.md +28 -0
  6. data/Gemfile.lock +73 -0
  7. data/LICENSE.txt +22 -0
  8. data/MissingFeatures.md +64 -0
  9. data/README.md +161 -16
  10. data/Rakefile +30 -0
  11. data/TODO.md +41 -0
  12. data/features/env.rb +60 -0
  13. data/features/example.feature +120 -0
  14. data/features/step_definitions/example_steps.rb +46 -0
  15. data/lib/terrestrial/abstract_record.rb +99 -0
  16. data/lib/terrestrial/association_loaders.rb +52 -0
  17. data/lib/terrestrial/collection_mutability_proxy.rb +81 -0
  18. data/lib/terrestrial/configurations/conventional_association_configuration.rb +186 -0
  19. data/lib/terrestrial/configurations/conventional_configuration.rb +302 -0
  20. data/lib/terrestrial/dataset.rb +49 -0
  21. data/lib/terrestrial/deleted_record.rb +20 -0
  22. data/lib/terrestrial/dirty_map.rb +42 -0
  23. data/lib/terrestrial/graph_loader.rb +63 -0
  24. data/lib/terrestrial/graph_serializer.rb +91 -0
  25. data/lib/terrestrial/identity_map.rb +22 -0
  26. data/lib/terrestrial/lazy_collection.rb +74 -0
  27. data/lib/terrestrial/lazy_object_proxy.rb +55 -0
  28. data/lib/terrestrial/many_to_many_association.rb +138 -0
  29. data/lib/terrestrial/many_to_one_association.rb +66 -0
  30. data/lib/terrestrial/mapper_facade.rb +137 -0
  31. data/lib/terrestrial/one_to_many_association.rb +66 -0
  32. data/lib/terrestrial/public_conveniencies.rb +139 -0
  33. data/lib/terrestrial/query_order.rb +32 -0
  34. data/lib/terrestrial/relation_mapping.rb +50 -0
  35. data/lib/terrestrial/serializer.rb +18 -0
  36. data/lib/terrestrial/short_inspection_string.rb +18 -0
  37. data/lib/terrestrial/struct_factory.rb +17 -0
  38. data/lib/terrestrial/subset_queries_proxy.rb +11 -0
  39. data/lib/terrestrial/upserted_record.rb +15 -0
  40. data/lib/terrestrial/version.rb +1 -1
  41. data/lib/terrestrial.rb +5 -2
  42. data/sequel_mapper.gemspec +31 -0
  43. data/spec/config_override_spec.rb +193 -0
  44. data/spec/custom_serializers_spec.rb +49 -0
  45. data/spec/deletion_spec.rb +101 -0
  46. data/spec/graph_persistence_spec.rb +313 -0
  47. data/spec/graph_traversal_spec.rb +121 -0
  48. data/spec/new_graph_persistence_spec.rb +71 -0
  49. data/spec/object_identity_spec.rb +70 -0
  50. data/spec/ordered_association_spec.rb +51 -0
  51. data/spec/persistence_efficiency_spec.rb +224 -0
  52. data/spec/predefined_queries_spec.rb +62 -0
  53. data/spec/proxying_spec.rb +88 -0
  54. data/spec/querying_spec.rb +48 -0
  55. data/spec/readme_examples_spec.rb +35 -0
  56. data/spec/sequel_mapper/abstract_record_spec.rb +244 -0
  57. data/spec/sequel_mapper/collection_mutability_proxy_spec.rb +135 -0
  58. data/spec/sequel_mapper/deleted_record_spec.rb +59 -0
  59. data/spec/sequel_mapper/dirty_map_spec.rb +214 -0
  60. data/spec/sequel_mapper/lazy_collection_spec.rb +119 -0
  61. data/spec/sequel_mapper/lazy_object_proxy_spec.rb +140 -0
  62. data/spec/sequel_mapper/public_conveniencies_spec.rb +58 -0
  63. data/spec/sequel_mapper/upserted_record_spec.rb +59 -0
  64. data/spec/spec_helper.rb +36 -0
  65. data/spec/support/blog_schema.rb +38 -0
  66. data/spec/support/have_persisted_matcher.rb +19 -0
  67. data/spec/support/mapper_setup.rb +221 -0
  68. data/spec/support/mock_sequel.rb +193 -0
  69. data/spec/support/object_graph_setup.rb +139 -0
  70. data/spec/support/seed_data_setup.rb +165 -0
  71. data/spec/support/sequel_persistence_setup.rb +19 -0
  72. data/spec/support/sequel_test_support.rb +166 -0
  73. metadata +207 -13
  74. data/.travis.yml +0 -4
  75. data/bin/console +0 -14
  76. data/bin/setup +0 -7
  77. data/terrestrial.gemspec +0 -23
@@ -0,0 +1,139 @@
1
+ require "terrestrial/serializer"
2
+ require "terrestrial/struct_factory"
3
+
4
+ RSpec.shared_context "object graph setup" do
5
+
6
+ before do
7
+ setup_circular_references_avoiding_stack_overflow
8
+ end
9
+
10
+ def setup_circular_references_avoiding_stack_overflow
11
+ biscuits_post_comment.commenter = hansel
12
+ cat_biscuits_category.posts = [ biscuits_post ]
13
+ end
14
+
15
+ class PlainObject
16
+ def self.with_members(*list, &block)
17
+ Class.new(self).tap { |klass|
18
+ klass.instance_variable_set(:@members, list)
19
+ klass.class_exec(&block) if block
20
+ klass.send(:attr_accessor, *list)
21
+ }
22
+ end
23
+
24
+ def self.members
25
+ @members
26
+ end
27
+
28
+ def initialize(attrs)
29
+ members.sort == attrs.keys.sort or (
30
+ raise(ArgumentError.new("Expected `#{self.class.members}` got `#{attrs.keys}"))
31
+ )
32
+
33
+ members.each { |member| send("#{member}=", attrs.fetch(member)) }
34
+ end
35
+
36
+ def members
37
+ self.class.members
38
+ end
39
+
40
+ def to_h
41
+ Hash[members.map { |field| [field, send(field)] }]
42
+ end
43
+ end
44
+
45
+ User ||= PlainObject.with_members(:id, :first_name, :last_name, :email, :posts)
46
+ Post ||= PlainObject.with_members(:id, :subject, :body, :comments, :categories, :created_at)
47
+ Comment ||= PlainObject.with_members(:id, :commenter, :body)
48
+ Category ||= PlainObject.with_members(:id, :name, :posts)
49
+
50
+ let(:factories) {
51
+ {
52
+ users: User.method(:new),
53
+ posts: Post.method(:new),
54
+ comments: Comment.method(:new),
55
+ categories: Category.method(:new),
56
+ categories_to_posts: ->(x){x},
57
+ noop: ->(x){x},
58
+ }
59
+ }
60
+
61
+ let(:default_serializer) {
62
+ ->(fields) {
63
+ ->(object) {
64
+ Terrestrial::Serializer.new(fields, object).to_h
65
+ }
66
+ }
67
+ }
68
+
69
+ let(:null_serializer) {
70
+ ->(_fields) {
71
+ ->(x){x}
72
+ }
73
+ }
74
+
75
+ let(:hansel) {
76
+ factories.fetch(:users).call(
77
+ id: "users/1",
78
+ first_name: "Hansel",
79
+ last_name: "Trickett",
80
+ email: "hansel@tricketts.org",
81
+ posts: [
82
+ biscuits_post,
83
+ sleep_post,
84
+ ],
85
+ )
86
+ }
87
+
88
+ let(:biscuits_post) {
89
+ factories.fetch(:posts).call(
90
+ id: "posts/1",
91
+ subject: "Biscuits",
92
+ body: "I like them",
93
+ comments: [
94
+ biscuits_post_comment,
95
+ ],
96
+ categories: [
97
+ cat_biscuits_category,
98
+ ],
99
+ created_at: Time.parse("2015-09-05T15:00:00+01:00"),
100
+ )
101
+ }
102
+
103
+ let(:sleep_post) {
104
+ factories.fetch(:posts).call(
105
+ id: "posts/2",
106
+ subject: "Sleeping",
107
+ body: "I do it three times purrr day",
108
+ comments: [],
109
+ categories: [
110
+ chilling_category,
111
+ ],
112
+ created_at: Time.parse("2015-09-02T15:00:00+01:00"),
113
+ )
114
+ }
115
+
116
+ let(:biscuits_post_comment) {
117
+ factories.fetch(:comments).call(
118
+ id: "comments/1",
119
+ body: "oh noes",
120
+ commenter: nil,
121
+ )
122
+ }
123
+
124
+ let(:cat_biscuits_category) {
125
+ factories.fetch(:categories).call(
126
+ id: "categories/1",
127
+ name: "Cat biscuits",
128
+ posts: [],
129
+ )
130
+ }
131
+
132
+ let(:chilling_category) {
133
+ factories.fetch(:categories).call(
134
+ id: "categories/2",
135
+ name: "Chillaxing",
136
+ posts: [],
137
+ )
138
+ }
139
+ end
@@ -0,0 +1,165 @@
1
+ require "support/object_graph_setup"
2
+ RSpec.shared_context "seed data setup" do
3
+ include_context "object graph setup"
4
+
5
+ before {
6
+ insert_records(datastore, seeded_records)
7
+ }
8
+
9
+ let(:seeded_records) {
10
+ [
11
+ [ :users, hansel_record ],
12
+ [ :users, jasper_record ],
13
+ [ :users, poppy_record ],
14
+ [ :posts, biscuits_post_record ],
15
+ [ :posts, sleep_post_record ],
16
+ [ :posts, catch_frogs_post_record ],
17
+ [ :posts, chew_up_boxes_post_record ],
18
+ [ :comments, biscuits_post_comment_record ],
19
+ [ :categories, cat_biscuits_category_record ],
20
+ [ :categories, eating_and_sleeping_category_record ],
21
+ [ :categories, hunting_category_record ],
22
+ [ :categories, messing_stuff_up_category_record ],
23
+ *categories_to_posts_records.map { |record|
24
+ [ :categories_to_posts, record ]
25
+ },
26
+ ]
27
+ }
28
+
29
+ let(:hansel_record) {
30
+ {
31
+ id: "users/1",
32
+ first_name: "Hansel",
33
+ last_name: "Trickett",
34
+ email: "hansel@tricketts.org",
35
+ }
36
+ }
37
+
38
+ let(:jasper_record) {
39
+ {
40
+ id: "users/2",
41
+ first_name: "Jasper",
42
+ last_name: "Trickett",
43
+ email: "jasper@tricketts.org",
44
+ }
45
+ }
46
+
47
+ let(:poppy_record) {
48
+ {
49
+ id: "users/3",
50
+ first_name: "Poppy",
51
+ last_name: "Herzog",
52
+ email: "poppy@herzog.info",
53
+ }
54
+ }
55
+
56
+ let(:biscuits_post_record) {
57
+ {
58
+ id: "posts/1",
59
+ subject: "Biscuits",
60
+ body: "I like them",
61
+ author_id: "users/1",
62
+ created_at: Time.parse("2015-09-05T15:00:00+01:00"),
63
+ }
64
+ }
65
+
66
+ let(:sleep_post_record) {
67
+ {
68
+ id: "posts/2",
69
+ subject: "Sleeping",
70
+ body: "I do it three times purrr day",
71
+ author_id: "users/1",
72
+ created_at: Time.parse("2015-09-02T15:00:00+01:00"),
73
+ }
74
+ }
75
+
76
+ let(:catch_frogs_post_record) {
77
+ {
78
+ id: "posts/3",
79
+ subject: "Catching frongs",
80
+ body: "I love them while at the same time I hate them",
81
+ author_id: "users/2",
82
+ created_at: Time.parse("2015-09-03T15:00:00+01:00"),
83
+ }
84
+ }
85
+
86
+ let(:chew_up_boxes_post_record) {
87
+ {
88
+ id: "posts/4",
89
+ subject: "Chewing up boxes",
90
+ body: "I love them, and yet I destory them",
91
+ author_id: "users/2",
92
+ created_at: Time.parse("2015-09-10T11:00:00+01:00"),
93
+ }
94
+ }
95
+
96
+ let(:biscuits_post_comment_record) {
97
+ {
98
+ id: "comments/1",
99
+ body: "oh noes",
100
+ post_id: "posts/1",
101
+ commenter_id: "users/1",
102
+ }
103
+ }
104
+
105
+ let(:cat_biscuits_category_record) {
106
+ {
107
+ id: "categories/1",
108
+ name: "Cat biscuits",
109
+ }
110
+ }
111
+
112
+ let(:eating_and_sleeping_category_record) {
113
+ {
114
+ id: "categories/2",
115
+ name: "Eating and sleeping",
116
+ }
117
+ }
118
+
119
+ let(:hunting_category_record) {
120
+ {
121
+ id: "categories/3",
122
+ name: "Hunting",
123
+ }
124
+ }
125
+
126
+ let(:messing_stuff_up_category_record) {
127
+ {
128
+ id: "categories/4",
129
+ name: "Messing stuff up",
130
+ }
131
+ }
132
+
133
+ let(:categories_to_posts_records) {
134
+ [
135
+ {
136
+ post_id: "posts/1",
137
+ category_id: "categories/1",
138
+ },
139
+ {
140
+ post_id: "posts/1",
141
+ category_id: "categories/2",
142
+ },
143
+ {
144
+ post_id: "posts/2",
145
+ category_id: "categories/2",
146
+ },
147
+ {
148
+ post_id: "posts/3",
149
+ category_id: "categories/2",
150
+ },
151
+ {
152
+ post_id: "posts/3",
153
+ category_id: "categories/3",
154
+ },
155
+ {
156
+ post_id: "posts/4",
157
+ category_id: "categories/3",
158
+ },
159
+ {
160
+ post_id: "posts/4",
161
+ category_id: "categories/4",
162
+ },
163
+ ]
164
+ }
165
+ end
@@ -0,0 +1,19 @@
1
+ require "support/sequel_test_support"
2
+
3
+ RSpec.shared_context "sequel persistence setup" do
4
+ include Terrestrial::SequelTestSupport
5
+
6
+ before { truncate_tables }
7
+
8
+ let(:datastore) {
9
+ db_connection.tap { |db|
10
+ # The query_counter will let us make assertions about how efficiently
11
+ # the database is being used
12
+ db.loggers << query_counter
13
+ }
14
+ }
15
+
16
+ let(:query_counter) {
17
+ Terrestrial::SequelTestSupport::QueryCounter.new
18
+ }
19
+ end
@@ -0,0 +1,166 @@
1
+ require "sequel"
2
+
3
+ module Terrestrial
4
+ module SequelTestSupport
5
+ def create_database
6
+ `psql postgres --command "CREATE DATABASE $PGDATABASE;"`
7
+ end
8
+ module_function :create_database
9
+
10
+ def drop_database
11
+ `psql postgres --command "DROP DATABASE $PGDATABASE;"`
12
+ end
13
+ module_function :drop_database
14
+
15
+ def drop_tables(tables = db_connection.tables)
16
+ tables.each do |table_name|
17
+ db_connection.drop_table(table_name, cascade: true)
18
+ end
19
+ end
20
+ module_function :drop_tables
21
+
22
+ def truncate_tables(tables = db_connection.tables)
23
+ tables.each do |table_name|
24
+ db_connection[table_name].truncate(cascade: true)
25
+ end
26
+ end
27
+ module_function :truncate_tables
28
+
29
+ def db_connection
30
+ Sequel.default_timezone = :utc
31
+ @db_connection ||= Sequel.postgres(
32
+ host: ENV.fetch("PGHOST"),
33
+ user: ENV.fetch("PGUSER"),
34
+ database: ENV.fetch("PGDATABASE"),
35
+ )
36
+ end
37
+ module_function :db_connection
38
+
39
+ def create_tables(schema)
40
+ schema.fetch(:tables).each do |table_name, fields|
41
+ db_connection.create_table(table_name) do
42
+ fields.each do |field|
43
+ type = field.fetch(:type)
44
+ name = field.fetch(:name)
45
+ options = field.fetch(:options, {})
46
+
47
+ column(name, type, options)
48
+ end
49
+ end
50
+ end
51
+
52
+ schema.fetch(:foreign_keys).each do |(table, fk_col, foreign_table, key_col)|
53
+ db_connection.alter_table(table) do
54
+ add_foreign_key([fk_col], foreign_table, key: key_col, deferrable: false, on_delete: :set_null)
55
+ end
56
+ end
57
+
58
+ schema.fetch(:tables).keys
59
+ end
60
+ module_function :create_tables
61
+
62
+ def insert_records(datastore, records)
63
+ records.each { |(namespace, record)|
64
+ datastore[namespace].insert(record)
65
+ }
66
+ end
67
+
68
+ class QueryCounter
69
+ def initialize
70
+ reset
71
+ end
72
+
73
+ def read_count
74
+ read_count_with_describes -
75
+ list_tables_query_count -
76
+ describe_table_queries_count
77
+ end
78
+
79
+ def delete_count
80
+ @info.count { |query|
81
+ /\A\([0-9\.]+s\) DELETE/i === query
82
+ }
83
+ end
84
+
85
+ def read_count_with_describes
86
+ @info.count { |query|
87
+ /\A\([0-9\.]+s\) SELECT/i === query
88
+ }
89
+ end
90
+
91
+ def update_count
92
+ updates.count
93
+ end
94
+
95
+ def updates
96
+ @info
97
+ .map { |query| query.gsub(/\A\([0-9\.]+s\) /, "") }
98
+ .select { |query| query.start_with?("UPDATE") }
99
+ end
100
+
101
+ def show_queries
102
+ puts @info.join("\n")
103
+ end
104
+
105
+ def info(message)
106
+ @info.push(message)
107
+ end
108
+
109
+ def error(message)
110
+ @error.push(message)
111
+ end
112
+
113
+ def warn(message)
114
+ @warn.push(message)
115
+ end
116
+
117
+ def reset
118
+ @described_table_queries = []
119
+ @info = []
120
+ @error = []
121
+ @warn = []
122
+ end
123
+
124
+ private
125
+
126
+ def list_tables_query_count
127
+ @info.count { |query| list_tables_query_pattern.match(query) }
128
+ end
129
+
130
+ def describe_table_queries_count
131
+ describe_table_queries.count
132
+ end
133
+
134
+ def describe_table_queries
135
+ # TODO this could probably be better solved with finite automata
136
+ described_table_queries = []
137
+
138
+ queries_without_table_list
139
+ .take_while { |query|
140
+ described_table_queries.push(query)
141
+ described_table_query_pattern.match(query) &&
142
+ described_table_queries.length == described_table_queries.uniq.length
143
+ }
144
+ end
145
+
146
+ def queries_without_table_list
147
+ @info
148
+ .drop_while { |query|
149
+ !list_tables_query_pattern.match(query)
150
+ }
151
+ .drop_while { |query|
152
+ list_tables_query_pattern.match(query)
153
+ }
154
+ end
155
+
156
+
157
+ def list_tables_query_pattern
158
+ /\A\([0-9\.]+s\) SELECT "relname" FROM "pg_class"/
159
+ end
160
+
161
+ def described_table_query_pattern
162
+ /\A\([0-9\.]+s\) SELECT \* FROM "[^"]+" LIMIT 1/i
163
+ end
164
+ end
165
+ end
166
+ end