Sqlite: Generalize invalid id handling

An id is now always saved as a null value and a null value is always
converted to an invalid id if you are requesting an id. It prevents
accidental conversion of null values into a id with the value of zero.
It prevent too writing of the value -1 for an id. This has two
advantages. Sqlite can optimize null values if there are only null
values following in a row. And with strict tables it is forbidden to use
a null value as a key. So there can be no accidential invalid ids
anymore in the database.

Change-Id: I5ec813f2fe8e686324eab6dd632c03d5007e093d
Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org>
Reviewed-by: <github-actions-qt-creator@cristianadam.eu>
Reviewed-by: Tim Jenssen <tim.jenssen@qt.io>
This commit is contained in:
Marco Bubke
2022-07-20 22:52:25 +02:00
parent 09b60ffec3
commit a5d44fb32b
4 changed files with 144 additions and 16 deletions

View File

@@ -137,13 +137,18 @@ void BaseStatement::step() const
next(); next();
} }
void BaseStatement::bind(int index, NullValue) void BaseStatement::bindNull(int index)
{ {
int resultCode = sqlite3_bind_null(m_compiledStatement.get(), index); int resultCode = sqlite3_bind_null(m_compiledStatement.get(), index);
if (resultCode != SQLITE_OK) if (resultCode != SQLITE_OK)
checkForBindingError(resultCode); checkForBindingError(resultCode);
} }
void BaseStatement::bind(int index, NullValue)
{
bindNull(index);
}
void BaseStatement::bind(int index, int value) void BaseStatement::bind(int index, int value)
{ {
int resultCode = sqlite3_bind_int(m_compiledStatement.get(), index, value); int resultCode = sqlite3_bind_int(m_compiledStatement.get(), index, value);

View File

@@ -89,6 +89,7 @@ public:
template<typename Type> template<typename Type>
Type fetchValue(int column) const; Type fetchValue(int column) const;
void bindNull(int index);
void bind(int index, NullValue); void bind(int index, NullValue);
void bind(int index, int value); void bind(int index, int value);
void bind(int index, long long value); void bind(int index, long long value);
@@ -106,7 +107,10 @@ public:
template<typename Type, typename = std::enable_if_t<Type::IsBasicId::value>> template<typename Type, typename = std::enable_if_t<Type::IsBasicId::value>>
void bind(int index, Type id) void bind(int index, Type id)
{ {
bind(index, &id); if (id)
bind(index, &id);
else
bindNull(index);
} }
template<typename Enumeration, std::enable_if_t<std::is_enum_v<Enumeration>, bool> = true> template<typename Enumeration, std::enable_if_t<std::is_enum_v<Enumeration>, bool> = true>
@@ -477,10 +481,14 @@ private:
typename = std::enable_if_t<ConversionType::IsBasicId::value>> typename = std::enable_if_t<ConversionType::IsBasicId::value>>
constexpr operator ConversionType() constexpr operator ConversionType()
{ {
if constexpr (std::is_same_v<typename ConversionType::DatabaseType, int>) if (statement.fetchType(column) == Type::Integer) {
return ConversionType::create(statement.fetchIntValue(column)); if constexpr (std::is_same_v<typename ConversionType::DatabaseType, int>)
else return ConversionType::create(statement.fetchIntValue(column));
return ConversionType::create(statement.fetchLongLongValue(column)); else
return ConversionType::create(statement.fetchLongLongValue(column));
}
return ConversionType{};
} }
template<typename Enumeration, std::enable_if_t<std::is_enum_v<Enumeration>, bool> = true> template<typename Enumeration, std::enable_if_t<std::is_enum_v<Enumeration>, bool> = true>

View File

@@ -2556,7 +2556,7 @@ public:
" UNION ALL " " UNION ALL "
" SELECT prototypeId, typeSelection.level+1 FROM types JOIN typeSelection " " SELECT prototypeId, typeSelection.level+1 FROM types JOIN typeSelection "
" USING(typeId) WHERE prototypeId IS NOT NULL)" " USING(typeId) WHERE prototypeId IS NOT NULL)"
"SELECT nullif(propertyTypeId, -1), propertyDeclarationId, propertyTraits " "SELECT propertyTypeId, propertyDeclarationId, propertyTraits "
" FROM propertyDeclarations JOIN typeSelection USING(typeId) " " FROM propertyDeclarations JOIN typeSelection USING(typeId) "
" WHERE name=?2 ORDER BY level LIMIT 1", " WHERE name=?2 ORDER BY level LIMIT 1",
database}; database};
@@ -2588,16 +2588,16 @@ public:
mutable ReadStatement<3> selectAllSourcesStatement{ mutable ReadStatement<3> selectAllSourcesStatement{
"SELECT sourceName, sourceContextId, sourceId FROM sources", database}; "SELECT sourceName, sourceContextId, sourceId FROM sources", database};
mutable ReadStatement<6, 1> selectTypeByTypeIdStatement{ mutable ReadStatement<6, 1> selectTypeByTypeIdStatement{
"SELECT sourceId, t.name, t.typeId, ifnull(prototypeId, -1), accessSemantics, pd.name " "SELECT sourceId, t.name, t.typeId, prototypeId, accessSemantics, pd.name "
"FROM types AS t LEFT JOIN propertyDeclarations AS pd " "FROM types AS t LEFT JOIN propertyDeclarations AS pd "
" ON defaultPropertyId=propertyDeclarationId WHERE t.typeId=?", " ON defaultPropertyId=propertyDeclarationId WHERE t.typeId=?",
database}; database};
mutable ReadStatement<4, 1> selectExportedTypesByTypeIdStatement{ mutable ReadStatement<4, 1> selectExportedTypesByTypeIdStatement{
"SELECT moduleId, name, ifnull(majorVersion, -1), ifnull(minorVersion, -1) FROM " "SELECT moduleId, name, majorVersion, minorVersion FROM "
"exportedTypeNames WHERE typeId=?", "exportedTypeNames WHERE typeId=?",
database}; database};
mutable ReadStatement<6> selectTypesStatement{ mutable ReadStatement<6> selectTypesStatement{
"SELECT sourceId, t.name, t.typeId, ifnull(prototypeId, -1), accessSemantics, pd.name " "SELECT sourceId, t.name, t.typeId, prototypeId, accessSemantics, pd.name "
"FROM types AS t LEFT JOIN propertyDeclarations AS pd " "FROM types AS t LEFT JOIN propertyDeclarations AS pd "
" ON defaultPropertyId=propertyDeclarationId", " ON defaultPropertyId=propertyDeclarationId",
database}; database};
@@ -2617,13 +2617,13 @@ public:
"DELETE FROM signalDeclarations WHERE typeId=?", database}; "DELETE FROM signalDeclarations WHERE typeId=?", database};
WriteStatement<1> deleteTypeStatement{"DELETE FROM types WHERE typeId=?", database}; WriteStatement<1> deleteTypeStatement{"DELETE FROM types WHERE typeId=?", database};
mutable ReadStatement<4, 1> selectPropertyDeclarationsByTypeIdStatement{ mutable ReadStatement<4, 1> selectPropertyDeclarationsByTypeIdStatement{
"SELECT name, nullif(propertyTypeId, -1), propertyTraits, (SELECT name FROM " "SELECT name, propertyTypeId, propertyTraits, (SELECT name FROM "
"propertyDeclarations WHERE propertyDeclarationId=pd.aliasPropertyDeclarationId) FROM " "propertyDeclarations WHERE propertyDeclarationId=pd.aliasPropertyDeclarationId) FROM "
"propertyDeclarations AS pd WHERE typeId=?", "propertyDeclarations AS pd WHERE typeId=?",
database}; database};
ReadStatement<6, 1> selectPropertyDeclarationsForTypeIdStatement{ ReadStatement<6, 1> selectPropertyDeclarationsForTypeIdStatement{
"SELECT name, propertyTraits, propertyTypeId, propertyImportedTypeNameId, " "SELECT name, propertyTraits, propertyTypeId, propertyImportedTypeNameId, "
"propertyDeclarationId, ifnull(aliasPropertyDeclarationId, -1) FROM propertyDeclarations " "propertyDeclarationId, aliasPropertyDeclarationId FROM propertyDeclarations "
"WHERE typeId=? ORDER BY name", "WHERE typeId=? ORDER BY name",
database}; database};
ReadWriteStatement<1, 5> insertPropertyDeclarationStatement{ ReadWriteStatement<1, 5> insertPropertyDeclarationStatement{
@@ -2756,7 +2756,7 @@ public:
"name=?3", "name=?3",
database}; database};
mutable ReadStatement<5, 2> selectDocumentImportForSourceIdStatement{ mutable ReadStatement<5, 2> selectDocumentImportForSourceIdStatement{
"SELECT importId, sourceId, moduleId, ifnull(majorVersion, -1), ifnull(minorVersion, -1) " "SELECT importId, sourceId, moduleId, majorVersion, minorVersion "
"FROM documentImports WHERE sourceId IN carray(?1) AND kind=?2 ORDER BY sourceId, " "FROM documentImports WHERE sourceId IN carray(?1) AND kind=?2 ORDER BY sourceId, "
"moduleId, majorVersion, minorVersion", "moduleId, majorVersion, minorVersion",
database}; database};
@@ -2803,7 +2803,7 @@ public:
database}; database};
ReadStatement<5, 1> selectAliasPropertiesDeclarationForPropertiesWithTypeIdStatement{ ReadStatement<5, 1> selectAliasPropertiesDeclarationForPropertiesWithTypeIdStatement{
"SELECT alias.typeId, alias.propertyDeclarationId, alias.propertyImportedTypeNameId, " "SELECT alias.typeId, alias.propertyDeclarationId, alias.propertyImportedTypeNameId, "
"alias.aliasPropertyDeclarationId, ifnull(alias.aliasPropertyDeclarationTailId, -1) FROM " "alias.aliasPropertyDeclarationId, alias.aliasPropertyDeclarationTailId FROM "
"propertyDeclarations AS alias JOIN propertyDeclarations AS target ON " "propertyDeclarations AS alias JOIN propertyDeclarations AS target ON "
"alias.aliasPropertyDeclarationId=target.propertyDeclarationId OR " "alias.aliasPropertyDeclarationId=target.propertyDeclarationId OR "
"alias.aliasPropertyDeclarationTailId=target.propertyDeclarationId WHERE " "alias.aliasPropertyDeclarationTailId=target.propertyDeclarationId WHERE "
@@ -3074,9 +3074,9 @@ public:
" USING(typeId) ORDER BY name", " USING(typeId) ORDER BY name",
database}; database};
mutable ReadStatement<2> selectTypesWithDefaultPropertyStatement{ mutable ReadStatement<2> selectTypesWithDefaultPropertyStatement{
"SELECT typeId, ifnull(defaultPropertyId, -1) FROM types ORDER BY typeId", database}; "SELECT typeId, defaultPropertyId FROM types ORDER BY typeId", database};
WriteStatement<2> updateDefaultPropertyIdStatement{ WriteStatement<2> updateDefaultPropertyIdStatement{
"UPDATE types SET defaultPropertyId=nullif(?2, -1) WHERE typeId=?1", database}; "UPDATE types SET defaultPropertyId=?2 WHERE typeId=?1", database};
WriteStatement<1> updateDefaultPropertyIdToNullStatement{ WriteStatement<1> updateDefaultPropertyIdToNullStatement{
"UPDATE types SET defaultPropertyId=NULL WHERE defaultPropertyId=?1", database}; "UPDATE types SET defaultPropertyId=NULL WHERE defaultPropertyId=?1", database};
mutable ReadStatement<1, 1> selectInfoTypeByTypeIdStatement{ mutable ReadStatement<1, 1> selectInfoTypeByTypeIdStatement{

View File

@@ -53,6 +53,11 @@ using Sqlite::ReadWriteStatement;
using Sqlite::Value; using Sqlite::Value;
using Sqlite::WriteStatement; using Sqlite::WriteStatement;
enum class BasicIdEnumeration { TestId };
using TestLongLongId = Sqlite::BasicId<BasicIdEnumeration::TestId, long long>;
using TestIntId = Sqlite::BasicId<BasicIdEnumeration::TestId, int>;
template<typename Type> template<typename Type>
bool compareValue(SqliteTestStatement<2, 1> &statement, Type value, int column) bool compareValue(SqliteTestStatement<2, 1> &statement, Type value, int column)
{ {
@@ -237,6 +242,18 @@ TEST_F(SqliteStatement, BindNull)
database.execute("INSERT INTO test VALUES (NULL, 323, 344)"); database.execute("INSERT INTO test VALUES (NULL, 323, 344)");
SqliteTestStatement<2, 1> statement("SELECT name, number FROM test WHERE name IS ?", database); SqliteTestStatement<2, 1> statement("SELECT name, number FROM test WHERE name IS ?", database);
statement.bindNull(1);
statement.next();
ASSERT_TRUE(statement.fetchValueView(0).isNull());
ASSERT_THAT(statement.fetchValue<int>(1), 323);
}
TEST_F(SqliteStatement, BindNullValue)
{
database.execute("INSERT INTO test VALUES (NULL, 323, 344)");
SqliteTestStatement<2, 1> statement("SELECT name, number FROM test WHERE name IS ?", database);
statement.bind(1, Sqlite::NullValue{}); statement.bind(1, Sqlite::NullValue{});
statement.next(); statement.next();
@@ -244,6 +261,60 @@ TEST_F(SqliteStatement, BindNull)
ASSERT_THAT(statement.fetchValue<int>(1), 323); ASSERT_THAT(statement.fetchValue<int>(1), 323);
} }
TEST_F(SqliteStatement, BindInvalidIntIdToNull)
{
TestIntId id;
SqliteTestStatement<0, 1> statement("INSERT INTO test VALUES ('id', 323, ?)", database);
statement.bind(1, id);
statement.next();
SqliteTestStatement<1, 1> readStatement("SELECT value FROM test WHERE name='id'", database);
readStatement.next();
ASSERT_THAT(readStatement.fetchType(0), Sqlite::Type::Null);
}
TEST_F(SqliteStatement, BindIntId)
{
TestIntId id{TestIntId::create(42)};
SqliteTestStatement<0, 1> statement("INSERT INTO test VALUES ('id', 323, ?)", database);
statement.bind(1, id);
statement.next();
SqliteTestStatement<1, 1> readStatement("SELECT value FROM test WHERE name='id'", database);
readStatement.next();
ASSERT_THAT(readStatement.fetchType(0), Sqlite::Type::Integer);
ASSERT_THAT(readStatement.fetchIntValue(0), 42);
}
TEST_F(SqliteStatement, BindInvalidLongLongIdToNull)
{
TestLongLongId id;
SqliteTestStatement<0, 1> statement("INSERT INTO test VALUES ('id', 323, ?)", database);
statement.bind(1, id);
statement.next();
SqliteTestStatement<1, 1> readStatement("SELECT value FROM test WHERE name='id'", database);
readStatement.next();
ASSERT_THAT(readStatement.fetchType(0), Sqlite::Type::Null);
}
TEST_F(SqliteStatement, BindLongLongId)
{
TestLongLongId id{TestLongLongId::create(42)};
SqliteTestStatement<0, 1> statement("INSERT INTO test VALUES ('id', 323, ?)", database);
statement.bind(1, id);
statement.next();
SqliteTestStatement<1, 1> readStatement("SELECT value FROM test WHERE name='id'", database);
readStatement.next();
ASSERT_THAT(readStatement.fetchType(0), Sqlite::Type::Integer);
ASSERT_THAT(readStatement.fetchIntValue(0), 42);
}
TEST_F(SqliteStatement, BindString) TEST_F(SqliteStatement, BindString)
{ {
SqliteTestStatement<2, 1> statement("SELECT name, number FROM test WHERE name=?", database); SqliteTestStatement<2, 1> statement("SELECT name, number FROM test WHERE name=?", database);
@@ -1158,6 +1229,50 @@ TEST_F(SqliteStatement, GetTupleValueAndMultipleQueryValue)
ASSERT_THAT(value, Eq(Tuple{"bar", "blah", 1})); ASSERT_THAT(value, Eq(Tuple{"bar", "blah", 1}));
} }
TEST_F(SqliteStatement, GetSingleInvalidLongLongId)
{
TestLongLongId id;
WriteStatement<1>("INSERT INTO test VALUES ('id', 323, ?)", database).write(id);
ReadStatement<1, 0> statement("SELECT value FROM test WHERE name='id'", database);
auto value = statement.value<TestLongLongId>();
ASSERT_FALSE(value.isValid());
}
TEST_F(SqliteStatement, GetSingleLongLongId)
{
TestLongLongId id{TestLongLongId::create(42)};
WriteStatement<1>("INSERT INTO test VALUES ('id', 323, ?)", database).write(id);
ReadStatement<1, 0> statement("SELECT value FROM test WHERE name='id'", database);
auto value = statement.value<TestLongLongId>();
ASSERT_THAT(&value, Eq(42));
}
TEST_F(SqliteStatement, GetSingleInvalidIntId)
{
TestIntId id;
WriteStatement<1>("INSERT INTO test VALUES ('id', 323, ?)", database).write(id);
ReadStatement<1, 0> statement("SELECT value FROM test WHERE name='id'", database);
auto value = statement.value<TestIntId>();
ASSERT_FALSE(value.isValid());
}
TEST_F(SqliteStatement, GetSingleIntId)
{
TestIntId id{TestIntId::create(42)};
WriteStatement<1>("INSERT INTO test VALUES ('id', 323, ?)", database).write(id);
ReadStatement<1, 0> statement("SELECT value FROM test WHERE name='id'", database);
auto value = statement.value<TestIntId>();
ASSERT_THAT(&value, Eq(42));
}
TEST_F(SqliteStatement, GetValueCallsReset) TEST_F(SqliteStatement, GetValueCallsReset)
{ {
struct Value struct Value