Oracle7 Server Application Developer's Guide | ![]() Library |
![]() Product |
![]() Contents |
![]() Index |
Although some Oracle tools and applications simplify or mask the use of SQL, all database operations are performed using SQL. Any other data access method would circumvent the security built into Oracle and potentially compromise data security and integrity.
For many Oracle tools, several of the phases are performed automatically. Most users need not be concerned with or aware of this level of detail. However, you might find this information useful when writing Oracle applications. Refer to the Oracle7 Server Tuning manual for a description of each phase of SQL statement processing for each type of SQL statement.
When FIPS flagging is active, your SQL statements are checked to see whether they include extensions that go beyond the ANSI/ISO SQL92 standard. If any non-standard constructs are found, the Oracle Server flags them as errors and displays the violating syntax.
The FIPS flagging feature supports flagging through interactive SQL statements submitted using Server Manager or SQL*Plus. The Oracle Precompilers and SQL*Module also support FIPS flagging of embedded and module language SQL.
When flagging is on and non-standard SQL is encountered, the message returned is
ORA-00097: Use of Oracle SQL feature not in SQL92 level Level
where level can be either ENTRY, INTERMEDIATE, or FULL.
Figure 3 - 1. The Stages in Processing a SQL Statement
A transfer of funds between two accounts (the transaction or logical unit of work), for example, should include the debit to one account (one SQL statement) and the credit to another account (one SQL statement). Both actions should either fail or succeed together as a unit of work; the credit should not be committed without the debit. Other non-related actions, such as a new deposit to one account, should not be included in the transfer of funds transaction.
COMMIT WORK;
COMMIT;
The COMMIT command allows you to include the COMMENT parameter along with a comment (less than 50 characters) that provides information about the transaction being committed. This option is useful for including information about the origin of the transaction when you commit distributed transactions:
COMMIT COMMENT 'Dallas/Accts_pay/Trans_type 10B';
For additional information about committing in-doubt distributed transactions, see Oracle7 Server Distributed Systems, Volume I.
ROLLBACK WORK;
ROLLBACK;
The WORK option of the ROLLBACK command has no function.
To roll back to a savepoint defined in the current transaction, the TO option of the ROLLBACK command must be used. For example, either of the following statements rolls back the current transaction to the savepoint named POINT1:
ROLLBACK TO SAVEPOINT point1;
ROLLBACK TO point1;
For additional information about rolling back in-doubt distributed transactions see Oracle7 Server Distributed Systems, Volume I.
SAVEPOINT add_emp1;
If you create a second savepoint with the same identifier as an earlier savepoint, the earlier savepoint is erased. After a savepoint has been created, you can roll back to the savepoint.
There is no limit on the number of active savepoints per session. An active savepoint is one that has been specified since the last commit or rollback.
SQL Statement | Results |
SAVEPOINT A; | First savepoint of this transaction. |
DELETE . . . ; | First DML statement of this transaction. |
SAVEPOINT b; | Second savepoint of this transaction. |
INSERT INTO . . . ; | Second DML statement of this transaction. |
SAVEPOINT c; | Third savepoint of this transaction. |
UPDATE . . . ; | Third DML statement of this transaction. |
ROLLBACK TO c; | UPDATE statement is rolled back, savepoint C remains defined. |
ROLLBACK TO b; | INSERT statement is rolled back, savepoint C is lost, savepoint B remains defined. |
ROLLBACK TO c; | ORA-01086 error; savepoint C no longer defined. |
INSERT INTO . . . ; | New DML statement in this transaction. |
COMMIT; | Commits all actions performed by the first DML statement (the DELETE statement) and the last DML statement (the second INSERT statement). All other statements (the second and the third statements) of the transaction had been rolled back before the COMMIT. The savepoint A is no longer active. |
A read-only transaction does not acquire any additional data locks to provide transaction-level read consistency. The multi-version consistency model used for statement-level read consistency is used to provide transaction-level read consistency; all queries return information with respect to the system control number (SCN) determined when the read-only transaction begins. Because no data locks are acquired, other transactions can query and update data being queried concurrently by a read-only transaction.
Changed data blocks queried by a read-only transaction are reconstructed using data from rollback segments. Therefore, long running read-only transactions sometimes receive a "snapshot too old" error (ORA-01555). Create more, or larger, rollback segments to avoid this. Alternatively, you could issue long-running queries when online transaction processing is at a minimum, or you could obtain a shared lock on the table you were querying, prohibiting any other modifications during the transaction.
A read-only transaction is started with a SET TRANSACTION statement that includes the READ ONLY option. For example:
SET TRANSACTION READ ONLY;
The SET TRANSACTION statement must be the first statement of a new transaction; if any DML statements (including queries) or other non-DDL statements (such as SET ROLE) precede a SET TRANSACTION READ ONLY statement, an error is returned. Once a SET TRANSACTION READ ONLY statement successfully executes, only SELECT (without a FOR UPDATE clause), COMMIT, ROLLBACK, or non-DML statements (such as SET ROLE, ALTER SYSTEM, LOCK TABLE) are allowed in the transaction. Otherwise, an error is returned. A COMMIT, ROLLBACK, or DDL statement terminates the read-only transaction (a DDL statement causes an implicit commit of the read-only transaction and commits in its own transaction).
A cursor is a handle to a specific private SQL area. In other words, a cursor can be thought of as a name for a specific private SQL area. A PL/SQL cursor variable enables the retrieval of multiple rows from a stored procedure. Cursor variables allow you to pass cursors as parameters in your 3GL application. Cursor variables are described in the PL/SQL User's Guide and Reference.
Although most Oracle users rely on the automatic cursor handling of the Oracle utilities, the programmatic interfaces offer application designers more control over cursors. In application development, a cursor is a named resource available to a program, which can be specifically used for parsing SQL statements embedded within the application.
By opening several cursors, the parsed representation of several SQL statements can be saved. Repeated execution of the same SQL statements can thus begin at the describe, define, bind, or execute step, saving the repeated cost of opening cursors and parsing.
transaction level
system level
An instance can be started with non-default locking by adjusting the initialization parameters SERIALIZABLE and ROW LOCKING.
The following sections describe each option available for overriding the default locking of Oracle. The initialization parameter DML_LOCKS determines the maximum number of DML locks allowed (see the Oracle7 Server Reference manual for a discussion of parameters). The default value should be sufficient; however, if you are using additional manual locks, you may need to increase this value.
Warning: If you override the default locking of Oracle at any level, be sure that the overriding locking procedures operate correctly; that is, be sure that data integrity is guaranteed, data concurrency is acceptable, and deadlocks are not possible or are appropriately handled.
LOCK TABLE emp, dept IN EXCLUSIVE MODE NOWAIT;
You can specify several tables or views to lock in the same mode; however, only a single lock mode can be specified per LOCK TABLE statement.
Note: When a table is locked, all rows of the table are locked. No other user can modify the table.
You can also indicate if you do or do not want to wait to acquire the lock. If you specify the NOWAIT option, you only acquire the table lock if it is immediately available. Otherwise an error is returned to notify that the lock is not available at this time. In this case, you can attempt to lock the resource at a later time. If NOWAIT is omitted, the transaction does not proceed until the requested table lock is acquired. If the wait for a table lock is excessive, you might want to cancel the lock operation and retry at a later time; you can code this logic into your applications.
Note: A distributed transaction waiting for a table lock can timeout waiting for the requested lock if the elapsed amount of time reaches the interval set by the initialization parameter DISTRIBUTED_LOCK_TIMEOUT. Because no data has been modified, no actions are necessary as a result of the time-out. Your application should proceed as if a deadlock has been encountered. For more information on distributed transactions, refer to Oracle7 Server Distributed Systems, Volume I.
The following paragraphs provide guidance on when it can be advantageous to acquire each type of table lock using the LOCK TABLE command.
ROW SHARE and ROW EXCLUSIVE
LOCK TABLE table IN ROW SHARE MODE;
LOCK TABLE table IN ROW EXCLUSIVE MODE;
Row share and row exclusive table locks offer the highest degree of concurrency. Conditions that possibly warrant the explicit acquisition of a row share or row exclusive table lock include the following:
LOCK TABLE table IN SHARE MODE;
Share table locks are rather restrictive data locks. The following conditions could warrant the explicit acquisition of a share table lock:
For example, assume that two tables, EMP and BUDGET, require a consistent set of data in a third table, DEPT. That is, for a given department number, you want to update the information in both of these tables, and ensure that no new members are added to the department between these two transactions.
Although this scenario is quite rare, it can be accommodated by locking the DEPT table in SHARE MODE, as shown in the following example. Because the DEPT table is not highly volatile, few, if any, users would need to update it while it was locked for the updates to EMP and BUDGET.
LOCK TABLE dept IN SHARE MODE UPDATE EMP SET sal = sal * 1.1 WHERE deptno IN (SELECT deptno FROM dept WHERE loc = 'DALLAS') UPDATE budget SET totsal = totsal * 1.1 WHERE deptno IN (SELECT deptno FROM dept WHERE loc = 'DALLAS') COMMIT /* This releases the lock */
SHARE ROW EXCLUSIVE
LOCK TABLE table IN SHARE ROW EXCLUSIVE MODE;
Conditions that warrant the explicit acquisition of a share row exclusive table lock include the following:
LOCK TABLE table IN EXCLUSIVE MODE;
Conditions that warrant the explicit acquisition of an exclusive table lock include the following:
You can use a SELECT ... FOR UPDATE statement to lock a row without actually changing it. For example, several triggers in Chapter 9 show how to implement referential integrity. In the EMP_DEPT_CHECK trigger (see page 9 - 27), the row that contains the referenced parent key value is locked to guarantee that it remains for the duration of the transaction; if the parent key is updated or deleted, referential integrity would be violated.
SELECT ... FOR UPDATE statements are often used by interactive programs that allow a user to modify fields of one or more specific rows (which might take some time); row locks on the rows are acquired so that only a single interactive program user is updating the rows at any given time.
If a SELECT ... FOR UPDATE statement is used when defining a cursor, the rows in the return set are locked before the first fetch, when the cursor is opened; rows are not individually locked as they are fetched from the cursor. Locks are only released when the transaction that opened the cursor is committed or rolled back; locks are not released when a cursor is closed.
Each row in the return set of a SELECT ... FOR UPDATE statement is locked individually; the SELECT ... FOR UPDATE statement waits until the other transaction releases the conflicting row lock. Therefore, if a SELECT ... FOR UPDATE statement locks many rows in a table and the table experiences reasonable update activity, it would most likely improve performance if you instead acquired an exclusive table lock.
When acquiring row locks with SELECT ... FOR UPDATE, you can indicate if you do or do not want to wait to acquire the lock. If you specify the NOWAIT option, you only acquire the row lock if it is immediately possible. Otherwise, an error is returned to notify you that the lock is not possible at this time. In this case, you can attempt to lock the row later. If NOWAIT is omitted, the transaction does not proceed until the requested row lock is acquired. If the wait for a row lock is excessive, users might want to cancel the lock operation and retry later; you can code such logic into your applications.
As described , a distributed transaction waiting for a row lock can timeout waiting for the requested lock if the elapsed amount of time reaches the interval set by the initialization parameter DISTRIBUTED_LOCK_TIMEOUT.
The settings for these parameters should be changed only when an instance is shut down. If multiple instances are accessing a single database, all instances should use the same setting for these parameters.
Function/Procedure | Description | Refer to Page |
ALLOCATE_UNIQUE | Allocate a unique lock ID to a named lock. | 3 - 20 |
REQUEST | Request a lock of a specific mode. | 3 - 21 |
CONVERT | Convert a lock from one mode to another. | 3 - 23 |
RELEASE | Release a lock. | 3 - 24 |
SLEEP | Put a procedure to sleep for a specified time. | 3 - 25 |
User locks are automatically released when a session terminates.
Warning: This implementation does not efficiently support more than a few hundred locks per session. Oracle strongly recommends that you develop a standard convention be developed for using these user locks. This avoids conflicts among procedures trying to use the same locks. For example, you might want to include your company name as part of the lock name to ensure that your lock names do not conflict with lock names used in any Oracle supplied applications.
Warning: Named user locks may be less efficient, as Oracle uses SQL to determine the lock associated with a given name.
The parameters for the ALLOCATE_UNIQUE procedure are described in Table 3 - 4. The syntax for this procedure is shown below.
DBMS_LOCK.ALLOCATE_UNIQUE(lockname IN VARCHAR2, lockhandle OUT VARCHAR2, expiration_secs IN INTEGER DEFAULT 864000);
The parameters for the REQUEST function are described in Table 3 - 5 and the possible return values and their meanings are described in Table 3 - 6. The syntax for this function is shown below.
DBMS_LOCK.REQUEST(id IN INTEGER || lockhandle IN VARCHAR2, lockmode IN INTEGER DEFAULT X_MODE, timeout IN INTEGER DEFAULT MAXWAIT, release_on_commit IN BOOLEAN DEFAULT FALSE, RETURN INTEGER;
The default values, such as X_MODE and MAXWAIT, are defined in the DBMS_LOCK package specification. See the package specification, available on-line, for the current default values.
Parameter | Description |
ID or LOCKHANDLE | Specify the user assigned lock identifier, from 0 to 1073741823, or the lock handle, returned by ALLOCATE_UNIQUE, of the lock whose mode you want to change. |
LOCKMODE | Specify the mode that you are requesting for the lock. The available modes and their associated integer identifiers are listed below. The abbreviations for these locks, as they appear in the V$ views and Server Manager monitors are shown in parentheses. 1 - null mode 2 - row share mode (ULRS) 3 - row exclusive mode (ULRX) 4 - share mode (ULS) 5 - share row exclusive mode (ULRSX) 6 - exclusive mode (ULX) Each of these lock modes is explained in the Oracle7 Server Concepts manual. |
TIMEOUT | Specify the number of seconds to continue trying to grant the lock. If the lock cannot be granted within this time period, the call returns a value of 1 (timeout). |
RELEASE_ON_COMMIT | Set this parameter to TRUE to release the lock on commit or rollback. Otherwise, the lock is held until it is explicitly released or until the end of the session. |
Return Value | Description |
0 | success |
1 | timeout |
2 | deadlock |
3 | parameter error |
4 | already own lock specified by ID or LOCKHANDLE |
5 | illegal lock handle |
The parameters for the CONVERT function are described in Table 3 - 7 and the possible return values and their meanings are described in Table 3 - 8. The syntax for this function is shown below.
DBMS_LOCK.CONVERT( id IN INTEGER || lockhandle IN VARCHAR2, lockmode IN INTEGER, timeout IN NUMBER DEFAULT MAXWAIT) RETURN INTEGER;
Parameter | Description |
ID or LOCKHANDLE | Specify the user assigned lock identifier, from 0 to 1073741823, or the lock handle, returned by ALLOCATE_UNIQUE, of the lock whose mode you want to change. |
LOCKMODE | Specify the new mode that you want to assign to the given lock. The available modes and their associated integer identifiers are listed below. The abbreviations for these locks, as they appear in the V$ views and Server Manager monitors are shown in parentheses. 1 - null mode 2 - row share mode (ULRS) 3 - row exclusive mode (ULRX) 4 - share mode (ULS) 5 - share row exclusive mode (ULRSX) 6 - exclusive mode (ULX) Each of these lock modes is explained in the Oracle7 Server Concepts manual. |
TIMEOUT | Specify the number of seconds to continue trying to change the lock mode. If the lock cannot be converted within this time period, the call returns a value of 1 (timeout). |
Return Value | Description |
0 | success |
1 | timeout |
2 | deadlock |
3 | parameter error |
4 | don't own lock specified by ID or LOCKHANDLE |
5 | illegal lock handle |
The parameters for the RELEASE function are described in Table 3 - 9 and the possible return values and their meanings are described in Table 3 - 10. The syntax for this function is shown below.
DBMS_LOCK.RELEASE(id IN INTEGER) RETURN INTEGER;
DBMS_LOCK.RELEASE(lockhandle IN VARCHAR2) RETURN INTEGER;
Parameter | Description |
ID or LOCKHANDLE | Specify the user-assigned lock identifier, from 0 to 1073741823, or the lock handle, returned by ALLOCATE_UNIQUE, of the lock that you want to release. |
Return Value | Description |
0 | success |
3 | parameter error |
4 | do not own lock specified by ID or LOCKHANDLE |
5 | illegal lock handle |
The parameters for the SLEEP procedure are described in Table 3 - 11. The syntax for the SLEEP procedure is shown below.
DBMS_LOCK.SLEEP(seconds IN NUMBER);
Parameter | Description |
SECONDS | Specify the amount of time, in seconds, to suspend the session. The smallest increment can be entered in hundredths of a second; for example, 1.95 is a legal time value. |
***************************************************************** * Print Check * * Any cashier may issue a refund to a customer returning goods. * * Refunds under $50 are given in cash, above that by check. * * This code prints the check. The one printer is opened by all * * the cashiers to avoid the overhead of opening and closing it * * for every check. This means that lines of output from multiple* * cashiers could become interleaved if we don't ensure exclusive* * access to the printer. The DBMS_LOCK package is used to * * ensure exclusive access. * ***************************************************************** CHECK-PRINT * * Get the lock "handle" for the printer lock. MOVE "CHECKPRINT" TO LOCKNAME-ARR. MOVE 10 TO LOCKNAME-LEN. EXEC SQL EXECUTE BEGIN DBMS_LOCK.ALLOCATE_UNIQUE ( :LOCKNAME, :LOCKHANDLE ); END; END-EXEC. * * Lock the printer in exclusive mode (default mode). EXEC SQL EXECUTE BEGIN DBMS_LOCK.REQUEST ( :LOCKHANDLE ); END; END-EXEC. * We now have exclusive use of the printer, print the check. ... * * Unlock the printer so other people can use it * EXEC SQL EXECUTE BEGIN DBMS_LOCK.RELEASE ( :LOCKHANDLE ); END; END-EXEC.
Server Manager Monitors (Lock and Latch Monitors)
The Monitor feature of Server Manager provides two monitors for displaying lock information of an instance. Refer to the Oracle7 Server Manager User's Guide for complete information about the Server Manager monitors.
UTLLOCKT.SQL
The UTLLOCKT.SQL script displays a simple character lock wait-for graph in tree structured fashion. Using any ad hoc SQL tool (such as SQL*Plus) to execute the script, it prints the sessions in the system that are waiting for locks and the corresponding blocking locks. The location of this script file is operating system dependent. (You must have run the CATBLOCK.SQL script before using UTLLOCKT.SQL.)
If a transaction (A) attempts to update or delete a row that has been locked by another transaction B (by way of a DML or SELECT FOR UPDATE statement), then A's DML command blocks until B commits or rolls back. Once B commits, transaction A can see changes that B has made to the database.
For most applications, this concurrency model is the appropriate one. In some cases, however, it is advantageous to allow transactions to be serializable. Serializable transactions must execute in such a way that they appear to be executing one at a time (serially), rather than concurrently. In other words, concurrent transactions executing in serialized mode are only permitted to make database changes that they could have made if the transactions were scheduled to run one after the other.
The ANSI/ISO SQL standard SQL92 defines three possible kinds of transaction interaction, and four levels of isolation that provide increasing protection against these interactions. These interactions and isolation levels are summarized in Table 3 - 12.
READ UNCOMMITTED
Oracle never permits ``dirty reads.'' This is not required for high throughput with Oracle.
READ COMMITTED
Oracle meets the READ COMMITTED isolation standard. This is the default mode for all Oracle applications. Note that since an Oracle query only sees data that was committed at the beginning of the query (the snapshot time), Oracle offers more consistency than actually required by the ANSI/ISO SQL92 standards for READ COMMITTED isolation.
REPEATABLE READ
Oracle does not support this isolation level, except as provided by SERIALIZABLE.
SERIALIZABLE
You can set this isolation level using the SET TRANSACTION command or the ALTER SESSION command, as described .
Figure 3 - 2. Time Line for Two Transactions
When a serializable transaction fails with an ORA-08177 error (``cannot serialize access''), the application can take any of several actions:
ORA-01453: SET TRANSACTION must be first statement of transaction
Use the ALTER SESSION command to set the transaction isolation level on a session-wide basis. See the Oracle7 Server SQL Reference for the complete syntax of the SET TRANSACTION and ALTER SESSION commands.
Figure 3 - 3 two different transactions that perform application-level checks to maintain the referential integrity parent/child relationship between two tables. One transaction reads the parent table to determine that a row with a specific primary key value exists before inserting corresponding child rows. The other transaction checks to see that no corresponding detail rows exist before proceeding to delete a parent row. In this case, both transactions assume (but do not ensure) that data they read will not change before the transaction completes.
Figure 3 - 3. Referential Integrity Checks
Note that the read issued by transaction A does not prevent transaction B from deleting the parent row. Likewise, transaction B's query for child rows does not prevent the insertion of child rows by transaction A. Therefore the above scenario leaves in the database a child row with no corresponding parent row. This result would occur even if both A and B are SERIALIZABLE transactions, because neither transaction prevents the other from making changes in the data it reads to check consistency.
As this example illustrates, for some transactions, application developers must specifically ensure that the data read by one transaction is not concurrently written by another. This requires a greater degree of transaction isolation than defined by SQL92 SERIALIZABLE mode.
Referential integrity can also be enforced in Oracle7 using database triggers, instead of a separate query as in transaction A above. For example, an INSERT into the child table can fire a PRE-INSERT row-level trigger to check for the corresponding parent row. The trigger queries the parent table using SELECT FOR UPDATE, ensuring that parent row (if it exists) will remain in the database for the duration of the transaction inserting the child row. If the corresponding parent row does not exist, the trigger rejects the insert of the child row.
SQL statements issued by a database trigger execute in the context of the SQL statement that caused the trigger to fire. All SQL statements executed within a trigger see the database in the same state as the triggering statement. Thus, in a READ COMMITTED transaction, the SQL statements in a trigger see the database as of the beginning of the triggering statement's execution, and in a transaction executing in SERIALIZABLE mode, the SQL statements see the database as of the beginning of the transaction. In either case, the use of SELECT FOR UPDATE by the trigger will correctly enforce referential integrity as explained above.
Oracle7 provides transactions executing in READ COMMITTED mode with transaction set consistency on a per-statement basis (since all rows read by a query must have been committed before the query began). Similarly, Oracle7 SERIALIZABLE mode provides transaction set consistency on a per-transaction basis, since all statements in a SERIALIZABLE transaction execute with respect to an image of the database as of the beginning of the transaction.
In other database systems (unlike in Oracle7), a single query run in READ COMMITTED mode provides results that are not transaction set consistent. The query is not transaction set consistent because it may see only a subset of the changes made by another transaction. This means, for example, that a join of a master table with a detail table could see a master record inserted by another transaction, but not the corresponding details inserted by that transaction, or vice versa. Oracle7's READ COMMITTED mode will not experience this effect, and so provides a greater degree of consistency than read-locking systems.
In read-locking systems, at the cost of preventing concurrent updates, SQL92 REPEATABLE READ isolation provides transaction set consistency at the statement level, but not at the transaction level. The absence of phantom protection means two queries issued by the same transaction can see data committed by different sets of other transactions. Only the throughput-limiting and deadlock-susceptible SERIALIZABLE mode in these systems provides transaction set consistency at the transaction level.
For environments with many concurrent users rapidly submitting transactions, designers must assess transaction performance requirements in terms of the expected transaction arrival rate and response time demands, and choose an isolation level that provides the required degree of consistency while satisfying performance expectations. Frequently, for high performance environments, the choice of isolation levels involves making a tradeoff between consistency and concurrency (transaction throughput).
Both Oracle7 isolation modes provide high levels of consistency and concurrency (and performance) through the combination of row-level locking and Oracle's multi-version concurrency control system. Because readers and writers don't block one another in Oracle7, while queries still see consistent data, both READ COMMITTED and SERIALIZABLE isolation provide a high level of concurrency for high performance, without the need for reading uncommitted ("dirty") data.
READ COMMITTED isolation can provide considerably more concurrency with a somewhat increased risk of inconsistent results (due to phantoms and non-repeatable reads) for some transactions. The SERIALIZABLE isolation level provides somewhat more consistency by protecting against phantoms and non-repeatable reads, and may be important where a read/write transaction executes a query more than once. However, SERIALIZABLE mode requires applications to check for the "can't serialize access" error, and can significantly reduce throughput in an environment with many concurrent transactions accessing the same data for update. Application logic that checks database consistency must take into account the fact reads don't block writes in either mode.
ORA-08177: Can't serialize access for this transaction.
When you get an ORA-08177 error, the appropriate action is to roll back the current transaction, and re-execute it. After a rollback, the transaction acquires a new transaction snapshot, and the DML operation is likely to succeed.
Since a rollback and repeat of the transaction is required, it is good development practice to put DML statements that might conflict with other concurrent transactions towards the beginning of your transaction, whenever possible.
![]() ![]() Prev Next |
![]() Copyright © 1996 Oracle Corporation. All Rights Reserved. |
![]() Library |
![]() Product |
![]() Contents |
![]() Index |