rom-sql 0.7.0 → 0.8.0

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