active_record-mti 0.0.1 → 0.0.2
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.
- 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
|