logidze 0.11.0 → 1.2.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/CHANGELOG.md +79 -4
- data/LICENSE.txt +1 -1
- data/README.md +305 -102
- data/lib/generators/logidze/fx_helper.rb +17 -0
- data/lib/generators/logidze/inject_sql.rb +18 -0
- data/lib/generators/logidze/install/USAGE +6 -1
- data/lib/generators/logidze/install/functions/logidze_capture_exception.sql +23 -0
- data/lib/generators/logidze/install/functions/logidze_compact_history.sql +38 -0
- data/lib/generators/logidze/install/functions/logidze_filter_keys.sql +27 -0
- data/lib/generators/logidze/install/functions/logidze_logger.sql +203 -0
- data/lib/generators/logidze/install/functions/logidze_snapshot.sql +33 -0
- data/lib/generators/logidze/install/functions/logidze_version.sql +21 -0
- data/lib/generators/logidze/install/install_generator.rb +43 -1
- data/lib/generators/logidze/install/templates/hstore.rb.erb +1 -1
- data/lib/generators/logidze/install/templates/migration.rb.erb +19 -232
- data/lib/generators/logidze/install/templates/migration_fx.rb.erb +41 -0
- data/lib/generators/logidze/model/model_generator.rb +53 -13
- data/lib/generators/logidze/model/templates/migration.rb.erb +57 -36
- data/lib/generators/logidze/model/triggers/logidze.sql +6 -0
- data/lib/logidze.rb +37 -14
- data/lib/logidze/engine.rb +9 -0
- data/lib/logidze/has_logidze.rb +1 -1
- data/lib/logidze/history.rb +2 -11
- data/lib/logidze/ignore_log_data.rb +1 -3
- data/lib/logidze/meta.rb +43 -16
- data/lib/logidze/model.rb +51 -44
- data/lib/logidze/utils/check_pending.rb +57 -0
- data/lib/logidze/utils/function_definitions.rb +49 -0
- data/lib/logidze/utils/pending_migration_error.rb +25 -0
- data/lib/logidze/version.rb +1 -1
- metadata +69 -77
- data/.gitattributes +0 -3
- data/.github/ISSUE_TEMPLATE.md +0 -20
- data/.github/PULL_REQUEST_TEMPLATE.md +0 -29
- data/.gitignore +0 -40
- data/.rubocop.yml +0 -55
- data/.travis.yml +0 -42
- data/Gemfile +0 -15
- data/Rakefile +0 -28
- data/assets/pg_log_data_chart.png +0 -0
- data/bench/performance/README.md +0 -109
- data/bench/performance/diff_bench.rb +0 -38
- data/bench/performance/insert_bench.rb +0 -22
- data/bench/performance/memory_profile.rb +0 -56
- data/bench/performance/setup.rb +0 -315
- data/bench/performance/update_bench.rb +0 -38
- data/bench/triggers/Makefile +0 -56
- data/bench/triggers/Readme.md +0 -58
- data/bench/triggers/bench.sql +0 -6
- data/bench/triggers/hstore_trigger_setup.sql +0 -38
- data/bench/triggers/jsonb_minus_2_setup.sql +0 -47
- data/bench/triggers/jsonb_minus_setup.sql +0 -49
- data/bench/triggers/keys2_trigger_setup.sql +0 -44
- data/bench/triggers/keys_trigger_setup.sql +0 -50
- data/bin/console +0 -8
- data/bin/setup +0 -9
- data/gemfiles/rails42.gemfile +0 -6
- data/gemfiles/rails5.gemfile +0 -6
- data/gemfiles/rails52.gemfile +0 -6
- data/gemfiles/rails6.gemfile +0 -6
- data/gemfiles/railsmaster.gemfile +0 -7
- data/lib/logidze/ignore_log_data/association.rb +0 -11
- data/lib/logidze/ignore_log_data/ignored_columns.rb +0 -46
- data/lib/logidze/migration.rb +0 -20
- data/logidze.gemspec +0 -41
@@ -0,0 +1,6 @@
|
|
1
|
+
CREATE TRIGGER logidze_on_<%= table_name %>
|
2
|
+
BEFORE UPDATE OR INSERT ON <%= table_name %> FOR EACH ROW
|
3
|
+
WHEN (coalesce(current_setting('logidze.disabled', true), '') <> 'on')
|
4
|
+
-- Parameters: history_size_limit (integer), timestamp_column (text), filtered_columns (text[]),
|
5
|
+
-- include_columns (boolean), debounce_time_ms (integer)
|
6
|
+
EXECUTE PROCEDURE logidze_logger(<%= logidze_logger_parameters %>);
|
data/lib/logidze.rb
CHANGED
@@ -5,6 +5,7 @@ require "logidze/version"
|
|
5
5
|
# Logidze provides tools for adding in-table JSON-based audit to DB tables
|
6
6
|
# and ActiveRecord extensions to work with changes history.
|
7
7
|
module Logidze
|
8
|
+
require "ruby-next"
|
8
9
|
require "logidze/history"
|
9
10
|
require "logidze/model"
|
10
11
|
require "logidze/versioned_association"
|
@@ -19,31 +20,53 @@ module Logidze
|
|
19
20
|
class << self
|
20
21
|
# Determines if Logidze should append a version to the log after updating an old version.
|
21
22
|
attr_accessor :append_on_undo
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
def associations_versioning
|
26
|
-
@associations_versioning || false
|
27
|
-
end
|
28
|
-
|
23
|
+
# Determines whether associations versioning is enabled or not
|
24
|
+
attr_accessor :associations_versioning
|
29
25
|
# Determines if Logidze should exclude log data from SELECT statements
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
26
|
+
attr_accessor :ignore_log_data_by_default
|
27
|
+
# Whether #at should return self or nil when log_data is nil
|
28
|
+
attr_accessor :return_self_if_log_data_is_empty
|
29
|
+
# Determines what Logidze should do when upgrade is needed (:raise | :warn | :ignore)
|
30
|
+
attr_reader :on_pending_upgrade
|
35
31
|
|
36
32
|
# Temporary disable DB triggers.
|
37
33
|
#
|
38
34
|
# @example
|
39
35
|
# Logidze.without_logging { Post.update_all(active: true) }
|
40
36
|
def without_logging
|
37
|
+
with_logidze_setting("logidze.disabled", "on") { yield }
|
38
|
+
end
|
39
|
+
|
40
|
+
# Instruct Logidze to create a full snapshot for the new versions, not a diff
|
41
|
+
#
|
42
|
+
# @example
|
43
|
+
# Logidze.with_full_snapshot { post.touch }
|
44
|
+
def with_full_snapshot
|
45
|
+
with_logidze_setting("logidze.full_snapshot", "on") { yield }
|
46
|
+
end
|
47
|
+
|
48
|
+
def on_pending_upgrade=(mode)
|
49
|
+
if %i[raise warn ignore].exclude? mode
|
50
|
+
raise ArgumentError, "Unknown on_pending_upgrade option `#{mode.inspect}`. Expecting :raise, :warn or :ignore"
|
51
|
+
end
|
52
|
+
@on_pending_upgrade = mode
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def with_logidze_setting(name, value)
|
41
58
|
ActiveRecord::Base.transaction do
|
42
|
-
ActiveRecord::Base.connection.execute "SET LOCAL
|
59
|
+
ActiveRecord::Base.connection.execute "SET LOCAL #{name} TO #{value};"
|
43
60
|
res = yield
|
44
|
-
ActiveRecord::Base.connection.execute "SET LOCAL
|
61
|
+
ActiveRecord::Base.connection.execute "SET LOCAL #{name} TO DEFAULT;"
|
45
62
|
res
|
46
63
|
end
|
47
64
|
end
|
48
65
|
end
|
66
|
+
|
67
|
+
self.append_on_undo = false
|
68
|
+
self.associations_versioning = false
|
69
|
+
self.ignore_log_data_by_default = false
|
70
|
+
self.return_self_if_log_data_is_empty = true
|
71
|
+
self.on_pending_upgrade = :ignore
|
49
72
|
end
|
data/lib/logidze/engine.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "logidze"
|
4
|
+
require "logidze/utils/check_pending"
|
4
5
|
|
5
6
|
module Logidze
|
6
7
|
class Engine < Rails::Engine # :nodoc:
|
@@ -11,5 +12,13 @@ module Logidze
|
|
11
12
|
ActiveRecord::Base.send :include, Logidze::HasLogidze
|
12
13
|
end
|
13
14
|
end
|
15
|
+
|
16
|
+
initializer "check Logidze function versions" do |app|
|
17
|
+
if config.logidze.on_pending_upgrade != :ignore
|
18
|
+
ActiveSupport.on_load(:active_record) do
|
19
|
+
app.config.app_middleware.use Logidze::Utils::CheckPending
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
14
23
|
end
|
15
24
|
end
|
data/lib/logidze/has_logidze.rb
CHANGED
@@ -11,8 +11,8 @@ module Logidze
|
|
11
11
|
# Include methods to work with history.
|
12
12
|
#
|
13
13
|
def has_logidze(ignore_log_data: Logidze.ignore_log_data_by_default)
|
14
|
-
include Logidze::Model
|
15
14
|
include Logidze::IgnoreLogData
|
15
|
+
include Logidze::Model
|
16
16
|
|
17
17
|
@ignore_log_data = ignore_log_data
|
18
18
|
|
data/lib/logidze/history.rb
CHANGED
@@ -17,15 +17,6 @@ module Logidze
|
|
17
17
|
delegate :size, to: :versions
|
18
18
|
delegate :responsible_id, :meta, to: :current_version
|
19
19
|
|
20
|
-
### Rails 4 ###
|
21
|
-
def self.dump(object)
|
22
|
-
ActiveSupport::JSON.encode(object)
|
23
|
-
end
|
24
|
-
|
25
|
-
def self.load(json)
|
26
|
-
new(json) if json.present?
|
27
|
-
end
|
28
|
-
|
29
20
|
def initialize(data)
|
30
21
|
@data = data
|
31
22
|
end
|
@@ -57,7 +48,7 @@ module Logidze
|
|
57
48
|
end
|
58
49
|
|
59
50
|
# Return diff from the initial state to specified time or version.
|
60
|
-
# Optional `data`
|
51
|
+
# Optional `data` parameter can be used as initial diff state.
|
61
52
|
def changes_to(time: nil, version: nil, data: {}, from: 0)
|
62
53
|
raise ArgumentError, "Time or version must be specified" if time.nil? && version.nil?
|
63
54
|
|
@@ -108,7 +99,7 @@ module Logidze
|
|
108
99
|
|
109
100
|
# Return nearest (from the bottom) version to the specified time
|
110
101
|
def find_by_time(time)
|
111
|
-
versions.
|
102
|
+
versions.reverse_each.find { |v| v.time <= time }
|
112
103
|
end
|
113
104
|
|
114
105
|
def dup
|
@@ -5,9 +5,7 @@ module Logidze
|
|
5
5
|
extend ActiveSupport::Concern
|
6
6
|
|
7
7
|
included do
|
8
|
-
if Rails::VERSION::MAJOR ==
|
9
|
-
require "logidze/ignore_log_data/ignored_columns"
|
10
|
-
elsif Rails::VERSION::MAJOR == 5
|
8
|
+
if Rails::VERSION::MAJOR == 5
|
11
9
|
require "logidze/ignore_log_data/cast_attribute_patch"
|
12
10
|
include CastAttributePatch
|
13
11
|
end
|
data/lib/logidze/meta.rb
CHANGED
@@ -3,18 +3,19 @@
|
|
3
3
|
module Logidze # :nodoc:
|
4
4
|
# Provide methods to attach meta information
|
5
5
|
module Meta
|
6
|
-
def with_meta(meta, &block)
|
7
|
-
|
6
|
+
def with_meta(meta, transactional: true, &block)
|
7
|
+
wrapper = transactional ? MetaWithTransaction : MetaWithoutTransaction
|
8
|
+
wrapper.wrap_with(meta, &block)
|
8
9
|
end
|
9
10
|
|
10
|
-
def with_responsible(responsible_id, &block)
|
11
|
+
def with_responsible(responsible_id, transactional: true, &block)
|
11
12
|
return yield if responsible_id.nil?
|
12
13
|
|
13
14
|
meta = {Logidze::History::Version::META_RESPONSIBLE => responsible_id}
|
14
|
-
with_meta(meta, &block)
|
15
|
+
with_meta(meta, transactional: transactional, &block)
|
15
16
|
end
|
16
17
|
|
17
|
-
class
|
18
|
+
class MetaWrapper # :nodoc:
|
18
19
|
def self.wrap_with(meta, &block)
|
19
20
|
new(meta, &block).perform
|
20
21
|
end
|
@@ -29,14 +30,12 @@ module Logidze # :nodoc:
|
|
29
30
|
end
|
30
31
|
|
31
32
|
def perform
|
32
|
-
|
33
|
+
raise ArgumentError, "Block must be given" unless block
|
33
34
|
return block.call if meta.nil?
|
34
35
|
|
35
|
-
|
36
|
+
call_block_in_meta_context
|
36
37
|
end
|
37
38
|
|
38
|
-
private
|
39
|
-
|
40
39
|
def call_block_in_meta_context
|
41
40
|
prev_meta = current_meta
|
42
41
|
|
@@ -44,10 +43,9 @@ module Logidze # :nodoc:
|
|
44
43
|
|
45
44
|
pg_set_meta_param(current_meta)
|
46
45
|
result = block.call
|
47
|
-
pg_reset_meta_param(prev_meta)
|
48
|
-
|
49
46
|
result
|
50
47
|
ensure
|
48
|
+
pg_reset_meta_param(prev_meta)
|
51
49
|
meta_stack.pop
|
52
50
|
end
|
53
51
|
|
@@ -60,20 +58,49 @@ module Logidze # :nodoc:
|
|
60
58
|
Thread.current[:meta]
|
61
59
|
end
|
62
60
|
|
63
|
-
def
|
64
|
-
|
65
|
-
connection.execute("SET LOCAL logidze.meta = #{encoded_meta};")
|
61
|
+
def encode_meta(value)
|
62
|
+
connection.quote(ActiveSupport::JSON.encode(value))
|
66
63
|
end
|
67
64
|
|
68
65
|
def pg_reset_meta_param(prev_meta)
|
69
66
|
if prev_meta.empty?
|
70
|
-
|
67
|
+
pg_clear_meta_param
|
71
68
|
else
|
72
69
|
pg_set_meta_param(prev_meta)
|
73
70
|
end
|
74
71
|
end
|
75
72
|
end
|
76
73
|
|
77
|
-
|
74
|
+
class MetaWithTransaction < MetaWrapper # :nodoc:
|
75
|
+
private
|
76
|
+
|
77
|
+
def call_block_in_meta_context
|
78
|
+
connection.transaction { super }
|
79
|
+
end
|
80
|
+
|
81
|
+
def pg_set_meta_param(value)
|
82
|
+
connection.execute("SET LOCAL logidze.meta = #{encode_meta(value)};")
|
83
|
+
end
|
84
|
+
|
85
|
+
def pg_clear_meta_param
|
86
|
+
connection.execute("SET LOCAL logidze.meta TO DEFAULT;")
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
class MetaWithoutTransaction < MetaWrapper # :nodoc:
|
91
|
+
private
|
92
|
+
|
93
|
+
def pg_set_meta_param(value)
|
94
|
+
connection.execute("SET logidze.meta = #{encode_meta(value)};")
|
95
|
+
end
|
96
|
+
|
97
|
+
def pg_clear_meta_param
|
98
|
+
connection.execute("SET logidze.meta TO DEFAULT;")
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
private_constant :MetaWrapper
|
103
|
+
private_constant :MetaWithTransaction
|
104
|
+
private_constant :MetaWithoutTransaction
|
78
105
|
end
|
79
106
|
end
|
data/lib/logidze/model.rb
CHANGED
@@ -3,43 +3,28 @@
|
|
3
3
|
require "active_support"
|
4
4
|
|
5
5
|
module Logidze
|
6
|
-
|
7
|
-
def self.show_ts_deprecation_for(meth)
|
8
|
-
warn(
|
9
|
-
"[Deprecation] Usage of #{meth}(time) will be removed in the future releases, "\
|
10
|
-
"use #{meth}(time: ts) instead"
|
11
|
-
)
|
12
|
-
end
|
13
|
-
end
|
6
|
+
using RubyNext
|
14
7
|
|
15
8
|
# Extends model with methods to browse history
|
16
9
|
module Model
|
17
|
-
require "logidze/history/type"
|
10
|
+
require "logidze/history/type"
|
18
11
|
|
19
12
|
extend ActiveSupport::Concern
|
20
13
|
|
21
14
|
included do
|
22
|
-
|
23
|
-
serialize :log_data, Logidze::History
|
24
|
-
else
|
25
|
-
attribute :log_data, Logidze::History::Type.new
|
26
|
-
end
|
15
|
+
attribute :log_data, Logidze::History::Type.new
|
27
16
|
|
28
17
|
delegate :version, to: :log_data, prefix: "log"
|
29
18
|
end
|
30
19
|
|
31
20
|
module ClassMethods # :nodoc:
|
32
21
|
# Return records reverted to specified time
|
33
|
-
def at(
|
34
|
-
|
35
|
-
time ||= ts
|
36
|
-
all.map { |record| record.at(time: time, version: version) }.compact
|
22
|
+
def at(time: nil, version: nil)
|
23
|
+
all.to_a.filter_map { |record| record.at(time: time, version: version) }
|
37
24
|
end
|
38
25
|
|
39
26
|
# Return changes made to records since specified time
|
40
|
-
def diff_from(
|
41
|
-
Deprecations.show_ts_deprecation_for(".diff_from") if ts
|
42
|
-
time ||= ts
|
27
|
+
def diff_from(time: nil, version: nil)
|
43
28
|
all.map { |record| record.diff_from(time: time, version: version) }
|
44
29
|
end
|
45
30
|
|
@@ -58,6 +43,28 @@ module Logidze
|
|
58
43
|
def reset_log_data
|
59
44
|
without_logging { update_all(log_data: nil) }
|
60
45
|
end
|
46
|
+
|
47
|
+
# Initialize log_data with the current state if it's null
|
48
|
+
def create_logidze_snapshot(timestamp: nil, only: nil, except: nil)
|
49
|
+
args = ["'null'"]
|
50
|
+
|
51
|
+
args[0] = "'#{timestamp}'" if timestamp
|
52
|
+
|
53
|
+
columns = only || except
|
54
|
+
|
55
|
+
if columns
|
56
|
+
args[1] = "'{#{columns.join(",")}}'"
|
57
|
+
args[2] = only ? "true" : "false"
|
58
|
+
end
|
59
|
+
|
60
|
+
without_logging do
|
61
|
+
where(log_data: nil).update_all(
|
62
|
+
<<~SQL
|
63
|
+
log_data = logidze_snapshot(to_jsonb(#{quoted_table_name}), #{args.join(", ")})
|
64
|
+
SQL
|
65
|
+
)
|
66
|
+
end
|
67
|
+
end
|
61
68
|
end
|
62
69
|
|
63
70
|
# Use this to convert Ruby time to milliseconds
|
@@ -69,14 +76,15 @@ module Logidze
|
|
69
76
|
# If time/version is less then the first version, then return nil.
|
70
77
|
# If time/version is greater then the last version, then return self.
|
71
78
|
# rubocop: disable Metrics/MethodLength
|
72
|
-
def at(
|
73
|
-
Deprecations.show_ts_deprecation_for("#at") if ts
|
74
|
-
|
79
|
+
def at(time: nil, version: nil)
|
75
80
|
return at_version(version) if version
|
76
81
|
|
77
|
-
time ||= ts
|
78
82
|
time = parse_time(time)
|
79
83
|
|
84
|
+
unless log_data
|
85
|
+
return Logidze.return_self_if_log_data_is_empty ? self : nil
|
86
|
+
end
|
87
|
+
|
80
88
|
return nil unless log_data.exists_ts?(time)
|
81
89
|
|
82
90
|
if log_data.current_ts?(time)
|
@@ -91,12 +99,11 @@ module Logidze
|
|
91
99
|
# rubocop: enable Metrics/MethodLength
|
92
100
|
|
93
101
|
# Revert record to the version at specified time (without saving to DB)
|
94
|
-
def at!(
|
95
|
-
Deprecations.show_ts_deprecation_for("#at!") if ts
|
96
|
-
|
102
|
+
def at!(time: nil, version: nil)
|
97
103
|
return at_version!(version) if version
|
98
104
|
|
99
|
-
|
105
|
+
raise ArgumentError, "#log_data is empty" unless log_data
|
106
|
+
|
100
107
|
time = parse_time(time)
|
101
108
|
|
102
109
|
return self if log_data.current_ts?(time)
|
@@ -119,6 +126,8 @@ module Logidze
|
|
119
126
|
|
120
127
|
# Revert record to the specified version (without saving to DB)
|
121
128
|
def at_version!(version)
|
129
|
+
raise ArgumentError, "#log_data is empty" unless log_data
|
130
|
+
|
122
131
|
return self if log_data.version == version
|
123
132
|
return false unless log_data.find_by_version(version)
|
124
133
|
|
@@ -131,13 +140,11 @@ module Logidze
|
|
131
140
|
#
|
132
141
|
# post.diff_from(time: 2.days.ago) # or post.diff_from(version: 2)
|
133
142
|
# #=> { "id" => 1, "changes" => { "title" => { "old" => "Hello!", "new" => "World" } } }
|
134
|
-
def diff_from(
|
135
|
-
Deprecations.show_ts_deprecation_for("#diff_from") if ts
|
136
|
-
time ||= ts
|
143
|
+
def diff_from(version: nil, time: nil)
|
137
144
|
time = parse_time(time) if time
|
138
|
-
changes = log_data
|
145
|
+
changes = log_data&.diff_from(time: time, version: version)&.tap do |v|
|
139
146
|
deserialize_changes!(v)
|
140
|
-
end
|
147
|
+
end || {}
|
141
148
|
|
142
149
|
changes.delete_if { |k, _v| deleted_column?(k) }
|
143
150
|
|
@@ -206,7 +213,7 @@ module Logidze
|
|
206
213
|
|
207
214
|
# Loads log_data field from the database, stores to the attributes hash and returns it
|
208
215
|
def reload_log_data
|
209
|
-
self.log_data = self.class.where(self.class.primary_key => id).pluck(
|
216
|
+
self.log_data = self.class.where(self.class.primary_key => id).pluck("#{self.class.table_name}.log_data").first
|
210
217
|
end
|
211
218
|
|
212
219
|
# Nullify log_data column for a single record
|
@@ -214,6 +221,12 @@ module Logidze
|
|
214
221
|
self.class.without_logging { update_column(:log_data, nil) }
|
215
222
|
end
|
216
223
|
|
224
|
+
def create_logidze_snapshot!(**opts)
|
225
|
+
self.class.where(self.class.primary_key => id).create_logidze_snapshot(**opts)
|
226
|
+
|
227
|
+
reload_log_data
|
228
|
+
end
|
229
|
+
|
217
230
|
protected
|
218
231
|
|
219
232
|
def apply_diff(version, diff)
|
@@ -226,7 +239,7 @@ module Logidze
|
|
226
239
|
end
|
227
240
|
|
228
241
|
def apply_column_diff(column, value)
|
229
|
-
return if deleted_column?(column)
|
242
|
+
return if deleted_column?(column) || column == "log_data"
|
230
243
|
|
231
244
|
write_attribute column, deserialize_value(column, value)
|
232
245
|
end
|
@@ -240,14 +253,8 @@ module Logidze
|
|
240
253
|
object_at
|
241
254
|
end
|
242
255
|
|
243
|
-
|
244
|
-
|
245
|
-
@attributes[column].type.type_cast_from_database(value)
|
246
|
-
end
|
247
|
-
else
|
248
|
-
def deserialize_value(column, value)
|
249
|
-
@attributes[column].type.deserialize(value)
|
250
|
-
end
|
256
|
+
def deserialize_value(column, value)
|
257
|
+
@attributes[column].type.deserialize(value)
|
251
258
|
end
|
252
259
|
|
253
260
|
def deleted_column?(column)
|