groue / GRDB.swift

A toolkit for SQLite databases, with a focus on application development

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

GRDB/Spatialite ?

Tybion opened this issue · comments

Is it possible to use spatialite with GRDB, or can this be made possible?

Following is sqlite3 code that runs with spatialite from https://github.com/smbkr/libspatialite-ios ..

// Thank you to Kodeco for the sqlite3 code ..
// https://www.kodeco.com/6620276-sqlite-with-swift-tutorial-getting-started

let documentsUrl =  FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let docsPath = documentsUrl.path  // no trailing slash
let shortname =  "spatialtest6.sqlite"

let fullname = docsPath + "/" + shortname
           
var db: OpaquePointer?
print(shortname)
if sqlite3_open(fullname, &db) != SQLITE_OK {
   print("Unable to open database - \(shortname).")
}

var spconnect: OpaquePointer?
spatialite_initialize()
print("Spatialite version: \(spatialite_version()!)")
spatialite_init_ex(db, &spconnect, 1)

//    spatialite_init_ex((sqlite3 *) sqliteHandle, spatialiteConnection, 0);
let tableName = "features"
let createTableString = """
CREATE TABLE \(tableName) (
id integer PRIMARY KEY,
title text,
descr text);
"""
var stmnt: OpaquePointer?
print(createTableString)
if sqlite3_prepare_v2(db, createTableString, -1, &stmnt, nil) == SQLITE_OK {
    if sqlite3_step(stmnt) == SQLITE_DONE {
      print("Table created.")
    } else {
      print("Table not created.")
    }
} else {
    print("Statement not prepared.")
}

var stmnt4: OpaquePointer?
let sqlStr = "SELECT InitSpatialMetaData('WGS84');"
print(sqlStr)
if sqlite3_prepare_v2(db, sqlStr, -1, &stmnt4, nil) == SQLITE_OK {
    if sqlite3_step(stmnt4) == SQLITE_ROW {
        let result = sqlite3_column_int(stmnt4, 0)
        print("Query Result: \(result)")
    } else {
        print("Query returned no results.")
    }
} else {
    let errorMessage = String(cString: sqlite3_errmsg(db))
    print("Query is not prepared \(errorMessage)")
}

let sqlSpatial = [
        "SELECT AddGeometryColumn('\(tableName)', 'geom', 4326, 'POINT', 'XY');",
        "SELECT CreateSpatialIndex('\(tableName)', 'geom');"]
for sql in sqlSpatial {
    var stmt: OpaquePointer?
    print(sql)
    if sqlite3_prepare_v2(db, sql, -1, &stmt, nil) == SQLITE_OK {
        if sqlite3_step(stmt) == SQLITE_ROW {
            let result = sqlite3_column_int(stmt, 0)
            print("Query Result: \(result)")
        } else {
            print("Query returned no results.")
        }
    } else {
        let errorMessage = String(cString: sqlite3_errmsg(db))
        print("Query is not prepared \(errorMessage)")
    }
}

sqlite3_finalize(stmnt)
let insertStr = "INSERT INTO \(tableName) (id, title, descr, geom) " +
        "VALUES ( ?, ?, ?, ST_GeomFromText('POINT(126.0 -34.0)', 4326) );"
print(insertStr)
var stmnt2: OpaquePointer?
if sqlite3_prepare_v2(db, insertStr, -1, &stmnt2, nil) == SQLITE_OK {
    let id: Int32 = 0
    let title: NSString = "Pelican"
    let descr: NSString = "Likes fish"
    sqlite3_bind_int(stmnt2, 1, id)
    sqlite3_bind_text(stmnt2, 2, title.utf8String, -1, nil)
    sqlite3_bind_text(stmnt2, 3, descr.utf8String, -1, nil)
    if sqlite3_step(stmnt2) == SQLITE_DONE {
        print("Inserted row.")
    } else {
      print("Could not insert row.")
    }
} else {
    print("Insert statement not prepared.")
}
sqlite3_finalize(stmnt2)

var queryStr = "SELECT id, title, descr, ST_AsText(geom) AS wkt FROM features;"
print(queryStr)
var stmnt3: OpaquePointer?
if sqlite3_prepare_v2(db, queryStr, -1, &stmnt3, nil) == SQLITE_OK {
    if sqlite3_step(stmnt3) == SQLITE_ROW {
        let id = sqlite3_column_int(stmnt3, 0)
        guard let result = sqlite3_column_text(stmnt3, 1) else {
            print("Query result is nil")
            return
        }
        let title = String(cString: result)
        let descr = String(cString: sqlite3_column_text(stmnt3, 2))
        let wkt = String(cString: sqlite3_column_text(stmnt3, 3))
        print("Query Result: \(id), \(title), \(descr), \(wkt)")
    } else {
        print("Query returned no results.")
    }
} else {
    let errorMessage = String(cString: sqlite3_errmsg(db))
    print("Query is not prepared \(errorMessage)")
}
sqlite3_finalize(stmnt3)

The output is ..

spatialtest6.sqlite
Spatialite version: 0x00000001047610e4
SpatiaLite version ..: 4.4.0-RC1	Supported Extensions:
	- 'VirtualShape'	[direct Shapefile access]
	- 'VirtualDbf'		[direct DBF access]
	- 'VirtualText'		[direct CSV/TXT access]
	- 'VirtualNetwork'	[Dijkstra shortest path]
	- 'RTree'		[Spatial Index - R*Tree]
	- 'MbrCache'		[Spatial Index - MBR cache]
	- 'VirtualSpatialIndex'	[R*Tree metahandler]
	- 'VirtualElementary'	[ElemGeoms metahandler]
	- 'VirtualKNN'	[K-Nearest Neighbors metahandler]
	- 'VirtualFDO'		[FDO-OGR interoperability]
	- 'VirtualGPKG'	[OGC GeoPackage interoperability]
	- 'VirtualBBox'		[BoundingBox tables]
	- 'SpatiaLite'		[Spatial SQL - OGC]
PROJ.4 version ......: Rel. 4.9.3, 15 August 2016
GEOS version ........: 3.6.1-CAPI-1.10.1 r0
TARGET CPU ..........: arm64-apple-darwin23.1.0
CREATE TABLE features (
id integer PRIMARY KEY,
title text,
descr text);
Table created.
SELECT InitSpatialMetaData('WGS84');
Query Result: 1
SELECT AddGeometryColumn('features', 'geom', 4326, 'POINT', 'XY');
Query Result: 1
SELECT CreateSpatialIndex('features', 'geom');
Query Result: 1
INSERT INTO features (id, title, descr, geom) VALUES ( ?, ?, ?, ST_GeomFromText('POINT(126.0 -34.0)', 4326) );
Inserted row.
SELECT id, title, descr, ST_AsText(geom) AS wkt FROM features;
Query Result: 0, Pelican, Likes fish, POINT(126 -34)

The only spatialite code just after the database open is ..

var spconnect: OpaquePointer?
spatialite_initialize()
print("Spatialite version: \(spatialite_version()!)")
spatialite_init_ex(db, &spconnect, 1)

After the connection is made to db (the pointer to the .sqlite database) then all the remaining code is sqlite3, but the SQL language includes all the spatialite functions and utilities - eg. 'SELECT InitSpatialMetaData();'

As you can see the sqlite3 code is fairly ugly. To be able to do this using GRDB would be great.

I guess you can, yes. Please report the results of your investigations!

I don't understand. I am asking if it can be done. For example, can the 'db' pointer created in sqlite3_open(fullname, &db) be exposed from a DatabaseQueue object so that I can run spatialite_init_ex(db, &spconnect, 1) - or is there more to it than that?

For example, can the 'db' pointer [...] be exposed from a DatabaseQueue object

Sure. This is mentioned in the main README, and also in the documentation.

For example, the beginning of your sample code could be translated this way:

let dbQueue = try DatabaseQueue(path: "...")

try dbQueue.inDatabase { db in
    spatialite_initialize()
    var spconnect: OpaquePointer?
    spatialite_init_ex(db.sqliteConnection, &spconnect, 1)

    try db.execute(sql: "CREATE TABLE ...")

    ...
}

I hope those hints will allow you to start exploring Spatialite with GRDB. I'm closing the issue. If you have further specific questions, please open new issues!

Thanks, Gwendal. Brilliant - just what I needed.

Thanks Gwendal, you asked for the result .. much, much nicer code - all fully working - use it in doco if you like ..

import SQLite3
import Spatial
import GRDB

// ------- other code -------

let fileManager = FileManager.default
let documentsUrl =  FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let docsPath = documentsUrl.path  // no trailing slash
let shortname =  "spatialtest.sqlite"
let tablename = shortname.replacingOccurrences(of: ".sqlite", with: "").lowercased()
let fullname = docsPath + "/" + shortname
   
do {
   print("Delete \(shortname)")
   try fileManager.removeItem(atPath: fullname)

   print("Create .sqlite file, initialize spatialite and connect to the database")
   let dbQueue = try DatabaseQueue(path: fullname)
   try dbQueue.inDatabase { db in
       spatialite_initialize()
       spatialite_alloc_connection()
       var spconnect: OpaquePointer?
       spatialite_init_ex(db.sqliteConnection, &spconnect, 1)
      
       print("Create \(tablename) table")
       try db.create(table: tablename, ifNotExists: true) { t in
           t.autoIncrementedPrimaryKey("ogc_fid")
           t.column("title", .text)
           t.column("descr", .text)
       }
       
       print("Create spatialite metadata structure")
       try db.execute(literal: "SELECT InitSpatialMetaData('WGS84');")
       
       print("Add POINT geometry field & create a spatial index for it")
       let sqlSpatial = [
               "SELECT AddGeometryColumn('\(tablename)', 'geom', 4326, 'POINT', 'XY');",
               "SELECT CreateSpatialIndex('\(tablename)', 'geom');"]
       for sql in sqlSpatial {
           try db.execute(sql: sql)
       }
       
       let insertStr = "INSERT INTO \(tablename) (title, descr, geom) " +
               "VALUES (?, ?, ST_GeomFromText('POINT(126.0 -34.0)', 4326) );"
       print(insertStr)
       try db.execute(sql: insertStr, arguments: ["Pelican", "Thermaling up high."])

       let sqlStr = "SELECT ogc_fid, title, descr, ST_AsText(geom) AS wkt FROM \(tablename);"
       print(sqlStr)
       let rows = try Row.fetchCursor(db, sql:sqlStr, arguments: [])
       while let row = try rows.next() {
           let id = row["ogc_fid"]
           let title = row["title"]
           let descr = row["descr"]
           let wkt = row["wkt"]
           print("ogc_fid=\(id!), title=\(title!), desc=\(descr!), geom=\(wkt!)")
       }
       
   }
} catch let error as NSError {
   let str = "\(error.debugDescription)"
   print(str)
   return
}
      

So it works 👍 Thanks for sharing Tybion :-)