active_record-mti 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +74 -0
- data/LICENSE.txt +22 -0
- data/README.md +88 -0
- data/Rakefile +2 -0
- data/active_record-mti.gemspec +26 -0
- data/lib/active_record/mti.rb +8 -0
- data/lib/active_record/mti/calculations.rb +50 -0
- data/lib/active_record/mti/connection_adapters/postgresql/schema_statements.rb +96 -0
- data/lib/active_record/mti/inheritance.rb +79 -0
- data/lib/active_record/mti/query_methods.rb +20 -0
- data/lib/active_record/mti/railtie.rb +21 -0
- data/lib/active_record/mti/schema_dumper.rb +177 -0
- data/lib/active_record/mti/version.rb +5 -0
- metadata +16 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 70e92bb458152badc92fe1d0f6aa3ac255419211
|
4
|
+
data.tar.gz: ec759e61c4042d9fa8119f3c5602fa674b226260
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8156561ef762ff46a270d9907f1df24e74824fd4314a96ca365d30d03700cf92bc8e6a96c8f68f598680b98185fbbc2642900b2b32302303e81ceda50b596ba0
|
7
|
+
data.tar.gz: 4d453baef7016ff52e81c366bb54bc8a095f35fce78186847ee605a2c2252810b258fb7e6f4e0e8bfc3be93d2623069802421cf4d0757e6f1267bf6a52002761
|
data/.gitignore
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
2
|
+
#
|
3
|
+
# If you find yourself ignoring temporary files generated by your text editor
|
4
|
+
# or operating system, you probably want to add a global ignore instead:
|
5
|
+
# git config --global core.excludesfile ~/.gitignore_global
|
6
|
+
|
7
|
+
# Database config and secrets
|
8
|
+
/config/database.yml
|
9
|
+
/config/secrets.yml
|
10
|
+
|
11
|
+
# Ignore bundler config
|
12
|
+
/.bundle
|
13
|
+
|
14
|
+
# Ignore client credentials
|
15
|
+
/config/client_api_credentials.yml
|
16
|
+
|
17
|
+
# Ignore the default SQLite database.
|
18
|
+
/db/*.sqlite3
|
19
|
+
|
20
|
+
# Ignore all logfiles and tempfiles.
|
21
|
+
/log/*.log
|
22
|
+
/tmp
|
23
|
+
/db/structure.sql
|
24
|
+
/doc/app/*
|
25
|
+
/vendor/cldr/*
|
26
|
+
/public/uploads/*
|
27
|
+
/public/photo/*
|
28
|
+
/public/test/*
|
29
|
+
/test/assets/*
|
30
|
+
/spec/assets/*
|
31
|
+
/public/assets/**
|
32
|
+
.powenv
|
33
|
+
.rvmrc
|
34
|
+
.env
|
35
|
+
.ruby-version
|
36
|
+
|
37
|
+
# Compiled source #
|
38
|
+
###################
|
39
|
+
*.com
|
40
|
+
*.class
|
41
|
+
*.dll
|
42
|
+
*.exe
|
43
|
+
*.o
|
44
|
+
*.so
|
45
|
+
|
46
|
+
# Packages #
|
47
|
+
############
|
48
|
+
# it's better to unpack these files and commit the raw source
|
49
|
+
# git has its own built in compression methods
|
50
|
+
*.7z
|
51
|
+
*.dmg
|
52
|
+
*.gz
|
53
|
+
*.iso
|
54
|
+
*.jar
|
55
|
+
*.rar
|
56
|
+
*.tar
|
57
|
+
*.zip
|
58
|
+
|
59
|
+
# Logs and databases #
|
60
|
+
######################
|
61
|
+
*.log
|
62
|
+
*.sql
|
63
|
+
*.sqlite
|
64
|
+
|
65
|
+
# OS generated files #
|
66
|
+
######################
|
67
|
+
.DS_Store
|
68
|
+
.DS_Store?
|
69
|
+
._*
|
70
|
+
.Spotlight-V100
|
71
|
+
.Trashes
|
72
|
+
Icon?
|
73
|
+
ehthumbs.db
|
74
|
+
Thumbs.db
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2016 Dale Stevens
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
# ActiveRecord::MTI
|
2
|
+
|
3
|
+
Allows for true native inheritance of tables in PostgreSQL
|
4
|
+
|
5
|
+
Currently requires Rails 4.2
|
6
|
+
|
7
|
+
## Usage
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
gem 'active_record-mti'
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
|
19
|
+
$ gem install active_record-mti
|
20
|
+
|
21
|
+
### Migrations
|
22
|
+
|
23
|
+
In your migrations define a table to inherit from another table:
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
class CreateAccounts < ActiveRecord::Migration
|
27
|
+
def change
|
28
|
+
# Things is the head of or inheritance tree representing all things
|
29
|
+
# both tangible and intangible. Can be considered the vertices in
|
30
|
+
# the graph.
|
31
|
+
create_table :accounts do |t|
|
32
|
+
t.jsonb :settings
|
33
|
+
t.timestamps
|
34
|
+
end
|
35
|
+
|
36
|
+
create_table :users, inherits: :accounts do |t|
|
37
|
+
t.string :firstname
|
38
|
+
t.string :lastname
|
39
|
+
end
|
40
|
+
|
41
|
+
create_table :developers, inherits: :users do |t|
|
42
|
+
t.string :url
|
43
|
+
t.string :api_key
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
```
|
49
|
+
|
50
|
+
### Schema.rb
|
51
|
+
|
52
|
+
A schema will be created that reflects the inheritance chain so that rake:db:schema:load will work
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
ctiveRecord::Schema.define(version: 20160910024954) do
|
56
|
+
|
57
|
+
create_table "accounts", force: :cascade do |t|
|
58
|
+
t.jsonb "settings"
|
59
|
+
t.datetime "created_at"
|
60
|
+
t.datetime "updated_at"
|
61
|
+
end
|
62
|
+
|
63
|
+
create_table "users", inherits: "accounts" do |t|
|
64
|
+
t.string "firstname"
|
65
|
+
t.string "lastname"
|
66
|
+
end
|
67
|
+
|
68
|
+
create_table "developers", inherits: "users" do |t|
|
69
|
+
t.string "url"
|
70
|
+
t.string "api_key"
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
```
|
75
|
+
|
76
|
+
### In your application code
|
77
|
+
|
78
|
+
ActiveRecord queries work as usual with the following differences:
|
79
|
+
|
80
|
+
* The default query of "*" is changed to include the OID of each row for subclass discrimination. The default select will be `SELECT cast("accounts"."tableoid"::regclass AS text), "accounts".*`
|
81
|
+
|
82
|
+
## Contributing
|
83
|
+
|
84
|
+
1. Fork it ( https://github.com/[my-github-username]/active_record-mti/fork )
|
85
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
86
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
87
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
88
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'active_record/mti/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "active_record-mti"
|
8
|
+
spec.version = ActiveRecord::MTI::VERSION
|
9
|
+
spec.authors = ["Dale Stevens"]
|
10
|
+
spec.email = ["dale@twilightcoders.net"]
|
11
|
+
spec.summary = %q{Multi Table Inheritance for PostgreSQL in Rails}
|
12
|
+
spec.description = %q{Allows use of native inherited tables in PostgreSQL}
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.6"
|
22
|
+
spec.add_development_dependency 'rake', '~> 0'
|
23
|
+
spec.add_runtime_dependency 'rails', '~> 4.1', '> 4.1'
|
24
|
+
spec.add_runtime_dependency 'pg', '~> 0'
|
25
|
+
|
26
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module MTI
|
3
|
+
module Calculations
|
4
|
+
|
5
|
+
private
|
6
|
+
|
7
|
+
def execute_simple_calculation(operation, column_name, distinct) #:nodoc:
|
8
|
+
# Postgresql doesn't like ORDER BY when there are no GROUP BY
|
9
|
+
relation = unscope(:order)
|
10
|
+
|
11
|
+
column_alias = column_name
|
12
|
+
|
13
|
+
bind_values = nil
|
14
|
+
|
15
|
+
if operation == "count" && (relation.limit_value || relation.offset_value)
|
16
|
+
# Shortcut when limit is zero.
|
17
|
+
return 0 if relation.limit_value == 0
|
18
|
+
|
19
|
+
query_builder = build_count_subquery(relation, column_name, distinct)
|
20
|
+
bind_values = query_builder.bind_values + relation.bind_values
|
21
|
+
else
|
22
|
+
column = aggregate_column(column_name)
|
23
|
+
|
24
|
+
select_value = operation_over_aggregate_column(column, operation, distinct)
|
25
|
+
|
26
|
+
column_alias = select_value.alias
|
27
|
+
column_alias ||= @klass.connection.column_name_for_operation(operation, select_value)
|
28
|
+
relation.select_values = [select_value]
|
29
|
+
|
30
|
+
# Only use the last projection (probably the COUNT(*)) all others don't matter
|
31
|
+
# relation.arel.projections = [relation.arel.projections.last].compact if @klass.using_multi_table_inheritance?
|
32
|
+
relation.arel.projections.shift if @klass.using_multi_table_inheritance?
|
33
|
+
|
34
|
+
query_builder = relation.arel
|
35
|
+
bind_values = query_builder.bind_values + relation.bind_values
|
36
|
+
end
|
37
|
+
|
38
|
+
result = @klass.connection.select_all(query_builder, nil, bind_values)
|
39
|
+
row = result.first
|
40
|
+
value = row && row.values.first
|
41
|
+
column = result.column_types.fetch(column_alias) do
|
42
|
+
type_for(column_name)
|
43
|
+
end
|
44
|
+
|
45
|
+
type_cast_calculated_value(value, column, operation)
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module MTI
|
3
|
+
module ConnectionAdapters
|
4
|
+
module PostgreSQL
|
5
|
+
module SchemaStatements
|
6
|
+
# Creates a new table with the name +table_name+. +table_name+ may either
|
7
|
+
# be a String or a Symbol.
|
8
|
+
#
|
9
|
+
# Add :inherits options for Postgres table inheritance. If a table is inherited then
|
10
|
+
# the primary key column is also inherited. Therefore the :primary_key options is set to false
|
11
|
+
# so we don't duplicate that colume.
|
12
|
+
#
|
13
|
+
# However the primary key column from the parent is not inherited as primary key so
|
14
|
+
# we manually add it. Lastly we also create indexes on the child table to match those
|
15
|
+
# on the parent table since indexes are also not inherited.
|
16
|
+
def create_table(table_name, options = {})
|
17
|
+
if options[:inherits]
|
18
|
+
options[:id] = false
|
19
|
+
options.delete(:primary_key)
|
20
|
+
end
|
21
|
+
|
22
|
+
if schema = options.delete(:schema)
|
23
|
+
# If we specify a schema then we only create it if it doesn't exist
|
24
|
+
# and we only force create it if only the specific schema is in the search path
|
25
|
+
table_name = "#{schema}.#{table_name}"
|
26
|
+
end
|
27
|
+
|
28
|
+
if parent_table = options.delete(:inherits)
|
29
|
+
options[:options] = ["INHERITS (#{parent_table})", options[:options]].compact.join
|
30
|
+
end
|
31
|
+
|
32
|
+
td = create_table_definition table_name, options[:temporary], options[:options], options[:as]
|
33
|
+
|
34
|
+
if options[:id] != false && !options[:as]
|
35
|
+
pk = options.fetch(:primary_key) do
|
36
|
+
Base.get_primary_key table_name.to_s.singularize
|
37
|
+
end
|
38
|
+
|
39
|
+
if pk.is_a?(Array)
|
40
|
+
td.primary_keys pk
|
41
|
+
else
|
42
|
+
td.primary_key pk, options.fetch(:id, :primary_key), options
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
yield td if block_given?
|
47
|
+
|
48
|
+
if options[:force] && data_source_exists?(table_name)
|
49
|
+
drop_table(table_name, options)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Rails 5 wont create an empty column list which we might have if we're
|
53
|
+
# working with inherited tables. So we need to do that manually
|
54
|
+
sql = schema_creation.accept(td)
|
55
|
+
# sql = sql.sub("INHERITS", "() INHERITS") if td.columns.empty?
|
56
|
+
|
57
|
+
result = execute sql
|
58
|
+
|
59
|
+
if parent_table
|
60
|
+
parent_table_primary_key = primary_key(parent_table)
|
61
|
+
execute "ALTER TABLE #{table_name} ADD PRIMARY KEY (#{parent_table_primary_key})"
|
62
|
+
indexes(parent_table).each do |index|
|
63
|
+
add_index table_name, index.columns, :unique => index.unique
|
64
|
+
end
|
65
|
+
# triggers_for_table(parent_table).each do |trigger|
|
66
|
+
# name = trigger.first
|
67
|
+
# definition = trigger.second.merge(on: table_name)
|
68
|
+
# create_trigger name, definition
|
69
|
+
# end
|
70
|
+
end
|
71
|
+
|
72
|
+
td.indexes.each_pair { |c,o| add_index table_name, c, o }
|
73
|
+
end
|
74
|
+
|
75
|
+
# Parent of inherited table
|
76
|
+
def parent_tables(table_name)
|
77
|
+
sql = <<-SQL
|
78
|
+
SELECT pg_namespace.nspname, pg_class.relname
|
79
|
+
FROM pg_catalog.pg_inherits
|
80
|
+
INNER JOIN pg_catalog.pg_class ON (pg_inherits.inhparent = pg_class.oid)
|
81
|
+
INNER JOIN pg_catalog.pg_namespace ON (pg_class.relnamespace = pg_namespace.oid)
|
82
|
+
WHERE inhrelid = '#{table_name}'::regclass
|
83
|
+
SQL
|
84
|
+
result = exec_query(sql, "SCHEMA")
|
85
|
+
result.map{|a| a['relname']}
|
86
|
+
end
|
87
|
+
|
88
|
+
def parent_table(table_name)
|
89
|
+
parents = parent_tables(table_name)
|
90
|
+
parents.first
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
# == Multi table inheritance
|
3
|
+
#
|
4
|
+
# PostgreSQL allows for table inheritance. To enable this in ActiveRecord, ensure that the
|
5
|
+
# inheritance_column is named "tableoid" (can be changed by setting <tt>Base.inheritance_column</tt>).
|
6
|
+
# This means that an inheritance looking like this:
|
7
|
+
#
|
8
|
+
# class Company < ActiveRecord::Base;
|
9
|
+
# self.inheritance_column = 'tableoid'
|
10
|
+
# end
|
11
|
+
# class Firm < Company; end
|
12
|
+
# class Client < Company; end
|
13
|
+
# class PriorityClient < Client; end
|
14
|
+
#
|
15
|
+
# When you do <tt>Firm.create(name: "37signals")</tt>, this record will be saved in
|
16
|
+
# the firms table which inherits from companies. You can then fetch this row again using
|
17
|
+
# <tt>Company.where(name: '37signals').first</tt> and it will return a Firm object.
|
18
|
+
#
|
19
|
+
# Note, all the attributes for all the cases are kept in the same table. Read more:
|
20
|
+
# http://www.martinfowler.com/eaaCatalog/singleTableInheritance.html
|
21
|
+
#
|
22
|
+
module MTI
|
23
|
+
module Inheritance
|
24
|
+
extend ActiveSupport::Concern
|
25
|
+
|
26
|
+
module ClassMethods
|
27
|
+
|
28
|
+
# We know we're using multi-table inheritance if the inheritance_column is not actually
|
29
|
+
# present in the DB structure. Thereby implying the inheritance_column is inferred.
|
30
|
+
# To further isolate usage of multi-table inheritance, the inheritance column must be set
|
31
|
+
# to 'tableoid'
|
32
|
+
def using_multi_table_inheritance?(klass = self)
|
33
|
+
@using_multi_table_inheritance ||= if klass.columns_hash.include?(klass.inheritance_column)
|
34
|
+
false
|
35
|
+
elsif klass.inheritance_column == 'tableoid' && (klass.descendants.select{ |d| d.table_name != klass.table_name }.any?)
|
36
|
+
true
|
37
|
+
else
|
38
|
+
false
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
# Called by +instantiate+ to decide which class to use for a new
|
45
|
+
# record instance. For single-table inheritance, we check the record
|
46
|
+
# for a +type+ column and return the corresponding class.
|
47
|
+
def discriminate_class_for_record(record)
|
48
|
+
if using_single_table_inheritance?(record)
|
49
|
+
find_sti_class(record[inheritance_column])
|
50
|
+
elsif using_multi_table_inheritance?(base_class)
|
51
|
+
find_mti_class(record)
|
52
|
+
else
|
53
|
+
super
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Search descendants for one who's table_name is equal to the returned tableoid.
|
58
|
+
# This indicates the class of the record
|
59
|
+
def find_mti_class(record)
|
60
|
+
descendants.find(Proc.new{ self }) { |d| d.table_name == record['tableoid'] }
|
61
|
+
end
|
62
|
+
|
63
|
+
# Type condition only applies if it's STI, otherwise it's
|
64
|
+
# done for free by querying the inherited table in MTI
|
65
|
+
def type_condition(table = arel_table)
|
66
|
+
if using_multi_table_inheritance?
|
67
|
+
nil
|
68
|
+
else
|
69
|
+
sti_column = table[inheritance_column]
|
70
|
+
sti_names = ([self] + descendants).map { |model| model.sti_name }
|
71
|
+
|
72
|
+
sti_column.in(sti_names)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module MTI
|
3
|
+
module QueryMethods
|
4
|
+
|
5
|
+
private
|
6
|
+
|
7
|
+
# Retrieve the OID as well on a default select
|
8
|
+
def build_select(arel)
|
9
|
+
arel.project("cast(\"#{klass.table_name}\".\"tableoid\"::regclass as text)") if @klass.using_multi_table_inheritance?
|
10
|
+
# arel.project("\"#{klass.table_name}\".\"tableoid\"::regclass as \"#{klass.inheritance_column}\"") if @klass.using_multi_table_inheritance?
|
11
|
+
if select_values.any?
|
12
|
+
arel.project(*arel_columns(select_values.uniq))
|
13
|
+
else
|
14
|
+
arel.project(@klass.arel_table[Arel.star])
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'active_record/mti/schema_dumper'
|
2
|
+
require 'active_record/mti/inheritance'
|
3
|
+
require 'active_record/mti/query_methods'
|
4
|
+
require 'active_record/mti/calculations'
|
5
|
+
require 'active_record/mti/connection_adapters/postgresql/schema_statements'
|
6
|
+
|
7
|
+
module ActiveRecord
|
8
|
+
module MTI
|
9
|
+
class Railtie < Rails::Railtie
|
10
|
+
initializer 'active_record-mti.inheritance.initialization' do |_app|
|
11
|
+
::ActiveRecord::Base.send :include, Inheritance
|
12
|
+
::ActiveRecord::Relation.send :include, QueryMethods
|
13
|
+
::ActiveRecord::Relation.send :include, ActiveRecord::MTI::Calculations
|
14
|
+
|
15
|
+
::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send :include, ConnectionAdapters::PostgreSQL::SchemaStatements
|
16
|
+
::ActiveRecord::SchemaDumper.send :include, ActiveRecord::MTI::SchemaDumper
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
@@ -0,0 +1,177 @@
|
|
1
|
+
# Modified SchemaDumper that knows how to dump
|
2
|
+
# inherited tables. Key is that we have to dump parent
|
3
|
+
# tables before we dump child tables (of course).
|
4
|
+
# In addition we have to make sure we don't dump columns
|
5
|
+
# that are inherited.
|
6
|
+
module ActiveRecord
|
7
|
+
# = Active Record Schema Dumper
|
8
|
+
#
|
9
|
+
# This class is used to dump the database schema for some connection to some
|
10
|
+
# output format (i.e., ActiveRecord::Schema).
|
11
|
+
module MTI
|
12
|
+
module SchemaDumper #:nodoc:
|
13
|
+
extend ActiveSupport::Concern
|
14
|
+
|
15
|
+
|
16
|
+
included do
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def dumped_tables
|
21
|
+
@dumped_tables ||= []
|
22
|
+
end
|
23
|
+
|
24
|
+
# Output table and columns - but don't output columns that are inherited from
|
25
|
+
# a parent table.
|
26
|
+
#
|
27
|
+
# TODO: Qualify with the schema name IF the table is in a schema other than the first
|
28
|
+
# schema in the search path (not including the $user schema)
|
29
|
+
def table(table, stream)
|
30
|
+
return if already_dumped?(table)
|
31
|
+
if parent_table = @connection.parent_table(table)
|
32
|
+
table(parent_table, stream)
|
33
|
+
parent_column_names = @connection.columns(parent_table).map(&:name)
|
34
|
+
end
|
35
|
+
|
36
|
+
columns = @connection.columns(table)
|
37
|
+
begin
|
38
|
+
tbl = StringIO.new
|
39
|
+
|
40
|
+
# first dump primary key column
|
41
|
+
pk = @connection.primary_key(table)
|
42
|
+
|
43
|
+
tbl.print " create_table #{remove_prefix_and_suffix(table).inspect}"
|
44
|
+
if parent_table
|
45
|
+
tbl.print %Q(, inherits: "#{parent_table}")
|
46
|
+
else
|
47
|
+
pkcol = columns.detect { |c| c.name == pk }
|
48
|
+
if pkcol
|
49
|
+
if pk != 'id'
|
50
|
+
tbl.print %Q(, primary_key: "#{pk}")
|
51
|
+
elsif pkcol.sql_type == 'bigint'
|
52
|
+
tbl.print ", id: :bigserial"
|
53
|
+
elsif pkcol.sql_type == 'uuid'
|
54
|
+
tbl.print ", id: :uuid"
|
55
|
+
tbl.print %Q(, default: #{pkcol.default_function.inspect})
|
56
|
+
end
|
57
|
+
else
|
58
|
+
tbl.print ", id: false"
|
59
|
+
end
|
60
|
+
tbl.print ", force: :cascade"
|
61
|
+
end
|
62
|
+
tbl.puts " do |t|"
|
63
|
+
|
64
|
+
# then dump all non-primary key columns
|
65
|
+
column_specs = columns.map do |column|
|
66
|
+
raise StandardError, "Unknown type '#{column.sql_type}' for column '#{column.name}'" unless @connection.valid_type?(column.type)
|
67
|
+
next if column.name == pk
|
68
|
+
|
69
|
+
# Except columns in parent table
|
70
|
+
next if parent_column_names && parent_column_names.include?(column.name)
|
71
|
+
|
72
|
+
@connection.column_spec(column, @types)
|
73
|
+
end.compact
|
74
|
+
|
75
|
+
# find all migration keys used in this table
|
76
|
+
keys = @connection.migration_keys
|
77
|
+
|
78
|
+
# figure out the lengths for each column based on above keys
|
79
|
+
lengths = keys.map { |key|
|
80
|
+
column_specs.map { |spec|
|
81
|
+
spec[key] ? spec[key].length + 2 : 0
|
82
|
+
}.max
|
83
|
+
}
|
84
|
+
|
85
|
+
# the string we're going to sprintf our values against, with standardized column widths
|
86
|
+
format_string = lengths.map{ |len| "%-#{len}s" }
|
87
|
+
|
88
|
+
# find the max length for the 'type' column, which is special
|
89
|
+
type_length = column_specs.map{ |column| column[:type].length }.max
|
90
|
+
|
91
|
+
# add column type definition to our format string
|
92
|
+
format_string.unshift " t.%-#{type_length}s "
|
93
|
+
|
94
|
+
format_string *= ''
|
95
|
+
|
96
|
+
column_specs.each do |colspec|
|
97
|
+
values = keys.zip(lengths).map{ |key, len| colspec.key?(key) ? colspec[key] + ", " : " " * len }
|
98
|
+
values.unshift colspec[:type]
|
99
|
+
tbl.print((format_string % values).gsub(/,\s*$/, ''))
|
100
|
+
tbl.puts
|
101
|
+
end
|
102
|
+
|
103
|
+
tbl.puts " end"
|
104
|
+
tbl.puts
|
105
|
+
|
106
|
+
indexes(table, tbl)
|
107
|
+
|
108
|
+
tbl.rewind
|
109
|
+
stream.print tbl.read
|
110
|
+
rescue => e
|
111
|
+
stream.puts "# Could not dump table #{table.inspect} because of following #{e.class}"
|
112
|
+
stream.puts "# #{e.message}"
|
113
|
+
stream.puts
|
114
|
+
end
|
115
|
+
|
116
|
+
dumped_tables << table
|
117
|
+
stream
|
118
|
+
end
|
119
|
+
|
120
|
+
# Output indexes but don't output indexes that are inherited from parent tables
|
121
|
+
# since those will be created by create_table.
|
122
|
+
def indexes(table, stream)
|
123
|
+
if (indexes = @connection.indexes(table)).any?
|
124
|
+
if parent_table = @connection.parent_table(table)
|
125
|
+
parent_indexes = @connection.indexes(parent_table)
|
126
|
+
end
|
127
|
+
|
128
|
+
indexes.delete_if {|i| is_parent_index?(i, parent_indexes) } if parent_indexes
|
129
|
+
return if indexes.empty?
|
130
|
+
|
131
|
+
add_index_statements = indexes.map do |index|
|
132
|
+
statement_parts = [
|
133
|
+
('add_index ' + remove_prefix_and_suffix(index.table).inspect),
|
134
|
+
index.columns.inspect,
|
135
|
+
('name: ' + index.name.inspect),
|
136
|
+
]
|
137
|
+
statement_parts << 'unique: true' if index.unique
|
138
|
+
|
139
|
+
index_lengths = (index.lengths || []).compact
|
140
|
+
statement_parts << ('length: ' + Hash[index.columns.zip(index.lengths)].inspect) unless index_lengths.empty?
|
141
|
+
|
142
|
+
index_orders = (index.orders || {})
|
143
|
+
statement_parts << ('order: ' + index.orders.inspect) unless index_orders.empty?
|
144
|
+
|
145
|
+
statement_parts << ('where: ' + index.where.inspect) if index.where
|
146
|
+
|
147
|
+
statement_parts << ('using: ' + index.using.inspect) if index.using
|
148
|
+
|
149
|
+
statement_parts << ('type: ' + index.type.inspect) if index.type
|
150
|
+
|
151
|
+
' ' + statement_parts.join(', ')
|
152
|
+
end
|
153
|
+
|
154
|
+
stream.puts add_index_statements.sort.join("\n")
|
155
|
+
stream.puts
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
|
160
|
+
def remove_prefix_and_suffix(table)
|
161
|
+
table.gsub(/^(#{ActiveRecord::Base.table_name_prefix})(.+)(#{ActiveRecord::Base.table_name_suffix})$/, "\\2")
|
162
|
+
end
|
163
|
+
|
164
|
+
def already_dumped?(table)
|
165
|
+
dumped_tables.include? table
|
166
|
+
end
|
167
|
+
|
168
|
+
def is_parent_index?(index, parent_indexes)
|
169
|
+
parent_indexes.each do |pindex|
|
170
|
+
return true if pindex.columns == index.columns
|
171
|
+
end
|
172
|
+
return false
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: active_record-mti
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dale Stevens
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-09-
|
11
|
+
date: 2016-09-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -78,7 +78,20 @@ email:
|
|
78
78
|
executables: []
|
79
79
|
extensions: []
|
80
80
|
extra_rdoc_files: []
|
81
|
-
files:
|
81
|
+
files:
|
82
|
+
- ".gitignore"
|
83
|
+
- LICENSE.txt
|
84
|
+
- README.md
|
85
|
+
- Rakefile
|
86
|
+
- active_record-mti.gemspec
|
87
|
+
- lib/active_record/mti.rb
|
88
|
+
- lib/active_record/mti/calculations.rb
|
89
|
+
- lib/active_record/mti/connection_adapters/postgresql/schema_statements.rb
|
90
|
+
- lib/active_record/mti/inheritance.rb
|
91
|
+
- lib/active_record/mti/query_methods.rb
|
92
|
+
- lib/active_record/mti/railtie.rb
|
93
|
+
- lib/active_record/mti/schema_dumper.rb
|
94
|
+
- lib/active_record/mti/version.rb
|
82
95
|
homepage: ''
|
83
96
|
licenses:
|
84
97
|
- MIT
|