db_leftovers 0.7.0 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.html CHANGED
@@ -1,10 +1,10 @@
1
1
  <h1>db_leftovers</h1>
2
2
 
3
- <p>db_leftovers lets you define indexes and foreign keys for your Rails app
3
+ <p>db_leftovers lets you define indexes, foreign keys, and CHECK constraints 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 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.
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.
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 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>
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>
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,38 +31,48 @@ 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 except <code>opts</code>, which is a hash.
37
+ All parameters are strings/symbols 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
- These options are supported:</p>
39
+ The only option that is supported is <code>:on_delete</code>, which may have any of these values:</p>
40
40
 
41
41
  <ul>
42
+ <li><code>nil</code> Indicates that attempting to delete the referenced row should fail (the default).</li>
42
43
  <li><code>:set_null</code> Indicates that the foreign key should be set to null if the referenced row is deleted.</li>
43
44
  <li><code>:cascade</code> Indicates that the referencing row should be deleted if the referenced row is deleted.</li>
44
45
  </ul>
45
46
 
46
- <p>These options are mutually exclusive. They should probably be consolidated into a single option like <code>:on_delete</code>.</p>
47
-
48
47
  <h4>Examples</h4>
49
48
 
50
49
  <pre><code>foreign_key :books, :author_id, :authors, :id
51
50
  foreign_key :books, :publisher_id, :publishers
52
- foreign_key :pages, :book_id, :books, :id, :cascade =&gt; true
51
+ foreign_key :pages, :book_id, :books, :id, :on_delete =&gt; :cascade
53
52
  </code></pre>
54
53
 
55
- <h3>table table_name, &amp;block</h3>
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'
62
+ </code></pre>
63
+
64
+ <h3>table(table_name, &amp;block)</h3>
56
65
 
57
66
  <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>
58
67
 
59
68
  <pre><code>table :books do
60
69
  index :author_id
61
70
  foreign_key :publisher_id, :publishers
71
+ check :books_have_positive_pages, 'page_count &gt; 0'
62
72
  end
63
73
  </code></pre>
64
74
 
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>
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>
66
76
 
67
77
  <h2>Running db_leftovers</h2>
68
78
 
@@ -71,17 +81,39 @@ end
71
81
  <pre><code>rake db:leftovers
72
82
  </code></pre>
73
83
 
84
+ <p>db_leftovers will notice whenever an index, foreign key, or CHECK constraint needs to be added, dropped, or changed.
85
+ It will even catch cases where the name of the managed object is the same, but its attributes have changed.
86
+ For instance, if you previously required books to have at least 1 page, but now you are introducing a "pamphlet"
87
+ and want books to have at least 100 pages, you can change your config file to say:</p>
88
+
89
+ <pre><code>check :books, :books_have_positive_pages, 'page_count &gt;= 100'
90
+ </code></pre>
91
+
92
+ <p>and db_leftovers will notice the changed expression. It will drop and re-add the constraint.</p>
93
+
94
+ <p>One caveat, however: we pull the current expression from the database, and sometimes Postgres does things like
95
+ add type conversions. If instance, suppose you said <code>check :users, :email_length, 'LENGTH(email) &gt; 2'</code>.
96
+ The second time you run db_leftovers, it will read the expression from Postgres and get <code>LENGTH((email)::text) &gt; 2</code>,
97
+ and so it will drop and re-create the constraint.
98
+ It will drop and re-create it every time you run the rake task.
99
+ To get around this, make sure your config file uses the same expression as printed by db_leftovers in the rake output.
100
+ This can also happen for index WHERE clauses, fixable by a similar workaround.</p>
101
+
102
+ <p>To print messages even about indexes/foreign keys/constraints that haven't changed, you can say:</p>
103
+
104
+ <pre><code>rake db:leftovers VERBOSE=true
105
+ </code></pre>
106
+
107
+ <p>or</p>
108
+
109
+ <pre><code>rake db:leftovers DB_LEFTOVERS_VERBOSE=true
110
+ </code></pre>
111
+
74
112
  <h2>Known Issues</h2>
75
113
 
76
114
  <ul>
77
- <li><p>db_leftovers only supports PostgreSQL databases.
78
- If you want to add support for something else, just send me a pull request!</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>
115
+ <li>db_leftovers only supports PostgreSQL databases.
116
+ If you want to add support for something else, just send me a pull request!</li>
85
117
  </ul>
86
118
 
87
119
  <h2>Contributing to db_leftovers</h2>
@@ -98,5 +130,5 @@ verbosity.</p></li>
98
130
 
99
131
  <h2>Copyright</h2>
100
132
 
101
- <p>Copyright (c) 2012 Paul A. Jungwirth. See LICENSE.txt for
102
- further details.</p>
133
+ <p>Copyright (c) 2012 Paul A. Jungwirth.
134
+ See LICENSE.txt for further details.</p>
data/README.md CHANGED
@@ -78,6 +78,23 @@ db\_leftovers comes with a Rake task named `db:leftovers`. So you can update you
78
78
 
79
79
  rake db:leftovers
80
80
 
81
+ db\_leftovers will notice whenever an index, foreign key, or CHECK constraint needs to be added, dropped, or changed.
82
+ It will even catch cases where the name of the managed object is the same, but its attributes have changed.
83
+ For instance, if you previously required books to have at least 1 page, but now you are introducing a "pamphlet"
84
+ and want books to have at least 100 pages, you can change your config file to say:
85
+
86
+ check :books, :books_have_positive_pages, 'page_count >= 100'
87
+
88
+ and db\_leftovers will notice the changed expression. It will drop and re-add the constraint.
89
+
90
+ One caveat, however: we pull the current expression from the database, and sometimes Postgres does things like
91
+ add type conversions. If instance, suppose you said `check :users, :email_length, 'LENGTH(email) > 2'`.
92
+ The second time you run db\_leftovers, it will read the expression from Postgres and get `LENGTH((email)::text) > 2`,
93
+ and so it will drop and re-create the constraint.
94
+ It will drop and re-create it every time you run the rake task.
95
+ To get around this, make sure your config file uses the same expression as printed by db\_leftovers in the rake output.
96
+ This can also happen for index WHERE clauses, fixable by a similar workaround.
97
+
81
98
  To print messages even about indexes/foreign keys/constraints that haven't changed, you can say:
82
99
 
83
100
  rake db:leftovers VERBOSE=true
@@ -87,26 +104,14 @@ or
87
104
  rake db:leftovers DB_LEFTOVERS_VERBOSE=true
88
105
 
89
106
 
107
+
108
+
90
109
  Known Issues
91
110
  ------------
92
111
 
93
112
  * db\_leftovers only supports PostgreSQL databases.
94
113
  If you want to add support for something else, just send me a pull request!
95
114
 
96
- * db\_leftovers will not notice if an foreign key/constraint definition changes.
97
- Right now it only checks for existence/non-existence.
98
- You can get around this by adding a version number to your constraint names,
99
- so if you want to force books to have at least 12 pages, you can say this:
100
-
101
- `check :books_have_positive_pages2, 'page_count >= 12'`
102
-
103
- Then the old constraint will be dropped and the new one will be added.
104
-
105
- However, db\_leftovers *does* check for index definitions, so if you
106
- make an existing index unique, add a column, remove a WHERE clause, or
107
- anything else, it will notice and drop and re-create the index.
108
- I'm working on doing the same thing for foreign keys/constraints,
109
- but it's not done just yet.
110
115
 
111
116
 
112
117
 
data/TODO CHANGED
@@ -1,7 +1,3 @@
1
- - Write the README
2
- - Support altering index and foreign keys if their definition changes
3
- - don't show "{index,fk} already exists" unless you set the verbosity higher
4
-
5
1
  - Abbreviated notation for conventional foreign keys:
6
2
 
7
3
  table :books do
@@ -16,3 +12,14 @@
16
12
  In other words, the params are foreign_key(from_table, [from_column], to_table, [to_column], [opts]).
17
13
 
18
14
 
15
+ - Print the actual WHERE-clause/CHECK expression used by postgres whenever the index/constraint is created (even the first time).
16
+
17
+ - Refactor DatabaseInterface so all methods are instance methods and Definition/DSL uses an instance, set according to the Rails database adapter and also settable to a test subclass, so we can change the definition from test to test.
18
+
19
+ - Add optional tests that use a real Postgres/MySQL/SQLite if available.
20
+ - Run all the same tests, but against a real DatabaseInterface instance.
21
+
22
+ - Make sure everything works if the current db user has access to multiple databases: only run the code on the db given in the Rails database.yml.
23
+
24
+ - Support MySQL
25
+
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.7.0
1
+ 0.8.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.7.0"
8
+ s.version = "0.8.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-29"
12
+ s.date = "2012-09-30"
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 = [
@@ -25,6 +25,7 @@ Gem::Specification.new do |s|
25
25
  "LICENSE.txt",
26
26
  "README.md",
27
27
  "Rakefile",
28
+ "TODO",
28
29
  "VERSION",
29
30
  "db_leftovers.gemspec",
30
31
  "lib/db_leftovers.rb",
@@ -35,7 +35,7 @@ module DBLeftovers
35
35
  ORDER BY t.relname, i.relname
36
36
  EOQ
37
37
  ActiveRecord::Base.connection.select_rows(sql).each do |indexrelid, indrelid, table_name, index_name, is_unique, column_numbers, where_clause|
38
- where_clause = where_clause.gsub(/^\((.*)\)$/, '\1') if where_clause # strip surrounding parens
38
+ where_clause = remove_outer_parens(where_clause) if where_clause
39
39
  ret[index_name] = Index.new(
40
40
  table_name,
41
41
  column_names_for_index(indrelid, column_numbers.split(",")),
@@ -97,17 +97,23 @@ module DBLeftovers
97
97
  end
98
98
 
99
99
  def lookup_all_constraints
100
+ # TODO: Constraint it to the database for the current Rails project:
100
101
  ret = {}
101
102
  sql = <<-EOQ
102
- SELECT t.constraint_name, t.table_name
103
- FROM information_schema.table_constraints t
104
- WHERE t.constraint_type = 'CHECK'
105
- AND EXISTS (SELECT 1
106
- FROM information_schema.constraint_column_usage c
107
- WHERE t.constraint_name = c.constraint_name)
103
+ SELECT c.conname,
104
+ t.relname
105
+ FROM pg_catalog.pg_constraint c,
106
+ pg_catalog.pg_class t,
107
+ pg_catalog.pg_namespace n
108
+ WHERE c.contype = 'c'
109
+ AND c.conrelid = t.oid
110
+ AND t.relkind = 'r'
111
+ AND n.oid = t.relnamespace
112
+ AND n.nspname NOT IN ('pg_catalog', 'pg_toast')
113
+ AND pg_catalog.pg_table_is_visible(t.oid)
108
114
  EOQ
109
- ActiveRecord::Base.connection.select_rows(sql).each do |constr_name, on_table|
110
- ret[constr_name] = Constraint.new(constr_name, on_table, nil)
115
+ ActiveRecord::Base.connection.select_rows(sql).each do |constr_name, on_table, check_expr|
116
+ ret[constr_name] = Constraint.new(constr_name, on_table, remove_outer_parens(check_expr))
111
117
  end
112
118
  return ret
113
119
  end
@@ -175,6 +181,9 @@ module DBLeftovers
175
181
  end
176
182
  end
177
183
 
184
+ def remove_outer_parens(str)
185
+ str.gsub(/^\((.*)\)$/, '\1')
186
+ end
178
187
 
179
188
 
180
189
  end
@@ -112,9 +112,9 @@ module DBLeftovers
112
112
  when STATUS_EXISTS
113
113
  puts "Constraint already exists: #{chk.constraint_name} on #{chk.on_table}" if @verbose
114
114
  when STATUS_CHANGED
115
- @db.execute_drop_constraint(constraint_name, chk.on_table)
115
+ @db.execute_drop_constraint(chk.constraint_name, chk.on_table)
116
116
  @db.execute_add_constraint(chk)
117
- puts "Dropped & re-created CHECK constraint: #{chk.constraint_name} on #{chk.on_table}"
117
+ puts "Dropped & re-created CHECK constraint: #{chk.constraint_name} on #{chk.on_table} as #{chk.check}"
118
118
  when STATUS_NEW
119
119
  @db.execute_add_constraint(chk)
120
120
  puts "Created CHECK constraint: #{chk.constraint_name} on #{chk.on_table}"
@@ -425,4 +425,56 @@ describe DBLeftovers do
425
425
  }.should raise_error(RuntimeError, "`:cascade => true` should now be `:on_delete => :cascade`")
426
426
  end
427
427
 
428
+ it "should create CHECK constraints on an empty database" do
429
+ DBLeftovers::DatabaseInterface.starts_with([], [], [])
430
+ DBLeftovers::Definition.define do
431
+ check :books, :books_have_positive_pages, 'pages_count > 0'
432
+ end
433
+ DBLeftovers::DatabaseInterface.sqls.should have(1).item
434
+ DBLeftovers::DatabaseInterface.should have_seen_sql <<-EOQ
435
+ ALTER TABLE books ADD CONSTRAINT books_have_positive_pages CHECK (pages_count > 0)
436
+ EOQ
437
+ end
438
+
439
+ it "should create CHECK constraints inside a table block" do
440
+ DBLeftovers::DatabaseInterface.starts_with([], [], [])
441
+ DBLeftovers::Definition.define do
442
+ table :books do
443
+ check :books_have_positive_pages, 'pages_count > 0'
444
+ end
445
+ end
446
+ DBLeftovers::DatabaseInterface.sqls.should have(1).item
447
+ DBLeftovers::DatabaseInterface.should have_seen_sql <<-EOQ
448
+ ALTER TABLE books ADD CONSTRAINT books_have_positive_pages CHECK (pages_count > 0)
449
+ EOQ
450
+ end
451
+
452
+ it "should remove obsolete CHECK constraints" do
453
+ DBLeftovers::DatabaseInterface.starts_with([], [], [
454
+ DBLeftovers::Constraint.new(:books_have_positive_pages, :books, 'pages_count > 0')
455
+ ])
456
+ DBLeftovers::Definition.define do
457
+ end
458
+ DBLeftovers::DatabaseInterface.sqls.should have(1).item
459
+ DBLeftovers::DatabaseInterface.should have_seen_sql <<-EOQ
460
+ ALTER TABLE books DROP CONSTRAINT books_have_positive_pages
461
+ EOQ
462
+ end
463
+
464
+ it "should drop and re-create changed CHECK constraints" do
465
+ DBLeftovers::DatabaseInterface.starts_with([], [], [
466
+ DBLeftovers::Constraint.new(:books_have_positive_pages, :books, 'pages_count > 0')
467
+ ])
468
+ DBLeftovers::Definition.define do
469
+ check :books, :books_have_positive_pages, 'pages_count > 12'
470
+ end
471
+ DBLeftovers::DatabaseInterface.sqls.should have(2).items
472
+ DBLeftovers::DatabaseInterface.should have_seen_sql <<-EOQ
473
+ ALTER TABLE books DROP CONSTRAINT books_have_positive_pages
474
+ EOQ
475
+ DBLeftovers::DatabaseInterface.should have_seen_sql <<-EOQ
476
+ ALTER TABLE books ADD CONSTRAINT books_have_positive_pages CHECK (pages_count > 12)
477
+ EOQ
478
+ end
479
+
428
480
  end
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.7.0
4
+ version: 0.8.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-29 00:00:00.000000000 Z
12
+ date: 2012-09-30 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails
@@ -109,6 +109,7 @@ files:
109
109
  - LICENSE.txt
110
110
  - README.md
111
111
  - Rakefile
112
+ - TODO
112
113
  - VERSION
113
114
  - db_leftovers.gemspec
114
115
  - lib/db_leftovers.rb
@@ -123,7 +124,6 @@ files:
123
124
  - spec/db_leftovers_spec.rb
124
125
  - spec/spec_helper.rb
125
126
  - README.html
126
- - TODO
127
127
  homepage: http://github.com/pjungwir/db_leftovers
128
128
  licenses:
129
129
  - MIT
@@ -139,7 +139,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
139
139
  version: '0'
140
140
  segments:
141
141
  - 0
142
- hash: 258395923
142
+ hash: 856836431
143
143
  required_rubygems_version: !ruby/object:Gem::Requirement
144
144
  none: false
145
145
  requirements: