Brucey 3 лет назад
Родитель
Сommit
17a8f6bf76

+ 6 - 0
.gitignore

@@ -0,0 +1,6 @@
+
+.DS_Store
+.bmx
+*.a
+*.i
+*.i2

+ 1455 - 0
core.mod/core.bmx

@@ -0,0 +1,1455 @@
+' 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.<br>
+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.
+	<p>Check #hasError and #error for details of any problems.</p>
+	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.
+	<p>Check #hasError and #error for details of any problems.</p>
+	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).
+	<p>Check #hasError and #error for details of any problems.</p>
+	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.<br>
+	Use #error to retrieve the #TDatabaseError object.
+	End Rem
+	Method hasError:Int()
+		If _lastError Then
+			Return _lastError.isSet()
+		End If
+		
+		Return False
+	End Method
+	
+	Method setError(error:String, append:String = Null, eType:Int, errorValue:Int = 0)
+		If Not _lastError Then
+			_lastError = TDatabaseError.Create(Self, error, append, eType, errorValue)
+		Else
+			_lastError.error = error
+			
+			If append Then
+				_lastError.error:+ " : " + append
+			End If
+			
+			_lastError.errorValue = errorValue
+			_lastError.errorType = eType
+		End If
+	End Method
+
+	Method databaseHandle:Byte Ptr() Abstract
+	
+	Method createResultSet:TQueryResultSet() Abstract
+	
+	Method nativeErrorMessage:String(err:Int) Abstract
+	
+	Rem
+	bbdoc: Determines if the database has support for Prepare/Execute statements.
+	returns: True if the driver supports Prepare/Execute statements.
+	End Rem
+	Method hasPrepareSupport:Int() Abstract
+
+	Rem
+	bbdoc: Determines if the database has transactioning support.
+	returns: True if the driver supports transactions.
+	End Rem
+	Method hasTransactionSupport:Int() Abstract
+	
+	Method free()
+		If _lastError Then
+			_lastError = Null
+		End If
+	End Method
+	
+	Method Delete()
+		free()
+	End Method
+	
+End Type
+
+
+Rem
+bbdoc: A Query object for executing queries and navigating the result sets.
+End Rem
+Type TDatabaseQuery
+
+	Field conn:TDBConnection
+	
+	Field resultSet:TQueryResultSet
+
+	
+	Rem
+	bbdoc: Creates a new #TDatabaseQuery using the supplied @connection.
+	End Rem
+	Function Create:TDatabaseQuery(connection:TDBConnection)
+		Local this:TDatabaseQuery = New TDatabaseQuery
+		
+		this.conn = connection
+		
+		Return this
+	End Function
+
+	Rem
+	bbdoc: Prepares an SQL statement for execution.
+	returns: True if the prepare succeeded.
+	about: Check connection #hasError and #error for details of any problems.
+	End Rem
+	Method prepare:Int(statement:String)
+		If Not resultSet Then
+			resultSet = conn.createResultSet()
+		Else
+			resultSet.clear()
+			resultSet._isActive = False
+
+		End If
+
+		If statement = Null Or statement.length = 0 Then
+
+			conn.setError("Cannot prepare 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.prepare(statement) Then
+
+			' on success, reset the last error.
+			If conn._lasterror Then
+				conn._lasterror.reset()
+			End If
+
+			Return True
+		Else
+			Return False
+		End If
+
+	End Method
+	
+	Rem
+	bbdoc: Executes an SQL statement.
+	returns: True if the execute succeeded.
+	about: For a previously prepared statement, pass Null into this method.
+	<p>Check connection #hasError and #error for details of any problems.</p>
+	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.
+	<p>Check connection #hasError and #error for details of any problems.</p>
+	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.<br>
+	A value of -1 means that this is undetermined by the database driver.
+	End Rem
+	Field length:Int = -1
+	Rem
+	bbdoc: Decimal precision.
+	about: Only applicable for DBFloat and DBDouble field types.<br>
+	A value of -1 means that this is undetermined by the database driver.
+	End Rem
+	Field precision:Int = -1
+	Field value:TDBType
+	Rem
+	bbdoc: Whether this field is required or not.
+	about: True if field is optional (can be NULL), False if field is required (NOT NULL).<br>
+	A value of -1 means that this is undetermined by the database driver.
+	End Rem
+	Field nullable:Int = -1
+	
+	' driver field type
+	Field dtype:Int
+	Field dflags:Int
+	
+	Function Create:TQueryField(name:String, fType:Int)
+		Local this:TQueryField = New TQueryField
+		
+		this.name = name
+		this.fType = fType
+		
+		Return this
+	End Function
+	
+	Method clear()
+		value = Null
+	End Method
+	
+	Method setValue(v:Object)
+		If TDBType(v) Then
+			value = TDBType(v)
+		End If
+	End Method
+	
+	Method Delete()
+		clear()
+	End Method
+	
+End Type
+
+Rem
+bbdoc: Contains details of the last error from the driver.
+End Rem
+Type TDatabaseError
+
+	Const ERROR_NONE:Int = 0
+	Const ERROR_TRANSACTION:Int = 1
+	Const ERROR_CONNECTION:Int = 2
+	Const ERROR_STATEMENT:Int = 3
+	Const ERROR_UNKOWN:Int = 4
+
+	Field db:TDBConnection
+	Rem
+	bbdoc: The error text.
+	End Rem
+	Field error:String
+	Rem
+	bbdoc: The type of error.
+	about: Can be one of ERROR_NONE, ERROR_TRANSACTION, ERROR_CONNECTION, ERROR_STATEMENT or ERROR_UNKOWN.
+	End Rem
+	Field errorType:Int
+	Rem
+	bbdoc: The "native" error value.
+	about: Refer to the specific database documentation for details.
+	End Rem
+	Field errorValue:Int
+
+	Function Create:TDatabaseError(db:TDBConnection, error:String, append:String = Null, errorType:Int, errorValue:Int)
+		Local this:TDatabaseError = New TDatabaseError
+		
+		this.db = db
+		this.error = error
+		
+		If append Then
+			this.error:+ " : " + append
+		End If
+
+		If errorValue Then
+			this.error:+ " (" + errorValue + ") "
+		End If
+		
+		this.errorValue = errorValue
+		this.errorType = errorType
+		
+		Return this
+	End Function
+	
+	Method reset()
+		error = Null
+		errorValue = 0
+		errorType = 0
+	End Method
+	
+	Rem
+	bbdoc: Determines if there is an outstanding error.
+	returns: True if this represents an error.
+	End Rem
+	Method isSet:Int()
+		Return error <> Null And error.length > 0
+	End Method
+	
+	Rem
+	bbdoc: Returns the full error details.
+	End Rem
+	Method toString:String()
+		Return "(" + errorType + ") " + error + " : " + nativeError()
+	End Method
+	
+	Method nativeError:String()
+		If db Then
+			Return db.nativeErrorMessage(errorValue)
+		End If
+	End Method
+	
+	Method Delete()
+		db = Null
+	End Method
+End Type
+
+Rem
+bbdoc: 
+End Rem
+Type TDBTable
+
+	Rem
+	bbdoc: 
+	End Rem
+	Field name:String
+
+	Rem
+	bbdoc: 
+	End Rem
+	Field columns:TDBColumn[]
+	
+	Rem
+	bbdoc: 
+	End Rem
+	Field ddl:String
+	
+	Method SetCountColumns(count:Int)
+		columns = New TDBColumn[count]
+	End Method
+	
+	Method SetColumn(index:Int, col:TDBColumn)
+		columns[index] = col
+	End Method
+	
+End Type
+
+Rem
+bbdoc: 
+End Rem
+Type TDBColumn
+
+	Rem
+	bbdoc: 
+	End Rem
+	Field name:String
+	Rem
+	bbdoc: 
+	End Rem
+	Field dbType:Int
+	Rem
+	bbdoc: 
+	End Rem
+	Field nullable:Int
+	Rem
+	bbdoc: 
+	End Rem
+	Field defaultValue:TDBType
+
+	Function Create:TDBColumn(name:String, dbType:Int, nullable:Int, defaultValue:TDBType)
+		Local this:TDBColumn = New TDBColumn
+		this.name = name
+		this.dbType = dbType
+		this.nullable = nullable
+		this.defaultValue = defaultValue
+		Return this
+	End Function
+	
+End Type
+
+
+Extern
+	Function _strlen:Int(s:Byte Ptr) = "strlen"
+End Extern
+
+' Convert from Max to UTF8
+Function convertISO8859toUTF8:String(text:String)
+	If Not text Then
+		Return ""
+	End If
+	
+	Local l:Int = text.length
+	If l = 0 Then
+		Return ""
+	End If
+	
+	Local count:Int = 0
+	Local s:Byte[] = New Byte[l * 3]
+	
+	For Local i:Int = 0 Until l
+		Local char:Int = text[i]
+
+		If char < 128 Then
+			s[count] = char
+			count:+ 1
+			Continue
+		Else If char<2048
+			s[count] = char/64 | 192
+			count:+ 1
+			s[count] = char Mod 64 | 128
+			count:+ 1
+			Continue
+		Else
+			s[count] =  char/4096 | 224
+			count:+ 1
+			s[count] = char/64 Mod 64 | 128
+			count:+ 1
+			s[count] = char Mod 64 | 128
+			count:+ 1
+			Continue
+		EndIf
+		
+	Next
+
+	Return String.fromBytes(s, count)
+End Function
+
+' Convert from UTF8 to Max
+Function convertUTF8toISO8859:String(s:Byte Ptr)
+
+	Local l:Int = _strlen(s)
+
+	Local b:Short[] = New Short[l]
+	Local bc:Int = -1
+	Local c:Int
+	Local d:Int
+	Local e:Int
+	For Local i:Int = 0 Until l
+
+		bc:+1
+		c = s[i]
+		If c<128 
+			b[bc] = c
+			Continue
+		End If
+		i:+1
+		d=s[i]
+		If c<224 
+			b[bc] = (c-192)*64+(d-128)
+			Continue
+		End If
+		i:+1
+		e = s[i]
+		If c < 240 
+			b[bc] = (c-224)*4096+(d-128)*64+(e-128)
+			If b[bc] = 8233 Then
+				b[bc] = 10
+			End If
+			Continue
+		End If
+	Next
+
+	Return String.fromshorts(b, bc + 1)
+End Function
+
+Function sizedUTF8toISO8859:String(s:Byte Ptr, size:Int)
+
+	Local l:Int = size
+	Local b:Short[] = New Short[l]
+	Local bc:Int = -1
+	Local c:Int
+	Local d:Int
+	Local e:Int
+	For Local i:Int = 0 Until l
+
+		c = s[i]
+		If c = 0 Continue
+
+		bc:+1
+		If c<128
+			b[bc] = c
+			Continue
+		End If
+		i:+1
+		d=s[i]
+		If c<224 
+			b[bc] = (c-192)*64+(d-128)
+			Continue
+		End If
+		i:+1
+		e = s[i]
+		If c < 240 
+			b[bc] = (c-224)*4096+(d-128)*64+(e-128)
+			If b[bc] = 8233 Then
+				b[bc] = 10
+			End If
+			Continue
+		End If
+	Next
+
+	Return String.fromshorts(b, bc + 1)
+End Function
+
+
+
+Type TDatabaseLoader
+	Field _type:String
+	Field _succ:TDatabaseLoader
+
+	Method LoadDatabase:TDBConnection( dbname:String = Null, host:String = Null, ..
+		port:Int = Null, user:String = Null, password:String = Null, ..
+		server:String = Null, options:String = Null ) Abstract
+
+End Type
+
+
+Private
+
+Global _loaders:TDatabaseloader
+
+Public
+
+Function AddDatabaseLoader( loader:TDatabaseLoader )
+	If loader._succ Return
+	loader._succ = _loaders
+	_loaders = loader
+End Function
+
+Rem
+bbdoc: Loads a database engine of the specific @dbType.
+about: Optionally, the function takes a set of parameters that can be used to connect to the
+database at load time.<br>
+See the specific database module documentation for correct @dbType name.
+End Rem
+Function LoadDatabase:TDBConnection( dbType:String, dbname:String = Null, host:String = Null, ..
+		port:Int = Null, user:String = Null, password:String = Null, server:String = Null, ..
+		options:String = Null )
+
+	Local loader:TDatabaseLoader = _loaders
+	
+	While loader
+		If loader._type = dbType Then
+			Local db:TDBConnection = loader.LoadDatabase(dbname, host, port, user, password, server, options)
+			If db Return db
+		End If
+		loader = loader._succ
+	Wend
+
+End Function

+ 759 - 0
core.mod/dbtypes.bmx

@@ -0,0 +1,759 @@
+' 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
+
+Import BRL.Blitz
+
+Import "glue.c"
+Import "strptime.bmx"
+
+Const DBTYPE_STRING:Int = 1
+Const DBTYPE_INT:Int = 2
+Const DBTYPE_DOUBLE:Int = 3
+Const DBTYPE_FLOAT:Int = 4
+Const DBTYPE_LONG:Int = 5
+Const DBTYPE_DATE:Int = 6
+Const DBTYPE_BLOB:Int = 7
+Const DBTYPE_DATETIME:Int = 8
+Const DBTYPE_TIME:Int = 9
+
+Rem
+bbdoc: The Base for database field types.
+about: This type, and its sub-types are used to transparently convert data between BlitzMax
+and the database.<br>
+Currently implemented types are,
+<ul>
+<li>TDBString</li>
+<li>TDBInt</li>
+<li>TDBDouble</li>
+<li>TDBFloat</li>
+<li>TDBLong</li>
+<li>TDBDate (see note below)</li>
+<li>TDBBlob (see note below)</li>
+</ul>
+<p>
+<b>Please Note:</b> Currently TDBDate and TDBBlob are not fully supported/implemented. I still haven't decided on the
+best way to do these - especially the date stuff.<br>
+I'm thinking of adding support for Date, DateTime and Time type fields, which are generally supported on
+most databases. The only problem is, Blitz doesn't really handle them yet !!<br>
+Perhaps I need to build a Date/Calendar module... :-p
+</p>
+End Rem
+Type TDBType
+
+	Field _isNull:Int = True
+
+	Method clear() Abstract
+	Method kind:Int() Abstract
+	
+	Method getString:String()
+		Return Null
+	End Method
+	
+	Method isNull:Int()
+		Return _isNull
+	End Method
+		
+	Method getInt:Int()
+		Return 0
+	End Method
+	
+	Method getDouble:Double()
+		Return 0
+	End Method
+	
+	Method getFloat:Float()
+		Return 0
+	End Method
+
+	Method getLong:Long()
+		Return 0
+	End Method
+
+	Method getDate:Long()
+		Return 0
+	End Method
+	
+	Method getBlob:Byte Ptr()
+		Return Null
+	End Method
+
+	Method setLong(v:Long)
+		Assert 0, "setLong not supported on this TDBType"
+	End Method
+
+	Method setDouble(v:Double)
+		Assert 0, "setDouble not supported on this TDBType"
+	End Method
+
+	Method setString(v:String)
+		Assert 0, "setString not supported on this TDBType"
+	End Method
+
+	Method setInt(v:Int)
+		Assert 0, "setInt not supported on this TDBType"
+	End Method
+
+	Method SetFloat(v:Float)
+		Assert 0, "setFloat not supported on this TDBType"
+	End Method
+
+	Method setDate(v:Long)
+		Assert 0, "setDate not supported on this TDBType"
+	End Method
+	
+	Method setBlob(v:Byte Ptr, s:Int, copy:Int = True)
+		Assert 0, "setBlob not supported on this TDBType"
+	End Method
+	
+	Method size:Int()
+		Assert 0, "size not supported on this TDBType"
+	End Method
+End Type
+
+Rem
+bbdoc: A String-type field.
+End Rem
+Type TDBString Extends TDBType
+	Field value:String
+	
+	Rem
+	bbdoc: Creates a new TDBString object with @value.
+	End Rem
+	Function Set:TDBString(value:String)
+		Local this:TDBString = New TDBString
+		this.setString(value)
+		Return this
+	End Function
+	
+	Rem
+	bbdoc: Returns the string value.
+	returns: The string or Null.
+	End Rem
+	Method getString:String()
+		Return value
+	End Method
+
+	Rem
+	bbdoc: Sets the string value.
+	End Rem
+	Method setString(v:String)
+		If v Then
+			value = v
+			_isNull = False
+		Else
+			value = Null
+			_isNull = True
+		End If
+	End Method
+
+	Rem
+	bbdoc: Returns the size of the string.
+	returns: The size, or 0 if Null.
+	End Rem
+	Method size:Int()
+		If value Then
+			Return value.length
+		End If
+		Return 0
+	End Method
+
+	Method clear()
+		value = Null
+		_isNull = True
+	End Method
+
+	Method kind:Int()
+		Return DBTYPE_STRING
+	End Method
+End Type
+
+Rem
+bbdoc: An Integer-type field.
+End Rem
+Type TDBInt Extends TDBType
+	Field value:Int
+
+	Rem
+	bbdoc: Creates a new TDBInt object with @value.
+	End Rem
+	Function Set:TDBInt(value:Int)
+		Local this:TDBInt = New TDBInt
+		this.setInt(value)
+		Return this
+	End Function
+	
+	Rem
+	bbdoc: Returns the int value.
+	End Rem
+	Method getInt:Int()
+		Return value
+	End Method
+	
+	' Allows to return as a long.
+	Rem
+	bbdoc: Returns a long representation of the value.
+	End Rem
+	Method getLong:Long()
+		Return Long(value)
+	End Method
+
+	Rem
+	bbdoc: Sets the int value.
+	End Rem
+	Method setInt(v:Int)
+		value = v
+		_isNull = False
+	End Method
+
+	Method clear()
+		value = 0
+		_isNull = True
+	End Method
+	
+	Method kind:Int()
+		Return DBTYPE_INT
+	End Method
+
+End Type
+
+Rem
+bbdoc: A Double-type field.
+End Rem
+Type TDBDouble Extends TDBType
+	Field value:Double
+
+	Rem
+	bbdoc: Creates a new TDBDouble object with @value.
+	End Rem
+	Function Set:TDBDouble(value:Double)
+		Local this:TDBDouble = New TDBDouble
+		this.setDouble(value)
+		Return this
+	End Function
+
+	Rem
+	bbdoc: Returns the double value.
+	End Rem
+	Method getDouble:Double()
+		Return value
+	End Method
+
+	' since doubles and floats are similar... allow doubles to return Floats too.
+	Rem
+	bbdoc: Returns the float representation of the double value.
+	End Rem
+	Method getFloat:Float()
+		Return Float(value)
+	End Method
+
+	Rem
+	bbdoc: Sets the double value.
+	End Rem
+	Method setDouble(v:Double)
+		value = v
+		_isNull = False
+	End Method
+
+	Method clear()
+		value = 0
+		_isNull = True
+	End Method
+
+	Method kind:Int()
+		Return DBTYPE_DOUBLE
+	End Method
+
+End Type
+
+Rem
+bbdoc: A Float-type field.
+End Rem
+Type TDBFloat Extends TDBType
+	Field value:Float
+	
+	Rem
+	bbdoc: Creates a new TDBFloat object with @value.
+	End Rem
+	Function Set:TDBFloat(value:Float)
+		Local this:TDBFloat = New TDBFloat
+		this.SetFloat(value)
+		Return this
+	End Function
+
+	Rem
+	bbdoc: Returns the float value.
+	End Rem
+	Method getFloat:Float()
+		Return value
+	End Method
+	
+	Rem
+	bbdoc: Returns the double representation of the float value.
+	End Rem
+	Method getDouble:Double()
+		Return Double(value)
+	End Method
+
+	Rem
+	bbdoc: Sets the float value.
+	End Rem
+	Method SetFloat(v:Float)
+		value = v
+		_isNull = False
+	End Method
+
+	Method clear()
+		value = 0
+		_isNull = True
+	End Method
+	
+	Method kind:Int()
+		Return DBTYPE_FLOAT
+	End Method
+
+End Type
+
+Rem
+bbdoc: A Long-type field.
+End Rem
+Type TDBLong Extends TDBType
+	Field value:Long
+
+	Rem
+	bbdoc: Creates a new TDBLong object with @value.
+	End Rem
+	Function Set:TDBLong(value:Long)
+		Local this:TDBLong = New TDBLong
+		this.setLong(value)
+		Return this
+	End Function
+
+	Rem
+	bbdoc: Returns the long value.
+	End Rem
+	Method getLong:Long()
+		Return value
+	End Method
+	
+	Rem
+	bbdoc: Returns the int representation of the long value.
+	End Rem
+	Method getInt:Int()
+		Return Int(value)
+	End Method
+
+	Rem
+	bbdoc: Sets the long value.
+	End Rem
+	Method setLong(v:Long)
+		value = v
+		_isNull = False
+	End Method
+	
+	Method clear()
+		value = 0
+		_isNull = True
+	End Method
+
+	Method kind:Int()
+		Return DBTYPE_LONG
+	End Method
+
+End Type
+
+Type TDBDateBase Extends TDBType
+
+	Field value:Long
+
+	Method format:String(fmt:String) Abstract
+	
+End Type
+
+
+Rem
+bbdoc: A Date-type field.
+about: <b>Note:</b> This type may change!
+End Rem
+Type TDBDate Extends TDBDateBase
+
+	Field _year:Int = 1900
+	Field _month:Int = 1
+	Field _day:Int = 1
+
+	Function Set:TDBDate(year:Int, Month:Int, day:Int)
+		Local this:TDBDate = New TDBDate 
+		this.setFromParts(year, Month, day)
+		Return this
+	End Function
+
+	Function SetWithLong:TDBDate(value:Long)
+		Local this:TDBDate = New TDBDate
+		this.setDate(value)
+		Return this
+	End Function
+	
+	Rem
+	bbdoc: Creates a TDBDate from a string.
+	about: The date should be in the format: YYYY-MM-DD
+	End Rem
+	Function SetFromString:TDBDate(date:String)
+		Local y:Int, m:Int, d:Int, hh:Int, mm:Int, ss:Int
+		If dbStrptime(date, "%Y-%m-%d", y, m, d, hh, mm, ss)
+			Return Set(y, m, d)
+		End If
+	End Function
+
+	Method getDate:Long()
+		Return value
+	End Method
+	
+	Method getYear:Int()
+		Return _year
+	End Method
+	
+	Method getMonth:Int()
+		Return _month
+	End Method
+	
+	Method getDay:Int()
+		Return _day
+	End Method
+	
+	Method setFromParts(y:Int, m:Int, d:Int)
+		_year = y
+		_month = m
+		_day = d
+
+		_calcDateValue(Varptr value, _year, _month, _day, 0, 0, 0)
+		_isNull = False
+	End Method
+
+	Rem
+	bbdoc: Formats the DateTime using the specified formatting
+	End Rem
+	Method format:String(fmt:String = "%Y-%m-%d")
+		Return _formatDate:String(fmt, _year, _month, _day, 0, 0, 0)
+	End Method
+
+	Method getString:String()
+		Return format()
+	End Method
+	
+	Method setDate(v:Long)
+		value = v
+		_isNull = False
+	End Method
+
+	Method clear() 
+		value = 0
+		_isNull = True
+	End Method
+
+	Method kind:Int()
+		Return DBTYPE_DATE
+	End Method
+
+End Type
+
+Rem
+bbdoc: A Blob-type field.
+about: A blob is binary data, like an image.<br>
+Many databases allow storage of binary data.
+End Rem
+Type TDBBlob Extends TDBType
+	Field value:Byte Ptr
+	Field _size:Int
+	Field _owner:Int
+
+	Rem
+	bbdoc: Creates an instance of TDBBlob with binary data of the specified @size.
+	about: Note: If @copy is True, creates a <b>COPY</b> of the data (using MemAlloc/MemCopy).
+	End Rem
+	Function Set:TDBBlob(value:Byte Ptr, size:Int, copy:Int = True)
+		Local this:TDBBlob = New TDBBlob
+		this.setBlob(value, size, copy)
+		Return this
+	End Function
+
+	Method getBlob:Byte Ptr()
+		Return value
+	End Method
+
+	Rem
+	bbdoc: 
+	about: Note: If @copy is True, creates a <b>COPY</b> of the data (using MemAlloc/MemCopy).
+	End Rem
+	Method setBlob(v:Byte Ptr, s:Int, copy:Int = True)
+		If value Then
+			' clear first, or we have memory lying about!
+			clear()
+		End If
+
+		_size = s
+		If copy And _size > 0 Then
+			_owner = True
+?bmxng
+			value = MemAlloc(Size_T(_size))
+			MemCopy(value, v, Size_T(_size))
+?Not bmxng
+			value = MemAlloc(_size)
+			MemCopy(value, v, _size)
+?
+		Else
+			value = v
+		End If
+		
+		_isNull = False
+	End Method
+
+	Method size:Int()
+		Return _size
+	End Method
+
+	Method clear()
+		If value Then
+			If _owner Then
+				MemFree(value)
+			End If
+			value = Null
+			_isNull = True
+		End If
+	End Method
+
+	Method kind:Int()
+		Return DBTYPE_BLOB
+	End Method
+
+	Method Delete()
+		clear()
+	End Method
+	
+End Type
+
+Rem
+bbdoc: A DateTime-type field.
+about: <b>Note:</b> This type may change!
+End Rem
+Type TDBDateTime Extends TDBDateBase 
+
+	Field _year:Int = 1900
+	Field _month:Int = 1
+	Field _day:Int = 1
+	Field _hour:Int
+	Field _minute:Int
+	Field _second:Int
+	Field value:Long
+
+	Function Set:TDBDateTime(year:Int, Month:Int, day:Int, hours:Int, mins:Int, secs:Int)
+		Local this:TDBDateTime = New TDBDateTime
+		this.setFromParts(year, Month, day, hours, mins, secs)
+		Return this
+	End Function
+
+	Function SetWithLong:TDBDateTime(value:Long)
+		Local this:TDBDateTime = New TDBDateTime
+		this.setDate(value)
+		Return this
+	End Function
+
+	Rem
+	bbdoc: Creates a TDBDateTime from a string.
+	about: The datetime should be in the format: YYYY-MM-DD HH:MM:SS
+	End Rem
+	Function SetFromString:TDBDateTime(date:String)
+		Local y:Int, m:Int, d:Int, hh:Int, mm:Int, ss:Int
+		If dbStrptime(date, "%Y-%m-%d %H:%M:%S", y, m, d, hh, mm, ss)
+			Return Set(y, m, d, hh, mm, ss)
+		End If
+	End Function
+
+	Method setFromParts(y:Int, m:Int, d:Int, hh:Int, mm:Int, ss:Int)
+		_year = y
+		_month = m
+		_day = d
+		_hour = hh
+		_minute = mm
+		_second = ss
+		
+		_calcDateValue(Varptr value, _year, _month, _day, _hour, _minute, _second)
+		_isNull = False
+	End Method
+
+	Method getDate:Long()
+		Return value
+	End Method
+
+	Method getYear:Int()
+		Return _year
+	End Method
+	
+	Method getMonth:Int()
+		Return _month
+	End Method
+	
+	Method getDay:Int()
+		Return _day
+	End Method
+	
+	Method getHour:Int()
+		Return _hour
+	End Method
+	
+	Method getMinute:Int()
+		Return _minute
+	End Method
+	
+	Method getSecond:Int()
+		Return _second
+	End Method
+	
+	Rem
+	bbdoc: Formats the DateTime using the specified formatting
+	End Rem
+	Method format:String(fmt:String = "%Y-%m-%d %H:%M:%S")
+		Return _formatDate:String(fmt, _year, _month, _day, _hour, _minute, _second)
+	End Method
+
+	Method getString:String()
+		Return format()
+	End Method
+
+	Method setDate(v:Long)
+		value = v
+
+		_isNull = False
+	End Method
+
+	Method clear() 
+		value = 0
+		_isNull = True
+	End Method
+
+	Method kind:Int()
+		Return DBTYPE_DATETIME
+	End Method
+
+End Type
+
+Rem
+bbdoc: A Time-type field.
+about: <b>Note:</b> This type may change!
+End Rem
+Type TDBTime Extends TDBDateBase 
+
+	Field _hour:Int
+	Field _minute:Int
+	Field _second:Int
+	Field value:Long
+
+	Function Set:TDBTime(hours:Int, mins:Int, secs:Int)
+		Local this:TDBTime = New TDBTime 
+		this.setFromParts(hours, mins, secs)
+		Return this
+	End Function
+
+	Function SetWithLong:TDBTime(value:Long)
+		Local this:TDBTime = New TDBTime
+		this.setDate(value)
+		Return this
+	End Function
+
+	Rem
+	bbdoc: Creates a TDBTime from a string.
+	about: The datetime should be in the format: YYYY-MM-DD HH:MM:SS
+	End Rem
+	Function SetFromString:TDBTime(date:String)
+		Local y:Int, m:Int, d:Int, hh:Int, mm:Int, ss:Int
+		If dbStrptime(date, "%H:%M:%S", y, m, d, hh, mm, ss)
+			Return Set(hh, mm, ss)
+		End If
+	End Function
+
+	Method getDate:Long()
+		Return value
+	End Method
+
+	Method setDate(v:Long)
+		value = v
+		Local time:Long = v
+		_second = time Mod 60
+		time :/ 60
+		_minute = time Mod 60
+		time :/60
+		_hour = time Mod 24
+		_isNull = False
+	End Method
+
+	Method setFromParts(hh:Int, mm:Int, ss:Int)
+		_hour = hh
+		_minute = mm
+		_second = ss
+		
+		_calcDateValue(Varptr value, 1900, 1, 0, _hour, _minute, _second)
+		_isNull = False
+	End Method
+
+	Method getHour:Int()
+		Return _hour
+	End Method
+	
+	Method getMinute:Int()
+		Return _minute
+	End Method
+	
+	Method getSecond:Int()
+		Return _second
+	End Method
+
+	Rem
+	bbdoc: Formats the DateTime using the specified formatting
+	End Rem
+	Method format:String(fmt:String = "%H:%M:%S")
+		Return _formatDate:String(fmt, 1900, 1, 0, _hour, _minute, _second)
+	End Method
+
+	Method getString:String()
+		Return format()
+	End Method
+
+	Method clear() 
+		value = 0
+		_isNull = True
+	End Method
+
+	Method kind:Int()
+		Return DBTYPE_TIME
+	End Method
+
+End Type
+
+Extern
+	Function _calcDateValue(value:Long Ptr, y:Int, m:Int, d:Int, hh:Int, mm:Int, ss:Int)
+	Function 	_formatDate:String(format:String, y:Int, m:Int, d:Int, hh:Int, mm:Int, ss:Int)
+End Extern
+
+

BIN
core.mod/doc/database_logo.png


+ 113 - 0
core.mod/doc/intro.bbdoc

@@ -0,0 +1,113 @@
+<img src="database_logo.png" align="right" />
+<p>
+The Database Framework module is the core of a set of cross-platform modules to allow you to connect
+to a variety of different databases using a standard set of Types and Functions.<br>
+Using a standard framework means that you don't have to learn or know anything about the underlying database API, since the framework takes care of all the nitty-gritty for you. This way, you only have to worry about the data and the SQLs to access it.
+</p>
+<p>Most databases use, or try to be close to the SQL92 (ANSI-standard) specification, which means that generally, you can re-use the same SQL statements on different databases without worrying about it. Obviously, if you decide to use database-specific SQL, you will have to be careful if you decide you want to then use a different type of database.
+</p>
+<p>Since there are many good online resources, tutorials, and books available that discuss and teach SQL, its use won't be described in great detail in this documentation. We leave it up to you to find out what you need to know ;-)
+</p>
+<h2>Getting Started</h2>
+<p>To connect to a database you need one of the available Database Driver modules.<br>
+As of this version there are currently drivers available for :
+<ul>
+<li>mSQL - (<a href="../../dbmsql.mod/doc/commands.html"> Docs </a>)</li>
+<li>MySQL - (<a href="../../dbmysql.mod/doc/commands.html"> Docs </a>)</li>
+<li>ODBC - (<a href="../../dbodbc.mod/doc/commands.html"> Docs </a>)</li>
+<li>PostgreSQL - (<a href="../../dbpostgresql.mod/doc/commands.html"> Docs </a>)</li>
+<li>SQLite - (<a href="../../dbsqlite.mod/doc/commands.html"> Docs </a>)</li>
+<li>Xbase - (<a href="../../dbxbase.mod/doc/commands.html"> Docs </a>)</li>
+</ul>
+</p>
+<h3>Opening a Connection</h3>
+<p>A <a href="#TDBConnection">TDBConnection</a> object is your interface to the database.<br>
+To create one, you should use the <a href="#LoadDatabase">LoadDatabase</a> function, passing in the relevant parameters. The most important parameter is <i>dbType</i>, which tells the Framework which kind of driver it should load for this connection.<br>
+It is much like the way other "loaders" work in BlitzMax, allowing you connect to several different types of database within the same application.<br>
+<a href="#LoadDatabase">LoadDatabase</a> takes other parameters, each of which may or may not be applicable for a certain driver - see the driver documentation for details.
+</p>
+<p>
+<a href="#LoadDatabase">LoadDatabase</a> will return Null if no valid driver was found.</p>
+<p>If you've provided enough information, the Framework will try to open a connection 
+to the database for you. You can check both <a href="#hasError">hasError</a> and the <a href="#isOpen">isOpen</a> method on the connection
+to determine whether or not it succeeded.
+</p>
+<h3>Communicating with the database</h3>
+<p>Once a connection is open, it's time to start working with the database.</p>
+<p>The Framework has two ways of performing actions on a database.<br>
+The first is to simply execute a query. The second is to prepare the query, and then execute it.</p>
+<p>The first method works like this:
+<pre>db.<a href="#executeQuery">executeQuery</a>("DROP TABLE if exists person")</pre>
+The statement is executed immediately on the database.</p>
+<p> The second method, prepare then execute, requires a bit more work to use, but the advantage over the first method is that although the initial prepare may be relatively slow, it allows multiple subsequent executions of the SQL without having to re-process it each time. (and is therefore more efficient over all)
+</p>
+<p>
+You begin by creating a <a href="#TDatabaseQuery">TDatabaseQuery</a> object,
+<pre>Local query:TDatabaseQuery = TDatabaseQuery.Create(db)</pre>
+The next step is to prepare the query,
+<pre>query.<a href="#prepare">prepare</a>("INSERT INTO person values (NULL, ?, ?)")</pre>
+</p>
+<p>
+With prepared statements/queries you can use placeholders to represent a value that you want to use when you execute it, much like a program variable. In the example above, there are two placeholders, specified by question marks. (<b>Note</b>: Check the database driver documentation for details of placeholder formats)
+</p>
+<p>
+Before the executing the query you need to bind each placeholder with a value. For example:
+<pre>
+For Local i:Int = 0 Until myArray.length
+	query.<a href="#setString">setString</a>(0, myArray[i].forename)
+	query.<a href="#setString">setString</a>(1, myArray[i].surname)
+
+	query.<a href="#execute">execute</a>()
+Next
+</pre>
+As you can see, for each new "insertion" we bind a new piece of data to each placeholder. The execution itself is very fast because the SQL has already been prepared.<br>
+The <b>add&lt;dbtype&gt;()</b> methods are also available for the supported types, which adds a new bind value to the end of the bindings. (see <a href="#addString">addString</a>, <a href="#addInt">addInt</a>, <a href="#addLong">addLong</a>, <a href="#addFloat">addFloat</a> and <a href="#addDouble">addDouble</a>).
+</p>
+<p>
+For SELECT statements, the TDBConnection <a href="#executeQuery">executeQuery</a> method also returns a <a href="#TDatabaseQuery">TDatabaseQuery</a> object.<br>
+A TDatabaseQuery object can be used to process all the rows of data returned from the SELECT. For example:
+</p>
+<pre>Local query:TDatabaseQuery = db.executeQuery("SELECT * FROM person")</pre>
+or, for prepared queries,
+<pre>query.execute()</pre>
+<p>
+There are two ways to get the row data from the SELECT.
+<ul>
+<li>Use the TDatabaseQuery <a href="#nextRow">nextRow</a> method, which fetches the next row, returning True if the fetch was successful, or False if there is no more data.<br>
+Once a row is fetched, you can access the row record using the TDatabaseQuery <a href="#rowRecord">rowRecord</a> method.
+<pre>While query.<a href="#nextRow">nextRow</a>()
+	Local record:<a href="#TQueryRecord">TQueryRecord</a> = query.<a href="#rowRecord">rowRecord</a>()
+	' ...
+Wend</pre>
+</li>
+<li>Use the &quot;EachIn&quot; support, as you would for a TList. For example:
+<pre>For Local record:<a href="#TQueryRecord">TQueryRecord</a> = EachIn query
+	' ...
+Next</pre>
+</li>
+</ul>
+</p>
+<p>The <a href="#rowsAffected">rowsAffected</a> method can be used to determine the number of rows affected by a delete, insert or update.
+</p>
+<h3>Transactions</h3>
+<p>
+Most modern databases support transactions of some kind.<br>
+A transaction is a block of work that doesn't become finalized on the database until you
+commit it. If at some point you want to cancel the transaction, you can "roll" it back to
+the state it was in before you started. This means that you won't have half-processed changes
+in your data if the server/connection goes down half-way through.
+</p>
+<p>To begin a transaction, you can use the <a href="#startTransaction">startTransaction</a> method.<br>
+Once the transaction is started, you should end it by calling either <a href="#commit">commit</a> or <a href="#rollback">rollback</a>.<br>
+If you close the connection before ending the transaction, the state of the transaction is undetermined - see the specific database documentation for details. It is better to end the transaction yourself :-)
+</p>
+<p>When not in a transaction, the default is for all database changing queries (like insert, delete, update, etc) to <b>auto-commit</b>. That is, the database will reflect the changes immediately.
+</p>
+<h2>Examples</h2>
+<p>The following examples use the DBSQLite module to demonstrate use of the Framework.
+<ul>
+<li><a href="../examples/example_01.bmx">example_01.bmx</a></li>
+<li><a href="../examples/example_02.bmx">example_02.bmx</a></li>
+<li><a href="../examples/example_03.bmx">example_03.bmx</a></li>
+</ul>
+</p>

+ 79 - 0
core.mod/examples/example_01.bmx

@@ -0,0 +1,79 @@
+' Basic querying
+
+SuperStrict
+
+Framework BaH.DBSQLite
+Import BRL.filesystem
+Import BRL.StandardIO
+
+' delete the db file if it already exists (just to tidy up the examples!)
+DeleteFile("maxtest.db")
+
+' load the database driver, creating and opening the database
+Local db:TDBConnection = LoadDatabase("SQLITE", "maxtest.db")
+
+If Not db Then
+	Print "No valid driver!"
+	End
+End If
+
+' check for errors
+If db.hasError() Then
+	errorAndClose(db)
+End If
+
+' if the connection is open, do something with it...
+If db.isOpen() Then
+
+	Local names:String[][] = [ ..
+		[ "Alfred", "Aho" ],   ..
+		[ "Brian", "Kernighan" ], ..
+		[ "Peter", "Weinberger" ] ]
+
+	' Create a new table
+	Local s:String = "CREATE TABLE person (id integer primary key AUTOINCREMENT, " + ..
+	  " forename varchar(30)," + ..
+	  " surname varchar(30) )"
+
+	' execute the SQL
+	db.executeQuery(s)
+
+	If db.hasError() Then
+		errorAndClose(db)
+	End If
+
+	' insert some data into the database
+	For Local i:Int = 0 Until names.length
+	
+		' hard-coding the SQLs with data...
+		db.executeQuery("INSERT INTO person values (NULL, '" + names[i][0] + "', '" + names[i][1] + "')")
+		
+		If db.hasError() Then
+			errorAndClose(db)
+		End If
+	Next
+	
+	' retrieve data from the database
+	Local query:TDatabaseQuery = db.executeQuery("SELECT * from person")
+	
+	If db.hasError() Then
+		errorAndClose(db)
+	End If
+
+	' iterate over the retrieved rows
+	While query.nextRow()
+		Local record:TQueryRecord = query.rowRecord()
+		
+		Print "Name = " + record.value(1).getString() + " " + record.value(2).getString()
+	Wend
+	
+	' close the connection!
+	db.close()
+	
+End If
+
+Function errorAndClose(db:TDBConnection)
+	Print db.error().toString()
+	db.close()
+	End
+End Function

+ 95 - 0
core.mod/examples/example_02.bmx

@@ -0,0 +1,95 @@
+' Using a prepared statement
+
+SuperStrict
+
+Framework BaH.DBSQLite
+Import BRL.filesystem
+Import BRL.StandardIO
+
+' delete the db file if it already exists (just to tidy up the examples!)
+DeleteFile("maxtest.db")
+
+' load the database driver, creating and opening the database
+Local db:TDBConnection = LoadDatabase("SQLITE", "maxtest.db")
+
+If Not db Then
+	Print "No valid driver!"
+	End
+End If
+
+' check for errors
+If db.hasError() Then
+	errorAndClose(db)
+End If
+
+
+' if the connection is open, do something with it...
+If db.isOpen() Then
+
+	Local names:String[][] = [ ..
+		[ "Alfred", "Aho" ],   ..
+		[ "Brian", "Kernighan" ], ..
+		[ "Peter", "Weinberger" ] ]
+
+
+	' Create a new table
+	Local s:String = "CREATE TABLE person (id integer primary key AUTOINCREMENT, " + ..
+	  " forename varchar(30)," + ..
+	  " surname varchar(30) )"
+
+	' execute the SQL
+	db.executeQuery(s)
+
+	If db.hasError() Then
+		errorAndClose(db)
+	End If
+
+	' get a new query object 
+	Local query:TDatabaseQuery = TDatabaseQuery.Create(db)
+
+	' prepare the insert statement
+	' by preparing it once, the database can reuse it on succesive inserts which is more efficient.
+	query.prepare("INSERT INTO person values (NULL, ?, ?)")
+	
+	If db.hasError() Then
+		errorAndClose(db)
+	End If
+
+	' iterate overy the array inserting new entries
+	For Local i:Int = 0 Until names.length
+		query.bindValue(0, TDBString.Set(names[i][0]))
+		query.bindValue(1, TDBString.Set(names[i][1]))
+
+		' execute the prepared statement with the bound values
+		query.execute()
+		
+		If db.hasError() Then
+			errorAndClose(db)
+		End If
+	Next
+	
+	' retrieve data from the database
+	query = db.executeQuery("SELECT * from person")
+	
+	If db.hasError() Then
+		errorAndClose(db)
+	End If
+
+	' iterate over the retrieved rows
+	While query.nextRow()
+		Local record:TQueryRecord = query.rowRecord()
+		
+		Print "Name = " + record.value(1).getString() + " " + record.value(2).getString()
+	Wend
+	
+	' close the connection!
+	db.close()
+	
+End If
+
+Function errorAndClose(db:TDBConnection)
+	Print db.error().toString()
+	db.close()
+	End
+End Function
+

+ 100 - 0
core.mod/examples/example_03.bmx

@@ -0,0 +1,100 @@
+' Using a prepared insert and prepared selects
+
+SuperStrict
+
+Framework BaH.DBSQLite
+Import BRL.filesystem
+Import BRL.StandardIO
+
+' delete the db file if it already exists (just to tidy up the examples!)
+DeleteFile("maxtest.db")
+
+' load the database driver, creating and opening the database
+Local db:TDBConnection = LoadDatabase("SQLITE", "maxtest.db")
+
+If Not db Then
+	Print "No valid driver!"
+	End
+End If
+
+' check for errors
+If db.hasError() Then
+	errorAndClose(db)
+End If
+
+
+' if the connection is open, do something with it...
+If db.isOpen() Then
+
+	Local names:String[][] = [ ..
+		[ "Alfred", "Aho" ],   ..
+		[ "Brian", "Kernighan" ], ..
+		[ "Peter", "Weinberger" ] ]
+
+
+	' Create a new table
+	Local s:String = "CREATE TABLE person (id integer primary key AUTOINCREMENT, " + ..
+	  " forename varchar(30)," + ..
+	  " surname varchar(30) )"
+
+	' execute the SQL
+	db.executeQuery(s)
+
+	If db.hasError() Then
+		errorAndClose(db)
+	End If
+
+	' get a new query object 
+	Local query:TDatabaseQuery = TDatabaseQuery.Create(db)
+
+	' prepare the insert statement
+	' by preparing it once, the database can reuse it on succesive inserts which is more efficient.
+	query.prepare("INSERT INTO person values (NULL, ?, ?)")
+	
+	If db.hasError() Then
+		errorAndClose(db)
+	End If
+
+	' iterate overy the array inserting new entries
+	For Local i:Int = 0 Until names.length
+		query.setString(0, names[i][0])
+		query.setString(1, names[i][1])
+
+		' execute the prepared statement with the bound values
+		query.execute()
+		
+		If db.hasError() Then
+			errorAndClose(db)
+		End If
+	Next
+
+	' retrieve data from the database
+	query.prepare("SELECT * from person WHERE surname LIKE ?")
+	
+	If db.hasError() Then
+		errorAndClose(db)
+	End If
+	
+	' bind the value
+	query.addString("%n%")
+	
+	' execute the prepared query
+	query.execute()
+
+	' iterate over the retrieved rows
+	For Local record:TQueryRecord = EachIn query
+		
+		Print "Name = " + record.value(1).getString() + " " + record.value(2).getString()
+	Next
+	
+	' close the connection!
+	db.close()
+	
+End If
+
+Function errorAndClose(db:TDBConnection)
+	Print db.error().toString()
+	db.close()
+	End
+End Function
+

+ 62 - 0
core.mod/glue.c

@@ -0,0 +1,62 @@
+/*
+  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.
+*/
+
+#include <time.h>
+#include "blitz.h"
+
+void _calcDateValue(BBInt64 * value, int y, int m, int d, int hh, int mm, int ss) {
+	struct tm stm;
+	stm.tm_year = y - 1900;
+	stm.tm_mon = m - 1;
+	stm.tm_mday = d;
+	stm.tm_hour = hh;
+	stm.tm_min = mm;
+	stm.tm_sec = ss;
+
+	*value = (BBInt64)(mktime(&stm));
+}
+
+BBString * _formatDate(BBString * format, int y, int m, int d, int hh, int mm, int ss) {
+
+	char buffer [1024];
+	struct tm stm;
+	char * p = bbStringToUTF8String(format);
+
+	stm.tm_year = y - 1900;
+	stm.tm_mon = m - 1;
+	stm.tm_mday = d;
+	stm.tm_hour = hh;
+	stm.tm_min = mm;
+	stm.tm_sec = ss;
+
+	strftime (buffer, 1024, p, &stm);
+	
+	bbMemFree(p);
+	
+	return bbStringFromUTF8String(buffer);
+}
+

+ 204 - 0
core.mod/strptime.bmx

@@ -0,0 +1,204 @@
+' 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 <copyright holder> 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
+
+Extern
+	Function isspace:Int(char:Int)
+	Function isdigit:Int(char:Int)
+End Extern
+
+Function dbStrptime:Int(date:String, format:String, y:Int Var, m:Int Var, d:Int Var, hh:Int Var, mm:Int Var, ss:Int Var)
+
+	Local b:Int = 0
+	Local c:Int = 0
+	Local p:Int = 0
+
+	While p < format.length
+	
+		c = p
+		p:+1
+	
+		If format[c] <> 37 Then ' "%"
+		
+			If isspace(format[c]) Then
+				While b < date.length And isspace(date[b])
+					b:+1
+				Wend
+			Else
+				If format[c] <> date[b] Then
+					Return False
+				End If
+
+				b:+ 1
+
+			End If
+			
+			Continue
+			
+		End If
+	
+		c = p
+		p:+ 1
+		
+		If c >= format.length Then
+			Return 0
+		End If
+	
+		Select format[c]
+			Case 37 ' "%"
+				If date[b] <> 37 Then  ' "%"
+					Return False
+				End If
+				
+				Continue
+			Case 77, 83  ' "M", "S"
+				If b < date.length And Not isspace(date[b]) Then
+				
+					If Not isdigit(date[b]) Then
+						Return False
+					End If
+					
+					Local i:Int = 0
+					While b < date.length And isdigit(date[b])
+						i:* 10 
+						i:+ date[b] - 48 ' "0"
+						b:+ 1
+					Wend
+					
+					If i > 59 Then
+						Return False
+					End If
+					
+					If format[c] = 77 Then ' "M"
+						mm = i
+					Else
+						ss = i
+					End If
+					
+				Else
+					Continue
+				End If
+			Case 72   ' "H"
+
+				If Not isdigit(date[b]) Then
+					Return False
+				End If
+
+				Local i:Int = 0
+				While b < date.length And isdigit(date[b])
+					i:* 10 
+					i:+ date[b] - 48 ' "0"
+					b:+ 1
+				Wend
+
+				If i > 23 Then
+					Return False
+				End If
+				
+				hh = i
+
+			Case 100  ' "d"
+
+				If Not isdigit(date[b]) Then
+					Return False
+				End If
+
+				Local i:Int = 0
+				While b < date.length And isdigit(date[b])
+					i:* 10 
+					i:+ date[b] - 48 ' "0"
+					b:+ 1
+				Wend
+				
+				If i > 31 Then
+					Return False
+				End If
+				
+				d = i
+
+			Case 109  ' "m"
+			
+				If Not isdigit(date[b]) Then
+					Return False
+				End If
+				
+				Local i:Int = 0
+				While b < date.length And isdigit(date[b])
+					i:* 10 
+					i:+ date[b] - 48 ' "0"
+					b:+ 1
+				Wend
+				
+				If i < 1 Or i > 12 Then
+					Return False
+				End If
+				
+				m = i
+				
+			Case 89   ' "Y"
+				If b < date.length And Not isspace(date[b]) Then
+				
+					If Not isdigit(date[b]) Then
+						Return False
+					End If
+				
+					Local i:Int = 0
+					While b < date.length And isdigit(date[b])
+						i:* 10 
+						i:+ date[b] - 48 ' "0"
+						b:+ 1
+					Wend
+					
+					y = i
+					
+				End If
+		End Select
+
+		If b < date.length And isspace(date[b]) Then
+			While p < format.length And Not isspace(format[p])
+				p:+ 1
+			Wend
+		End If
+	
+	Wend
+	
+	Return True
+	
+End Function
+
+
+Rem
+' test
+
+Local y:Int, m:Int, d:Int, hh:Int, mm:Int, ss:Int
+Local date:String = "2007-02-05 12:03:25"
+Local format:String = "%Y-%m-%d %H:%M:%S"
+DebugStop
+Print strptime(date, format, y, m, d, hh, mm, ss)
+
+Print y + ", " + m + ", " + d + ", " + hh + ", " + mm + ", " + ss
+End Rem
+