Merge pull request #8722 from m-kuhn/expression_function_sqlite_fetch_and_increment

Expression function sqlite_fetch_and_increment
This commit is contained in:
Matthias Kuhn 2018-12-24 12:07:12 +01:00 committed by GitHub
commit d2b35753be
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 175 additions and 1 deletions

View File

@ -0,0 +1,16 @@
{
"name": "sqlite_fetch_and_increment",
"type": "function",
"description": "Manage autoincrementing values in sqlite databases.<p>SQlite default values can only be applied on insert and not prefetched.</p><p>This makes it impossible to acquire an incremented primary key via AUTO_INCREMENT before creating the row in the database. Sidenote: with postgres, this works via the option <i>evaluate default values</i>.</p><p>When adding new features with relations, it is really nice to be able to already add children for a parent, while the parents form is still open and hence the parent feature uncommitted.</p><p>To get around this limitation, this function can be used to manage sequence values in a separate table on sqlite based formats like gpkg.</p><p>The sequence table will be filtered for a sequence id (filter_attribute and filter_value) and the current value of the id_field will be incremented by 1 ond the incremented value returned.</p><p>If additional columns require values to be specified, the default_value map can be used for this purpose.</p><p><b>Note</b><br/>This function modifies the target sqlite table. It is intended for usage with default value configurations for attributes.</p>",
"arguments": [
{"arg":"database", "description":"Path to the sqlite file"},
{"arg":"table", "description":"Name of the table that manages the sequences"},
{"arg":"id_field", "description":"Name of the field that contains the current value"},
{"arg":"filter_attribute", "description":"Name the field that contains a unique identifier for this sequence. Must have a UNIQUE index."},
{"arg":"filter_value", "description":"Name of the sequence to use."},
{"arg":"default_values", "description":"Map with default values for additional columns on the table. The values need to be fully quoted. Functions are allowed.", "optional": true}
],
"examples": [
{ "expression":"sqlite_fetch_and_increment(layer_property(@layer, 'path'), 'sequence_table', 'last_unique_id', 'sequence_id', 'global', map('last_change','date(''now'')','user','''' || @user_account_name || ''''))", "returns":"0"}
]
}

View File

@ -47,6 +47,7 @@
#include "qgsfieldformatter.h"
#include "qgsvectorlayerfeatureiterator.h"
#include "qgsproviderregistry.h"
#include "sqlite3.h"
const QString QgsExpressionFunction::helpText() const
{
@ -1360,6 +1361,82 @@ static QVariant fcnNumSelected( const QVariantList &values, const QgsExpressionC
return layer->selectedFeatureCount();
}
static QVariant fcnSqliteFetchAndIncrement( const QVariantList &values, const QgsExpressionContext *, QgsExpression *parent, const QgsExpressionNodeFunction * )
{
const QString database = values.at( 0 ).toString();
const QString table = values.at( 1 ).toString();
const QString idColumn = values.at( 2 ).toString();
const QString filterAttribute = values.at( 3 ).toString();
const QVariant filterValue = values.at( 4 ).toString();
const QVariantMap defaultValues = values.at( 5 ).toMap();
// read from database
sqlite3_database_unique_ptr sqliteDb;
sqlite3_statement_unique_ptr sqliteStatement;
if ( sqliteDb.open_v2( database, SQLITE_OPEN_READWRITE, nullptr ) != SQLITE_OK )
{
parent->setEvalErrorString( QObject::tr( "Could not open sqlite database %1. Error %2. " ).arg( database, sqliteDb.errorMessage() ) );
return QVariant();
}
QString currentValSql;
currentValSql = QStringLiteral( "SELECT %1 FROM %2" ).arg( QgsSqliteUtils::quotedIdentifier( idColumn ), QgsSqliteUtils::quotedIdentifier( table ) );
if ( !filterAttribute.isNull() )
{
currentValSql += QStringLiteral( " WHERE %1 = %2" ).arg( QgsSqliteUtils::quotedIdentifier( filterAttribute ), QgsSqliteUtils::quotedValue( filterValue ) );
}
int result;
sqliteStatement = sqliteDb.prepare( currentValSql, result );
if ( result == SQLITE_OK )
{
qlonglong nextId = 0;
if ( sqliteStatement.step() == SQLITE_ROW )
{
nextId = sqliteStatement.columnAsInt64( 0 ) + 1;
}
QString upsertSql;
upsertSql = QStringLiteral( "INSERT OR REPLACE INTO %1" ).arg( QgsSqliteUtils::quotedIdentifier( table ) );
QStringList cols;
QStringList vals;
cols << QgsSqliteUtils::quotedIdentifier( idColumn );
vals << QgsSqliteUtils::quotedValue( nextId );
if ( !filterAttribute.isNull() )
{
cols << QgsSqliteUtils::quotedIdentifier( filterAttribute );
vals << QgsSqliteUtils::quotedValue( filterValue );
}
for ( QVariantMap::const_iterator iter = defaultValues.constBegin(); iter != defaultValues.constEnd(); ++iter )
{
cols << QgsSqliteUtils::quotedIdentifier( iter.key() );
vals << iter.value().toString();
}
upsertSql += QLatin1String( " (" ) + cols.join( ',' ) + ')';
upsertSql += QLatin1String( " VALUES " );
upsertSql += '(' + vals.join( ',' ) + ')';
QString errorMessage;
result = sqliteDb.exec( upsertSql, errorMessage );
if ( result == SQLITE_OK )
{
return nextId;
}
else
{
parent->setEvalErrorString( QStringLiteral( "Could not increment value: SQLite error: \"%1\" (%2)." ).arg( errorMessage, QString::number( result ) ) );
return QVariant();
}
}
return QVariant(); // really?
}
static QVariant fcnConcat( const QVariantList &values, const QgsExpressionContext *, QgsExpression *parent, const QgsExpressionNodeFunction * )
{
QString concat;
@ -4916,6 +4993,20 @@ const QList<QgsExpressionFunction *> &QgsExpression::Functions()
QSet<QString>()
);
sFunctions
<< new QgsStaticExpressionFunction(
QStringLiteral( "sqlite_fetch_and_increment" ),
QgsExpressionFunction::ParameterList()
<< QgsExpressionFunction::Parameter( QStringLiteral( "database" ) )
<< QgsExpressionFunction::Parameter( QStringLiteral( "table" ) )
<< QgsExpressionFunction::Parameter( QStringLiteral( "id_field" ) )
<< QgsExpressionFunction::Parameter( QStringLiteral( "filter_attribute" ) )
<< QgsExpressionFunction::Parameter( QStringLiteral( "filter_value" ) )
<< QgsExpressionFunction::Parameter( QStringLiteral( "default_values" ), true ),
fcnSqliteFetchAndIncrement,
QStringLiteral( "Record and Attributes" )
);
// **Fields and Values** functions
QgsStaticExpressionFunction *representValueFunc = new QgsStaticExpressionFunction( QStringLiteral( "represent_value" ), QgsExpressionFunction::ParameterList() << QgsExpressionFunction::Parameter( QStringLiteral( "attribute" ) ) << QgsExpressionFunction::Parameter( QStringLiteral( "field_name" ), true ), fcnRepresentValue, QStringLiteral( "Record and Attributes" ) );

View File

@ -92,6 +92,21 @@ sqlite3_statement_unique_ptr sqlite3_database_unique_ptr::prepare( const QString
return s;
}
int sqlite3_database_unique_ptr::exec( const QString &sql, QString &errorMessage ) const
{
char *errMsg;
int ret = sqlite3_exec( get(), sql.toUtf8(), nullptr, nullptr, &errMsg );
if ( errMsg )
{
errorMessage = QString::fromUtf8( errMsg );
sqlite3_free( errMsg );
}
return ret;
}
QString QgsSqliteUtils::quotedString( const QString &value )
{
if ( value.isNull() )

View File

@ -21,6 +21,8 @@
#define SIP_NO_FILE
#include "qgis_core.h"
#include "qgis_sip.h"
#include <memory>
#include <QString>
@ -135,8 +137,17 @@ class CORE_EXPORT sqlite3_database_unique_ptr : public std::unique_ptr< sqlite3,
* Prepares a \a sql statement, returning the result. The \a resultCode
* argument will be filled with the sqlite3 result code.
*/
sqlite3_statement_unique_ptr prepare( const QString &sql, int &resultCode ) const;
sqlite3_statement_unique_ptr prepare( const QString &sql, int &resultCode SIP_OUT ) const;
/**
* Executes the \a sql command in the database. Multiple sql queries can be run within
* one single command.
* Errors are reported to \a errorMessage.
* Returns SQLITE_OK in case of success or an sqlite error code.
*
* \since QGIS 3.6
*/
int exec( const QString &sql, QString &errorMessage SIP_OUT ) const;
};
/**

View File

@ -1556,6 +1556,47 @@ class TestQgsExpression: public QObject
}
}
void test_sqliteFetchAndIncrement()
{
QTemporaryDir dir;
QString testGpkgName = QStringLiteral( "humanbeings.gpkg" );
QFile::copy( QStringLiteral( TEST_DATA_DIR ) + '/' + testGpkgName, dir.filePath( testGpkgName ) );
QgsExpressionContext context;
QgsExpressionContextScope *scope = new QgsExpressionContextScope();
scope->setVariable( QStringLiteral( "test_database" ), dir.filePath( testGpkgName ) );
scope->setVariable( QStringLiteral( "username" ), "some_username" );
context << scope;
// Test database file does not exist
QgsExpression exp1( QStringLiteral( "sqlite_fetch_and_increment('/path/does/not/exist', 'T_KEY_OBJECT', 'T_LastUniqueId', 'T_Key', 'T_Id')" ) );
exp1.evaluate( &context );
QCOMPARE( exp1.hasEvalError(), true );
const QString evalErrorString1 = exp1.evalErrorString();
QVERIFY2( evalErrorString1.contains( "/path/does/not/exist" ), QStringLiteral( "Path not found in %1" ).arg( evalErrorString1 ).toUtf8().constData() );
QVERIFY2( evalErrorString1.contains( "Error" ), QStringLiteral( "\"Error\" not found in %1" ).arg( evalErrorString1 ).toUtf8().constData() );
// Test default values are not properly quoted
QgsExpression exp2( QStringLiteral( "sqlite_fetch_and_increment(@test_database, 'T_KEY_OBJECT', 'T_LastUniqueId', 'T_Key', 'T_Id', map('T_LastChange','date(''now'')','T_CreateDate','date(''now'')','T_User', @username))" ) );
exp2.evaluate( &context );
QCOMPARE( exp2.hasEvalError(), true );
const QString evalErrorString2 = exp2.evalErrorString();
QVERIFY2( evalErrorString2.contains( "some_username" ), QStringLiteral( "'some_username' not found in '%1'" ).arg( evalErrorString2 ).toUtf8().constData() );
// Test incrementation logic
QgsExpression exp( QStringLiteral( "sqlite_fetch_and_increment(@test_database, 'T_KEY_OBJECT', 'T_LastUniqueId', 'T_Key', 'T_Id', map('T_LastChange','date(''now'')','T_CreateDate','date(''now'')','T_User','''me'''))" ) );
QVariant res = exp.evaluate( &context );
QCOMPARE( res.toInt(), 0 );
res = exp.evaluate( &context );
if ( exp.hasEvalError() )
qDebug() << exp.evalErrorString();
QCOMPARE( exp.hasEvalError(), false );
QCOMPARE( res.toInt(), 1 );
}
void aggregate_data()
{
QTest::addColumn<QString>( "string" );

BIN
tests/testdata/humanbeings.gpkg vendored Normal file

Binary file not shown.