Index: Setup/verify.lst ================================================================== --- Setup/verify.lst +++ Setup/verify.lst @@ -405,10 +405,11 @@ testlinq/testlinq.2008.csproj testlinq/testlinq.2010.csproj testlinq/testlinq.2012.csproj Tests/ Tests/all.eagle + Tests/authorizer.eagle Tests/backup.eagle Tests/basic.eagle Tests/common.eagle Tests/empty.eagle Tests/installer.eagle Index: System.Data.SQLite/SQLite3.cs ================================================================== --- System.Data.SQLite/SQLite3.cs +++ System.Data.SQLite/SQLite3.cs @@ -2043,10 +2043,15 @@ { SQLiteErrorCode n = UnsafeNativeMethods.sqlite3_rekey(_sql, newPasswordBytes, (newPasswordBytes == null) ? 0 : newPasswordBytes.Length); if (n != SQLiteErrorCode.Ok) throw new SQLiteException(n, GetLastError()); } #endif + + internal override void SetAuthorizerHook(SQLiteAuthorizerCallback func) + { + UnsafeNativeMethods.sqlite3_set_authorizer(_sql, func, IntPtr.Zero); + } internal override void SetUpdateHook(SQLiteUpdateCallback func) { UnsafeNativeMethods.sqlite3_update_hook(_sql, func, IntPtr.Zero); } Index: System.Data.SQLite/SQLiteBase.cs ================================================================== --- System.Data.SQLite/SQLiteBase.cs +++ System.Data.SQLite/SQLiteBase.cs @@ -361,10 +361,11 @@ #if INTEROP_CODEC internal abstract void SetPassword(byte[] passwordBytes); internal abstract void ChangePassword(byte[] newPasswordBytes); #endif + internal abstract void SetAuthorizerHook(SQLiteAuthorizerCallback func); internal abstract void SetUpdateHook(SQLiteUpdateCallback func); internal abstract void SetCommitHook(SQLiteCommitCallback func); internal abstract void SetTraceCallback(SQLiteTraceCallback func); internal abstract void SetRollbackHook(SQLiteRollbackCallback func); internal abstract SQLiteErrorCode SetLogCallback(SQLiteLogCallback func); Index: System.Data.SQLite/SQLiteConnection.cs ================================================================== --- System.Data.SQLite/SQLiteConnection.cs +++ System.Data.SQLite/SQLiteConnection.cs @@ -476,15 +476,17 @@ internal bool _binaryGuid; internal long _version; + private event SQLiteAuthorizerEventHandler _authorizerHandler; private event SQLiteUpdateEventHandler _updateHandler; private event SQLiteCommitHandler _commitHandler; private event SQLiteTraceEventHandler _traceHandler; private event EventHandler _rollbackHandler; + private SQLiteAuthorizerCallback _authorizerCallback; private SQLiteUpdateCallback _updateCallback; private SQLiteCommitCallback _commitCallback; private SQLiteTraceCallback _traceCallback; private SQLiteRollbackCallback _rollbackCallback; #endregion @@ -2222,10 +2224,13 @@ cmd.ExecuteNonQuery(); } } } + if (_authorizerHandler != null) + _sql.SetAuthorizerHook(_authorizerCallback); + if (_commitHandler != null) _sql.SetCommitHook(_commitCallback); if (_updateHandler != null) _sql.SetUpdateHook(_updateCallback); @@ -3935,10 +3940,44 @@ tbl.EndLoadData(); tbl.AcceptChanges(); return tbl; } + + /// + /// This event is raised whenever SQLite encounters an action covered by the + /// authorizer during query preparation. Changing the value of the + /// property will determine if + /// the specific action will be allowed, ignored, or denied. For the entire + /// duration of the event, the associated connection and statement objects + /// must not be modified, either directly or indirectly, by the called code. + /// + public event SQLiteAuthorizerEventHandler Authorize + { + add + { + CheckDisposed(); + + if (_authorizerHandler == null) + { + _authorizerCallback = new SQLiteAuthorizerCallback(AuthorizerCallback); + if (_sql != null) _sql.SetAuthorizerHook(_authorizerCallback); + } + _authorizerHandler += value; + } + remove + { + CheckDisposed(); + + _authorizerHandler -= value; + if (_authorizerHandler == null) + { + if (_sql != null) _sql.SetAuthorizerHook(null); + _authorizerCallback = null; + } + } + } /// /// This event is raised whenever SQLite makes an update/delete/insert into the database on /// this connection. It only applies to the given connection. /// @@ -3965,10 +4004,29 @@ if (_sql != null) _sql.SetUpdateHook(null); _updateCallback = null; } } } + + private SQLiteAuthorizerReturnCode AuthorizerCallback( + IntPtr pUserData, + SQLiteAuthorizerActionCode actionCode, + IntPtr pArgument1, + IntPtr pArgument2, + IntPtr pDatabase, + IntPtr pAuthContext) + { + AuthorizerEventArgs eventArgs = new AuthorizerEventArgs(pUserData, actionCode, + SQLiteBase.UTF8ToString(pArgument1, -1), SQLiteBase.UTF8ToString(pArgument2, -1), + SQLiteBase.UTF8ToString(pDatabase, -1), SQLiteBase.UTF8ToString(pAuthContext, -1), + SQLiteAuthorizerReturnCode.Ok); + + if (_authorizerHandler != null) + _authorizerHandler(this, eventArgs); + + return eventArgs.ReturnCode; + } private void UpdateCallback(IntPtr puser, int type, IntPtr database, IntPtr table, Int64 rowid) { _updateHandler(this, new UpdateEventArgs( SQLiteBase.UTF8ToString(database, -1), @@ -4106,10 +4164,22 @@ Off = 2, } #if !PLATFORM_COMPACTFRAMEWORK [UnmanagedFunctionPointer(CallingConvention.Cdecl)] +#endif + internal delegate SQLiteAuthorizerReturnCode SQLiteAuthorizerCallback( + IntPtr pUserData, + SQLiteAuthorizerActionCode actionCode, + IntPtr pArgument1, + IntPtr pArgument2, + IntPtr pDatabase, + IntPtr pAuthContext + ); + +#if !PLATFORM_COMPACTFRAMEWORK + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] #endif internal delegate void SQLiteUpdateCallback(IntPtr puser, int type, IntPtr database, IntPtr table, Int64 rowid); #if !PLATFORM_COMPACTFRAMEWORK [UnmanagedFunctionPointer(CallingConvention.Cdecl)] @@ -4124,10 +4194,19 @@ #if !PLATFORM_COMPACTFRAMEWORK [UnmanagedFunctionPointer(CallingConvention.Cdecl)] #endif internal delegate void SQLiteRollbackCallback(IntPtr puser); + /// + /// Raised when authorization is required to perform an action contained + /// within a SQL query. + /// + /// The connection performing the action. + /// A that contains the + /// event data. + public delegate void SQLiteAuthorizerEventHandler(object sender, AuthorizerEventArgs e); + /// /// Raised when a transaction is about to be committed. To roll back a transaction, set the /// rollbackTrans boolean value to true. /// /// The connection committing the transaction @@ -4192,10 +4271,127 @@ int remainingPages, int totalPages, bool retry ); #endregion + + /////////////////////////////////////////////////////////////////////////////////////////////// + + /// + /// The data associated with a call into the authorizer. + /// + public class AuthorizerEventArgs : EventArgs + { + /// + /// The user-defined native data associated with this event. Currently, + /// this will always contain the value of . + /// + public readonly IntPtr UserData; + + /// + /// The action code responsible for the current call into the authorizer. + /// + public readonly SQLiteAuthorizerActionCode ActionCode; + + /// + /// The first string argument for the current call into the authorizer. + /// The exact value will vary based on the action code, see the + /// enumeration for possible + /// values. + /// + public readonly string Argument1; + + /// + /// The second string argument for the current call into the authorizer. + /// The exact value will vary based on the action code, see the + /// enumeration for possible + /// values. + /// + public readonly string Argument2; + + /// + /// The database name for the current call into the authorizer, if + /// applicable. + /// + public readonly string Database; + + /// + /// The name of the inner-most trigger or view that is responsible for + /// the access attempt or a null value if this access attempt is directly + /// from top-level SQL code. + /// + public readonly string Context; + + /// + /// The return code for the current call into the authorizer. + /// + public SQLiteAuthorizerReturnCode ReturnCode; + + /////////////////////////////////////////////////////////////////////////////////////////// + + /// + /// Constructs an instance of this class with default property values. + /// + private AuthorizerEventArgs() + { + this.UserData = IntPtr.Zero; + this.ActionCode = SQLiteAuthorizerActionCode.None; + this.Argument1 = null; + this.Argument2 = null; + this.Database = null; + this.Context = null; + this.ReturnCode = SQLiteAuthorizerReturnCode.Ok; + } + + /////////////////////////////////////////////////////////////////////////////////////////// + + /// + /// Constructs an instance of this class with specific property values. + /// + /// + /// The user-defined native data associated with this event. + /// + /// + /// The authorizer action code. + /// + /// + /// The first authorizer argument. + /// + /// + /// The second authorizer argument. + /// + /// + /// The database name, if applicable. + /// + /// + /// The name of the inner-most trigger or view that is responsible for + /// the access attempt or a null value if this access attempt is directly + /// from top-level SQL code. + /// + /// + /// The authorizer return code. + /// + internal AuthorizerEventArgs( + IntPtr pUserData, + SQLiteAuthorizerActionCode actionCode, + string argument1, + string argument2, + string database, + string context, + SQLiteAuthorizerReturnCode returnCode + ) + : this() + { + this.UserData = pUserData; + this.ActionCode = actionCode; + this.Argument1 = argument1; + this.Argument2 = argument2; + this.Database = database; + this.Context = context; + this.ReturnCode = returnCode; + } + } /////////////////////////////////////////////////////////////////////////////////////////////// /// /// Whenever an update event is triggered on a connection, this enum will indicate Index: System.Data.SQLite/SQLiteConvert.cs ================================================================== --- System.Data.SQLite/SQLiteConvert.cs +++ System.Data.SQLite/SQLiteConvert.cs @@ -1500,10 +1500,247 @@ /// Use the default command execution type. Using this value is the same /// as using the value. /// Default = NonQuery /* TODO: Good default? */ } + + /// + /// The action code responsible for the current call into the authorizer. + /// + public enum SQLiteAuthorizerActionCode + { + /// + /// No action is being performed. This value should not be used from + /// external code. + /// + None = -1, + + /// + /// No longer used. + /// + Copy = 0, + + /// + /// An index will be created. The action-specific arguments are the + /// index name and the table name. + /// + /// + CreateIndex = 1, + + /// + /// A table will be created. The action-specific arguments are the + /// table name and a null value. + /// + CreateTable = 2, + + /// + /// A temporary index will be created. The action-specific arguments + /// are the index name and the table name. + /// + CreateTempIndex = 3, + + /// + /// A temporary table will be created. The action-specific arguments + /// are the table name and a null value. + /// + CreateTempTable = 4, + + /// + /// A temporary trigger will be created. The action-specific arguments + /// are the trigger name and the table name. + /// + CreateTempTrigger = 5, + + /// + /// A temporary view will be created. The action-specific arguments are + /// the view name and a null value. + /// + CreateTempView = 6, + + /// + /// A trigger will be created. The action-specific arguments are the + /// trigger name and the table name. + /// + CreateTrigger = 7, + + /// + /// A view will be created. The action-specific arguments are the view + /// name and a null value. + /// + CreateView = 8, + + /// + /// A DELETE statement will be executed. The action-specific arguments + /// are the table name and a null value. + /// + Delete = 9, + + /// + /// An index will be dropped. The action-specific arguments are the + /// index name and the table name. + /// + DropIndex = 10, + + /// + /// A table will be dropped. The action-specific arguments are the tables + /// name and a null value. + /// + DropTable = 11, + + /// + /// A temporary index will be dropped. The action-specific arguments are + /// the index name and the table name. + /// + DropTempIndex = 12, + + /// + /// A temporary table will be dropped. The action-specific arguments are + /// the table name and a null value. + /// + DropTempTable = 13, + + /// + /// A temporary trigger will be dropped. The action-specific arguments + /// are the trigger name and the table name. + /// + DropTempTrigger = 14, + + /// + /// A temporary view will be dropped. The action-specific arguments are + /// the view name and a null value. + /// + DropTempView = 15, + + /// + /// A trigger will be dropped. The action-specific arguments are the + /// trigger name and the table name. + /// + DropTrigger = 16, + + /// + /// A view will be dropped. The action-specific arguments are the view + /// name and a null value. + /// + DropView = 17, + + /// + /// An INSERT statement will be executed. The action-specific arguments + /// are the table name and a null value. + /// + Insert = 18, + + /// + /// A PRAGMA statement will be executed. The action-specific arguments + /// are the name of the PRAGMA and the new value or a null value. + /// + Pragma = 19, + + /// + /// A table column will be read. The action-specific arguments are the + /// table name and the column name. + /// + Read = 20, + + /// + /// A SELECT statement will be executed. The action-specific arguments + /// are both null values. + /// + Select = 21, + + /// + /// A transaction will be started, committed, or rolled back. The + /// action-specific arguments are the name of the operation (BEGIN, + /// COMMIT, or ROLLBACK) and a null value. + /// + Transaction = 22, + + /// + /// An UPDATE statement will be executed. The action-specific arguments + /// are the table name and the column name. + /// + Update = 23, + + /// + /// A database will be attached to the connection. The action-specific + /// arguments are the database file name and a null value. + /// + Attach = 24, + + /// + /// A database will be detached from the connection. The action-specific + /// arguments are the database name and a null value. + /// + Detach = 25, + + /// + /// The schema of a table will be altered. The action-specific arguments + /// are the database name and the table name. + /// + AlterTable = 26, + + /// + /// An index will be deleted and then recreated. The action-specific + /// arguments are the index name and a null value. + /// + Reindex = 27, + + /// + /// A table will be analyzed to gathers statistics about it. The + /// action-specific arguments are the table name and a null value. + /// + Analyze = 28, + + /// + /// A virtual table will be created. The action-specific arguments are + /// the table name and the module name. + /// + CreateVtable = 29, + + /// + /// A virtual table will be dropped. The action-specific arguments are + /// the table name and the module name. + /// + DropVtable = 30, + + /// + /// A SQL function will be called. The action-specific arguments are a + /// null value and the function name. + /// + Function = 31, + + /// + /// A savepoint will be created, released, or rolled back. The + /// action-specific arguments are the name of the operation (BEGIN, + /// RELEASE, or ROLLBACK) and the savepoint name. + /// + Savepoint = 32 + } + + /// + /// The return code for the current call into the authorizer. + /// + public enum SQLiteAuthorizerReturnCode + { + /// + /// The action will be allowed. + /// + Ok = 0, + + /// + /// The overall action will be disallowed and an error message will be + /// returned from the query preparation method. + /// + Deny = 1, + + /// + /// The specific action will be disallowed; however, the overall action + /// will continue. The exact effects of this return code vary depending + /// on the specific action, please refer to the SQLite core library + /// documentation for futher details. + /// + Ignore = 2 + } /// /// Class used internally to determine the datatype of a column in a resultset /// internal sealed class SQLiteType Index: System.Data.SQLite/UnsafeNativeMethods.cs ================================================================== --- System.Data.SQLite/UnsafeNativeMethods.cs +++ System.Data.SQLite/UnsafeNativeMethods.cs @@ -1467,10 +1467,17 @@ [DllImport(SQLITE_DLL)] #endif internal static extern SQLiteErrorCode sqlite3_rekey(IntPtr db, byte[] key, int keylen); #endif +#if !PLATFORM_COMPACTFRAMEWORK + [DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] +#else + [DllImport(SQLITE_DLL)] +#endif + internal static extern IntPtr sqlite3_set_authorizer(IntPtr db, SQLiteAuthorizerCallback func, IntPtr pvUser); + #if !PLATFORM_COMPACTFRAMEWORK [DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] #else [DllImport(SQLITE_DLL)] #endif ADDED Tests/authorizer.eagle Index: Tests/authorizer.eagle ================================================================== --- /dev/null +++ Tests/authorizer.eagle @@ -0,0 +1,160 @@ +############################################################################### +# +# authorizer.eagle -- +# +# Written by Joe Mistachkin. +# Released to the public domain, use at your own risk! +# +############################################################################### + +package require Eagle +package require Eagle.Library +package require Eagle.Test + +runTestPrologue + +############################################################################### + +package require System.Data.SQLite.Test +runSQLiteTestPrologue + +############################################################################### + +runTest {test authorizer-1.1 {SQLiteConnection Authorize event} -setup { + proc onAuthorize { sender e } { + # + # NOTE: Filter out the "noise" by allowing all standard + # events on the "sqlite_*" tables. + # + set noiseActionCodes [list \ + CreateTable CreateIndex Read Insert Update Delete] + + if {[$e ActionCode] in $noiseActionCodes && \ + [string match "sqlite_*" [$e Argument1]]} then { + return + } + + lappend ::data [list \ + [$e UserData] [$e ActionCode] [$e Argument1] \ + [$e Argument2] [$e Database] [$e Context]] + + if {[$e ActionCode] eq "CreateTable" && \ + [$e Argument1] eq "tDeny"} then { + $e ReturnCode Deny + } + } + + setupDb [set fileName authorizer-1.1.db] +} -body { + set connection [getDbConnection] + + set callback onAuthorize + object invoke $connection add_Authorize $callback + + set results [list] + + set sql [list \ + CreateTable {CREATE TABLE t1(x);} \ + CreateIndex {CREATE INDEX i1 ON t1(x);} \ + CreateTrigger {CREATE TRIGGER tr1 BEFORE INSERT ON t1 + BEGIN + SELECT RAISE(IGNORE); + END;} \ + CreateView {CREATE VIEW v1 AS SELECT * FROM t1;} \ + CreateTempTable {CREATE TEMPORARY TABLE t2(x);} \ + CreateTempIndex {CREATE INDEX i2 ON t2(x);} \ + CreateTempTrigger {CREATE TEMPORARY TRIGGER tr2 BEFORE INSERT ON t2 + BEGIN + SELECT RAISE(IGNORE); + END;} \ + CreateTempView {CREATE TEMPORARY VIEW v2 AS SELECT * FROM t2;} \ + Pragma {PRAGMA journal_mode=WAL;} \ + Function {SELECT julianday('now');} \ + Read {SELECT x FROM t1;} \ + Select {SELECT * FROM t1;} \ + Insert {INSERT INTO t1(x) VALUES(1);} \ + Update {UPDATE t1 SET x = x - 1;} \ + Delete {DELETE FROM t1;} \ + AlterTable {ALTER TABLE t1 ADD COLUMN y;} \ + Reindex {REINDEX t1;} \ + Analyze {ANALYZE t1;} \ + DropTempView {DROP VIEW v2;} \ + DropTempTrigger {DROP TRIGGER tr2;} \ + DropTempIndex {DROP INDEX i2;} \ + DropTempTable {DROP TABLE t2;} \ + DropView {DROP VIEW v1;} \ + DropTrigger {DROP TRIGGER tr1;} \ + DropIndex {DROP INDEX i1;} \ + DropTable {DROP TABLE t1;} \ + Transaction {BEGIN; SELECT 0; COMMIT;} \ + Savepoint {SAVEPOINT s1; RELEASE SAVEPOINT s1;} \ + Attach {ATTACH DATABASE ':memory:' AS d1;} \ + Detach {DETACH DATABASE d1;} \ + CreateVtable {CREATE VIRTUAL TABLE t3 USING fts4(x TEXT);} \ + DropVtable {DROP TABLE t3;} \ + CreateTable {CREATE TABLE tDeny(x);}] + + foreach {name value} $sql { + set data [list]; set code [catch {sql execute $db $value} result] + set result [lindex [split [string map [list \r\n \n] $result] \n] 0] + lappend results [list $name $data $code $result] + } + lappend results [isTableInDb tDeny] + + set results +} -cleanup { + catch {object invoke $connection remove_Authorize $callback} + catch {object removecallback $callback} + + cleanupDb $fileName + + freeDbConnection + + unset -nocomplain data result code value name sql results callback \ + connection db fileName + + rename onAuthorize "" +} -constraints \ +{eagle monoBug28 command.sql compile.DATA SQLite System.Data.SQLite} -result \ +{{CreateTable {{0 CreateTable t1 {} main {}}} 0 0} {CreateIndex {{0 CreateIndex\ +i1 t1 main {}} {0 Reindex i1 {} main {}}} 0 0} {CreateTrigger {{0 CreateTrigger\ +tr1 t1 main {}}} 0 0} {CreateView {{0 CreateView v1 {} main {}}} 0 0}\ +{CreateTempTable {{0 CreateTempTable t2 {} temp {}}} 0 0} {CreateTempIndex {{0\ +CreateTempIndex i2 t2 temp {}} {0 Reindex i2 {} temp {}}} 0 0}\ +{CreateTempTrigger {{0 CreateTempTrigger tr2 t2 temp {}}} 0 0} {CreateTempView\ +{{0 CreateTempView v2 {} temp {}}} 0 0} {Pragma {{0 Pragma journal_mode WAL {}\ +{}}} 0 0} {Function {{0 Select {} {} {} {}} {0 Function {} julianday {} {}}} 0\ +0} {Read {{0 Select {} {} {} {}} {0 Read t1 x main {}}} 0 0} {Select {{0 Select\ +{} {} {} {}} {0 Read t1 x main {}}} 0 0} {Insert {{0 Insert t1 {} main {}} {0\ +Select {} {} {} tr1}} 0 0} {Update {{0 Read t1 x main {}} {0 Update t1 x main\ +{}}} 0 0} {Delete {{0 Delete t1 {} main {}}} 0 0} {AlterTable {{0 AlterTable\ +main t1 {} {}} {0 Function {} substr {} {}} {0 Function {} substr {} {}}} 0 0}\ +{Reindex {{0 Reindex i1 {} main {}}} 0 0} {Analyze {{0 Analyze t1 {} main {}}\ +{0 Select {} {} {} {}} {0 Select {} {} {} {}} {0 Function {} count {} {}} {0\ +Select {} {} {} {}}} 0 0} {DropTempView {{0 DropTempView v2 {} temp {}} {0\ +Delete v2 {} temp {}}} 0 0} {DropTempTrigger {{0 DropTempTrigger tr2 t2 temp\ +{}}} 0 0} {DropTempIndex {{0 DropTempIndex i2 t2 temp {}}} 0 0} {DropTempTable\ +{{0 DropTempTable t2 {} temp {}} {0 Delete t2 {} temp {}}} 0 0} {DropView {{0\ +DropView v1 {} main {}} {0 Delete v1 {} main {}}} 0 0} {DropTrigger {{0\ +DropTrigger tr1 t1 main {}}} 0 0} {DropIndex {{0 DropIndex i1 t1 main {}}} 0 0}\ +{DropTable {{0 DropTable t1 {} main {}} {0 Delete t1 {} main {}}} 0 0}\ +{Transaction {{0 Transaction BEGIN {} {} {}} {0 Select {} {} {} {}} {0\ +Transaction COMMIT {} {} {}}} 0 0} {Savepoint {{0 Savepoint BEGIN s1 {} {}} {0\ +Savepoint RELEASE s1 {} {}}} 0 0} {Attach {{0 Attach :memory: {} {} {}}} 0 0}\ +{Detach {{0 Detach d1 {} {} {}}} 0 0} {CreateVtable {{0 CreateVtable t3 fts4\ +main {}} {0 CreateTable t3_content {} main {}} {0 CreateTable t3_segments {}\ +main {}} {0 CreateTable t3_segdir {} main {}} {0 CreateTable t3_docsize {} main\ +{}} {0 CreateTable t3_stat {} main {}} {0 Pragma page_size {} main {}}} 0 0}\ +{DropVtable {{0 DropVtable t3 fts4 main {}} {0 Delete t3 {} main {}} {0\ +DropTable t3_content {} main {}} {0 Delete t3_content {} main {}} {0 DropTable\ +t3_segments {} main {}} {0 Delete t3_segments {} main {}} {0 DropTable\ +t3_segdir {} main {}} {0 Delete t3_segdir {} main {}} {0 DropTable t3_docsize\ +{} main {}} {0 Delete t3_docsize {} main {}} {0 DropTable t3_stat {} main {}}\ +{0 Delete t3_stat {} main {}}} 0 0} {CreateTable {{0 CreateTable tDeny {} main\ +{}}} 1 {System.Data.SQLite.SQLiteException (0x80004005): authorization denied}}\ +False}} + +############################################################################### + +runSQLiteTestEpilogue +runTestEpilogue Index: Tests/common.eagle ================================================================== --- Tests/common.eagle +++ Tests/common.eagle @@ -997,10 +997,20 @@ # return [expr {[sql execute -execute scalar $db \ "SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name = ?;" \ [list param1 String $name]] > 0}] } + + proc trimSql { sql } { + set result [string map [list \r\n " " \r " " \n " "] $sql] + + while {[string first " " $result] != -1} { + set result [string map [list " " " "] $result] + } + + return $result + } proc executeSql { sql {execute none} {fileName ""} } { if {[string length $fileName] == 0} then {set fileName :memory:} setupDb $fileName "" "" "" "" "" false false false false memDb