
The Use of assert() in SQLite - henning
https://www.sqlite.org/assert.html
======
klodolph
I’d like to mention Go. This seems to me like a pointless quibble about how to
catch and handle errors, the kind of argument you see buried in the HN
comments and gloss over because you’ve heard it a bunch of times before. The
assert() macro in C is roughly just if ... { panic() } in Go. You want that,
use it. So the idea that getting rid of assert is “essentially telling
developers that they are not allowed to do run-time verification of
invariants” is completely nonsensical.

The Go philosophy against assert() and assertion libraries is largely that the
error messages produced by assert() are low quality. The idea is that you
should not just have a stack trace, you should have a good error message. The
assert() macro can give you a stack trace with some effort, but rarely gives
you a good message. Good use of panic() is easy and gives you both. It’s also
used mainly for runtime invariant checking, although there are some other ways
it can be used (since it can be caught).

~~~
repsilat
> _The assert() macro in C is roughly just if ... { panic() } in Go_

The article very clearly says that (unlike normal C behaviour) it's a no-op in
release builds of SQLite, because otherwise they get a ~3x slowdown. Does Go
compile away those `if` statements?

~~~
crawshaw
I find the fastest way to introduce bugs into programs is to compile programs
differently in development than in production. It also warps developer's
perspectives of how their program performs. As they say at NASA, "Test what
you fly, fly what you test."

That said, you can do this easily enough in Go. Define a package:

    
    
      package debug
    
      const Debug = true
    
      func Assert(cond bool, msg string) {
            if Debug && cond {
                    panic(msg)
            }
      }
    

Have your build system generate the file that contains Debug = true. In
optimization mode, make it Debug = false. As it is const, the compiler will
propagate it, Assert will be inlined and the dead code removed. If you don't
like relying on the compiler, have your code generator generate empty Assert
functions. If you are working in a very bad code base that catches all panics,
you'll need an extra 5 lines of code in the implementation of Assert, and your
stack traces won't be pretty. (Roughly: put the condition check in a
goroutine, block the Assert call on a channel signal from that goroutine.) I'd
recommend the first project be to remove those panic catch-alls.

But really, all of this is only appropriate when you are far out in the weeds.
Ship your program with those asserts. If they are too slow for production they
are too slow for development.

~~~
kasey_junk
That will still evaluate the cond in all cases. Passing a func will make it
lazy but might have performance implications (I’d have to test it).

~~~
crawshaw
It won't, because of inlining and dead code removal. (At least, it didn't back
around Go 1.11 when I last tested this.)

~~~
kasey_junk
Maybe if the thing that is generating the cond at the call location is inline-
able, but not if it's doing an expensive operation.

[https://play.golang.org/p/Ts6QqRrWwuQ](https://play.golang.org/p/Ts6QqRrWwuQ)

I couldn't get that to not trigger the lock.

[later] I could get it to remove it if you pass in a func instead.

~~~
chronial
Your Cond() has a side-effect. It can never be optimized away.

~~~
kasey_junk
Correct. Which is one of the differences between this approach & macros.
Though the lazy approach appears to work.

------
chiph
> The CORRUPT_DB macro is used in many assert() statements. In functional
> testing builds, CORRUPT_DB references a global variable that is true if the
> database file might contain corruption. This variable is true by default,
> since we do not normally know whether or not a database is corrupt, but
> during testing while working on databases that are known to be well-formed,
> that global variable can be set to false.

This is interesting - this really reveals their philosophy where they assume
that until proven to be good, stuff is considered to be broken.

------
AndyKelley
Not only does zig embrace this philosophy, it introduces the concept of
"language-level assertions". For example, integer operations, both signed and
unsigned, assert that no overflow occured. Likewise many of the casts, such as
non truncating integer conversion, assert that the numerical value is
unchanged.

In safety-protected build modes assertion failures trigger a panic, which is
globally overridable, but the default behavior is to print a full stack trace.

Finally, the assertion behavior can be modified at any scope. So one can
identify the performance bottlenecks and turn assertion failures into
undefined behavior.

------
FrozenVoid
For reference, they use this code instead of assert:

    
    
        #if defined(SQLITE_COVERAGE_TEST) || defined(SQLITE_MUTATION_TEST)
        # define ALWAYS(X)      (1)
        # define NEVER(X)       (0)
        #elif !defined(NDEBUG)
        # define ALWAYS(X)      ((X)?1:(assert(0),0))
        # define NEVER(X)       ((X)?(assert(0),1):0)
        #else
        # define ALWAYS(X)      (X)
        # define NEVER(X)       (X)
        #endif

~~~
bayareanative
ALWAYS and NEVER don't pass-through their values consistently as claimed in
debug builds, they pass through truthiness instead, with actual values in
release builds. Perhaps this would be better for _debug_ builds:

    
    
        #define ALWAYS(X) (assert(X), (X))
        #define NEVER(X) (assert(!(X)), (X))

~~~
pawelmurias
That would duplicate X

------
nh2
I've always found the name `assert()` to be particularly bad.

Many people do not know that their compilers optimise away assertions in `-O`
("release") builds.

They use assertions for control flow and input validation, which is very
wrong.

I believe that could be trivially fixed by giving functions proper names that
indicate what they do or what they are intended for. For example, calling such
an optimised-away assertion function `debug_only_assert()` would immediately
rule out such unintentional misuse.

~~~
technion
Huh? I was worried I had been doing this wrong this whole time, but I haven't
been able to replicate that with release builds.

[https://gist.github.com/technion/7c74fad9efd1f14244e25a1cee3...](https://gist.github.com/technion/7c74fad9efd1f14244e25a1cee372a0e)

~~~
zmodem
In many settings, "release build" implies passing -DNDEBUG, which disables the
asserts.

------
shaklee3
> The SQLite developers believe that the lack of assert() disqualifies Go as a
> language for serious development work.

Great comment.

~~~
zaroth
It seemed random and out of place to me. Like some sort of Wiki vandalism.

Why do I care about their opinion of Go in SQLite dev docs?

~~~
smitty1e
Whoever wrote that was probably shooting from the Hipp.

------
macintux
I haven’t worked with a lot of code bases, but am I correct in assuming this
level of precision with error handling is fairly rare? Impressive stuff.

Orthogonal: one of my favorite talking points about Erlang is the fact that
nearly every line of code is an assertion, and that those assertions are
always enabled, even in production.

You’d not want to use Erlang for truly high performance code, granted.

~~~
vemv
Interesting point about Erlang! Where can I find more info on that specific
aspect?

~~~
macintux
If you can tolerate a video, my talk from Midwest.io covers it to some degree.

It’s really just a happy byproduct of the overall design of the language and
VM. The two primary features that make it happen are immutability and
ubiquitous pattern matching.

[https://youtu.be/E18shi1qIHU](https://youtu.be/E18shi1qIHU)

------
pornel
Ooh, I love the idea of `testcase(X)` to instrument test coverage.

~~~
gshrikant
I'm probably being a blockhead but I didn't quite get how the testcase macro
is used in instrumenting coverage. Can you explain that?

~~~
nairboon
The testcase macro is the following:

    
    
      # define testcase(X)  if( X ){ sqlite3Coverage(__LINE__); }
    
    

If the testcase(condition) always evaluates to true, the code coverage
analyzer would complain that the if ( X ) {sqlite3...} statement never
evaluates to false and the branch coverage drops below 100%, the converse
holds conversely if it was never evaluated to true in the first place. Only if
the code using the testcase macro gets repeatedly called in a way so that the
testcase macro evaluates sometimes to true and sometimes to false, the
coverage stays at 100%.

sqlite3Coverage() does some dummy work so that the call will not be optimized
away by the compiler.

------
draw_down
“The SQLite developers believe that the lack of assert() disqualifies Go as a
language for serious development work.“

Spicy!

------
kalecserk
> In SQLite, the presence of assert(X) means that the developers have a proof
> that X is always true.

Almost impossible to prove anything in a sufficiently complex system such as
SQLite. The difference between assert and always seems arbitrary IMHO

~~~
saagarjha
The difference is that assert has been “proved” to be true, while ALWAYS is
merely _thought_ to be true.

~~~
chris_wot
I’m still uncertain what they mean by “prove” though.

~~~
kevingadd
sqlite has extremely thorough test coverage with something approaching 100%
branch coverage and such wide production usage it's basically proven that a
lot of this stuff works.

~~~
nn3
Most (all?) of the production usage probably doesn't enable the assert though.

