Skip to content

Conversation

@drew-fc
Copy link

@drew-fc drew-fc commented Jan 24, 2026

Problem

When SQLite3.Step() returns an error, the error message is retrieved using SQLite3.GetErrmsg() after Finalize() or
Reset() is called. However, these cleanup functions clear SQLite's error state, causing GetErrmsg() to return "not an
error" instead of the actual error message.

Related issue: praeclarum/sqlite-net#1041


Fix 1: SQLiteCommand.ExecuteNonQuery()

Before (line ~3066):
public int ExecuteNonQuery ()
{
if (_conn.Trace) {
_conn.Tracer?.Invoke ("Executing: " + this);
}

  var r = SQLite3.Result.OK;
  var stmt = Prepare ();
  r = SQLite3.Step (stmt);
  Finalize (stmt);                                              // ← Error state cleared here
  if (r == SQLite3.Result.Done) {
      int rowsAffected = SQLite3.Changes (_conn.Handle);
      return rowsAffected;
  }
  else if (r == SQLite3.Result.Error) {
      string msg = SQLite3.GetErrmsg (_conn.Handle);            // ← Too late! Returns "not an error"
      throw SQLiteException.New (r, msg);
  }
  else if (r == SQLite3.Result.Constraint) {
      if (SQLite3.ExtendedErrCode (_conn.Handle) == SQLite3.ExtendedResult.ConstraintNotNull) {
          throw NotNullConstraintViolationException.New (r, SQLite3.GetErrmsg (_conn.Handle));
      }
  }

  throw SQLiteException.New (r, SQLite3.GetErrmsg (_conn.Handle));

}

After:
public int ExecuteNonQuery ()
{
if (_conn.Trace) {
_conn.Tracer?.Invoke ("Executing: " + this);
}

  var stmt = Prepare ();
  try {
      // FIX: Use try/finally to ensure Finalize runs, and throw exceptions
      // BEFORE finally block so GetErrmsg is called while error state is valid.
      // See: https://github.com/praeclarum/sqlite-net/issues/1041
      var r = SQLite3.Step (stmt);
      if (r == SQLite3.Result.Done || r == SQLite3.Result.Row) {
          int rowsAffected = SQLite3.Changes (_conn.Handle);
          return rowsAffected;
      }
      else if (r == SQLite3.Result.Error) {
          throw SQLiteException.New (r, SQLite3.GetErrmsg (_conn.Handle));  // ← Called before Finalize
      }
      else if (r == SQLite3.Result.Constraint) {
          if (SQLite3.ExtendedErrCode (_conn.Handle) == SQLite3.ExtendedResult.ConstraintNotNull) {
              throw NotNullConstraintViolationException.New (r, SQLite3.GetErrmsg (_conn.Handle));
          }
      }
      throw SQLiteException.New (r, SQLite3.GetErrmsg (_conn.Handle));
  }
  finally {
      Finalize (stmt);  // ← Cleanup happens AFTER exception is created with correct message
  }

}


Fix 2: PreparedSqlLiteInsertCommand.ExecuteNonQuery()

Before (line ~3740):
public int ExecuteNonQuery (object[] source)
{
if (Initialized && Statement == NullStatement) {
throw new ObjectDisposedException (nameof (PreparedSqlLiteInsertCommand));
}

  if (Connection.Trace) {
      Connection.Tracer?.Invoke ("Executing: " + CommandText);
  }

  var r = SQLite3.Result.OK;

  if (!Initialized) {
      Statement = SQLite3.Prepare2 (Connection.Handle, CommandText);
      Initialized = true;
  }

  //bind the values.
  if (source != null) {
      for (int i = 0; i < source.Length; i++) {
          SQLiteCommand.BindParameter (Statement, i + 1, source[i], Connection.StoreDateTimeAsTicks,

Connection.DateTimeStringFormat, Connection.StoreTimeSpanAsTicks);
}
}
r = SQLite3.Step (Statement);

  if (r == SQLite3.Result.Done) {
      int rowsAffected = SQLite3.Changes (Connection.Handle);
      SQLite3.Reset (Statement);
      return rowsAffected;
  }
  else if (r == SQLite3.Result.Error) {
      string msg = SQLite3.GetErrmsg (Connection.Handle);       // ← Called before Reset (OK)
      SQLite3.Reset (Statement);
      throw SQLiteException.New (r, msg);
  }
  else if (r == SQLite3.Result.Constraint && SQLite3.ExtendedErrCode (Connection.Handle) ==

SQLite3.ExtendedResult.ConstraintNotNull) {
SQLite3.Reset (Statement); // ← Reset BEFORE GetErrmsg
throw NotNullConstraintViolationException.New (r, SQLite3.GetErrmsg (Connection.Handle)); // ← Too late!
}
else {
SQLite3.Reset (Statement); // ← Reset BEFORE GetErrmsg
throw SQLiteException.New (r, SQLite3.GetErrmsg (Connection.Handle)); // ← Too late!
}
}

After:
public int ExecuteNonQuery (object[] source)
{
if (Initialized && Statement == NullStatement) {
throw new ObjectDisposedException (nameof (PreparedSqlLiteInsertCommand));
}

  if (Connection.Trace) {
      Connection.Tracer?.Invoke ("Executing: " + CommandText);
  }

  if (!Initialized) {
      Statement = SQLite3.Prepare2 (Connection.Handle, CommandText);
      Initialized = true;
  }

  //bind the values.
  if (source != null) {
      for (int i = 0; i < source.Length; i++) {
          SQLiteCommand.BindParameter (Statement, i + 1, source[i], Connection.StoreDateTimeAsTicks,

Connection.DateTimeStringFormat, Connection.StoreTimeSpanAsTicks);
}
}

  try {
      // FIX: Use try/finally to ensure Reset runs, and throw exceptions
      // BEFORE finally block so GetErrmsg is called while error state is valid.
      // See: https://github.com/praeclarum/sqlite-net/issues/1041
      var r = SQLite3.Step (Statement);
      if (r == SQLite3.Result.Done || r == SQLite3.Result.Row) {
          int rowsAffected = SQLite3.Changes (Connection.Handle);
          return rowsAffected;
      }
      else if (r == SQLite3.Result.Error) {
          throw SQLiteException.New (r, SQLite3.GetErrmsg (Connection.Handle));
      }
      else if (r == SQLite3.Result.Constraint) {
          if (SQLite3.ExtendedErrCode (Connection.Handle) == SQLite3.ExtendedResult.ConstraintNotNull) {
              throw NotNullConstraintViolationException.New (r, SQLite3.GetErrmsg (Connection.Handle));
          }
      }
      throw SQLiteException.New (r, SQLite3.GetErrmsg (Connection.Handle));
  }
  finally {
      SQLite3.Reset (Statement);  // ← Cleanup happens AFTER exception is created
  }

}


Notes

  • This fix follows the same pattern already used in ExecuteScalar (which correctly uses try/finally)
  • The Row result is treated as success to handle statements like PRAGMAs that return data
  • Reviewed by Gemini 2.5 Pro and GPT-5.2 - both confirmed the fix is correct

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant