chrono_model 1.2.2 → 2.0.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 +4 -4
- data/LICENSE +19 -20
- data/README.md +62 -40
- data/lib/active_record/connection_adapters/chronomodel_adapter.rb +17 -11
- data/lib/active_record/tasks/chronomodel_database_tasks.rb +64 -23
- data/lib/chrono_model/adapter/ddl.rb +168 -153
- data/lib/chrono_model/adapter/indexes.rb +99 -94
- data/lib/chrono_model/adapter/migrations.rb +81 -104
- data/lib/chrono_model/adapter/migrations_modules/legacy.rb +41 -0
- data/lib/chrono_model/adapter/migrations_modules/stable.rb +41 -0
- data/lib/chrono_model/adapter/tsrange.rb +20 -5
- data/lib/chrono_model/adapter/upgrade.rb +89 -91
- data/lib/chrono_model/adapter.rb +64 -31
- data/lib/chrono_model/chrono.rb +17 -0
- data/lib/chrono_model/conversions.rb +15 -9
- data/lib/chrono_model/db_console.rb +9 -0
- data/lib/chrono_model/json.rb +9 -6
- data/lib/chrono_model/patches/as_of_time_holder.rb +2 -2
- data/lib/chrono_model/patches/as_of_time_relation.rb +2 -2
- data/lib/chrono_model/patches/association.rb +15 -12
- data/lib/chrono_model/patches/batches.rb +17 -0
- data/lib/chrono_model/patches/db_console.rb +20 -4
- data/lib/chrono_model/patches/join_node.rb +4 -4
- data/lib/chrono_model/patches/preloader.rb +41 -11
- data/lib/chrono_model/patches/relation.rb +53 -8
- data/lib/chrono_model/patches.rb +3 -1
- data/lib/chrono_model/railtie.rb +29 -24
- data/lib/chrono_model/time_gate.rb +3 -3
- data/lib/chrono_model/time_machine/history_model.rb +65 -31
- data/lib/chrono_model/time_machine/time_query.rb +65 -49
- data/lib/chrono_model/time_machine/timeline.rb +52 -28
- data/lib/chrono_model/time_machine.rb +66 -25
- data/lib/chrono_model/utilities.rb +3 -3
- data/lib/chrono_model/version.rb +3 -1
- data/lib/chrono_model.rb +31 -36
- metadata +39 -136
- data/.gitignore +0 -21
- data/.rspec +0 -2
- data/.travis.yml +0 -41
- data/Gemfile +0 -4
- data/README.sql +0 -161
- data/Rakefile +0 -25
- data/chrono_model.gemspec +0 -33
- data/gemfiles/rails_5.0.gemfile +0 -6
- data/gemfiles/rails_5.1.gemfile +0 -6
- data/gemfiles/rails_5.2.gemfile +0 -6
- data/spec/aruba/dbconsole_spec.rb +0 -25
- data/spec/aruba/fixtures/database_with_default_username_and_password.yml +0 -14
- data/spec/aruba/fixtures/database_without_username_and_password.yml +0 -11
- data/spec/aruba/fixtures/empty_structure.sql +0 -27
- data/spec/aruba/fixtures/migrations/56/20160812190335_create_impressions.rb +0 -10
- data/spec/aruba/fixtures/migrations/56/20171115195229_add_temporal_extension_to_impressions.rb +0 -10
- data/spec/aruba/fixtures/railsapp/config/application.rb +0 -17
- data/spec/aruba/fixtures/railsapp/config/boot.rb +0 -5
- data/spec/aruba/fixtures/railsapp/config/environments/development.rb +0 -38
- data/spec/aruba/migrations_spec.rb +0 -48
- data/spec/aruba/rake_task_spec.rb +0 -71
- data/spec/chrono_model/adapter/base_spec.rb +0 -157
- data/spec/chrono_model/adapter/ddl_spec.rb +0 -243
- data/spec/chrono_model/adapter/indexes_spec.rb +0 -72
- data/spec/chrono_model/adapter/migrations_spec.rb +0 -312
- data/spec/chrono_model/conversions_spec.rb +0 -43
- data/spec/chrono_model/history_models_spec.rb +0 -32
- data/spec/chrono_model/json_ops_spec.rb +0 -59
- data/spec/chrono_model/time_machine/as_of_spec.rb +0 -188
- data/spec/chrono_model/time_machine/changes_spec.rb +0 -50
- data/spec/chrono_model/time_machine/counter_cache_race_spec.rb +0 -46
- data/spec/chrono_model/time_machine/default_scope_spec.rb +0 -37
- data/spec/chrono_model/time_machine/history_spec.rb +0 -104
- data/spec/chrono_model/time_machine/keep_cool_spec.rb +0 -27
- data/spec/chrono_model/time_machine/manipulations_spec.rb +0 -84
- data/spec/chrono_model/time_machine/model_identification_spec.rb +0 -46
- data/spec/chrono_model/time_machine/sequence_spec.rb +0 -74
- data/spec/chrono_model/time_machine/sti_spec.rb +0 -100
- data/spec/chrono_model/time_machine/time_query_spec.rb +0 -261
- data/spec/chrono_model/time_machine/timeline_spec.rb +0 -63
- data/spec/chrono_model/time_machine/timestamps_spec.rb +0 -43
- data/spec/chrono_model/time_machine/transactions_spec.rb +0 -69
- data/spec/config.travis.yml +0 -5
- data/spec/config.yml.example +0 -9
- data/spec/spec_helper.rb +0 -33
- data/spec/support/adapter/helpers.rb +0 -53
- data/spec/support/adapter/structure.rb +0 -44
- data/spec/support/aruba.rb +0 -44
- data/spec/support/connection.rb +0 -70
- data/spec/support/matchers/base.rb +0 -56
- data/spec/support/matchers/column.rb +0 -99
- data/spec/support/matchers/function.rb +0 -79
- data/spec/support/matchers/index.rb +0 -69
- data/spec/support/matchers/schema.rb +0 -39
- data/spec/support/matchers/table.rb +0 -275
- data/spec/support/time_machine/helpers.rb +0 -47
- data/spec/support/time_machine/structure.rb +0 -111
- data/sql/json_ops.sql +0 -56
- data/sql/uninstall-json_ops.sql +0 -24
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
module ChronoTest::Matchers
|
|
2
|
-
class Base
|
|
3
|
-
include ActiveRecord::Sanitization::ClassMethods
|
|
4
|
-
|
|
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
|
-
|
|
19
|
-
def connection
|
|
20
|
-
ChronoTest.connection
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def temporal_schema
|
|
24
|
-
ChronoModel::Adapter::TEMPORAL_SCHEMA
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def history_schema
|
|
28
|
-
ChronoModel::Adapter::HISTORY_SCHEMA
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
def public_schema
|
|
32
|
-
'public'
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def select_value(sql, binds, name = nil)
|
|
36
|
-
result = exec_query(sql, binds, name || 'select_value')
|
|
37
|
-
result.rows.first.try(:[], 0)
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def select_values(sql, binds, name = nil)
|
|
41
|
-
result = exec_query(sql, binds, name || 'select_values')
|
|
42
|
-
result.rows.map(&:first)
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def select_rows(sql, binds, name = nil)
|
|
46
|
-
exec_query(sql, binds, name || 'select_rows').rows
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
private
|
|
50
|
-
def exec_query(sql, binds, name)
|
|
51
|
-
sql = sanitize_sql_array([ sql, *Array.wrap(binds) ])
|
|
52
|
-
connection.exec_query(sql, name)
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
end
|
|
@@ -1,99 +0,0 @@
|
|
|
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 description
|
|
11
|
-
'have columns'
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def matches?(table)
|
|
15
|
-
super(table)
|
|
16
|
-
|
|
17
|
-
@matches = @columns.inject({}) do |h, (name, type)|
|
|
18
|
-
h.update([name, type] => has_column?(name, type))
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
@matches.values.all?
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def failure_message
|
|
25
|
-
message_matches("expected #{@schema}.#{table} to have")
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def failure_message_when_negated
|
|
29
|
-
message_matches("expected #{@schema}.#{table} to not have")
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
protected
|
|
33
|
-
def has_column?(name, type)
|
|
34
|
-
column_type(name) == [name, type]
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def column_type(name)
|
|
38
|
-
table = "#{@schema}.#{self.table}"
|
|
39
|
-
|
|
40
|
-
select_rows(<<-SQL, [table, name], 'Check column').first
|
|
41
|
-
SELECT attname, FORMAT_TYPE(atttypid, atttypmod)
|
|
42
|
-
FROM pg_attribute
|
|
43
|
-
WHERE attrelid = ?::regclass::oid
|
|
44
|
-
AND attname = ?
|
|
45
|
-
SQL
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
private
|
|
49
|
-
def message_matches(message)
|
|
50
|
-
(message << ' ').tap do |message|
|
|
51
|
-
message << @matches.map do |(name, type), match|
|
|
52
|
-
"a #{name}(#{type}) column" unless match
|
|
53
|
-
end.compact.to_sentence
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def have_columns(*args)
|
|
59
|
-
HaveColumns.new(*args)
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
class HaveTemporalColumns < HaveColumns
|
|
64
|
-
def initialize(columns)
|
|
65
|
-
super(columns, temporal_schema)
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def have_temporal_columns(*args)
|
|
70
|
-
HaveTemporalColumns.new(*args)
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
class HaveHistoryColumns < HaveColumns
|
|
75
|
-
def initialize(columns)
|
|
76
|
-
super(columns, history_schema)
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
def have_history_columns(*args)
|
|
81
|
-
HaveHistoryColumns.new(*args)
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
class HaveHistoryExtraColumns < HaveColumns
|
|
86
|
-
def initialize
|
|
87
|
-
super([
|
|
88
|
-
['validity', 'tsrange'],
|
|
89
|
-
['recorded_at', 'timestamp without time zone'],
|
|
90
|
-
['hid', 'integer']
|
|
91
|
-
], history_schema)
|
|
92
|
-
end
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
def have_history_extra_columns
|
|
96
|
-
HaveHistoryExtraColumns.new
|
|
97
|
-
end
|
|
98
|
-
end
|
|
99
|
-
end
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
module ChronoTest::Matchers
|
|
2
|
-
|
|
3
|
-
module Column
|
|
4
|
-
class HaveFunctions < ChronoTest::Matchers::Base
|
|
5
|
-
def initialize(functions, schema = 'public')
|
|
6
|
-
@functions = functions
|
|
7
|
-
@schema = schema
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
def description
|
|
11
|
-
'have functions'
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def matches?(table)
|
|
15
|
-
super(table)
|
|
16
|
-
|
|
17
|
-
@matches = @functions.inject({}) do |h, name|
|
|
18
|
-
h.update(name => has_function?(name))
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
@matches.values.all?
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def failure_message
|
|
25
|
-
message_matches("expected #{@schema}.#{table} to have")
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def failure_message_when_negated
|
|
29
|
-
message_matches("expected #{@schema}.#{table} to not have")
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
protected
|
|
33
|
-
def has_function?(name)
|
|
34
|
-
select_value(<<-SQL, [name], 'Check function') == true
|
|
35
|
-
SELECT EXISTS(
|
|
36
|
-
SELECT 1
|
|
37
|
-
FROM pg_catalog.pg_proc p, pg_catalog.pg_namespace n
|
|
38
|
-
WHERE p.pronamespace = n.oid
|
|
39
|
-
AND n.nspname = 'public'
|
|
40
|
-
AND p.proname = ?
|
|
41
|
-
)
|
|
42
|
-
SQL
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
private
|
|
46
|
-
def message_matches(message)
|
|
47
|
-
(message << ' ').tap do |message|
|
|
48
|
-
message << @matches.map do |name, match|
|
|
49
|
-
"a #{name} function"
|
|
50
|
-
end.compact.to_sentence
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def have_functions(*args)
|
|
56
|
-
HaveFunctions.new(*args)
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
class HaveHistoryFunctions < HaveFunctions
|
|
60
|
-
def initialize
|
|
61
|
-
@function_templates = [
|
|
62
|
-
'chronomodel_%s_insert',
|
|
63
|
-
'chronomodel_%s_update',
|
|
64
|
-
'chronomodel_%s_delete',
|
|
65
|
-
]
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def matches?(table)
|
|
69
|
-
@functions = @function_templates.map {|t| t % [table] }
|
|
70
|
-
|
|
71
|
-
super(table)
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def have_history_functions
|
|
76
|
-
HaveHistoryFunctions.new
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
end
|
|
@@ -1,69 +0,0 @@
|
|
|
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 description
|
|
14
|
-
'have index'
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def matches?(table)
|
|
18
|
-
super(table)
|
|
19
|
-
|
|
20
|
-
select_values(<<-SQL, [ table, name, schema ], 'Check index') == columns
|
|
21
|
-
SELECT a.attname
|
|
22
|
-
FROM pg_class t
|
|
23
|
-
JOIN pg_index d ON t.oid = d.indrelid
|
|
24
|
-
JOIN pg_class i ON i.oid = d.indexrelid
|
|
25
|
-
JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(d.indkey)
|
|
26
|
-
WHERE i.relkind = 'i'
|
|
27
|
-
AND t.relname = ?
|
|
28
|
-
AND i.relname = ?
|
|
29
|
-
AND i.relnamespace = (
|
|
30
|
-
SELECT oid FROM pg_namespace WHERE nspname = ?
|
|
31
|
-
)
|
|
32
|
-
ORDER BY a.attname
|
|
33
|
-
SQL
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def failure_message
|
|
37
|
-
"expected #{schema}.#{table} to have a #{name} index on #{columns}"
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def failure_message_when_negated
|
|
41
|
-
"expected #{schema}.#{table} to not have a #{name} index on #{columns}"
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def have_index(*args)
|
|
46
|
-
HaveIndex.new(*args)
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
class HaveTemporalIndex < HaveIndex
|
|
50
|
-
def initialize(name, columns)
|
|
51
|
-
super(name, columns, temporal_schema)
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def have_temporal_index(*args)
|
|
56
|
-
HaveTemporalIndex.new(*args)
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
class HaveHistoryIndex < HaveIndex
|
|
60
|
-
def initialize(name, columns)
|
|
61
|
-
super(name, columns, history_schema)
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def have_history_index(*args)
|
|
66
|
-
HaveHistoryIndex.new(*args)
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
end
|
|
@@ -1,39 +0,0 @@
|
|
|
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 description
|
|
14
|
-
'be in schema'
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def failure_message
|
|
18
|
-
"expected to be in schema #@expected, but was in #@current"
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def failure_message_when_negated
|
|
22
|
-
"expected to be in schema #@current, but was in #@expected"
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def matches?(*)
|
|
26
|
-
@current = select_value(<<-SQL, [], 'Current schema')
|
|
27
|
-
SHOW search_path
|
|
28
|
-
SQL
|
|
29
|
-
|
|
30
|
-
@current == @expected
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def be_in_schema(schema)
|
|
35
|
-
BeInSchema.new(schema)
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
end
|
|
@@ -1,275 +0,0 @@
|
|
|
1
|
-
require 'support/matchers/base'
|
|
2
|
-
|
|
3
|
-
module ChronoTest::Matchers
|
|
4
|
-
|
|
5
|
-
module Table
|
|
6
|
-
class Base < ChronoTest::Matchers::Base
|
|
7
|
-
|
|
8
|
-
protected
|
|
9
|
-
# Database statements
|
|
10
|
-
#
|
|
11
|
-
def relation_exists?(options)
|
|
12
|
-
schema = options[:in]
|
|
13
|
-
kind = options[:kind] == :view ? 'v' : 'r'
|
|
14
|
-
|
|
15
|
-
select_value(<<-SQL, [ table, schema ], 'Check table exists') == true
|
|
16
|
-
SELECT EXISTS (
|
|
17
|
-
SELECT 1
|
|
18
|
-
FROM pg_class c
|
|
19
|
-
LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
20
|
-
WHERE c.relkind = '#{kind}'
|
|
21
|
-
AND c.relname = ?
|
|
22
|
-
AND n.nspname = ?
|
|
23
|
-
)
|
|
24
|
-
SQL
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
# ##################################################################
|
|
29
|
-
# Checks that a table exists in the Public schema
|
|
30
|
-
#
|
|
31
|
-
class HavePublicBacking < Base
|
|
32
|
-
def matches?(table)
|
|
33
|
-
super(table)
|
|
34
|
-
|
|
35
|
-
relation_exists? :in => public_schema
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def description
|
|
39
|
-
'be in the public schema'
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def failure_message
|
|
43
|
-
"expected #{table} to exist in the #{public_schema} schema"
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def failure_message_when_negated
|
|
47
|
-
"expected #{table} to not exist in the #{public_schema} schema"
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def have_public_backing
|
|
52
|
-
HavePublicBacking.new
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
# ##################################################################
|
|
57
|
-
# Checks that a table exists in the Temporal schema
|
|
58
|
-
#
|
|
59
|
-
class HaveTemporalBacking < Base
|
|
60
|
-
def matches?(table)
|
|
61
|
-
super(table)
|
|
62
|
-
|
|
63
|
-
relation_exists? :in => temporal_schema
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def description
|
|
67
|
-
'be in the temporal schema'
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def failure_message
|
|
71
|
-
"expected #{table} to exist in the #{temporal_schema} schema"
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def failure_message_when_negated
|
|
75
|
-
"expected #{table} to not exist in the #{temporal_schema} schema"
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
def have_temporal_backing
|
|
80
|
-
HaveTemporalBacking.new
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
# ##################################################################
|
|
85
|
-
# Checks that a table exists in the History schema and inherits from
|
|
86
|
-
# the one in the Temporal schema
|
|
87
|
-
#
|
|
88
|
-
class HaveHistoryBacking < Base
|
|
89
|
-
def matches?(table)
|
|
90
|
-
super(table)
|
|
91
|
-
|
|
92
|
-
table_exists? &&
|
|
93
|
-
inherits_from_temporal? &&
|
|
94
|
-
has_consistency_constraint? &&
|
|
95
|
-
has_history_indexes?
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def description
|
|
99
|
-
'be in history schema'
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
def failure_message
|
|
103
|
-
"expected #{table} ".tap do |message|
|
|
104
|
-
message << [
|
|
105
|
-
("to exist in the #{history_schema} schema" unless @existance),
|
|
106
|
-
("to inherit from #{temporal_schema}.#{table}" unless @inheritance),
|
|
107
|
-
("to have a timeline consistency constraint" unless @constraint),
|
|
108
|
-
("to have history indexes" unless @indexes)
|
|
109
|
-
].compact.to_sentence
|
|
110
|
-
end
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
def failure_message_when_negated
|
|
114
|
-
"expected #{table} ".tap do |message|
|
|
115
|
-
message << [
|
|
116
|
-
("to not exist in the #{history_schema} schema" if @existance),
|
|
117
|
-
("to not inherit from #{temporal_schema}.#{table}" if @inheritance),
|
|
118
|
-
("to not have a timeline consistency constraint" if @constraint),
|
|
119
|
-
("to not have history indexes" if @indexes)
|
|
120
|
-
].compact.to_sentence
|
|
121
|
-
end
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
private
|
|
125
|
-
def table_exists?
|
|
126
|
-
@existance = relation_exists? :in => history_schema
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
def inherits_from_temporal?
|
|
130
|
-
binds = ["#{history_schema}.#{table}", "#{temporal_schema}.#{table}"]
|
|
131
|
-
|
|
132
|
-
@inheritance = select_value(<<-SQL, binds, 'Check inheritance') == true
|
|
133
|
-
SELECT EXISTS (
|
|
134
|
-
SELECT 1 FROM pg_catalog.pg_inherits
|
|
135
|
-
WHERE inhrelid = ?::regclass::oid
|
|
136
|
-
AND inhparent = ?::regclass::oid
|
|
137
|
-
)
|
|
138
|
-
SQL
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
def has_history_indexes?
|
|
142
|
-
binds = [ history_schema, table ]
|
|
143
|
-
|
|
144
|
-
indexes = select_values(<<-SQL, binds, 'Check history indexes')
|
|
145
|
-
SELECT indexdef FROM pg_indexes
|
|
146
|
-
WHERE schemaname = ?
|
|
147
|
-
AND tablename = ?
|
|
148
|
-
SQL
|
|
149
|
-
|
|
150
|
-
fqtn = [history_schema, table].join('.')
|
|
151
|
-
|
|
152
|
-
expected = [
|
|
153
|
-
"CREATE INDEX index_#{table}_temporal_on_lower_validity ON #{fqtn} USING btree (lower(validity))",
|
|
154
|
-
"CREATE INDEX index_#{table}_temporal_on_upper_validity ON #{fqtn} USING btree (upper(validity))",
|
|
155
|
-
"CREATE INDEX index_#{table}_temporal_on_validity ON #{fqtn} USING gist (validity)",
|
|
156
|
-
|
|
157
|
-
"CREATE INDEX #{table}_inherit_pkey ON #{fqtn} USING btree (id)",
|
|
158
|
-
"CREATE INDEX #{table}_instance_history ON #{fqtn} USING btree (id, recorded_at)",
|
|
159
|
-
"CREATE UNIQUE INDEX #{table}_pkey ON #{fqtn} USING btree (hid)",
|
|
160
|
-
"CREATE INDEX #{table}_recorded_at ON #{fqtn} USING btree (recorded_at)",
|
|
161
|
-
"CREATE INDEX #{table}_timeline_consistency ON #{fqtn} USING gist (id, validity)"
|
|
162
|
-
]
|
|
163
|
-
|
|
164
|
-
@indexes = (expected - indexes).empty?
|
|
165
|
-
end
|
|
166
|
-
|
|
167
|
-
def has_consistency_constraint?
|
|
168
|
-
binds = {
|
|
169
|
-
conname: connection.timeline_consistency_constraint_name(table),
|
|
170
|
-
connamespace: history_schema,
|
|
171
|
-
conrelid: [history_schema, table].join('.'),
|
|
172
|
-
attname: connection.primary_key(table)
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
@constraint = select_value(<<-SQL, binds, 'Check Consistency Constraint') == true
|
|
176
|
-
SELECT EXISTS (
|
|
177
|
-
SELECT 1 FROM pg_catalog.pg_constraint
|
|
178
|
-
WHERE conname = :conname
|
|
179
|
-
AND contype = 'x'
|
|
180
|
-
AND conrelid = :conrelid::regclass
|
|
181
|
-
AND connamespace = (
|
|
182
|
-
SELECT oid FROM pg_catalog.pg_namespace WHERE nspname = :connamespace
|
|
183
|
-
)
|
|
184
|
-
AND conkey = (
|
|
185
|
-
SELECT array_agg(attnum) FROM pg_catalog.pg_attribute
|
|
186
|
-
WHERE attname IN (:attname, 'validity')
|
|
187
|
-
AND attrelid = :conrelid::regclass
|
|
188
|
-
)
|
|
189
|
-
)
|
|
190
|
-
SQL
|
|
191
|
-
end
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
def have_history_backing
|
|
195
|
-
HaveHistoryBacking.new
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
# ##################################################################
|
|
200
|
-
# Checks that a table exists in the Public schema, is an updatable
|
|
201
|
-
# view and has an INSERT, UPDATE and DELETE triggers.
|
|
202
|
-
#
|
|
203
|
-
class HavePublicInterface < Base
|
|
204
|
-
def matches?(table)
|
|
205
|
-
super(table)
|
|
206
|
-
|
|
207
|
-
view_exists? && [ is_updatable?, has_triggers? ].all?
|
|
208
|
-
end
|
|
209
|
-
|
|
210
|
-
def description
|
|
211
|
-
'be an updatable view'
|
|
212
|
-
end
|
|
213
|
-
|
|
214
|
-
def failure_message
|
|
215
|
-
"expected #{table} ".tap do |message|
|
|
216
|
-
message << [
|
|
217
|
-
("to exist in the #{public_schema} schema" unless @existance ),
|
|
218
|
-
('to be an updatable view' unless @updatable ),
|
|
219
|
-
('to have an INSERT trigger' unless @insert_trigger),
|
|
220
|
-
('to have an UPDATE trigger' unless @update_trigger),
|
|
221
|
-
('to have a DELETE trigger' unless @delete_trigger)
|
|
222
|
-
].compact.to_sentence
|
|
223
|
-
end
|
|
224
|
-
end
|
|
225
|
-
|
|
226
|
-
def failure_message_when_negated
|
|
227
|
-
"expected #{table} ".tap do |message|
|
|
228
|
-
message << [
|
|
229
|
-
("to not exist in the #{public_schema} schema" if @existance ),
|
|
230
|
-
('to not be an updatable view' if @updatable ),
|
|
231
|
-
('to not have an INSERT trigger' if @insert_trigger),
|
|
232
|
-
('to not have an UPDATE trigger' if @update_trigger),
|
|
233
|
-
('to not have a DELETE trigger' if @delete_trigger)
|
|
234
|
-
].compact.to_sentence
|
|
235
|
-
end
|
|
236
|
-
end
|
|
237
|
-
|
|
238
|
-
private
|
|
239
|
-
def view_exists?
|
|
240
|
-
@existance = relation_exists? :in => public_schema, :kind => :view
|
|
241
|
-
end
|
|
242
|
-
|
|
243
|
-
def is_updatable?
|
|
244
|
-
binds = [ public_schema, table ]
|
|
245
|
-
|
|
246
|
-
@updatable = select_value(<<-SQL, binds, 'Check updatable') == 'YES'
|
|
247
|
-
SELECT is_updatable FROM information_schema.views
|
|
248
|
-
WHERE table_schema = ? AND table_name = ?
|
|
249
|
-
SQL
|
|
250
|
-
end
|
|
251
|
-
|
|
252
|
-
def has_triggers?
|
|
253
|
-
triggers = select_values(<<-SQL, [ public_schema, table ], 'Check triggers')
|
|
254
|
-
SELECT t.tgname
|
|
255
|
-
FROM pg_catalog.pg_trigger t, pg_catalog.pg_class c, pg_catalog.pg_namespace n
|
|
256
|
-
WHERE t.tgrelid = c.relfilenode
|
|
257
|
-
AND n.oid = c.relnamespace
|
|
258
|
-
AND n.nspname = ?
|
|
259
|
-
AND c.relname = ?;
|
|
260
|
-
SQL
|
|
261
|
-
|
|
262
|
-
@insert_trigger = triggers.include? 'chronomodel_insert'
|
|
263
|
-
@update_trigger = triggers.include? 'chronomodel_update'
|
|
264
|
-
@delete_trigger = triggers.include? 'chronomodel_delete'
|
|
265
|
-
|
|
266
|
-
@insert_trigger && @update_trigger && @delete_trigger
|
|
267
|
-
end
|
|
268
|
-
end
|
|
269
|
-
|
|
270
|
-
def have_public_interface
|
|
271
|
-
HavePublicInterface.new
|
|
272
|
-
end
|
|
273
|
-
end
|
|
274
|
-
|
|
275
|
-
end
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
module ChronoTest::TimeMachine
|
|
2
|
-
|
|
3
|
-
# This module contains helpers used throughout the
|
|
4
|
-
# +ChronoModel::TimeMachine+ specs.
|
|
5
|
-
#
|
|
6
|
-
module Helpers
|
|
7
|
-
def self.included(base)
|
|
8
|
-
base.extend(self)
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
def adapter
|
|
12
|
-
ChronoTest.connection
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def with_revert
|
|
16
|
-
adapter.transaction do
|
|
17
|
-
adapter.create_savepoint 'revert'
|
|
18
|
-
|
|
19
|
-
yield
|
|
20
|
-
|
|
21
|
-
adapter.exec_rollback_to_savepoint 'revert'
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
# If a context object is given, evaluates the given
|
|
26
|
-
# block in its instance context, then defines a `ts`
|
|
27
|
-
# on it, backed by an Array, and adds the current
|
|
28
|
-
# database timestamp to it.
|
|
29
|
-
#
|
|
30
|
-
# If a context object is not given, the block is
|
|
31
|
-
# evaluated in the current context and the above
|
|
32
|
-
# mangling is done on the blocks' return value.
|
|
33
|
-
#
|
|
34
|
-
def ts_eval(ctx = nil, &block)
|
|
35
|
-
ret = (ctx || self).instance_eval(&block)
|
|
36
|
-
(ctx || ret).tap do |obj|
|
|
37
|
-
obj.singleton_class.instance_eval do
|
|
38
|
-
define_method(:ts) { @_ts ||= [] }
|
|
39
|
-
end unless obj.methods.include?(:ts)
|
|
40
|
-
|
|
41
|
-
now = ChronoTest.connection.select_value('select now()::timestamp') + 'Z'
|
|
42
|
-
obj.ts.push(Time.parse(now))
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
end
|