Sugar.Data: Handling SQLiteConnection, SQLiteQueryResult, SQLiteException in Swift/Silver

I’m trying to figure out a complete Sugar.Data Swift (Silver) working snippet.
I’m covering iOS only here:

libSugar.Data.a (static library), ARM64.
Sugar.Data.h (public header)

This one starts with

// Header generated by RemObjects Elements for Cocoa 

#include <stdint.h>
#include <stdbool.h>
#include <stddef.h>

#import <objc/NSObject.h>
#import <Foundation/Foundation.h>

@class SQLiteConnection;
@class SQLiteQueryResult;
@class SQLiteException;

@interface SQLiteConnection: NSObject
...
  • In Xcode setup Obj-C Bridging-Header that references Sugar.Data public header:

#import "Sugar.Data.h"

  • Code Snippet

First I need a Swift function that detects database sql, and return its path or nil (this is not pretty much straightforward so I prefer to append this func as well) Also this function will copy a .sql init file from app bundle if any:

func checkDatabaseURL() -> NSURL? {
        
        let fileManager = NSFileManager.defaultManager()
        let urls = fileManager.URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)
        
        // If array of path is empty the document folder not found
        guard urls.count == 0 else {
            let finalDatabaseURL = urls.first!.URLByAppendingPathComponent("testdb.sql")
            // Check if file reachable, and if reacheble just return path
            guard finalDatabaseURL.checkResourceIsReachableAndReturnError(nil) else {
                // Check if file is exists in bundle folder
                if let bundleURL = NSBundle.mainBundle().URLForResource("testdb", withExtension: "sql") {
                    // if exist we will copy it
                    do {
                        try fileManager.copyItemAtURL(bundleURL, toURL: finalDatabaseURL)
                    } catch _ {
                        print("File copy failed!")
                    }
                } else {
                    print("Our file not exist in bundle folder")
                    return finalDatabaseURL
                }
                return finalDatabaseURL
            }
            return finalDatabaseURL
        }
        return nil
    }
  • At this point I can safely create or load a existing sql database:

             let fname = checkDatabaseURL()?.absoluteString;
             print(fname);
             if let fpath = fname { // unwrap optional
             
             let dbConn:SQLiteConnection = SQLiteConnection.init(fpath, false, true); // name, readonly, createifneeded
             print(dbConn);
             
             } catch let error as SQLiteException {
                 print("sql error");
                 print(error.description)
             } catch let error as NSError {
                 print("undefined error");
                 print(error.description)
             }
         }
    

Now, that I have a SQLiteConnection instance, I can populate the database (if I did not before) in the unwrap block of the if...then

            let dbConn:SQLiteConnection = SQLiteConnection.init(fname, false, true); // name, readonly, createifneeded
            let SQL = "CREATE TABLE IF NOT EXISTS CACHE (ID INTEGER PRIMARY KEY AUTOINCREMENT, CACHE_KEY TEXT UNIQUE, CACHE_VALUE TEXT, TIMESTAMP TEXT);";
            
            print(SQL);
            
            //var VALUES: AutoreleasingUnsafeMutablePointer<NSObject?> = nil;
            
            let INSERT = "INSERT OR REPLACE INTO CACHE (cache_key, cache_value, timestamp) VALUES (\"USER\",\"LORETO\",\"20160111\");";
            dbConn.Execute(INSERT,nil);

Please note here, that native sqlite.a static class can crash without anyway to catch the runtime exception:

<SQLiteConnection: 0x13464a160>
2016-01-12 11:55:30.042 CrossTest[1757:591182] *** Terminating app due to uncaught exception 'SQLite', reason: 'Not in an transaction'
*** First throw call stack:

Said that (so double check your SQL),
First problem here, I was not able to pass to the statement the values in the form of a prepared statement like:

let INSERT = "INSERT OR REPLACE INTO CACHE (cache_key, cache_value, timestamp) VALUES (?,?,?);";

According to the public header this is a NSObject * *:

- (int64_t)Execute:(/* mapped */ NSString *)aSQL :(NSObject * * /* dynamicarray */)aArgValues;

  • Now I can query the ResultSet like:

               do {
                  
                  let RES=try databaseExecuteQuery(dbConn, query: SQL);
                  print(RES);
                  if let result:SQLiteQueryResult = RES { // unwrap optional
                      if ( !result.IsNull ) {
                          // first row
                          while (  result.MoveNext() ) {
                            // next cursor row
                         }
                      }
                  }
                  
                  
              } catch let error as SQLiteException {
                  print("sql error");
                  print(error.description)
              } catch let error as NSError {
                  print("undefined error");
                  print(error.description)
              }
    

Where I have used a throws func to handle the exceptions like:

func databaseExecuteQuery(dbConn:SQLiteConnection, query:String) throws -> SQLiteQueryResult? {
        let RES:SQLiteQueryResult = dbConn.ExecuteQuery(query , nil);
        return RES;
    }

Here, I maybe wrong, since the NSException should be converted to a NSError (through ObjC class and bridging headers) according to minimal knowledge of Swift Error Handling (this is suggested here: http://stackoverflow.com/questions/32758811/catching-nsexception-in-swift ).

Anyways, now that I have an instance of ResultSet SQLiteQueryResult I can go through it:

                   if ( !result.IsNull ) {
                        print( result.GetString( 0 ), result.GetString( 1 ), result.GetString( 2 ) );
                        while (  result.MoveNext() ) {
                            print( result.GetString( 0 ), result.GetString( 1 ), result.GetString( 2 ) );
                        }
                    }

Second problem here I cannot see any row in the ResultSet, so the INSERT OR REPLACE failed, without errors, so this failed:

let INSERT = "INSERT OR REPLACE INTO CACHE (cache_key, cache_value, timestamp) VALUES (\"USER\",\"LORETO\",\"20160111\");";
dbConn.Execute(INSERT,nil);

So I suppose that this will work only as a prepared statement, but I was not able so far to figure out which is the AutoreleasingUnsafeMutablePointer to pass as

dbConn.Execute(INSERT,VALUES);

FTR, Sugar libraries all probably not be very consumable form Xcode. Sugar makes intensive use of Elements compiler features such as mapped types, and it’s designed only to be used from the Elements compiler (in all three languages), not as a library than can be consumed from other compilers (be it Xcode’s ObjC or Swift, the Java Language, or even Visual C#).

Yes, I see. I was working on a sort of sketch to run in Fire actually, due to the complete language help tools (auto completion, etc.) in Xcode and to the fact that I am pretty new to Fire. The idea is that this will run in Fire as soon as I’m more confident with it (currently I’m using Xcode pretty much like a language support tool, and the compile the stable version to Fire).

Note: Right now I’m also on Sugar.IO / Fire to access to documents folder on Android / iOS to have basic file system access.

@mh What about the Sugar.Data SQLiteConnection query execution with prepared statements, any docs about this?

It’s defined as:
method Execute(aSQL: String; params aArgValues: array of Object): Int64;

you should able to do:
.execute(“insert or replace into cache … values (?, ?, ?)”, [val1, val2, val2]);

(From within Silver, not Swift, that parameter isn’t accessible from XCode)

so there are 3 calls you can use:



    // insert and return the last insert id
    method ExecuteInsert(aSQL: String; params aArgValues: array of Object): Int64;
    // execute and return the number of affected rows
    method Execute(aSQL: String; params aArgValues: array of Object): Int64;
    // select
    method ExecuteQuery(aSQL: String; params aArgValues: array of Object): SQLiteQueryResult;

ExecuteInsert is meant for insert where you care about the inserted auto inc id, this is the returned value.
Execute is for update/delete/insert where you want the nr of affected rows.

ExecuteQuery is meant for selecting data, it returns a SQLiteQueryResult which can be used to query the resultset.

All 3 take two parameters without a prefix: , the first is an SQL query; the second an array with the parameters.

SQLiteQueryResult can be used to get a select result, the usage generally looks like:

while result.MoveNext() {
  result.GetString(0) << field 1
  result.GetInt64(1) << field 2
}

@ck So, I did (just an example)

                  do {
			let INSERT = "INSERT OR REPLACE INTO CACHE (cache_key, cache_value, timestamp) VALUES (?,?,?);";
			conn.Execute(INSERT,["KEY","PIPPO","20150101"]);
			let SELECT = "SELECT * from CACHE"
			let result:SQLiteQueryResult=conn.ExecuteQuery(SELECT);
			while result.MoveNext() {
				writeLn( result.GetString( 0 ) ); // col1
				writeLn( result.GetString( 1 ) ); // col2
				writeLn( result.GetString( 2 ) ); // col3
			}
		} catch let error as SQLiteException {
			writeLn("sql error");
			writeLn( error.description.ToString() );
		}  

but the result prints out bad string chars on the console like:

<SQLiteQueryResult: 0x13fe4b2c0>
2
櫨ꀛ
櫨ꀛ

confirmed from sqlite shell:

sqlite> select * from cache;
2|櫨ꀛ|櫨ꀛ|

You are right; Looks like there was a bug in the Sugar data code for Cocoa. I’ve fixed it here:

import Foundation
import Sugar.Data

println("The magic happens here.")
__try {
    var sq = SQLiteConnection("test.db", false, true)
    sq.Execute("create table if not exists  test (a, b, c)");

    sq.ExecuteInsert("DELETE FROM test")
    sq.ExecuteInsert("insert into test values (?, ?, ?)",
    ["hello", 15, 12.1])
    var sqd = sq.ExecuteQuery("select * from test")

    while sqd.MoveNext() {
        writeLn("0: " + sqd.GetString(0))
        writeLn("1: " + sqd.GetInt(1))
        writeLn("2: " + sqd.GetDouble(2))
    }
} __catch e: Exception {
    writeLn("Got:" + e)
}

compiles and works now.

Thank you. I have compiled again Sugar on latest commit bafb9540ad64dc3adca6f62fbd02db7379e8b17c and tried on my project - https://github.com/loretoparisi/swift-promise-example -, but I still see the issue.
I put the libSugar.fix and libSugar.Data.fx in libraries folder mylibs (in order that Fire takes this one instead of the one in its bundle):

CrossTest/
CrossTest/StaticLibrary/
CrossTest/StaticLibrary/mylibs/
CrossTest/StaticLibrary/mylibs/libSugar.fx
CrossTest/StaticLibrary/mylibs/libSugar.Data.fx

and the Android libs:

CrossTest/
CrossTest/StaticLibrary/
CrossTest/StaticLibrary/ClassLibrary2/
CrossTest/StaticLibrary/ClassLibrary2/mylibs/
CrossTest/StaticLibrary/ClassLibrary2/mylibs/sugar.jar
CrossTest/StaticLibrary/ClassLibrary2/mylibs/sugar.data.jar

The project is Silver(Swift) on Fire Beta 8.3.92.1915

Could you please check against those ones?

Thank you!

Which one fails for you, java or cocoa? I fixed Cocoa (As that’s the one mentioned above)

Wait, it works, I forgot to update the static library in the app project!

Database created at /var/mobile/Containers/Data/Application/D5D82C6B-B411-4C5F-84FE-EB05D2AB4D54/Library/Application Support/db/db.sql
<SQLiteQueryResult: 0x13c6501f0>
1
USER_765609548
PIPPO

great! Going to check Android as well in few minutes!

@ck So I was checking Android and I have found that when catching an exception I get this error:

Caused by: java.lang.NoSuchMethodError: No static method getdescription(Ljava/lang/Object;)Ljava/lang/String; in class Lswift/$anonymoustype$7; or its super classes (declaration of 'swift.$anonymoustype$7' appears in /data/app/musixmatch.com.sampleandroidapp-2/base.apk)

in

// create database file
			do {
				
				Sugar.IO.FolderUtils.Create(dbPath);
			

} catch let error as SugarIOException {
				writeLn("sql file error");
				writeLn( error.description.ToString() );
				return nil;
			}

this is due to error.description that is not implemented in Cooper?

the .description property is a Cocoa’ism as it comes from NSObject; it won’t be on Cooper. i believe you can just print error.ToString(), or even just error itself.

Right, I did in this way:

func logError(var message:String, var error:Object?) ->() {
		writeLn(message);
		#if cocoa
			if let err = error {
				writeLn( err.description.ToString() );
			}
		#else if java
			if let err = error {
				writeLn(error);
			}
		#endif
	}

why not just

func logError(var message:String, var error:Object?) ->() {
		writeLn(message);
			if let error = error {
				writeLn(error);
			}
	}

haha! that’s true, thanks.

I confirm that now the ResultSet works fine. To traverse the array the sample code to insert test tuples and query them

                  do {
				
				let rndIndex=(Sugar.Random()).NextInt();
				let key="USER_"+Sugar.Convert.ToString(rndIndex);
				
				let INSERT = "INSERT OR REPLACE INTO CACHE (cache_key, cache_value, timestamp) VALUES (?,?,?);";
				conn.Execute(INSERT,[key,"PIPPO","20150101"]);
				
				let SELECT = "SELECT * from CACHE"
				let result:SQLiteQueryResult=conn.ExecuteQuery(SELECT);
				writeLn( result );
				
				while result.MoveNext() {
					writeLn(  result.GetString( 0 ) ); // col1
					writeLn( result.GetString( 1 ) ); // col2
					writeLn( result.GetString( 2 ) ); // col3
				}
			
			} catch let error as SQLiteException {
				logError("sql error",error:error);
			}

that logs called several times:

<SQLiteQueryResult: 0x14d55c870>
1
USER_765609548
PIPPO
2
USER_-1994321905
PIPPO
3
USER_-105582610
PIPPO

The complete working example (iOS and Android): https://github.com/loretoparisi/swift-promise-example

Note: An issue on Android ( see: Shared Project: Android app and iOS app sample apps )prevents this work at this time, I’m on it right now.