db_leftovers 0.6.0 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +0 -0
- data/Gemfile +0 -0
- data/Gemfile.lock +0 -0
- data/LICENSE.txt +0 -0
- data/README.html +21 -49
- data/README.md +0 -0
- data/Rakefile +0 -0
- data/TODO +12 -2
- data/VERSION +1 -1
- data/db_leftovers.gemspec +2 -2
- data/lib/db_leftovers/constraint.rb +11 -1
- data/lib/db_leftovers/database_interface.rb +40 -9
- data/lib/db_leftovers/definition.rb +0 -0
- data/lib/db_leftovers/dsl.rb +28 -8
- data/lib/db_leftovers/foreign_key.rb +6 -1
- data/lib/db_leftovers/index.rb +0 -0
- data/lib/db_leftovers/table_dsl.rb +0 -0
- data/lib/db_leftovers.rb +0 -0
- data/lib/tasks/leftovers.rake +0 -0
- data/spec/db_leftovers_spec.rb +28 -4
- data/spec/spec_helper.rb +0 -0
- metadata +3 -3
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
|
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
|
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 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
|
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
|
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 => 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
|
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
|
-
|
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, :
|
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 > 0'
|
52
|
+
foreign_key :pages, :book_id, :books, :id, :cascade => true
|
62
53
|
</code></pre>
|
63
54
|
|
64
|
-
<h3>table
|
55
|
+
<h3>table table_name, &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 > 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
|
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
|
100
|
-
Right now it only checks for existence/non-existence
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
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
|
-
|
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
|
-
-
|
6
|
-
|
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.
|
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.
|
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-
|
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
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
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,
|
63
|
-
|
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
|
data/lib/db_leftovers/dsl.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
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.
|
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
|
data/lib/db_leftovers/index.rb
CHANGED
File without changes
|
File without changes
|
data/lib/db_leftovers.rb
CHANGED
File without changes
|
data/lib/tasks/leftovers.rake
CHANGED
File without changes
|
data/spec/db_leftovers_spec.rb
CHANGED
@@ -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.
|
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.
|
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-
|
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:
|
142
|
+
hash: 258395923
|
143
143
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
144
144
|
none: false
|
145
145
|
requirements:
|