db_leftovers 1.0.0 → 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.html +48 -6
- data/README.md +51 -9
- data/TODO +10 -1
- data/VERSION +1 -1
- data/db_leftovers.gemspec +1 -1
- data/lib/db_leftovers/mysql_database_interface.rb +14 -3
- data/lib/db_leftovers/postgres_database_interface.rb +0 -6
- data/spec/mysql_spec.rb +2 -2
- metadata +2 -2
data/README.html
CHANGED
@@ -1,29 +1,51 @@
|
|
1
1
|
<h1>db_leftovers</h1>
|
2
2
|
|
3
|
-
<p>
|
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
|
-
Whenever you edit the DSL, you can re-run the rake task and db_leftovers will alter your database
|
6
|
+
Whenever you edit the DSL, you can re-run the rake task and db_leftovers will alter your database accordingly.
|
7
7
|
This is useful because of the following limitations in vanilla Rails:</p>
|
8
8
|
|
9
9
|
<ul>
|
10
10
|
<li>There are no built-in migration methods to create foreign keys or CHECK constraints.</li>
|
11
11
|
<li>Even if created, foreign keys and CHECK constraints won't appear in your schema.rb.</li>
|
12
|
+
<li>The built-in <code>add_index</code> method doesn't support WHERE clauses on your indexes.</li>
|
12
13
|
<li>If you're using Heroku, <code>db:push</code> and <code>db:pull</code> won't transfer your foreign keys and CHECK constraints.</li>
|
13
14
|
<li>Creating indexes in your migrations makes it hard to manage them.</li>
|
14
15
|
</ul>
|
15
16
|
|
16
|
-
<p>That last point deserves some elaboration.
|
17
|
+
<p>That last point deserves some elaboration. First, using <code>add_index</code> in your migrations is bug-prone because without rare developer discipline, you wind up missing indexes in some environments. (My rule is "never change a migration after a <code>git push</code>," but I haven't seen this followed elsewhere, and there is nothing that automatically enforces it.) Also, since missing indexes don't cause errors, it's easy to be missing one and not notice until users start complaining about performance.</p>
|
18
|
+
|
19
|
+
<p>Second, Scattering <code>add_index</code> methods throughout migrations also doesn't match the workflow of optimizing database queries. Hopefully you create appropriate indexes when you set up your tables originally, but in practice you often need to go back later and add/remove indexes according to your database usage patterns. Or you just forget the indexes, because you're thinking about modeling the data, not optimizing the queries.
|
20
|
+
It's easier to vet and analyze database indexes if you can see them all in one place,
|
21
|
+
and db_leftovers lets you do that easily.
|
22
|
+
And since you can rest assured that each environment conforms to the same definition, you don't need to second-guess yourself about indexes that are present in development but missing in production.
|
23
|
+
Db_leftovers lets you rest assured that each environment conforms to a definition that is easy to read and checked into version control.</p>
|
17
24
|
|
18
25
|
<p>At present db_leftovers supports PostgreSQL and MySQL, although since MySQL doesn't support index WHERE clauses or CHECK constraints, using that functionality will raise errors. (If you need to share the same definitions across Postgres and MySQL, you can run arbitrary Ruby code inside the DSL to avoid defining unsupported objects when run against MySQL.)</p>
|
19
26
|
|
20
27
|
<h2>Configuration File</h2>
|
21
28
|
|
22
|
-
<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>.
|
29
|
+
<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>. It should look something like this:</p>
|
30
|
+
|
31
|
+
<pre><code>DBLeftovers::Definition.define do
|
32
|
+
|
33
|
+
table :users do
|
34
|
+
index :email, :unique => true
|
35
|
+
check :registered_at_unless_guest, "role_name = 'guest' OR registered_at IS NOT NULL"
|
36
|
+
end
|
37
|
+
|
38
|
+
foreign_key :orders, :users
|
39
|
+
|
40
|
+
# . .
|
41
|
+
end
|
42
|
+
</code></pre>
|
43
|
+
|
44
|
+
<p>Within the DSL file, the following methods are supported:</p>
|
23
45
|
|
24
46
|
<h3>index(table_name, columns, [opts])</h3>
|
25
47
|
|
26
|
-
<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>
|
48
|
+
<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/symbol or a list of strings/symbols. Opts is a hash with the following possible keys:</p>
|
27
49
|
|
28
50
|
<ul>
|
29
51
|
<li><p><code>:name</code> The name of the index. Defaults to <code>index_</code><em>table_name</em><code>_on_</code><em>column_names</em>, like the <code>add_index</code> method from Rails migrations.</p></li>
|
@@ -124,10 +146,30 @@ MySQL doesn't have this problem because it doesn't support CHECK constraints or
|
|
124
146
|
<pre><code>rake db:leftovers DB_LEFTOVERS_VERBOSE=true
|
125
147
|
</code></pre>
|
126
148
|
|
149
|
+
<h2>Capistrano Integration</h2>
|
150
|
+
|
151
|
+
<p>I recommend running <code>rake db:migrate</code> any time you deploy, and then running <code>rake db:leftovers</code> after that. Here is what you need in your <code>config/deploy.rb</code> to make that happen:</p>
|
152
|
+
|
153
|
+
<pre><code>set :rails_env, "production"
|
154
|
+
|
155
|
+
namespace :db do
|
156
|
+
desc "Set up constraints and indexes"
|
157
|
+
task :leftovers do
|
158
|
+
run("cd #{deploy_to}/current && bundle exec rake db:leftovers RAILS_ENV=#{rails_env}")
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
after :deploy, 'deploy:migrate'
|
163
|
+
after 'deploy:migrate', 'db:leftovers'
|
164
|
+
</code></pre>
|
165
|
+
|
166
|
+
<p>You could also change this code to <em>not</em> run migrations after each deploy, if you like. But in that case I'd recommend not running db:leftovers until after the latest migrations (if any), since new entries in the DSL are likely to reference newly-created tables/columns.</p>
|
167
|
+
|
127
168
|
<h2>Known Issues</h2>
|
128
169
|
|
129
170
|
<ul>
|
130
|
-
<li>When db_leftovers interrogates your database for the currently-defined indexes et cetera, it doesn't filter things by the current database name. So if you have mutliple Rails projects all accessible to the same user, you'll wind up changing more than you like (probably by DROPing things).</li>
|
171
|
+
<li><p>When db_leftovers interrogates your database for the currently-defined indexes et cetera, it doesn't filter things by the current database name. So if you have mutliple Rails projects all accessible to the same user, you'll wind up changing more than you like (probably by DROPing things).</p></li>
|
172
|
+
<li><p>It'd be nice to have another Rake task that will read your database and generate a new <code>db_leftovers.rb</code> file, to help people migrating existing projects that already have lots of tables.</p></li>
|
131
173
|
</ul>
|
132
174
|
|
133
175
|
<h2>Contributing to db_leftovers</h2>
|
data/README.md
CHANGED
@@ -1,29 +1,50 @@
|
|
1
1
|
db\_leftovers
|
2
2
|
=============
|
3
3
|
|
4
|
-
|
4
|
+
Db\_leftovers lets you define indexes, foreign keys, and CHECK constraints for your Rails app
|
5
5
|
in one place using an easy-to-read DSL,
|
6
6
|
then run a rake task to bring your database up-to-date.
|
7
|
-
Whenever you edit the DSL, you can re-run the rake task and db\_leftovers will alter your database
|
7
|
+
Whenever you edit the DSL, you can re-run the rake task and db\_leftovers will alter your database accordingly.
|
8
8
|
This is useful because of the following limitations in vanilla Rails:
|
9
9
|
|
10
10
|
* There are no built-in migration methods to create foreign keys or CHECK constraints.
|
11
11
|
* Even if created, foreign keys and CHECK constraints won't appear in your schema.rb.
|
12
|
+
* The built-in `add_index` method doesn't support WHERE clauses on your indexes.
|
12
13
|
* If you're using Heroku, `db:push` and `db:pull` won't transfer your foreign keys and CHECK constraints.
|
13
14
|
* Creating indexes in your migrations makes it hard to manage them.
|
14
15
|
|
15
|
-
That last point deserves some elaboration.
|
16
|
+
That last point deserves some elaboration. First, using `add_index` in your migrations is bug-prone because without rare developer discipline, you wind up missing indexes in some environments. (My rule is "never change a migration after a `git push`," but I haven't seen this followed elsewhere, and there is nothing that automatically enforces it.) Also, since missing indexes don't cause errors, it's easy to be missing one and not notice until users start complaining about performance.
|
17
|
+
|
18
|
+
Second, scattering `add_index` methods throughout migrations also doesn't match the workflow of optimizing database queries. Hopefully you create appropriate indexes when you set up your tables originally, but in practice you often need to go back later and add/remove indexes according to your database usage patterns. Or you just forget the indexes, because you're thinking about modeling the data, not optimizing the queries.
|
19
|
+
It's easier to vet and analyze database indexes if you can see them all in one place,
|
20
|
+
and db\_leftovers lets you do that easily.
|
21
|
+
And since you can rest assured that each environment conforms to the same definition, you don't need to second-guess yourself about indexes that are present in development but missing in production.
|
22
|
+
Db\_leftovers gives you confidence that your database matches a definition that is easy to read and checked into version control.
|
16
23
|
|
17
24
|
At present db\_leftovers supports PostgreSQL and MySQL, although since MySQL doesn't support index WHERE clauses or CHECK constraints, using that functionality will raise errors. (If you need to share the same definitions across Postgres and MySQL, you can run arbitrary Ruby code inside the DSL to avoid defining unsupported objects when run against MySQL.)
|
18
25
|
|
19
26
|
Configuration File
|
20
27
|
------------------
|
21
28
|
|
22
|
-
db\_leftovers reads a file named `config/db_leftovers.rb` to find out which indexes and constraints you want in your database. This file is a DSL implemented in Ruby, sort of like `config/routes.rb`.
|
29
|
+
db\_leftovers reads a file named `config/db_leftovers.rb` to find out which indexes and constraints you want in your database. This file is a DSL implemented in Ruby, sort of like `config/routes.rb`. It should look something like this:
|
30
|
+
|
31
|
+
DBLeftovers::Definition.define do
|
32
|
+
|
33
|
+
table :users do
|
34
|
+
index :email, :unique => true
|
35
|
+
check :registered_at_unless_guest, "role_name = 'guest' OR registered_at IS NOT NULL"
|
36
|
+
end
|
37
|
+
|
38
|
+
foreign_key :orders, :users
|
39
|
+
|
40
|
+
# . .
|
41
|
+
end
|
42
|
+
|
43
|
+
Within the DSL file, the following methods are supported:
|
23
44
|
|
24
45
|
### index(table\_name, columns, [opts])
|
25
46
|
|
26
|
-
This ensures that you have an index on the given table and column(s). The `columns` parameter can be either a string or a list of strings. Opts is a hash with the following possible keys:
|
47
|
+
This ensures that you have an index on the given table and column(s). The `columns` parameter can be either a string/symbol or a list of strings/symbols. Opts is a hash with the following possible keys:
|
27
48
|
|
28
49
|
* `:name` The name of the index. Defaults to `index_`*table\_name*`_on_`*column\_names*, like the `add_index` method from Rails migrations.
|
29
50
|
|
@@ -60,7 +81,7 @@ With implicit column names:
|
|
60
81
|
foreign_key :books, :co_author_id, :authors
|
61
82
|
foreign_key :books, :co_author_id, :authors, :on_delete => :cascade
|
62
83
|
|
63
|
-
### check(
|
84
|
+
### check(on\_table, constraint\_name, expression)
|
64
85
|
|
65
86
|
This ensures that you have a CHECK constraint on the given table with the given name and expression.
|
66
87
|
All parameters are strings or symbols.
|
@@ -99,7 +120,7 @@ and want books to have at least 100 pages, you can change your config file to sa
|
|
99
120
|
and db\_leftovers will notice the changed expression. It will drop and re-add the constraint.
|
100
121
|
|
101
122
|
One caveat, however: we pull the current expression from the database, and sometimes Postgres does things like
|
102
|
-
add type conversions. If instance, suppose you said `check :users, :email_length, 'LENGTH(email) > 2'`.
|
123
|
+
add type conversions and extra parentheses. If instance, suppose you said `check :users, :email_length, 'LENGTH(email) > 2'`.
|
103
124
|
The second time you run db\_leftovers, it will read the expression from Postgres and get `LENGTH((email)::text) > 2`,
|
104
125
|
and so it will drop and re-create the constraint.
|
105
126
|
It will drop and re-create it every time you run the rake task.
|
@@ -118,12 +139,33 @@ or
|
|
118
139
|
|
119
140
|
|
120
141
|
|
142
|
+
Capistrano Integration
|
143
|
+
----------------------
|
144
|
+
|
145
|
+
I recommend running `rake db:migrate` any time you deploy, and then running `rake db:leftovers` after that. Here is what you need in your `config/deploy.rb` to make that happen:
|
146
|
+
|
147
|
+
set :rails_env, "production"
|
148
|
+
|
149
|
+
namespace :db do
|
150
|
+
desc "Set up constraints and indexes"
|
151
|
+
task :leftovers do
|
152
|
+
run("cd #{deploy_to}/current && bundle exec rake db:leftovers RAILS_ENV=#{rails_env}")
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
after :deploy, 'deploy:migrate'
|
157
|
+
after 'deploy:migrate', 'db:leftovers'
|
158
|
+
|
159
|
+
You could also change this code to *not* run migrations after each deploy, if you like. But in that case I'd recommend not running db:leftovers until after the latest migrations (if any), since new entries in the DSL are likely to reference newly-created tables/columns.
|
160
|
+
|
161
|
+
|
162
|
+
|
121
163
|
Known Issues
|
122
164
|
------------
|
123
165
|
|
124
|
-
*
|
166
|
+
* Multi-column foreign keys are not supported. This shouldn't be a problem for a Rails project, unless you are using a legacy database. If you need this functionality, let me know and I'll look into adding it.
|
125
167
|
|
126
|
-
|
168
|
+
* It'd be nice to have another Rake task that will read your database and generate a new `db_leftovers.rb` file, to help people migrating existing projects that already have lots of tables.
|
127
169
|
|
128
170
|
|
129
171
|
Contributing to db\_leftovers
|
data/TODO
CHANGED
@@ -1,2 +1,11 @@
|
|
1
|
-
-
|
1
|
+
- It'd be nice to have another Rake task that will read your database and generate a new `db_leftovers.rb` file, to help people migrating existing projects that already have lots of tables.
|
2
|
+
|
3
|
+
- Support multi-column foreign keys.
|
4
|
+
|
5
|
+
- Support custom names for foreign keys. (But then watch out excluding these from the list of indexes on MySQL!)
|
6
|
+
|
7
|
+
- Support CREATE INDEX CONCURRENTLY on Postgres as an option to Rake or the `define` call.
|
8
|
+
|
9
|
+
- Support NOT VALID for Postgres 9.1+ foreign keys and Postgres 9.2+ CHECK constraints.
|
10
|
+
|
2
11
|
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
1.0.
|
1
|
+
1.0.1
|
data/db_leftovers.gemspec
CHANGED
@@ -2,12 +2,12 @@ module DBLeftovers
|
|
2
2
|
|
3
3
|
class MysqlDatabaseInterface < GenericDatabaseInterface
|
4
4
|
|
5
|
-
def initialize(conn=nil)
|
5
|
+
def initialize(conn=nil, database_name=nil)
|
6
6
|
@conn = conn || ActiveRecord::Base.connection
|
7
|
+
@db_name = database_name || ActiveRecord::Base.configurations[Rails.env]['database']
|
7
8
|
end
|
8
9
|
|
9
10
|
def lookup_all_indexes
|
10
|
-
# TODO: Constrain it to the database for the current Rails project:
|
11
11
|
ret = {}
|
12
12
|
@conn.select_values("SHOW TABLES").each do |table_name|
|
13
13
|
indexes = {}
|
@@ -36,7 +36,6 @@ module DBLeftovers
|
|
36
36
|
|
37
37
|
def lookup_all_foreign_keys
|
38
38
|
# TODO: Support multi-column foreign keys:
|
39
|
-
# TODO: Constrain it to the database for the current Rails project:
|
40
39
|
ret = {}
|
41
40
|
sql = <<-EOQ
|
42
41
|
SELECT c.constraint_name,
|
@@ -49,6 +48,7 @@ module DBLeftovers
|
|
49
48
|
information_schema.key_column_usage k
|
50
49
|
WHERE c.constraint_schema = k.constraint_schema
|
51
50
|
AND c.constraint_name = k.constraint_name
|
51
|
+
AND c.constraint_schema IN (#{target_databases_quoted})
|
52
52
|
EOQ
|
53
53
|
@conn.select_rows(sql).each do |constr_name, from_table, from_column, to_table, to_column, del_type|
|
54
54
|
del_type = case del_type
|
@@ -79,5 +79,16 @@ module DBLeftovers
|
|
79
79
|
execute_sql %{ALTER TABLE #{from_table} DROP FOREIGN KEY #{constraint_name}}
|
80
80
|
end
|
81
81
|
|
82
|
+
private
|
83
|
+
|
84
|
+
def target_databases_quoted
|
85
|
+
case @db_name
|
86
|
+
when Array
|
87
|
+
@db_name.map{|x| "'#{x}'"}.join(", ")
|
88
|
+
else
|
89
|
+
"'#{@db_name}'"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
82
93
|
end
|
83
94
|
end
|
@@ -7,8 +7,6 @@ module DBLeftovers
|
|
7
7
|
end
|
8
8
|
|
9
9
|
def lookup_all_indexes
|
10
|
-
# TODO: Constrain it to the database for the current Rails project:
|
11
|
-
# (current_database(), current_schema())
|
12
10
|
ret = {}
|
13
11
|
sql = <<-EOQ
|
14
12
|
SELECT ix.indexrelid,
|
@@ -58,8 +56,6 @@ module DBLeftovers
|
|
58
56
|
# confdeltype: a=nil, c=cascade, n=null
|
59
57
|
ret = {}
|
60
58
|
# TODO: Support multi-column foreign keys:
|
61
|
-
# TODO: Constrain it to the database for the current Rails project:
|
62
|
-
# (current_database(), current_schema())
|
63
59
|
sql = <<-EOQ
|
64
60
|
SELECT c.conname,
|
65
61
|
t1.relname,
|
@@ -103,8 +99,6 @@ module DBLeftovers
|
|
103
99
|
end
|
104
100
|
|
105
101
|
def lookup_all_constraints
|
106
|
-
# TODO: Constrain it to the database for the current Rails project:
|
107
|
-
# (current_database(), current_schema())
|
108
102
|
ret = {}
|
109
103
|
sql = <<-EOQ
|
110
104
|
SELECT c.conname,
|
data/spec/mysql_spec.rb
CHANGED
@@ -18,9 +18,9 @@ describe DBLeftovers::MysqlDatabaseInterface do
|
|
18
18
|
it "WARN: Skipping MySQL tests because no database found. Use spec/config/database.yml to configure one."
|
19
19
|
else
|
20
20
|
before do
|
21
|
-
|
21
|
+
y = test_database_yml('mysql')
|
22
22
|
@conn = test_db_connection(nil, y)
|
23
|
-
@db = DBLeftovers::MysqlDatabaseInterface.new(@conn)
|
23
|
+
@db = DBLeftovers::MysqlDatabaseInterface.new(@conn, y['database'])
|
24
24
|
drop_all_mysql_tables(@conn, y['database'])
|
25
25
|
fresh_tables(@conn, y['database'])
|
26
26
|
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: 1.0.
|
4
|
+
version: 1.0.1
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -147,7 +147,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
147
147
|
version: '0'
|
148
148
|
segments:
|
149
149
|
- 0
|
150
|
-
hash:
|
150
|
+
hash: -672806461406731014
|
151
151
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
152
152
|
none: false
|
153
153
|
requirements:
|