Skip to content

Commit

Permalink
SpatiaLite support for importing layers with binary fields
Browse files Browse the repository at this point in the history
Fixes #36705

(cherry picked from commit ad5b8f8)
  • Loading branch information
audun authored and nyalldawson committed Jun 2, 2020
1 parent ee0e5e9 commit 8540575
Show file tree
Hide file tree
Showing 3 changed files with 147 additions and 3 deletions.
8 changes: 8 additions & 0 deletions src/providers/spatialite/qgsspatialitefeatureiterator.cpp
Expand Up @@ -588,6 +588,14 @@ QVariant QgsSpatiaLiteFeatureIterator::getFeatureAttribute( sqlite3_stmt *stmt,
return sqlite3_column_double( stmt, ic );
}

if ( sqlite3_column_type( stmt, ic ) == SQLITE_BLOB )
{
// BLOB value
int blob_size = sqlite3_column_bytes( stmt, ic );
const char *blob = static_cast<const char *>( sqlite3_column_blob( stmt, ic ) );
return QByteArray( blob, blob_size );
}

if ( sqlite3_column_type( stmt, ic ) == SQLITE_TEXT )
{
// TEXT value
Expand Down
89 changes: 87 additions & 2 deletions src/providers/spatialite/qgsspatialiteprovider.cpp
Expand Up @@ -116,6 +116,12 @@ bool QgsSpatiaLiteProvider::convertField( QgsField &field )
break;
}

case QVariant::ByteArray:
fieldType = QStringLiteral( "BLOB" );
fieldSize = -1;
fieldPrec = -1;
break;

default:
return false;
}
Expand Down Expand Up @@ -688,6 +694,10 @@ static TypeSubType getVariantType( const QString &type )
{
return TypeSubType( QVariant::List, QVariant::Invalid );
}
else if ( type == QLatin1String( "blob" ) )
{
return TypeSubType( QVariant::ByteArray, QVariant::Invalid );
}
else if ( type == QLatin1String( "timestamp" ) ||
type == QLatin1String( "datetime" ) )
{
Expand Down Expand Up @@ -739,6 +749,11 @@ void QgsSpatiaLiteProvider::loadFieldsAbstractInterface( gaiaVectorLayerPtr lyr
fieldType = QVariant::Double;
type = "DOUBLE";
}
if ( fld->BlobValuesCount != 0 )
{
fieldType = QVariant::ByteArray;
type = "BLOB";
}
mAttributeFields.append( QgsField( name, fieldType, type, 0, 0, QString() ) );
}
fld = fld->Next;
Expand Down Expand Up @@ -4241,6 +4256,12 @@ bool QgsSpatiaLiteProvider::addFeatures( QgsFeatureList &flist, Flags flags )
QByteArray ba = stringVal.toUtf8();
sqlite3_bind_text( stmt, ++ia, ba.constData(), ba.size(), SQLITE_TRANSIENT );
}
else if ( type == QVariant::ByteArray )
{
// binding a BLOB value
const QByteArray ba = v.toByteArray();
sqlite3_bind_blob( stmt, ++ia, ba.constData(), ba.size(), SQLITE_TRANSIENT );
}
else if ( type == QVariant::StringList || type == QVariant::List )
{
const QByteArray ba = QgsJsonUtils::encodeValue( v ).toUtf8();
Expand Down Expand Up @@ -4562,6 +4583,10 @@ bool QgsSpatiaLiteProvider::changeAttributeValues( const QgsChangedAttributesMap
for ( QgsChangedAttributesMap::const_iterator iter = attr_map.begin(); iter != attr_map.end(); ++iter )
{
// Loop over all changed features
//
// For each changed feature, create an update string like
// "UPDATE table SET simple_column=23.5, complex_column=? WHERE primary_key=fid"
// On any update failure, changes to all features will be rolled back

QgsFeatureId fid = iter.key();

Expand All @@ -4576,10 +4601,13 @@ bool QgsSpatiaLiteProvider::changeAttributeValues( const QgsChangedAttributesMap
QString sql = QStringLiteral( "UPDATE %1 SET " ).arg( QgsSqliteUtils::quotedIdentifier( mTableName ) );
bool first = true;

// keep track of map of parameter index to value
QMap<int, QVariant> bindings;
int bind_parameter_idx = 1;

// cycle through the changed attributes of the feature
for ( QgsAttributeMap::const_iterator siter = attrs.begin(); siter != attrs.end(); ++siter )
{
// Loop over all changed attributes
try
{
QgsField fld = field( siter.key() );
Expand Down Expand Up @@ -4626,6 +4654,12 @@ bool QgsSpatiaLiteProvider::changeAttributeValues( const QgsChangedAttributesMap
return false;
}
}
else if ( type == QVariant::ByteArray )
{
// binding a BLOB value
sql += QStringLiteral( "%1=?" ).arg( QgsSqliteUtils::quotedIdentifier( fld.name() ) );
bindings[ bind_parameter_idx++ ] = val;
}
else if ( type == QVariant::DateTime )
{
sql += QStringLiteral( "%1=%2" ).arg( QgsSqliteUtils::quotedIdentifier( fld.name() ), QgsSqliteUtils::quotedString( val.toDateTime().toString( QStringLiteral( "yyyy-MM-ddThh:mm:ss" ) ) ) );
Expand All @@ -4647,9 +4681,60 @@ bool QgsSpatiaLiteProvider::changeAttributeValues( const QgsChangedAttributesMap
}
sql += QStringLiteral( " WHERE %1=%2" ).arg( QgsSqliteUtils::quotedIdentifier( mPrimaryKey ) ).arg( fid );

ret = exec_sql( sql.toUtf8(), errMsg );
// prepare SQLite statement
sqlite3_stmt *stmt = nullptr;
ret = sqlite3_prepare_v2( sqliteHandle( ), sql.toUtf8().constData(), -1, &stmt, nullptr );
if ( ret != SQLITE_OK )
{
// some unexpected error occurred during preparation
const char *err = sqlite3_errmsg( sqliteHandle( ) );
errMsg = static_cast<char *>( sqlite3_malloc( strlen( err ) + 1 ) );
strcpy( errMsg, err );
handleError( sql, errMsg, savepointId );
return false;
}

// bind variables not handled directly in the string
for ( auto i = bindings.cbegin(); i != bindings.cend(); ++i )
{
int parameter_idx = i.key();
const QVariant val = i.value();
switch ( val.type() )
{
case QVariant::ByteArray:
{
const QByteArray ba = val.toByteArray();
sqlite3_bind_blob( stmt, parameter_idx, ba.constData(), ba.size(), SQLITE_TRANSIENT );
break;
}

default:
// This will only happen if the above code is changed to bind more types,
// but the programmer has forgotten to handle the type here. Fatal error.
sqlite3_finalize( stmt );
Q_ASSERT( false );
}

if ( ret != SQLITE_OK )
{
// some unexpected error occurred during binding
const char *err = sqlite3_errmsg( sqliteHandle( ) );
errMsg = static_cast<char *>( sqlite3_malloc( strlen( err ) + 1 ) );
strcpy( errMsg, err );
handleError( sql, errMsg, savepointId );
sqlite3_finalize( stmt );
return false;
}
}

ret = sqlite3_step( stmt );
sqlite3_finalize( stmt );
if ( ret != SQLITE_DONE )
{
// some unexpected error occurred during execution of update query
const char *err = sqlite3_errmsg( sqliteHandle( ) );
errMsg = static_cast<char *>( sqlite3_malloc( strlen( err ) + 1 ) );
strcpy( errMsg, err );
handleError( sql, errMsg, savepointId );
return false;
}
Expand Down
53 changes: 52 additions & 1 deletion tests/src/python/test_provider_spatialite.py
Expand Up @@ -40,7 +40,7 @@
from qgis.testing import start_app, unittest
from utilities import unitTestDataPath
from providertestbase import ProviderTestCase
from qgis.PyQt.QtCore import QObject, QVariant
from qgis.PyQt.QtCore import QObject, QVariant, QByteArray

from qgis.utils import spatialite_connect

Expand Down Expand Up @@ -270,6 +270,17 @@ def setUpClass(cls):
sql = "SELECT AddGeometryColumn('unique_not_null_constraints', 'geometry', 4326, 'POINT', 'XY')"
cur.execute(sql)

# blob test table
sql = "CREATE TABLE blob_table ( id INTEGER NOT NULL PRIMARY KEY, fld1 BLOB )"
cur.execute(sql)
sql = """
INSERT INTO blob_table VALUES
(1, X'0053514C697465'),
(2, NULL),
(3, X'53514C697465')
"""
cur.execute(sql)

# Commit all test data
cur.execute("COMMIT")
con.close()
Expand Down Expand Up @@ -1451,6 +1462,46 @@ def testAddFeatureNoFields(self):
self.assertEqual(vl.getFeature(
1).geometry().asWkt().upper(), 'POINT (9 45)')

def testBLOBType(self):
"""Test binary field"""
vl = QgsVectorLayer('dbname=%s table="blob_table" sql=' % self.dbname, "testBLOBType", "spatialite")
self.assertTrue(vl.isValid())

fields = vl.dataProvider().fields()
self.assertEqual(fields.at(fields.indexFromName('fld1')).type(), QVariant.ByteArray)

values = {feat['id']: feat['fld1'] for feat in vl.getFeatures()}
expected = {
1: QByteArray(b'\x00SQLite'),
2: QByteArray(),
3: QByteArray(b'SQLite')
}
self.assertEqual(values, expected)

# change attribute value
self.assertTrue(vl.dataProvider().changeAttributeValues(
{1: {1: QByteArray(b'bbbvx')}}))
values = {feat['id']: feat['fld1'] for feat in vl.getFeatures()}
expected = {
1: QByteArray(b'bbbvx'),
2: QByteArray(),
3: QByteArray(b'SQLite')
}
self.assertEqual(values, expected)

# add feature
f = QgsFeature()
f.setAttributes([4, QByteArray(b'cccc')])
self.assertTrue(vl.dataProvider().addFeature(f))
values = {feat['id']: feat['fld1'] for feat in vl.getFeatures()}
expected = {
1: QByteArray(b'bbbvx'),
2: QByteArray(),
3: QByteArray(b'SQLite'),
4: QByteArray(b'cccc')
}
self.assertEqual(values, expected)

def testTransaction(self):
"""Test spatialite transactions"""

Expand Down

0 comments on commit 8540575

Please sign in to comment.