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.
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