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 +55 -23
- data/README.md +19 -14
- data/TODO +11 -4
- data/VERSION +1 -1
- data/db_leftovers.gemspec +3 -2
- data/lib/db_leftovers/database_interface.rb +18 -9
- data/lib/db_leftovers/dsl.rb +2 -2
- data/spec/db_leftovers_spec.rb +52 -0
- metadata +4 -4
data/README.html
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
<h1>db_leftovers</h1>
|
2
2
|
|
3
|
-
<p>db_leftovers lets you define indexes
|
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
|
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
|
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
|
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 => true
|
32
32
|
</code></pre>
|
33
33
|
|
34
|
-
<h3>foreign_key
|
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
|
-
|
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, :
|
51
|
+
foreign_key :pages, :book_id, :books, :id, :on_delete => :cascade
|
53
52
|
</code></pre>
|
54
53
|
|
55
|
-
<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 > 0'
|
62
|
+
</code></pre>
|
63
|
+
|
64
|
+
<h3>table(table_name, &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 > 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 >= 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) > 2'</code>.
|
96
|
+
The second time you run db_leftovers, it will read the expression from Postgres and get <code>LENGTH((email)::text) > 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
|
78
|
-
If you want to add support for something else, just send me a pull request!</
|
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.
|
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.
|
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.
|
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-
|
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
|
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
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
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,
|
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
|
data/lib/db_leftovers/dsl.rb
CHANGED
@@ -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}"
|
data/spec/db_leftovers_spec.rb
CHANGED
@@ -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.
|
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-
|
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:
|
142
|
+
hash: 856836431
|
143
143
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
144
144
|
none: false
|
145
145
|
requirements:
|