aspgems-redhillonrails_core 2.0.0.beta1
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/CHANGELOG +194 -0
- data/Gemfile +3 -0
- data/MIT-LICENSE +20 -0
- data/README +161 -0
- data/Rakefile +59 -0
- data/init.rb +1 -0
- data/lib/redhillonrails_core.rb +46 -0
- data/lib/redhillonrails_core/active_record/base.rb +45 -0
- data/lib/redhillonrails_core/active_record/connection_adapters/abstract/column.rb +14 -0
- data/lib/redhillonrails_core/active_record/connection_adapters/abstract/foreign_key_definition.rb +63 -0
- data/lib/redhillonrails_core/active_record/connection_adapters/abstract/index_definition.rb +11 -0
- data/lib/redhillonrails_core/active_record/connection_adapters/abstract/schema_statements.rb +23 -0
- data/lib/redhillonrails_core/active_record/connection_adapters/abstract/table_definition.rb +27 -0
- data/lib/redhillonrails_core/active_record/connection_adapters/abstract_adapter.rb +84 -0
- data/lib/redhillonrails_core/active_record/connection_adapters/mysql_adapter.rb +131 -0
- data/lib/redhillonrails_core/active_record/connection_adapters/mysql_column.rb +8 -0
- data/lib/redhillonrails_core/active_record/connection_adapters/postgresql_adapter.rb +131 -0
- data/lib/redhillonrails_core/active_record/connection_adapters/sqlite3_adapter.rb +115 -0
- data/lib/redhillonrails_core/active_record/schema.rb +25 -0
- data/lib/redhillonrails_core/active_record/schema_dumper.rb +75 -0
- data/lib/redhillonrails_core/version.rb +3 -0
- data/lib/tasks/db/comments.rake +9 -0
- data/redhillonrails_core.gemspec +27 -0
- data/spec/connections/mysql/connection.rb +18 -0
- data/spec/connections/mysql2/connection.rb +18 -0
- data/spec/connections/postgresql/connection.rb +15 -0
- data/spec/connections/sqlite3/connection.rb +14 -0
- data/spec/foreign_key_definition_spec.rb +21 -0
- data/spec/foreign_key_spec.rb +100 -0
- data/spec/index_definition_spec.rb +145 -0
- data/spec/index_spec.rb +67 -0
- data/spec/models/comment.rb +5 -0
- data/spec/models/post.rb +6 -0
- data/spec/models/user.rb +5 -0
- data/spec/schema/schema.rb +21 -0
- data/spec/schema_dumper_spec.rb +138 -0
- data/spec/spec_helper.rb +16 -0
- data/spec/support/reference.rb +66 -0
- metadata +159 -0
@@ -0,0 +1,115 @@
|
|
1
|
+
module RedhillonrailsCore
|
2
|
+
module ActiveRecord
|
3
|
+
module ConnectionAdapters
|
4
|
+
module Sqlite3Adapter
|
5
|
+
def self.included(base)
|
6
|
+
base.class_eval do
|
7
|
+
alias_method_chain :tables, :redhillonrails_core
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def move_table(from, to, options = {}, &block) #:nodoc:
|
12
|
+
copy_table(from, to, options, &block)
|
13
|
+
drop_table(from, options)
|
14
|
+
end
|
15
|
+
|
16
|
+
def add_foreign_key(from_table_name, from_column_names, to_table_name, to_column_names, options = {})
|
17
|
+
initialize_sqlite3_foreign_key_table
|
18
|
+
from_column_names = Array(from_column_names)
|
19
|
+
to_column_names = Array(to_column_names)
|
20
|
+
fk_name = options[:name] || ["fk", from_table_name, *to_column_names].join("_")
|
21
|
+
|
22
|
+
columns = %w(name from_table_name from_column_names to_table_name to_column_names)
|
23
|
+
values = [fk_name, from_table_name, from_column_names.join(","), to_table_name, to_column_names.join(",")]
|
24
|
+
|
25
|
+
quoted_values = values.map { |x| quote(x.to_s) }.join(",")
|
26
|
+
|
27
|
+
# TODO: support options
|
28
|
+
|
29
|
+
insert <<-SQL
|
30
|
+
INSERT INTO #{sqlite3_foreign_key_table}(#{quoted_columns(columns)})
|
31
|
+
VALUES (#{quoted_values})
|
32
|
+
SQL
|
33
|
+
end
|
34
|
+
|
35
|
+
def remove_foreign_key(table_name, foreign_key_name, options = {})
|
36
|
+
return if options[:temporary] == true
|
37
|
+
initialize_sqlite3_foreign_key_table
|
38
|
+
|
39
|
+
rows_deleted = delete <<-SQL
|
40
|
+
DELETE FROM #{sqlite3_foreign_key_table}
|
41
|
+
WHERE #{quote_column_name("name")} = #{quote(foreign_key_name.to_s)}
|
42
|
+
AND #{quote_column_name("from_table_name")} = #{quote(table_name.to_s)}
|
43
|
+
SQL
|
44
|
+
|
45
|
+
if rows_deleted != 1
|
46
|
+
raise ActiveRecord::ActiveRecordError, "Foreign-key '#{foreign_key_name}' on table '#{table_name}' not found"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def tables_with_redhillonrails_core(name = nil)
|
51
|
+
tables_without_redhillonrails_core.reject{ |name| name == sqlite3_foreign_key_table }
|
52
|
+
end
|
53
|
+
|
54
|
+
def foreign_keys(table_name, name = nil)
|
55
|
+
load_foreign_keys("from_table_name", table_name, name)
|
56
|
+
end
|
57
|
+
|
58
|
+
def reverse_foreign_keys(table_name, name = nil)
|
59
|
+
load_foreign_keys("to_table_name", table_name, name)
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def quoted_columns(columns)
|
65
|
+
columns.map { |x| quote_column_name(x) }.join(",")
|
66
|
+
end
|
67
|
+
|
68
|
+
def sqlite3_foreign_key_table
|
69
|
+
"sqlite3_foreign_keys"
|
70
|
+
end
|
71
|
+
|
72
|
+
def initialize_sqlite3_foreign_key_table
|
73
|
+
unless sqlite3_foreign_key_table_exists?
|
74
|
+
create_table(sqlite3_foreign_key_table, :id => false) do |t|
|
75
|
+
t.string "name", :null => false
|
76
|
+
t.string "from_table_name", :null => false
|
77
|
+
t.string "from_column_names", :null => false
|
78
|
+
t.string "to_table_name", :null => false
|
79
|
+
t.string "to_column_names", :null => false
|
80
|
+
end
|
81
|
+
add_index(sqlite3_foreign_key_table, "name", :unique => true)
|
82
|
+
add_index(sqlite3_foreign_key_table, "from_table_name", :unique => false)
|
83
|
+
add_index(sqlite3_foreign_key_table, "to_table_name", :unique => false)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def sqlite3_foreign_key_table_exists?
|
88
|
+
tables_without_redhillonrails_core.detect { |name| name == sqlite3_foreign_key_table }
|
89
|
+
end
|
90
|
+
|
91
|
+
def load_foreign_keys(discriminating_column, table_name, name = nil)
|
92
|
+
if sqlite3_foreign_key_table_exists?
|
93
|
+
rows = select_all(<<-SQL, name)
|
94
|
+
SELECT *
|
95
|
+
FROM #{sqlite3_foreign_key_table}
|
96
|
+
WHERE #{quote_column_name(discriminating_column)} = #{quote(table_name.to_s)}
|
97
|
+
SQL
|
98
|
+
else
|
99
|
+
rows = []
|
100
|
+
end
|
101
|
+
|
102
|
+
rows.map do |row|
|
103
|
+
ForeignKeyDefinition.new(
|
104
|
+
row["name"],
|
105
|
+
row["from_table_name"], row["from_column_names"].split(","),
|
106
|
+
row["to_table_name"], row["to_column_names"].split(",")
|
107
|
+
)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module RedhillonrailsCore::ActiveRecord
|
2
|
+
module Schema
|
3
|
+
def self.included(base)
|
4
|
+
base.extend(ClassMethods)
|
5
|
+
end
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
def self.extended(base)
|
9
|
+
class << base
|
10
|
+
attr_accessor :defining
|
11
|
+
alias :defining? :defining
|
12
|
+
|
13
|
+
alias_method_chain :define, :redhillonrails_core
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def define_with_redhillonrails_core(info={}, &block)
|
18
|
+
self.defining = true
|
19
|
+
define_without_redhillonrails_core(info, &block)
|
20
|
+
ensure
|
21
|
+
self.defining = false
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module RedhillonrailsCore::ActiveRecord
|
2
|
+
module SchemaDumper
|
3
|
+
def self.included(base)
|
4
|
+
base.class_eval do
|
5
|
+
private
|
6
|
+
alias_method_chain :tables, :redhillonrails_core
|
7
|
+
alias_method_chain :indexes, :redhillonrails_core
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def tables_with_redhillonrails_core(stream)
|
14
|
+
@foreign_keys = StringIO.new
|
15
|
+
begin
|
16
|
+
tables_without_redhillonrails_core(stream)
|
17
|
+
@foreign_keys.rewind
|
18
|
+
stream.print @foreign_keys.read
|
19
|
+
views(stream)
|
20
|
+
ensure
|
21
|
+
@foreign_keys = nil
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def indexes_with_redhillonrails_core(table, stream)
|
26
|
+
if (indexes = @connection.indexes(table)).any?
|
27
|
+
add_index_statements = indexes.map do |index|
|
28
|
+
statement_parts = [('add_index ' + index.table.inspect)]
|
29
|
+
statement_parts << index.columns.inspect
|
30
|
+
statement_parts << (':name => ' + index.name.inspect)
|
31
|
+
statement_parts << ':unique => true' if index.unique
|
32
|
+
# This only used in postgresql
|
33
|
+
statement_parts << ':case_sensitive => false' unless index.case_sensitive?
|
34
|
+
|
35
|
+
if index.respond_to?(:lengths)
|
36
|
+
index_lengths = index.lengths.compact if index.lengths.is_a?(Array)
|
37
|
+
statement_parts << (':length => ' + Hash[*index.columns.zip(index.lengths).flatten].inspect) if index_lengths.present?
|
38
|
+
end
|
39
|
+
|
40
|
+
' ' + statement_parts.join(', ')
|
41
|
+
end
|
42
|
+
|
43
|
+
stream.puts add_index_statements.sort.join("\n")
|
44
|
+
stream.puts
|
45
|
+
end
|
46
|
+
|
47
|
+
foreign_keys(table, @foreign_keys)
|
48
|
+
end
|
49
|
+
|
50
|
+
def foreign_keys(table, stream)
|
51
|
+
foreign_keys = @connection.foreign_keys(table)
|
52
|
+
|
53
|
+
foreign_keys.sort! do |a, b|
|
54
|
+
a.column_names.sort <=> b.column_names.sort
|
55
|
+
end
|
56
|
+
|
57
|
+
foreign_keys.each do |foreign_key|
|
58
|
+
stream.print " "
|
59
|
+
stream.print foreign_key.to_dump
|
60
|
+
stream.puts
|
61
|
+
end
|
62
|
+
stream.puts unless foreign_keys.empty?
|
63
|
+
end
|
64
|
+
|
65
|
+
def views(stream)
|
66
|
+
views = @connection.views
|
67
|
+
views.each do |view_name|
|
68
|
+
definition = @connection.view_definition(view_name)
|
69
|
+
stream.print " create_view #{view_name.inspect}, #{definition.inspect}"
|
70
|
+
stream.puts
|
71
|
+
end
|
72
|
+
stream.puts unless views.empty?
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
namespace :db do
|
2
|
+
desc "Describe all the tables in the database by reading the table comments"
|
3
|
+
task :comments => :environment do
|
4
|
+
ActiveRecord::Base.connection.tables.sort.each do |table_name|
|
5
|
+
comment = ActiveRecord::Base.connection.table_comment(table_name)
|
6
|
+
puts "#{table_name} - #{comment}"
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "redhillonrails_core/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "aspgems-redhillonrails_core"
|
7
|
+
s.version = RedhillonrailsCore::VERSION.dup
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["Michał Łomnicki", "Paco Guzmán"]
|
10
|
+
s.email = ["michal.lomnicki@gmail.com", "fjguzman@aspgems.com"]
|
11
|
+
s.homepage = "https://github.com/aspgems/redhillonrails_core"
|
12
|
+
s.summary = "Adds support in ActiveRecord for foreign_keys, complex indexes and other database-related stuff"
|
13
|
+
s.description = "Adds support in ActiveRecord for foreign_keys, complex indexes and other database-related stuff. Easily create foreign_keys, complex indexes and views."
|
14
|
+
|
15
|
+
s.files = `git ls-files`.split("\n")
|
16
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
17
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
18
|
+
s.require_paths = ["lib"]
|
19
|
+
|
20
|
+
s.add_dependency("activerecord", ">= 2")
|
21
|
+
|
22
|
+
s.add_development_dependency("rspec", "~> 2.5.0")
|
23
|
+
s.add_development_dependency("pg")
|
24
|
+
s.add_development_dependency("mysql")
|
25
|
+
s.add_development_dependency("mysql2")
|
26
|
+
s.add_development_dependency("sqlite3-ruby", "~> 1.3.1")
|
27
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
print "Using MySQL\n"
|
2
|
+
require 'logger'
|
3
|
+
|
4
|
+
ActiveRecord::Base.logger = Logger.new("debug.log")
|
5
|
+
|
6
|
+
ActiveRecord::Base.configurations = {
|
7
|
+
'redhillonrails' => {
|
8
|
+
:adapter => 'mysql',
|
9
|
+
:database => 'rh_core_unittest',
|
10
|
+
:username => 'rh_core',
|
11
|
+
:encoding => 'utf8',
|
12
|
+
:socket => '/var/run/mysqld/mysqld.sock',
|
13
|
+
:min_messages => 'warning'
|
14
|
+
}
|
15
|
+
|
16
|
+
}
|
17
|
+
|
18
|
+
ActiveRecord::Base.establish_connection 'redhillonrails'
|
@@ -0,0 +1,18 @@
|
|
1
|
+
print "Using MySQL2\n"
|
2
|
+
require 'logger'
|
3
|
+
|
4
|
+
ActiveRecord::Base.logger = Logger.new("debug.log")
|
5
|
+
|
6
|
+
ActiveRecord::Base.configurations = {
|
7
|
+
'redhillonrails' => {
|
8
|
+
:adapter => 'mysql2',
|
9
|
+
:database => 'rh_core_unittest',
|
10
|
+
:username => 'rh_core',
|
11
|
+
:encoding => 'utf8',
|
12
|
+
:socket => '/var/run/mysqld/mysqld.sock',
|
13
|
+
:min_messages => 'warning'
|
14
|
+
}
|
15
|
+
|
16
|
+
}
|
17
|
+
|
18
|
+
ActiveRecord::Base.establish_connection 'redhillonrails'
|
@@ -0,0 +1,15 @@
|
|
1
|
+
print "Using PostgreSQL\n"
|
2
|
+
require 'logger'
|
3
|
+
|
4
|
+
ActiveRecord::Base.logger = Logger.new("debug.log")
|
5
|
+
|
6
|
+
ActiveRecord::Base.configurations = {
|
7
|
+
'redhillonrails' => {
|
8
|
+
:adapter => 'postgresql',
|
9
|
+
:database => 'redhillonrails_core_test',
|
10
|
+
:min_messages => 'warning'
|
11
|
+
}
|
12
|
+
|
13
|
+
}
|
14
|
+
|
15
|
+
ActiveRecord::Base.establish_connection 'redhillonrails'
|
@@ -0,0 +1,14 @@
|
|
1
|
+
print "Using SQLite3\n"
|
2
|
+
require 'logger'
|
3
|
+
|
4
|
+
ActiveRecord::Base.logger = Logger.new("debug.log")
|
5
|
+
|
6
|
+
ActiveRecord::Base.configurations = {
|
7
|
+
'redhillonrails' => {
|
8
|
+
:adapter => 'sqlite3',
|
9
|
+
:database => File.expand_path(File.dirname(__FILE__) + 'redhillonrails_core.db')
|
10
|
+
}
|
11
|
+
|
12
|
+
}
|
13
|
+
|
14
|
+
ActiveRecord::Base.establish_connection 'redhillonrails'
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
require 'models/user'
|
4
|
+
|
5
|
+
describe "Foreign Key definition" do
|
6
|
+
|
7
|
+
let(:definition) { RedhillonrailsCore::ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new("posts_user_fkey", :posts, :user, :users, :id) }
|
8
|
+
|
9
|
+
it "it is dumped to sql with quoted values" do
|
10
|
+
definition.to_sql.should == %Q{CONSTRAINT posts_user_fkey FOREIGN KEY (#{quote_column_name('user')}) REFERENCES #{quote_table_name('users')} (#{quote_column_name('id')})}
|
11
|
+
end
|
12
|
+
|
13
|
+
def quote_table_name(table)
|
14
|
+
ActiveRecord::Base.connection.quote_table_name(table)
|
15
|
+
end
|
16
|
+
|
17
|
+
def quote_column_name(column)
|
18
|
+
ActiveRecord::Base.connection.quote_column_name(column)
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
require 'models/user'
|
4
|
+
require 'models/post'
|
5
|
+
require 'models/comment'
|
6
|
+
|
7
|
+
describe "Foreign Key" do
|
8
|
+
|
9
|
+
let(:migration) { ::ActiveRecord::Migration }
|
10
|
+
|
11
|
+
context "when is added", "posts(author_id)" do
|
12
|
+
|
13
|
+
before(:each) do
|
14
|
+
add_foreign_key(:posts, :author_id, :users, :id, :on_update => :cascade, :on_delete => :restrict)
|
15
|
+
end
|
16
|
+
|
17
|
+
after(:each) do
|
18
|
+
fk = Post.foreign_keys.detect { |fk| fk.column_names == %w[author_id] }
|
19
|
+
remove_foreign_key(:posts, fk.name)
|
20
|
+
end
|
21
|
+
|
22
|
+
it "references users(id)" do
|
23
|
+
Post.should reference(:users, :id).on(:author_id)
|
24
|
+
end
|
25
|
+
|
26
|
+
it "cascades on update" do
|
27
|
+
Post.should reference(:users).on_update(:cascade)
|
28
|
+
end
|
29
|
+
|
30
|
+
it "restricts on delete" do
|
31
|
+
Post.should reference(:users).on_delete(:restrict)
|
32
|
+
end
|
33
|
+
|
34
|
+
it "is available in Post.foreign_keys" do
|
35
|
+
Post.foreign_keys.collect(&:column_names).should include(%w[author_id])
|
36
|
+
end
|
37
|
+
|
38
|
+
it "is available in User.reverse_foreign_keys" do
|
39
|
+
User.reverse_foreign_keys.collect(&:column_names).should include(%w[author_id])
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
context "when is dropped", "comments(post_id)" do
|
45
|
+
|
46
|
+
let(:foreign_key_name) { Comment.foreign_keys.detect { |definition| definition.column_names == %w[post_id] }.name }
|
47
|
+
|
48
|
+
before(:each) do
|
49
|
+
remove_foreign_key(:comments, foreign_key_name)
|
50
|
+
end
|
51
|
+
|
52
|
+
after(:each) do
|
53
|
+
add_foreign_key(:comments, :post_id, :posts, :id)
|
54
|
+
end
|
55
|
+
|
56
|
+
it "doesn't reference posts(id)" do
|
57
|
+
Comment.should_not reference(:posts).on(:post_id)
|
58
|
+
end
|
59
|
+
|
60
|
+
it "is no longer available in Post.foreign_keys" do
|
61
|
+
Comment.foreign_keys.collect(&:column_names).should_not include(%w[post_id])
|
62
|
+
end
|
63
|
+
|
64
|
+
it "is no longer available in User.reverse_foreign_keys" do
|
65
|
+
Post.reverse_foreign_keys.collect(&:column_names).should_not include(%w[post_id])
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
context "when referencing column and column is removed" do
|
71
|
+
|
72
|
+
let(:foreign_key_name) { Comment.foreign_keys.detect { |definition| definition.column_names == %w[post_id] }.name }
|
73
|
+
|
74
|
+
it "should remove foreign keys" do
|
75
|
+
remove_foreign_key(:comments, foreign_key_name)
|
76
|
+
Post.reverse_foreign_keys.collect { |fk| fk.column_names == %w[post_id] && fk.table_name == "comments" }.should be_empty
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
|
81
|
+
protected
|
82
|
+
def add_foreign_key(*args)
|
83
|
+
migration.suppress_messages do
|
84
|
+
migration.add_foreign_key(*args)
|
85
|
+
end
|
86
|
+
User.reset_column_information
|
87
|
+
Post.reset_column_information
|
88
|
+
Comment.reset_column_information
|
89
|
+
end
|
90
|
+
|
91
|
+
def remove_foreign_key(*args)
|
92
|
+
migration.suppress_messages do
|
93
|
+
migration.remove_foreign_key(*args)
|
94
|
+
end
|
95
|
+
User.reset_column_information
|
96
|
+
Post.reset_column_information
|
97
|
+
Comment.reset_column_information
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|