1 module dpq2.answer; 2 3 @trusted: 4 5 public import dpq2.types.to_d_types; 6 public import dpq2.types.to_bson; 7 public import dpq2.oids; 8 9 import dpq2; 10 11 import core.vararg; 12 import std..string: toStringz, fromStringz; 13 import std.exception: enforceEx; 14 import core.exception: OutOfMemoryError; 15 import std.bitmanip: bigEndianToNative; 16 import std.typecons: Nullable; 17 18 /// Result table's cell coordinates 19 private struct Coords 20 { 21 size_t row; /// Row 22 size_t col; /// Column 23 } 24 25 package immutable final class ResultContainer 26 { 27 // ResultContainer allows only one copy of PGresult* due to avoid double free. 28 // For the same reason this class is declared as final. 29 private PGresult* result; 30 alias result this; 31 32 nothrow invariant() 33 { 34 assert( result != null ); 35 } 36 37 package this(immutable PGresult* r) 38 { 39 assert(r); 40 41 result = r; 42 } 43 44 ~this() 45 { 46 assert(result != null); 47 48 PQclear(result); 49 } 50 } 51 52 /// Contains result of query regardless of whether it contains an error or data answer 53 immutable class Result 54 { 55 private ResultContainer result; 56 57 package this(immutable ResultContainer r) 58 { 59 result = r; 60 } 61 62 @property 63 ExecStatusType status() nothrow 64 { 65 return PQresultStatus(result); 66 } 67 68 @property 69 string statusString() 70 { 71 return to!string(fromStringz(PQresStatus(status))); 72 } 73 74 @property 75 string resultErrorMessage() 76 { 77 return to!string( PQresultErrorMessage(result) ); 78 } 79 80 immutable(Answer) getAnswer() 81 { 82 return new immutable Answer(result); 83 } 84 85 debug string toString() 86 { 87 import std.ascii: newline; 88 89 string err = resultErrorMessage(); 90 91 return statusString()~(err.length != 0 ? newline~err : ""); 92 } 93 } 94 95 /// Contains result of query with valid data answer 96 immutable class Answer : Result 97 { 98 package this(immutable ResultContainer r) 99 { 100 super(r); 101 102 checkAnswerForErrors(); 103 } 104 105 private void checkAnswerForErrors() 106 { 107 switch(status) 108 { 109 case PGRES_COMMAND_OK: 110 case PGRES_TUPLES_OK: 111 break; 112 113 case PGRES_EMPTY_QUERY: 114 throw new AnswerException(ExceptionType.EMPTY_QUERY, 115 "Empty query", __FILE__, __LINE__); 116 117 case PGRES_FATAL_ERROR: 118 throw new AnswerException(ExceptionType.FATAL_ERROR, 119 resultErrorMessage, __FILE__, __LINE__); 120 121 default: 122 throw new AnswerException(ExceptionType.UNDEFINED_FIXME, 123 "Please report if you came across this error! status="~to!string(status)~": "~statusString~"\r\n"~ 124 resultErrorMessage, __FILE__, __LINE__); 125 } 126 } 127 128 /// Returns the command status tag from the SQL command that generated the PGresult 129 /** 130 * Commonly this is just the name of the command, but it might include 131 * additional data such as the number of rows processed. The caller should 132 * not free the result directly. It will be freed when the associated 133 * PGresult handle is passed to PQclear. 134 */ 135 @property string cmdStatus() 136 { 137 return to!string( PQcmdStatus(result) ); 138 } 139 140 /// Returns row count 141 @property size_t length() nothrow { return PQntuples(result); } 142 143 /// Returns column count 144 @property size_t columnCount() nothrow { return PQnfields(result); } 145 146 /// Returns column format 147 ValueFormat columnFormat( const size_t colNum ) 148 { 149 assertCol( colNum ); 150 return cast(ValueFormat) PQfformat(result, to!int(colNum)); 151 } 152 153 /// Returns column Oid 154 @property OidType OID( size_t colNum ) 155 { 156 assertCol( colNum ); 157 158 return oid2oidType(PQftype(result, to!int(colNum))); 159 } 160 161 @property bool isSupportedArray( const size_t colNum ) 162 { 163 assertCol(colNum); 164 165 return dpq2.oids.isSupportedArray(OID(colNum)); 166 } 167 168 /// Returns column number by field name 169 size_t columnNum( string columnName ) 170 { 171 size_t n = PQfnumber(result, toStringz(columnName)); 172 173 if( n == -1 ) 174 throw new AnswerException(ExceptionType.COLUMN_NOT_FOUND, 175 "Column '"~columnName~"' is not found", __FILE__, __LINE__); 176 177 return n; 178 } 179 180 /// Returns column name by field number 181 string columnName( in size_t colNum ) 182 { 183 const char* s = PQfname(cast(PGresult*) result, to!int(colNum)); // FIXME: result should be a const 184 185 if( s == null ) 186 throw new AnswerException( 187 ExceptionType.OUT_OF_RANGE, 188 "Column "~to!string(colNum)~" is out of range 0.."~to!string(columnCount), 189 __FILE__, __LINE__ 190 ); 191 192 return to!string(fromStringz(s)); 193 } 194 195 /// Returns row of cells 196 immutable (Row) opIndex(in size_t row) 197 { 198 return immutable Row( 199 cast(immutable)(this), // legal because this.ctor is immutable 200 row 201 ); 202 } 203 204 debug override string toString() 205 { 206 import std.ascii: newline; 207 208 string res; 209 210 foreach(row; rangify(this)) 211 res ~= row.toString~newline; 212 213 return super.toString~newline~res; 214 } 215 216 @property 217 private string resultErrorField(int fieldcode) 218 { 219 return to!string( PQresultErrorField(cast(PGresult*)result, fieldcode) ); // FIXME: result should be a const 220 } 221 222 private void assertCol( const size_t c ) 223 { 224 if(!(c < columnCount)) 225 throw new AnswerException( 226 ExceptionType.OUT_OF_RANGE, 227 "Column "~to!string(c)~" is out of range 0.."~to!string(columnCount)~" of result columns", 228 __FILE__, __LINE__ 229 ); 230 } 231 232 private void assertRow( const size_t r ) 233 { 234 if(!(r < length)) 235 throw new AnswerException( 236 ExceptionType.OUT_OF_RANGE, 237 "Row "~to!string(r)~" is out of range 0.."~to!string(length)~" of result rows", 238 __FILE__, __LINE__ 239 ); 240 } 241 242 private void assertCoords( const Coords c ) 243 { 244 assertRow( c.row ); 245 assertCol( c.col ); 246 } 247 } 248 249 auto rangify(T)(T obj) 250 { 251 struct Rangify(T) 252 { 253 T obj; 254 alias obj this; 255 256 private int curr; 257 258 this(T o) 259 { 260 obj = o; 261 } 262 263 @property auto front(){ return obj[curr]; } 264 @property void popFront(){ ++curr; } 265 @property bool empty(){ return curr >= obj.length; } 266 } 267 268 return Rangify!(T)(obj); 269 } 270 271 /// Represents one row from the answer table 272 immutable struct Row 273 { 274 private Answer answer; 275 private size_t row; 276 277 this(immutable Answer answer, in size_t row) 278 { 279 answer.assertRow( row ); 280 281 this.answer = answer; 282 this.row = row; 283 } 284 285 /// Returns cell size 286 @property 287 size_t size( const size_t col ) 288 { 289 answer.assertCol(col); 290 return PQgetlength(answer.result, to!int(row), to!int(col)); 291 } 292 293 /// Value NULL checking 294 /// Do not confuse it with Nullable's isNull property 295 @property 296 bool isNULL( const size_t col ) 297 { 298 answer.assertCol(col); 299 300 return PQgetisnull(answer.result, to!int(row), to!int(col)) != 0; 301 } 302 303 immutable (Nullable!Value) opIndex(in size_t col) 304 { 305 answer.assertCoords( Coords( row, col ) ); 306 307 auto v = cast(immutable) PQgetvalue(answer.result, to!int(row), to!int(col)); 308 auto s = size( col ); 309 310 Nullable!Value r; 311 312 if(!isNULL(col)) 313 { 314 // it is legal to cast here because immutable value will be returned 315 r = Value(cast(ubyte[]) v[0..s], answer.OID(col), answer.columnFormat(col)); 316 } 317 318 return cast(immutable) r; 319 } 320 321 immutable (Nullable!Value) opIndex(in string column) 322 { 323 return opIndex(columnNum(column)); 324 } 325 326 /// Returns column number by field name 327 size_t columnNum( string columnName ) 328 { 329 return answer.columnNum( columnName ); 330 } 331 332 /// Returns column name by field number 333 string columnName( in size_t colNum ) 334 { 335 return answer.columnName( colNum ); 336 } 337 338 /// Returns column count 339 @property size_t length() { return answer.columnCount(); } 340 341 debug string toString() 342 { 343 string res; 344 345 foreach(val; rangify(this)) 346 res ~= dpq2.answer.toString(val)~"\t"; 347 348 return res; 349 } 350 } 351 352 /// Link to the cell of the answer table 353 struct Value // TODO: better to make it immutable, but Nullable don't allow use it with const or immutable 354 { 355 package ValueFormat format; 356 package OidType oidType; 357 package ubyte[] value; 358 359 this(ubyte[] value, in OidType t, in ValueFormat f = ValueFormat.BINARY) 360 { 361 this.value = value; 362 format = f; 363 oidType = t; 364 } 365 366 @property 367 bool isSupportedArray() const 368 { 369 return dpq2.oids.isSupportedArray(oidType); 370 } 371 372 @property 373 immutable (Array) asArray() immutable 374 { 375 if(!isSupportedArray) 376 throw new AnswerException(ExceptionType.NOT_ARRAY, 377 "Format of the column is "~to!string(oidType)~", isn't supported array", 378 __FILE__, __LINE__ 379 ); 380 381 return immutable Array(this); 382 } 383 } 384 385 debug string toString(immutable (Nullable!Value) v) 386 { 387 return v.isNull ? "NULL" : v.toBson.toString; 388 } 389 390 private struct ArrayHeader_net // network byte order 391 { 392 ubyte[4] ndims; // number of dimensions of the array 393 ubyte[4] dataoffset_ign; // offset for data, removed by libpq. may be it contains isNULL flag! 394 ubyte[4] OID; // element type OID 395 } 396 397 private struct Dim_net // network byte order 398 { 399 ubyte[4] dim_size; // number of elements in dimension 400 ubyte[4] lbound; // unknown 401 } 402 403 package struct ArrayProperties 404 { 405 OidType OID; 406 int nDims; /// Number of dimensions 407 int[] dimsSize; /// Dimensions sizes info 408 size_t nElems; /// Total elements 409 size_t dataOffset; 410 411 this(in Value cell) 412 { 413 const ArrayHeader_net* h = cast(ArrayHeader_net*) cell.value.ptr; 414 nDims = bigEndianToNative!int(h.ndims); 415 OID = oid2oidType(bigEndianToNative!Oid(h.OID)); 416 417 if(!(nDims > 0)) 418 throw new AnswerException(ExceptionType.SMALL_DIMENSIONS_NUM, 419 "Dimensions number is too small, it must be positive value", 420 __FILE__, __LINE__ 421 ); 422 423 dataOffset = ArrayHeader_net.sizeof + Dim_net.sizeof * nDims; 424 425 auto ds = new int[ nDims ]; 426 427 // Recognize dimensions of array 428 int n_elems = 1; 429 for( auto i = 0; i < nDims; ++i ) 430 { 431 Dim_net* d = (cast(Dim_net*) (h + 1)) + i; 432 433 int dim_size = bigEndianToNative!int( d.dim_size ); 434 int lbound = bigEndianToNative!int(d.lbound); 435 436 if(!(dim_size > 0)) 437 throw new AnswerException(ExceptionType.FATAL_ERROR, 438 "Dimension size isn't positive ("~to!string(dim_size)~")", 439 __FILE__, __LINE__ 440 ); 441 442 // FIXME: What is lbound in postgresql array reply? 443 if(!(lbound == 1)) 444 throw new AnswerException(ExceptionType.UNDEFINED_FIXME, 445 "Please report if you came across this error! lbound=="~to!string(lbound), 446 __FILE__, __LINE__ 447 ); 448 449 ds[i] = dim_size; 450 n_elems *= dim_size; 451 } 452 453 nElems = n_elems; 454 dimsSize = ds; 455 } 456 } 457 458 /// Link to the cell of the answer table 459 immutable struct Array 460 { 461 ArrayProperties ap; 462 alias ap this; 463 464 private ubyte[][] elements; 465 private bool[] elementIsNULL; 466 467 this(immutable Value cell) 468 { 469 if(!(cell.format == ValueFormat.BINARY)) 470 throw new AnswerException(ExceptionType.NOT_BINARY, 471 msg_NOT_BINARY, __FILE__, __LINE__); 472 473 ap = cast(immutable) ArrayProperties(cell); 474 475 // Looping through all elements and fill out index of them 476 { 477 auto elements = new immutable (ubyte)[][ nElems ]; 478 auto elementIsNULL = new bool[ nElems ]; 479 480 size_t curr_offset = ap.dataOffset; 481 482 for(uint i = 0; i < nElems; ++i ) 483 { 484 ubyte[int.sizeof] size_net; // network byte order 485 size_net[] = cell.value[ curr_offset .. curr_offset + size_net.sizeof ]; 486 uint size = bigEndianToNative!uint( size_net ); 487 if( size == size.max ) // NULL magic number 488 { 489 elementIsNULL[i] = true; 490 size = 0; 491 } 492 else 493 { 494 elementIsNULL[i] = false; 495 } 496 curr_offset += size_net.sizeof; 497 elements[i] = cell.value[curr_offset .. curr_offset + size]; 498 curr_offset += size; 499 } 500 501 this.elements = elements.idup; 502 this.elementIsNULL = elementIsNULL.idup; 503 } 504 } 505 506 /// Returns Value struct by index 507 immutable (Nullable!Value) opIndex(int n) 508 { 509 return getValue(n); 510 } 511 512 /// Returns Value struct 513 /// Useful for multidimensional arrays 514 immutable (Nullable!Value) getValue( ... ) 515 { 516 auto n = coords2Serial( _argptr, _arguments ); 517 518 Nullable!Value r; 519 520 if(!elementIsNULL[n]) 521 { 522 // it is legal to cast here because immutable value will be returned 523 r = Value(cast(ubyte[]) elements[n], OID); 524 } 525 526 return cast(immutable) r; 527 } 528 529 /// Value NULL checking 530 bool isNULL( ... ) 531 { 532 auto n = coords2Serial( _argptr, _arguments ); 533 return elementIsNULL[n]; 534 } 535 536 private size_t coords2Serial( va_list _argptr, TypeInfo[] _arguments ) 537 { 538 assert( _arguments.length > 0, "Number of the arguments must be more than 0" ); 539 540 // Variadic args parsing 541 auto args = new int[ _arguments.length ]; 542 543 if(!(nDims == args.length)) 544 throw new AnswerException( 545 ExceptionType.OUT_OF_RANGE, 546 "Mismatched dimensions number in arguments and server reply", 547 __FILE__, __LINE__ 548 ); 549 550 for( uint i; i < args.length; ++i ) 551 { 552 assert( _arguments[i] == typeid(int) ); 553 args[i] = va_arg!(int)(_argptr); 554 555 if(!(dimsSize[i] > args[i])) 556 throw new AnswerException( 557 ExceptionType.OUT_OF_RANGE, 558 "Out of range", 559 __FILE__, __LINE__ 560 ); 561 } 562 563 // Calculates serial number of the element 564 auto inner = args.length - 1; // inner dimension 565 auto element_num = args[inner]; // serial number of the element 566 uint s = 1; // perpendicular to a vector which size is calculated currently 567 for( auto i = inner; i > 0; --i ) 568 { 569 s *= dimsSize[i]; 570 element_num += s * args[i-1]; 571 } 572 573 assert( element_num <= nElems ); 574 return element_num; 575 } 576 } 577 578 /// Notify 579 class Notify 580 { 581 private immutable PGnotify* n; 582 583 this(immutable PGnotify* pgn ) 584 { 585 n = pgn; 586 cast(void) enforceEx!OutOfMemoryError(n, "Can't write notify"); 587 } 588 589 ~this() 590 { 591 PQfreemem( cast(void*) n ); 592 } 593 594 /// Returns notification condition name 595 @property string name() { return to!string( n.relname ); } 596 597 /// Returns notification parameter 598 @property string extra() { return to!string( n.extra ); } 599 600 /// Returns process ID of notifying server process 601 @property size_t pid() { return n.be_pid; } 602 603 nothrow invariant() 604 { 605 assert( n != null ); 606 } 607 } 608 609 immutable msg_NOT_BINARY = "Format of the column is not binary"; 610 611 /// Exception types 612 enum ExceptionType 613 { 614 UNDEFINED_FIXME, /// Undefined, please report if you came across this error 615 COLUMN_NOT_FOUND, /// Column is not found 616 OUT_OF_RANGE, 617 NOT_ARRAY, /// Format of the column isn't array 618 NOT_BINARY, /// Format of the column isn't binary 619 NOT_TEXT, /// Format of the column isn't text string 620 NOT_IMPLEMENTED, /// Support of this type isn't implemented (or format isn't matches to specified D type) 621 SMALL_DIMENSIONS_NUM, 622 SIZE_MISMATCH, 623 EMPTY_QUERY, 624 FATAL_ERROR 625 } 626 627 /// Exception 628 class AnswerException : Dpq2Exception 629 { 630 ExceptionType type; /// Exception type 631 632 this(ExceptionType t, string msg, string file, size_t line) 633 { 634 type = t; 635 super(msg, file, line); 636 } 637 } 638 639 void _integration_test( string connParam ) 640 { 641 import core.exception: AssertError; 642 643 auto conn = new Connection; 644 conn.connString = connParam; 645 conn.connect(); 646 647 { 648 string sql_query = 649 "select now() as time, 'abc'::text as field_name, 123, 456.78\n"~ 650 "union all\n"~ 651 652 "select now(), 'def'::text, 456, 910.11\n"~ 653 "union all\n"~ 654 655 "select NULL, 'ijk_АБВГД'::text, 789, 12345.115345"; 656 657 auto e = conn.exec(sql_query); 658 659 assert( e[1][2].as!PGtext == "456" ); 660 assert( e[2][1].as!PGtext == "ijk_АБВГД" ); 661 assert( !e[0].isNULL(0) ); 662 assert( e[2].isNULL(0) ); 663 assert( e.columnNum( "field_name" ) == 1 ); 664 assert( e[1]["field_name"].as!PGtext == "def" ); 665 } 666 667 QueryParams p; 668 p.resultFormat = ValueFormat.BINARY; 669 p.sqlCommand = "SELECT "~ 670 "-32761::smallint, "~ 671 "-2147483646::integer, "~ 672 "'first line\nsecond line'::text, "~ 673 "array[[[1, 2, 3], "~ 674 "[4, 5, 6]], "~ 675 676 "[[7, 8, 9], "~ 677 "[10, 11,12]], "~ 678 679 "[[13,14,NULL], "~ 680 "[16,17,18]]]::integer[] as test_array, "~ 681 "NULL,"~ 682 "array[11,22,NULL,44]::integer[] as small_array, "~ 683 "array['1','23',NULL,'789A']::text[] as text_array"; 684 685 auto r = conn.exec( p ); 686 687 { 688 assert( r[0].isNULL(4) ); 689 assert( !r[0].isNULL(2) ); 690 691 assert( r.OID(3) == OidType.Int4Array ); 692 assert( r.isSupportedArray(3) ); 693 assert( !r.isSupportedArray(2) ); 694 auto v = r[0]["test_array"]; 695 assert( v.isSupportedArray ); 696 assert( !r[0][2].isSupportedArray ); 697 auto a = v.asArray; 698 assert( a.OID == OidType.Int4 ); 699 assert( a.getValue(2,1,2).as!PGinteger == 18 ); 700 assert( a.isNULL(2,0,2) ); 701 assert( !a.isNULL(2,1,2) ); 702 assert( r[0]["small_array"].asArray[1].as!PGinteger == 22 ); 703 assert( r[0]["small_array"].asArray[2].isNull ); 704 assert( r[0]["text_array"].asArray[2].isNull ); 705 assert( r.columnName(3) == "test_array" ); 706 assert( r[0].columnName(3) == "test_array" ); 707 708 { 709 bool isNullFlag = false; 710 try 711 cast(void) r[0][4].as!PGsmallint; 712 catch(AssertError) 713 isNullFlag = true; 714 finally 715 assert(isNullFlag); 716 } 717 } 718 719 // Notifies test 720 conn.exec( "listen test_notify; notify test_notify" ); 721 assert( conn.getNextNotify.name == "test_notify" ); 722 723 // Async query test 1 724 conn.sendQuery( "select 123; select 456; select 789" ); 725 while( conn.getResult() !is null ){} 726 assert( conn.getResult() is null ); // removes null answer at the end 727 728 // Async query test 2 729 conn.sendQuery( p ); 730 while( conn.getResult() !is null ){} 731 assert( conn.getResult() is null ); // removes null answer at the end 732 733 { 734 // Range test 735 auto rowsRange = rangify(r); 736 size_t count = 0; 737 738 foreach(row; rowsRange) 739 foreach(elem; rangify(row)) 740 count++; 741 742 assert(count == 7); 743 } 744 745 assert(r.toString.length > 40); 746 747 destroy(r); 748 749 { 750 bool exceptionFlag = false; 751 752 try conn.exec("WRONG SQL QUERY"); 753 catch(AnswerException e) 754 { 755 exceptionFlag = true; 756 assert(e.msg.length > 20); // error message check 757 } 758 finally 759 assert(exceptionFlag); 760 } 761 }