logidze 1.0.0.rc1 → 1.2.1

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: 747454e7c38a1969f39ed1f4b9d9142b2afc48d7ed7c34534b82abcac88dc4d7
4
- data.tar.gz: 2522172435f01b7d4441479f5f0c3e1bf08cde1f36e6ac9c745c8c8a20bd1d6f
3
+ metadata.gz: be9e11a375ab711ba02349e3cd14fbd4503af6572d969ced6db24bd36407bf05
4
+ data.tar.gz: '03885f890051fc938cba4089798bcc8b1b753a5ae941c5d93907536e1685ff94'
5
5
  SHA512:
6
- metadata.gz: cbbcbe791ca4f682f75c188c0b65db6f8244dea84a6cb5390a85c980921f1f84bf3676962dbb5792cf5ac2d7a24f0dbf1ac6b916f7bf03a1a13a1820ebc89507
7
- data.tar.gz: 4514014a739bc452aa7ad8fb26495a96aa651bfa3c8c4784021c1e2d13874c003c57e24b28ac6397f5ca5239e4e65dd76f0565df7af69ad29bbc8dd63630cc8b
6
+ metadata.gz: 24bc6a181263eaeb98762e8c183ba74b717a3195d2809460ceeddfbeefdc63c4b72e3c5da2b8426bba2722c765a41c27bf3ff2d3e265b5760a561795e205ee5a
7
+ data.tar.gz: 96f0e22ccad449d642320a9bf105e05ed15623ecaa1a52e453bfda356df2dbf9cb13eda235c6e288e1cf62d0484234f92104673c7ef15c9fbfe135d8bb757b16
data/CHANGELOG.md CHANGED
@@ -2,7 +2,40 @@
2
2
 
3
3
  ## master (unreleased)
4
4
 
5
- ## 1.0.0.rc1 (2020-09-01)
5
+ ## 1.2.1 (2022-01-13)
6
+
7
+ - [Fixes [#207](https://github.com/palkan/logidze/issues/207)] Add support for the use of `table_name_prefix` or `table_name_suffix`. ([@cavi21][])
8
+
9
+ - [Fixes [#205](https://github.com/palkan/logidze/issues/205)] Allow `rails destroy logidze:model SomeModel` to delete the migration file. ([@danielmklein][])
10
+
11
+ ## 1.2.0 (2021-06-11)
12
+
13
+ - Add user-defined exception handling ([@skryukov][])
14
+
15
+ By default, Logidze raises an exception which causes the entire transaction to fail.
16
+ To change this behavior, it's now possible to override `logidze_capture_exception(error_data jsonb)` function.
17
+
18
+ - [Fixes [#69](https://github.com/palkan/logidze/issues/69)] Fallback on NUMERIC_VALUE_OUT_OF_RANGE exception ([@skryukov][])
19
+
20
+ - [Fixes [#192](https://github.com/palkan/logidze/issues/192)] Skip `log_data` column during `apply_column_diff` ([@skryukov][])
21
+
22
+ ## 1.1.0 (2021-03-31)
23
+
24
+ - Add pending upgrade checks [Experimental]. ([@skryukov][])
25
+
26
+ Now Logidze can check for a pending upgrade. Use `Logidze.pending_upgrade = :warn` to be notified by warning, or `Logidze.pending_upgrade = :error` if you want Logidze to raise an error.
27
+
28
+ - [Fixes [#171](https://github.com/palkan/logidze/issues/171)] Stringify jsonb column values within snapshots. ([@skryukov][])
29
+
30
+ - [Fixes [#175](https://github.com/palkan/logidze/issues/175)] Set dynamic ActiveRecord version for migrations. ([@skryukov][])
31
+
32
+ - [Fixes [#184](https://github.com/palkan/logidze/issues/184)] Remove Rails meta-gem dependency ([@bf4][])
33
+
34
+ ## 1.0.0 (2020-11-09)
35
+
36
+ - Add `--name` option to model generator to specify the migration name. ([@palkan][])
37
+
38
+ When you update Logidze installation for a model multiple times, you might hit the `DuplicateMigrationNameError` (see [#167](https://github.com/palkan/logidze/issues/167)).
6
39
 
7
40
  - Add `.with_full_snapshot` to add full snapshots to the log instead of diffs. ([@palkan][])
8
41
 
@@ -330,3 +363,7 @@ This is a quick fix for a more general problem (see [#59](https://github.com/pal
330
363
  [@zocoi]: https://github.com/zocoi
331
364
  [@duderman]: https://github.com/duderman
332
365
  [@oleg-kiviljov]: https://github.com/oleg-kiviljov
366
+ [@skryukov]: https://github.com/skryukov
367
+ [@bf4]: https://github.com/bf4
368
+ [@cavi21]: https://github.com/cavi21
369
+ [@danielmklein]: https://github.com/danielmklein
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  [![Cult Of Martians](http://cultofmartians.com/assets/badges/badge.svg)](http://cultofmartians.com)
2
2
  [![Gem Version](https://badge.fury.io/rb/logidze.svg)](https://rubygems.org/gems/logidze)
3
- ![Build](https://github.com/palkan/logidze/workflows/Build/badge.svg)
3
+ [![Build](https://github.com/palkan/logidze/workflows/Build/badge.svg)](https://github.com/palkan/logidze/actions)
4
4
  [![Open Source Helpers](https://www.codetriage.com/palkan/logidze/badges/users.svg)](https://www.codetriage.com/palkan/logidze)
5
5
 
6
6
  # Logidze
@@ -10,8 +10,6 @@ Logidze provides tools for logging DB records changes when using PostgreSQL (>=9
10
10
  Logidze allows you to create a DB-level log (using triggers) and gives you an API to browse this log.
11
11
  The log is stored with the record itself in JSONB column. No additional tables required.
12
12
 
13
- **❗️ IMPORTANT:** This page contains documentation for the upcoming v1.0. For the latest release documentation see the [0-stable branch](https://github.com/palkan/logidze/tree/0-stable). If you're just starting with Logidze, we recommend using the latest release candidate (`1.0.0.rc1`).
14
-
15
13
  🤔 [How is Logidze pronounced?](https://github.com/palkan/logidze/issues/73)
16
14
 
17
15
  Other requirements:
@@ -24,18 +22,19 @@ Other requirements:
24
22
 
25
23
  ## Links
26
24
 
25
+ - [Logidze 1.0: Active Record, Postgres, Rails, and time travel](https://evilmartians.com/chronicles/logidze-1-0-active-record-postgresql-rails-and-time-travel?utm_source=logidze)
27
26
  - [Logidze: for all those tired of versioning data](https://evilmartians.com/chronicles/introducing-logidze?utm_source=logidze)
28
27
 
29
28
  ## Table of contents
30
29
 
31
- - [Main concepts](#main-concepts)
32
30
  - [Installation & Configuration](#installation)
33
31
  - [Using with schema.rb](#using-with-schemarb)
34
- - [Configuration models](#configuration-models)
32
+ - [Configuring models](#configuring-models)
35
33
  - [Backfill data](#backfill-data)
36
- - [Log size limit](#log-size-limit)
34
+ - [Log size limits](#log-size-limits)
37
35
  - [Tracking only selected columns](#tracking-only-selected-columns)
38
36
  - [Logs timestamps](#logs-timestamps)
37
+ - [Undoing a Generated Invocation](#undoing-a-generated-invocation)
39
38
  - [Usage](#usage)
40
39
  - [Basic API](#basic-api)
41
40
  - [Track meta information](#track-meta-information)
@@ -46,6 +45,7 @@ Other requirements:
46
45
  - [Associations versioning](#associations-versioning)
47
46
  - [Dealing with large logs](#dealing-with-large-logs)
48
47
  - [Handling records deletion](#handling-records-deletion)
48
+ - [Handling PG exceptions](#handling-pg-exceptions)
49
49
  - [Upgrading](#upgrading)
50
50
  - [Log format](#log-format)
51
51
  - [Troubleshooting 🚨](#troubleshooting)
@@ -56,7 +56,7 @@ Other requirements:
56
56
  Add Logidze to your application's Gemfile:
57
57
 
58
58
  ```ruby
59
- gem "logidze", "1.0.0.rc1"
59
+ gem "logidze", "~> 1.1"
60
60
  ```
61
61
 
62
62
  Install required DB extensions and create trigger function:
@@ -135,8 +135,8 @@ Model.create_logidze_snapshot
135
135
  Model.create_logidze_snapshot(timestamp: :created_at)
136
136
 
137
137
  # filter columns
138
- Model.create_logidze_snapshot(only: %(name))
139
- Model.create_logidze_snapshot(except: %(password))
138
+ Model.create_logidze_snapshot(only: %w[name])
139
+ Model.create_logidze_snapshot(except: %w[password])
140
140
 
141
141
  # or call a similar method (but with !) on a record
142
142
 
@@ -179,6 +179,16 @@ bundle exec rails generate logidze:model Post --timestamp_column time
179
179
  bundle exec rails generate logidze:model Post --timestamp_column nil # "null" and "false" will also work
180
180
  ```
181
181
 
182
+ ### Undoing a Generated Invocation
183
+
184
+ If you would like to re-do your `rails generate` anew, as with other generators you can use `rails destroy` to revert it, which will delete the migration file and undo the injection of `has_logidze` into the model file:
185
+
186
+ ```sh
187
+ bundle exec rails destroy logidze:model Post
188
+ ```
189
+
190
+ **IMPORTANT**: If you use non-UTC time zone for Active Record (`config.active_record.default_timezone`), you MUST always infer log timestamps from a timestamp column (e.g., when back-filling data); otherwise, you may end up with inconsistent logs ([#199](https://github.com/palkan/logidze/issues/199)). In general, we recommend using UTC as the database time unless there is a very strong reason not to.
191
+
182
192
  ## Usage
183
193
 
184
194
  ### Basic API
@@ -275,11 +285,13 @@ Logidze.append_on_undo = true
275
285
  You can store any meta information you want inside your version (it could be IP address, user agent, etc.). To add it you should wrap your code with a block:
276
286
 
277
287
  ```ruby
278
- Logidze.with_meta(ip: request.ip) do
288
+ Logidze.with_meta({ip: request.ip}) do
279
289
  post.save!
280
290
  end
281
291
  ```
282
292
 
293
+ **NOTE:** You should pass metadata as a Hash; passing keyword arguments doesn't work in Ruby 3.0+.
294
+
283
295
  Meta expects a hash to be passed so you won't need to encode and decode JSON manually.
284
296
 
285
297
  By default `.with_meta` wraps the block into a DB transaction. That could lead to an unexpected behavior, especially, when using `.with_meta` within an around_action. To avoid wrapping the block into a DB transaction use `transactional: false` option.
@@ -436,12 +448,21 @@ If you want to keep changes history after records deletion as well, consider usi
436
448
 
437
449
  See also the discussion: [#61](https://github.com/palkan/logidze/issues/61).
438
450
 
451
+ ## Handling PG exceptions
452
+
453
+ By default, Logidze raises an exception which causes the entire transaction to fail.
454
+ To change this behavior, it's now possible to override `logidze_capture_exception(error_data jsonb)` function.
455
+
456
+ For example, you may want to raise a warning instead of an exception and complete the transaction without updating log_data.
457
+
458
+ Related issues: [#193](https://github.com/palkan/logidze/issues/193)
459
+
439
460
  ## Upgrading
440
461
 
441
462
  We try to make an upgrade process as simple as possible. For now, the only required action is to create and run a migration:
442
463
 
443
464
  ```sh
444
- rails generate logidze:install --update
465
+ bundle exec rails generate logidze:install --update
445
466
  ```
446
467
 
447
468
  This updates core `logdize_logger` DB function. No need to update tables or triggers.
@@ -451,23 +472,37 @@ This updates core `logdize_logger` DB function. No need to update tables or trig
451
472
  If you want to update Logidze settings for the model, run migration with `--update` flag:
452
473
 
453
474
  ```sh
454
- rails generate logidze:model Post --update --only=title,body,rating
475
+ bundle exec rails generate logidze:model Post --update --only=title,body,rating
455
476
  ```
456
477
 
478
+ You can also use the `--name` option to specify the migration name to avoid duplicate migration names:
479
+
480
+ ```sh
481
+ $ bundle exec rails generate logidze:model Post --update --only=title,body,rating --name add_only_filter_to_posts_log_data
482
+
483
+ create db/migrate/20202309142344_add_only_filter_to_posts_log_data.rb
484
+ ```
485
+
486
+ ### Pending upgrade check [Experimental]
487
+
488
+ Logidze can check for a pending upgrade. Use `Logidze.on_pending_upgrade = :warn` to be notified by warning, or `Logidze.on_pending_upgrade = :error` if you want Logidze to raise an error.
489
+
457
490
  ### Upgrading from 0.x to 1.0 (edge)
458
491
 
459
492
  #### Schema and migrations
460
493
 
461
- Most SQL functions definitions has changed without backward compatibility.
494
+ Most SQL function definitions have changed without backward compatibility.
462
495
  Perform the following steps to upgrade:
463
496
 
464
- 1. Re-install Logidze: `rails generate logidze:install --update`.
497
+ 1. Re-install Logidze: `bundle exec rails generate logidze:install --update`.
498
+
499
+ 1. Re-install Logidze triggers **for all models**: `bundle exec rails generate logidze:model <model> --update`.
465
500
 
466
- 1. Re-install Logidze triggers **for all models**: `rails generate logidze:model <model> --update`.
501
+ **NOTE:** If you had previously specified whitelist/blacklist attributes, you will need to include the `--only`/`--except` [option](#tracking-only-selected-columns) as appropriate. You can easily copy these column lists from the previous logidze migration for the model.
467
502
 
468
503
  1. Remove the `include Logidze::Migration` line from the old migration files (if any)—this module has been removed.
469
504
 
470
- Rewrite the migrations to not use the `#current_setting(name)` and `#current_setting_missing_supported?` methods or copy them from the latest [0.x release](https://github.com/palkan/logidze/blob/0-stable/lib/logidze/migration.rb).
505
+ Rewrite legacy logidze migrations to not use the `#current_setting(name)` and `#current_setting_missing_supported?` methods, or copy them from the latest [0.x release](https://github.com/palkan/logidze/blob/0-stable/lib/logidze/migration.rb).
471
506
 
472
507
  #### API changes
473
508
 
@@ -532,11 +567,27 @@ First, when restoring data dumps you should consider using `--disable-triggers`
532
567
 
533
568
  When restoring data dumps for a particular PostgreSQL schema (e.g., when using Apartment), you may encounter the issue with non-existent Logidze functions. That happens because `pg_dump` adds `SELECT pg_catalog.set_config('search_path', '', false);`, and, thus, breaks our existing triggers/functions, because they live either in "public" or in a tenant's namespace (see [this thread](https://postgrespro.com/list/thread-id/2448092)).
534
569
 
570
+ ### `PG::NumericValueOutOfRange: ERROR: value overflows numeric format`
571
+
572
+ Due to the usage of `hstore_to_jsonb_loose` under the hood, there could be a situation when you have a string representing a number in the scientific notation (e.g., "557236406134e62000323100"). Postgres would try to convert it to a number (a pretty big one, for sure) and fail with the exception.
573
+
574
+ Related issues: [#69](https://github.com/palkan/logidze/issues/69).
575
+
535
576
  ## Development
536
577
 
537
- We use [Dip](https://github.com/bibendi/dip) for development. Provision the project by running `dip provision` and then use `dip bundle`, `dip rspec` or `dip bash` to interact with a Docker development environment.
578
+ This project requires a PostgreSQL instance running with the following setup:
579
+
580
+ ```sh
581
+ # For testing
582
+ createdb -h postgres -U postgres logidze_test
583
+
584
+ # For benchmarks
585
+ createdb -h postgres -U postgres logidze_bench
586
+ createdb -h postgres -U postgres logidze_perf_bench
587
+ psql -d logidze_bench -c 'CREATE EXTENSION IF NOT EXISTS hstore;'
588
+ ```
538
589
 
539
- If you prefer developing on your local machine, make user you have Postgres installed and run `./bin/setup`.
590
+ This project is compatible with [Reusable Docker environment](https://evilmartians.com/chronicles/reusable-development-containers-with-docker-compose-and-dip) setup.
540
591
 
541
592
  ## Contributing
542
593
 
@@ -0,0 +1,23 @@
1
+ CREATE OR REPLACE FUNCTION logidze_capture_exception(error_data jsonb) RETURNS boolean AS $body$
2
+ -- version: 1
3
+ BEGIN
4
+ -- Feel free to change this function to change Logidze behavior on exception.
5
+ --
6
+ -- Return `false` to raise exception or `true` to commit record changes.
7
+ --
8
+ -- `error_data` contains:
9
+ -- - returned_sqlstate
10
+ -- - message_text
11
+ -- - pg_exception_detail
12
+ -- - pg_exception_hint
13
+ -- - pg_exception_context
14
+ -- - schema_name
15
+ -- - table_name
16
+ -- Learn more about available keys:
17
+ -- https://www.postgresql.org/docs/9.6/plpgsql-control-structures.html#PLPGSQL-EXCEPTION-DIAGNOSTICS-VALUES
18
+ --
19
+
20
+ return false;
21
+ END;
22
+ $body$
23
+ LANGUAGE plpgsql;
@@ -1,5 +1,5 @@
1
- -- version: 1
2
1
  CREATE OR REPLACE FUNCTION logidze_compact_history(log_data jsonb, cutoff integer DEFAULT 1) RETURNS jsonb AS $body$
2
+ -- version: 1
3
3
  DECLARE
4
4
  merged jsonb;
5
5
  BEGIN
@@ -1,5 +1,5 @@
1
- -- version: 1
2
1
  CREATE OR REPLACE FUNCTION logidze_filter_keys(obj jsonb, keys text[], include_columns boolean DEFAULT false) RETURNS jsonb AS $body$
2
+ -- version: 1
3
3
  DECLARE
4
4
  res jsonb;
5
5
  key text;
@@ -1,5 +1,5 @@
1
- -- version: 1
2
1
  CREATE OR REPLACE FUNCTION logidze_logger() RETURNS TRIGGER AS $body$
2
+ -- version: 2
3
3
  DECLARE
4
4
  changes jsonb;
5
5
  version jsonb;
@@ -9,26 +9,32 @@ CREATE OR REPLACE FUNCTION logidze_logger() RETURNS TRIGGER AS $body$
9
9
  history_limit integer;
10
10
  debounce_time integer;
11
11
  current_version integer;
12
- merged jsonb;
12
+ k text;
13
13
  iterator integer;
14
14
  item record;
15
15
  columns text[];
16
16
  include_columns boolean;
17
17
  ts timestamp with time zone;
18
18
  ts_column text;
19
+ err_sqlstate text;
20
+ err_message text;
21
+ err_detail text;
22
+ err_hint text;
23
+ err_context text;
24
+ err_table_name text;
25
+ err_schema_name text;
26
+ err_jsonb jsonb;
27
+ err_captured boolean;
19
28
  BEGIN
20
29
  ts_column := NULLIF(TG_ARGV[1], 'null');
21
30
  columns := NULLIF(TG_ARGV[2], 'null');
22
31
  include_columns := NULLIF(TG_ARGV[3], 'null');
23
32
 
24
33
  IF TG_OP = 'INSERT' THEN
25
- -- always exclude log_data column
26
- changes := to_jsonb(NEW.*) - 'log_data';
27
-
28
34
  IF columns IS NOT NULL THEN
29
- snapshot = logidze_snapshot(changes, ts_column, columns, include_columns);
35
+ snapshot = logidze_snapshot(to_jsonb(NEW.*), ts_column, columns, include_columns);
30
36
  ELSE
31
- snapshot = logidze_snapshot(changes, ts_column);
37
+ snapshot = logidze_snapshot(to_jsonb(NEW.*), ts_column);
32
38
  END IF;
33
39
 
34
40
  IF snapshot#>>'{h, -1, c}' != '{}' THEN
@@ -38,13 +44,10 @@ CREATE OR REPLACE FUNCTION logidze_logger() RETURNS TRIGGER AS $body$
38
44
  ELSIF TG_OP = 'UPDATE' THEN
39
45
 
40
46
  IF OLD.log_data is NULL OR OLD.log_data = '{}'::jsonb THEN
41
- -- always exclude log_data column
42
- changes := to_jsonb(NEW.*) - 'log_data';
43
-
44
47
  IF columns IS NOT NULL THEN
45
- snapshot = logidze_snapshot(changes, ts_column, columns, include_columns);
48
+ snapshot = logidze_snapshot(to_jsonb(NEW.*), ts_column, columns, include_columns);
46
49
  ELSE
47
- snapshot = logidze_snapshot(changes, ts_column);
50
+ snapshot = logidze_snapshot(to_jsonb(NEW.*), ts_column);
48
51
  END IF;
49
52
 
50
53
  IF snapshot#>>'{h, -1, c}' != '{}' THEN
@@ -89,11 +92,37 @@ CREATE OR REPLACE FUNCTION logidze_logger() RETURNS TRIGGER AS $body$
89
92
  changes := '{}';
90
93
 
91
94
  IF (coalesce(current_setting('logidze.full_snapshot', true), '') = 'on') THEN
92
- changes = hstore_to_jsonb_loose(hstore(NEW.*));
95
+ BEGIN
96
+ changes = hstore_to_jsonb_loose(hstore(NEW.*));
97
+ EXCEPTION
98
+ WHEN NUMERIC_VALUE_OUT_OF_RANGE THEN
99
+ changes = row_to_json(NEW.*)::jsonb;
100
+ FOR k IN (SELECT key FROM jsonb_each(changes))
101
+ LOOP
102
+ IF jsonb_typeof(changes->k) = 'object' THEN
103
+ changes = jsonb_set(changes, ARRAY[k], to_jsonb(changes->>k));
104
+ END IF;
105
+ END LOOP;
106
+ END;
93
107
  ELSE
94
- changes = hstore_to_jsonb_loose(
95
- hstore(NEW.*) - hstore(OLD.*)
96
- );
108
+ BEGIN
109
+ changes = hstore_to_jsonb_loose(
110
+ hstore(NEW.*) - hstore(OLD.*)
111
+ );
112
+ EXCEPTION
113
+ WHEN NUMERIC_VALUE_OUT_OF_RANGE THEN
114
+ changes = (SELECT
115
+ COALESCE(json_object_agg(key, value), '{}')::jsonb
116
+ FROM
117
+ jsonb_each(row_to_json(NEW.*)::jsonb)
118
+ WHERE NOT jsonb_build_object(key, value) <@ row_to_json(OLD.*)::jsonb);
119
+ FOR k IN (SELECT key FROM jsonb_each(changes))
120
+ LOOP
121
+ IF jsonb_typeof(changes->k) = 'object' THEN
122
+ changes = jsonb_set(changes, ARRAY[k], to_jsonb(changes->>k));
123
+ END IF;
124
+ END LOOP;
125
+ END;
97
126
  END IF;
98
127
 
99
128
  changes = changes - 'log_data';
@@ -145,6 +174,30 @@ CREATE OR REPLACE FUNCTION logidze_logger() RETURNS TRIGGER AS $body$
145
174
  END IF;
146
175
 
147
176
  return NEW;
177
+ EXCEPTION
178
+ WHEN OTHERS THEN
179
+ GET STACKED DIAGNOSTICS err_sqlstate = RETURNED_SQLSTATE,
180
+ err_message = MESSAGE_TEXT,
181
+ err_detail = PG_EXCEPTION_DETAIL,
182
+ err_hint = PG_EXCEPTION_HINT,
183
+ err_context = PG_EXCEPTION_CONTEXT,
184
+ err_schema_name = SCHEMA_NAME,
185
+ err_table_name = TABLE_NAME;
186
+ err_jsonb := jsonb_build_object(
187
+ 'returned_sqlstate', err_sqlstate,
188
+ 'message_text', err_message,
189
+ 'pg_exception_detail', err_detail,
190
+ 'pg_exception_hint', err_hint,
191
+ 'pg_exception_context', err_context,
192
+ 'schema_name', err_schema_name,
193
+ 'table_name', err_table_name
194
+ );
195
+ err_captured = logidze_capture_exception(err_jsonb);
196
+ IF err_captured THEN
197
+ return NEW;
198
+ ELSE
199
+ RAISE;
200
+ END IF;
148
201
  END;
149
202
  $body$
150
203
  LANGUAGE plpgsql;
@@ -1,8 +1,10 @@
1
- -- version: 1
2
1
  CREATE OR REPLACE FUNCTION logidze_snapshot(item jsonb, ts_column text DEFAULT NULL, columns text[] DEFAULT NULL, include_columns boolean DEFAULT false) RETURNS jsonb AS $body$
2
+ -- version: 3
3
3
  DECLARE
4
4
  ts timestamp with time zone;
5
+ k text;
5
6
  BEGIN
7
+ item = item - 'log_data';
6
8
  IF ts_column IS NULL THEN
7
9
  ts := statement_timestamp();
8
10
  ELSE
@@ -13,6 +15,13 @@ CREATE OR REPLACE FUNCTION logidze_snapshot(item jsonb, ts_column text DEFAULT N
13
15
  item := logidze_filter_keys(item, columns, include_columns);
14
16
  END IF;
15
17
 
18
+ FOR k IN (SELECT key FROM jsonb_each(item))
19
+ LOOP
20
+ IF jsonb_typeof(item->k) = 'object' THEN
21
+ item := jsonb_set(item, ARRAY[k], to_jsonb(item->>k));
22
+ END IF;
23
+ END LOOP;
24
+
16
25
  return json_build_object(
17
26
  'v', 1,
18
27
  'h', jsonb_build_array(
@@ -1,8 +1,9 @@
1
- -- version: 1
2
1
  CREATE OR REPLACE FUNCTION logidze_version(v bigint, data jsonb, ts timestamp with time zone) RETURNS jsonb AS $body$
2
+ -- version: 2
3
3
  DECLARE
4
4
  buf jsonb;
5
5
  BEGIN
6
+ data = data - 'log_data';
6
7
  buf := jsonb_build_object(
7
8
  'ts',
8
9
  (extract(epoch from ts) * 1000)::bigint,
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "rails/generators"
4
4
  require "rails/generators/active_record"
5
+ require "logidze/utils/function_definitions"
5
6
  require_relative "../inject_sql"
6
7
  require_relative "../fx_helper"
7
8
 
@@ -14,8 +15,6 @@ module Logidze
14
15
  include InjectSql
15
16
  include FxHelper
16
17
 
17
- class FuncDef < Struct.new(:name, :version, :signature); end
18
-
19
18
  source_root File.expand_path("templates", __dir__)
20
19
  source_paths << File.expand_path("functions", __dir__)
21
20
 
@@ -80,21 +79,7 @@ module Logidze
80
79
  end
81
80
 
82
81
  def function_definitions
83
- @function_definitions ||=
84
- begin
85
- Dir.glob(File.join(__dir__, "functions", "*.sql")).map do |path|
86
- name = path.match(/([^\/]+)\.sql/)[1]
87
-
88
- file = File.open(path)
89
- header = file.readline
90
-
91
- version = header.match(/version:\s+(\d+)/)[1].to_i
92
- parameters = file.readline.match(/CREATE OR REPLACE FUNCTION\s+[\w_]+\((.*)\)/)[1]
93
- signature = parameters.split(/\s*,\s*/).map { |param| param.split(/\s+/, 2).last.sub(/\s+DEFAULT .*$/, "") }.join(", ")
94
-
95
- FuncDef.new(name, version, signature)
96
- end
97
- end
82
+ @function_definitions ||= Logidze::Utils::FunctionDefinitions.from_fs
98
83
  end
99
84
  end
100
85
 
@@ -1,4 +1,4 @@
1
- class <%= @migration_class_name %> < ActiveRecord::Migration[5.0]
1
+ class <%= @migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
2
2
  def change
3
3
  enable_extension :hstore
4
4
  end
@@ -1,4 +1,4 @@
1
- class <%= @migration_class_name %> < ActiveRecord::Migration[5.0]
1
+ class <%= @migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
2
2
  def up
3
3
  <%- if update? -%>
4
4
  # Drop legacy functions (<1.0)
@@ -1,4 +1,4 @@
1
- class <%= @migration_class_name %> < ActiveRecord::Migration[5.0]
1
+ class <%= @migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
2
2
  def change
3
3
  <%- if update? -%>
4
4
  reversible do |dir|
@@ -35,6 +35,8 @@ module Logidze
35
35
  class_option :timestamp_column, type: :string, optional: true,
36
36
  desc: "Specify timestamp column"
37
37
 
38
+ class_option :name, type: :string, optional: true, desc: "Migration name"
39
+
38
40
  class_option :update, type: :boolean, optional: true,
39
41
  desc: "Define whether this is an update migration"
40
42
 
@@ -43,7 +45,7 @@ module Logidze
43
45
  warn "Use only one: --only or --except"
44
46
  exit(1)
45
47
  end
46
- migration_template "migration.rb.erb", "db/migrate/#{migration_file_name}"
48
+ migration_template "migration.rb.erb", "db/migrate/#{migration_name}.rb"
47
49
  end
48
50
 
49
51
  def generate_fx_trigger
@@ -62,6 +64,8 @@ module Logidze
62
64
 
63
65
  no_tasks do
64
66
  def migration_name
67
+ return options[:name] if options[:name].present?
68
+
65
69
  if update?
66
70
  "update_logidze_for_#{plural_table_name}"
67
71
  else
@@ -69,8 +73,9 @@ module Logidze
69
73
  end
70
74
  end
71
75
 
72
- def migration_file_name
73
- "#{migration_name}.rb"
76
+ def full_table_name
77
+ config = ActiveRecord::Base
78
+ "#{config.table_name_prefix}#{table_name}#{config.table_name_suffix}"
74
79
  end
75
80
 
76
81
  def limit
@@ -1,4 +1,4 @@
1
- class <%= @migration_class_name %> < ActiveRecord::Migration[5.0]
1
+ class <%= @migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
2
2
  def change
3
3
  <%- unless update? || only_trigger? -%>
4
4
  add_column :<%= table_name %>, :log_data, :jsonb
@@ -12,14 +12,18 @@ class <%= @migration_class_name %> < ActiveRecord::Migration[5.0]
12
12
  dir.up do
13
13
  <%- if update? -%>
14
14
  # Drop legacy trigger if any (<1.0)
15
- execute "DROP TRIGGER IF EXISTS logidze_on_<%= table_name %> on <%= table_name %>;"
15
+ execute <<~SQL
16
+ DROP TRIGGER IF EXISTS "logidze_on_<%= full_table_name %>" on "<%= full_table_name %>";
17
+ SQL
16
18
 
17
19
  <%- end -%>
18
20
  create_trigger :logidze_on_<%= table_name %>, on: :<%= table_name %>
19
21
  end
20
22
 
21
23
  dir.down do
22
- execute "DROP TRIGGER IF EXISTS logidze_on_<%= table_name %> on <%= table_name %>;"
24
+ execute <<~SQL
25
+ DROP TRIGGER IF EXISTS "logidze_on_<%= full_table_name %>" on "<%= full_table_name %>";
26
+ SQL
23
27
  end
24
28
  end
25
29
  <%- end -%>
@@ -27,7 +31,9 @@ class <%= @migration_class_name %> < ActiveRecord::Migration[5.0]
27
31
  reversible do |dir|
28
32
  dir.up do
29
33
  <%- if update? -%>
30
- execute "DROP TRIGGER IF EXISTS logidze_on_<%= table_name %> on <%= table_name %>;"
34
+ execute <<~SQL
35
+ DROP TRIGGER IF EXISTS "logidze_on_<%= full_table_name %>" on "<%= full_table_name %>";
36
+ SQL
31
37
 
32
38
  <%- end -%>
33
39
  execute <<~SQL
@@ -44,7 +50,9 @@ class <%= @migration_class_name %> < ActiveRecord::Migration[5.0]
44
50
  # Uncomment this line if you want to raise an error.
45
51
  # raise ActiveRecord::IrreversibleMigration
46
52
  <%- else -%>
47
- execute "DROP TRIGGER IF EXISTS logidze_on_<%= table_name %> on <%= table_name %>;"
53
+ execute <<~SQL
54
+ DROP TRIGGER IF EXISTS "logidze_on_<%= full_table_name %>" on "<%= full_table_name %>";
55
+ SQL
48
56
  <%- end -%>
49
57
  end
50
58
  end
@@ -54,7 +62,7 @@ class <%= @migration_class_name %> < ActiveRecord::Migration[5.0]
54
62
  reversible do |dir|
55
63
  dir.up do
56
64
  execute <<~SQL
57
- UPDATE <%= table_name %> as t
65
+ UPDATE "<%= full_table_name %>" as t
58
66
  SET log_data = logidze_snapshot(<%= logidze_snapshot_parameters %>);
59
67
  SQL
60
68
  end
@@ -1,5 +1,5 @@
1
- CREATE TRIGGER logidze_on_<%= table_name %>
2
- BEFORE UPDATE OR INSERT ON <%= table_name %> FOR EACH ROW
1
+ CREATE TRIGGER <%= %Q("logidze_on_#{full_table_name}") %>
2
+ BEFORE UPDATE OR INSERT ON <%= %Q("#{full_table_name}") %> FOR EACH ROW
3
3
  WHEN (coalesce(current_setting('logidze.disabled', true), '') <> 'on')
4
4
  -- Parameters: history_size_limit (integer), timestamp_column (text), filtered_columns (text[]),
5
5
  -- include_columns (boolean), debounce_time_ms (integer)
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "logidze"
4
+ require "logidze/utils/check_pending"
4
5
 
5
6
  module Logidze
6
7
  class Engine < Rails::Engine # :nodoc:
@@ -11,5 +12,13 @@ module Logidze
11
12
  ActiveRecord::Base.send :include, Logidze::HasLogidze
12
13
  end
13
14
  end
15
+
16
+ initializer "check Logidze function versions" do |app|
17
+ if config.logidze.on_pending_upgrade != :ignore
18
+ ActiveSupport.on_load(:active_record) do
19
+ app.config.app_middleware.use Logidze::Utils::CheckPending
20
+ end
21
+ end
22
+ end
14
23
  end
15
24
  end
@@ -48,7 +48,7 @@ module Logidze
48
48
  end
49
49
 
50
50
  # Return diff from the initial state to specified time or version.
51
- # Optional `data` paramater can be used as initial diff state.
51
+ # Optional `data` parameter can be used as initial diff state.
52
52
  def changes_to(time: nil, version: nil, data: {}, from: 0)
53
53
  raise ArgumentError, "Time or version must be specified" if time.nil? && version.nil?
54
54
 
data/lib/logidze/model.rb CHANGED
@@ -213,7 +213,7 @@ module Logidze
213
213
 
214
214
  # Loads log_data field from the database, stores to the attributes hash and returns it
215
215
  def reload_log_data
216
- self.log_data = self.class.where(self.class.primary_key => id).pluck("#{self.class.table_name}.log_data").first
216
+ self.log_data = self.class.where(self.class.primary_key => id).pluck("#{self.class.table_name}.log_data".to_sym).first
217
217
  end
218
218
 
219
219
  # Nullify log_data column for a single record
@@ -239,7 +239,7 @@ module Logidze
239
239
  end
240
240
 
241
241
  def apply_column_diff(column, value)
242
- return if deleted_column?(column)
242
+ return if deleted_column?(column) || column == "log_data"
243
243
 
244
244
  write_attribute column, deserialize_value(column, value)
245
245
  end
@@ -253,14 +253,8 @@ module Logidze
253
253
  object_at
254
254
  end
255
255
 
256
- if Rails::VERSION::MAJOR < 5
257
- def deserialize_value(column, value)
258
- @attributes[column].type.type_cast_from_database(value)
259
- end
260
- else
261
- def deserialize_value(column, value)
262
- @attributes[column].type.deserialize(value)
263
- end
256
+ def deserialize_value(column, value)
257
+ @attributes[column].type.deserialize(value)
264
258
  end
265
259
 
266
260
  def deleted_column?(column)
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "./function_definitions"
4
+ require_relative "./pending_migration_error"
5
+
6
+ module Logidze
7
+ module Utils
8
+ # This Rack middleware is used to verify that all functions are up to date
9
+ class CheckPending
10
+ def initialize(app)
11
+ @app = app
12
+ @needs_check = true
13
+ @mutex = Mutex.new
14
+ end
15
+
16
+ delegate :connection, to: ActiveRecord::Base
17
+
18
+ def call(env)
19
+ @mutex.synchronize do
20
+ if @needs_check
21
+ notify_or_raise! if needs_migration?
22
+ end
23
+ @needs_check = false
24
+ end
25
+
26
+ @app.call(env)
27
+ end
28
+
29
+ private
30
+
31
+ def notify_or_raise!
32
+ case Logidze.on_pending_upgrade
33
+ when :warn
34
+ warn "\n**************************************************\n"\
35
+ "⛔️ WARNING: Logidze needs an upgrade and might not work correctly.\n"\
36
+ "Please, make sure to run `bundle exec rails generate logidze:install --update` "\
37
+ "and apply generated migration."\
38
+ "\n**************************************************\n\n"
39
+ when :raise
40
+ raise Logidze::Utils::PendingMigrationError, "Logidze needs upgrade. Run `bundle exec rails generate logidze:install --update` and apply generated migration."
41
+ end
42
+ end
43
+
44
+ def needs_migration?
45
+ (library_function_versions - pg_function_versions).any?
46
+ end
47
+
48
+ def pg_function_versions
49
+ Logidze::Utils::FunctionDefinitions.from_db.map { |func| [func.name, func.version] }
50
+ end
51
+
52
+ def library_function_versions
53
+ @library_function_versions ||= Logidze::Utils::FunctionDefinitions.from_fs.map { |func| [func.name, func.version] }
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Logidze
4
+ module Utils
5
+ class FuncDef < Struct.new(:name, :version, :signature); end
6
+
7
+ module FunctionDefinitions
8
+ class << self
9
+ def from_fs
10
+ function_paths = Dir.glob(File.join(__dir__, "..", "..", "generators", "logidze", "install", "functions", "*.sql"))
11
+ function_paths.map do |path|
12
+ name = path.match(/([^\/]+)\.sql/)[1]
13
+
14
+ file = File.open(path)
15
+ header, version_comment = file.readline, file.readline
16
+
17
+ signature = parse_signature(header)
18
+ version = parse_version(version_comment)
19
+ FuncDef.new(name, version, signature)
20
+ end
21
+ end
22
+
23
+ def from_db
24
+ query = <<~SQL
25
+ SELECT pp.proname, pg_get_functiondef(pp.oid) AS definition
26
+ FROM pg_proc pp
27
+ WHERE pp.proname like 'logidze_%'
28
+ ORDER BY pp.oid;
29
+ SQL
30
+ ActiveRecord::Base.connection.execute(query).map do |row|
31
+ version = parse_version(row["definition"])
32
+ FuncDef.new(row["proname"], version, nil)
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def parse_version(line)
39
+ line.match(/version:\s+(\d+)/)&.[](1).to_i
40
+ end
41
+
42
+ def parse_signature(line)
43
+ parameters = line.match(/CREATE OR REPLACE FUNCTION\s+[\w_]+\((.*)\)/)[1]
44
+ parameters.split(/\s*,\s*/).map { |param| param.split(/\s+/, 2).last.sub(/\s+DEFAULT .*$/, "") }.join(", ")
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module Logidze
6
+ module Utils
7
+ class PendingMigrationError < StandardError
8
+ if Rails::VERSION::MAJOR >= 6
9
+ require "active_record"
10
+ require "active_support/actionable_error"
11
+ include ActiveSupport::ActionableError
12
+
13
+ action "Upgrade Logidze" do
14
+ Rails::Generators.invoke("logidze:install", ["--update"])
15
+ ActiveRecord::Tasks::DatabaseTasks.migrate
16
+ if ActiveRecord::Base.dump_schema_after_migration
17
+ ActiveRecord::Tasks::DatabaseTasks.dump_schema(
18
+ ActiveRecord::Base.connection_db_config
19
+ )
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Logidze
4
- VERSION = "1.0.0.rc1"
4
+ VERSION = "1.2.1"
5
5
  end
data/lib/logidze.rb CHANGED
@@ -26,6 +26,8 @@ module Logidze
26
26
  attr_accessor :ignore_log_data_by_default
27
27
  # Whether #at should return self or nil when log_data is nil
28
28
  attr_accessor :return_self_if_log_data_is_empty
29
+ # Determines what Logidze should do when upgrade is needed (:raise | :warn | :ignore)
30
+ attr_reader :on_pending_upgrade
29
31
 
30
32
  # Temporary disable DB triggers.
31
33
  #
@@ -35,7 +37,7 @@ module Logidze
35
37
  with_logidze_setting("logidze.disabled", "on") { yield }
36
38
  end
37
39
 
38
- # Instructure Logidze to create a full snapshot for the new versions, not a diff
40
+ # Instruct Logidze to create a full snapshot for the new versions, not a diff
39
41
  #
40
42
  # @example
41
43
  # Logidze.with_full_snapshot { post.touch }
@@ -43,6 +45,13 @@ module Logidze
43
45
  with_logidze_setting("logidze.full_snapshot", "on") { yield }
44
46
  end
45
47
 
48
+ def on_pending_upgrade=(mode)
49
+ if %i[raise warn ignore].exclude? mode
50
+ raise ArgumentError, "Unknown on_pending_upgrade option `#{mode.inspect}`. Expecting :raise, :warn or :ignore"
51
+ end
52
+ @on_pending_upgrade = mode
53
+ end
54
+
46
55
  private
47
56
 
48
57
  def with_logidze_setting(name, value)
@@ -59,4 +68,5 @@ module Logidze
59
68
  self.associations_versioning = false
60
69
  self.ignore_log_data_by_default = false
61
70
  self.return_self_if_log_data_is_empty = true
71
+ self.on_pending_upgrade = :ignore
62
72
  end
metadata CHANGED
@@ -1,17 +1,31 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: logidze
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0.rc1
4
+ version: 1.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - palkan
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-09-01 00:00:00.000000000 Z
11
+ date: 2022-01-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: rails
14
+ name: railties
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '5.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '5.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activerecord
15
29
  requirement: !ruby/object:Gem::Requirement
16
30
  requirements:
17
31
  - - ">="
@@ -149,6 +163,7 @@ files:
149
163
  - lib/generators/logidze/fx_helper.rb
150
164
  - lib/generators/logidze/inject_sql.rb
151
165
  - lib/generators/logidze/install/USAGE
166
+ - lib/generators/logidze/install/functions/logidze_capture_exception.sql
152
167
  - lib/generators/logidze/install/functions/logidze_compact_history.sql
153
168
  - lib/generators/logidze/install/functions/logidze_filter_keys.sql
154
169
  - lib/generators/logidze/install/functions/logidze_logger.sql
@@ -172,6 +187,9 @@ files:
172
187
  - lib/logidze/ignore_log_data/cast_attribute_patch.rb
173
188
  - lib/logidze/meta.rb
174
189
  - lib/logidze/model.rb
190
+ - lib/logidze/utils/check_pending.rb
191
+ - lib/logidze/utils/function_definitions.rb
192
+ - lib/logidze/utils/pending_migration_error.rb
175
193
  - lib/logidze/version.rb
176
194
  - lib/logidze/versioned_association.rb
177
195
  homepage: http://github.com/palkan/logidze
@@ -183,7 +201,7 @@ metadata:
183
201
  documentation_uri: http://github.com/palkan/logidze
184
202
  homepage_uri: http://github.com/palkan/logidze
185
203
  source_code_uri: http://github.com/palkan/logidze
186
- post_install_message:
204
+ post_install_message:
187
205
  rdoc_options: []
188
206
  require_paths:
189
207
  - lib
@@ -194,12 +212,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
194
212
  version: 2.5.0
195
213
  required_rubygems_version: !ruby/object:Gem::Requirement
196
214
  requirements:
197
- - - ">"
215
+ - - ">="
198
216
  - !ruby/object:Gem::Version
199
- version: 1.3.1
217
+ version: '0'
200
218
  requirements: []
201
- rubygems_version: 3.0.6
202
- signing_key:
219
+ rubygems_version: 3.2.22
220
+ signing_key:
203
221
  specification_version: 4
204
222
  summary: PostgreSQL JSONB-based model changes tracking
205
223
  test_files: []