mirror of
https://github.com/jbranchaud/til
synced 2026-09-02 17:51:47 +00:00
Compare commits
8
Commits
fe019c3338
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ee23956d2 | ||
|
|
7e4a02e66f | ||
|
|
965bade9d4 | ||
|
|
2a2a962864 | ||
|
|
a1708116bb | ||
|
|
85a6dd3644 | ||
|
|
453e9edbdf | ||
|
|
bcf957cabd |
@@ -10,7 +10,7 @@ working across different projects via [VisualMode](https://www.visualmode.dev/).
|
|||||||
|
|
||||||
For a steady stream of TILs, [sign up for my newsletter](https://visualmode.kit.com/newsletter).
|
For a steady stream of TILs, [sign up for my newsletter](https://visualmode.kit.com/newsletter).
|
||||||
|
|
||||||
_1869 TILs and counting..._
|
_1877 TILs and counting..._
|
||||||
|
|
||||||
See some of the other learning resources I work on:
|
See some of the other learning resources I work on:
|
||||||
|
|
||||||
@@ -172,6 +172,7 @@ If you've learned something here, support my efforts writing daily TILs by
|
|||||||
- [Monitor Usage Limits From CLI](claude-code/monitor-usage-limits-from-cli.md)
|
- [Monitor Usage Limits From CLI](claude-code/monitor-usage-limits-from-cli.md)
|
||||||
- [Open Current Prompt In Default Editor](claude-code/open-current-prompt-in-default-editor.md)
|
- [Open Current Prompt In Default Editor](claude-code/open-current-prompt-in-default-editor.md)
|
||||||
- [Resume Specific Session](claude-code/resume-specific-session.md)
|
- [Resume Specific Session](claude-code/resume-specific-session.md)
|
||||||
|
- [Set Permission Mode When Starting Session](claude-code/set-permission-mode-when-starting-session.md)
|
||||||
- [Stash The Current Prompt To Send Another First](claude-code/stash-the-current-prompt-to-send-another-first.md)
|
- [Stash The Current Prompt To Send Another First](claude-code/stash-the-current-prompt-to-send-another-first.md)
|
||||||
|
|
||||||
### Clojure
|
### Clojure
|
||||||
@@ -1105,6 +1106,7 @@ If you've learned something here, support my efforts writing daily TILs by
|
|||||||
- [Escape Curly Braces Within Formatted String](python/escape-curly-braces-within-formatted-string.md)
|
- [Escape Curly Braces Within Formatted String](python/escape-curly-braces-within-formatted-string.md)
|
||||||
- [Experiment With SQLite Queries In Memory](python/experiment-with-sqlite-queries-in-memory.md)
|
- [Experiment With SQLite Queries In Memory](python/experiment-with-sqlite-queries-in-memory.md)
|
||||||
- [Force Remaining Arguments To Be Named](python/force-remaining-arguments-to-be-named.md)
|
- [Force Remaining Arguments To Be Named](python/force-remaining-arguments-to-be-named.md)
|
||||||
|
- [Generate Sample PDFs With ReportLab](python/generate-sample-pdfs-with-reportlab.md)
|
||||||
- [Get Absolute Seconds From `timedelta` Object](python/get-absolute-seconds-from-timedelta-object.md)
|
- [Get Absolute Seconds From `timedelta` Object](python/get-absolute-seconds-from-timedelta-object.md)
|
||||||
- [Get Quotient And Remainder In One Operation](python/get-quotient-and-remainder-in-one-operation.md)
|
- [Get Quotient And Remainder In One Operation](python/get-quotient-and-remainder-in-one-operation.md)
|
||||||
- [Globally Install CLI Tool With UV](python/globally-install-cli-tool-with-uv.md)
|
- [Globally Install CLI Tool With UV](python/globally-install-cli-tool-with-uv.md)
|
||||||
@@ -1124,12 +1126,14 @@ If you've learned something here, support my efforts writing daily TILs by
|
|||||||
- [Parse Relative Time To datetime Object](python/parse-relative-time-to-datetime-object.md)
|
- [Parse Relative Time To datetime Object](python/parse-relative-time-to-datetime-object.md)
|
||||||
- [Publish A Package To A Test Env As A Dry Run](python/publish-a-package-to-a-test-env-as-a-dry-run.md)
|
- [Publish A Package To A Test Env As A Dry Run](python/publish-a-package-to-a-test-env-as-a-dry-run.md)
|
||||||
- [Reclassify Certain Packages As Dev Dependencies](python/reclassify-certain-packages-as-dev-dependencies.md)
|
- [Reclassify Certain Packages As Dev Dependencies](python/reclassify-certain-packages-as-dev-dependencies.md)
|
||||||
|
- [Register SQLite Adapter To Serialize Datetimes](python/register-sqlite-adapter-to-serialize-datetimes.md)
|
||||||
- [Resurface Exceptions Swallowed By Click Under Test](python/resurface-exceptions-swallowed-by-click-under-test.md)
|
- [Resurface Exceptions Swallowed By Click Under Test](python/resurface-exceptions-swallowed-by-click-under-test.md)
|
||||||
- [Select Implementation With Class Registry](python/select-implementation-with-class-registry.md)
|
- [Select Implementation With Class Registry](python/select-implementation-with-class-registry.md)
|
||||||
- [Set Up Pyright Type Checking In GitHub](python/set-up-pyright-type-checking-in-github.md)
|
- [Set Up Pyright Type Checking In GitHub](python/set-up-pyright-type-checking-in-github.md)
|
||||||
- [Skip Specific Pytest Test Cases](python/skip-specific-pytest-test-cases.md)
|
- [Skip Specific Pytest Test Cases](python/skip-specific-pytest-test-cases.md)
|
||||||
- [Sort A List Of Dataclass Instances](python/sort-a-list-of-dataclass-instances.md)
|
- [Sort A List Of Dataclass Instances](python/sort-a-list-of-dataclass-instances.md)
|
||||||
- [Sort Normalized Version Of Data](python/sort-normalized-version-of-data.md)
|
- [Sort Normalized Version Of Data](python/sort-normalized-version-of-data.md)
|
||||||
|
- [Specify Parameter Name For Click Option](python/specify-parameter-name-for-click-option.md)
|
||||||
- [Start Jupyter Notebook With Extra Packages](python/start-jupyter-notebook-with-extra-packages.md)
|
- [Start Jupyter Notebook With Extra Packages](python/start-jupyter-notebook-with-extra-packages.md)
|
||||||
- [Start The Debugger When A Test Errors](python/start-the-debugger-when-a-test-errors.md)
|
- [Start The Debugger When A Test Errors](python/start-the-debugger-when-a-test-errors.md)
|
||||||
- [Store And Access Immutable Data In A Tuple](python/store-and-access-immutable-data-in-a-tuple.md)
|
- [Store And Access Immutable Data In A Tuple](python/store-and-access-immutable-data-in-a-tuple.md)
|
||||||
@@ -1648,9 +1652,11 @@ If you've learned something here, support my efforts writing daily TILs by
|
|||||||
|
|
||||||
### SQLite
|
### SQLite
|
||||||
|
|
||||||
|
- [Add Check Constraint To Existing Column](sqlite/add-check-constraint-to-existing-column.md)
|
||||||
- [Display Results In Readable Column Format](sqlite/display-results-in-readable-column-format.md)
|
- [Display Results In Readable Column Format](sqlite/display-results-in-readable-column-format.md)
|
||||||
- [Explore The Database Schema](sqlite/explore-the-database-schema.md)
|
- [Explore The Database Schema](sqlite/explore-the-database-schema.md)
|
||||||
- [Manage Lightweight Schema Migrations With `user_version`](sqlite/manage-lightweight-schema-migrations-with-user-version.md)
|
- [Manage Lightweight Schema Migrations With `user_version`](sqlite/manage-lightweight-schema-migrations-with-user-version.md)
|
||||||
|
- [Output Query Result In Nicely Formatted Table](sqlite/output-query-result-in-nicely-formatted-table.md)
|
||||||
|
|
||||||
### Streaming
|
### Streaming
|
||||||
|
|
||||||
@@ -1680,6 +1686,7 @@ If you've learned something here, support my efforts writing daily TILs by
|
|||||||
- [Change Base Directory Of Existing Session](tmux/change-base-directory-of-existing-session.md)
|
- [Change Base Directory Of Existing Session](tmux/change-base-directory-of-existing-session.md)
|
||||||
- [Change Base Directory Without Detaching](tmux/change-base-directory-without-detaching.md)
|
- [Change Base Directory Without Detaching](tmux/change-base-directory-without-detaching.md)
|
||||||
- [Change The Default Prefix Key](tmux/change-the-default-prefix-key.md)
|
- [Change The Default Prefix Key](tmux/change-the-default-prefix-key.md)
|
||||||
|
- [Connect To Individual Overmind Processes Via tmux](tmux/connect-to-individual-overmind-processes-via-tmux.md)
|
||||||
- [Create A Named tmux Session](tmux/create-a-named-tmux-session.md)
|
- [Create A Named tmux Session](tmux/create-a-named-tmux-session.md)
|
||||||
- [Create A New Session In A New Server](tmux/create-a-new-session-in-a-new-server.md)
|
- [Create A New Session In A New Server](tmux/create-a-new-session-in-a-new-server.md)
|
||||||
- [Cycle Through Layouts](tmux/cycle-through-layouts.md)
|
- [Cycle Through Layouts](tmux/cycle-through-layouts.md)
|
||||||
@@ -1752,6 +1759,7 @@ If you've learned something here, support my efforts writing daily TILs by
|
|||||||
- [Check SSH Key Fingerprints Of Known Hosts](unix/check-ssh-key-fingerprints-of-known-hosts.md)
|
- [Check SSH Key Fingerprints Of Known Hosts](unix/check-ssh-key-fingerprints-of-known-hosts.md)
|
||||||
- [Check The Current Working Directory](unix/check-the-current-working-directory.md)
|
- [Check The Current Working Directory](unix/check-the-current-working-directory.md)
|
||||||
- [Check The Installed OpenSSL Version](unix/check-the-installed-openssl-version.md)
|
- [Check The Installed OpenSSL Version](unix/check-the-installed-openssl-version.md)
|
||||||
|
- [Check What Is Inside A Zip File](unix/check-what-is-inside-a-zip-file.md)
|
||||||
- [Clear The Screen](unix/clear-the-screen.md)
|
- [Clear The Screen](unix/clear-the-screen.md)
|
||||||
- [Combine All My TILs Into A Single File](unix/combine-all-my-tils-into-a-single-file.md)
|
- [Combine All My TILs Into A Single File](unix/combine-all-my-tils-into-a-single-file.md)
|
||||||
- [Command Line Length Limitations](unix/command-line-length-limitations.md)
|
- [Command Line Length Limitations](unix/command-line-length-limitations.md)
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# Set Permission Mode When Starting Session
|
||||||
|
|
||||||
|
The way I typically use Claude Code day-to-day is with a couple long-running
|
||||||
|
sessions for one to two clones of the project. I start a session with `claude`
|
||||||
|
and then hit `shift+tab` until I've toggled it to _auto_ mode. I do tightly
|
||||||
|
scoped features and `/clear` the context in between each.
|
||||||
|
|
||||||
|
I get used to being in _auto_ mode, so whenever I start a new `claude` session I
|
||||||
|
forget to first toggle from _manual_ to _auto_ mode.
|
||||||
|
|
||||||
|
This is where the
|
||||||
|
[`--permission-mode`](https://code.claude.com/docs/en/permission-modes) flag can
|
||||||
|
help. I can start a session directly in _auto_ mode like so:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
❯ claude --permission-mode auto
|
||||||
|
```
|
||||||
|
|
||||||
|
Or if I know I want to generate a plan first, I can start it in _plan_ mode.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
❯ claude --permission-mode plan
|
||||||
|
```
|
||||||
|
|
||||||
|
There is also the `--dangerously-skip-permissions` flag which is equivalent to
|
||||||
|
`--permission-mode bypassPermissions`. I tend to stay away from those unless I'm
|
||||||
|
working from a sandboxed dev container.
|
||||||
|
|
||||||
|
See `claude --help` for more details.
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# Generate Sample PDFs With ReportLab
|
||||||
|
|
||||||
|
The `reportlab` package has a `pdfgen` module that provides a `Canvas` API for
|
||||||
|
programmatically constructing a PDF.
|
||||||
|
|
||||||
|
I recently needed the most basic use of this to quickly construct a sample PDF
|
||||||
|
that I could use as I experimented with [different `qpdf`
|
||||||
|
commands](/workflow/remove-pages-from-a-pdf.md). I wanted to generate a 10-page
|
||||||
|
PDF where each page included a large font number in the center. This way I could
|
||||||
|
clearly see what was the first, second, third, and so on pages. Then I could
|
||||||
|
quickly verify that the `qpdf` manipulations I was doing were working as
|
||||||
|
expected.
|
||||||
|
|
||||||
|
I was able to do this without any setup or install. Using a `uv` one-liner (does
|
||||||
|
a 10-line heredoc count as a one-liner?), I generated a PDF with some basic
|
||||||
|
canvas rendering on each page.
|
||||||
|
|
||||||
|
```python
|
||||||
|
❯ uv run --with reportlab python - <<'EOF'
|
||||||
|
from reportlab.pdfgen import canvas
|
||||||
|
N, W, H = 10, 612, 792
|
||||||
|
c = canvas.Canvas("test.pdf", pagesize=(W, H))
|
||||||
|
for i in range(1, N + 1):
|
||||||
|
c.setFont("Helvetica-Bold", 220)
|
||||||
|
c.drawCentredString(W / 2, H / 2 - 80, str(i))
|
||||||
|
c.setFont("Helvetica", 28)
|
||||||
|
c.drawCentredString(W / 2, 100, f"page {i} of {N}")
|
||||||
|
c.showPage()
|
||||||
|
c.save()
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
The `--with` makes the `reportlab` package available to the command being run.
|
||||||
|
The command being run is `python` which evaluates that block of heredoc text.
|
||||||
|
|
||||||
|
Within the heredoc, I import `canvas` from the `reportlab` package that has been
|
||||||
|
made available. I then create an in-memory PDF of a standard size. Then for-loop
|
||||||
|
10 times to build each page of the PDF with a big number in the middle and a
|
||||||
|
smaller `page x of N` lower down on the page. Finally, I save the PDF which
|
||||||
|
writes it to the directory I executed this whole command from.
|
||||||
|
|
||||||
|
I had Claude help with the `canvas` API, however there are some decent
|
||||||
|
[`reportlab.pdfgen` docs
|
||||||
|
here](https://docs.reportlab.com/reportlab/userguide/ch2_graphics/).
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
# Register SQLite Adapter To Serialize Datetimes
|
||||||
|
|
||||||
|
SQLite doesn't have a datetime or timestamp data type, so that information has
|
||||||
|
to be stored as `text` or a unix epoch `int`. That means when using Python's
|
||||||
|
`sqlite3` module to perform writes I need to tell it what shape to write a
|
||||||
|
datetime value.
|
||||||
|
|
||||||
|
I could manually convert `datetime` values everywhere they are involved in
|
||||||
|
_write_ operations.
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Prepare `sessions` insert payload
|
||||||
|
session_data = {
|
||||||
|
"active": 1 if active else 0,
|
||||||
|
"project_id": project_id,
|
||||||
|
"start_time": datetime.isoformat(session.start_time),
|
||||||
|
"end_time": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
if session.end_time:
|
||||||
|
session_data["end_time"] = datetime.isoformat(session.end_time)
|
||||||
|
|
||||||
|
# Insert the new active session
|
||||||
|
cursor = self.conn.execute(
|
||||||
|
"""
|
||||||
|
insert into sessions (active, project_id, start_time, end_time)
|
||||||
|
values (:active, :project_id, :start_time, :end_time)
|
||||||
|
returning id;
|
||||||
|
""",
|
||||||
|
session_data,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
I've used `datetime.isoformat` above which formats `datetime` objects like so:
|
||||||
|
|
||||||
|
```python
|
||||||
|
>>> datetime.now().isoformat()
|
||||||
|
'2026-08-28T11:52:04.709907'
|
||||||
|
```
|
||||||
|
|
||||||
|
I'd like to make two improvements.
|
||||||
|
|
||||||
|
1. I want these `datetime` values to be formatted instead like `2026-08-28T18:15:27.213Z`.
|
||||||
|
2. I want `datetime` values to be serialized automatically in the specific shape
|
||||||
|
without having to manually convert them everywhere.
|
||||||
|
|
||||||
|
I can achieve both of these things by [registering an adapter with `sqlite3`](https://docs.python.org/3/library/sqlite3.html#how-to-register-adapter-callables)
|
||||||
|
that handles the serialize of `datetime` objects.
|
||||||
|
|
||||||
|
First, I define a function that can perform the `datetime` to `str` conversion.
|
||||||
|
I decided to put this in `db.py` where I have some other database-specific
|
||||||
|
functions.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
def to_db(dt: datetime) -> str:
|
||||||
|
if dt.tzinfo is None or dt.utcoffset() is None:
|
||||||
|
raise ValueError(f"Unable to store naive datetime: {dt!r}")
|
||||||
|
dt = dt.astimezone(timezone.utc)
|
||||||
|
return f"{dt:%Y-%m-%dT%H:%M:%S}.{dt.microsecond // 1000:03d}Z"
|
||||||
|
```
|
||||||
|
|
||||||
|
Then I register the adapter before creating the connection that gets used for
|
||||||
|
database interactions.
|
||||||
|
|
||||||
|
```python
|
||||||
|
import sqlite3
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from sqlite3 import Connection
|
||||||
|
|
||||||
|
def initialize_conn(db_file: Path) -> Connection:
|
||||||
|
# register adapters
|
||||||
|
sqlite3.register_adapter(datetime, to_db)
|
||||||
|
|
||||||
|
conn: Connection = sqlite3.connect(db_file)
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
|
||||||
|
return conn
|
||||||
|
```
|
||||||
|
|
||||||
|
Then I can run write operations with `datetime` objects knowing they will be
|
||||||
|
correctly serialized.
|
||||||
|
|
||||||
|
```python
|
||||||
|
with self.conn:
|
||||||
|
query = "update sessions set active = :active, end_time = :end_time where active = 1;"
|
||||||
|
self.conn.execute(
|
||||||
|
query,
|
||||||
|
{"active": 0, "end_time": session.end_time},
|
||||||
|
)
|
||||||
|
```
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# Specify Parameter Name For Click Option
|
||||||
|
|
||||||
|
[Click](https://click.palletsprojects.com/en/stable/)'s option decorator
|
||||||
|
provides a versatile way to define flags for a command. It has good defaults
|
||||||
|
that minimize the aspects of a flag that I need to be explicit about.
|
||||||
|
|
||||||
|
For example, a boolean `--init` flag for the `config` command could be specified
|
||||||
|
like so:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@cli.command()
|
||||||
|
@click.option(
|
||||||
|
"--init",
|
||||||
|
help="Initialize a config file with minimal defaults",
|
||||||
|
is_flag=True,
|
||||||
|
)
|
||||||
|
@pass_cli
|
||||||
|
def config(cli_ctx: CliContext, init: bool):
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Notice, in particular, that the flag string (`--init`) that I pass as the first
|
||||||
|
argument to `@click.option` has to correspond to the name of the parameter
|
||||||
|
`init`. Click passes all the defined options as keyword arguments when invoking
|
||||||
|
`config`. If the `init` parameter was changed to `initial`, there would be a
|
||||||
|
runtime error like this: `TypeError: config() got an unexpected keyword argument 'init'`.
|
||||||
|
|
||||||
|
Like I said though, Click is flexible when I need it to be. I can leave the flag
|
||||||
|
name as it is, but specify a different name to be used for the function
|
||||||
|
parameter. I found this useful when I realized that as I added support for a
|
||||||
|
`--json` flag I was inadvertently superseding the `json` import.
|
||||||
|
|
||||||
|
The second positional argument to `@click.option` can be included to rename that
|
||||||
|
parameter:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.option(
|
||||||
|
"--json",
|
||||||
|
"use_json",
|
||||||
|
help="Output all info details in JSON format",
|
||||||
|
is_flag=True,
|
||||||
|
)
|
||||||
|
@pass_cli
|
||||||
|
def info(cli_ctx: CliContext, use_json: bool):
|
||||||
|
# ...
|
||||||
|
|
||||||
|
if use_json:
|
||||||
|
click.echo(json.dumps(info_details, indent=2))
|
||||||
|
```
|
||||||
|
|
||||||
|
Both of these code blocks are excerpts from my [`py-vmt` time tracker project](https://github.com/jbranchaud/py-vmt).
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# Add Check Constraint To Existing Column
|
||||||
|
|
||||||
|
To not bury the lede, SQLite does not support adding a check constraint to an
|
||||||
|
existing column. Instead, if I want to add one after the fact, I can reconstruct
|
||||||
|
the table with the check constraint specified up front and then migrate the
|
||||||
|
data.
|
||||||
|
|
||||||
|
Here is a `sessions` table that includes `start_time` and `end_time` columns
|
||||||
|
that use the `text` data type to represent points in time.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
sqlite> .schema sessions
|
||||||
|
CREATE TABLE sessions (
|
||||||
|
id integer primary key,
|
||||||
|
active integer not null check (active in (0, 1)),
|
||||||
|
project_id integer not null references projects(id) on delete cascade,
|
||||||
|
start_time text not null,
|
||||||
|
end_time text,
|
||||||
|
created_at text not null default (datetime('now')),
|
||||||
|
updated_at text not null default (datetime('now'))
|
||||||
|
);
|
||||||
|
CREATE UNIQUE INDEX idx_sessions_single_active
|
||||||
|
on sessions(active)
|
||||||
|
where active = 1;
|
||||||
|
```
|
||||||
|
|
||||||
|
I want `start_time` and `end_time` to both enforce the shape of the timestamp
|
||||||
|
strings with `check` constraints. To do that, I need to start a transaction,
|
||||||
|
create a new version of the table with the check constraints, migrate the data,
|
||||||
|
rename `sessions` to `sessions_old`, rename `sessions_new` to `sessions` (that's
|
||||||
|
the in-place swap), and then commit the transaction. `sessions_old` can be
|
||||||
|
dropped later once I feel good about the migration.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
begin transaction;
|
||||||
|
|
||||||
|
create table sessions_new (
|
||||||
|
id integer primary key,
|
||||||
|
active integer not null check (active in (0, 1)),
|
||||||
|
project_id integer not null references projects(id) on delete cascade,
|
||||||
|
start_time text not null,
|
||||||
|
end_time text,
|
||||||
|
created_at text not null default (datetime('now')),
|
||||||
|
updated_at text not null default (datetime('now')),
|
||||||
|
check(start_time is strftime('%Y-%m-%dT%H:%M:%fZ', start_time)),
|
||||||
|
check(end_time is strftime('%Y-%m-%dT%H:%M:%fZ', end_time))
|
||||||
|
);
|
||||||
|
|
||||||
|
insert into sessions_new (
|
||||||
|
id, active, project_id, start_time, end_time, created_at, updated_at
|
||||||
|
)
|
||||||
|
select id, active, project_id, start_time, end_time, created_at, updated_at
|
||||||
|
from sessions;
|
||||||
|
|
||||||
|
alter table sessions rename to sessions_old;
|
||||||
|
|
||||||
|
alter table sessions_new rename to sessions;
|
||||||
|
|
||||||
|
drop index if exists idx_sessions_single_active; -- on sessions_old
|
||||||
|
|
||||||
|
create unique index idx_sessions_single_active
|
||||||
|
on sessions(active)
|
||||||
|
where active = 1;
|
||||||
|
|
||||||
|
commit;
|
||||||
|
```
|
||||||
|
|
||||||
|
Notice that after the table renames I also drop index (now pointing to
|
||||||
|
`sessions_old`) and recreate it for the _new_ `sessions` table.
|
||||||
|
|
||||||
|
This approach worked well for my situation, but may not be a one-size-fits-all
|
||||||
|
solution. Depending on how the database is deployed, the size of the tables, and
|
||||||
|
usage, this approach may not scale. Always do a dry-run of database migrations
|
||||||
|
like this.
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
# Output Query Result In Nicely Formatted Table
|
||||||
|
|
||||||
|
When I start a fresh SQLite connection and run a query, all the results are
|
||||||
|
squished together in a way that is poorly formatted, hard to read, and missing
|
||||||
|
column headers.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
sqlite> select sessions.id, start_time, end_time, projects.name from sessions join projects on projects.id = sessions.project_id limit 3;
|
||||||
|
1|2026-07-26T21:15:50.062936+00:00|2026-07-26T21:53:13.990Z|taco
|
||||||
|
2|2026-07-26T21:53:40.019946+00:00|2026-07-26T22:14:09.168Z|TIL
|
||||||
|
3|2026-08-02T17:00:16.119169+00:00|2026-08-02T17:30:16.119Z|py-vmt
|
||||||
|
```
|
||||||
|
|
||||||
|
I can drastically improve the look of this by turning _headers_ on and switching
|
||||||
|
to _box_ mode.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
sqlite> .headers on
|
||||||
|
sqlite> .mode box
|
||||||
|
sqlite> select sessions.id, start_time, end_time, projects.name from sessions join projects on projects.id = sessions.project_id limit 3;
|
||||||
|
┌────┬──────────────────────────────────┬──────────────────────────┬────────┐
|
||||||
|
│ id │ start_time │ end_time │ name │
|
||||||
|
├────┼──────────────────────────────────┼──────────────────────────┼────────┤
|
||||||
|
│ 1 │ 2026-07-26T21:15:50.062936+00:00 │ 2026-07-26T21:53:13.990Z │ taco │
|
||||||
|
│ 2 │ 2026-07-26T21:53:40.019946+00:00 │ 2026-07-26T22:14:09.168Z │ TIL │
|
||||||
|
│ 3 │ 2026-08-02T17:00:16.119169+00:00 │ 2026-08-02T17:30:16.119Z │ py-vmt │
|
||||||
|
└────┴──────────────────────────────────┴──────────────────────────┴────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
I personally find that much easier on the eyes. It is also a nicer format to
|
||||||
|
copy and paste into a post like this or a formatted code block that I'm sharing
|
||||||
|
with a colleague.
|
||||||
|
|
||||||
|
I can also do this directly from the CLI with a one-liner using `-header` and
|
||||||
|
`-box` like so:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
❯ sqlite3 /Users/lastword/.local/share/vmt/sessions.db -header -box "select sessions.id, start_time, end_time, projects.name from sessions join projects on projects.id = sessions.project_id limit 3"
|
||||||
|
┌────┬──────────────────────────────────┬──────────────────────────┬────────┐
|
||||||
|
│ id │ start_time │ end_time │ name │
|
||||||
|
├────┼──────────────────────────────────┼──────────────────────────┼────────┤
|
||||||
|
│ 1 │ 2026-07-26T21:15:50.062936+00:00 │ 2026-07-26T21:53:13.990Z │ taco │
|
||||||
|
│ 2 │ 2026-07-26T21:53:40.019946+00:00 │ 2026-07-26T22:14:09.168Z │ TIL │
|
||||||
|
│ 3 │ 2026-08-02T17:00:16.119169+00:00 │ 2026-08-02T17:30:16.119Z │ py-vmt │
|
||||||
|
└────┴──────────────────────────────────┴──────────────────────────┴────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
Run the `.help` dot-command from a SQLite prompt for a full listing of these
|
||||||
|
commands. See also `sqlite3 --help` from the CLI for usage details about all
|
||||||
|
flags.
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# Connect To Individual Overmind Processes Via tmux
|
||||||
|
|
||||||
|
A common `Procfile.dev` in a Rails project might look something like this.
|
||||||
|
|
||||||
|
```
|
||||||
|
web: bin/rails server -p $PORT
|
||||||
|
vite: bin/vite dev
|
||||||
|
worker: bundle exec good_job start
|
||||||
|
```
|
||||||
|
|
||||||
|
Instead of starting up each process that needs to be running for development to
|
||||||
|
work, I can instead run a tool that reads the procfile and sets it all up for me
|
||||||
|
-- like `overmind`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
❯ overmind start -f Procfile.dev
|
||||||
|
```
|
||||||
|
|
||||||
|
What's cool about `overmind` is that it starts its own `tmux` session and then
|
||||||
|
runs each of these processes in its own window.
|
||||||
|
|
||||||
|
I can connect to any one of them by name with `overmind connect <name>`. Or I
|
||||||
|
can connect to the session defaulting to the first window with just `overmind
|
||||||
|
connect`.
|
||||||
|
|
||||||
|
If I need to see what is going on with my background jobs, I'll run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
❯ overmind connect worker
|
||||||
|
```
|
||||||
|
|
||||||
|
This behaves like any other tmux session, so I can use my prefix key (`ctrl-z`
|
||||||
|
in my case) to access tmux-specific keybindings. Most notably, once I'm done
|
||||||
|
looking, I'll want to hit `ctrl-z d` to detach from the session.
|
||||||
|
|
||||||
|
I'm already using tmux as my daily driver which means its easy for me to end up
|
||||||
|
in a nested tmux session if I connect while already in my development session.
|
||||||
|
To help with that, I set up [a forwarding
|
||||||
|
prefix](set-up-forwarding-prefix-for-nested-session.md).
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# Check What Is Inside A Zip File
|
||||||
|
|
||||||
|
Before unzipping a file, it can be useful to check the contents. You can learn a
|
||||||
|
few things from doing this. First off, are all the files nested under some extra
|
||||||
|
top-level directory? How many files are in there? How big are certain files? Etc.
|
||||||
|
|
||||||
|
The `-l` flag to `unzip` can help answer these questions.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
❯ unzip -l zipped-files.zip
|
||||||
|
Archive: zipped-files.zip
|
||||||
|
Length Date Time Name
|
||||||
|
--------- ---------- ----- ----
|
||||||
|
0 08-22-2026 08:54 files-to-zip/
|
||||||
|
14 08-22-2026 08:54 files-to-zip/data.json
|
||||||
|
38 08-22-2026 08:53 files-to-zip/notes.md
|
||||||
|
14 08-22-2026 08:53 files-to-zip/hello.txt
|
||||||
|
--------- -------
|
||||||
|
66 4 files
|
||||||
|
```
|
||||||
|
|
||||||
|
Notice that the first line is for `files-to-zip/` which tells us that everything
|
||||||
|
is nested under the top-level directory that was probably the target of the
|
||||||
|
`zip` command. Everything is organized into a table with labeled columns which
|
||||||
|
allows us to answer our questions and get a good idea of what is in there.
|
||||||
|
|
||||||
|
In my experience, zip files tend to have dozens to hundreds of files, so it is
|
||||||
|
even better to pipe the above to a pager like `less`. That way you can search
|
||||||
|
and navigate through everything that's in there.
|
||||||
|
|
||||||
|
See `man unzip` for more details.
|
||||||
Reference in New Issue
Block a user