sqlite-foreigner 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,126 @@
1
+ h1. Sqlite Foreigner
2
+
3
+ Sqlite Foreigner is a Rails foreign key migration helper which supports adding
4
+ AND enforcing foreign key constraints on Sqlite3 databases.
5
+
6
+ h2. The Story
7
+
8
+ With a lack of support for easily adding foreign key constraints to sqlite databases
9
+ I decided to create my own based on "Matt Higgins Foreigner":http://github.com/matthuhiggins/foreigner/
10
+
11
+ h2. Some Examples
12
+
13
+ Sqlite Foreigner allows you to do the following in your migration files
14
+ <pre>
15
+ create_table :comments do |t|
16
+ t.references :posts, :foreign_key => true, :null => false
17
+ end
18
+ </pre>
19
+ Which will generate the following SQL:
20
+ <pre>
21
+ CREATE TABLE "comments" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
22
+ "post_id" integer NOT NULL,
23
+ FOREIGN KEY ("post_id") REFERENCES "posts"(id));
24
+ </pre>
25
+
26
+ Go a different column name?
27
+ <pre>
28
+ create_table :comments do |t|
29
+ t.references :article, :null => false
30
+ t.foreign_key :posts, :column => :article_id
31
+ end
32
+ </pre>
33
+ Which generates:
34
+ <pre>
35
+ CREATE TABLE "comments" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
36
+ "article_id" integer NOT NULL,
37
+ FOREIGN KEY ("article_id") REFERENCES "posts"(id));
38
+ </pre>
39
+
40
+ Want to specify a dependency (nullify or delete)?
41
+ <pre>
42
+ create_table :comments do |t|
43
+ t.references :posts, :foreign_key => {:dependent => :delete}, :null => false
44
+ end
45
+ </pre>
46
+ Generates:
47
+ <pre>
48
+ CREATE TABLE "comments" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
49
+ "post_id" integer NOT NULL,
50
+ FOREIGN KEY ("post_id") REFERENCES "posts"(id) ON DELETE CASCADE);
51
+ </pre>
52
+ Or:
53
+ <pre>
54
+ create_table :comments do |t|
55
+ t.references :article, :null => false
56
+ t.foreign_key :posts, :column => :article_id, :dependent => :nullify
57
+ end
58
+ </pre>
59
+ Which generates:
60
+ <pre>
61
+ CREATE TABLE "comments" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
62
+ "article_id" integer NOT NULL,
63
+ FOREIGN KEY ("article_id") REFERENCES "posts"(id) ON DELETE SET NULL);
64
+ </pre>
65
+
66
+ h2. Enforcing constraints
67
+
68
+ SQLite does not enforce database constraints out of the box
69
+ This provides you with the flexibility in choosing whether or not to enforce
70
+ constraints at the DB level or not.
71
+
72
+ In order to enforce your constraints:
73
+ <pre>
74
+ script/dbconsole
75
+ .genfkey --exec
76
+ </pre>
77
+
78
+ While your in the console run:
79
+ <pre>
80
+ .schema
81
+ </pre>
82
+ to see your constraints implemented as triggers
83
+
84
+ h2. schema.rb
85
+
86
+ All of the constrants are updated in schema.rb
87
+ when you run:
88
+ <pre>
89
+ rake db:migrate
90
+ rake db:schema:dump
91
+ </pre>
92
+
93
+ This allows you to see the state of your migratons and
94
+ take advantage of using <pre>rake db:schema:load</pre>
95
+
96
+ h2. Limitations
97
+
98
+ Since SQLite does not have complete ALTER TABLE support
99
+ you cannot use the following syntax:
100
+ <pre>
101
+ add_foreign_key
102
+ remove_foreign_key
103
+ </pre>
104
+
105
+ Therefore you must add your foreign keys when you define your table,
106
+ which may involve editing existing migration files instead of generating new ones
107
+
108
+ h2. Installation
109
+
110
+ Add the following to environment.rb:
111
+ <pre>
112
+ config.gem "sqlite-foreigner", :lib => "foreigner", :source => "http://gemcutter.org"
113
+ </pre>
114
+
115
+ Then run:
116
+ <pre>
117
+ sudo rake gems:install
118
+ </pre>
119
+
120
+ h2. See also
121
+
122
+ Need support for other databases?
123
+ Check out "dwilkie-foreigner":http://github.com/dwilkie/foreigner/tree/master
124
+
125
+ Copyright (c) 2009 David Wilkie, released under the MIT license
126
+
@@ -0,0 +1,22 @@
1
+ require 'foreigner/connection_adapters/abstract/schema_definitions'
2
+ require 'foreigner/connection_adapters/abstract/schema_statements'
3
+ require 'foreigner/connection_adapters/sql_2003'
4
+ require 'foreigner/schema_dumper'
5
+
6
+ module ActiveRecord
7
+ module ConnectionAdapters
8
+ include Foreigner::ConnectionAdapters::SchemaStatements
9
+ include Foreigner::ConnectionAdapters::SchemaDefinitions
10
+ end
11
+
12
+ SchemaDumper.class_eval do
13
+ include Foreigner::SchemaDumper
14
+ end
15
+
16
+ Base.class_eval do
17
+ if connection_pool.spec.config[:adapter].downcase == "sqlite3"
18
+ require "foreigner/connection_adapters/sqlite3_adapter"
19
+ end
20
+ end
21
+ end
22
+
@@ -0,0 +1,84 @@
1
+ module Foreigner
2
+ module ConnectionAdapters
3
+ class ForeignKeyDefinition < Struct.new(:from_table, :to_table, :options) #:nodoc:
4
+ end
5
+
6
+ module SchemaDefinitions
7
+ def self.included(base)
8
+ base::TableDefinition.class_eval do
9
+ include Foreigner::ConnectionAdapters::TableDefinition
10
+ end
11
+ end
12
+ end
13
+
14
+ module TableDefinition
15
+ class ForeignKey < Struct.new(:base, :to_table, :options)
16
+ def to_sql
17
+ base.foreign_key_definition(to_table, options)
18
+ end
19
+ alias to_s :to_sql
20
+ end
21
+
22
+ def self.included(base)
23
+ base.class_eval do
24
+ include InstanceMethods
25
+ alias_method_chain :references, :foreign_keys
26
+ alias_method_chain :to_sql, :foreign_keys
27
+ end
28
+ end
29
+ module InstanceMethods
30
+ # Adds a :foreign_key option to TableDefinition.references.
31
+ # If :foreign_key is true, a foreign key constraint is added to the table.
32
+ # You can also specify a hash, which is passed as foreign key options.
33
+ #
34
+ # ===== Examples
35
+ # ====== Add goat_id column and a foreign key to the goats table.
36
+ # t.references(:goat, :foreign_key => true)
37
+ # ====== Add goat_id column and a cascading foreign key to the goats table.
38
+ # t.references(:goat, :foreign_key => {:dependent => :delete})
39
+ #
40
+ # Note: No foreign key is created if :polymorphic => true is used.
41
+ def references_with_foreign_keys(*args)
42
+ options = args.extract_options!
43
+ fk_options = options.delete(:foreign_key)
44
+
45
+ if fk_options && !options[:polymorphic]
46
+ fk_options = {} if fk_options == true
47
+ args.each { |to_table| foreign_key(to_table, fk_options) }
48
+ end
49
+
50
+ references_without_foreign_keys(*(args << options))
51
+ end
52
+
53
+ # Defines a foreign key for the table. +to_table+ can be a single Symbol, or
54
+ # an Array of Symbols.
55
+ #
56
+ # ===== Examples
57
+ # ====== Creating a simple foreign key
58
+ # t.foreign_key(:people)
59
+ # ====== Defining the column
60
+ # t.foreign_key(:people, :column => :sender_id)
61
+ # ====== Specify cascading foreign key
62
+ # t.foreign_key(:people, :dependent => :delete)
63
+ def foreign_key(to_table, options = {})
64
+ if @base.supports_foreign_keys?
65
+ to_table = to_table.to_s.pluralize if ActiveRecord::Base.pluralize_table_names
66
+ foreign_keys << ForeignKey.new(@base, to_table, options)
67
+ end
68
+ end
69
+
70
+ def to_sql_with_foreign_keys
71
+ sql = to_sql_without_foreign_keys
72
+ sql << ', ' << (foreign_keys * ', ') if foreign_keys.present?
73
+ sql
74
+ end
75
+
76
+ private
77
+ def foreign_keys
78
+ @foreign_keys ||= []
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+
@@ -0,0 +1,23 @@
1
+ module Foreigner
2
+ module ConnectionAdapters
3
+ module SchemaStatements
4
+ def self.included(base)
5
+ base::AbstractAdapter.class_eval do
6
+ include Foreigner::ConnectionAdapters::AbstractAdapter
7
+ end
8
+ end
9
+ end
10
+
11
+ module AbstractAdapter
12
+ def supports_foreign_keys?
13
+ false
14
+ end
15
+
16
+ # Return the foreign keys for the schema_dumper
17
+ def foreign_keys(table_name)
18
+ []
19
+ end
20
+ end
21
+ end
22
+ end
23
+
@@ -0,0 +1,28 @@
1
+ module Foreigner
2
+ module ConnectionAdapters
3
+ module Sql2003
4
+ def supports_foreign_keys?
5
+ true
6
+ end
7
+
8
+ def foreign_key_definition(to_table, options = {})
9
+ column = options[:column] || "#{to_table.to_s.singularize}_id"
10
+ dependency = dependency_sql(options[:dependent])
11
+
12
+ sql = "FOREIGN KEY (#{quote_column_name(column)}) REFERENCES #{quote_table_name(to_table)}(id)"
13
+ sql << " #{dependency}" unless dependency.blank?
14
+ sql
15
+ end
16
+
17
+ private
18
+ def dependency_sql(dependency)
19
+ case dependency
20
+ when :nullify then "ON DELETE SET NULL"
21
+ when :delete then "ON DELETE CASCADE"
22
+ else ""
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+
@@ -0,0 +1,46 @@
1
+ require 'foreigner/connection_adapters/sql_2003'
2
+
3
+ module Foreigner
4
+ module ConnectionAdapters
5
+ module SQLite3Adapter
6
+ include Foreigner::ConnectionAdapters::Sql2003
7
+
8
+ def foreign_keys(table_name)
9
+ foreign_keys = []
10
+ create_table_info = select_value %{
11
+ SELECT sql
12
+ FROM sqlite_master
13
+ WHERE sql LIKE '%FOREIGN KEY%'
14
+ AND name = '#{table_name}'
15
+ }
16
+ unless create_table_info.nil?
17
+ fk_columns = create_table_info.scan(/FOREIGN KEY\s*\(\"([^\"]+)\"\)/)
18
+ fk_tables = create_table_info.scan(/REFERENCES\s*\"([^\"]+)\"/)
19
+ fk_references = create_table_info.scan(/REFERENCES[^\,]+/)
20
+ if fk_columns.size == fk_tables.size && fk_references.size == fk_columns.size
21
+ fk_columns.each_with_index do |fk_column, index|
22
+ if fk_references[index] =~ /ON DELETE CASCADE/
23
+ fk_references[index] = :delete
24
+ elsif fk_references[index] =~ /ON DELETE SET NULL/
25
+ fk_references[index] = :nullify
26
+ else
27
+ fk_references[index] = nil
28
+ end
29
+ foreign_keys << ForeignKeyDefinition.new(table_name, fk_tables[index][0], :column => fk_column[0], :dependent => fk_references[index])
30
+ end
31
+ end
32
+ end
33
+ foreign_keys
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ module ActiveRecord
40
+ module ConnectionAdapters
41
+ SQLite3Adapter.class_eval do
42
+ include Foreigner::ConnectionAdapters::SQLite3Adapter
43
+ end
44
+ end
45
+ end
46
+
@@ -0,0 +1,122 @@
1
+ module Foreigner
2
+ module SchemaDumper
3
+ def self.included(base)
4
+ base.class_eval do
5
+ include InstanceMethods
6
+ alias_method_chain :table, :foreign_keys
7
+ end
8
+ end
9
+
10
+ module InstanceMethods
11
+
12
+ def table_with_foreign_keys(table, stream)
13
+ if @connection.class == ActiveRecord::ConnectionAdapters::SQLite3Adapter
14
+ foreign_key_table(table, stream)
15
+ else
16
+ table_without_foreign_keys(table, stream)
17
+ end
18
+ end
19
+
20
+ private
21
+ # This is almost direct copy from
22
+ # active_record/schema.dumper with the add_foreign_keys method
23
+ # inserted into the middle
24
+ def foreign_key_table(table, stream)
25
+ columns = @connection.columns(table)
26
+ begin
27
+ tbl = StringIO.new
28
+
29
+ # first dump primary key column
30
+ if @connection.respond_to?(:pk_and_sequence_for)
31
+ pk, pk_seq = @connection.pk_and_sequence_for(table)
32
+ elsif @connection.respond_to?(:primary_key)
33
+ pk = @connection.primary_key(table)
34
+ end
35
+ pk ||= 'id'
36
+
37
+ tbl.print " create_table #{table.inspect}"
38
+ if columns.detect { |c| c.name == pk }
39
+ if pk != 'id'
40
+ tbl.print %Q(, :primary_key => "#{pk}")
41
+ end
42
+ else
43
+ tbl.print ", :id => false"
44
+ end
45
+ tbl.print ", :force => true"
46
+ tbl.puts " do |t|"
47
+
48
+ # then dump all non-primary key columns
49
+ column_specs = columns.map do |column|
50
+ raise StandardError, "Unknown type '#{column.sql_type}' for column '#{column.name}'" if @types[column.type].nil?
51
+ next if column.name == pk
52
+ spec = {}
53
+ spec[:name] = column.name.inspect
54
+ spec[:type] = column.type.to_s
55
+ spec[:limit] = column.limit.inspect if column.limit != @types[column.type][:limit] && column.type != :decimal
56
+ spec[:precision] = column.precision.inspect if !column.precision.nil?
57
+ spec[:scale] = column.scale.inspect if !column.scale.nil?
58
+ spec[:null] = 'false' if !column.null
59
+ spec[:default] = default_string(column.default) if column.has_default?
60
+ (spec.keys - [:name, :type]).each{ |k| spec[k].insert(0, "#{k.inspect} => ")}
61
+ spec
62
+ end.compact
63
+
64
+ # find all migration keys used in this table
65
+ keys = [:name, :limit, :precision, :scale, :default, :null] & column_specs.map(&:keys).flatten
66
+
67
+ # figure out the lengths for each column based on above keys
68
+ lengths = keys.map{ |key| column_specs.map{ |spec| spec[key] ? spec[key].length + 2 : 0 }.max }
69
+
70
+ # the string we're going to sprintf our values against, with standardized column widths
71
+ format_string = lengths.map{ |len| "%-#{len}s" }
72
+
73
+ # find the max length for the 'type' column, which is special
74
+ type_length = column_specs.map{ |column| column[:type].length }.max
75
+
76
+ # add column type definition to our format string
77
+ format_string.unshift " t.%-#{type_length}s "
78
+
79
+ format_string *= ''
80
+
81
+ column_specs.each do |colspec|
82
+ values = keys.zip(lengths).map{ |key, len| colspec.key?(key) ? colspec[key] + ", " : " " * len }
83
+ values.unshift colspec[:type]
84
+ tbl.print((format_string % values).gsub(/,\s*$/, ''))
85
+ tbl.puts
86
+ end
87
+
88
+ # add the foreign keys
89
+ add_foreign_keys(table, tbl)
90
+
91
+ tbl.puts " end"
92
+ tbl.puts
93
+
94
+ indexes(table, tbl)
95
+
96
+ tbl.rewind
97
+ stream.print tbl.read
98
+ rescue => e
99
+ stream.puts "# Could not dump table #{table.inspect} because of following #{e.class}"
100
+ stream.puts "# #{e.message}"
101
+ stream.puts
102
+ end
103
+
104
+ stream
105
+ end
106
+
107
+ def add_foreign_keys(table_name, stream)
108
+ if (foreign_keys = @connection.foreign_keys(table_name)).any?
109
+ add_foreign_key_statements = foreign_keys.map do |foreign_key|
110
+ statement_parts = [" t.foreign_key " + foreign_key.to_table.inspect]
111
+ statement_parts << (':column => ' + foreign_key.options[:column].inspect)
112
+ statement_parts << (':dependent => ' + foreign_key.options[:dependent].inspect)
113
+ ' ' + statement_parts.join(', ')
114
+ end
115
+
116
+ stream.puts add_foreign_key_statements.sort.join("\n")
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
122
+
metadata ADDED
@@ -0,0 +1,63 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sqlite-foreigner
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.0
5
+ platform: ruby
6
+ authors:
7
+ - David Wilkie
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2010-02-01 00:00:00 +07:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: Enforcable foreign key constraints for Rails migrations and SQLite databases
17
+ email: dwilkie@gmail.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - README.textile
24
+ files:
25
+ - README.textile
26
+ - lib/foreigner.rb
27
+ - lib/foreigner/schema_dumper.rb
28
+ - lib/foreigner/connection_adapters/sql_2003.rb
29
+ - lib/foreigner/connection_adapters/sqlite3_adapter.rb
30
+ - lib/foreigner/connection_adapters/abstract/schema_definitions.rb
31
+ - lib/foreigner/connection_adapters/abstract/schema_statements.rb
32
+ has_rdoc: true
33
+ homepage: http://github.com/dwilkie/sqlite3-foreigner/tree/master
34
+ licenses: []
35
+
36
+ post_install_message:
37
+ rdoc_options:
38
+ - --line-numbers
39
+ - --main
40
+ - README.textile
41
+ require_paths:
42
+ - lib
43
+ required_ruby_version: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: "0"
48
+ version:
49
+ required_rubygems_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: "0"
54
+ version:
55
+ requirements: []
56
+
57
+ rubyforge_project:
58
+ rubygems_version: 1.3.5
59
+ signing_key:
60
+ specification_version: 1
61
+ summary: Enforcable foreign key constraints for Rails migrations and SQLite databases
62
+ test_files: []
63
+