chrono_model 0.9.2 → 0.10.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
  SHA1:
3
- metadata.gz: 4e2c2ec8b0a57f2078ffabc946175c86b70b7201
4
- data.tar.gz: 6ad882f7558a940c9e3766e5abf6a6101107ffd2
3
+ metadata.gz: 67617b7e3802061ce8bff217ddbed05e33dea83e
4
+ data.tar.gz: 0e0172b002fe5f355461acb4c643ce8867394058
5
5
  SHA512:
6
- metadata.gz: 92eff9be8173deeb5092553a23403ff87918b5c993338825a79783470b74ec0988419ecc5f3c62075da785954e353f0d1f9447301c8c8b21349e7f9fc312788c
7
- data.tar.gz: d62529c7d3c49aae5dde87ab4304377bdc290dffa7054b2fca4c421f6a7042b001cc4cf3dd7237af45bb81210c406488491b837661f0652b4c906faac647b854
6
+ metadata.gz: f1b8ad21da3c8a9856c7865f1586a45760552c4d8b60e2999ff98ba5ea91d62f75ecf573ba0fdc3df5c7c048b7f9245b2a0427d2d2d5c1c005a12d84315a45c3
7
+ data.tar.gz: 1b94a4c721bc99472464b6160fd99c3e4e7ca66fddb893bfc538d83863dbd1ecdd30624f49ce020643dddce44f6d79d7ecbe1d0d10f415f18eb91a47a1710721
data/.travis.yml CHANGED
@@ -1,8 +1,16 @@
1
1
  rvm:
2
- - 2.0.0
3
- - 2.1
4
- - 2.2.5
5
- - 2.3.1
2
+ - 2.2.7
3
+ - 2.3
4
+ - 2.4
5
+
6
+ gemfile:
7
+ - gemfiles/rails_4.2.gemfile
8
+ - gemfiles/rails_5.0.gemfile
9
+
10
+ matrix:
11
+ exclude:
12
+ - rvm: 2.4
13
+ gemfile: gemfiles/rails_4.2.gemfile
6
14
 
7
15
  sudo: false
8
16
 
@@ -13,9 +21,14 @@ addons:
13
21
  postgresql: "9.4"
14
22
  apt:
15
23
  packages: postgresql-plpython-9.4
24
+ code_climate:
25
+ repo_token: dedfb7472ee410eec459bff3681d9a8fd8dd237e9bd7e8675a7c8eb7e253bba9
16
26
 
17
27
  before_script:
18
28
  - psql -c "CREATE DATABASE chronomodel;" -U postgres
19
29
 
20
30
  script:
21
- - bundle exec rake TEST_CONFIG=./spec/config.travis.yml CODECLIMATE_REPO_TOKEN=dedfb7472ee410eec459bff3681d9a8fd8dd237e9bd7e8675a7c8eb7e253bba9
31
+ - bundle exec rake TEST_CONFIG=./spec/config.travis.yml
32
+
33
+ after_success:
34
+ - bundle exec codeclimate-test-reporter
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Temporal database system on PostgreSQL using [updatable views][], [table inheritance][] and [INSTEAD OF triggers][].
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
4
  [![Dependency Status][deps-status-badge]][deps-status]
@@ -25,21 +25,23 @@ beneath.
25
25
 
26
26
  The application model is backed by an updatable view in the default `public`
27
27
  schema that behaves like a plain table to any database client. When data in
28
- manipulated on it, INSTEAD OF [triggers][] redirect the manipulations to
29
- concrete tables.
28
+ manipulated on it, INSTEAD OF [triggers][pg-triggers] redirect the manipulations
29
+ to concrete tables.
30
30
 
31
- Current data is hold in a table in the `temporal` [schema][], while history in
32
- hold in a specular one in the `history` schema. The latter [inherits][] from
33
- the former, to get automated schema updates and for free and other benefits.
31
+ *Current* data is held in a table in the `temporal` [schema][pg-schema], while
32
+ *History* is held in a table in the `history` schema that [inherits][pg-table-inheritance]
33
+ from the *Current* one, to get automated schema updates for free and other
34
+ benefits.
34
35
 
35
- The current time is taken using [`current_timestamp`][], so that multiple data
36
- manipulations in the same transaction on the same records always create a
37
- single history entry (they are _squashed_ together).
36
+ The current time is taken using [`current_timestamp`][pg-current-timestamp], so
37
+ that multiple data manipulations in the same transaction on the same records
38
+ always create a single history entry (they are _squashed_ together).
38
39
 
39
- [Partitioning][] of history is also possible: this design [fits the
40
- requirements][partitioning-excl-constraints] but it's not implemented yet.
40
+ [Partitioning][pg-partitioning] of history is also possible: this design [fits the
41
+ requirements][pg-partitioning-excl-constraints] but it's not implemented yet.
41
42
 
42
- See [README.sql][] for a SQL example defining the machinery for a simple table.
43
+ See [README.sql][cm-readme-sql] for a SQL example defining the machinery for a
44
+ simple table.
43
45
 
44
46
 
45
47
  ## Active Record integration
@@ -52,9 +54,10 @@ Data extraction at a single point in time and even `JOIN`s between temporal and
52
54
  non-temporal data is implemented using sub-selects and a `WHERE` generated by
53
55
  the provided `TimeMachine` module to be included in your models.
54
56
 
55
- The `WHERE` is optimized using [GiST indexes][] on the `tsrange` defining
56
- record validity. Overlapping history is prevented through [exclusion
57
- constraints][] and the [btree_gist][] extension.
57
+ The `WHERE` is optimized using [GiST indexes][pg-gist-indexes] on the
58
+ `tsrange` defining record validity. Overlapping history is prevented
59
+ through [exclusion constraints][pg-exclusion-constraints] and the
60
+ [btree_gist][pg-btree-gist] extension.
58
61
 
59
62
  All timestamps are _forcibly_ stored in as UTC, bypassing the
60
63
  `default_timezone` setting.
@@ -62,10 +65,11 @@ All timestamps are _forcibly_ stored in as UTC, bypassing the
62
65
 
63
66
  ## Requirements
64
67
 
65
- * Ruby >= 2.0 (1.9 is still supported, but support will be dropped soon).
66
- * Active Record = 4.2
68
+ * Ruby >= 2.2 (1.9 to 2.1 could still work, but aren't supported).
69
+ * Active Record 4.2 or 5.0 (4.2 doesn't work with Ruby 2.4, though)
67
70
  * PostgreSQL >= 9.3
68
- * The `btree_gist` and `plpython` PostgreSQL extensions:
71
+ * The `btree_gist` PostgreSQL extension
72
+ * The `plpython` PostgreSQL extension if you have *JSON* (*not* JSONB) columns
69
73
 
70
74
  With Homebrew:
71
75
 
@@ -79,7 +83,7 @@ With Apt:
79
83
 
80
84
  Add this line to your application's Gemfile:
81
85
 
82
- gem 'chrono_model', github: 'ifad/chronomodel'
86
+ gem 'chrono_model'
83
87
 
84
88
  And then execute:
85
89
 
@@ -147,6 +151,7 @@ change_table :your_table, :temporal => true, :copy_data => true, :validity => '1
147
151
  Please note that `change_table` requires you to use *old_style* `up` and
148
152
  `down` migrations. It cannot work with Rails 3-style `change` migrations.
149
153
 
154
+
150
155
  ## Selective Journaling
151
156
 
152
157
  By default UPDATEs only to the `updated_at` field are not recorded in the
@@ -159,8 +164,8 @@ options to `create_table`:
159
164
  * `:no_journal => %w( fld1 fld2 .. )` - do not record changes to the specified fields
160
165
  * `:full_journal => true` - record changes to *all* fields, including `updated_at`.
161
166
 
162
- These options are stored as JSON in the [COMMENT][] area of the public view,
163
- alongside with the ChronoModel version that created them.
167
+ These options are stored as JSON in the [COMMENT][pg-comment] area of the
168
+ public view, alongside with the ChronoModel version that created them.
164
169
 
165
170
  This is visible in `psql` if you issue a `\d+`. Example after a test run:
166
171
 
@@ -246,12 +251,15 @@ SELECT "countries".* FROM (
246
251
  ) AS "compositions" ON compositions.country_id = countries.id
247
252
  ```
248
253
 
249
- More methods are provided, see the [TimeMachine][] source for more information.
254
+ More methods are provided, see the [TimeMachine][cm-timemachine] source for
255
+ more information.
256
+
250
257
 
251
258
  ## History manipulation
252
259
 
253
260
  History objects can be changed and `.save`d just like any other record.
254
261
 
262
+
255
263
  ## Running tests
256
264
 
257
265
  You need a running PostgreSQL >= 9.3 instance. Create `spec/config.yml` with the
@@ -263,20 +271,21 @@ You need to connect as a database superuser, because specs need to create the
263
271
  Run `rake`. SQL queries are logged to `spec/debug.log`. If you want to see them
264
272
  in your output, use `rake VERBOSE=true`.
265
273
 
266
- ## Usage with JSON columns
267
274
 
268
- [JSON][json-type] does not provide an [equality operator][json-func].
275
+ ## Usage with JSON (*not* JSONB) columns
276
+
277
+ [JSON][pg-json-type] does not provide an [equality operator][pg-json-func].
269
278
  As both unnecessary update suppression and selective journaling require
270
279
  comparing the OLD and NEW rows fields, this fails by default.
271
280
 
272
281
  ChronoModel provides a naive JSON equality operator using a naive
273
- comparison of JSON objects [implemented in pl/python][json-opclass].
282
+ comparison of JSON objects [implemented in pl/python][pg-json-opclass].
274
283
 
275
284
  To load the opclass you can use the `ChronoModel::Json.create`
276
285
  convenience method. If you don't use JSON don't bother doing this.
277
286
 
278
- If you are on Postgres 9.4, you are strongly encouraged to use JSONB,
279
- that has an equality operator built-in, it's faster and stricter, and
287
+ If you are on Postgres 9.4, you are **strongly encouraged to use JSONB**,
288
+ as it has an equality operator built-in, it's faster and stricter, and
280
289
  offers many more indexing abilities and better performance than JSON.
281
290
 
282
291
  ## Caveats
@@ -293,15 +302,13 @@ offers many more indexing abilities and better performance than JSON.
293
302
  `db:structure:load`.
294
303
  Two helper tasks are also added, `db:data:dump` and `db:data:load`.
295
304
 
296
- * `.includes` is quirky when using `.as_of`.
297
-
298
- * The choice of using subqueries instead of [Common Table Expressions][]
299
- was dictated by the fact that CTEs [currently acts as an optimization
300
- fence][cte-optimization-fence].
301
- If it will be possible [to opt-out of the fence][cte-opt-out-fence] in
302
- the future, they will be probably be used again as they were [in the
303
- past][chronomodel-cte-impl], because the resulting queries were more
304
- readable, and do not inhibit using `.from()` on the `AR::Relation`.
305
+ * The choice of using subqueries instead of [Common Table Expressions]
306
+ [pg-ctes] was dictated by the fact that CTEs [currently act as an
307
+ optimization fence][pg-cte-optimization-fence].
308
+ If it will be possible [to opt-out of the fence][pg-cte-opt-out-fence]
309
+ in the future, they will be probably be used again as they were [in the
310
+ past][cm-cte-impl], because the resulting queries were more readable,
311
+ and do not inhibit using `.from()` on the `AR::Relation`.
305
312
 
306
313
 
307
314
  ## Contributing
@@ -313,6 +320,19 @@ offers many more indexing abilities and better performance than JSON.
313
320
  5. Create new Pull Request
314
321
 
315
322
 
323
+ ## Special mention
324
+
325
+ An special mention has to be made to [Paolo Zaccagnini][gh-pzac]
326
+ for all his effort in highlighting the improvements and best decisions
327
+ taken over the life cycle of the design and implementation of Chronomodel
328
+ while using it in many important projects.
329
+
330
+
331
+ ## Denominazione d'Origine Controllata
332
+
333
+ This software is Made in Italy :it: :smile:.
334
+
335
+
316
336
  [build-status]: https://travis-ci.org/ifad/chronomodel
317
337
  [build-status-badge]: https://travis-ci.org/ifad/chronomodel.svg
318
338
  [deps-status]: https://gemnasium.com/ifad/chronomodel
@@ -327,34 +347,34 @@ offers many more indexing abilities and better performance than JSON.
327
347
  [chronos-image]: http://i.imgur.com/8NObYiZl.jpg
328
348
  [rebelle-society]: http://www.rebellesociety.com/2012/10/11/the-writers-way-week-two-facing-procrastination/chronos_oeuvre_grand1/
329
349
 
330
- [updatable views]: http://www.postgresql.org/docs/9.4/static/sql-createview.html#SQL-CREATEVIEW-UPDATABLE-VIEWS
331
- [table inheritance]: http://www.postgresql.org/docs/9.4/static/ddl-inherit.html
332
- [INSTEAD OF triggers]: http://www.postgresql.org/docs/9.4/static/sql-createtrigger.html
333
350
  [wp-scd-2]: http://en.wikipedia.org/wiki/Slowly_changing_dimension#Type_2
334
351
  [wp-scd-4]: http://en.wikipedia.org/wiki/Slowly_changing_dimension#Type_4
335
- [triggers]: http://www.postgresql.org/docs/9.4/static/trigger-definition.html
336
- [schema]: http://www.postgresql.org/docs/9.4/static/ddl-schemas.html
337
- [inherits]: http://www.postgresql.org/docs/9.4/static/ddl-inherit.html
338
- [`current_timestamp`]: http://www.postgresql.org/docs/9.4/interactive/functions-datetime.html#FUNCTIONS-DATETIME-TABLE
339
-
340
- [Partitioning]: http://www.postgresql.org/docs/9.4/static/ddl-partitioning.html)
341
- [partitioning-excl-constraints]: http://www.postgresql.org/docs/9.4/static/ddl-partitioning.html#DDL-PARTITIONING-CONSTRAINT-EXCLUSION
342
- [README.sql]: https://github.com/ifad/chronomodel/blob/master/README.sql
343
- [GiST indexes]: http://www.postgresql.org/docs/9.4/static/gist.html
344
- [exclusion constraints]: http://www.postgresql.org/docs/9.4/static/sql-createtable.html#SQL-CREATETABLE-EXCLUDE
345
- [btree_gist]: http://www.postgresql.org/docs/9.4/static/btree-gist.html
346
- [COMMENT]: http://www.postgresql.org/docs/9.4/static/sql-comment.html
347
- [TimeMachine]: https://github.com/ifad/chronomodel/blob/master/lib/chrono_model/time_machine.rb
348
352
 
349
- [r4-tsrange-broken]: https://github.com/rails/rails/pull/13793#issuecomment-34608093
350
- [r4-tsrange-incomplete]: https://github.com/rails/rails/issues/14010)
353
+ [pg-updatable-views]: http://www.postgresql.org/docs/9.4/static/sql-createview.html#SQL-CREATEVIEW-UPDATABLE-VIEWS
354
+ [pg-table-inheritance]: http://www.postgresql.org/docs/9.4/static/ddl-inherit.html
355
+ [pg-instead-of-triggers]: http://www.postgresql.org/docs/9.4/static/sql-createtrigger.html
356
+ [pg-triggers]: http://www.postgresql.org/docs/9.4/static/trigger-definition.html
357
+ [pg-schema]: http://www.postgresql.org/docs/9.4/static/ddl-schemas.html
358
+ [pg-current-timestamp]: http://www.postgresql.org/docs/9.4/interactive/functions-datetime.html#FUNCTIONS-DATETIME-TABLE
359
+ [pg-partitioning]: http://www.postgresql.org/docs/9.4/static/ddl-partitioning.html
360
+ [pg-partitioning-excl-constraints]: http://www.postgresql.org/docs/9.4/static/ddl-partitioning.html#DDL-PARTITIONING-CONSTRAINT-EXCLUSION
361
+ [pg-gist-indexes]: http://www.postgresql.org/docs/9.4/static/gist.html
362
+ [pg-exclusion-constraints]: http://www.postgresql.org/docs/9.4/static/sql-createtable.html#SQL-CREATETABLE-EXCLUDE
363
+ [pg-btree-gist]: http://www.postgresql.org/docs/9.4/static/btree-gist.html
364
+ [pg-comment]: http://www.postgresql.org/docs/9.4/static/sql-comment.html
351
365
  [pg-tsrange-and-ruby]: https://bugs.ruby-lang.org/issues/6864
352
- [chronomodel-0.5]: https://github.com/ifad/chronomodel/tree/c2daa0f
353
- [Common Table Expressions]: http://www.postgresql.org/docs/9.4/static/queries-with.html
354
- [cte-optimization-fence]: http://archives.postgresql.org/pgsql-hackers/2012-09/msg00700.php
355
- [cte-opt-out-fence]: http://archives.postgresql.org/pgsql-hackers/2012-10/msg00024.php
356
- [chronomodel-cte-impl]: https://github.com/ifad/chronomodel/commit/18f4c4b
357
-
358
- [json-type]: http://www.postgresql.org/docs/9.4/static/datatype-json.html
359
- [json-func]: http://www.postgresql.org/docs/9.4/static/functions-json.html
360
- [json-opclass]: https://github.com/ifad/chronomodel/blob/master/sql/json_ops.sql
366
+ [pg-ctes]: http://www.postgresql.org/docs/9.4/static/queries-with.html
367
+ [pg-cte-optimization-fence]: http://archives.postgresql.org/pgsql-hackers/2012-09/msg00700.php
368
+ [pg-cte-opt-out-fence]: http://archives.postgresql.org/pgsql-hackers/2012-10/msg00024.php
369
+ [pg-json-type]: http://www.postgresql.org/docs/9.4/static/datatype-json.html
370
+ [pg-json-func]: http://www.postgresql.org/docs/9.4/static/functions-json.html
371
+ [pg-json-opclass]: https://github.com/ifad/chronomodel/blob/master/sql/json_ops.sql
372
+
373
+ [r4-tsrange-broken]: https://github.com/rails/rails/pull/13793#issuecomment-34608093
374
+ [r4-tsrange-incomplete]: https://github.com/rails/rails/issues/14010
375
+
376
+ [cm-readme-sql]: https://github.com/ifad/chronomodel/blob/master/README.sql
377
+ [cm-timemachine]: https://github.com/ifad/chronomodel/blob/master/lib/chrono_model/time_machine.rb
378
+ [cm-cte-impl]: https://github.com/ifad/chronomodel/commit/18f4c4b
379
+
380
+ [gh-pzac]: https://github.com/pzac
data/chrono_model.gemspec CHANGED
@@ -15,7 +15,7 @@ Gem::Specification.new do |gem|
15
15
  gem.require_paths = ["lib"]
16
16
  gem.version = ChronoModel::VERSION
17
17
 
18
- gem.add_dependency "activerecord", "~> 4.2.0"
18
+ gem.add_dependency 'activerecord', '>= 4.2.0', '< 5.1.0'
19
19
  gem.add_dependency "pg"
20
20
  gem.add_dependency "multi_json"
21
21
 
@@ -25,5 +25,6 @@ Gem::Specification.new do |gem|
25
25
  gem.add_development_dependency 'rspec'
26
26
  gem.add_development_dependency 'rake'
27
27
  gem.add_development_dependency 'fuubar'
28
+ gem.add_development_dependency 'simplecov'
28
29
  gem.add_development_dependency 'codeclimate-test-reporter'
29
- end
30
+ end
@@ -0,0 +1,5 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "activerecord", "~> 4.2.0"
4
+
5
+ gemspec :path => "../"
@@ -0,0 +1,5 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "activerecord", "~> 5.0.0"
4
+
5
+ gemspec :path => "../"
@@ -17,7 +17,12 @@ module ActiveRecord
17
17
  conn_params[:dbname] = conn_params.delete(:database) if conn_params[:database]
18
18
 
19
19
  # Forward only valid config params to PGconn.connect.
20
- conn_params.keep_if { |k, _| VALID_CONN_PARAMS.include?(k) }
20
+ valid_conn_param_keys = if ActiveRecord::VERSION::MAJOR == 4
21
+ VALID_CONN_PARAMS
22
+ else
23
+ PGconn.conndefaults_hash.keys + [:requiressl]
24
+ end
25
+ conn_params.slice!(*valid_conn_param_keys)
21
26
 
22
27
  # The postgres drivers don't allow the creation of an unconnected PGconn object,
23
28
  # so just pass a nil connection object for the time being.
@@ -71,9 +71,9 @@ module ChronoModel
71
71
  end
72
72
  end
73
73
 
74
- # Rename indexes
74
+
75
+ # Rename indexes on history schema
75
76
  #
76
- pkey = primary_key(new_name)
77
77
  _on_history_schema do
78
78
  standard_index_names = %w(
79
79
  inherit_pkey instance_history pkey
@@ -90,15 +90,31 @@ module ChronoModel
90
90
  end
91
91
  end
92
92
 
93
- # Rename functions
93
+ # Rename indexes on temporal schema
94
+ #
95
+ _on_temporal_schema do
96
+ temporal_indexes = indexes(new_name)
97
+ temporal_indexes.map(&:name).each do |old_idx_name|
98
+ if old_idx_name =~ /^index_#{name}_on_(?<columns>.+)/
99
+ new_idx_name = "index_#{new_name}_on_#{$~['columns']}"
100
+ execute "ALTER INDEX #{old_idx_name} RENAME TO #{new_idx_name}"
101
+ end
102
+ end
103
+ end
104
+
105
+ # Drop view
106
+ #
107
+ execute "DROP VIEW #{name}"
108
+
109
+ # Drop functions
94
110
  #
95
111
  %w( insert update delete ).each do |func|
96
- execute "ALTER FUNCTION chronomodel_#{name}_#{func}() RENAME TO chronomodel_#{new_name}_#{func}"
112
+ execute "DROP FUNCTION chronomodel_#{name}_#{func}()"
97
113
  end
98
114
 
99
- # Rename the public view
115
+ # Create view and functions
100
116
  #
101
- execute "ALTER VIEW #{name} RENAME TO #{new_name}"
117
+ chrono_create_view_for(new_name)
102
118
 
103
119
  TableCache.del! name
104
120
  TableCache.add! new_name
@@ -447,8 +463,8 @@ module ChronoModel
447
463
  #
448
464
  def is_chrono?(table)
449
465
  TableCache.fetch(table) do
450
- _on_temporal_schema { table_exists?(table) } &&
451
- _on_history_schema { table_exists?(table) }
466
+ _on_temporal_schema { chrono_data_source_exists?(table) } &&
467
+ _on_history_schema { chrono_data_source_exists?(table) }
452
468
  end
453
469
 
454
470
  rescue ActiveRecord::StatementInvalid => e
@@ -630,7 +646,7 @@ module ChronoModel
630
646
  def chrono_metadata_for(table)
631
647
  comment = select_value(
632
648
  "SELECT obj_description(#{quote(table)}::regclass)",
633
- "ChronoModel metadata for #{table}") if table_exists?(table)
649
+ "ChronoModel metadata for #{table}") if chrono_data_source_exists?(table)
634
650
 
635
651
  MultiJson.load(comment || '{}').with_indifferent_access
636
652
  end
@@ -693,7 +709,7 @@ module ChronoModel
693
709
 
694
710
  # SELECT - return only current data
695
711
  #
696
- execute "DROP VIEW #{table}" if table_exists? table
712
+ execute "DROP VIEW #{table}" if chrono_data_source_exists? table
697
713
  execute "CREATE VIEW #{table} AS SELECT * FROM ONLY #{current}"
698
714
 
699
715
  # Set default values on the view (closes #12)
@@ -701,10 +717,19 @@ module ChronoModel
701
717
  chrono_metadata_set(table, options.merge(:chronomodel => VERSION))
702
718
 
703
719
  columns(table).each do |column|
704
- default = column.default.nil? ? column.default_function : quote(column.default, column)
720
+ default = if column.default.nil?
721
+ column.default_function
722
+ else
723
+ if ActiveRecord::VERSION::MAJOR == 4
724
+ quote(column.default, column)
725
+ else # Rails 5 and beyond
726
+ quote(column.default)
727
+ end
728
+ end
729
+
705
730
  next if column.name == pk || default.nil?
706
731
 
707
- execute "ALTER VIEW #{table} ALTER COLUMN #{column.name} SET DEFAULT #{default}"
732
+ execute "ALTER VIEW #{table} ALTER COLUMN #{quote_column_name(column.name)} SET DEFAULT #{default}"
708
733
  end
709
734
 
710
735
  columns = columns(table).map {|c| quote_column_name(c.name)}
@@ -889,6 +914,16 @@ module ChronoModel
889
914
  end
890
915
  end
891
916
 
917
+ def chrono_data_source_exists?(table_name)
918
+ if ActiveRecord::VERSION::MAJOR >= 5
919
+ data_source_exists?(table_name)
920
+ else
921
+ # On Rails 4, table_exists? has the same behaviour, checking if both
922
+ # a view or table exists
923
+ table_exists?(table_name)
924
+ end
925
+ end
926
+
892
927
  def _on_temporal_schema(nesting = true, &block)
893
928
  on_schema(TEMPORAL_SCHEMA, nesting, &block)
894
929
  end
@@ -55,6 +55,52 @@ module ChronoModel
55
55
 
56
56
  end
57
57
  end
58
+
59
+ def build_preloader
60
+ ActiveRecord::Associations::Preloader.new(as_of_time: as_of_time)
61
+ end
62
+ end
63
+
64
+ # Patches ActiveRecord::Associations::Preloader to add support for
65
+ # temporal associations. This is tying itself to Rails internals
66
+ # and it is ugly :-(.
67
+ #
68
+ module Preloader
69
+ attr_reader :options
70
+
71
+ NULL_RELATION = ActiveRecord::Associations::Preloader::NULL_RELATION
72
+ AS_OF_PRELOAD_SCOPE = Struct.new(:as_of_time, *NULL_RELATION.members)
73
+
74
+ def initialize(options = {})
75
+ @options = options.freeze
76
+ end
77
+
78
+ def preload(records, associations, given_preload_scope = nil)
79
+ if (as_of_time = options[:as_of_time])
80
+ preload_scope = AS_OF_PRELOAD_SCOPE.new
81
+
82
+ preload_scope.as_of_time = as_of_time
83
+ given_preload_scope ||= NULL_RELATION
84
+
85
+ NULL_RELATION.members.each do |member|
86
+ preload_scope[member] = given_preload_scope[member]
87
+ end
88
+ end
89
+
90
+ super records, associations, preload_scope
91
+ end
92
+
93
+ module Association
94
+ def build_scope
95
+ scope = super
96
+
97
+ if preload_scope.respond_to?(:as_of_time)
98
+ scope = scope.as_of(preload_scope.as_of_time)
99
+ end
100
+
101
+ return scope
102
+ end
103
+ end
58
104
  end
59
105
 
60
106
  # Patches ActiveRecord::Associations::Association to add support for
@@ -201,6 +201,16 @@ module ChronoModel
201
201
 
202
202
  # Define the History constant inside the subclass
203
203
  subclass.const_set :History, history
204
+
205
+ history.instance_eval do
206
+ # Monkey patch of ActiveRecord::Inheritance.
207
+ # STI fails when a Foo::History record has Foo as type in the
208
+ # inheritance column; AR expects the type to be an instance of the
209
+ # current class or a descendant (or self).
210
+ def find_sti_class(type_name)
211
+ super(type_name + "::History")
212
+ end
213
+ end
204
214
  end
205
215
 
206
216
  # Returns a read-only representation of this record as it was +time+ ago.
@@ -392,9 +402,8 @@ module ChronoModel
392
402
  if t == :now || t == :today
393
403
  now_for_column(column)
394
404
  else
395
- [connection.quote(t, column),
396
- primitive_type_for_column(column)
397
- ].join('::')
405
+ quoted_t = connection.quote(connection.quoted_date(t))
406
+ [quoted_t, primitive_type_for_column(column)].join('::')
398
407
  end
399
408
  end
400
409
 
@@ -3,12 +3,12 @@ module ChronoModel
3
3
  module Conversions
4
4
  extend self
5
5
 
6
- ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/
6
+ ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(?:\.(\d+))?\z/
7
7
 
8
8
  def string_to_utc_time(string)
9
9
  if string =~ ISO_DATETIME
10
- microsec = ($7.to_f * 1_000_000).to_i
11
- Time.utc $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec
10
+ usec = $7.nil? ? '000000' : $7.ljust(6, '0') # .1 is .100000, not .000001
11
+ Time.utc $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, usec.to_i
12
12
  end
13
13
  end
14
14
 
@@ -51,7 +51,8 @@ module ChronoModel
51
51
 
52
52
  connection.execute %[
53
53
  UPDATE #{quoted_table_name}
54
- SET "validity" = tsrange(#{connection.quote(from)}, #{connection.quote(to)})
54
+ SET "validity" = tsrange(#{connection.quote(from)}, #{connection.quote(to)}),
55
+ "recorded_at" = #{connection.quote(from)}
55
56
  WHERE "hid" = #{hid.to_i}
56
57
  ]
57
58
  end
@@ -1,3 +1,3 @@
1
1
  module ChronoModel
2
- VERSION = "0.9.2"
2
+ VERSION = "0.10.0"
3
3
  end
data/lib/chrono_model.rb CHANGED
@@ -14,7 +14,6 @@ if defined?(Rails)
14
14
  require 'chrono_model/railtie'
15
15
  end
16
16
 
17
-
18
17
  ActiveRecord::Associations::Association.instance_eval do
19
18
  prepend ChronoModel::Patches::Association
20
19
  end
@@ -22,3 +21,11 @@ end
22
21
  ActiveRecord::Relation.instance_eval do
23
22
  prepend ChronoModel::Patches::Relation
24
23
  end
24
+
25
+ ActiveRecord::Associations::Preloader.instance_eval do
26
+ prepend ChronoModel::Patches::Preloader
27
+ end
28
+
29
+ ActiveRecord::Associations::Preloader::Association.instance_eval do
30
+ prepend ChronoModel::Patches::Preloader::Association
31
+ end
data/spec/adapter_spec.rb CHANGED
@@ -93,12 +93,22 @@ describe ChronoModel::Adapter do
93
93
  context ':temporal => true' do
94
94
  before :all do
95
95
  adapter.create_table table, :temporal => true, &columns
96
+ adapter.add_index table, :test
97
+ adapter.add_index table, [:foo, :bar]
96
98
 
97
99
  adapter.rename_table table, renamed
98
100
  end
99
101
  after(:all) { adapter.drop_table(renamed) }
100
102
 
101
103
  it_should_behave_like 'temporal table'
104
+
105
+ it 'renames indexes' do
106
+ new_index_names = adapter.indexes(renamed).map(&:name)
107
+ expected_index_names = [[:test], [:foo, :bar]].map do |idx_cols|
108
+ "index_#{renamed}_on_#{idx_cols.join('_and_')}"
109
+ end
110
+ expect(new_index_names.to_set).to eq expected_index_names.to_set
111
+ end
102
112
  end
103
113
 
104
114
  context ':temporal => false' do
@@ -14,11 +14,11 @@ describe 'JSON equality operator' do
14
14
  ChronoModel::Json.drop
15
15
  end
16
16
 
17
- it { expect(adapter.select_value(%[ SELECT '{"a":1}'::json = '{"a":1}'::json ])).to eq 't' }
18
- it { expect(adapter.select_value(%[ SELECT '{"a":1}'::json = '{"a" : 1}'::json ])).to eq 't' }
19
- it { expect(adapter.select_value(%[ SELECT '{"a":1}'::json = '{"a":2}'::json ])).to eq 'f' }
20
- it { expect(adapter.select_value(%[ SELECT '{"a":1,"b":2}'::json = '{"b":2,"a":1}'::json ])).to eq 't' }
21
- it { expect(adapter.select_value(%[ SELECT '{"a":1,"b":2,"x":{"c":4,"d":5}}'::json = '{"b":2, "x": { "d": 5, "c": 4}, "a":1}'::json ])).to eq 't' }
17
+ it { expect(adapter.select_value(%[ SELECT '{"a":1}'::json = '{"a":1}'::json ])).to eq AR_TRUE }
18
+ it { expect(adapter.select_value(%[ SELECT '{"a":1}'::json = '{"a" : 1}'::json ])).to eq AR_TRUE }
19
+ it { expect(adapter.select_value(%[ SELECT '{"a":1}'::json = '{"a":2}'::json ])).to eq AR_FALSE }
20
+ it { expect(adapter.select_value(%[ SELECT '{"a":1,"b":2}'::json = '{"b":2,"a":1}'::json ])).to eq AR_TRUE }
21
+ it { expect(adapter.select_value(%[ SELECT '{"a":1,"b":2,"x":{"c":4,"d":5}}'::json = '{"b":2, "x": { "d": 5, "c": 4}, "a":1}'::json ])).to eq AR_TRUE }
22
22
 
23
23
  context 'on a temporal table' do
24
24
  before :all do
data/spec/spec_helper.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
2
2
  #
3
- require 'codeclimate-test-reporter'
4
- CodeClimate::TestReporter.start
3
+ require 'simplecov'
4
+ SimpleCov.start
5
5
 
6
6
  require 'chrono_model'
7
7
 
@@ -11,6 +11,9 @@ require 'support/matchers/table'
11
11
  require 'support/matchers/column'
12
12
  require 'support/matchers/index'
13
13
 
14
+ # Rails 5 returns a True/FalseClass
15
+ AR_TRUE, AR_FALSE = ActiveRecord::VERSION::MAJOR == 4 ? ['t', 'f'] : [true, false]
16
+
14
17
  RSpec.configure do |config|
15
18
  config.include(ChronoTest::Matchers::Schema)
16
19
  config.include(ChronoTest::Matchers::Table)
@@ -12,7 +12,7 @@ module ChronoTest::Matchers
12
12
  schema = options[:in]
13
13
  kind = options[:kind] == :view ? 'v' : 'r'
14
14
 
15
- select_value(<<-SQL, [ table, schema ], 'Check table exists') == 't'
15
+ select_value(<<-SQL, [ table, schema ], 'Check table exists') == AR_TRUE
16
16
  SELECT EXISTS (
17
17
  SELECT 1
18
18
  FROM pg_class c
@@ -129,7 +129,7 @@ module ChronoTest::Matchers
129
129
  def inherits_from_temporal?
130
130
  binds = ["#{history_schema}.#{table}", "#{temporal_schema}.#{table}"]
131
131
 
132
- @inheritance = select_value(<<-SQL, binds, 'Check inheritance') == 't'
132
+ @inheritance = select_value(<<-SQL, binds, 'Check inheritance') == AR_TRUE
133
133
  SELECT EXISTS (
134
134
  SELECT 1 FROM pg_catalog.pg_inherits
135
135
  WHERE inhrelid = ?::regclass::oid
@@ -172,7 +172,7 @@ module ChronoTest::Matchers
172
172
  attname: connection.primary_key(table)
173
173
  }
174
174
 
175
- @constraint = select_value(<<-SQL, binds, 'Check Consistency Constraint') == 't'
175
+ @constraint = select_value(<<-SQL, binds, 'Check Consistency Constraint') == AR_TRUE
176
176
  SELECT EXISTS (
177
177
  SELECT 1 FROM pg_catalog.pg_constraint
178
178
  WHERE conname = :conname
@@ -24,6 +24,8 @@ describe ChronoModel::TimeMachine do
24
24
  #
25
25
  baz = Baz.create :name => 'baz', :bar => bar
26
26
 
27
+ # Specs start here
28
+ #
27
29
  describe '.chrono?' do
28
30
  subject { model.chrono? }
29
31
 
@@ -69,8 +71,6 @@ describe ChronoModel::TimeMachine do
69
71
  it { is_expected.to include(Publication) }
70
72
  end
71
73
 
72
- # Specs start here
73
- #
74
74
  describe '.chrono_models' do
75
75
  subject { ChronoModel::TimeMachine.chrono_models }
76
76
 
@@ -134,6 +134,28 @@ describe ChronoModel::TimeMachine do
134
134
  it { expect(bar.as_of(bar.ts[3]).foo.name).to eq 'new foo' }
135
135
  end
136
136
 
137
+ describe 'supports historical queries with includes()' do
138
+ it { expect(Foo.as_of(foo.ts[0]).includes(:bars).first.bars).to eq [] }
139
+ it { expect(Foo.as_of(foo.ts[1]).includes(:bars).first.bars).to eq [] }
140
+ it { expect(Foo.as_of(foo.ts[2]).includes(:bars).first.bars).to eq [bar] }
141
+
142
+ it { expect(Foo.as_of(bar.ts[0]).includes(:bars).first.bars.first.name).to eq 'bar' }
143
+ it { expect(Foo.as_of(bar.ts[1]).includes(:bars).first.bars.first.name).to eq 'foo bar' }
144
+ it { expect(Foo.as_of(bar.ts[2]).includes(:bars).first.bars.first.name).to eq 'bar bar' }
145
+ it { expect(Foo.as_of(bar.ts[3]).includes(:bars).first.bars.first.name).to eq 'new bar' }
146
+
147
+
148
+ it { expect(Bar.as_of(bar.ts[0]).includes(:foo).first.foo).to eq foo }
149
+ it { expect(Bar.as_of(bar.ts[1]).includes(:foo).first.foo).to eq foo }
150
+ it { expect(Bar.as_of(bar.ts[2]).includes(:foo).first.foo).to eq foo }
151
+ it { expect(Bar.as_of(bar.ts[3]).includes(:foo).first.foo).to eq foo }
152
+
153
+ it { expect(Bar.as_of(bar.ts[0]).includes(:foo).first.foo.name).to eq 'foo bar' }
154
+ it { expect(Bar.as_of(bar.ts[1]).includes(:foo).first.foo.name).to eq 'foo bar' }
155
+ it { expect(Bar.as_of(bar.ts[2]).includes(:foo).first.foo.name).to eq 'new foo' }
156
+ it { expect(Bar.as_of(bar.ts[3]).includes(:foo).first.foo.name).to eq 'new foo' }
157
+ end
158
+
137
159
  it 'doesn\'t raise RecordNotFound when no history records are found' do
138
160
  expect { foo.as_of(1.minute.ago) }.to_not raise_error
139
161
  expect(foo.as_of(1.minute.ago)).to be(nil)
@@ -0,0 +1,43 @@
1
+ require 'spec_helper'
2
+
3
+ describe ChronoModel::Conversions do
4
+
5
+ describe 'string_to_utc_time' do
6
+ subject { described_class.string_to_utc_time(string) }
7
+
8
+ context 'given a valid UTC time string' do
9
+ let(:string) { '2017-02-06 09:46:31.129626' }
10
+
11
+ it { is_expected.to be_a(Time) }
12
+ it { expect(subject.year).to eq 2017 }
13
+ it { expect(subject.month).to eq 2 }
14
+ it { expect(subject.day).to eq 6 }
15
+ it { expect(subject.hour).to eq 9 }
16
+ it { expect(subject.min).to eq 46 }
17
+ it { expect(subject.sec).to eq 31 }
18
+ it { expect(subject.usec).to eq 129626 } # Ref Issue #32
19
+ end
20
+
21
+ context 'given a valid UTC string without least significant zeros' do
22
+ let(:string) { '2017-02-06 09:46:31.129' }
23
+
24
+ it { is_expected.to be_a(Time) }
25
+ it { expect(subject.usec).to eq 129000 } # Ref Issue #32
26
+ end
27
+
28
+ context 'given an invalid UTC time string' do
29
+ let(:string) { 'foobar' }
30
+
31
+ it { is_expected.to be(nil) }
32
+ end
33
+ end
34
+
35
+ describe 'time_to_utc_string' do
36
+ subject { described_class.time_to_utc_string(time) }
37
+
38
+ let(:time) { Time.utc(1981, 4, 11, 2, 42, 10, 123456) }
39
+
40
+ it { is_expected.to eq '1981-04-11 02:42:10.123456' }
41
+ end
42
+
43
+ end
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: 0.9.2
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Marcello Barnaba
@@ -9,22 +9,28 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2017-02-08 00:00:00.000000000 Z
12
+ date: 2017-07-05 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activerecord
16
16
  requirement: !ruby/object:Gem::Requirement
17
17
  requirements:
18
- - - "~>"
18
+ - - ">="
19
19
  - !ruby/object:Gem::Version
20
20
  version: 4.2.0
21
+ - - "<"
22
+ - !ruby/object:Gem::Version
23
+ version: 5.1.0
21
24
  type: :runtime
22
25
  prerelease: false
23
26
  version_requirements: !ruby/object:Gem::Requirement
24
27
  requirements:
25
- - - "~>"
28
+ - - ">="
26
29
  - !ruby/object:Gem::Version
27
30
  version: 4.2.0
31
+ - - "<"
32
+ - !ruby/object:Gem::Version
33
+ version: 5.1.0
28
34
  - !ruby/object:Gem::Dependency
29
35
  name: pg
30
36
  requirement: !ruby/object:Gem::Requirement
@@ -137,6 +143,20 @@ dependencies:
137
143
  - - ">="
138
144
  - !ruby/object:Gem::Version
139
145
  version: '0'
146
+ - !ruby/object:Gem::Dependency
147
+ name: simplecov
148
+ requirement: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ type: :development
154
+ prerelease: false
155
+ version_requirements: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
140
160
  - !ruby/object:Gem::Dependency
141
161
  name: codeclimate-test-reporter
142
162
  requirement: !ruby/object:Gem::Requirement
@@ -169,6 +189,8 @@ files:
169
189
  - README.sql
170
190
  - Rakefile
171
191
  - chrono_model.gemspec
192
+ - gemfiles/rails_4.2.gemfile
193
+ - gemfiles/rails_5.0.gemfile
172
194
  - lib/active_record/connection_adapters/chronomodel_adapter.rb
173
195
  - lib/chrono_model.rb
174
196
  - lib/chrono_model/adapter.rb
@@ -194,6 +216,7 @@ files:
194
216
  - spec/support/matchers/table.rb
195
217
  - spec/time_machine_spec.rb
196
218
  - spec/time_query_spec.rb
219
+ - spec/utils_spec.rb
197
220
  - sql/json_ops.sql
198
221
  - sql/uninstall-json_ops.sql
199
222
  homepage: https://github.com/ifad/chronomodel
@@ -215,7 +238,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
215
238
  version: '0'
216
239
  requirements: []
217
240
  rubyforge_project:
218
- rubygems_version: 2.4.8
241
+ rubygems_version: 2.5.1
219
242
  signing_key:
220
243
  specification_version: 4
221
244
  summary: Temporal extensions (SCD Type II) for Active Record
@@ -234,3 +257,4 @@ test_files:
234
257
  - spec/support/matchers/table.rb
235
258
  - spec/time_machine_spec.rb
236
259
  - spec/time_query_spec.rb
260
+ - spec/utils_spec.rb