chrono_model 0.5.3 → 0.8.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 +7 -0
- data/.rspec +1 -1
- data/.travis.yml +7 -0
- data/Gemfile +10 -1
- data/LICENSE +3 -1
- data/README.md +239 -136
- data/README.sql +108 -94
- data/chrono_model.gemspec +5 -4
- data/lib/active_record/connection_adapters/chronomodel_adapter.rb +42 -0
- data/lib/chrono_model.rb +0 -8
- data/lib/chrono_model/adapter.rb +346 -212
- data/lib/chrono_model/patches.rb +21 -8
- data/lib/chrono_model/railtie.rb +1 -13
- data/lib/chrono_model/time_gate.rb +2 -2
- data/lib/chrono_model/time_machine.rb +153 -87
- data/lib/chrono_model/utils.rb +35 -8
- data/lib/chrono_model/version.rb +1 -1
- data/spec/adapter_spec.rb +154 -14
- data/spec/config.yml.example +1 -0
- data/spec/json_ops_spec.rb +48 -0
- data/spec/support/connection.rb +4 -9
- data/spec/support/helpers.rb +27 -2
- data/spec/support/matchers/column.rb +5 -2
- data/spec/support/matchers/index.rb +4 -0
- data/spec/support/matchers/schema.rb +4 -0
- data/spec/support/matchers/table.rb +94 -21
- data/spec/time_machine_spec.rb +62 -28
- data/spec/time_query_spec.rb +227 -0
- data/sql/json_ops.sql +56 -0
- data/sql/uninstall-json_ops.sql +24 -0
- metadata +44 -18
- data/lib/chrono_model/compatibility.rb +0 -31
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: da72ced6e70c0fc98ba5b78417421557dfd22708
|
4
|
+
data.tar.gz: 6f6c9b32d810cceb820f5e6b502baef95add1c26
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 1f7c0e8f7e1db361fb037295122f03562359ba497495a279e53b9aa7d48cbecac4c2396bd5cff88ea609a5a2a62cc70060d24c9e8047f9482dca27d2633e61de
|
7
|
+
data.tar.gz: 7226296b3c3e0442f9c9795e556711c8a8d7cfc963da315eaaa8f50f55c91146c356ab93c3045646c2d564086ecafb7ee2df912a0082e96942e2391bbdc56653
|
data/.rspec
CHANGED
@@ -1,2 +1,2 @@
|
|
1
1
|
--color
|
2
|
-
--format
|
2
|
+
--format Fuubar
|
data/.travis.yml
CHANGED
@@ -1,12 +1,19 @@
|
|
1
1
|
rvm:
|
2
2
|
- 1.9.3
|
3
3
|
- 2.0.0
|
4
|
+
- 2.1.1
|
4
5
|
|
5
6
|
services:
|
6
7
|
- postgresql
|
7
8
|
|
8
9
|
bundler_args: --without development
|
9
10
|
|
11
|
+
before_install:
|
12
|
+
- sudo service postgresql stop
|
13
|
+
- sudo apt-get update
|
14
|
+
- sudo apt-get install postgresql-9.3 postgresql-plpython-9.3
|
15
|
+
- sudo service postgresql start 9.3
|
16
|
+
|
10
17
|
before_script:
|
11
18
|
- psql -c "CREATE DATABASE chronomodel;" -U postgres
|
12
19
|
|
data/Gemfile
CHANGED
@@ -4,11 +4,20 @@ source 'https://rubygems.org'
|
|
4
4
|
gemspec
|
5
5
|
|
6
6
|
group :development do
|
7
|
-
gem 'debugger'
|
8
7
|
gem 'pry'
|
8
|
+
gem 'hirb'
|
9
|
+
|
10
|
+
gem(
|
11
|
+
case RUBY_VERSION.to_f
|
12
|
+
when 1.8 then 'ruby-debug'
|
13
|
+
when 1.9 then 'debugger'
|
14
|
+
else 'byebug'
|
15
|
+
end
|
16
|
+
)
|
9
17
|
end
|
10
18
|
|
11
19
|
group :test do
|
12
20
|
gem 'rspec'
|
13
21
|
gem 'rake'
|
22
|
+
gem 'fuubar'
|
14
23
|
end
|
data/LICENSE
CHANGED
data/README.md
CHANGED
@@ -1,228 +1,331 @@
|
|
1
|
-
#
|
1
|
+
# Temporal database system on PostgreSQL using [updatable views][], [table inheritance][] and [INSTEAD OF triggers][]. [![Build Status][build-status-badge]][build-status] [![Dependency Status][deps-status-badge]][deps-status] [![Code Climate][code-analysis-badge]][code-analysis]
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
[
|
3
|
+
ChronoModel does what Oracle sells as "Flashback Queries", but with standard
|
4
|
+
SQL on free PostgreSQL. Academically speaking, ChronoModel implements a
|
5
|
+
[Type-2 Slowly-Changing Dimension][wp-scd-2] with [history tables][wp-scd-4].
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
|
7
|
+
All history keeping happens inside the database system, freeing application
|
8
|
+
code from having to deal with it. ChronoModel implements all the required
|
9
|
+
features in Ruby on Rails' ORM to leverage the database temporal structure
|
10
|
+
beneath.
|
10
11
|
|
11
|
-
[](https://travis-ci.org/ifad/chronomodel)
|
12
|
-
[](https://gemnasium.com/ifad/chronomodel)
|
13
|
-
[](https://codeclimate.com/github/ifad/chronomodel)
|
14
12
|
|
15
|
-
|
16
|
-
having to deal with it.
|
13
|
+
## Design
|
17
14
|
|
18
|
-
The application model is backed by an updatable view
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
having a `SERIAL` primary key.
|
15
|
+
The application model is backed by an updatable view in the default `public`
|
16
|
+
schema that behaves like a plain table to any database client. When data in
|
17
|
+
manipulated on it, INSTEAD OF [triggers][] redirect the manipulations to
|
18
|
+
concrete tables.
|
23
19
|
|
24
|
-
Current data is hold in a table in the `temporal` [schema]
|
25
|
-
|
26
|
-
|
27
|
-
automated schema updates for free. Partitioning of history is even possible but not implemented
|
28
|
-
yet.
|
20
|
+
Current data is hold in a table in the `temporal` [schema][], while history in
|
21
|
+
hold in a specular one in the `history` schema. The latter [inherits][] from
|
22
|
+
the former, to get automated schema updates and for free and other benefits.
|
29
23
|
|
30
|
-
The
|
24
|
+
The current time is taken using [`current_timestamp`][], so that multiple data
|
25
|
+
manipulations in the same transaction on the same records always create a
|
26
|
+
single history entry (they are _squashed_ together).
|
31
27
|
|
32
|
-
|
33
|
-
|
34
|
-
migrations.
|
28
|
+
[Partitioning][] of history is also possible: this design [fits the
|
29
|
+
requirements][partitioning-excl-constraints] but it's not implemented yet.
|
35
30
|
|
36
|
-
|
37
|
-
is implemented using sub-selects and a `WHERE` generated by the provided `TimeMachine` module to
|
38
|
-
be included in your models.
|
31
|
+
See [README.sql][] for a SQL example defining the machinery for a simple table.
|
39
32
|
|
40
|
-
The `WHERE` is optimized using a spatial GiST index in which time is represented represented by
|
41
|
-
boxes and the filtering is done using the overlapping (`&&`) geometry operator.
|
42
33
|
|
43
|
-
|
44
|
-
setting.
|
34
|
+
## Active Record integration
|
45
35
|
|
46
|
-
|
47
|
-
|
36
|
+
All Active Record schema migration statements are decorated with code that
|
37
|
+
handles the temporal structure by e.g. keeping the triggers in sync or
|
38
|
+
dropping/recreating it when required by your migrations.
|
39
|
+
|
40
|
+
Data extraction at a single point in time and even `JOIN`s between temporal and
|
41
|
+
non-temporal data is implemented using sub-selects and a `WHERE` generated by
|
42
|
+
the provided `TimeMachine` module to be included in your models.
|
43
|
+
|
44
|
+
The `WHERE` is optimized using [GiST indexes][] on the `tsrange` defining
|
45
|
+
record validity. Overlapping history is prevented through [exclusion
|
46
|
+
constraints][] and the [btree_gist][] extension.
|
47
|
+
|
48
|
+
All timestamps are _forcibly_ stored in as UTC, bypassing the
|
49
|
+
`default_timezone` setting.
|
48
50
|
|
49
51
|
|
50
52
|
## Requirements
|
51
53
|
|
52
|
-
* Ruby
|
53
|
-
* Active Record
|
54
|
-
* PostgreSQL
|
54
|
+
* Ruby >= 1.9.3
|
55
|
+
* Active Record >= 4.0
|
56
|
+
* PostgreSQL >= 9.3
|
57
|
+
* The `btree_gist` PostgreSQL extension
|
55
58
|
|
56
59
|
|
57
60
|
## Installation
|
58
61
|
|
59
62
|
Add this line to your application's Gemfile:
|
60
63
|
|
61
|
-
gem 'chrono_model', :
|
64
|
+
gem 'chrono_model', github: 'ifad/chronomodel'
|
62
65
|
|
63
66
|
And then execute:
|
64
67
|
|
65
68
|
$ bundle
|
66
69
|
|
67
70
|
|
68
|
-
##
|
71
|
+
## Configuration
|
69
72
|
|
70
|
-
|
73
|
+
Configure your `config/database.yml` to use the `chronomodel` adapter:
|
74
|
+
|
75
|
+
```yaml
|
76
|
+
development:
|
77
|
+
adapter: chronomodel
|
78
|
+
username: ...
|
79
|
+
```
|
80
|
+
|
81
|
+
## Schema creation
|
71
82
|
|
72
|
-
|
83
|
+
ChronoModel hooks all `ActiveRecord::Migration` methods to make them temporal
|
84
|
+
aware.
|
73
85
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
86
|
+
```ruby
|
87
|
+
create_table :countries, temporal: true do |t|
|
88
|
+
t.string :common_name
|
89
|
+
t.references :currency
|
90
|
+
# ...
|
91
|
+
end
|
92
|
+
```
|
79
93
|
|
80
|
-
|
81
|
-
|
82
|
-
by the other schema statements. E.g.:
|
94
|
+
This creates the _temporal_ table, its inherited _history_ one the _public_
|
95
|
+
view and all the trigger machinery. Every other housekeeping of the temporal
|
96
|
+
structure is handled behind the scenes by the other schema statements. E.g.:
|
83
97
|
|
84
|
-
* `rename_table` - renames tables, views, sequences, indexes and
|
98
|
+
* `rename_table` - renames tables, views, sequences, indexes and triggers
|
85
99
|
* `drop_table` - drops the temporal table and all dependant objects
|
86
|
-
* `add_column` - adds the column to the current table and updates
|
87
|
-
* `rename_column` - renames the current table column and updates the
|
88
|
-
* `remove_column` - removes the current table column and updates the
|
89
|
-
* `add_index` - creates the index in
|
90
|
-
* `remove_index` - removes the index from
|
100
|
+
* `add_column` - adds the column to the current table and updates triggers
|
101
|
+
* `rename_column` - renames the current table column and updates the triggers
|
102
|
+
* `remove_column` - removes the current table column and updates the triggers
|
103
|
+
* `add_index` - creates the index in both _temporal_ and _history_ tables
|
104
|
+
* `remove_index` - removes the index from both tables
|
91
105
|
|
92
106
|
|
93
107
|
## Adding Temporal extensions to an existing table
|
94
108
|
|
95
109
|
Use `change_table`:
|
96
110
|
|
97
|
-
|
111
|
+
```ruby
|
112
|
+
change_table :your_table, temporal: true
|
113
|
+
```
|
98
114
|
|
99
115
|
If you want to also set up the history from your current data:
|
100
116
|
|
101
|
-
|
117
|
+
```ruby
|
118
|
+
change_table :your_table, temporal: true, copy_data: true
|
119
|
+
```
|
102
120
|
|
103
121
|
This will create an history record for each record in your table, setting its
|
104
|
-
validity from midnight, January 1st, 1 CE. You can set a specific validity
|
105
|
-
|
122
|
+
validity from midnight, January 1st, 1 CE. You can set a specific validity with
|
123
|
+
the `:validity` option:
|
124
|
+
|
125
|
+
```ruby
|
126
|
+
change_table :your_table, :temporal => true, :copy_data => true, :validity => '1977-01-01'
|
127
|
+
```
|
128
|
+
|
129
|
+
Please note that `change_table` requires you to use *old_style* `up` and
|
130
|
+
`down` migrations. It cannot work with Rails 3-style `change` migrations.
|
131
|
+
|
132
|
+
## Selective Journaling
|
133
|
+
|
134
|
+
By default UPDATEs only to the `updated_at` field are not recorded in the
|
135
|
+
history.
|
136
|
+
|
137
|
+
You can also choose which fields are to be journaled, passing the following
|
138
|
+
options to `create_table`:
|
139
|
+
|
140
|
+
* `:journal => %w( fld1 fld2 .. .. )` - record changes in the history only when changing specified fields
|
141
|
+
* `:no_journal => %w( fld1 fld2 .. )` - do not record changes to the specified fields
|
142
|
+
* `:full_journal => true` - record changes to *all* fields, including `updated_at`.
|
143
|
+
|
144
|
+
These options are stored as JSON in the [COMMENT][] area of the public view,
|
145
|
+
alongside with the ChronoModel version that created them.
|
106
146
|
|
107
|
-
|
147
|
+
This is visible in `psql` if you issue a `\d+`. Example after a test run:
|
148
|
+
|
149
|
+
chronomodel=# \d+
|
150
|
+
List of relations
|
151
|
+
Schema | Name | Type | Owner | Size | Description
|
152
|
+
--------+---------------+----------+-------------+------------+-----------------------------------------------------------------
|
153
|
+
public | bars | view | chronomodel | 0 bytes | {"temporal":true,"chronomodel":"0.7.0.alpha"}
|
154
|
+
public | foos | view | chronomodel | 0 bytes | {"temporal":true,"chronomodel":"0.7.0.alpha"}
|
155
|
+
public | plains | table | chronomodel | 0 bytes |
|
156
|
+
public | test_table | view | chronomodel | 0 bytes | {"temporal":true,"journal":["foo"],"chronomodel":"0.7.0.alpha"}
|
108
157
|
|
109
158
|
|
110
159
|
## Data querying
|
111
160
|
|
112
|
-
|
113
|
-
plain table. If you want to do as-of-date queries, you need to include the
|
114
|
-
`ChronoModel::TimeMachine` module in your model.
|
161
|
+
Include the `ChronoModel::TimeMachine` module in your model.
|
115
162
|
|
116
|
-
|
117
|
-
|
163
|
+
```ruby
|
164
|
+
module Country < ActiveRecord::Base
|
165
|
+
include ChronoModel::TimeMachine
|
118
166
|
|
119
|
-
|
120
|
-
|
167
|
+
has_many :compositions
|
168
|
+
end
|
169
|
+
```
|
121
170
|
|
122
|
-
This will create a `Country::History` model inherited from `Country`, and
|
123
|
-
|
171
|
+
This will create a `Country::History` model inherited from `Country`, and add
|
172
|
+
an `as_of` class method.
|
124
173
|
|
125
|
-
|
174
|
+
```ruby
|
175
|
+
Country.as_of(1.year.ago)
|
176
|
+
```
|
126
177
|
|
127
178
|
Will execute:
|
128
179
|
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
box(
|
136
|
-
point( date_part( 'epoch', "history"."addresses"."valid_from" ), 0 ),
|
137
|
-
point( date_part( 'epoch', "history"."addresses"."valid_to" ), 0 ),
|
138
|
-
)
|
139
|
-
) AS "countries"
|
180
|
+
```sql
|
181
|
+
SELECT "countries".* FROM (
|
182
|
+
SELECT "history"."countries".* FROM "history"."countries"
|
183
|
+
WHERE '#{1.year.ago}' <@ "history"."countries"."validity"
|
184
|
+
) AS "countries"
|
185
|
+
```
|
140
186
|
|
141
|
-
|
187
|
+
The returned `ActiveRecord::Relation` will then hold and pass along the
|
188
|
+
timestamp given to the first `.as_of()` call to queries on associated entities.
|
189
|
+
E.g.:
|
142
190
|
|
143
|
-
|
191
|
+
```ruby
|
192
|
+
Country.as_of(1.year.ago).first.compositions
|
193
|
+
```
|
144
194
|
|
145
195
|
Will execute:
|
146
196
|
|
147
|
-
|
148
|
-
|
197
|
+
```sql
|
198
|
+
SELECT "countries".*, '#{1.year.ago}' AS as_of_time FROM (
|
199
|
+
SELECT "history"."countries".* FROM "history"."countries"
|
200
|
+
WHERE '#{1.year.ago}' <@ "history"."countries"."validity"
|
201
|
+
) AS "countries" LIMIT 1
|
202
|
+
```
|
149
203
|
|
150
|
-
|
151
|
-
SELECT "history"."compositions".* FROM "history"."compositions"
|
152
|
-
WHERE box(
|
153
|
-
point( date_part( 'epoch', '#{above_timestamp}'::timestamp ), 0 ),
|
154
|
-
point( date_part( 'epoch', '#{above_timestamp}'::timestamp ), 0 )
|
155
|
-
) &&
|
156
|
-
box(
|
157
|
-
point( date_part( 'epoch', "history"."addresses"."valid_from" ), 0 ),
|
158
|
-
point( date_part( 'epoch', "history"."addresses"."valid_to" ), 0 ),
|
159
|
-
)
|
160
|
-
) AS "compositions" WHERE country_id = X
|
204
|
+
and then, using the above fetched `as_of_time` timestamp, expand to:
|
161
205
|
|
162
|
-
|
206
|
+
```sql
|
207
|
+
SELECT * FROM (
|
208
|
+
SELECT "history"."compositions".* FROM "history"."compositions"
|
209
|
+
WHERE '#{as_of_time}' <@ "history"."compositions"."validity"
|
210
|
+
) AS "compositions" WHERE country_id = X
|
211
|
+
```
|
163
212
|
|
164
|
-
|
213
|
+
`.joins` works as well:
|
165
214
|
|
166
|
-
|
215
|
+
```ruby
|
216
|
+
Country.as_of(1.month.ago).joins(:compositions)
|
217
|
+
```
|
167
218
|
|
168
|
-
|
169
|
-
# .. countries history query ..
|
170
|
-
) AS "countries" INNER JOIN (
|
171
|
-
# .. compositions history query ..
|
172
|
-
) AS "compositions" ON compositions.country_id = countries.id
|
219
|
+
Expands to:
|
173
220
|
|
174
|
-
|
175
|
-
|
176
|
-
|
221
|
+
```sql
|
222
|
+
SELECT "countries".* FROM (
|
223
|
+
SELECT "history"."countries".* FROM "history"."countries"
|
224
|
+
WHERE '#{1.month.ago}' <@ "history"."countries"."validity"
|
225
|
+
) AS "countries" INNER JOIN (
|
226
|
+
SELECT "history"."compositions".* FROM "history"."compositions"
|
227
|
+
WHERE '#{1.month.ago}' <@ "history"."compositions"."validity"
|
228
|
+
) AS "compositions" ON compositions.country_id = countries.id
|
229
|
+
```
|
177
230
|
|
231
|
+
More methods are provided, see the [TimeMachine][] source for more information.
|
178
232
|
|
179
233
|
## Running tests
|
180
234
|
|
181
|
-
You need a running
|
235
|
+
You need a running PostgreSQL 9.3 instance. Create `spec/config.yml` with the
|
182
236
|
connection authentication details (use `spec/config.yml.example` as template).
|
183
237
|
|
184
|
-
|
185
|
-
|
238
|
+
You need to connect as a database superuser, because specs need to create the
|
239
|
+
`btree_gist` extension.
|
240
|
+
|
241
|
+
Run `rake`. SQL queries are logged to `spec/debug.log`. If you want to see them
|
242
|
+
in your output, use `rake VERBOSE=true`.
|
243
|
+
|
244
|
+
## Usage with JSON columns
|
245
|
+
|
246
|
+
[JSON][json-type] does not provide an [equality operator][json-func].
|
247
|
+
As both unnecessary update suppression and selective journaling require
|
248
|
+
comparing the OLD and NEW rows fields, this fails by default.
|
249
|
+
|
250
|
+
ChronoModel provides a naive JSON equality operator using a naive
|
251
|
+
comparison of JSON objects [implemented in pl/python][json-opclass].
|
252
|
+
|
253
|
+
To load the opclass you can use the `ChronoModel::Json.create`
|
254
|
+
convenience method. If you don't use JSON don't bother doing this.
|
186
255
|
|
187
256
|
## Caveats
|
188
257
|
|
189
|
-
*
|
258
|
+
* Rails 4 support requires disabling tsrange parsing support, as it
|
259
|
+
[is broken][r4-tsrange-broken] and [incomplete][r4-tsrange-incomplete]
|
260
|
+
as of now, mainly due to a [design clash with ruby][pg-tsrange-and-ruby].
|
261
|
+
|
262
|
+
* There is (yet) no upgrade path from [v0.5][chronomodel-0.5],
|
263
|
+
(PG 9.0-compatible, `box()` and hacks) to v0.6 and up (9.3-only, `tsrange`
|
264
|
+
and _less_ hacks).
|
265
|
+
|
266
|
+
* The triggers and temporal indexes cannot be saved in schema.rb. The AR
|
190
267
|
schema dumper is quite basic, and it isn't (currently) extensible.
|
191
268
|
As we're using many database-specific features, Chronomodel forces the
|
192
269
|
usage of the `:sql` schema dumper, and included rake tasks override
|
193
270
|
`db:schema:dump` and `db:schema:load` to do `db:structure:dump` and
|
194
|
-
`db:structure:load`.
|
195
|
-
and `db:data:load`.
|
196
|
-
|
197
|
-
* `.includes` still doesn't work, but it'll fixed.
|
198
|
-
|
199
|
-
* The history queries are very verbose, they should be factored out using a
|
200
|
-
`FUNCTION`.
|
271
|
+
`db:structure:load`.
|
272
|
+
Two helper tasks are also added, `db:data:dump` and `db:data:load`.
|
201
273
|
|
202
|
-
*
|
203
|
-
class that inherits from the PostgreSQL adapter, and that relies on some
|
204
|
-
private APIs. This should be made more maintainable, maybe by requiring
|
205
|
-
the use of `adapter: chronomodel` or `adapter: chrono_postgresql`.
|
274
|
+
* `.includes` is quirky when using `.as_of`.
|
206
275
|
|
207
|
-
*
|
208
|
-
[currently](http://archives.postgresql.org/pgsql-hackers/2012-08/msg01094.php)
|
209
|
-
no way to identify a subtransaction belonging to the current transaction.
|
210
|
-
|
211
|
-
* The choice of using subqueries instead of [Common Table Expressions](http://www.postgresql.org/docs/9.0/static/queries-with.html)
|
276
|
+
* The choice of using subqueries instead of [Common Table Expressions][]
|
212
277
|
was dictated by the fact that CTEs [currently acts as an optimization
|
213
|
-
fence]
|
214
|
-
If it will be possible [to opt-out of the
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
resulting queries are much more readable, and do not inhibit using
|
219
|
-
`.from()` from ARel.
|
278
|
+
fence][cte-optimization-fence].
|
279
|
+
If it will be possible [to opt-out of the fence][cte-opt-out-fence] in
|
280
|
+
the future, they will be probably be used again as they were [in the
|
281
|
+
past][chronomodel-cte-impl], because the resulting queries were more
|
282
|
+
readable, and do not inhibit using `.from()` on the `AR::Relation`.
|
220
283
|
|
221
284
|
|
222
285
|
## Contributing
|
223
286
|
|
224
287
|
1. Fork it
|
225
288
|
2. Create your feature branch (`git checkout -b my-new-feature`)
|
226
|
-
3. Commit your changes (`git commit -am 'Added some feature'`)
|
289
|
+
3. Commit your changes (`git commit -am 'Added some great feature'`)
|
227
290
|
4. Push to the branch (`git push origin my-new-feature`)
|
228
291
|
5. Create new Pull Request
|
292
|
+
|
293
|
+
|
294
|
+
[build-status]: https://travis-ci.org/ifad/chronomodel
|
295
|
+
[build-status-badge]: https://travis-ci.org/ifad/chronomodel.png?branch=master
|
296
|
+
[deps-status]: https://gemnasium.com/ifad/chronomodel
|
297
|
+
[deps-status-badge]: https://gemnasium.com/ifad/chronomodel.png
|
298
|
+
[code-analysis]: https://codeclimate.com/github/ifad/chronomodel
|
299
|
+
[code-analysis-badge]: https://codeclimate.com/github/ifad/chronomodel.png
|
300
|
+
|
301
|
+
[updatable views]: http://www.postgresql.org/docs/9.3/static/sql-createview.html#SQL-CREATEVIEW-UPDATABLE-VIEWS
|
302
|
+
[table inheritance]: http://www.postgresql.org/docs/9.3/static/ddl-inherit.html
|
303
|
+
[INSTEAD OF triggers]: http://www.postgresql.org/docs/9.3/static/sql-createtrigger.html
|
304
|
+
[wp-scd-2]: http://en.wikipedia.org/wiki/Slowly_changing_dimension#Type_2
|
305
|
+
[wp-scd-4]: http://en.wikipedia.org/wiki/Slowly_changing_dimension#Type_4
|
306
|
+
[triggers]: http://www.postgresql.org/docs/9.3/static/trigger-definition.html
|
307
|
+
[schema]: http://www.postgresql.org/docs/9.3/static/ddl-schemas.html
|
308
|
+
[inherits]: http://www.postgresql.org/docs/9.3/static/ddl-inherit.html
|
309
|
+
[`current_timestamp`]: http://www.postgresql.org/docs/9.3/interactive/functions-datetime.html#FUNCTIONS-DATETIME-TABLE
|
310
|
+
|
311
|
+
[Partitioning]: http://www.postgresql.org/docs/9.3/static/ddl-partitioning.html)
|
312
|
+
[partitioning-excl-constraints]: http://www.postgresql.org/docs/9.3/static/ddl-partitioning.html#DDL-PARTITIONING-CONSTRAINT-EXCLUSION
|
313
|
+
[README.sql]: https://github.com/ifad/chronomodel/blob/master/README.sql
|
314
|
+
[GiST indexes]: http://www.postgresql.org/docs/9.3/static/gist.html
|
315
|
+
[exclusion constraints]: http://www.postgresql.org/docs/9.3/static/sql-createtable.html#SQL-CREATETABLE-EXCLUDE
|
316
|
+
[btree_gist]: http://www.postgresql.org/docs/9.3/static/btree-gist.html
|
317
|
+
[COMMENT]: http://www.postgresql.org/docs/9.3/static/sql-comment.html
|
318
|
+
[TimeMachine]: https://github.com/ifad/chronomodel/blob/master/lib/chrono_model/time_machine.rb
|
319
|
+
|
320
|
+
[r4-tsrange-broken]: https://github.com/rails/rails/pull/13793#issuecomment-34608093
|
321
|
+
[r4-tsrange-incomplete]: https://github.com/rails/rails/issues/14010)
|
322
|
+
[pg-tsrange-and-ruby]: https://bugs.ruby-lang.org/issues/6864
|
323
|
+
[chronomodel-0.5]: https://github.com/ifad/chronomodel/tree/c2daa0f
|
324
|
+
[Common Table Expressions]: http://www.postgresql.org/docs/9.3/static/queries-with.html
|
325
|
+
[cte-optimization-fence]: http://archives.postgresql.org/pgsql-hackers/2012-09/msg00700.php
|
326
|
+
[cte-opt-out-fence]: http://archives.postgresql.org/pgsql-hackers/2012-10/msg00024.php
|
327
|
+
[chronomodel-cte-impl]: https://github.com/ifad/chronomodel/commit/18f4c4b
|
328
|
+
|
329
|
+
[json-type]: http://www.postgresql.org/docs/9.3/static/datatype-json.html
|
330
|
+
[json-func]: http://www.postgresql.org/docs/9.3/static/functions-json.html
|
331
|
+
[json-opclass]: https://github.com/ifad/chronomodel/blob/master/sql/json_ops.sql
|