oplogjam 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f3e28985e83d5c6975c304d1c074123dcb76252d
4
+ data.tar.gz: 47d3168f31751fe699f0cb1234e2e4d242dea7e4
5
+ SHA512:
6
+ metadata.gz: f3f1c4c27bcffad1182bf376fa3bcab07735299deeaf750607bc9f4f6c42caa2e0cc38da30462636a0f8c2e07e7426ff9be02fb979570bf9383db1be12102d24
7
+ data.tar.gz: 5888d3534fb4e01d200af5d3c562e7efe844419514f84c04dcd6901f94888f9137e529a2227d768125dbc70215ba701d5f6d550d8eb3e355fe7fc117a7ad6b1f
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2017 Paul Mucur
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,738 @@
1
+ # Oplogjam [![Build Status](https://travis-ci.org/mudge/oplogjam.svg)](https://travis-ci.org/mudge/oplogjam)
2
+
3
+ **Current version:** 0.1.0
4
+ **Supported Ruby versions:** 2.0, 2.1, 2.2
5
+ **Supported MongoDB versions:** 2.4, 2.6, 3.0, 3.2, 3.4
6
+ **Supported PostgreSQL versions:** 9.5, 9.6
7
+
8
+ An experiment in writing a "safe" MongoDB oplog tailer that converts documents to PostgreSQL JSONB in Ruby.
9
+
10
+ Based on experiences running [Stripe's now deprecated MoSQL project](https://github.com/stripe/mosql) in production, this project provides a core library which stores all MongoDB documents in the same standard table schema in PostgreSQL but leaves all configuration and orchestration to the user. This means that this library can be used to _power_ an end-to-end MoSQL replacement but does not provide all functionality itself.
11
+
12
+ At its heart, the library connects to a [MongoDB replica set oplog](https://docs.mongodb.com/manual/core/replica-set-oplog/) and provides an abstraction to users so they can iterate over the operations in the oplog and transform those into equivalent PostgreSQL SQL statements.
13
+
14
+ ```ruby
15
+ DB = Sequel.connect('postgres:///acme')
16
+ mongo = Mongo::Client.new('mongodb://localhost')
17
+
18
+ Oplogjam::Oplog.new(mongo).operations.each do |operation|
19
+ operation.apply('acme.widgets' => DB[:widgets], 'acme.anvils' => DB[:anvils])
20
+ end
21
+ ```
22
+
23
+ ## Requirements
24
+
25
+ * A [MongoDB replica set](https://docs.mongodb.com/manual/replication/);
26
+ * [PostgreSQL 9.5](https://www.postgresql.org/docs/9.5/static/release-9-5.html) or newer (for [`INSERT ON CONFLICT`](https://www.postgresql.org/docs/9.5/static/sql-insert.html#SQL-ON-CONFLICT) and [`jsonb_set`](https://www.postgresql.org/docs/9.5/static/functions-json.html#FUNCTIONS-JSON-PROCESSING-TABLE) support);
27
+ * A PostgreSQL database with the [`uuid-ossp` extension](https://www.postgresql.org/docs/current/static/uuid-ossp.html).
28
+
29
+ ## Why does `apply` take a mapping?
30
+
31
+ This library expects to replay operations on MongoDB collections on equivalent PostgreSQL tables. As the MongoDB oplog contains _all_ operations on a replica set in a single collection, you must provide a _mapping_ between MongoDB namespaces (e.g. a database and collection name such as `foo.bar` for a collection `bar` in the database `foo`) and PostgreSQL tables (represented by [Sequel datasets](https://github.com/jeremyevans/sequel/blob/master/doc/dataset_basics.rdoc)). Any operations for namespaces not included in the mapping will be ignored.
32
+
33
+ For example, if we only want to replay operations on `foo.bar` to a table `foo_bar` in PostgreSQL, we might have a mapping like so:
34
+
35
+ ```ruby
36
+ DB = Sequel.connect('postgres:///oplogjam_test')
37
+ mapping = { 'foo.bar' => DB[:foo_bar] }
38
+ ```
39
+
40
+ Then we can pass this mapping when we call `apply` on an operation, e.g.
41
+
42
+ ```ruby
43
+ oplog.operations.each do |operation|
44
+ operation.apply(mapping)
45
+ end
46
+ ```
47
+
48
+ In order for this to work, the PostgreSQL table `foo_bar` must have the following schema:
49
+
50
+ ```
51
+ Table "public.foo_bar"
52
+ Column | Type | Modifiers
53
+ ------------+-----------------------------+-------------------------------------
54
+ uuid | uuid | not null default uuid_generate_v1()
55
+ id | jsonb | not null
56
+ document | jsonb | not null
57
+ created_at | timestamp without time zone | not null
58
+ updated_at | timestamp without time zone | not null
59
+ deleted_at | timestamp without time zone |
60
+ Indexes:
61
+ "foo_bar_pkey" PRIMARY KEY, btree (uuid)
62
+ "foo_bar_id_deleted_at_key" UNIQUE CONSTRAINT, btree (id, deleted_at)
63
+ "foo_bar_id_index" UNIQUE, btree (id) WHERE deleted_at IS NULL
64
+ ```
65
+
66
+ We can create this ourselves or use [`Oplogjam::Schema`](#oplogjamschema) to do it for us:
67
+
68
+ ```ruby
69
+ schema = Oplogjam::Schema.new(DB)
70
+ schema.create_table(:foo_bar)
71
+ schema.import(collection, :foo_bar) # Optionally import data from a MongoDB collection
72
+ schema.add_indexes(:foo_bar)
73
+ ```
74
+
75
+ ## Why does this project exist?
76
+
77
+ Since maintenance of MoSQL by Stripe was ended, there have been several major changes that affect anyone designing a system to replay a MongoDB oplog in PostgreSQL:
78
+
79
+ * The MongoDB driver ecosystem was overhauled and the Ruby driver API changed significantly;
80
+ * PostgreSQL 9.5 introduced new JSONB operations such as `jsonb_set` for updating fields in JSONB objects;
81
+ * PostgreSQL 9.5 also introduced `INSERT ON CONFLICT` for effectively "upserting" duplicate records on `INSERT`.
82
+
83
+ Running MoSQL in production also revealed that we didn't need its rich support for transforming MongoDB documents into typical relational schema with typed columns but instead relied entirely on its JSONB support: effectively mirroring the MongoDB document by storing it in a single JSONB column.
84
+
85
+ With that specific use case in mind, I wanted to explore whether a library to _safely_ transform arbitrary MongoDB operations into SQL could be done in Ruby and remain somewhat idiomatic.
86
+
87
+ ## Why doesn't this project come with some sort of executable?
88
+
89
+ While the library is more opinionated about the data schema of PostgreSQL, it doesn't attempt to make any decisions about how you decide which collections you want to replicate and where they should be replicated to. Similarly, connecting to databases, logging, etc. are all left up to the user as something that can might differ wildly.
90
+
91
+ While in future I may add an executable, for now this is up to the user to manage.
92
+
93
+ ## API Documentation
94
+
95
+ * [`Oplogjam::Oplog`](#oplogjamoplog)
96
+ * [`Oplogjam::Oplog.new(client)`](#oplogjamoplognewclient)
97
+ * [`Oplogjam::Oplog#operations([query])`](#oplogjamoplogoperationsquery)
98
+ * [`Oplogjam::Schema`](#oplogjamschema)
99
+ * [`Oplogjam::Schema.new(db)`](#oplogjamschemadb)
100
+ * [`Oplogjam::Schema#create_table(name)`](#oplogjamschemacreate_tablename)
101
+ * [`Oplogjam::Schema#add_indexes(name)`](#oplogjamschemaadd_indexesname)
102
+ * [`Oplogjam::Schema#import(collection, name, batch_size = 100)`](#oplogjamschemaimportcollection-name-batch_size)
103
+ * [`Oplogjam::Operation`](#oplogjamoperation)
104
+ * [`Oplogjam::Operation.from(bson)`](#oplogjamoperationfrombson)
105
+ * [`Oplogjam::Noop`](#oplogjamnoop)
106
+ * [`Oplogjam::Noop.from(bson)`](#oplogjamnoopfrombson)
107
+ * [`Oplogjam::Noop#message`](#oplogjamnoopmessage)
108
+ * [`Oplogjam::Noop#id`](#oplogjamnoopid)
109
+ * [`Oplogjam::Noop#timestamp`](#oplogjamnooptimestamp)
110
+ * [`Oplogjam::Noop#ts`](#oplogjamnoopts)
111
+ * [`Oplogjam::Noop#==(other)`](#oplogjamnoopother)
112
+ * [`Oplogjam::Noop#apply(mapping)`](#oplogjamnoopapplymapping)
113
+ * [`Oplogjam::Insert`](#oplogjaminsert)
114
+ * [`Oplogjam::Insert.from(bson)`](#oplogjaminsertfrombson)
115
+ * [`Oplogjam::Insert#id`](#oplogjaminsertid)
116
+ * [`Oplogjam::Insert#namespace`](#oplogjaminsertnamespace)
117
+ * [`Oplogjam::Insert#document`](#oplogjaminsertdocument)
118
+ * [`Oplogjam::Insert#timestamp`](#oplogjaminserttimestamp)
119
+ * [`Oplogjam::Insert#ts`](#oplogjaminsertts)
120
+ * [`Oplogjam::Insert#==(other)`](#oplogjaminsertother)
121
+ * [`Oplogjam::Insert#apply(mapping)`](#oplogjaminsertapplymapping)
122
+ * [`Oplogjam::Update`](#oplogjamupdate)
123
+ * [`Oplogjam::Update.from(bson)`](#oplogjamupdatefrombson)
124
+ * [`Oplogjam::Update#id`](#oplogjamupdateid)
125
+ * [`Oplogjam::Update#namespace`](#oplogjamupdatenamespace)
126
+ * [`Oplogjam::Update#update`](#oplogjamupdateupdate)
127
+ * [`Oplogjam::Update#query`](#oplogjamupdatequery)
128
+ * [`Oplogjam::Update#timestamp`](#oplogjamupdatetimestamp)
129
+ * [`Oplogjam::Update#ts`](#oplogjamupdatets)
130
+ * [`Oplogjam::Update#==(other)`](#oplogjamupdateother)
131
+ * [`Oplogjam::Update#apply(mapping)`](#oplogjamupdateapplymapping)
132
+ * [`Oplogjam::Delete`](#oplogjamdelete)
133
+ * [`Oplogjam::Delete.from(bson)`](#oplogjamdeletefrombson)
134
+ * [`Oplogjam::Delete#id`](#oplogjamdeleteid)
135
+ * [`Oplogjam::Delete#namespace`](#oplogjamdeletenamespace)
136
+ * [`Oplogjam::Delete#query`](#oplogjamdeletequery)
137
+ * [`Oplogjam::Delete#timestamp`](#oplogjamdeletetimestamp)
138
+ * [`Oplogjam::Delete#ts`](#oplogjamdeletets)
139
+ * [`Oplogjam::Delete#==(other)`](#oplogjamdeleteother)
140
+ * [`Oplogjam::Delete#apply(mapping)`](#oplogjamdeleteapplymapping)
141
+ * [`Oplogjam::ApplyOps`](#oplogjamapplyops)
142
+ * [`Oplogjam::ApplyOps.from(bson)`](#oplogjamapplyopsfrombson)
143
+ * [`Oplogjam::ApplyOps#id`](#oplogjamapplyopsid)
144
+ * [`Oplogjam::ApplyOps#namespace`](#oplogjamapplyopsnamespace)
145
+ * [`Oplogjam::ApplyOps#operations`](#oplogjamapplyopsoperations)
146
+ * [`Oplogjam::ApplyOps#timestamp`](#oplogjamapplyopstimestamp)
147
+ * [`Oplogjam::ApplyOps#ts`](#oplogjamapplyopsts)
148
+ * [`Oplogjam::ApplyOps#==(other)`](#oplogjamapplyopsother)
149
+ * [`Oplogjam::ApplyOps#apply(mapping)`](#oplogjamapplyopsapplymapping)
150
+ * [`Oplogjam::Command`](#oplogjamcommand)
151
+ * [`Oplogjam::Command.from(bson)`](#oplogjamcommandfrombson)
152
+ * [`Oplogjam::Command#id`](#oplogjamcommandid)
153
+ * [`Oplogjam::Command#namespace`](#oplogjamcommandnamespace)
154
+ * [`Oplogjam::Command#command`](#oplogjamcommandcommand)
155
+ * [`Oplogjam::Command#timestamp`](#oplogjamcommandtimestamp)
156
+ * [`Oplogjam::Command#ts`](#oplogjamcommandts)
157
+ * [`Oplogjam::Command#==(other)`](#oplogjamcommandother)
158
+ * [`Oplogjam::Command#apply(mapping)`](#oplogjamcommandapplymapping)
159
+
160
+ ### `Oplogjam::Oplog`
161
+
162
+ An object representing a MongoDB oplog.
163
+
164
+ #### `Oplogjam::Oplog.new(client)`
165
+
166
+ ```ruby
167
+ mongo = Mongo::Client.new('mongodb://localhost')
168
+ Oplogjam::Oplog.new(mongo)
169
+ ```
170
+
171
+ Return a new [`Oplogjam::Oplog`](#oplogjamoplog) for the given [`Mongo::Client`](http://api.mongodb.com/ruby/current/Mongo/Client.html) `client` connected to a replica set.
172
+
173
+ #### `Oplogjam::Oplogjam#operations([query])`
174
+
175
+ ```ruby
176
+ oplog.operations.each do |operation|
177
+ # Do something with operation
178
+ end
179
+
180
+ oplog.operations('ts' => { '$gt' => BSON::Timestamp.new(123456, 1) })
181
+ ```
182
+
183
+ Return an infinite `Enumerator` yielding [`Operation`](#oplogjamoperation)s from the [`Oplog`](#oplogjamoplog) with an optional MongoDB `query` which will affect the results from the underlying oplog.
184
+
185
+ ### `Oplogjam::Schema`
186
+
187
+ A class to manage the PostgreSQL schema used by Oplogjam (e.g. creating tables, importing data, etc.).
188
+
189
+ #### `Oplogjam::Schema.new(db)`
190
+
191
+ ```ruby
192
+ DB = Sequel.connect('postgres:///oplogjam_test')
193
+ schema = Oplogjam::Schema.new(DB)
194
+ ```
195
+
196
+ Return a new [`Oplogjam::Schema`](#oplogjamschema) for the given [Sequel database connection](http://sequel.jeremyevans.net/rdoc/classes/Sequel/Database.html).
197
+
198
+ #### `Oplogjam::Schema#create_table(name)`
199
+
200
+ ```ruby
201
+ schema.create_table(:foo_bar)
202
+ ```
203
+
204
+ Attempt to create a table for Oplogjam's use in PostgreSQL with the given `name` if it doesn't already exist. Note that the `name` may be a single `String`, `Symbol` or a [Sequel qualified identifier](https://github.com/jeremyevans/sequel/blob/master/doc/sql.rdoc#identifiers) if you're using [PostgreSQL schema](https://www.postgresql.org/docs/current/static/ddl-schemas.html).
205
+
206
+ A table will be created with the following schema:
207
+
208
+ * `uuid`: a UUID v1 primary key (v1 so that they are sequential);
209
+ * `id`: a `jsonb` representation of the primary key of the MongoDB document;
210
+ * `document`: a `jsonb` representation of the entire MongoDB document;
211
+ * `created_at`: the `timestamp` when this row was created by Oplogjam (_not_ by MongoDB);
212
+ * `updated_at`: the `timestamp` when this row was last updated by Oplogjam (_not_ by MongoDB);
213
+ * `deleted_at`: the `timestamp` when this row was deleted by Oplogjam (_not_ by MongoDB).
214
+
215
+ If the table already exists, the method will do nothing.
216
+
217
+ #### `Oplogjam::Schema#add_indexes(name)`
218
+
219
+ ```ruby
220
+ schema.add_indexes(name)
221
+ ```
222
+
223
+ Add the following indexes and constraints to the table with the given `name` if they don't already exist:
224
+
225
+ * A unique index on `id` and `deleted_at` so no two records can have the same MongoDB ID and deletion time;
226
+ * A partial unique index on `id` where `deleted_at` is `NULL` so no two records can have the same ID and not be deleted.
227
+
228
+ Note that the `name` may be a single `String`, `Symbol` or a [Sequel qualified identifier](https://github.com/jeremyevans/sequel/blob/master/doc/sql.rdoc#identifiers) if you're using PostgreSQL schema.
229
+
230
+ If the indexes already exist on the given table, the method will do nothing.
231
+
232
+ #### `Oplogjam::Schema#import(collection, name)`
233
+
234
+ ```ruby
235
+ schema.import(mongo[:bar], :foo_bar)
236
+ ```
237
+
238
+ Batch import all existing documents from a given [`Mongo::Collection`](http://api.mongodb.com/ruby/current/Mongo/Collection.html) `collection` into the PostgreSQL table with the given `name`. Note that the `name` may be a single `String`, `Symbol` or [Sequel qualified identifier](https://github.com/jeremyevans/sequel/blob/master/doc/sql.rdoc#identifiers) if you're using PostgreSQL schema.
239
+
240
+ For performance, it's better to import existing data _before_ adding indexes to the table (hence the separate [`create_table`](#oplogjamschemacreate_tablename) and [`add_indexes`](#oplogjamschemaadd_indexesname) methods).
241
+
242
+ ### `Oplogjam::Operation`
243
+
244
+ A class representing a single MongoDB oplog operation.
245
+
246
+ #### `Oplogjam::Operation.from(bson)`
247
+
248
+ ```ruby
249
+ Oplogjam::Operation.from(document)
250
+ ```
251
+
252
+ Convert a BSON document representing a MongoDB oplog operation into a corresponding Ruby object:
253
+
254
+ * `Oplogjam::Noop`
255
+ * `Oplogjam::Insert`
256
+ * `Oplogjam::Update`
257
+ * `Oplogjam::Delete`
258
+ * `Oplogjam::ApplyOps`
259
+ * `Oplogjam::Command`
260
+
261
+ Raises a `Oplogjam::InvalidOperation` if the type of operation is not recognised.
262
+
263
+ ### `Oplogjam::Noop`
264
+
265
+ A class representing a MongoDB no-op.
266
+
267
+ #### `Oplogjam::Noop.from(bson)`
268
+
269
+ ```ruby
270
+ Oplogjam::Noop.from(document)
271
+ ```
272
+
273
+ Convert a BSON document representing a MongoDB oplog no-op into an `Oplogjam::Noop` instance.
274
+
275
+ Raises a `Oplogjam::InvalidNoop` error if the given document is not a valid no-op.
276
+
277
+ #### `Oplogjam::Noop#message`
278
+
279
+ ```ruby
280
+ noop.message
281
+ #=> "initiating set"
282
+ ```
283
+
284
+ Return the internal message of the no-op.
285
+
286
+ #### `Oplogjam::Noop#id`
287
+
288
+ ```ruby
289
+ noop.id
290
+ #=> -2135725856567446411
291
+ ```
292
+
293
+ Return the internal, unique identifier for the no-op.
294
+
295
+ #### `Oplogjam::Noop#timestamp`
296
+
297
+ ```ruby
298
+ noop.timestamp
299
+ #=> 2017-09-09 16:11:18 +0100
300
+ ```
301
+
302
+ Return the time of the no-op as a `Time`.
303
+
304
+ #### `Oplogjam::Noop#ts`
305
+
306
+ ```ruby
307
+ noop.ts
308
+ #=> #<BSON::Timestamp:0x007fcadfa44500 @increment=1, @seconds=1479419535>
309
+ ```
310
+
311
+ Return the raw, underlying BSON Timestamp of the no-op.
312
+
313
+ #### `Oplogjam::Noop#==(other)`
314
+
315
+ ```ruby
316
+ noop == other_noop
317
+ #=> false
318
+ ```
319
+
320
+ Compares the identifiers of two no-ops and returns true if they are equal.
321
+
322
+ #### `Oplogjam::Noop#apply(mapping)`
323
+
324
+ ```ruby
325
+ noop.apply('foo.bar' => DB[:bar])
326
+ ```
327
+
328
+ Apply this no-op to a mapping of MongoDB namespaces (e.g. `foo.bar`) to Sequel datasets representing PostgreSQL tables. As no-ops do nothing, this performs no operation.
329
+
330
+ ### `Oplogjam::Insert`
331
+
332
+ A class representing a MongoDB insert.
333
+
334
+ #### `Oplogjam::Insert.from(bson)`
335
+
336
+ ```ruby
337
+ Oplogjam::Insert.from(document)
338
+ ```
339
+
340
+ Convert a BSON document representing a MongoDB oplog insert into an `Oplogjam::Insert` instance.
341
+
342
+ Raises a `Oplogjam::InvalidInsert` error if the given document is not a valid insert.
343
+
344
+ #### `Oplogjam::Insert#id`
345
+
346
+ ```ruby
347
+ insert.id
348
+ #=> -2135725856567446411
349
+ ```
350
+
351
+ Return the internal, unique identifier for the insert.
352
+
353
+ #### `Oplogjam::Insert#namespace`
354
+
355
+ ```ruby
356
+ insert.namespace
357
+ #=> "foo.bar"
358
+ ```
359
+
360
+ Return the namespace the insert affects. This will be a `String` of the form `database.collection`, e.g. `foo.bar`.
361
+
362
+ #### `Oplogjam::Insert#document`
363
+
364
+ ```ruby
365
+ insert.document
366
+ #=> {"_id"=>1}
367
+ ```
368
+
369
+ Return the `BSON::Document` being inserted.
370
+
371
+ #### `Oplogjam::Insert#timestamp`
372
+
373
+ ```ruby
374
+ insert.timestamp
375
+ #=> 2017-09-09 16:11:18 +0100
376
+ ```
377
+
378
+ Return the time of the insert as a `Time`.
379
+
380
+ #### `Oplogjam::Insert#ts`
381
+
382
+ ```ruby
383
+ insert.ts
384
+ #=> #<BSON::Timestamp:0x007fcadfa44500 @increment=1, @seconds=1479419535>
385
+ ```
386
+
387
+ Return the raw, underlying BSON Timestamp of the insert.
388
+
389
+ #### `Oplogjam::Insert#==(other)`
390
+
391
+ ```ruby
392
+ insert == other_insert
393
+ #=> false
394
+ ```
395
+
396
+ Compares the identifiers of two inserts and returns true if they are equal.
397
+
398
+ #### `Oplogjam::Insert#apply(mapping)`
399
+
400
+ ```ruby
401
+ insert.apply('foo.bar' => DB[:bar])
402
+ ```
403
+
404
+ Apply this insert to a mapping of MongoDB namespaces (e.g. `foo.bar`) to Sequel datasets representing PostgreSQL tables. If the namespace of the insert maps to a dataset in the mapping, insert this document into the dataset with the following values:
405
+
406
+ * A unique UUID v1 identifier;
407
+ * The value of the document's `_id` stored as a JSONB value;
408
+ * The entire document stored as a JSONB value;
409
+ * The current time as `created_at`;
410
+ * The current time as `updated_at`.
411
+
412
+ ### `Oplogjam::Update`
413
+
414
+ A class representing a MongoDB update.
415
+
416
+ #### `Oplogjam::Update.from(bson)`
417
+
418
+ ```ruby
419
+ Oplogjam::Update.from(document)
420
+ ```
421
+
422
+ Convert a BSON document representing a MongoDB oplog update into an `Oplogjam::Update` instance.
423
+
424
+ Raises a `Oplogjam::InvalidUpdate` error if the given document is not a valid update.
425
+
426
+ #### `Oplogjam::Update#id`
427
+
428
+ ```ruby
429
+ update.id
430
+ #=> -2135725856567446411
431
+ ```
432
+
433
+ Return the internal, unique identifier for the update.
434
+
435
+ #### `Oplogjam::Update#namespace`
436
+
437
+ ```ruby
438
+ update.namespace
439
+ #=> "foo.bar"
440
+ ```
441
+
442
+ Return the namespace the update affects. This will be a `String` of the form `database.collection`, e.g. `foo.bar`.
443
+
444
+ #### `Oplogjam::Update#update`
445
+
446
+ ```ruby
447
+ update.update
448
+ #=> {"$set"=>{"name"=>"Alice"}}
449
+ ```
450
+
451
+ Return the update to be applied as a `BSON::Document`.
452
+
453
+ #### `Oplogjam::Update#query`
454
+
455
+ ```ruby
456
+ update.query
457
+ #=> {"_id"=>1}
458
+ ```
459
+
460
+ Return the query identifying which documents should be updated as a `BSON::Document`.
461
+
462
+ #### `Oplogjam::Update#timestamp`
463
+
464
+ ```ruby
465
+ update.timestamp
466
+ #=> 2017-09-09 16:11:18 +0100
467
+ ```
468
+
469
+ Return the time of the update as a `Time`.
470
+
471
+ #### `Oplogjam::Update#ts`
472
+
473
+ ```ruby
474
+ update.ts
475
+ #=> #<BSON::Timestamp:0x007fcadfa44500 @increment=1, @seconds=1479419535>
476
+ ```
477
+
478
+ Return the raw, underlying BSON Timestamp of the update.
479
+
480
+ #### `Oplogjam::Update#==(other)`
481
+
482
+ ```ruby
483
+ update == other_update
484
+ #=> false
485
+ ```
486
+
487
+ Compares the identifiers of two updates and returns true if they are equal.
488
+
489
+ #### `Oplogjam::Update#apply(mapping)`
490
+
491
+ ```ruby
492
+ update.apply('foo.bar' => DB[:bar])
493
+ ```
494
+
495
+ Apply this update to a mapping of MongoDB namespaces (e.g. `foo.bar`) to Sequel datasets representing PostgreSQL tables. If the namespace of the update maps to a dataset in the mapping, perform the update by finding the relevant row based on the query and transforming the MongoDB update into an equivalent PostgreSQL update.
496
+
497
+ This will also update the `updated_at` column of the target document to the current time.
498
+
499
+ ### `Oplogjam::Delete`
500
+
501
+ A class representing a MongoDB deletion.
502
+
503
+ #### `Oplogjam::Delete.from(bson)`
504
+
505
+ ```ruby
506
+ Oplogjam::Delete.from(document)
507
+ ```
508
+
509
+ Convert a BSON document representing a MongoDB oplog delete into an `Oplogjam::Delete` instance.
510
+
511
+ Raises a `Oplogjam::InvalidDelete` error if the given document is not a valid delete.
512
+
513
+ #### `Oplogjam::Delete#id`
514
+
515
+ ```ruby
516
+ delete.id
517
+ #=> -2135725856567446411
518
+ ```
519
+
520
+ Return the internal, unique identifier for the delete.
521
+
522
+ #### `Oplogjam::Delete#namespace`
523
+
524
+ ```ruby
525
+ delete.namespace
526
+ #=> "foo.bar"
527
+ ```
528
+
529
+ Return the namespace the delete affects. This will be a `String` of the form `database.collection`, e.g. `foo.bar`.
530
+
531
+ #### `Oplogjam::Delete#query`
532
+
533
+ ```ruby
534
+ delete.query
535
+ #=> {"_id"=>1}
536
+ ```
537
+
538
+ Return the query identifying which documents should be deleted as a `BSON::Document`.
539
+
540
+ #### `Oplogjam::Delete#timestamp`
541
+
542
+ ```ruby
543
+ delete.timestamp
544
+ #=> 2017-09-09 16:11:18 +0100
545
+ ```
546
+
547
+ Return the time of the delete as a `Time`.
548
+
549
+ #### `Oplogjam::Delete#ts`
550
+
551
+ ```ruby
552
+ delete.ts
553
+ #=> #<BSON::Timestamp:0x007fcadfa44500 @increment=1, @seconds=1479419535>
554
+ ```
555
+
556
+ Return the raw, underlying BSON Timestamp of the delete.
557
+
558
+ #### `Oplogjam::Delete#==(other)`
559
+
560
+ ```ruby
561
+ delete == other_delete
562
+ #=> false
563
+ ```
564
+
565
+ Compares the identifiers of two deletes and returns true if they are equal.
566
+
567
+ #### `Oplogjam::Delete#apply(mapping)`
568
+
569
+ ```ruby
570
+ delete.apply('foo.bar' => DB[:bar])
571
+ ```
572
+
573
+ Apply this delete to a mapping of MongoDB namespaces (e.g. `foo.bar`) to Sequel datasets representing PostgreSQL tables. If the namespace of the delete maps to a dataset in the mapping, perform a soft deletion by finding the relevant row based on the query and setting `deleted_at` to the current time.
574
+
575
+ This will also update the `updated_at` column of the target document to the current time.
576
+
577
+ ### `Oplogjam::ApplyOps`
578
+
579
+ A class representing a series of MongoDB operations in a single operation.
580
+
581
+ #### `Oplogjam::ApplyOps.from(bson)`
582
+
583
+ ```ruby
584
+ Oplogjam::ApplyOps.from(document)
585
+ ```
586
+
587
+ Convert a BSON document representing a MongoDB oplog apply ops into an `Oplogjam::ApplyOps` instance.
588
+
589
+ Raises a `Oplogjam::InvalidApplyOps` error if the given document is not a valid apply ops.
590
+
591
+ #### `Oplogjam::ApplyOps#id`
592
+
593
+ ```ruby
594
+ apply_ops.id
595
+ #=> -2135725856567446411
596
+ ```
597
+
598
+ Return the internal, unique identifier for the apply ops.
599
+
600
+ #### `Oplogjam::ApplyOps#namespace`
601
+
602
+ ```ruby
603
+ apply_ops.namespace
604
+ #=> "foo.bar"
605
+ ```
606
+
607
+ Return the namespace the apply ops affects. This will be a `String` of the form `database.collection`, e.g. `foo.bar`.
608
+
609
+ #### `Oplogjam::ApplyOps#operations`
610
+
611
+ ```ruby
612
+ apply_ops.operations
613
+ #=> [#<Oplogjam::Insert ...>]
614
+ ```
615
+
616
+ Return the operations within the apply ops as Oplogjam operations of the appropriate type.
617
+
618
+ #### `Oplogjam::ApplyOps#timestamp`
619
+
620
+ ```ruby
621
+ apply_ops.timestamp
622
+ #=> 2017-09-09 16:11:18 +0100
623
+ ```
624
+
625
+ Return the time of the apply ops as a `Time`.
626
+
627
+ #### `Oplogjam::ApplyOps#ts`
628
+
629
+ ```ruby
630
+ apply_ops.ts
631
+ #=> #<BSON::Timestamp:0x007fcadfa44500 @increment=1, @seconds=1479419535>
632
+ ```
633
+
634
+ Return the raw, underlying BSON Timestamp of the apply ops.
635
+
636
+ #### `Oplogjam::ApplyOps#==(other)`
637
+
638
+ ```ruby
639
+ apply_ops == other_apply_ops
640
+ #=> false
641
+ ```
642
+
643
+ Compares the identifiers of two apply ops and returns true if they are equal.
644
+
645
+ #### `Oplogjam::ApplyOps#apply(mapping)`
646
+
647
+ ```ruby
648
+ apply_ops.apply('foo.bar' => DB[:bar])
649
+ ```
650
+
651
+ Apply all of the operations inside this apply ops to a mapping of MongoDB namespaces (e.g. `foo.bar`) to Sequel datasets representing PostgreSQL tables. If the namespace of the operations map to a dataset in the mapping, apply them as described in each operation type's `apply` method.
652
+
653
+ ### `Oplogjam::Command`
654
+
655
+ A class representing a MongoDB command.
656
+
657
+ #### `Oplogjam::Command.from(bson)`
658
+
659
+ ```ruby
660
+ Oplogjam::Command.from(document)
661
+ ```
662
+
663
+ Convert a BSON document representing a MongoDB oplog command into an `Oplogjam::Command` instance.
664
+
665
+ Raises a `Oplogjam::InvalidCommand` error if the given document is not a valid command.
666
+
667
+ #### `Oplogjam::Command#id`
668
+
669
+ ```ruby
670
+ command.id
671
+ #=> -2135725856567446411
672
+ ```
673
+
674
+ Return the internal, unique identifier for the command.
675
+
676
+ #### `Oplogjam::Command#namespace`
677
+
678
+ ```ruby
679
+ command.namespace
680
+ #=> "foo.bar"
681
+ ```
682
+
683
+ Return the namespace the command affects. This will be a `String` of the form `database.collection`, e.g. `foo.bar`.
684
+
685
+ #### `Oplogjam::Command#command`
686
+
687
+ ```ruby
688
+ command.command
689
+ #=> {"create"=>"bar"}
690
+ ```
691
+
692
+ Return the contents of the command as a `BSON::Document`.
693
+
694
+ #### `Oplogjam::Command#timestamp`
695
+
696
+ ```ruby
697
+ command.timestamp
698
+ #=> 2017-09-09 16:11:18 +0100
699
+ ```
700
+
701
+ Return the time of the command as a `Time`.
702
+
703
+ #### `Oplogjam::Command#ts`
704
+
705
+ ```ruby
706
+ command.ts
707
+ #=> #<BSON::Timestamp:0x007fcadfa44500 @increment=1, @seconds=1479419535>
708
+ ```
709
+
710
+ Return the raw, underlying BSON Timestamp of the command.
711
+
712
+ #### `Oplogjam::Command#==(other)`
713
+
714
+ ```ruby
715
+ command == other_command
716
+ #=> false
717
+ ```
718
+
719
+ Compares the identifiers of two commands and returns true if they are equal.
720
+
721
+ #### `Oplogjam::Command#apply(mapping)`
722
+
723
+ ```ruby
724
+ command.apply('foo.bar' => DB[:bar])
725
+ ```
726
+
727
+ Apply this command to a mapping of MongoDB namespaces (e.g. `foo.bar`) to Sequel datasets representing PostgreSQL tables. As commands have no equivalent in PostgreSQL, this performs no operation.
728
+
729
+ ## Acknowledgements
730
+
731
+ * [Stripe's MoSQL](https://github.com/stripe/mosql)
732
+ * [Stripe's Mongoriver](https://github.com/stripe/mongoriver/)
733
+
734
+ ## License
735
+
736
+ Copyright © 2017 Paul Mucur.
737
+
738
+ Distributed under the MIT License.