db_leftovers 0.6.0 → 0.7.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.
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: