sequel_postgresql_triggers 1.4.0 → 1.5.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
  SHA256:
3
- metadata.gz: 3b5d81f0071528eb70130d8ebe3093a365c0d024e250583d8ea7e35d5b7af263
4
- data.tar.gz: 18bb2a79d2642636f34fce1f0f4be315e90592c3be8c8a8d5f587d753061d360
3
+ metadata.gz: d0c46791ed8af6edc2e71f5e7964625fe66811eb76d7a99c085b5157336f2d43
4
+ data.tar.gz: 0d3cae20446b2f4d334746077959d8415cb815c9afcb4212a57c35ff2bc8ab88
5
5
  SHA512:
6
- metadata.gz: 615949150a244df98c696da8472ebeb40ae5c09812457f5ecca61197a10754e489c431a68d2a44df91eb5f17e229e6e39b4d3b4f4cf6ca28be520b2145345e3c
7
- data.tar.gz: ca67361ebd569731121ad3f482e47d0320d0658144fd4d7bd21734fcf3542ae84d3fb592483813980fbb4f7e8b52866575c4f0c7efb8566d34760d884f66207c
6
+ metadata.gz: 19e85c00deff747bd3d89bad0d3dfd5d64105cd9191d4a336e004f5a862b097734fa5fe81793ac5bb8f32c8b849f5d28e1c9ef0975f038679f66357689702408
7
+ data.tar.gz: 1a59b39455c23b6e41af7825de4e6a9c53ba0c394ddb48296e4636f31b5a34bb86f13524562edb104050791d47fa4ac39a7ec665234dcb110ced6bf5c862798f
@@ -1,4 +1,4 @@
1
- Copyright (c) 2008-2017 Jeremy Evans
1
+ Copyright (c) 2008-2018 Jeremy Evans
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining a copy
4
4
  of this software and associated documentation files (the "Software"), to
@@ -95,7 +95,7 @@ opts :: options hash
95
95
 
96
96
  === Counter Cache - pgt_counter_cache
97
97
 
98
- This takes quite a few arguments (see the RDoc) and sets up a
98
+ This takes many arguments and sets up a
99
99
  counter cache so that when the counted table is inserted to
100
100
  or deleted from, records in the main table are updated with the
101
101
  count of the corresponding records in the counted table. The counter
@@ -167,7 +167,7 @@ table :: name of table
167
167
 
168
168
  === Touch Propagation - pgt_touch
169
169
 
170
- This takes several arguments (again, see the RDoc) and sets up a
170
+ This takes several arguments and sets up a
171
171
  trigger that watches one table for changes, and touches timestamps
172
172
  of related rows in a separate table.
173
173
 
@@ -198,6 +198,62 @@ Options:
198
198
  :referenced_function_name :: function name for trigger function on referenced table
199
199
  :referenced_trigger_name :: trigger name for referenced table
200
200
 
201
+ === Force Defaults - pgt_force_defaults
202
+
203
+ This takes 2 arguments, a table and a hash of column default values, and sets
204
+ up an insert trigger that will override user submitted or database
205
+ default values and use the values given when setting up the trigger.
206
+ This is mostly useful in situations where multiple database accounts
207
+ are used where one account has insert permissions but not update
208
+ permissions, and you want to ensure that inserted rows have specific
209
+ column values to enforce security requirements.
210
+
211
+ Arguments:
212
+ table :: The name of the table
213
+ defaults :: A hash of default values to enforce, where keys are column names
214
+ and values are the default values to enforce
215
+
216
+ === JSON Audit Logging - pgt_json_audit_log_setup and pg_json_audit_log
217
+
218
+ These methods setup an auditing function where updates and deletes log
219
+ the previous values to a central auditing table in JSON format.
220
+
221
+ ==== pgt_json_audit_log_setup
222
+
223
+ This creates an audit table and a trigger function that will log
224
+ previous values to the audit table. This returns the name of the
225
+ trigger function created, which should be passed to
226
+ +pgt_json_audit_log+.
227
+
228
+ Arguments:
229
+ table :: The name of the table storing the audit logs.
230
+
231
+ Options:
232
+ function_opts :: Options to pass to +create_function+ when creating
233
+ the trigger function.
234
+
235
+ The audit log table will store the following columns:
236
+
237
+ txid :: The 64-bit transaction ID for the transaction that made the modification (txid_current())
238
+ at :: The timestamp of the transaction that made the modification (CURRENT_TIMESTAMP)
239
+ user :: The database user name that made the modification (CURRENT_USER)
240
+ schema :: The schema containing the table that was modified (TG_TABLE_SCHEMA)
241
+ table :: The table that was modified (TG_TABLE_NAME)
242
+ action :: The type of modification, either DELETE or UPDATE (TG_OP)
243
+ prior :: A jsonb column with the contents of the row before the modification (to_jsonb(OLD))
244
+
245
+ ==== pgt_json_audit_log
246
+
247
+ This adds a trigger to the table that will log previous values to the
248
+ audting table for updates and deletes.
249
+
250
+ Arguments:
251
+ table :: The name of the table to audit
252
+ function :: The name of the trigger function to call to log changes
253
+
254
+ Note that it is probably a bad idea to use the same table argument
255
+ to both +pgt_json_audit_log_setup+ and +pgt_json_audit_log+.
256
+
201
257
  == License
202
258
 
203
259
  This library is released under the MIT License. See the MIT-LICENSE
@@ -6,8 +6,8 @@ module Sequel
6
6
  module Postgres
7
7
  PGT_DEFINE = proc do
8
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}"
9
+ trigger_name = opts[:trigger_name] || "pgt_cc_#{pgt_mangled_table_name(main_table)}__#{main_table_id_column}__#{counter_column}__#{counted_table_id_column}"
10
+ function_name = opts[:function_name] || "pgt_cc_#{pgt_mangled_table_name(main_table)}__#{main_table_id_column}__#{counter_column}__#{pgt_mangled_table_name(counted_table)}__#{counted_table_id_column}"
11
11
 
12
12
  table = quote_schema_table(main_table)
13
13
  id_column = quote_identifier(counted_table_id_column)
@@ -37,7 +37,7 @@ module Sequel
37
37
 
38
38
  def pgt_created_at(table, column, opts={})
39
39
  trigger_name = opts[:trigger_name] || "pgt_ca_#{column}"
40
- function_name = opts[:function_name] || "pgt_ca_#{table}__#{column}"
40
+ function_name = opts[:function_name] || "pgt_ca_#{pgt_mangled_table_name(table)}__#{column}"
41
41
  col = quote_identifier(column)
42
42
  pgt_trigger(table, trigger_name, function_name, [:insert, :update], <<-SQL)
43
43
  BEGIN
@@ -51,6 +51,21 @@ module Sequel
51
51
  SQL
52
52
  end
53
53
 
54
+ def pgt_force_defaults(table, defaults, opts={})
55
+ cols = defaults.keys.sort.join('_')
56
+ trigger_name = opts[:trigger_name] || "pgt_fd_#{cols}"
57
+ function_name = opts[:function_name] || "pgt_fd_#{pgt_mangled_table_name(table)}__#{cols}"
58
+ lines = defaults.map do |column, v|
59
+ "NEW.#{quote_identifier(column)} = #{literal(v)};"
60
+ end
61
+ pgt_trigger(table, trigger_name, function_name, [:insert], <<-SQL)
62
+ BEGIN
63
+ #{lines.join("\n")}
64
+ RETURN NEW;
65
+ END;
66
+ SQL
67
+ end
68
+
54
69
  def pgt_immutable(table, *columns)
55
70
  opts = columns.last.is_a?(Hash) ? columns.pop : {}
56
71
  trigger_name = opts[:trigger_name] || "pgt_im_#{columns.join('__')}"
@@ -67,9 +82,37 @@ module Sequel
67
82
  pgt_trigger(table, trigger_name, function_name, :update, "BEGIN #{ifs} RETURN NEW; END;")
68
83
  end
69
84
 
85
+ def pgt_json_audit_log_setup(table, opts={})
86
+ function_name = opts[:function_name] || "pgt_jal_#{pgt_mangled_table_name(table)}"
87
+ create_table(table) do
88
+ Bignum :txid, :null=>false, :index=>true
89
+ DateTime :at, :default=>Sequel::CURRENT_TIMESTAMP, :null=>false
90
+ String :user, :null=>false
91
+ String :schema, :null=>false
92
+ String :table, :null=>false
93
+ String :action, :null=>false
94
+ jsonb :prior, :null=>false
95
+ end
96
+ create_function(function_name, (<<-SQL), {:language=>:plpgsql, :returns=>:trigger, :replace=>true}.merge(opts[:function_opts]||{}))
97
+ BEGIN
98
+ INSERT INTO #{quote_schema_table(table)} (txid, at, "user", "schema", "table", action, prior) VALUES
99
+ (txid_current(), CURRENT_TIMESTAMP, CURRENT_USER, TG_TABLE_SCHEMA, TG_TABLE_NAME, TG_OP, to_jsonb(OLD));
100
+ IF (TG_OP = 'DELETE') THEN
101
+ RETURN OLD;
102
+ END IF;
103
+ RETURN NEW;
104
+ END;
105
+ SQL
106
+ function_name
107
+ end
108
+
109
+ def pgt_json_audit_log(table, function, opts={})
110
+ create_trigger(table, (opts[:trigger_name] || "pgt_jal_#{pgt_mangled_table_name(table)}"), function, :events=>[:update, :delete], :each_row=>true, :after=>true)
111
+ end
112
+
70
113
  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}"
114
+ trigger_name = opts[:trigger_name] || "pgt_sc_#{pgt_mangled_table_name(main_table)}__#{main_table_id_column}__#{sum_column}__#{summed_table_id_column}"
115
+ function_name = opts[:function_name] || "pgt_sc_#{pgt_mangled_table_name(main_table)}__#{main_table_id_column}__#{sum_column}__#{pgt_mangled_table_name(summed_table)}__#{summed_table_id_column}__#{summed_column}"
73
116
 
74
117
  table = quote_schema_table(main_table)
75
118
  id_column = quote_identifier(summed_table_id_column)
@@ -111,10 +154,10 @@ module Sequel
111
154
  summed_table_fk_column = opts.fetch(:summed_table_fk_column)
112
155
 
113
156
  summed_column_slug = summed_column.is_a?(String) || summed_column.is_a?(Symbol) ? "__#{summed_column}" : ""
114
- trigger_name = opts[:trigger_name] || "pgt_stmc_#{main_table}__#{main_table_id_column}__#{sum_column}__#{summed_table_id_column}#{summed_column_slug}__#{main_table_fk_column}__#{summed_table_fk_column}"
115
- function_name = opts[:function_name] || "pgt_stmc_#{main_table}__#{main_table_id_column}__#{sum_column}__#{summed_table}__#{summed_table_id_column}#{summed_column_slug}__#{join_table}__#{main_table_fk_column}__#{summed_table_fk_column}"
116
- join_trigger_name = opts[:join_trigger_name] || "pgt_stmc_join_#{main_table}__#{main_table_id_column}__#{sum_column}__#{summed_table_id_column}#{summed_column_slug}__#{main_table_fk_column}__#{summed_table_fk_column}"
117
- 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_slug}__#{join_table}__#{main_table_fk_column}__#{summed_table_fk_column}"
157
+ trigger_name = opts[:trigger_name] || "pgt_stmc_#{pgt_mangled_table_name(main_table)}__#{main_table_id_column}__#{sum_column}__#{summed_table_id_column}#{summed_column_slug}__#{main_table_fk_column}__#{summed_table_fk_column}"
158
+ function_name = opts[:function_name] || "pgt_stmc_#{pgt_mangled_table_name(main_table)}__#{main_table_id_column}__#{sum_column}__#{pgt_mangled_table_name(summed_table)}__#{summed_table_id_column}#{summed_column_slug}__#{pgt_mangled_table_name(join_table)}__#{main_table_fk_column}__#{summed_table_fk_column}"
159
+ join_trigger_name = opts[:join_trigger_name] || "pgt_stmc_join_#{pgt_mangled_table_name(main_table)}__#{main_table_id_column}__#{sum_column}__#{summed_table_id_column}#{summed_column_slug}__#{main_table_fk_column}__#{summed_table_fk_column}"
160
+ join_function_name = opts[:join_function_name] || "pgt_stmc_join_#{pgt_mangled_table_name(main_table)}__#{main_table_id_column}__#{sum_column}__#{pgt_mangled_table_name(summed_table)}__#{summed_table_id_column}#{summed_column_slug}__#{pgt_mangled_table_name(join_table)}__#{main_table_fk_column}__#{summed_table_fk_column}"
118
161
 
119
162
  orig_summed_table = summed_table
120
163
  orig_join_table = join_table
@@ -171,8 +214,8 @@ module Sequel
171
214
  end
172
215
 
173
216
  def pgt_touch(main_table, touch_table, column, expr, opts={})
174
- trigger_name = opts[:trigger_name] || "pgt_t_#{main_table}__#{touch_table}"
175
- function_name = opts[:function_name] || "pgt_t_#{main_table}__#{touch_table}"
217
+ trigger_name = opts[:trigger_name] || "pgt_t_#{pgt_mangled_table_name(main_table)}__#{pgt_mangled_table_name(touch_table)}"
218
+ function_name = opts[:function_name] || "pgt_t_#{pgt_mangled_table_name(main_table)}__#{pgt_mangled_table_name(touch_table)}"
176
219
  cond = lambda{|source| expr.map{|k,v| "#{quote_identifier(k)} = #{source}.#{quote_identifier(v)}"}.join(" AND ")}
177
220
  same_id = expr.map{|k,v| "NEW.#{quote_identifier(v)} = OLD.#{quote_identifier(v)}"}.join(" AND ")
178
221
 
@@ -204,7 +247,7 @@ module Sequel
204
247
 
205
248
  def pgt_updated_at(table, column, opts={})
206
249
  trigger_name = opts[:trigger_name] || "pgt_ua_#{column}"
207
- function_name = opts[:function_name] || "pgt_ua_#{table}__#{column}"
250
+ function_name = opts[:function_name] || "pgt_ua_#{pgt_mangled_table_name(table)}__#{column}"
208
251
  pgt_trigger(table, trigger_name, function_name, [:insert, :update], <<-SQL)
209
252
  BEGIN
210
253
  NEW.#{quote_identifier(column)} := CURRENT_TIMESTAMP;
@@ -216,9 +259,9 @@ module Sequel
216
259
  def pgt_foreign_key_array(opts={})
217
260
  table, column, rtable, rcolumn = opts.values_at(:table, :column, :referenced_table, :referenced_column)
218
261
  trigger_name = opts[:trigger_name] || "pgt_fka_#{column}"
219
- function_name = opts[:function_name] || "pgt_fka_#{table}__#{column}"
262
+ function_name = opts[:function_name] || "pgt_fka_#{pgt_mangled_table_name(table)}__#{column}"
220
263
  rtrigger_name = opts[:referenced_trigger_name] || "pgt_rfka_#{column}"
221
- rfunction_name = opts[:referenced_function_name] || "pgt_rfka_#{table}__#{column}"
264
+ rfunction_name = opts[:referenced_function_name] || "pgt_rfka_#{pgt_mangled_table_name(table)}__#{column}"
222
265
  col = quote_identifier(column)
223
266
  tab = quote_identifier(table)
224
267
  rcol = quote_identifier(rcolumn)
@@ -281,6 +324,11 @@ module Sequel
281
324
  create_function(function_name, definition, :language=>:plpgsql, :returns=>:trigger, :replace=>true)
282
325
  create_trigger(table, trigger_name, function_name, :events=>events, :each_row=>true, :after=>opts[:after])
283
326
  end
327
+
328
+ # Mangle the schema name so it can be used in an unquoted_identifier
329
+ def pgt_mangled_table_name(table)
330
+ quote_schema_table(table).gsub('"', '').gsub(/[^A-Za-z0-9]/, '_').gsub(/_+/, '_')
331
+ end
284
332
  end
285
333
 
286
334
  module PGTMethods
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env ruby
2
2
  require 'rubygems'
3
3
  require 'sequel'
4
- require 'minitest/spec'
4
+
5
+ ENV['MT_NO_PLUGINS'] = '1' # Work around stupid autoloading of plugins
6
+ gem 'minitest'
5
7
  require 'minitest/autorun'
6
8
 
7
9
  DB = Sequel.connect(ENV['PGT_SPEC_DB']||'postgres:///spgt_test?user=postgres')
@@ -16,578 +18,632 @@ else
16
18
  end
17
19
  DB.extension :pg_array
18
20
 
19
- describe "PostgreSQL Triggers" do
21
+ describe "PostgreSQL Counter Cache Trigger" do
22
+ before do
23
+ DB.create_table(:accounts){integer :id; integer :num_entries, :default=>0}
24
+ DB.create_table(:entries){integer :id; integer :account_id}
25
+ DB.pgt_counter_cache(:accounts, :id, :num_entries, :entries, :account_id, :function_name=>:spgt_counter_cache)
26
+ DB[:accounts].insert(:id=>1)
27
+ DB[:accounts].insert(:id=>2)
28
+ end
29
+
30
+ after do
31
+ DB.drop_table(:entries, :accounts)
32
+ DB.drop_function(:spgt_counter_cache)
33
+ end
34
+
35
+ it "should modify counter cache when adding or removing records" do
36
+ DB[:accounts].order(:id).select_map(:num_entries).must_equal [0, 0]
37
+
38
+ DB[:entries].insert(:id=>1, :account_id=>1)
39
+ DB[:accounts].order(:id).select_map(:num_entries).must_equal [1, 0]
40
+
41
+ DB[:entries].insert(:id=>2, :account_id=>1)
42
+ DB[:accounts].order(:id).select_map(:num_entries).must_equal [2, 0]
43
+
44
+ DB[:entries].insert(:id=>3, :account_id=>nil)
45
+ DB[:accounts].order(:id).select_map(:num_entries).must_equal [2, 0]
46
+
47
+ DB[:entries].where(:id=>3).update(:account_id=>2)
48
+ DB[:accounts].order(:id).select_map(:num_entries).must_equal [2, 1]
49
+
50
+ DB[:entries].where(:id=>2).update(:account_id=>2)
51
+ DB[:accounts].order(:id).select_map(:num_entries).must_equal [1, 2]
52
+
53
+ DB[:entries].where(:id=>2).update(:account_id=>nil)
54
+ DB[:accounts].order(:id).select_map(:num_entries).must_equal [1, 1]
55
+
56
+ DB[:entries].where(:id=>2).update(:id=>4)
57
+ DB[:accounts].order(:id).select_map(:num_entries).must_equal [1, 1]
58
+
59
+ DB[:entries].where(:id=>4).update(:account_id=>2)
60
+ DB[:accounts].order(:id).select_map(:num_entries).must_equal [1, 2]
61
+
62
+ DB[:entries].where(:id=>4).update(:account_id=>nil)
63
+ DB[:accounts].order(:id).select_map(:num_entries).must_equal [1, 1]
64
+
65
+ DB[:entries].filter(:id=>4).delete
66
+ DB[:accounts].order(:id).select_map(:num_entries).must_equal [1, 1]
67
+
68
+ DB[:entries].delete
69
+ DB[:accounts].order(:id).select_map(:num_entries).must_equal [0, 0]
70
+ end
71
+ end
72
+
73
+ describe "PostgreSQL Created At Trigger" do
74
+ before do
75
+ DB.create_table(:accounts){integer :id; timestamp :added_on}
76
+ DB.pgt_created_at(:accounts, :added_on, :function_name=>:spgt_created_at)
77
+ end
78
+
79
+ after do
80
+ DB.drop_table(:accounts)
81
+ DB.drop_function(:spgt_created_at)
82
+ end
83
+
84
+ it "should set the column upon insertion and ignore modifications afterward" do
85
+ DB[:accounts].insert(:id=>1)
86
+ t = DB[:accounts].get(:added_on)
87
+ t.strftime('%F').must_equal Date.today.strftime('%F')
88
+ DB[:accounts].update(:added_on=>Date.today - 60)
89
+ DB[:accounts].get(:added_on).must_equal t
90
+ DB[:accounts].insert(:id=>2)
91
+ ds = DB[:accounts].select(:added_on)
92
+ DB[:accounts].select((Sequel::SQL::NumericExpression.new(:NOOP, ds.filter(:id=>2)) > ds.filter(:id=>1)).as(:x)).first[:x].must_equal true
93
+ DB[:accounts].filter(:id=>1).update(:id=>3)
94
+ DB[:accounts].select((Sequel::SQL::NumericExpression.new(:NOOP, ds.filter(:id=>2)) > ds.filter(:id=>3)).as(:x)).first[:x].must_equal true
95
+ end
96
+ end
97
+
98
+ describe "PostgreSQL Immutable Trigger" do
99
+ before do
100
+ DB.create_table(:accounts){integer :id; integer :balance, :default=>0}
101
+ DB.pgt_immutable(:accounts, :balance, :function_name=>:spgt_immutable)
102
+ DB[:accounts].insert(:id=>1)
103
+ end
104
+
105
+ after do
106
+ DB.drop_table(:accounts)
107
+ DB.drop_function(:spgt_immutable)
108
+ end
109
+
110
+ it "should allow modifying columns not marked as immutable" do
111
+ DB[:accounts].update(:id=>2)
112
+ end
113
+
114
+ it "should allow updating a column to its existing value" do
115
+ DB[:accounts].update(:balance=>0)
116
+ DB[:accounts].update(:balance=>Sequel.*(:balance, :balance))
117
+ end
118
+
119
+ it "should not allow modifying a column's value" do
120
+ proc{DB[:accounts].update(:balance=>1)}.must_raise(Sequel::DatabaseError)
121
+ end
122
+
123
+ it "should handle NULL values correctly" do
124
+ proc{DB[:accounts].update(:balance=>nil)}.must_raise(Sequel::DatabaseError)
125
+ DB[:accounts].delete
126
+ DB[:accounts].insert(:id=>1, :balance=>nil)
127
+ DB[:accounts].update(:balance=>nil)
128
+ proc{DB[:accounts].update(:balance=>0)}.must_raise(Sequel::DatabaseError)
129
+ end
130
+ end
131
+
132
+ describe "PostgreSQL Sum Cache Trigger" do
133
+ before do
134
+ DB.create_table(:accounts){integer :id; integer :balance, :default=>0}
135
+ DB.create_table(:entries){integer :id; integer :account_id; integer :amount}
136
+ DB.pgt_sum_cache(:accounts, :id, :balance, :entries, :account_id, :amount, :function_name=>:spgt_sum_cache)
137
+ DB[:accounts].insert(:id=>1)
138
+ DB[:accounts].insert(:id=>2)
139
+ end
140
+
141
+ after do
142
+ DB.drop_table(:entries, :accounts)
143
+ DB.drop_function(:spgt_sum_cache)
144
+ end
145
+
146
+ it "should modify sum cache when adding, updating, or removing records" do
147
+ DB[:accounts].order(:id).select_map(:balance).must_equal [0, 0]
148
+
149
+ DB[:entries].insert(:id=>1, :account_id=>1, :amount=>100)
150
+ DB[:accounts].order(:id).select_map(:balance).must_equal [100, 0]
151
+
152
+ DB[:entries].insert(:id=>2, :account_id=>1, :amount=>200)
153
+ DB[:accounts].order(:id).select_map(:balance).must_equal [300, 0]
154
+
155
+ DB[:entries].insert(:id=>3, :account_id=>nil, :amount=>500)
156
+ DB[:accounts].order(:id).select_map(:balance).must_equal [300, 0]
157
+
158
+ DB[:entries].where(:id=>3).update(:account_id=>2)
159
+ DB[:accounts].order(:id).select_map(:balance).must_equal [300, 500]
160
+
161
+ DB[:entries].exclude(:id=>2).update(:amount=>Sequel.*(:amount, 2))
162
+ DB[:accounts].order(:id).select_map(:balance).must_equal [400, 1000]
163
+
164
+ DB[:entries].where(:id=>2).update(:account_id=>2)
165
+ DB[:accounts].order(:id).select_map(:balance).must_equal [200, 1200]
166
+
167
+ DB[:entries].where(:id=>2).update(:account_id=>nil)
168
+ DB[:accounts].order(:id).select_map(:balance).must_equal [200, 1000]
169
+
170
+ DB[:entries].where(:id=>2).update(:id=>4)
171
+ DB[:accounts].order(:id).select_map(:balance).must_equal [200, 1000]
172
+
173
+ DB[:entries].where(:id=>4).update(:account_id=>2)
174
+ DB[:accounts].order(:id).select_map(:balance).must_equal [200, 1200]
175
+
176
+ DB[:entries].where(:id=>4).update(:account_id=>nil)
177
+ DB[:accounts].order(:id).select_map(:balance).must_equal [200, 1000]
178
+
179
+ DB[:entries].filter(:id=>4).delete
180
+ DB[:accounts].order(:id).select_map(:balance).must_equal [200, 1000]
181
+
182
+ DB[:entries].delete
183
+ DB[:accounts].order(:id).select_map(:balance).must_equal [0, 0]
184
+ end
185
+ end
186
+
187
+ describe "PostgreSQL Sum Cache Trigger with arbitrary expression" do
188
+ before do
189
+ DB.create_table(:accounts){integer :id; integer :nonzero_entries_count, :default=>0}
190
+ DB.create_table(:entries){integer :id; integer :account_id; integer :amount}
191
+ DB.pgt_sum_cache(:accounts, :id, :nonzero_entries_count, :entries, :account_id, Sequel.case({0=>0}, 1, :amount), :function_name=>:spgt_sum_cache)
192
+ DB[:accounts].insert(:id=>1)
193
+ DB[:accounts].insert(:id=>2)
194
+ end
195
+
196
+ after do
197
+ DB.drop_table(:entries, :accounts)
198
+ DB.drop_function(:spgt_sum_cache)
199
+ end
200
+
201
+ it "should modify sum cache when adding, updating, or removing records" do
202
+ DB[:accounts].order(:id).select_map(:nonzero_entries_count).must_equal [0, 0]
203
+
204
+ DB[:entries].insert(:id=>1, :account_id=>1, :amount=>100)
205
+ DB[:accounts].order(:id).select_map(:nonzero_entries_count).must_equal [1, 0]
206
+
207
+ DB[:entries].insert(:id=>2, :account_id=>1, :amount=>200)
208
+ DB[:accounts].order(:id).select_map(:nonzero_entries_count).must_equal [2, 0]
209
+
210
+ DB[:entries].insert(:id=>3, :account_id=>nil, :amount=>500)
211
+ DB[:accounts].order(:id).select_map(:nonzero_entries_count).must_equal [2, 0]
212
+
213
+ DB[:entries].where(:id=>3).update(:account_id=>2)
214
+ DB[:accounts].order(:id).select_map(:nonzero_entries_count).must_equal [2, 1]
215
+
216
+ DB[:entries].exclude(:id=>2).update(:amount=>Sequel.*(:amount, 2))
217
+ DB[:accounts].order(:id).select_map(:nonzero_entries_count).must_equal [2, 1]
218
+
219
+ DB[:entries].where(:id=>2).update(:account_id=>2)
220
+ DB[:accounts].order(:id).select_map(:nonzero_entries_count).must_equal [1, 2]
221
+
222
+ DB[:entries].where(:id=>2).update(:account_id=>nil)
223
+ DB[:accounts].order(:id).select_map(:nonzero_entries_count).must_equal [1, 1]
224
+
225
+ DB[:entries].where(:id=>2).update(:id=>4)
226
+ DB[:accounts].order(:id).select_map(:nonzero_entries_count).must_equal [1, 1]
227
+
228
+ DB[:entries].where(:id=>4).update(:account_id=>2)
229
+ DB[:accounts].order(:id).select_map(:nonzero_entries_count).must_equal [1, 2]
230
+
231
+ DB[:entries].where(:id=>4).update(:account_id=>nil)
232
+ DB[:accounts].order(:id).select_map(:nonzero_entries_count).must_equal [1, 1]
233
+
234
+ DB[:entries].filter(:id=>4).delete
235
+ DB[:accounts].order(:id).select_map(:nonzero_entries_count).must_equal [1, 1]
236
+
237
+ DB[:entries].delete
238
+ DB[:accounts].order(:id).select_map(:nonzero_entries_count).must_equal [0, 0]
239
+ end
240
+ end
241
+
242
+
243
+ describe "PostgreSQL Sum Through Many Cache Trigger" do
244
+ before do
245
+ DB.create_table(:parents){primary_key :id; integer :balance, :default=>0, :null=>false}
246
+ DB.create_table(:children){primary_key :id; integer :amount, :null=>false}
247
+ DB.create_table(:links){integer :parent_id, :null=>false; integer :child_id, :null=>false; unique [:parent_id, :child_id]}
248
+ DB.pgt_sum_through_many_cache(
249
+ :main_table=>:parents,
250
+ :sum_column=>:balance,
251
+ :summed_table=>:children,
252
+ :summed_column=>:amount,
253
+ :join_table=>:links,
254
+ :main_table_fk_column=>:parent_id,
255
+ :summed_table_fk_column=>:child_id,
256
+ :function_name=>:spgt_stm_cache,
257
+ :join_function_name=>:spgt_stm_cache_join
258
+ )
259
+ DB[:parents].insert(:id=>1)
260
+ DB[:parents].insert(:id=>2)
261
+ end
262
+
263
+ after do
264
+ DB.drop_table(:links, :parents, :children)
265
+ DB.drop_function(:spgt_stm_cache)
266
+ DB.drop_function(:spgt_stm_cache_join)
267
+ end
268
+
269
+ it "should modify sum cache when adding, updating, or removing records" do
270
+ DB[:parents].order(:id).select_map(:balance).must_equal [0, 0]
271
+
272
+ DB[:children].insert(:id=>1, :amount=>100)
273
+ DB[:links].insert(:parent_id=>1, :child_id=>1)
274
+ DB[:parents].order(:id).select_map(:balance).must_equal [100, 0]
275
+
276
+ DB[:children].insert(:id=>2, :amount=>200)
277
+ DB[:links].insert(:parent_id=>1, :child_id=>2)
278
+ DB[:parents].order(:id).select_map(:balance).must_equal [300, 0]
279
+
280
+ DB[:children].insert(:id=>3, :amount=>500)
281
+ DB[:parents].order(:id).select_map(:balance).must_equal [300, 0]
282
+ DB[:links].insert(:parent_id=>2, :child_id=>3)
283
+ DB[:parents].order(:id).select_map(:balance).must_equal [300, 500]
284
+
285
+ DB[:links].where(:parent_id=>2, :child_id=>3).update(:parent_id=>1)
286
+ DB[:parents].order(:id).select_map(:balance).must_equal [800, 0]
287
+
288
+ DB[:children].insert(:id=>4, :amount=>400)
289
+ DB[:links].where(:parent_id=>1, :child_id=>3).update(:child_id=>4)
290
+ DB[:parents].order(:id).select_map(:balance).must_equal [700, 0]
291
+
292
+ DB[:links].where(:parent_id=>1, :child_id=>4).update(:parent_id=>2, :child_id=>3)
293
+ DB[:parents].order(:id).select_map(:balance).must_equal [300, 500]
294
+
295
+ DB[:children].exclude(:id=>2).update(:amount=>Sequel.*(:amount, 2))
296
+ DB[:parents].order(:id).select_map(:balance).must_equal [400, 1000]
297
+
298
+ DB[:links].where(:parent_id=>1, :child_id=>2).update(:parent_id=>2)
299
+ DB[:parents].order(:id).select_map(:balance).must_equal [200, 1200]
300
+
301
+ DB[:links].where(:parent_id=>2, :child_id=>2).update(:parent_id=>1)
302
+ DB[:parents].order(:id).select_map(:balance).must_equal [400, 1000]
303
+
304
+ DB[:links].where(:parent_id=>1, :child_id=>2).update(:child_id=>3)
305
+ DB[:parents].order(:id).select_map(:balance).must_equal [1200, 1000]
306
+
307
+ DB[:links].insert(:parent_id=>2, :child_id=>4)
308
+ DB[:parents].order(:id).select_map(:balance).must_equal [1200, 1800]
309
+
310
+ DB[:children].filter(:id=>4).delete
311
+ DB[:parents].order(:id).select_map(:balance).must_equal [1200, 1000]
312
+
313
+ DB[:links].filter(:parent_id=>1, :child_id=>1).delete
314
+ DB[:parents].order(:id).select_map(:balance).must_equal [1000, 1000]
315
+
316
+ DB[:children].insert(:id=>4, :amount=>400)
317
+ DB[:parents].order(:id).select_map(:balance).must_equal [1000, 1400]
318
+
319
+ DB[:children].delete
320
+ DB[:parents].order(:id).select_map(:balance).must_equal [0, 0]
321
+
322
+ DB[:children].multi_insert([{:id=>2, :amount=>200}, {:id=>1, :amount=>200}, {:id=>3, :amount=>1000}, {:id=>4, :amount=>400}])
323
+ DB[:parents].order(:id).select_map(:balance).must_equal [1000, 1400]
324
+
325
+ DB[:links].where(:child_id=>3).update(:child_id=>2)
326
+ DB[:parents].order(:id).select_map(:balance).must_equal [200, 600]
327
+
328
+ DB[:children].update(:amount=>10)
329
+ DB[:parents].order(:id).select_map(:balance).must_equal [10, 20]
330
+
331
+ DB[:links].delete
332
+ DB[:parents].order(:id).select_map(:balance).must_equal [0, 0]
333
+ end
334
+ end
335
+
336
+ describe "PostgreSQL Sum Through Many Cache Trigger with arbitrary expression" do
20
337
  before do
21
- DB.create_language(:plpgsql) if DB.server_version < 90000
338
+ DB.create_table(:parents){primary_key :id; integer :nonzero_entries_count, :default=>0, :null=>false}
339
+ DB.create_table(:children){primary_key :id; integer :amount, :null=>false}
340
+ DB.create_table(:links){integer :parent_id, :null=>false; integer :child_id, :null=>false; unique [:parent_id, :child_id]}
341
+ DB.pgt_sum_through_many_cache(
342
+ :main_table=>:parents,
343
+ :sum_column=>:nonzero_entries_count,
344
+ :summed_table=>:children,
345
+ :summed_column=>Sequel.case({0=>0}, 1, :amount),
346
+ :join_table=>:links,
347
+ :main_table_fk_column=>:parent_id,
348
+ :summed_table_fk_column=>:child_id,
349
+ :function_name=>:spgt_stm_cache,
350
+ :join_function_name=>:spgt_stm_cache_join
351
+ )
352
+ DB[:parents].insert(:id=>1)
353
+ DB[:parents].insert(:id=>2)
22
354
  end
355
+
23
356
  after do
24
- DB.drop_language(:plpgsql, :cascade=>true) if DB.server_version < 90000
25
- end
26
-
27
- describe "PostgreSQL Counter Cache Trigger" do
28
- before do
29
- DB.create_table(:accounts){integer :id; integer :num_entries, :default=>0}
30
- DB.create_table(:entries){integer :id; integer :account_id}
31
- DB.pgt_counter_cache(:accounts, :id, :num_entries, :entries, :account_id, :function_name=>:spgt_counter_cache)
32
- DB[:accounts].insert(:id=>1)
33
- DB[:accounts].insert(:id=>2)
34
- end
35
-
36
- after do
37
- DB.drop_table(:entries, :accounts)
38
- DB.drop_function(:spgt_counter_cache)
39
- end
40
-
41
- it "Should modify counter cache when adding or removing records" do
42
- DB[:accounts].order(:id).select_map(:num_entries).must_equal [0, 0]
43
-
44
- DB[:entries].insert(:id=>1, :account_id=>1)
45
- DB[:accounts].order(:id).select_map(:num_entries).must_equal [1, 0]
46
-
47
- DB[:entries].insert(:id=>2, :account_id=>1)
48
- DB[:accounts].order(:id).select_map(:num_entries).must_equal [2, 0]
49
-
50
- DB[:entries].insert(:id=>3, :account_id=>nil)
51
- DB[:accounts].order(:id).select_map(:num_entries).must_equal [2, 0]
52
-
53
- DB[:entries].where(:id=>3).update(:account_id=>2)
54
- DB[:accounts].order(:id).select_map(:num_entries).must_equal [2, 1]
55
-
56
- DB[:entries].where(:id=>2).update(:account_id=>2)
57
- DB[:accounts].order(:id).select_map(:num_entries).must_equal [1, 2]
58
-
59
- DB[:entries].where(:id=>2).update(:account_id=>nil)
60
- DB[:accounts].order(:id).select_map(:num_entries).must_equal [1, 1]
61
-
62
- DB[:entries].where(:id=>2).update(:id=>4)
63
- DB[:accounts].order(:id).select_map(:num_entries).must_equal [1, 1]
64
-
65
- DB[:entries].where(:id=>4).update(:account_id=>2)
66
- DB[:accounts].order(:id).select_map(:num_entries).must_equal [1, 2]
67
-
68
- DB[:entries].where(:id=>4).update(:account_id=>nil)
69
- DB[:accounts].order(:id).select_map(:num_entries).must_equal [1, 1]
70
-
71
- DB[:entries].filter(:id=>4).delete
72
- DB[:accounts].order(:id).select_map(:num_entries).must_equal [1, 1]
73
-
74
- DB[:entries].delete
75
- DB[:accounts].order(:id).select_map(:num_entries).must_equal [0, 0]
76
- end
77
- end
78
-
79
- describe "PostgreSQL Created At Trigger" do
80
- before do
81
- DB.create_table(:accounts){integer :id; timestamp :added_on}
82
- DB.pgt_created_at(:accounts, :added_on, :function_name=>:spgt_created_at)
83
- end
84
-
85
- after do
86
- DB.drop_table(:accounts)
87
- DB.drop_function(:spgt_created_at)
88
- end
89
-
90
- it "Should set the column upon insertion and ignore modifications afterward" do
91
- DB[:accounts].insert(:id=>1)
92
- t = DB[:accounts].get(:added_on)
93
- t.strftime('%F').must_equal Date.today.strftime('%F')
94
- DB[:accounts].update(:added_on=>Date.today - 60)
95
- DB[:accounts].get(:added_on).must_equal t
96
- DB[:accounts].insert(:id=>2)
97
- ds = DB[:accounts].select(:added_on)
98
- DB[:accounts].select((Sequel::SQL::NumericExpression.new(:NOOP, ds.filter(:id=>2)) > ds.filter(:id=>1)).as(:x)).first[:x].must_equal true
99
- DB[:accounts].filter(:id=>1).update(:id=>3)
100
- DB[:accounts].select((Sequel::SQL::NumericExpression.new(:NOOP, ds.filter(:id=>2)) > ds.filter(:id=>3)).as(:x)).first[:x].must_equal true
101
- end
102
- end
103
-
104
- describe "PostgreSQL Immutable Trigger" do
105
- before do
106
- DB.create_table(:accounts){integer :id; integer :balance, :default=>0}
107
- DB.pgt_immutable(:accounts, :balance, :function_name=>:spgt_immutable)
108
- DB[:accounts].insert(:id=>1)
109
- end
110
-
111
- after do
112
- DB.drop_table(:accounts)
113
- DB.drop_function(:spgt_immutable)
114
- end
115
-
116
- it "Should allow modifying columns not marked as immutable" do
117
- DB[:accounts].update(:id=>2)
118
- end
119
-
120
- it "Should allow updating a column to its existing value" do
121
- DB[:accounts].update(:balance=>0)
122
- DB[:accounts].update(:balance=>Sequel.*(:balance, :balance))
123
- end
124
-
125
- it "Should not allow modifying a column's value" do
126
- proc{DB[:accounts].update(:balance=>1)}.must_raise(Sequel::DatabaseError)
127
- end
128
-
129
- it "Should handle NULL values correctly" do
130
- proc{DB[:accounts].update(:balance=>nil)}.must_raise(Sequel::DatabaseError)
131
- DB[:accounts].delete
132
- DB[:accounts].insert(:id=>1, :balance=>nil)
133
- DB[:accounts].update(:balance=>nil)
134
- proc{DB[:accounts].update(:balance=>0)}.must_raise(Sequel::DatabaseError)
135
- end
136
- end
137
-
138
- describe "PostgreSQL Sum Cache Trigger" do
139
- before do
140
- DB.create_table(:accounts){integer :id; integer :balance, :default=>0}
141
- DB.create_table(:entries){integer :id; integer :account_id; integer :amount}
142
- DB.pgt_sum_cache(:accounts, :id, :balance, :entries, :account_id, :amount, :function_name=>:spgt_sum_cache)
143
- DB[:accounts].insert(:id=>1)
144
- DB[:accounts].insert(:id=>2)
145
- end
146
-
147
- after do
148
- DB.drop_table(:entries, :accounts)
149
- DB.drop_function(:spgt_sum_cache)
150
- end
151
-
152
- it "Should modify sum cache when adding, updating, or removing records" do
153
- DB[:accounts].order(:id).select_map(:balance).must_equal [0, 0]
154
-
155
- DB[:entries].insert(:id=>1, :account_id=>1, :amount=>100)
156
- DB[:accounts].order(:id).select_map(:balance).must_equal [100, 0]
157
-
158
- DB[:entries].insert(:id=>2, :account_id=>1, :amount=>200)
159
- DB[:accounts].order(:id).select_map(:balance).must_equal [300, 0]
160
-
161
- DB[:entries].insert(:id=>3, :account_id=>nil, :amount=>500)
162
- DB[:accounts].order(:id).select_map(:balance).must_equal [300, 0]
163
-
164
- DB[:entries].where(:id=>3).update(:account_id=>2)
165
- DB[:accounts].order(:id).select_map(:balance).must_equal [300, 500]
166
-
167
- DB[:entries].exclude(:id=>2).update(:amount=>Sequel.*(:amount, 2))
168
- DB[:accounts].order(:id).select_map(:balance).must_equal [400, 1000]
169
-
170
- DB[:entries].where(:id=>2).update(:account_id=>2)
171
- DB[:accounts].order(:id).select_map(:balance).must_equal [200, 1200]
172
-
173
- DB[:entries].where(:id=>2).update(:account_id=>nil)
174
- DB[:accounts].order(:id).select_map(:balance).must_equal [200, 1000]
175
-
176
- DB[:entries].where(:id=>2).update(:id=>4)
177
- DB[:accounts].order(:id).select_map(:balance).must_equal [200, 1000]
178
-
179
- DB[:entries].where(:id=>4).update(:account_id=>2)
180
- DB[:accounts].order(:id).select_map(:balance).must_equal [200, 1200]
181
-
182
- DB[:entries].where(:id=>4).update(:account_id=>nil)
183
- DB[:accounts].order(:id).select_map(:balance).must_equal [200, 1000]
184
-
185
- DB[:entries].filter(:id=>4).delete
186
- DB[:accounts].order(:id).select_map(:balance).must_equal [200, 1000]
187
-
188
- DB[:entries].delete
189
- DB[:accounts].order(:id).select_map(:balance).must_equal [0, 0]
190
- end
191
- end
192
-
193
- describe "PostgreSQL Sum Cache Trigger with arbitrary expression" do
194
- before do
195
- DB.create_table(:accounts){integer :id; integer :nonzero_entries_count, :default=>0}
196
- DB.create_table(:entries){integer :id; integer :account_id; integer :amount}
197
- DB.pgt_sum_cache(:accounts, :id, :nonzero_entries_count, :entries, :account_id, Sequel.case({0=>0}, 1, :amount), :function_name=>:spgt_sum_cache)
198
- DB[:accounts].insert(:id=>1)
199
- DB[:accounts].insert(:id=>2)
200
- end
201
-
202
- after do
203
- DB.drop_table(:entries, :accounts)
204
- DB.drop_function(:spgt_sum_cache)
205
- end
206
-
207
- it "Should modify sum cache when adding, updating, or removing records" do
208
- DB[:accounts].order(:id).select_map(:nonzero_entries_count).must_equal [0, 0]
209
-
210
- DB[:entries].insert(:id=>1, :account_id=>1, :amount=>100)
211
- DB[:accounts].order(:id).select_map(:nonzero_entries_count).must_equal [1, 0]
212
-
213
- DB[:entries].insert(:id=>2, :account_id=>1, :amount=>200)
214
- DB[:accounts].order(:id).select_map(:nonzero_entries_count).must_equal [2, 0]
215
-
216
- DB[:entries].insert(:id=>3, :account_id=>nil, :amount=>500)
217
- DB[:accounts].order(:id).select_map(:nonzero_entries_count).must_equal [2, 0]
218
-
219
- DB[:entries].where(:id=>3).update(:account_id=>2)
220
- DB[:accounts].order(:id).select_map(:nonzero_entries_count).must_equal [2, 1]
221
-
222
- DB[:entries].exclude(:id=>2).update(:amount=>Sequel.*(:amount, 2))
223
- DB[:accounts].order(:id).select_map(:nonzero_entries_count).must_equal [2, 1]
224
-
225
- DB[:entries].where(:id=>2).update(:account_id=>2)
226
- DB[:accounts].order(:id).select_map(:nonzero_entries_count).must_equal [1, 2]
227
-
228
- DB[:entries].where(:id=>2).update(:account_id=>nil)
229
- DB[:accounts].order(:id).select_map(:nonzero_entries_count).must_equal [1, 1]
230
-
231
- DB[:entries].where(:id=>2).update(:id=>4)
232
- DB[:accounts].order(:id).select_map(:nonzero_entries_count).must_equal [1, 1]
233
-
234
- DB[:entries].where(:id=>4).update(:account_id=>2)
235
- DB[:accounts].order(:id).select_map(:nonzero_entries_count).must_equal [1, 2]
236
-
237
- DB[:entries].where(:id=>4).update(:account_id=>nil)
238
- DB[:accounts].order(:id).select_map(:nonzero_entries_count).must_equal [1, 1]
239
-
240
- DB[:entries].filter(:id=>4).delete
241
- DB[:accounts].order(:id).select_map(:nonzero_entries_count).must_equal [1, 1]
242
-
243
- DB[:entries].delete
244
- DB[:accounts].order(:id).select_map(:nonzero_entries_count).must_equal [0, 0]
245
- end
246
- end
247
-
248
-
249
- describe "PostgreSQL Sum Through Many Cache Trigger" do
250
- before do
251
- DB.create_table(:parents){primary_key :id; integer :balance, :default=>0, :null=>false}
252
- DB.create_table(:children){primary_key :id; integer :amount, :null=>false}
253
- DB.create_table(:links){integer :parent_id, :null=>false; integer :child_id, :null=>false; unique [:parent_id, :child_id]}
254
- DB.pgt_sum_through_many_cache(
255
- :main_table=>:parents,
256
- :sum_column=>:balance,
257
- :summed_table=>:children,
258
- :summed_column=>:amount,
259
- :join_table=>:links,
260
- :main_table_fk_column=>:parent_id,
261
- :summed_table_fk_column=>:child_id,
262
- :function_name=>:spgt_stm_cache,
263
- :join_function_name=>:spgt_stm_cache_join
264
- )
265
- DB[:parents].insert(:id=>1)
266
- DB[:parents].insert(:id=>2)
267
- end
268
-
269
- after do
270
- DB.drop_table(:links, :parents, :children)
271
- DB.drop_function(:spgt_stm_cache)
272
- DB.drop_function(:spgt_stm_cache_join)
273
- end
274
-
275
- it "Should modify sum cache when adding, updating, or removing records" do
276
- DB[:parents].order(:id).select_map(:balance).must_equal [0, 0]
277
-
278
- DB[:children].insert(:id=>1, :amount=>100)
279
- DB[:links].insert(:parent_id=>1, :child_id=>1)
280
- DB[:parents].order(:id).select_map(:balance).must_equal [100, 0]
281
-
282
- DB[:children].insert(:id=>2, :amount=>200)
283
- DB[:links].insert(:parent_id=>1, :child_id=>2)
284
- DB[:parents].order(:id).select_map(:balance).must_equal [300, 0]
285
-
286
- DB[:children].insert(:id=>3, :amount=>500)
287
- DB[:parents].order(:id).select_map(:balance).must_equal [300, 0]
288
- DB[:links].insert(:parent_id=>2, :child_id=>3)
289
- DB[:parents].order(:id).select_map(:balance).must_equal [300, 500]
357
+ DB.drop_table(:links, :parents, :children)
358
+ DB.drop_function(:spgt_stm_cache)
359
+ DB.drop_function(:spgt_stm_cache_join)
360
+ end
361
+
362
+ it "should modify sum cache when adding, updating, or removing records" do
363
+ DB[:parents].order(:id).select_map(:nonzero_entries_count).must_equal [0, 0]
290
364
 
291
- DB[:links].where(:parent_id=>2, :child_id=>3).update(:parent_id=>1)
292
- DB[:parents].order(:id).select_map(:balance).must_equal [800, 0]
293
-
294
- DB[:children].insert(:id=>4, :amount=>400)
295
- DB[:links].where(:parent_id=>1, :child_id=>3).update(:child_id=>4)
296
- DB[:parents].order(:id).select_map(:balance).must_equal [700, 0]
365
+ DB[:children].insert(:id=>1, :amount=>100)
366
+ DB[:links].insert(:parent_id=>1, :child_id=>1)
367
+ DB[:parents].order(:id).select_map(:nonzero_entries_count).must_equal [1, 0]
297
368
 
298
- DB[:links].where(:parent_id=>1, :child_id=>4).update(:parent_id=>2, :child_id=>3)
299
- DB[:parents].order(:id).select_map(:balance).must_equal [300, 500]
369
+ DB[:children].insert(:id=>2, :amount=>200)
370
+ DB[:links].insert(:parent_id=>1, :child_id=>2)
371
+ DB[:parents].order(:id).select_map(:nonzero_entries_count).must_equal [2, 0]
300
372
 
301
- DB[:children].exclude(:id=>2).update(:amount=>Sequel.*(:amount, 2))
302
- DB[:parents].order(:id).select_map(:balance).must_equal [400, 1000]
373
+ DB[:children].insert(:id=>3, :amount=>500)
374
+ DB[:parents].order(:id).select_map(:nonzero_entries_count).must_equal [2, 0]
375
+ DB[:links].insert(:parent_id=>2, :child_id=>3)
376
+ DB[:parents].order(:id).select_map(:nonzero_entries_count).must_equal [2, 1]
303
377
 
304
- DB[:links].where(:parent_id=>1, :child_id=>2).update(:parent_id=>2)
305
- DB[:parents].order(:id).select_map(:balance).must_equal [200, 1200]
378
+ DB[:links].where(:parent_id=>2, :child_id=>3).update(:parent_id=>1)
379
+ DB[:parents].order(:id).select_map(:nonzero_entries_count).must_equal [3, 0]
306
380
 
307
- DB[:links].where(:parent_id=>2, :child_id=>2).update(:parent_id=>1)
308
- DB[:parents].order(:id).select_map(:balance).must_equal [400, 1000]
381
+ DB[:children].insert(:id=>4, :amount=>400)
382
+ DB[:links].where(:parent_id=>1, :child_id=>3).update(:child_id=>4)
383
+ DB[:parents].order(:id).select_map(:nonzero_entries_count).must_equal [3, 0]
309
384
 
310
- DB[:links].where(:parent_id=>1, :child_id=>2).update(:child_id=>3)
311
- DB[:parents].order(:id).select_map(:balance).must_equal [1200, 1000]
385
+ DB[:links].where(:parent_id=>1, :child_id=>4).update(:parent_id=>2, :child_id=>3)
386
+ DB[:parents].order(:id).select_map(:nonzero_entries_count).must_equal [2, 1]
312
387
 
313
- DB[:links].insert(:parent_id=>2, :child_id=>4)
314
- DB[:parents].order(:id).select_map(:balance).must_equal [1200, 1800]
388
+ DB[:children].exclude(:id=>2).update(:amount=>Sequel.*(:amount, 2))
389
+ DB[:parents].order(:id).select_map(:nonzero_entries_count).must_equal [2, 1]
315
390
 
316
- DB[:children].filter(:id=>4).delete
317
- DB[:parents].order(:id).select_map(:balance).must_equal [1200, 1000]
391
+ DB[:links].where(:parent_id=>1, :child_id=>2).update(:parent_id=>2)
392
+ DB[:parents].order(:id).select_map(:nonzero_entries_count).must_equal [1, 2]
318
393
 
319
- DB[:links].filter(:parent_id=>1, :child_id=>1).delete
320
- DB[:parents].order(:id).select_map(:balance).must_equal [1000, 1000]
394
+ DB[:links].where(:parent_id=>2, :child_id=>2).update(:parent_id=>1)
395
+ DB[:parents].order(:id).select_map(:nonzero_entries_count).must_equal [2, 1]
321
396
 
322
- DB[:children].insert(:id=>4, :amount=>400)
323
- DB[:parents].order(:id).select_map(:balance).must_equal [1000, 1400]
397
+ DB[:links].where(:parent_id=>1, :child_id=>2).update(:child_id=>3)
398
+ DB[:parents].order(:id).select_map(:nonzero_entries_count).must_equal [2, 1]
324
399
 
325
- DB[:children].delete
326
- DB[:parents].order(:id).select_map(:balance).must_equal [0, 0]
400
+ DB[:links].insert(:parent_id=>2, :child_id=>4)
401
+ DB[:parents].order(:id).select_map(:nonzero_entries_count).must_equal [2, 2]
327
402
 
328
- DB[:children].multi_insert([{:id=>2, :amount=>200}, {:id=>1, :amount=>200}, {:id=>3, :amount=>1000}, {:id=>4, :amount=>400}])
329
- DB[:parents].order(:id).select_map(:balance).must_equal [1000, 1400]
403
+ DB[:children].filter(:id=>4).delete
404
+ DB[:parents].order(:id).select_map(:nonzero_entries_count).must_equal [2, 1]
330
405
 
331
- DB[:links].where(:child_id=>3).update(:child_id=>2)
332
- DB[:parents].order(:id).select_map(:balance).must_equal [200, 600]
406
+ DB[:links].filter(:parent_id=>1, :child_id=>1).delete
407
+ DB[:parents].order(:id).select_map(:nonzero_entries_count).must_equal [1, 1]
333
408
 
334
- DB[:children].update(:amount=>10)
335
- DB[:parents].order(:id).select_map(:balance).must_equal [10, 20]
409
+ DB[:children].insert(:id=>4, :amount=>400)
410
+ DB[:parents].order(:id).select_map(:nonzero_entries_count).must_equal [1, 2]
336
411
 
337
- DB[:links].delete
338
- DB[:parents].order(:id).select_map(:balance).must_equal [0, 0]
339
- end
412
+ DB[:children].delete
413
+ DB[:parents].order(:id).select_map(:nonzero_entries_count).must_equal [0, 0]
414
+
415
+ DB[:children].multi_insert([{:id=>2, :amount=>200}, {:id=>1, :amount=>200}, {:id=>3, :amount=>1000}, {:id=>4, :amount=>400}])
416
+ DB[:parents].order(:id).select_map(:nonzero_entries_count).must_equal [1, 2]
417
+
418
+ DB[:links].where(:child_id=>3).update(:child_id=>2)
419
+ DB[:parents].order(:id).select_map(:nonzero_entries_count).must_equal [1, 2]
420
+
421
+ DB[:children].update(:amount=>10)
422
+ DB[:parents].order(:id).select_map(:nonzero_entries_count).must_equal [1, 2]
423
+
424
+ DB[:links].delete
425
+ DB[:parents].order(:id).select_map(:nonzero_entries_count).must_equal [0, 0]
340
426
  end
427
+ end
341
428
 
342
- describe "PostgreSQL Sum Through Many Cache Trigger with arbitrary expression" do
343
- before do
344
- DB.create_table(:parents){primary_key :id; integer :nonzero_entries_count, :default=>0, :null=>false}
345
- DB.create_table(:children){primary_key :id; integer :amount, :null=>false}
346
- DB.create_table(:links){integer :parent_id, :null=>false; integer :child_id, :null=>false; unique [:parent_id, :child_id]}
347
- DB.pgt_sum_through_many_cache(
348
- :main_table=>:parents,
349
- :sum_column=>:nonzero_entries_count,
350
- :summed_table=>:children,
351
- :summed_column=>Sequel.case({0=>0}, 1, :amount),
352
- :join_table=>:links,
353
- :main_table_fk_column=>:parent_id,
354
- :summed_table_fk_column=>:child_id,
355
- :function_name=>:spgt_stm_cache,
356
- :join_function_name=>:spgt_stm_cache_join
357
- )
358
- DB[:parents].insert(:id=>1)
359
- DB[:parents].insert(:id=>2)
360
- end
429
+ describe "PostgreSQL Updated At Trigger" do
430
+ before do
431
+ DB.create_table(:accounts){integer :id; timestamp :changed_on}
432
+ DB.pgt_updated_at(:accounts, :changed_on, :function_name=>:spgt_updated_at)
433
+ end
361
434
 
362
- after do
363
- DB.drop_table(:links, :parents, :children)
364
- DB.drop_function(:spgt_stm_cache)
365
- DB.drop_function(:spgt_stm_cache_join)
366
- end
435
+ after do
436
+ DB.drop_table(:accounts)
437
+ DB.drop_function(:spgt_updated_at)
438
+ end
367
439
 
368
- it "Should modify sum cache when adding, updating, or removing records" do
369
- DB[:parents].order(:id).select_map(:nonzero_entries_count).must_equal [0, 0]
440
+ it "should set the column always to the current timestamp" do
441
+ DB[:accounts].insert(:id=>1)
442
+ t = DB[:accounts].get(:changed_on)
443
+ t.strftime('%F').must_equal Date.today.strftime('%F')
444
+ DB[:accounts].insert(:id=>2)
445
+ ds = DB[:accounts].select(:changed_on)
446
+ DB[:accounts].select((Sequel::SQL::NumericExpression.new(:NOOP, ds.filter(:id=>2)) > ds.filter(:id=>1)).as(:x)).first[:x].must_equal true
447
+ DB[:accounts].filter(:id=>1).update(:id=>3)
448
+ DB[:accounts].select((Sequel::SQL::NumericExpression.new(:NOOP, ds.filter(:id=>3)) > ds.filter(:id=>2)).as(:x)).first[:x].must_equal true
449
+ end
450
+ end
370
451
 
371
- DB[:children].insert(:id=>1, :amount=>100)
372
- DB[:links].insert(:parent_id=>1, :child_id=>1)
373
- DB[:parents].order(:id).select_map(:nonzero_entries_count).must_equal [1, 0]
452
+ describe "PostgreSQL Touch Trigger" do
453
+ before do
454
+ DB.create_table(:parents){integer :id1; integer :id2; integer :child_id; timestamp :changed_on}
455
+ DB.create_table(:children){integer :id; integer :parent_id1; integer :parent_id2; timestamp :changed_on}
456
+ end
374
457
 
375
- DB[:children].insert(:id=>2, :amount=>200)
376
- DB[:links].insert(:parent_id=>1, :child_id=>2)
377
- DB[:parents].order(:id).select_map(:nonzero_entries_count).must_equal [2, 0]
458
+ after do
459
+ DB.drop_table(:children, :parents)
460
+ DB.drop_function(:spgt_touch)
461
+ DB.drop_function(:spgt_touch2) if @spgt_touch2
462
+ end
378
463
 
379
- DB[:children].insert(:id=>3, :amount=>500)
380
- DB[:parents].order(:id).select_map(:nonzero_entries_count).must_equal [2, 0]
381
- DB[:links].insert(:parent_id=>2, :child_id=>3)
382
- DB[:parents].order(:id).select_map(:nonzero_entries_count).must_equal [2, 1]
464
+ it "should update the timestamp column of the related table when adding, updating or removing records" do
465
+ DB.pgt_touch(:children, :parents, :changed_on, {:id1=>:parent_id1}, :function_name=>:spgt_touch)
466
+ d = Date.today
467
+ d30 = Date.today - 30
468
+ DB[:parents].insert(:id1=>1, :changed_on=>d30)
469
+ DB[:parents].insert(:id1=>2, :changed_on=>d30)
470
+ DB[:children].insert(:id=>1, :parent_id1=>1)
471
+ DB[:parents].order(:id1).select_map(:changed_on).map{|t| t.strftime('%F')}.must_equal [d.strftime('%F'), d30.strftime('%F')]
472
+
473
+ DB[:parents].update(:changed_on=>d30)
474
+ DB[:children].update(:id=>2)
475
+ DB[:parents].order(:id1).select_map(:changed_on).map{|t| t.strftime('%F')}.must_equal [d.strftime('%F'), d30.strftime('%F')]
476
+
477
+ DB[:parents].update(:changed_on=>d30)
478
+ DB[:children].update(:parent_id1=>2)
479
+ DB[:parents].order(:id1).select_map(:changed_on).map{|t| t.strftime('%F')}.must_equal [d.strftime('%F'), d.strftime('%F')]
480
+
481
+ DB[:parents].update(:changed_on=>d30)
482
+ DB[:children].update(:parent_id1=>nil)
483
+ DB[:parents].order(:id1).select_map(:changed_on).map{|t| t.strftime('%F')}.must_equal [d30.strftime('%F'), d.strftime('%F')]
484
+
485
+ DB[:parents].update(:changed_on=>d30)
486
+ DB[:children].update(:parent_id2=>1)
487
+ DB[:parents].order(:id1).select_map(:changed_on).map{|t| t.strftime('%F')}.must_equal [d30.strftime('%F'), d30.strftime('%F')]
488
+
489
+ DB[:parents].update(:changed_on=>d30)
490
+ DB[:children].update(:parent_id1=>2)
491
+ DB[:parents].order(:id1).select_map(:changed_on).map{|t| t.strftime('%F')}.must_equal [d30.strftime('%F'), d.strftime('%F')]
492
+
493
+ DB[:parents].update(:changed_on=>d30)
494
+ DB[:children].delete
495
+ DB[:parents].order(:id1).select_map(:changed_on).map{|t| t.strftime('%F')}.must_equal [d30.strftime('%F'), d.strftime('%F')]
496
+
497
+ DB[:parents].update(:changed_on=>d30)
498
+ DB[:children].insert(:id=>2, :parent_id1=>nil)
499
+ DB[:parents].order(:id1).select_map(:changed_on).map{|t| t.strftime('%F')}.must_equal [d30.strftime('%F'), d30.strftime('%F')]
500
+ DB[:children].where(:id=>2).delete
501
+ DB[:parents].order(:id1).select_map(:changed_on).map{|t| t.strftime('%F')}.must_equal [d30.strftime('%F'), d30.strftime('%F')]
502
+ end
383
503
 
384
- DB[:links].where(:parent_id=>2, :child_id=>3).update(:parent_id=>1)
385
- DB[:parents].order(:id).select_map(:nonzero_entries_count).must_equal [3, 0]
504
+ it "should update the timestamp column of the related table when there is a composite foreign key" do
505
+ DB.pgt_touch(:children, :parents, :changed_on, {:id1=>:parent_id1, :id2=>:parent_id2}, :function_name=>:spgt_touch)
506
+ DB[:parents].insert(:id1=>1, :id2=>2, :changed_on=>Date.today - 30)
507
+ DB[:children].insert(:id=>1, :parent_id1=>1, :parent_id2=>2)
508
+ DB[:parents].get(:changed_on).strftime('%F').must_equal Date.today.strftime('%F')
509
+ DB[:parents].update(:changed_on=>Date.today - 30)
510
+ DB[:children].update(:id=>2)
511
+ DB[:parents].get(:changed_on).strftime('%F').must_equal Date.today.strftime('%F')
512
+ DB[:parents].update(:changed_on=>Date.today - 30)
513
+ DB[:children].delete
514
+ DB[:parents].get(:changed_on).strftime('%F').must_equal Date.today.strftime('%F')
515
+ end
516
+
517
+ it "should update timestamps correctly when two tables touch each other" do
518
+ DB.pgt_touch(:children, :parents, :changed_on, {:id1=>:parent_id1}, :function_name=>:spgt_touch)
519
+ @spgt_touch2 = true
520
+ DB.pgt_touch(:parents, :children, :changed_on, {:id=>:child_id}, :function_name=>:spgt_touch2)
521
+ DB[:parents].insert(:id1=>1, :child_id=>1, :changed_on=>Date.today - 30)
522
+ DB[:children].insert(:id=>1, :parent_id1=>1, :changed_on=>Date.today - 30)
523
+ DB[:parents].get(:changed_on).strftime('%F').must_equal Date.today.strftime('%F')
524
+ DB[:children].get(:changed_on).strftime('%F').must_equal Date.today.strftime('%F')
525
+ time = DB[:parents].get(:changed_on)
526
+ DB[:parents].update(:id2=>4)
527
+ DB[:parents].get(:changed_on).must_be :>, time
528
+ DB[:children].get(:changed_on).must_be :>, time
529
+ time = DB[:parents].get(:changed_on)
530
+ DB[:children].update(:id=>1)
531
+ DB[:parents].get(:changed_on).must_be :>, time
532
+ DB[:children].get(:changed_on).must_be :>, time
533
+ time = DB[:parents].get(:changed_on)
534
+ DB[:children].delete
535
+ DB[:parents].get(:changed_on).must_be :>, time
536
+ end
537
+
538
+ it "should update the timestamp on the related table if that timestamp is initially NULL" do
539
+ DB.pgt_touch(:children, :parents, :changed_on, {:id1=>:parent_id1}, :function_name=>:spgt_touch)
540
+ DB[:parents].insert(:id1=>1, :changed_on=>nil)
541
+ DB[:children].insert(:id=>1, :parent_id1=>1)
542
+ changed_on = DB[:parents].get(:changed_on)
543
+ changed_on.wont_equal nil
544
+ changed_on.strftime('%F').must_equal Date.today.strftime('%F')
545
+ end
546
+ end
547
+
548
+ describe "PostgreSQL Array Foreign Key Trigger" do
549
+ before do
550
+ DB.create_table(:accounts){Integer :id, :primary_key=>true}
551
+ DB.create_table(:entries){Integer :id, :primary_key=>true; column :account_ids, 'integer[]'}
552
+ DB.pgt_foreign_key_array(:table=>:entries, :column=>:account_ids, :referenced_table=>:accounts, :referenced_column=>:id, :function_name=>:spgt_foreign_key_array, :referenced_function_name=>:spgt_referenced_foreign_key_array)
553
+ end
386
554
 
387
- DB[:children].insert(:id=>4, :amount=>400)
388
- DB[:links].where(:parent_id=>1, :child_id=>3).update(:child_id=>4)
389
- DB[:parents].order(:id).select_map(:nonzero_entries_count).must_equal [3, 0]
555
+ after do
556
+ DB.drop_table(:entries, :accounts)
557
+ DB.drop_function(:spgt_foreign_key_array)
558
+ DB.drop_function(:spgt_referenced_foreign_key_array)
559
+ end
390
560
 
391
- DB[:links].where(:parent_id=>1, :child_id=>4).update(:parent_id=>2, :child_id=>3)
392
- DB[:parents].order(:id).select_map(:nonzero_entries_count).must_equal [2, 1]
561
+ it "should raise error for queries that violate referential integrity, and allow other queries" do
562
+ proc{DB[:entries].insert(:id=>10, :account_ids=>Sequel.pg_array([1]))}.must_raise Sequel::DatabaseError
563
+ DB[:entries].insert(:id=>10, :account_ids=>nil)
564
+ DB[:entries].update(:account_ids=>Sequel.pg_array([], :integer))
565
+ DB[:accounts].insert(:id=>1)
566
+ proc{DB[:entries].insert(:id=>10, :account_ids=>Sequel.pg_array([1, 1]))}.must_raise Sequel::DatabaseError
567
+ DB[:entries].update(:account_ids=>Sequel.pg_array([1]))
568
+ proc{DB[:entries].update(:account_ids=>Sequel.pg_array([2]))}.must_raise Sequel::DatabaseError
569
+ DB[:accounts].insert(:id=>2)
570
+ proc{DB[:entries].insert(:id=>10, :account_ids=>Sequel.pg_array([[1], [2]]))}.must_raise Sequel::DatabaseError
571
+ DB[:entries].update(:account_ids=>Sequel.pg_array([2]))
572
+ DB[:entries].all.must_equal [{:id=>10, :account_ids=>[2]}]
573
+ DB[:entries].update(:account_ids=>Sequel.pg_array([1, 2]))
574
+ DB[:entries].all.must_equal [{:id=>10, :account_ids=>[1, 2]}]
575
+ DB[:entries].update(:account_ids=>Sequel.pg_array([1]))
576
+ DB[:accounts].where(:id=>1).update(:id=>1)
577
+ DB[:accounts].where(:id=>2).update(:id=>3)
578
+ proc{DB[:accounts].where(:id=>1).update(:id=>2)}.must_raise Sequel::DatabaseError
579
+ proc{DB[:accounts].where(:id=>1).delete}.must_raise Sequel::DatabaseError
580
+ DB[:accounts].where(:id=>3).count.must_equal 1
581
+ DB[:accounts].where(:id=>3).delete
582
+ proc{DB[:accounts].delete}.must_raise Sequel::DatabaseError
583
+ DB[:entries].delete
584
+ DB[:accounts].delete
585
+ end
586
+ end
393
587
 
394
- DB[:children].exclude(:id=>2).update(:amount=>Sequel.*(:amount, 2))
395
- DB[:parents].order(:id).select_map(:nonzero_entries_count).must_equal [2, 1]
588
+ describe "PostgreSQL Force Defaults Trigger" do
589
+ before do
590
+ DB.create_table(:accounts){integer :id; integer :a, :default=>0; String :b; integer :c; integer :d, :default=>4}
591
+ DB.pgt_force_defaults(:accounts, {:a=>1, :b=>"'\\a", :c=>nil}, :function_name=>:spgt_force_defaults)
592
+ @ds = DB[:accounts]
593
+ end
396
594
 
397
- DB[:links].where(:parent_id=>1, :child_id=>2).update(:parent_id=>2)
398
- DB[:parents].order(:id).select_map(:nonzero_entries_count).must_equal [1, 2]
595
+ after do
596
+ DB.drop_table(:accounts)
597
+ DB.drop_function(:spgt_force_defaults)
598
+ end
399
599
 
400
- DB[:links].where(:parent_id=>2, :child_id=>2).update(:parent_id=>1)
401
- DB[:parents].order(:id).select_map(:nonzero_entries_count).must_equal [2, 1]
600
+ it "should override default values when inserting" do
601
+ @ds.insert
602
+ DB[:accounts].first.must_equal(:id=>nil, :a=>1, :b=>"'\\a", :c=>nil, :d=>4)
402
603
 
403
- DB[:links].where(:parent_id=>1, :child_id=>2).update(:child_id=>3)
404
- DB[:parents].order(:id).select_map(:nonzero_entries_count).must_equal [2, 1]
405
-
406
- DB[:links].insert(:parent_id=>2, :child_id=>4)
407
- DB[:parents].order(:id).select_map(:nonzero_entries_count).must_equal [2, 2]
408
-
409
- DB[:children].filter(:id=>4).delete
410
- DB[:parents].order(:id).select_map(:nonzero_entries_count).must_equal [2, 1]
411
-
412
- DB[:links].filter(:parent_id=>1, :child_id=>1).delete
413
- DB[:parents].order(:id).select_map(:nonzero_entries_count).must_equal [1, 1]
414
-
415
- DB[:children].insert(:id=>4, :amount=>400)
416
- DB[:parents].order(:id).select_map(:nonzero_entries_count).must_equal [1, 2]
417
-
418
- DB[:children].delete
419
- DB[:parents].order(:id).select_map(:nonzero_entries_count).must_equal [0, 0]
420
-
421
- DB[:children].multi_insert([{:id=>2, :amount=>200}, {:id=>1, :amount=>200}, {:id=>3, :amount=>1000}, {:id=>4, :amount=>400}])
422
- DB[:parents].order(:id).select_map(:nonzero_entries_count).must_equal [1, 2]
423
-
424
- DB[:links].where(:child_id=>3).update(:child_id=>2)
425
- DB[:parents].order(:id).select_map(:nonzero_entries_count).must_equal [1, 2]
426
-
427
- DB[:children].update(:amount=>10)
428
- DB[:parents].order(:id).select_map(:nonzero_entries_count).must_equal [1, 2]
429
-
430
- DB[:links].delete
431
- DB[:parents].order(:id).select_map(:nonzero_entries_count).must_equal [0, 0]
432
- end
433
- end
434
-
435
- describe "PostgreSQL Updated At Trigger" do
436
- before do
437
- DB.create_table(:accounts){integer :id; timestamp :changed_on}
438
- DB.pgt_updated_at(:accounts, :changed_on, :function_name=>:spgt_updated_at)
439
- end
440
-
441
- after do
442
- DB.drop_table(:accounts)
443
- DB.drop_function(:spgt_updated_at)
444
- end
445
-
446
- it "Should set the column always to the current timestamp" do
447
- DB[:accounts].insert(:id=>1)
448
- t = DB[:accounts].get(:changed_on)
449
- t.strftime('%F').must_equal Date.today.strftime('%F')
450
- DB[:accounts].insert(:id=>2)
451
- ds = DB[:accounts].select(:changed_on)
452
- DB[:accounts].select((Sequel::SQL::NumericExpression.new(:NOOP, ds.filter(:id=>2)) > ds.filter(:id=>1)).as(:x)).first[:x].must_equal true
453
- DB[:accounts].filter(:id=>1).update(:id=>3)
454
- DB[:accounts].select((Sequel::SQL::NumericExpression.new(:NOOP, ds.filter(:id=>3)) > ds.filter(:id=>2)).as(:x)).first[:x].must_equal true
455
- end
456
- end
457
-
458
- describe "PostgreSQL Touch Trigger" do
459
- before do
460
- DB.create_table(:parents){integer :id1; integer :id2; integer :child_id; timestamp :changed_on}
461
- DB.create_table(:children){integer :id; integer :parent_id1; integer :parent_id2; timestamp :changed_on}
462
- end
463
-
464
- after do
465
- DB.drop_table(:children, :parents)
466
- DB.drop_function(:spgt_touch)
467
- DB.drop_function(:spgt_touch2) if @spgt_touch2
468
- end
469
-
470
- it "Should update the timestamp column of the related table when adding, updating or removing records" do
471
- DB.pgt_touch(:children, :parents, :changed_on, {:id1=>:parent_id1}, :function_name=>:spgt_touch)
472
- d = Date.today
473
- d30 = Date.today - 30
474
- DB[:parents].insert(:id1=>1, :changed_on=>d30)
475
- DB[:parents].insert(:id1=>2, :changed_on=>d30)
476
- DB[:children].insert(:id=>1, :parent_id1=>1)
477
- DB[:parents].order(:id1).select_map(:changed_on).map{|t| t.strftime('%F')}.must_equal [d.strftime('%F'), d30.strftime('%F')]
478
-
479
- DB[:parents].update(:changed_on=>d30)
480
- DB[:children].update(:id=>2)
481
- DB[:parents].order(:id1).select_map(:changed_on).map{|t| t.strftime('%F')}.must_equal [d.strftime('%F'), d30.strftime('%F')]
482
-
483
- DB[:parents].update(:changed_on=>d30)
484
- DB[:children].update(:parent_id1=>2)
485
- DB[:parents].order(:id1).select_map(:changed_on).map{|t| t.strftime('%F')}.must_equal [d.strftime('%F'), d.strftime('%F')]
486
-
487
- DB[:parents].update(:changed_on=>d30)
488
- DB[:children].update(:parent_id1=>nil)
489
- DB[:parents].order(:id1).select_map(:changed_on).map{|t| t.strftime('%F')}.must_equal [d30.strftime('%F'), d.strftime('%F')]
490
-
491
- DB[:parents].update(:changed_on=>d30)
492
- DB[:children].update(:parent_id2=>1)
493
- DB[:parents].order(:id1).select_map(:changed_on).map{|t| t.strftime('%F')}.must_equal [d30.strftime('%F'), d30.strftime('%F')]
494
-
495
- DB[:parents].update(:changed_on=>d30)
496
- DB[:children].update(:parent_id1=>2)
497
- DB[:parents].order(:id1).select_map(:changed_on).map{|t| t.strftime('%F')}.must_equal [d30.strftime('%F'), d.strftime('%F')]
498
-
499
- DB[:parents].update(:changed_on=>d30)
500
- DB[:children].delete
501
- DB[:parents].order(:id1).select_map(:changed_on).map{|t| t.strftime('%F')}.must_equal [d30.strftime('%F'), d.strftime('%F')]
502
-
503
- DB[:parents].update(:changed_on=>d30)
504
- DB[:children].insert(:id=>2, :parent_id1=>nil)
505
- DB[:parents].order(:id1).select_map(:changed_on).map{|t| t.strftime('%F')}.must_equal [d30.strftime('%F'), d30.strftime('%F')]
506
- DB[:children].where(:id=>2).delete
507
- DB[:parents].order(:id1).select_map(:changed_on).map{|t| t.strftime('%F')}.must_equal [d30.strftime('%F'), d30.strftime('%F')]
508
- end
509
-
510
- it "Should update the timestamp column of the related table when there is a composite foreign key" do
511
- DB.pgt_touch(:children, :parents, :changed_on, {:id1=>:parent_id1, :id2=>:parent_id2}, :function_name=>:spgt_touch)
512
- DB[:parents].insert(:id1=>1, :id2=>2, :changed_on=>Date.today - 30)
513
- DB[:children].insert(:id=>1, :parent_id1=>1, :parent_id2=>2)
514
- DB[:parents].get(:changed_on).strftime('%F').must_equal Date.today.strftime('%F')
515
- DB[:parents].update(:changed_on=>Date.today - 30)
516
- DB[:children].update(:id=>2)
517
- DB[:parents].get(:changed_on).strftime('%F').must_equal Date.today.strftime('%F')
518
- DB[:parents].update(:changed_on=>Date.today - 30)
519
- DB[:children].delete
520
- DB[:parents].get(:changed_on).strftime('%F').must_equal Date.today.strftime('%F')
521
- end
522
-
523
- it "Should update timestamps correctly when two tables touch each other" do
524
- DB.pgt_touch(:children, :parents, :changed_on, {:id1=>:parent_id1}, :function_name=>:spgt_touch)
525
- @spgt_touch2 = true
526
- DB.pgt_touch(:parents, :children, :changed_on, {:id=>:child_id}, :function_name=>:spgt_touch2)
527
- DB[:parents].insert(:id1=>1, :child_id=>1, :changed_on=>Date.today - 30)
528
- DB[:children].insert(:id=>1, :parent_id1=>1, :changed_on=>Date.today - 30)
529
- DB[:parents].get(:changed_on).strftime('%F').must_equal Date.today.strftime('%F')
530
- DB[:children].get(:changed_on).strftime('%F').must_equal Date.today.strftime('%F')
531
- time = DB[:parents].get(:changed_on)
532
- DB[:parents].update(:id2=>4)
533
- DB[:parents].get(:changed_on).must_be :>, time
534
- DB[:children].get(:changed_on).must_be :>, time
535
- time = DB[:parents].get(:changed_on)
536
- DB[:children].update(:id=>1)
537
- DB[:parents].get(:changed_on).must_be :>, time
538
- DB[:children].get(:changed_on).must_be :>, time
539
- time = DB[:parents].get(:changed_on)
540
- DB[:children].delete
541
- DB[:parents].get(:changed_on).must_be :>, time
542
- end
543
-
544
- it "Should update the timestamp on the related table if that timestamp is initially NULL" do
545
- DB.pgt_touch(:children, :parents, :changed_on, {:id1=>:parent_id1}, :function_name=>:spgt_touch)
546
- DB[:parents].insert(:id1=>1, :changed_on=>nil)
547
- DB[:children].insert(:id=>1, :parent_id1=>1)
548
- changed_on = DB[:parents].get(:changed_on)
549
- changed_on.wont_equal nil
550
- changed_on.strftime('%F').must_equal Date.today.strftime('%F')
551
- end
552
- end
553
-
554
- describe "PostgreSQL Array Foreign Key Trigger" do
555
- before do
556
- DB.create_table(:accounts){Integer :id, :primary_key=>true}
557
- DB.create_table(:entries){Integer :id, :primary_key=>true; column :account_ids, 'integer[]'}
558
- DB.pgt_foreign_key_array(:table=>:entries, :column=>:account_ids, :referenced_table=>:accounts, :referenced_column=>:id, :function_name=>:spgt_foreign_key_array, :referenced_function_name=>:spgt_referenced_foreign_key_array)
559
- end
560
-
561
- after do
562
- DB.drop_table(:entries, :accounts)
563
- DB.drop_function(:spgt_foreign_key_array)
564
- DB.drop_function(:spgt_referenced_foreign_key_array)
565
- end
566
-
567
- it "should raise error for queries that violate referential integrity, and allow other queries" do
568
- proc{DB[:entries].insert(:id=>10, :account_ids=>Sequel.pg_array([1]))}.must_raise Sequel::DatabaseError
569
- DB[:entries].insert(:id=>10, :account_ids=>nil)
570
- DB[:entries].update(:account_ids=>Sequel.pg_array([], :integer))
571
- DB[:accounts].insert(:id=>1)
572
- proc{DB[:entries].insert(:id=>10, :account_ids=>Sequel.pg_array([1, 1]))}.must_raise Sequel::DatabaseError
573
- DB[:entries].update(:account_ids=>Sequel.pg_array([1]))
574
- proc{DB[:entries].update(:account_ids=>Sequel.pg_array([2]))}.must_raise Sequel::DatabaseError
575
- DB[:accounts].insert(:id=>2)
576
- proc{DB[:entries].insert(:id=>10, :account_ids=>Sequel.pg_array([[1], [2]]))}.must_raise Sequel::DatabaseError
577
- DB[:entries].update(:account_ids=>Sequel.pg_array([2]))
578
- DB[:entries].all.must_equal [{:id=>10, :account_ids=>[2]}]
579
- DB[:entries].update(:account_ids=>Sequel.pg_array([1, 2]))
580
- DB[:entries].all.must_equal [{:id=>10, :account_ids=>[1, 2]}]
581
- DB[:entries].update(:account_ids=>Sequel.pg_array([1]))
582
- DB[:accounts].where(:id=>1).update(:id=>1)
583
- DB[:accounts].where(:id=>2).update(:id=>3)
584
- proc{DB[:accounts].where(:id=>1).update(:id=>2)}.must_raise Sequel::DatabaseError
585
- proc{DB[:accounts].where(:id=>1).delete}.must_raise Sequel::DatabaseError
586
- DB[:accounts].where(:id=>3).count.must_equal 1
587
- DB[:accounts].where(:id=>3).delete
588
- proc{DB[:accounts].delete}.must_raise Sequel::DatabaseError
589
- DB[:entries].delete
590
- DB[:accounts].delete
591
- end
604
+ @ds.delete
605
+ @ds.insert(:id=>10, :a=>11, :b=>12, :c=>13, :d=>14)
606
+ DB[:accounts].first.must_equal(:id=>10, :a=>1, :b=>"'\\a", :c=>nil, :d=>14)
592
607
  end
593
608
  end
609
+
610
+
611
+ describe "PostgreSQL JSON Audit Logging" do
612
+ before do
613
+ DB.extension :pg_json
614
+ DB.create_table(:accounts){integer :id; integer :a}
615
+ DB.pgt_json_audit_log_setup(:table_audit_logs, :function_name=>:spgt_audit_log)
616
+ DB.pgt_json_audit_log(:accounts, :spgt_audit_log)
617
+ @ds = DB[:accounts]
618
+ @ds.insert(:id=>1)
619
+ @logs = DB[:table_audit_logs].reverse(:at)
620
+ end
621
+
622
+ after do
623
+ DB.drop_table(:accounts, :table_audit_logs)
624
+ DB.drop_function(:spgt_audit_log)
625
+ end
626
+
627
+ it "should previous values in JSON format for inserts and updates" do
628
+ @logs.first.must_be_nil
629
+
630
+ @ds.update(:id=>2, :a=>3)
631
+ @ds.all.must_equal [{:id=>2, :a=>3}]
632
+ h = @logs.first
633
+ h.delete(:at).to_i.must_be_within_delta(10, DB.get(Sequel::CURRENT_TIMESTAMP).to_i)
634
+ h.delete(:user).must_be_kind_of(String)
635
+ txid1 = h.delete(:txid)
636
+ txid1.must_be_kind_of(Integer)
637
+ h.must_equal(:schema=>"public", :table=>"accounts", :action=>"UPDATE", :prior=>{"a"=>nil, "id"=>1})
638
+
639
+ @ds.delete
640
+ @ds.all.must_equal []
641
+ h = @logs.first
642
+ h.delete(:at).to_i.must_be_within_delta(10, DB.get(Sequel::CURRENT_TIMESTAMP).to_i)
643
+ h.delete(:user).must_be_kind_of(String)
644
+ txid2 = h.delete(:txid)
645
+ txid2.must_be_kind_of(Integer)
646
+ txid2.must_be :>, txid1
647
+ h.must_equal(:schema=>"public", :table=>"accounts", :action=>"DELETE", :prior=>{"a"=>3, "id"=>2})
648
+ end
649
+ end if DB.server_version >= 90400