Hi, While I was hacking ovirt-server, I have found that it's currently restricted to Postgres DB. Even if I like postgres for serious work on a server, I really prefer to hack/dev locally on a Sqlite or MySQL DB. I have googled on rails in order to find a good answer for the "foreign key problem" which forces OVirt to stay on pg. I have found a plugin on this particular problem, named foreigner : http://github.com/matthuhiggins/foreigner It provides a rails' syntax to common operation on them. For instance : add_foreign_key(from_table, to_table, options) remove_foreign_key(from_table, options) foreign_keys(table_name) I really like to keep my development computer to run as fast as possible (ie without a database service), so I have even tried to integrate this plugin into OVirt, in a transparent way for existing db. After a couple of patch on the plugin itself and some hack on migrations, it seems that I manage to do it. The 3 patchs following this email allows OVirt to support multiple open source database without losing functionality, without losing existing databases and with a small readability enhancement on db migrations. What do you think of it ? Feel free to contact me by email, on this list or by irc (nick: Coren`) Regards, -- Michel Loiseleur
Michel Loiseleur
2009-Sep-28 20:08 UTC
[Ovirt-devel] [PATCH 1/3] Add foreigner plugin from Matthu Higgins, allowing to manage foreign key in a multi-db way.
Signed-off-by: Michel Loiseleur <mloiseleur at linagora.com> --- src/vendor/plugins/foreigner/MIT-LICENSE | 20 +++ src/vendor/plugins/foreigner/README | 106 +++++++++++++ src/vendor/plugins/foreigner/Rakefile | 23 +++ src/vendor/plugins/foreigner/foreigner.gemspec | 45 ++++++ src/vendor/plugins/foreigner/init.rb | 1 + src/vendor/plugins/foreigner/install.rb | 1 + src/vendor/plugins/foreigner/lib/foreigner.rb | 21 +++ .../abstract/schema_definitions.rb | 155 ++++++++++++++++++++ .../abstract/schema_statements.rb | 75 ++++++++++ .../foreigner/connection_adapters/mysql_adapter.rb | 43 ++++++ .../connection_adapters/postgresql_adapter.rb | 46 ++++++ .../lib/foreigner/connection_adapters/sql_2003.rb | 58 ++++++++ .../foreigner/lib/foreigner/schema_dumper.rb | 45 ++++++ .../plugins/foreigner/tasks/foreigner_tasks.rake | 4 + src/vendor/plugins/foreigner/test/helper.rb | 8 + .../plugins/foreigner/test/mysql_adapter_test.rb | 83 +++++++++++ src/vendor/plugins/foreigner/uninstall.rb | 1 + 17 files changed, 735 insertions(+), 0 deletions(-) create mode 100644 src/vendor/plugins/foreigner/MIT-LICENSE create mode 100644 src/vendor/plugins/foreigner/README create mode 100644 src/vendor/plugins/foreigner/Rakefile create mode 100644 src/vendor/plugins/foreigner/foreigner.gemspec create mode 100644 src/vendor/plugins/foreigner/init.rb create mode 100644 src/vendor/plugins/foreigner/install.rb create mode 100644 src/vendor/plugins/foreigner/lib/foreigner.rb create mode 100644 src/vendor/plugins/foreigner/lib/foreigner/connection_adapters/abstract/schema_definitions.rb create mode 100644 src/vendor/plugins/foreigner/lib/foreigner/connection_adapters/abstract/schema_statements.rb create mode 100644 src/vendor/plugins/foreigner/lib/foreigner/connection_adapters/mysql_adapter.rb create mode 100644 src/vendor/plugins/foreigner/lib/foreigner/connection_adapters/postgresql_adapter.rb create mode 100644 src/vendor/plugins/foreigner/lib/foreigner/connection_adapters/sql_2003.rb create mode 100644 src/vendor/plugins/foreigner/lib/foreigner/schema_dumper.rb create mode 100644 src/vendor/plugins/foreigner/tasks/foreigner_tasks.rake create mode 100644 src/vendor/plugins/foreigner/test/helper.rb create mode 100644 src/vendor/plugins/foreigner/test/mysql_adapter_test.rb create mode 100644 src/vendor/plugins/foreigner/uninstall.rb diff --git a/src/vendor/plugins/foreigner/MIT-LICENSE b/src/vendor/plugins/foreigner/MIT-LICENSE new file mode 100644 index 0000000..9376605 --- /dev/null +++ b/src/vendor/plugins/foreigner/MIT-LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2009 [name of plugin creator] + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/src/vendor/plugins/foreigner/README b/src/vendor/plugins/foreigner/README new file mode 100644 index 0000000..4a1a8b5 --- /dev/null +++ b/src/vendor/plugins/foreigner/README @@ -0,0 +1,106 @@ +Foreigner +========+ +Rails does not come with methods to add foreign keys. Foreigner introduces a few +methods to your migrations for adding and removing foreign key constraints. + +Since each adapter implements the API, migrations using Foreigner will continue to +work on databases that do not support foreign keys, such as sqlite3. + +Installation +------------ + +Install as a plugin: + + ruby script/plugin install git://github.com/matthuhiggins/foreigner.git + +Install as a gem by adding the following to environment.rb: + + config.gem "matthuhiggins-foreigner", :lib => "foreigner" + +API +--- + +An adapter implementing the Foreigner API implements three methods. +(Options are documented in connection_adapters/abstract/schema_definitions.rb): + + add_foreign_key(from_table, to_table, options) + remove_foreign_key(from_table, options) + foreign_keys(table_name) + +Example +------- + +The most common use of foreign keys is to reference a table that a model belongs to. +For example, given the following model: + + class Comment < ActiveRecord::Base + belongs_to :post + end + + class Post < ActiveRecord::Base + has_many :comments, :dependent => :delete_all + end + +You should add a foreign key in your migration: + + add_foreign_key(:comments, :posts) + +The :dependent option can be moved from the has_many definition to the foreign key: + + add_foreign_key(:comments, :posts, :dependent => :delete) + +If the column is named article_id instead of post_id, use the :column option: + + add_foreign_key(:comments, :posts, :column => 'article_id') + +Lastly, a name can be specified for the foreign key constraint: + + add_foreign_key(:comments, :posts, :name => 'comment_article_foreign_key') + +Create/Change Table Shorthand +----------------------------- + +Foreigner adds extra behavior to create_table and change_table, which lets you define foreign keys using shorthand. + +Create the comments table with a foreign key to posts: + +create_table :comments do |t| + t.integer :post_id + t.foreign_key :posts +end + +Add a missing foreign key to comments: + +change_table :comments do |t| + t.foreign_key :posts, :dependent => :delete +end + +t.foreign_key accepts the same options as add_foreign_key. + + +Additional t.references option +------------------------------ + +Foreigner extends table.references with the :foreign_key option. Pass true, and the default +foreign key options are used: + +create_table :comments do |t| + t.references :post, :foreign_key => true +end + +An options hash can also be passed. It accepts the same options as add_foreign_key: + +change_table :comments do |t| + t.references :author, :foreign_key => {:dependent => :destroy} +end + +By default, t.references will not generate a foreign key. + +schema.rb +--------- + +Similar to indexes, the foreign keys in your database are automatically dumped to schema.rb. +This allows you to use foreign keys without fighting Rails! + +Copyright (c) 2009 Matthew Higgins, released under the MIT license diff --git a/src/vendor/plugins/foreigner/Rakefile b/src/vendor/plugins/foreigner/Rakefile new file mode 100644 index 0000000..122a501 --- /dev/null +++ b/src/vendor/plugins/foreigner/Rakefile @@ -0,0 +1,23 @@ +require 'rake' +require 'rake/testtask' +require 'rake/rdoctask' + +desc 'Default: run unit tests.' +task :default => :test + +desc 'Test the foreigner plugin.' +Rake::TestTask.new(:test) do |t| + t.libs << 'lib' + t.libs << 'test' + t.pattern = 'test/**/*_test.rb' + t.verbose = true +end + +desc 'Generate documentation for the foreigner plugin.' +Rake::RDocTask.new(:rdoc) do |rdoc| + rdoc.rdoc_dir = 'rdoc' + rdoc.title = 'Foreigner' + rdoc.options << '--line-numbers' << '--inline-source' + rdoc.rdoc_files.include('README') + rdoc.rdoc_files.include('lib/**/*.rb') +end diff --git a/src/vendor/plugins/foreigner/foreigner.gemspec b/src/vendor/plugins/foreigner/foreigner.gemspec new file mode 100644 index 0000000..c988664 --- /dev/null +++ b/src/vendor/plugins/foreigner/foreigner.gemspec @@ -0,0 +1,45 @@ +# -*- encoding: utf-8 -*- + +Gem::Specification.new do |s| + s.name = %q{foreigner} + s.version = "0.2.1" + + s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version+ s.authors = ["Matthew Higgins"] + s.date = %q{2009-09-07} + s.email = %q{developer at matthewhiggins.com} + s.extra_rdoc_files = ["README"] + s.files = %w( + MIT-LICENSE + Rakefile + README + lib/foreigner.rb + lib/foreigner + lib/foreigner/schema_dumper.rb + lib/foreigner/connection_adapters + lib/foreigner/connection_adapters/sql_2003.rb + lib/foreigner/connection_adapters/mysql_adapter.rb + lib/foreigner/connection_adapters/postgresql_adapter.rb + lib/foreigner/connection_adapters/abstract/schema_definitions.rb + lib/foreigner/connection_adapters/abstract/schema_statements.rb + test/helper.rb + test/mysql_adapter_test.rb + ) + s.homepage = "http://github.com/matthuhiggins/foreigner/tree/master" + s.rdoc_options = ["--line-numbers", "--main", "README"] + s.require_paths = %w(lib) + s.rubyforge_project = "foreigner" + s.rubygems_version = "1.3.4" + s.summary = "Foreign keys for Rails migrations" + s.description = "Foreign keys for Rails migrations" + + if s.respond_to? :specification_version then + current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION + s.specification_version = 1 + + if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then + else + end + else + end +end \ No newline at end of file diff --git a/src/vendor/plugins/foreigner/init.rb b/src/vendor/plugins/foreigner/init.rb new file mode 100644 index 0000000..8b33fb9 --- /dev/null +++ b/src/vendor/plugins/foreigner/init.rb @@ -0,0 +1 @@ +require 'foreigner' diff --git a/src/vendor/plugins/foreigner/install.rb b/src/vendor/plugins/foreigner/install.rb new file mode 100644 index 0000000..f7732d3 --- /dev/null +++ b/src/vendor/plugins/foreigner/install.rb @@ -0,0 +1 @@ +# Install hook code here diff --git a/src/vendor/plugins/foreigner/lib/foreigner.rb b/src/vendor/plugins/foreigner/lib/foreigner.rb new file mode 100644 index 0000000..0cccdf5 --- /dev/null +++ b/src/vendor/plugins/foreigner/lib/foreigner.rb @@ -0,0 +1,21 @@ +require 'foreigner/connection_adapters/abstract/schema_statements' +require 'foreigner/connection_adapters/abstract/schema_definitions' +require 'foreigner/connection_adapters/sql_2003' +require 'foreigner/schema_dumper' + +module ActiveRecord + module ConnectionAdapters + include Foreigner::ConnectionAdapters::SchemaStatements + include Foreigner::ConnectionAdapters::SchemaDefinitions + end + + SchemaDumper.class_eval do + include Foreigner::SchemaDumper + end + + Base.class_eval do + if %w(MySQL PostgreSQL).include? connection.adapter_name + require "foreigner/connection_adapters/#{connection.adapter_name.downcase}_adapter" + end + end +end \ No newline at end of file diff --git a/src/vendor/plugins/foreigner/lib/foreigner/connection_adapters/abstract/schema_definitions.rb b/src/vendor/plugins/foreigner/lib/foreigner/connection_adapters/abstract/schema_definitions.rb new file mode 100644 index 0000000..e417c5b --- /dev/null +++ b/src/vendor/plugins/foreigner/lib/foreigner/connection_adapters/abstract/schema_definitions.rb @@ -0,0 +1,155 @@ +module Foreigner + module ConnectionAdapters + class ForeignKeyDefinition < Struct.new(:from_table, :to_table, :options) #:nodoc: + end + + module SchemaDefinitions + def self.included(base) + base::TableDefinition.class_eval do + include Foreigner::ConnectionAdapters::TableDefinition + end + + base::Table.class_eval do + include Foreigner::ConnectionAdapters::Table + end + end + end + + module TableDefinition + class ForeignKey < Struct.new(:base, :to_table, :options) + def to_sql + base.foreign_key_definition(to_table, options) + end + alias to_s :to_sql + end + + def self.included(base) + base.class_eval do + include InstanceMethods + alias_method_chain :references, :foreign_keys + alias_method_chain :to_sql, :foreign_keys + end + end + + module InstanceMethods + # Adds a :foreign_key option to TableDefinition.references. + # If :foreign_key is true, a foreign key constraint is added to the table. + # You can also specify a hash, which is passed as foreign key options. + # + # ===== Examples + # ====== Add goat_id column and a foreign key to the goats table. + # t.references(:goat, :foreign_key => true) + # ====== Add goat_id column and a cascading foreign key to the goats table. + # t.references(:goat, :foreign_key => {:dependent => :delete}) + # + # Note: No foreign key is created if :polymorphic => true is used. + # Note: If no name is specified, the database driver creates one for you! + def references_with_foreign_keys(*args) + options = args.extract_options! + fk_options = options.delete(:foreign_key) + + if fk_options && !options[:polymorphic] + fk_options = {} if fk_options == true + args.each { |to_table| foreign_key(to_table, fk_options) } + end + + references_without_foreign_keys(*(args << options)) + end + + # Defines a foreign key for the table. +to_table+ can be a single Symbol, or + # an Array of Symbols. See SchemaStatements#add_foreign_key + # + # ===== Examples + # ====== Creating a simple foreign key + # t.foreign_key(:people) + # ====== Defining the column + # t.foreign_key(:people, :column => :sender_id) + # ====== Creating a named foreign key + # t.foreign_key(:people, :column => :sender_id, :name => 'sender_foreign_key') + # ====== Defining the column of the +to_table+. + # t.foreign_key(:people, :column => :sender_id, :primary_key => :person_id) + def foreign_key(to_table, options = {}) + if @base.supports_foreign_keys? + to_table = to_table.to_s.pluralize if ActiveRecord::Base.pluralize_table_names + foreign_keys << ForeignKey.new(@base, to_table, options) + end + end + + def to_sql_with_foreign_keys + sql = to_sql_without_foreign_keys + sql << ', ' << (foreign_keys * ', ') if foreign_keys.present? + sql + end + + private + def foreign_keys + @foreign_keys ||= [] + end + end + end + + module Table + def self.included(base) + base.class_eval do + include InstanceMethods + alias_method_chain :references, :foreign_keys + end + end + + module InstanceMethods + # Adds a new foreign key to the table. +to_table+ can be a single Symbol, or + # an Array of Symbols. See SchemaStatements#add_foreign_key + # + # ===== Examples + # ====== Creating a simple foreign key + # t.foreign_key(:people) + # ====== Defining the column + # t.foreign_key(:people, :column => :sender_id) + # ====== Creating a named foreign key + # t.foreign_key(:people, :column => :sender_id, :name => 'sender_foreign_key') + # ====== Defining the column of the +to_table+. + # t.foreign_key(:people, :column => :sender_id, :primary_key => :person_id) + def foreign_key(to_table, options = {}) + @base.add_foreign_key(@table_name, to_table, options) + end + + # Remove the given foreign key from the table. + # + # ===== Examples + # ====== Remove the suppliers_company_id_fk in the suppliers table. + # t.remove_foreign_key :companies + # ====== Remove the foreign key named accounts_branch_id_fk in the accounts table. + # remove_foreign_key :column => :branch_id + # ====== Remove the foreign key named party_foreign_key in the accounts table. + # remove_index :name => :party_foreign_key + def remove_foreign_key(options = {}) + @base.remove_foreign_key(@table_name, options) + end + + # Adds a :foreign_key option to TableDefinition.references. + # If :foreign_key is true, a foreign key constraint is added to the table. + # You can also specify a hash, which is passed as foreign key options. + # + # ===== Examples + # ====== Add goat_id column and a foreign key to the goats table. + # t.references(:goat, :foreign_key => true) + # ====== Add goat_id column and a cascading foreign key to the goats table. + # t.references(:goat, :foreign_key => {:dependent => :delete}) + # + # Note: No foreign key is created if :polymorphic => true is used. + def references_with_foreign_keys(*args) + options = args.extract_options! + polymorphic = options[:polymorphic] + fk_options = options.delete(:foreign_key) + + references_without_foreign_keys(*(args << options)) + + if fk_options && !polymorphic + fk_options = {} if fk_options == true + args.each { |to_table| foreign_key(to_table, fk_options) } + end + end + end + end + end +end diff --git a/src/vendor/plugins/foreigner/lib/foreigner/connection_adapters/abstract/schema_statements.rb b/src/vendor/plugins/foreigner/lib/foreigner/connection_adapters/abstract/schema_statements.rb new file mode 100644 index 0000000..f611dd4 --- /dev/null +++ b/src/vendor/plugins/foreigner/lib/foreigner/connection_adapters/abstract/schema_statements.rb @@ -0,0 +1,75 @@ +module Foreigner + module ConnectionAdapters + module SchemaStatements + def self.included(base) + base::AbstractAdapter.class_eval do + include Foreigner::ConnectionAdapters::AbstractAdapter + end + end + end + + module AbstractAdapter + def supports_foreign_keys? + false + end + + # Adds a new foreign key to the +from_table+, referencing the primary key of +to_table+ + # + # The foreign key will be named after the from and to tables unless you pass + # <tt>:name</tt> as an option. + # + # ===== Examples + # ====== Creating a foreign key + # add_foreign_key(:comments, :posts) + # generates + # ALTER TABLE `comments` ADD CONSTRAINT + # `comments_post_id_fk` FOREIGN KEY (`post_id`) REFERENCES `posts` (`id`) + # + # ====== Creating a named foreign key + # add_foreign_key(:comments, :posts, :name => 'comments_belongs_to_posts') + # generates + # ALTER TABLE `comments` ADD CONSTRAINT + # `comments_belongs_to_posts` FOREIGN KEY (`post_id`) REFERENCES `posts` (`id`) + # + # ====== Creating a cascading foreign_key on a custom column + # add_foreign_key(:people, :people, :column => 'best_friend_id', :dependent => :nullify) + # generates + # ALTER TABLE `people` ADD CONSTRAINT + # `people_best_friend_id_fk` FOREIGN KEY (`best_friend_id`) REFERENCES `people` (`id`) + # ON DELETE SET NULL + # + # === Supported options + # [:column] + # Specify the column name on the from_table that references the to_table. By default this is guessed + # to be the singular name of the to_table with "_id" suffixed. So a to_table of :posts will use "post_id" + # as the default <tt>:column</tt>. + # [:primary_key] + # Specify the column name on the to_table that is referenced by this foreign key. By default this is + # assumed to be "id". + # [:name] + # Specify the name of the foreign key constraint. This defaults to use from_table and foreign key column. + # [:dependent] + # If set to <tt>:delete</tt>, the associated records in from_table are deleted when records in to_table table are deleted. + # If set to <tt>:nullify</tt>, the foreign key column is set to +NULL+. + def add_foreign_key(from_table, to_table, options = {}) + end + + # Remove the given foreign key from the table. + # + # ===== Examples + # ====== Remove the suppliers_company_id_fk in the suppliers table. + # remove_foreign_key :suppliers, :companies + # ====== Remove the foreign key named accounts_branch_id_fk in the accounts table. + # remove_foreign_key :accounts, :column => :branch_id + # ====== Remove the foreign key named party_foreign_key in the accounts table. + # remove_foreign_key :accounts, :name => :party_foreign_key + def remove_foreign_key(from_table, options) + end + + # Return the foreign keys for the schema_dumper + def foreign_keys(table_name) + [] + end + end + end +end diff --git a/src/vendor/plugins/foreigner/lib/foreigner/connection_adapters/mysql_adapter.rb b/src/vendor/plugins/foreigner/lib/foreigner/connection_adapters/mysql_adapter.rb new file mode 100644 index 0000000..d831d24 --- /dev/null +++ b/src/vendor/plugins/foreigner/lib/foreigner/connection_adapters/mysql_adapter.rb @@ -0,0 +1,43 @@ +module Foreigner + module ConnectionAdapters + module MysqlAdapter + include Foreigner::ConnectionAdapters::Sql2003 + + def foreign_keys(table_name) + fk_info = select_all %{ + SELECT fk.referenced_table_name as 'to_table' + ,fk.referenced_column_name as 'primary_key' + ,fk.column_name as 'column' + ,fk.constraint_name as 'name' + FROM information_schema.key_column_usage fk + WHERE fk.referenced_column_name is not null + AND fk.table_schema = '#{@config[:database]}' + AND fk.table_name = '#{table_name}' + } + + create_table_info = select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"] + + fk_info.map do |row| + options = {:column => row['column'], :name => row['name'], :primary_key => row['primary_key']} + + if create_table_info =~ /CONSTRAINT #{quote_column_name(row['name'])} FOREIGN KEY .* REFERENCES .* ON DELETE (CASCADE|SET NULL)/ + if $1 == 'CASCADE' + options[:dependent] = :delete + elsif $1 == 'SET NULL' + options[:dependent] = :nullify + end + end + ForeignKeyDefinition.new(table_name, row['to_table'], options) + end + end + end + end +end + +module ActiveRecord + module ConnectionAdapters + MysqlAdapter.class_eval do + include Foreigner::ConnectionAdapters::MysqlAdapter + end + end +end diff --git a/src/vendor/plugins/foreigner/lib/foreigner/connection_adapters/postgresql_adapter.rb b/src/vendor/plugins/foreigner/lib/foreigner/connection_adapters/postgresql_adapter.rb new file mode 100644 index 0000000..be662b3 --- /dev/null +++ b/src/vendor/plugins/foreigner/lib/foreigner/connection_adapters/postgresql_adapter.rb @@ -0,0 +1,46 @@ +module Foreigner + module ConnectionAdapters + module PostgreSQLAdapter + include Foreigner::ConnectionAdapters::Sql2003 + + def foreign_keys(table_name) + fk_info = select_all %{ + SELECT tc.constraint_name as name + ,ccu.table_name as to_table + ,ccu.column_name as primary_key + ,kcu.column_name as column + ,rc.delete_rule as dependency + FROM information_schema.table_constraints tc + JOIN information_schema.key_column_usage kcu + USING (constraint_catalog, constraint_schema, constraint_name) + JOIN information_schema.referential_constraints rc + USING (constraint_catalog, constraint_schema, constraint_name) + JOIN information_schema.constraint_column_usage ccu + USING (constraint_catalog, constraint_schema, constraint_name) + WHERE tc.constraint_type = 'FOREIGN KEY' + AND tc.constraint_catalog = '#{@config[:database]}' + AND tc.table_name = '#{table_name}' + } + + fk_info.map do |row| + options = {:column => row['column'], :name => row['name'], :primary_key = row['primary_key']} + + if row['dependency'] == 'CASCADE' + options[:dependent] = :delete + elsif row['dependency'] == 'SET NULL' + options[:dependent] = :nullify + end + ForeignKeyDefinition.new(table_name, row['to_table'], options) + end + end + end + end +end + +module ActiveRecord + module ConnectionAdapters + PostgreSQLAdapter.class_eval do + include Foreigner::ConnectionAdapters::PostgreSQLAdapter + end + end +end \ No newline at end of file diff --git a/src/vendor/plugins/foreigner/lib/foreigner/connection_adapters/sql_2003.rb b/src/vendor/plugins/foreigner/lib/foreigner/connection_adapters/sql_2003.rb new file mode 100644 index 0000000..1a1019b --- /dev/null +++ b/src/vendor/plugins/foreigner/lib/foreigner/connection_adapters/sql_2003.rb @@ -0,0 +1,58 @@ +module Foreigner + module ConnectionAdapters + module Sql2003 + def supports_foreign_keys? + true + end + + def add_foreign_key(from_table, to_table, options = {}) + column = options[:column] || "#{to_table.to_s.singularize}_id" + foreign_key_name = foreign_key_name(from_table, column, options) + + sql + "ALTER TABLE #{quote_table_name(from_table)} " + + "ADD CONSTRAINT #{quote_column_name(foreign_key_name)} " + + foreign_key_definition(to_table, options) + + execute(sql) + end + + def foreign_key_definition(to_table, options = {}) + column = options[:column] || "#{to_table.to_s.singularize}_id" + primary_key = options[:primary_key] || "id" + dependency = dependency_sql(options[:dependent]) + + sql = "FOREIGN KEY (#{quote_column_name(column)}) REFERENCES #{quote_table_name(to_table)}(#{primary_key})" + sql << " #{dependency}" unless dependency.blank? + sql + end + + def remove_foreign_key(table, options) + if Hash === options + foreign_key_name = foreign_key_name(table, options[:column], options) + else + foreign_key_name = foreign_key_name(table, "#{options.to_s.singularize}_id") + end + + execute "ALTER TABLE #{quote_table_name(table)} DROP FOREIGN KEY #{quote_column_name(foreign_key_name)}" + end + + private + def foreign_key_name(table, column, options = {}) + if options[:name] + options[:name] + else + "#{table}_#{column}_fk" + end + end + + def dependency_sql(dependency) + case dependency + when :nullify then "ON DELETE SET NULL" + when :delete then "ON DELETE CASCADE" + else "" + end + end + end + end +end \ No newline at end of file diff --git a/src/vendor/plugins/foreigner/lib/foreigner/schema_dumper.rb b/src/vendor/plugins/foreigner/lib/foreigner/schema_dumper.rb new file mode 100644 index 0000000..2c31090 --- /dev/null +++ b/src/vendor/plugins/foreigner/lib/foreigner/schema_dumper.rb @@ -0,0 +1,45 @@ +module Foreigner + module SchemaDumper + def self.included(base) + base.class_eval do + include InstanceMethods + alias_method_chain :tables, :foreign_keys + end + end + + module InstanceMethods + def tables_with_foreign_keys(stream) + tables_without_foreign_keys(stream) + @connection.tables.sort.each do |table| + foreign_keys(table, stream) + end + end + + private + def foreign_keys(table_name, stream) + if (foreign_keys = @connection.foreign_keys(table_name)).any? + add_foreign_key_statements = foreign_keys.map do |foreign_key| + statement_parts = [ ('add_foreign_key ' + foreign_key.from_table.inspect) ] + statement_parts << foreign_key.to_table.inspect + statement_parts << (':name => ' + foreign_key.options[:name].inspect) + + if foreign_key.options[:column] != "#{foreign_key.to_table.singularize}_id" + statement_parts << (':column => ' + foreign_key.options[:column].inspect) + end + if foreign_key.options[:primary_key] != 'id' + statement_parts << (':primary_key => ' + foreign_key.options[:primary_key].inspect) + end + if foreign_key.options[:dependent].present? + statement_parts << (':dependent => ' + foreign_key.options[:dependent].inspect) + end + + ' ' + statement_parts.join(', ') + end + + stream.puts add_foreign_key_statements.sort.join("\n") + stream.puts + end + end + end + end +end \ No newline at end of file diff --git a/src/vendor/plugins/foreigner/tasks/foreigner_tasks.rake b/src/vendor/plugins/foreigner/tasks/foreigner_tasks.rake new file mode 100644 index 0000000..a5028e3 --- /dev/null +++ b/src/vendor/plugins/foreigner/tasks/foreigner_tasks.rake @@ -0,0 +1,4 @@ +# desc "Explaining what the task does" +# task :foreigner do +# # Task goes here +# end diff --git a/src/vendor/plugins/foreigner/test/helper.rb b/src/vendor/plugins/foreigner/test/helper.rb new file mode 100644 index 0000000..05eab24 --- /dev/null +++ b/src/vendor/plugins/foreigner/test/helper.rb @@ -0,0 +1,8 @@ +require 'test/unit' +require 'rubygems' +require 'active_support' +require 'active_support/test_case' +require 'active_record' +require 'active_record/test_case' +require 'active_record/connection_adapters/mysql_adapter' +require 'foreigner' \ No newline at end of file diff --git a/src/vendor/plugins/foreigner/test/mysql_adapter_test.rb b/src/vendor/plugins/foreigner/test/mysql_adapter_test.rb new file mode 100644 index 0000000..8cf977f --- /dev/null +++ b/src/vendor/plugins/foreigner/test/mysql_adapter_test.rb @@ -0,0 +1,83 @@ +require File.dirname(__FILE__) + '/helper' + +class MysqlAdapterTest < ActiveRecord::TestCase + include Foreigner::MysqlAdapter + + def test_add_without_options + assert_equal( + "ALTER TABLE `employees` ADD CONSTRAINT `employees_company_id_fk` FOREIGN KEY (`company_id`) REFERENCES `companies`(id)", + add_foreign_key(:employees, :companies) + ) + end + + def test_add_with_name + assert_equal( + "ALTER TABLE `employees` ADD CONSTRAINT `favorite_company_fk` FOREIGN KEY (`company_id`) REFERENCES `companies`(id)", + add_foreign_key(:employees, :companies, :name => 'favorite_company_fk') + ) + end + + def test_add_with_column + assert_equal( + "ALTER TABLE `employees` ADD CONSTRAINT `employees_last_employer_id_fk` FOREIGN KEY (`last_employer_id`) REFERENCES `companies`(id)", + add_foreign_key(:employees, :companies, :column => 'last_employer_id') + ) + end + + def test_add_with_column_and_name + assert_equal( + "ALTER TABLE `employees` ADD CONSTRAINT `favorite_company_fk` FOREIGN KEY (`last_employer_id`) REFERENCES `companies`(id)", + add_foreign_key(:employees, :companies, :column => 'last_employer_id', :name => 'favorite_company_fk') + ) + end + + def test_add_with_delete_dependency + assert_equal( + "ALTER TABLE `employees` ADD CONSTRAINT `employees_company_id_fk` FOREIGN KEY (`company_id`) REFERENCES `companies`(id) " + + "ON DELETE CASCADE", + add_foreign_key(:employees, :companies, :dependent => :delete) + ) + end + + def test_add_with_nullify_dependency + assert_equal( + "ALTER TABLE `employees` ADD CONSTRAINT `employees_company_id_fk` FOREIGN KEY (`company_id`) REFERENCES `companies`(id) " + + "ON DELETE SET NULL", + add_foreign_key(:employees, :companies, :dependent => :nullify) + ) + end + + def test_remove_by_table + assert_equal( + "ALTER TABLE `suppliers` DROP FOREIGN KEY `suppliers_company_id_fk`", + remove_foreign_key(:suppliers, :companies) + ) + end + + def test_remove_by_name + assert_equal( + "ALTER TABLE `suppliers` DROP FOREIGN KEY `belongs_to_supplier`", + remove_foreign_key(:suppliers, :name => "belongs_to_supplier") + ) + end + + def test_remove_by_column + assert_equal( + "ALTER TABLE `suppliers` DROP FOREIGN KEY `suppliers_ship_to_id_fk`", + remove_foreign_key(:suppliers, :column => "ship_to_id") + ) + end + + private + def execute(sql, name = nil) + sql + end + + def quote_column_name(name) + "`#{name}`" + end + + def quote_table_name(name) + quote_column_name(name).gsub('.', '`.`') + end +end \ No newline at end of file diff --git a/src/vendor/plugins/foreigner/uninstall.rb b/src/vendor/plugins/foreigner/uninstall.rb new file mode 100644 index 0000000..9738333 --- /dev/null +++ b/src/vendor/plugins/foreigner/uninstall.rb @@ -0,0 +1 @@ +# Uninstall hook code here -- 1.6.2.5
Michel Loiseleur
2009-Sep-28 20:08 UTC
[Ovirt-devel] [PATCH 2/3] enhance & fix foreigner plugin to manage correctly named foreign key
Signed-off-by: Michel Loiseleur <mloiseleur at linagora.com> --- .../abstract/schema_definitions.rb | 26 ++++++++++---------- .../lib/foreigner/connection_adapters/sql_2003.rb | 21 +++++++-------- 2 files changed, 23 insertions(+), 24 deletions(-) diff --git a/src/vendor/plugins/foreigner/lib/foreigner/connection_adapters/abstract/schema_definitions.rb b/src/vendor/plugins/foreigner/lib/foreigner/connection_adapters/abstract/schema_definitions.rb index e417c5b..f31fc2b 100644 --- a/src/vendor/plugins/foreigner/lib/foreigner/connection_adapters/abstract/schema_definitions.rb +++ b/src/vendor/plugins/foreigner/lib/foreigner/connection_adapters/abstract/schema_definitions.rb @@ -8,17 +8,17 @@ module Foreigner base::TableDefinition.class_eval do include Foreigner::ConnectionAdapters::TableDefinition end - + base::Table.class_eval do include Foreigner::ConnectionAdapters::Table end end end - + module TableDefinition class ForeignKey < Struct.new(:base, :to_table, :options) def to_sql - base.foreign_key_definition(to_table, options) + base.foreign_key_definition(@table_name, to_table, options) end alias to_s :to_sql end @@ -30,18 +30,18 @@ module Foreigner alias_method_chain :to_sql, :foreign_keys end end - + module InstanceMethods # Adds a :foreign_key option to TableDefinition.references. # If :foreign_key is true, a foreign key constraint is added to the table. # You can also specify a hash, which is passed as foreign key options. - # + # # ===== Examples # ====== Add goat_id column and a foreign key to the goats table. # t.references(:goat, :foreign_key => true) # ====== Add goat_id column and a cascading foreign key to the goats table. # t.references(:goat, :foreign_key => {:dependent => :delete}) - # + # # Note: No foreign key is created if :polymorphic => true is used. # Note: If no name is specified, the database driver creates one for you! def references_with_foreign_keys(*args) @@ -55,7 +55,7 @@ module Foreigner references_without_foreign_keys(*(args << options)) end - + # Defines a foreign key for the table. +to_table+ can be a single Symbol, or # an Array of Symbols. See SchemaStatements#add_foreign_key # @@ -74,13 +74,13 @@ module Foreigner foreign_keys << ForeignKey.new(@base, to_table, options) end end - + def to_sql_with_foreign_keys sql = to_sql_without_foreign_keys sql << ', ' << (foreign_keys * ', ') if foreign_keys.present? sql end - + private def foreign_keys @foreign_keys ||= [] @@ -112,7 +112,7 @@ module Foreigner def foreign_key(to_table, options = {}) @base.add_foreign_key(@table_name, to_table, options) end - + # Remove the given foreign key from the table. # # ===== Examples @@ -125,17 +125,17 @@ module Foreigner def remove_foreign_key(options = {}) @base.remove_foreign_key(@table_name, options) end - + # Adds a :foreign_key option to TableDefinition.references. # If :foreign_key is true, a foreign key constraint is added to the table. # You can also specify a hash, which is passed as foreign key options. - # + # # ===== Examples # ====== Add goat_id column and a foreign key to the goats table. # t.references(:goat, :foreign_key => true) # ====== Add goat_id column and a cascading foreign key to the goats table. # t.references(:goat, :foreign_key => {:dependent => :delete}) - # + # # Note: No foreign key is created if :polymorphic => true is used. def references_with_foreign_keys(*args) options = args.extract_options! diff --git a/src/vendor/plugins/foreigner/lib/foreigner/connection_adapters/sql_2003.rb b/src/vendor/plugins/foreigner/lib/foreigner/connection_adapters/sql_2003.rb index 1a1019b..957111b 100644 --- a/src/vendor/plugins/foreigner/lib/foreigner/connection_adapters/sql_2003.rb +++ b/src/vendor/plugins/foreigner/lib/foreigner/connection_adapters/sql_2003.rb @@ -4,25 +4,24 @@ module Foreigner def supports_foreign_keys? true end - + def add_foreign_key(from_table, to_table, options = {}) column = options[:column] || "#{to_table.to_s.singularize}_id" - foreign_key_name = foreign_key_name(from_table, column, options) - sql - "ALTER TABLE #{quote_table_name(from_table)} " + - "ADD CONSTRAINT #{quote_column_name(foreign_key_name)} " + - foreign_key_definition(to_table, options) - + sql = "ALTER TABLE #{quote_table_name(from_table)} ADD " << + foreign_key_definition(from_table, to_table, options) + execute(sql) end - - def foreign_key_definition(to_table, options = {}) + + def foreign_key_definition(from_table, to_table, options = {}) column = options[:column] || "#{to_table.to_s.singularize}_id" + foreign_key_name = foreign_key_name(from_table, column, options) primary_key = options[:primary_key] || "id" dependency = dependency_sql(options[:dependent]) - sql = "FOREIGN KEY (#{quote_column_name(column)}) REFERENCES #{quote_table_name(to_table)}(#{primary_key})" + sql = "CONSTRAINT #{quote_column_name(foreign_key_name)} " + sql << "FOREIGN KEY (#{quote_column_name(column)}) REFERENCES #{quote_table_name(to_table)}(#{primary_key})" sql << " #{dependency}" unless dependency.blank? sql end @@ -36,7 +35,7 @@ module Foreigner execute "ALTER TABLE #{quote_table_name(table)} DROP FOREIGN KEY #{quote_column_name(foreign_key_name)}" end - + private def foreign_key_name(table, column, options = {}) if options[:name] -- 1.6.2.5
Loiseleur Michel wrote:> Hi, > > While I was hacking ovirt-server, I have found that it's currently > restricted to Postgres DB. Even if I like postgres for serious work on > a server, I really prefer to hack/dev locally on a Sqlite or MySQL DB. > > I have googled on rails in order to find a good answer for the > "foreign key problem" which forces OVirt to stay on pg. I have found a > plugin on this particular problem, named foreigner : > http://github.com/matthuhiggins/foreigner > > It provides a rails' syntax to common operation on them. For instance : > > add_foreign_key(from_table, to_table, options) > remove_foreign_key(from_table, options) > foreign_keys(table_name) > > I really like to keep my development computer to run as fast as > possible (ie without a database service), so I have even tried to > integrate this plugin into OVirt, in a transparent way for existing db. > > After a couple of patch on the plugin itself and some hack on > migrations, it seems that I manage to do it. The 3 patchs following > this email allows OVirt to support multiple open source database > without losing functionality, without losing existing databases and > with a small readability enhancement on db migrations. > > What do you think of it ? Feel free to contact me by email, on this > list or by irc (nick: Coren`) > > Regards,Thank you greatly for this. I agree, and am sure many others do that multiple database support would be great. I applied all three patches but ran into some problems when running them. Comments are in line in the patches. -Mo
Mohammed Morsi
2009-Oct-14 19:13 UTC
[Ovirt-devel] [PATCH 1/3] Add foreigner plugin from Matthu Higgins, allowing to manage foreign key in a multi-db way.
Michel Loiseleur wrote:> .... > --- /dev/null > +++ b/src/vendor/plugins/foreigner/lib/foreigner/connection_adapters/postgresql_adapter.rb > @@ -0,0 +1,46 @@ > +module Foreigner > + module ConnectionAdapters > + module PostgreSQLAdapter > + include Foreigner::ConnectionAdapters::Sql2003 > + > + def foreign_keys(table_name) > + fk_info = select_all %{ > + SELECT tc.constraint_name as name > + ,ccu.table_name as to_table > + ,ccu.column_name as primary_key > + ,kcu.column_name as column > + ,rc.delete_rule as dependency > + FROM information_schema.table_constraints tc > + JOIN information_schema.key_column_usage kcu > + USING (constraint_catalog, constraint_schema, constraint_name) > + JOIN information_schema.referential_constraints rc > + USING (constraint_catalog, constraint_schema, constraint_name) > + JOIN information_schema.constraint_column_usage ccu > + USING (constraint_catalog, constraint_schema, constraint_name) > + WHERE tc.constraint_type = 'FOREIGN KEY' > + AND tc.constraint_catalog = '#{@config[:database]}' > + AND tc.table_name = '#{table_name}' > + } > + > + fk_info.map do |row| > + options = {:column => row['column'], :name => row['name'], :primary_key = row['primary_key']} >Syntax error here prevents mongrel from starting up, the last "=" should be changed to a "=>" like so :primary_key => row['primary_key'] After I fix this mongrel/rails starts up and the wui works again. -Mo
Loiseleur Michel
2009-Oct-15 09:23 UTC
[Ovirt-devel] [PATCH] enhance & fix foreigner plugin to manage correctly named foreign key
This patch fixes current foreigner plugins in order to : 1) be able to add constraint on a named table 2) be able to drop fk with postgres Signed-off-by: Loiseleur Michel <mloiseleur at linagora.com> --- .../abstract/schema_definitions.rb | 2 +- .../connection_adapters/postgresql_adapter.rb | 15 ++++++++++++- .../lib/foreigner/connection_adapters/sql_2003.rb | 21 +++++++++---------- 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/src/vendor/plugins/foreigner/lib/foreigner/connection_adapters/abstract/schema_definitions.rb b/src/vendor/plugins/foreigner/lib/foreigner/connection_adapters/abstract/schema_definitions.rb index e417c5b..3598d05 100644 --- a/src/vendor/plugins/foreigner/lib/foreigner/connection_adapters/abstract/schema_definitions.rb +++ b/src/vendor/plugins/foreigner/lib/foreigner/connection_adapters/abstract/schema_definitions.rb @@ -18,7 +18,7 @@ module Foreigner module TableDefinition class ForeignKey < Struct.new(:base, :to_table, :options) def to_sql - base.foreign_key_definition(to_table, options) + base.foreign_key_definition(@table_name, to_table, options) end alias to_s :to_sql end diff --git a/src/vendor/plugins/foreigner/lib/foreigner/connection_adapters/postgresql_adapter.rb b/src/vendor/plugins/foreigner/lib/foreigner/connection_adapters/postgresql_adapter.rb index be662b3..fec81cd 100644 --- a/src/vendor/plugins/foreigner/lib/foreigner/connection_adapters/postgresql_adapter.rb +++ b/src/vendor/plugins/foreigner/lib/foreigner/connection_adapters/postgresql_adapter.rb @@ -23,7 +23,7 @@ module Foreigner } fk_info.map do |row| - options = {:column => row['column'], :name => row['name'], :primary_key = row['primary_key']} + options = {:column => row['column'], :name => row['name'], :primary_key => row['primary_key']} if row['dependency'] == 'CASCADE' options[:dependent] = :delete @@ -33,6 +33,17 @@ module Foreigner ForeignKeyDefinition.new(table_name, row['to_table'], options) end end + + def remove_foreign_key(table, options) + if Hash === options + foreign_key_name = foreign_key_name(table, options[:column], options) + else + foreign_key_name = foreign_key_name(table, "#{options.to_s.singularize}_id") + end + + execute "ALTER TABLE #{quote_table_name(table)} DROP CONSTRAINT #{quote_column_name(foreign_key_name)}" + end + end end end @@ -43,4 +54,4 @@ module ActiveRecord include Foreigner::ConnectionAdapters::PostgreSQLAdapter end end -end \ No newline at end of file +end diff --git a/src/vendor/plugins/foreigner/lib/foreigner/connection_adapters/sql_2003.rb b/src/vendor/plugins/foreigner/lib/foreigner/connection_adapters/sql_2003.rb index 1a1019b..957111b 100644 --- a/src/vendor/plugins/foreigner/lib/foreigner/connection_adapters/sql_2003.rb +++ b/src/vendor/plugins/foreigner/lib/foreigner/connection_adapters/sql_2003.rb @@ -4,25 +4,24 @@ module Foreigner def supports_foreign_keys? true end - + def add_foreign_key(from_table, to_table, options = {}) column = options[:column] || "#{to_table.to_s.singularize}_id" - foreign_key_name = foreign_key_name(from_table, column, options) - sql - "ALTER TABLE #{quote_table_name(from_table)} " + - "ADD CONSTRAINT #{quote_column_name(foreign_key_name)} " + - foreign_key_definition(to_table, options) - + sql = "ALTER TABLE #{quote_table_name(from_table)} ADD " << + foreign_key_definition(from_table, to_table, options) + execute(sql) end - - def foreign_key_definition(to_table, options = {}) + + def foreign_key_definition(from_table, to_table, options = {}) column = options[:column] || "#{to_table.to_s.singularize}_id" + foreign_key_name = foreign_key_name(from_table, column, options) primary_key = options[:primary_key] || "id" dependency = dependency_sql(options[:dependent]) - sql = "FOREIGN KEY (#{quote_column_name(column)}) REFERENCES #{quote_table_name(to_table)}(#{primary_key})" + sql = "CONSTRAINT #{quote_column_name(foreign_key_name)} " + sql << "FOREIGN KEY (#{quote_column_name(column)}) REFERENCES #{quote_table_name(to_table)}(#{primary_key})" sql << " #{dependency}" unless dependency.blank? sql end @@ -36,7 +35,7 @@ module Foreigner execute "ALTER TABLE #{quote_table_name(table)} DROP FOREIGN KEY #{quote_column_name(foreign_key_name)}" end - + private def foreign_key_name(table, column, options = {}) if options[:name] -- 1.6.2.5