Merge lp:~kevin-wright-1/u1db-qt/synchronizer-18-juni-i into lp:u1db-qt
- synchronizer-18-juni-i
- Merge into trunk
Status: | Merged |
---|---|
Merged at revision: | 100 |
Proposed branch: | lp:~kevin-wright-1/u1db-qt/synchronizer-18-juni-i |
Merge into: | lp:u1db-qt |
Diff against target: |
2269 lines (+1981/-7) (has conflicts) 13 files modified
CMakeLists.txt (+2/-2) examples/u1db-qt-example-6/u1db-qt-example-6.qdoc (+387/-0) examples/u1db-qt-example-6/u1db-qt-example-6.qml (+227/-0) modules/U1db/CMakeLists.txt (+4/-0) modules/U1db/plugin.cpp (+2/-0) src/CMakeLists.txt (+5/-1) src/database.cpp (+319/-2) src/database.h (+20/-1) src/index.cpp (+13/-1) src/query.cpp (+9/-0) src/query.h (+2/-0) src/synchronizer.cpp (+879/-0) src/synchronizer.h (+112/-0) Text conflict in src/database.cpp Text conflict in src/index.cpp |
To merge this branch: | bzr merge lp:~kevin-wright-1/u1db-qt/synchronizer-18-juni-i |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
U1DB Qt developers | Pending | ||
Review via email: mp+170623@code.launchpad.net |
Commit message
Description of the change
Cris Dywan (kalikiana) wrote : | # |
- 100. By Kevin Wright
-
Migrated listTransaction
sSince function from Syncrhonizer class to Database class. Previously this class used the path of a database file rather than an instance of an active Database class' database connection. With the function now hosted by the Database class itself the path of the file and opening of the database connection within the scope of the function is no longer required (i.e. the class' active database connection is used instead). - 101. By Kevin Wright
-
Migrated getSyncLogInfoF
romLocalDb function from Synchronizer class to Database class, and renamed getSyncLogInfo. Previously this class used the path of a database file rather than an instance of an active Database class' database connection. With the function now hosted by the Database class itself the path of the file and opening of the database connection within the scope of the function is no longer required (i.e. the class' active database connection is used instead). This function is used by the Synchronizer class' getLastSyncInfo rmation function to retrieve data from the source and target databases. The data represents information that one database has about the other the last time they synced (if ever). - 102. By Kevin Wright
-
Fixed a regression relating to the active source Database for Synchronizer. In the function syncLocalToLocal is the ability to overide the main source database (the 'source' property) with another database when a query is defined within an individual 'target' definition (with the key 'source_query').
- 103. By Kevin Wright
-
Added the initial steps for sync with a remote database. There are several steps in the archtitecure for remote sync, whereby the server and client communicate information between each other (e.g. previous sync transations, new data). This commit consists of the first step, where the client initiates the sync transaction and the server responds with known information about previous syncs if any. Also in this commit is the first roughed in code for the second stage of a sync transaction, where the client (aka source) begins sharing information about new data since the previous sync between the two databases (if any previous transaction occured).
- 104. By Kevin Wright
-
This commit continues the work to support the second stage of a local to remote sync transaction, where the client (aka source) begins sharing information about new data since the previous sync between the two databases (if any previous transaction occured). The code now is up to the point of initiating the post to the remote server, but at this time is showing bad request errors. The post is not sending complete data at this time, which may be the reason for the bad request error.
- 105. By Kevin Wright
-
This commit continues the work to support the second stage of a local to remote sync transaction, where the client (aka source) begins sharing information about new data since the previous sync between the two databases (if any previous transaction occured). The post is sending complete data as of this commit, but the server is still replying with a bad request error. On the surface everything appears to comply with the high level architecture documentation, but something is obviously still incorrect.
Preview Diff
1 | === modified file 'CMakeLists.txt' |
2 | --- CMakeLists.txt 2013-05-01 22:49:50 +0000 |
3 | +++ CMakeLists.txt 2013-07-04 14:13:28 +0000 |
4 | @@ -7,11 +7,12 @@ |
5 | # Dependencies |
6 | include(FindPkgConfig) |
7 | find_package(Qt5Core REQUIRED) |
8 | +find_package(Qt5Network REQUIRED) |
9 | find_package(Qt5Sql REQUIRED) |
10 | add_definitions(-DWITHQT5=1) |
11 | |
12 | set(U1DB_QT_LIBNAME u1db-qt5) |
13 | -set(QT_PKGCONFIG_DEPENDENCIES "Qt5Core Qt5Quick Qt5Sql") |
14 | +set(QT_PKGCONFIG_DEPENDENCIES "Qt5Core Qt5Network Qt5Quick Qt5Sql") |
15 | set(QT_U1DB_PKGCONFIG_FILE lib${U1DB_QT_LIBNAME}.pc) |
16 | |
17 | # Build flags |
18 | @@ -39,4 +40,3 @@ |
19 | install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${QT_U1DB_PKGCONFIG_FILE} |
20 | DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/pkgconfig |
21 | ) |
22 | - |
23 | |
24 | === added directory 'examples/u1db-qt-example-6' |
25 | === added file 'examples/u1db-qt-example-6/u1db-qt-example-6.qdoc' |
26 | --- examples/u1db-qt-example-6/u1db-qt-example-6.qdoc 1970-01-01 00:00:00 +0000 |
27 | +++ examples/u1db-qt-example-6/u1db-qt-example-6.qdoc 2013-07-04 14:13:28 +0000 |
28 | @@ -0,0 +1,387 @@ |
29 | +/*! |
30 | + |
31 | +\page u1db-qt-tutorial-6.html |
32 | + |
33 | +\title U1Db-Qt Synchronizing Tutorial |
34 | + |
35 | +This tutorial is designed to demonstrate a variety of essential U1Db-Qt functionality and usage, including: |
36 | + |
37 | +\list 1 |
38 | + \li Synchronizing two databases |
39 | + \li Utilizing the U1db-Qt Index element |
40 | + \li Various approaches to define U1db-Qt Document elements when using the Index element |
41 | + \li Partnering the U1db-Qt Index element and a QML ListView element |
42 | +\endlist |
43 | + |
44 | +\section1 Storing Data |
45 | + |
46 | +\section2 The Database Element |
47 | + |
48 | +\section3 Creating a Database |
49 | + |
50 | +A Database is very simple to create. It only needs an id and a path where the file will be created. A Database is a model, which can be used by elements, such as the ListView further in this example. |
51 | + |
52 | +\code |
53 | +U1db.Database { |
54 | + id: aDatabase |
55 | + path: "aDatabase4" |
56 | +} |
57 | +\endcode |
58 | + |
59 | +\section1 The Document Element |
60 | + |
61 | +\section2 Declaring Documents (at Runtime) |
62 | + |
63 | +A Document can be instantiated at runtime, or generated dynamically. The examples below demonstrate the former. |
64 | + |
65 | +A very basic Document could include its unique 'id' and 'docId' properties. While it is not mandatory to define these properties, in some cases they can be convenient references. More advanced applications would likely find these very useful, and in some cases may be an absolute necessity to achieve the objectives of the program. |
66 | + |
67 | +This example of a very simple Document will not initially do anything, until more properties are added and defined: |
68 | + |
69 | +\code |
70 | +U1db.Document { |
71 | + id: aDocument1 |
72 | + docId: 'helloworld1' |
73 | +} |
74 | +\endcode |
75 | + |
76 | +A basic but still practical Document definition contains several essential properties. In addition to 'id' and 'docId' (discussed above), the 'database', 'create', and 'defaults' properties are also very important, and are introduced below. |
77 | + |
78 | +The 'database' property ensures that the Document is attached to an already defined (or possibly soon to be defined one) identified by its id (in this case 'aDatabase'). For example: |
79 | + |
80 | +\code |
81 | +U1db.Document { |
82 | + id: aDocument1 |
83 | + database: aDatabase |
84 | + docId: 'helloworld1' |
85 | +} |
86 | +\endcode |
87 | + |
88 | +Should the Database not already contain a Document with the same docId ('hellowworld1' in this example) when a 'create' property is present and set to true it will be generated. For example: |
89 | + |
90 | +\code |
91 | +U1db.Document { |
92 | + id: aDocument1 |
93 | + database: aDatabase |
94 | + docId: 'helloworld1' |
95 | + create: true |
96 | +} |
97 | +\endcode |
98 | + |
99 | +However, the Document still requires some data to be useful, which is what the 'defaults' property provides. The value of 'defaults' is a map of data that will be stored in the database (again when the create property is et to true). It contain key:value pairs, where the value can be a string, number, or nested object (e.g. additional fields, lists). For example: |
100 | + |
101 | +\code |
102 | +U1db.Document { |
103 | + id: aDocument1 |
104 | + database: aDatabase |
105 | + docId: 'helloworld1' |
106 | + create: true |
107 | + defaults:{"hello": { "world": { "message":"Hello World", "id": 1 } } } |
108 | +} |
109 | +\endcode |
110 | + |
111 | +As mentioned above, lists can also be nested in Document data. Lists provide a convenient method for producing multiple instances of the same key (AKA 'field' or 'sub-field'). The example code below shows valid use of the 'message' and 'id' sub-fields multiple times within the same object. |
112 | + |
113 | +\code |
114 | +U1db.Document { |
115 | + id: aDocument2 |
116 | + database: aDatabase |
117 | + docId: 'helloworld2' |
118 | + create: true |
119 | + defaults:{"hello": { "world": [ |
120 | + { "message":"Hello World", "id": 2 }, |
121 | + { "message":"Hello World", "id": 2.5 } |
122 | + ] } } |
123 | +} |
124 | +\endcode |
125 | + |
126 | +When the default Javascript Object Notation itself is formatted with appropriate line breaks and indentation, it becomes easier to visualize an embedded list, containing sub-fields 'message' and 'id' (and their respective values): |
127 | + |
128 | +\code |
129 | +{"hello": |
130 | + { "world": |
131 | + [ |
132 | + { "message":"Hello World", "id": 2 }, |
133 | + { "message":"Hello World", "id": 2.5 } |
134 | + ] |
135 | + } |
136 | +} |
137 | +\endcode |
138 | + |
139 | +In dot notation these sub-fields are represented by 'hello.world.message' and 'hello.world.id' respectively. Later in this tutorial these will be utilized within the 'expression' property of U1Db-Qt's Index element, in close collaboration with a QML ListView's delegates. |
140 | + |
141 | + |
142 | +Normally when a docId already exists in a database, and when the set flag is set to true, the value in 'defaults' will be ignored (and the existing data in the database will remain untouched). Sometimes a developer needs to easily overwrite the data in an existing document. The 'contents' property can be used for just that purpose. When 'contents' is defined, its value will replace existing data in the database, for the document identified by the docId. In addition, 'contents' can be used to add new documents, in the same way as the 'create: true' + 'defaults' combination does; in other words, if the document defined by 'docId' does not exist it will be created. |
143 | + |
144 | +\code |
145 | +U1db.Document { |
146 | + id: aDocument3 |
147 | + database: aDatabase |
148 | + docId: 'helloworld3' |
149 | + contents:{"hello": { "world": [ |
150 | + { "message":"Hello World", "id": 3 }, |
151 | + { "message":"Hello World", "id": 3.33 }, |
152 | + { "message":"Hello World", "id": 3.66 } |
153 | + ] } } |
154 | +} |
155 | +\endcode |
156 | + |
157 | +If 'defaults' exists, 'create' is set to 'true' (or 'false' for that matter) and 'contents' is also defined, it is the latter that takes precidence. In other words, 'create' and 'defaults' will be ignored. The following example demonstrates this scenario: |
158 | + |
159 | +\code |
160 | +U1db.Document { |
161 | + id: aDocument3 |
162 | + database: aDatabase |
163 | + docId: 'helloworld3' |
164 | + create: true |
165 | + default:{"hello": { "world": [{ "message":"Hello World", "id": 3 }] } } |
166 | + contents:{"hello": { "world": [ |
167 | + { "message":"Hello World", "id": 3 }, |
168 | + { "message":"Hello World", "id": 3.33 }, |
169 | + { "message":"Hello World", "id": 3.66 } |
170 | + ] } } |
171 | +} |
172 | +\endcode |
173 | + |
174 | +This snippet simply represents the absence of the 'create' property, which is synonymous with 'create: false'. The Document can still be recognized within the application, but until applicable properties (such as those outlined above) are added and/or modified then nothing will be added or modified in the database, and this instance may have very little practical value. |
175 | + |
176 | +\code |
177 | +U1db.Document { |
178 | + id: aDocument4 |
179 | + database: aDatabase |
180 | + docId: 'helloworld4' |
181 | + defaults:{"hello": { "world": { "message":"Hello World", "id": 4 } } } |
182 | +} |
183 | +\endcode |
184 | + |
185 | +\section3 Samples of Stored Documents |
186 | + |
187 | +The data stored in the database after defining the above Document elements (and then running the application, will consist of the following: |
188 | + |
189 | +\table |
190 | +\header |
191 | + \li docId |
192 | + \li content |
193 | +\row |
194 | + |
195 | + \li 'helloworld1' |
196 | + \li |
197 | +\code |
198 | +{ |
199 | + "hello": { |
200 | + "world": { |
201 | + "id": 1, |
202 | + "message": "Hello World" |
203 | + } |
204 | + } |
205 | +} |
206 | + |
207 | +\endcode |
208 | + |
209 | + |
210 | +\row |
211 | + |
212 | + \li 'helloworld2' |
213 | + \li |
214 | +\code |
215 | +{ |
216 | + "hello": { |
217 | + "world": [ |
218 | + { |
219 | + "id": 2, |
220 | + "message": "Hello World" |
221 | + }, |
222 | + { |
223 | + "id": 2.5, |
224 | + "message": "Hello World" |
225 | + } |
226 | + ] |
227 | + } |
228 | +} |
229 | +\endcode |
230 | + |
231 | +\row |
232 | + |
233 | + \li 'helloworld3' |
234 | + \li |
235 | +\code |
236 | +{ |
237 | + "hello": { |
238 | + "world": [ |
239 | + { |
240 | + "id": 3, |
241 | + "message": "Hello World" |
242 | + }, |
243 | + { |
244 | + "id": 3.33, |
245 | + "message": "Hello World" |
246 | + }, |
247 | + { |
248 | + "id": 3.66, |
249 | + "message": "Hello World" |
250 | + } |
251 | + ] |
252 | + } |
253 | +} |
254 | +\endcode |
255 | + |
256 | + |
257 | +\endtable |
258 | + |
259 | +\section1 Retrieving Data |
260 | + |
261 | +To retrieve the Documents that were declared earlier requires two additional elements: Index and Query. |
262 | + |
263 | +\section2 The Index Element |
264 | + |
265 | +\section3 Creating and Index Element |
266 | + |
267 | +The Index element requires both a unique 'id' and a pointer to a 'database' in order to begin becoming useful, as demonstrated here: |
268 | + |
269 | +\code |
270 | +U1db.Index{ |
271 | + database: aDatabase |
272 | + id: by_helloworld |
273 | +} |
274 | +\endcode |
275 | + |
276 | +In the future, the Index element will support on disk storage of appropriate results / data. At the present time only in memory indexing is done, but once the storing capability is implemented, defining and identifying it is as simple as using the 'name' property (which will be stored in the database along with the relvent data that goes with it). The snippet below shows the use of the 'name' property: |
277 | + |
278 | +\code |
279 | +U1db.Index{ |
280 | + database: aDatabase |
281 | + id: by_helloworld |
282 | + //name: "by-helloworld" |
283 | +} |
284 | +\endcode |
285 | + |
286 | +The Index element describes, using dot notation, the fields and sub-fields where the developer expects to find information. That information is defined in a list, and added as the value for the 'expression' property. The list can contain one or more entries, as exemplified here (the property is commented out due to its current status): |
287 | + |
288 | +\code |
289 | +U1db.Index{ |
290 | + database: aDatabase |
291 | + id: by_helloworld |
292 | + //name: "by-helloworld" |
293 | + expression: ["hello.world.id","hello.world.message"] |
294 | +} |
295 | +\endcode |
296 | + |
297 | +\section2 The QueryElement |
298 | + |
299 | +\section3 Creating a Query Element |
300 | + |
301 | +The Query element has two responsibilities: a bridge from Database+Index to other parts of the application, as well as further filtering of data in the database (in addition to what Index provides). |
302 | + |
303 | +In order to fulfil its duties as a bridge to an Index (and Database), the 'index' property must point to an Index element, identified by its 'id'. For example: |
304 | + |
305 | +\code |
306 | +U1db.Query{ |
307 | + id: aQuery |
308 | + index: by_helloworld |
309 | +} |
310 | +\endcode |
311 | + |
312 | +While Index helps to filter data based on 'where' it is located (e.g. field.sub-field), Query helps determine the additional set of criteria for 'what' is being searched for. The intent of the 'query' property is to provide the mechanism for defnining the search criteria, but at the time of writing that functionality is not yet available. However, once the implementation is in place, using it is only requires defining the property's value (e.g. "Hello World"). Wild card searches using '*' are supported, which is the default query (i.e. if 'query' is not set it is assumed to be '*'). For example (the property is commented out due to its current status): |
313 | + |
314 | +\code |
315 | +U1db.Query{ |
316 | + id: aQuery |
317 | + index: by_helloworld |
318 | + //query: "*" |
319 | +} |
320 | +\endcode |
321 | + |
322 | +When the 'query' property becomes available, only wildcard search definitions for "starts with" will be suppoprted. Thus the following would be supported: |
323 | + |
324 | +\code |
325 | +U1db.Query{ |
326 | + id: aQuery |
327 | + index: by_helloworld |
328 | + //query: "Hello*" |
329 | +} |
330 | +\endcode |
331 | + |
332 | + |
333 | +But this would not: |
334 | + |
335 | +\code |
336 | +U1db.Query{ |
337 | + id: aQuery |
338 | + index: by_helloworld |
339 | + //query: "*World" |
340 | +} |
341 | +\endcode |
342 | + |
343 | +Note: again, the 'query' property is commented out in the above two snippets due to its current status |
344 | + |
345 | +\section1 Using Data |
346 | + |
347 | +\section2 Data and the Application UI |
348 | + |
349 | +\section3 Using Data With Models and Views |
350 | + |
351 | +This simple snippet represents how to attach a ListModel to a ListView. In this instance the model 'aQuery' is representative of the Query + Index combination defined earlier: |
352 | + |
353 | +\code |
354 | +ListView { |
355 | + width: units.gu(45) |
356 | + height: units.gu(80) |
357 | + model: aQuery |
358 | +} |
359 | +\endcode |
360 | + |
361 | +\section4 Data and Delegates |
362 | + |
363 | +How a model and ListView + delegates work together is a common QML concept, and not specific to U1Db-Qt. However, the asynchronous nature of this relationship is important to understand. When using QML ListView, delegates will be created based on particular properties such as the size of the application window, ListView, and delegate itself (amongst other factors). Each delegate can then represent a Document retrieved from the Database based on the record's index. This example demonstrates some of the property definitions that contribute to determining the number of delegates a ListView will contain: |
364 | + |
365 | +\code |
366 | +ListView { |
367 | + width: units.gu(45) |
368 | + height: units.gu(80) |
369 | + model: aQuery |
370 | + |
371 | + delegate: Text { |
372 | + x: 66; y: 77 |
373 | + } |
374 | + |
375 | +} |
376 | +\endcode |
377 | + |
378 | + |
379 | +When the number of Documents is less than or equal to the number of delegates then there is a one to one mapping of index to delegate (e.g. the first delegate will represent the Document with an index = 0; the second, index = 1; and so on). |
380 | + |
381 | +When there are more Documents than delegates the ListView will request a new index depending on the situation (e.g. a user scrolls up or down). For example, if a ListView has 10 delegates, but 32 Documents to handle, when a user initially scrolls the first delegate will change from representing the Document with index = 0 to the Document that might have index = 8; the second, from index = 1 to index = 9; ...; the 10th delegate from index = 9 to index = 17. A second scrolling gesture the first index may change to 15, and the final index 24. And so on. Scrolling in the opposite direction will have a similar effect, but the Document index numbers for each delegate will obviously start to decline (towards their original values). |
382 | + |
383 | +The following snippet, which modifies the above delegate definition, could demonstrate this effect if there were enough Documents to do so (i.e. some number greater than the number of delegates): |
384 | + |
385 | +\code |
386 | +ListView { |
387 | + width: units.gu(45) |
388 | + height: units.gu(80) |
389 | + model: aQuery |
390 | + |
391 | + delegate: Text { |
392 | + x: 66; y: 77 |
393 | + text: index |
394 | + } |
395 | + |
396 | +} |
397 | +\endcode |
398 | + |
399 | +The object called 'contents' contains one or more properties. This example demonstrates the retrieval of data based on the U1db.Index defined earlier (id: by-helloworld). In this instance the Index contained two expressions simultaniously, "hello.world.id" and "hello.world.message" |
400 | + |
401 | +\code |
402 | +ListView { |
403 | + width: units.gu(45) |
404 | + height: units.gu(80) |
405 | + model: aQuery |
406 | + |
407 | + delegate: Text { |
408 | + x: 66; y: 77 |
409 | + text: "(" + index + ") '" + contents.message + " " + contents.id + "'" |
410 | + } |
411 | + |
412 | +} |
413 | +\endcode |
414 | + |
415 | +*/ |
416 | |
417 | === added file 'examples/u1db-qt-example-6/u1db-qt-example-6.qml' |
418 | --- examples/u1db-qt-example-6/u1db-qt-example-6.qml 1970-01-01 00:00:00 +0000 |
419 | +++ examples/u1db-qt-example-6/u1db-qt-example-6.qml 2013-07-04 14:13:28 +0000 |
420 | @@ -0,0 +1,227 @@ |
421 | +/* |
422 | + * Copyright (C) 2013 Canonical, Ltd. |
423 | + * |
424 | + * Authors: |
425 | + * Kevin Wright <kevin.wright@canonical.com> |
426 | + * |
427 | + * This program is free software; you can redistribute it and/or modify |
428 | + * it under the terms of the GNU Lesser General Public License as published by |
429 | + * the Free Software Foundation; version 3. |
430 | + * |
431 | + * This program is distributed in the hope that it will be useful, |
432 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
433 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
434 | + * GNU Lesser General Public License for more details. |
435 | + * |
436 | + * You should have received a copy of the GNU Lesser General Public License |
437 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
438 | + */ |
439 | + |
440 | +import QtQuick 2.0 |
441 | +import U1db 1.0 as U1db |
442 | +import Ubuntu.Components 0.1 |
443 | + |
444 | + |
445 | +Item { |
446 | + |
447 | + width: units.gu(45) |
448 | + height: units.gu(80) |
449 | + |
450 | + U1db.Database { |
451 | + id: aDatabase |
452 | + path: "aDatabase6" |
453 | + } |
454 | + |
455 | + U1db.Database { |
456 | + id: aTargetDatabase |
457 | + path: "aTargetDatabase6" |
458 | + } |
459 | + |
460 | + U1db.Document { |
461 | + id: aDocument1 |
462 | + database: aDatabase |
463 | + docId: 'helloworld' |
464 | + create: true |
465 | + defaults:{"hello": { "world": { "message":"Hello World", "id": 1 } } } |
466 | + |
467 | + } |
468 | + |
469 | + U1db.Document { |
470 | + id: aDocument2 |
471 | + database: aTargetDatabase |
472 | + docId: 'helloworld' |
473 | + create: true |
474 | + defaults:{"hello": { "world": [ |
475 | + { "message":"Hello World", "id": 2 }, |
476 | + { "message":"Hello World", "id": 2.5 } |
477 | + ] } } |
478 | + } |
479 | + |
480 | + U1db.Index{ |
481 | + database: aDatabase |
482 | + id: by_helloworld |
483 | + expression: ["hello.world.id","hello.world.message"] |
484 | + } |
485 | + |
486 | + U1db.Query{ |
487 | + id: aQuery |
488 | + index: by_helloworld |
489 | + query: [{"id":"*"},{"message":"Hel*"}] |
490 | + } |
491 | + |
492 | + U1db.Index{ |
493 | + database: aTargetDatabase |
494 | + id: by_target_helloworld |
495 | + expression: ["hello.world.id","hello.world.message"] |
496 | + } |
497 | + |
498 | + U1db.Query{ |
499 | + id: aTargetQuery |
500 | + index: by_target_helloworld |
501 | + query: [{"id":"*"},{"message":"Hel*"}] |
502 | + } |
503 | + |
504 | + U1db.Synchronizer{ |
505 | + id: aSynchronizer |
506 | + source: aDatabase |
507 | + targets: [{remote:true}, |
508 | + {remote:false, |
509 | + id:aTargetQuery.index.database, |
510 | + resolve_to_source:true}, |
511 | + {remote:true, |
512 | + ip:"127.0.0.1", |
513 | + port:":7777", |
514 | + name:"example1.u1db", |
515 | + resolve_to_source:true}, |
516 | + {remote:"OK"}] |
517 | + synchronize: false |
518 | + } |
519 | + |
520 | + MainView { |
521 | + |
522 | + id: u1dbView |
523 | + width: units.gu(45) |
524 | + height: units.gu(80) |
525 | + anchors.top: parent.top; |
526 | + |
527 | + Tabs { |
528 | + id: tabs |
529 | + anchors.fill: parent |
530 | + |
531 | + Tab { |
532 | + objectName: "Tab1" |
533 | + |
534 | + title: i18n.tr("Hello U1Db!") |
535 | + |
536 | + page: Page { |
537 | + id: helloPage |
538 | + |
539 | + Rectangle { |
540 | + width: units.gu(45) |
541 | + height: units.gu(40) |
542 | + anchors.top: parent.top; |
543 | + border.width: 1 |
544 | + |
545 | + Text { |
546 | + id: sourceLabel |
547 | + anchors.top: parent.top; |
548 | + font.bold: true |
549 | + text: "aDatabase6 Contents" |
550 | + } |
551 | + |
552 | + |
553 | + ListView { |
554 | + id: sourceListView |
555 | + width: units.gu(45) |
556 | + height: units.gu(20) |
557 | + anchors.top: sourceLabel.bottom; |
558 | + model: aQuery |
559 | + |
560 | + delegate: Text { |
561 | + wrapMode: Text.WordWrap |
562 | + x: 6; y: 77 |
563 | + text: { |
564 | + text: "(" + index + ") '" + contents.message + " " + contents.id + "'" |
565 | + } |
566 | + } |
567 | + } |
568 | + |
569 | + Text { |
570 | + id: targetLabel |
571 | + anchors.top: sourceListView.bottom; |
572 | + font.bold: true |
573 | + text: "aTargetDatabase6 Contents" |
574 | + } |
575 | + |
576 | + |
577 | + ListView { |
578 | + |
579 | + width: units.gu(45) |
580 | + height: units.gu(20) |
581 | + anchors.top: targetLabel.bottom; |
582 | + model: aTargetQuery |
583 | + |
584 | + delegate: Text { |
585 | + wrapMode: Text.WordWrap |
586 | + x: 6; y: 77 |
587 | + text: { |
588 | + text: "(" + index + ") '" + contents.message + " " + contents.id + "'" |
589 | + } |
590 | + } |
591 | + } |
592 | + } |
593 | + |
594 | + Rectangle { |
595 | + id: lowerRectangle |
596 | + width: units.gu(45) |
597 | + height: units.gu(35) |
598 | + anchors.bottom: parent.bottom; |
599 | + border.width: 1 |
600 | + |
601 | + Text { |
602 | + id: errorsLabel |
603 | + anchors.top: parent.top; |
604 | + font.bold: true |
605 | + text: "Log:" |
606 | + } |
607 | + |
608 | + ListView { |
609 | + |
610 | + parent: lowerRectangle |
611 | + width: units.gu(45) |
612 | + height: units.gu(30) |
613 | + anchors.top: errorsLabel.bottom; |
614 | + model: aSynchronizer |
615 | + delegate:Text { |
616 | + width: units.gu(40) |
617 | + anchors.left: parent.left |
618 | + anchors.right: parent.right |
619 | + wrapMode: Text.WordWrap |
620 | + text: { |
621 | + text: errors |
622 | + } |
623 | + } |
624 | + } |
625 | + |
626 | + Button{ |
627 | + parent: lowerRectangle |
628 | + anchors.bottom: parent.bottom; |
629 | + text: "Sync" |
630 | + onClicked: aSynchronizer.synchronize = true |
631 | + anchors.left: parent.left |
632 | + anchors.right: parent.right |
633 | + |
634 | + } |
635 | + |
636 | + } |
637 | + } |
638 | + |
639 | + } |
640 | + |
641 | + } |
642 | + |
643 | + } |
644 | + |
645 | +} |
646 | + |
647 | + |
648 | |
649 | === modified file 'modules/U1db/CMakeLists.txt' |
650 | --- modules/U1db/CMakeLists.txt 2013-05-01 22:49:50 +0000 |
651 | +++ modules/U1db/CMakeLists.txt 2013-07-04 14:13:28 +0000 |
652 | @@ -1,4 +1,5 @@ |
653 | find_package(Qt5Quick REQUIRED) |
654 | +find_package(Qt5Network REQUIRED) |
655 | |
656 | get_target_property(QMAKE_EXECUTABLE Qt5::qmake LOCATION) |
657 | # See http://doc-snapshot.qt-project.org/5.0/qtcore/qlibraryinfo.html#LibraryLocation-enum |
658 | @@ -13,6 +14,7 @@ |
659 | include_directories( |
660 | ${CMAKE_CURRENT_SOURCE_DIR} |
661 | ${Qt5Sql_INCLUDE_DIRS} |
662 | + ${Qt5Network_INCLUDE_DIRS} |
663 | ) |
664 | |
665 | add_library(U1DBPlugin SHARED ${U1DBPlugin_SRCS}) |
666 | @@ -21,6 +23,7 @@ |
667 | target_link_libraries(U1DBPlugin |
668 | ${U1DB_QT_LIBNAME} |
669 | ${Qt5Quick_LIBRARIES} |
670 | + ${Qt5Network_LIBRARIES} |
671 | ) |
672 | |
673 | include_directories( |
674 | @@ -28,6 +31,7 @@ |
675 | ${U1DB_INCLUDE_DIRS} |
676 | ${CMAKE_CURRENT_BINARY_DIR} |
677 | ${Qt5Quick_INCLUDE_DIRS} |
678 | + ${Qt5Network_INCLUDE_DIRS} |
679 | ) |
680 | |
681 | # copy qmldir file into build directory for shadow builds |
682 | |
683 | === modified file 'modules/U1db/plugin.cpp' |
684 | --- modules/U1db/plugin.cpp 2013-02-15 11:43:45 +0000 |
685 | +++ modules/U1db/plugin.cpp 2013-07-04 14:13:28 +0000 |
686 | @@ -21,6 +21,7 @@ |
687 | #include "document.h" |
688 | #include "index.h" |
689 | #include "query.h" |
690 | +#include "synchronizer.h" |
691 | #include "plugin.h" |
692 | #include <qqml.h> |
693 | |
694 | @@ -32,5 +33,6 @@ |
695 | qmlRegisterType<Document>(uri, 1, 0, "Document"); |
696 | qmlRegisterType<Index>(uri, 1, 0, "Index"); |
697 | qmlRegisterType<Query>(uri, 1, 0, "Query"); |
698 | + qmlRegisterType<Synchronizer>(uri, 1, 0, "Synchronizer"); |
699 | } |
700 | |
701 | |
702 | === modified file 'src/CMakeLists.txt' |
703 | --- src/CMakeLists.txt 2013-04-23 10:39:54 +0000 |
704 | +++ src/CMakeLists.txt 2013-07-04 14:13:28 +0000 |
705 | @@ -6,6 +6,7 @@ |
706 | document.cpp |
707 | index.cpp |
708 | query.cpp |
709 | + synchronizer.cpp |
710 | ) |
711 | |
712 | # Generated files |
713 | @@ -14,6 +15,7 @@ |
714 | moc_document.cpp |
715 | moc_index.cpp |
716 | moc_query.cpp |
717 | + moc_synchronizer.cpp |
718 | ) |
719 | set_directory_properties(PROPERTIES ADDITIONAL_MAKE_CLEAN_FILES "${U1DB_QT_GENERATED}") |
720 | |
721 | @@ -27,6 +29,7 @@ |
722 | ${CMAKE_CURRENT_SOURCE_DIR} |
723 | ${CMAKE_CURRENT_BINARY_DIR} |
724 | ${Qt5Core_INCLUDE_DIRS} |
725 | + ${Qt5Network_INCLUDE_DIRS} |
726 | ${Qt5Sql_INCLUDE_DIRS} |
727 | ${U1DB_INCLUDE_DIRS} |
728 | ) |
729 | @@ -35,6 +38,7 @@ |
730 | target_link_libraries(${U1DB_QT_LIBNAME} |
731 | ${Qt5Core_LIBRARIES} |
732 | ${Qt5Sql_LIBRARIES} |
733 | + ${Qt5Network_LIBRARIES} |
734 | ${U1DB_LDFLAGS} |
735 | ) |
736 | |
737 | @@ -50,7 +54,7 @@ |
738 | LIBRARY DESTINATION lib${LIB_SUFFIX} |
739 | ) |
740 | |
741 | -install(FILES global.h database.h document.h index.h query.h |
742 | +install(FILES global.h database.h document.h index.h query.h synchronizer.h |
743 | DESTINATION ${INCLUDE_INSTALL_DIR} |
744 | ) |
745 | |
746 | |
747 | === modified file 'src/database.cpp' |
748 | --- src/database.cpp 2013-04-29 23:54:19 +0000 |
749 | +++ src/database.cpp 2013-07-04 14:13:28 +0000 |
750 | @@ -106,7 +106,11 @@ |
751 | /* A unique ID is used for the connection name to ensure that we aren't |
752 | re-using or replacing other opend databases. */ |
753 | if (!m_db.isValid()) |
754 | +<<<<<<< TREE |
755 | m_db = QSqlDatabase::addDatabase("QSQLITE", QUuid::createUuid().toString()); |
756 | +======= |
757 | + m_db = QSqlDatabase::addDatabase("QSQLITE",QUuid::createUuid().toString()); |
758 | +>>>>>>> MERGE-SOURCE |
759 | if (!m_db.isValid()) |
760 | return setError("QSqlDatabase error"); |
761 | m_db.setDatabaseName(path); |
762 | @@ -250,6 +254,7 @@ |
763 | return QVariant(); |
764 | } |
765 | |
766 | + |
767 | /*! |
768 | Returns the contents of a document by \a docId in a form that QML recognizes |
769 | as a Variant object, it's identical to Document::getContents() with the |
770 | @@ -284,6 +289,9 @@ |
771 | return setError(QString("Failed to get document %1: %2\n%3").arg(docId).arg(query.lastError().text()).arg(query.lastQuery())) ? QVariant() : QVariant(); |
772 | } |
773 | |
774 | +/*! |
775 | + The increaseVectorClockRev(int oldRev) function is deprecated. |
776 | + */ |
777 | static int |
778 | increaseVectorClockRev(int oldRev) |
779 | { |
780 | @@ -291,6 +299,233 @@ |
781 | } |
782 | |
783 | /*! |
784 | + The getNextDocRevisionNumber(QString doc_id) function |
785 | +creates a new revision number. It returns a string for use |
786 | +in the document table's 'doc_rev' field. |
787 | + */ |
788 | + |
789 | +QString Database::getNextDocRevisionNumber(QString doc_id) |
790 | +{ |
791 | + |
792 | + QString revision_number = getReplicaUid()+":1"; |
793 | + |
794 | + QString current_revision_number = getCurrentDocRevisionNumber(doc_id); |
795 | + |
796 | + /*! |
797 | + Some revisions contain information from previous |
798 | +conflicts/syncs. Revisions are delimited by '|'. |
799 | + |
800 | + */ |
801 | + |
802 | + QStringList current_revision_list = current_revision_number.split("|"); |
803 | + |
804 | + Q_FOREACH (QString current_revision, current_revision_list) { |
805 | + |
806 | + /*! |
807 | + Each revision contains two pieces of information, |
808 | +the uid of the database that made the revsion, and a counter |
809 | +for the revsion. This information is delimited by ':'. |
810 | + |
811 | + */ |
812 | + |
813 | + QStringList current_revision_number_list = current_revision.split(":"); |
814 | + |
815 | + if(current_revision_number_list[0]==getReplicaUid()) { |
816 | + |
817 | + /*! |
818 | + If the current revision uid is the same as this Database's uid the counter portion is increased by one. |
819 | + |
820 | + */ |
821 | + |
822 | + int revision_generation_number = current_revision_number_list[1].toInt()+1; |
823 | + |
824 | + revision_number = getReplicaUid()+":"+QString::number(revision_generation_number); |
825 | + |
826 | + } |
827 | + else { |
828 | + |
829 | + /*! |
830 | + If the current revision uid is not the same as this Database's uid then the revision represents a change that originated in another database. |
831 | + |
832 | + */ |
833 | + //revision_number+="|"+current_revision; |
834 | + |
835 | + /* ##KW## Not sure if the above is necessary, |
836 | + *and did not appear to be working as intended either. |
837 | + * |
838 | + * Commented out, but maybe OK to delete. |
839 | + */ |
840 | + } |
841 | + |
842 | + } |
843 | + |
844 | + /*! |
845 | + The Database UID has curly brackets, but they are not required for the revision number and need to be removed. |
846 | + |
847 | + */ |
848 | + |
849 | + revision_number = revision_number.replace("{",""); |
850 | + |
851 | + revision_number = revision_number.replace("}",""); |
852 | + |
853 | + return revision_number; |
854 | + |
855 | +} |
856 | + |
857 | +/*! |
858 | + |
859 | + The getCurrentDocRevisionNumber(QString doc_id) function |
860 | +returns the current string value from the document table's |
861 | +doc_rev field. |
862 | + |
863 | + */ |
864 | + |
865 | +QString Database::getCurrentDocRevisionNumber(QString doc_id){ |
866 | + if (!initializeIfNeeded()) |
867 | + return QString(); |
868 | + |
869 | + QSqlQuery query(m_db.exec()); |
870 | + |
871 | + query.prepare("SELECT doc_rev from document WHERE doc_id = :docId"); |
872 | + query.bindValue(":docId", doc_id); |
873 | + if (query.exec()) |
874 | + { |
875 | + while (query.next()) |
876 | + { |
877 | + return query.value("doc_rev").toString(); |
878 | + } |
879 | + |
880 | + } |
881 | + return QString(); |
882 | +} |
883 | + |
884 | +/*! |
885 | + * \brief Database::updateSyncLog |
886 | + * \param uid |
887 | + * \param generation |
888 | + * \param transaction_id |
889 | + * |
890 | + * This method is used at the end of a synchronization session, |
891 | + * to update the database with the latest information known about the peer |
892 | + * database that was synced against. |
893 | + */ |
894 | +void Database::updateSyncLog(bool insert, QString uid, QString generation, QString transaction_id) |
895 | +{ |
896 | + |
897 | + if (!initializeIfNeeded()) |
898 | + return; |
899 | + |
900 | + QSqlQuery query(m_db.exec()); |
901 | + |
902 | + if(insert==true){ |
903 | + query.prepare("INSERT INTO sync_log(known_generation,known_transation_id,known_transation_id) VALUES(:knownGeneration, :knownTransactionId, :replicaUid)"); |
904 | + |
905 | + } |
906 | + else{ |
907 | + query.prepare("UPDATE sync_log SET known_generation = :knownGeneration, known_transation_id = :knownTransactionId WHERE replica_uid = :replicaUid"); |
908 | + } |
909 | + |
910 | + |
911 | + query.bindValue(":replicaUid", uid); |
912 | + query.bindValue(":knownGeneration", generation); |
913 | + query.bindValue(":knownTransactionId", transaction_id); |
914 | + if (!query.exec()) |
915 | + { |
916 | + qDebug() << query.lastError(); |
917 | + |
918 | + } |
919 | + |
920 | +} |
921 | + |
922 | +void Database::updateDocRevisionNumber(QString doc_id,QString revision){ |
923 | + if (!initializeIfNeeded()) |
924 | + return; |
925 | + |
926 | + QSqlQuery query(m_db.exec()); |
927 | + |
928 | + query.prepare("UPDATE document SET doc_rev = :revisionId WHERE doc_id = :docId"); |
929 | + query.bindValue(":docId", doc_id); |
930 | + query.bindValue(":revisionId", revision); |
931 | + if (!query.exec()) |
932 | + { |
933 | + qDebug() << query.lastError(); |
934 | + |
935 | + } |
936 | + |
937 | +} |
938 | + |
939 | +/*! |
940 | + The getCurrentGenerationNumber() function searches for the |
941 | +current generation number from the sqlite_sequence table. |
942 | +The return value can then be used during a synchronization session, |
943 | +amongst other things. |
944 | + |
945 | + */ |
946 | + |
947 | +int Database::getCurrentGenerationNumber(){ |
948 | + |
949 | + int sequence_number = -1; |
950 | + |
951 | + QSqlQuery query(m_db.exec()); |
952 | + |
953 | + query.prepare("SELECT seq FROM sqlite_sequence WHERE name = 'transaction_log'"); |
954 | + |
955 | + if (query.exec()) |
956 | + { |
957 | + while (query.next()) |
958 | + { |
959 | + sequence_number = (query.value("seq").toInt()); |
960 | + |
961 | + } |
962 | + |
963 | + } |
964 | + |
965 | + return sequence_number; |
966 | + |
967 | +} |
968 | + |
969 | +/*! |
970 | + The generateNewTransactionId() function generates a random |
971 | +transaction id string, for use when creating new transations. |
972 | + |
973 | + */ |
974 | + |
975 | +QString Database::generateNewTransactionId(){ |
976 | + |
977 | + QString uid = "T-"+QUuid::createUuid().toString(); |
978 | + uid = uid.replace("}",""); |
979 | + uid = uid.replace("{",""); |
980 | + return uid; |
981 | + |
982 | +} |
983 | + |
984 | +/*! |
985 | + Each time a document in the Database is created or updated a |
986 | +new transaction is performed, and information about it inserted into the |
987 | +transation_log table using the createNewTransaction(QString doc_id) |
988 | +function. |
989 | + |
990 | + */ |
991 | + |
992 | +int Database::createNewTransaction(QString doc_id){ |
993 | + |
994 | + QString transaction_id = generateNewTransactionId(); |
995 | + |
996 | + QSqlQuery query(m_db.exec()); |
997 | + |
998 | + QString queryString = "INSERT INTO transaction_log(doc_id, transaction_id) VALUES('"+doc_id+"', '"+transaction_id+"')"; |
999 | + |
1000 | + if (!query.exec(queryString)){ |
1001 | + return -1; |
1002 | + } |
1003 | + else{ |
1004 | + return 0; |
1005 | + } |
1006 | + |
1007 | + return -1; |
1008 | +} |
1009 | + |
1010 | +/*! |
1011 | Updates the existing \a contents of the document identified by \a docId if |
1012 | there's no error. |
1013 | If no \a docId is given or \a docId is an empty string the \a contents will be |
1014 | @@ -308,12 +543,16 @@ |
1015 | /* FIXME: Conflicts */ |
1016 | |
1017 | int newRev = increaseVectorClockRev(7/*contents.rev*/); |
1018 | + // ##KW## maybe this can be removed as it is replaced by revision_number. |
1019 | + |
1020 | + QString revision_number = getNextDocRevisionNumber(newOrEmptyDocId); |
1021 | + |
1022 | QSqlQuery query(m_db.exec()); |
1023 | if (oldDoc.isValid()) |
1024 | { |
1025 | query.prepare("UPDATE document SET doc_rev=:docRev, content=:docJson WHERE doc_id = :docId"); |
1026 | query.bindValue(":docId", newOrEmptyDocId); |
1027 | - query.bindValue(":docRev", newRev); |
1028 | + query.bindValue(":docRev", revision_number); |
1029 | // Parse Variant from QML as JsonDocument, fallback to string |
1030 | QString json(QJsonDocument::fromVariant(contents).toJson()); |
1031 | query.bindValue(":docJson", json.isEmpty() ? contents : json); |
1032 | @@ -323,6 +562,9 @@ |
1033 | query.bindValue(":docId", newOrEmptyDocId); |
1034 | if (!query.exec()) |
1035 | return setError(QString("Failed to delete document field %1: %2\n%3").arg(newOrEmptyDocId).arg(query.lastError().text()).arg(query.lastQuery())) ? -1 : -1; |
1036 | + |
1037 | + createNewTransaction(newOrEmptyDocId); |
1038 | + |
1039 | } |
1040 | else |
1041 | { |
1042 | @@ -333,12 +575,14 @@ |
1043 | |
1044 | query.prepare("INSERT INTO document (doc_id, doc_rev, content) VALUES (:docId, :docRev, :docJson)"); |
1045 | query.bindValue(":docId", newOrEmptyDocId); |
1046 | - query.bindValue(":docRev", newRev); |
1047 | + query.bindValue(":docRev", revision_number); |
1048 | // Parse Variant from QML as JsonDocument, fallback to string |
1049 | QJsonDocument json(QJsonDocument::fromVariant(contents)); |
1050 | query.bindValue(":docJson", json.isEmpty() ? contents : json.toJson()); |
1051 | if (!query.exec()) |
1052 | return setError(QString("Failed to put document %1: %2\n%3").arg(docId).arg(query.lastError().text()).arg(query.lastQuery())) ? -1 : -1; |
1053 | + |
1054 | + createNewTransaction(newOrEmptyDocId); |
1055 | } |
1056 | |
1057 | beginResetModel(); |
1058 | @@ -353,6 +597,14 @@ |
1059 | return newRev; |
1060 | } |
1061 | |
1062 | +void Database::resetModel(){ |
1063 | + |
1064 | + beginResetModel(); |
1065 | + endResetModel(); |
1066 | + |
1067 | +} |
1068 | + |
1069 | + |
1070 | /*! |
1071 | Returns a list of all stored documents by their docId. |
1072 | */ |
1073 | @@ -516,6 +768,71 @@ |
1074 | return list; |
1075 | } |
1076 | |
1077 | +/* Handy functions for synchronization. */ |
1078 | + |
1079 | +QList<QString> Database::listTransactionsSince(int generation){ |
1080 | + |
1081 | + QList<QString> list; |
1082 | + |
1083 | + if (!initializeIfNeeded()) |
1084 | + return list; |
1085 | + |
1086 | + QSqlQuery query(m_db.exec()); |
1087 | + |
1088 | + QString queryStmt = "SELECT generation, doc_id, transaction_id FROM transaction_log where generation > "+QString::number(generation); |
1089 | + |
1090 | + if (query.exec(queryStmt)) |
1091 | + { |
1092 | + while (query.next()) |
1093 | + { |
1094 | + list.append(query.value("generation").toString()+"|"+query.value("doc_id").toString()+"|"+query.value("transaction_id").toString()); |
1095 | + } |
1096 | + |
1097 | + return list; |
1098 | + |
1099 | + } |
1100 | + |
1101 | + return list; |
1102 | + |
1103 | +} |
1104 | + |
1105 | +QMap<QString,QVariant> Database::getSyncLogInfo(QMap<QString,QVariant> lastSyncInformation, QString uid, QString prefix){ |
1106 | + |
1107 | + if (!initializeIfNeeded()) |
1108 | + return lastSyncInformation; |
1109 | + |
1110 | + QString queryStmt = "SELECT known_transaction_id, known_generation FROM sync_log WHERE replica_uid = '"+uid +"'"; |
1111 | + |
1112 | + QSqlQuery query(m_db.exec()); |
1113 | + |
1114 | + if (query.exec(queryStmt)) |
1115 | + { |
1116 | + while (query.next()) |
1117 | + { |
1118 | + lastSyncInformation.insert(prefix + "_replica_generation", query.value(1).toInt()); |
1119 | + lastSyncInformation.insert(prefix + "_replica_transaction_id",query.value(0).toString()); |
1120 | + return lastSyncInformation; |
1121 | + } |
1122 | + |
1123 | + } |
1124 | + |
1125 | + /* Below does not work from the Database class. |
1126 | + * This is leftover from the migration of this function |
1127 | + * from the Synchronizer class. |
1128 | + * |
1129 | + * Why is it left here? As a reminder to do something to |
1130 | + * resolve the issue of not being able to update Sychnronizer's |
1131 | + * m_errors from the Database class. |
1132 | + */ |
1133 | + |
1134 | + //m_errors.append(m_db.lastError().text()); |
1135 | + |
1136 | + |
1137 | + return lastSyncInformation; |
1138 | +} |
1139 | + |
1140 | + |
1141 | + |
1142 | QT_END_NAMESPACE_U1DB |
1143 | |
1144 | #include "moc_database.cpp" |
1145 | |
1146 | === modified file 'src/database.h' |
1147 | --- src/database.h 2013-04-23 15:17:24 +0000 |
1148 | +++ src/database.h 2013-07-04 14:13:28 +0000 |
1149 | @@ -36,10 +36,12 @@ |
1150 | public: |
1151 | Database(QObject* parent = 0); |
1152 | |
1153 | + |
1154 | // QAbstractListModel |
1155 | QVariant data(const QModelIndex & index, int role = Qt::DisplayRole) const; |
1156 | QHash<int, QByteArray>roleNames() const; |
1157 | int rowCount(const QModelIndex & parent = QModelIndex()) const; |
1158 | + void resetModel(); |
1159 | |
1160 | QString getPath(); |
1161 | void setPath(const QString& path); |
1162 | @@ -51,6 +53,18 @@ |
1163 | Q_INVOKABLE QString putIndex(const QString& index_name, QStringList expressions); |
1164 | Q_INVOKABLE QStringList getIndexExpressions(const QString& indexName); |
1165 | Q_INVOKABLE QStringList getIndexKeys(const QString& indexName); |
1166 | + |
1167 | + /* Functions handy for Synchronization */ |
1168 | + QString getNextDocRevisionNumber(QString doc_id); |
1169 | + QString getCurrentDocRevisionNumber(QString doc_id); |
1170 | + void updateDocRevisionNumber(QString doc_id,QString revision); |
1171 | + void updateSyncLog(bool insert, QString uid, QString generation, QString transaction_id); |
1172 | + QList<QString> listTransactionsSince(int generation); |
1173 | + QMap<QString,QVariant> getSyncLogInfo(QMap<QString,QVariant> lastSyncInformation, QString uid, QString prefix); |
1174 | + |
1175 | + |
1176 | + |
1177 | + |
1178 | Q_SIGNALS: |
1179 | void pathChanged(const QString& path); |
1180 | void errorChanged(const QString& error); |
1181 | @@ -63,7 +77,7 @@ |
1182 | */ |
1183 | void docLoaded(const QString& docId, QVariant content) const; |
1184 | private: |
1185 | - Q_DISABLE_COPY(Database) |
1186 | + //Q_DISABLE_COPY(Database) |
1187 | QString m_path; |
1188 | QSqlDatabase m_db; |
1189 | QString m_error; |
1190 | @@ -73,6 +87,11 @@ |
1191 | bool initializeIfNeeded(const QString& path=":memory:"); |
1192 | bool setError(const QString& error); |
1193 | QString getDocIdByRow(int row) const; |
1194 | + |
1195 | + int createNewTransaction(QString doc_id); |
1196 | + QString generateNewTransactionId(); |
1197 | + int getCurrentGenerationNumber(); |
1198 | + |
1199 | }; |
1200 | |
1201 | QT_END_NAMESPACE_U1DB |
1202 | |
1203 | === modified file 'src/index.cpp' |
1204 | --- src/index.cpp 2013-05-10 13:22:44 +0000 |
1205 | +++ src/index.cpp 2013-07-04 14:13:28 +0000 |
1206 | @@ -146,7 +146,19 @@ |
1207 | if (m_expression == expression) |
1208 | return; |
1209 | |
1210 | - m_expression = expression; |
1211 | +<<<<<<< TREE |
1212 | + m_expression = expression; |
1213 | +======= |
1214 | + m_expression = expression; |
1215 | + |
1216 | + if (m_database) |
1217 | + { |
1218 | + m_database->putIndex(m_name, expression); |
1219 | + Q_EMIT dataInvalidated(); |
1220 | + } |
1221 | + |
1222 | + |
1223 | +>>>>>>> MERGE-SOURCE |
1224 | |
1225 | if (m_database) |
1226 | { |
1227 | |
1228 | === modified file 'src/query.cpp' |
1229 | --- src/query.cpp 2013-05-01 23:06:01 +0000 |
1230 | +++ src/query.cpp 2013-07-04 14:13:28 +0000 |
1231 | @@ -59,6 +59,7 @@ |
1232 | QVariant |
1233 | Query::data(const QModelIndex & index, int role) const |
1234 | { |
1235 | + |
1236 | if (role == 0) // contents |
1237 | return m_results.at(index.row()); |
1238 | if (role == 1) // docId |
1239 | @@ -150,8 +151,16 @@ |
1240 | |
1241 | } |
1242 | |
1243 | + resetModel(); |
1244 | + |
1245 | Q_EMIT documentsChanged(m_documents); |
1246 | Q_EMIT resultsChanged(m_results); |
1247 | + |
1248 | +} |
1249 | + |
1250 | +void Query::resetModel(){ |
1251 | + beginResetModel(); |
1252 | + endResetModel(); |
1253 | } |
1254 | |
1255 | /*! |
1256 | |
1257 | === modified file 'src/query.h' |
1258 | --- src/query.h 2013-04-25 13:38:33 +0000 |
1259 | +++ src/query.h 2013-07-04 14:13:28 +0000 |
1260 | @@ -52,6 +52,8 @@ |
1261 | QStringList getDocuments(); |
1262 | QList<QVariant> getResults(); |
1263 | |
1264 | + void resetModel(); |
1265 | + |
1266 | Q_SIGNALS: |
1267 | void indexChanged(Index* index); |
1268 | void queryChanged(QVariant query); |
1269 | |
1270 | === added file 'src/synchronizer.cpp' |
1271 | --- src/synchronizer.cpp 1970-01-01 00:00:00 +0000 |
1272 | +++ src/synchronizer.cpp 2013-07-04 14:13:28 +0000 |
1273 | @@ -0,0 +1,879 @@ |
1274 | +/* |
1275 | + * Copyright (C) 2013 Canonical, Ltd. |
1276 | + * |
1277 | + * Authors: |
1278 | + * Kevin Wright <kevin.wright@canonical.com> |
1279 | + * |
1280 | + * This program is free software; you can redistribute it and/or modify |
1281 | + * it under the terms of the GNU Lesser General Public License as published by |
1282 | + * the Free Software Foundation; version 3. |
1283 | + * |
1284 | + * This program is distributed in the hope that it will be useful, |
1285 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
1286 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1287 | + * GNU Lesser General Public License for more details. |
1288 | + * |
1289 | + * You should have received a copy of the GNU Lesser General Public License |
1290 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
1291 | + */ |
1292 | + |
1293 | +#include <QDebug> |
1294 | +#include <QSqlQuery> |
1295 | +#include <QFile> |
1296 | +#include <QSqlError> |
1297 | +#include <QUuid> |
1298 | +#include <QStringList> |
1299 | +#include <QJsonDocument> |
1300 | + |
1301 | +#include "synchronizer.h" |
1302 | +#include "private.h" |
1303 | + |
1304 | +QT_BEGIN_NAMESPACE_U1DB |
1305 | + |
1306 | +/*! |
1307 | + \class Synchronizer |
1308 | + \inmodule U1db |
1309 | + \ingroup modules |
1310 | + |
1311 | + \brief The Synchronizer class handles synchronizing between two databases. |
1312 | + |
1313 | +*/ |
1314 | + |
1315 | +/* |
1316 | + |
1317 | + Below this line are methods that are standards in declarative API design. They include the element instantiation, default settings for properties and get/set methods for properties. |
1318 | + |
1319 | +*/ |
1320 | + |
1321 | +/*! |
1322 | + Instantiate a new Synchronizer with an optional \a parent, usually by declaring it as a QML item. |
1323 | + |
1324 | + Synchronizer elements sync two databases together, a 'source' database and a remote or local 'target' database. |
1325 | + |
1326 | + */ |
1327 | + |
1328 | +Synchronizer::Synchronizer(QObject *parent) : |
1329 | + QAbstractListModel(parent), m_synchronize(false) |
1330 | +{ |
1331 | + QObject::connect(this, &Synchronizer::syncChanged, this, &Synchronizer::onSyncChanged); |
1332 | +} |
1333 | + |
1334 | +/*! |
1335 | + \internal |
1336 | + *Used to implement QAbstractListModel |
1337 | + *Implements the variables exposed to the Delegate in a model |
1338 | + */ |
1339 | +QVariant |
1340 | +Synchronizer::data(const QModelIndex & index, int role) const |
1341 | +{ |
1342 | + if (role == 0) // errors |
1343 | + return m_errors.at(index.row()); |
1344 | + return QVariant(); |
1345 | +} |
1346 | + |
1347 | +/*! |
1348 | + \internal |
1349 | + Used to implement QAbstractListModel |
1350 | + The number of rows: the number of documents given by the query. |
1351 | + */ |
1352 | +int |
1353 | +Synchronizer::rowCount(const QModelIndex & parent) const |
1354 | +{ |
1355 | + return m_errors.count(); |
1356 | +} |
1357 | + |
1358 | +/*! |
1359 | + \internal |
1360 | + Used to implement QAbstractListModel |
1361 | + Defines \b{errors} as the variable exposed to the Delegate in a model |
1362 | + \b{index} is supported out of the box. |
1363 | + */ |
1364 | +QHash<int, QByteArray> |
1365 | +Synchronizer::roleNames() const |
1366 | +{ |
1367 | + QHash<int, QByteArray> roles; |
1368 | + roles.insert(0, "errors"); |
1369 | + return roles; |
1370 | +} |
1371 | + |
1372 | + |
1373 | +/*! |
1374 | + \property Synchronizer::source |
1375 | + */ |
1376 | +void Synchronizer::setSource(Database* source) |
1377 | +{ |
1378 | + |
1379 | + if (m_source == source) |
1380 | + return; |
1381 | + |
1382 | + if (m_source) |
1383 | + QObject::disconnect(m_source, 0, this, 0); |
1384 | + |
1385 | + m_source = source; |
1386 | + |
1387 | + Q_EMIT sourceChanged(source); |
1388 | +} |
1389 | + |
1390 | +/*! |
1391 | + \property Synchronizer::targets |
1392 | + */ |
1393 | +void Synchronizer::setTargets(QVariant targets) |
1394 | +{ |
1395 | + |
1396 | + if (m_targets == targets) |
1397 | + return; |
1398 | + |
1399 | + //if (m_targets) |
1400 | + // QObject::disconnect(m_targets, 0, this, 0); |
1401 | + |
1402 | + m_targets = targets; |
1403 | + Q_EMIT targetsChanged(targets); |
1404 | +} |
1405 | + |
1406 | +/*! |
1407 | + \property Synchronizer::synchronize |
1408 | + */ |
1409 | +void Synchronizer::setSync(bool synchronize) |
1410 | +{ |
1411 | + |
1412 | + if (m_synchronize == synchronize) |
1413 | + return; |
1414 | + |
1415 | + m_synchronize = synchronize; |
1416 | + Q_EMIT syncChanged(synchronize); |
1417 | +} |
1418 | + |
1419 | +/*! |
1420 | + \property Synchronizer::errors |
1421 | + */ |
1422 | +void Synchronizer::setResolveToSource(bool resolve_to_source) |
1423 | +{ |
1424 | + if (m_resolve_to_source == resolve_to_source) |
1425 | + return; |
1426 | + |
1427 | + m_resolve_to_source = resolve_to_source; |
1428 | + Q_EMIT resolveToSourceChanged(resolve_to_source); |
1429 | +} |
1430 | + |
1431 | +/*! |
1432 | + \property Synchronizer::errors |
1433 | + */ |
1434 | +void Synchronizer::setErrors(QList<QString> errors) |
1435 | +{ |
1436 | + if (m_errors == errors) |
1437 | + return; |
1438 | + |
1439 | + m_errors = errors; |
1440 | + Q_EMIT errorsChanged(errors); |
1441 | +} |
1442 | + |
1443 | +/*! |
1444 | + |
1445 | + */ |
1446 | +Database* Synchronizer::getSource() |
1447 | +{ |
1448 | + return m_source; |
1449 | +} |
1450 | + |
1451 | +/*! |
1452 | + |
1453 | + */ |
1454 | +QVariant Synchronizer::getTargets() |
1455 | +{ |
1456 | + return m_targets; |
1457 | +} |
1458 | + |
1459 | + |
1460 | +/*! |
1461 | + |
1462 | + */ |
1463 | +bool Synchronizer::getSync() |
1464 | +{ |
1465 | + return m_synchronize; |
1466 | +} |
1467 | + |
1468 | + |
1469 | +/*! |
1470 | + |
1471 | + */ |
1472 | +bool Synchronizer::getResolveToSource(){ |
1473 | + return m_resolve_to_source; |
1474 | +} |
1475 | + |
1476 | +/*! |
1477 | + |
1478 | + */ |
1479 | +QList<QString> Synchronizer::getErrors(){ |
1480 | + return m_errors; |
1481 | +} |
1482 | + |
1483 | + |
1484 | + |
1485 | +/* |
1486 | + |
1487 | + Below this line represents the unique API methods that are not part of standard declarative API design. |
1488 | + |
1489 | +*/ |
1490 | + |
1491 | +/*! |
1492 | + The onSyncChanged(bool synchronize) method is where all the sync |
1493 | +magic starts to happen. |
1494 | + |
1495 | + */ |
1496 | +void Synchronizer::onSyncChanged(bool synchronize){ |
1497 | + |
1498 | + Database* source = getSource(); |
1499 | + |
1500 | + QList<QVariant> sync_targets; |
1501 | + |
1502 | + /*! |
1503 | + * The validator map contains key and value pair definitions, |
1504 | + *that are used to confirm that the values provided for each |
1505 | + *database target are of the expected type for a particular key. |
1506 | + */ |
1507 | + |
1508 | + QMap<QString,QString>validator; |
1509 | + |
1510 | + validator.insert("remote","bool"); |
1511 | + validator.insert("location","QString"); |
1512 | + validator.insert("resolve_to_source","bool"); |
1513 | + validator.insert("errors", "QStringList"); |
1514 | + |
1515 | + /*! |
1516 | + * The mandatory map contains the keys that are used to confirm |
1517 | + *that a database target definition contains all the mandatory keys |
1518 | + *necessary for synchronizing. |
1519 | + */ |
1520 | + |
1521 | + QList<QString>mandatory; |
1522 | + |
1523 | + mandatory.append("remote"); |
1524 | + mandatory.append("resolve_to_source"); |
1525 | + |
1526 | + if(synchronize == true){ |
1527 | + |
1528 | + /*! |
1529 | + A list of valid sync target databases is generated by calling the getValidTargets(validator, mandatory) method, and adding the return value to a QList (sync_targets). |
1530 | + */ |
1531 | + |
1532 | + sync_targets=getValidTargets(validator, mandatory); |
1533 | + |
1534 | + /*! |
1535 | + * Once the list of sync targets has been generated the sync activity |
1536 | + *can be initiated via the synchronizeTargets function. |
1537 | + */ |
1538 | + |
1539 | + synchronizeTargets(source, sync_targets); |
1540 | + |
1541 | + /*! |
1542 | + * After the synchronization is complete the model is reset so that |
1543 | + *log and error messages are available at the application level. |
1544 | + * |
1545 | + */ |
1546 | + |
1547 | + beginResetModel(); |
1548 | + endResetModel(); |
1549 | + |
1550 | + /*! |
1551 | + The convenience signals errorsChanged and syncCompleted are |
1552 | +emitted after the model has been reset. |
1553 | + */ |
1554 | + |
1555 | + Q_EMIT errorsChanged(m_errors); |
1556 | + Q_EMIT syncCompleted(); |
1557 | + |
1558 | + /*! |
1559 | + * The sync boolean value is reset to its default value (false) |
1560 | + *once all sync activity is complete. |
1561 | + */ |
1562 | + |
1563 | + setSync(false); |
1564 | + |
1565 | + } |
1566 | + else{ |
1567 | + |
1568 | + } |
1569 | +} |
1570 | + |
1571 | + |
1572 | +void Synchronizer::remotePostSyncInfoFinished(QNetworkReply* reply) |
1573 | +{ |
1574 | + |
1575 | + QUrl postUrl = reply->request().url(); |
1576 | + |
1577 | + QByteArray data = reply->readAll(); |
1578 | + |
1579 | + QString replyData = QString(data); |
1580 | + |
1581 | + qDebug() << replyData; |
1582 | + |
1583 | + reply->close(); |
1584 | + |
1585 | +} |
1586 | + |
1587 | +void Synchronizer::remoteGetSyncInfoFinished(QNetworkReply* reply) |
1588 | +{ |
1589 | + |
1590 | + QUrl postUrl = reply->request().url(); |
1591 | + |
1592 | + QByteArray data = reply->readAll(); |
1593 | + |
1594 | + QString replyData = QString(data); |
1595 | + |
1596 | + reply->close(); |
1597 | + |
1598 | + QVariantMap replyMap; |
1599 | + |
1600 | + QJsonDocument replyJson = QJsonDocument::fromJson(replyData.toUtf8()); |
1601 | + |
1602 | + QVariant replyVariant = replyJson.toVariant(); |
1603 | + |
1604 | + replyMap = replyVariant.toMap(); |
1605 | + |
1606 | + double source_replica_generation = replyMap["source_replica_generation"].toDouble(); |
1607 | + QString source_replica_uid = replyMap["source_replica_uid"].toString(); |
1608 | + QString source_transaction_id = replyMap["source_transaction_id"].toString(); |
1609 | + double target_replica_generation = replyMap["target_replica_generation"].toDouble(); |
1610 | + QString target_replica_transaction_id = replyMap["target_replica_transaction_id"].toString(); |
1611 | + QString target_replica_uid = replyMap["target_replica_uid"].toString(); |
1612 | + |
1613 | + QNetworkRequest request(postUrl); |
1614 | + |
1615 | + request.setHeader(QNetworkRequest::ContentTypeHeader,"application/x-u1db-sync-stream"); |
1616 | + |
1617 | + QNetworkAccessManager *manager = new QNetworkAccessManager(this); |
1618 | + |
1619 | + connect(manager, &QNetworkAccessManager::finished, this, &Synchronizer::remotePostSyncInfoFinished); |
1620 | + |
1621 | + QString postData; |
1622 | + |
1623 | + postData = "[\r\n"; |
1624 | + postData += "{\"last_known_generation\": 0, \"last_known_trans_id\": \"\"},\r\n"; |
1625 | + |
1626 | + QList<QString> transactions = m_source->listTransactionsSince(source_replica_generation); |
1627 | + |
1628 | + Q_FOREACH(QString transaction,transactions){ |
1629 | + QStringList transactionData = transaction.split("|"); |
1630 | + postData += "{\"id\": \""+transactionData[1]+"\", \"rev\": \""+m_source->getCurrentDocRevisionNumber(transactionData[1])+"\", \"content\": \"{}\", \"generation\": "+transactionData[0]+", \"trans_id\": \""+transactionData[2]+"\"},\r\n"; |
1631 | + } |
1632 | + |
1633 | + |
1634 | + postData += "]"; |
1635 | + |
1636 | + QNetworkReply *nextReply = manager->post(QNetworkRequest(request),postData.toUtf8()); |
1637 | + |
1638 | + qDebug()<<nextReply->errorString() << " " << postData.toUtf8(); |
1639 | + |
1640 | + |
1641 | +} |
1642 | + |
1643 | +/*! |
1644 | + * \brief Synchronizer::getValidTargets confirms that each sync target definition is valid. |
1645 | + * \param validator |
1646 | + * \param mandatory |
1647 | + * \return |
1648 | + */ |
1649 | + |
1650 | +QList<QVariant> Synchronizer::getValidTargets(QMap<QString,QString>validator, QList<QString>mandatory){ |
1651 | + |
1652 | + QList<QVariant> sync_targets; |
1653 | + |
1654 | + int index = 0; |
1655 | + |
1656 | + QList<QVariant> targets = getTargets().toList(); |
1657 | + |
1658 | + Q_FOREACH (QVariant target_variant, targets) |
1659 | + { |
1660 | + index++; |
1661 | + QString index_number = QString::number(index); |
1662 | + |
1663 | + QMap<QString, QVariant> target = target_variant.toMap(); |
1664 | + |
1665 | + bool valid = true; |
1666 | + bool complete = true; |
1667 | + |
1668 | + QMapIterator<QString, QVariant> i(target); |
1669 | + while (i.hasNext()) { |
1670 | + |
1671 | + i.next(); |
1672 | + |
1673 | + //qDebug() << i.value().typeName(); |
1674 | + |
1675 | + if(validator.contains(i.key())&&validator[i.key()]!=i.value().typeName()){ |
1676 | + valid = false; |
1677 | + |
1678 | + m_errors.append("<b><font color=\"red\">Database "+index_number+"</font></b>: For Key: `" + i.key() + "` Expecting type `" + validator[i.key()] + "`, but received type `" + i.value().typeName()+"`"); |
1679 | + target.insert("sync",false); |
1680 | + |
1681 | + break; |
1682 | + } |
1683 | + |
1684 | + if(valid==false){ |
1685 | + targets.removeOne(target); |
1686 | + break; |
1687 | + } |
1688 | + else{ |
1689 | + QListIterator<QString> j(mandatory); |
1690 | + |
1691 | + while(j.hasNext()){ |
1692 | + QString value = j.next(); |
1693 | + if(!target.contains(value)){ |
1694 | + m_errors.append("<b><font color=\"red\">Database "+index_number+"</font></b>: Expected key `" + value + "`, but it is not present."); |
1695 | + target.insert("sync",false); |
1696 | + targets.removeOne(target); |
1697 | + complete = false; |
1698 | + break; |
1699 | + } |
1700 | + } |
1701 | + if(complete==false){ |
1702 | + break; |
1703 | + } |
1704 | + |
1705 | + } |
1706 | + } |
1707 | + |
1708 | + if(target.contains("sync")&&target["sync"]==false){ |
1709 | + m_errors.append("<b><font color=\"red\">Error</font></b>: Database Index "+index_number+" was not synced due to errors. Please check its properties and try again later."); |
1710 | + } |
1711 | + else |
1712 | + { |
1713 | + target.insert("sync",true); |
1714 | + sync_targets.append(target); |
1715 | + } |
1716 | + |
1717 | + } |
1718 | + |
1719 | + return sync_targets; |
1720 | + |
1721 | +} |
1722 | + |
1723 | +void Synchronizer::synchronizeTargets(Database *source, QVariant targets){ |
1724 | + |
1725 | + if(targets.typeName()== QStringLiteral("QVariantList")){ |
1726 | + |
1727 | + QList<QVariant> target_list = targets.toList(); |
1728 | + |
1729 | + QListIterator<QVariant> i(target_list); |
1730 | + |
1731 | + int target_index = -1; |
1732 | + |
1733 | + while(i.hasNext()){ |
1734 | + |
1735 | + target_index++; |
1736 | + |
1737 | + QVariant target = i.next(); |
1738 | + |
1739 | + if(target.typeName()== QStringLiteral("QVariantMap")){ |
1740 | + QMap<QString,QVariant> target_map = target.toMap(); |
1741 | + |
1742 | + if(target_map.contains("remote")&&target_map["remote"]==false){ |
1743 | + if(target_map.contains("sync")&&target_map["sync"]==true){ |
1744 | + m_errors.append("<b><font color=\"green\">Log</font></b>: Valid target index "+QString::number(target_index)+" good for local to local sync with source database."); |
1745 | + syncLocalToLocal(source, target_map); |
1746 | + } |
1747 | + } |
1748 | + else if(target_map.contains("remote")&&target_map["remote"]==true){ |
1749 | + if(target_map.contains("sync")&&target_map["sync"]==true){ |
1750 | + m_errors.append("<b><font color=\"red\">Error</font></b>: Remote database sync is under construction. Valid target index "+QString::number(target_index)+" was not synced. Try again later."); |
1751 | + //ip |
1752 | + //port |
1753 | + //name |
1754 | + //GET /thedb/sync-from/my_replica_uid |
1755 | + |
1756 | + QString source_uid = getUidFromLocalDb(source->getPath()); |
1757 | + QString get_string = target_map["name"].toString()+"/sync-from/"+source_uid; |
1758 | + QString url_string = "http://"+target_map["ip"].toString(); |
1759 | + QString full_get_request = url_string+"/"+get_string; |
1760 | + |
1761 | + QNetworkAccessManager *manager = new QNetworkAccessManager(this); |
1762 | + |
1763 | + |
1764 | + QUrl url(full_get_request); |
1765 | + url.setPort(7777); |
1766 | + |
1767 | + |
1768 | + QNetworkRequest request(url); |
1769 | + |
1770 | + connect(manager, &QNetworkAccessManager::finished, this, &Synchronizer::remoteGetSyncInfoFinished); |
1771 | + |
1772 | + QNetworkReply *reply = manager->get(QNetworkRequest(request)); |
1773 | + |
1774 | + qDebug() << "Reply: " << reply->errorString(); |
1775 | + |
1776 | + } |
1777 | + } |
1778 | + else{ |
1779 | + m_errors.append("<b><font color=\"red\">Undefined Error</font></b>: Valid target index "+QString::number(target_index)+" was not synced. Please check its properties and try again later."); |
1780 | + } |
1781 | + } |
1782 | + |
1783 | + |
1784 | + } |
1785 | + |
1786 | + } |
1787 | + |
1788 | +} |
1789 | + |
1790 | +void Synchronizer::syncLocalToLocal(Database *sourceDb, QMap<QString,QVariant> target) |
1791 | +{ |
1792 | + |
1793 | + QString target_db_name = target["location"].toString(); |
1794 | + |
1795 | + Database *targetDb; |
1796 | + Index *targetIndex; |
1797 | + Query *targetQuery; |
1798 | + |
1799 | + Index *sourceIndex; |
1800 | + Query *sourceQuery; |
1801 | + |
1802 | + /* |
1803 | + * ##KW## The two lists below are currently not used for anything |
1804 | + * as they were added for functionality that turned out not to be |
1805 | + * necessary. They are left here in case they might be convenient |
1806 | + * for something else. |
1807 | + * |
1808 | + * Note: These relate to the function syncDocument's return value. |
1809 | + */ |
1810 | + |
1811 | + QList<QVariant> source_results; |
1812 | + QList<QVariant> target_results; |
1813 | + |
1814 | + if(target.contains("id")){ |
1815 | + targetDb = (Database*)target["id"].value<QObject*>(); |
1816 | + } |
1817 | + |
1818 | + if(target.contains("target_query")){ |
1819 | + targetQuery = (Query*)target["target_query"].value<QObject*>(); |
1820 | + targetIndex = targetQuery->getIndex(); |
1821 | + targetDb = targetIndex->getDatabase(); |
1822 | + } |
1823 | + |
1824 | + if(target.contains("source_query")){ |
1825 | + sourceQuery = (Query*)target["source_query"].value<QObject*>(); |
1826 | + sourceIndex = sourceQuery->getIndex(); |
1827 | + sourceDb = sourceIndex->getDatabase(); |
1828 | + } |
1829 | + |
1830 | + if(sourceDb == NULL || targetDb == NULL){ |
1831 | + m_errors.append("<b><font color=\"Red\">Error</font></b>: Either the source or target database does not exist or is not active."); |
1832 | + return; |
1833 | + } |
1834 | + |
1835 | + QMap<QString,QVariant> lastSyncInformation; |
1836 | + |
1837 | + lastSyncInformation.insert("target_replica_uid",getUidFromLocalDb(target_db_name)); |
1838 | + lastSyncInformation.insert("target_replica_generation",""); |
1839 | + lastSyncInformation.insert("target_replica_transaction_id",-1); |
1840 | + lastSyncInformation.insert("source_replica_uid",getUidFromLocalDb(sourceDb->getPath())); |
1841 | + lastSyncInformation.insert("source_replica_generation",""); |
1842 | + lastSyncInformation.insert("source_replica_transaction_id",-1); |
1843 | + |
1844 | + lastSyncInformation = getLastSyncInformation(sourceDb, targetDb, false, lastSyncInformation); |
1845 | + |
1846 | + QList<QString> transactionsFromSource; |
1847 | + QList<QString> transactionsFromTarget; |
1848 | + |
1849 | + // Check if target and source have ever been synced before |
1850 | + |
1851 | + if(lastSyncInformation["target_replica_uid"].toString() != "" && lastSyncInformation["target_replica_generation"].toString() != "" && lastSyncInformation["target_replica_transaction_id"].toInt() != -1 && lastSyncInformation["source_replica_uid"].toString() != "" && lastSyncInformation["source_replica_generation"].toString() != "" && lastSyncInformation["source_replica_transaction_id"].toInt() != -1) |
1852 | + { |
1853 | + |
1854 | + m_errors.append("<b><font color=\"green\">Log</font></b>:"+sourceDb->getPath()+" and "+target_db_name+" have previously been synced. Sync procedure will commence."); |
1855 | + |
1856 | + //Do some syncing |
1857 | + |
1858 | + transactionsFromSource = sourceDb->listTransactionsSince(lastSyncInformation["source_replica_generation"].toInt()); |
1859 | + |
1860 | + transactionsFromTarget = targetDb->listTransactionsSince(lastSyncInformation["target_replica_generation"].toInt()); |
1861 | + |
1862 | + |
1863 | + } |
1864 | + else{ |
1865 | + |
1866 | + m_errors.append("<b><font color=\"Orange\">Warning</font></b>:"+sourceDb->getPath()+" and "+target_db_name+" have no details of ever being synced."); |
1867 | + |
1868 | + //There is a first time for everything, let's sync! |
1869 | + |
1870 | + transactionsFromSource = sourceDb->listTransactionsSince(0); |
1871 | + |
1872 | + transactionsFromTarget = targetDb->listTransactionsSince(0); |
1873 | + } |
1874 | + |
1875 | + /*! |
1876 | + * With two distinct lists present, it is now possible to check what |
1877 | + * updates should be made, or new documents created in one or the other |
1878 | + * database, depending on conditions. |
1879 | + * |
1880 | + * However, two additional lists containing transactions IDs are required |
1881 | + * because the information is contained within delimited strings (see |
1882 | + * below for details). |
1883 | + * |
1884 | + */ |
1885 | + |
1886 | + QList<QString> transactionIdsFromSource; |
1887 | + QList<QString> transactionIdsFromTarget; |
1888 | + |
1889 | + Q_FOREACH(QString sourceTransaction, transactionsFromSource){ |
1890 | + |
1891 | + /*! |
1892 | + * Each sourceTransaction is a pipe delimited string containing |
1893 | + * generation number, document ID, and transaction ID details |
1894 | + * in that order. |
1895 | + * |
1896 | + * Splitting the string into its component pieces provides a |
1897 | + * document ID (the second key in the list). |
1898 | + */ |
1899 | + |
1900 | + QStringList transactionDetails = sourceTransaction.split("|"); |
1901 | + |
1902 | + /*! |
1903 | + * It is only necessary to have unique instances of the |
1904 | + * document ID. |
1905 | + */ |
1906 | + |
1907 | + if(!transactionIdsFromSource.contains(transactionDetails[1])) |
1908 | + transactionIdsFromSource.append(transactionDetails[1]); |
1909 | + |
1910 | + } |
1911 | + |
1912 | + Q_FOREACH(QString targetTransaction, transactionsFromTarget){ |
1913 | + |
1914 | + /*! |
1915 | + * Each targetTransaction is a pipe delimited string containing |
1916 | + * generation number, document ID, and transaction ID details |
1917 | + * in that order. |
1918 | + * |
1919 | + * Splitting the string into its component pieces provides a |
1920 | + * document ID (the second key in the list). |
1921 | + */ |
1922 | + |
1923 | + QStringList transactionDetails = targetTransaction.split("|"); |
1924 | + |
1925 | + /*! |
1926 | + * It is only necessary to have unique instances of the |
1927 | + * document ID. |
1928 | + */ |
1929 | + |
1930 | + if(!transactionIdsFromTarget.contains(transactionDetails[1])) |
1931 | + transactionIdsFromTarget.append(transactionDetails[1]); |
1932 | + |
1933 | + } |
1934 | + |
1935 | + /*! |
1936 | + * The transactions IDs are searched for in the list of changes |
1937 | + * from the other database since the last sync (or from the start |
1938 | + * if this is the first sync) to determine whether to update an |
1939 | + * existing document, or create a new one, depending on conditions. |
1940 | + */ |
1941 | + |
1942 | + Q_FOREACH(QString sourceTransaction, transactionIdsFromSource){ |
1943 | + |
1944 | + if(transactionIdsFromTarget.contains(sourceTransaction)){ |
1945 | + if(target["resolve_to_source"].toBool()==true) |
1946 | + |
1947 | + //Update a Document from Source to Target |
1948 | + |
1949 | + if(target.contains("id")){ |
1950 | + target_results.append(syncDocument(sourceDb, targetDb, sourceTransaction)); |
1951 | + } |
1952 | + } |
1953 | + else{ |
1954 | + |
1955 | + //New Document from Source to Target |
1956 | + |
1957 | + if(target.contains("id")){ |
1958 | + target_results.append(syncDocument(sourceDb, targetDb, sourceTransaction)); |
1959 | + } |
1960 | + |
1961 | + } |
1962 | + |
1963 | + } |
1964 | + |
1965 | + Q_FOREACH(QString targetTransaction, transactionIdsFromTarget){ |
1966 | + |
1967 | + if(transactionIdsFromSource.contains(targetTransaction)){ |
1968 | + if(target["resolve_to_source"].toBool()==false){ |
1969 | + |
1970 | + //Update a Document from Target to Source |
1971 | + |
1972 | + if(target.contains("id")){ |
1973 | + source_results.append(syncDocument(targetDb, sourceDb, targetTransaction)); |
1974 | + } |
1975 | + } |
1976 | + } |
1977 | + else{ |
1978 | + |
1979 | + //New Document from Target to Source |
1980 | + |
1981 | + if(target.contains("id")){ |
1982 | + source_results.append(syncDocument(targetDb, sourceDb, targetTransaction)); |
1983 | + } |
1984 | + } |
1985 | + } |
1986 | + |
1987 | + |
1988 | + if(lastSyncInformation["target_replica_transaction_id"].toInt()==-1&&lastSyncInformation["source_replica_transaction_id"].toInt()==-1){ |
1989 | + //targetDb->updateSyncLog(true, QString uid, QString generation, QString transaction_id); |
1990 | + //sourceDb->updateSyncLog(true, QString uid, QString generation, QString transaction_id); |
1991 | + } |
1992 | + else{ |
1993 | + //targetDb->updateSyncLog(false, QString uid, QString generation, QString transaction_id); |
1994 | + //sourceDb->updateSyncLog(true, QString uid, QString generation, QString transaction_id); |
1995 | + } |
1996 | + |
1997 | + |
1998 | + |
1999 | + /* The source replica asks the target replica for the information it has stored about the last time these two replicas were synchronised (if ever).*/ |
2000 | + |
2001 | + |
2002 | + /* |
2003 | + |
2004 | + The application wishing to synchronise sends the following GET request to the server: |
2005 | + |
2006 | + GET /thedb/sync-from/my_replica_uid |
2007 | + |
2008 | + Where thedb is the name of the database to be synchronised, and my_replica_uid is the replica id of the application’s (i.e. the local, or synchronisation source) database |
2009 | + |
2010 | + */ |
2011 | + |
2012 | + /* |
2013 | + |
2014 | + The target responds with a JSON document that looks like this: |
2015 | + |
2016 | + { |
2017 | + "target_replica_uid": "other_replica_uid", |
2018 | + "target_replica_generation": 12, |
2019 | + "target_replica_transaction_id": "T-sdkfj92292j", |
2020 | + "source_replica_uid": "my_replica_uid", |
2021 | + "source_replica_generation": 23, |
2022 | + "source_transaction_id": "T-39299sdsfla8" |
2023 | + } |
2024 | + |
2025 | + With all the information it has stored for the most recent synchronisation between itself and this particular source replica. In this case it tells us that the synchronisation target believes that when it and the source were last synchronised, the target was at generation 12 and the source at generation 23. |
2026 | + |
2027 | + */ |
2028 | + |
2029 | + |
2030 | + /* The source replica validates that its information regarding the last synchronisation is consistent with the target’s information, and raises an error if not. (This could happen for instance if one of the replicas was lost and restored from backup, or if a user inadvertently tries to synchronise a copied database.) */ |
2031 | + |
2032 | + |
2033 | + |
2034 | + /* The source replica generates a list of changes since the last change the target replica knows of. */ |
2035 | + |
2036 | + |
2037 | + |
2038 | + /* The source replica checks what the last change is it knows about on the target replica. */ |
2039 | + |
2040 | + |
2041 | + |
2042 | + /* If there have been no changes on either replica that the other side has not seen, the synchronisation stops here. */ |
2043 | + |
2044 | + |
2045 | + |
2046 | + /* The source replica sends the changed documents to the target, along with what the latest change is that it knows about on the target replica. */ |
2047 | + |
2048 | + |
2049 | + |
2050 | + /* The target processes the changed documents, and records the source replica’s latest change. */ |
2051 | + |
2052 | + |
2053 | + |
2054 | + /* The target responds with the documents that have changes that the source does not yet know about. */ |
2055 | + |
2056 | + /* The source processes the changed documents, and records the target replica’s latest change. */ |
2057 | + |
2058 | + |
2059 | + |
2060 | + /* If the source has seen no changes unrelated to the synchronisation during this whole process, it now sends the target what its latest change is, so that the next synchronisation does not have to consider changes that were the result of this one.*/ |
2061 | + |
2062 | + |
2063 | +} |
2064 | + |
2065 | +QVariant Synchronizer::syncDocument(Database *from, Database *to, QString docId) |
2066 | +{ |
2067 | + QVariant document = from->getDoc(docId); |
2068 | + to->putDoc(document, docId); |
2069 | + QString revision = from->getCurrentDocRevisionNumber(docId); |
2070 | + to->updateDocRevisionNumber(docId,revision); |
2071 | + |
2072 | + return document; |
2073 | +} |
2074 | + |
2075 | +QMap<QString,QVariant> Synchronizer::getLastSyncInformation(Database *sourceDb, Database *targetDb, bool remote, QMap<QString,QVariant> lastSyncInformation){ |
2076 | + |
2077 | + if(remote == true){ |
2078 | + |
2079 | + m_errors.append("<b><font color=\"red\">Error</font></b>: Remote database sync is under construction. Nothing was synced. Try again later."); |
2080 | + |
2081 | + return lastSyncInformation; |
2082 | + |
2083 | + } |
2084 | + else{ |
2085 | + |
2086 | + lastSyncInformation["source_replica_uid"].toString(); |
2087 | + |
2088 | + lastSyncInformation = targetDb->getSyncLogInfo(lastSyncInformation, lastSyncInformation["source_replica_uid"].toString(),"target"); |
2089 | + |
2090 | + lastSyncInformation = sourceDb->getSyncLogInfo(lastSyncInformation, lastSyncInformation["target_replica_uid"].toString(),"source"); |
2091 | + |
2092 | + } |
2093 | + |
2094 | + return lastSyncInformation; |
2095 | + |
2096 | +} |
2097 | + |
2098 | + |
2099 | +QString Synchronizer::getUidFromLocalDb(QString dbFileName) |
2100 | +{ |
2101 | + |
2102 | + QString dbUid; |
2103 | + |
2104 | + QSqlDatabase db; |
2105 | + |
2106 | + db = QSqlDatabase::addDatabase("QSQLITE",QUuid::createUuid().toString()); |
2107 | + |
2108 | + QFile db_file(dbFileName); |
2109 | + |
2110 | + if(!db_file.exists()) |
2111 | + { |
2112 | + m_errors.append("<b><font color\"red\">Error</font></b>: Local database " + dbFileName + " does not exist."); |
2113 | + return dbUid; |
2114 | + } |
2115 | + else |
2116 | + { |
2117 | + db.setDatabaseName(dbFileName); |
2118 | + if (!db.open()){ |
2119 | + m_errors.append(db.lastError().text()); |
2120 | + } |
2121 | + else{ |
2122 | + QSqlQuery query (db.exec("SELECT value FROM u1db_config WHERE name = 'replica_uid'")); |
2123 | + |
2124 | + if(!query.lastError().isValid() && query.next()){ |
2125 | + dbUid = query.value(0).toString(); |
2126 | + db.close(); |
2127 | + |
2128 | + dbUid = dbUid.replace("{",""); |
2129 | + |
2130 | + dbUid = dbUid.replace("}",""); |
2131 | + |
2132 | + return dbUid; |
2133 | + } |
2134 | + else{ |
2135 | + qDebug() << query.lastError().text(); |
2136 | + db.close(); |
2137 | + return dbUid; |
2138 | + } |
2139 | + } |
2140 | + |
2141 | + } |
2142 | + |
2143 | + return dbUid; |
2144 | +} |
2145 | + |
2146 | + |
2147 | + |
2148 | + |
2149 | +QT_END_NAMESPACE_U1DB |
2150 | + |
2151 | +#include "moc_synchronizer.cpp" |
2152 | + |
2153 | |
2154 | === added file 'src/synchronizer.h' |
2155 | --- src/synchronizer.h 1970-01-01 00:00:00 +0000 |
2156 | +++ src/synchronizer.h 2013-07-04 14:13:28 +0000 |
2157 | @@ -0,0 +1,112 @@ |
2158 | +/* |
2159 | + * Copyright (C) 2013 Canonical, Ltd. |
2160 | + * |
2161 | + * Authors: |
2162 | + * Kevin Wright <kevin.wright@canonical.com> |
2163 | + * |
2164 | + * This program is free software; you can redistribute it and/or modify |
2165 | + * it under the terms of the GNU Lesser General Public License as published by |
2166 | + * the Free Software Foundation; version 3. |
2167 | + * |
2168 | + * This program is distributed in the hope that it will be useful, |
2169 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
2170 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
2171 | + * GNU Lesser General Public License for more details. |
2172 | + * |
2173 | + * You should have received a copy of the GNU Lesser General Public License |
2174 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
2175 | + */ |
2176 | + |
2177 | +#ifndef U1DB_SYNCHRONIZER_H |
2178 | +#define U1DB_SYNCHRONIZER_H |
2179 | + |
2180 | +#include <QtCore/QObject> |
2181 | +#include <QSqlDatabase> |
2182 | +#include <QVariant> |
2183 | +#include <QMetaType> |
2184 | +#include <QtNetwork> |
2185 | +#include <QNetworkAccessManager> |
2186 | + |
2187 | + |
2188 | +#include "database.h" |
2189 | +#include "index.h" |
2190 | +#include "query.h" |
2191 | + |
2192 | +QT_BEGIN_NAMESPACE_U1DB |
2193 | + |
2194 | +class Q_DECL_EXPORT Synchronizer : public QAbstractListModel { |
2195 | + Q_OBJECT |
2196 | +#ifdef Q_QDOC |
2197 | + Q_PROPERTY(Database* source READ getSource WRITE setSource NOTIFY sourceChanged) |
2198 | +#else |
2199 | + Q_PROPERTY(QT_PREPEND_NAMESPACE_U1DB(Database*) source READ getSource WRITE setSource NOTIFY sourceChanged) |
2200 | +#endif |
2201 | + Q_PROPERTY(bool synchronize READ getSync WRITE setSync NOTIFY syncChanged) |
2202 | + Q_PROPERTY(bool resolve_to_source READ getResolveToSource WRITE setResolveToSource NOTIFY resolveToSourceChanged) |
2203 | + Q_PROPERTY(QVariant targets READ getTargets WRITE setTargets NOTIFY targetsChanged) |
2204 | + Q_PROPERTY(QList<QString> errors READ getErrors WRITE setErrors NOTIFY errorsChanged) |
2205 | + |
2206 | +public: |
2207 | + Synchronizer(QObject* parent = 0); |
2208 | + |
2209 | + // QAbstractListModel |
2210 | + QVariant data(const QModelIndex & index, int role = Qt::DisplayRole) const; |
2211 | + QHash<int, QByteArray>roleNames() const; |
2212 | + int rowCount(const QModelIndex & parent = QModelIndex()) const; |
2213 | + |
2214 | + QList<QVariant> getValidTargets(QMap<QString,QString>validator, QList<QString>mandatory); |
2215 | + QMap<QString,QVariant> getLastSyncInformation(Database *sourceDb, Database *targetDb, bool remote, QMap<QString,QVariant> lastSyncInformation); |
2216 | + |
2217 | + |
2218 | + Database* getSource(); |
2219 | + QVariant getTargets(); |
2220 | + bool getSync(); |
2221 | + bool getResolveToSource(); |
2222 | + QList<QString> getErrors(); |
2223 | + |
2224 | + void setSource(Database* source); |
2225 | + void setTargets(QVariant targets); |
2226 | + void setSync(bool synchronize); |
2227 | + void setResolveToSource(bool resolve_to_source); |
2228 | + void setErrors(QList<QString> errors); |
2229 | + |
2230 | + |
2231 | + void syncWithLocalTarget(Database *source, Database *target, bool resolve_to_source); |
2232 | + void syncWithRemoteTarget(Database *source, QString target_url, bool resolve_to_source); |
2233 | + void syncLocalToLocal(Database *sourceDb, QMap<QString,QVariant> target); |
2234 | + void synchronizeTargets(Database *source, QVariant targets); |
2235 | + |
2236 | + QVariant syncDocument(Database *from, Database *to, QString docId); |
2237 | + QString getUidFromLocalDb(QString dbFileName); |
2238 | + |
2239 | + |
2240 | + |
2241 | + |
2242 | +Q_SIGNALS: |
2243 | + void sourceChanged(Database* source); |
2244 | + void targetsChanged(QVariant targets); |
2245 | + void syncChanged(bool synchronize); |
2246 | + void resolveToSourceChanged(bool resolve_to_source); |
2247 | + void errorsChanged(QList<QString> errors); |
2248 | + void syncCompleted(); |
2249 | +private: |
2250 | + //Q_DISABLE_COPY(Synchronizer) |
2251 | + Database* m_source; |
2252 | + bool m_synchronize; |
2253 | + bool m_resolve_to_source; |
2254 | + QVariant m_targets; |
2255 | + QList<QString> m_errors; |
2256 | + |
2257 | + void onSyncChanged(bool synchronize); |
2258 | + void remoteGetSyncInfoFinished(QNetworkReply* reply); |
2259 | + void remotePostSyncInfoFinished(QNetworkReply* reply); |
2260 | + |
2261 | +}; |
2262 | + |
2263 | +QT_END_NAMESPACE_U1DB |
2264 | + |
2265 | + |
2266 | + |
2267 | +#endif // U1DB_SYNCHRONIZER_H |
2268 | + |
2269 | + |
syncLocalToLocal should use U1db::Database to re-use the initialization, which would also give it the existing sanity checks
the logic of onSyncChanged is probably best moved into a separate callback. Changing resolve_to_source and targets would trigger it as well. Thus it becomes possible to delay/ thread the real sync and avoid any delay at startup.
Even with keeping in mind it's incomplete //uncommented code and conflicts make reviewing a little hard.
I love the description of the sync via HTTP/ JSON, this would be nice to get into one qdoc-ified comment and make it accessible as documentation.