schema_plus_core 0.1.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.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.travis.yml +18 -0
- data/Gemfile +5 -0
- data/LICENSE.txt +22 -0
- data/README.md +434 -0
- data/Rakefile +9 -0
- data/gemfiles/Gemfile.base +4 -0
- data/gemfiles/activerecord-4.2/Gemfile.base +3 -0
- data/gemfiles/activerecord-4.2/Gemfile.mysql2 +10 -0
- data/gemfiles/activerecord-4.2/Gemfile.postgresql +10 -0
- data/gemfiles/activerecord-4.2/Gemfile.sqlite3 +10 -0
- data/lib/schema_plus/core.rb +27 -0
- data/lib/schema_plus/core/active_record/base.rb +22 -0
- data/lib/schema_plus/core/active_record/connection_adapters/abstract_adapter.rb +43 -0
- data/lib/schema_plus/core/active_record/connection_adapters/abstract_mysql_adapter.rb +17 -0
- data/lib/schema_plus/core/active_record/connection_adapters/mysql2_adapter.rb +64 -0
- data/lib/schema_plus/core/active_record/connection_adapters/postgresql_adapter.rb +46 -0
- data/lib/schema_plus/core/active_record/connection_adapters/sqlite3_adapter.rb +43 -0
- data/lib/schema_plus/core/active_record/connection_adapters/table_definition.rb +36 -0
- data/lib/schema_plus/core/active_record/migration/command_recorder.rb +16 -0
- data/lib/schema_plus/core/active_record/schema_dumper.rb +102 -0
- data/lib/schema_plus/core/middleware.rb +71 -0
- data/lib/schema_plus/core/schema_dump.rb +123 -0
- data/lib/schema_plus/core/sql_struct.rb +30 -0
- data/lib/schema_plus/core/version.rb +5 -0
- data/schema_dev.yml +8 -0
- data/schema_plus_core.gemspec +31 -0
- data/spec/dumper_spec.rb +53 -0
- data/spec/middleware_spec.rb +175 -0
- data/spec/spec_helper.rb +35 -0
- data/spec/sql_struct_spec.rb +29 -0
- data/spec/support/enableable.rb +30 -0
- data/spec/support/test_dumper.rb +42 -0
- data/spec/support/test_reporter.rb +57 -0
- metadata +212 -0
@@ -0,0 +1,71 @@
|
|
1
|
+
module SchemaPlus
|
2
|
+
module Core
|
3
|
+
module Middleware
|
4
|
+
module Query
|
5
|
+
module Exec
|
6
|
+
ENV = [:connection, :sql, :query_name, :binds, :result]
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
module Schema
|
11
|
+
module Tables
|
12
|
+
# :database and :like are only for mysql
|
13
|
+
# :table_name is only for sqlite3
|
14
|
+
ENV = [:connection, :query_name, :table_name, :database, :like, :tables]
|
15
|
+
end
|
16
|
+
|
17
|
+
module Indexes
|
18
|
+
ENV = [:connection, :query_name, :table_name, :index_definitions]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
module Migration
|
23
|
+
module Column
|
24
|
+
ENV = [:caller, :operation, :table_name, :column_name, :type, :options]
|
25
|
+
end
|
26
|
+
|
27
|
+
module Index
|
28
|
+
ENV = [:caller, :operation, :table_name, :column_names, :options]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
module Sql
|
33
|
+
module ColumnOptions
|
34
|
+
ENV = [:caller, :connection, :sql, :column, :options]
|
35
|
+
end
|
36
|
+
|
37
|
+
module IndexComponents
|
38
|
+
ENV = [:connection, :table_name, :column_names, :options, :sql]
|
39
|
+
end
|
40
|
+
|
41
|
+
module Table
|
42
|
+
ENV = [:caller, :connection, :table_definition, :sql]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
module Dumper
|
47
|
+
module Initial
|
48
|
+
ENV = [:dumper, :connection, :dump, :initial]
|
49
|
+
end
|
50
|
+
module Tables
|
51
|
+
ENV = [:dumper, :connection, :dump]
|
52
|
+
end
|
53
|
+
module Table
|
54
|
+
ENV = [:dumper, :connection, :dump, :table]
|
55
|
+
end
|
56
|
+
module Indexes
|
57
|
+
ENV = [:dumper, :connection, :dump, :table]
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
module Model
|
62
|
+
module Columns
|
63
|
+
ENV = [:model, :columns]
|
64
|
+
end
|
65
|
+
module ResetColumnInformation
|
66
|
+
ENV = [:model]
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
module SchemaPlus
|
2
|
+
module Core
|
3
|
+
class SchemaDump
|
4
|
+
include TSort
|
5
|
+
|
6
|
+
attr_reader :initial, :tables, :dependencies, :data
|
7
|
+
attr_accessor :final, :trailer
|
8
|
+
|
9
|
+
def initialize(dumper)
|
10
|
+
@dumper = dumper
|
11
|
+
@dependencies = Hash.new { |h, k| h[k] = [] }
|
12
|
+
@initial = []
|
13
|
+
@tables = {}
|
14
|
+
@final = []
|
15
|
+
@data = OpenStruct.new # a place for middleware to leave data
|
16
|
+
end
|
17
|
+
|
18
|
+
def depends(tablename, dependents)
|
19
|
+
@tables[tablename] ||= false # placeholder for dependencies applied before defining the table
|
20
|
+
@dependencies[tablename] += Array.wrap(dependents)
|
21
|
+
end
|
22
|
+
|
23
|
+
def assemble(stream)
|
24
|
+
stream.puts @initial.join("\n") if initial.any?
|
25
|
+
assemble_tables(stream)
|
26
|
+
final.each do |statement|
|
27
|
+
stream.puts " #{statement}"
|
28
|
+
end
|
29
|
+
stream.puts @trailer
|
30
|
+
end
|
31
|
+
|
32
|
+
def assemble_tables(stream)
|
33
|
+
tsort().each do |table|
|
34
|
+
@tables[table].assemble(stream) if @tables[table]
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def tsort_each_node(&block)
|
39
|
+
@tables.keys.sort.each(&block)
|
40
|
+
end
|
41
|
+
|
42
|
+
def tsort_each_child(tablename, &block)
|
43
|
+
@dependencies[tablename].sort.uniq.reject{|t| @dumper.ignored? t}.each(&block)
|
44
|
+
end
|
45
|
+
|
46
|
+
class Table < KeyStruct[:name, :pname, :options, :columns, :indexes, :statements, :trailer]
|
47
|
+
def initialize(*args)
|
48
|
+
super
|
49
|
+
self.columns ||= []
|
50
|
+
self.indexes ||= []
|
51
|
+
self.statements ||= []
|
52
|
+
self.trailer ||= []
|
53
|
+
end
|
54
|
+
|
55
|
+
def assemble(stream)
|
56
|
+
stream.write " create_table #{pname.inspect}"
|
57
|
+
stream.write ", #{options}" unless options.blank?
|
58
|
+
stream.puts " do |t|"
|
59
|
+
typelen = columns.map{|col| col.type.length}.max
|
60
|
+
namelen = columns.map{|col| col.name.length}.max
|
61
|
+
columns.each do |column|
|
62
|
+
stream.write " "
|
63
|
+
column.assemble(stream, typelen, namelen)
|
64
|
+
stream.puts ""
|
65
|
+
end
|
66
|
+
statements.each do |statement|
|
67
|
+
stream.puts " #{statement}"
|
68
|
+
end
|
69
|
+
stream.puts " end"
|
70
|
+
indexes.each do |index|
|
71
|
+
stream.write " add_index #{pname.inspect}, "
|
72
|
+
index.assemble(stream)
|
73
|
+
stream.puts ""
|
74
|
+
end
|
75
|
+
trailer.each do |statement|
|
76
|
+
stream.puts " #{statement}"
|
77
|
+
end
|
78
|
+
stream.puts ""
|
79
|
+
end
|
80
|
+
|
81
|
+
class Column < KeyStruct[:name, :type, :options, :comments]
|
82
|
+
|
83
|
+
def add_option(option)
|
84
|
+
self.options = [options, option].reject(&:blank?).join(', ')
|
85
|
+
end
|
86
|
+
|
87
|
+
def add_comment(comment)
|
88
|
+
self.comments = [comments, comment].reject(&:blank?).join('; ')
|
89
|
+
end
|
90
|
+
|
91
|
+
def assemble(stream, typelen, namelen)
|
92
|
+
stream.write "t.%-#{typelen}s " % type
|
93
|
+
if options.blank? && comments.blank?
|
94
|
+
stream.write name.inspect
|
95
|
+
else
|
96
|
+
pr = name.inspect
|
97
|
+
pr += "," unless options.blank?
|
98
|
+
stream.write "%-#{namelen+3}s " % pr
|
99
|
+
end
|
100
|
+
stream.write "#{options}" unless options.blank?
|
101
|
+
stream.write " " unless options.blank? or comments.blank?
|
102
|
+
stream.write "# #{comments}" unless comments.blank?
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
class Index < KeyStruct[:name, :columns, :options]
|
107
|
+
|
108
|
+
def add_option(option)
|
109
|
+
self.options = [options, option].reject(&:blank?).join(', ')
|
110
|
+
end
|
111
|
+
|
112
|
+
def assemble(stream)
|
113
|
+
stream.write [
|
114
|
+
columns.inspect,
|
115
|
+
"name: #{name.inspect}",
|
116
|
+
options
|
117
|
+
].reject(&:blank?).join(", ")
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module SchemaPlus
|
2
|
+
module Core
|
3
|
+
module SqlStruct
|
4
|
+
IndexComponents = KeyStruct[:name, :type, :columns, :options, :algorithm, :using]
|
5
|
+
|
6
|
+
class Table < KeyStruct[:command, :name, :body, :options, :quotechar]
|
7
|
+
def parse!(sql)
|
8
|
+
m = sql.strip.match %r{
|
9
|
+
^
|
10
|
+
(?<command>.*\bTABLE\b) \s*
|
11
|
+
(?<quote>['"`])(?<name>\S+)\k<quote> \s*
|
12
|
+
\( \s*
|
13
|
+
(?<body>.*) \s*
|
14
|
+
\) \s*
|
15
|
+
(?<options> \S.*)?
|
16
|
+
$
|
17
|
+
}xi
|
18
|
+
self.command = m[:command]
|
19
|
+
self.quotechar = m[:quote]
|
20
|
+
self.name = m[:name]
|
21
|
+
self.body = m[:body]
|
22
|
+
self.options = m[:options]
|
23
|
+
end
|
24
|
+
def assemble
|
25
|
+
["#{command} #{quotechar}#{name}#{quotechar} (#{body})", options].reject(&:blank?).join(" ")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/schema_dev.yml
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'schema_plus/core/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "schema_plus_core"
|
8
|
+
gem.version = SchemaPlus::Core::VERSION
|
9
|
+
gem.authors = ["ronen barzel"]
|
10
|
+
gem.email = ["ronen@barzel.org"]
|
11
|
+
gem.summary = %q{Provides an internal extension API to ActiveRecord}
|
12
|
+
gem.description = %q{Provides an internal extension API to ActiveRecord, in the form of middleware-style callback stacks}
|
13
|
+
gem.homepage = "https://github.com/SchemaPlus/schema_plus_core"
|
14
|
+
gem.license = "MIT"
|
15
|
+
|
16
|
+
gem.files = `git ls-files -z`.split("\x0")
|
17
|
+
gem.executables = gem.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
19
|
+
gem.require_paths = ["lib"]
|
20
|
+
|
21
|
+
gem.add_dependency "activerecord", "~> 4.2"
|
22
|
+
gem.add_dependency "schema_monkey", "~> 2.0"
|
23
|
+
|
24
|
+
gem.add_development_dependency "bundler", "~> 1.7"
|
25
|
+
gem.add_development_dependency "rake", "~> 10.0"
|
26
|
+
gem.add_development_dependency "rspec", "~> 3.0.0"
|
27
|
+
gem.add_development_dependency "rspec-given"
|
28
|
+
gem.add_development_dependency "schema_dev", "~> 3.1"
|
29
|
+
gem.add_development_dependency "simplecov"
|
30
|
+
gem.add_development_dependency "simplecov-gem-profile"
|
31
|
+
end
|
data/spec/dumper_spec.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe SchemaMonkey::Middleware::Dumper do
|
4
|
+
|
5
|
+
let(:migration) { ::ActiveRecord::Migration }
|
6
|
+
|
7
|
+
before(:each) do
|
8
|
+
migration.create_table "things" do |t|
|
9
|
+
t.integer :column
|
10
|
+
t.index :column
|
11
|
+
end
|
12
|
+
migration.create_table "other" do |t|
|
13
|
+
t.references :thing
|
14
|
+
end
|
15
|
+
migration.add_foreign_key("other", "things")
|
16
|
+
end
|
17
|
+
|
18
|
+
context TestDumper::Middleware::Dumper::Initial do
|
19
|
+
Then { expect(dump).to match /Schema[.]define.*do\s+#{middleware}/ }
|
20
|
+
end
|
21
|
+
|
22
|
+
context TestDumper::Middleware::Dumper::Tables do
|
23
|
+
Then { expect(dump).to match /create_table "other".*create_table "#{middleware}".*create_table "things"/m }
|
24
|
+
end
|
25
|
+
|
26
|
+
context TestDumper::Middleware::Dumper::Table do
|
27
|
+
Then { expect(dump).to match /t[.]integer.*option: #{middleware} \# comment: #{middleware}/ }
|
28
|
+
Then { expect(dump).to match /statement: #{middleware}\s+end\s+(add_index.*)?\s+trailer: #{middleware}/ }
|
29
|
+
end
|
30
|
+
|
31
|
+
context TestDumper::Middleware::Dumper::Indexes do
|
32
|
+
Then { expect(dump).to match /add_index.*#{middleware}/ }
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def middleware
|
39
|
+
described_class
|
40
|
+
end
|
41
|
+
|
42
|
+
def dump
|
43
|
+
begin
|
44
|
+
middleware.enable
|
45
|
+
stream = StringIO.new
|
46
|
+
ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream)
|
47
|
+
return stream.string
|
48
|
+
ensure
|
49
|
+
middleware.disable
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
@@ -0,0 +1,175 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe SchemaMonkey::Middleware do
|
4
|
+
|
5
|
+
let(:migration) { ::ActiveRecord::Migration }
|
6
|
+
let(:connection) { ::ActiveRecord::Base.connection }
|
7
|
+
|
8
|
+
Given {
|
9
|
+
migration.create_table "things"
|
10
|
+
class Thing < ActiveRecord::Base ; end
|
11
|
+
}
|
12
|
+
|
13
|
+
context SchemaMonkey::Middleware::Query do
|
14
|
+
|
15
|
+
Given { migration.add_column("things", "column1", "integer") }
|
16
|
+
Given(:thing) { Thing.create! }
|
17
|
+
|
18
|
+
context TestReporter::Middleware::Query::Exec do
|
19
|
+
Then { expect_middleware(enable: {sql: /SELECT column1/}) { connection.select_values("SELECT column1 FROM things") } }
|
20
|
+
Then { expect_middleware(enable: {sql: /^UPDATE/}) { thing.update_attributes!(column1: 3) } }
|
21
|
+
Then { expect_middleware(enable: {sql: /^DELETE/}) { thing.delete } }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
context SchemaMonkey::Middleware::Schema do
|
26
|
+
|
27
|
+
context TestReporter::Middleware::Schema::Tables do
|
28
|
+
Then { expect_middleware { connection.tables() } }
|
29
|
+
end
|
30
|
+
|
31
|
+
context TestReporter::Middleware::Schema::Indexes do
|
32
|
+
Then { expect_middleware { connection.indexes("things") } }
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
context SchemaMonkey::Middleware::Migration do
|
38
|
+
|
39
|
+
context TestReporter::Middleware::Migration::Column do
|
40
|
+
Given { migration.add_column("things", "column1", "integer") }
|
41
|
+
Then { expect_middleware(env: {operation: :add}) { migration.add_column("things", "column2", "integer") } }
|
42
|
+
# Note, sqlite3 emits both a :change and a :define
|
43
|
+
Then { expect_middleware(enable: {operation: :change}) { migration.change_column("things", "column1", "integer") } }
|
44
|
+
Then { expect_middleware(enable: {type: :reference}, env: {column_name: "ref_id"}) { migration.add_reference("things", "ref") } }
|
45
|
+
|
46
|
+
Given(:change) {
|
47
|
+
Class.new ::ActiveRecord::Migration do
|
48
|
+
def change
|
49
|
+
change_table("things") do |t|
|
50
|
+
t.integer "column2"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
}
|
55
|
+
Then { expect_middleware(env: {operation: :record}) { change.migrate(:down) } }
|
56
|
+
Then { expect_middleware(env: {operation: :define, type: :primary_key}) { migration.create_table "other" } }
|
57
|
+
Then { expect_middleware(env: {operation: :define}) { table_statement(:integer, "column1") } }
|
58
|
+
Then { expect_middleware(enable: {type: :reference}, env: {operation: :define, column_name: "ref_id"}) { table_statement(:references, "ref") } }
|
59
|
+
Then { expect_middleware(enable: {type: :reference}, env: {operation: :define, column_name: "ref_id"}) { table_statement(:belongs_to, "ref") } }
|
60
|
+
end
|
61
|
+
|
62
|
+
context TestReporter::Middleware::Migration::Index do
|
63
|
+
Given { migration.add_column("things", "column1", "integer") }
|
64
|
+
Then { expect_middleware { table_statement(:index, "id") } }
|
65
|
+
Then { expect_middleware { migration.add_index("things", "column1") } }
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
context SchemaMonkey::Middleware::Sql do
|
71
|
+
context TestReporter::Middleware::Sql::ColumnOptions do
|
72
|
+
Then { expect_middleware { migration.add_column("things", "column1", "integer") } }
|
73
|
+
end
|
74
|
+
|
75
|
+
context TestReporter::Middleware::Sql::IndexComponents do
|
76
|
+
Given { migration.add_column("things", "column1", "integer") }
|
77
|
+
Then { expect_middleware { migration.add_index("things", "column1") } }
|
78
|
+
end
|
79
|
+
|
80
|
+
context TestReporter::Middleware::Sql::Table do
|
81
|
+
Then { expect_middleware { migration.create_table "other" } }
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
context SchemaMonkey::Middleware::Model do
|
86
|
+
|
87
|
+
context TestReporter::Middleware::Model::Columns do
|
88
|
+
Then { expect_middleware { Thing.columns } }
|
89
|
+
end
|
90
|
+
|
91
|
+
context TestReporter::Middleware::Model::ResetColumnInformation do
|
92
|
+
Then { expect_middleware { Thing.reset_column_information } }
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
|
97
|
+
context SchemaMonkey::Middleware::Dumper do
|
98
|
+
|
99
|
+
let(:dumper) { ::ActiveRecord::SchemaDumper }
|
100
|
+
|
101
|
+
context TestReporter::Middleware::Dumper::Initial do
|
102
|
+
Then { expect_middleware { dump } }
|
103
|
+
end
|
104
|
+
|
105
|
+
context TestReporter::Middleware::Dumper::Tables do
|
106
|
+
Then { expect_middleware { dump } }
|
107
|
+
end
|
108
|
+
|
109
|
+
context TestReporter::Middleware::Dumper::Table do
|
110
|
+
Then { expect_middleware(env: {table: { name: "things"} }) { dump } }
|
111
|
+
end
|
112
|
+
|
113
|
+
context TestReporter::Middleware::Dumper::Indexes do
|
114
|
+
Then { expect_middleware(env: {table: { name: "things"} }) { dump } }
|
115
|
+
end
|
116
|
+
|
117
|
+
private
|
118
|
+
|
119
|
+
def dump
|
120
|
+
::ActiveRecord::SchemaDumper.dump(connection, StringIO.new)
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
124
|
+
|
125
|
+
def table_statement(method, *args)
|
126
|
+
migration.create_table("other", force: :cascade) do |t|
|
127
|
+
t.send method, *args
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def env_match(env, matcher, bool: false)
|
132
|
+
matcher.each do |key, val|
|
133
|
+
actual = env.send key
|
134
|
+
case val
|
135
|
+
when Hash
|
136
|
+
val.each do |subkey, subval|
|
137
|
+
subactual = actual.send subkey
|
138
|
+
if bool
|
139
|
+
return false unless subactual == subval
|
140
|
+
else
|
141
|
+
expect(subactual).to eq subval
|
142
|
+
end
|
143
|
+
end
|
144
|
+
when Regexp
|
145
|
+
if bool
|
146
|
+
return false unless actual =~ val
|
147
|
+
else
|
148
|
+
expect(actual).to match val
|
149
|
+
end
|
150
|
+
else
|
151
|
+
if bool
|
152
|
+
return false unless actual == val
|
153
|
+
else
|
154
|
+
expect(actual).to eq val
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
true if bool
|
159
|
+
end
|
160
|
+
|
161
|
+
def expect_middleware(env: {}, enable: {})
|
162
|
+
middleware = described_class
|
163
|
+
begin
|
164
|
+
middleware.enable(-> (_env) { env_match(_env, enable, bool: true) })
|
165
|
+
expect { yield }.to raise_error { |error|
|
166
|
+
expect(error).to be_a TestReporter::Called
|
167
|
+
expect(error.middleware).to eq middleware
|
168
|
+
env_match(error.env, env)
|
169
|
+
}
|
170
|
+
ensure
|
171
|
+
middleware.disable
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
end
|