unit SQLiteTable3; { Simple classes for using SQLite's exec and get_table. TSQLiteDatabase wraps the calls to open and close an SQLite database. It also wraps SQLite_exec for queries that do not return a result set TSQLiteTable wraps sqlite_get_table. It allows accessing fields by name as well as index and can step through a result set with the Next procedure. Adapted by Tim Anderson (tim@itwriting.com) Originally created by Pablo Pissanetzky (pablo@myhtpc.net) Modified and enhanced by Lukas Gebauer } interface {$IFDEF FPC} {$MODE Delphi} {$ENDIF} uses {$ifdef win32} Windows, {$endif} SQLite3, Classes, SysUtils; const dtInt = 1; dtNumeric = 2; dtStr = 3; dtBlob = 4; dtNull = 5; type ESQLiteException = class(Exception) end; TSQLiteTable = class; TSQLiteDatabase = class private fDB: TSQLiteDB; fInTrans: boolean; procedure RaiseError(s: string; SQL: string); public constructor Create(const FileName: string); destructor Destroy; override; function GetTable(const SQL: string): TSQLiteTable; procedure ExecSQL(const SQL: string); function GetTableValue(const SQL: string): int64; function GetTableFloat(const SQL: string): real; function GetTableString(const SQL: string): string; procedure UpdateBlob(const SQL: string; BlobData: TStream); procedure BeginTransaction; procedure Commit; procedure Rollback; function TableExists(TableName: string): boolean; function ContainsColumn(Table: String; Column: String) : boolean; function GetLastInsertRowID: int64; procedure SetTimeout(Value: integer); function version: string; published property isTransactionOpen: boolean read fInTrans; end; TSQLiteTable = class private fResults: TList; fRowCount: cardinal; fColCount: cardinal; fCols: TStringList; fColTypes: TList; fRow: cardinal; function GetFields(I: cardinal): string; function GetEOF: boolean; function GetBOF: boolean; function GetColumns(I: integer): string; function GetFieldByName(FieldName: string): string; function GetFieldIndex(FieldName: string): integer; function GetCount: integer; function GetCountResult: integer; public constructor Create(DB: TSQLiteDatabase; const SQL: string); destructor Destroy; override; function FieldAsInteger(I: cardinal): int64; function FieldAsBlob(I: cardinal): TMemoryStream; function FieldAsBlobText(I: cardinal): string; function FieldIsNull(I: cardinal): boolean; function FieldAsString(I: cardinal): string; function FieldAsDouble(I: cardinal): double; function Next: boolean; function Previous: boolean; property EOF: boolean read GetEOF; property BOF: boolean read GetBOF; property Fields[I: cardinal]: string read GetFields; property FieldByName[FieldName: string]: string read GetFieldByName; property FieldIndex[FieldName: string]: integer read GetFieldIndex; property Columns[I: integer]: string read GetColumns; property ColCount: cardinal read fColCount; property RowCount: cardinal read fRowCount; property Row: cardinal read fRow; function MoveFirst: boolean; function MoveLast: boolean; property Count: integer read GetCount; // The property CountResult is used when you execute count(*) queries. // It returns 0 if the result set is empty or the value of the // first field as an integer. property CountResult: integer read GetCountResult; end; procedure DisposePointer(ptr: pointer); cdecl; implementation procedure DisposePointer(ptr: pointer); cdecl; begin if assigned(ptr) then freemem(ptr); end; //------------------------------------------------------------------------------ // TSQLiteDatabase //------------------------------------------------------------------------------ constructor TSQLiteDatabase.Create(const FileName: string); var Msg: pchar; iResult: integer; begin inherited Create; self.fInTrans := False; Msg := nil; try iResult := SQLite3_Open(PChar(FileName), Fdb); if iResult <> SQLITE_OK then if Assigned(Fdb) then begin Msg := Sqlite3_ErrMsg(Fdb); raise ESqliteException.CreateFmt('Failed to open database "%s" : %s', [FileName, Msg]); end else raise ESqliteException.CreateFmt('Failed to open database "%s" : unknown error', [FileName]); //set a few configs self.ExecSQL('PRAGMA SYNCHRONOUS=NORMAL;'); // self.ExecSQL('PRAGMA full_column_names = 1;'); self.ExecSQL('PRAGMA temp_store = MEMORY;'); finally if Assigned(Msg) then SQLite3_Free(Msg); end; end; //.............................................................................. destructor TSQLiteDatabase.Destroy; begin if self.fInTrans then self.ExecSQL('ROLLBACK;'); //assume rollback if Assigned(fDB) then SQLite3_Close(fDB); inherited; end; function TSQLiteDatabase.GetLastInsertRowID: int64; begin Result := Sqlite3_LastInsertRowID(self.fDB); end; //.............................................................................. procedure TSQLiteDatabase.RaiseError(s: string; SQL: string); //look up last error and raise an exception with an appropriate message var Msg: PChar; begin Msg := nil; if sqlite3_errcode(self.fDB) <> SQLITE_OK then Msg := sqlite3_errmsg(self.fDB); if Msg <> nil then raise ESqliteException.CreateFmt(s + ' "%s" : %s', [SQL, Msg]) else raise ESqliteException.CreateFmt(s, [SQL, 'No message']); end; procedure TSQLiteDatabase.ExecSQL(const SQL: string); var Stmt: TSQLiteStmt; NextSQLStatement: Pchar; iStepResult: integer; begin try if Sqlite3_Prepare(self.fDB, PChar(SQL), -1, Stmt, NextSQLStatement) <> SQLITE_OK then RaiseError('Error executing SQL', SQL); if (Stmt = nil) then RaiseError('Could not prepare SQL statement', SQL); iStepResult := Sqlite3_step(Stmt); if (iStepResult <> SQLITE_DONE) then RaiseError('Error executing SQL statement', SQL); finally if Assigned(Stmt) then Sqlite3_Finalize(stmt); end; end; procedure TSQLiteDatabase.UpdateBlob(const SQL: string; BlobData: TStream); var iSize: integer; ptr: pointer; Stmt: TSQLiteStmt; Msg: Pchar; NextSQLStatement: Pchar; iStepResult: integer; iBindResult: integer; begin //expects SQL of the form 'UPDATE MYTABLE SET MYFIELD = ? WHERE MYKEY = 1' if pos('?', SQL) = 0 then RaiseError('SQL must include a ? parameter', SQL); Msg := nil; try if Sqlite3_Prepare(self.fDB, PChar(SQL), -1, Stmt, NextSQLStatement) <> SQLITE_OK then RaiseError('Could not prepare SQL statement', SQL); if (Stmt = nil) then RaiseError('Could not prepare SQL statement', SQL); //now bind the blob data iSize := BlobData.size; GetMem(ptr, iSize); if (ptr = nil) then raise ESqliteException.CreateFmt('Error getting memory to save blob', [SQL, 'Error']); BlobData.position := 0; BlobData.Read(ptr^, iSize); iBindResult := SQLite3_BindBlob(stmt, 1, ptr, iSize, @DisposePointer); if iBindResult <> SQLITE_OK then RaiseError('Error binding blob to database', SQL); iStepResult := Sqlite3_step(Stmt); if (iStepResult <> SQLITE_DONE) then RaiseError('Error executing SQL statement', SQL); finally if Assigned(Stmt) then Sqlite3_Finalize(stmt); if Assigned(Msg) then SQLite3_Free(Msg); end; end; //.............................................................................. function TSQLiteDatabase.GetTable(const SQL: string): TSQLiteTable; begin Result := TSQLiteTable.Create(Self, SQL); end; function TSQLiteDatabase.GetTableValue(const SQL: string): int64; var Table: TSQLiteTable; begin Table := self.GetTable(SQL); try Result := Table.FieldAsInteger(0); finally Table.Free; end; end; function TSQLiteDatabase.GetTableFloat(const SQL: string): real; var Table: TSQLiteTable; begin Table := self.GetTable(SQL); try Result := Table.FieldAsDouble(0); finally Table.Free; end; end; function TSQLiteDatabase.GetTableString(const SQL: string): string; var Table: TSQLiteTable; begin Table := self.GetTable(SQL); try Result := Table.FieldAsString(0); finally Table.Free; end; end; procedure TSQLiteDatabase.BeginTransaction; begin if not self.fInTrans then begin self.ExecSQL('BEGIN TRANSACTION;'); self.fInTrans := True; end else raise ESqliteException.Create('Transaction already open'); end; procedure TSQLiteDatabase.Commit; begin self.ExecSQL('COMMIT;'); self.fInTrans := False; end; procedure TSQLiteDatabase.Rollback; begin self.ExecSQL('ROLLBACK;'); self.fInTrans := False; end; function TSQLiteDatabase.TableExists(TableName: string): boolean; var sql: string; ds: TSqliteTable; begin //returns true if table exists in the database sql := 'select [sql] from sqlite_master where [type] = ''table'' and lower(name) = ''' + lowercase(TableName) + ''' '; ds := self.GetTable(sql); try Result := (ds.Count > 0); finally ds.Free; end; end; //from usdx 1.1 alpha function TSQLiteDatabase.ContainsColumn(Table: String; Column: String) : boolean; var sql: string; ds: TSqliteTable; begin sql := 'PRAGMA TABLE_INFO('+Table+');'; ds := self.GetTable(sql); try Result := false; while (ds.Next() and not Result and not ds.EOF) do begin if ds.FieldAsString(1) = Column then Result := true; end; finally ds.Free; end; end; procedure TSQLiteDatabase.SetTimeout(Value: integer); begin SQLite3_BusyTimeout(self.fDB, Value); end; function TSQLiteDatabase.version: string; begin Result := SQLite3_Version; end; //------------------------------------------------------------------------------ // TSQLiteTable //------------------------------------------------------------------------------ constructor TSQLiteTable.Create(DB: TSQLiteDatabase; const SQL: string); var Stmt: TSQLiteStmt; NextSQLStatement: Pchar; iStepResult: integer; ptr: pointer; iNumBytes: integer; thisBlobValue: TMemoryStream; thisStringValue: pstring; thisDoubleValue: pDouble; thisIntValue: pInt64; thisColType: pInteger; i: integer; DeclaredColType: Pchar; ActualColType: integer; ptrValue: Pchar; begin try self.fRowCount := 0; self.fColCount := 0; //if there are several SQL statements in SQL, NextSQLStatment points to the //beginning of the next one. Prepare only prepares the first SQL statement. if Sqlite3_Prepare(DB.fDB, PChar(SQL), -1, Stmt, NextSQLStatement) <> SQLITE_OK then DB.RaiseError('Error executing SQL', SQL); if (Stmt = nil) then DB.RaiseError('Could not prepare SQL statement', SQL); iStepResult := Sqlite3_step(Stmt); while (iStepResult <> SQLITE_DONE) do begin case iStepResult of SQLITE_ROW: begin Inc(fRowCount); if (fRowCount = 1) then begin //get data types fCols := TStringList.Create; fColTypes := TList.Create; fColCount := SQLite3_ColumnCount(stmt); for i := 0 to Pred(fColCount) do fCols.Add(AnsiUpperCase(Sqlite3_ColumnName(stmt, i))); for i := 0 to Pred(fColCount) do begin new(thisColType); DeclaredColType := Sqlite3_ColumnDeclType(stmt, i); if DeclaredColType = nil then thisColType^ := Sqlite3_ColumnType(stmt, i) //use the actual column type instead //seems to be needed for last_insert_rowid else if (DeclaredColType = 'INTEGER') or (DeclaredColType = 'INT') or (DeclaredColType = 'BOOLEAN') then thisColType^ := dtInt else if (DeclaredColType = 'NUMERIC') or (DeclaredColType = 'FLOAT') or (DeclaredColType = 'DOUBLE') or (DeclaredColType = 'REAL') then thisColType^ := dtNumeric else if DeclaredColType = 'BLOB' then thisColType^ := dtBlob else thisColType^ := dtStr; fColTypes.Add(thiscoltype); end; fResults := TList.Create; end; //get column values for i := 0 to Pred(ColCount) do begin ActualColType := Sqlite3_ColumnType(stmt, i); if (ActualColType = SQLITE_NULL) then fResults.Add(nil) else if pInteger(fColTypes[i])^ = dtInt then begin new(thisintvalue); thisintvalue^ := Sqlite3_ColumnInt64(stmt, i); fResults.Add(thisintvalue); end else if pInteger(fColTypes[i])^ = dtNumeric then begin new(thisdoublevalue); thisdoublevalue^ := Sqlite3_ColumnDouble(stmt, i); fResults.Add(thisdoublevalue); end else if pInteger(fColTypes[i])^ = dtBlob then begin iNumBytes := Sqlite3_ColumnBytes(stmt, i); if iNumBytes = 0 then thisblobvalue := nil else begin thisblobvalue := TMemoryStream.Create; thisblobvalue.position := 0; ptr := Sqlite3_ColumnBlob(stmt, i); thisblobvalue.writebuffer(ptr^, iNumBytes); end; fResults.Add(thisblobvalue); end else begin new(thisstringvalue); ptrValue := Sqlite3_ColumnText(stmt, i); setstring(thisstringvalue^, ptrvalue, strlen(ptrvalue)); fResults.Add(thisstringvalue); end; end; end; SQLITE_BUSY: raise ESqliteException.CreateFmt('Could not prepare SQL statement', [SQL, 'SQLite is Busy']); else DB.RaiseError('Could not retrieve data', SQL); end; iStepResult := Sqlite3_step(Stmt); end; fRow := 0; finally if Assigned(Stmt) then Sqlite3_Finalize(stmt); end; end; //.............................................................................. destructor TSQLiteTable.Destroy; var i: cardinal; iColNo: integer; begin if Assigned(fResults) then begin for i := 0 to fResults.Count - 1 do begin //check for blob type iColNo := (i mod fColCount); case pInteger(self.fColTypes[iColNo])^ of dtBlob: TMemoryStream(fResults[i]).Free; dtStr: if fResults[i] <> nil then begin setstring(string(fResults[i]^), nil, 0); dispose(fResults[i]); end; else dispose(fResults[i]); end; end; fResults.Free; end; if Assigned(fCols) then fCols.Free; if Assigned(fColTypes) then for i := 0 to fColTypes.Count - 1 do dispose(fColTypes[i]); fColTypes.Free; inherited; end; //.............................................................................. function TSQLiteTable.GetColumns(I: integer): string; begin Result := fCols[I]; end; //.............................................................................. function TSQLiteTable.GetCountResult: integer; begin if not EOF then Result := StrToInt(Fields[0]) else Result := 0; end; function TSQLiteTable.GetCount: integer; begin Result := FRowCount; end; //.............................................................................. function TSQLiteTable.GetEOF: boolean; begin Result := fRow >= fRowCount; end; function TSQLiteTable.GetBOF: boolean; begin Result := fRow <= 0; end; //.............................................................................. function TSQLiteTable.GetFieldByName(FieldName: string): string; begin Result := GetFields(self.GetFieldIndex(FieldName)); end; function TSQLiteTable.GetFieldIndex(FieldName: string): integer; begin if (fCols = nil) then begin raise ESqliteException.Create('Field ' + fieldname + ' Not found. Empty dataset'); exit; end; if (fCols.count = 0) then begin raise ESqliteException.Create('Field ' + fieldname + ' Not found. Empty dataset'); exit; end; Result := fCols.IndexOf(AnsiUpperCase(FieldName)); if (result < 0) then begin raise ESqliteException.Create('Field not found in dataset: ' + fieldname) end; end; //.............................................................................. function TSQLiteTable.GetFields(I: cardinal): string; var thisvalue: pstring; thistype: integer; begin Result := ''; if EOF then raise ESqliteException.Create('Table is at End of File'); //integer types are not stored in the resultset //as strings, so they should be retrieved using the type-specific //methods thistype := pInteger(self.fColTypes[I])^; case thistype of dtStr: begin thisvalue := self.fResults[(self.frow * self.fColCount) + I]; if (thisvalue <> nil) then Result := thisvalue^ else Result := ''; end; dtInt: Result := IntToStr(self.FieldAsInteger(I)); dtNumeric: Result := FloatToStr(self.FieldAsDouble(I)); dtBlob: Result := self.FieldAsBlobText(I); else Result := ''; end; end; function TSqliteTable.FieldAsBlob(I: cardinal): TMemoryStream; begin if EOF then raise ESqliteException.Create('Table is at End of File'); if (self.fResults[(self.frow * self.fColCount) + I] = nil) then Result := nil else if pInteger(self.fColTypes[I])^ = dtBlob then Result := TMemoryStream(self.fResults[(self.frow * self.fColCount) + I]) else raise ESqliteException.Create('Not a Blob field'); end; function TSqliteTable.FieldAsBlobText(I: cardinal): string; var MemStream: TMemoryStream; Buffer: PChar; begin Result := ''; MemStream := self.FieldAsBlob(I); if MemStream <> nil then if MemStream.Size > 0 then begin MemStream.position := 0; Buffer := stralloc(MemStream.Size + 1); MemStream.readbuffer(Buffer[0], MemStream.Size); (Buffer + MemStream.Size)^ := chr(0); SetString(Result, Buffer, MemStream.size); strdispose(Buffer); end; end; function TSqliteTable.FieldAsInteger(I: cardinal): int64; begin if EOF then //raise ESqliteException.Create('Table is at End of File'); Result := 0 else if (self.fResults[(self.frow * self.fColCount) + I] = nil) then Result := 0 else if pInteger(self.fColTypes[I])^ = dtInt then Result := pInt64(self.fResults[(self.frow * self.fColCount) + I])^ else if pInteger(self.fColTypes[I])^ = dtNumeric then Result := trunc(strtofloat(pString(self.fResults[(self.frow * self.fColCount) + I])^)) else raise ESqliteException.Create('Not an integer or numeric field'); end; function TSqliteTable.FieldAsDouble(I: cardinal): double; begin if EOF then raise ESqliteException.Create('Table is at End of File'); if (self.fResults[(self.frow * self.fColCount) + I] = nil) then Result := 0 else if pInteger(self.fColTypes[I])^ = dtInt then Result := pInt64(self.fResults[(self.frow * self.fColCount) + I])^ else if pInteger(self.fColTypes[I])^ = dtNumeric then Result := pDouble(self.fResults[(self.frow * self.fColCount) + I])^ else raise ESqliteException.Create('Not an integer or numeric field'); end; function TSqliteTable.FieldAsString(I: cardinal): string; begin if EOF then raise ESqliteException.Create('Table is at End of File'); if (self.fResults[(self.frow * self.fColCount) + I] = nil) then Result := '' else Result := self.GetFields(I); end; function TSqliteTable.FieldIsNull(I: cardinal): boolean; var thisvalue: pointer; begin if EOF then raise ESqliteException.Create('Table is at End of File'); thisvalue := self.fResults[(self.frow * self.fColCount) + I]; Result := (thisvalue = nil); end; //.............................................................................. function TSQLiteTable.Next: boolean; begin Result := False; if not EOF then begin Inc(fRow); Result := True; end; end; function TSQLiteTable.Previous: boolean; begin Result := False; if not BOF then begin Dec(fRow); Result := True; end; end; function TSQLiteTable.MoveFirst: boolean; begin Result := False; if self.fRowCount > 0 then begin fRow := 0; Result := True; end; end; function TSQLiteTable.MoveLast: boolean; begin Result := False; if self.fRowCount > 0 then begin fRow := fRowCount - 1; Result := True; end; end; end.