Home
Updated July 2008
How it's Done
- The Snags
How it's Done - Modifying the SQLite code
Demonstration Data Files
SQLite Performance and Virus-Scanners
Demonstration Application
Visual BASIC Declaration and Programming Guidelines
Handling Unicode and UTF-8
Conclusion
Updates and News
Things Left To Do
Downloads
Windows is supported from
NT4.0/SP6a to Windows XP
VB Declares are included
with the demo project
SQLite 3.5.9 VB DLL and Demo
Download
the optimal XP/Vista VC++ 9.0 version
SQLite 3.5.9 VB DLL and Demo
Download
the NT/2K legacy-compatible VC++ 5.0 version
Quick-Demo project (VC9 - Windows XP)
Quick-Demo Project (VC5 - NT/2k)
Download the Example Database and SQL Query Separately
|
Using SQLITE 3 from Visual BASIC 5 and 6 A brief offshoot from the VBToolbox project was having a quick look
at SQLite 3 and example code-wrappers for Visual BASIC. I already included a distro of
SQLite with MicroApache as it
is awesomely tiny and well-designed. Whilst the examples and tutorials were good they were
mainly targeted at VB6 or higher. The main show-stopper for me using VB5 was the need to
return a Variant array via an external DLL declare. My copy of VB 5 simply wouldn't let me
do that. As far as I could see it wasn't possible to return a String or Variant array from
an externally declared function. The two tutorials at Tannertech and Persistent Realities were
very useful and informative but I just couldn't use the code in VB5 and they were probably
intended for VB 6 only.
This kinda stumped me but I knew I already had reliable
code working which returned Variant String arrays either via function body or via a
function parameter so I bit the bullet and completely rewrote the code in "C"
using my recently-acquired free copy of Visual
C++ 2008 Express Edition. It turned out to be a major headache and far more difficult
than described in many tutorials but here's the resulting DLL running SQLite version 3.5.9
...
The Albums demo database being queried successfully in the VB5 IDE
using the rewritten code and my SQLite DLL
How it's Done - The Snags
This may be an ongoing "howto" as there is so
much fiddly stuff to put in especially if not using an older C compiler.
The main issues I found were...
- Visual BASIC 5 won't let me declare this:
...
Private Declare Function sqlite_get_table Lib
"SQLite3.dll" (ByVal DB_Handle As Long, ByVal SQLString As String, _
ByRef ErrStr As String) As Variant()
Instead, I can only have this (which isn't actually as big a problem as you may may think)
...
Private Declare Function
sqlite_get_table Lib "SQLite3.dll" (ByVal DB_Handle As Long, ByVal SQLString As
String, _
ByRef ErrStr As String) As Variant
As long as the returned Variant is properly checked then a non-dimensioned Variant - Dim
v As Variant - can be handled as an array when returned from a function-call with
no problems whatsoever. There is sufficient information stored internally within the
Variant to reference any array-contents.
- VC++ 2008 - Unicode has to be disabled: Project->Properties->Configuration
Properties->General->Project Defaults->Character Set (set to "Not
Set")
Failing to do this I found that you can run into problems with unexpected Unicode strings
starting with a NULL (0x00) character which result in "empty" C strings
- If you're not sure about UNICODE status then these declares
may be helpful during compilation...
#ifdef UNICODE
#pragma message( "UNICODE is enabled: " __FILE__
)
#else
#pragma message( "UNICODE is NOT enabled: " __FILE__ )
#endif
#undef UNICODE // You can always disable UNICODE support using these
two undefs
#undef _UNICODE |
Choosing the wrong return-type for a function, regardless
of whether the VB declare matches the "C" prototype led to corrupted function
parameter strings. I wasted about 3 hours on this after finding that strings contained
nothing but 0x00,0x00,0x00... Changing the return type by experimenting cured this
problem. I guess the stack or heap can get affected sometimes even when the declarations
are legit. Everything has to be just right. Abandoning the use of SAFEARRAY
returns and deciding to completely rewrite the example code cured this huge headache.
- A DEF file is required. Get one from the DLL ZIP download here (direct ZIP link).
You need to unzip it, copy the DEF file to your project folder,
drag the DEF file to your project from there,
then tell VC++ to use this file. Project->Properties->Configuration
Properties->Linker->Input->Module Definition File (enter sqlite3.def)
- Several exports need to be disabled from your DEF file as
they cause compile errors. AFAIK these are only used internally by SQLite
The def file should contain all functions with these caveats (v1.00 shown below)
EXPORTS new exports //////////////
GetArrayDimensions // Non-SQLite Toolbox Function to return the dimensions of a
Variant
GetArrayRows // Non-SQLite Toolbox Function to return the number of rows in a
2x matrix Variant
sqlite_get_table
sqlite_rowcount
;changed functions //////////////
sqlite3_libversion // Now returns a LPSTR
sqlite3_libversion_number // Now returns a long/DWORD (not an int)
;EXPORTS disabled //////////////
;sqlite3_config
;sqlite3_initialize
;sqlite3_next_stmt
;sqlite3_shutdown
;sqlite3_status;
;sqlite3_os_end
;sqlite3_os_init
- Spurious 64-bit integer warnings in the SQLite code using
MSVC 2008
Disable these using: Project -> Properties -> C/C++ -> General ->
Detect 64-bits portability Issues -> NO
- Mountains of compiler warnings with the SQLite code using
MSVC 2008. Many of these could have been prevented by use of judicious casting within the
code. However, to criticise the work of someone who's a far better coder than I would be
churlish.
Disable these by adding the following to "C" modules:
#pragma warning(disable : 4244 4018 4311 4267 4267 4312) // Most dubious assignments etc.
#pragma warning(disable : 4996) // Localtime |
- Some of the example code seems to contain some bizarre
errors such as returning an int as a string for the SQL version number. As the pages are
quite old now (2007?) I'm guessing that writing the wrapper was sufficiently tedious for
the code not to have been updated a great deal. It is always worth scanning through code
you find on the net, never assume it is free from conceptual, logical or other errors
(that definitely goes for my code too).
- I decided to dump VB_SQLITE_VERSION
constant as given in the Tannertech code and use the actual SQLite version string and
DWORD values
- Code which is actually active is "greyed-out" as
not being active by pre-processor definition in VC++ 2008. I was getting complaints about
code which was supposed to be "disabled" and assumed the compiler was being
strict, but the code was, in actual fact was being compiled up in the amalgamated C
source. I'm guessing VC got confused? Whilst writing this up I noted a block of
#defined where the colour had changed without due cause half way through a block of 12
statements.
- libversion_number is listed as returning an
int but the value returned is too high to be contained with an int and returns a negative
value. This may be due to flawed assumptions on the size of an integer. I changed this to
DWORD wherever possible (although MSVC++ 2008 did complain). Where this was not possible
it was changed to long (const long - see below)
- Several areas have constants being returned as non
"const". These need to be changed. I don't believe it is good practice to permit
const values to be changed. Even if VB allows it the underlying "C" code should
reflect the intentions.
- For some reason BSTRs are used with the possible assumption
that VB cannot or will not cast Unicode strings to ANSI. Reading the Microsoft MSDN
contents and doing a lot of experimentation during the development of VBToolbox confirms
that strings are passed just fine as ANSI. This means that some of the recommended changes
from char* to BSTR aren't really needed.
- Conversion to wide character (Unicode). Is often shown in
examples converting to CP_ACP (ANSI). Combine this with an assumption
that the result will be a byte-pair string in Unicode format led to some peculiar and
possibly difficult to spot bugs where the allocated string length was twice that which was
actually needed. Converting ANSI to ANSI doesn't need a larger string. Maybe it make for
good forward-compatibility and may be more code needs to be added to check for code-page
expansion. Debug testing showed that the excess string simply returned a double-length
string back to VB which was packed with random garbage. I dumped the need for BSTRs
wherever possible and dealt with raw ANSI. This may mean that Unicode support quality may
vary in other locales but hey, I don't have all week to spend on this and UK/US does me
fine. If you want Chinese or Korean locale then get out your compiler and add the extra
code-page conversion code!.
- SAFEARRAY strings are very badly documented. I'm still not
100% sure that SafeArrayPutElement() allocates memory internally for any
BSTR etc. which is passed to it or whether it simply allocates space for the pointer. I am
assuming that it does allocate space which VB then reclaims when it destroys the Variant
as it goes out of scope.
For something this critical to reliability the Microsoft documentation really should make
it absolutely crystal-clear. I've read the contents of countless forums
(particularly on many of the issues above and its clear that even some of the guys from
Microsoft get it wrong. This shows in some example code too which is often just plain
wrong. E.g. the VB4 Variant example code which resorts to using global pointers for
SAFEARRAY returns instead of correctly allocating to variant (pparray or parray). This
means that subsequent calls to your function overwrites the contents of any previous
VARIANT (oops!).
- Use of intermediary VARIANTs to store a BSTR in SAFEARRAY
seemed a bit pointless to me since the examples return everything in a 2-dimensional
string array (rows and columns of strings). The only need I could see for this was to be
able to return a Variant() array. I wasn't able to do this so I was able to dump a fair
bit of code.
- I also decided to dump the OLE temporary memory allocation
functions seen in example-code and use SysAllocStringLen and SysFreeString
instead.
- Assigning rows and columns can be confusing and as far as I
can recall I had to reverse the row/column order in order to be able to use the VB
function Ubound() to retrieve the number of columns. This left the SQLite row-count
function to return the rows but I decided also to add another useful VARIANT function
which returns the number of rows in a 2-dimensional VARIANT array which dispenses with the
need for that SQLite call and deals directly with the VARIANT object returned. This means
I can also reuse the code elsewhere as a generic function. Therefore
UBound(<Variant>) returns the number of columns
GetArrayDimensions(<Variant>) returns the number of rows
- MSVC 2008 rejected the following code which
I commonly use to create MessageBoxes to help debug a DLL. No idea why but it thinks the
const isn't a const!
const int MSG_MAX=256;
char msg[MSG_MAX+1]=""; // MSVC says MSG_MAX isn't a const - const expected ? |
- Not checking or inadequate checking of API returns. I'm a
bit paranoid about checking everything as I hate spending time tracking down problems.
Finding unexpected problems within a DLL can be a pain and very time-consuming so it pays
to trap for all exceptions from the get-go and it doesn't mean that much extra code.
- The modified code throws a warning 60993 in static const sqlite3_api_routines sqlite3Apis = { ...
but it doesn't seem to matter.
- I needed to compile some of my own code up using CPP enhancements over
"C" but the MSVC 2008 compiler defaults to "C". Since it excepts
comments and uses C++ commenting I can't fathom this. Nor could I find a useful #pragma
which could enable CPP for a single module. Its either global or nothing which means that
linked "C" code would probably break. In the end I renamed my SQLite-VB.C file
as SQLite-VB.CPP which resolved the problem. A pragma would have been far better.
- I ran into the old problem of the missing CHM help files for listview
objects. I'm sure I found them once upon a time but far too often doing stuff with
ListViews is guesswork due to poor, or in this case, absent, documentation.
- Re-include blocks need to be added to prevent accidental re-inclusion
e.g:
#ifndef SQLITE_VB_H
#define SQLITE_VB_H
#ifndef SQLITE_VB_C
#define SQLITE_VB_C |
- Finally, the returned object is not a SAFEARRAY but a
VARIANT (not a VARIANT far* either) which means that I have to create a VARIANT instance
within the function and assign the correct descriptors to it before returning the whole
object via the function-body. Fingers firmly-crossed that VB destroys it properly.
Page top
How It's Done - Modifying the SQLite Code and Adding New
Code
Most of the Tannertech and Persistent Realities tutorial steps are valid but my
specific needs for VB and some other concerns I had meant I deviated a fair bit with some
of the function specs.
My changed declares are as follows (These need to be changed in SQLite3.C and
SQLite3.H and in the function pointer struct mentioned above)
AFAIK _stdcall is the same as __stdcall
(1, versus 2 underscore prefixes)
const char* _stdcall sqlite3_libversion(void);
const int _stdcall sqlite3_libversion_number(void);
int _stdcall sqlite3_close(sqlite3 *);int _stdcall sqlite3_open(const
char *filename, /*
Database filename (UTF-8) */
sqlite3 **ppDb /* OUT: SQLite db handle */
);
int _stdcall sqlite3_open16(const void *filename, /* Database filename (UTF-16) */
sqlite3 **ppDb /* OUT: SQLite db handle */
);
SQLITE_API int _stdcall sqlite3_open_v2(const char *filename, /* Database filename (UTF-8) */
sqlite3 **ppDb, /* OUT: SQLite db handle */
int flags, /* Flags */
const char *zVfs /* Name of VFS
module to use */
); |
In SQLite-VB.H I added the following, although I've left sqlite_get_table
to self-prototype (I don't believe in pointless prototyping)
#ifndef SQLITE_VB_H
#define SQLITE_VB_H
#include <windows.h>
#include <stdio.h>
#include <io.h>
#include <oleauto.h>
#include <wtypes.h>
#include "sqlite3.h"//#define
VB_SQLITE_VERSION "3.0.0"
//VARIANT __stdcall sqlite_get_table(sqlite3 * , const char *, BSTR * );
//SAFEARRAY * __stdcall sqlite_get_table(sqlite3 * , const char *, BSTR * );
// Other functions self-prototype in
SQLITE-VB.CPP
LPCSTR __stdcall sqlite_libversion(void);
const int __stdcall sqlite_rowcount(void);
#endif |
The SQLite-VB.C (or CPP depending on your compiler needs) file needs to have
some substantial code added to create the VARIANT matrix, some helper VARIANT functions
and the modifications recommended in the other tutorials such as...
#ifndef SQLITE_VB_C
#define SQLITE_VB_C#include <windows.h>
#include <stdio.h>
#include <io.h>
#include <oleauto.h>
#include <wtypes.h>
#include "sqlite3.h"
#include "sqlite-vb.h"
#ifdef UNICODE
#pragma message( "UNICODE is set: " __FILE__ )
#else
#pragma message( "UNICODE is NOT set: " __FILE__ )
#endif
#undef UNICODE
#undef _UNICODE
SQLITE_API const DWORD _stdcall
sqlite3_libversion_number(void)
{
return (const
DWORD)SQLITE_VERSION_NUMBER;
}
// Various message() trace functions (omitted for clarity)
long _stdcall GetArrayRows(const VARIANT FAR* v)
{
// implementation omitted for clarity
}
long _stdcall GetArrayDimensions(const VARIANT FAR* v )
{
// implementation omitted for clarity
}
const int _stdcall sqlite_rowcount(void)
{
return (const int)NumberofRowsReturned;
}
VARIANT _stdcall
sqlite_get_table(
sqlite3* db, // The database on which the SQL executes
LPCSTR pszSql, // The SQL to be executed
LPSTR ErrMsg) // Write error messages here change to BSTR
{
// implementation omitted for clarity
}
LPCSTR __stdcall
sqlite3_libversion() // BSTR is not reqd VB will
cast to ANSI
{ // Return the SQLite version string - yep, it works!
return (LPCSTR)SysAllocStringByteLen(
SQLITE_VERSION,strlen(SQLITE_VERSION));
}
#endif |
A few more changes were made to the demo code. I completely
rewrote the listview generation code and tidied up the interface a little bit as well as
adding the ability to generate a demo database, use the clipboard and a few other
features. The listview will automatically-resize if you change the size of the form and
the columns auto-resize to match the input data.
Page top
Demonstration DataFiles
Example Files
So few demo programs like this come with a
datafile you can sensibly query so I knocked up a small music CD database using
information from Wikipedia. You can download the DB and an SQL query file from here (ZIP format).
You can view the SQL file here
The Tannertech site offers a small
one with their own, older SQLite v3.4.2 DLL project.
Demo SQL Files
WARNING: The demo SQL file DROPS both demo tables in the current database without prompting
(the database in the filename input area). These tables are called Albums and CDs. * NEVER RUN ANY SQL FILE WITHOUT READING THE
SQL SCRIPT FULLY!!! *
I added the ability to load an SQL script file from within
the SQLite-VB interface. This affords the ability to have REMarks in the
file which are not loaded into the query window. Please, always read the raw SQL file
using Notepad. SQLite uses two hyphens "--" as a remark. REM is
not part of the SQLite syntax.
All of the examples from Nikolai Shokhirev's SQL tutorial
pages should be useable on this test database including complex JOINs etc.
Create the tables using (you can use both consecutively
separated by a semi-colon) ...
CREATE TABLE IF NOT EXISTS Artists (ArtistID INTEGER PRIMARY KEY,
ArtistName TEXT);
CREATE TABLE IF NOT EXISTS CDs (CDID INTEGER PRIMARY KEY, ArtistID INTEGER NOT NULL, Title
TEXT NOT NULL, Date TEXT); |
Insert records using say...
INSERT INTO Artists (ArtistID,ArtistName) values (NULL,'Puddle of
Mudd');
INSERT INTO Artists (ArtistID,ArtistName) values (NULL,'Frank Zappa');INSERT
INTO CDs (CDID,ArtistID,Title,Date) values (NULL,1,'Come Clean','2001');
INSERT INTO CDs (CDID,ArtistID,Title,Date) values (NULL,2,'Weasels Ripped My
Flesh','1970'); |
Query the database using ...
SELECT t1.ArtistName,CDs.Title,CDs.date
FROM Artists t1, CDs
WHERE t1.ArtistID=CDs.ArtistID
ORDER BY ArtistName |
Or, to match an exact Artist Name ...
-- SQL - List the best Rush albums
SELECT t1.ArtistName,CDs.Title,CDs.date
FROM Artists t1, CDs
WHERE t1.ArtistID=CDs.ArtistID AND ArtistName='Rush' AND CDs.date<1990
ORDER BY Title |
If you want to see if SQLite will "fall-over" try
this (it returns over 5,000 rows) ...
(AFAIK it's called the
Cartesian Product)
SELECT t1.ArtistName,CDs.Title,CDs.date
FROM Artists t1, CDs
ORDER BY Title |
Finally, drop the tables (not other tables in the same
database file) when you have finished using ...
DROP TABLE IF EXISTS artists;
DROP TABLE IF EXISTS CDs; |
You can also "wrap" the entire transaction in the
following for vastly-improved speed ...
BEGIN; -- SQL commands...
COMMIT; |
If you want to play with the application to any greater
extent you will need to refer to the SQLite
variant of SQL.
You can create and drop tables as well as insert and query
data. Unfortunately I'm not an SQL expert so you will have to excuse any poor SQL demo
which accompanies the demo program.The small CD Music database has about 50 records.
SQLite has no SHOW TABLES command and one
needs to use .tables at the SQLITE> prompt. However it can be emulated using
SELECT name FROM sqlite_master WHERE type = 'table'
or ...
SELECT * FROM sqlite_master
Here are a few test-queries for the Tannertech database
should you care to use it: (MyPrograms6.db)
- EXPLAIN SELECT * from files
- SELECT * FROM files where xbedescription like '%ake%'
- SELECT * FROM files where isize >2600000000 and idfile
<30
- SELECT * FROM files where strfilename like '%quake%'
- SELECT * FROM files where strfilename like '%apps%'
- SELECT idfile,strfilename FROM files where strfilename
like '%apps%'
- SELECT idfile,strfilename FROM files where strfilename
NOT like '%apps%'
- SELECT * FROM files where strfilename NOT like '%apps%'
AND itimesplayed>0
|
Page top
Virus-Scanners and SQLite
Performance Problems
Also, I found that adding data natively in Windows XP was
extremely slow. Many seconds, possibly a minute, just to add 50 records. This might be due
to the method of structuring the query or perhaps may be a side-effect of the interface.
Bizarrely, I found that creating and inputting the test (Albums.db) data set was almost
instantaneous on Windows NT running under QEMU 8 in XP/SP2 as well - so its either
specific to the file-system, CPU (emulated on QEMU), virus-scanner or operating system on
XP.
Looking up further inserts can be speeded up massively by
using a BEGIN; ... COMMIT; transaction-wrapper but NT is still about 10
times faster so this doesn't explain why a PC emulator running a 12 year old operating
system is faster than native XP on a 1800Ghz CPU!. NT wasn't running a virus-scanner
though so I thought I'd do some checks also with the AVG-Professional
7.5 resident shield disabled.
The results are most interesting - it appears to be the
virus-scanner at fault... Turning off scanning of files with no filename-extension
dramatically improved performance.
SQLite also creates a file called <databasename>.db-journal. I excluded *.db
and *.db-journal but it had no effect so I probably had the syntax wrong.
Excluding db-journal,db seemed to have an effect.
Times |
Windows NT/Emulated PC (QEMU)
No virus-scanner installed |
XP Professional SP2/1800Ghz CPU
AVG 7.5 Professional |
Drop Demo |
1 second |
0.719 |
Query Demo |
0.250 seconds |
0.470 seconds
0.016 seconds |
Create new tables and insert demo data
Approx. 45 records and 2 tablesUsing either the demonstration app
menu
create/fill or supplied ALBUMS.SQL script |
0.025 seconds (atomic commit)
0.313 seconds (BEGIN..COMMIT)No speed issues |
Worst
21.0 seconds (atomic commit - menu create/fill)
40.0+ seconds (full atomic commit - menu create/fill)
Best
0.650 best with AVG disabled - supplied example transactional SQL script
1.250 average with AVG resident shield disabled (menu DB create/fill)
4.125 with AVG resident shield enabled - (menu DB create/fill)
0.79 seconds (using demo ALBUMS.SQL with BEGIN...COMMIT
4.5 seconds (BEGIN..COMMIT) (VC9 compiled DLL)
3.734 seconds (BEGIN..COMMIT) (VC5 compiled DLL) |
Performance - Atomic Transactions
I also expected to be able to run a DROP TABLE followed by
CREATE TABLE, INSERT and query all in one file but that didn't seem to work, then I
discovered a thing called "atomic
commit transactions" and a BEGIN;... COMMIT wrapper.. You need
to run DROP TABLE separate from CREATE TABLE and queries separate to both of those unless
you use transaction-wrappers.
Can SQLite and the VBSQLite DLL - or Demo Goes
"Bang"?
Here's a screenshot of a cross-join on the "SQLSpy"
demo "world.db3" database. The cross-join resulted in just under a million rows
(974,881 rows) being returned and the query consumed around 1.5Gb (all of my XP system
RAM!). The VM took around 1.5Gb and CPU maxed-out at about 90% during accumulation of the
results during which RAM was being consumed at about 1Mb/sec. .The query took 23 minutes
and I had to shut down most of my apps. I also suspect AVG 7.5 resident shield may have
been scanning the DB3 file. Yet, VBSQLite didn't fall over. Note that you can check the
count using SELECT COUNT (*) from city,country
Page top
Demonstration Application
All Versions of Windows
The demo app uses a ListView and CommonDialog control. You
need to have Microsoft Common Controls (MSCOMCTL32.OCX) and Common Dialog (COMDLG32.OCX)
installed on your system. Usually in the \WINDOWS\SYSTEM32 folder. There
is no setup program as yet - just the DLL and test EXE.
Copy any OCXs you need to your Windows\System32\ folder
then register them on your system using REGSVR32.EXE <filename>.OCX
The EXE and DLL files are UPX-compressed. If you suspect
problems then uncompress them using UPX -d <filename>
Windows
XP and Vista You will also need to have MSVCR90.DLL
etc. on your system (MSVC++ runtime 9.0) as the DLL was compiled with MSVC 9.0
Download
the optimal VC++ 9.0 version of VBSQLite.DLL and test program (207Kb ZIP) |
<No image> |
Windows
NT 4.0 and Windows 2000 MSVC++ runtime 9.0
is most probably not going to be installed (or supported?) for NT/2K so I have compiled a
separate version of the DLL using MSVC 5.0 (Visual Studio 97)
If like me you are a fan of Windows NT for use as a
portable O/S then you may be interested.
I use NT4.0/SP6a with QEMU as my portable O/S in preference to Linux so I have compiled
this version for my own use.
Error 53: Not found <dllname> where
the DLL is present in the PATH means that one of the DLL's dependencies rather than the
DLL itself are missing. You can check this using a copy of Dependency Walker.
Image: SQLite VB 1.x and test app
running on Windows NT4.0/SP6a (opposite) - click for larger image.
Download
the legacy-compatible VC++ 5.0 version of VBSQLite.DLL and test program
(220Kb ZIP) |
|
Visual BASIC Declarations
The following essential export declarations apply to DLL VBSQlite3.dll
release v1.04 - sqlite_get_table() or sqlite_exec()
are the main functions you need to use.
REM SQLite functions //
Private Declare Function sqlite3_libversion Lib "vbsqlite3.dll" () _
As String
Private Declare Function sqlite3_libversion_number Lib _
"vbsqlite3.dll" () As Long
Public Declare Function sqlite3_open Lib "vbsqlite3.dll" ( _
ByVal FileName As String, _
ByRef DB_Handle As Long) As Integer
Public Declare Function sqlite3_open_v2 Lib "vbsqlite3.dll" ( _
ByVal FileName As String, _
ByRef DB_Handle As Long, ByVal Flags As Integer, _
Optional ByVal zVfs As String = vbNullString) As Integer
Public Declare Function sqlite3_close Lib "vbsqlite3.dll" ( _
ByRef DB_Handle As Long) As Integer
Private Declare Function sqlite3_last_insert_rowid Lib _
"vbsqlite3.dll" (ByVal DB_Handle As Long) As Long
Private Declare Function sqlite3_changes Lib "vbsqlite3.dll" _
(ByVal DB_Handle As Long) As Long
Private Declare Function sqlite_get_table Lib "vbsqlite3.dll" ( _
ByVal DB_Handle As Long, _
ByVal SQLString As String, _
ByRef SQLErrorString As String, _
ByRef SQLReturnCode As Integer) As Variant
REM Must Call SQLString USing StrPtr(SQLString & vbNullChar)
Public Declare Function sqlite_get_table_unicode Lib _
"vbsqlite3.dll" _
(ByVal DB_Handle As Long, _
ByVal SQLString As Long, _
ByRef SQLErrorString As String, _
ByRef SQLReturnCode As Integer) As Variant
Public Declare Sub sqlite_progress_handler Lib "vbsqlite3.dll" ( _
ByVal DB_Handle As Long, _
ByVal OpcodeCount As Integer, _
ByVal pCallbackFunct As Any)
REM sqlite_progress_handler() must Call a Function prototyped As:
REM Public Function SQLiteProgressCallback() As Integer
Public Declare Function sqlite_exec Lib "vbsqlite3.dll" ( _
ByVal DB_Handle As Long, _
ByVal SQLString As String, _
ByVal pCallbackFunct As Any, _
ByRef pArgument As Any, _
ByRef SQLErrorString As String) As Integer
REM Must Call SQLString USing StrPtr(SQLString & vbNullChar)
Public Declare Function sqlite_exec_unicode Lib "vbsqlite3.dll" ( _
ByVal DB_Handle As Long, _
ByVal SQLString As Long, _
ByVal pCallbackFunct As Any, _
ByRef pArgument As Any, _
ByRef SQLErrorString As String) As Integer
REM sqlite_exec() must Call a VB Function prototyped As:
REM Function ExecCallback(pArgument As Any, ByRef Cols As Integer, _
REM ByRef sValues() As String, ByRef sNames() As String) _
REM As Integer
Private Declare Function sqlite_rowcount Lib "vbsqlite3.dll" () _
As Long
Public Declare Function sqlite_exec_rowcount Lib _
"vbsqlite3.dll" () As Long
REM Does Not trigger switch For exec_query_unicode etc.
REM Affects only the resulting data.
REM You can Call a non-Unicode query With Unicode results
Public Declare Sub SetUnicodeConversion Lib "vbsqlite3.dll"
(ByVal BoolState As Boolean)
REM Toolbox VB5+ functions //
Private Declare Function GetArrayDimensions Lib "vbsqlite3.dll" _
(ByRef V As Variant) As Long
Private Declare Function GetArrayRows Lib "vbsqlite3.dll" _
(ByRef V As Variant) As Long
REM DLL Version control //
Private Declare Function VBSQLLibTimeStamp Lib "vbsqlite3.dll" () _
As String
Private Declare Function VBSQLLibVersion Lib "vbsqlite3.dll" () _
As Integer
Private Declare Function MSVCVersion Lib "vbsqlite3.dll" () _
As Integer
|
Object calling or database-querying within VB is as simple
as this...
Dim V As Variant
Dim i As Integer
Dim DB As Long
Dim ErrorString As String
Dim ReturnCode As Integer
Dim QueryString As String
QueryString="Select * from files"
sqlite3_open DBFile, DB
If DB>0 Then
V= sqlite_get_table(DB, QueryString, ErrorString,ReturnCode)
If Not IsEmpty(V) Then
Debug.Print "Table has "; Ubound(V); " column(s)"
Debug.Print "Table is of type "; TypeName(V)
Debug.Print "Table has "; GetArrayDimensions(V); " dimensions - should be 2"
Debug.Print "Table has "; GetArrayRows(V); " row(s)" ' +1 higher than rowcount
Debug.Print "Query returned "; sqlite_query_rowcount(); " row(s)"
For i=LBound(V) To UBound(V)
Debug.Print "Header row:"; i; "=" V(i,0) ' Array Is addressed As Column,Row
Next
If GetArrayRows(V)>1 Then
' // Address and use the data
Endif
Endif
Endif
|
Download the Quick-Demo
project to try a very simple VB query (ZIP format) - view the brief source code here
Page top
Programming Guidelines
- You should never access the Variant returned from sqlite_get_table()
unless it has been checked using IsEmpty()
- The returned Variant is addressed in Column/Row order
V(c,r). This is a necessity of being able to use UBound() and LBound()
on array zero
- The column and row array offsets are base 0 with row 0 being
the header row and data starting at row #1
- Note that the table rowcount will always be +1 higher than
the returned number of SQL rows (since it contains the table headers)
- You should not attempt to access the table data unless the
table rowcount has been checked and is >0 (or if the array rowcount is
>1)
- You cannot iterate through the returned Variant using a For
Each ... Next loop. It is not a collection of Variants but a 2D string-array held
in a Variant
- Although most SQLite functions are exported none of the
others listed in the DEF file have been tested under VB5
- Doing data-conversion from string to numeric values by
fetching data from a ListView will be a real pain. ListViews are a pain anyway
If you aren't comfortable with the data being held in a Variant or have problems with
re-entrancy or data-overwriting then you may be better-off fetching the data into a local
2D string-array and writing a function which takes a string array matrix and populates
your ListView. That way you can fetch the data from the String array and address it
directly for any maths and conversions you need to do.
- Where callback functions are required do NOT change the
signature of the called-back VB function or your program will crash
I intend to do a little more testing then release the
compiled DLL for public use - with no warranties obviously and I can't guarantee that
there are no bugs left in the code or regressions due to changing the SQLite code. The
resulting DLL should be useable not only in VB5 but other versions of VB and other
dialects of BASIC.
Page top
Handling Unicode and UTF-8
SQLite uses
UTF-8 internally (with the ability to use UTF-16), whereas Windows has, for some years
now, used UTF-16 or "Unicode" as a multi-byte character set default. That is,
unlike ANSI and ASCII each character is represented with more than one byte. There may be
some confusion as Microsoft tend to refer to single-character mapped character sets as
"multibyte" and those represented by more than one character as
"wide". The term is applied as the normal 8-bit character is represented by a
"wider" 16-bit character.
Although UTF-8 can represent a very wide range of
international character-sets it can be used for most everyday purposes by those in the
West freely intermingled with ANSI. The only problem someone in the UK or US might get is
with the occasional accented characters. Anyone using a non-English character set will
probably not be able to make much use of an SQLite interface unless UTF-8 or UTF-16 is
supported.
The problem with Visual BASIC is that, whilst it stores
strings internally as Unicode it automatically translates into ANSI and back into Unicode
when calling DLLs. This means that special arrangements have to be made in order to pass a
UTF-8 encoded string to a DLL so that we can query using non-ANSI characters such as
accents. Also, while SQLite returns nicely-encoded UTF-8 data in it's results this is lost
when VB strips the data back into ANSI at the end of it's return-journey unless
arrangements are made within the DLL to handle this. From v1.04 this DLL supports Unicode
or "wide" character strings in the form of UTF-8.
The drawback to Unicode is that each character takes two
bytes (two ANSI characters/16-bits) to represent. This may not seem much of a deal until
you return say half-a-million records in your results wanting twice the memory space you
have available. SQL can be very greedy for memory. Since ANSI is far more lightweight on
memory use the default for the library is for Unicode support to be disabled by default.
You can turn it on by calling SetUnicodeConversion() with a value of True
or False to enable/disable Unicode results. This only affects returned data though; if you
want to send a Unicode (UTF-8) query with special characters to SQLite then use one of the
two Unicode variant searches, either sqlite_get_table_unicode() or sqlite_exec_unicode().
Unless you have good reasons for using the Unicode versions and cannot do without them
then then you may be able to make more efficient use of memory by using the standard
VBSqlite functions.
You should also be aware that if calling one of the *
_unicode() search functions that since you will be forcing VB to send a Unicode
string that it will not bother to null-terminate the string so you will need to do this so
the DLL knows how long the string it receives is. You should therefore call the enhanced
Unicode search functions like this...
Dim UseUnicode as Boolean
UseUnicode=True
SetUnicodeConversion UseUnicode
' ... more code ...
If UseUnicode Then
QueryStr = QueryStr & vbNullChar
' We MUST add a terminating NULL char to process as Unicode
ReturnCode = sqlite_exec_unicode(DB, StrPtr(QueryStr), AddressOf
SQLCallbackMain, 0, ErrorString)
Else
ReturnCode = sqlite_exec(DB, QueryStr, AddressOf SQLCallbackMain, 0, ErrorString)
End If |
It is possible to test the functionality using the SQLSpy
"world.db3" demo-database with the following query for a location with an
accented name (São Paulo) ...
SELECT * from city,country
where countrycode='BRA' and city.name like '%São Paulo%' |
If you run the same query in the demo app with Unicode
disabled (unticked) then you should get no results returned.
Conclusion
Although fairly painful to convert the resulting code seems
to work nicely; although having everything returned as a string may be a pain it is
exactly the same end-result as for the previous tutorials. Other SQLite functions may
possibly be used to determine the type of each column and convert as required. Conversion
from Variant String is pretty simple. The result here is a modest test program and a
useful DLL.
During long queries, taking perhaps 10 minutes or more,
some callback interface is required to provide a progress-bar or other reassurance to the
user.
It is clear that returning the entire query-result in a
single variant is a memory-greedy method. Where this is transferred into say a ListView or
other display/handling object then there will, at some point, be three copies of that
table held in memory. One by SQLite, the second in the returned Variant and the third in
the display object. This is of no consequence for small queries but for those returning
maybe a million rows it can prove to be a problem. The alternative is to query SQLite then
fetch the rows into a Variant a row-at-a-time and place into the query object before
(optionally) deleting the Variant. This should reduce the memory footprint by 30%. A test
"cross-join" showed the memory used in a small table to be about 750Mb so a
ballpark guess is a reduction of say 250Mb on a query of this size.
I noted that adding items to the listview using sqlite_exec()
used large amounts of Virtual Memory but little physical memory until the search was
terminated. At that point most of the VM was transferred and physical memory was swallowed
up. A 300,000 row cross-join query on the world.db3 file used 1.2Gb of VM which was then
transferred into physical memory. Fortunately I had 1.5Gb installed so I could limp along
and terminate the app. I added the ability to clear the ListView object and doing this
then showed used memory to fall right back down to 35Mb so I'm confident that this is an
indictment of the greediness of ListView objects for storage rather than the symptom of a
memory-leak in the sqlite_exec() callback code. The moral is, if you are storing any more
than a few thousand rows it may be more sensible to write out the results to a temporary
file or another database. Although I confess I am no SQL expert I can see that cross-joins
(or Cartesian products) are excellent for trying to stress a DB application to death.
There is huge potential for the use of the SQLite engine
and some of the the VBToolbox console/CGI
features to quickly generate server back-end CGI applications. Remember that you can also
link to PHP and PHP databases since PHP already handles SQLite very well. You may also be
able to port data between MS-Access and SQLite.
Finally, SQLite is a powerful, incredibly compact and fast
database to use with Visual BASIC (once any virus-scanner issues are resolved)
Using SQLite 2 Databases With This DLL
You can export your SQLite 2 database into an SQL backup
file and should be able to re-import the file back into a database for testing purposes up
to the size limit of the text box input area which is coded to workaround the artificial
64Kb Visual BASIC limit . Better still use the command-line EXE downloadable from
SQLite.org
sqlite2.exe path\to\oldfile.db .dump > backupfile
sqlite3.exe path\to\newfile.db < backupfile |
Other SQLite Add-Ons
Firefox has a very nice SQLite database-manager add on (XPI) available from here
SQLSpy
is also extremely good and you may want to look at SQLite Code Factory
Page top
Updates and News
See above for the latest VB
declares
- v1.04 - 28/07/2008 - Added Unicode query
support via sqlite_get_table_unicode() and sqlite_exec_unicode()
Please note that both of these methods require the use of StrPtr() rather than converting
the query to a byte-array
Also added the Quick-Demo project - a
very simple VB5 project including source, EXE, DLL and example database
- v1.03 - 26/07/2008 - Demo app version
update - adds callbacks to sqlite_get_table, cached updates to sqlite_exec. Added limits
to number of records displayed in the listview with option to export the whole data set as
a CSV file if larger. Now works fine with up to a million rows. Remember this is only a
demo/test app. Added Unicode (UTF-16) support to the DLL with configurable Unicode support
on the test app.
- v1.02 - 24/07/2008 - (Pending release)
Added sqlite_exec() which calls sqlite3_exec with a callback interface to a VB function of
your own choosing
The SQLite/VB callback function can cancel the transaction, which may be useful should you
inadvertently submit a silly query
- v1.01 - 22/07/2008 - A few bugfixes. Added
code to report more version information. VC9 and VC5 versions to support NT/2K
Legacy Windows NT/2K support
- v1.00 - 21/07/2008 - First version offering
sqlite_get_data rewritten from inspiration by other code to return a Variant() array
Very fast but no ability to cancel a transaction which can be dangerous if you submit one
which uses all your system memory
Things Left To Do
Update: April 2010 - This updates will
probably never get done now as there has been no general interest in the project, nor have
I found a use for the module with MicroApache.
It was interesting to do at the time but my time is now swallowed up with other things.
- Persuade someone to do some in-the-field testing -
especially for memory-leaks
- Functions to find the data type for each SQL column and map
to VB types for conversion
- Functions to detect what tables are in the DB etc.
- Rather than editing the SQLite source, in future I will
rewrite the lot as a set of wrapper-functions
- Add bugfixes and other essential functions exported from
SQLite
- Document the functionality of each exported function
- Provide the ability to save/export data in the demo
application so it may be of some practical use
- DONE Implement sqlite_cancel()
to cancel "apparently hung" queries
Page top
Copyright Information
This page and software written by the page owner, unless
otherwise stated, is Copyright (c) M Shaw 2008-2021 but released as freeware
SQLite is in the Public Domain under free licence, and is
the product of D Richard Hipp and his team at Hwacl Applied
Software Research
The example source-code in this page was highlighted statically using CGIHighlight HTML source code highlighter
|