sequel_postgresql_triggers 1.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c908ae3d087271626842952b60bc22365793a3d9
4
- data.tar.gz: 1cf7c4c70437c36ab0852d566d600756314c90d5
3
+ metadata.gz: 4d2e84d8e162f972268dc9839f8f744651ff15ca
4
+ data.tar.gz: 343a12a7c9531e694a92eeb1a037821095401bc2
5
5
  SHA512:
6
- metadata.gz: 6f10299a3e24a54147217af11e85317484b02b43a0f9faba1120f5f1bfdd402b8f281c4b403f0f785175c94196d6079a9719667965383102e5732f6e0ca1c9b9
7
- data.tar.gz: 06f7baa57db107b2fec9d169ebe03f87b737f611adbc302e8af3f0dda2695d0c96d74dd68b50be4b1c7cb0715c4f40b1b17318f41bdf1ed2755defdf94e70ea2
6
+ metadata.gz: 931984b649f29b90b738e00546c0c388fbea363136afc736cfcd69a0ff69b36d90ec5f429d2f5c3157b41b246eb9d9aaa13798f30c5eb12a205ed6f4bbe27c54
7
+ data.tar.gz: 7061eb0436fbe68db834aa130b503a834871bc2f554025fd851ce2411edd3bf964697d84a1feb01a979e40f644862b15ffd386d967a02849ca9b6c0ac45af61f
data/README.rdoc ADDED
@@ -0,0 +1,175 @@
1
+ = Sequel PostgreSQL Triggers
2
+
3
+ Sequel PostgreSQL Triggers is a small enhancement to Sequel allowing
4
+ a user to easily handle the following types of columns:
5
+
6
+ * Timestamp Columns (Created At/Updated At)
7
+ * Counter/Sum Caches
8
+ * Immutable Columns
9
+ * Touch Propogation
10
+
11
+ It handles these internally to the database via triggers, so even if
12
+ other applications access the database (without using Sequel), things
13
+ will still work (unless the database superuser disables triggers).
14
+
15
+ To use this, load the +pg_triggers+ extension into the Sequel::Database
16
+ object:
17
+
18
+ DB.extension :pg_triggers
19
+
20
+ Then you can call the pgt_* methods it adds on your Sequel::Database
21
+ object:
22
+
23
+ DB.pgt_created_at(:table_name, :created_at)
24
+
25
+ Most commonly, this is used in migrations, with a structure similar
26
+ to:
27
+
28
+ Sequel.migration do
29
+ up do
30
+ extension :pg_triggers
31
+
32
+ pgt_created_at(:table_name,
33
+ :created_at,
34
+ :function_name=>:table_name_set_created_at,
35
+ :trigger_name=>:set_created_at)
36
+ end
37
+
38
+ down do
39
+ drop_trigger(:table_name, :set_created_at)
40
+ drop_function(:table_name_set_created_at)
41
+ end
42
+ end
43
+
44
+ Note that you only need to load this extension when defining the
45
+ triggers, you don't need to load this extension when your
46
+ application is running.
47
+
48
+ To use any of these methods before PostgreSQL 9.0, you have to add
49
+ the plpgsql procedural language to PostgreSQL, which you can do with:
50
+
51
+ DB.create_language(:plpgsql)
52
+
53
+ If you want to load this extension globally for all PostgreSQL
54
+ databases, you can do:
55
+
56
+ require 'sequel_postgresql_triggers'
57
+
58
+ However, global modification is discouraged and only remains for
59
+ backwards compatibility.
60
+
61
+ == Triggers
62
+
63
+ All of the public methods this extension adds take the following options
64
+ in their opts hash:
65
+
66
+ :function_name :: The name of the function to use. This is important
67
+ to specify if you want an easy way to drop the function.
68
+ :trigger_name :: The name of the trigger to use. This is important
69
+ to specify if you want an easy way to drop the trigger.
70
+
71
+ === Created At Columns - pgt_created_at
72
+
73
+ pgt_created_at takes the table and column given and makes it so that
74
+ upon insertion, the column is set to the CURRENT_TIMESTAMP, and that
75
+ upon update, the column's value is always set to the previous value.
76
+ This is sort of like an immutable column, but it doesn't bring up an
77
+ error if you try to change it, it just ignores it.
78
+
79
+ Arguments:
80
+ table :: name of table
81
+ column :: column in table that should be a created at timestamp column
82
+ opts :: option hash
83
+
84
+ === Updated At Columns - pgt_updated_at
85
+
86
+ Similar to pgt_created_at, takes a table and column and makes it so
87
+ that upon insertion, the column is set to CURRENT_TIMESTAMP. It
88
+ differs that upon update, the column is also set to CURRENT_TIMESTAMP.
89
+
90
+ Arguments:
91
+ table :: name of table
92
+ column :: column in table that should be a updated at timestamp column
93
+ opts :: options hash
94
+
95
+ === Counter Cache - pgt_counter_cache
96
+
97
+ This takes quite a few arguments (see the RDoc) and sets up a
98
+ counter cache so that when the counted table is inserted to
99
+ or deleted from, records in the main table are updated with the
100
+ count of the corresponding records in the counted table. The counter
101
+ cache column must have a default of 0 for this to work correctly.
102
+
103
+ Arguments:
104
+ main_table :: name of table holding counter cache column
105
+ main_table_id_column :: column in main table matching counted_table_id_column in counted_table
106
+ counter_column :: column in main table containing the counter cache
107
+ counted_table :: name of table being counted
108
+ counted_table_id_column :: column in counted_table matching main_table_id_column in main_table
109
+ opts :: options hash
110
+
111
+ === Sum Cache - pgt_sum_cache
112
+
113
+ Similar to pgt_counter_cache, except instead of storing a count
114
+ of records in the main table, it stores the sum on one of the
115
+ columns in summed table. The sum cache column must have a default
116
+ of 0 for this to work correctly.
117
+
118
+ Arguments:
119
+ main_table :: name of table holding counter cache column
120
+ main_table_id_column :: column in main table matching counted_table_id_column in counted_table
121
+ sum_column :: column in main table containing the sum cache
122
+ summed_table :: name of table being summed
123
+ summed_table_id_column :: column in summed_table matching main_table_id_column in main_table
124
+ summed_column :: column in summed_table being summed
125
+ opts :: options hash
126
+
127
+ === Sum Through Many Cache - pgt_sum_through_many_cache
128
+
129
+ Similar to pgt_sum_cache, except instead of a one-to-many relationship,
130
+ it supports a many-to-many relationship with a single join table. The
131
+ sum cache column must have a default of 0 for this to work correctly.
132
+
133
+ This takes a single options hash argument, supporting the following options
134
+ in addition to the standard options:
135
+ :main_table :: name of table holding sum cache column
136
+ :main_table_id_column :: primary key column in main table referenced by main_table_fk_column (default: :id)
137
+ :sum_column :: column in main table containing the sum cache, must be NOT NULL and default to 0
138
+ :summed_table :: name of table being summed
139
+ :summed_table_id_column :: primary key column in summed_table referenced by summed_table_fk_column (default: :id)
140
+ :summed_column :: column in summed_table being summed, must be NOT NULL
141
+ :join_table :: name of table which joins main_table with summed_table
142
+ :main_table_fk_column :: column in join_table referencing main_table_id_column, must be NOT NULL
143
+ :summed_table_fk_column :: column in join_table referencing summed_table_id_column, must be NOT NULL
144
+
145
+ === Immutable Columns - pgt_immutable
146
+
147
+ This takes a table name and one or more column names, and adds
148
+ an update trigger that raises an exception if you try to modify
149
+ the value of any of the columns.
150
+
151
+ Arguments:
152
+ table :: name of table
153
+ *columns :: All columns in the table that should be immutable. Can end with options hash.
154
+
155
+ === Touch Propagation - pgt_touch
156
+
157
+ This takes several arguments (again, see the RDoc) and sets up a
158
+ trigger that watches one table for changes, and touches timestamps
159
+ of related rows in a separate table.
160
+
161
+ Arguments:
162
+ main_table :: name of table that is being watched for changes
163
+ touch_table :: name of table that needs to be touched
164
+ column :: name of timestamp column to be touched
165
+ expr :: hash or array that represents the columns that define the relationship
166
+ opts :: options hash
167
+
168
+ == License
169
+
170
+ This library is released under the MIT License. See the MIT-LICENSE
171
+ file for details.
172
+
173
+ == Author
174
+
175
+ Jeremy Evans <code@jeremyevans.net>
@@ -0,0 +1,225 @@
1
+ # The pg_triggers extension adds support to the Database instance for easily
2
+ # creating triggers and trigger returning functions for common needs.
3
+
4
+ #
5
+ module Sequel
6
+ module Postgres
7
+ PGT_DEFINE = proc do
8
+ def pgt_counter_cache(main_table, main_table_id_column, counter_column, counted_table, counted_table_id_column, opts={})
9
+ trigger_name = opts[:trigger_name] || "pgt_cc_#{main_table}__#{main_table_id_column}__#{counter_column}__#{counted_table_id_column}"
10
+ function_name = opts[:function_name] || "pgt_cc_#{main_table}__#{main_table_id_column}__#{counter_column}__#{counted_table}__#{counted_table_id_column}"
11
+
12
+ table = quote_schema_table(main_table)
13
+ id_column = quote_identifier(counted_table_id_column)
14
+ main_column = quote_identifier(main_table_id_column)
15
+ count_column = quote_identifier(counter_column)
16
+
17
+ pgt_trigger(counted_table, trigger_name, function_name, [:insert, :update, :delete], <<-SQL)
18
+ BEGIN
19
+ IF (TG_OP = 'UPDATE' AND (NEW.#{id_column} = OLD.#{id_column} OR (OLD.#{id_column} IS NULL AND NEW.#{id_column} IS NULL))) THEN
20
+ RETURN NEW;
21
+ ELSE
22
+ IF ((TG_OP = 'INSERT' OR TG_OP = 'UPDATE') AND NEW.#{id_column} IS NOT NULL) THEN
23
+ UPDATE #{table} SET #{count_column} = #{count_column} + 1 WHERE #{main_column} = NEW.#{id_column};
24
+ END IF;
25
+ IF ((TG_OP = 'DELETE' OR TG_OP = 'UPDATE') AND OLD.#{id_column} IS NOT NULL) THEN
26
+ UPDATE #{table} SET #{count_column} = #{count_column} - 1 WHERE #{main_column} = OLD.#{id_column};
27
+ END IF;
28
+ END IF;
29
+
30
+ IF (TG_OP = 'DELETE') THEN
31
+ RETURN OLD;
32
+ END IF;
33
+ RETURN NEW;
34
+ END;
35
+ SQL
36
+ end
37
+
38
+ def pgt_created_at(table, column, opts={})
39
+ trigger_name = opts[:trigger_name] || "pgt_ca_#{column}"
40
+ function_name = opts[:function_name] || "pgt_ca_#{table}__#{column}"
41
+ col = quote_identifier(column)
42
+ pgt_trigger(table, trigger_name, function_name, [:insert, :update], <<-SQL)
43
+ BEGIN
44
+ IF (TG_OP = 'UPDATE') THEN
45
+ NEW.#{col} := OLD.#{col};
46
+ ELSIF (TG_OP = 'INSERT') THEN
47
+ NEW.#{col} := CURRENT_TIMESTAMP;
48
+ END IF;
49
+ RETURN NEW;
50
+ END;
51
+ SQL
52
+ end
53
+
54
+ def pgt_immutable(table, *columns)
55
+ opts = columns.last.is_a?(Hash) ? columns.pop : {}
56
+ trigger_name = opts[:trigger_name] || "pgt_im_#{columns.join('__')}"
57
+ function_name = opts[:function_name] || "pgt_im_#{columns.join('__')}"
58
+ ifs = columns.map do |c|
59
+ old = "OLD.#{quote_identifier(c)}"
60
+ new = "NEW.#{quote_identifier(c)}"
61
+ <<-END
62
+ IF #{new} IS DISTINCT FROM #{old} THEN
63
+ RAISE EXCEPTION 'Attempted #{c} update: Old: %, New: %', #{old}, #{new};
64
+ END IF;
65
+ END
66
+ end.join("\n")
67
+ pgt_trigger(table, trigger_name, function_name, :update, "BEGIN #{ifs} RETURN NEW; END;")
68
+ end
69
+
70
+ def pgt_sum_cache(main_table, main_table_id_column, sum_column, summed_table, summed_table_id_column, summed_column, opts={})
71
+ trigger_name = opts[:trigger_name] || "pgt_sc_#{main_table}__#{main_table_id_column}__#{sum_column}__#{summed_table_id_column}"
72
+ function_name = opts[:function_name] || "pgt_sc_#{main_table}__#{main_table_id_column}__#{sum_column}__#{summed_table}__#{summed_table_id_column}__#{summed_column}"
73
+
74
+ table = quote_schema_table(main_table)
75
+ id_column = quote_identifier(summed_table_id_column)
76
+ summed_column = quote_identifier(summed_column)
77
+ main_column = quote_identifier(main_table_id_column)
78
+ sum_column = quote_identifier(sum_column)
79
+
80
+ pgt_trigger(summed_table, trigger_name, function_name, [:insert, :delete, :update], <<-SQL)
81
+ BEGIN
82
+ IF (TG_OP = 'UPDATE' AND NEW.#{id_column} = OLD.#{id_column}) THEN
83
+ UPDATE #{table} SET #{sum_column} = #{sum_column} + NEW.#{summed_column} - OLD.#{summed_column} WHERE #{main_column} = NEW.#{id_column};
84
+ ELSE
85
+ IF ((TG_OP = 'INSERT' OR TG_OP = 'UPDATE') AND NEW.#{id_column} IS NOT NULL) THEN
86
+ UPDATE #{table} SET #{sum_column} = #{sum_column} + NEW.#{summed_column} WHERE #{main_column} = NEW.#{id_column};
87
+ END IF;
88
+ IF ((TG_OP = 'DELETE' OR TG_OP = 'UPDATE') AND OLD.#{id_column} IS NOT NULL) THEN
89
+ UPDATE #{table} SET #{sum_column} = #{sum_column} - OLD.#{summed_column} WHERE #{main_column} = OLD.#{id_column};
90
+ END IF;
91
+ END IF;
92
+ IF (TG_OP = 'DELETE') THEN
93
+ RETURN OLD;
94
+ END IF;
95
+ RETURN NEW;
96
+ END;
97
+ SQL
98
+ end
99
+
100
+ def pgt_sum_through_many_cache(opts={})
101
+ main_table = opts.fetch(:main_table)
102
+ main_table_id_column = opts.fetch(:main_table_id_column, :id)
103
+ sum_column = opts.fetch(:sum_column)
104
+ summed_table = opts.fetch(:summed_table)
105
+ summed_table_id_column = opts.fetch(:summed_table_id_column, :id)
106
+ summed_column = opts.fetch(:summed_column)
107
+ join_table = opts.fetch(:join_table)
108
+ main_table_fk_column = opts.fetch(:main_table_fk_column)
109
+ summed_table_fk_column = opts.fetch(:summed_table_fk_column)
110
+
111
+ trigger_name = opts[:trigger_name] || "pgt_stmc_#{main_table}__#{main_table_id_column}__#{sum_column}__#{summed_table_id_column}__#{summed_column}__#{main_table_fk_column}__#{summed_table_fk_column}"
112
+ function_name = opts[:function_name] || "pgt_stmc_#{main_table}__#{main_table_id_column}__#{sum_column}__#{summed_table}__#{summed_table_id_column}__#{summed_column}__#{join_table}__#{main_table_fk_column}__#{summed_table_fk_column}"
113
+ join_trigger_name = opts[:join_trigger_name] || "pgt_stmc_join_#{main_table}__#{main_table_id_column}__#{sum_column}__#{summed_table_id_column}__#{summed_column}__#{main_table_fk_column}__#{summed_table_fk_column}"
114
+ join_function_name = opts[:join_function_name] || "pgt_stmc_join_#{main_table}__#{main_table_id_column}__#{sum_column}__#{summed_table}__#{summed_table_id_column}__#{summed_column}__#{join_table}__#{main_table_fk_column}__#{summed_table_fk_column}"
115
+
116
+ orig_summed_table = summed_table
117
+ orig_join_table = join_table
118
+
119
+ main_table = quote_schema_table(main_table)
120
+ main_table_id_column = quote_schema_table(main_table_id_column)
121
+ sum_column = quote_schema_table(sum_column)
122
+ summed_table = quote_schema_table(summed_table)
123
+ summed_table_id_column = quote_schema_table(summed_table_id_column)
124
+ summed_column = quote_schema_table(summed_column)
125
+ join_table = quote_schema_table(join_table)
126
+ main_table_fk_column = quote_schema_table(main_table_fk_column)
127
+ summed_table_fk_column = quote_schema_table(summed_table_fk_column)
128
+
129
+ pgt_trigger(orig_summed_table, trigger_name, function_name, [:insert, :delete, :update], <<-SQL)
130
+ BEGIN
131
+ IF (TG_OP = 'UPDATE' AND NEW.#{summed_table_id_column} = OLD.#{summed_table_id_column}) THEN
132
+ UPDATE #{main_table} SET #{sum_column} = #{sum_column} + NEW.#{summed_column} - OLD.#{summed_column} WHERE #{main_table_id_column} IN (SELECT #{main_table_fk_column} FROM #{join_table} WHERE #{summed_table_fk_column} = NEW.#{summed_table_id_column});
133
+ ELSE
134
+ IF (TG_OP = 'INSERT' OR TG_OP = 'UPDATE') THEN
135
+ UPDATE #{main_table} SET #{sum_column} = #{sum_column} + NEW.#{summed_column} WHERE #{main_table_id_column} IN (SELECT #{main_table_fk_column} FROM #{join_table} WHERE #{summed_table_fk_column} = NEW.#{summed_table_id_column});
136
+ END IF;
137
+ IF (TG_OP = 'DELETE' OR TG_OP = 'UPDATE') THEN
138
+ UPDATE #{main_table} SET #{sum_column} = #{sum_column} - OLD.#{summed_column} WHERE #{main_table_id_column} IN (SELECT #{main_table_fk_column} FROM #{join_table} WHERE #{summed_table_fk_column} = OLD.#{summed_table_id_column});
139
+ END IF;
140
+ END IF;
141
+ IF (TG_OP = 'DELETE') THEN
142
+ RETURN OLD;
143
+ END IF;
144
+ RETURN NEW;
145
+ END;
146
+ SQL
147
+
148
+ pgt_trigger(orig_join_table, join_trigger_name, join_function_name, [:insert, :delete, :update], <<-SQL)
149
+ BEGIN
150
+ IF (NOT (TG_OP = 'UPDATE' AND NEW.#{main_table_fk_column} = OLD.#{main_table_fk_column} AND NEW.#{summed_table_fk_column} = OLD.#{summed_table_fk_column})) THEN
151
+ IF (TG_OP = 'INSERT' OR TG_OP = 'UPDATE') THEN
152
+ UPDATE #{main_table} SET #{sum_column} = #{sum_column} + (SELECT #{summed_column} FROM #{summed_table} WHERE #{summed_table_id_column} = NEW.#{summed_table_fk_column}) WHERE #{main_table_id_column} = NEW.#{main_table_fk_column};
153
+ END IF;
154
+ IF (TG_OP = 'DELETE' OR TG_OP = 'UPDATE') THEN
155
+ UPDATE #{main_table} SET #{sum_column} = #{sum_column} - (SELECT #{summed_column} FROM #{summed_table} WHERE #{summed_table_id_column} = OLD.#{summed_table_fk_column}) WHERE #{main_table_id_column} = OLD.#{main_table_fk_column};
156
+ END IF;
157
+ END IF;
158
+ IF (TG_OP = 'DELETE') THEN
159
+ RETURN OLD;
160
+ END IF;
161
+ RETURN NEW;
162
+ END;
163
+ SQL
164
+ end
165
+
166
+ def pgt_touch(main_table, touch_table, column, expr, opts={})
167
+ trigger_name = opts[:trigger_name] || "pgt_t_#{main_table}__#{touch_table}"
168
+ function_name = opts[:function_name] || "pgt_t_#{main_table}__#{touch_table}"
169
+ cond = lambda{|source| expr.map{|k,v| "#{quote_identifier(k)} = #{source}.#{quote_identifier(v)}"}.join(" AND ")}
170
+ same_id = expr.map{|k,v| "NEW.#{quote_identifier(v)} = OLD.#{quote_identifier(v)}"}.join(" AND ")
171
+
172
+ table = quote_schema_table(touch_table)
173
+ col = quote_identifier(column)
174
+ update = lambda{|source| " UPDATE #{table} SET #{col} = CURRENT_TIMESTAMP WHERE #{cond[source]} AND ((#{col} <> CURRENT_TIMESTAMP) OR (#{col} IS NULL));"}
175
+
176
+ sql = <<-SQL
177
+ BEGIN
178
+ IF (TG_OP = 'UPDATE' AND (#{same_id})) THEN
179
+ #{update['NEW']}
180
+ ELSE
181
+ IF (TG_OP = 'INSERT' OR TG_OP = 'UPDATE') THEN
182
+ #{update['NEW']}
183
+ END IF;
184
+ IF (TG_OP = 'DELETE' OR TG_OP = 'UPDATE') THEN
185
+ #{update['OLD']}
186
+ END IF;
187
+ END IF;
188
+
189
+ IF (TG_OP = 'DELETE') THEN
190
+ RETURN OLD;
191
+ END IF;
192
+ RETURN NEW;
193
+ END;
194
+ SQL
195
+ pgt_trigger(main_table, trigger_name, function_name, [:insert, :delete, :update], sql, :after=>true)
196
+ end
197
+
198
+ def pgt_updated_at(table, column, opts={})
199
+ trigger_name = opts[:trigger_name] || "pgt_ua_#{column}"
200
+ function_name = opts[:function_name] || "pgt_ua_#{table}__#{column}"
201
+ pgt_trigger(table, trigger_name, function_name, [:insert, :update], <<-SQL)
202
+ BEGIN
203
+ NEW.#{quote_identifier(column)} := CURRENT_TIMESTAMP;
204
+ RETURN NEW;
205
+ END;
206
+ SQL
207
+ end
208
+
209
+ private
210
+
211
+ # Add or replace a function that returns trigger to handle the action,
212
+ # and add a trigger that calls the function.
213
+ def pgt_trigger(table, trigger_name, function_name, events, definition, opts={})
214
+ create_function(function_name, definition, :language=>:plpgsql, :returns=>:trigger, :replace=>true)
215
+ create_trigger(table, trigger_name, function_name, :events=>events, :each_row=>true, :after=>opts[:after])
216
+ end
217
+ end
218
+
219
+ module PGTMethods
220
+ class_eval(&PGT_DEFINE)
221
+ end
222
+ end
223
+
224
+ Database.register_extension(:pg_triggers, Postgres::PGTMethods)
225
+ end
@@ -1,290 +1,9 @@
1
+ require 'sequel/extensions/pg_triggers'
2
+
1
3
  module Sequel
2
4
  module Postgres
3
- # Add the pgt_* methods so that any Sequel database connecting to PostgreSQL
4
- # can use them. All of these methods require the plpgsql procedural language
5
- # be added to the PostgreSQL database before they can be used. On PostgreSQL
6
- # 9.0 and later versions, it is installed by default. For older versions,
7
- # you can install it with:
8
- #
9
- # DB.create_language(:plpgsql)
10
- #
11
- # All of the public methods take the following options in their opts hash:
12
- #
13
- # * :function_name: The name of the function to use. This is important
14
- # to specify if you want an easy way to drop the function.
15
- # * :trigger_name: The name of the trigger to use. This is important
16
- # to specify if you want an easy way to drop the trigger.
17
5
  module DatabaseMethods
18
- # Turns a column in the main table into a counter cache. A counter cache is a
19
- # column in the main table with the number of rows in the counted table
20
- # for the matching id. Arguments:
21
- # * main_table : name of table holding counter cache column
22
- # * main_table_id_column : column in main table matching counted_table_id_column in counted_table
23
- # * counter_column : column in main table containing the counter cache
24
- # * counted_table : name of table being counted
25
- # * counted_table_id_column : column in counted_table matching main_table_id_column in main_table
26
- # * opts : option hash, see module documentation
27
- def pgt_counter_cache(main_table, main_table_id_column, counter_column, counted_table, counted_table_id_column, opts={})
28
- trigger_name = opts[:trigger_name] || "pgt_cc_#{main_table}__#{main_table_id_column}__#{counter_column}__#{counted_table_id_column}"
29
- function_name = opts[:function_name] || "pgt_cc_#{main_table}__#{main_table_id_column}__#{counter_column}__#{counted_table}__#{counted_table_id_column}"
30
-
31
- table = quote_schema_table(main_table)
32
- id_column = quote_identifier(counted_table_id_column)
33
- main_column = quote_identifier(main_table_id_column)
34
- count_column = quote_identifier(counter_column)
35
-
36
- pgt_trigger(counted_table, trigger_name, function_name, [:insert, :update, :delete], <<-SQL)
37
- BEGIN
38
- IF (TG_OP = 'UPDATE' AND (NEW.#{id_column} = OLD.#{id_column} OR (OLD.#{id_column} IS NULL AND NEW.#{id_column} IS NULL))) THEN
39
- RETURN NEW;
40
- ELSE
41
- IF ((TG_OP = 'INSERT' OR TG_OP = 'UPDATE') AND NEW.#{id_column} IS NOT NULL) THEN
42
- UPDATE #{table} SET #{count_column} = #{count_column} + 1 WHERE #{main_column} = NEW.#{id_column};
43
- END IF;
44
- IF ((TG_OP = 'DELETE' OR TG_OP = 'UPDATE') AND OLD.#{id_column} IS NOT NULL) THEN
45
- UPDATE #{table} SET #{count_column} = #{count_column} - 1 WHERE #{main_column} = OLD.#{id_column};
46
- END IF;
47
- END IF;
48
-
49
- IF (TG_OP = 'DELETE') THEN
50
- RETURN OLD;
51
- END IF;
52
- RETURN NEW;
53
- END;
54
- SQL
55
- end
56
-
57
- # Turns a column in the table into a created at timestamp column, which
58
- # always contains the timestamp the record was inserted into the database.
59
- # Arguments:
60
- # * table : name of table
61
- # * column : column in table that should be a created at timestamp column
62
- # * opts : option hash, see module documentation
63
- def pgt_created_at(table, column, opts={})
64
- trigger_name = opts[:trigger_name] || "pgt_ca_#{column}"
65
- function_name = opts[:function_name] || "pgt_ca_#{table}__#{column}"
66
- col = quote_identifier(column)
67
- pgt_trigger(table, trigger_name, function_name, [:insert, :update], <<-SQL)
68
- BEGIN
69
- IF (TG_OP = 'UPDATE') THEN
70
- NEW.#{col} := OLD.#{col};
71
- ELSIF (TG_OP = 'INSERT') THEN
72
- NEW.#{col} := CURRENT_TIMESTAMP;
73
- END IF;
74
- RETURN NEW;
75
- END;
76
- SQL
77
- end
78
-
79
- # Makes all given columns in the given table immutable, so an exception
80
- # is raised if there is an attempt to modify the value when updating the
81
- # record. Arguments:
82
- # * table : name of table
83
- # * columns : All columns in the table that should be immutable. Can end with a hash of options, see module documentation.
84
- def pgt_immutable(table, *columns)
85
- opts = columns.last.is_a?(Hash) ? columns.pop : {}
86
- trigger_name = opts[:trigger_name] || "pgt_im_#{columns.join('__')}"
87
- function_name = opts[:function_name] || "pgt_im_#{columns.join('__')}"
88
- ifs = columns.map do |c|
89
- old = "OLD.#{quote_identifier(c)}"
90
- new = "NEW.#{quote_identifier(c)}"
91
- <<-END
92
- IF #{new} IS DISTINCT FROM #{old} THEN
93
- RAISE EXCEPTION 'Attempted #{c} update: Old: %, New: %', #{old}, #{new};
94
- END IF;
95
- END
96
- end.join("\n")
97
- pgt_trigger(table, trigger_name, function_name, :update, "BEGIN #{ifs} RETURN NEW; END;")
98
- end
99
-
100
- # Turns a column in the main table into a sum cache. A sum cache is a
101
- # column in the main table with the sum of a column in the summed table
102
- # for the matching id. Arguments:
103
- # * main_table : name of table holding counter cache column
104
- # * main_table_id_column : column in main table matching counted_table_id_column in counted_table
105
- # * sum_column : column in main table containing the sum cache
106
- # * summed_table : name of table being summed
107
- # * summed_table_id_column : column in summed_table matching main_table_id_column in main_table
108
- # * summed_column : column in summed_table being summed
109
- # * opts : option hash, see module documentation
110
- def pgt_sum_cache(main_table, main_table_id_column, sum_column, summed_table, summed_table_id_column, summed_column, opts={})
111
- trigger_name = opts[:trigger_name] || "pgt_sc_#{main_table}__#{main_table_id_column}__#{sum_column}__#{summed_table_id_column}"
112
- function_name = opts[:function_name] || "pgt_sc_#{main_table}__#{main_table_id_column}__#{sum_column}__#{summed_table}__#{summed_table_id_column}__#{summed_column}"
113
-
114
- table = quote_schema_table(main_table)
115
- id_column = quote_identifier(summed_table_id_column)
116
- summed_column = quote_identifier(summed_column)
117
- main_column = quote_identifier(main_table_id_column)
118
- sum_column = quote_identifier(sum_column)
119
-
120
- pgt_trigger(summed_table, trigger_name, function_name, [:insert, :delete, :update], <<-SQL)
121
- BEGIN
122
- IF (TG_OP = 'UPDATE' AND NEW.#{id_column} = OLD.#{id_column}) THEN
123
- UPDATE #{table} SET #{sum_column} = #{sum_column} + NEW.#{summed_column} - OLD.#{summed_column} WHERE #{main_column} = NEW.#{id_column};
124
- ELSE
125
- IF ((TG_OP = 'INSERT' OR TG_OP = 'UPDATE') AND NEW.#{id_column} IS NOT NULL) THEN
126
- UPDATE #{table} SET #{sum_column} = #{sum_column} + NEW.#{summed_column} WHERE #{main_column} = NEW.#{id_column};
127
- END IF;
128
- IF ((TG_OP = 'DELETE' OR TG_OP = 'UPDATE') AND OLD.#{id_column} IS NOT NULL) THEN
129
- UPDATE #{table} SET #{sum_column} = #{sum_column} - OLD.#{summed_column} WHERE #{main_column} = OLD.#{id_column};
130
- END IF;
131
- END IF;
132
- IF (TG_OP = 'DELETE') THEN
133
- RETURN OLD;
134
- END IF;
135
- RETURN NEW;
136
- END;
137
- SQL
138
- end
139
-
140
- # Turns a column in the main table into a sum cache through a join table.
141
- # A sum cache is a column in the main table with the sum of a column in the
142
- # summed table for the matching id. The join table must have NOT NULL constraints
143
- # on the foreign keys to the main table and summed table and a
144
- # composite unique constraint on both foreign keys.
145
- #
146
- # Arguments:
147
- # * opts : option hash, see module documentation, and below.
148
- # * :main_table: name of table holding sum cache column
149
- # * :main_table_id_column: primary key column in main table referenced by main_table_fk_column (default: :id)
150
- # * :sum_column: column in main table containing the sum cache, must be NOT NULL and default to 0
151
- # * :summed_table: name of table being summed
152
- # * :summed_table_id_column: primary key column in summed_table referenced by summed_table_fk_column (default: ;id)
153
- # * :summed_column: column in summed_table being summed, must be NOT NULL
154
- # * :join_table: name of table which joins main_table with summed_table
155
- # * :main_table_fk_column: column in join_table referencing main_table_id_column, must be NOT NULL
156
- # * :summed_table_fk_column: column in join_table referencing summed_table_id_column, must be NOT NULL
157
- def pgt_sum_through_many_cache(opts={})
158
- main_table = opts.fetch(:main_table)
159
- main_table_id_column = opts.fetch(:main_table_id_column, :id)
160
- sum_column = opts.fetch(:sum_column)
161
- summed_table = opts.fetch(:summed_table)
162
- summed_table_id_column = opts.fetch(:summed_table_id_column, :id)
163
- summed_column = opts.fetch(:summed_column)
164
- join_table = opts.fetch(:join_table)
165
- main_table_fk_column = opts.fetch(:main_table_fk_column)
166
- summed_table_fk_column = opts.fetch(:summed_table_fk_column)
167
-
168
- trigger_name = opts[:trigger_name] || "pgt_stmc_#{main_table}__#{main_table_id_column}__#{sum_column}__#{summed_table_id_column}__#{summed_column}__#{main_table_fk_column}__#{summed_table_fk_column}"
169
- function_name = opts[:function_name] || "pgt_stmc_#{main_table}__#{main_table_id_column}__#{sum_column}__#{summed_table}__#{summed_table_id_column}__#{summed_column}__#{join_table}__#{main_table_fk_column}__#{summed_table_fk_column}"
170
- join_trigger_name = opts[:join_trigger_name] || "pgt_stmc_join_#{main_table}__#{main_table_id_column}__#{sum_column}__#{summed_table_id_column}__#{summed_column}__#{main_table_fk_column}__#{summed_table_fk_column}"
171
- join_function_name = opts[:join_function_name] || "pgt_stmc_join_#{main_table}__#{main_table_id_column}__#{sum_column}__#{summed_table}__#{summed_table_id_column}__#{summed_column}__#{join_table}__#{main_table_fk_column}__#{summed_table_fk_column}"
172
-
173
- orig_summed_table = summed_table
174
- orig_join_table = join_table
175
-
176
- main_table = quote_schema_table(main_table)
177
- main_table_id_column = quote_schema_table(main_table_id_column)
178
- sum_column = quote_schema_table(sum_column)
179
- summed_table = quote_schema_table(summed_table)
180
- summed_table_id_column = quote_schema_table(summed_table_id_column)
181
- summed_column = quote_schema_table(summed_column)
182
- join_table = quote_schema_table(join_table)
183
- main_table_fk_column = quote_schema_table(main_table_fk_column)
184
- summed_table_fk_column = quote_schema_table(summed_table_fk_column)
185
-
186
- pgt_trigger(orig_summed_table, trigger_name, function_name, [:insert, :delete, :update], <<-SQL)
187
- BEGIN
188
- IF (TG_OP = 'UPDATE' AND NEW.#{summed_table_id_column} = OLD.#{summed_table_id_column}) THEN
189
- UPDATE #{main_table} SET #{sum_column} = #{sum_column} + NEW.#{summed_column} - OLD.#{summed_column} WHERE #{main_table_id_column} IN (SELECT #{main_table_fk_column} FROM #{join_table} WHERE #{summed_table_fk_column} = NEW.#{summed_table_id_column});
190
- ELSE
191
- IF (TG_OP = 'INSERT' OR TG_OP = 'UPDATE') THEN
192
- UPDATE #{main_table} SET #{sum_column} = #{sum_column} + NEW.#{summed_column} WHERE #{main_table_id_column} IN (SELECT #{main_table_fk_column} FROM #{join_table} WHERE #{summed_table_fk_column} = NEW.#{summed_table_id_column});
193
- END IF;
194
- IF (TG_OP = 'DELETE' OR TG_OP = 'UPDATE') THEN
195
- UPDATE #{main_table} SET #{sum_column} = #{sum_column} - OLD.#{summed_column} WHERE #{main_table_id_column} IN (SELECT #{main_table_fk_column} FROM #{join_table} WHERE #{summed_table_fk_column} = OLD.#{summed_table_id_column});
196
- END IF;
197
- END IF;
198
- IF (TG_OP = 'DELETE') THEN
199
- RETURN OLD;
200
- END IF;
201
- RETURN NEW;
202
- END;
203
- SQL
204
-
205
- pgt_trigger(orig_join_table, join_trigger_name, join_function_name, [:insert, :delete, :update], <<-SQL)
206
- BEGIN
207
- IF (NOT (TG_OP = 'UPDATE' AND NEW.#{main_table_fk_column} = OLD.#{main_table_fk_column} AND NEW.#{summed_table_fk_column} = OLD.#{summed_table_fk_column})) THEN
208
- IF (TG_OP = 'INSERT' OR TG_OP = 'UPDATE') THEN
209
- UPDATE #{main_table} SET #{sum_column} = #{sum_column} + (SELECT #{summed_column} FROM #{summed_table} WHERE #{summed_table_id_column} = NEW.#{summed_table_fk_column}) WHERE #{main_table_id_column} = NEW.#{main_table_fk_column};
210
- END IF;
211
- IF (TG_OP = 'DELETE' OR TG_OP = 'UPDATE') THEN
212
- UPDATE #{main_table} SET #{sum_column} = #{sum_column} - (SELECT #{summed_column} FROM #{summed_table} WHERE #{summed_table_id_column} = OLD.#{summed_table_fk_column}) WHERE #{main_table_id_column} = OLD.#{main_table_fk_column};
213
- END IF;
214
- END IF;
215
- IF (TG_OP = 'DELETE') THEN
216
- RETURN OLD;
217
- END IF;
218
- RETURN NEW;
219
- END;
220
- SQL
221
- end
222
-
223
- # When rows in a table are updated, touches a timestamp of related rows
224
- # in another table.
225
- # Arguments:
226
- # * main_table : name of table that is being watched for changes
227
- # * touch_table : name of table that needs to be touched
228
- # * column : name of timestamp column to be touched
229
- # * expr : hash or array that represents the columns that define the relationship
230
- # * opts : option hash, see module documentation
231
- def pgt_touch(main_table, touch_table, column, expr, opts={})
232
- trigger_name = opts[:trigger_name] || "pgt_t_#{main_table}__#{touch_table}"
233
- function_name = opts[:function_name] || "pgt_t_#{main_table}__#{touch_table}"
234
- cond = lambda{|source| expr.map{|k,v| "#{quote_identifier(k)} = #{source}.#{quote_identifier(v)}"}.join(" AND ")}
235
- same_id = expr.map{|k,v| "NEW.#{quote_identifier(v)} = OLD.#{quote_identifier(v)}"}.join(" AND ")
236
-
237
- table = quote_schema_table(touch_table)
238
- col = quote_identifier(column)
239
- update = lambda{|source| " UPDATE #{table} SET #{col} = CURRENT_TIMESTAMP WHERE #{cond[source]} AND ((#{col} <> CURRENT_TIMESTAMP) OR (#{col} IS NULL));"}
240
-
241
- sql = <<-SQL
242
- BEGIN
243
- IF (TG_OP = 'UPDATE' AND (#{same_id})) THEN
244
- #{update['NEW']}
245
- ELSE
246
- IF (TG_OP = 'INSERT' OR TG_OP = 'UPDATE') THEN
247
- #{update['NEW']}
248
- END IF;
249
- IF (TG_OP = 'DELETE' OR TG_OP = 'UPDATE') THEN
250
- #{update['OLD']}
251
- END IF;
252
- END IF;
253
-
254
- IF (TG_OP = 'DELETE') THEN
255
- RETURN OLD;
256
- END IF;
257
- RETURN NEW;
258
- END;
259
- SQL
260
- pgt_trigger(main_table, trigger_name, function_name, [:insert, :delete, :update], sql, :after=>true)
261
- end
262
-
263
- # Turns a column in the table into a updated at timestamp column, which
264
- # always contains the timestamp the record was inserted or last updated.
265
- # Arguments:
266
- # * table : name of table
267
- # * column : column in table that should be a updated at timestamp column
268
- # * opts : option hash, see module documentation
269
- def pgt_updated_at(table, column, opts={})
270
- trigger_name = opts[:trigger_name] || "pgt_ua_#{column}"
271
- function_name = opts[:function_name] || "pgt_ua_#{table}__#{column}"
272
- pgt_trigger(table, trigger_name, function_name, [:insert, :update], <<-SQL)
273
- BEGIN
274
- NEW.#{quote_identifier(column)} := CURRENT_TIMESTAMP;
275
- RETURN NEW;
276
- END;
277
- SQL
278
- end
279
-
280
- private
281
-
282
- # Add or replace a function that returns trigger to handle the action,
283
- # and add a trigger that calls the function.
284
- def pgt_trigger(table, trigger_name, function_name, events, definition, opts={})
285
- create_function(function_name, definition, :language=>:plpgsql, :returns=>:trigger, :replace=>true)
286
- create_trigger(table, trigger_name, function_name, :events=>events, :each_row=>true, :after=>opts[:after])
287
- end
6
+ class_eval(&PGT_DEFINE)
288
7
  end
289
8
  end
290
9
  end
@@ -7,17 +7,12 @@ require 'minitest/autorun'
7
7
  DB = Sequel.connect(ENV['PGT_SPEC_DB']||'postgres:///spgt_test?user=postgres')
8
8
 
9
9
  $:.unshift(File.join(File.dirname(File.dirname(File.expand_path(__FILE__))), 'lib'))
10
- require 'sequel_postgresql_triggers'
11
-
12
- if defined?(RSpec)
13
- require 'rspec/version'
14
- if RSpec::Version::STRING >= '2.11.0'
15
- RSpec.configure do |config|
16
- config.expect_with :rspec do |c|
17
- c.syntax = :should
18
- end
19
- end
20
- end
10
+ if ENV['PGT_GLOBAL'] == '1'
11
+ puts "Running specs with global modification"
12
+ require 'sequel_postgresql_triggers'
13
+ else
14
+ puts "Running specs with extension"
15
+ DB.extension :pg_triggers
21
16
  end
22
17
 
23
18
  describe "PostgreSQL Triggers" do
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sequel_postgresql_triggers
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeremy Evans
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-07-12 00:00:00.000000000 Z
11
+ date: 2016-09-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sequel
@@ -31,7 +31,8 @@ extensions: []
31
31
  extra_rdoc_files: []
32
32
  files:
33
33
  - MIT-LICENSE
34
- - README
34
+ - README.rdoc
35
+ - lib/sequel/extensions/pg_triggers.rb
35
36
  - lib/sequel_postgresql_triggers.rb
36
37
  - spec/sequel_postgresql_triggers_spec.rb
37
38
  homepage: https://github.com/jeremyevans/sequel_postgresql_triggers
@@ -45,7 +46,7 @@ rdoc_options:
45
46
  - "--title"
46
47
  - 'Sequel PostgreSQL Triggers: Database enforced timestamps, immutable columns, and
47
48
  counter/sum caches'
48
- - README
49
+ - README.rdoc
49
50
  - MIT-LICENSE
50
51
  - lib
51
52
  require_paths:
@@ -65,5 +66,6 @@ rubyforge_project:
65
66
  rubygems_version: 2.5.1
66
67
  signing_key:
67
68
  specification_version: 4
68
- summary: Database enforced timestamps, immutable columns, and counter/sum caches
69
+ summary: Database enforced timestamps, immutable columns, counter/sum caches, and
70
+ touch propogation
69
71
  test_files: []
data/README DELETED
@@ -1,75 +0,0 @@
1
- = Sequel PostgreSQL Triggers
2
-
3
- Sequel PostgreSQL Triggers is a small enhancement to Sequel allowing
4
- a user to easily handle the following types of columns:
5
-
6
- * Timestamp Columns (Created At/Updated At)
7
- * Counter/Sum Caches
8
- * Immutable Columns
9
-
10
- It handles these internally to the database via triggers, so even if
11
- other applications access the database (without using Sequel), things
12
- will still work (unless the database superuser disables triggers).
13
-
14
- To use any of these methods before PostgreSQL 9.0, you have to add
15
- the plpgsql procedural language to PostgreSQL, which you can do with:
16
-
17
- DB.create_language(:plpgsql)
18
-
19
- == Triggers
20
-
21
- === Created At Columns - pgt_created_at
22
-
23
- pgt_created_at takes the table and column given and makes it so that
24
- upon insertion, the column is set to the CURRENT_TIMESTAMP, and that
25
- upon update, the column's value is always set to the previous value.
26
- This is sort of like an immutable column, but it doesn't bring up an
27
- error if you try to change it, it just ignores it.
28
-
29
- === Updated At Columns - pgt_updated_at
30
-
31
- Similar to pgt_created_at, takes a table and column and makes it so
32
- that upon insertion, the column is set to CURRENT_TIMESTAMP. It
33
- differs that upon update, the column is also set to CURRENT_TIMESTAMP.
34
-
35
- === Counter Cache - pgt_counter_cache
36
-
37
- This takes quite a few arguments (see the RDoc) and sets up a
38
- counter cache so that when the counted table is inserted to
39
- or deleted from, records in the main table are updated with the
40
- count of the corresponding records in the counted table. The counter
41
- cache column must have a default of 0 for this to work correctly.
42
-
43
- === Sum Cache - pgt_sum_cache
44
-
45
- Similar to pgt_counter_cache, except instead of storing a count
46
- of records in the main table, it stores the sum on one of the
47
- columns in summed table. The sum cache column must have a default
48
- of 0 for this to work correctly.
49
-
50
- === Sum Through Many Cache - pgt_sum_through_many_cache
51
-
52
- Similar to pgt_sum_cache, except instead of a one-to-many relationship,
53
- it supports a many-to-many relationship with a single join table. The
54
- sum cache column must have a default of 0 for this to work correctly.
55
-
56
- === Immutable Columns - pgt_immutable
57
-
58
- This takes a table name and one or more column names, and adds
59
- an update trigger that raises an exception if you try to modify
60
- the value of any of the columns.
61
-
62
- === Touch Propagation - pgt_touch
63
-
64
- This takes several arguments (again, see the RDoc) and sets up a
65
- trigger that watches one table for changes, and touches timestamps
66
- of related rows in a separate table.
67
-
68
- == License
69
-
70
- This library is released under the MIT License. See the MIT-LICENSE
71
- file for details.
72
-
73
- == Author
74
-
75
- Jeremy Evans <code@jeremyevans.net>