Last updated: January 16 2025 09:40

SQLite so light,
Where mortals find the way,
Data in stillness.

SQLite for [mere] mortals

Dear Reader, this document is a work in progress and is not yet complete. It may contain typos, as well as factual, logical or grammatical errors. :) I’d love it if you could send me your thoughts via email using the name sqlitemortal and the gmail domain!

This website helps me scratch my own itch. Over time, I’ve realized that the scope of what I use SQLite for is limited, and as a result, the number of features I’m familiar with is also limited, which is natural. For this reason, I’ve decided to review things I already know and learn things I don’t know yet using the learning-by-writing method. I’m working on it primarily for myself, but I’ll be happy if you also find it useful for your own needs.

SQLite is serverless. Learn this mantra. When people discuss using it over a network and compare it to server-based database systems, it’s either due to ignorance or trolling. SQLite is not designed to replace or compete with those systems. Currently, there is no competitor to SQLite for its intended use. SQLite is absolutely unique.

SQLite does not compete with client/server databases. SQLite competes with fopen() — D. Richard Hipp.

This website is regularly updated. It’s not something I create and forget. SQLite is evolving, and new features or fixes are constantly being added, which means the content of this website changes as well. The information about the latest update is at the beginning of the page.

I’m working on it mostly in the evenings so if you like it you can buy me a cheap Deathbrew :)

Obtaining SQLite

To obtain SQLite visit the download secion of the official SQLite website.

You’ll see options for different operating systems. Select the appropriate one based on whether you’re using Windows, macOS, or Linux. For most users, downloading the precompiled binaries is the easiest option. If you are running Linux or macOS SQLite is often pre-installed, but if not you can use your package manager to install it.

Compiling SQLite from sources

The best bet to compile your very own sqlite3 is to download the file named sqlite-autoconf-[version].tar.gz, and use the autoconf machinery

tar zxvf it, cd into unpacked directory and run the following commands:

./configure --prefix=/home/$USER/app/sqlite3 --enable-editline
make
make install

If everything went smoothly, the directory specified in the --prefix option of the configure program should have the following structure and content:

skull: ~ $: tree app/sqlite3/
app/sqlite3/
|-- bin/
|   `-- sqlite3*
|-- include/
|   |-- sqlite3.h
|   `-- sqlite3ext.h
|-- lib/
|   |-- libsqlite3.a
|   |-- libsqlite3.la*
|   |-- libsqlite3.so.8.6*
|   `-- pkgconfig/
|       `-- sqlite3.pc
`-- share/
   `-- man/
        `-- man1/
            `-- sqlite3.1

7 directories, 8 files 
skull: ~ $:

Note: SQLite is statically compiled, to play with it, the only file you need is the sqlite3 executable in bin directory. You can place it anywhere and delete the rest.

Optionally, you can create a symlink to the sqlite3 binary and place it in a directory that is part of your $PATH variable. For example, you can do this as follows:

ln -s /home/$USER/app/sqlite3/bin/sqlite3 /home/$USER/bin/sqlite3

Note: It’s worth noting that the only OS I’ve been using on desktop and servers since 2017 is OpenBSD. Therefore, I might be mistaken regarding other operating systems. As shown in the configure command, I use editline library; on Linux or macOS, it might be readline, so you will need to use --enable-readline option.

SQLite database

An SQLite database is a single ordinary file that holds all the data for an application. It organizes the data into tables, which are like lists with rows and columns, making it easy to store and retrieve information. Everything is packed into one file, so there’s no need for a separate system to manage it. You can think of an SQLite database file like a spreadsheet file. It’s a single file that stores all the data in an organized way, with tables similar to sheets in a spreadsheet.

You can perform any operations on this file that you do with other files, such as copying, renaming, deleting it, and just like any other file you send by email or through an instant messenger, the SQLite database file can be easily shared with your friends and colleagues. It works across different operating systems, whether the recipient is using Windows, macOS, or Linux, without any compatibility issues. Sharing the file is as simple as sending any other document.

SQLite shell

The program used to create and work with SQLite databases has the surprising name sqlite3 (sqlite3.exe on Windows). It is often referred to as the sqlite shell. This program is used in the terminal, in the command-line interface (CLI). There are also graphical user interfaces (GUIs) available, and personally, I sometimes use a program called SQLite Browser. However, GUIs will not be the focus of this website.

Entering and leaving the shell

Open your terminal window and issue the following command:

skull: ~ $: sqlite3

On Windows simply run sqlite3.exe

If everything works well sqlite3 program will show a brief banner message and then prompt you to enter commands:

SQLite version 3.47.2 2024-12-07 20:39:59
Enter ".help" for usage hints.
Connected to a transient in-memory database.
Use ".open FILENAME" to reopen on a persistent database.
sqlite>

Now, to exit the shell, type .exit and press Enter key. It’s easier than leaving the vi editor for the first time.

sqlite>.exit

Command .exit is so-called dot-command. If you run .help dot-command you’ll see the list of all dot-commands available.

Creating or opening database

In SQLite, creating or opening a database is essentially the same. If the database does not exist, it is automatically created.

If we omit the database file name, sqlite3 creates a temporary in-memory database, which is destroyed after the sqlite3 shell exits.

There are two ways to create or open a database. The first is by issuing the sqlite3 command with the database name:

skull: ~ $: sqlite3 database1.sq3

The second way is by issuing sqlite3 command without database file name at first:

skull: ~ $: sqlite3

and then issue the dot-command .open with database file name

sqlite> .open database1.sq3

Note: As you may have noticed, I use the “.sq3” extension for database file names. You can use any other extension, or none at all, depending on the naming rules of your operating system. The “.sq3” extension allows me (since it’s a bit unusual) to easily find all SQLite databases on my computer. It also indicates the major SQLite version that created the database… I’m already looking forward to SQLite4 :)

Dot-commands explained

SQLite dot-commands are special commands used within the SQLite command-line interface (CLI) to perform various tasks that aren’t directly related to SQL queries. They begin with a dot (.), and unlike SQL commands, they’re processed by the SQLite shell rather than the database engine itself.

To list available dot-commands open the shell and type .help into command prompt. Shortened output looks like this:

sqlite> .help
.archive ...             Manage SQL archives
.auth ON|OFF             Show authorizer callbacks
.backup ?DB? FILE        Backup DB (default "main") to FILE
.bail on|off             Stop after hitting an error.  Default OFF
.cd DIRECTORY            Change the working directory to DIRECTORY
.changes on|off          Show number of rows changed by SQL
.check GLOB              Fail if output since .testcase does not match
.clone NEWDB             Clone data into NEWDB from the existing database
....
sqlite>

If you want to know more about particular command type .help .command into command prompt:

sqlite> .help .backup
.backup ?DB? FILE        Backup DB (default "main") to FILE
   Options:
     --append            Use the appendvfs
     --async             Write to FILE without journal and fsync()
.save ?OPTIONS? FILE     Write database to FILE (an alias for .backup ...)
sqlite>

Executing SQL commands

In the SQLite shell, every command must end with a semicolon (;) to signal its end. To execute a command, simply press the enter key. Commands can span multiple lines if needed, but they must still end with a semicolon.

Commands that insert, update, or delete data usually don’t return anything, while commands like SELECT will return data in the form of a result set, typically displayed as rows and columns.

Example of multi-line SQL command input:

sqlite> CREATE TABLE books (
(x1...> id INTEGER PRIMARY KEY,
(x1...> title TEXT,
(x1...> description TEXT,
(x1...> author TEXT
(x1...>);
sqlite>

Note: ...> is called a continuation prompt. (x1…> indicates that there is one open parenthesis without a corresponding closing parenthesis.

Example of single-line SQL command input:

sqlite> INSERT INTO books (title, description, author) VALUES ('Book 1 Title', 'Description of the first book.', 'Author 1');
sqlite> INSERT INTO books (title, description, author) VALUES ('Book 2 Title', 'Description of the second book.', 'Author 2');
sqlite> INSERT INTO books (title, description, author) VALUES ('Book 3 Title', 'Description of the third book.', 'Author 3');
sqlite> ....

Showing SQL data output in various ways

There are various ways how to display result of a command returning data. This is when the dot-command .mode comes to the scene. Use this command without arguments to see current display mode

sqlite> .mode

Use .help mode dot-command to view all available diplay modes with short description

sqlite> .help .mode

The default display mode is list. To change it run the following command:

sqlite> .mode column
sqlite> select * from books;
id  title         description                       author
--  ------------  --------------------------------  --------
1   Book 1 Title  Description of the first book.    Author 1
2   Book 2 Title  Description of the second book.   Author 1
3   Book 3 Title  Description of the third book.    Author 1
...
sqlite>

Reading SQL commands from file

When you are in SQLite shell sqlite3 reads input directly from keyboard, whether it’s SQL statement or dot-commands.

If you want to execute an SQL from a file while still being able to enter other commands interactively, you can use the .read dot-command. This command accepts a single argument, usually the name of the file containg the input text:

sqlite> .read commands.sql

Note: commands.sql may contain dot-commands

Writing SQL data output into file

If you want to write output of SELECT statement into file instead of standard output you can use dot-command .output and set mode and column separator for it:

skull: ~ $: sqlite example.sq3
SQLite version 3.47.2 2024-12-07 20:39:59
Enter ".help" for usage hints.
sqlite> .mode list
sqlite> .separator ,
sqlite> .output out.txt
sqlite> select * from books;
sqlite> .exit
skull: ~ $: cat out.txt
1,Book 1 Title,Description of the first book.,Author 1
2,Book 2 Title,Description of the second book.,Author 1
3,Book 3 Title,Description of the third book.,Author 1
4,Book 4 Title,Description of the fourth book.,Author 2
5,Book 5 Title,Description of the fifth book.,Author 2
6,Book 6 Title,Description of the sixth book.,Author 3
7,Book 7 Title,Description of the seventh book.,Author 3
skull: ~ $:

Reading file into table column

You can store data from a file (commonly binary data) into a single column using the readfile() function. For example, if you want to store an image in a table column, you can do it like this:

sqlite> CREATE TABLE icons(name TEXT, img BLOB);
sqlite> INSERT INTO icons(name, img) VALUES ('book', readfile('book.jpg'));

Writing to file from table column

To extract data (typically BLOB) from a column and save it into a file, you can use the writefile() function. For example:

sqlite> SELECT writefile('book.jpg', img) FROM icons WHERE name='book';

Editing column content in external program

This feature is great! It’s made possible by the edit() function, which allows you to edit the content of a particular column using an external program. For example, I use it when I need to edit markdown files stored in the database. You can invoke it like this:

sqlite> UPDATE md_files SET content=edit(content) WHERE ...;

This SQL command opens your default editor (e.g., vi), where you can change the content. Once you exit the editor, the content of the column for that particular row is updated.

If you want to update an image stored in a column, you can run something like this:

sqlite> UPDATE images SET img=edit(img, 'inkscape') WHERE ...;

You can also use an external program as a viewer. In this case, you simply ignore the return value:

sqlite> SELECT length(edit(img, 'inkscape')) WHERE ...;

Data import and export in text form

Importing data into SQLite

To import data into SQLite, you can use the .import command. This command requires two arguments: the source file to be read and the SQLite table where the data will be inserted.

Example CSV file:

id;title;description;author
1;1984;Dystopian novel by George Orwell;George Orwell
2;Animal Farm;Political satire by George Orwell;George Orwell
3;Brave New World;Dystopian novel by Aldous Huxley;Aldous Huxley
4;The Doors of Perception;Essay by Aldous Huxley;Aldous Huxley
5;Fahrenheit 451;Dystopian novel by Ray Bradbury;Ray Bradbury
...

In this example, the first row contains column names. SQLite can automatically create the target table based on these column names during import. If you prefer to create the table manually, you must use the --skip option with value 1 in the .import command to skip the header row.

sqlite> CREATE TABLE books (
(x1...> id INTEGER PRIMARY KEY,
(x1...> title TEXT,
(x1...> description TEXT,
(x1...> author TEXT
(x1...>);
sqlite>

Now we can import data from a file where the column separator is a semicolon (;) and the record separator is a newline character (\n). Since we manually created the table, we use the --skip option with the .import command to skip the header row.

sqlite> .mode csv
sqlite> .separator ;
sqlite> .import --skip 1 books.csv books
sqlite> .mode column
sqlite> select * from books;
id  title                    description                        author
--  -----------------------  ---------------------------------  -------------------
1   1984                     Dystopian novel by George Orwell   George Orwell
2   Animal Farm              Political satire by George Orwell  George Orwell
3   Brave New World          Dystopian novel by Aldous Huxley   Aldous Huxley
4   The Doors of Perception  Essay by Aldous Huxley             Aldous Huxley
5   Fahrenheit 451           Dystopian novel by Ray Bradbury    Ray Bradbury
...

Exporting data from SQLite

To export data from SQLite, use the .once command. This command requires a single argument, the file name where the exported data will be written.

Steps for exporting:

sqlite> .headers on
sqlite> .mode csv
sqlite> .separator |
sqlite> .once books_export.csv
sqlite> select * from books;

All data returned by the SELECT query are now stored in the books_export.csv file, including a header row and using the specified separator.

Exporting whole database structure and data into dump text file

Exporting a database’s structure and data is useful for future restoration. The export file contains SQL commands to recreate tables and reinsert the data.

If you want to export everything, use the following commands. This will create a database1.sql file, which can later be used to restore the entire database. Use the .output dot-command again to restore the output to the terminal to prevent additional content from being written to the file.

sqlite> .output database1.sql
sqlite> .dump
sqlite> .output stdout

To export only the table structure, use the .schema dot-command after setting up the output file:

sqlite> .output database1.sql
sqlite> .schema
sqlite> .output stdout

To export only the INSERT statements with data, use the –data-only switch with the .dump dot-command:

sqlite> .output database1.sql
sqlite> .dump --data-only
sqlite> .output stdout

Restore database structure and data from dump text file

To restore an entire database from a dump file, start by creating a new database. Then, use the .read command to execute the SQL statements from the dump file (e.g., database1.sql). This process recreates the database structure and inserts the data as specified in the dump file.

skull: ~ $: sqlite3 database2.sq3
SQLite version 3.44.2 2023-11-24 11:41:44
Enter ".help" for usage hints.
sqlite> .read database1.sql
sqlite>

Ensure the dump file database1.sql contains all necessary SQL commands for both structure and data. This typically happens when the file was generated using the .dump command without option like –data-only or with .schema dot-command.

The database file being restored to should not already contain conflicting structures or data, as this might result in errors during restoration.

SQLite3 shell resource file

There are certain settings for the SQLite shell that you will use regularly. These settings can be preconfigured and automatically loaded every time you start the sqlite3 program. To do this, simply create a file named .sqliterc in your home directory and add the desired dot-commands to it. For example:

.mode column
# .mode line
# .headers on
.prompt "sqlite> " "   ...> "

Lines starting with the hash symbol (#) are comments and will be ignored by the shell.


The SQLite shell is a simple and powerful tool for working with SQLite databases. You can run SQL queries and use dot-commands to manage your database easily. It’s easy to use, whether you’re a beginner or an experienced developer.

The shell is a broad topic with many features to explore. We’ll come back to some of its tools and tricks as we dive deeper into SQLite. For now, enjoy its simplicity and reliability as you work with your databases!


SQLite in CLI and scripts

It’s no surprise that you can use the sqlite3 program in CLI and shell scripts just like any other command-line tool. The basic syntax is:

You can run sqlite3 --help to view all available options. Here are a few examples of how to use it:

Using sqlite3 program in CLI

skull: ~ $: sqlite3 database1.sq3 '.backup backup.sq3'
skull: ~ $: sqlite3 -csv database1.sq3 'select * from books' | otherprogram ...

Using sqlite3 program in shell scripts

  1. Creating a books table:
#!/bin/sh

sqlite3 database1.sq3 <<EOF
create table if not exists books (
    id integer primary key,
    title text,
    description text,
    author text
);
EOF
  1. Inserting data into table:
#!/bin/sh

sqlite3 database1.sq3 <<EOF
    INSERT INTO books (title, description, author) VALUES ('Book 1 Title', 'Description of the first book.', 'Author 1');
    INSERT INTO books (title, description, author) VALUES ('Book 2 Title', 'Description of the second book.', 'Author 2');
    INSERT INTO books (title, description, author) VALUES ('Book 3 Title', 'Description of the third book.', 'Author 3');
EOF

Selecting data and outputting to a CSV file:

#!/bin.sh

sqlite3 -header -csv database1.sq3 'SELECT * FROM books;' > books.csv

Using sqlite3 for archiving

An “SQLite archive” is a file container, much like a ZIP archive or Tarball, but built on an SQLite database. It exemplifies the broader concept that an SQLite database can function as a container, storing numerous smaller data components.

SQLite archive characteristics:

Creating new archive of *.c files in current directory:

skull: ~ $: sqlite3 sources.sqlar -Ac *.c

Adding and updating files in an existing archive

skull: ~ $: sqlite3 sources.sqlar -Au *.h

List files in archive:

skull: ~ $: sqlite3 sources.sqlar -Atv

To extract all files:

skull: ~ $: sqlite3 sources.sqlar -Ax

To view all options and usage hints use only -A option with no additional arguments.

Reading ZIP archives as tables

In addition to SQLite database files, the sqlite3 program can read and write ZIP archives. Simply provide a ZIP archive filename instead of an SQLite database filename in the command line or .open command. sqlite3 will detect the file as a ZIP archive and open it, regardless of the file extension. This means you can open JAR, DOCX, ODP, or other ZIP-based files.

A ZIP archive appears as a database with a single table and the following schema:

CREATE TABLE zip(
  name,     -- Name of the file
    mode,     -- Unix-style file permissions
    mtime,    -- Timestamp, seconds since 1970
    sz,       -- File size after decompression
    rawdata,  -- Raw compressed file data
    data,     -- Uncompressed file content
    method    -- ZIP compression method code
);

To extract data from the ZIP archive you can use writefile() function:

sqlite> sqlite> SELECT writefile(name,content) FROM zip
   ...> WHERE name LIKE 'docProps/%';

The sqlite3 program is not just a shell; it can be used in CLI and shell scripts for various tasks. Whether it’s reading and writing ZIP archives or automating SQL commands, the versatility of sqlite3 makes it a valuable tool for many use cases. By integrating it into shell scripts, you can simplify tasks and enhance productivity.


SQLite, Brief and Bold

High reliability

SQLite. Reliable. Steady as stone. Used by billions. Time-tested and true.

Key takeaways:

Flexible typing

Are you seriously telling me that you don’t check the input data, so now you need a strict type system at the database level?

SQLite’s type system is flexible. Columns adapt to data types, not the reverse. No need for strict definitions; just use column names.

Key points:

SQLite adjusts columns to the data:

Key takeaways:

Introducing fantasy data type, because when our site reaches the scale of FB, we will need to migrate :)

sqlite> create table t1 (
(x1...>   id,
(x1...>   name text,
(x1...>   age fantasy
(x1...> );
sqlite>

That’s it.

Default values

The DEFAULT clause in SQLite simplifies data management by providing predefined values when no explicit input is given. These defaults can be constants, expressions, or special keywords like CURRENTDATE or CURRENTTIMESTAMP, allowing for flexible and automated initialization.

Expressions are evaluated for each inserted row, ensuring context-specific defaults. This feature promotes efficiency and consistency in handling table data, especially for columns with predictable or standard values, minimizing manual input while maintaining data integrity.

Simple example

CREATE TABLE books (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  title TEXT NOT NULL,
  author TEXT DEFAULT "John Doe",
  published_date TEXT DEFAULT CURRENT_DATE,
  pages INTEGER,
  rating REAL,
  created_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%f', 'now'))
);

INSERT INTO books (title, pages, rating) VALUES('The SQLite book', 520, 4.9);

Backups

Heads on :)

I do not use SQLite over a network, and I have never encountered a corrupted database in a real-world scenario that required recovery. In my case, the application and database reside on the same machine and disk drive.

After performing the backup, it’s always a good idea to verify the integrity of the resulting file to ensure everything is in order, using PRAGMA integrity_check or PRAGMA quick_check.

Method 1: cp command

If the database is not in use, a simple cp command is sufficient to create a backup. If SQLite operates in WAL mode, there may be additional files related to the database, such as the -wal file or the -shm file.

Method 2: .backup dot-command

If the database is live, I rely on the .backup dot-command. I trust the SQLite author to know best practices for safely handling backups in such situations.

This approach prioritizes simplicity while adhering to SQLite’s built-in mechanisms for data safety.

There are at least two steps involved in performing a backup this way:

skull: ~ $: sqlite3 database1.sq3 '.backup database1.backup.sq3'
skull: ~ $: sqlite3 database1.backup.sq3 'pragma integrity_check'

Of course, the backup can be automated by creating a shell script containing these and other commands, while also checking the return values. For example, PRAGMA integrity_check literally returns the value ‘ok’ if everything went well. :)

Method 3: sqlite3-rsync

sqlite3-rsync is a utility that synchronizes SQLite databases, ensuring the REPLICA matches the ORIGIN as a point-in-time snapshot.

It supports both local and remote databases, handles “live” databases during operation, and uses a bandwidth-efficient protocol. It is ideal for creating consistent database replicas even while ORIGIN is actively used.

sqlite3-rsync is included in SQLite precompiled binaries available on SQLite download page I think.

There is no binary available for OpenBSD, so in my case, I tried to compile it from source. To do so, I needed the Tcl source code, so I downloaded Tcl 9.0 sources. The next steps were as follows:

$ cd ~/tmp
$ git clone https://github.com/sqlite/sqlite.git
$ cd sqlite
$ git checkout sqlite3-rsync
$ ./configure --with-tcl=/home/$USER/Downloads/tcl9.0.1/unix
$ make sqlite3-rsync

Because of some limitations, and also because this tool is relatively new, it’s wise to read the sqlite3-rsync documentation. If you know a simpler way to compile it from source, please let me know. I’d be glad to include it.

Method 4: VACUUM INTO

Additionally, there’s the VACUUM INTO SQL command, which can be used instead of .backup dot-command for creating vacuumed copy of the live database.

I personally work with methods 1 and 2. There are many ways and techniques to perform a backup, but I have no experience with them so far, as they depend on the environment in which the database operates.

WAL mode

In regular database systems, when you make a change, the database writes the new data directly into the main database file. This can be slow, especially when many people or programs are using the database at the same time.

WAL mode (Write-Ahead Logging) is a special mode in SQLite that makes things faster and safer.

When you make a change in WAL mode:

SQLite uses Rollback Journal Mode by default. To enable Write-Ahead Logging (WAL) mode, use the following command:

sqlite> PRAGMA journal_mode = WAL;

In scenarios where performance is more critical than durability, you can adjust the synchronous mode for better speed. For example:

sqlite> PRAGMA synchronous = NORMAL;

PRAGMA Synchronous Levels Explained

OFF

NORMAL

FULL

… disadvantages …

Altering tables

SQLite provides basic support for altering tables, such as adding new columns, renaming tables, and renaming columns. However, it lacks direct methods for more complex operations due to its lightweight design, which prioritizes simplicity and performance. As a result, workarounds are often necessary in such cases.

These workarounds involve creating a new table with the desired columns, copying data from the original table into it, deleting the original table, and renaming the new table to match the name of the original one.

1. Renamimg table:

The RENAME TO operation renames a table from table-name to new-table-name. This change is limited to renaming within the same database.

Key Notes:

ALTER TABLE books RENAME TO bookshelf;

2. Adding new column at the end of the table:

The ADD COLUMN operation adds a new column to the table. The new column is always added at the end.

Column Constraints:

Restrictions:

No default values like CURRENT_TIME or CURRENT_TIMESTAMP are allowed.

ALTER TABLE books ADD COLUMN pages INTEGER;

3. Renaming a column:

The RENAME COLUMN TO operation changes a column name from column-name to new-column-name within the table.

Key Notes:

ALTER TABLE books RENAME COLUMN pages TO total_pages;

Primary keys

… rowid, integer primary key, autoincrementing …

Date and time

… there is no separate type

Indexes

what it is

how to do it … explain plan

advantages

disadvantages

Indexing expression instead of column value

Foreign keys

FTS4? FTS5 … mention external content tables too

SQLite limits or being on thin ice :)

… and much more :)


Philosophy


Why I personally like SQLite and use it for everyday tasks:

Freedom: The source code is in the public domain. You can do anything you want with it, including using it for commercial purposes.

Minimalism: It’s minimalist both in the sense that it’s small, simple and in the sense that it doesn’t talk when it doesn’t need to.

Creativity: Its properties, which differ from other database systems, or the features that are missing and others might consider weaknesses, force me to be creative.

Speed: SQLite is simply fast. Fastest in the field of the intended use.

Stability: It’s written in ANSI-C, which ensures it doesn’t have to contend with frequent changes to the language specification.

Predictability: Everything above stays true because SQLite has a steady roadmap and the author sticks to his principles without following fashionable trends

Learning by writing

It is an iterative process - “write, review, learn, repeat” - until the concept is fully understood and retained!

Another great way to describe the cyclical nature of learning-by-writing is to compare it to the “rinse and repeat” concept, which is well known to people in IT.

You learn something, write about it, reflect on what you’ve written, identify areas that need more work, and then go back to learn more. This process reinforces the knowledge, allowing you to build on it each time you revisit it.