' Copyright (c) 2007-2022 Bruce A Henderson
' All rights reserved.
'
' Redistribution and use in source and binary forms, with or without
' modification, are permitted provided that the following conditions are met:
' * Redistributions of source code must retain the above copyright
' notice, this list of conditions and the following disclaimer.
' * Redistributions in binary form must reproduce the above copyright
' notice, this list of conditions and the following disclaimer in the
' documentation and/or other materials provided with the distribution.
' * Neither the auther nor the names of its contributors may be used to
' endorse or promote products derived from this software without specific
' prior written permission.
'
' THIS SOFTWARE IS PROVIDED BY Bruce A Henderson ``AS IS'' AND ANY
' EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
' WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
' DISCLAIMED. IN NO EVENT SHALL Bruce A Henderson BE LIABLE FOR ANY
' DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
' (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
' LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
' ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
' (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
' SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
'
SuperStrict
Rem
bbdoc: Database Framework
End Rem
Module Database.Core
ModuleInfo "Version: 1.09"
ModuleInfo "Author: Bruce A Henderson"
ModuleInfo "License: BSD"
ModuleInfo "Copyright: Bruce A Henderson"
ModuleInfo "Modserver: BRL"
ModuleInfo "History: 1.09"
ModuleInfo "History: Refactored use of 'enum'."
ModuleInfo "History: 1.08"
ModuleInfo "History: Fixed prepared statement reuse issue with some drivers."
ModuleInfo "History: Added some integrity checks to TQueryRecord methods."
ModuleInfo "History: Added getTableInfo(), TDBTable and TDBColum."
ModuleInfo "History: Improvements to TDBBlob."
ModuleInfo "History: 1.07"
ModuleInfo "History: Resets error status before execution of new query."
ModuleInfo "History: 1.06"
ModuleInfo "History: Implementation of Date, DateTime and Time types."
ModuleInfo "History: 1.05"
ModuleInfo "History: Improved object cleanup."
ModuleInfo "History: 1.04"
ModuleInfo "History: Improved getFieldByName efficiency."
ModuleInfo "History: Added TQueryRecord helper methods for type/name retrieval - getXXXByName()."
ModuleInfo "History: 1.03"
ModuleInfo "History: Fixed clearing of lasterror after successful query prepare/execute."
ModuleInfo "History: 1.02"
ModuleInfo "History: Added TDatabaseQuery helper binding functions for set/add values."
ModuleInfo "History: Docs update."
ModuleInfo "History: 1.01"
ModuleInfo "History: Fixed Null exception on re-prepare."
ModuleInfo "History: Added TDatabaseQuery clearBindValues() method."
ModuleInfo "History: Added getter methods to TQueryRecord for String, Int, Long, Float and Double."
ModuleInfo "History: Added hasPrepareSupport() and hasTransactionSupport() methods."
ModuleInfo "History: 1.00"
ModuleInfo "History: Initial Release."
Import BRL.LinkedList
Import BRL.Map
Import "dbtypes.bmx"
Const SQL_BeforeFirstRow:Int = -1
Const SQL_AfterLastRow:Int = -2
Rem
bbdoc: Represents a connection to a database.
about: Usually, creating a #TDBConnection object is done through a call to #LoadDatabase with an
appropriate dbtype parameter.
End Rem
Type TDBConnection Abstract
' the native handle
Field handle:Byte Ptr
Field _dbname:String
Field _host:String
Field _port:Int
Field _user:String
Field _password:String
Field _options:String
Field _server:String
Field _isOpen:Int = False
Field _lastError:TDatabaseError
' actual implementation in the driver
Function Create:TDBConnection(dbname:String = Null, host:String = Null, ..
port:Int = Null, user:String = Null, password:String = Null, ..
server:String = Null, options:String = Null) Abstract
Method Init(dbname:String, host:String, port:Int, user:String, password:String, server:String, options:String)
_dbname = dbname
_host = host
_port = port
_user = user
_password = password
_options = options
_server = server
End Method
Rem
bbdoc: Closes the database connection.
about: Check #hasError and #error for details of any problems.
End Rem
Method close() Abstract
Rem
bbdoc: Commits a database transaction.
returns: True if successful.
about: Calling this method is only valid for a previous call to #startTransaction.
Check #hasError and #error for details of any problems.
End Rem Method commit:Int() Abstract Rem bbdoc: Returns a list of table names for the current database. End Rem Method getTables:String[]() Abstract Rem bbdoc: End Rem Method getTableInfo:TDBTable(tableName:String, withDDL:Int = False) Abstract Rem bbdoc: Attempts to open a new database connection. returns: True if successful. about: Check #hasError and #error for details of any problems. End Rem Method open:Int(user:String = Null, pass:String = Null) Abstract Rem bbdoc: Rolls back a database transaction. returns: True if successful. about: Calling this method is only valid for a previous call to #startTransaction.Check #hasError and #error for details of any problems.
End Rem Method rollback:Int() Abstract Rem bbdoc: Starts a database transaction. returns: True if successful. about: Once a transaction has started, it should be eventually closed with a call to either #rollback (if the transaction should be abandoned) or #commit (to save all database changes).Check #hasError and #error for details of any problems.
End Rem Method startTransaction:Int() Abstract Rem bbdoc: Executes an sql statement. returns: A new #TDatabaseQuery object. about: Check #hasError and #error for details of any problems. End Rem Method executeQuery:TDatabaseQuery(sql:String) resetError() Local query:TDatabaseQuery = TDatabaseQuery.Create(Self) If sql And sql.length > 0 Then If query.execute(sql) Then ' reset error... resetError() End If End If Return query End Method Rem bbdoc: Determines if the database connection is open. returns: True if the connection is open. End Rem Method isOpen:Int() Return _isOpen End Method Rem bbdoc: Returns the database name. returns: The database name. End Rem Method getDatabaseName:String() Return _dbName End Method Rem bbdoc: Returns the connection host. returns: The host, or Null. about: Not all drivers require a Host. End Rem Method getHost:String() Return _dbName End Method Rem bbdoc: Returns the connection port number. returns: The port number, or 0. about: Not all drivers require a Port number. End Rem Method getPortNumber:Int() Return _port End Method Rem bbdoc: Returns the last database error. returns: A #TDatabaseError object. about: Will always return a valid #TDatabaseError object. End Rem Method error:TDatabaseError() If Not _lastError Then _lastError = New TDatabaseError End If Return _lastError End Method Rem bbdoc: Resets error. End Rem Method resetError() If _lasterror Then _lasterror.reset() End If End Method Rem bbdoc: Determines if there is an outstanding error. returns: True if there is an error.Check connection #hasError and #error for details of any problems.
End Rem Method execute:Int(statement:String = Null) If statement Then If Not resultSet Then resultSet = conn.createResultSet() Else resultSet.clear() resultSet._isActive = False End If If statement.Trim().length = 0 Then conn.setError("Cannot execute empty statement", Null, TDatabaseError.ERROR_STATEMENT) Return False End If resultSet.query = statement.Trim() If Not conn.isOpen() Then conn.setError("The connection is not open", Null, TDatabaseError.ERROR_CONNECTION) Return False End If If resultSet.executeQuery(statement) Then ' on success, reset the last error. If conn._lasterror Then conn._lasterror.reset() End If Return True Else Return False End If Else If Not conn.isOpen() Then conn.setError("The connection is not open", Null, TDatabaseError.ERROR_CONNECTION) Return False End If If resultSet.execute() Then ' on success, reset the last error. If conn._lasterror Then conn._lasterror.reset() End If Return True Else Return False End If End If End Method Rem bbdoc: Returns the value of the field at @index. returns: A #TDBType object or Null. End Rem Method value:TDBType(index:Int) If isActive() And index > SQL_BeforeFirstRow Then Return resultSet.dataValue(index) End If Return Null End Method Rem bbdoc: Retrieves the next row in the result set. returns: True if a row was retrieved. about: Each call to this method populates a #TQueryRecord which can be retrieved via the #record method.Check connection #hasError and #error for details of any problems.
End Rem Method nextRow:Int() If Not isActive() Then Return False End If Local result:Int Select rowIndex() Case SQL_BeforeFirstRow result = resultSet.firstRow() Return result Case SQL_AfterLastRow Return False Default If Not resultSet.nextRow() Then resultSet.setRowIndex(SQL_AfterLastRow) ' done with the resultset... resultSet.reset() Return False End If End Select Return True End Method Method isActive:Int() Return resultSet And resultSet.isActive() End Method Method rowIndex:Int() Return resultSet.rowIndex() End Method Rem bbdoc: Returns the record for the query. End Rem Method rowRecord:TQueryRecord() Local r:TQueryRecord = resultSet.rowRecord() ' if the resultSet is valid we can fill in the values. If resultSet.isValid() Then Local c:Int = r.count() For Local i:Int = 0 Until c r.setValue(i, value(i)) Next End If Return r End Method Rem bbdoc: Binds a #TDBType value at the specified position. about: If a bind value already exists at the specified @position, it is replaced with the new one. End Rem Method bindValue(position:Int, value:TDBType) If resultSet Then resultSet.bindValue(position, value) End If End Method Rem bbdoc: Adds a new #TDBType bind value. about: The value is added to the end of the current list of bind values. End Rem Method addBindValue(value:TDBType) If resultSet Then resultSet.addBindValue(value) End If End Method Rem bbdoc: Binds the String @value at the specified @position. about: If a bind value already exists at the specified @position, it is replaced with the new one. End Rem Method setString(position:Int, value:String) bindValue(position, TDBString.Set(value)) End Method Rem bbdoc: Binds the Int @value at the specified @position. about: If a bind value already exists at the specified @position, it is replaced with the new one. End Rem Method setInt(position:Int, value:Int) bindValue(position, TDBInt.Set(value)) End Method Rem bbdoc: Binds the Long @value at the specified @position. about: If a bind value already exists at the specified @position, it is replaced with the new one. End Rem Method setLong(position:Int, value:Long) bindValue(position, TDBLong.Set(value)) End Method Rem bbdoc: Binds the Float @value at the specified @position. about: If a bind value already exists at the specified @position, it is replaced with the new one. End Rem Method setFloat(position:Int, value:Float) bindValue(position, TDBFloat.Set(value)) End Method Rem bbdoc: Binds the Double @value at the specified @position. about: If a bind value already exists at the specified @position, it is replaced with the new one. End Rem Method setDouble(position:Int, value:Double) bindValue(position, TDBDouble.Set(value)) End Method Rem bbdoc: Adds a new String bind value. about: The value is added to the end of the current list of bind values. End Rem Method addString(value:String) addBindValue(TDBString.Set(value)) End Method Rem bbdoc: Adds a new Int bind value. about: The value is added to the end of the current list of bind values. End Rem Method addInt(value:Int) addBindValue(TDBInt.Set(value)) End Method Rem bbdoc: Adds a new Long bind value. about: The value is added to the end of the current list of bind values. End Rem Method addLong(value:Long) addBindValue(TDBLong.Set(value)) End Method Rem bbdoc: Adds a new Float bind value. about: The value is added to the end of the current list of bind values. End Rem Method addFloat(value:Float) addBindValue(TDBFloat.Set(value)) End Method Rem bbdoc: Adds a new Double bind value. about: The value is added to the end of the current list of bind values. End Rem Method addDouble(value:Double) addBindValue(TDBDouble.Set(value)) End Method Rem bbdoc: Clears the query bind values. End Rem Method clearBindValues() If resultSet Then resultSet.clearBindValues() End If End Method Rem bbdoc: Returns the id of the last inserted row. about: Results returned from this method on anything other than an insert on a table with an auto-incrementing field, are undetermined. End Rem Method lastInsertedId:Long() If resultSet Then Return resultSet.lastInsertedId() End If End Method Rem bbdoc: Returns the number of rows affected by the previously executed statement. about: Only really useful for inserts, updates and deletes. That is, results on selects are undetermined. End Rem Method rowsAffected:Int() If resultSet Then Return resultSet.rowsAffected() End If Return -1 End Method ' "eachin" support Method ObjectEnumerator:TRowEnumerator() Local enumerator:TRowEnumerator = New TRowEnumerator enumerator.query = Self Return enumerator End Method Method free() If resultSet Then resultSet.free() resultSet = Null End If If conn Then conn = Null End If End Method Method Delete() free() End Method End Type ' "eachin" support Type TRowEnumerator Method HasNext:Int() Local result:Int = query.nextRow() If result Then record = query.rowRecord() If record._isEmptySet Then Return False End If End If Return result End Method Method NextObject:Object() Return record End Method Method Delete() query = Null record = Null End Method '***** PRIVATE ***** Field query:TDatabaseQuery Field record:TQueryRecord End Type ' Implementation specific result set. ' You probably shouldn't be using any of this type's methods directly... Type TQueryResultSet Field conn:TDBConnection Field stmtHandle:Byte Ptr Field query:String Field _isActive:Int = False Field index:Int = SQL_BeforeFirstRow Field values:TDBType[] Field bindCount:Int Field boundValues:TDBType[] Field rec:TQueryRecord ' actual implementation in the driver Function Create:TQueryResultSet(db:TDBConnection, sql:String = Null) Abstract Method Init(db:TDBConnection, sql:String) conn = db query = sql End Method Method clearBindValues() If boundValues Then For Local i:Int = 0 Until boundValues.length If boundValues[i] Then boundValues[i].clear() 'boundValues[i] = Null End If Next 'boundValues = Null bindCount = 0 End If If values Then For Local i:Int = 0 Until values.length If values[i] Then values[i].clear() 'values[i] = Null End If Next 'values = Null End If End Method Method clear() clearBindValues() End Method Method executeQuery:Int(statement:String) Abstract Method prepare:Int(statement:String) Abstract Method execute:Int() Abstract Method firstRow:Int() Abstract Method nextRow:Int() Abstract Method lastInsertedId:Long() Abstract Method rowsAffected:Int() Abstract Function dbTypeFromNative:Int(name:String, _type:Int = 0, _flags:Int = 0) Abstract Method rowRecord:TQueryRecord() If Not isActive() Then Return TQueryRecord.Create() End If Return rec End Method Method dataValue:TDBType(index:Int) If isActive() And rec And Not rec.isEmpty() Then If index >= 0 And index < rec.count() Then Return values[index] End If End If Return Null End Method Method rowIndex:Int() Return index End Method Method setRowIndex(i:Int) index = i End Method Method isActive:Int() Return _isActive End Method Method isValid:Int() Return index <> SQL_BeforeFirstRow And index <> SQL_AfterLastRow End Method Method resetValues(size:Int) If values Then For Local i:Int = 0 Until values.length If values[i] Then values[i].clear() End If Next values = Null End If values = New TDBType[size] End Method Method resetBindCount() bindCount = 0 End Method Method addBindValue(value:TDBType) bindValue(bindCount, value) End Method Method bindValue(index:Int, value:TDBType) If Not boundValues Then boundValues = New TDBType[index + 1] End If ' extend the array if required If boundValues.length <= index Then boundValues = boundValues[..index + 1] End If ' amend bindCount if required If index > bindCount Then bindCount = index End If ' bindCount represents the length, so if last index matches, need to increment it. If index = bindCount Then bindCount :+ 1 End If ' a value already exists here... remove it first. If boundValues[index] Then boundValues[index].clear() boundValues[index] = Null End If boundValues[index] = value End Method Method reset() End Method Method free() clear() values = Null boundValues = Null rec = Null End Method Method Delete() free() End Method End Type Rem bbdoc: A specific record (or row) for a result set. End Rem Type TQueryRecord Field _empty:Int = True Field _isEmptySet:Int = False Field fields:TQueryField[] Field fieldsMap:TMap Function Create:TQueryRecord() Local this:TQueryRecord = New TQueryRecord Return this End Function Rem bbdoc: Returns the #TQueryField object at @index. End Rem Method getField:TQueryField(index:Int) Return fields[index] End Method Rem bbdoc: Returns the named #TQueryField object. End Rem Method getFieldByName:TQueryField(name:String) Return TQueryField(fieldsMap.valueForKey(name)) End Method Rem bbdoc: The index (position) of the field @name in the record. returns: The field index, or -1 if not found. End Rem Method indexOf:Int(name:String) For Local i:Int = 0 Until fields.length If name = fields[i].name Then Return i End If Next Return -1 End Method Rem bbdoc: A count of the number of fields in the record. returns: The field count. End Rem Method count:Int() Return fields.length End Method Method isEmpty:Int() Return _empty End Method Method setValue(index:Int, value:Object) If index >= 0 And index < fields.length Then fields[index].setValue(value) End If End Method Rem bbdoc: Returns the value of the field at @index returns: a #TDBType value object or Null. End Rem Method value:TDBType(index:Int) If fields Then If index >= 0 And index < fields.length Then Return fields[index].value End If End If Return Null End Method Rem bbdoc: Returns the string value at @index about: The result is undetermined if the value at @index is not a string field. End Rem Method getString:String(index:Int) Local v:TDBType = value(index) If v Then Return v.getString() End If End Method Rem bbdoc: Returns the string value for the field @name about: The result is undetermined if the value at @name is not a string field. End Rem Method getStringByName:String(name:String) Local f:TQueryField = getFieldByName(name) If f And f.value Then Return f.value.getString() End If End Method Rem bbdoc: Returns the int value at @index about: The result is undetermined if the value at @index is not an int field. End Rem Method getInt:Int(index:Int) Local v:TDBType = value(index) If v Then Return v.getInt() End If End Method Rem bbdoc: Returns the int value for the field @name about: The result is undetermined if the value at @name is not an int field. End Rem Method getIntByName:Int(name:String) Local f:TQueryField = getFieldByName(name) If f And f.value Then Return f.value.getInt() End If End Method Rem bbdoc: Returns the long value at @index about: The result is undetermined if the value at @index is not a long field. End Rem Method getLong:Long(index:Int) Local v:TDBType = value(index) If v Then Return v.getLong() End If End Method Rem bbdoc: Returns the long value for the field @name about: The result is undetermined if the value at @name is not a long field. End Rem Method getLongByName:Long(name:String) Local f:TQueryField = getFieldByName(name) If f And f.value Then Return f.value.getLong() End If End Method Rem bbdoc: Returns the float value at @index about: The result is undetermined if the value at @index is not a float field. End Rem Method getFloat:Float(index:Int) Local v:TDBType = value(index) If v Then Return v.getFloat() End If End Method Rem bbdoc: Returns the float value for the field @name about: The result is undetermined if the value at @name is not a float field. End Rem Method getFloatByName:Float(name:String) Local f:TQueryField = getFieldByName(name) If f And f.value Then Return f.value.getFloat() End If End Method Rem bbdoc: Returns the double value at @index about: The result is undetermined if the value at @index is not a double field. End Rem Method getDouble:Double(index:Int) Local v:TDBType = value(index) If v Then Return v.getDouble() End If End Method Rem bbdoc: Returns the double value for the field @name about: The result is undetermined if the value at @name is not a double field. End Rem Method getDoubleByName:String(name:String) Local f:TQueryField = getFieldByName(name) If f And f.value Then Return f.value.getDouble() End If End Method Method clear() If fields Then For Local i:Int = 0 Until fields.length If fields[i] Then fields[i].clear() End If Next fields = Null fieldsMap.clear() fieldsMap = Null End If _empty = True _isEmptySet = True End Method Method Init(size:Int) If fields Then clear() End If fields = New TQueryField[size] fieldsMap = New TMap _empty = False _isEmptySet = False End Method Method setField(index:Int, theField:TQueryField) If fields Then fields[index] = theField fieldsMap.insert(theField.name, theField) End If End Method Method setIsEmptySet() _isEmptySet = True End Method Method Delete() Clear() End Method End Type Rem bbdoc: A field definition, including a value if part of a result set record. End Rem Type TQueryField Rem bbdoc: The field name End Rem Field name:String Rem bbdoc: Field type End Rem Field fType:Int Rem bbdoc: Field size. about: Dependent on the type of field. For a DBString field it would indicate number of characters.