rom-sql 0.7.0 → 0.8.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 (88) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +1 -0
  3. data/.travis.yml +12 -7
  4. data/CHANGELOG.md +28 -0
  5. data/Gemfile +6 -9
  6. data/README.md +5 -4
  7. data/circle.yml +10 -0
  8. data/lib/rom/plugins/relation/sql/auto_combine.rb +16 -3
  9. data/lib/rom/plugins/relation/sql/auto_wrap.rb +3 -2
  10. data/lib/rom/sql/association.rb +75 -0
  11. data/lib/rom/sql/association/many_to_many.rb +86 -0
  12. data/lib/rom/sql/association/many_to_one.rb +60 -0
  13. data/lib/rom/sql/association/name.rb +70 -0
  14. data/lib/rom/sql/association/one_to_many.rb +9 -0
  15. data/lib/rom/sql/association/one_to_one.rb +46 -0
  16. data/lib/rom/sql/association/one_to_one_through.rb +9 -0
  17. data/lib/rom/sql/commands.rb +2 -0
  18. data/lib/rom/sql/commands/create.rb +2 -2
  19. data/lib/rom/sql/commands/delete.rb +0 -1
  20. data/lib/rom/sql/commands/postgres.rb +76 -0
  21. data/lib/rom/sql/commands/update.rb +6 -3
  22. data/lib/rom/sql/commands_ext/postgres.rb +17 -0
  23. data/lib/rom/sql/gateway.rb +23 -15
  24. data/lib/rom/sql/header.rb +7 -1
  25. data/lib/rom/sql/plugin/assoc_macros.rb +3 -3
  26. data/lib/rom/sql/plugin/associates.rb +50 -9
  27. data/lib/rom/sql/qualified_attribute.rb +53 -0
  28. data/lib/rom/sql/relation.rb +76 -25
  29. data/lib/rom/sql/relation/reading.rb +138 -35
  30. data/lib/rom/sql/relation/writing.rb +21 -0
  31. data/lib/rom/sql/schema.rb +35 -0
  32. data/lib/rom/sql/schema/associations_dsl.rb +68 -0
  33. data/lib/rom/sql/schema/dsl.rb +27 -0
  34. data/lib/rom/sql/schema/inferrer.rb +80 -0
  35. data/lib/rom/sql/support/active_support_notifications.rb +27 -17
  36. data/lib/rom/sql/types.rb +11 -0
  37. data/lib/rom/sql/types/pg.rb +26 -0
  38. data/lib/rom/sql/version.rb +1 -1
  39. data/rom-sql.gemspec +4 -2
  40. data/spec/integration/association/many_to_many_spec.rb +137 -0
  41. data/spec/integration/association/many_to_one_spec.rb +110 -0
  42. data/spec/integration/association/one_to_many_spec.rb +58 -0
  43. data/spec/integration/association/one_to_one_spec.rb +57 -0
  44. data/spec/integration/association/one_to_one_through_spec.rb +90 -0
  45. data/spec/integration/combine_spec.rb +24 -24
  46. data/spec/integration/commands/create_spec.rb +215 -168
  47. data/spec/integration/commands/delete_spec.rb +88 -46
  48. data/spec/integration/commands/update_spec.rb +141 -60
  49. data/spec/integration/commands/upsert_spec.rb +83 -0
  50. data/spec/integration/gateway_spec.rb +9 -17
  51. data/spec/integration/migration_spec.rb +3 -5
  52. data/spec/integration/plugins/associates_spec.rb +168 -0
  53. data/spec/integration/plugins/auto_wrap_spec.rb +46 -0
  54. data/spec/integration/read_spec.rb +80 -77
  55. data/spec/integration/relation_schema_spec.rb +180 -0
  56. data/spec/integration/schema_inference_spec.rb +67 -0
  57. data/spec/integration/setup_spec.rb +22 -0
  58. data/spec/{support → integration/support}/active_support_notifications_spec.rb +0 -0
  59. data/spec/{support → integration/support}/rails_log_subscriber_spec.rb +0 -0
  60. data/spec/shared/database_setup.rb +46 -8
  61. data/spec/shared/relations.rb +8 -0
  62. data/spec/shared/users_and_accounts.rb +10 -0
  63. data/spec/shared/users_and_tasks.rb +20 -2
  64. data/spec/spec_helper.rb +64 -11
  65. data/spec/support/helpers.rb +9 -0
  66. data/spec/unit/association/many_to_many_spec.rb +89 -0
  67. data/spec/unit/association/many_to_one_spec.rb +81 -0
  68. data/spec/unit/association/name_spec.rb +68 -0
  69. data/spec/unit/association/one_to_many_spec.rb +62 -0
  70. data/spec/unit/association/one_to_one_spec.rb +62 -0
  71. data/spec/unit/association/one_to_one_through_spec.rb +69 -0
  72. data/spec/unit/association_errors_spec.rb +2 -4
  73. data/spec/unit/gateway_spec.rb +12 -3
  74. data/spec/unit/migration_tasks_spec.rb +3 -3
  75. data/spec/unit/migrator_spec.rb +2 -4
  76. data/spec/unit/{combined_associations_spec.rb → plugin/assoc_macros/combined_associations_spec.rb} +13 -19
  77. data/spec/unit/{many_to_many_spec.rb → plugin/assoc_macros/many_to_many_spec.rb} +9 -15
  78. data/spec/unit/{many_to_one_spec.rb → plugin/assoc_macros/many_to_one_spec.rb} +9 -14
  79. data/spec/unit/plugin/assoc_macros/one_to_many_spec.rb +78 -0
  80. data/spec/unit/plugin/base_view_spec.rb +11 -11
  81. data/spec/unit/plugin/pagination_spec.rb +62 -62
  82. data/spec/unit/relation_spec.rb +218 -146
  83. data/spec/unit/schema_spec.rb +15 -14
  84. data/spec/unit/types_spec.rb +40 -0
  85. metadata +105 -21
  86. data/.rubocop.yml +0 -74
  87. data/.rubocop_todo.yml +0 -21
  88. data/spec/unit/one_to_many_spec.rb +0 -83
@@ -0,0 +1,180 @@
1
+ RSpec.describe 'Inferring schema from database' do
2
+ include_context 'database setup'
3
+
4
+ with_adapters do
5
+ context "when database schema exists" do
6
+ it "infers the schema from the database relations" do
7
+ conf.relation(:users)
8
+
9
+ expect(container.relations.users.to_a)
10
+ .to eql(container.gateways[:default][:users].to_a)
11
+ end
12
+ end
13
+
14
+ context "for empty database schemas" do
15
+ it "returns an empty schema" do
16
+ drop_tables
17
+
18
+ expect { container.not_here }.to raise_error(NoMethodError)
19
+ end
20
+ end
21
+
22
+ context 'defining associations' do
23
+ it "allows defining a one-to-many" do
24
+ class Test::Posts < ROM::Relation[:sql]
25
+ schema(:posts) do
26
+ associations do
27
+ one_to_many :tags
28
+ end
29
+ end
30
+ end
31
+
32
+ assoc = ROM::SQL::Association::OneToMany.new(:posts, :tags)
33
+
34
+ expect(Test::Posts.associations[:tags]).to eql(assoc)
35
+ end
36
+
37
+ it "allows defining a one-to-many using has_many shortcut" do
38
+ class Test::Posts < ROM::Relation[:sql]
39
+ schema(:posts) do
40
+ associations do
41
+ has_many :tags
42
+ end
43
+ end
44
+ end
45
+
46
+ assoc = ROM::SQL::Association::OneToMany.new(:posts, :tags)
47
+
48
+ expect(Test::Posts.associations[:tags]).to eql(assoc)
49
+ end
50
+
51
+ it "allows defining a one-to-one" do
52
+ class Test::Users < ROM::Relation[:sql]
53
+ schema(:users) do
54
+ associations do
55
+ one_to_one :accounts
56
+ end
57
+ end
58
+ end
59
+
60
+ assoc = ROM::SQL::Association::OneToOne.new(:users, :accounts)
61
+
62
+ expect(Test::Users.associations[:accounts]).to eql(assoc)
63
+ end
64
+
65
+ it "allows defining a one-to-one using has_one shortcut" do
66
+ class Test::Users < ROM::Relation[:sql]
67
+ schema(:users) do
68
+ associations do
69
+ has_one :account
70
+ end
71
+ end
72
+ end
73
+
74
+ assoc = ROM::SQL::Association::OneToOne.new(:users, :accounts, as: :account)
75
+
76
+ expect(Test::Users.associations[:account]).to eql(assoc)
77
+ expect(Test::Users.associations[:account].target).to be_aliased
78
+ end
79
+
80
+ it "allows defining a one-to-one using has_one shortcut with an alias" do
81
+ class Test::Users < ROM::Relation[:sql]
82
+ schema(:users) do
83
+ associations do
84
+ has_one :account, as: :user_account
85
+ end
86
+ end
87
+ end
88
+
89
+ assoc = ROM::SQL::Association::OneToOne.new(:users, :accounts, as: :user_account)
90
+
91
+ expect(Test::Users.associations[:user_account]).to eql(assoc)
92
+ expect(Test::Users.associations[:user_account].target).to be_aliased
93
+ end
94
+
95
+ it "allows defining a one-to-one-through" do
96
+ class Test::Users < ROM::Relation[:sql]
97
+ schema(:users) do
98
+ associations do
99
+ one_to_one :cards, through: :accounts
100
+ end
101
+ end
102
+ end
103
+
104
+ assoc = ROM::SQL::Association::OneToOneThrough.new(:users, :cards, through: :accounts)
105
+
106
+ expect(Test::Users.associations[:cards]).to eql(assoc)
107
+ end
108
+
109
+ it "allows defining a many-to-one" do
110
+ class Test::Tags < ROM::Relation[:sql]
111
+ schema(:tags) do
112
+ associations do
113
+ many_to_one :posts
114
+ end
115
+ end
116
+ end
117
+
118
+ assoc = ROM::SQL::Association::ManyToOne.new(:tags, :posts)
119
+
120
+ expect(Test::Tags.associations[:posts]).to eql(assoc)
121
+ end
122
+
123
+ it "allows defining a many-to-one using belongs_to shortcut" do
124
+ class Test::Tags < ROM::Relation[:sql]
125
+ schema(:tags) do
126
+ associations do
127
+ belongs_to :post
128
+ end
129
+ end
130
+ end
131
+
132
+ assoc = ROM::SQL::Association::ManyToOne.new(:tags, :posts, as: :post)
133
+
134
+ expect(Test::Tags.associations[:post]).to eql(assoc)
135
+ end
136
+
137
+ it "allows defining a many-to-one using belongs_to shortcut" do
138
+ class Test::Tags < ROM::Relation[:sql]
139
+ schema(:tags) do
140
+ associations do
141
+ belongs_to :post, as: :post_tag
142
+ end
143
+ end
144
+ end
145
+
146
+ assoc = ROM::SQL::Association::ManyToOne.new(:tags, :posts, as: :post_tag)
147
+
148
+ expect(Test::Tags.associations[:post_tag]).to eql(assoc)
149
+ end
150
+
151
+ it "allows defining a many-to-many" do
152
+ class Test::Posts < ROM::Relation[:sql]
153
+ schema(:posts) do
154
+ associations do
155
+ one_to_many :tags, through: :posts_tags
156
+ end
157
+ end
158
+ end
159
+
160
+ assoc = ROM::SQL::Association::ManyToMany.new(:posts, :tags, through: :posts_tags)
161
+
162
+ expect(Test::Posts.associations[:tags]).to eql(assoc)
163
+ end
164
+
165
+ it "allows defining a many-to-one with a custom name" do
166
+ class Test::Tags < ROM::Relation[:sql]
167
+ schema(:tags) do
168
+ associations do
169
+ many_to_one :published_posts, relation: :posts
170
+ end
171
+ end
172
+ end
173
+
174
+ assoc = ROM::SQL::Association::ManyToOne.new(:tags, :published_posts, relation: :posts)
175
+
176
+ expect(Test::Tags.associations[:published_posts]).to eql(assoc)
177
+ end
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,67 @@
1
+ RSpec.describe 'Schema inference' do
2
+ include_context 'database setup'
3
+
4
+ before do
5
+ conn.drop_table?(:test_inferrence)
6
+
7
+ conn.create_table :test_inferrence do
8
+ primary_key :id
9
+ String :text, null: false
10
+ Boolean :flag, null: false
11
+ Date :date
12
+ DateTime :datetime, null: false
13
+ Decimal :money, null: false
14
+ Bytea :data
15
+ end
16
+ end
17
+
18
+ let(:schema) { container.relations[dataset].schema }
19
+
20
+ context 'inferring attributes' do
21
+ before do
22
+ dataset = self.dataset
23
+ conf.relation(dataset) do
24
+ schema(dataset, infer: true)
25
+ end
26
+ end
27
+
28
+ context 'for simple table' do
29
+ let(:dataset) { :users }
30
+
31
+ it 'can infer attributes for dataset' do
32
+ expect(schema.attributes).to eql(
33
+ id: ROM::SQL::Types::Serial.meta(name: :id),
34
+ name: ROM::SQL::Types::Strict::String.meta(name: :name)
35
+ )
36
+ end
37
+ end
38
+
39
+ context 'for a table with FKs' do
40
+ let(:dataset) { :tasks }
41
+
42
+ it 'can infer attributes for dataset' do
43
+ expect(schema.attributes).to eql(
44
+ id: ROM::SQL::Types::Serial.meta(name: :id),
45
+ title: ROM::SQL::Types::Strict::String.optional.meta(name: :title),
46
+ user_id: ROM::SQL::Types::Strict::Int.optional.meta(name: :user_id, foreign_key: true, relation: :users)
47
+ )
48
+ end
49
+ end
50
+
51
+ context 'for complex table' do
52
+ let(:dataset) { :test_inferrence }
53
+
54
+ it 'can infer attributes for dataset' do
55
+ expect(schema.attributes).to eql(
56
+ id: ROM::SQL::Types::Serial.meta(name: :id),
57
+ text: ROM::SQL::Types::Strict::String.meta(name: :text),
58
+ flag: ROM::SQL::Types::Strict::Bool.meta(name: :flag),
59
+ date: ROM::SQL::Types::Strict::Date.optional.meta(name: :date),
60
+ datetime: ROM::SQL::Types::Strict::Time.meta(name: :datetime),
61
+ money: ROM::SQL::Types::Strict::Decimal.meta(name: :money),
62
+ data: ROM::SQL::Types::Strict::String.optional.meta(name: :data)
63
+ )
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,22 @@
1
+ RSpec.describe 'ROM.container' do
2
+ include_context 'database setup'
3
+
4
+ with_adapters do
5
+ let(:rom) do
6
+ ROM.container(:sql, uri) do |conf|
7
+ conf.default.create_table(:dragons) do
8
+ primary_key :id
9
+ column :name, String
10
+ end
11
+ end
12
+ end
13
+
14
+ after do
15
+ rom.gateways[:default].drop_table(:dragons)
16
+ end
17
+
18
+ it 'creates tables within the setup block' do
19
+ expect(rom.relations[:dragons]).to be_kind_of(ROM::SQL::Relation)
20
+ end
21
+ end
22
+ end
@@ -1,16 +1,29 @@
1
1
  shared_context 'database setup' do
2
- let(:uri) { DB_URI }
2
+ let(:uri) do
3
+ if defined?(DB_URI)
4
+ DB_URI
5
+ else
6
+ POSTGRES_DB_URI
7
+ end
8
+ end
9
+
3
10
  let(:conn) { Sequel.connect(uri) }
4
- let(:configuration) { ROM::Configuration.new(:sql, conn).use(:macros) }
5
- let(:container) { ROM.container(configuration) }
11
+ let(:conf) { ROM::Configuration.new(:sql, conn) }
12
+ let(:container) { ROM.container(conf) }
13
+ let(:relations) { container.relations }
14
+ let(:commands) { container.commands }
6
15
 
7
16
  def drop_tables
8
- [:tasks, :users, :tags, :task_tags, :rabbits, :carrots, :schema_migrations].each do |name|
17
+ %i(task_tags users_tasks tasks tags
18
+ subscriptions cards accounts
19
+ posts users
20
+ rabbits carrots schema_migrations
21
+ ).each do |name|
9
22
  conn.drop_table?(name)
10
23
  end
11
24
  end
12
25
 
13
- before do
26
+ before do |example|
14
27
  conn.loggers << LOGGER
15
28
 
16
29
  drop_tables
@@ -18,14 +31,13 @@ shared_context 'database setup' do
18
31
  conn.create_table :users do
19
32
  primary_key :id
20
33
  String :name, null: false
21
- index :name, unique: true
22
- check { char_length(name) > 2 }
34
+ check { char_length(name) > 2 } if postgres?(example)
23
35
  end
24
36
 
25
37
  conn.create_table :tasks do
26
38
  primary_key :id
27
39
  foreign_key :user_id, :users
28
- String :title
40
+ String :title, unique: true
29
41
  end
30
42
 
31
43
  conn.create_table :tags do
@@ -38,6 +50,32 @@ shared_context 'database setup' do
38
50
  Integer :tag_id
39
51
  Integer :task_id
40
52
  end
53
+
54
+ conn.create_table :posts do
55
+ primary_key :post_id
56
+ foreign_key :author_id, :users
57
+ String :title
58
+ String :body
59
+ end
60
+
61
+ conn.create_table :accounts do
62
+ primary_key :id
63
+ Integer :user_id
64
+ String :number
65
+ Decimal :balance
66
+ end
67
+
68
+ conn.create_table :cards do
69
+ primary_key :id
70
+ Integer :account_id
71
+ String :pan
72
+ end
73
+
74
+ conn.create_table :subscriptions do
75
+ primary_key :id
76
+ Integer :card_id
77
+ String :service
78
+ end
41
79
  end
42
80
 
43
81
  after do
@@ -0,0 +1,8 @@
1
+ RSpec.shared_context 'relations' do
2
+ include_context 'database setup'
3
+
4
+ before do
5
+ conf.relation(:users)
6
+ conf.relation(:tasks)
7
+ end
8
+ end
@@ -0,0 +1,10 @@
1
+ shared_context 'users and accounts' do
2
+ include_context 'database setup'
3
+
4
+ before do
5
+ conn[:users].insert id: 1, name: 'Piotr'
6
+ conn[:accounts].insert id: 1, user_id: 1, number: '42', balance: 10_000.to_d
7
+ conn[:cards].insert id: 1, account_id: 1, pan: '*6789'
8
+ conn[:subscriptions].insert id: 1, card_id: 1, service: 'aws'
9
+ end
10
+ end
@@ -2,9 +2,27 @@ shared_context 'users and tasks' do
2
2
  include_context 'database setup'
3
3
 
4
4
  before do
5
- conn[:users].insert id: 1, name: 'Piotr'
6
- conn[:tasks].insert id: 1, user_id: 1, title: 'Finish ROM'
5
+ conn[:users].insert id: 1, name: 'Jane'
6
+ conn[:users].insert id: 2, name: 'Joe'
7
+
8
+ conn[:tasks].insert id: 1, user_id: 2, title: "Joe's task"
9
+ conn[:tasks].insert id: 2, user_id: 1, title: "Jane's task"
10
+
7
11
  conn[:tags].insert id: 1, name: 'important'
8
12
  conn[:task_tags].insert(tag_id: 1, task_id: 1)
13
+
14
+ conn[:posts].insert(
15
+ post_id: 1,
16
+ author_id: 2,
17
+ title: "Joe's post",
18
+ body: 'Joe wrote sutin'
19
+ )
20
+
21
+ conn[:posts].insert(
22
+ post_id: 2,
23
+ author_id: 1,
24
+ title: "Jane's post",
25
+ body: 'Jane wrote sutin'
26
+ )
9
27
  end
10
28
  end
data/spec/spec_helper.rb CHANGED
@@ -3,7 +3,7 @@
3
3
  require 'bundler'
4
4
  Bundler.setup
5
5
 
6
- if RUBY_ENGINE == 'rbx'
6
+ if RUBY_ENGINE == 'ruby' && RUBY_VERSION == '2.3.1'
7
7
  require 'codeclimate-test-reporter'
8
8
  CodeClimate::TestReporter.start
9
9
  end
@@ -11,37 +11,90 @@ end
11
11
  require 'rom-sql'
12
12
  require 'rom/sql/rake_task'
13
13
 
14
- # FIXME: why do we need to require it manually??
15
- require 'sequel/adapters/postgres'
16
- require 'active_support/inflector'
17
-
18
14
  require 'logger'
15
+ require 'tempfile'
16
+
19
17
  begin
20
18
  require 'byebug'
21
19
  rescue LoadError # rubocop:disable Lint/HandleExceptions
22
20
  end
23
21
 
24
22
  LOGGER = Logger.new(File.open('./log/test.log', 'a'))
25
- DB_URI = 'postgres://localhost/rom_sql'
26
23
 
27
- root = Pathname(__FILE__).dirname
24
+ if defined? JRUBY_VERSION
25
+ SQLITE_DB_URI = 'jdbc:sqlite:::memory'
26
+ POSTGRES_DB_URI = 'jdbc:postgresql://localhost/rom_sql'
27
+ MYSQL_DB_URI = 'jdbc:mysql://localhost/rom_sql?user=root'
28
+ else
29
+ SQLITE_DB_URI = 'sqlite::memory'
30
+ POSTGRES_DB_URI = 'postgres://localhost/rom_sql'
31
+ MYSQL_DB_URI = 'mysql2://root@localhost/rom_sql'
32
+ end
33
+
34
+ URIS = { postgres: POSTGRES_DB_URI, sqlite: SQLITE_DB_URI, mysql: MYSQL_DB_URI }
35
+ ADAPTERS = URIS.keys
36
+ PG_LTE_95 = ENV.fetch('PG_LTE_95', 'true') == 'true'
37
+
38
+ SPEC_ROOT = root = Pathname(__FILE__).dirname
28
39
  TMP_PATH = root.join('../tmp')
29
40
 
30
- Dir[root.join('shared/*.rb').to_s].each { |f| require f }
41
+ Dir[root.join('shared/**/*')].each { |f| require f }
42
+ Dir[root.join('support/**/*')].each { |f| require f }
43
+
44
+ require 'rom/support/deprecations'
45
+ ROM::Deprecations.set_logger!(root.join('../log/deprecations.log'))
46
+
47
+ def db?(type, example = nil)
48
+ if example
49
+ example.metadata[:adapter] == type
50
+ else
51
+ defined?(DB_URI) && DB_URI.include?(type.to_s)
52
+ end
53
+ end
54
+
55
+ def postgres?(example = nil)
56
+ db?(:postgres, example)
57
+ end
58
+
59
+ def mysql?(example = nil)
60
+ db?(:mysql, example)
61
+ end
62
+
63
+ def with_adapter(adapter, &block)
64
+ Object.const_set(:DB_URI, URIS[:mysql])
65
+ block.call
66
+ Object.send(:remove_const, :DB_URI)
67
+ end
68
+
69
+ def with_adapters(*args, &block)
70
+ adapters = args.empty? || args[0] == :all ? ADAPTERS : args
71
+
72
+ adapters.each do |adapter|
73
+ context("with #{adapter}", adapter: adapter, &block)
74
+ end
75
+ end
31
76
 
32
77
  RSpec.configure do |config|
78
+ config.disable_monkey_patching
79
+
33
80
  config.before(:suite) do
34
81
  tmp_test_dir = TMP_PATH.join('test')
35
82
  FileUtils.rm_r(tmp_test_dir) if File.exist?(tmp_test_dir)
36
83
  FileUtils.mkdir_p(tmp_test_dir)
37
84
  end
38
85
 
86
+ config.around(adapter: :mysql) do |example|
87
+ with_adapter(:mysql) { example.run }
88
+ end
89
+
39
90
  config.before do
40
- @constants = Object.constants
91
+ module Test
92
+ end
41
93
  end
42
94
 
43
95
  config.after do
44
- added_constants = Object.constants - @constants
45
- added_constants.each { |name| Object.send(:remove_const, name) }
96
+ Object.send(:remove_const, :Test)
46
97
  end
98
+
99
+ config.include(Helpers, helpers: true)
47
100
  end