I’ve been playing with SQLite lately. It’s quite usable from AppleScript via the command line tool sqlite3
and do shell script
, but it seems to me that this approach has several drawbacks from AppleScript.
The biggest issue is that everything is reduced to text. So you can end up doing a lot of text manipulation just to get stuff in and out. This is especially the case where there are a lot of results. And while it’s not slow, it could be faster.
There is a third-party framework, FMDB
, that puts an Objective-C wrapper around SQLite, so it can be called from AppleScriptObjC. Unfortunately some of it is lower level and more complicated than would be ideal for AppleScript.
So I’ve written a new framework, FMDBAS
. This is the FMDB framework wrapped in a more AppleScript-friendly face (but maintaining access to the more advanced stuff for those who need it). Initial tests show it’s faster – in some cases considerably so. It’s main advantages are:
-
Easy use of SQL
?
placeholders, so you can optionally pass parameters as lists containing differing classes. You can handle classes such as dates and images much more easily. -
Query results are returned in either arrays/lists or dictionaries/records for each row, so there’s no need to massage a string to extract the results.
-
Query results are returned initially as Cocoa arrays. You can coerce them to AppleScript lists (a simple “
as list
”), but in the case of large results it’s often quicker to deal with the raw arrays. And because arrays are returned as a simple pointer, large result sets are returned much, much quicker. -
Support for transactions. You can batch updates together for greater efficiency.
-
Lower overhead than shell, and combined with transactions this means there’s less need to build long statements to perform things like multiple insertions.
-
Better error handling.
There are a few cons:
-
You need to add the framework to your system. I may wrap it in a library at some stage.
-
You need at least a basic understanding of AppleScriptObjC syntax to use it.
-
It won’t work under versions earlier than 10.10 at this stage.
It’s still at the early testing stage, and I’m looking for interested volunteers. If that’s you, please email me.
As a taster, here are two scripts borrowed from Adam Bell’s macscripter.net Introduction to SQLite3 tutorial http://www.macscripter.net/viewtopic.php?id=24744, one to build a database and another to read it. (I’ve modified the first slightly to avoid problems running it repeatedly.) Here’s the first script using the do shell script method:
set loc to space & "~/desktop/TestDB.db" & space
set head to "sqlite3" & loc & quote
set tail to quote
-- "head" tells SQLite where to put our db if it doesn't exist, identifies it if it does.
-- "head" is the opening statement of every future command to our db.
-- "tail" ends every query started with "head".
-- Next, we set up a table and give the columns labels (there can be several).
-- Note the space between the semicolon (which ends every line) and the quote.
set tableName to "mods"
set newTbl to "drop table " & tableName & ";create table " & tableName & "(firstname, lastname, country); "
-- Now we set up the data to be entered in the table
set d1 to "insert into mods values('Ray', 'Barber', 'USA'); "
set d2 to "insert into mods values('jj', 'Sancho', 'Spain'); "
set d3 to "insert into mods values('Adam', 'Bell', 'Canada'); "
set d4 to "insert into mods values('Bruce', 'Phillips', 'USA'); "
set d5 to "insert into mods values('Kim', 'Hunter', 'USA'); "
set d6 to "insert into mods values('Kevin', 'Bradley', 'USA'); "
-- And finally, build the SQLite query and execute it
do shell script head & newTbl & d1 & d2 & d3 & d4 & d5 & d6 & tail
And here’s the FMDBAS equivalent:
use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use framework "FMDBAS"
use scripting additions
-- create and open db
set thePath to current application's NSString's stringWithString:"~/desktop/TestDB.db"
set theDb to current application's FMDatabase's databaseWithPathAS:(thePath's stringByExpandingTildeInPath())
theDb's openWithFlagsAS:(current application's FMDBASOpenReadWriteCreate)
-- create table, clearing if necessary it
set tableName to "mods"
theDb's executeUpdateASBool:("drop table if exists " & tableName)
theDb's executeUpdateAS:("create table " & tableName & " (firstname, lastname, country)")
-- enter data
theDb's beginTransactionAS()
repeat with aList in {{"Ray", "Barber", "USA"}, {"jj", "Sancho", "Spain"}, {"Adam", "Bell", "Canada"}, {"Bruce", "Phillips", "USA"}, {"Kim", "Hunter", "USA"}, {"Kevin", "Bradley", "USA"}}
(theDb's executeUpdateAS:"insert into mods values(?, ?, ?)" withArguments:aList)
end repeat
theDb's commitAS()
-- close database
theDb's closeAS()
Here’s the shell script reading example:
-- Combining the path into the head:
set head to "sqlite3 ~/desktop/TestDB.db " & quote
-- Now the query:
set Spain to "select firstname, lastname from mods where country = 'Spain'; "
-- Get the answer (which will appear in your script as a list, so we'll name the parts
set {firstname, lastName} to words of (do shell script head & Spain & quote)
--> {"jj", "Sancho"}
set tLast to "select lastname from mods where firstname = 'Bruce'; "
set surname to do shell script head & tLast & quote --> "Phillips "
And the FMDBAS version:
use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use framework "FMDBAS"
use scripting additions
-- create and open db
set thePath to current application's NSString's stringWithString:"~/desktop/TestDB.db"
set theDb to current application's FMDatabase's databaseWithPathAS:(thePath's stringByExpandingTildeInPath())
theDb's openWithFlagsAS:(current application's FMDBASOpenReadOnly)
-- query it
set theResult to theDb's executeQueryAS:("select firstname, lastname from mods where country = 'Spain'")
-- *or*:
-- set otherResult to theDb's executeQueryAS:("select firstname, lastname from mods where country = ?") withArguments:{"Spain"}
set surname to theDb's executeQueryAS:("select lastname from mods where firstname = ?") withArguments:{"Bruce"}
theDb's closeAS()
return {theResult as list, surname as list}
These are tiny examples, but the first case is about 10x faster, and the second closer to 100x faster.