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