sqlite-foreigner 0.5.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.
@@ -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
+