chrono_model 0.3.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.
@@ -0,0 +1,7 @@
1
+ # CREATE ROLE chronomodel LOGIN ENCRYPTED PASSWORD 'chronomodel' CREATEDB;
2
+ #
3
+ hostname: localhost
4
+ username: chronomodel
5
+ password: chronomodel
6
+ database: chronomodel
7
+ #
@@ -0,0 +1,22 @@
1
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
2
+ #
3
+ require 'chrono_model'
4
+
5
+ require 'support/connection'
6
+ require 'support/matchers/schema'
7
+ require 'support/matchers/table'
8
+ require 'support/matchers/column'
9
+ require 'support/matchers/index'
10
+
11
+ RSpec.configure do |config|
12
+ config.treat_symbols_as_metadata_keys_with_true_values = true
13
+
14
+ config.include(ChronoTest::Matchers::Schema)
15
+ config.include(ChronoTest::Matchers::Table)
16
+ config.include(ChronoTest::Matchers::Column)
17
+ config.include(ChronoTest::Matchers::Index)
18
+
19
+ config.before :suite do
20
+ ChronoTest.recreate_database!
21
+ end
22
+ end
@@ -0,0 +1,74 @@
1
+ require 'pathname'
2
+ require 'active_record'
3
+ require 'active_support/core_ext/logger'
4
+
5
+ module ChronoTest
6
+ extend self
7
+
8
+ AR = ActiveRecord::Base
9
+ log = ENV['VERBOSE'].present? ? $stderr : 'spec/debug.log'.tap{|f| File.truncate(f, 0)}
10
+ AR.logger = ::Logger.new(log).tap do |l|
11
+ l.level = 0
12
+ end
13
+
14
+ def connect!(spec = self.config)
15
+ unless ENV['VERBOSE'].present?
16
+ spec = spec.merge(:min_messages => 'WARNING')
17
+ end
18
+ AR.establish_connection spec
19
+ end
20
+
21
+ def logger
22
+ AR.logger
23
+ end
24
+
25
+ def connection
26
+ AR.connection
27
+ end
28
+
29
+ def recreate_database!
30
+ database = config.fetch(:database)
31
+ connect! config.merge(:database => 'postgres')
32
+
33
+ unless AR.supports_chrono?
34
+ raise 'Your postgresql version is not supported. >= 9.0 is required.'
35
+ end
36
+
37
+ connection.drop_database database
38
+ connection.create_database database
39
+
40
+ ensure
41
+ connect!
42
+ logger.info "Connected to #{config}"
43
+ end
44
+
45
+ def config
46
+ @config ||= YAML.load(config_file.read).tap do |conf|
47
+ conf.symbolize_keys!
48
+ conf.update(:adapter => 'postgresql')
49
+
50
+ def conf.to_s
51
+ 'pgsql://%s:%s@%s/%s' % [
52
+ self[:username], self[:password], self[:hostname], self[:database]
53
+ ]
54
+ end
55
+ end
56
+
57
+ rescue Errno::ENOENT
58
+ $stderr.puts <<EOM
59
+
60
+ Please define your AR database configuration
61
+ in spec/config.yml or reference your own configuration
62
+ file using the TEST_CONFIG environment variable
63
+ EOM
64
+
65
+ abort
66
+ end
67
+
68
+ def config_file
69
+ Pathname(ENV['TEST_CONFIG'] ||
70
+ File.join(File.dirname(__FILE__), '..', 'config.yml')
71
+ )
72
+ end
73
+
74
+ end
@@ -0,0 +1,123 @@
1
+ module ChronoTest::Helpers
2
+
3
+ module Adapter
4
+ def self.included(base)
5
+ base.let!(:adapter) { ChronoTest.connection }
6
+ base.extend DSL
7
+ end
8
+
9
+ def columns
10
+ DSL.columns
11
+ end
12
+
13
+ module DSL
14
+ def with_temporal_table(&block)
15
+ context ':temporal => true' do
16
+ before(:all) { adapter.create_table(table, :temporal => true, &columns) }
17
+ after(:all) { adapter.drop_table table }
18
+
19
+ instance_eval(&block)
20
+ end
21
+ end
22
+
23
+ def with_plain_table(&block)
24
+ context ':temporal => false' do
25
+ before(:all) { adapter.create_table(table, :temporal => false, &columns) }
26
+ after(:all) { adapter.drop_table table }
27
+
28
+ instance_eval(&block)
29
+ end
30
+ end
31
+
32
+ def columns(&block)
33
+ DSL.columns = block.call
34
+ end
35
+
36
+ class << self
37
+ attr_accessor :columns
38
+ end
39
+ end
40
+ end
41
+
42
+ module TimeMachine
43
+ def self.included(base)
44
+ base.let!(:adapter) { ChronoTest.connection }
45
+ base.extend(DSL)
46
+ end
47
+
48
+ module DSL
49
+ def setup_schema!
50
+ # Set up database structure
51
+ #
52
+ before(:all) do
53
+ adapter.create_table 'foos', :temporal => true do |t|
54
+ t.string :name
55
+ t.integer :fooity
56
+ end
57
+
58
+ adapter.create_table 'bars', :temporal => true do |t|
59
+ t.string :name
60
+ t.references :foo
61
+ end
62
+
63
+ adapter.create_table 'bazs' do |t|
64
+ t.string :name
65
+ t.references :bar
66
+ end
67
+ end
68
+
69
+ after(:all) do
70
+ adapter.drop_table 'foos'
71
+ adapter.drop_table 'bars'
72
+ adapter.drop_table 'bazs'
73
+ end
74
+ end
75
+
76
+ Models = lambda {
77
+ class ::Foo < ActiveRecord::Base
78
+ include ChronoModel::TimeMachine
79
+
80
+ has_many :bars
81
+ end
82
+
83
+ class ::Bar < ActiveRecord::Base
84
+ include ChronoModel::TimeMachine
85
+
86
+ belongs_to :foo
87
+ has_one :baz
88
+ end
89
+
90
+ class ::Baz < ActiveRecord::Base
91
+ belongs_to :baz
92
+ end
93
+ }
94
+
95
+ def define_models!
96
+ before(:all) { Models.call }
97
+ end
98
+
99
+ end
100
+
101
+ # If a context object is given, evaluates the given
102
+ # block in its instance context, then defines a `ts`
103
+ # on it, backed by an Array, and adds the current
104
+ # database timestamp to it.
105
+ #
106
+ # If a context object is not given, the block is
107
+ # evaluated in the current context and the above
108
+ # mangling is done on the blocks' return value.
109
+ #
110
+ def ts_eval(ctx = nil, &block)
111
+ ret = (ctx || self).instance_eval(&block)
112
+ (ctx || ret).tap do |obj|
113
+ obj.singleton_class.instance_eval do
114
+ define_method(:ts) { @_ts ||= [] }
115
+ end unless obj.methods.include?(:ts)
116
+
117
+ now = adapter.select_value('select now()::timestamp')
118
+ obj.ts.push(Time.parse(now))
119
+ end
120
+ end
121
+ end
122
+
123
+ end
@@ -0,0 +1,59 @@
1
+ require 'ostruct'
2
+
3
+ module ChronoTest::Matchers
4
+ class Base
5
+ attr_reader :table
6
+
7
+ def matches?(table)
8
+ table = table.table_name if table.respond_to?(:table_name)
9
+ @table = table
10
+ end
11
+ private :matches? # This is an abstract class
12
+
13
+ def failure_message_for_should_not
14
+ failure_message_for_should.gsub(/to /, 'to not ')
15
+ end
16
+
17
+ protected
18
+ def connection
19
+ ChronoTest.connection
20
+ end
21
+
22
+ def temporal_schema
23
+ ChronoModel::Adapter::TEMPORAL_SCHEMA
24
+ end
25
+
26
+ def history_schema
27
+ ChronoModel::Adapter::HISTORY_SCHEMA
28
+ end
29
+
30
+ def public_schema
31
+ 'public'
32
+ end
33
+
34
+ def select_value(sql, binds, name = nil)
35
+ result = exec_query(sql, binds, name || 'select_value')
36
+ result.rows.first.try(:[], 0)
37
+ end
38
+
39
+ def select_values(sql, binds, name = nil)
40
+ result = exec_query(sql, binds, name || 'select_values')
41
+ result.rows.map(&:first)
42
+ end
43
+
44
+ def select_rows(sql, binds, name = nil)
45
+ exec_query(sql, binds, name || 'select_rows').rows
46
+ end
47
+
48
+ private
49
+ FooColumn = OpenStruct.new(:name => '')
50
+
51
+ def exec_query(sql, binds, name)
52
+ binds.map! do |col, val|
53
+ val ? [col, val] : [FooColumn, col]
54
+ end
55
+ connection.exec_query(sql, name, binds)
56
+ end
57
+ end
58
+
59
+ end
@@ -0,0 +1,83 @@
1
+ module ChronoTest::Matchers
2
+
3
+ module Column
4
+ class HaveColumns < ChronoTest::Matchers::Base
5
+ def initialize(columns, schema = 'public')
6
+ @columns = columns
7
+ @schema = schema
8
+ end
9
+
10
+ def matches?(table)
11
+ super(table)
12
+
13
+ @matches = @columns.inject({}) do |h, (name, type)|
14
+ h.update([name, type] => has_column?(name, type))
15
+ end
16
+
17
+ @matches.values.all?
18
+ end
19
+
20
+ def failure_message_for_should
21
+ "expected #{@schema}.#{table} to have ".tap do |message|
22
+ message << @matches.map do |(name, type), match|
23
+ "a #{name}(#{type}) column" unless match
24
+ end.compact.to_sentence
25
+ end
26
+ end
27
+
28
+ protected
29
+ def has_column?(name, type)
30
+ table = "#{@schema}.#{self.table}"
31
+
32
+ select_rows(<<-SQL, [table, name], 'Check column').first == [name, type]
33
+ SELECT attname, FORMAT_TYPE(atttypid, atttypmod)
34
+ FROM pg_attribute
35
+ WHERE attrelid = $1::regclass::oid
36
+ AND attname = $2
37
+ SQL
38
+ end
39
+ end
40
+
41
+ def have_columns(*args)
42
+ HaveColumns.new(*args)
43
+ end
44
+
45
+
46
+ class HaveTemporalColumns < HaveColumns
47
+ def initialize(columns)
48
+ super(columns, temporal_schema)
49
+ end
50
+ end
51
+
52
+ def have_temporal_columns(*args)
53
+ HaveTemporalColumns.new(*args)
54
+ end
55
+
56
+
57
+ class HaveHistoryColumns < HaveColumns
58
+ def initialize(columns)
59
+ super(columns, history_schema)
60
+ end
61
+ end
62
+
63
+ def have_history_columns(*args)
64
+ HaveHistoryColumns.new(*args)
65
+ end
66
+
67
+
68
+ class HaveHistoryExtraColumns < HaveColumns
69
+ def initialize
70
+ super([
71
+ ['valid_from', 'timestamp without time zone'],
72
+ ['valid_to', 'timestamp without time zone'],
73
+ ['recorded_at', 'timestamp without time zone'],
74
+ ['hid', 'integer']
75
+ ], history_schema)
76
+ end
77
+ end
78
+
79
+ def have_history_extra_columns
80
+ HaveHistoryExtraColumns.new
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,61 @@
1
+ module ChronoTest::Matchers
2
+
3
+ module Index
4
+ class HaveIndex < ChronoTest::Matchers::Base
5
+ attr_reader :name, :columns, :schema
6
+
7
+ def initialize(name, columns, schema = 'public')
8
+ @name = name
9
+ @columns = columns.sort
10
+ @schema = schema
11
+ end
12
+
13
+ def matches?(table)
14
+ super(table)
15
+
16
+ select_values(<<-SQL, [ table, name, schema ], 'Check index') == columns
17
+ SELECT a.attname
18
+ FROM pg_class t
19
+ JOIN pg_index d ON t.oid = d.indrelid
20
+ JOIN pg_class i ON i.oid = d.indexrelid
21
+ JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(d.indkey)
22
+ WHERE i.relkind = 'i'
23
+ AND t.relname = $1
24
+ AND i.relname = $2
25
+ AND i.relnamespace = (
26
+ SELECT oid FROM pg_namespace WHERE nspname = $3
27
+ )
28
+ ORDER BY a.attname
29
+ SQL
30
+ end
31
+
32
+ def failure_message_for_should
33
+ "expected #{schema}.#{table} to have a #{name} index on #{columns}"
34
+ end
35
+ end
36
+
37
+ def have_index(*args)
38
+ HaveIndex.new(*args)
39
+ end
40
+
41
+ class HaveTemporalIndex < HaveIndex
42
+ def initialize(name, columns)
43
+ super(name, columns, temporal_schema)
44
+ end
45
+ end
46
+
47
+ def have_temporal_index(*args)
48
+ HaveTemporalIndex.new(*args)
49
+ end
50
+
51
+ class HaveHistoryIndex < HaveIndex
52
+ def initialize(name, columns)
53
+ super(name, columns, history_schema)
54
+ end
55
+ end
56
+
57
+ def have_history_index(*args)
58
+ HaveHistoryIndex.new(*args)
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,31 @@
1
+ require 'support/matchers/base'
2
+
3
+ module ChronoTest::Matchers
4
+
5
+ module Schema
6
+ class BeInSchema < ChronoTest::Matchers::Base
7
+
8
+ def initialize(expected)
9
+ @expected = expected
10
+ @expected = '"$user", public' if @expected == :default
11
+ end
12
+
13
+ def failure_message_for_should
14
+ "expected to be in schema #@expected, but was in #@current"
15
+ end
16
+
17
+ def matches?(*)
18
+ @current = select_value(<<-SQL, [], 'Current schema')
19
+ SHOW search_path
20
+ SQL
21
+
22
+ @current == @expected
23
+ end
24
+ end
25
+
26
+ def be_in_schema(schema)
27
+ BeInSchema.new(schema)
28
+ end
29
+ end
30
+
31
+ end