<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>PostgreSQL on CodeBytes - The Curious Engineer</title><link>https://chris-ayers.com/tags/postgresql/</link><description>Recent content in PostgreSQL on CodeBytes - The Curious Engineer</description><generator>Hugo -- gohugo.io</generator><language>en-US</language><copyright>© 2026 Chris Ayers</copyright><lastBuildDate>Sat, 07 Nov 2026 21:15:21 -0500</lastBuildDate><atom:link href="https://chris-ayers.com/tags/postgresql/index.xml" rel="self" type="application/rss+xml"/><item><title>Put the Debugging Tools Next to the App</title><link>https://chris-ayers.com/posts/aspire-field-notes-terminals-and-repls/</link><pubDate>Sat, 07 Nov 2026 21:15:21 -0500</pubDate><guid>https://chris-ayers.com/posts/aspire-field-notes-terminals-and-repls/</guid><description>&lt;p&gt;The catalog looks wrong. Before changing the API, you want to answer a smaller question: are the expected products actually in its database?&lt;/p&gt;&#10;&lt;p&gt;Logs tell you what the application reported. A database query lets you inspect the stored rows directly. Aspire 13.6.4 can open an opt-in PostgreSQL client beside the dashboard, without making you find a port, install &lt;code&gt;psql&lt;/code&gt;, or copy a password.&lt;/p&gt;</description><content:encoded>&lt;p&gt;The catalog looks wrong. Before changing the API, you want to answer a smaller question: are the expected products actually in its database?&lt;/p&gt;&#10;&lt;p&gt;Logs tell you what the application reported. A database query lets you inspect the stored rows directly. Aspire 13.6.4 can open an opt-in PostgreSQL client beside the dashboard, without making you find a port, install &lt;code&gt;psql&lt;/code&gt;, or copy a password.&lt;/p&gt;&#10;&lt;p&gt;The main exercise in this part of &lt;a href="https://chris-ayers.com/series/aspire-field-notes/" &gt;Aspire Field Notes&lt;/a&gt; is straightforward: open the catalog&amp;rsquo;s database client, inspect the three seeded products, and close the client. An optional terminal tape makes those same stored-data checks repeatable. The sample does not deliberately corrupt data or reproduce a new failure; its known seed gives you a baseline for that diagnostic workflow.&lt;/p&gt;&#10;&#10;&lt;h2 class="relative group"&gt;Start with the catalog&#10; &#10; &#10; &#10; &#10;&lt;/h2&gt;&#10;&lt;p&gt;Use &lt;strong&gt;Aspire CLI and AppHost SDK 13.6.4&lt;/strong&gt; with the companion&amp;rsquo;s .NET 10 SDK and Node.js prerequisites. The &lt;a href="https://github.com/codebytes/blog-samples/tree/main/aspire-field-notes" target="_blank" rel="noreferrer"&gt;sample README&lt;/a&gt; has the setup; run the commands below from &lt;code&gt;aspire-field-notes/&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;Use the catalog AppHost from &lt;a href="https://chris-ayers.com/posts/aspire-field-notes-model-the-whole-app/" &gt;part two&lt;/a&gt;, with a running Docker or Podman engine and the README&amp;rsquo;s one-time &lt;code&gt;postgres-password&lt;/code&gt; secret setup. You do not need a local &lt;code&gt;psql&lt;/code&gt; installation.&lt;/p&gt;&#10;&lt;p&gt;Select the catalog AppHost explicitly so the commands do not depend on your current CLI selection. If your catalog is already running without REPL support, stop that run with &lt;code&gt;aspire stop --apphost catalog/Catalog.AppHost/Catalog.AppHost.csproj&lt;/code&gt; before starting it with the opt-in flag below.&lt;/p&gt;&#10;&#10;&lt;h2 class="relative group"&gt;Inspect PostgreSQL without moving its password&#10; &#10; &#10; &#10; &#10;&lt;/h2&gt;&#10;&lt;p&gt;Start the catalog with REPL support enabled:&lt;/p&gt;&#10;&lt;div class="highlight-wrapper"&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;apphost&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;catalog/Catalog.AppHost/Catalog.AppHost.csproj&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;Diagnostics__EnableRepl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; aspire start --apphost &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$apphost&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; --isolated&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&#10;&lt;p&gt;In the dashboard, wait for &lt;strong&gt;api&lt;/strong&gt; to become &lt;strong&gt;Healthy&lt;/strong&gt;, then choose &lt;strong&gt;postgres &amp;gt; Actions &amp;gt; REPL&lt;/strong&gt;. API readiness matters here because the API seeds &lt;code&gt;catalog_items&lt;/code&gt; before accepting traffic.&lt;/p&gt;&#10;&lt;p&gt;The catalog AppHost already contains this opt-in guard:&lt;/p&gt;&#10;&lt;div class="highlight-wrapper"&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ExecutionContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsRunMode&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetValue&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;Diagnostics:EnableRepl&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithRepl&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&#10;&lt;p&gt;It applies &lt;code&gt;WithRepl()&lt;/code&gt; to the &lt;code&gt;postgres&lt;/code&gt; server, not the &lt;code&gt;catalogdb&lt;/code&gt; database. The flag enables the diagnostic command only for this local run; it is not a requirement for ordinary catalog startup.&lt;/p&gt;&#10;&lt;p&gt;The menu action opens the container&amp;rsquo;s bundled &lt;code&gt;psql&lt;/code&gt; client in the dashboard dock, authenticated with the resource&amp;rsquo;s credentials. If the dock is hidden, press the backtick (&lt;code&gt;`&lt;/code&gt;) key outside terminal input or a text field; use &lt;strong&gt;F6&lt;/strong&gt; to move focus out of terminal input first.&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;&#10;&lt;p&gt;The session starts in the &lt;code&gt;postgres&lt;/code&gt; database, not the application&amp;rsquo;s &lt;code&gt;catalogdb&lt;/code&gt;. Switch to &lt;code&gt;catalogdb&lt;/code&gt;, then ask which database and role the client is using, which products are stored, and how many rows exist:&lt;/p&gt;&#10;&lt;div class="highlight-wrapper"&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="k"&gt;connect&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;catalogdb&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;BEGIN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;READ&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;ONLY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;current_database&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;current_user&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;catalog_items&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;BY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;catalog_items&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;ROLLBACK&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&#10;&lt;p&gt;Expect &lt;code&gt;current_database()&lt;/code&gt; to return &lt;code&gt;catalogdb&lt;/code&gt;, the rows to contain the mug, notebook, and sticker, and &lt;code&gt;products&lt;/code&gt; to be &lt;code&gt;3&lt;/code&gt;. These are database observations, not another call to &lt;code&gt;/api/catalog&lt;/code&gt;.&lt;/p&gt;&#10;&#10;&lt;figure class="full-width"&gt;&#10; &lt;img&#10; class="my-0 rounded-md"&#10; loading="lazy"&#10; decoding="async"&#10; fetchpriority="auto"&#10; alt="Aspire dashboard with the psql (postgres) terminal docked below the resources: it connects to catalogdb, begins a read-only transaction, and lists the mug, notebook, and sticker from catalog_items"&#10; width="1540"&#10; height="1600"&#10; src="https://chris-ayers.com/posts/aspire-field-notes-terminals-and-repls/psql-repl-dock_hu_8bb186d1af8be74b.png"&#10; srcset="https://chris-ayers.com/posts/aspire-field-notes-terminals-and-repls/psql-repl-dock_hu_8bb186d1af8be74b.png 800w,/posts/aspire-field-notes-terminals-and-repls/psql-repl-dock_hu_b78ddf3d107cccb3.png 1280w"&#10; sizes="(min-width: 768px) 50vw, 65vw"&#10; data-zoom-src="https://chris-ayers.com/posts/aspire-field-notes-terminals-and-repls/psql-repl-dock.png"&#10; /&gt;&#10; &#10; &lt;figcaption&gt;The docked client connects to catalogdb and returns the three seeded products from catalog_items.&lt;/figcaption&gt;&#10; &lt;/figure&gt;&#10;&lt;p&gt;If the rows are missing or different, investigate the seed and stored state. If the expected rows are present but the frontend still behaves incorrectly, this query has narrowed the investigation; it has not proved that the API, inventory call, or frontend is correct. Use the request trace from part two for those questions.&lt;/p&gt;&#10;&lt;p&gt;The read-only transaction limits this example&amp;rsquo;s queries. It does &lt;strong&gt;not&lt;/strong&gt; make the REPL a read-only security boundary: the same credentials may be able to modify or delete data in another transaction.&lt;/p&gt;&#10;&lt;p&gt;The final &lt;code&gt;\q&lt;/code&gt; matters: closing the tab alone can leave &lt;code&gt;psql&lt;/code&gt; alive inside the container.&lt;/p&gt;&#10;&#10;&lt;h3 class="relative group"&gt;Optional CLI alternative&#10; &#10; &#10; &#10; &#10;&lt;/h3&gt;&#10;&lt;p&gt;To automate readiness and open the same docked client without using the menu:&lt;/p&gt;&#10;&lt;div class="highlight-wrapper"&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;aspire &lt;span class="nb"&gt;wait&lt;/span&gt; api --apphost &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$apphost&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; --status healthy --timeout &lt;span class="m"&gt;120&lt;/span&gt; --non-interactive&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;aspire resource postgres repl --apphost &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$apphost&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; --non-interactive&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&#10;&lt;p&gt;These are alternatives to the dashboard steps, not extra startup requirements. The REPL command does not attach your shell; switch to the dashboard to use the client. Running both the command and the menu action opens two sessions, and each needs its own &lt;code&gt;\q&lt;/code&gt;.&lt;/p&gt;&#10;&#10;&lt;h3 class="relative group"&gt;A useful shortcut is also a permission&#10; &#10; &#10; &#10; &#10;&lt;/h3&gt;&#10;&lt;p&gt;REPL support is opt-in and local-run-only. It is available for PostgreSQL, MySQL, SQL Server, MongoDB, Redis, and Valkey in 13.6.&lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;&#10;&lt;p&gt;Enable it only for people you intend to give the resource&amp;rsquo;s actual access. The default &lt;code&gt;psql-client&lt;/code&gt; terminal described below also exposes database access during local runs. A remotely shared dashboard or tunnel changes who can reach these interfaces; it does not make either database client harmless.&lt;/p&gt;&#10;&lt;p&gt;For a repeated team operation, consider a resource command such as &lt;code&gt;show-schema-version&lt;/code&gt; instead of a full database prompt. Its implementation still needs authorization and clear failure reporting.&lt;/p&gt;&#10;&#10;&lt;h2 class="relative group"&gt;Make the database check repeatable&#10; &#10; &#10; &#10; &#10;&lt;/h2&gt;&#10;&lt;p&gt;A terminal tape is a text script that waits for a prompt, types commands, and checks the resulting screen. Here, it asks this catalog database whether the expected products are actually stored.&lt;/p&gt;&#10;&lt;p&gt;The docked &lt;code&gt;WithRepl()&lt;/code&gt; client cannot be a tape target. Tape playback joins a running resource configured with &lt;code&gt;WithTerminal()&lt;/code&gt;, so the automated path needs a separate &lt;code&gt;psql-client&lt;/code&gt; resource whose main process is &lt;code&gt;psql&lt;/code&gt;. The PostgreSQL server remains a background service; attaching a terminal to the server would not turn it into a SQL prompt.&lt;sup id="fnref:3"&gt;&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref"&gt;3&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;&#10;&lt;p&gt;Both clients inspect the same database, but they have different lifetimes. The REPL action creates an additional client inside the server container. The terminal-backed client is a normal local-run resource, with its own container and shared terminal input. Neither a tape nor closing a terminal viewer stops that resource.&lt;/p&gt;&#10;&#10;&lt;h3 class="relative group"&gt;A client that starts with the catalog&#10; &#10; &#10; &#10; &#10;&lt;/h3&gt;&#10;&lt;p&gt;The catalog AppHost includes &lt;code&gt;psql-client&lt;/code&gt; by default in local run mode, alongside the API, frontend, and PostgreSQL server. No terminal opt-in flag is needed, and the client is excluded from published deployments. It uses the PostgreSQL server&amp;rsquo;s pinned image version, but runs &lt;code&gt;psql&lt;/code&gt; instead of another database server. Aspire supplies the server address, port, user, and password through resource expressions; the database name comes from &lt;code&gt;catalogdb&lt;/code&gt;. There is no local &lt;code&gt;psql&lt;/code&gt; installation or password to copy.&lt;/p&gt;&#10;&lt;p&gt;These chained calls from the &lt;a href="https://github.com/codebytes/blog-samples/blob/main/aspire-field-notes/catalog/Catalog.AppHost/AppHost.cs" target="_blank" rel="noreferrer"&gt;client definition&lt;/a&gt; wire its connection and terminal:&lt;/p&gt;&#10;&lt;div class="highlight-wrapper"&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithEnvironment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;PGHOST&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Host&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithEnvironment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;PGPORT&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Port&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithEnvironment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;PGDATABASE&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;catalogdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DatabaseName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithEnvironment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;PGUSER&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UserNameReference&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithEnvironment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;PGPASSWORD&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PasswordParameter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithEnvironment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;PGOPTIONS&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;-c default_transaction_read_only=on -c statement_timeout=5000&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithEnvironment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;PSQL_HISTORY&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;/dev/null&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WaitFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithTerminal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Columns&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;160&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Rows&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&#10;&lt;p&gt;&lt;code&gt;WithTerminal()&lt;/code&gt; is experimental in 13.6; the sample scopes its &lt;code&gt;ASPIRETERMINAL001&lt;/code&gt; warning suppression to this client block. The client disables the pager and history, avoids loading personal &lt;code&gt;psql&lt;/code&gt; startup settings, and uses a stable &lt;code&gt;CATALOG_READY&amp;gt;&lt;/code&gt; prompt.&lt;/p&gt;&#10;&lt;p&gt;A normal catalog start includes the client:&lt;/p&gt;&#10;&lt;div class="highlight-wrapper"&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;apphost&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;catalog/Catalog.AppHost/Catalog.AppHost.csproj&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;aspire start --apphost &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$apphost&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; --isolated&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&#10;&lt;p&gt;Do not start a second run if this catalog AppHost is already running with the client. The earlier &lt;code&gt;Diagnostics__EnableRepl&lt;/code&gt; flag is needed only for the optional docked REPL action, not for &lt;code&gt;psql-client&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;The client waits for &lt;strong&gt;api&lt;/strong&gt; to become healthy, so the seeded rows exist before its prompt opens. Select &lt;strong&gt;psql-client &amp;gt; Console logs&lt;/strong&gt; in the dashboard. Once running, the resource defaults to the &lt;strong&gt;Terminal&lt;/strong&gt; view; if you selected it during startup and still see logs, choose &lt;strong&gt;Settings &amp;gt; Terminal&lt;/strong&gt;.&lt;/p&gt;&#10;&lt;p&gt;This client starts in &lt;code&gt;catalogdb&lt;/code&gt;, unlike the docked REPL. You can inspect it manually with the same &lt;code&gt;SELECT&lt;/code&gt; statements above, without &lt;code&gt;\connect&lt;/code&gt;. For tape playback, leave it running at an idle prompt with no partially typed input. Do not enter &lt;code&gt;\q&lt;/code&gt; yet.&lt;/p&gt;&#10;&#10;&lt;h3 class="relative group"&gt;Check the actual catalog, twice&#10; &#10; &#10; &#10; &#10;&lt;/h3&gt;&#10;&lt;p&gt;Run the checked-in &lt;a href="https://github.com/codebytes/blog-samples/blob/main/aspire-field-notes/terminals/catalog-inspect.tape" target="_blank" rel="noreferrer"&gt;catalog-inspect.tape&lt;/a&gt; directly from &lt;code&gt;aspire-field-notes/&lt;/code&gt;:&lt;/p&gt;&#10;&lt;div class="highlight-wrapper"&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;aspire &lt;span class="nb"&gt;wait&lt;/span&gt; psql-client --apphost &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$apphost&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; --status up --timeout &lt;span class="m"&gt;120&lt;/span&gt; --non-interactive&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;aspire terminal tape play psql-client &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --apphost &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$apphost&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --tape-file terminals/catalog-inspect.tape &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --timeout &lt;span class="m"&gt;20&lt;/span&gt; --non-interactive&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&#10;&lt;p&gt;Run the same playback command a second time. There is no Node helper, generated tape, or nonce-substitution step. The tape&amp;rsquo;s SQL runs inside a read-only transaction and checks:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;The connected database is &lt;code&gt;catalogdb&lt;/code&gt;.&lt;/li&gt;&#10;&lt;li&gt;The ordered SKUs are exactly &lt;code&gt;mug,notebook,sticker&lt;/code&gt;.&lt;/li&gt;&#10;&lt;li&gt;The table contains exactly three products.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;SQL checks the actual database, count, and SKU array before emitting a fixed result marker. Only the expected baseline can produce the &lt;code&gt;catalogdb_mug,notebook,sticker_3_PASS&lt;/code&gt; result; unexpected data produces a separate failure marker instead of echoing arbitrary stored text. The complete expected marker is not present in the typed SQL, so input echo cannot satisfy the assertion. The tape matches a complete result line, not an arbitrary substring.&lt;/p&gt;&#10;&lt;p&gt;The expected result is:&lt;/p&gt;&#10;&lt;div class="highlight-wrapper"&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;FIELD_NOTES_CATALOG_catalogdb_mug,notebook,sticker_3_PASS&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&#10;&lt;p&gt;Before querying, the tape clears the visible terminal and waits until the whole screen contains only the ready prompt and whitespace. That reset replaces the need for a nonce: an earlier result cannot satisfy the new check, and the unchanged tape can be replayed directly. If the screen does not clear, playback fails before running the SQL.&lt;/p&gt;&#10;&lt;p&gt;The reset sequence in the checked-in tape is:&lt;/p&gt;&#10;&lt;div class="highlight-wrapper"&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Enter&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Wait+Line /CATALOG_READY&amp;gt; *$/&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Ctrl+L&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Wait+Screen /^CATALOG_READY&amp;gt;[ \t\r\n]*$/&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&#10;&#10;&lt;h3 class="relative group"&gt;Check that a wrong expectation fails&#10; &#10; &#10; &#10; &#10;&lt;/h3&gt;&#10;&lt;p&gt;The separate &lt;a href="https://github.com/codebytes/blog-samples/blob/main/aspire-field-notes/terminals/catalog-inspect-negative.tape" target="_blank" rel="noreferrer"&gt;negative-control tape&lt;/a&gt; first requires the real three-product baseline, then deliberately expects four in its result predicate. It does not add or remove a product:&lt;/p&gt;&#10;&lt;div class="highlight-wrapper"&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;aspire terminal tape play psql-client &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --apphost &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$apphost&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --tape-file terminals/catalog-inspect-negative.tape &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --timeout &lt;span class="m"&gt;20&lt;/span&gt; --non-interactive&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&#10;&lt;p&gt;This command should fail with CLI exit code 16 because its expected-&lt;code&gt;PASS&lt;/code&gt; wait cannot match. The preceding wait must have observed &lt;code&gt;FIELD_NOTES_CATALOG_catalogdb_mug,notebook,sticker_3_FAIL&lt;/code&gt;; confirm that playback failed at the &lt;strong&gt;last &lt;code&gt;PASS&lt;/code&gt; wait&lt;/strong&gt;, not an earlier prompt, reset, or result wait. The final screen should contain that &lt;code&gt;FAIL&lt;/code&gt; result and no expected &lt;code&gt;PASS&lt;/code&gt;. A missing prompt or broken connection does not demonstrate that the wrong-count assertion was rejected.&lt;/p&gt;&#10;&lt;p&gt;Both tapes roll back before the final assertion, so a failed output wait does not leave that transaction open. Output waits are bounded at five seconds, and the command above sets a 20-second playback timeout. The CLI prints the final screen and reports playback failures; there is no wrapper translating the negative command&amp;rsquo;s failure into a success exit code.&lt;/p&gt;&#10;&lt;p&gt;These checks make the known-seed baseline repeatable. They still do not prove which database an API request reached or that the frontend is correct.&lt;/p&gt;&#10;&#10;&lt;h3 class="relative group"&gt;Keep playback scoped&#10; &#10; &#10; &#10; &#10;&lt;/h3&gt;&#10;&lt;p&gt;The client defaults to read-only sessions, and the tape rolls back its transaction. Those defaults are not a permission boundary: the underlying credentials can still have write access. Keep credentials and sensitive query results out of shared terminals and recordings.&lt;/p&gt;&#10;&lt;p&gt;Playback shares input and terminal size with other viewers; coordinate before typing or running a tape. A small primary viewer can wrap the result marker and break the screen check. Playback does not take exclusive input ownership or stop the client afterward.&lt;sup id="fnref1:3"&gt;&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref"&gt;3&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;&#10;&lt;p&gt;Container startup output can arrive before the terminal attaches. Each tape begins with an empty Enter to regenerate the prompt, then uses &lt;strong&gt;Ctrl+L&lt;/strong&gt; to clear the visible screen before checking the reset. The client keeps interactive line editing enabled so that key works. This is safe only at an idle prompt: do not run a tape with partially typed SQL or while another viewer is using the client.&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;psql-client&lt;/code&gt; stays running with the other local resources between tape runs. It is not an auto-reconnecting service: entering &lt;code&gt;\q&lt;/code&gt; ends its main process. To play another tape after quitting, stop the exact catalog AppHost and start it again with the normal startup command above. Closing its viewer alone does not end &lt;code&gt;psql&lt;/code&gt;.&lt;/p&gt;&#10;&#10;&lt;h2 class="relative group"&gt;Finish the experiment&#10; &#10; &#10; &#10; &#10;&lt;/h2&gt;&#10;&lt;p&gt;If you started a catalog AppHost for either path, stop only that run:&lt;/p&gt;&#10;&lt;div class="highlight-wrapper"&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;aspire stop --apphost &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$apphost&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; --non-interactive&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&#10;&lt;p&gt;Do not use broad process-name cleanup on a machine where other applications may be running.&lt;/p&gt;&#10;&lt;p&gt;That is the practical point of this exercise: inspect stored data in context, without moving connection details between tools. It answers a database question, not every question about the application.&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://github.com/codebytes/blog-samples/tree/main/aspire-field-notes/walkthroughs/03-terminals-and-repls" target="_blank" rel="noreferrer"&gt;Walkthrough 03&lt;/a&gt; has the exact commands for this catalog diagnostic.&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://chris-ayers.com/posts/aspire-field-notes-agents-with-evidence/" &gt;Next: give a coding agent the same evidence-driven loop&lt;/a&gt;.&lt;/p&gt;&#10;&lt;div class="footnotes" role="doc-endnotes"&gt;&#10;&lt;hr&gt;&#10;&lt;ol&gt;&#10;&lt;li id="fn:1"&gt;&#10;&lt;p&gt;&lt;a href="https://aspire.dev/integrations/databases/postgres/postgres-host/#open-an-interactive-repl" target="_blank" rel="noreferrer"&gt;PostgreSQL REPL authentication, initial database, and lifecycle&lt;/a&gt;.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:2"&gt;&#10;&lt;p&gt;&lt;a href="https://devblogs.microsoft.com/aspire/whats-new-aspire-13-6/" target="_blank" rel="noreferrer"&gt;Aspire 13.6 terminal and REPL additions&lt;/a&gt;.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:3"&gt;&#10;&lt;p&gt;&lt;a href="https://aspire.dev/dashboard/terminal-tape-playback/" target="_blank" rel="noreferrer"&gt;Terminal tape playback and supported targets&lt;/a&gt; and &lt;a href="https://aspire.dev/app-host/with-terminal/" target="_blank" rel="noreferrer"&gt;interactive resource processes with WithTerminal&lt;/a&gt;.&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&amp;#160;&lt;a href="#fnref1:3" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;/div&gt;</content:encoded><media:content xmlns:media="http://search.yahoo.com/mrss/" url="https://chris-ayers.com/posts/aspire-field-notes-terminals-and-repls/featured.png"/></item></channel></rss>