pg_sql_triggers 1.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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +120 -0
- data/CHANGELOG.md +52 -0
- data/Goal.md +294 -0
- data/LICENSE +21 -0
- data/README.md +294 -0
- data/RELEASE.md +270 -0
- data/Rakefile +16 -0
- data/app/assets/javascripts/pg_sql_triggers/application.js +5 -0
- data/app/assets/stylesheets/pg_sql_triggers/application.css +179 -0
- data/app/controllers/pg_sql_triggers/application_controller.rb +35 -0
- data/app/controllers/pg_sql_triggers/dashboard_controller.rb +42 -0
- data/app/controllers/pg_sql_triggers/generator_controller.rb +145 -0
- data/app/controllers/pg_sql_triggers/migrations_controller.rb +84 -0
- data/app/controllers/pg_sql_triggers/tables_controller.rb +44 -0
- data/app/models/pg_sql_triggers/application_record.rb +7 -0
- data/app/models/pg_sql_triggers/trigger_registry.rb +93 -0
- data/app/views/layouts/pg_sql_triggers/application.html.erb +72 -0
- data/app/views/pg_sql_triggers/dashboard/index.html.erb +225 -0
- data/app/views/pg_sql_triggers/generator/new.html.erb +370 -0
- data/app/views/pg_sql_triggers/generator/preview.html.erb +77 -0
- data/app/views/pg_sql_triggers/tables/index.html.erb +105 -0
- data/app/views/pg_sql_triggers/tables/show.html.erb +126 -0
- data/config/routes.rb +35 -0
- data/db/migrate/20251222000001_create_pg_sql_triggers_tables.rb +29 -0
- data/lib/generators/pg_sql_triggers/install_generator.rb +36 -0
- data/lib/generators/pg_sql_triggers/templates/README +36 -0
- data/lib/generators/pg_sql_triggers/templates/create_pg_sql_triggers_tables.rb +36 -0
- data/lib/generators/pg_sql_triggers/templates/initializer.rb +27 -0
- data/lib/generators/pg_sql_triggers/templates/trigger_migration.rb.erb +32 -0
- data/lib/generators/pg_sql_triggers/trigger_migration_generator.rb +60 -0
- data/lib/generators/trigger/migration_generator.rb +60 -0
- data/lib/pg_sql_triggers/database_introspection.rb +251 -0
- data/lib/pg_sql_triggers/drift.rb +24 -0
- data/lib/pg_sql_triggers/dsl/trigger_definition.rb +67 -0
- data/lib/pg_sql_triggers/dsl.rb +15 -0
- data/lib/pg_sql_triggers/engine.rb +29 -0
- data/lib/pg_sql_triggers/generator/form.rb +78 -0
- data/lib/pg_sql_triggers/generator/service.rb +251 -0
- data/lib/pg_sql_triggers/generator.rb +8 -0
- data/lib/pg_sql_triggers/migration.rb +15 -0
- data/lib/pg_sql_triggers/migrator.rb +237 -0
- data/lib/pg_sql_triggers/permissions/checker.rb +33 -0
- data/lib/pg_sql_triggers/permissions.rb +35 -0
- data/lib/pg_sql_triggers/registry/manager.rb +47 -0
- data/lib/pg_sql_triggers/registry/validator.rb +15 -0
- data/lib/pg_sql_triggers/registry.rb +36 -0
- data/lib/pg_sql_triggers/sql.rb +21 -0
- data/lib/pg_sql_triggers/testing/dry_run.rb +74 -0
- data/lib/pg_sql_triggers/testing/function_tester.rb +118 -0
- data/lib/pg_sql_triggers/testing/safe_executor.rb +66 -0
- data/lib/pg_sql_triggers/testing/syntax_validator.rb +124 -0
- data/lib/pg_sql_triggers/testing.rb +10 -0
- data/lib/pg_sql_triggers/version.rb +15 -0
- data/lib/pg_sql_triggers.rb +41 -0
- data/lib/tasks/trigger_migrations.rake +254 -0
- data/sig/pg_sql_triggers.rbs +4 -0
- metadata +260 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PgSqlTriggers
|
|
4
|
+
class DatabaseIntrospection
|
|
5
|
+
# Default tables to exclude from listing (Rails defaults and pg_sql_triggers internal tables)
|
|
6
|
+
DEFAULT_EXCLUDED_TABLES = %w[
|
|
7
|
+
ar_internal_metadata
|
|
8
|
+
schema_migrations
|
|
9
|
+
pg_sql_triggers_registry
|
|
10
|
+
trigger_migrations
|
|
11
|
+
].freeze
|
|
12
|
+
|
|
13
|
+
# Get list of all excluded tables (defaults + user-configured)
|
|
14
|
+
def excluded_tables
|
|
15
|
+
(DEFAULT_EXCLUDED_TABLES + Array(PgSqlTriggers.excluded_tables)).uniq
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Get list of all user tables in the database
|
|
19
|
+
def list_tables
|
|
20
|
+
sql = <<~SQL.squish
|
|
21
|
+
SELECT table_name
|
|
22
|
+
FROM information_schema.tables
|
|
23
|
+
WHERE table_schema = 'public'
|
|
24
|
+
AND table_type = 'BASE TABLE'
|
|
25
|
+
ORDER BY table_name
|
|
26
|
+
SQL
|
|
27
|
+
|
|
28
|
+
result = ActiveRecord::Base.connection.execute(sql)
|
|
29
|
+
tables = result.pluck("table_name")
|
|
30
|
+
tables.reject { |table| excluded_tables.include?(table) }
|
|
31
|
+
rescue StandardError => e
|
|
32
|
+
Rails.logger.error("Failed to fetch tables: #{e.message}") if defined?(Rails.logger)
|
|
33
|
+
[]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Validate that a table exists
|
|
37
|
+
def validate_table(table_name)
|
|
38
|
+
return { valid: false, error: "Table name cannot be blank" } if table_name.blank?
|
|
39
|
+
|
|
40
|
+
# Use case-insensitive comparison and sanitize input
|
|
41
|
+
sanitized_name = sanitize(table_name)
|
|
42
|
+
|
|
43
|
+
# First, check if table exists and get column count
|
|
44
|
+
column_count_sql = <<~SQL.squish
|
|
45
|
+
SELECT COUNT(*) as column_count
|
|
46
|
+
FROM information_schema.columns
|
|
47
|
+
WHERE table_schema = 'public'
|
|
48
|
+
AND LOWER(table_name) = LOWER('#{sanitized_name}')
|
|
49
|
+
SQL
|
|
50
|
+
|
|
51
|
+
column_result = ActiveRecord::Base.connection.execute(column_count_sql).first
|
|
52
|
+
column_count = column_result ? column_result["column_count"].to_i : 0
|
|
53
|
+
|
|
54
|
+
if column_count.positive?
|
|
55
|
+
# Get table comment separately
|
|
56
|
+
comment_sql = <<~SQL.squish
|
|
57
|
+
SELECT obj_description(c.oid, 'pg_class') as comment
|
|
58
|
+
FROM pg_class c
|
|
59
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
60
|
+
WHERE n.nspname = 'public'
|
|
61
|
+
AND LOWER(c.relname) = LOWER('#{sanitized_name}')
|
|
62
|
+
AND c.relkind = 'r'
|
|
63
|
+
SQL
|
|
64
|
+
|
|
65
|
+
comment_result = ActiveRecord::Base.connection.execute(comment_sql).first
|
|
66
|
+
comment = comment_result ? comment_result["comment"] : nil
|
|
67
|
+
|
|
68
|
+
{
|
|
69
|
+
valid: true,
|
|
70
|
+
table_name: table_name,
|
|
71
|
+
column_count: column_count,
|
|
72
|
+
comment: comment
|
|
73
|
+
}
|
|
74
|
+
else
|
|
75
|
+
{
|
|
76
|
+
valid: false,
|
|
77
|
+
error: "Table '#{table_name}' not found in database"
|
|
78
|
+
}
|
|
79
|
+
end
|
|
80
|
+
rescue StandardError => e
|
|
81
|
+
Rails.logger.error("Table validation error for '#{table_name}': #{e.message}")
|
|
82
|
+
{
|
|
83
|
+
valid: false,
|
|
84
|
+
error: e.message
|
|
85
|
+
}
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Get table columns
|
|
89
|
+
def table_columns(table_name)
|
|
90
|
+
sql = <<~SQL.squish
|
|
91
|
+
SELECT column_name, data_type, is_nullable
|
|
92
|
+
FROM information_schema.columns
|
|
93
|
+
WHERE table_schema = 'public'
|
|
94
|
+
AND table_name = '#{sanitize(table_name)}'
|
|
95
|
+
ORDER BY ordinal_position
|
|
96
|
+
SQL
|
|
97
|
+
|
|
98
|
+
result = ActiveRecord::Base.connection.execute(sql)
|
|
99
|
+
result.map do |row|
|
|
100
|
+
{
|
|
101
|
+
name: row["column_name"],
|
|
102
|
+
type: row["data_type"],
|
|
103
|
+
nullable: row["is_nullable"] == "YES"
|
|
104
|
+
}
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Check if function exists
|
|
109
|
+
def function_exists?(function_name)
|
|
110
|
+
sql = <<~SQL.squish
|
|
111
|
+
SELECT COUNT(*) as count
|
|
112
|
+
FROM pg_proc
|
|
113
|
+
WHERE proname = '#{sanitize(function_name)}'
|
|
114
|
+
SQL
|
|
115
|
+
|
|
116
|
+
result = ActiveRecord::Base.connection.execute(sql).first
|
|
117
|
+
result["count"].to_i.positive?
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Check if trigger exists
|
|
121
|
+
def trigger_exists?(trigger_name)
|
|
122
|
+
sql = <<~SQL.squish
|
|
123
|
+
SELECT COUNT(*) as count
|
|
124
|
+
FROM pg_trigger t
|
|
125
|
+
JOIN pg_class c ON t.tgrelid = c.oid
|
|
126
|
+
JOIN pg_namespace n ON c.relnamespace = n.oid
|
|
127
|
+
WHERE t.tgname = '#{sanitize(trigger_name)}'
|
|
128
|
+
AND n.nspname = 'public'
|
|
129
|
+
AND NOT t.tgisinternal
|
|
130
|
+
SQL
|
|
131
|
+
|
|
132
|
+
result = ActiveRecord::Base.connection.execute(sql).first
|
|
133
|
+
result["count"].to_i.positive?
|
|
134
|
+
rescue StandardError => e
|
|
135
|
+
Rails.logger.error("Failed to check if trigger exists: #{e.message}") if defined?(Rails.logger)
|
|
136
|
+
false
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Get all tables with their triggers and functions
|
|
140
|
+
def tables_with_triggers
|
|
141
|
+
# Get all tables
|
|
142
|
+
tables = list_tables
|
|
143
|
+
|
|
144
|
+
# Get all triggers from registry
|
|
145
|
+
triggers_by_table = PgSqlTriggers::TriggerRegistry.all.group_by(&:table_name)
|
|
146
|
+
|
|
147
|
+
# Get actual database triggers
|
|
148
|
+
db_triggers_sql = <<~SQL.squish
|
|
149
|
+
SELECT#{' '}
|
|
150
|
+
t.tgname as trigger_name,
|
|
151
|
+
c.relname as table_name,
|
|
152
|
+
p.proname as function_name,
|
|
153
|
+
pg_get_triggerdef(t.oid) as trigger_definition
|
|
154
|
+
FROM pg_trigger t
|
|
155
|
+
JOIN pg_class c ON t.tgrelid = c.oid
|
|
156
|
+
JOIN pg_proc p ON t.tgfoid = p.oid
|
|
157
|
+
JOIN pg_namespace n ON c.relnamespace = n.oid
|
|
158
|
+
WHERE NOT t.tgisinternal
|
|
159
|
+
AND n.nspname = 'public'
|
|
160
|
+
ORDER BY c.relname, t.tgname
|
|
161
|
+
SQL
|
|
162
|
+
|
|
163
|
+
db_triggers = {}
|
|
164
|
+
begin
|
|
165
|
+
result = ActiveRecord::Base.connection.execute(db_triggers_sql)
|
|
166
|
+
result.each do |row|
|
|
167
|
+
table_name = row["table_name"]
|
|
168
|
+
db_triggers[table_name] ||= []
|
|
169
|
+
db_triggers[table_name] << {
|
|
170
|
+
trigger_name: row["trigger_name"],
|
|
171
|
+
function_name: row["function_name"],
|
|
172
|
+
definition: row["trigger_definition"]
|
|
173
|
+
}
|
|
174
|
+
end
|
|
175
|
+
rescue StandardError => e
|
|
176
|
+
Rails.logger.error("Failed to fetch database triggers: #{e.message}")
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Combine registry and database triggers
|
|
180
|
+
tables.map do |table_name|
|
|
181
|
+
registry_triggers = triggers_by_table[table_name] || []
|
|
182
|
+
db_table_triggers = db_triggers[table_name] || []
|
|
183
|
+
|
|
184
|
+
{
|
|
185
|
+
table_name: table_name,
|
|
186
|
+
registry_triggers: registry_triggers.map do |t|
|
|
187
|
+
{
|
|
188
|
+
id: t.id,
|
|
189
|
+
trigger_name: t.trigger_name,
|
|
190
|
+
function_name: t.definition.present? ? JSON.parse(t.definition)["function_name"] : nil,
|
|
191
|
+
enabled: t.enabled,
|
|
192
|
+
version: t.version,
|
|
193
|
+
source: t.source,
|
|
194
|
+
function_body: t.function_body
|
|
195
|
+
}
|
|
196
|
+
end,
|
|
197
|
+
database_triggers: db_table_triggers,
|
|
198
|
+
trigger_count: registry_triggers.count + db_table_triggers.count
|
|
199
|
+
}
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Get triggers for a specific table
|
|
204
|
+
def table_triggers(table_name)
|
|
205
|
+
# From registry
|
|
206
|
+
registry_triggers = PgSqlTriggers::TriggerRegistry.for_table(table_name)
|
|
207
|
+
|
|
208
|
+
# From database
|
|
209
|
+
db_triggers_sql = <<~SQL.squish
|
|
210
|
+
SELECT#{' '}
|
|
211
|
+
t.tgname as trigger_name,
|
|
212
|
+
p.proname as function_name,
|
|
213
|
+
pg_get_triggerdef(t.oid) as trigger_definition
|
|
214
|
+
FROM pg_trigger t
|
|
215
|
+
JOIN pg_class c ON t.tgrelid = c.oid
|
|
216
|
+
JOIN pg_proc p ON t.tgfoid = p.oid
|
|
217
|
+
JOIN pg_namespace n ON c.relnamespace = n.oid
|
|
218
|
+
WHERE NOT t.tgisinternal
|
|
219
|
+
AND c.relname = '#{sanitize(table_name)}'
|
|
220
|
+
AND n.nspname = 'public'
|
|
221
|
+
ORDER BY t.tgname
|
|
222
|
+
SQL
|
|
223
|
+
|
|
224
|
+
db_triggers = []
|
|
225
|
+
begin
|
|
226
|
+
result = ActiveRecord::Base.connection.execute(db_triggers_sql)
|
|
227
|
+
result.each do |row|
|
|
228
|
+
db_triggers << {
|
|
229
|
+
trigger_name: row["trigger_name"],
|
|
230
|
+
function_name: row["function_name"],
|
|
231
|
+
definition: row["trigger_definition"]
|
|
232
|
+
}
|
|
233
|
+
end
|
|
234
|
+
rescue StandardError => e
|
|
235
|
+
Rails.logger.error("Failed to fetch database triggers: #{e.message}")
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
{
|
|
239
|
+
table_name: table_name,
|
|
240
|
+
registry_triggers: registry_triggers,
|
|
241
|
+
database_triggers: db_triggers
|
|
242
|
+
}
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
private
|
|
246
|
+
|
|
247
|
+
def sanitize(value)
|
|
248
|
+
ActiveRecord::Base.connection.quote_string(value.to_s)
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PgSqlTriggers
|
|
4
|
+
module Drift
|
|
5
|
+
autoload :Detector, "pg_sql_triggers/drift/detector"
|
|
6
|
+
autoload :Reporter, "pg_sql_triggers/drift/reporter"
|
|
7
|
+
|
|
8
|
+
# Drift states
|
|
9
|
+
MANAGED_IN_SYNC = "managed_in_sync"
|
|
10
|
+
MANAGED_DRIFTED = "managed_drifted"
|
|
11
|
+
MANUAL_OVERRIDE = "manual_override"
|
|
12
|
+
DISABLED = "disabled"
|
|
13
|
+
DROPPED = "dropped"
|
|
14
|
+
UNKNOWN = "unknown"
|
|
15
|
+
|
|
16
|
+
def self.detect(trigger_name = nil)
|
|
17
|
+
Detector.detect(trigger_name)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.report
|
|
21
|
+
Reporter.report
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PgSqlTriggers
|
|
4
|
+
module DSL
|
|
5
|
+
class TriggerDefinition
|
|
6
|
+
attr_accessor :name, :table_name, :events, :function_name, :environments, :condition
|
|
7
|
+
|
|
8
|
+
def initialize(name)
|
|
9
|
+
@name = name
|
|
10
|
+
@events = []
|
|
11
|
+
@version = 1
|
|
12
|
+
@enabled = false
|
|
13
|
+
@environments = []
|
|
14
|
+
@condition = nil
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def table(table_name)
|
|
18
|
+
@table_name = table_name
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def on(*events)
|
|
22
|
+
@events = events.map(&:to_s)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def function(function_name)
|
|
26
|
+
@function_name = function_name
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def version(version = nil)
|
|
30
|
+
if version.nil?
|
|
31
|
+
@version
|
|
32
|
+
else
|
|
33
|
+
@version = version
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def enabled(enabled = nil)
|
|
38
|
+
if enabled.nil?
|
|
39
|
+
@enabled
|
|
40
|
+
else
|
|
41
|
+
@enabled = enabled
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def when_env(*environments)
|
|
46
|
+
@environments = environments.map(&:to_s)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def when_condition(condition_sql)
|
|
50
|
+
@condition = condition_sql
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def to_h
|
|
54
|
+
{
|
|
55
|
+
name: @name,
|
|
56
|
+
table_name: @table_name,
|
|
57
|
+
events: @events,
|
|
58
|
+
function_name: @function_name,
|
|
59
|
+
version: @version,
|
|
60
|
+
enabled: @enabled,
|
|
61
|
+
environments: @environments,
|
|
62
|
+
condition: @condition
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PgSqlTriggers
|
|
4
|
+
module DSL
|
|
5
|
+
autoload :TriggerDefinition, "pg_sql_triggers/dsl/trigger_definition"
|
|
6
|
+
autoload :Builder, "pg_sql_triggers/dsl/builder"
|
|
7
|
+
|
|
8
|
+
def self.pg_sql_trigger(name, &block)
|
|
9
|
+
definition = TriggerDefinition.new(name)
|
|
10
|
+
definition.instance_eval(&block)
|
|
11
|
+
Registry.register(definition)
|
|
12
|
+
definition
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PgSqlTriggers
|
|
4
|
+
class Engine < ::Rails::Engine
|
|
5
|
+
isolate_namespace PgSqlTriggers
|
|
6
|
+
|
|
7
|
+
config.generators do |g|
|
|
8
|
+
g.test_framework :rspec
|
|
9
|
+
g.fixture_replacement :factory_bot
|
|
10
|
+
g.factory_bot dir: "spec/factories"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Configure assets
|
|
14
|
+
initializer "pg_sql_triggers.assets" do |app|
|
|
15
|
+
# Rails engines automatically add app/assets to paths, but we explicitly add
|
|
16
|
+
# the stylesheets and javascripts directories to ensure they're found
|
|
17
|
+
if app.config.respond_to?(:assets)
|
|
18
|
+
app.config.assets.paths << root.join("app/assets/stylesheets").to_s
|
|
19
|
+
app.config.assets.paths << root.join("app/assets/javascripts").to_s
|
|
20
|
+
app.config.assets.precompile += %w[pg_sql_triggers/application.css pg_sql_triggers/application.js]
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Load rake tasks
|
|
25
|
+
rake_tasks do
|
|
26
|
+
load root.join("lib/tasks/trigger_migrations.rake")
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PgSqlTriggers
|
|
4
|
+
module Generator
|
|
5
|
+
class Form
|
|
6
|
+
include ActiveModel::Model
|
|
7
|
+
|
|
8
|
+
attr_accessor :trigger_name, :table_name, :function_name,
|
|
9
|
+
:version, :enabled, :condition,
|
|
10
|
+
:generate_function_stub, :events, :environments,
|
|
11
|
+
:function_body
|
|
12
|
+
|
|
13
|
+
validates :trigger_name, presence: true,
|
|
14
|
+
format: {
|
|
15
|
+
with: /\A[a-z0-9_]+\z/,
|
|
16
|
+
message: "must contain only lowercase letters, numbers, and underscores"
|
|
17
|
+
}
|
|
18
|
+
validates :table_name, presence: true
|
|
19
|
+
validates :function_name, presence: true,
|
|
20
|
+
format: {
|
|
21
|
+
with: /\A[a-z0-9_]+\z/,
|
|
22
|
+
message: "must contain only lowercase letters, numbers, and underscores"
|
|
23
|
+
}
|
|
24
|
+
validates :version, presence: true, numericality: { only_integer: true, greater_than: 0 }
|
|
25
|
+
validates :function_body, presence: true
|
|
26
|
+
validate :at_least_one_event
|
|
27
|
+
validate :function_name_matches_body
|
|
28
|
+
|
|
29
|
+
def initialize(attributes = {})
|
|
30
|
+
super
|
|
31
|
+
@version ||= 1
|
|
32
|
+
# Convert enabled to boolean (Rails checkboxes send "0" or "1" as strings)
|
|
33
|
+
# Default to true for UI-generated triggers
|
|
34
|
+
@enabled = case @enabled
|
|
35
|
+
when false, "0", 0 then false
|
|
36
|
+
else true # true, "1", 1, or nil - default to true
|
|
37
|
+
end
|
|
38
|
+
@generate_function_stub = true if @generate_function_stub.nil?
|
|
39
|
+
@events ||= []
|
|
40
|
+
@environments ||= []
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def default_function_body
|
|
44
|
+
func_name = function_name.presence || "function_name"
|
|
45
|
+
<<~SQL.chomp
|
|
46
|
+
CREATE OR REPLACE FUNCTION #{func_name}()
|
|
47
|
+
RETURNS TRIGGER AS $$
|
|
48
|
+
BEGIN
|
|
49
|
+
-- Your trigger logic here
|
|
50
|
+
RETURN NEW;
|
|
51
|
+
END;
|
|
52
|
+
$$ LANGUAGE plpgsql;
|
|
53
|
+
SQL
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def at_least_one_event
|
|
59
|
+
return unless events.blank? || events.compact_blank.empty?
|
|
60
|
+
|
|
61
|
+
errors.add(:events, "must include at least one event")
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def function_name_matches_body
|
|
65
|
+
return if function_name.blank? || function_body.blank?
|
|
66
|
+
|
|
67
|
+
# Check if function_body contains the function_name in a CREATE FUNCTION statement
|
|
68
|
+
# Look for pattern: CREATE [OR REPLACE] FUNCTION function_name
|
|
69
|
+
function_pattern = /CREATE\s+(?:OR\s+REPLACE\s+)?FUNCTION\s+(?:[^(\s]+\.)?#{Regexp.escape(function_name)}\s*\(/i
|
|
70
|
+
return if function_body.match?(function_pattern)
|
|
71
|
+
|
|
72
|
+
expected_msg = "should define function '#{function_name}' " \
|
|
73
|
+
"(expected: CREATE [OR REPLACE] FUNCTION #{function_name}(...)"
|
|
74
|
+
errors.add(:function_body, expected_msg)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|