© Kevin Languedoc 2016

Kevin Languedoc, Build iOS Database Apps with Swift and SQLite, 10.1007/978-1-4842-2232-4_4

4. Altering Databases and Other Features

Kevin Languedoc

(1)Montreal, Canada

SQLite doesn’t have an extensive API for modifying databases. Nevertheless, it does still offer functions to alter tables, and we can still alter views, triggers, and indexes as well as functions.

This chapter will focus on demonstrating SQLite’s altering capabilities as well as on modifying a database using the platform’s other tools, including the following:

  • Altering table

  • Modifying views

  • Modifying indexes

  • Modifying triggers

  • Re-indexing a table

  • Deleting tables

  • Deleting views

  • Deleting indexes

  • Deleting triggers

In this section of the chapter, I will add the modifying database functionality as well as explore the other unique features of SQLite; for instance:

  • Collation sequences

  • The JSON extension

  • SQLite functions

  • Creating custom SQLite functions

  • Pragma expressions

  • SQLite limits

  • SQLite database corruption issues

Modifying Tables

Three options are available for modifying a SQLite table. You can rename a table using the RENAME command , add a column using the ALTER command , and re-index using the REINDEX command . There is no function or command to alter a column, but it is still possible.

You also need to keep in mind how to handle foreign keys and indexes. You can’t directly alter these using a built-in function or command, but it is still possible. Let us start with how to rename a table.

Renaming a Table

Renaming a table is a very simple task, as you only need to issue the RENAME clause in the ALTER TABLE SQLite command. However, a couple of caveats must be respected. First, you cannot move or copy a table from one database to another, either within the same file or in another database file. While we will look at this feature in more depth in a later chapter, SQLite allows for multiple databases in a single database file. Second, if you have foreign-key constraints defined, you will need to disable them first.

Foreign keys with references will be renamed as a result the renaming, both in the table being altered and in the referenced table. However, any indexes, views, and/or triggers that reference a table that is renamed must be manually modified.

Simple Table Renaming

Except for constraints, renaming a table only requires the following, for example:

ALTER TABLE main.producer RENAME main.wineries

Complex Table Renaming

Of course, if you have constraints on a table, or on indexes, or if you have triggers and views that reference the table that you are renaming, you should use the following procedure and make the alterations.

Run a SELECT query on the sqlite_master table to get a copy of the query you used to create the indexes, views, and triggers. You can use a query similar to this:

SELECT sql from main.sqlite_master

This query would return all SQL statements for each type of element in the database. I recommend saving this query either to a file or another database so that you have a reference to modify and re-create these elements later. You could also just extract the SQL statements for specific elements, like a view or trigger, in which case the SQL SELECT query would look something like the following:

SELECT sql FROM main.sqlite_master WHERE type = ‘view’ or type = ‘trigger’

This type of query would return all the statements associated with these database objects.

Once you have these queries in hand, you can disable any constraints by issuing the following command:

PRAGMA foreign_keys=OFF

Followed by the rename statement from earlier.

Once the renaming is complete, you re-create the indexes, views, and triggers using the CREATE function for each of the objects after you have dropped them from the database. To drop, you simply use the DROPcommand, as follows:

DROP viewname
DROP triggername
DROP indexname

Finally, re-enable the foreign keys using the PRAGMA foreign_keys= ON command, as follows:

PRAGMA foreign_keys=ON

These sequences of SQL queries can all be executed from the Db Mgr app using the Rename table menu command. Finally, you should run an UPDATEquery to update the sqlite_master table with any changes to your table, as follows:

UPDATE main.sqlite_master
SET sql = ‘ new creatw table query’
WHERE type = ‘table’
AND name = ‘your table name’
Warning

If you run an update on the sqlite_master and your update query has syntax errors, you will corrupt the sqlite_master table and your database. It is best to test your update query on a blank database or on a test table with a similar structure as the sqlite_master table to ensure that the update query works as expected.

In the next section, I will show how to add columns to an existing table.

Adding Columns

Adding columns to a SQLite database table is the second actual way of altering a SQLite database table. By this I mean that the ALTER command is only available for these two use cases. As with the renaming, the Add Column functionality has a few caveats that you need to adhere to in order to add columns to a SQLite database table; otherwise, you can define a new column using the CREATE TABLE statement.

  • The new column cannot be defined as a primary key.

  • The added column cannot have a date-time default value defined using the following functions: CURRENT_TIME, CURRENT_DATE, CURRENT_TIMESTAMP.

  • If the column is set to NOT NULL, then you must provide a default value.

  • If you are adding a column that will be used as reference to a foreign key, then the column must be set to NULL as a default value.

  • Each new column must be added individually.

To add a column to an existing table, you simply need to run a SQL query; for example:

Alter table main.tablename
Add column columnname datatype default value

Here are some actual examples:

Create table main.country(id integer  primary key not null autoincrement, name varchar not null)

Alter table main.country
Add column continent varchar null


Alter table main.country
Add column population integer null

Once the query is executed using sqlite3_exec or sqlite3_prepare_v2, sqlite3_step, and sqlite3_finalize, the alterations will be completed.

Re-indexing a Table

The third type of modification you can do to a table is to re-index its index. You can specify a single index to rebuild, or all of them, if there is more than one in a table. Likewise, if you have a collation sequence to arrange data in a database, you can re-index the collation sequence, and all indexes associated with the collation name will be re-indexed.

Re-indexing an index or collation sequence is accomplished using the REINDEX command followed by the schema name or collation name, as follows:

REINDEX collation-name
REINDEX main.table_name
REINDEX main.table_index_name

It is always useful to rebuild an index on a regular basis or after loading, reloading, or deleting a dataset from a table. This will also ensure that any unused space is removed and the sequence of the index is optimized, thus ensuring the fast data access.

Modifying Views

Views in a SQLite database cannot be modified or altered. You can only DROP a view and re-create it. To drop and create a view you can follow this sequence:

DROP VIEW schema.view_name
CREATE View schema.view_name as SELECT * or list of columns FROM schema.table_name WHERE where_clause

The column list WHERE clause in a view is often used to delimit the amount of data that is stored in a database. The WHERE is optional but resolves to a Boolean value.

Modifying Indexes

Like views, an index cannot be altered or modified. You can only DROP and CREATE an index. If you have more than one index in a table, you will need to DROP and CREATE them individually. Also, before you drop an index, it is best to get a copy of the SQL CREATE query that was used to create the index from the sqlite_master table, because this table will be automatically updated once you execute the DROP statement, just like it gets updated when you CREATE an index .

You can use the following sequence to alter an index:

DROP INDEX schema.index_name
CREATE optionally UNIQUE INDEX optionally IF NOT EXIST schema.index_name on schema.table_name(column(s)) WHERE where_clause

The use of UNIQUE and IF NOT EXIST are optional, as is the WHERE clause. The IF NOT EXIST would be redundant here, as the index wouldn’t yet exist. If the WHERE clause is used, then the index will be known as a partial index. Also note that you can use an expression instead of a column or sequence of columns to be indexed. In addition, a COLLATION_NAME can be added with a sort order of ASC or DESC to arrange the data in the index.

It is important to remember that when creating or modifying an index, the columns that you use for your index must be in the table where the index is. You cannot use columns in other tables.

Here is an example of using an expression for an index:

              DROP INDEX                                               main.wine_id
CREATE UNIQUE INDEX main.wine_id on main.wineries(id+name)

Using the a collation sequence, you would write a query similar to this example:

DROP INDEX main.wine_id
CREATE UNIQUE INDEX main.wine_id

You can also use a function call in your expressions as long as the return value is deterministic. In other words, you cannot use functions like RANDOM(). The function can be either one of the functions provided by the SQLite API or a custom function. I will demonstrate functions a little later. For example, you could use the following:

DROP INDEX main.wine_id
CREATE INDEX main_wine_id ON main.wineries(coalesce(name))

Modifying Triggers

Triggers , as we have seen elsewhere, cannot be altered. You can, however, DROP a trigger and CREATE a new one using any of the accepted API syntax that is permitted. Before dropping a trigger, you should run the SELECT query on the sqlite_master to get the existing SQL query that was used to create the original trigger. If you want to the sqlite_master table with the new version of the trigger, you will need to execute an UPDATE. However, as I warned before, if you execute an UPDATE on the sqlite_master table and your update statement has syntax errors, you run the risk of corrupting the table and scraping your database. It is always best to test on a secondary database first.

What I usually do is either execute a SELECT on the sqlite_master table to get the SQL statement or keep a copy of the SQL statement in a file, because the DROP statement will update the sqlite_master table. For instance, this sequence should be used to replace an existing trigger:

  • Retrieve copy of CREATE statement.

  • DROP trigger.

  • Modify the trigger’s logic.

  • CREATE the new trigger.

  • CREATE a test database.

  • UPDATE sqlite_master table in test database.

  • Fix syntax issues, if any.

  • UPDATE sqlite_master table in your database.

Here are some examples of SQLite triggers that you can execute through the Db Mgr app:

DROP TRIGGER main.trigger_name
CREATE TRIGGER main.trigger_name BEFORE DELETE ON main.table_name FOR EACH ROW WHEN expression is true or false BEGIN delete-statement
DROP TRIGGER main.trigger_name
CREATE TRIGGER main.trigger_name BEFORE UPDATE of column ON main.table_name FOR EACH ROW WHEN expression is true or false BEGIN update-statement
DROP TRIGGER main.trigger_name
CREATE TRIGGER main.trigger_name BEFORE INSERT ON main.table_name FOR EACH ROW WHEN expression is true or false BEGIN insert-statement
DROP TRIGGER main.trigger_name
CREATE TRIGGER main.trigger_name AFTER DELETE ON main.table_name FOR EACH ROW WHEN expression is true or false BEGIN delete-statement
DROP TRIGGER main.trigger_name
CREATE TRIGGER main.trigger_name AFTER UPDATE of column ON main.table_name FOR EACH ROW WHEN expression is true or false BEGIN update-statement
DROP TRIGGER main.trigger_name
CREATE TRIGGER main.trigger_name BEFORE INSERT ON main.table_name FOR EACH ROW WHEN expression is true or false BEGIN insert-statement
DROP TRIGGER main.trigger_name
CREATE TRIGGER main.trigger_name INSTEAD OF DELETE ON main.table_name FOR EACH ROW WHEN expression is true or false BEGIN delete-statement
DROP TRIGGER main.trigger_name
CREATE TRIGGER main.trigger_name INSTEAD OF UPDATE of column ON main.table_name FOR EACH ROW WHEN expression is true or false BEGIN update-statement
DROP TRIGGER main.trigger_name
CREATE TRIGGER main.trigger_name INSTEAD OF INSERT ON main.table_name FOR EACH ROW WHEN expression is true or false BEGIN insert-statement

You can also add a TEMP or TEMPORARY keyword between the CREATE and TRIGGER keywords if you only need a temporary trigger. These triggers will only exist while the database is open. Altering triggers in SQLite is really just creating a new trigger to replace the old one.

Adding and Altering Collation Sequences

Collation sequences are directives on how the data in a database is arranged or sorted. Collation structures are present in the real world, like for cataloguing systems used in libraries, for instance, or medical records.

You can create a collation sequence in SQLite by using the CREATE Collation command, which modifies the database by adding the sequence to the database. The collation sequence can be added when the database is first created, or afterward.

SQLite uses collation sequencing internally to determine the greater of lesser of two values. SQLite has three built-in collation sequence types:

  • Binary

  • Nocase

  • Rtrim

Binary

The BINARY collation sequence algorithm uses the memcmp() C function to compare two text values regardless of string encoding. memcmp() compares the first byte-area size of two strings. The BINARY collate option uses comparison operators (==, <, >, !=, IS, IS NOT, =>, <=) to compare two values.

NoCase

The NoCase collation sequence type compares two text values by converting the 26 ASCII uppercase characters into their lowercase equivalents. Note that it only compares ASCII because full UTF conversion would make the tables manageable.

Rtrim

Lastly, the RTRIM type is the same as BINARY except that it first trims any trailing spaces to the text values to be compared.

To use the collating sequence, you would add COLLATE to the column definition; for example:

CREATE TABLE Strings(
Id INTEGER PRIMARY KEY,
String1, //defaults to binary
String2 COLLATE BINARY,
String3 COLLATE NOCASE,
String4 COLLATE RTRIM
)

sqlite3_create_collation

Of course, you can create your own collation sequence by implementing the sqlite3_create_collation function . The function has three variants, whose logic is provided here:

int sqlite3_create_collation(
  sqlite3,
  const char *zName,
  int eTextRep,
  void *pArg,
  int(*xCompare)(void*,int,const void*,int,const void*)
);

The sqlite3_create_collation has five arguments. The first is the pointer to the database connection. In Swift this is represented by a COpaquePointer. The second argument, const char *zName, is the name of the collation sequence module. The third, eTextRep, is the text encoding of the string callback function and must implement one of the following types:

  • SQLITE_UTF8

  • SQLITE_UTF16LE

  • SQLITE_UTF16BE

  • SQLITE_UTF16

  • SQLITE_UTF16_ALIGNED

The next argument, *pArg, is an application pointer for the first argument of the callback function. The last argument is the callback function. sqlite3_create_collation_v2 is similar to the first except that it provides a Destroy argument, and sqlite3_create_collation16 provides the functionality to create collation sequences in a native 16 bits. See the following:

int sqlite3_create_collation_v2(
  sqlite3*,
  const char *zName,
  int eTextRep,
  void *pArg,
  int(*xCompare)(void*,int,const void*,int,const void*),
  void(*xDestroy)(void*)
);
int sqlite3_create_collation16(
  sqlite3*,
  const void *zName,
  int eTextRep,
  void *pArg,
  int(*xCompare)(void*,int,const void*,int,const void*)
);

All three functions can add, modify, and delete collation sequences from a SQLite database. In Swift these functions are implemented in the sqlite3.h header file as follows:

sqlite3_collation_needed
sqlite3_collation_needed16

They both take the same arguments as the versions in C. These functions are abbreviated versions of the former functions and are accessible as well through the standard SQLite C API. You would need to define these functions as COpaquePointers, UnsafeMutablePointer, Int32, and UnsafePointer<Int8 > respectively. For the 16-bit version, the last argument would be a 16-bit UnsafePointer rather than an UnsafePointer<Int8 > .

The SQLite DELETE Statement

Deleting elements from a SQLite database is implemented through the SQL DROP function. You can use the DROP function to remove tables, views, indexes, and triggers. When you drop a table or an index, the sqlite_master table is updated accordingly. For the views and triggers, you need to execute a DELETE record on the table.

The DROP statements are provided next and can be used as is through the DB Mgr app by entering the DROP function followed by the schema element to remove and the name of the element. Once you click on the Execute button, the element will be removed from the database. Removing or dropping a table will delete any data that is in that table. Of course, the database needs to be open first.

Deleting Tables

DROP TABLE schema.table_name

Deleting Views

DROP VIEW schema.view_name

Deleting Indices

DROP INDEX schema.index_name

Deleting Triggers

DROP TRIGGER schema.trigger_name

Deleting Collation Sequences

Custom collation functions are deleted when you invoke the Destroy argument or when the database connection is closed using the sqlite3_close function.

SQLite Functions

Unlike other relational database engines, SQLite doesn’t provide an API to create stored procedures, also known as SPROCS. However, you can define your own functions in addition to using the built-in functions in SQLite.

SQLite has several built-in functions that can be categorized as follows:

  • Core functions

  • Aggregate functions

  • Date/time functions

  • JSON functions

  • Standard functions

Core functions include functions abs, coalesce, ifnull, instr, glob, like, and length. They make up the core functionality of the SQLite platform. The Aggregate functions, as the name implies, provide functions like count, avg, min, max, total, and sum, among others. The Date/time functions allow you to obtain and manipulate date and time values. These functions include date, time, datetime, juliandate, date modifiers, and operators. For example, you can get the current date as follows:

SELECT date('now')
SELECT date('now', 'YYYY-MM-DD')
SELECT date('now', 'MMM/dd/YYYY', '-1 day')
SELECT date('now', 'MM-dd-yyyy', '-1month', '+7 days', '+1 year')

The JSON Extension

The JSON functions , officially known as the json1 extension, are a fairly new addition to SQLite. They aren’t available with the standard API. You need to install them manually if you need to use them. These functions are loaded at run-time. Using the JSON functions, you can store and parse data in the JSON format. The extension includes the functions listed here:

  • Json(json)

  • Json_array

  • Json_array_length(json)

  • Json_array_length(json. Path)

  • Json_extract

  • Json_insert

  • Json_object

  • Json_replace

  • Json_remove

  • Json_set

  • Json_type(json)

  • Json_type(json, path)

  • Json_valid

  • Json_group_array

  • Json_group_object

  • Json_each

  • Json_tree

The JSON functions that have json as a first parameter must be a valid JSON object, a number, a string, or a null value. Number and Null are interpreted as SQLite data types. The PATH argument must be a valid, well-formed path value that begins with $.

To load the json1 extension, you need to implement the loadable interface using sqlite3_load_extension, which is available in the C API through the bridge. You also have the sqlite3_enable_load_extension, which can disable the loading of extensions to prevent security leaks. See the following:

                sqlite3_load_extension                                  (db: COpaquePointer,zFile: UnsafePointer<Int8>, zProc: UnsafePointer<Int8>, pzErrMsg:UnsafeMutablePointer<UnsafeMutablePointer<Int8>>)

sqlite3_enable_load_extension (db: COpaquePointer, onoff: Int32)

The extension functions must be called when the database is open and running so that the extensions can be loaded as needed.

The first argument is the pointer to the SQLite database engine. The second argument is the file for the shared library. For the json1 extension, the file is not included in the default sqlite3.dylib library. The third argument is the entry point for the extension, while the fourth is a pointer for the error message:

var db:COpaquePointer?=nil
let lib_file: UnsafePointer<Int8>? = nil
let proc: UnsafePointer<Int8>?=nil
let err_json_msg:UnsafeMutablePointer<UnsafeMutablePointer<Int8>>? = nil


func load_extension()->Void{
      sqlite3_load_extension(db, lib_file, proc, err_json_msg)
    }
Note

The json1 sqlite3 extension is not included in the sqlite3 dylib, which references the sqlite3.h file in iOS 10. You could attempt to add it by downloading the sqlite3 source code and extracting the sqlite3.c file from the ext/misc directory. Then you would need to add it to your project, but this would probably create conflicts with the existing sqlite3 library in iOS. The other option would be to compile a new static Objective-C extension (.h) and add the sqlite3.h and sqlite3.c files to your project from the sqlite3 source code. Then, you would need to import the sqlite3.h file into the Objective-C extension (header). Next, you would need to create a new bridge header file and add it to the Swift compiler bridge configuration.

I have created a reference project using the sqlite3.c and sqlite3.h files from the actual code instead of the Xcode-supported sqlite3.dylib library.

The project is in GitHub ( https://github.com/kevlangdo/load_sqlite_json_extension ) and is experimental. I haven’t tested it, and it is outside of the context of this book, but it gives an example of how to load the json extension in an iOS project.

Creating Functions using Swift

Other than the SQLite functions included with the API and the extensions, you can create your own and attach them to your database. These extensions are written in C.

For the next iOS app project, the Wineries, I will need a function that can convert liters into ounces and vice versa. The code must be written in C using, in part, the SQLite API to convert values to and from SQLite.

SQLite values are as follows:

SQLITE_API const void *SQLITE_STDCALL sqlite3_value_blob(sqlite3_value*);
SQLITE_API int SQLITE_STDCALL sqlite3_value_bytes(sqlite3_value*);
SQLITE_API int SQLITE_STDCALL sqlite3_value_bytes16(sqlite3_value*);
SQLITE_API double SQLITE_STDCALL sqlite3_value_double(sqlite3_value*);
SQLITE_API int SQLITE_STDCALL sqlite3_value_int(sqlite3_value*);
SQLITE_API sqlite3_int64 SQLITE_STDCALL sqlite3_value_int64(sqlite3_value*);
SQLITE_API const unsigned char *SQLITE_STDCALL sqlite3_value_text(sqlite3_value*);
SQLITE_API const void *SQLITE_STDCALL sqlite3_value_text16(sqlite3_value*);
SQLITE_API const void *SQLITE_STDCALL sqlite3_value_text16le(sqlite3_value*);
SQLITE_API const void *SQLITE_STDCALL sqlite3_value_text16be(sqlite3_value*);
SQLITE_API int SQLITE_STDCALL sqlite3_value_type(sqlite3_value*);
SQLITE_API int SQLITE_STDCALL sqlite3_value_numeric_type(sqlite3_value*);

To create a SQLite function for Swift, you need to follow these steps:

  • As shown in Figure 4-1, add a C file using the C File template under the iOS categories. For my function, I will name it sizeconverter because it will convert volume from liters to ounces and vice versa. Xcode will ask if you want to add a header file; say yes.

    A427374_1_En_4_Fig1_HTML.jpg
    Figure 4-1. C File template
  • Next, you need to add C logic to the C file and import whatever C library into the header. You can also define your functions' signatures in the header file, which I am doing for this example. For example, I am adding the #include directive for the sqlite3.h header file.

  • Then, you need to add the C file to your bridge file using the #import statement (see the code that follows).

  • As shown in Figure 4-2, you need to add your header file to Build Phases > Compile Sources .

    A427374_1_En_4_Fig2_HTML.jpg
    Figure 4-2. Build phases

The code for the function (sizeconverter.c) is shown here, along with the header file and the changes to the SQLite3Bridge:

//
//  SQlite3Bridge.h
//  Db Mgr
//
//  Created by Kevin Languedoc on 2016-05-20.
//  Copyright © 2016 Kevin Languedoc. All rights reserved.
//


#ifndef SQlite3Bridge_h
#define SQlite3Bridge_h


#endif /* SQlite3Bridge_h */

// Add this code to import the sqlite3 header. The code above is supplied by the template
#import <sqlite3.h>
#import "sizeconverter.h"
The Adjust SQLite3Bridge Header File
//
//  sizeconverter.h
//  Db Mgr
//
//  Created by Kevin Languedoc on 2016-07-03.
//  Copyright © 2016 Kevin Languedoc. All rights reserved.
//


#ifndef sizeconverter_h
#define sizeconverter_h


#include <stdio.h>
#include <sqlite3.h>


static void sizeconverter(sqlite3_context *context, int argc,   sqlite3_value **argv);

#endif /* sizeconverter_h */
The sizeconverter header file
//  sizeconverter.c
//  Db Mgr
//
//  Created by Kevin Languedoc on 2016-07-03.
//  Copyright © 2016 Kevin Languedoc. All rights reserved.
//


#include "sizeconverter.h"
#include "SQlite3Bridge.h"


static void sizeconverter(sqlite3_context *context, int argc,   sqlite3_value **argv)
{


    double result = 0.0;
    const char *liter;
    const char *ounce;
    const char *us;
    const char *uk;


    us = "us";
    uk = "uk";
    liter = "l";
    ounce = "o";


    if (argc==3) {
        double from = sqlite3_value_double(argv[0]); // original volume
        const unsigned char *to = sqlite3_value_text(argv[1]); // liters or ounces
        const unsigned char *country = sqlite3_value_text(argv[2]); // us or uk


        const double us_uom = 33.8140226; // 1 Liter = 33.8140226 Ounces [Fluid, US]
        const double uk_uom = 35.195079; // 1 Liter = 35.195 079 Ounces [UK]


        if (country == (const unsigned char*)us && to == (const unsigned char*)liter) {
            result = from * us_uom;
        }else if (country == (const unsigned char*)uk && to == (const unsigned char*)liter){
            result = from * uk_uom;
        }else if (country == (const unsigned char*)us && to == (const unsigned char*)ounce){
            result = from / us_uom;
        }else if (country == (const unsigned char*)uk && to == (const unsigned char*)ounce){
            result = from / uk_uom;
        }


    }
    return sqlite3_result_double(context, result);
}

This is a simple C function that takes three arguments: the original volume, the target unit of measure, and the country, which can be United States or United Kingdom, since these have different liter conversion values because their ounces are based on either the Imperial system or the American system. Most other countries use the metric system.

With the function done and configured, we only need to use it in the Db Mgr app and attach it to the database.

Using Functions in a SQLite Database using Swift

Using custom SQLite functions requires using sqlite3_create_function. The first argument is the SQLite database pointer, which is a COpaquePointer; the second argument is the name of the function as a UTF-8 encoding string; the third parameter is the number of input parameters; and the fourth parameter is the text encoding type the function prefers. In my case, it is UTF-8, which is pretty standard, but it will accept any of the supported encodings. The fifth parameter is an arbitrary pointer that allows access through the function using the sqlite3_user_data() function . The last three arguments are pointers that implement the function or aggregate:

 func createSQLiteFunction()->Enums.SQLiteStatusCode{
        let funcname:String = “sizeconverter”
        return Enums.SQLiteStatusCode(rawValue:sqlite3_create_function(db, funcname.cString (String.encoding.utf8)!, 3, SQLITE_UTF8, nil, sizeconverter, nil, nil))!
}

Once the function is connected, you can use the function elsewhere in your INSERT, SELECT, or UPDATE queries, which we will look at later in this book in the chapters covering those subjects.

Pragma Statements

Pragma statements are a unique feature of the SQLite platform. These statements are used to set and control environment variables within a SQLite database and environment.

Pragma statements can be used like other queries, using the sqlite3_prepare_v2, sqlite3_step, and sqlite3_finalize functions once the database is open and the connection is established. However, some PRAGMAs will run during slite3_preapre_v2 or sqlite_step or both, depending on the Pragma statement.

For example:

let pragma:String = “PRAGMA schema.index_list( table_name )”

If(sqlite3_open(db.path, db) == SQLITE_OK){

If(sqlite3_prepare_v2(db, pragma.cString (String.encoding.utf8)!, -1, &sqlStatement, nil) == Enums.SQLiteStatusCode.ok.rawValue}{

}
}

SQLite has many Pragma statements that are used more than others (see https://www.sqlite.org/pragma.html for the full list). For instance, here are some useful ones:

  • Foreign_key_check

  • Foreign_key_list

  • Integrity_check

  • Automatic_index

  • Busy_timeout

  • Shrink_memory

  • Auto_vacuum

Foreign_key_check

PRAGMA schema.foreign_key_check;  
PRAGMA schema.foreign_key_check(table-name);

The foreign_key_check PRAGMA runs against the database or table to check for any foreign-key violations. It returns a row for each violation, so you could use the sqlite3_step function to retrieve the returning value. The resulting value has four columns, as follows:

  • The name of the name containing the reference.

  • The second value is the rowindex where the violation occurred.

  • The third column is the table that the foreign key refers to.

  • The last column is the foreign-key name.

Foreign_key_list

The Foreign_key_listPRAGMA returns the list of foreign keys in a database. It returns one row for each foreign-key constraint.

PRAGMA foreign_key_list(table-name)

Integrity_check

The integrity_checkPRAGMA does a sanity check on the entire database looking for missing indexes, broken records, out-of-order records, missing pages, and UNIQUE or NOT NULL violations, among other verifications. The results are returned as a single column describing the issue. If no problems are found, the returning column will contain the “OK” value only. The N refers to the max number of errors to return. It defaults to 100.

PRAGMA schema.integrity_check;  
PRAGMA schema.integrity_check(N)

Automatic_index

The automatic_indexPRAGMA allows the developer to query, set, or clear the automatic index on a table.

PRAGMA automatic_index;  
PRAGMA automatic_index = boolean;

Busy_timeout

This PRAGMA expression enables you to get the current timeout or set the busy timeout for query execution.

PRAGMA busy_timeout;  
PRAGMA busy_timeout = milliseconds;

Shrink_memory

This PRAGMA expression frees up memory as much as possible. This is handy for large databases or long-running queries. This is similar in concept to a garbage collector.

PRAGMA shrink_memory

Auto-vacuum

This PRAGMA removes empty space from the page_file, thus freeing up memory and shrinking the size on disk.

PRAGMA schema.auto_vacuum;
PRAGMA schema.auto_vacuum = 0 | NONE | 1 | FULL | 2 | INCREMENTAL;

Corrupting a SQLite Database

SQLite is an extremely stable technology and is very robust, but it is still possible to cause database files to become corrupt if you’re not careful.

For instance, if you move a database while the connection is open, you run the risk of corrupting the database. Since a SQLite file is a standard binary file, there is nothing that prevents a rogue thread or process from interfacing with. Within the context of an iOS sandbox where the app lives, this would be difficult but not impossible.

Another possible threat is trying to access the same file descriptor after it was closed and reopened. Also, trying to back up or restore a database while it is open can damage the file.

Broken file locks, which are managed by the filesystem, can damage the database file if you try to access it while a faulty lock is in place. Although there are many other corruption issues that can arise through the POSIX and by using different locking mechanisms or having multiple applications try to access the same database, the environment in which a SQLite database operates on iOS somewhat protects it from corruption.

However, one way to easily corrupt a database is to rename it while it is open. You have to be careful when moving a database—say, from the Resources bundle to the Documents directory—that the database is closed before attempting the operation.

Lastly, there are syncing issues that cause damage to a database. This could be an issue if you place a copy of the database on Dropbox, OneDrive, or iCloud or any other Cloud file storage and sharing platform and try to sync the database files while it is open, which can cause damage to the database file. Make sure that the connection is closed before attempting any of these operations.

SQLite Limits

The last word goes to the limits . SQLite is very happy storing large amounts of data within its storage facility but it still does have its limits. Many of the limits imposed on SQLite come from the OS or memory. For instance, memory can be confined to 32-bit or 64-bit. On an iPhone, you have 1 GB of RAM available for the whole device. On an iPad Pro, however, you have 4 GB of RAM. SQLite must work within these small confines.

Another limit can be the overall disk size. Since we are dealing with mobile devices, you have a finite amount of disk space available.

There are other limits that can be tweaked during runtime for the specific needs of the application. The default length of a blob is defined by the macro SQLITE_MAX_LENGTH with a value of a 1 billion bytes. However, you can change this default using the DSQLITE_MAX_LENGTH flag:

-DSQLITE_MAX_LENGTH=123456789

You can also change the maximum number of columns, indexes, or views, or the maximum number of update and insert terms or where clause terms, from the default value of 2000 to a max of 32767. But how many databases have you seen reaching these sizes, even on a server?

Another size limit you can change is the length of a query. The size is set at 1000000 through the DSQLITE_MAX_LENGTH macro. This value can be increased to 1073741824. As for tables, you can have up to 64 tables. Try that one if you can.

Another interesting limit is the maximum amount of parameters a function can have. The default value is 100, but this value can be modified using the SQLITE_MAX_FUNCTION_ARG macro. Also, the maximum amount of compound SELECT statements using joins is 500. This value can be changed using the SQLITE_MAX_COMPOUND_SELECT macro. You can attach up to 125 databases in a file, while the default is 10. The theoretical maximum number of rows in a table is 264 (18446744073709551616, or about 1.8e + 19). However, as the documentation states, this limit can never be reached, since a database file can have a maximum size of 164 terabytes, which far surpasses the physical limits of any iOS device.

Although SQLite has limits on other aspects of its architecture, these limits can never be reached within the confines of any iOS device. You will run out of physical space or resources before you reach SQLite’s limits.

Summary

This concludes this chapter. We have explored the different features available in the SQLite API for altering databases. We have also looked at PRAGMA statements and SQLite’s built-in functions. Finally, we built a custom SQLite function and added to the Winery database in the DB Mgr app.

The next several chapters will demonstrate how to perform CRUD operations on a SQLite database through the use of an iPhone app. We will also look at how to perform searches.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset