1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552
|
<HTML>
<HEAD>
<TITLE>prguide.htm</TITLE>
<LINK REL="ToC" HREF="httoc.htm">
<LINK REL="Index" HREF="htindex.htm">
<LINK REL="Next" HREF="prguide8.htm">
<LINK REL="Previous" HREF="prguide6.htm"></HEAD>
<BODY BGCOLOR="#FFFFFF">
<P ALIGN=CENTER>
<A HREF="prguide6.htm" TARGET="_self"><IMG SRC="graprev.gif" WIDTH = 32 HEIGHT = 32 BORDER = 0 ALT="Previous Page"></A>
<A HREF="httoc.htm" TARGET="_self"><IMG SRC="gratoc.gif" WIDTH = 32 HEIGHT = 32 BORDER = 0 ALT="TOC"></A>
<A HREF="htindex.htm" TARGET="_self"><IMG SRC="graindex.gif" WIDTH = 32 HEIGHT = 32 BORDER = 0 ALT="Index"></A>
<A HREF="prguide8.htm" TARGET="_self"><IMG SRC="granext.gif" WIDTH = 32 HEIGHT = 32 BORDER = 0 ALT="Next Page"></A>
<HR ALIGN=CENTER>
<P>
<UL>
<LI>
<A HREF="#E9E8" >Retrieving Results</A>
<UL>
<LI>
<A HREF="#E10E28" >Assigning Storage for Results (Binding)</A>
<LI>
<A HREF="#E10E29" >Determining the Characteristics of a Result Set</A>
<LI>
<A HREF="#E10E30" >Fetching Result Data</A>
<LI>
<A HREF="#E10E31" >Using Cursors</A>
<LI>
<A HREF="#E10E32" >ODBC Extensions for Results</A>
<UL>
<LI>
<A HREF="#E11E29" >Retrieving Data from Unbound Columns</A>
<LI>
<A HREF="#E11E30" >Assigning Storage for Rowsets (Binding)</A>
<UL>
<LI>
<A HREF="#E12E14" >Column-Wise Binding</A>
<LI>
<A HREF="#E12E15" >Row-Wise Binding</A></UL>
<LI>
<A HREF="#E11E31" >Retrieving Rowset Data</A>
<LI>
<A HREF="#E11E32" >Using Block and Scrollable Cursors</A>
<UL>
<LI>
<A HREF="#E12E16" >Block Cursors</A>
<LI>
<A HREF="#E12E17" >Scrollable Cursors</A>
<LI>
<A HREF="#E12E18" >Specifying the Cursor Type</A>
<LI>
<A HREF="#E12E19" >Specifying Cursor Concurrency</A></UL>
<LI>
<A HREF="#E11E33" >Using Bookmarks</A>
<LI>
<A HREF="#E11E34" >Modifying Result Set Data</A>
<UL>
<LI>
<A HREF="#E12E20" >Executing Positioned Update and Delete Statements</A>
<LI>
<A HREF="#E12E21" >Modifying Data with SQLSetPos</A></UL>
<LI>
<A HREF="#E11E35" >Processing Multiple Results</A></UL></UL></UL>
<HR ALIGN=CENTER>
<A NAME="E9E8"></A>
<H1>
<FONT FACE="Arial"><B>RETRIEVING RESULTS</B><A NAME="I2"></A></FONT></H1>
<BR>
<BLOCKQUOTE>
<P>A <B>SELECT</B> statement is used to retrieve data that meets a given set of specifications. For example, <B>SELECT * FROM EMPLOYEE WHERE </B><B>EMPNAME = "Jones"</B> is used to retrieve all columns of all rows in EMPLOYEE where the employee’s name is Jones. ODBC extension functions also can retrieve data. For example, <B>SQLColumns</B> retrieves data about columns in the data source. These sets of data, called result sets, can contain zero or more rows.<A NAME="I3"></A>
</BLOCKQUOTE>
<BLOCKQUOTE>
<P>Note that other SQL statements, such as <B>GRANT</B> or <B>REVOKE</B>, do not return result sets. For these statements, the return code from <B>SQLExecute</B> or <B>SQLExecDirect</B> is usually the only source of information as to whether the statement was successful. (For <B>INSERT</B>, <B>UPDATE</B>, and <B>DELETE</B> statements, an application can call <B>SQLRowCount</B> to return the number of affected rows.)<A NAME="I4"></A>
</BLOCKQUOTE>
<BLOCKQUOTE>
<P>The steps an application takes to process a result set depends on what is known about it.<A NAME="I5"></A>
</BLOCKQUOTE>
<UL>
<BLOCKQUOTE>
<LI><B>Known result set </B>The application knows the exact form of the SQL statement, and therefore the result set, at compile time. For example, the query <B>SELECT</B> <B>EMPNO</B>, <B>EMPNAME FROM EMPLOYEE</B> returns two specific columns.<A NAME="I6"></A>
</BLOCKQUOTE>
<BLOCKQUOTE>
<LI><B>Unknown result set </B>The application does not know the exact form of the SQL statement, and therefore the result set, at compile time. For example, the ad hoc query <B>SELECT * FROM EMPLOYEE</B> returns all currently defined columns in the EMPLOYEE table. The application may not be able to predict the format of these results prior to execution.
</BLOCKQUOTE></UL>
<A NAME="E10E28"></A>
<H2>
<FONT FACE="Arial"><B>Assigning Storage for Results (Binding)</B><A NAME="I7"></A></FONT></H2>
<BLOCKQUOTE>
<P>An application can assign storage for results before or after it executes an SQL statement. If an application prepares or executes the SQL statement first, it can inquire about the result set before it assigns storage for results. For example, if the result set is unknown, the application must retrieve the number of columns before it can assign storage for them.<A NAME="I8"></A><A NAME="I9"></A>
</BLOCKQUOTE>
<BLOCKQUOTE>
<P>To associate storage for a column of data, an application calls <B>SQLBindCol</B> and passes it the following information:<A NAME="I10"></A>
</BLOCKQUOTE>
<UL>
<BLOCKQUOTE>
<LI>The data type to which the data is to be converted. For more information, see "Converting Data from SQL to C Data Types" in Appendix D, "Data Types."<A NAME="I11"></A>
</BLOCKQUOTE>
<BLOCKQUOTE>
<LI>The address of an output buffer for the data. The application must allocate this buffer and it must be large enough to hold the data in the form to which it is converted.<A NAME="I12"></A>
</BLOCKQUOTE>
<BLOCKQUOTE>
<LI>The length of the output buffer. This value is ignored if the returned data has a fixed width in C, such as an integer, real number, or date structure.<A NAME="I13"></A>
</BLOCKQUOTE>
<BLOCKQUOTE>
<LI>The address of a storage buffer in which to return the number of bytes of available data.
</BLOCKQUOTE></UL>
<A NAME="E10E29"></A>
<H2>
<FONT FACE="Arial"><B>Determining the Characteristics of a Result Set</B><A NAME="I14"></A></FONT></H2>
<BLOCKQUOTE>
<P>To determine the characteristics of a result set, an application can:<A NAME="I15"></A><A NAME="I16"></A>
</BLOCKQUOTE>
<UL>
<BLOCKQUOTE>
<LI>Call <B>SQLNumResultCols</B> to determine how many columns a request returned.<A NAME="I17"></A><A NAME="I18"></A><A NAME="I19"></A>
</BLOCKQUOTE>
<BLOCKQUOTE>
<LI>Call <B>SQLColAttributes</B> or <B>SQLDescribeCol</B> to describe a column in the result set.<A NAME="I20"></A><A NAME="I21"></A><A NAME="I22"></A><A NAME="I23"></A><A NAME="I24"></A>
</BLOCKQUOTE></UL>
<BLOCKQUOTE>
<P>If the result set is unknown, an application can use the information returned by these functions to bind the columns in the result set. An application can call these functions at any time after a statement is prepared or executed. Note that, although <B>SQLRowCount</B> can sometimes return the number of rows in a result set, it is not guaranteed to do so. Few data sources support this functionality and interoperable applications should not rely on it.<A NAME="I25"></A>
</BLOCKQUOTE>
<BLOCKQUOTE>
<HR ALIGN=CENTER>
<NOTE>Note For optimal performance, an application should call <B>SQLColAttributes</B>, <B>SQLDescribeCol</B>, and <B>SQLNumResultCols</B> after a statement is executed. In data sources that emulate statement preparation, these functions sometimes execute more slowly before a statement is executed because the information returned by them is not readily available until after the statement is executed.</NOTE>
<HR ALIGN=CENTER>
</BLOCKQUOTE>
<A NAME="E10E30"></A>
<H2>
<FONT FACE="Arial"><B>Fetching Result Data</B><A NAME="I26"></A><A NAME="I27"></A><A NAME="I28"></A><A NAME="I29"></A></FONT></H2>
<BLOCKQUOTE>
<P>To retrieve a row of data from the result set, an application:<A NAME="I30"></A>
</BLOCKQUOTE>
<UL>
<BLOCKQUOTE>
<LI>1. Calls <B>SQLBindCol</B> to bind the columns of the result set to storage locations if it has not already done so.<A NAME="I31"></A>
</BLOCKQUOTE>
<BLOCKQUOTE>
<LI>2. Calls <B>SQLFetch</B> to move to the next row in the result set and retrieve data for all bound columns.<A NAME="I32"></A>
</BLOCKQUOTE></UL>
<A NAME="E10E31"></A>
<H2>
<FONT FACE="Arial"><B>Using Cursors</B><A NAME="I33"></A><A NAME="I34"></A></FONT></H2>
<BLOCKQUOTE>
<P>To keep track of its position in the result set, a driver maintains a cursor. The cursor is so named because it indicates the current position in the result set, just as the cursor on a CRT screen indicates current position.<A NAME="I35"></A>
</BLOCKQUOTE>
<BLOCKQUOTE>
<P>Each time an application calls <B>SQLFetch</B>, the driver moves the cursor to the next row and returns that row. The cursor supported by the core ODBC functions only scrolls forward, one row at a time. (To reretrieve a row of data that it has already retrieved from the result set, the application must close the cursor by calling <B>SQLFreeStmt</B> with the SQL_CLOSE option, reexecute the <B>SELECT</B> statement, and fetch rows with <B>SQLFetch</B> until the target row is retrieved.)<A NAME="I36"></A>
</BLOCKQUOTE>
<BLOCKQUOTE>
<HR ALIGN=CENTER>
<NOTE>Important Committing or rolling back a transaction, either by calling <B>SQLTransact</B> or by using the SQL_AUTOCOMMIT connection option, can cause the data source to close the cursors for all <I>hstmts</I> on an <I>hdbc</I>. For more information, see the SQL_CURSOR_COMMIT_BEHAVIOR and SQL_CURSOR_ROLLBACK_BEHAVIOR information types in <B>SQLGetInfo</B>.</NOTE>
<HR ALIGN=CENTER>
</BLOCKQUOTE>
<A NAME="E10E32"></A>
<H2>
<FONT FACE="Arial"><B>ODBC Extensions for Results</B><A NAME="I37"></A></FONT></H2>
<BLOCKQUOTE>
<P>ODBC extends the X/Open and SQL Access Group Call Level Interface to provide additional functions related to retrieving results. The remainder of this chapter describes these functions. To determine if a driver supports a specific function, an application calls <B>SQLGetFunctions</B>.
</BLOCKQUOTE>
<A NAME="E11E29"></A>
<H3>
<FONT FACE="Arial">Retrieving Data from Unbound Columns<A NAME="I38"></A><A NAME="I39"></A><A NAME="I40"></A><A NAME="I41"></A><A NAME="I42"></A><A NAME="I43"></A><A NAME="I44"></A></FONT></H3>
<BLOCKQUOTE>
<P>To retrieve data from unbound columns — that is, columns for which storage has not been assigned with <B>SQLBindCol</B> — an application uses <B>SQLGetData</B>. The application first calls <B>SQLFetch</B> or <B>SQLExtendedFetch</B> to position the cursor on the next row. It then calls <B>SQLGetData</B> to retrieve data from specific unbound columns.<A NAME="I45"></A>
</BLOCKQUOTE>
<BLOCKQUOTE>
<P>An application may retrieve data from both bound and unbound columns in the same row. It calls <B>SQLBindCol</B> to bind as many columns as desired. It calls <B>SQLFetch</B> or <B>SQLExtendedFetch</B> to position the cursor on the next row of the result set and retrieve all bound columns. It then calls <B>SQLGetData</B> to retrieve data from unbound columns.<A NAME="I46"></A>
</BLOCKQUOTE>
<BLOCKQUOTE>
<P>If the data type of a column is character, binary, or data source–specific and the column contains more data than can be retrieved in a single call, an application may call <B>SQLGetData</B> more than once for that column, as long as the data is being transferred to a buffer of type SQL_C_CHAR or SQL_C_BINARY. For example, data of the SQL_LONGVARBINARY and SQL_LONGVARCHAR types may need to be retrieved in several parts.<A NAME="I47"></A><A NAME="I48"></A><A NAME="I49"></A>
</BLOCKQUOTE>
<BLOCKQUOTE>
<P>For maximum interoperability, an application should only call <B>SQLGetData</B> for columns to the right of the rightmost bound column and then only in left-to-right order. To determine if a driver can return data with <B>SQLGetData</B> for any column (including unbound columns before the last bound column and any bound columns) or in any order, an application calls <B>SQLGetInfo</B> with the SQL_GETDATA_EXTENSIONS option.
</BLOCKQUOTE>
<A NAME="E11E30"></A>
<H3>
<FONT FACE="Arial">Assigning Storage for Rowsets (Binding)<A NAME="I50"></A></FONT></H3>
<BLOCKQUOTE>
<P>In addition to binding individual rows of data, an application can call <B>SQLBindCol</B> to assign storage for a <I>rowset</I> (one or more rows of data). By default, rowsets are bound in column-wise fashion. They can also be bound in row-wise fashion.<A NAME="I51"></A>
</BLOCKQUOTE>
<BLOCKQUOTE>
<P>To specify how many rows of data are in a rowset, an application calls <B>SQLSetStmtOption</B> with the SQL_ROWSET_SIZE option.
</BLOCKQUOTE>
<BLOCKQUOTE>
<A NAME="E12E14"></A>
<H4>
<FONT>Column-Wise Binding<A NAME="I52"></A><A NAME="I53"></A><A NAME="I54"></A></FONT></H4>
</BLOCKQUOTE>
<BLOCKQUOTE>
<P>To assign storage for column-wise bound results, an application performs the following steps for each column to be bound:<A NAME="I55"></A><A NAME="I56"></A>
</BLOCKQUOTE>
<UL>
<BLOCKQUOTE>
<LI>1. Allocates an array of data storage buffers. The array has as many elements as there are rows in the rowset.<A NAME="I57"></A>
</BLOCKQUOTE>
<BLOCKQUOTE>
<LI>2. Allocates an array of storage buffers to hold the number of bytes available to return for each data value. The array has as many elements as there are rows in the rowset.<A NAME="I58"></A>
</BLOCKQUOTE>
<BLOCKQUOTE>
<LI>3. Calls <B>SQLBindCol</B> and specifies the address of the data array, the size of one element of the data array, the address of the number-of-bytes array, and the type to which the data will be converted. When data is retrieved, the driver will use the array element size to determine where to store successive rows of data in the array.
</BLOCKQUOTE></UL>
<BLOCKQUOTE>
<A NAME="E12E15"></A>
<H4>
<FONT>Row-Wise Binding<A NAME="I59"></A><A NAME="I60"></A></FONT></H4>
</BLOCKQUOTE>
<BLOCKQUOTE>
<P>To assign storage for row-wise bound results, an application performs the following steps:<A NAME="I61"></A>
</BLOCKQUOTE>
<UL>
<BLOCKQUOTE>
<LI>1. Declares a structure that can hold a single row of retrieved data and the associated data lengths. (For each column to be bound, the structure contains one field to contain data and one field to contain the number of bytes of data available to return.)<A NAME="I62"></A>
</BLOCKQUOTE>
<BLOCKQUOTE>
<LI>2. Allocates an array of these structures. This array has as many elements as there are rows in the rowset.<A NAME="I63"></A>
</BLOCKQUOTE>
<BLOCKQUOTE>
<LI>3. Calls <B>SQLBindCol</B> for each column to be bound. In each call, the application specifies the address of the column’s data field in the first array element, the size of the data field, the address of the column’s number-of-bytes field in the first array element, and the type to which the data will be converted.<A NAME="I64"></A>
</BLOCKQUOTE>
<BLOCKQUOTE>
<LI>4. Calls <B>SQLSetStmtOption</B> with the SQL_BIND_TYPE option and specifies the size of the structure. When the data is retrieved, the driver will use the structure size to determine where to store successive rows of data in the array.
</BLOCKQUOTE></UL>
<A NAME="E11E31"></A>
<H3>
<FONT FACE="Arial">Retrieving Rowset Data<A NAME="I65"></A><A NAME="I66"></A><A NAME="I67"></A></FONT></H3>
<BLOCKQUOTE>
<P>Before it retrieves rowset data, an application calls <B>SQLSetStmtOption</B> with the SQL_ROWSET_SIZE option to specify the number of rows in the rowset. It then binds columns in the rowset with <B>SQLBindCol</B>. The rowset may be bound in column-wise or row-wise fashion. For more information, see "Assigning Storage for Rowsets (Binding)" previous in this chapter.<A NAME="I68"></A><A NAME="I69"></A><A NAME="I70"></A>
</BLOCKQUOTE>
<BLOCKQUOTE>
<P>To retrieve rowset data, an application calls <B>SQLExtendedFetch</B>.<A NAME="I71"></A><A NAME="I72"></A>
</BLOCKQUOTE>
<BLOCKQUOTE>
<P>For maximum interoperability, an application should not use <B>SQLGetData</B> to retrieve data from unbound columns in a block (more than one row) of data that has been retrieved with <B>SQLExtendedFetch</B>. To determine if a driver can return data with <B>SQLGetData</B> from a block of data, an application calls <B>SQLGetInfo</B> with the SQL_GETDATA_EXTENSIONS option.
</BLOCKQUOTE>
<A NAME="E11E32"></A>
<H3>
<FONT FACE="Arial">Using Block and Scrollable Cursors<A NAME="I73"></A></FONT></H3>
<BLOCKQUOTE>
<P>As originally designed, cursors in SQL only scroll forward through a result set, returning one row at a time. However, interactive applications often require forward and backward scrolling, absolute or relative positioning within the result set, and the ability to retrieve and update blocks of data, or <I>rowsets</I>.<A NAME="I74"></A><A NAME="I75"></A>
</BLOCKQUOTE>
<BLOCKQUOTE>
<P>To retrieve and update rowset data, ODBC provides a <I>block</I> cursor attribute. To allow an application to scroll forwards or backwards through the result set, or move to an absolute or relative position in the result set, ODBC provides a <I>scrollable</I> cursor attribute. Cursors may have one or both attributes.
</BLOCKQUOTE>
<BLOCKQUOTE>
<A NAME="E12E16"></A>
<H4>
<FONT>Block Cursors<A NAME="I76"></A><A NAME="I77"></A><A NAME="I78"></A></FONT></H4>
</BLOCKQUOTE>
<BLOCKQUOTE>
<P>An application calls <B>SQLSetStmtOption</B> with the SQL_ROWSET_SIZE option to specify the rowset size. The application can call <B>SQLSetStmtOption</B> to change the rowset size at any time. Each time the application calls <B>SQLExtendedFetch</B>, the driver returns the next <I>rowset size</I> rows of data. After the data is returned, the cursor points to the first row in the rowset. By default, the rowset size is one.
</BLOCKQUOTE>
<BLOCKQUOTE>
<A NAME="E12E17"></A>
<H4>
<FONT>Scrollable Cursors<A NAME="I79"></A><A NAME="I80"></A></FONT></H4>
</BLOCKQUOTE>
<BLOCKQUOTE>
<P>Applications have different needs in their ability to sense changes in the tables underlying a result set. For example, when balancing financial data, an accountant needs data that appears static; it is impossible to balance books when the data is continually changing. When selling concert tickets, a clerk needs up-to-the minute, or dynamic, data on which tickets are still available. Various cursor models are designed to meet these needs, each of which requires different sensitivities to changes in the tables underlying the result set.
</BLOCKQUOTE>
<BLOCKQUOTE>
<H5><B>Static Cursors</B><A NAME="I81"></A><A NAME="I82"></A></H5>
</BLOCKQUOTE>
<BLOCKQUOTE>
<P>At one extreme are <I>static</I> cursors, to which the data in the underlying tables appears to be static. The membership, order, and values in the result set used by a static cursor are generally fixed when the cursor is opened. Rows updated, deleted, or inserted by other users (including other cursors in the same application) are not detected by the cursor until it is closed and then reopened; the SQL_STATIC_SENSITIVITY information type returns whether the cursor can detect rows it has updated, deleted, or inserted.<A NAME="I83"></A>
</BLOCKQUOTE>
<BLOCKQUOTE>
<P>Static cursors are commonly implemented by taking a snapshot of the data or locking the result set. Note that in the former case, the cursor diverges from the underlying tables as other users make changes; in the latter case, other users are prohibited from changing the data.
</BLOCKQUOTE>
<BLOCKQUOTE>
<H5><B>Dynamic Cursors</B><A NAME="I84"></A><A NAME="I85"></A></H5>
</BLOCKQUOTE>
<BLOCKQUOTE>
<P>At the other extreme are <I>dynamic</I> cursors, to which the data appears to be dynamic. The membership, order, and values in the result set used by a dynamic cursor are ever-changing. Rows updated, deleted, or inserted by all users (the cursor, other cursors in the same application, and other applications) are detected by the cursor when data is next fetched. Although ideal for many situations, dynamic cursors are difficult to implement.
</BLOCKQUOTE>
<BLOCKQUOTE>
<H5><B>Keyset-Driven Cursors</B><A NAME="I86"></A><A NAME="I87"></A></H5>
</BLOCKQUOTE>
<BLOCKQUOTE>
<P>Between static and dynamic cursors are <I>keyset-driven</I> cursors, which have some of the attributes of each. Like static cursors, the membership and ordering of the result set of a keyset-driven cursor is generally fixed when the cursor is opened. Like dynamic cursors, most changes to the values in the underlying result set are visible to the cursor when data is next fetched.<A NAME="I88"></A>
</BLOCKQUOTE>
<BLOCKQUOTE>
<P>When a keyset-driven cursor is opened, the driver saves the keys for the entire result set, thus fixing the membership and order of the result set. As the cursor scrolls through the result set, the driver uses the keys in this <I>keyset</I> to retrieve the current data values for each row in the rowset. Because data values are retrieved only when the cursor scrolls to a given row, updates to that row by other users (including other cursors in the same application) after the cursor was opened are visible to the cursor.<A NAME="I89"></A><A NAME="I90"></A>
</BLOCKQUOTE>
<BLOCKQUOTE>
<P>If the cursor scrolls to a row of data that has been deleted by other users (including other cursors in the same application), the row appears as a <I>hole</I> in the result set, since the key is still in the keyset but the row is no longer in the result set. Updating the key values in a row is considered to be deleting the existing row and inserting a new row; therefore, rows of data for which the key values have been changed also appear as holes. When the driver encounters a hole in the result set, it returns a status code of SQL_ROW_DELETED for the row.<A NAME="I91"></A>
</BLOCKQUOTE>
<BLOCKQUOTE>
<P>Rows of data inserted into the result set by other users (including other cursors in the same application) after the cursor was opened are not visible to the cursor, since the keys for those rows are not in the keyset.<A NAME="I92"></A>
</BLOCKQUOTE>
<BLOCKQUOTE>
<P>The SQL_STATIC_SENSITIVITY information type returns whether the cursor can detect rows it has deleted or inserted. Because updating key values in a keyset-driven cursor is considered to be deleting the existing row and inserting a new row, keyset-driven cursors can always detect rows they have updated.
</BLOCKQUOTE>
<BLOCKQUOTE>
<H5><B>Mixed (Keyset/Dynamic) Cursors</B><A NAME="I93"></A><A NAME="I94"></A></H5>
</BLOCKQUOTE>
<BLOCKQUOTE>
<P>If a result set is large, it may be impractical for the driver to save the keys for the entire result set. Instead, the application can use a <I>mixed</I> cursor. In a mixed cursor, the keyset is smaller than the result set, but larger than the rowset.<A NAME="I95"></A>
</BLOCKQUOTE>
<BLOCKQUOTE>
<P>Within the boundaries of the keyset, a mixed cursor is keyset-driven, that is, the driver uses keys to retrieve the current data values for each row in the rowset. When a mixed cursor scrolls beyond the boundaries of the keyset, it becomes dynamic, that is, the driver simply retrieves the next <I>rowset size</I> rows of data. The driver then constructs a new keyset, which contains the new rowset.<A NAME="I96"></A>
</BLOCKQUOTE>
<BLOCKQUOTE>
<P>For example, assume a result set has 1000 rows and uses a mixed cursor with a keyset size of 100 and a rowset size of 10. When the cursor is opened, the driver (depending on the implementation) saves keys for the first 100 rows and retrieves data for the first 10 rows. If another user deletes row 11 and the cursor then scrolls to row 11, the cursor will detect a hole in the result set; the key for row 11 is in the keyset but the data is no longer in the result set. This is the same behavior as a keyset-driven cursor. However, if another user deletes row 101 and the cursor then scrolls to row 101, the cursor will not detect a hole; the key for the row 101 is not in the keyset. Instead, the cursor will retrieve the data for the row that was originally row 102. This is the same behavior as a dynamic cursor.
</BLOCKQUOTE>
<BLOCKQUOTE>
<A NAME="E12E18"></A>
<H4>
<FONT>Specifying the Cursor Type<A NAME="I97"></A></FONT></H4>
</BLOCKQUOTE>
<BLOCKQUOTE>
<P>To specify the cursor type, an application calls <B>SQLSetStmtOption</B> with the SQL_CURSOR_TYPE option. The application can specify a cursor that only scrolls forward, a static cursor, a dynamic cursor, a keyset-driven cursor, or a mixed cursor. If the application specifies a mixed cursor, it also specifies the size of the keyset used by the cursor.<A NAME="I98"></A>
</BLOCKQUOTE>
<BLOCKQUOTE>
<HR ALIGN=CENTER>
<NOTE>Note To use the ODBC cursor library, an application calls <B>SQLSetConnectOption</B> with the SQL_ODBC_CURSORS option before it connects to the data source. The cursor library supports block scrollable cursors. It also supports positioned update and delete statements.<A NAME="I99"></A></NOTE>
<HR ALIGN=CENTER>
</BLOCKQUOTE>
<BLOCKQUOTE>
<P>Unless the cursor is a forward-only cursor, an application calls <B>SQLExtendedFetch</B> to scroll the cursor backwards, forwards, or to an absolute or relative position in the result set. The application calls <B>SQLSetPos</B> to refresh the row currently pointed to by the cursor.
</BLOCKQUOTE>
<BLOCKQUOTE>
<A NAME="E12E19"></A>
<H4>
<FONT>Specifying Cursor Concurrency<A NAME="I100"></A><A NAME="I101"></A><A NAME="I102"></A><A NAME="I103"></A><A NAME="I104"></A><A NAME="I105"></A></FONT></H4>
</BLOCKQUOTE>
<BLOCKQUOTE>
<P><I>Concurrency</I> is the ability of more than one user to use the same data at the same time. A transaction is <I>serializable</I> if it is performed in a manner in which it appears as if no other transactions operate on the same data at the same time. For example, assume one transaction doubles data values and another adds 1 to data values. If the transactions are serializable and both attempt to operate on the values 0 and 10 at the same time, the final values will be 1 and 21 or 2 and 22, depending on which transaction is performed first. If the transactions are not serializable, the final values will be 1 and 21, 2 and 22, 1 and 22, or 2 and 21; the sets of values 1 and 22, and 2 and 21, are the result of the transactions acting on each value in a different order.<A NAME="I106"></A><A NAME="I107"></A>
</BLOCKQUOTE>
<BLOCKQUOTE>
<P>Serializability is considered necessary to maintain database integrity. For cursors, it is most easily implemented at the expense of concurrency by locking the result set. A compromise between serializability and concurrency is <I>optimistic concurrency control</I>. In a cursor using optimistic concurrency control, the driver does not lock rows when it retrieves them. When the application requests an update or delete operation, the driver or data source checks if the row has changed. If the row has not changed, the driver or data source prevents other transactions from changing the row until the operation is complete. If the row has changed, the transaction containing the update or delete operation fails.<A NAME="I108"></A>
</BLOCKQUOTE>
<BLOCKQUOTE>
<P>To specify the concurrency used by a cursor, an application calls <B>SQLSetStmtOption</B> with the SQL_CONCURRENCY option. The application can specify that the cursor is read-only, locks the result set, uses optimistic concurrency control and compares row versions to determine if a row has changed, or uses optimistic concurrency control and compares data values to determine if a row has changed. The application calls <B>SQLSetPos</B> to lock the row currently pointed to by the cursor, regardless of the specified cursor concurrency.
</BLOCKQUOTE>
<A NAME="E11E33"></A>
<H3>
<FONT FACE="Arial">Using Bookmarks<A NAME="I109"></A><A NAME="I110"></A></FONT></H3>
<BLOCKQUOTE>
<P>A bookmark is a 32-bit value that an application uses to return to a row. The application does not request that the driver places a bookmark on a row; instead, the application requests a bookmark that it can use to return to a row. For example, if a bookmark is a row number, an application requests the row number of a row and stores it. Later, the application passes this row number back to the driver and requests that the driver return to the row.<A NAME="I111"></A>
</BLOCKQUOTE>
<BLOCKQUOTE>
<P>Before opening the cursor, an application must call <B>SQLSetStmtOption</B> with the SQL_USE_BOOKMARKS option to inform the driver it will use bookmarks. After opening the cursor, the application retrieves bookmarks either from column 0 of the result set or by calling <B>SQLGetStmtOption</B> with the SQL_GET_BOOKMARK option. To retrieve a bookmark from the result set, the application either binds column 0 and calls <B>SQLExtendedFetch</B> or calls <B>SQLGetData</B>; in either case, the <I>fCType</I> argument must be set to SQL_C_BOOKMARK. To return to the row specified by a bookmark, the application calls <B>SQLExtendedFetch</B> with a fetch type of SQL_FETCH_BOOKMARK.<A NAME="I112"></A>
</BLOCKQUOTE>
<BLOCKQUOTE>
<P>If a bookmark requires more than 32 bits, such as when it is a key value, the driver maps the bookmarks requested by the application to 32-bit binary values. The 32-bit binary values are then returned to the application. Because this mapping may require considerable memory, applications should only bind column 0 of the result set if they will actually use bookmarks for most rows. Otherwise, they should call <B>SQLGetStmtOption</B> with the SQL_BOOKMARK statement option or call <B>SQLGetData</B> for column 0.<A NAME="I113"></A>
</BLOCKQUOTE>
<BLOCKQUOTE>
<P>Before an application opens a cursor with which it will use bookmarks, it:<A NAME="I114"></A>
</BLOCKQUOTE>
<UL>
<BLOCKQUOTE>
<LI>Calls <B>SQLSetStmtOption</B> with the SQL_USE_BOOKMARKS option and a value of SQL_UB_ON.<A NAME="I115"></A>
</BLOCKQUOTE></UL>
<BLOCKQUOTE>
<P>To retrieve a bookmark for the current row, an application:<A NAME="I116"></A>
</BLOCKQUOTE>
<UL>
<BLOCKQUOTE>
<LI>Retrieves the value from column 0 of the rowset. The application can either call <B>SQLBindCol</B> to bind column 0 before it calls <B>SQLExtendedFetch</B> or call <B>SQLGetData</B> to retrieve the data after it calls <B>SQLExtendedFetch</B>. In either case, the <I>fCType</I> argument must be SQL_C_BOOKMARK.
</BLOCKQUOTE></UL>
<BLOCKQUOTE>
<HR ALIGN=CENTER>
<NOTE>Note To determine whether it can call <B>SQLGetData</B> for a block (more than one row) of data and whether it can call <B>SQLGetData</B> for a column before the last bound column, an application calls <B>SQLGetInfo</B> with the SQL_GETDATA_EXTENSIONS information type.<A NAME="I117"></A></NOTE>
<HR ALIGN=CENTER>
</BLOCKQUOTE>
<UL>
<BLOCKQUOTE>
<LI> – Or –<A NAME="I118"></A>
</BLOCKQUOTE>
<BLOCKQUOTE>
<LI> Calls <B>SQLSetPos</B> with the SQL_POSITION option to position the cursor on the row and calls <B>SQLGetStmtOption</B> with the SQL_BOOKMARK option to retrieve the bookmark.<A NAME="I119"></A>
</BLOCKQUOTE></UL>
<BLOCKQUOTE>
<P>To return to the row specified by a bookmark (or a row a certain number of rows from the bookmark), an application:<A NAME="I120"></A>
</BLOCKQUOTE>
<UL>
<BLOCKQUOTE>
<LI>Calls <B>SQLExtendedFetch</B> with the <I>irow</I> argument set to the bookmark and the <I>fFetchType</I> argument set to SQL_FETCH_BOOKMARK. The driver returns the rowset starting with the row identified by the bookmark.
</BLOCKQUOTE></UL>
<A NAME="E11E34"></A>
<H3>
<FONT FACE="Arial">Modifying Result Set Data<A NAME="I121"></A><A NAME="I122"></A></FONT></H3>
<BLOCKQUOTE>
<P>ODBC provides two ways to modify data in the result set. Positioned update and delete statements are similar to such statements in embedded SQL. Calls to <B>SQLSetPos</B> allow an application to update, delete, or add new data without executing SQL statements.
</BLOCKQUOTE>
<BLOCKQUOTE>
<A NAME="E12E20"></A>
<H4>
<FONT>Executing Positioned Update and Delete Statements<A NAME="I123"></A><A NAME="I124"></A><A NAME="I125"></A><A NAME="I126"></A><A NAME="I127"></A><A NAME="I128"></A><A NAME="I129"></A><A NAME="I130"></A></FONT></H4>
</BLOCKQUOTE>
<BLOCKQUOTE>
<P>An application can update or delete the row in the result set currently pointed to by the cursor. This is known as a positioned update or delete statement. After executing a <B>SELECT</B> statement to create a result set, an application calls <B>SQLFetch</B> one or more times to position the cursor on the row to be updated or deleted. Alternatively, it fetches the rowset with <B>SQLExtendedFetch</B> and positions the cursor on the desired row by calling <B>SQLSetPos</B> with the SQL_POSITION option. To update or delete the row, the application then executes an SQL statement with the following syntax on a different <I>hstmt</I>:
</BLOCKQUOTE>
<BLOCKQUOTE>
<PRE><B>UPDATE </B><I>table-name</I>
<BR> <B>SET</B> <I>column-identifier</I> <B>= </B>{<I>expression</I> |<B> NULL</B>}
<BR><B> </B>[<B>,</B> <I>column-identifier</I> <B>=</B> {<I>expression</I> |<B> NULL</B>}]...
<BR> <B>WHERE CURRENT OF</B> <I>cursor-name</I>
<B>DELETE FROM </B><I>table-name</I> <B>WHERE CURRENT OF</B> <I>cursor-name</I><A NAME="I131"></A></PRE></BLOCKQUOTE>
<BLOCKQUOTE>
<P>Positioned update and delete statements require cursor names. An application can name a cursor with <B>SQLSetCursorName</B>. If the application has not named the cursor by the time the driver executes a <B>SELECT</B> statement, the driver generates a cursor name. To retrieve the cursor name for an <I>hstmt</I>, an application calls <B>SQLGetCursorName</B>.<A NAME="I132"></A>
</BLOCKQUOTE>
<BLOCKQUOTE>
<P>To execute a positioned update or delete statement, an application must follow these guidelines:<A NAME="I133"></A>
</BLOCKQUOTE>
<UL>
<BLOCKQUOTE>
<LI>The <B>SELECT</B> statement that creates the result set must use a <B>FOR UPDATE</B> clause.<A NAME="I134"></A>
</BLOCKQUOTE>
<BLOCKQUOTE>
<LI>The cursor name used in the <B>UPDATE</B> or <B>DELETE</B> statement must be the same as the cursor name associated with the <B>SELECT</B> statement.<A NAME="I135"></A>
</BLOCKQUOTE>
<BLOCKQUOTE>
<LI>The application must use different <I>hstmts</I> for the <B>SELECT</B> statement and the <B>UPDATE</B> or <B>DELETE</B> statement.<A NAME="I136"></A>
</BLOCKQUOTE>
<BLOCKQUOTE>
<LI>The <I>hstmts</I> for the <B>SELECT</B> statement and the <B>UPDATE</B> or <B>DELETE</B> statement must be on the same connection.<A NAME="I137"></A>
</BLOCKQUOTE></UL>
<BLOCKQUOTE>
<P>To determine if a data source supports positioned update and delete statements, an application calls <B>SQLGetInfo</B> with the SQL_POSITIONED_STATEMENTS option. For an example of code that performs a positioned update in a rowset, see <B>SQLSetPos</B> in Chapter 13, "Function Reference."<A NAME="I138"></A><A NAME="I139"></A><A NAME="I140"></A><A NAME="I141"></A><A NAME="I142"></A>
</BLOCKQUOTE>
<BLOCKQUOTE>
<HR ALIGN=CENTER>
<NOTE>Note In ODBC 1.0, positioned update, positioned delete, and <B>SELECT FOR </B><B>UPDATE</B> statements were part of the core SQL grammar; in ODBC 2.0, they are part of the extended grammar. Applications that use the SQL conformance level to determine whether these statements are supported also need to check the version number of the driver to correctly interpret the information. In particular, applications that use these features with ODBC 1.0 drivers need to explicitly check for these capabilities in ODBC 2.0 drivers.</NOTE>
<HR ALIGN=CENTER>
</BLOCKQUOTE>
<BLOCKQUOTE>
<A NAME="E12E21"></A>
<H4>
<FONT>Modifying Data with SQLSetPos<A NAME="I143"></A><A NAME="I144"></A></FONT></H4>
</BLOCKQUOTE>
<BLOCKQUOTE>
<P>To add, update, and delete rows of data, an application calls <B>SQLSetPos</B> and specifies the operation, the row number, and how to lock the row. Where new rows of data are added to the result set, and whether they are visible to the cursor is data source–defined.<A NAME="I145"></A>
</BLOCKQUOTE>
<BLOCKQUOTE>
<P>The row number determines both the number of the row in the rowset to update or delete and the index of the row in the rowset buffers from which to retrieve data to add or update. If the row number is 0, the operation affects all of the rows in the rowset.<A NAME="I146"></A>
</BLOCKQUOTE>
<BLOCKQUOTE>
<P><B>SQLSetPos</B> retrieves the data to update or add from the rowset buffers. It only updates those columns in a row that have been bound with <B>SQLBindCol</B> and do not have a length of SQL_IGNORE. However, it cannot add a new row of data unless all of the columns in the row are bound, are nullable, or have a default value.<A NAME="I147"></A>
</BLOCKQUOTE>
<BLOCKQUOTE>
<HR ALIGN=CENTER>
<NOTE>Note The rowset buffers are used both to send and retrieve data. To avoid overwriting existing data when it adds a new row of data, an application can allocate an extra row at the end of the rowset buffers to use as an add buffer.<A NAME="I148"></A><A NAME="I149"></A></NOTE>
<HR ALIGN=CENTER>
</BLOCKQUOTE>
<BLOCKQUOTE>
<P>To add a new row of data to the result set, an application:<A NAME="I150"></A>
</BLOCKQUOTE>
<UL>
<BLOCKQUOTE>
<LI>1. Places the data for each column the <I>rgbValue</I> buffers specified with <B>SQLBindCol</B>. To avoid overwriting an existing row of data, the application should allocate an extra row of the rowset buffers to use as an add buffer.<A NAME="I151"></A>
</BLOCKQUOTE>
<BLOCKQUOTE>
<LI>2. Places the length of each column in the <I>pcbValue</I> buffer specified with <B>SQLBindCol</B>; this only needs to be done for columns with an <I>fCType</I> of SQL_C_CHAR or SQL_C_BINARY. To use the default value for a column, the application specifies a length of SQL_IGNORE.
</BLOCKQUOTE></UL>
<BLOCKQUOTE>
<HR ALIGN=CENTER>
<NOTE>Note To add a new row of data to a result set, one of the following two conditions must be met:</NOTE>
<HR ALIGN=CENTER>
</BLOCKQUOTE>
<BLOCKQUOTE>
<HR ALIGN=CENTER>
<NOTE>All columns in the underlying tables must be bound
<BR>with <B>SQLBindCol</B>.</NOTE>
<HR ALIGN=CENTER>
</BLOCKQUOTE>
<BLOCKQUOTE>
<HR ALIGN=CENTER>
<NOTE>All unbound columns and all bound columns for
<BR>which the specified length is SQL_IGNORE must
<BR>accept NULL values or have default values.<A NAME="I152"></A></NOTE>
<HR ALIGN=CENTER>
</BLOCKQUOTE>
<UL>
<BLOCKQUOTE>
<LI> To determine if a row in a result set accepts NULL values, an application calls <B>SQLColAttributes</B>. To determine if a data source supports non-nullable columns, an application calls <B>SQLGetInfo</B> with the SQL_NON_NULLABLE flag.<A NAME="I153"></A>
</BLOCKQUOTE>
<BLOCKQUOTE>
<LI>3. Calls <B>SQLSetPos</B> with the <I>fOption</I> argument set to SQL_ADD. The <I>irow</I> argument determines the row in the rowset buffers from which the data is retrieved. For information about how an application sends data for data-at-execution columns, see <B>SQLSetPos</B> in Chapter 13, "Function Reference."<A NAME="I154"></A>
</BLOCKQUOTE>
<BLOCKQUOTE>
<LI> After the row is added, the row the cursor points to is unchanged.
</BLOCKQUOTE></UL>
<BLOCKQUOTE>
<HR ALIGN=CENTER>
<NOTE>Note Columns for long data types, such as SQL_LONGVARCHAR and SQL_LONGVARBINARY, are generally not bound. However, if an application uses <B>SQLSetPos</B> to send data for these columns, it must bind them with <B>SQLBindCol</B>. Unless the driver returns the SQL_GD_BOUND bit for the SQL_GETDATA_EXTENSIONS information type, the application must unbind them before calling <B>SQLGetData</B> to retrieve data from them.<A NAME="I155"></A></NOTE>
<HR ALIGN=CENTER>
</BLOCKQUOTE>
<BLOCKQUOTE>
<P>To update a row of data, an application:<A NAME="I156"></A>
</BLOCKQUOTE>
<UL>
<BLOCKQUOTE>
<LI>1. Modifies the data of each column to be updated in the <I>rgbValue</I> buffer specified with <B>SQLBindCol</B>.<A NAME="I157"></A>
</BLOCKQUOTE>
<BLOCKQUOTE>
<LI>2. Places the length of each column to be updated in the <I>pcbValue</I> buffer specified with <B>SQLBindCol</B>. This only needs to be done for columns with an <I>fCType</I> of SQL_C_CHAR or SQL_C_BINARY.<A NAME="I158"></A>
</BLOCKQUOTE>
<BLOCKQUOTE>
<LI>3. Sets the value of the <I>pcbValue</I> buffer for each bound column that is not to be updated to SQL_IGNORE.<A NAME="I159"></A>
</BLOCKQUOTE>
<BLOCKQUOTE>
<LI>4. Calls <B>SQLSetPos</B> with the <I>fOption</I> argument set to SQL_UPDATE. The <I>irow</I> argument specifies the number of the row in the rowset to modify and the index of row in the rowset buffer from which to retrieve the data. The cursor points to this row after it is updated.<A NAME="I160"></A>
</BLOCKQUOTE></UL>
<BLOCKQUOTE>
<P>For information about how an application sends data for data-at-execution columns, see <B>SQLSetPos</B> in Chapter 13, "Function Reference."<A NAME="I161"></A>
</BLOCKQUOTE>
<BLOCKQUOTE>
<P>To delete a row of data, an application:<A NAME="I162"></A>
</BLOCKQUOTE>
<UL>
<BLOCKQUOTE>
<LI>Calls <B>SQLSetPos</B> with the <I>fOption</I> argument set to SQL_DELETE. The <I>irow</I> argument specifies the number of the row in the rowset to delete. The cursor points to this row after it is deleted.
</BLOCKQUOTE></UL>
<BLOCKQUOTE>
<HR ALIGN=CENTER>
<NOTE>Note The application cannot perform any positioned operations, such as executing a positioned update or delete statement or calling <B>SQLGetData</B>, on a deleted row.<A NAME="I163"></A></NOTE>
<HR ALIGN=CENTER>
</BLOCKQUOTE>
<BLOCKQUOTE>
<P>To determine what operations a data source supports for <B>SQLSetPos</B>, an application calls <B>SQLGetInfo</B> with the SQL_POS_OPERATIONS flag.
</BLOCKQUOTE>
<A NAME="E11E35"></A>
<H3>
<FONT FACE="Arial">Processing Multiple Results<A NAME="I164"></A><A NAME="I165"></A></FONT></H3>
<BLOCKQUOTE>
<P><B>SELECT</B> statements return result sets. <B>UPDATE</B>, <B>INSERT</B>, and <B>DELETE</B> statements return a count of affected rows. If any of these statements are batched, submitted with arrays of parameters, or in procedures, they can return multiple result sets or counts.<A NAME="I166"></A><A NAME="I167"></A>
</BLOCKQUOTE>
<BLOCKQUOTE>
<P>To process a batch of statements, statement with arrays of parameters, or procedure returning multiple result sets or row counts, an application:<A NAME="I168"></A>
</BLOCKQUOTE>
<UL>
<BLOCKQUOTE>
<LI>1. Calls <B>SQLExecute</B> or <B>SQLExecDirect</B> to execute the statement or procedure.<A NAME="I169"></A>
</BLOCKQUOTE>
<BLOCKQUOTE>
<LI>2. Calls <B>SQLRowCount</B> to determine the number of rows affected by an <B>UPDATE</B>, <B>INSERT</B>, or <B>DELETE</B> statement. For statements or procedures that return result sets, the application calls functions to determine the characteristics of the result set and retrieve data from the result set.<A NAME="I170"></A>
</BLOCKQUOTE>
<BLOCKQUOTE>
<LI>3. Calls <B>SQLMoreResults</B> to determine if another result set or row count is available.<A NAME="I171"></A>
</BLOCKQUOTE>
<BLOCKQUOTE>
<LI>4. Repeats steps 2 and 3 until <B>SQLMoreResults</B> returns SQL_NO_DATA_FOUND.
</BLOCKQUOTE></UL><P ALIGN=CENTER>
<A HREF="prguide6.htm" TARGET="_self"><IMG SRC="graprev.gif" WIDTH = 32 HEIGHT = 32 BORDER = 0 ALT="Previous Page"></A>
<A HREF="httoc.htm" TARGET="_self"><IMG SRC="gratoc.gif" WIDTH = 32 HEIGHT = 32 BORDER = 0 ALT="TOC"></A>
<A HREF="htindex.htm" TARGET="_self"><IMG SRC="graindex.gif" WIDTH = 32 HEIGHT = 32 BORDER = 0 ALT="Index"></A>
<A HREF="prguide8.htm" TARGET="_self"><IMG SRC="granext.gif" WIDTH = 32 HEIGHT = 32 BORDER = 0 ALT="Next Page"></A>
<center><p><font SIZE=-2>Copyright © 1992-1997 Solid Information Technology Ltd All rights reserved.</font></p></center>
</BODY></HTML>
|