1 
2 
3 /**
4   gestiamo la parte di sincronia tra server e le varie tavole associate ai client.
5 
6 
7   */
8 module mars.sync;
9 
10 enum d =
11 `
12 BaseServerSideTable
13     clientSideTables[clientId] --- SyncOps[]
14 `;
15 
16 import std.algorithm;
17 import std.conv;
18 import std.datetime;
19 import std.format;
20 import std.meta;
21 import std.typecons;
22 import std.variant;
23 
24 import std.experimental.logger;
25 
26 import msgpack;
27 
28 import mars.defs;
29 import mars.pgsql;
30 import mars.msg;
31 import mars.server;
32 version(unittest) import mars.starwars;
33 
34 /**
35 A server side table is instantiated only once per marsServer, and they are stored into the 'tables'
36 structure of the marsServer.
37 
38 The instantiation usually is inside the application code: `InstantiateTables!(ctTables)(marsServer, [], [], [], [])`
39 */
40 class BaseServerSideTable(ClientT)
41 {
42     alias ClientType = ClientT;
43 
44     this(immutable(Table) definition){
45         this.definition = definition;
46     }
47 
48     auto createClientSideTable(string clientid){
49         auto cst = new ClientSideTable!ClientT();
50         cst.strategy = definition.cacheRows? Strategy.easilySyncAll : Strategy.easilySyncNone;
51         final switch(cst.strategy) with(Strategy) {
52             case easilySyncAll:
53                 cst.ops ~= new ClientImportValues!ClientT();
54                 break;
55             case easilySyncNone:
56                 break;
57         }
58         // new client, new client side table
59         assert( (clientid in clientSideTables) is null, format("cliendid:%s, clientSideTables:%s", clientid, clientSideTables.keys() ) );
60         clientSideTables[clientid] = cst;
61 
62         return cst;
63     }
64 
65     auto wipeClientSideTable(string clientid){
66         assert( (clientid in clientSideTables) !is null, clientid );
67         clientSideTables.remove(clientid);
68     }
69 
70     /// execute a sql select statement, and returns a vibe json array with the records as json
71     auto selectAsJson(Database db, string sqlSelect, Variant[string] parameters) in {
72         assert(db !is null);
73     } body {
74         import vibe.data.json;
75 
76         auto resultSet = db.executeQueryUnsafe(sqlSelect, parameters);
77         scope(exit) resultSet.close();
78 
79         Json jsonRecords = Json.emptyArray;
80         foreach(variantRow; resultSet){
81             Json jsonRow = Json.emptyArray;
82             foreach(i, variantField; variantRow){
83                 if(variantField.type == typeid(int)){ jsonRow ~= variantField.get!int; }
84                 else if(variantField.type == typeid(float)){ jsonRow ~= variantField.get!float; }
85                 else if(variantField.type == typeid(long)){ jsonRow ~= variantField.get!long; }
86                 else if(variantField.type == typeid(string)){ jsonRow ~= variantField.get!string; }
87                 else if(variantField.type == typeid(ubyte[])){
88                     // ... if I apply directly the '=~', the arrays are flattened!
89                     jsonRow ~= 0;
90                     jsonRow[jsonRow.length-1] = variantField.get!(ubyte[]).serializeToJson;
91                 }
92                 else {
93                     import std.stdio; writeln(variantField.type);
94                     assert(false);
95                 }
96             }
97             jsonRecords ~= 0; jsonRecords[jsonRecords.length-1] = jsonRow;
98         }
99         return jsonRecords;
100     }
101     version(unittest_starwars){
102         unittest {
103             AuthoriseError err; auto db = DatabaseService("127.0.0.1", 5432, "starwars").connect("jedi", "force", err);
104             auto table = new WhiteHole!BaseServerSideTable(Table());
105             auto json = table.selectAsJson(db, "select * from people", null);
106             //import std.stdio; writeln(json.toPrettyString());
107             assert(json[0][0] == "Luke");
108         }
109     }
110 
111     abstract immutable(ubyte)[] packRows(size_t offset = 0, size_t limit = long.max);
112     abstract immutable(ubyte)[] packRows(Database db, size_t offset = 0, size_t limit = long.max);
113     abstract size_t count() const;
114     abstract size_t count(Database) const;
115     abstract size_t countRowsToInsert() const;
116     abstract size_t countRowsToUpdate() const;
117     abstract size_t countRowsToDelete() const;
118     abstract size_t index() const;
119     abstract immutable(ubyte)[] packRowsToInsert();
120     abstract immutable(ubyte)[] packRowsToUpdate();
121     abstract immutable(ubyte)[] packRowsToDelete();
122 
123     abstract immutable(ubyte)[][2] insertRecord(Database, immutable(ubyte)[], ref InsertError, string, string);
124     abstract void updateRecord(Database, immutable(ubyte)[], immutable(ubyte)[], ref RequestState);
125     abstract immutable(ubyte)[]    deleteRecord(Database, immutable(ubyte)[], ref DeleteError, string, string);
126 
127     abstract void unsafeReset();
128 
129     immutable Table definition; 
130     private {
131 
132         /// Every server table has a collection of the linked client side tables. The key element is the identifier of
133         /// the client, so that the collection can be kept clean when a client connect/disconnects.
134         public ClientSideTable!(ClientT)*[string] clientSideTables;
135         
136         //public SynOp!ClientT[] ops;
137 
138     }
139 }
140 
141 class ServerSideTable(ClientT, immutable(Table) table) : BaseServerSideTable!ClientT
142 {
143     enum Definition = table; 
144     enum Columns = table.columns;
145      
146     alias ColumnsType = asD!Columns; /// an AliasSeq of the D types for the table columns...
147     alias ColumnsStruct = asStruct!table; 
148     alias KeysStruct = asPkStruct!table;
149 
150     this() { super(table); } 
151 
152     // interface needed to handle the records in a generic way ...
153 
154     /// returns the total number of records we are 'talking on' (filters? query?)
155     deprecated override size_t count() const { return fixtures.length; }
156     override size_t count(Database db) const {
157         static if( table.durable ){
158             return db.executeScalarUnsafe!size_t("select count(*) from %s".format(table.name));
159         }
160         else {
161             return fixtures.length;
162         }
163     }
164     //static if( ! table.durable ){ // XXX
165         override size_t countRowsToInsert() const { return toInsert.length; }
166         override size_t countRowsToUpdate() const { return toUpdate.length; }
167         override size_t countRowsToDelete() const { return toDelete.length; }
168     //}
169 
170     /// return the unique index identifier for this table, that's coming from the table definition in the app.d
171     override size_t index() const { return Definition.index; }
172 
173     /// returns 'limit' rows starting from 'offset'.
174     deprecated auto selectRows(size_t offset = 0, size_t limit = long.max) const  {
175         size_t till  = (limit + offset) > count ? count : (limit + offset);
176         return fixtures.values()[offset .. till];
177     }
178     /// returns 'limit' rows starting from 'offset'.
179     auto selectRows(Database db, size_t offset = 0, size_t limit = long.max) const {
180         static if(table.durable){
181             auto resultSet = db.executeQueryUnsafe!(asStruct!table)("select * from %s limit %d offset %d".format(
182                 table.name, limit, offset)
183             );
184             static if( Definition.decorateRows ){
185                 asSyncStruct!table[] rows;
186                 foreach(vr; resultSet){
187                     asStruct!table v = vr;
188                     asSyncStruct!table r;
189                     assignCommonFields!(typeof(r), typeof(v))(r, v);
190                     r.mars_who = "automation@server";
191                     r.mars_what = "imported";
192                     r.mars_when = Clock.currTime.toString(); 
193                     rows ~= r;
194                 }
195             }
196             else {
197                 asStruct!table[] rows;
198                 foreach(v; resultSet){
199                     rows ~= v;
200                 }
201             }
202             
203             
204             resultSet.close();
205             return rows;
206         }
207         else {
208             size_t till  = (limit + offset) > count(db) ? count(db) : (limit + offset);
209             return fixtures.values()[offset .. till];
210         }
211     }
212 
213     /// insert a new row in the server table, turning clients table out of sync
214     deprecated void insertRow(ColumnsStruct fixture){
215         KeysStruct keys = pkValues!(table)(fixture);
216         fixtures[keys] = fixture;
217         static if(table.decorateRows){
218             asSyncStruct!table rec;
219             assignCommonFields(rec, fixture);
220             with(rec){ mars_who = "automation@server"; mars_what = "inserted"; mars_when = Clock.currTime.toString(); }
221         }
222         else auto rec = fixture;
223         toInsert[keys] = rec;
224         foreach(ref cst; clientSideTables.values){
225             cst.ops ~= new ClientInsertValues!ClientT();
226         }
227     }
228 
229     /// insert a new row in the server table, turning clients table out of sync
230     ColumnsStruct insertRecord(Database db, ColumnsStruct record, ref InsertError err, string username, string clientid){
231         KeysStruct keys = pkValues!table(record);
232         static if(table.durable){
233             auto inserted = db.executeInsert!(table, ColumnsStruct)(record, err);
234         } else {
235             fixtures[keys] = record;
236             auto inserted = record;
237             err = InsertError.inserted;
238         }
239         if( err == InsertError.inserted ){
240             static if(table.decorateRows){
241                 asSyncStruct!table rec;
242                 assignCommonFields(rec, record);
243                 with(rec){ mars_who = username ~ "@" ~ clientid; mars_what = "inserted"; mars_when = Clock.currTime.toString(); }
244             }
245             else {
246                 auto rec = record;
247             }
248             toInsert[keys] = rec;
249             // ... don't propagate if not cached, or we are triggering a loot of refresh: stick with manual refresh done on client.
250             if(table.cacheRows){
251                 foreach(ref cst; clientSideTables.values){
252                     cst.ops ~= new ClientInsertValues!ClientT();
253                 }
254             }
255         }
256         return inserted;
257     }
258 
259     override immutable(ubyte)[][2] insertRecord(Database db, immutable(ubyte)[] data, ref InsertError err, string username, string clientId){
260         import  msgpack : pack, unpack, MessagePackException;
261         ColumnsStruct record;
262         try {
263             record = unpack!(ColumnsStruct, true)(data);
264         }
265         catch(MessagePackException exc){
266             errorf("mars - failed to unpack record to insert in '%s': maybe a wrong type of data in js", table.name);
267             errorf(exc.toString);
268             err = InsertError.unknownError;
269             return [[], []];
270         }
271         ColumnsStruct inserted = insertRecord(db, record, err, username, clientId);
272         return [
273             inserted.pack!(true).idup,
274             record.pkParamValues!table().pack!(true).idup // clientKeys
275         ];
276     }
277 
278     override void updateRecord(Database db, immutable(ubyte)[] encodedKeys, immutable(ubyte)[] encodedRecord, ref RequestState state){
279         asPkStruct!table keys;
280         ColumnsStruct record;
281         try { 
282             keys = unpack!(asPkStruct!table, true)(encodedKeys); 
283             record = unpack!(ColumnsStruct, true)(encodedRecord);
284         }
285         catch(MessagePackException exc){
286             errorf("mars - failed to unpack keys for record to update '%s': maybe a wrong type of data in js", table.name);
287             errorf(exc.toString);
288             state = RequestState.rejectedAsDecodingFailed;
289             return;
290         }
291         updateRecord(db, keys, record, state);
292     }
293 
294     void updateRecord(Database db, asPkStruct!table keys, ColumnsStruct record, ref RequestState state){
295         static if(table.durable){
296             db.executeUpdate!(table, asPkStruct!table, ColumnsStruct)(keys, record, state);
297         }
298         else { assert(false); }
299     }
300 
301     override immutable(ubyte)[] deleteRecord(Database db, immutable(ubyte)[] data, ref DeleteError err, string username, string clientid){
302         import msgpack : pack, unpack, MessagePackException;
303         asPkParamStruct!table keys;
304         try {
305             keys = unpack!(asPkParamStruct!table, true)(data);
306         }
307         catch(MessagePackException exc){
308             errorf("mars - failed to unpack keys for record to delete '%s': maybe a wrong type of data in js", table.name);
309             errorf(exc.toString);
310             err = DeleteError.unknownError;
311             return data;
312         }
313         deleteRecord(db, keys, err, username, clientid);
314         if( err != DeleteError.deleted ) return data;
315         return [];
316     }
317 
318     asPkParamStruct!table deleteRecord(Database db, asPkParamStruct!table keys, ref DeleteError err, string username, string clientid){
319         KeysStruct k;
320         assignFields(k, keys);
321         static if(table.durable){
322             db.executeDelete!(table, asPkParamStruct!table)(keys, err);
323         }
324         else {
325             fixtures.remove(k);
326             err = DeleteError.deleted;
327         }
328         if( err == DeleteError.deleted ){
329             static if(table.decorateRows){
330                 toDelete[k] = Sync(username ~ "@" ~ clientid, "deleted", Clock.currTime.toString());
331             }
332             else {
333                 toDelete[k] = 0;
334             }
335             foreach(ref cst; clientSideTables.values){
336                 cst.ops ~= new ClientDeleteValues!ClientT();
337             }
338         }
339         return keys;
340     }
341 
342     /// update row in the server table, turning the client tables out of sync
343     deprecated void updateRow(KeysStruct keys, ColumnsStruct record){
344         //KeysStruct keys = pkValues!table(record);
345         auto v = keys in toInsert;
346         if( v !is null ){
347             static if(table.decorateRows){
348                 asSyncStruct!table rec;
349                 assignCommonFields(rec, record);
350                 with(rec){ mars_who = "who@where"; mars_what = "updated"; mars_when = Clock.currTime.toString(); }
351             }
352             else {
353                 auto rec = record;
354             }
355             *v = rec;
356             assert( (keys in toUpdate) is null );
357         }
358         else {
359             auto v2 = keys in toUpdate;
360             if( v2 !is null ){
361                 *v2 = record;
362             }
363             else {
364                 toUpdate[keys] = record;
365             }
366         }
367         fixtures[keys] = record;
368         foreach(ref cst; clientSideTables.values){
369             cst.ops ~= new ClientUpdateValues!ClientT();
370         }
371     }
372 
373     /// update row in the server table, turning the client tables out of sync
374     void updateRow(Database db, KeysStruct keys, ColumnsStruct record){
375         static if( table.durable ){
376             import msgpack : pack;
377 
378             RequestState state;
379             db.executeUpdate!(table, KeysStruct, ColumnsStruct)(keys, record, state);
380             auto v = keys in toInsert;
381             if( v !is null ){
382                 static if(table.decorateRows){
383                     asSyncStruct!table rec;
384                     assignCommonFields(rec, record);
385                     with(rec){ mars_who = "who@where"; mars_what = "updated"; mars_when = Clock.currTime.toString(); }
386                 }
387                 else {
388                     auto rec = record;
389                 }
390                 *v = rec;
391                 assert( (keys in toUpdate) is null );
392             }
393             else {
394                 auto v2 = keys in toUpdate;
395                 if( v2 !is null ){
396                     *v2 = record;
397                 }
398                 else {
399                     toUpdate[keys] = record;
400                 }
401             }
402         }
403         else {
404             //KeysStruct keys = pkValues!table(record);
405             auto v = keys in toInsert;
406             if( v !is null ){
407                 static if(table.decorateRows){
408                     asSyncStruct!table rec;
409                     assignCommonFields(rec, record);
410                     with(rec){ mars_who = "who@where"; mars_what = "updated"; mars_when = Clock.currTime.toString(); }
411                 }
412                 else {
413                     auto rec = record;
414                 }
415                 *v = record;
416                 assert( (keys in toUpdate) is null );
417             }
418             else {
419                 v = keys in toUpdate;
420                 if( v !is null ){
421                     *v = record;
422                 }
423                 else {
424                     toUpdate[keys] = record;
425                 }
426             }
427             fixtures[keys] = record;
428         }
429         foreach(ref cst; clientSideTables.values){
430             cst.ops ~= new ClientUpdateValues!ClientT();
431         }
432     }
433 
434     /// returns the packet selected rows
435     override immutable(ubyte)[] packRows(size_t offset = 0, size_t limit = long.max) const {
436         import msgpack : pack;
437         return pack!(true)(selectRows(null, offset, limit)).idup;
438     }
439     /// returns the packet selected rows
440     override immutable(ubyte)[] packRows(Database db, size_t offset = 0, size_t limit = long.max) const {
441         import msgpack : pack;
442         return pack!(true)(selectRows(db, offset, limit)).idup;
443     }
444 
445     /// return the packet rows to insert in the client
446     override immutable(ubyte)[] packRowsToInsert() {
447         import msgpack : pack;
448         auto packed = pack!(true)(toInsert.values()).idup;
449         //toInsert = null; can't reset... this is called for every client
450         return packed;
451     }
452 
453     /// return the packet rows to delete in the client
454     override immutable(ubyte)[] packRowsToDelete() {
455         import msgpack : pack;
456         asSyncPkParamStruct!(table)[] whereKeys;
457         foreach(key; toDelete.keys()){
458             asSyncPkParamStruct!table whereKey;
459             assignFields(whereKey, key);
460             static if(table.decorateRows) assignCommonFields(whereKey, toDelete[key]);
461             whereKeys ~= whereKey;
462         }
463         auto packed = pack!(true)(whereKeys).idup;
464         //toInsert = null; can't reset... this is called for every client
465         return packed;
466     }
467 
468     /// return the packet rows to update in the client
469     override immutable(ubyte)[] packRowsToUpdate() {
470         static struct UpdateRecord {
471             KeysStruct keys;
472             asStruct!table record;
473         }
474         UpdateRecord[] records;
475         foreach(r; toUpdate.keys){
476             records ~= UpdateRecord(r, toUpdate[r]);
477         }
478 
479         import msgpack : pack;
480         auto packed = pack!(true)(records).idup;
481         //toUpdate = null; can't reset... this is called for every client
482         return packed;
483     }
484 
485     void loadFixture(ColumnsStruct fixture){
486         KeysStruct keys = pkValues!table(fixture);
487         fixtures[keys] = fixture;
488     }
489 
490     override void unsafeReset() {
491         //fixtures = null;
492         toInsert = null;
493         toUpdate = null;
494         toDelete = null;
495     }
496 
497     //static if( ! table.durable ){
498         asStruct!(table)[asPkStruct!(table)] fixtures;
499         static if(table.decorateRows){
500             asSyncStruct!(table)[asPkStruct!(table)] toInsert;
501             Sync[asPkStruct!(table)] toDelete;
502         }
503         else {
504             asStruct!(table)[asPkStruct!(table)] toInsert;
505             int[asPkStruct!(table)] toDelete;
506         }
507         asStruct!(table)[asPkStruct!(table)] toUpdate;
508 
509         // ... record inserted client side, already patched and inserted for this client.
510         //asStruct!(table)[string] notToInsert;
511     //}
512 }
513 
514 
515 struct ClientSideTable(ClientT)
516 {
517     private {
518         Strategy strategy = Strategy.easilySyncNone;
519         public SynOp!ClientT[] ops;
520     }
521 }
522 
523 private
524 {
525     enum Strategy { easilySyncAll, easilySyncNone }
526 
527     class SynOp(MarsClientT) {
528         abstract void execute(MarsClientT marsClient, ClientSideTable!(MarsClientT)* cst, BaseServerSideTable!MarsClientT sst);
529         abstract void execute(Database db, MarsClientT marsClient, ClientSideTable!(MarsClientT)* cst, BaseServerSideTable!MarsClientT sst);
530     }
531 
532     /// take all the rows in the server table and send them on the client table.
533     class ClientImportValues(MarsClientT) : SynOp!MarsClientT {
534 
535         override void execute(Database db, MarsClientT marsClient, ClientSideTable!(MarsClientT)* cst, BaseServerSideTable!MarsClientT sst)
536         {
537             assert(db !is null);
538 
539             // ... if the table is empty, simply do nothing ...
540             if( sst.count(db) > 0 ){
541                 auto payload = sst.packRows(db);
542 
543                 auto req = ImportRecordsReq(); with(req){
544                     tableIndex = sst.index;
545                     statementIndex = indexStatementFor(sst.index, "insert");
546                     encodedRecords = payload;
547                 }
548                 marsClient.sendRequest(req);
549                 if(marsClient.isConnected) auto rep = marsClient.receiveReply!ImportRecordsRep();
550             }
551         }
552         override void execute(MarsClientT marsClient, ClientSideTable!(MarsClientT)* cst, BaseServerSideTable!MarsClientT sst)
553         {
554             import mars.msg : ImportValuesRequest;
555             import std.conv : to;
556 
557             // ... if the table is empty, simply do nothing ...
558             if( sst.count > 0 ){
559                 auto payload = sst.packRows();
560 
561                 auto req = ImportRecordsReq();  with(req){
562                     tableIndex =sst.index;
563                     statementIndex = indexStatementFor(sst.index, "insert");
564                     encodedRecords = payload;
565                 }
566                 marsClient.sendRequest(req);
567                 if(marsClient.isConnected) auto rep = marsClient.receiveReply!ImportRecordsRep();
568             }
569         }
570     }
571 
572     class ClientInsertValues(MarsClientT) : SynOp!MarsClientT {
573         
574         override void execute(MarsClientT marsClient, ClientSideTable!(MarsClientT)* cst, BaseServerSideTable!MarsClientT sst)
575         {
576             if( sst.countRowsToInsert > 0 ){
577                 auto payload = sst.packRowsToInsert();
578                 auto req = InsertRecordsReq(); with(req){
579                     tableIndex = sst.index;
580                     statementIndex = indexStatementFor(sst.index, "insert");
581                     encodedRecords = payload;
582                 }
583                 marsClient.sendRequest(req);
584                 if(marsClient.isConnected) auto rep = marsClient.receiveReply!InsertRecordsRep();
585             }
586         }
587         override void execute(Database db, MarsClientT marsClient, ClientSideTable!(MarsClientT)* cst, BaseServerSideTable!MarsClientT sst)
588         {
589             if( sst.countRowsToInsert > 0 ){
590                 auto payload = sst.packRowsToInsert();
591                 auto req = InsertRecordsReq(); with(req){
592                     tableIndex = sst.index;
593                     statementIndex = indexStatementFor(sst.index, "insert");
594                     encodedRecords = payload;
595                 }
596                 marsClient.sendRequest(req);
597                 if(marsClient.isConnected) auto rep = marsClient.receiveReply!InsertRecordsRep();
598             }
599         }
600     }
601     
602     class ClientDeleteValues(MarsClientT) : SynOp!MarsClientT {
603         
604         override void execute(MarsClientT marsClient, ClientSideTable!(MarsClientT)* cst, BaseServerSideTable!MarsClientT sst)
605         {
606             if( sst.countRowsToDelete > 0 ){
607                 auto payload = sst.packRowsToDelete();
608                 auto req = DeleteRecordsReq(); with(req){
609                     tableIndex = sst.index;
610                     statementIndex = indexStatementFor(sst.index, "delete").to!int;
611                     encodedRecords = payload;
612                 }
613                 marsClient.sendRequest(req);
614                 if(marsClient.isConnected) auto rep = marsClient.receiveReply!DeleteRecordsRep();
615             }
616         }
617         override void execute(Database db, MarsClientT marsClient, ClientSideTable!(MarsClientT)* cst, BaseServerSideTable!MarsClientT sst){
618             if( sst.countRowsToDelete > 0 ){
619                 auto payload = sst.packRowsToDelete();
620                 auto req = DeleteRecordsReq(); with(req){
621                     tableIndex = sst.index;
622                     statementIndex = indexStatementFor(sst.index, "delete").to!int;
623                     encodedRecords = payload;
624                 }
625                 marsClient.sendRequest(req);
626                 if(marsClient.isConnected) auto rep = marsClient.receiveReply!DeleteRecordsRep();
627             }
628         }
629     }
630 
631     class ClientUpdateValues(MarsClientT) : SynOp!MarsClientT {
632 
633         override void execute(MarsClientT marsClient, ClientSideTable!(MarsClientT)* cst, BaseServerSideTable!MarsClientT sst)
634         {
635             import mars.msg : UpdateValuesRequest;
636             import std.conv :to;
637 
638             if( sst.countRowsToUpdate > 0 ){
639                 auto payload = sst.packRowsToUpdate();
640                 auto req = UpdateValuesRequest();
641                 req.statementIndex = indexStatementFor(sst.index, "update").to!int;
642                 req.bytes = payload;
643                 marsClient.sendRequest(req);
644             }
645         }
646         override void execute(Database db, MarsClientT marsClient, ClientSideTable!(MarsClientT)* cst, BaseServerSideTable!MarsClientT sst){}
647     }
648 
649     class ServerUpdateValues(MarsClientT) : SynOp!MarsClientT {
650         override void execute(Database db, MarsClientT marsClient, ClientSideTable* cst, BaseServerSideTable!MarsClientT sst){
651         }
652     }
653 }
654 
655 version(unittest)
656 {
657     struct MarsClientMock { void sendRequest(R)(R r){} }
658 }
659 unittest
660 {
661     /+
662     import std.range : zip;
663 
664     
665     auto t1 = immutable(Table)("t1", [Col("c1", Type.integer, false), Col("c2", Type.text, false)], [0], []);
666     auto sst = new ServerSideTable!(MarsClientMock, t1);
667     zip([1, 2, 3], ["a", "b", "c"]).each!( f => sst.loadFixture(sst.ColumnsStruct(f.expand)) );
668     
669     auto cst = sst.createClientSideTable();
670     // ... la strategia più semplice è syncronizzare subito TUTTO il contenuto nella client side ...
671     assert( cst.strategy == Strategy.easilySyncAll );
672     // ... e a questo punto, come minimo deve partire un comando di import di tutti i dati....
673     assert( cast(ClientImportValues!MarsClientMock)(sst.ops[$-1]) !is null );
674     // ... che eseguito si occupa di gestire il socket, e aggiornare client e server side instances.
675     auto op = sst.ops[$-1];
676     op.execute(MarsClientMock(), cst, sst);
677 
678     // ...posso aggiornare uno dei valori con update, in questo caso la primary key è la colonna c1
679     sst.updateRow(sst.KeysStruct(2), sst.ColumnsStruct(2, "z"));
680     assert( sst.fixtures[sst.KeysStruct(2)] == sst.ColumnsStruct(2, "z") );
681     +/
682 }
683 /+
684 unittest
685 {
686     version(unittest_starwars){
687         import mars.starwars;
688         enum schema = starwarsSchema();
689 
690         auto people = new ServerSideTable!(MarsClientMock, schema.tables[0]);
691         auto scores = new ServerSideTable!(MarsClientMock, schema.tables[3]);
692         auto databaseService = DatabaseService("127.0.0.1", 5432, "starwars");
693         AuthoriseError err;
694         auto db = databaseService.connect("jedi", "force", err);
695         db.executeUnsafe("begin transaction");
696         
697         auto rows = people.selectRows(db);
698         assert( rows[0] == luke, rows[0].to!string );
699 
700         auto paolo = Person("Paolo", "male", [0x00, 0x01, 0x02, 0x03, 0x04], 1.80);
701         InsertError ierr;
702         auto inserted = people.insertRecord(db, paolo, ierr);
703         assert(inserted == paolo);
704         
705 
706         //import std.stdio;
707         //foreach(row; rows) writeln("---->>>>>", row);
708         //assert(false);
709     }
710 }
711 +/
712