sequel_postgresql_triggers 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
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>