schema_plus_core 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/.travis.yml +18 -0
  4. data/Gemfile +5 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +434 -0
  7. data/Rakefile +9 -0
  8. data/gemfiles/Gemfile.base +4 -0
  9. data/gemfiles/activerecord-4.2/Gemfile.base +3 -0
  10. data/gemfiles/activerecord-4.2/Gemfile.mysql2 +10 -0
  11. data/gemfiles/activerecord-4.2/Gemfile.postgresql +10 -0
  12. data/gemfiles/activerecord-4.2/Gemfile.sqlite3 +10 -0
  13. data/lib/schema_plus/core.rb +27 -0
  14. data/lib/schema_plus/core/active_record/base.rb +22 -0
  15. data/lib/schema_plus/core/active_record/connection_adapters/abstract_adapter.rb +43 -0
  16. data/lib/schema_plus/core/active_record/connection_adapters/abstract_mysql_adapter.rb +17 -0
  17. data/lib/schema_plus/core/active_record/connection_adapters/mysql2_adapter.rb +64 -0
  18. data/lib/schema_plus/core/active_record/connection_adapters/postgresql_adapter.rb +46 -0
  19. data/lib/schema_plus/core/active_record/connection_adapters/sqlite3_adapter.rb +43 -0
  20. data/lib/schema_plus/core/active_record/connection_adapters/table_definition.rb +36 -0
  21. data/lib/schema_plus/core/active_record/migration/command_recorder.rb +16 -0
  22. data/lib/schema_plus/core/active_record/schema_dumper.rb +102 -0
  23. data/lib/schema_plus/core/middleware.rb +71 -0
  24. data/lib/schema_plus/core/schema_dump.rb +123 -0
  25. data/lib/schema_plus/core/sql_struct.rb +30 -0
  26. data/lib/schema_plus/core/version.rb +5 -0
  27. data/schema_dev.yml +8 -0
  28. data/schema_plus_core.gemspec +31 -0
  29. data/spec/dumper_spec.rb +53 -0
  30. data/spec/middleware_spec.rb +175 -0
  31. data/spec/spec_helper.rb +35 -0
  32. data/spec/sql_struct_spec.rb +29 -0
  33. data/spec/support/enableable.rb +30 -0
  34. data/spec/support/test_dumper.rb +42 -0
  35. data/spec/support/test_reporter.rb +57 -0
  36. 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
@@ -0,0 +1,5 @@
1
+ module SchemaPlus
2
+ module Core
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
data/schema_dev.yml ADDED
@@ -0,0 +1,8 @@
1
+ ruby:
2
+ - 2.1.5
3
+ activerecord:
4
+ - 4.2
5
+ db:
6
+ - mysql2
7
+ - sqlite3
8
+ - postgresql
@@ -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
@@ -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