1

I'm getting a curious error when inserting a record into a SQLite table. This is from the logs of a customer:

Microsoft.Data.Sqlite.SqliteException: SQLite Error 19: 'NOT NULL constraint failed: Events.details'.
   at Microsoft.Data.Sqlite.SqliteException.ThrowExceptionForRC(Int32 rc, sqlite3 db)
   at Microsoft.Data.Sqlite.SqliteDataReader.NextResult()
   at Microsoft.Data.Sqlite.SqliteCommand.ExecuteReader(CommandBehavior behavior)
   at Microsoft.Data.Sqlite.SqliteCommand.ExecuteReader()
   at Microsoft.Data.Sqlite.SqliteCommand.ExecuteNonQuery()
   at SqliteDemo.LogEvent(...) in ...

The pertinent bit of diagnostics is obviously this:

SQLite Error 19: 'NOT NULL constraint failed: Events.details'

…which seems clear enough, except for one thing: The details column lacks a NOT NULL constraint:

CREATE TABLE IF NOT EXISTS Events (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name STRING NOT NULL,
    time INTEGER NOT NULL,
    code INTEGER NOT NULL,
    details STRING,
    message STRING NOT NULL,
    status INTEGER NOT NULL )

I don't know the values that failed to insert, so an obvious next step would be to improve my diagnostics and wait for it to happen again. Or I could just add null guards to make sure it never happens again and forget about the whole thing.

But I'm curious:

  • Is it possible that SQLite's diagnostics are in error, and that it is one of the NOT NULL columns that is the actual culprit?
  • Why don't my unit tests have a problem with null detail values?
  • Can this be related to SQLite's dynamic approach to typing?
  • …or something completely different?

Any ideas?

Environment

  • Targeting .NET Framework 4.8 on Windows 10
  • Using Microsoft.Data.Sqlite 5.0.14
    (not System.Data.SQLite – though I'm unsure whether the distinction is relevant).

Code

I can't post our production code here. Instead, here is a complete, stand-alone C# program that I believe closely parallels the production code in all pertinent aspects.

using System;
using Microsoft.Data.Sqlite;

class SqliteDemo
{
    static void Main()
    {
        SqliteDemo demo = new SqliteDemo();
        demo.LogEvent("name 1", DateTime.Now, 42, "details", "message 1", 0);
        demo.LogEvent("name 2", DateTime.Now, 42, null, "message 2", 0); //*** Works...
    }

    readonly string connectionString = new SqliteConnectionStringBuilder
    {
        DataSource = "Test.db",
        Mode = SqliteOpenMode.ReadWriteCreate,
    }.ConnectionString;

    const string CreateTableSql =
        @"CREATE TABLE IF NOT EXISTS Events (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            name STRING NOT NULL,
            time INTEGER NOT NULL,
            code INTEGER NOT NULL,
            details STRING,
            message STRING NOT NULL,
            status INTEGER NOT NULL
        )";

    int LogEvent(string name, DateTime time,
            int code, string details, string message, short status)
    {
        using (SqliteConnection connection = CreateOpenConnection())
        {
            using (SqliteCommand command = CreateCommand(connection,
                "INSERT INTO Events(name, time, code, details, message, status) " +
                "VALUES(@name, @time, @code, @details, @message, @status)"))
            {
                AddParameter(command, "name", name);
                AddParameter(command, "time", time);
                AddParameter(command, "code", code);
                AddParameter(command, "details", details);
                AddParameter(command, "message", message);
                AddParameter(command, "status", status);

                return command.ExecuteNonQuery(); //*** SOMETIMES throws
            }
        }
    }

    static void AddParameter(SqliteCommand command, string name, object value)
    {
        command.Parameters.Add(new SqliteParameter(name, value ?? DBNull.Value));
    }

    static SqliteCommand CreateCommand(SqliteConnection connection, string sql)
    {
        SqliteCommand command = connection.CreateCommand();
        command.CommandText = sql;
        return command;
    }

    SqliteConnection CreateOpenConnection()
    {
        SqliteConnection connection = new SqliteConnection(connectionString);
        connection.Open();
        CreateTableIfNecessary(connection);
        return connection;
    }

    void CreateTableIfNecessary(SqliteConnection connection)
    {
        using (SqliteCommand command = CreateCommand(connection, CreateTableSql))
        {
            command.ExecuteNonQuery();
        }
    }
}
Petter Hesselberg
  • 5,062
  • 2
  • 24
  • 42
  • Which exact build of SQLite? – Charlieface Feb 10 '22 at 22:32
  • I assume you mean the C libraries? Hang on, and I'll dig it out... – Petter Hesselberg Feb 10 '22 at 22:35
  • `e_sqlite3.dll` does not have much in the way of metadata. I can say that the x64 version was last modified 17.08.2020 16:38 and is 1,570,816 bytes... – Petter Hesselberg Feb 10 '22 at 22:40
  • Sounds like 3.33.0. There are no official bug fixed but maybe a newer version would give different results. Are there any triggers on the table? – Charlieface Feb 10 '22 at 22:48
  • No triggers. The `CreateTableSql` string is all there is. The table is typically quite small (less than 1,000 rows) and everything works fine for the most part. Except for the occasional complaint about the `NOT NULL` **details** column. – Petter Hesselberg Feb 10 '22 at 22:57
  • (And while I know how to make it stop, I would dearly like to know what this is all about.) – Petter Hesselberg Feb 10 '22 at 22:58
  • 1
    Btw, it doesn't impact your question, I don't think, but SQLite doesn't have an affinity of "STRING" type, but does alias "STRING" to a numeric affinity, which can lead to some odd surprises in edge cases. In other words, using STRING for a column type in SQLite is almost always a problem. – Anon Coward Feb 11 '22 at 00:46
  • 1
    Execute `select * from pragma_table_info('Events');` to check the nulability of the column. – forpas Feb 11 '22 at 13:11

1 Answers1

1

From your code, it looks like you are passing message to your procedure, and in one case its value is null. But in a create table script you have a constraint message STRING NOT NULL which throws an error on insert (as null is not allowed to message column). Try to put empty string instead of null and see if the same error occurs.

Serhii
  • 723
  • 1
  • 4
  • 12
  • 1
    My bad -- I passed a null message just to verify that I **did** get the expected error in that case...and forgot to change it back. Fixed now. – Petter Hesselberg Feb 10 '22 at 22:37
  • I mean, the `message` column **does** have a **NOT NULL** constraint, so there the exception is expected. Not so the `details` column... – Petter Hesselberg Feb 10 '22 at 22:47
  • So, have you received 'NOT NULL constraint failed: Events.details' when you pass a `null` to `message` field? (or Events.message) – Serhii Feb 10 '22 at 22:49
  • When I passed `null` to `message` I got an error about `Events.message`, not `Events.details`. (This was expected, to be sure.) – Petter Hesselberg Feb 10 '22 at 23:00
  • Did you try to check if your production table really allows null to details? CreateTableIfNecessary creates table only if not exists in database. There could be a case when someone has created table with `NOT NULL` constraints, after that change the script of `details` to allow `null`, but the table wasn't dropped and recreated with new constraint – Serhii Feb 10 '22 at 23:07
  • The possibility occurred to me, but I don't think so. The database has only the one table, and after doing a bit of GIT archeology I'm pretty sure that column never had a null constraint. – Petter Hesselberg Feb 10 '22 at 23:09
  • 1
    https://www.sqlitetutorial.net/sqlite-tutorial/sqlite-describe-table/ Maybe you could do some investigation of that using this article? – Serhii Feb 10 '22 at 23:13