Weekly Reading — 2026-07-25

STOP pushing tables to PROD without testing. Use this one SQL command instead 👇 𝗠𝗜𝗡𝗨𝗦 It takes one dataset and 𝗠𝗜𝗡𝗨𝗦es the other. It gives you what's in the first one... but not in the… | José Siles | 86 comments

https://www.linkedin.com/posts/josesilesdata_stop-pushing-tables-to-prod-without-testing-activity-7450875782396166144-6X-0?utm_source=share&utm_medium=member_ios&rcm=ACoAAABSYL0BzA-Ra-ME9zoPy6kn-1Lq3-rn3qE

STOP pushing tables to PROD without testing. Use this one SQL command instead 👇 𝗠𝗜𝗡𝗨𝗦 It takes one dataset and 𝗠𝗜𝗡𝗨𝗦es the other. It gives you what's in the first one... but not in the second! It's the easiest way to spot differences. (𝘛𝘳𝘶𝘴𝘵 𝘮𝘦, 𝘮𝘰𝘴𝘵 𝘱𝘦𝘰𝘱𝘭𝘦 𝘴𝘬𝘪𝘱 𝘵𝘩𝘪𝘴 𝘵𝘦𝘴𝘵) If your Database doesn't allow it, don't worry! You can do the same with: 𝗘𝗫𝗖𝗘𝗣𝗧 in: - PostgreSQL - SQL Server - BigQuery Or you can replicate it with: 𝗟𝗘𝗙𝗧 𝗝𝗢𝗜𝗡 + 𝗜𝗦 𝗡𝗨𝗟𝗟 Check the infographic to see all 3 ways 👇🏼 --- ♻️ Repost if this was helpful, please! Follow 👉🏻 José for more about Data and AI

GOODBYE UNION ALL. GOODBYE Multiple table scans. GOODBYE Clunky aggregation queries. Hello 𝗚𝗥𝗢𝗨𝗣𝗜𝗡𝗚 𝗦𝗘𝗧𝗦! GROUPING SETS lets you get multiple levels of aggregation in a Single… | José Siles | 99 comments

https://www.linkedin.com/posts/josesilesdata_goodbye-union-all-goodbye-multiple-table-activity-7433496230359511040-v1ao?utm_source=share&utm_medium=member_ios&rcm=ACoAAABSYL0BzA-Ra-ME9zoPy6kn-1Lq3-rn3qE

I used to think data migration was just moving things from point A to point B. I was wrong. 😅 I'm currently working on migrating a full BI environment to Power BI, and honestly? It's been one of the most humbling experiences of my career so far. Nobody warns you that you'll spend more time questioning the old system than actually building the new one. You start pulling at one thread, and suddenly you're three hours deep into a report that nobody owns, built on logic that nobody documented, producing numbers that everyone trusts, but nobody can fully explain. 🤷🏻♂️ And that's kind of the point this work is regressive by nature. You're not innovating. You're excavating. Trying to understand decisions made years ago, in a different business context, by people who may not even be at the company anymore. I've also had people ask me, "Can't AI just do this for you?" And look, AI helps. It genuinely does. But there's a layer of this work that's purely human, sitting with stakeholders, asking the uncomfortable questions, and making judgment calls when the data just doesn't add up. No prompt is going to replace that conversation. 🤷 The biggest thing I've learned so far is simple: slow down at the start. The discovery phase feels unglamorous, but it saves you from a world of pain later on. If you've been through something similar, I'd genuinely love to hear how you handled it. Drop it in the comments 👇 #PowerBI #DataMigration #BusinessIntelligence #DataEngineering #BI #data

Auto-Delete old Mails from GMail

https://www.splitbrain.org/blog/2026-03/08-auto_delete_old_mails_from_gmail

My inbox is a mess. While I manage to maintain a somewhat close to “Inbox Zero” at work, my personal GMail's inbox currently sits at 2,215 emails 8-O. A large amount of those mails are notifications of some kind, like from Github for example.

GMail has some mechanisms to automatically sort mails and I do make use of that, but those filters are applied when the mail arrives only.

For notifications I want to see the mail. So it should land in my inbox. But when I have ignored it for a certain time, it's pretty sure I don't need it anymore. So I wanted a way to automatically delete certain mail threads that are older than a specified number of days.

So here is what I came up with using Google Apps Script.

/**
 * Main script to schedule
 *
 * Add one cleanEmails line per type of threads to clean
 */
function main() {
  cleanEmails(true, '<notifications@github.com>', 7);
}
 
 
/**
 * Find old thread based on given conditions
 * 
 * @param {bool} clean Set to false to not actually delete the mails
 * @param {string} searchString The string to search for
 * @param {int} minAgeDays The minimum age a thread (newest message) has to have
 * @param 
 */
function cleanEmails(clean, searchString, minAgeDays, headerField = 'from') {
 
  const threads = findRecentEmails(searchString, minAgeDays, headerField);
  console.log(`Found ${threads.length} threads`);
 
  threads.forEach((thread, i) => {
    logThread(thread);
    if(clean) {
      thread.moveToTrash();
      console.log('moved to Trash');
    }
  });
}
 
/**
 * Log a thread to console
 * 
 * @param {Thread} The gmail thread to log
 */
function logThread(thread) {
  const firstMsg = thread.getMessages()[0]; // newest message in thread
  const lastMsgDate = thread.getLastMessageDate();
  const dateStr = Utilities.formatDate(
    lastMsgDate,
    Session.getScriptTimeZone(),
    'yyyy-MM-dd HH:mm'
  );
 
  console.log(`Date: ${dateStr}, From: ${firstMsg.getFrom()}, Subject: ${thread.getFirstMessageSubject()}`);
}
 
/**
 * Find old thread based on given conditions
 * 
 * @param {string} searchString The string to search for
 * @param {int} minAgeDays The minimum age a thread (newest message) has to have
 * @param 
 */
function findRecentEmails(searchString, minAgeDays, headerField = 'from') {
  // Compute the cutoff date (messages must be older than this)
  const cutoffDate = new Date(Date.now() - minAgeDays * 24 * 60 * 60 * 1000);
  const cutoffFormatted = Utilities.formatDate(
    cutoffDate,
    Session.getScriptTimeZone(),
    'yyyy/MM/dd'
  );
 
  // Build Gmail search query:
  //   - headerField:searchString  → search in that header
  //   - before:YYYY/MM/DD         → older than cutoff
  //   - -is:starred               → skip starred
  const query = `${headerField}:${searchString} before:${cutoffFormatted} -is:starred`;
  console.log(`Query: ${query}`);
 
  // get the 50 newest threads with their newest message older than the cut-off date
  return GmailApp.search(query, 0, 75)
          .filter(thread => thread.getLastMessageDate() < cutoffDate)
          .sort((a, b) => b.getLastMessageDate() - a.getLastMessageDate())
          .slice(0, 50);
}

The script is scheduled to run main() every hour. For now it only handles my Github notification mails, but more will follow. The cleanEmails() function takes four parameters:

  1. clean: set it to false if you the method to run but not delete anything. The found threads will be printed to the console.

  2. searchString: this is what identifies the threads to delete, for example the sender's email address

  3. minAgeDays: defines how old the youngest message in the thread has to be to be considered for deletion

  4. headerField: defaults to from but you sometimes might want to search the subject instead.

Each call to cleanEmails() will delete the newest 50 threads matching your criteria. And by delete I mean move them to the “Trash” folder – GMail will delete them automatically after 30 days. This adds a bit of safety if something goes wrong.

With the script running every hour it will take a while until all my old Github notifications are gone, but that's fine. I heard that GMail can become sluggish when deleting too many mails at once.

Similar posts:

Gmail Backup and notmuch GUI

https://www.splitbrain.org/blog/2026-01/09-gmail_backup_notmuch_gui

Gmail Backup and notmuch GUI

I am still working adding services to my NAS and one thing that was on my todo list for a while was a proper GMail backup.

You never know when the powers that be deny you access to your mails for some obscure reasons. One guard is using my own domain. When I a can't use GMail anymore, I can just use my own mail server or a different provider. But it would suck losing all my old mail.

Backup

I don't necessarily need a running IMAP server to access the backup. But the backup should be in a format that allows my to point an IMAP server to the backup and be ready to go. That means I want the mails in a Maildir directory.

For the backup I am using GMail's IMAP server. To fetch your mails by IMAP you need to enable IMAP, have 2fa enabled (as you should in general) and create an app password. You also want to hide some of the default tags from IMAP. Basically only the INBOX, your sent mail and all your custom tags should be exposed. This avoids downloading a bunch of duplicates.

The GMail settings

To do the actual syncing, I decided to use the mbsync command from the isync package. More specifically I am using the theohbrothers/docker-isync Docker image.

The image will run one sync, then exit. I added a sleep after execution and then let Docker's restart policy taking care of running the next sync.

compose.yml
services:
  mbsync:
    image: theohbrothers/docker-isync:latest
    container_name: mbsync
    restart: unless-stopped
    user: "1000:100"
    environment:
      GMAIL_USER: ${GMAIL_USER}
      GMAIL_PASS: ${GMAIL_PASS}
    volumes:
      - ./volumes/vmail:/mail
      - ./config/mbsyncrc:/mbsyncrc:ro
    command: <
      sh -c 'while true; do
               echo "[$(date)] Running sync...";
               /sync;
               echo "[$(date)] Sync finished. Sleeping...";
               sleep 1800;
             done'

The configuration for the synchronization is relatively straight if forward. I added a bunch of comments for your and my understanding:

config/mbsyncrc
### Remote Config ###
IMAPStore gmail-remote
Host imap.gmail.com
Port 993
UserCmd "echo $GMAIL_USER"   # from environment
PassCmd "echo $GMAIL_PASS"   # from environment
AuthMechs LOGIN
TLSType IMAPS
PipelineDepth 1              # Avoid quota errors
Timeout 60                   # More graceful timeouts

### Local Config ###
MaildirStore gmail-local
Path /mail/
Inbox /mail/INBOX
SubFolders Verbatim

### Sync Settings ###
Channel gmail
Far :gmail-remote:
Near :gmail-local:
Patterns *
SyncState *
CopyArrivalDate yes          # Don't mess up message timestamps when moving them between folders.
Sync Pull                    # Download changes only, don't sync local changes up to the server.
Create Near                  # Automatically create new folders in the local copy.
Remove Near                  # Automatically remove deleted folders from the local copy.
Expunge Near                 # Expunge deleted messages from the local copy.

With the appropriate user and app password in a .env file, this will create your backup (the first run can take a loooong time).

Introducing notmore.

Next I wanted a way to look at my backup. Of course I could set up an IMAP server and point a mail client to it, but that seems relatively involved. What I wanted was a quick way to search through the Maildir.

A quick web search revealed notmuch - a xapian based mail search engine (and more that I am not using). It is super fast and simple to use. But it's CLI only. I wanted something with a web interface.

The general recommendation is netviel. A python/JavaScript interface to notmuch. Unfortunately it seems to be mostly abandoned with the last release 3 years ago and I had a really hard time getting it to work on modern Python versions within a Docker container.

So instead I decided to roll my own. I vibe engineered a PHP based search interface that wraps around the notmuch command line interface. It let's you search using notmuch's comprehensive syntax, displays the result threads and allows you to drill down into the individual mails. Attachments can also be downloaded through the interface. Simple, not more.

The tool is aptly named “notmore” and available on Github.

The repo also builds a ready to use Docker image. You can just point it at the Maildir created by the sync job above.

My full docker compose setup can be found in my github repository for the NAS docker setup.

Similar posts:

Comments

Setup Guide - msgvault

https://www.msgvault.io/setup/

Setup Guide

Install Release

macOS / Linux:

curl -fsSL https://msgvault.io/install.sh | bash

Windows (PowerShell):

powershell -ExecutionPolicy ByPass -c "irm https://msgvault.io/install.ps1 | iex"

The installer detects your OS and architecture, downloads the latest release from GitHub Releases, verifies the SHA-256 checksum, and installs the binary.

Windows releases include native AMD64 and ARM64 packages. On Windows ARM64, the PowerShell installer selects the native package when the release provides one and falls back to the AMD64 package under emulation for older releases.

Running on a headless server?

msgvault works on headless machines (SSH, VPS, NAS, Docker), but OAuth requires a browser for the initial authorization. You'll authorize on your local machine and copy the token file to the server. See Headless Server Setup for the copy-token workflow, or jump to the Remote Deployment guide for a full NAS/server setup with Docker Compose.

Verify the installation:

Conda-Forge

If you use conda or pixi:

# Using pixi (recommended)
pixi global install msgvault

# Using conda
conda install -c conda-forge msgvault

Build From Source

Requires Go 1.26+ and a C/C++ compiler (GCC or Clang). CGO is required because msgvault uses mattn/go-sqlite3 (SQLite with FTS5) and duckdb-go/v2 (Parquet analytics), both of which compile native extensions.

git clone https://github.com/kenn-io/msgvault.git
cd msgvault
make install

On macOS and Linux this installs to ~/.local/bin or $GOPATH/bin. For a debug build use make build, or make build-release for an optimized binary with stripped debug symbols.

On Windows, use the native PowerShell helper:

.\scripts\build.ps1          # Debug build
.\scripts\build.ps1 -Release # Optimized, stripped build

It detects AMD64 or ARM64 automatically and writes msgvault.exe in the repository root. See Development and Roadmap for the one-time MSYS2 compiler prerequisites.

Verify the installation:

Configure OAuth

Create a Google Cloud project, enable the Gmail API, and download your client_secret.json. If you plan to archive Google Calendar, enable the Google Calendar API too. See the full OAuth Setup Guide.

Where to put config.toml

msgvault stores all data (config, database, tokens, attachments) in a single directory. The default location depends on your platform:

Platform Data directory Config file
macOS / Linux ~/.msgvault/ ~/.msgvault/config.toml
Windows C:\Users\<you>\.msgvault\ C:\Users\<you>\.msgvault\config.toml

Tip

The .msgvault directory is created automatically the first time you run any msgvault command. If you're unsure of the exact path, run msgvault add-account [email protected]; the error message may show you where to create the config file.

To store data on a different drive or location, use the --home flag or set the MSGVAULT_HOME environment variable. If MSGVAULT_HOME is set, paths in the table above are relative to that directory instead:

Per-command (any platform):

msgvault sync --home E:/msgvault

Windows (PowerShell, persistent):

$env:MSGVAULT_HOME = "E:\msgvault"
# Or set it permanently:
[Environment]::SetEnvironmentVariable("MSGVAULT_HOME", "E:\msgvault", "User")

macOS / Linux (persistent):

export MSGVAULT_HOME=/mnt/data/msgvault

The --home flag takes priority over MSGVAULT_HOME. See Configuration for all options.

Create the config file

macOS / Linux:

[oauth]
client_secrets = "/path/to/client_secret.json"

Windows: use forward slashes in the path:

[oauth]
client_secrets = "C:/Users/you/Downloads/client_secret.json"

Add Your Account

This opens your browser for OAuth consent. For headless servers, see the copy-token workflow.

If you plan to deploy to a remote host (NAS, cloud VM, etc.), run msgvault setup after this step to generate a ready-to-run deployment bundle with Docker Compose and remote configuration. See the Remote Deployment guide.

Add an IMAP Account

To sync mail from a non-Gmail provider (Fastmail, Outlook, Yahoo, self-hosted, etc.), use add-imap:

You will be prompted for your password (or set MSGVAULT_IMAP_PASSWORD / pipe via stdin for scripting). The command tests the connection before saving credentials. Use an app-specific password if your provider supports them.

Common IMAP servers:

Provider Host Port Notes
Fastmail imap.fastmail.com 993 App password recommended
Outlook / Hotmail outlook.office365.com 993 Use add-o365 for OAuth (recommended); or app password with 2FA
Yahoo imap.mail.yahoo.com 993 App password required
iCloud imap.mail.me.com 993 App-specific password required
Gmail (IMAP) imap.gmail.com 993 Use add-account for Gmail API instead
Self-hosted your server hostname 993

For STARTTLS connections (port 143), add --starttls:

After adding the account, sync it the same way as a Gmail account:

IMAP accounts are stored in the same database as Gmail accounts. All tools (TUI, search, MCP, web server) work with IMAP messages the same way.

Microsoft 365 / Outlook.com

For Outlook, Hotmail, Live.com, and Microsoft 365 accounts, add-o365 provides OAuth-based access without app passwords. It auto-detects the correct IMAP host and configures XOAUTH2 authentication. See the OAuth Setup guide for details.

Yahoo App Passwords

Yahoo requires an App Password for IMAP access. Your regular Yahoo password will not work. To generate one:

  1. Go to Yahoo Account Security
  2. Under Generate and manage app passwords, click Generate app password
  3. Enter msgvault as the app name and copy the generated password
  4. Use this password when add-imap prompts for your credentials

Note

IMAP sync always performs a full scan of the mailbox. The sync (incremental) command falls back to a full sync for IMAP accounts because IMAP does not provide a change-tracking API like Gmail's History API. Messages already in the database are skipped efficiently.

Sync Email

What to Expect

The initial full sync downloads every message and attachment from Gmail, so it can take a while. In testing we have observed roughly 50 messages per second on fast internet, but the Gmail API has per-user quotas that may throttle throughput further. An account with hundreds of thousands of messages and large attachments may take several hours; an account with millions of messages could take significantly longer. We recommend starting with --limit or a date range to verify everything works before kicking off the full run.

The good news: syncs are resumable (see below), and once the initial sync is complete, incremental syncs only fetch new and changed messages, which is much faster.

Disk Usage

msgvault stores raw MIME data compressed with zlib (typically 3-5x compression). As a rough guide:

Gmail usage (Settings → Storage) SQLite DB on disk Parquet cache Attachments
5 GB ~1-2 GB < 10 MB varies
25 GB ~5-10 GB < 50 MB varies
100 GB ~20-40 GB < 100 MB varies

Gmail's "storage used" number includes attachments at full size. Your on-disk footprint depends on the ratio of message text to attachments:

  • Message metadata + bodies go into the SQLite database, compressed ~3-5x.
  • Attachments are extracted and stored as-is (PDFs, images, etc. are already compressed). Identical attachments across messages are deduplicated by content hash.
  • Parquet analytics cache is a lightweight projection for the TUI — typically a few MB even for large archives.

Use --limit or a date range for your first sync to gauge the ratio for your mailbox before committing to a full sync. After syncing, msgvault stats shows the actual sizes. See Data Storage for details on compression and storage layers.

Full Sync Flags

Flag Description
--limit N Download at most N messages
--after YYYY-MM-DD Only messages after this date
--before YYYY-MM-DD Only messages before this date
--query Gmail search query filter
--noresume Start fresh instead of resuming
--verbose Show detailed progress

Incremental Sync

After the initial full sync, use incremental sync for efficient updates. It uses the Gmail History API to fetch only new and changed messages:

Resumable Checkpoints

If a sync is interrupted (network error, Ctrl+C), run the same command again. It resumes from the last checkpoint:

# This resumes automatically
msgvault sync-full [email protected] --after 2024-01-01 --before 2024-02-01

Checkpoint data is stored in the sync_checkpoints table. Use --noresume to discard checkpoints and start over.

Rate Limiting

msgvault uses token bucket rate limiting to respect Gmail API quotas. The default is 5 requests per second, configurable in config.toml:

[sync]
rate_limit_qps = 5

Reduce this value if you encounter rate limit errors during large syncs.

Safety

Sync operations are read-only. They use only messages.list and messages.get Gmail APIs. No write operations are performed. Your Gmail data remains untouched.

Explore

# Search your archive
msgvault search from:[email protected]

# Launch the interactive TUI
msgvault tui

# View stats
msgvault stats
msgvault TUI showing the Senders view

See Searching and Interactive TUI for more.

Optional: Sync Google Calendar

To archive Calendar events alongside email, authorize Calendar access and run a calendar sync:

Calendar sync is read-only. Events become searchable with --message-type calendar_event; see Google Calendar for the full workflow, scheduled sync, and headless-server setup.

Optional: Sync Microsoft Teams

To archive Teams chats and channels, add Microsoft Graph permissions to your Microsoft app registration, authorize Teams, then sync:

Teams messages become searchable with --message-type teams. See Microsoft Teams for required Graph permissions, scheduling, and inline media backfill.

Optional: Configure Backups

Create a backup repository before relying on the archive as your source of truth:

msgvault backup init --repo ~/Backups/msgvault
msgvault backup create --repo ~/Backups/msgvault
msgvault backup verify --repo ~/Backups/msgvault

Record the repository in config.toml so future commands can omit --repo:

[backup]
repo = "~/Backups/msgvault"

See Backup for restore, verification, scheduling, and secret-handling details.

JSON in your SQL tables? Here’s how to handle it 👇 𝗟𝗔𝗧𝗘𝗥𝗔𝗟 𝗙𝗟𝗔𝗧𝗧𝗘𝗡 One of the most common Data Engineering tasks is turning nested JSON arrays into flat rows. In the example below,… | José Siles | 53 comments

https://www.linkedin.com/posts/josesilesdata_json-in-your-sql-tables-heres-how-to-handle-activity-7460297814066098176-3JMh?utm_source=share&utm_medium=member_ios&rcm=ACoAAABSYL0BzA-Ra-ME9zoPy6kn-1Lq3-rn3qE

JSON in your SQL tables? Here’s how to handle it 👇 𝗟𝗔𝗧𝗘𝗥𝗔𝗟 𝗙𝗟𝗔𝗧𝗧𝗘𝗡 One of the most common Data Engineering tasks is turning nested JSON arrays into flat rows. In the example below, we use LATERAL FLATTEN to explode one order into multiple rows. One row per item purchased. This example is in Snowflake, but you can achieve the same result in other engines: 🔍 BigQuery: CROSS JOIN UNNEST(column) 🧱 Databricks: LATERAL VIEW EXPLODE(column) 🐘 PostgreSQL: CROSS JOIN LATERAL unnest(column) Once you flatten it, the analysis is easier. Now you can easily answer questions like: ➜ How many items did we sell? ➜ Which item was sold the most? 👉 Nested data hides insights. Flattening reveals them. --- ♻️ Repost if you found it useful, please! 🔔 Follow José for more about SQL, Data and AI

What can I do to fight authoritarianism?

https://choosedemocracy.us/what-can-i-do/

People are resisting at every level. Tesla Takedowns. Refusing illegal orders. Court challenges. Boycotting. Thousands of protests. (More examples!)

Alongisde grounding yourself in these times, here are some starting points on how to orient and help fight back.

#1.

Get with Others to Act

When you’re alone it’s too easy to freeze. While keyboard warriors and protest attenders are important — you’ll feel the greatest strength if you gather with others semi regularly to plan together, share together, and act together.

#2.

Pressure a pillar of support to defect

Authoritarian regimes are only successful when society bows to the orders of the autocrat. We can effectively fight back by identifying and withdrawing our support from the “pillars of support” (military, media, corporations, etc.) that prop up these dangerous rulers. 

Watch this video to better understand the ‘Pillars of Support’. Then, pick a pillar you want to pressure and each day do at least one small thing to get them to defect.

Pick a path and then find an action that fits your degree of difficulty (we’ve categorized harder actions as those that require more time, people skills, and often a small group to launch with).


There is a lot of information here, so take a deep breath.  There is a Jewish teaching that says that you are not responsible for completing the work, nor are you allowed to desist from it.  We will succeed because millions of people do a couple things well, not because one person does a million things. 

While we wish we were not compelled to travel down this path of resistance.  We are excited to realize the collective beauty we can and will create together.

You got this.

Suggestions? Additions? Want further support?

Chris Paul Was Sent Home for Being Himself

https://www.newyorker.com/sports/sporting-scene/chris-paul-was-sent-home-for-being-himself

This fall, Chris Paul announced that he would spend his final season with the Los Angeles Clippers. So far, his second tenure with the team has been a disappointment, even if it was never supposed to be like the first. From 2011 to 2017, Paul helmed a Clippers team best known as Lob City, one of the most thrilling basketball teams in my lifetime. It was a squad that played at a breakneck pace, with the ball thrust into orbit by Paul, and hanging suspended in the arena for a moment, only for one of his hyper-athletic teammates (most likely from the duo of élite rim runners DeAndre Jordan and Blake Griffin) to snatch it out of the air and ram it through the rim. Lob City revived Clippers basketball, which had historically been an afterthought compared to the team they shared a city and arena with, the Los Angeles Lakers. The Lob City-era Clippers were not without controversy (last year, I wrote about the show “Clipped,” which narrativizes the fallout from the racism of the then owner Donald Sterling), but on the court, the Clippers were, for a time, the greatest show in California. Still, despite their dominance, they were never good enough to win a championship. This, too, will be a part of Paul’s legacy: throughout his career, he always made teams better. That was his M.O. He would go to a team that was either not very good, or good but not yet great, and he would instantly improve their odds, often while acting as a mentor to a younger star who needed his veteran wisdom. After his time on the Clippers, he defected to the conference rival Houston Rockets, then did stints in Oklahoma City, Phoenix, San Francisco, and San Antonio. At every stop, he improved the team’s win percentage, sometimes dramatically, and turned two of the teams into bona-fide contenders. But, for all of this success improving underperforming teams, Chris Paul has never actually won a championship. This isn’t uncommon. Countless Hall of Famers from the nineties either retired ringless, or spent the early two-thousands hanging on the coattails of contenders in an attempt to get a ring, because Michael Jordan and the Chicago Bulls so thoroughly dominated the decade. The Lob City Clippers couldn’t get past the dynastic Golden State Warriors. Neither could the Houston Rockets, though in 2018 they did come close, taking Kevin Durant and Steph Curry to seven games in the Western Conference Finals. Paul got closest to a ring in 2021, in Phoenix, when he won the first two games of the Finals before losing four straight to Giannis Antetokounmpo and the Milwaukee Bucks.

We live in an age of basketball in which rings are, increasingly, treated as the bottom line, despite them being a flawed heuristic for individual dominance. This means that Chris Paul’s legacy, to some, has an asterisk attached, a fact that is compounded by the his ability to make deep playoff runs so often. He just couldn’t drag a team past that point. He’ll make your team good, maybe even great, but never the greatest.

And so, this past off-season, he returned to the Clippers, not necessarily to chase a ring but to return to the franchise that raised him. “If I’m really honest, I wanted to get back and play here by any means necessary,” Paul said. “I didn’t even care what the team looked like. I just wanted to be home, be here with the Clippers.” It was clear that it would be his final season, and everyone assumed that he’d get a proper farewell tour featuring standing ovations and tribute videos—the things that future Hall of Famers sometimes get when they announce their final season. He would be a bench player, logging ten to twelve minutes a game while providing veteran leadership. It would be a feel-good season, and he’d be seen off generously.

That plan came to an end last week, when the Clippers abruptly sent Chris Paul home from a road game in Atlanta and put out a statement explaining that Paul would not be with the team for the rest of the season. The next day, ESPN’s Shams Charania reported on the reason for his departure: “Chris Paul and his leadership style clashed with the Clippers, sources tell ESPN. Paul has been vocal in holding management, coaches and players accountable, which the team felt became disruptive.” Charania also reported that Paul and the Clippers’ head coach, Ty Lue, had not been on speaking terms for “several weeks,” an alarming fact considering that the N.B.A. season itself is less than two months old. (Lue later told reporters this was not true.)

Zohran Mamdani and Mahmoud Khalil Are in on the Joke

https://www.newyorker.com/news/essay/zohran-mamdani-and-mahmoud-khalil-are-in-on-the-joke

These days, all I talk about and think about is the cognitive dissonance required to move through the world. Increasingly, I struggle to disentangle my many selves, to get on with the day. All my selves weep often. I try to have grace. I tell my friends that I’m no longer sure how anyone just drifts through the days, the months, without acknowledging the horrors. I imagine what it must be like to be able to turn off the parts of the world that unsettle you. It must feel like existing in an animated universe that adheres to cartoon physics: you fall from an inconceivable height and, landing, a cloud of dust billows up from the ground, but then you shake yourself off and keep moving.

I convince myself that I still laugh enough. Everyone I love would like to see an end to wars, would like to stop people from being snatched off the streets and deported, but some days we can’t march, because it’s so hot outside where I am that it is dangerous. It is dangerously hot outside, in part because of the climate consequences of the wars; they don’t stop and haven’t stopped for as long as we’ve been alive. The masjid on my block received threats last year, so community members put our money together to hire security. One of the elders joked that as long as the masjid was empty someone should feel free to burn it down—it would give us a good excuse to finally remodel. We laughed. I feel most Muslim when others might think that the joke is on my people, but my people are surviving, and so the joke is actually not on us at all.

At the end of Youssef’s show, he paced the stage in the near-silent aftermath of rousing applause. Then he began talking about two things that had left him hopeful that week. I clocked a couple in the row ahead of me mouthing “Zohran?” Then there he was, onstage, waving, taking in a standing ovation. He talked briefly about his vision for a different New York, one where people could advocate for Palestinian rights without the threat of persecution. And then Youssef introduced Khalil, who got an even bigger, more raucous ovation. He smiled wide and threw up a fist, a gesture that many in the audience returned.

Toward the end of Khalil’s short remarks, he looked at Mamdani as if it were just the two of them in the room, and he said, “I am excited about the possibility of raising my son in a city where you are mayor.” It was a stunning moment, one of those times when you could, if you were listening, hear a collective catching of breath before a fresh wave of applause. People made their way to the exits, some wiping away tears. I saw a friend, and we hugged. She said, “I didn’t realize I was crying, but now I can’t stop.”

As beautiful as that memory was, what lingered in my mind was the final joke of the night, when Khalil said that he was honored to be standing with Mamdani, “a man so principled that ICE hasn’t arrested him yet.” He paused and then, with perfect timing, added, “You can tell they are thinking about it.” Mamdani laughed. Youssef laughed. I laughed in my seat. It was a classically funny joke, made by someone who had been snatched from the lobby of his building by ICE agents, a joke lovingly directed at a mayoral candidate who has been threatened with deportation by the President and other political leaders. The joke was funny because of what some of us in the room knew and because of what the person telling it had lived through. The joke was funny because, though some of us in the room wept often, our laughter outpaced what we know of the world, for a few seconds at a time. ♦

Sly Stone’s Political and Musical Awakening

https://www.newyorker.com/culture/postscript/sly-stones-political-and-musical-awakening

In May of 1971, Marvin Gaye released what many consider to be his masterwork, “What’s Going On?” A song cycle told from the point of view of a Vietnam veteran returning to the United States after serving abroad, having witnessed all manner of suffering, violence, and injustice, it is not an entirely idyllic album. Yet the inquisitive nature of its title, and the question that foregrounds the work, suggests a faith in America held, if not by Gaye himself, then certainly by the speaker he invents to guide listeners through the forest of concerns the album conjures. We are to understand that the soldier in question left one place to go fight in another, and, upon returning home, found no improvement from the conditions he’d left.

By November of the same year, Sly Stone seemed to pose an answer: “There’s a Riot Goin’ On.” At the time that album was released, Stone, who died this week, at the age of eighty-two, was facing a make-or-break career inflection point. He and his band, Sly and the Family Stone, the multiracial, late-sixties-utopian-fever-dream ensemble, had ended the previous decade on a high, with the album “Stand!” garnering overwhelming acclaim. An accompanying performance at Woodstock left their label, Epic Records, eager for a next release. Yet, by 1971, all that had been put out were a couple of singles and a greatest-hits album. Stone missed recording deadlines. His relationship with the band had begun to fray amid creative disagreements, drug use, and Sly’s erratic behavior. Amid all of this, “There’s a Riot Goin’ On” landed as a jolt, as if he’d just shaken himself out of a long slumber, looked upon the country, and realized that he couldn’t stand carrying on in it the same way.

“There’s a Riot Goin’ On” acted almost as an inverse to some of the more upbeat, chart-friendly pursuits of Sly and the Family Stone’s earlier career. “Stand!” had been recorded at Pacific High Studios, in San Francisco. Just days after its release came Bloody Thursday, when police cracked down on antiwar protests at Berkeley, killing one and injuring at least a hundred and twenty-eight. Yet “Stand!” evinced a kind of relentless optimism, with exhilarating instrumentation ornamenting lyrics that were sometimes vague and gauzy. It closed with the song “You Can Make It If You Try,” which emanated a confidence that fortunes might change for us all. In the time between “Stand!” and “There’s a Riot Goin’ On,” Stone had faced increasing pressure from the Black Panthers, whose leaders wanted him to be more politically direct, and to de-integrate his band. The idealism of the sixties was dead, and with its death came the harsh realities of the new moment: political assassinations, expanded police violence, a shifting civil-rights movement, and a sense of disillusionment that seemed to haunt Stone as he searched for a purpose beyond hippie-culture stardom.

The grooves on “Riot” were deeper and darker, with collisions of vocals that in some moments barely resembled language at all. There was a kind of performance of apathy on the album which felt like both a reflection of Sly’s lived reality—a kind of rage that led itself to confusion, and then exhaustion—and a mimicry of the hippie movement, interrogating where it had left people and who it had left behind. On the first track, “Luv N’ Haight,” Sly’s voice is a distorted mumble as he sings the opening lyrics: “Feel so good inside myself / don’t wanna move.” As the song progresses, it becomes a polyphonic collision of sentiments: feel so good feel so good wanna move wanna move coming up against feel so good inside myself, don’t wanna move. It is hard to tell where one phrase peels off from the other, or whether the two statements are even at odds with each other. They exist within the same addled mind: We have to do something; we don’t know what it is. There was a sense of both resolve and resignation on the album, a heartbreak that seems almost in conversation with the hopeful ending of “Stand!” Now, over stark and downbeat instrumentals, Stone implied that you, like he, may have been dreaming.