524 lines
18 KiB
Text
524 lines
18 KiB
Text
;; @module sqlite3.lsp
|
|
;; @description SQLite3 database interface routines
|
|
;; @version 1.6 - comments redone for automatic documentation
|
|
;; @version 1.7 - D.C. fixed getting types when null values are present
|
|
;; @version 1.8 - D.C. made 64-Bit integers work
|
|
;; @version 1.9 - new library detection routine
|
|
;; @version 2.0 - added documentation for close
|
|
;; @version 2.1 - use default functor for query
|
|
;; @version 2.2 - detection for NEWLISP64, lib path for OpenBSD, tested for 64-bit
|
|
;; @version 2.3 - C.H. added parameter binding for safer SQL (guard against SQL-injection)
|
|
;; @version 2.4 - doc changes
|
|
;; @version 2.5 - changed sqlite3_bind_blob to sqlite3_bind_text in function bind-parameter
|
|
;; @version 2.61 - added function <tt>sql3:colnames</tt>.
|
|
;; @version 2.7 - changed deprecated <tt>name</tt> to <tt>term</tt>, <tt>inc</tt> to <tt>++</tt>
|
|
;; @version 2.71 - minor doc changes
|
|
;; @version 2.72 - add support for CentOS 6 Linux 64Bit
|
|
;; @version 2.73 - doc additions
|
|
;; @version 2.83- added sqlite3 library path for UBUNTU 12.04/10 64-bit and others
|
|
;; @author Lutz Mueller 2004-2013, Dmitri Cherniak 2007, Clemens Hintze 2009
|
|
;;
|
|
;; <h2>Module for SQLite3 database bindings</h2>
|
|
;; To use this module include the following 'load' or 'module' statement at the
|
|
;; beginning of the program file:
|
|
;; <pre>
|
|
;; (load "/usr/share/newlisp/modules/sqlite3.lsp")
|
|
;; ; or shorter
|
|
;; (module "sqlite3.lsp")
|
|
;; </pre>
|
|
;; Test the module:
|
|
;; <pre>
|
|
;; (test-sqlite3)
|
|
;; </pre>
|
|
;; This function, located at the and of the module file, exercises
|
|
;; most of the functions.
|
|
;;
|
|
;; SQLite version 3.0 introduced a new database format and is incompatible
|
|
;; whith the previous 2.1 to 2.8 format. Old SQLite 2.x based databases can
|
|
;; be converted using the old and new sqlite client application:
|
|
;;
|
|
;; sqlite OLD.DB .dump | sqlite3 NEW.DB
|
|
;;
|
|
;; While in sqlite 2.8 all returned fields where of string type, SQLite3
|
|
;; returns, text, integer or float. Blobs are returned as text and NULLs
|
|
;; are returned as nil.
|
|
;;
|
|
;; See also the documentation at @link http://sqlite.org sqlite.org
|
|
;;
|
|
;; <h2>Requirements:</h2>
|
|
;; One of the libraries sqlite3.dll for MS Windows or libsqlite3.so for UNIX like
|
|
;; operating systems is required from http://www.sqlite.org.
|
|
;;
|
|
;; SQLite is an <in-process> database. The library contains the whole database
|
|
;; system. An extra database server is not required. SQLite also has limited
|
|
;; mutiuser capabilities for accessing a common database from several programs
|
|
;; at the same time. See the documentation at @link http://sqlite.org sqlite.org
|
|
;; for details.
|
|
;;
|
|
;; The following is a short example how to use SQLite3:
|
|
;;
|
|
;; @example
|
|
;; (sql3:open "MYDB") ; opens/creates a database returns a handle (ignore)
|
|
;; ; or 'nil' on failure
|
|
;;
|
|
;; (sql3:sql "select * from mytable;") ; make a SQL query, return result
|
|
;; (sql3 "select * from mytable;") ; use default functor as alias
|
|
;;
|
|
;; (sql3:error) ; return error text
|
|
;;
|
|
;; (sql3:close) ; close the database
|
|
|
|
;; Function calls returning 'nil' signal that an error has occurred. The
|
|
;; function 'sql3:error' can then be used to get details about the error
|
|
;; as a text string.
|
|
;;
|
|
;; At the bottom of the source file 'sqlite3.lsp' a test routine called
|
|
;; 'test-sqlite3' can be found to test for correct installation of SQLite.
|
|
|
|
; make this module compatible with version less than 10.1.11
|
|
(when (< (sys-info -2) 10111)
|
|
(constant (global 'term) name))
|
|
|
|
(when (< (sys-info -2) 10110)
|
|
(constant (global '++) inc))
|
|
|
|
(context 'sql3)
|
|
|
|
; fetch-row and keep-type functions depend on this
|
|
(set 'NEWLISP64 (not (zero? (& (sys-info -1) 256))))
|
|
|
|
; set library to path-name of the library on your platform OS
|
|
;
|
|
(set 'files (list
|
|
"/usr/lib/libsqlite3.so" ; SuSE Linux
|
|
"/usr/local/lib/libsqlite3.so" ; Linux, BSD, Solaris
|
|
"/usr/pkg/lib/libsqlite3.so" ; NetBSD
|
|
"/usr/local/lib/libsqlite3.so.13.3" ; OpenBSD 4.6
|
|
"/usr/lib/libsqlite3.0.dylib" ; Mac OSX Darwin
|
|
"/usr/lib64/libsqlite3.so" ; for 64Bit Fedora CentOS 6 Linux
|
|
"/usr/lib/x86_64-linux-gnu/libsqlite3.so" ; for UBUNTU 64-bit
|
|
"/usr/lib/x86_64-linux-gnu/libsqlite3.so.0"
|
|
"/usr/lib/i386-linux-gnu/libsqlite3.so" ; for UBUNTU 32-bit
|
|
"/usr/lib/i386-linux-gnu/libsqlite3.so.0"
|
|
"sqlite3.dll" ; Windows DLL path and current directory
|
|
(string (env "PROGRAMFILES") "/sqlite3/sqlite3.dll") ; Windows SQLite3 std install
|
|
))
|
|
|
|
|
|
(set 'library (files (or
|
|
(find true (map file? files))
|
|
(throw-error "cannot find sqlite3 library"))))
|
|
|
|
(import library "sqlite3_open" "cdecl")
|
|
(import library "sqlite3_close" "cdecl")
|
|
(import library "sqlite3_prepare" "cdecl")
|
|
(import library "sqlite3_bind_blob" "cdecl")
|
|
(import library "sqlite3_bind_double" "cdecl")
|
|
(import library "sqlite3_bind_null" "cdecl")
|
|
(import library "sqlite3_bind_parameter_count" "cdecl")
|
|
(import library "sqlite3_bind_parameter_index" "cdecl")
|
|
(import library "sqlite3_bind_parameter_name" "cdecl")
|
|
(import library "sqlite3_bind_text" "cdecl")
|
|
(import library "sqlite3_bind_text16" "cdecl")
|
|
(import library "sqlite3_step" "cdecl")
|
|
(import library "sqlite3_column_count" "cdecl")
|
|
(import library "sqlite3_column_name" "cdecl")
|
|
(import library "sqlite3_column_type" "cdecl")
|
|
(import library "sqlite3_column_int64" "cdecl")
|
|
(import library "sqlite3_column_double" "cdecl")
|
|
(import library "sqlite3_column_text" "cdecl")
|
|
(import library "sqlite3_column_blob" "cdecl")
|
|
(import library "sqlite3_column_bytes" "cdecl")
|
|
(import library "sqlite3_finalize" "cdecl")
|
|
(import library "sqlite3_get_table" "cdecl")
|
|
(import library "sqlite3_last_insert_rowid" "cdecl")
|
|
(import library "sqlite3_changes" "cdecl")
|
|
(import library "sqlite3_busy_timeout" "cdecl")
|
|
(import library "sqlite3_errmsg" "cdecl")
|
|
|
|
|
|
; gloablly used vars and constants
|
|
|
|
(define db nil) ; database handle
|
|
(define dbp "\000\000\000\000\000\000\000\000") ; ptr to database handle
|
|
(define error-message nil) ; error message
|
|
(define col-names '()) ; list of column headers
|
|
(define col-types '()) ; list of column types
|
|
(define pstm "\000\000\000\000\000\000\000\000") ; ptr to compiled sql
|
|
|
|
(constant 'SQLITE_OK 0)
|
|
(constant 'SQLITE_ROW 100)
|
|
(constant 'SQLITE_DONE 101)
|
|
|
|
(constant 'SQLITE_TYPES '(
|
|
0
|
|
SQLITE_INTEGER
|
|
SQLITE_FLOAT
|
|
SQLITE_TEXT
|
|
SQLITE_BLOB
|
|
SQLITE_NULL))
|
|
|
|
;; @syntax (sql3:open <str-db-name>)
|
|
;; @param <str-db-name> The name of the database.
|
|
;; @return A database handle (discard), or 'nil' on failure.
|
|
;; Opens or creates a database. If the database does exist it gets opened,
|
|
;; else a new database with the name given is created.
|
|
;; If trying to open a database that already has been opened 'nil' is returned
|
|
;; and an error text can be retrieved using 'sql3:error'.
|
|
|
|
(define (sql3:open db-name)
|
|
; only open if not alrady done
|
|
(if (not db)
|
|
(begin
|
|
(set 'result (sqlite3_open db-name dbp))
|
|
(if (!= result SQLITE_OK)
|
|
(set 'db nil)
|
|
(if NEWLISP64
|
|
(set 'db (get-long dbp))
|
|
(set 'db (get-int dbp)))
|
|
))
|
|
(begin
|
|
(set 'error-message "A database is already open")
|
|
nil))
|
|
)
|
|
|
|
;; @syntax (sql3:close)
|
|
;; @return Returns 'true' on success;
|
|
;; Closes the currently open database.
|
|
|
|
(define (sql3:close) ;; overwrite the close in MAIN
|
|
(if db (begin
|
|
(sqlite3_close db)
|
|
(set 'db nil)
|
|
true)))
|
|
|
|
|
|
;; @syntax (sql3:sql <str-sql> [<sql-args>])
|
|
;; @param <str-sql> The SQL statement.
|
|
;; @param <sql-args> Parameters for the SQL statement's host variables
|
|
;;
|
|
;; Executes the SQL statement in <str-sql>. For 'select' statements a table
|
|
;; of the result set is returned or '()' for the empty set. For other statements
|
|
;; 'true' is returned for a successful outcome. On failure 'nil' is returened
|
|
;; and 'sql3:error' can be used to retrieve the error text.
|
|
;;
|
|
;; If the parameter <sql-args> is given, it has either to be a list of values (if
|
|
;; the SQL statement use the '?' type of host variables) or an association list
|
|
;; whose every association is formed like (<varname> <value>). The <varname> is
|
|
;; the name of the host variable used in the SQL statement e.g. ':name' or '?123'.
|
|
;;
|
|
;; Strings are bound to host variables as BLOBs. That mean the data will be passed
|
|
;; as is, without any further modification.
|
|
;;
|
|
;; Using host variables is much safer than passing those values via string
|
|
;; composition as no SQL quoting problem can occur (SQL injection attack).
|
|
;; For example:
|
|
;;
|
|
;; @example
|
|
;; ; traditional usage
|
|
;; (sql3:sql "select * from persons where age > 18;")
|
|
;;
|
|
;; ; safer usage using SQLite parameter binding
|
|
;; (sql3:sql "select * from persons where age > ?;" '(18))
|
|
;;
|
|
;; ; bind parameters from association lists
|
|
;; (sql3:sql "select * from persons where name like :name;" '((":name" "Do%")))
|
|
;; (sql3:sql "select * from persons where age > :a and name like :n;" '((":n" "Do%") (":a" 18)))
|
|
|
|
|
|
(define (sql sql-str sql-args)
|
|
(set 'result nil 'done nil 'error-message nil)
|
|
(set 'sqarray '());
|
|
(set 'col-names '());
|
|
(set 'col-types '());
|
|
|
|
; set up parameters for sqlite3_prepare() call
|
|
(set 'ppstm "\000\000\000\000\000\000\000\000") ; pointer to statement ptr
|
|
(set 'pptail "\000\000\000\000\000\000\000\000") ; pointer to statement tail
|
|
|
|
; compile the sql statment
|
|
(if db (set 'result (sqlite3_prepare db sql-str -1 ppstm pptail)))
|
|
|
|
; set up parameters for sqlite3_step() call
|
|
(if NEWLISP64
|
|
(set 'pstm (get-long ppstm))
|
|
(set 'pstm (get-int ppstm)))
|
|
|
|
; bind parameters to sql stament if necessary
|
|
(if (and (= result SQLITE_OK) sql-args)
|
|
(let (argi 0)
|
|
(dolist (entry sql-args (!= result SQLITE_OK))
|
|
(if (list? entry)
|
|
(set 'result (bind-parameter pstm (first entry) (last entry)))
|
|
(set 'result (bind-parameter pstm (++ argi) entry))
|
|
)))
|
|
)
|
|
|
|
; execute the compiled statement
|
|
(if (= result SQLITE_OK)
|
|
(while (not done)
|
|
;; execute statement until done/101 or
|
|
(set 'result (sqlite3_step pstm))
|
|
(set 'num-cols (sqlite3_column_count pstm))
|
|
(if (empty? col-names) (set 'col-names (get-names pstm num-cols)))
|
|
(set 'col-types (get-types pstm num-cols))
|
|
(if (= result SQLITE_ROW)
|
|
(push (get-values pstm num-cols) sqarray -1)
|
|
(set 'done true) ;; received done/101 or error
|
|
))
|
|
)
|
|
|
|
; if done/101 finalize
|
|
(if (= result SQLITE_DONE)
|
|
(begin
|
|
(set 'result (sqlite3_finalize pstm))
|
|
; for 'select' statements return the array else 'true'
|
|
(if (> num-cols 0) sqarray true))
|
|
(if (= result 0) true (set-error))))
|
|
|
|
|
|
(define (bind-parameter pstm param value)
|
|
(let (idx param)
|
|
(unless (integer? param)
|
|
(set 'idx (sqlite3_bind_parameter_index pstm
|
|
(if (symbol? param) (term param) (string param)))))
|
|
(cond
|
|
((float? value) (sqlite3_bind_double pstm idx (float value)))
|
|
;((string? value) (sqlite3_bind_blob pstm idx value (length value) -1))
|
|
((string? value) (sqlite3_bind_text pstm idx value (length value) -1))
|
|
((nil? value) (sqlite3_bind_null pstm idx))
|
|
(true (sqlite3_bind_text pstm idx (string value) (length (string value)) -1)) )) )
|
|
|
|
|
|
(define (get-values pstm cols)
|
|
(set 'row '())
|
|
(dotimes (idx cols)
|
|
(set 'i (int idx)) ; all loop vars are float
|
|
(case (nth idx col-types idx)
|
|
; (SQLITE_INTEGER
|
|
; (push (sqlite3_column_int pstm i) row -1))
|
|
; fixed for 64-bit, thanks Dmitry
|
|
(SQLITE_INTEGER
|
|
(set 'pstr (sqlite3_column_text pstm i))
|
|
(if (= pstr 0)
|
|
(push nil row -1)
|
|
(push (int (get-string pstr)) row -1)))
|
|
(SQLITE_FLOAT
|
|
(set 'pstr (sqlite3_column_text pstm i))
|
|
(if (= pstr 0)
|
|
(push nil row -1)
|
|
(push (float (get-string pstr)) row -1)))
|
|
(SQLITE_TEXT
|
|
(set 'pstr (sqlite3_column_text pstm i))
|
|
(if (= pstr 0)
|
|
(push "" row -1)
|
|
(push (get-string pstr) row -1)))
|
|
(SQLITE_BLOB
|
|
(set 'pstr (sqlite3_column_blob pstm i))
|
|
(set 'len (sqlite3_column_bytes pstm i))
|
|
(set 'buff (dup "\000" len))
|
|
(if (= pstr 0)
|
|
(push "" row -1)
|
|
(begin
|
|
(cpymem pstr buff len)
|
|
(push buff row -1))))
|
|
(SQLITE_NULL
|
|
(push nil row -1))))
|
|
row)
|
|
|
|
(define (get-names pstm cols)
|
|
(set 'row '())
|
|
(dotimes (idx cols)
|
|
(set 'i (int idx)) ; all loop vars are float
|
|
(set 'ps (sqlite3_column_name pstm i))
|
|
(if (= ps 0) ;; check for null pointer to result
|
|
(push "" row -1)
|
|
(push (get-string ps) row -1)))
|
|
row)
|
|
|
|
(define (get-types pstm cols)
|
|
(set 'row '())
|
|
(dotimes (idx cols)
|
|
(set 'i (int idx)) ; all loop vars are float
|
|
(push (nth (sqlite3_column_type pstm i) SQLITE_TYPES) row -1))
|
|
row)
|
|
|
|
(define sql3:sql3 sql)
|
|
|
|
;; @syntax (sql3:colnames)
|
|
;; @return A list of column header names.
|
|
;; Returns a list of column header names for the last query. This is
|
|
;; a function wrapper around the internal variable <tt>sql3:col-names</tt>.
|
|
|
|
(define (colnames) col-names)
|
|
|
|
|
|
;; @syntax (sql3:rowid)
|
|
;; @return The last row id from last 'insert'.
|
|
;; Gets the id of the last row inserted.
|
|
|
|
(define (rowid)
|
|
(if db (sqlite3_last_insert_rowid db)))
|
|
|
|
;; @syntax (sql3:tables)
|
|
;; @return A list of tables in the database.
|
|
|
|
(define (tables)
|
|
(if db (begin
|
|
(set 'lst (sql "select tbl_name from sqlite_master")) ))
|
|
(if lst (set 'lst (first (transpose lst)))))
|
|
|
|
|
|
;; @syntax (sql3:columns <str-tabel-name>)
|
|
;; @return A list of column names for a table.
|
|
|
|
(define (columns aTable)
|
|
(if (list? (sql (append "select * from " aTable " where 0;")))
|
|
col-names))
|
|
|
|
|
|
;; @syntax (sql3:changes)
|
|
;; @return The Number of rows changed/affected by the last SQL statement.
|
|
|
|
(define (changes)
|
|
(if db (sqlite3_changes db)))
|
|
|
|
|
|
|
|
;; @syntax (sql3:timeout <num-milli-seconds>)
|
|
;; @return 'true' on success or 'nil' on failure.
|
|
;; Sets busy timeout in milliseconds.
|
|
|
|
(define (timeout ms)
|
|
(if db (zero? (sqlite3_busy_timeout db (int ms)))))
|
|
|
|
|
|
|
|
;; @syntax (sql3:error)
|
|
;; @return The error text of the last error occured in 'sql3:sql'.
|
|
|
|
(define (error) error-message)
|
|
|
|
(define (set-error)
|
|
(set 'result (sqlite3_errmsg db))
|
|
(if (= result 0)
|
|
(set 'error-message nil)
|
|
(set 'error-message (get-string result))
|
|
nil
|
|
)
|
|
)
|
|
|
|
|
|
(context 'MAIN)
|
|
|
|
; -------------------------------------------------------------------------
|
|
;
|
|
; test the database routines
|
|
;
|
|
; if there is an old "SQLITE3-TEST" db from an earlier sqlite 2.8 delete it first
|
|
;
|
|
(define (test-sqlite3)
|
|
(if (sql3:open "SQLITE3-TEST")
|
|
(println "database opened/created, ... Ok")
|
|
(println "problem opening/creating database"))
|
|
|
|
(if (sql3:sql "create table fruits (name CHAR(20), qty INT(3), price FLOAT(10), blobtext BLOB);")
|
|
(println "created table fruits, ... Ok")
|
|
(println "problem creating table fruits"))
|
|
|
|
(if (sql3:sql "insert into fruits values ('apples', 11, 1.234, X'41424300010101');")
|
|
(println "inserted, last row id: " (sql3:rowid) ", ... Ok")
|
|
(println "problem inserting row"))
|
|
|
|
(if (sql3:sql "insert into fruits values ('oranges', 22, 2.345, X'42434400020202');")
|
|
(println "inserted, last row id: " (sql3:rowid) ", ... Ok")
|
|
(println "problem inserting row"))
|
|
|
|
(if (sql3:sql "insert into fruits values ('bananas', 33, 3.456, X'44454600030303');")
|
|
(println "inserted, last row id: " (sql3:rowid) ", ... Ok")
|
|
(println "problem inserting row"))
|
|
|
|
; Definition of a small helper function for the tests to emulate the X'...' argument
|
|
; quoting of SQL
|
|
|
|
(define (hexstring hexstr)
|
|
(join (map (fn (s) (pack "c" (int s 0 16))) (find-all ".." hexstr))))
|
|
|
|
; Following statement was modified below to show, how to use host variables with
|
|
; the SQL INSERT statement.
|
|
; (if (sql3:sql "insert into fruits values (:name, :qty, :price, X'47484900040404');"
|
|
; '((":name" "grapes") (":qty" 123456789012345678) (":price" 7,89)))
|
|
; (println "inserted, last row id: " (sql3:rowid) ", ... Ok")
|
|
; (println "problem inserting row"))
|
|
|
|
(if (sql3:sql "insert into fruits values (?, ?, ?, ?);"
|
|
(list "grapes" 123456789012345678 (div 789 100) (hexstring "47484900040404")))
|
|
(println "inserted, last row id: " (sql3:rowid) ", ... Ok")
|
|
(println "problem inserting row: " (sql3:error)))
|
|
|
|
(set 'sqarray (sql3:sql "select * from fruits;"))
|
|
|
|
(if sqarray
|
|
(begin
|
|
(println "selected rows: ")
|
|
(map println sqarray)
|
|
(println "column names with sql3:col-names: ")
|
|
(map println (sql3:colnames))
|
|
(println "... Ok")
|
|
)
|
|
(println "problem with select"))
|
|
|
|
(if (= (sql3:sql "select name from fruits where qty < ? order by name;" '(33))
|
|
'(("apples") ("oranges")))
|
|
(println "select via host parameter (type '?'), ... Ok")
|
|
(println "problem with selecting via host parameters (type '?')"))
|
|
|
|
(if (= (sql3:sql "select name from fruits where qty < :qty order by name;" '((":qty" 33)))
|
|
'(("apples") ("oranges")))
|
|
(println "select via host parameter (type ':VVV'), ... Ok")
|
|
(println "problem with selecting via host parameters (type ':VVV')"))
|
|
|
|
(if (= (sql3:sql "select name from fruits where qty < ?2 order by name;" '(("?2" 33)))
|
|
'(("apples") ("oranges")))
|
|
(println "select via host parameter (type '?NNN'), ... Ok")
|
|
(println "problem with selecting via host parameters (type '?NNN')"))
|
|
|
|
(if (= (sql3:sql "select name from fruits where qty < @par order by name;" '(("@par" 33)))
|
|
'(("apples") ("oranges")))
|
|
(println "select via host parameter (type '@VVV'), ... Ok")
|
|
(println "problem with selecting via host parameters (type '@VVV')"))
|
|
|
|
(if (= (sql3:sql "select name from fruits where qty < $par order by name;" '(("$par" 33)))
|
|
'(("apples") ("oranges")))
|
|
(println "select via host parameter (type '$VVV'), ... Ok")
|
|
(println "problem with selecting via host parameters (type '$VVV')"))
|
|
|
|
|
|
; SQL injection has no chance:
|
|
|
|
(print "try to drop table fruits via SQL injection attack ... ")
|
|
|
|
(if (sql3:sql "select * from fruits where name = ?;" '("''; drop table fruits;"))
|
|
(println "OUCH! Table was dropped via SQL injection!!!")
|
|
(println "no luck, table was safe against SQL injection."))
|
|
|
|
(if (sql3:sql "delete from fruits where 1;")
|
|
(println "deleted, rows affected: " (sql3:changes) ", ... Ok")
|
|
(println "problem deleting rows"))
|
|
|
|
(if (list? (sql3:tables) )
|
|
(println "tables: " (sql3:tables) ", ... Ok")
|
|
(println "problem in sql3:tables"))
|
|
|
|
(if (list? (sql3:columns "fruits") )
|
|
(println "columns: " (sql3:columns "fruits") ", ... Ok")
|
|
(println "problem in sql3:columns"))
|
|
|
|
(if (sql3 "drop table fruits;")
|
|
(println "table fruits dropped, ... Ok")
|
|
(println "problem dropping table fruits"))
|
|
|
|
(sql3:close)
|
|
)
|
|
|
|
; eof ;
|