chrono_model 0.9.2 → 0.10.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
  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