db_leftovers 0.6.0 → 0.7.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/.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:
|