Chapter 15. Database Interaction

Table of Contents

1. Database Connection
2. Direct Execution of an SQL Query
3. Processing the Result of an SQL Query
3.1. Modifying Cursor Position
3.2. Cursor Deletion
4. Preparation and Execution of SQL Queries
4.1. Dynamic Construction of SQL Strings
4.2. Using Parametric SQL Queries
5. Transactions
6. The Search Class
6.1. Search Class Properties
6.2. Construction of SQL Queries Using the ToSQL Method

Biferno can interact with the most common commercial and open source relational DBMS. Two implementation of the database interface are available, using either native drivers (when available) or ODBC (Open DataBase Connectivity). ODBC is a standard database access method that can be used by a client application to request data from a DBMS. DBMS interaction uses SQL (Structured Query Language). SQL is a standard query language that allows to analyze, read, write, delete and modify data stored in a relational database.

This chapter illustrates the functionality provided in Biferno to communicate with a database. We assume a good understanding of SQL in the examples that will be presented in the following, since a detailed discussion of SQL as a query language is well beyond the scope of this manual.

How does ODBC work?

ODBC is an API (Application Programming Interface) that client applications can use to request data from a server. Client applications using ODBC send their queries to an ODBC driver manager that runs on the same client machine. The ODBC driver manager recognizes the data source specified in the query and knows which ODBC driver is associated to that data source. The query, coded in the SQL query language, is sent via the appropriate driver to the database on the server, and the response is returned to the client application via the ODBC driver manager.

What is a DSN?

A DSN (Data Source Name) can be seen as a shortcut that allows a client application to connect directly to a database using ODBC. A DSN identifies a database with a unique name, while additional information is associated to the corresponding ODBC data source (among others, database server address and security options, such as username and password). We refer the interested reader to the in-depth discussion of ODBC in Roger E. Sanders, "ODBC 3.5 - Developer's Guide", McGraw-Hill.

1. Database Connection

Before discussing how to interface to a database with a Biferno script it is necessary to briefly describe the elements that define an SQL database and to clarify the terminology we will use in the following.

Until now we have used the word database indifferently to refer to the DBMS as well as to the true database. A database is actually a data structure managed by a DBMS. Many individual entities, which are separate but correlated, can exist within a database: tables, indices, sequences, etc. The actual data is stored in tables, which are organized in columns (fields) and rows (records).

For exemplification purposes, assume to have access to a DBMS where, in a database called mydb, we have created a users table that contains information on the users of an on- line service. The information is structured along the same lines as described in the description of the user class in Chapter 11, User classes. To connect to the database we will use an ODBC data source with a DSN called MYDB. We assume that the corresponding data source has been previously defined via the ODBC driver manager.

Table 15.1. Structure of the users table

user_idfirst_namelast_namesexageusernamepassword
1JamesKirkM38jtkirkcaptain
2LeonardMcCoyM42lmccoybones
3MontgomeryScottM51scottybeammeup
4NyotaUhuraF34nuhuracommoff
5PavelChekovM28pcheckovnavigator

Table 15.1, “Structure of the users table” shows the structure of the users table. The table is organized in seven columns and contains five rows (records). The user_id column contains a unique numerical index for each record, which is used as a primary key during search and comparison operations. The values of this column are automatically calculated and inserted by the DBMS when a new record is created.

Finally we are ready to show the procedure used in Biferno to connect to a data source via ODBC (connection via native drivers uses a similar approach).

A db class variable, which represents the connection itself, i.e. a communication channel with the database, has to be instantiated. After this step, methods of the db class applied to the corresponding object allow database interaction. The constructor of the db class is as follows:

db(string initString, string db)
    

The initString parameter identifies the connection string that contains all necessary parameters to connect to the database. The db parameter identifies the kind of database that we want to connect to. For an ODBC connection the connection string has to contain the username and password to be used to gain database access (if requested), and the db parameter must have value "odbc". An example is:

dbconn = db("DSN=datasource;UID=username;PWD=password", "odbc")
    

If we use a native driver, the connection string is different. An example is the connection to a MySQL database via the Biferno native driver, which uses a syntax similar to the following:

dbconn = db("mysqlhost, mysqluser, mysqlpassword, mydb", "mysql")
    

The db parameter is the name of the driver, which is usually the same as the DBMS name (the parameter is case-sensitive).

The connection string syntax (first constructor parameter) and the driver name (second constructor parameter) are listed in the following table for several native drivers available at the time of this writing:

Table 15.2. Connecting via Native Drivers

DatabaseConnection StringDriver Name
MySQL"[host], [user], [password], [db]""mysql"
Postgres"hostaddr=[host] dbname=[db] user=[user]""postgres"
Oracle"[instance], [username], [password]""oracle"

With the exception of the parameters of the constructor method, all methods of the db class do not depend on the particular DBMS or ODBC used. The following discussion is independent of the particular database used, unless explicitly noted.

2. Direct Execution of an SQL Query

This section discusses a practical example of database interaction and illustrates the main methods of the db class that allow to execute SQL queries on a database and to obtain the resulting information.

In the following script we connect to the MYDB data source (which we will use in all our examples), then we interrogate the users table and send in output to the browser the content of all records, formatted as an HTML table.

<html>
	<body>
<?
	dbconn = db("DSN=MYDB;UID=mydbuser;PWD=enterprise", "odbc")
	query = "SELECT first_name, last_name, sex, age FROM users"
	dbconn.Exec(query)
	nrec = dbconn.GetCurRecs()
	if (nrec)
		{
?>
		<table border="0" cellpadding="4" cellspacing="0">
<?
			for (i = 1; i <= nrec; i++)
				{
					recArray = dbconn.FetchRec()
?>
			<tr>
				<td>$recArray["first_name"]$</td>
				<td>$recArray["last_name"]$</td>
				<td>$recArray["sex"]$</td>
				<td>$recArray["age"]$</td>
			</tr>
<?
				}
?>
		</table>
<?
		}
?>
	</body>
</html>
    

After instantiation of the dbconn variable of class db, we send an SQL query to the database using the Exec method on the dbconn object. This method has the following prototype:

int Exec(string sql_statement, int mode=defaultMode, int rowSetSize, 
boolean freeCurs)
    

The Exec method sends the query contained in the sql_statement parameter to the database and returns an integer number that represents a cursor identifier. In an SQL- capable database, a cursor indicates the current position within a group of records resulting from the execution of an SQL query (record set). A cursor determines which record in a record set will be returned to the application at the next data request from that record set. A cursor can be considered as an entity identifying both the whole record set and the current record position within the record set.

In the previous example the cursor has not been explicitly used because most methods of the db class operate by default on the current cursor, which is the one returned by the last call to the Exec method.

The mode parameter of the Exec method specifies the type of cursor that should be created (static or dynamic) and can assume the values defined by the defaultMode, staticMode and dynamicMode constants of the db class. The default value of this parameter is defaultMode, and in this case the db class chooses the most efficient cursor type depending on the driver and database type.

Static and Dynamic Cursors

A static cursor is a cursor associated to a static record set resulting from an SQL query. Static cursors do not report modifications made by other application (or by other connections/users of the same application) to the record set after their creation, but they do recognize modifications made by the same application within the same connection. Static cursors can request all records in a single query to the database server.

A dynamic cursor reports all modifications to its record set after its creation, including those made by other applications or users. Finally, dynamic cursors usually request records to the database server in a sequence of steps.

The freeCurs parameter can be used to indicate that the cursor can be automatically disposed by Biferno because it will be no longer used in the code (see the Section "Cursor Deletion" in the following). The rowSetSize parameter specifies the number of records that should be extracted by the cursor when the application performs a data request to the current record set. For performance optimization reasons it is advisable to use a value corresponding to the number of records that the application will present together on a page. If no value is specified for the rowSetSize parameter, a default value is chosen that optimizes performance for the driver used.

This cursor characteristic can be modified also after its creation using the RowSetSize method, with prototype:

void RowSetSize(int size, int cursorID)
    

If the cursorID parameter is omitted, the method acts on the current cursor, which, as mentioned, is the common behavior for all db class methods that operate on cursors.

3. Processing the Result of an SQL Query

Going back to the previous example, after execution of SQL query, the GetCurRecs method is invoked on the dbconn variable. This method returns the total number of records for a cursor, and has prototype:

int GetCurRecs(int cursorID)
    

If an SQL query modifies, inserts, or deletes records from a table, the number of affected records can be discovered calling the GetAffectedRecs method, with prototype:

int GetAffectedRecs(int cursorID)
    

To extract a record from a cursor the FetchRec method is available with prototype:

array FetchRec(int cursorID, boolean undefNULL)
    

When invoked, the FetchRec method advances the cursor to the record following the current record in the record set, and returns in an associative array the record content. On the first invocation the FetchRec method returns the first record of the current record set. Only values corresponding to columns specified in the SQL query are returned in the array. If the true value is passed as the undefNULL parameter, array elements corresponding to null values (NULL) in the record are undefined (notice that the default for null values is to contain the empty string).

In our example, after the first call to the FetchRec method, the recArray array will have the following structure:

array(first_name:James, last_name:Kirk, sex:M, age:38)
    

Element index names in the associative array that is returned correspond to column names in the users table that are included in the SQL query "SELECT first_name, last_name, sex, age FROM users". Values of the array elements returned by the FetchRec methods are always of the string class, regardless of the native type of table columns.

Calling the FetchRec method in a loop causes the cursor to iterate through the record set extracting records one at a time. When the cursor is positioned on the last record of the set, the next invocation of the FetchRec method returns an empty array. Based on this behavior, the loop in the previous example can be rewritten as follows, using a while instruction instead of the for instruction:

<?
	while (recArray = dbconn.FetchRec())
		{
			...
		}
?>
    

The loop exit condition is based on the size of the recArray array (when the size is zero, i.e. the array is empty, the loop terminates). This shows how the implicit typecast from the array class to the boolean class is actually done on the base of the dim array property. In other terms, writing: if (my_array) has the same effect as writing: if (my_array.dim > 0).

3.1. Modifying Cursor Position

The main characteristic (and limitation) of the FetchRec method is that the cursor can be moved only forwards in a sequential fashion. To position the cursor on an arbitrary record of the current set, we can use the Seek method with prototype:

void Seek(int index, int cursorID)
     

The index parameter represents the numerical index of the record that we want to position the cursor on. If we write:

dbconn.Seek(1)
     

we will position the current cursor on the first record of the set. If the specified index is greater than the number of records in the current set, the cursor is positioned after the last record and a successive invocation of the FetchRec method returns an empty array. The index of the record currently under the cursor can be obtained using the Tell method with prototype:

int Tell(int cursorID)
     

An example is:

curRecIndex = dbconn.Tell()
     

3.2. Cursor Deletion

When a script uses more than one cursor, it is sometimes necessary to delete a cursor that is no longer necessary, because there is a limit in Biferno on the number of cursors that can be simultaneously used on the same db variable. Cursor deletion also optimizes database performance by freeing resources.

To eliminate a cursor the Free method is available with prototype:

void Free(int cursorID)
     

There are cases when the cursor is no longer necessary after the Exec method was called. In such cases a value of true can be passed to the fourth argument of the Exec (freeCurs), which causes the cursor to be automatically deleted and eliminates the need for a subsequent call to Free. Usually the cursor is necessary when executing SELECT-like SQL statements to be able to subsequently invoke the GetCurRecs, FetchRec etc. methods. Sometimes the cursor is ignored for SQL statements such as UPDATE, DELETE, INSERT (nonetheless the cursor might still be necessary, e.g. to be able to execute the GetAffectedRecs method).

All cursors associated to a database connection are automatically eliminated when the corresponding db class variable is deleted. This is because cursors associated to database connections stored in variables (local or global) having the current script as their scope are deleted when script execution terminates. If we use cursors associated to connections stored in variables with wider scope (e.g. application), these cursors will have to be explicitly eliminated using the Free method when they are no longer necessary or when the script is exited.

4. Preparation and Execution of SQL Queries

We have described how to execute an SQL query using the Exec method, which immediately submits the query to the database asking immediate execution. This method should be used when a query is used only once within a Biferno script.

For queries that will be repeatedly used the ExecPrepared method can be a better alternative. With this method queries are prepared using the Prepare method, which only submits them to the database server for later execution.

It should be noted that, in Biferno, not all DBMS have native support for prepared queries.

In practice it is only worthwhile to use the two-step SQL query strategy (preparation, execution) if we make use of parametric queries, because in this case the same query template can be used multiple times with different values.

The following script shows a simple preparation and execution sequence for a non-parametric SQL query with the goal of illustrating the methods that are used. Parametric SQL queries are discussed in the following.

<?
	// Database connection
	dbconn = db("DSN=MYDB;UID=mydbuser;PWD=enterprise", "odbc")
	// Query preparation
	query = "SELECT * FROM users WHERE last_name = 'Kirk'"
	prep_id = dbconn.Prepare(query)
	// Selection and execution of the prepared query
	curs_id = dbconn.GetPrepared(prep_id)
	dbconn.ExecPrepared(curs_id)
	// Processing of results
	if (dbconn.GetCurRecs(curs_id))
		{
			recArray = dbconn.FetchRec(curs_id)
		}
	// Cursor deletion
	dbconn.Free(curs_id)
?>
    

After the database connection has been established we prepare our SQL query calling the Prepare method with prototype:

int Prepare(string sql_statement, int totPrepared=1, int mode=defaultMode,
int rowSetSize)
    

This method prepares the query corresponding to the sql_statement parameter and returns an integer value representing a unique identifier associated to the prepared query.

The totPrepared parameter specifies the number of queries to be prepared, i.e. the number of cursors associated to the query that are created by the database at preparation time. It might be convenient to prepare in advance (when the application is started) a certain number of parametric queries, which might be the maximum number of users that can access the application at the same time. In this way the application performance can be improved, especially if using ODBC, because the execution of prepared queries requires fewer information exchanges between the client and the database. The difference can run as high as 50%.

The mode and rowSetSize parameters have the same meaning as for the Exec method. Before executing a previously prepared query, the corresponding cursor must be obtained from the database. Toward this end we use the GetPrepared method, passing as a parameter the identifier of the prepared query we want to execute. The prototype is as follows:

int GetPrepared(int prepareID)
    

The invocation returns a new cursor for the prepared query (identified by prepareID). To execute the prepared query we call the ExecPrepared method, passing the cursor identifier obtained from the GetPrepared method as parameter. The prototype of the ExecPrepared method is as follows:

void ExecPrepared(int cursorID)
    

After query execution, the results can be processed acting on the cursor using the previously described methods (GetCurRecs, FetchRec, etc.).

4.1. Dynamic Construction of SQL Strings

Considering the previous example, assume that the string corresponding to the surname to retrieve from the users table is obtained from user input (e.g. from a text field within a HTML form). If we call lname the variable associated to the value inserted by the user, we can dynamically build our SQL string as follows:

<?
	lname = db.Escape(lname)
	query = "SELECT * FROM users WHERE last_name = '" + lname + "'"
?>
     

The static method Escape of the db class transforms a literal string into the appropriate format for insertion in an SQL statement. The prototype is as follows:

static string Escape(string str)
     

This method escapes single quotes that might be present in the string by doubling them (see also the RealEscape method of class db in "Biferno: Reference Guide").

4.2. Using Parametric SQL Queries

A parametric SQL query contains internally substitution markers (a question mark "?"), which may be substituted by values before query execution.

SQL markers can be used in the WHERE clause of a SELECT statement, in the SET clause of an UPDATE statement, and in the VALUES clause of an INSERT statement. Markers can not be used to make table or column names parametric.

We will now modify the script used as an example of preparation and execution of an SQL query to use a parametric query. The first step is to prepare the query, e.g. by inserting the following code rows in the "Biferno.config.bfr" configuration file of our application:

<?
	// Database connection
	application aDBconn = db("DSN=MYDB;UID=mydbuser;PWD=enterprise", "odbc")
	// Query preparation
	local query = "SELECT * FROM users WHERE last_name = ?"
	application aQuery_selname = aDBconn.Prepare(query)
?>
     

Since the database connection must be established before a query can be prepared, the connection must be established in the "Biferno.config.bfr" file.

This technique allows to open a single database connection at application startup. By associating the connection to a variable with application scope, the connection will be visible to and usable by all application scripts, avoiding reconnection at every request.

The following script assigns the value "Kirk" to the lname variable (in practice this variable is likely to be associated to an input value) and, after associating the variable to the query marker using the Bind method, executes the query and processes its results.

<?
	lname = "Kirk"

	// Selection of prepared query
	curs_id = aDBconn.GetPrepared(aQuery_selname)
	// Association of the lname variable to query marker
	aDBconn.Bind(1, "lname", lname.length, inputBindMode, curs_id)
	// Execution of prepared query
	aDBconn.ExecPrepared(curs_id)
	// Results processing
	if (aDBconn.GetCurRecs(curs_id))
		{
			recArray = aDBconn.FetchRec(curs_id)
		}
	// Cursor deletion
	aDBconn.Free(curs_id)
?>
     

The Bind method allows to associate a variable to a marker in an SQL query and has the following prototype:

void Bind(int pos, string variableName, int bytes, int mode, int cursorID)
     

When the query is executed via the ExecPrepared the current value of the variable specified by the variableName parameter is substituted to the marker corresponding to the position identified by the pos parameter and the resulting SQL statement is submitted to the database (more precisely, the pos parameter specifies the index of the occurrence of the ? character within the SQL string).

The bytes parameter denotes the length of the variable called variableName. The mode parameter is the kind of Bind that we are executing. In this case we are passing an input variable and therefore we use the db.inputBindMode value. The Bind method can be also used to obtain return values, e.g. from a stored procedure. In this case db.outputBindMode or db.inputOutputBindMode should be used for the mode parameter.

The cursorID parameter supplies the cursor identifier returned by the GetPrepared method.

The Bind method can associate a variable that has not been defined. The important point is that this variable must be defined when the ExecPrepared method is called, or an error will be generated.

The usefulness of the Bind method is apparent when the value of the variable associated to the SQL parametric string is modified within a loop. In the following example we see how the value corresponding to an array element with index varying within a for loop can be associated to a parametric query.

<html>
<body>
<?
namesArray = array("John", "James", "Andrew", "Leonard")
names = namesArray.dim

dbconn = db("DSN=MYDB;UID=mydbuser;PWD=enterprise", "odbc")
query = "SELECT first_name, last_name, sex, age FROM users WHERE first_name = ?"
prep_id = dbconn.Prepare(query)
curs_id = dbconn.GetPrepared(prep_id)
dbconn.Bind(1, "namesArray[i]", namesArray[i].length, 
				 inputBindMode, curs_id)
for (i = 1; i <= names; i++)
	{
		dbconn.ExecPrepared(curs_id)
		nrec = dbconn.GetCurRecs(curs_id)
		if (nrec)
			{
?>
	<table border="0" cellpadding="4" cellspacing="0">
<?
			while (recArray = dbconn.FetchRec(curs_id))
				{
?>
		<tr>
			<td>$recArray["first_name"]$</td>
			<td>$recArray["last_name"]$</td>
			<td>$recArray["sex"]$</td>
			<td>$recArray["age"]$</td>
		</tr>
<?
				}
			}
?>
			</table>
<?
	}
dbconn.Free(curs_id)
?>
</body>
</html>
     

Internally to the for cycle the ExecPrepared method is called. The method executes the parametric query, where the value of the element of the namesArray array corresponding to the current value of the loop index (i) is substituted to the ? marker. Notice how the array name followed by the index between square brackets is passed to the Bind method. Of course both the array and the index must be defined when the ExecPrepared method is called. With this technique, the search string is changed automatically at every loop iteration.

The BindAll method is similar to the Bind method, and allows to associate a variable to all cursors associated to a prepared query (the Bind method associates the variable to a single cursor). The BindAll method has the following prototype:

void BindAll(int pos, string variableName, int bytes, int mode, int poolID)
     

Let's see an example of the use of this method. In the "Biferno.config.bfr" configuration file of our application we write the following code lines:

<?
	application aDBconn = db("DSN=MYDB;UID=mydbuser;PWD=enterprise", "odbc")
	local query = "SELECT * FROM users WHERE last_name = ?"
	application aQuery_selname = aDBconn.Prepare(query, 16)
	aDBconn.BindAll(1, "lname", 255, inputBindMode, aQuery_selname)
?>
     

Using the Prepare method 16 identical parametric queries are sent to the database and the database generates 16 cursors associated to the parametric query. On the next line, the lname variable (which is not yet defined when the "Biferno.config.bfr" is executed) is associated to all 16 cursors associated to the prepared query by calling the BindAll method and passing the query identifier returned by the Prepare method. The BindAll method must be called right after preparation of the query and before the GetPrepared method is invoked. Notice that the length of the lname variable is not known when the BindAll method is called, and thus the value 255 is passed for the bytes parameter. When executing the prepared query (ExecPrepared call), the code should check that the length of lname does not exceed 255 bytes.

Using this technique we avoid calling the Bind method multiple times to associate the lname variable to the individual cursors returned by the GetPrepared method in the application scripts where the prepared query will be executed.

5. Transactions

A transaction is a sequence of one or more SQL queries executed by an application that are grouped in a single logical unit in such a way that their effect on the database can be reversed. The queries typically modify the database (e.g. by record, table or index insertion, modification, deletion).

The documentation of the database we interface to should be carefully consulted, because not all DBMS support transactions. In the ODBC case transactions support is defined by the specific driver. In the MySQL case, for which Biferno implements a native MySQL driver, transactions are also supported.

The db class makes three methods available to manage transactions:

  • Transaction, to initiate a transaction.

  • Commit, to finalize the result of a transaction.

  • RollBack, to undo the result of a transaction.

All these methods have no parameters and do not return any value. The following script provides an example of the use of these methods:

<?
	// Database connection (e.g. via ODBC)
	dbconn = db("DSN=MYDB;UID=mydbuser;PWD=enterprise", "odbc")
	// Start of a transaction
	dbconn.Transaction()
	// Query execution (e.g. record modification)
	query = "UPDATE users SET age = 40 WHERE user_id = 1"
	dbconn.Exec(query)
	// End of the transaction and finalization of the modification
	dbconn.Commit()
	// or, to undo the database modification
	// dbconn.RollBack()
?>
    

It should be noted that both the Commit and the RollBack methods close the transaction and that the RollBack method can not be used to undo the effect of a transaction closed by Commit method.

6. The Search Class

This class supports decoding of a string containing logical operators (e.g. | or &) and parentheses to make it easier to manipulate the substrings (keywords) relevant to a database search. In other terms, this class is useful to execute archive searches on the base of formatted strings input by the user in a HTML form.

The search class is only used to prepare the string and can not be directly used to execute a database search. To do this, the search string supplied by the user must be first translated into an SQL string of equivalent meaning using the search class.

Table 15.3. Operators supported by the search class in search strings

Logical OperatorCharacters used
ANDAmpersand (&)
ORVertical bar (|)
NOTExclamation mark (!)
Wildcard characterStar (*)

To instantiate a search class variable a suitably formatted string should be passed to the class constructor. Table 15.3, “Operators supported by the search class in search strings” lists the operators supported in a formatted search string for the search class (default characters).

The following example of search strings clarify the use of these operators:

<?
	mySearch = search("movies & sport")
	mySearch = search("movies | sport")
	mySearch = search("sport & !tennis")
	mySearch = search("movi*")
?>
    

The first search string means: Find all elements containing both "movies" and "sport". The second string means: Find all elements containing either "movies" or "sport". The third string means: Find all elements containing "movies" but not "sport". The fourth string means: Find all elements containing words starting with "movi".

The wildcard character can be used either as the first character of the search substring, or as its last character, or both as its first and last character. Its meaning is "starts with", "ends with", or "contains" the substring, respectively.

More complex strings can be implemented using parentheses, as in:

<?
	mySearch = search("movies & (action | horror)")
	mySearch = search("(soccer & maradona) | (cars & senna)")
?>
    

Different characters (or strings) can be specified for the operators that can be used in search strings, both at application and script level. The default application level operators can be changed by changing the values of the SEARCH_AND, SEARCH_OR, SEARCH_NOT and SEARCH_WILD configuration variables in the "Biferno.config.bfr" file. Default operators can be modified for the current script only using the SetOption static method of the search class, with prototype:

static void SetOption(string search_and, string search_or, 
		      string search_not, string search_wild)
    

The method is used as follows:

<?
	search.SetOption("AND", "OR", "NOT", "%")
?>
    

After this call to the SetOption method the search class will recognize in search strings, within the current script, the SQL-style operators and wildcard character.

6.1. Search Class Properties

The search class has a number of properties that allow to obtain information on the associated search string and to modify the meaning of the different substrings. Substrings are single words (or groups of words separated by spaces) and limited by logical operators and parentheses.

The tot read-only property contains the total number of substrings in the search string. The mode property contains the implicit concatenation logical operator for substrings of the search string consisting in space-separated words. This property is used at writing time, when the kind of SQL query that will be generated by the ToSQL method can be defined by the concatenation of strings contained in a search class variable and by assigning one of the logical operators and or or (search class constants) to the mode property. We will show some examples in the following.

The string read-only property is an array containing all substrings of the search string. The oper read/write property is an array containing all logical operators separating substrings of the search string. The possible values of the array elements are the constants all, allNot, and, andNot, or and orNot of the search class.

The group read/write property is an array containing, for each substring of the search string, an identifier indicating the associated type of parenthesis (open, closed, none). In correspondence to an open parenthesis the array contains the value of the openPar constant of the search class. In correspondence to a closed parenthesis the array contains the value of the closePar constant of the search class. The noPar constant denotes the absence of a parenthesis.

The findType read/write property is an array containing, for each substring of the search string, the kind of comparison corresponding to the position of a wildcard character in that string to use during database search. For each substring one of the values specified by the begins, ends and contains constants of the search class will be used.

6.2. Construction of SQL Queries Using the ToSQL Method

We will now show some examples of use of the ToSQL method of the search class to construct SQL strings for database search. The ToSQL method has the following prototype:

string ToSQL(string fieldName, boolean isNumeric, boolean lowerSQL)
     

The fieldName parameter is the search column name (if no value is supplied, the method uses the "[field]" default value). The isNumeric Boolean parameter specifies if the column is a numeric column, i.e. if values should be enclosed between single quotes in the SQL string. The default value is false, based on the fact that searches are more commonly performed on text fields. The lowerSQL Boolean parameter specifies that the result string should use lowercase SQL keywords (the default is uppercase keywords).

The ToSQL method returns an SQL string equivalent to the search associated to the search class object on which the method was called.

We assume in the following script that the search string assigned to the src_str variable was input by the user in a HTML form. Based on this string, a search is executed on the last_name field of the users table (see Table 15.3, “Operators supported by the search class in search strings”).

<html>
<body>
<?
src_str = "K* | M*"

dbconn = db("DSN=MYDB;UID=mydbuser;PWD=enterprise", "odbc")
query = "SELECT first_name, last_name FROM users WHERE "
query += search(src_str).ToSQL("last_name")
curs_id = dbconn.Exec(query)
if (dbconn.GetCurRecs(curs_id))
{
while (arr_rec = dbconn.FetchRec(curs_id))
	print(arr_rec["first_name"]+" "+arr_rec["last_name"]+"<br>")
}
?>
</body>
</html>
     

The ToSQL method is used in the above code to generate, starting from a search class object, an SQL string to be inserted after the WHERE clause in the SQL SELECT statement. In the case of the "K* | M*" search string that is contained in the src_str variable, the string is translated into its SQL equivalent: last_name LIKE 'K%' OR last_name LIKE 'M%'. The query resulting from this procedure selects all records in the users table where the last_name field contains a string starting with the letters K or M.

The following example shows the combined use of the ToSQL method and of the value of the mode property of the search class. Assume that an input word sequence separated by spaces should result in a database search for the same words separated by AND operators. Notice that this is not normally the case, as spaces are usually not considered as separators in search strings and a group of words is treated as a single substring. The script shows the technique to use in this case:

<?
	src_str = "movies sport music"
	my_src = search(src_str)
	my_src.mode = search.and // Search for the AND of the words
	query = "SELECT * FROM tv_channels WHERE "
	query += my_src.ToSQL("category")
?>
     

The SQL string contained in the query variable after script execution has terminated has the following value:

SELECT * FROM tv_channels WHERE category = 'movies' AND category = 
'sport' AND category = 'music'.
     

The use of the mode property of the search class can be a valid alternative to the use of operators, because it allows to simplify search strings while maintaining a degree of flexibility.

An obvious limit to the above script is the fact that keywords are compared with the field content using the "equals" operator. If we wish to search for all records containing one or more keywords of the search string without forcing the user to specify wildcard characters at the beginning and at the end of all keywords, we can operate as in the following example:

<?
function SetFindType(search *inSrc, int inFind)
{
	if (inFind == 0 || inFind == search.begins || inFind == search.ends || \
		inFind == search.contains)
	{
		nFind = inSrc.findType.dim
		for (i = 1; i <= nFind; i++)
			inSrc.findType[i] = inFind
	}
}

src_str = "movies sport music"
my_src = search(src_str)
my_src.mode = search.or // Search for the OR of the words
SetFindType(&my_src, search.contains)
query = "SELECT * FROM tv_channels WHERE "
query += my_src.ToSQL("category")
?>
     

The SQL query resulting from script execution is:

SELECT * FROM tv_channels WHERE category LIKE '%movies%' OR category LIKE 
'%sport%' OR category LIKE '%music%'.
     

The SetFindType function modifies the value of the findType property for all substrings of a search class variable passed by reference and it assigns one of the constant values begins, ends or contains to the property (the zero value removes the property).