terrestrial 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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