db_leftovers 0.6.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
data/.document CHANGED
File without changes
data/Gemfile CHANGED
File without changes
data/Gemfile.lock CHANGED
File without changes
data/LICENSE.txt CHANGED
File without changes
data/README.html CHANGED
@@ -1,10 +1,10 @@
1
1
  <h1>db_leftovers</h1>
2
2
 
3
- <p>db_leftovers lets you define indexes, foreign keys, and CHECK constraints for your Rails app
3
+ <p>db_leftovers lets you define indexes and foreign keys for your Rails app
4
4
  in one place using an easy-to-read DSL,
5
5
  then run a rake task to bring your database up-to-date.
6
- I wrote this because I didn't want indexes and constraints scattered throughout my migrations or buried in my <code>schema.rb</code>, and I wanted a command I could run to ensure that they matched across my development, test, staging, and production databases.
7
- This was particularly a problem for Heroku projects, because <code>db:push</code> and <code>db:pull</code> do not transfer your foreign keys or other constraints.
6
+ I wrote this because I didn't want indexes and foreign keys scattered throughout my migrations or buried in my <code>schema.rb</code>, and I wanted a command I could run to ensure that they matched across my development, test, staging, and production databases.
7
+ This was particularly a problem for Heroku projects, because <code>db:push</code> and <code>db:pull</code> do not transfer your foreign keys.
8
8
  But now that it's written, I'm finding it useful on non-Heroku projects as well.</p>
9
9
 
10
10
  <p>At present db_leftovers works only on PostgreSQL databases,
@@ -12,9 +12,9 @@ but it could easily be extended to cover other RDBMSes.</p>
12
12
 
13
13
  <h2>Configuration File</h2>
14
14
 
15
- <p>db_leftovers reads a file named <code>config/db_leftovers.rb</code> to find out which indexes and constraints you want in your database. This file is a DSL implemented in Ruby, sort of like <code>config/routes.rb</code>. There are only a few methods:</p>
15
+ <p>db_leftovers reads a file named <code>config/db_leftovers.rb</code> to find out which indexes and foreign keys you want in your database. This file is a DSL implemented in Ruby, sort of like <code>config/routes.rb</code>. There are only a few methods:</p>
16
16
 
17
- <h3>index(table_name, columns, [opts])</h3>
17
+ <h3>index table_name, columns, [opts]</h3>
18
18
 
19
19
  <p>This ensures that you have an index on the given table and column(s). The <code>columns</code> parameter can be either a string or a list of strings. Opts is a hash with the following possible keys:</p>
20
20
 
@@ -31,48 +31,38 @@ index :books, [:publisher_id, :published_at]
31
31
  index :books, :isbn, :unique =&gt; true
32
32
  </code></pre>
33
33
 
34
- <h3>foreign_key(from_table, from_column, to_table, [to_column, [opts]])</h3>
34
+ <h3>foreign_key from_table, from_column, to_table, [to_column, [opts]]</h3>
35
35
 
36
36
  <p>This ensures that you have a foreign key relating the given tables and columns.
37
- All parameters are strings/symbols except <code>opts</code>, which is a hash.
37
+ All parameters are strings except <code>opts</code>, which is a hash.
38
38
  If you don't pass anything for <code>opts</code>, you can leave off the <code>to_column</code> parameter, and it will default to <code>:id</code>.
39
- The only option that is supported is <code>:on_delete</code>, which may have any of these values:</p>
39
+ These options are supported:</p>
40
40
 
41
41
  <ul>
42
- <li><code>nil</code> Indicates that attempting to delete the referenced row should fail (the default).</li>
43
42
  <li><code>:set_null</code> Indicates that the foreign key should be set to null if the referenced row is deleted.</li>
44
43
  <li><code>:cascade</code> Indicates that the referencing row should be deleted if the referenced row is deleted.</li>
45
44
  </ul>
46
45
 
46
+ <p>These options are mutually exclusive. They should probably be consolidated into a single option like <code>:on_delete</code>.</p>
47
+
47
48
  <h4>Examples</h4>
48
49
 
49
50
  <pre><code>foreign_key :books, :author_id, :authors, :id
50
51
  foreign_key :books, :publisher_id, :publishers
51
- foreign_key :pages, :book_id, :books, :id, :on_delete =&gt; :cascade
52
- </code></pre>
53
-
54
- <h3>check(constraint_name, on_table, expression)</h3>
55
-
56
- <p>This ensures that you have a CHECK constraint on the given table with the given name and expression.
57
- All parameters are strings or symbols.</p>
58
-
59
- <h4>Examples</h4>
60
-
61
- <pre><code>check :books, :books_have_positive_pages, 'page_count &gt; 0'
52
+ foreign_key :pages, :book_id, :books, :id, :cascade =&gt; true
62
53
  </code></pre>
63
54
 
64
- <h3>table(table_name, &amp;block)</h3>
55
+ <h3>table table_name, &amp;block</h3>
65
56
 
66
57
  <p>The <code>table</code> call is just a convenience so you can group all a table's indexes and foreign keys together and not keep repeating the table name. You use it like this:</p>
67
58
 
68
59
  <pre><code>table :books do
69
60
  index :author_id
70
61
  foreign_key :publisher_id, :publishers
71
- check :books_have_positive_pages, 'page_count &gt; 0'
72
62
  end
73
63
  </code></pre>
74
64
 
75
- <p>You can repeat <code>table</code> calls for the same table several times if you like. This lets you put your indexes in one place and your foreign keys in another, for example.</p>
65
+ <p>You can repeat <code>table</code> calls for the same table several times if you like. This lets you put your indexes in one place and your foreign keys in another.</p>
76
66
 
77
67
  <h2>Running db_leftovers</h2>
78
68
 
@@ -81,35 +71,17 @@ end
81
71
  <pre><code>rake db:leftovers
82
72
  </code></pre>
83
73
 
84
- <p>To print messages even about indexes/foreign keys/constraints that haven't changed, you can say:</p>
85
-
86
- <pre><code>rake db:leftovers VERBOSE=true
87
- </code></pre>
88
-
89
- <p>or</p>
90
-
91
- <pre><code>rake db:leftovers DB_LEFTOVERS_VERBOSE=true
92
- </code></pre>
93
-
94
74
  <h2>Known Issues</h2>
95
75
 
96
76
  <ul>
97
77
  <li><p>db_leftovers only supports PostgreSQL databases.
98
78
  If you want to add support for something else, just send me a pull request!</p></li>
99
- <li><p>db_leftovers will not notice if an foreign key/constraint definition changes.
100
- Right now it only checks for existence/non-existence.
101
- You can get around this by adding a version number to your constraint names,
102
- so if you want to force books to have at least 12 pages, you can say this:</p>
103
-
104
- <p><code>check :books_have_positive_pages2, 'page_count &gt;= 12'</code></p>
105
-
106
- <p>Then the old constraint will be dropped and the new one will be added.</p>
107
-
108
- <p>However, db_leftovers <em>does</em> check for index definitions, so if you
109
- make an existing index unique, add a column, remove a WHERE clause, or
110
- anything else, it will notice and drop and re-create the index.
111
- I'm working on doing the same thing for foreign keys/constraints,
112
- but it's not done just yet.</p></li>
79
+ <li><p>db_leftovers will not notice if an index/foreign key definition changes.
80
+ Right now it only checks for existence/non-existence.</p></li>
81
+ <li><p>If your database is mostly up-to-date, then running the Rake task will spam
82
+ you with messages about how this index and that foreign key already exist.
83
+ These should be hidden by default and shown only if you request a higher
84
+ verbosity.</p></li>
113
85
  </ul>
114
86
 
115
87
  <h2>Contributing to db_leftovers</h2>
@@ -126,5 +98,5 @@ but it's not done just yet.</p></li>
126
98
 
127
99
  <h2>Copyright</h2>
128
100
 
129
- <p>Copyright (c) 2012 Paul A. Jungwirth.
130
- See LICENSE.txt for further details.</p>
101
+ <p>Copyright (c) 2012 Paul A. Jungwirth. See LICENSE.txt for
102
+ further details.</p>
data/README.md CHANGED
File without changes
data/Rakefile CHANGED
File without changes
data/TODO CHANGED
@@ -2,7 +2,17 @@
2
2
  - Support altering index and foreign keys if their definition changes
3
3
  - don't show "{index,fk} already exists" unless you set the verbosity higher
4
4
 
5
- - Use Sequel to introspect databases for their indexes, to get easy support for non-Postgres databases.
6
- - BUT: Sequel can't return partial indexes (i.e. indexes created with a WHERE clause), and using those kind of indexes is a major feature for db_leftovers. So this will have to wait until I can get a patch accepted to let at least the Postgres Sequel adapter optionally query for those type of indexes.
5
+ - Abbreviated notation for conventional foreign keys:
6
+
7
+ table :books do
8
+ foreign_key :authors
9
+ foreign_key :publishers, :on_delete => :cascade
10
+
11
+ # and with a funny FK column but implicit PK of 'id':
12
+
13
+ foreign_key :ghost_writer_id, :authors, :on_delete => :cascade
14
+ end
15
+
16
+ In other words, the params are foreign_key(from_table, [from_column], to_table, [to_column], [opts]).
7
17
 
8
18
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.6.0
1
+ 0.7.0
data/db_leftovers.gemspec CHANGED
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = "db_leftovers"
8
- s.version = "0.6.0"
8
+ s.version = "0.7.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Paul A. Jungwirth"]
12
- s.date = "2012-09-27"
12
+ s.date = "2012-09-29"
13
13
  s.description = " Define indexes and foreign keys for your Rails app\n in one place using an easy-to-read DSL,\n then run a rake task to bring your database up-to-date.\n"
14
14
  s.email = "pj@illuminatedcomputing.com"
15
15
  s.extra_rdoc_files = [
@@ -8,6 +8,16 @@ module DBLeftovers
8
8
  @on_table = on_table.to_s
9
9
  @check = check
10
10
  end
11
- end
12
11
 
12
+ def equals(other)
13
+ other.constraint_name == constraint_name and
14
+ other.on_table == on_table and
15
+ other.check == check
16
+ end
17
+
18
+ def to_s
19
+ "<#{@constraint_name}: #{@on_table} CHECK (#{@check})>"
20
+ end
21
+
22
+ end
13
23
  end
@@ -3,6 +3,7 @@ module DBLeftovers
3
3
  class DatabaseInterface
4
4
 
5
5
  def lookup_all_indexes
6
+ # TODO: Constraint it to the database for the current Rails project:
6
7
  ret = {}
7
8
  sql = <<-EOQ
8
9
  SELECT ix.indexrelid,
@@ -49,18 +50,48 @@ module DBLeftovers
49
50
 
50
51
 
51
52
  def lookup_all_foreign_keys
53
+ # confdeltype: a=nil, c=cascade, n=null
52
54
  ret = {}
55
+ # TODO: Support multi-column foreign keys:
56
+ # TODO: Constraint it to the database for the current Rails project:
53
57
  sql = <<-EOQ
54
- SELECT t.constraint_name, t.table_name, k.column_name, t.constraint_type, c.table_name, c.column_name
55
- FROM information_schema.table_constraints t,
56
- information_schema.constraint_column_usage c,
57
- information_schema.key_column_usage k
58
- WHERE t.constraint_name = c.constraint_name
59
- AND k.constraint_name = c.constraint_name
60
- AND t.constraint_type = 'FOREIGN KEY'
58
+ SELECT c.conname,
59
+ t1.relname,
60
+ a1.attname,
61
+ t2.relname,
62
+ a2.attname,
63
+ c.confdeltype
64
+ FROM pg_catalog.pg_constraint c,
65
+ pg_catalog.pg_class t1,
66
+ pg_catalog.pg_class t2,
67
+ pg_catalog.pg_attribute a1,
68
+ pg_catalog.pg_attribute a2,
69
+ pg_catalog.pg_namespace n1,
70
+ pg_catalog.pg_namespace n2
71
+ WHERE c.conrelid = t1.oid
72
+ AND c.confrelid = t2.oid
73
+ AND c.contype = 'f'
74
+ AND a1.attrelid = t1.oid
75
+ AND a1.attnum = ANY(c.conkey)
76
+ AND a2.attrelid = t2.oid
77
+ AND a2.attnum = ANY(c.confkey)
78
+ AND t1.relkind = 'r'
79
+ AND t2.relkind = 'r'
80
+ AND n1.oid = t1.relnamespace
81
+ AND n2.oid = t2.relnamespace
82
+ AND n1.nspname NOT IN ('pg_catalog', 'pg_toast')
83
+ AND n2.nspname NOT IN ('pg_catalog', 'pg_toast')
84
+ AND pg_catalog.pg_table_is_visible(t1.oid)
85
+ AND pg_catalog.pg_table_is_visible(t2.oid)
61
86
  EOQ
62
- ActiveRecord::Base.connection.select_rows(sql).each do |constr_name, from_table, from_column, constr_type, to_table, to_column|
63
- ret[constr_name] = ForeignKey.new(constr_name, from_table, from_column, to_table, to_column)
87
+ ActiveRecord::Base.connection.select_rows(sql).each do |constr_name, from_table, from_column, to_table, to_column, del_type|
88
+ del_type = case del_type
89
+ when 'a'; nil
90
+ when 'c'; :cascade
91
+ when 'n'; :set_null
92
+ else; raise "Unknown del type: #{del_type}"
93
+ end
94
+ ret[constr_name] = ForeignKey.new(constr_name, from_table, from_column, to_table, to_column, :on_delete => del_type)
64
95
  end
65
96
  return ret
66
97
  end
File without changes
@@ -80,9 +80,14 @@ module DBLeftovers
80
80
  # First create any new foreign keys:
81
81
  @foreign_keys_by_table.each do |table_name, fks|
82
82
  fks.each do |fk|
83
- if foreign_key_exists?(fk)
83
+ case foreign_key_status(fk)
84
+ when STATUS_EXISTS
84
85
  puts "Foreign Key already exists: #{fk.constraint_name} on #{fk.from_table}" if @verbose
85
- else
86
+ when STATUS_CHANGED
87
+ @db.execute_drop_foreign_key(fk.constraint_name, fk.from_table, fk.from_column)
88
+ @db.execute_add_foreign_key(fk)
89
+ puts "Dropped & re-created foreign key: #{fk.constraint_name} on #{fk.from_table}"
90
+ when STATUS_NEW
86
91
  @db.execute_add_foreign_key(fk)
87
92
  puts "Created foreign key: #{fk.constraint_name} on #{fk.from_table}"
88
93
  end
@@ -103,9 +108,14 @@ module DBLeftovers
103
108
  # First create any new constraints:
104
109
  @constraints_by_table.each do |table_name, chks|
105
110
  chks.each do |chk|
106
- if constraint_exists?(chk)
111
+ case constraint_status(chk)
112
+ when STATUS_EXISTS
107
113
  puts "Constraint already exists: #{chk.constraint_name} on #{chk.on_table}" if @verbose
108
- else
114
+ when STATUS_CHANGED
115
+ @db.execute_drop_constraint(constraint_name, chk.on_table)
116
+ @db.execute_add_constraint(chk)
117
+ puts "Dropped & re-created CHECK constraint: #{chk.constraint_name} on #{chk.on_table}"
118
+ when STATUS_NEW
109
119
  @db.execute_add_constraint(chk)
110
120
  puts "Created CHECK constraint: #{chk.constraint_name} on #{chk.on_table}"
111
121
  end
@@ -152,12 +162,22 @@ module DBLeftovers
152
162
  end
153
163
  end
154
164
 
155
- def foreign_key_exists?(fk)
156
- @old_foreign_keys[fk.constraint_name]
165
+ def foreign_key_status(fk)
166
+ old = @old_foreign_keys[fk.constraint_name]
167
+ if old
168
+ return old.equals(fk) ? STATUS_EXISTS : STATUS_CHANGED
169
+ else
170
+ return STATUS_NEW
171
+ end
157
172
  end
158
173
 
159
- def constraint_exists?(chk)
160
- @old_constraints[chk.constraint_name]
174
+ def constraint_status(chk)
175
+ old = @old_constraints[chk.constraint_name]
176
+ if old
177
+ return old.equals(chk) ? STATUS_EXISTS : STATUS_CHANGED
178
+ else
179
+ return STATUS_NEW
180
+ end
161
181
  end
162
182
 
163
183
  def name_constraint(from_table, from_column)
@@ -31,7 +31,12 @@ module DBLeftovers
31
31
  other.from_column == from_column and
32
32
  other.to_table == to_table and
33
33
  other.to_column == to_column and
34
- other.opts == opts
34
+ other.set_null == set_null and
35
+ other.cascade == cascade
36
+ end
37
+
38
+ def to_s
39
+ "<#{@constraint_name}: from #{@from_table}.#{@from_column} to #{@to_table}.#{@to_column} #{if @set_null; "ON DELETE SET NULL "; elsif @cascade; "ON DELETE CASCADE "; else ""; end}>"
35
40
  end
36
41
 
37
42
  end
File without changes
File without changes
data/lib/db_leftovers.rb CHANGED
File without changes
File without changes
@@ -13,15 +13,13 @@ class DBLeftovers::DatabaseInterface
13
13
  end
14
14
 
15
15
  def execute_sql(sql)
16
- @@sqls << sql
16
+ @@sqls << DBLeftovers::DatabaseInterface.normal_whitespace(sql)
17
17
  end
18
18
 
19
19
  def self.saw_sql(sql)
20
20
  # puts sqls.join("\n\n\n")
21
21
  # Don't fail if only the whitespace is different:
22
- sqls.map{|x| x.gsub(/\s+/m, ' ').strip}.include?(
23
- sql.gsub(/\s+/m, ' ').strip
24
- )
22
+ sqls.include?(normal_whitespace(sql))
25
23
  end
26
24
 
27
25
  def self.starts_with(indexes=[], foreign_keys=[], constraints=[])
@@ -43,6 +41,12 @@ class DBLeftovers::DatabaseInterface
43
41
  @@constraints
44
42
  end
45
43
 
44
+ private
45
+
46
+ def self.normal_whitespace(sql)
47
+ sql.gsub(/\s/m, ' ').gsub(/ +/, ' ').strip
48
+ end
49
+
46
50
  end
47
51
 
48
52
  RSpec::Matchers.define :have_seen_sql do |sql|
@@ -292,6 +296,26 @@ describe DBLeftovers do
292
296
 
293
297
 
294
298
 
299
+ it "should create foreign keys when they have been redefined" do
300
+ DBLeftovers::DatabaseInterface.starts_with([], [
301
+ DBLeftovers::ForeignKey.new('fk_books_shelf_id', 'books', 'shelf_id', 'shelves', 'id'),
302
+ DBLeftovers::ForeignKey.new('fk_books_author_id', 'books', 'author_id', 'authors', 'id')
303
+ ])
304
+ DBLeftovers::Definition.define do
305
+ table :books do
306
+ foreign_key :shelf_id, :shelves, :id, :on_delete => :cascade
307
+ foreign_key :author_id, :authors, :id, :on_delete => :set_null
308
+ end
309
+ end
310
+ DBLeftovers::DatabaseInterface.sqls.should have(4).items
311
+ DBLeftovers::DatabaseInterface.sqls[0].should =~ /ALTER TABLE books DROP CONSTRAINT fk_books_shelf_id/
312
+ DBLeftovers::DatabaseInterface.sqls[1].should =~ /ALTER TABLE books ADD CONSTRAINT fk_books_shelf_id/
313
+ DBLeftovers::DatabaseInterface.sqls[2].should =~ /ALTER TABLE books DROP CONSTRAINT fk_books_author_id/
314
+ DBLeftovers::DatabaseInterface.sqls[3].should =~ /ALTER TABLE books ADD CONSTRAINT fk_books_author_id/
315
+ end
316
+
317
+
318
+
295
319
  it "should support creating multi-column indexes" do
296
320
  DBLeftovers::DatabaseInterface.starts_with
297
321
  DBLeftovers::Definition.define do
data/spec/spec_helper.rb CHANGED
File without changes
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: db_leftovers
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.7.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-09-27 00:00:00.000000000 Z
12
+ date: 2012-09-29 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails
@@ -139,7 +139,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
139
139
  version: '0'
140
140
  segments:
141
141
  - 0
142
- hash: -2522100486146355365
142
+ hash: 258395923
143
143
  required_rubygems_version: !ruby/object:Gem::Requirement
144
144
  none: false
145
145
  requirements: