347 private links
How to do a migration properly without downtime? Avoid a transaction on all rows.
Method:
- add new column as nullable
- new writes use the new column and read from the old-and new columns
- Deploy
- Fix the old data in a batch. Pause briefly between each batch and run another one once the database load is correct.
The short version: one file, one process, and the heaviest page in the app served 315 million requests a day on a M1-laptop. For almost anything you are building, SQLite in WAL mode is enough, and the Postgres container you spun up out of habit was never needed.
The SQLite setup:
const db = new Database('chirp.db');
db.pragma('journal_mode = WAL'); // readers and the writer stop blocking each other
db.pragma('synchronous = NORMAL'); // fsync at checkpoints, not on every commit
db.pragma('busy_timeout = 5000'); // wait for the write lock instead of throwing
db.pragma('foreign_keys = ON'); // off by default, which surprises people
db.pragma('cache_size = -64000'); // 64MB of page cache
Under WAL, across every scenario, zero SQLITE_BUSY errors. Not "few." Zero.
Limits of SQLite:
- Reads stop scaling once anything writes.
- There is only one writer.
- One machine: the recovery time is always the time needed to restore a file.
Reach for Postgres when you have many writers contending on the same rows, when you need read replicas or automatic failover, when you need a real analytics engine over hundreds of millions of rows, or when your team genuinely needs the extension ecosystem. Those are real reasons."
Advantages of SQLite:
- Backups are a file copy. From the shell:
sqlite3 chirp.db "VACUUM INTO 'backup-$(date +%F).db'" - Local development is one file.
- Resetting the database is
rm - Tests get a real database each
- Deploys are a binary and a file
- There is nothing to operate
To optimize SQLite:
- focus on single-core speed
- buy enough RAM to hold the database: wait for GBs
- local NVMe. Never put SQLite on network storage.
- Pay for a dedicated core if you care about p99.
- Use
stricttables - Only INT, INTEGER, REAL, TEXT, BLOB, and ANY are allowed, and ANY is there when you actually want a key-value column. Store your JSON in a TEXT column and use the JSON functions on it.
- Prepare every query on startup. Making it on every request is a way to make SQLite look slow
The hardest part is user acquisition either way. Don't loose time on operations.
The reflex to start with a database server is a habit, not an engineering decision. It made sense when SQLite did lock the whole file on every write. That stopped being true a long time ago, and the tooling caught up: WAL for concurrency, STRICT for type safety, Litestream for replication.
But after programming in Rust for 10 years, I think that your coding style has the biggest impact on how your Rust code will look and feel.
People often say Rust’s syntax is ugly, but I’d argue the syntax is the least interesting thing about Rust. The semantics (the bits and pieces the language provides to express your ideas and how those bits combine to build interesting things) are much more important.
Parsing a .env can be clunky in basic algorithm, but elegant in Rust.
Tips:
read_to_string()instead of using a path, opening a file, creating a vector, adding the content to the vector to make a string- Use type inference:
let mut cfg = HashMap::new() - Lean into the typesystem:
.lines()to split strings safely and iterates over the linessplit_once()to get each key-value pair for each lines
- leverage error handling: use an enum to list all possible errors with important values and
thiserrorcan handle the error message
fn parse_config_file(path: &str) -> Result<HashMap<String, String>, ParseError> {
let content = read_to_string(path)?;
let mut config = HashMap::new();
for line in content.lines() {
match KeyValue::try_from(line) {
Ok(kv) => { config.insert(kv.key, kv.value); },
Err(ParseError::InvalidLine(_)) => continue, // Skip invalid lines
Err(e) => return Err(e), // Fail on any other error
}
}
Ok(config)
}
and why this code structure offers more extensibility!
Rust’s beauty is in its semantics and the core mechanics it provides: ownership, borrowing, pattern matching, traits, and so on. If you merely look at its (admittedly foreign) syntax, you overlook the real elegance of the language.
If there is anything that makes Rust “ugly”, it isn’t its syntax but the fact that it doesn’t hide the complexity underneath. Rust values explicitness and you have to deal with the harsh reality that computing is messy. Turns out our assumptions about a program’s execution are often wrong and our mental models are flawed.
Fortunately, we can encapsulate a lot of the complexity behind ergonomic abstractions; it just takes some effort! So don’t worry: once you start to confront your bad habits and look around for better abstractions, Rust stops being ugly.
That’s also the reason NaN !== NaN. If NaN behaved like a number and had a value equal to itself, well, you could accidentally do math with it: NaN / NaN would result in 1, and that would mean that a calculation containing a NaN result could ultimately result in an incorrect number rather than an easily-spotted “hey, something went wrong in here” NaN flag.
How to check the value can be used for a calculation?
typeof theValue === "number": to me too, it feels clunky th compare strings in order to use a numberNumber.isNaN()does exactly what it means: it checks for theNaN- but the global function
isNaN()returns true "“if I tried to make you into a number, would that work, or would you end up being NaN?"
And here is the problem: the encryption performs 2 passes over the data: first to encrypt, then to compute the authentication tag.
As we've seen before, it's bad because you will pay huge penalties when loading / unloading your data to / from memory to / from SIMD registers multiple times, even if you AES-CTR and GHash implementation are optimized to the mooooon.
Attribute creep and excessive use of foreign keys shows me is that in order to use ORMs effectively, you still need to know SQL. My contention with ORMs is that, if you need to know SQL, just use SQL since it prevents the need to know how non-SQL gets translated to SQL.
I’ve found myself thinking about the database as just another data type that has an API: the queries. The queries return values of some type, which are represented as some object in the program. By moving away from thinking of the objects in my application as something to be stored in a database (the raison d’être for ORMs) and instead thinking of the database as a (large and complex) data type, I’ve found working with a database from an application to be much simpler. And wondering why I didn’t see it earlier.
A utility for running arbitrary commands when files change. Uses kqueue(2) or inotify(7) to avoid polling. entr was written to facilitate rapid feedback on the command line.
headingoffset is a new HTML attribute to increase the upcoming heading levels. It's definitely not there yet https://caniuse.com/?search=headingoffset
How can I style an H3 when there's no H3 tag?
With the selector :heading(3).
But be aware of the browser support: inexistant (https://caniuse.com/mdn-css_selectors_heading)
TL;DR
Use shortand when it's obvious (padding-block and padding-inline have a great padding shorthand);
When each value is a distinct type (an animation for example)
When the syntax has its own rules until it stops carrying its weight (background)
That's a way
Note: In SQLite rowid tables are implemented as B+-Trees where all content is stored in the leaves of the tree, whereas WITHOUT ROWID tables are implemented using ordinary B-Trees with content stored on both leaves and intermediate nodes.
UUIDs take 5 to 16x times slower to insert. UUID without row id in SQLite are only 2 to 3 times slower.
Not random insertion is also useful to write in a database efficiently.
- choose fonts wisely
- optimize the font display:
body { text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } - golden ratio for line-height
- keep the amount of characters per line to 90
- prefer SVG and WEBP formats