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