db_leftovers 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.
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: