chrono_model 2.0.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e9dc931f360178771e212906ec3a02d1ff49ffc2393b457a7f531b101bc1e8ce
4
- data.tar.gz: 7606b75d5eaf453e0d0f90c760607af50bdf862744f7048cc2c8061f41018bc5
3
+ metadata.gz: b8e1a4cdc29f1fa0435f82c9277674a7085e6caf445ea45673995864038d8a1b
4
+ data.tar.gz: 60df7e87f659a392cf22e8a754b608d7d1fc030d4f63a30dde4af1644ab7917d
5
5
  SHA512:
6
- metadata.gz: e64bea647172199e03703f0f4be8538d48276a8cb02fedb2309a68913ed6dad910d01f9ed4498fe345119ff1a12e25a0dda16f2b5976a0cb1b587dc4548eef4a
7
- data.tar.gz: e326f5fce5a0fc94df9e23618840edb6baeaf307e85bbb3449fad7540cd85863be533caca5e30d073fc880a2759ce21ceed74f471dbba4959ead30e9ce43e5b5
6
+ metadata.gz: 22333be09e65a5347f3413976eea41581b76c7ad94dc138924cd7e5e8f6cb36cd572567f6b195c896a14262d96a3623f7b49116ed2856dcf58441ad4f43ceff2
7
+ data.tar.gz: 9d96851d9dc3947e485a1c09c89eb06c0e5b07a0ddf9c43e75bdd1c3bb13ca4df599e7f4e8050268ba30c00de7878d1a6c496db1f70b2e3e9c76f0bb10d3d63c
data/LICENSE CHANGED
@@ -1,8 +1,8 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2012-2022 Marcello Barnaba <m.barnaba@ifad.org>
4
- Copyright (c) 2012-2022 Peter J. Brindisi <p.brindisi@ifad.org>
5
- Copyright (c) 2012-2022 IFAD
3
+ Copyright (c) 2012-2024 Marcello Barnaba <m.barnaba@ifad.org>
4
+ Copyright (c) 2012-2024 Peter J. Brindisi <p.brindisi@ifad.org>
5
+ Copyright (c) 2012-2024 IFAD
6
6
 
7
7
  Permission is hereby granted, free of charge, to any person obtaining a copy
8
8
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -1,7 +1,6 @@
1
1
  # Temporal database system on PostgreSQL using [updatable views][pg-updatable-views], [table inheritance][pg-table-inheritance] and [INSTEAD OF triggers][pg-instead-of-triggers].
2
2
 
3
3
  [![Build Status][build-status-badge]][build-status]
4
- [![Legacy Build Status][legacy-build-status-badge]][build-status]
5
4
  [![Code Climate][code-analysis-badge]][code-analysis]
6
5
  [![Test Coverage][test-coverage-badge]][test-coverage]
7
6
  [![Gem Version][gem-version-badge]][gem-version]
@@ -63,29 +62,24 @@ All timestamps are _forcibly_ stored in as UTC, bypassing the
63
62
 
64
63
  ## Requirements
65
64
 
66
- * Ruby >= 2.2.2
67
- * Active Record >= 5.0. See the [detailed supported versions matrix on Ruby GitHub Actions workflows](https://github.com/ifad/chronomodel/blob/master/.github/workflows)
68
- * PostgreSQL >= 9.4 (legacy support for 9.3)
65
+ * Ruby >= 3.0
66
+ * Active Record >= 7.0. See the [detailed supported versions matrix on Ruby GitHub Actions workflows](https://github.com/ifad/chronomodel/blob/master/.github/workflows)
67
+ * PostgreSQL >= 9.4
69
68
  * The `btree_gist` PostgreSQL extension
70
69
 
71
- With Homebrew:
72
-
73
- brew install postgres
74
-
75
- With apt:
76
-
77
- apt-get install postgresql-11
78
-
79
70
  ## Installation
80
71
 
81
72
  Add this line to your application's Gemfile:
82
73
 
83
- gem 'chrono_model'
74
+ ```ruby
75
+ gem 'chrono_model'
76
+ ```
84
77
 
85
78
  And then execute:
86
79
 
87
- $ bundle
88
-
80
+ ```sh
81
+ $ bundle
82
+ ```
89
83
 
90
84
  ## Configuration
91
85
 
@@ -100,7 +94,7 @@ development:
100
94
  Configure Active Record in your `config/application.rb` to use the `:sql` schema
101
95
  format:
102
96
 
103
- ```rb
97
+ ```ruby
104
98
  config.active_record.schema_format = :sql
105
99
  ```
106
100
 
@@ -122,7 +116,7 @@ view and all the trigger machinery. Every other housekeeping of the temporal
122
116
  structure is handled behind the scenes by the other schema statements. E.g.:
123
117
 
124
118
  * `rename_table` - renames tables, views, sequences, indexes and triggers
125
- * `drop_table` - drops the temporal table and all dependant objects
119
+ * `drop_table` - drops the temporal table and all dependent objects
126
120
  * `add_column` - adds the column to the current table and updates triggers
127
121
  * `rename_column` - renames the current table column and updates the triggers
128
122
  * `remove_column` - removes the current table column and updates the triggers
@@ -193,10 +187,12 @@ occur (see https://github.com/ifad/chronomodel/issues/71).
193
187
  In such cases, ensure to add `no_journal: %w( your_counter_cache_column_name )`
194
188
  to your `create_table`. Example:
195
189
 
196
- create_table 'sections', temporal: true, no_journal: %w( articles_count ) do |t|
197
- t.string :name
198
- t.integer :articles_count, default: 0
199
- end
190
+ ```ruby
191
+ create_table 'sections', temporal: true, no_journal: %w[articles_count] do |t|
192
+ t.string :name
193
+ t.integer :articles_count, default: 0
194
+ end
195
+ ```
200
196
 
201
197
  ## Data querying
202
198
 
@@ -283,12 +279,12 @@ cannot be deleted.
283
279
 
284
280
  ChronoModel currently performs upgrades by dropping and re-creating the views
285
281
  that give access to current data. If you have built other database objects on
286
- these views, the upgrade cannot be performed automatically as the dependant
282
+ these views, the upgrade cannot be performed automatically as the dependent
287
283
  objects must be dropped first.
288
284
 
289
285
  When booting, ChronoModel will issue a warning in your logs about the need of
290
286
  a structure upgrade. Structure usually changes across versions. In this case,
291
- you need to set up a rake task that drops your dependant objects, runs
287
+ you need to set up a rake task that drops your dependent objects, runs
292
288
  ChronoModel.upgrade! and then re-creates them.
293
289
 
294
290
  A migration system should be introduced, but it is seen as overkill for now,
@@ -305,7 +301,9 @@ You need to connect as a database superuser, because specs need to create the
305
301
 
306
302
  To run the full test suite, use
307
303
 
308
- rake
304
+ ```sh
305
+ $ rake
306
+ ```
309
307
 
310
308
  SQL queries are logged to `spec/debug.log`. If you want to see them in your
311
309
  output, set the `VERBOSE=true` environment variable.
@@ -314,36 +312,22 @@ Some tests check the nominal execution of rake tasks within a test Rails app,
314
312
  and those are quite time consuming. You can run the full ChronoModel tests
315
313
  only against ActiveRecord by using
316
314
 
317
- rspec spec/chrono_model
315
+ ```sh
316
+ $ rspec spec/chrono_model
317
+ ```
318
318
 
319
319
  Ensure to run the full test suite before pushing.
320
320
 
321
- ## Usage with JSON (*not* JSONB) columns
322
-
323
- **DEPRECATED**: Please migrate to JSONB. It has an equality operator built-in,
324
- it's faster and stricter, and offers many more indexing abilities and better
325
- performance than JSON. It is going to be desupported soon because PostgreSQL 10
326
- does not support these anymore.
327
-
328
- The [JSON][pg-json-type] does not provide an [equality operator][pg-json-func].
329
- As both unnecessary update suppression and selective journaling require
330
- comparing the OLD and NEW rows fields, this fails by default.
331
-
332
- ChronoModel provides a naive and heavyweight JSON equality operator using
333
- [pl/python][pg-json-opclass] and associated Postgres objects.
334
-
335
- To set up you can use
336
-
337
- ```ruby
338
- require 'chrono_model/json'
339
- ChronoModel::Json.create
340
- ```
341
-
342
321
  ## Caveats
343
322
 
344
- * Rails 4+ support requires disabling tsrange parsing support, as it
345
- [is broken][r4-tsrange-broken] and [incomplete][r4-tsrange-incomplete]
346
- as of now, mainly due to a [design clash with ruby][pg-tsrange-and-ruby].
323
+ * Considering the nature of modern applications, it's crucial to understand
324
+ that the database time does not necessarily align with the application time
325
+ due to the delay introduced by communication between the application and
326
+ the database server. Consequently, there is no assurance that the application
327
+ time will always be less than the database time. Therefore, relying solely
328
+ on `created_at` and `updated_at` fields as timestamps to determine the state
329
+ of an object at a specific point in time within the application could
330
+ lead to inaccuracies.
347
331
 
348
332
  * The triggers and temporal indexes cannot be saved in schema.rb. The AR
349
333
  schema dumper is quite basic, and it isn't (currently) extensible.
@@ -364,13 +348,16 @@ ChronoModel::Json.create
364
348
  * Foreign keys are not supported. [See issue #174][gh-issue-174]
365
349
 
366
350
  * There may be unexpected results when combining eager loading and joins.
367
- [See issue #186][gh-issue-186]
351
+ [See issue #186][gh-issue-186]
368
352
 
369
353
  * Global ID ignores historical objects. [See issue #192][gh-issue-192]
370
354
 
371
355
  * Different historical objects are considered the identical. [See issue
372
356
  #206][gh-issue-206]
373
357
 
358
+ * Use with caution when implementing inline editing features, as Chronomodel
359
+ creates a new record for each modification. This will lead to increased
360
+ storage requirements and bloated history
374
361
 
375
362
  ## Contributing
376
363
 
@@ -402,7 +389,6 @@ This software is Made in Italy :it: :smile:.
402
389
  [docs-analysis-badge]: https://inch-ci.org/github/ifad/chronomodel.svg?branch=master
403
390
  [gem-version]: https://rubygems.org/gems/chrono_model
404
391
  [gem-version-badge]: https://badge.fury.io/rb/chrono_model.svg
405
- [legacy-build-status-badge]: https://github.com/ifad/chronomodel/actions/workflows/legacy_ruby.yml/badge.svg
406
392
  [test-coverage]: https://codeclimate.com/github/ifad/chronomodel
407
393
  [test-coverage-badge]: https://codeclimate.com/github/ifad/chronomodel/badges/coverage.svg
408
394
 
@@ -423,7 +409,6 @@ This software is Made in Italy :it: :smile:.
423
409
  [pg-exclusion-constraints]: https://www.postgresql.org/docs/9.4/sql-createtable.html#SQL-CREATETABLE-EXCLUDE
424
410
  [pg-btree-gist]: https://www.postgresql.org/docs/9.4/btree-gist.html
425
411
  [pg-comment]: https://www.postgresql.org/docs/9.4/sql-comment.html
426
- [pg-tsrange-and-ruby]: https://bugs.ruby-lang.org/issues/6864
427
412
  [pg-ctes]: https://www.postgresql.org/docs/9.4/queries-with.html
428
413
  [pg-cte-optimization-fence]: https://www.postgresql.org/message-id/201209191305.44674.db@kavod.com
429
414
  [pg-cte-opt-out-fence]: https://www.postgresql.org/message-id/CAHyXU0zpM5+Dsb_pKxDmm-ZoWUAt=SkHHaiK_DBqcmtxTas6Nw@mail.gmail.com
@@ -431,9 +416,6 @@ This software is Made in Italy :it: :smile:.
431
416
  [pg-json-func]: https://www.postgresql.org/docs/9.4/functions-json.html
432
417
  [pg-json-opclass]: https://github.com/ifad/chronomodel/blob/master/sql/json_ops.sql
433
418
 
434
- [r4-tsrange-broken]: https://github.com/rails/rails/pull/13793#issuecomment-34608093
435
- [r4-tsrange-incomplete]: https://github.com/rails/rails/issues/14010
436
-
437
419
  [cm-readme-sql]: https://github.com/ifad/chronomodel/blob/master/README.sql
438
420
  [cm-timemachine]: https://github.com/ifad/chronomodel/blob/master/lib/chrono_model/time_machine.rb
439
421
  [cm-cte-impl]: https://github.com/ifad/chronomodel/commit/18f4c4b
@@ -16,9 +16,7 @@ module ActiveRecord
16
16
  def chronomodel_connection(config) # :nodoc:
17
17
  return chronomodel_adapter_class.new(config) if ActiveRecord::VERSION::STRING >= '7.1'
18
18
 
19
- conn_params = config.symbolize_keys
20
-
21
- conn_params.delete_if { |_, v| v.nil? }
19
+ conn_params = config.symbolize_keys.compact
22
20
 
23
21
  # Map ActiveRecords param names to PGs.
24
22
  conn_params[:user] = conn_params.delete(:username) if conn_params[:username]
@@ -28,7 +26,7 @@ module ActiveRecord
28
26
  valid_conn_param_keys = PG::Connection.conndefaults_hash.keys + [:requiressl]
29
27
  conn_params.slice!(*valid_conn_param_keys)
30
28
 
31
- conn = PG.connect(conn_params) if ActiveRecord::VERSION::MAJOR >= 6
29
+ conn = PG.connect(conn_params)
32
30
 
33
31
  adapter = ChronoModel::Adapter.new(conn, logger, conn_params, config)
34
32
 
@@ -40,10 +38,6 @@ module ActiveRecord
40
38
  adapter.chrono_setup!
41
39
 
42
40
  adapter
43
- rescue ::PG::Error => e
44
- raise ActiveRecord::NoDatabaseError if e.message.include?(conn_params[:dbname])
45
-
46
- raise
47
41
  end
48
42
  end
49
43
  end
@@ -24,8 +24,6 @@ module ActiveRecord
24
24
  filename = arguments.first
25
25
  sql = File.read(filename).gsub(/CREATE SCHEMA (?!IF NOT EXISTS)/, '\&IF NOT EXISTS ')
26
26
  File.open(filename, 'w') { |file| file << sql }
27
-
28
- remove_sql_header_comments(filename) if ActiveRecord::VERSION::STRING < '5.1'
29
27
  end
30
28
 
31
29
  def data_dump(target)
@@ -48,15 +46,8 @@ module ActiveRecord
48
46
 
49
47
  private
50
48
 
51
- # In Rails 6.1.x the configuration instance variable is not available
52
- # and it's been replaced by @configuration_hash (which is frozen).
53
49
  def chronomodel_configuration
54
- @chronomodel_configuration ||=
55
- if defined?(@configuration_hash)
56
- @configuration_hash
57
- else
58
- configuration.with_indifferent_access
59
- end
50
+ @chronomodel_configuration ||= @configuration_hash
60
51
  end
61
52
 
62
53
  # If a schema search path is defined in the configuration file, it will
@@ -77,46 +68,15 @@ module ActiveRecord
77
68
 
78
69
  chronomodel_schema_search_path = "#{schema_search_path},#{CHRONOMODEL_SCHEMAS.join(',')}"
79
70
 
80
- if defined?(@configuration_hash)
81
- @configuration_hash = @configuration_hash.dup
82
- @configuration_hash[:schema_search_path] = chronomodel_schema_search_path
83
- @configuration_hash.freeze
84
- else
85
- configuration['schema_search_path'] = chronomodel_schema_search_path
86
- end
71
+ @configuration_hash = @configuration_hash.dup
72
+ @configuration_hash[:schema_search_path] = chronomodel_schema_search_path
73
+ @configuration_hash.freeze
87
74
  end
88
75
 
89
76
  def reset_configuration!
90
- if defined?(@configuration_hash)
91
- @configuration_hash = @configuration_hash.dup
92
- @configuration_hash[:schema_search_path] = @original_schema_search_path
93
- @configuration_hash.freeze
94
- else
95
- configuration['schema_search_path'] = @original_schema_search_path
96
- end
97
- end
98
-
99
- unless private_instance_methods.include?(:remove_sql_header_comments)
100
- def remove_sql_header_comments(filename)
101
- sql_comment_begin = '--'
102
- removing_comments = true
103
- tempfile = Tempfile.open('uncommented_structure.sql')
104
- begin
105
- File.foreach(filename) do |line|
106
- unless removing_comments && (line.start_with?(sql_comment_begin) || line.blank?)
107
- tempfile << line
108
- removing_comments = false
109
- end
110
- end
111
- ensure
112
- tempfile.close
113
- end
114
- FileUtils.mv(tempfile.path, filename)
115
- end
116
- end
117
-
118
- unless private_instance_methods.include?(:psql_env)
119
- alias psql_env set_psql_env
77
+ @configuration_hash = @configuration_hash.dup
78
+ @configuration_hash[:schema_search_path] = @original_schema_search_path
79
+ @configuration_hash.freeze
120
80
  end
121
81
 
122
82
  def schema_search_path
@@ -82,7 +82,7 @@ module ChronoModel
82
82
  # allow setting the PK to a specific value (think migration scenario).
83
83
  #
84
84
  def chrono_create_INSERT_trigger(table, pk, current, history, fields, values)
85
- execute <<-SQL.strip_heredoc # rubocop:disable Rails/SquishedSQLHeredocs
85
+ execute <<-SQL.strip_heredoc # rubocop:disable Rails/SquishedSQLHeredocs,Rails/StripHeredoc
86
86
  CREATE OR REPLACE FUNCTION chronomodel_#{table}_insert() RETURNS TRIGGER AS $$
87
87
  BEGIN
88
88
  #{insert_sequence_sql(pk, current)} INTO #{current} ( #{pk}, #{fields} )
@@ -135,7 +135,7 @@ module ChronoModel
135
135
 
136
136
  journal &= columns
137
137
 
138
- execute <<-SQL.strip_heredoc # rubocop:disable Rails/SquishedSQLHeredocs
138
+ execute <<-SQL.strip_heredoc # rubocop:disable Rails/SquishedSQLHeredocs,Rails/StripHeredoc
139
139
  CREATE OR REPLACE FUNCTION chronomodel_#{table}_update() RETURNS TRIGGER AS $$
140
140
  DECLARE _now timestamp;
141
141
  DECLARE _hid integer;
@@ -189,7 +189,7 @@ module ChronoModel
189
189
  # DELETEd in the same transaction.
190
190
  #
191
191
  def chrono_create_DELETE_trigger(table, pk, current, history)
192
- execute <<-SQL.strip_heredoc # rubocop:disable Rails/SquishedSQLHeredocs
192
+ execute <<-SQL.strip_heredoc # rubocop:disable Rails/SquishedSQLHeredocs,Rails/StripHeredoc
193
193
  CREATE OR REPLACE FUNCTION chronomodel_#{table}_delete() RETURNS TRIGGER AS $$
194
194
  DECLARE _now timestamp;
195
195
  BEGIN
@@ -42,7 +42,7 @@ module ChronoModel
42
42
  return if upgrade.empty?
43
43
 
44
44
  logger.warn 'ChronoModel: There are tables needing a structure upgrade, and ChronoModel structures need to be recreated.'
45
- logger.warn 'ChronoModel: Please run ChronoModel.upgrade! to attempt the upgrade. If you have dependant database objects'
45
+ logger.warn 'ChronoModel: Please run ChronoModel.upgrade! to attempt the upgrade. If you have dependent database objects'
46
46
  logger.warn 'ChronoModel: the upgrade will fail and you have to drop the dependent objects, run .upgrade! and create them'
47
47
  logger.warn 'ChronoModel: again. Sorry. Some features or the whole library may not work correctly until upgrade is complete.'
48
48
  logger.warn "ChronoModel: Tables pending upgrade: #{upgrade}"
@@ -3,16 +3,10 @@
3
3
  require 'active_record/connection_adapters/postgresql_adapter'
4
4
 
5
5
  require 'chrono_model/adapter/migrations'
6
-
7
- if ActiveRecord::VERSION::STRING >= '6.1'
8
- require 'chrono_model/adapter/migrations_modules/stable'
9
- else
10
- require 'chrono_model/adapter/migrations_modules/legacy'
11
- end
6
+ require 'chrono_model/adapter/migrations_modules/stable'
12
7
 
13
8
  require 'chrono_model/adapter/ddl'
14
9
  require 'chrono_model/adapter/indexes'
15
- require 'chrono_model/adapter/tsrange'
16
10
  require 'chrono_model/adapter/upgrade'
17
11
 
18
12
  module ChronoModel
@@ -24,7 +18,6 @@ module ChronoModel
24
18
  include ChronoModel::Adapter::Migrations
25
19
  include ChronoModel::Adapter::DDL
26
20
  include ChronoModel::Adapter::Indexes
27
- include ChronoModel::Adapter::TSRange
28
21
  include ChronoModel::Adapter::Upgrade
29
22
 
30
23
  # The schema holding current data
@@ -4,23 +4,10 @@ module ChronoModel
4
4
  module Conversions
5
5
  module_function
6
6
 
7
- ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(?:\.(\d+))?\z/.freeze
8
-
9
- # rubocop:disable Style/PerlBackrefs
10
- def string_to_utc_time(string)
11
- return string if string.is_a?(Time)
12
-
13
- return unless string =~ ISO_DATETIME
14
-
15
- # .1 is .100000, not .000001
16
- usec = $7.ljust(6, '0') unless $7.nil?
17
-
18
- Time.utc $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, usec.to_i
19
- end
20
- # rubocop:enable Style/PerlBackrefs
7
+ ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(?:\.(\d+))?\z/
21
8
 
22
9
  def time_to_utc_string(time)
23
- time.to_formatted_s(:db) << '.' << format('%06d', time.usec)
10
+ time.to_fs(:db) << '.' << format('%06d', time.usec)
24
11
  end
25
12
  end
26
13
  end
@@ -2,8 +2,4 @@
2
2
 
3
3
  require 'chrono_model/patches/db_console'
4
4
 
5
- if Rails.version < '6.1'
6
- Rails::DBConsole.prepend ChronoModel::Patches::DBConsole::Config
7
- else
8
- Rails::DBConsole.prepend ChronoModel::Patches::DBConsole::DbConfig
9
- end
5
+ Rails::DBConsole.prepend ChronoModel::Patches::DBConsole::DbConfig
@@ -4,16 +4,6 @@ module ChronoModel
4
4
  module Patches
5
5
  # This class is a dummy relation whose scope is only to pass around the
6
6
  # as_of_time parameters across ActiveRecord call chains.
7
- #
8
- # With AR 5.2 a simple relation can be used, as the only required argument
9
- # is the model. 5.0 and 5.1 require more arguments, that are passed here.
10
- #
11
- class AsOfTimeRelation < ActiveRecord::Relation
12
- if ActiveRecord::VERSION::STRING.to_f < 5.2
13
- def initialize(klass, table: klass.arel_table, predicate_builder: klass.predicate_builder, values: {})
14
- super(klass, table, predicate_builder, values)
15
- end
16
- end
17
- end
7
+ class AsOfTimeRelation < ActiveRecord::Relation; end
18
8
  end
19
9
  end
@@ -3,14 +3,10 @@
3
3
  module ChronoModel
4
4
  module Patches
5
5
  module Batches
6
- module BatchEnumerator
7
- def each(&block)
8
- if @relation.try(:history?)
9
- @relation.with_hid_pkey { super }
10
- else
11
- super
12
- end
13
- end
6
+ def in_batches(**)
7
+ return super unless try(:history?)
8
+
9
+ with_hid_pkey { super }
14
10
  end
15
11
  end
16
12
  end
@@ -5,17 +5,15 @@ module ChronoModel
5
5
  module Relation
6
6
  include ChronoModel::Patches::AsOfTimeHolder
7
7
 
8
- if ActiveRecord::Associations::Preloader.instance_methods.include?(:call)
9
- def preload_associations(records) # :nodoc:
10
- preload = preload_values
11
- preload += includes_values unless eager_loading?
12
- scope = StrictLoadingScope if strict_loading_value
13
-
14
- preload.each do |associations|
15
- ActiveRecord::Associations::Preloader.new(
16
- records: records, associations: associations, scope: scope, model: model, as_of_time: as_of_time
17
- ).call
18
- end
8
+ def preload_associations(records) # :nodoc:
9
+ preload = preload_values
10
+ preload += includes_values unless eager_loading?
11
+ scope = StrictLoadingScope if strict_loading_value
12
+
13
+ preload.each do |associations|
14
+ ActiveRecord::Associations::Preloader.new(
15
+ records: records, associations: associations, scope: scope, model: model, as_of_time: as_of_time
16
+ ).call
19
17
  end
20
18
  end
21
19
 
@@ -7,30 +7,11 @@ module ChronoModel
7
7
  TASKS_CLASS = ActiveRecord::Tasks::ChronomodelDatabaseTasks
8
8
 
9
9
  # Register our database tasks under our adapter name
10
- if Rails.version < '5.2'
11
- ActiveRecord::Tasks::DatabaseTasks.register_task(/chronomodel/, TASKS_CLASS)
12
- else
13
- ActiveRecord::Tasks::DatabaseTasks.register_task(/chronomodel/, TASKS_CLASS.to_s)
14
- end
10
+ ActiveRecord::Tasks::DatabaseTasks.register_task(/chronomodel/, TASKS_CLASS.to_s)
15
11
 
16
12
  rake_tasks do
17
13
  def task_config
18
- if Rails.version < '6.1'
19
- ActiveRecord::Tasks::DatabaseTasks.current_config.with_indifferent_access
20
- else
21
- ActiveRecord::Base.connection_db_config
22
- end
23
- end
24
-
25
- if Rails.version < '6.1'
26
- # Make schema:dump and schema:load invoke structure:dump and structure:load
27
- Rake::Task['db:schema:dump'].clear.enhance(['environment']) do
28
- Rake::Task['db:structure:dump'].invoke
29
- end
30
-
31
- Rake::Task['db:schema:load'].clear.enhance(['environment']) do
32
- Rake::Task['db:structure:load'].invoke
33
- end
14
+ ActiveRecord::Base.connection_db_config
34
15
  end
35
16
 
36
17
  desc 'Dumps database into db/data.NOW.sql or file specified via DUMP='
@@ -107,7 +107,7 @@ module ChronoModel
107
107
  # name has the "::History" suffix but that is never going to be
108
108
  # present in the data.
109
109
  #
110
- # As such it is overriden here to return the same contents that
110
+ # As such it is overridden here to return the same contents that
111
111
  # the parent would have returned.
112
112
  delegate :sti_name, to: :superclass
113
113
 
@@ -132,7 +132,7 @@ module ChronoModel
132
132
  end
133
133
 
134
134
  # The history id is `hid`, but this cannot set as primary key
135
- # or temporal assocations will break. Solutions are welcome.
135
+ # or temporal associations will break. Solutions are welcome.
136
136
  def id
137
137
  hid
138
138
  end
@@ -204,24 +204,24 @@ module ChronoModel
204
204
  self.class.superclass.find(rid)
205
205
  end
206
206
 
207
- def record # :nodoc:
208
- ActiveSupport::Deprecation.warn '.record is deprecated in favour of .current_version'
209
- current_version
210
- end
211
-
207
+ # Return `nil` instead of -Infinity/Infinity to preserve current
208
+ # Chronomodel behaviour and avoid failures with Rails 7.0 and
209
+ # unbounded time ranges
210
+ #
211
+ # Check if `begin` and `end` are `Time` because validity is a `tsrange`
212
+ # column, so it is either `Time`, `nil`, and in some cases Infinity.
213
+ #
214
+ # Ref: rails/rails#45099
215
+ # TODO: consider removing when Rails 7.0 support will be dropped
212
216
  def valid_from
213
- validity.first
217
+ validity.begin if validity.begin.is_a?(Time)
214
218
  end
215
219
 
216
220
  def valid_to
217
- validity.last
221
+ validity.end if validity.end.is_a?(Time)
218
222
  end
219
223
  alias as_of_time valid_to
220
224
 
221
- def recorded_at
222
- ChronoModel::Conversions.string_to_utc_time attributes_before_type_cast['recorded_at']
223
- end
224
-
225
225
  # Starting from Rails 6.0, `.read_attribute` will use the memoized
226
226
  # `primary_key` if it detects that the attribute name is `id`.
227
227
  #
@@ -46,7 +46,8 @@ module ChronoModel
46
46
  end
47
47
 
48
48
  def time_for_time_query(t, column)
49
- if t == :now || t == :today
49
+ case t
50
+ when :now, :today
50
51
  now_for_column(column)
51
52
  else
52
53
  quoted_t = connection.quote(connection.quoted_date(t))
@@ -59,7 +59,7 @@ module ChronoModel
59
59
  relation = relation.from("public.#{quoted_table_name}") unless chrono?
60
60
  relation = relation.where(id: rid) if rid
61
61
 
62
- sql = "SELECT ts FROM ( #{relation.to_sql} ) AS foo WHERE ts IS NOT NULL".dup
62
+ sql = +"SELECT ts FROM ( #{relation.to_sql} ) AS foo WHERE ts IS NOT NULL"
63
63
 
64
64
  if options.key?(:before)
65
65
  sql << " AND ts < '#{Conversions.time_to_utc_string(options[:before])}'"
@@ -81,9 +81,7 @@ module ChronoModel
81
81
  sql << " LIMIT #{options[:limit].to_i}" if options.key?(:limit)
82
82
 
83
83
  connection.on_schema(Adapter::HISTORY_SCHEMA) do
84
- connection.select_values(sql, "#{name} periods").map! do |ts|
85
- Conversions.string_to_utc_time ts
86
- end
84
+ connection.select_values(sql, "#{name} periods")
87
85
  end
88
86
  end
89
87
 
@@ -22,37 +22,28 @@ module ChronoModel
22
22
  ChronoModel.history_models[table_name] = history
23
23
 
24
24
  class << self
25
- if Rails.version >= '7.0'
26
- def subclasses(with_history: false)
27
- subclasses = super()
28
- subclasses.reject!(&:history?) unless with_history
29
- subclasses
30
- end
31
-
32
- def subclasses_with_history
33
- subclasses(with_history: true)
34
- end
25
+ def subclasses(with_history: false)
26
+ subclasses = super()
27
+ subclasses.reject!(&:history?) unless with_history
28
+ subclasses
29
+ end
35
30
 
36
- # `direct_descendants` is deprecated method in 7.0 and has been
37
- # removed in 7.1
38
- if method_defined?(:direct_descendants)
39
- alias_method :direct_descendants_with_history, :subclasses_with_history
40
- alias_method :direct_descendants, :subclasses
41
- end
31
+ def subclasses_with_history
32
+ subclasses(with_history: true)
33
+ end
42
34
 
43
- # Ruby 3.1 has a native subclasses method and descendants is
44
- # implemented with recursion of subclasses
45
- if Class.method_defined?(:subclasses)
46
- def descendants_with_history
47
- subclasses_with_history.concat(subclasses.flat_map(&:descendants_with_history))
48
- end
49
- end
50
- else
51
- alias_method :descendants_with_history, :descendants
35
+ # `direct_descendants` is deprecated method in 7.0 and has been
36
+ # removed in 7.1
37
+ if method_defined?(:direct_descendants)
38
+ alias_method :direct_descendants_with_history, :subclasses_with_history
39
+ alias_method :direct_descendants, :subclasses
40
+ end
52
41
 
53
- alias_method :direct_descendants_with_history, :direct_descendants
54
- def direct_descendants
55
- direct_descendants_with_history.reject(&:history?)
42
+ # Ruby 3.1 has a native subclasses method and descendants is
43
+ # implemented with recursion of subclasses
44
+ if Class.method_defined?(:subclasses)
45
+ def descendants_with_history
46
+ subclasses_with_history.concat(subclasses.flat_map(&:descendants_with_history))
56
47
  end
57
48
  end
58
49
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ChronoModel
4
- VERSION = '2.0.0'
4
+ VERSION = '4.0.0'
5
5
  end
data/lib/chrono_model.rb CHANGED
@@ -33,7 +33,7 @@ module ChronoModel
33
33
  # Computed upon inclusion of the +TimeMachine+ module.
34
34
  #
35
35
  def self.history_models
36
- @_history_models ||= {}
36
+ @history_models ||= {}
37
37
  end
38
38
  end
39
39
 
@@ -56,7 +56,7 @@ ActiveSupport.on_load :active_record do
56
56
 
57
57
  ActiveRecord::Associations::Preloader::ThroughAssociation.prepend ChronoModel::Patches::Preloader::ThroughAssociation
58
58
 
59
- ActiveRecord::Batches::BatchEnumerator.prepend ChronoModel::Patches::Batches::BatchEnumerator
59
+ ActiveRecord::Batches.prepend ChronoModel::Patches::Batches
60
60
  end
61
61
 
62
62
  ActiveSupport.on_load :after_initialize do
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: chrono_model
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 4.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Marcello Barnaba
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2024-01-09 00:00:00.000000000 Z
12
+ date: 2024-05-12 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activerecord
@@ -17,14 +17,14 @@ dependencies:
17
17
  requirements:
18
18
  - - ">="
19
19
  - !ruby/object:Gem::Version
20
- version: '5.0'
20
+ version: '7.0'
21
21
  type: :runtime
22
22
  prerelease: false
23
23
  version_requirements: !ruby/object:Gem::Requirement
24
24
  requirements:
25
25
  - - ">="
26
26
  - !ruby/object:Gem::Version
27
- version: '5.0'
27
+ version: '7.0'
28
28
  - !ruby/object:Gem::Dependency
29
29
  name: multi_json
30
30
  requirement: !ruby/object:Gem::Requirement
@@ -41,152 +41,12 @@ dependencies:
41
41
  version: '0'
42
42
  - !ruby/object:Gem::Dependency
43
43
  name: pg
44
- requirement: !ruby/object:Gem::Requirement
45
- requirements:
46
- - - ">"
47
- - !ruby/object:Gem::Version
48
- version: '1.1'
49
- type: :runtime
50
- prerelease: false
51
- version_requirements: !ruby/object:Gem::Requirement
52
- requirements:
53
- - - ">"
54
- - !ruby/object:Gem::Version
55
- version: '1.1'
56
- - !ruby/object:Gem::Dependency
57
- name: aruba
58
- requirement: !ruby/object:Gem::Requirement
59
- requirements:
60
- - - ">="
61
- - !ruby/object:Gem::Version
62
- version: '0'
63
- type: :development
64
- prerelease: false
65
- version_requirements: !ruby/object:Gem::Requirement
66
- requirements:
67
- - - ">="
68
- - !ruby/object:Gem::Version
69
- version: '0'
70
- - !ruby/object:Gem::Dependency
71
- name: bundler
72
- requirement: !ruby/object:Gem::Requirement
73
- requirements:
74
- - - ">="
75
- - !ruby/object:Gem::Version
76
- version: '0'
77
- type: :development
78
- prerelease: false
79
- version_requirements: !ruby/object:Gem::Requirement
80
- requirements:
81
- - - ">="
82
- - !ruby/object:Gem::Version
83
- version: '0'
84
- - !ruby/object:Gem::Dependency
85
- name: byebug
86
- requirement: !ruby/object:Gem::Requirement
87
- requirements:
88
- - - ">="
89
- - !ruby/object:Gem::Version
90
- version: '0'
91
- type: :development
92
- prerelease: false
93
- version_requirements: !ruby/object:Gem::Requirement
94
- requirements:
95
- - - ">="
96
- - !ruby/object:Gem::Version
97
- version: '0'
98
- - !ruby/object:Gem::Dependency
99
- name: fuubar
100
- requirement: !ruby/object:Gem::Requirement
101
- requirements:
102
- - - ">="
103
- - !ruby/object:Gem::Version
104
- version: '0'
105
- type: :development
106
- prerelease: false
107
- version_requirements: !ruby/object:Gem::Requirement
108
- requirements:
109
- - - ">="
110
- - !ruby/object:Gem::Version
111
- version: '0'
112
- - !ruby/object:Gem::Dependency
113
- name: hirb
114
- requirement: !ruby/object:Gem::Requirement
115
- requirements:
116
- - - ">="
117
- - !ruby/object:Gem::Version
118
- version: '0'
119
- type: :development
120
- prerelease: false
121
- version_requirements: !ruby/object:Gem::Requirement
122
- requirements:
123
- - - ">="
124
- - !ruby/object:Gem::Version
125
- version: '0'
126
- - !ruby/object:Gem::Dependency
127
- name: pry
128
- requirement: !ruby/object:Gem::Requirement
129
- requirements:
130
- - - ">="
131
- - !ruby/object:Gem::Version
132
- version: '0'
133
- type: :development
134
- prerelease: false
135
- version_requirements: !ruby/object:Gem::Requirement
136
- requirements:
137
- - - ">="
138
- - !ruby/object:Gem::Version
139
- version: '0'
140
- - !ruby/object:Gem::Dependency
141
- name: rails
142
44
  requirement: !ruby/object:Gem::Requirement
143
45
  requirements:
144
46
  - - ">="
145
47
  - !ruby/object:Gem::Version
146
48
  version: '0'
147
- type: :development
148
- prerelease: false
149
- version_requirements: !ruby/object:Gem::Requirement
150
- requirements:
151
- - - ">="
152
- - !ruby/object:Gem::Version
153
- version: '0'
154
- - !ruby/object:Gem::Dependency
155
- name: rake
156
- requirement: !ruby/object:Gem::Requirement
157
- requirements:
158
- - - ">="
159
- - !ruby/object:Gem::Version
160
- version: '0'
161
- type: :development
162
- prerelease: false
163
- version_requirements: !ruby/object:Gem::Requirement
164
- requirements:
165
- - - ">="
166
- - !ruby/object:Gem::Version
167
- version: '0'
168
- - !ruby/object:Gem::Dependency
169
- name: rspec
170
- requirement: !ruby/object:Gem::Requirement
171
- requirements:
172
- - - ">="
173
- - !ruby/object:Gem::Version
174
- version: '0'
175
- type: :development
176
- prerelease: false
177
- version_requirements: !ruby/object:Gem::Requirement
178
- requirements:
179
- - - ">="
180
- - !ruby/object:Gem::Version
181
- version: '0'
182
- - !ruby/object:Gem::Dependency
183
- name: simplecov
184
- requirement: !ruby/object:Gem::Requirement
185
- requirements:
186
- - - ">="
187
- - !ruby/object:Gem::Version
188
- version: '0'
189
- type: :development
49
+ type: :runtime
190
50
  prerelease: false
191
51
  version_requirements: !ruby/object:Gem::Requirement
192
52
  requirements:
@@ -211,14 +71,11 @@ files:
211
71
  - lib/chrono_model/adapter/ddl.rb
212
72
  - lib/chrono_model/adapter/indexes.rb
213
73
  - lib/chrono_model/adapter/migrations.rb
214
- - lib/chrono_model/adapter/migrations_modules/legacy.rb
215
74
  - lib/chrono_model/adapter/migrations_modules/stable.rb
216
- - lib/chrono_model/adapter/tsrange.rb
217
75
  - lib/chrono_model/adapter/upgrade.rb
218
76
  - lib/chrono_model/chrono.rb
219
77
  - lib/chrono_model/conversions.rb
220
78
  - lib/chrono_model/db_console.rb
221
- - lib/chrono_model/json.rb
222
79
  - lib/chrono_model/patches.rb
223
80
  - lib/chrono_model/patches/as_of_time_holder.rb
224
81
  - lib/chrono_model/patches/as_of_time_relation.rb
@@ -252,14 +109,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
252
109
  requirements:
253
110
  - - ">="
254
111
  - !ruby/object:Gem::Version
255
- version: 2.2.2
112
+ version: '3.0'
256
113
  required_rubygems_version: !ruby/object:Gem::Requirement
257
114
  requirements:
258
115
  - - ">="
259
116
  - !ruby/object:Gem::Version
260
117
  version: '0'
261
118
  requirements: []
262
- rubygems_version: 3.5.3
119
+ rubygems_version: 3.5.9
263
120
  signing_key:
264
121
  specification_version: 4
265
122
  summary: Temporal extensions (SCD Type II) for Active Record
@@ -1,41 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ChronoModel
4
- class Adapter
5
- module MigrationsModules
6
- module Legacy
7
- # If adding an index to a temporal table, add it to the one in the
8
- # temporal schema and to the history one. If the `:unique` option is
9
- # present, it is removed from the index created in the history table.
10
- #
11
- def add_index(table_name, column_name, options = {})
12
- return super unless is_chrono?(table_name)
13
-
14
- transaction do
15
- on_temporal_schema { super }
16
-
17
- # Uniqueness constraints do not make sense in the history table
18
- options = options.dup.tap { |o| o.delete(:unique) } if options[:unique].present?
19
-
20
- on_history_schema { super(table_name, column_name, options) }
21
- end
22
- end
23
-
24
- # If removing an index from a temporal table, remove it both from the
25
- # temporal and the history schemas.
26
- #
27
- def remove_index(table_name, options = {})
28
- return super unless is_chrono?(table_name)
29
-
30
- transaction do
31
- on_temporal_schema { super }
32
-
33
- on_history_schema { super }
34
- end
35
- end
36
- end
37
- end
38
- end
39
- end
40
-
41
- ChronoModel::Adapter::Migrations.include ChronoModel::Adapter::MigrationsModules::Legacy
@@ -1,72 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ChronoModel
4
- class Adapter < ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
5
- module TSRange
6
- # HACK: Redefine tsrange parsing support, as it is broken currently.
7
- #
8
- # This self-made API is here because currently AR4 does not support
9
- # open-ended ranges. The reasons are poor support in Ruby:
10
- #
11
- # https://bugs.ruby-lang.org/issues/6864
12
- #
13
- # and an instable interface in Active Record:
14
- #
15
- # https://github.com/rails/rails/issues/13793
16
- # https://github.com/rails/rails/issues/14010
17
- #
18
- # so, for now, we are implementing our own.
19
- #
20
- class Type < ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Range
21
- OID = 3908
22
-
23
- def cast_value(value)
24
- return if value == 'empty'
25
- return value if value.is_a?(::Array)
26
-
27
- extracted = extract_bounds(value)
28
-
29
- from = Conversions.string_to_utc_time extracted[:from]
30
- to = Conversions.string_to_utc_time extracted[:to]
31
-
32
- [from, to]
33
- end
34
-
35
- def extract_bounds(value)
36
- from, to = value[1..-2].split(',')
37
-
38
- from_bound =
39
- if value[1] == ',' || from == '-infinity'
40
- nil
41
- else
42
- from[1..-2]
43
- end
44
-
45
- to_bound =
46
- if value[-2] == ',' || to == 'infinity'
47
- nil
48
- else
49
- to[1..-2]
50
- end
51
-
52
- {
53
- from: from_bound,
54
- to: to_bound
55
- }
56
- end
57
- end
58
-
59
- def initialize_type_map(m = type_map)
60
- super.tap do
61
- typ = ChronoModel::Adapter::TSRange::Type
62
- oid = typ::OID
63
-
64
- ar_type = type_map.fetch(oid)
65
- cm_type = typ.new(ar_type.subtype, ar_type.type)
66
-
67
- type_map.register_type oid, cm_type
68
- end
69
- end
70
- end
71
- end
72
- end
@@ -1,31 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ChronoModel
4
- module Json
5
- extend self
6
-
7
- def create
8
- ActiveSupport::Deprecation.warn <<-MSG.squish
9
- ChronoModel: JSON ops are deprecated. Please migrate to JSONB.
10
- MSG
11
-
12
- adapter.execute 'CREATE OR REPLACE LANGUAGE plpythonu'
13
- adapter.execute File.read(sql('json_ops.sql'))
14
- end
15
-
16
- def drop
17
- adapter.execute File.read(sql('uninstall-json_ops.sql'))
18
- adapter.execute 'DROP LANGUAGE IF EXISTS plpythonu'
19
- end
20
-
21
- private
22
-
23
- def sql(file)
24
- "#{File.dirname(__FILE__)}/../../sql/#{file}"
25
- end
26
-
27
- def adapter
28
- ActiveRecord::Base.connection
29
- end
30
- end
31
- end