chrono_model 2.0.0 → 4.0.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: 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