<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Joan Frey, auteur/autrice sur dbi Blog</title>
	<atom:link href="https://www.dbi-services.com/blog/author/joanfrey/feed/" rel="self" type="application/rss+xml" />
	<link>https://www.dbi-services.com/blog/author/joanfrey/</link>
	<description></description>
	<lastBuildDate>Fri, 26 Jun 2026 19:11:43 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	

<image>
	<url>https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/05/cropped-favicon_512x512px-min-32x32.png</url>
	<title>Joan Frey, auteur/autrice sur dbi Blog</title>
	<link>https://www.dbi-services.com/blog/author/joanfrey/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>Highly Available, Load-Balanced PostgreSQL with Patroni, HAProxy, and Keepalived</title>
		<link>https://www.dbi-services.com/blog/highly-available-load-balanced-postgresql-with-patroni-haproxy-and-keepalived/</link>
					<comments>https://www.dbi-services.com/blog/highly-available-load-balanced-postgresql-with-patroni-haproxy-and-keepalived/#respond</comments>
		
		<dc:creator><![CDATA[Joan Frey]]></dc:creator>
		<pubDate>Fri, 26 Jun 2026 19:11:41 +0000</pubDate>
				<category><![CDATA[Database Administration & Monitoring]]></category>
		<category><![CDATA[Database management]]></category>
		<category><![CDATA[Operating systems]]></category>
		<category><![CDATA[HAProxy]]></category>
		<category><![CDATA[keepalived]]></category>
		<category><![CDATA[Load]]></category>
		<category><![CDATA[load balancer]]></category>
		<category><![CDATA[load balancing]]></category>
		<category><![CDATA[postgresql]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=45342</guid>

					<description><![CDATA[<p>Patroni runs your PostgreSQL cluster and handles failover, promoting a replica the moment the primary dies and recording the change in its distributed store (etcd, Consul, or ZooKeeper). That part works on its own. Your applications still need one stable address to connect to, and they need writes to reach the primary while reads spread [&#8230;]</p>
<p>L’article <a href="https://www.dbi-services.com/blog/highly-available-load-balanced-postgresql-with-patroni-haproxy-and-keepalived/">Highly Available, Load-Balanced PostgreSQL with Patroni, HAProxy, and Keepalived</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Patroni runs your PostgreSQL cluster and handles failover, promoting a replica the moment the primary dies and recording the change in its distributed store (etcd, Consul, or ZooKeeper). That part works on its own.</p>



<p class="wp-block-paragraph">Your applications still need one stable address to connect to, and they need writes to reach the primary while reads spread across replicas. HAProxy handles that routing, with a floating IP from Keepalived in front of it.</p>



<h1 id="h-how-the-tools-work-together" class="wp-block-heading">How the tools work together</h1>



<p class="wp-block-paragraph">Three components stands between your application and the database.</p>



<p class="wp-block-paragraph"><strong>Patroni</strong> manages replication and failover, and it runs an agent on every PostgreSQL node. Each agent exposes a small REST API (port 8008 by default) that reports that node&#8217;s role.</p>



<p class="wp-block-paragraph"><strong>HAProxy</strong> accepts client connections and forwards them to the right node. It asks Patroni&#8217;s REST API which node is the primary and which are replicas, then sends each connection to a matching node.</p>



<p class="wp-block-paragraph"><strong>Keepalived</strong> publishes a virtual IP that floats between your HAProxy hosts using VRRP. Your application connects to the VIP, so one HAProxy host going down doesn&#8217;t take the whole entry point with it.</p>



<p class="wp-block-paragraph">Your application talks to the VIP. Keepalived points the VIP at a live HAProxy. HAProxy forwards the connection to whichever PostgreSQL node Patroni reports as healthy for that role.</p>



<h1 id="h-the-health-check-method" class="wp-block-heading">The health-check method</h1>



<p class="wp-block-paragraph">HAProxy checks one port and routes to another.</p>



<p class="wp-block-paragraph">Patroni&#8217;s REST API returns an HTTP status that depends on the node&#8217;s role:</p>



<ul class="wp-block-list">
<li><code>GET /</code> returns <code>200</code> only on the leader (the primary). A non-leader node returns <code>503</code>.</li>



<li><code>GET /primary</code> is the explicit name for the same leader check.</li>



<li><code>GET /replica</code> returns <code>200</code> only on a running replica.</li>



<li><code>GET /read-only</code> returns <code>200</code> on the primary or a replica, any node that can serve a read.</li>
</ul>



<p class="wp-block-paragraph">In our case, HAProxy runs its health check against the API port (8008) and reads that status code, then forwards the SQL connection to the database port (5432). A node receives traffic only when its API answers <code>200</code> for the role that listener cares about. Point a listener&#8217;s check at <code>/</code> and it follows the primary. Point it at <code>/replica</code> and it follows the replicas. Patroni promotes a new leader, the status codes change, and HAProxy moves traffic to match within a couple of health-check cycles.</p>



<h1 id="h-a-first-and-simple-working-configuration" class="wp-block-heading">A first and simple working configuration</h1>



<p class="wp-block-paragraph">A two-node setup with <code>10.5.5.147</code> and <code>10.5.5.148</code> looks like this. One listener handles writes, the other handles reads.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
listen PG1
    bind *:5000
    option httpchk
    http-check expect status 200
    default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions
    server postgresql_10.5.5.147_5432 10.5.5.147:5432 maxconn 100 check port 8008
    server postgresql_10.5.5.148_5432 10.5.5.148:5432 maxconn 100 check port 8008

listen PG1_ro
    bind *:5001
    option httpchk GET /replica
    http-check expect status 200
    default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions
    server postgresql_10.5.5.147_5432 10.5.5.147:5432 maxconn 100 check port 8008
    server postgresql_10.5.5.148_5432 10.5.5.148:5432 maxconn 100 check port 8008
</pre></div>


<p class="wp-block-paragraph">This example runs PostgreSQL on port 5432 and the Patroni API on 8008, so swap in whatever ports your deployment uses (the defaults are 5432 and 8008).</p>



<p class="wp-block-paragraph">Line by line:</p>



<ul class="wp-block-list">
<li><code>bind *:5000</code> and <code>bind *:5001</code> are the two addresses your applications connect to. Send writes to 5000 and reads to 5001.</li>



<li><code>option httpchk</code> (with no path) on the first listener checks Patroni&#8217;s root endpoint. Only the leader answers <code>200</code>, so HAProxy sends port 5000 traffic to the current primary.</li>



<li><code>option httpchk GET /replica</code> on the second listener checks the replica endpoint, so HAProxy sends port 5001 traffic to a replica.</li>



<li><code>http-check expect status 200</code> tells HAProxy that <code>200</code> means healthy and anything else means down.</li>



<li><code>inter 3s fall 3 rise 2</code> checks every 3 seconds, marks a server down after 3 failures, and brings it back after 2 successes.</li>



<li><code>on-marked-down shutdown-sessions</code> kills existing connections to a server the instant HAProxy marks it down, so clients reconnect and get rerouted instead of hanging on a dead node.</li>



<li><code>check port 8008</code> is the trick in action: health checks hit the Patroni API on 8008 while HAProxy forwards traffic to PostgreSQL on 5432.</li>



<li><code>maxconn 100</code> limit connections per server so you don&#8217;t exhaust PostgreSQL&#8217;s connection slots.</li>
</ul>



<p class="wp-block-paragraph">For a primary plus one or more replicas, this routes writes and reads to the right node and survives a failover.</p>



<h1 id="h-the-failure-mode-hiding-in-the-read-path" class="wp-block-heading">The failure mode hiding in the read path</h1>



<p class="wp-block-paragraph">Imagine a two-node cluster: one primary, one replica. The replica goes down. Maybe it crashed, maybe Patroni is mid-switchover and no standby exists for a few seconds.</p>



<p class="wp-block-paragraph">Your read traffic hits port 5001. That listener marks a server up only when <code>GET /replica</code> returns <code>200</code>, and right now no node is a replica. HAProxy has zero usable servers in the pool, so it refuses the connection. Read queries start failing.</p>



<p class="wp-block-paragraph">The primary is up the entire time, and it can serve those reads. Your config won&#8217;t send them there, because you told the read listener to look for replicas and nothing else. You&#8217;ve turned a degraded cluster that could still serve reads into a read outage. You feel this most on small clusters, and each failover passes through a window where the old primary becomes a replica and no standby is available yet. In the worst case, your replica is down, and one of your application is connecting to port 5001, resulting in errors.</p>



<h1 id="h-the-fix-fall-back-to-the-primary" class="wp-block-heading">The fix: fall back to the primary</h1>



<p class="wp-block-paragraph">Send reads to the primary when the read listener runs out of replicas, instead of dropping them.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
listen PG1
    bind *:5000
    option httpchk
    http-check expect status 200
    default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions
    server postgresql_10.5.5.147_5432 10.5.5.147:5432 maxconn 100 check port 8008
    server postgresql_10.5.5.148_5432 10.5.5.148:5432 maxconn 100 check port 8008

listen PG1_ro
    bind *:5001
    option httpchk GET /replica
    http-check expect status 200
    use_backend PG1_ro_leader if { nbsrv(PG1_ro) eq 0 }
    default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions
    server postgresql_10.5.5.147_5432 10.5.5.147:5432 maxconn 100 check port 8008
    server postgresql_10.5.5.148_5432 10.5.5.148:5432 maxconn 100 check port 8008

backend PG1_ro_leader
    option httpchk GET /primary
    http-check expect status 200
    default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions
    server postgresql_10.5.5.147_5432 10.5.5.147:5432 maxconn 100 check port 8008
    server postgresql_10.5.5.148_5432 10.5.5.148:5432 maxconn 100 check port 8008
</pre></div>


<p class="wp-block-paragraph">This new line carries the whole fix:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
use_backend PG1_ro_leader if { nbsrv(PG1_ro) eq 0 }
</pre></div>


<p class="wp-block-paragraph"><code>nbsrv(PG1_ro)</code> counts the usable servers in the <code>PG1_ro</code> pool, which here means the number of available replicas, since those servers pass the check only when <code>GET /replica</code> returns <code>200</code>. While at least one replica is up, the count stays above zero, the condition is false, and reads stay on the replicas. The moment the last replica drops, <code>nbsrv(PG1_ro)</code> hits zero, the condition fires, and HAProxy diverts reads to the <code>PG1_ro_leader</code> backend.</p>



<p class="wp-block-paragraph">That backend health-checks with <code>GET /primary</code>, so the only server it counts as up is the current primary. HAProxy sends reads to the primary until a replica returns, then shifts them back to the replica pool once a replica passes its check again.</p>



<p class="wp-block-paragraph">Three names have to agree for this to work. The backend you define (<code>backend PG1_ro_leader</code>), the backend you route to (<code>use_backend PG1_ro_leader</code>), and the pool you count (<code>nbsrv(PG1_ro)</code>) all reference the real section names. Drop in a stale name from an earlier version and HAProxy either refuses to start or counts the wrong pool.</p>



<h2 class="wp-block-heading">The /read-only shortcut and what it costs</h2>



<p class="wp-block-paragraph">Patroni offers <code>GET /read-only</code>, which returns <code>200</code> on the primary and the replicas alike. Point the read listener there and both the primary and the replicas serve reads, no fallback backend needed.</p>



<p class="wp-block-paragraph">The cost is read load on the primary even when your replicas are healthy and idle. The fallback approach keeps reads off the primary until the replicas are gone, then leans on it as a safety net. You protect the primary&#8217;s write capacity during normal operation and still keep reads alive during a replica outage.</p>



<p class="wp-block-paragraph">To keep lagging replicas out of the read pool, Patroni accepts a threshold on the replica check, for example <code>GET /replica?lag=10MB</code>, which fails any replica more than 10 MB behind. Pair that with the fallback and HAProxy drops the lagging replicas from rotation while reads still have somewhere to go.</p>



<h1 class="wp-block-heading">Keepalived: removing HAProxy as a single point of failure</h1>



<p class="wp-block-paragraph">One HAProxy host fronting the cluster moves the single point of failure up a layer. Run HAProxy on two hosts and let Keepalived float a virtual IP between them with VRRP. Your application connects to the VIP, and whichever HAProxy holds it answers.</p>



<p class="wp-block-paragraph">A minimal <code>keepalived.conf</code> on the primary HAProxy host:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
vrrp_script chk_haproxy {
    script &quot;killall -0 haproxy&quot;   # succeeds while the haproxy process is alive
    interval 2
    weight 2
}

vrrp_instance VI_1 {
    interface eth0
    state MASTER
    virtual_router_id 51
    priority 101
    advert_int 1
    authentication {
        auth_type PASS
        auth_pass changeme
    }
    virtual_ipaddress {
        10.0.0.1
    }
    track_script {
        chk_haproxy
    }
}
</pre></div>


<p class="wp-block-paragraph">The second HAProxy host runs the same file with <code>state BACKUP</code> and a lower <code>priority</code> (100). Both advertise over VRRP, and the higher priority holds the VIP. <code>chk_haproxy</code> runs every two seconds. If HAProxy dies on the active host, its priority drops and the backup takes the VIP, so an HAProxy crash on one host no longer takes the entry point down with it.</p>



<p class="wp-block-paragraph">Point your applications at <code>10.0.0.1:5000</code> for writes and <code>10.5.5.100:5001</code> for reads. Your applications never see which physical HAProxy does the work.</p>



<h1 class="wp-block-heading">Summary</h1>



<p class="wp-block-paragraph">Patroni keeps the cluster healthy and picks the leader. HAProxy turns Patroni&#8217;s REST API into routing, sending writes to the primary and reads to the replicas by health-checking the API port while forwarding to the database port. The naive read-only listener drops reads when the last replica goes down, even though the primary could serve them. Adding <code>use_backend ... if { nbsrv(...) eq 0 }</code> with a primary-checking backend closes that gap, and a lag threshold on the replica check keeps stale standbys out of rotation. Keepalived puts a floating VIP in front of two HAProxy instances so the proxy layer survives a host failure too.</p>



<p class="wp-block-paragraph">Writes reach the primary and reads spread across the replicas. Reads stay up as long as one node in the cluster is alive.</p>



<p class="wp-block-paragraph">Let me know if you find any improvements to this configuration <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f600.png" alt="😀" class="wp-smiley" style="height: 1em; max-height: 1em;" /> </p>
<p>L’article <a href="https://www.dbi-services.com/blog/highly-available-load-balanced-postgresql-with-patroni-haproxy-and-keepalived/">Highly Available, Load-Balanced PostgreSQL with Patroni, HAProxy, and Keepalived</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.dbi-services.com/blog/highly-available-load-balanced-postgresql-with-patroni-haproxy-and-keepalived/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Upgrade RHEL from 9.6 to 10.1 (when running PostgreSQL/Patroni)</title>
		<link>https://www.dbi-services.com/blog/upgrade-rhel-from-9-6-to-10-1-when-running-postgresql-patroni/</link>
					<comments>https://www.dbi-services.com/blog/upgrade-rhel-from-9-6-to-10-1-when-running-postgresql-patroni/#respond</comments>
		
		<dc:creator><![CDATA[Joan Frey]]></dc:creator>
		<pubDate>Fri, 26 Jun 2026 10:39:40 +0000</pubDate>
				<category><![CDATA[Database Administration & Monitoring]]></category>
		<category><![CDATA[Operating systems]]></category>
		<category><![CDATA[PostgreSQL]]></category>
		<category><![CDATA[10]]></category>
		<category><![CDATA[leapp]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[operating system]]></category>
		<category><![CDATA[os]]></category>
		<category><![CDATA[RHEL]]></category>
		<category><![CDATA[upgrade]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=43285</guid>

					<description><![CDATA[<p>Upgrading from RHEL 9.6 to 10.1 is not just a routine update, it’s a major platform shift. When your server runs PostgreSQL compiled from source and a Patroni-managed cluster, the complexity increases significantly. System libraries change, Python environments break, ICU versions evolve, and your database binaries may no longer start after reboot. In this guide, [&#8230;]</p>
<p>L’article <a href="https://www.dbi-services.com/blog/upgrade-rhel-from-9-6-to-10-1-when-running-postgresql-patroni/">Upgrade RHEL from 9.6 to 10.1 (when running PostgreSQL/Patroni)</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Upgrading from RHEL 9.6 to 10.1 is not just a routine update, it’s a major platform shift. When your server runs PostgreSQL compiled from source and a Patroni-managed cluster, the complexity increases significantly. System libraries change, Python environments break, ICU versions evolve, and your database binaries may no longer start after reboot.</p>



<p class="wp-block-paragraph">In this guide, I walk through a real-world in-place upgrade using Leapp, covering preparation, resolving high-severity warnings, executing the upgrade, recompiling PostgreSQL, fixing collation mismatches, and restoring Patroni.</p>



<h2 class="wp-block-heading" id="h-i-preparation">I. Preparation</h2>



<p class="wp-block-paragraph">Before the upgrade, you must ensure the current OS is healthy and fully patched.</p>



<h3 class="wp-block-heading" id="h-1-pause-high-availability">1. Pause High Availability</h3>



<p class="wp-block-paragraph">Prevent Patroni from triggering a failover during the reboot cycles. If you are using a single PostgreSQL cluster, stop it by stopping the service or by using pg_ctl stop.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
patronictl -c /etc/patroni/patroni.yml pause
systemctl stop patroni
</pre></div>


<h3 class="wp-block-heading" id="h-2-fix-subscription-amp-perform-full-update">2. Fix Subscription &amp; Perform Full Update</h3>



<p class="wp-block-paragraph">I&#8217;m using an old VM for this blog, and If just like me, you see 403 Forbidden errors on repositories like codeready-builder, refresh your registration:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
&#x5B;root@patroni2 ~]# sudo dnf update -y
Updating Subscription Management repositories.

This system is registered with an entitlement server, but is not receiving updates. You can use subscription-manager to assign subscriptions.

Red Hat CodeReady Linux Builder for RHEL 9 x86_64 (RPMs)                                                                                                     761  B/s | 480  B     00:00
Errors during downloading metadata for repository &#039;codeready-builder-for-rhel-9-x86_64-rpms&#039;:
  - Status code: 403 for https://cdn.redhat.com/content/dist/rhel9/9/x86_64/codeready-builder/os/repodata/repomd.xml (IP: 23.206.57.92)
Error: Failed to download metadata for repo &#039;codeready-builder-for-rhel-9-x86_64-rpms&#039;: Cannot download repomd.xml: Cannot download repodata/repomd.xml: All mirrors were tried

&#x5B;root@patroni2 ~]# sudo subscription-manager clean
&#x5B;root@patroni2 ~]# sudo subscription-manager register --force
&#x5B;root@patroni2 ~]# sudo subscription-manager attach --auto
&#x5B;root@patroni2 ~]# sudo subscription-manager refresh

&#x5B;root@patroni2 ~]# sudo dnf update -y
...
Complete!

&#x5B;root@patroni2 ~]# reboot
</pre></div>


<h2 class="wp-block-heading" id="h-ii-the-leapp-upgrade-to-10-1">II. The Leapp Upgrade to 10.1</h2>



<h3 class="wp-block-heading" id="h-1-install-amp-analyze">1. Install &amp; Analyze</h3>



<p class="wp-block-paragraph">In this first phase, we are preparing the system for a major in-place upgrade using Leapp, the official upgrade framework for Red Hat–based distributions. When we install the package:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
&#x5B;root@patroni2 ~]# dnf install leapp-upgrade -y
...
Installed:
  leapp-0.20.0-1.el9.noarch                leapp-deps-0.20.0-1.el9.noarch          leapp-upgrade-el9toel10-0.23.0-1.el9.noarch       leapp-upgrade-el9toel10-deps-0.23.0-1.el9.noarch
  libdb-utils-5.3.28-57.el9_6.x86_64       python3-leapp-0.20.0-1.el9.noarch       systemd-container-252-55.el9_7.7.x86_64

Complete!
</pre></div>


<p class="wp-block-paragraph">When running leapp preupgrade &#8211;target 10.1, we are not performing the upgrade. Instead, Leapp performs a full system audit to determine if the server is ready for RHEL 10.1. It checks:</p>



<ul class="wp-block-list">
<li>Installed packages and their compatibility</li>



<li>Deprecated or removed libraries</li>



<li>Kernel drivers that will not exist in RHEL 10</li>



<li>Bootloader configuration (GRUB2)</li>



<li>GPG key validity</li>



<li>Custom system-level modifications (like dynamic linker changes)</li>



<li>&#8230;</li>
</ul>



<p class="wp-block-paragraph">Think of this step as a dry-run with intelligence.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
&#x5B;root@patroni2 ~]# sudo leapp preupgrade --target 10.1

...

============================================================
                      REPORT OVERVIEW
============================================================

HIGH and MEDIUM severity reports:
    1. GRUB2 core will be automatically updated during the upgrade
    2. Detected customized configuration for dynamic linker.
    3. Leapp detected loaded kernel drivers which are no longer maintained in RHEL 10.
    4. Failed to read GPG keys from provided key files
    5. Berkeley DB (libdb) has been detected on your system

Reports summary:
    Errors:                      0
    Inhibitors:                  0
    HIGH severity reports:       4
    MEDIUM severity reports:     1
    LOW severity reports:        1
    INFO severity reports:       3

Before continuing, review the full report below for details about discovered problems and possible remediation instructions:
    A report has been generated at /var/log/leapp/leapp-report.txt
    A report has been generated at /var/log/leapp/leapp-report.json
</pre></div>


<p class="wp-block-paragraph">After running the pre-upgrade analysis, the next step is to carefully review:</p>



<pre class="wp-block-preformatted">/var/log/leapp/leapp-report.txt</pre>



<p class="wp-block-paragraph">What we are looking for first is simple:</p>



<ul class="wp-block-list">
<li>Errors: 0</li>



<li>Inhibitors: 0</li>
</ul>



<p class="wp-block-paragraph">If an Inhibitor is present, the upgrade will be blocked entirely.<br>In my case, there were no blockers, but I did have several high severity warnings.</p>



<p class="wp-block-paragraph">High severity does not mean the upgrade will fail.<br>It means: This could break something, review it carefully.</p>



<p class="wp-block-paragraph">Let’s look at one concrete example from my system.</p>



<h3 class="wp-block-heading" id="h-2-high-severity-example-dynamic-linker-customization">2. High Severity Example – Dynamic Linker Customization</h3>



<p class="wp-block-paragraph">Leapp detected that my system had a custom dynamic linker configuration:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
Risk Factor: high

Title: Detected customized configuration for dynamic linker.

Summary: Custom configurations to the dynamic linker could potentially impact the upgrade in a negative way. The custom configuration includes modifications to /etc/ld.so.conf, custom or modified drop in config files in the /etc/ld.so.conf.d directory and additional entries in the LD_LIBRARY_PATH or LD_PRELOAD variables. These modifications configure the dynamic linker to use different libraries that might not be provided by Red Hat products or might not be present during the whole upgrade process. The following custom configurations were detected by leapp:

- The following drop in config files were marked as custom:

    - /etc/ld.so.conf.d/postgres.conf

Remediation: &#x5B;hint] Remove or revert the custom dynamic linker configurations and apply the changes using the ldconfig command. In case of possible active software collections we suggest disabling them persistently.

Key: cc9bd972af70b7a27f66a37b11a00dcfcb73b1bc

----------------------------------------
</pre></div>


<h4 class="wp-block-heading" id="h-what-does-this-actually-mean">What does this actually mean?</h4>



<p class="wp-block-paragraph">The dynamic linker (ld.so) is responsible for loading shared libraries at runtime.</p>



<p class="wp-block-paragraph">By modifying:</p>



<ul class="wp-block-list">
<li>/etc/ld.so.conf</li>



<li>files in /etc/ld.so.conf.d/</li>



<li>LD_LIBRARY_PATH</li>



<li><code>LD_PR</code>E<code>LOAD</code></li>
</ul>



<p class="wp-block-paragraph">we are telling the system to load non-standard or custom libraries. In PostgreSQL environments (especially with custom builds or extensions), this is common practice. However, during a major OS upgrade, these custom paths might:</p>



<ul class="wp-block-list">
<li>Point to libraries that do not exist in RHEL 10</li>



<li>Override new system libraries</li>



<li>Break dependency resolution mid-upgrade</li>
</ul>



<p class="wp-block-paragraph">Leapp flags this because it cannot guarantee consistency during the transition phase. In my case, it shouldn&#8217;t be an issue, because inside postgres.conf, I only have a path aiming to the lib directories of my PostgreSQL installation, which will not change, but we will still see how to prevent an error.</p>



<h4 class="wp-block-heading" id="h-understanding-the-remediation">Understanding the Remediation</h4>



<p class="wp-block-paragraph">The report clearly suggests:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Remove or revert the custom dynamic linker configurations and apply the changes using the ldconfig command.</p>
</blockquote>



<p class="wp-block-paragraph">In my case, the configuration was related to PostgreSQL, so temporarily removing it is safe for the upgrade preparation phase. Instead of deleting it permanently, I moved it aside:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
&#x5B;root@patroni2 ~]# sudo mv /etc/ld.so.conf.d/postgres.conf /tmp/postgres.conf.bak
&#x5B;root@patroni2 ~]# sudo ldconfig
</pre></div>


<p class="wp-block-paragraph">ldconfig rebuilds the system library cache now based only on standard paths.</p>



<h4 class="wp-block-heading" id="h-re-run-the-preupgrade-check">Re-Run the Preupgrade Check</h4>



<p class="wp-block-paragraph">After remediation, always re-run the preupgrade command and check the report again. If the fix was successful:</p>



<ul class="wp-block-list">
<li>The severity should disappear from the REPORT OVERVIEW</li>



<li>The issue should no longer appear in leapp-report.txt</li>
</ul>



<p class="wp-block-paragraph">This validation loop is important. We are progressively cleaning the system until it is fully compliant for upgrade.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
============================================================
                      REPORT OVERVIEW
============================================================

HIGH and MEDIUM severity reports:
    1. Leapp detected loaded kernel drivers which are no longer maintained in RHEL 10.
    2. GRUB2 core will be automatically updated during the upgrade
    3. Failed to read GPG keys from provided key files
    4. Berkeley DB (libdb) has been detected on your system

Reports summary:
    Errors:                      0
    Inhibitors:                  0
    HIGH severity reports:       3
    MEDIUM severity reports:     1
    LOW severity reports:        1
    INFO severity reports:       3

Before continuing, review the full report below for details about discovered problems and possible remediation instructions:
    A report has been generated at /var/log/leapp/leapp-report.txt
    A report has been generated at /var/log/leapp/leapp-report.json
</pre></div>


<p class="wp-block-paragraph">Only once the report is clean, or fully understood, should we proceed to the actual upgrade execution.</p>



<h3 class="wp-block-heading" id="h-2-execute-the-upgrade">2. Execute the upgrade</h3>



<p class="wp-block-paragraph">Once all errors and inhibitors are resolved, and high-severity findings have been reviewed or remediated, we are finally ready to perform the actual in-place upgrade. This is the moment where Leapp transitions from analysis mode to execution mode.</p>



<h4 class="wp-block-heading" id="h-about-the-repository-warning">About the Repository Warning</h4>



<p class="wp-block-paragraph">During the preupgrade phase, Leapp informed us that codeready-builder-&#8230; repositories are not officially supported during the upgrade process and are excluded by default.</p>



<p class="wp-block-paragraph">This is expected behavior as Leapp only enables a minimal, controlled set of repositories to ensure:</p>



<ul class="wp-block-list">
<li>Package consistency</li>



<li>Dependency resolution stability</li>



<li>Predictable upgrade paths</li>
</ul>



<p class="wp-block-paragraph">However, in PostgreSQL environments, some packages (extensions, development headers, libraries) may depend on CodeReady Builder. If a repository is truly required during the upgrade, we must explicitly enable it using:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
--enablerepo &lt;repoid&gt;
</pre></div>


<h4 class="wp-block-heading" id="h-running-the-upgrade">Running the Upgrade</h4>



<p class="wp-block-paragraph">Since I need CodeReady Builder for PostgreSQLdependencies, I ran:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
leapp upgrade --target 10.1 --enablerepo codeready-builder-for-rhel-10-x86_64-rpms
</pre></div>


<h4 class="wp-block-heading">What happens when we run this command?</h4>



<p class="wp-block-paragraph">At this stage, Leapp:</p>



<ul class="wp-block-list">
<li>Resolves and downloads required RHEL 10 packages</li>



<li>Builds a temporary upgrade environment</li>



<li>Prepares a special upgrade initramfs</li>



<li>Modifies the bootloader (GRUB) to boot into the upgrade environment on next reboot</li>
</ul>



<p class="wp-block-paragraph">The system is not upgraded immediately. The actual OS transition happens during the next boot.</p>



<p class="wp-block-paragraph">After the command completes, Leapp generates another report. Just like in the preupgrade phase, verify:</p>



<ul class="wp-block-list">
<li>Errors: 0</li>



<li>Inhibitors: 0</li>
</ul>



<p class="wp-block-paragraph">If everything looks clean, we can proceed.</p>



<h4 class="wp-block-heading" id="h-reboot-the-real-upgrade-begins">Reboot – The Real Upgrade Begins</h4>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
# reboot
</pre></div>


<p class="wp-block-paragraph">This is where the real upgrade starts. During boot:</p>



<ul class="wp-block-list">
<li>The system enters a temporary upgrade environment</li>



<li>Packages are replaced</li>



<li>Obsolete components are removed</li>



<li>Configuration files are migrated</li>



<li>The new RHEL 10 kernel is installed</li>
</ul>



<p class="wp-block-paragraph">This phase can take several minutes depending on your VM/server resources. My VM doesn&#8217;t have many resources and it took me around 30 minutes. Be patient, interrupting this process can leave the system in an inconsistent state.</p>



<h4 class="wp-block-heading" id="h-verifying-the-upgrade">Verifying the Upgrade</h4>



<p class="wp-block-paragraph">Once the server is back online, confirm the OS version:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
&#x5B;root@patroni2 ~]# cat /etc/os-release
NAME=&quot;Red Hat Enterprise Linux&quot;
VERSION=&quot;10.1 (Coughlan)&quot;
ID=&quot;rhel&quot;
ID_LIKE=&quot;centos fedora&quot;
VERSION_ID=&quot;10.1&quot;
PLATFORM_ID=&quot;platform:el10&quot;
PRETTY_NAME=&quot;Red Hat Enterprise Linux 10.1 (Coughlan)&quot;
ANSI_COLOR=&quot;0;31&quot;
LOGO=&quot;fedora-logo-icon&quot;
CPE_NAME=&quot;cpe:/o:redhat:enterprise_linux:10.1&quot;
HOME_URL=&quot;https://www.redhat.com/&quot;
VENDOR_NAME=&quot;Red Hat&quot;
VENDOR_URL=&quot;https://www.redhat.com/&quot;
DOCUMENTATION_URL=&quot;https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/10&quot;
BUG_REPORT_URL=&quot;https://issues.redhat.com/&quot;
</pre></div>


<p class="wp-block-paragraph">REDHAT_BUGZILLA_PRODUCT=&#8221;Red Hat Enterprise Linux 10&#8243;<br>REDHAT_BUGZILLA_PRODUCT_VERSION=10.1<br>REDHAT_SUPPORT_PRODUCT=&#8221;Red Hat Enterprise Linux&#8221;<br>REDHAT_SUPPORT_PRODUCT_VERSION=&#8221;10.1&#8243;</p>



<p class="wp-block-paragraph">This confirms that we are now running RHEL 10.1.</p>



<h2 class="wp-block-heading" id="h-iii-post-upgrade-database-recovery">III. Post-Upgrade Database Recovery</h2>



<p class="wp-block-paragraph">Once you login to RHEL 10.1, your Postgres binaries in /u01 will fail because libicuuc.so.67 (from RHEL 9) is missing. It can also fail because of other libraries.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
14:45:55 postgres@patroni2:/home/postgres/ &#x5B;test-op-patroni] pgstart
/u01/app/postgres/product/17/db_6/bin/postgres: error while loading shared libraries: libicuuc.so.67: cannot open shared object file: No such file or directory
no data was returned by command &quot;&quot;/u01/app/postgres/product/17/db_6/bin/postgres&quot; -V&quot;
command not found
program &quot;postgres&quot; is needed by pg_ctl but was not found in the same directory as &quot;/u01/app/postgres/product/17/db_6/bin/pg_ctl&quot;
</pre></div>


<h3 class="wp-block-heading" id="h-1-recompile-postgresql">1. Recompile PostgreSQL</h3>



<p class="wp-block-paragraph">Since you installed from source, you must re compile PostgreSQL with the new RHEL 10 system libraries. Here is the command I personally use to build it, with the postgres user:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
postgres@patroni2:/home/postgres/ &#x5B;dummy] MAJOR=&quot;17&quot;
postgres@patroni2:/home/postgres/ &#x5B;dummy] MINOR=&quot;6&quot;
postgres@patroni2:/home/postgres/ &#x5B;dummy] tar axf postgresql-${MAJOR}.${MINOR}.tar.gz
postgres@patroni2:/home/postgres/ &#x5B;dummy] mkdir build; cd $_
postgres@patroni2:/home/postgres/ &#x5B;dummy] export PGHOME=&quot;/u01/app/postgres/product/${MAJOR}/db_${MINOR}&quot;
postgres@patroni2:/home/postgres/ &#x5B;dummy] export SEGSIZE=2
postgres@patroni2:/home/postgres/ &#x5B;dummy] export BLOCKSIZE=8
postgres@patroni2:/home/postgres/ &#x5B;dummy] meson setup . ../postgresql-${MAJOR}.${MINOR}
postgres@patroni2:/home/postgres/ &#x5B;dummy] meson configure -Dprefix=${PGHOME}                   -Dbindir=${PGHOME}/bin                   -Ddatadir=${PGHOME}/share                   -Dincludedir=${PGHOME}/include                   -Dlibdir=${PGHOME}/lib                   -Dsysconfdir=${PGHOME}/etc                   -Dpgport=5432                   -Dplperl=enabled                   -Dplpython=enabled                   -Dssl=openssl                   -Dpam=enabled                   -Dldap=enabled                   -Dlibxml=enabled                   -Dlibxslt=enabled                   -Dsegsize=${SEGSIZE}                   -Dblocksize=${BLOCKSIZE}                   -Dllvm=enabled                   -Duuid=ossp                   -Dzstd=enabled                   -Dlz4=enabled                   -Dzstd=enabled                   -Dgssapi=enabled                   -Dsystemd=enabled                   -Dicu=enabled                   -Dsystem_tzdata=/usr/share/zoneinfo                   -Dextra_version=&quot; dbi services build&quot;
postgres@patroni2:/home/postgres/ &#x5B;dummy] ninja
postgres@patroni2:/home/postgres/ &#x5B;dummy] ninja install
</pre></div>


<h3 class="wp-block-heading" id="h-2-restore-library-paths">2. Restore Library Paths</h3>



<p class="wp-block-paragraph">[root@patroni2 ~]# mv /tmp/postgres.conf.bak /etc/ld.so.conf.d/postgres.conf<br>[root@patroni2 ~]# ldconfig</p>



<h3 class="wp-block-heading" id="h-3-start-amp-fix-collation-mismatch">3. Start &amp; Fix Collation Mismatch</h3>



<p class="wp-block-paragraph">Postgres will now start, but will warn you about Collation Version Mismatches (2.34 vs 2.39).</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
15:09:55 postgres@patroni2:/home/postgres/build/ &#x5B;test-op-patroni] pgstart
waiting for server to start.... done
server started
15:10:28 postgres@patroni2:/home/postgres/build/ &#x5B;test-op-patroni] psql
WARNING:  database &quot;postgres&quot; has a collation version mismatch
DETAIL:  The database was created using collation version 2.34, but the operating system provides version 2.39.
HINT:  Rebuild all objects in this database that use the default collation and run ALTER DATABASE postgres REFRESH COLLATION VERSION, or build PostgreSQL with the right library version.
psql (17.6 dbi services build)
Type &quot;help&quot; for help.
</pre></div>


<p class="wp-block-paragraph">Inside Postgres, run for every database:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
ALTER DATABASE postgres REFRESH COLLATION VERSION;
ALTER DATABASE

-- Repeat for other DBs if applicable
REINDEX DATABASE postgres;
</pre></div>


<p class="wp-block-paragraph">Your PostgreSQL is now starting properly and your server has been upgraded.</p>



<h3 class="wp-block-heading" id="h-4-in-case-of-a-patroni-cluster">4. In case of a patroni cluster</h3>



<p class="wp-block-paragraph">Since the system Python version has changed after the OS upgrade, your old .local venv is invalid. You must recreate it. Here is how I install patroni using the postgres user:</p>



<p class="wp-block-paragraph">$ python3 -m venv .local<br>$ .local/bin/pip3 install &#8211;upgrade pip<br>$ .local/bin/pip3 install &#8211;upgrade setuptools<br>$ .local/bin/pip3 install wheel<br>$ .local/bin/pip3 install psycopg[binary]<br>$ .local/bin/pip3 install python-etcd<br>$ .local/bin/pip3 install patroni<br>$ .local/bin/patroni version</p>



<p class="wp-block-paragraph">Check if the cluster sees the member again. If patronictl list is empty, a restart of the service is usually required to re-register with etcd.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
10:45:40 postgres@patroni2:/home/postgres/ &#x5B;test-op-patroni] patronictl list
+ Cluster: test-op-patroni (7565882985963789761) -+-----+------------+-----+
| Member | Host | Role | State | TL | Receive LSN | Lag | Replay LSN | Lag |
+--------+------+------+-------+----+-------------+-----+------------+-----+
+--------+------+------+-------+----+-------------+-----+------------+-----+
10:45:48 postgres@patroni2:/home/postgres/ &#x5B;test-op-patroni] sudo systemctl restart patroni
10:45:55 postgres@patroni2:/home/postgres/ &#x5B;test-op-patroni] patronictl list
+ Cluster: test-op-patroni (7565882985963789761) --+---------+----+-------------+-----+------------+-----+
| Member                 | Host           | Role   | State   | TL | Receive LSN | Lag | Replay LSN | Lag |
+------------------------+----------------+--------+---------+----+-------------+-----+------------+-----+
| patroni-tst-op-geapg02 | 192.168.56.142 | Leader | running | 11 |             |     |            |     |
+------------------------+----------------+--------+---------+----+-------------+-----+------------+-----+

</pre></div>


<h2 class="wp-block-heading" id="h-iv-conclusion">IV. Conclusion</h2>



<p class="wp-block-paragraph">Upgrading from RHEL 9.6 to 10.1 is a big move. It’s not just a simple update; it’s a total shift in the system&#8217;s foundation. Between hardware driver changes and library updates, you really have to pay attention to the details to keep your database running.</p>



<p class="wp-block-paragraph">RHEL 10.1 is a great, modern platform, but you can&#8217;t just click &#8220;update&#8221; and hope for the best. By planning the upgrade, planning to rebuild your binaries, refreshing your database objects, you can make the jump without the drama. Take the pre-upgrade report seriously, it’s there for a reason!</p>



<p class="wp-block-paragraph">That said, for a production PostgreSQL cluster, especially one managed with Patroni and etcd, I would not recommend this in-place upgrade approach. Even if Leapp makes the process technically possible, you are still:</p>



<ul class="wp-block-list">
<li>Modifying the operating system in place</li>



<li>Replacing core libraries underneath a running database stack</li>



<li>Trusting automated dependency resolution during a major version jump</li>
</ul>



<p class="wp-block-paragraph">In production, risk reduction should always be the priority.</p>



<p class="wp-block-paragraph">Instead, I strongly recommend provisioning new VMs or physical servers, installing RHEL 10.1 from scratch, deploying PostgreSQL, Patroni, and etcd cleanly, rebuilding the cluster from best practices, and then migrating the data from the old environment to the new one using replication or another appropriate method.</p>



<p class="wp-block-paragraph">Sometimes the safest upgrade… is a new cluster.</p>
<p>L’article <a href="https://www.dbi-services.com/blog/upgrade-rhel-from-9-6-to-10-1-when-running-postgresql-patroni/">Upgrade RHEL from 9.6 to 10.1 (when running PostgreSQL/Patroni)</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.dbi-services.com/blog/upgrade-rhel-from-9-6-to-10-1-when-running-postgresql-patroni/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>PostgreSQL Anonymizer: Simple Data Masking for DBAs</title>
		<link>https://www.dbi-services.com/blog/postgresql-anonymizer-simple-data-masking-for-dbas/</link>
					<comments>https://www.dbi-services.com/blog/postgresql-anonymizer-simple-data-masking-for-dbas/#respond</comments>
		
		<dc:creator><![CDATA[Joan Frey]]></dc:creator>
		<pubDate>Fri, 27 Feb 2026 10:08:50 +0000</pubDate>
				<category><![CDATA[PostgreSQL]]></category>
		<category><![CDATA[anon]]></category>
		<category><![CDATA[anonymization]]></category>
		<category><![CDATA[pg_anonymizer]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=43200</guid>

					<description><![CDATA[<p>Sensitive data (names, emails, phone numbers, personal identifiers…) should not be freely exposed outside production. When you refresh a production database to a test or staging environment, or when analysts need access to real-looking data, anonymization becomes critical. The PostgreSQL Anonymizer extension (often called anon) is an open‑source extension that lets you mask, fake, shuffle, [&#8230;]</p>
<p>L’article <a href="https://www.dbi-services.com/blog/postgresql-anonymizer-simple-data-masking-for-dbas/">PostgreSQL Anonymizer: Simple Data Masking for DBAs</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Sensitive data (names, emails, phone numbers, personal identifiers…) should not be freely exposed outside production. When you refresh a production database to a test or staging environment, or when analysts need access to real-looking data, anonymization becomes critical.</p>



<p class="wp-block-paragraph">The PostgreSQL Anonymizer extension (often called anon) is an open‑source extension that lets you mask, fake, shuffle, or generalize data directly inside PostgreSQL, using simple SQL rules. This article explains what it is, how it works, how to install it on PostgreSQL 18.1 running on Red Hat Enterprise Linux 10.1, and how to use it with clear, beginner‑friendly examples.</p>



<p class="wp-block-paragraph">The target audience is everyone, but especially beginner DBAs who want a practical, command‑line–focused introduction.</p>



<h2 class="wp-block-heading" id="h-i-what-is-postgresql-anonymizer">I. What is PostgreSQL Anonymizer?</h2>



<p class="wp-block-paragraph">PostgreSQL Anonymizer is an extension that helps protect sensitive data by replacing it with fake or obfuscated values.</p>



<p class="wp-block-paragraph">Instead of exporting data and anonymizing it with scripts, the rules live inside the database itself. You declare how a column should be anonymized, and PostgreSQL applies that rule automatically.</p>



<p class="wp-block-paragraph">Typical use cases:</p>



<ul class="wp-block-list">
<li>Refreshing production data into test / staging environments</li>



<li>Giving developers or analysts access to realistic but non‑sensitive data</li>



<li>Producing anonymized database dumps for external sharing</li>



<li>Helping comply with GDPR and other privacy regulations</li>
</ul>



<p class="wp-block-paragraph">The extension supports three main approaches:</p>



<ol start="1" class="wp-block-list">
<li>Static masking – permanently replaces data in tables</li>



<li>Dynamic masking – masks data on the fly for specific users</li>



<li>Anonymous dumps – exports an already anonymized dump</li>
</ol>



<h2 class="wp-block-heading" id="h-ii-how-does-it-work">II. How does it work ?</h2>



<p class="wp-block-paragraph">PostgreSQL Anonymizer uses PostgreSQL’s security labels mechanism. You attach a label to a column that says: “When this data is anonymized, use <em>this function</em>.”</p>



<p class="wp-block-paragraph">Example:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
SECURITY LABEL FOR anon ON customer.email IS &#039;MASKED WITH FUNCTION anon.fake_email()&#039;;
</pre></div>


<p class="wp-block-paragraph">Once declared:</p>



<ul class="wp-block-list">
<li>Static masking rewrites the table using those rules</li>



<li>Dynamic masking rewrites query results for masked users</li>



<li>Dumps automatically apply the same rules</li>
</ul>



<p class="wp-block-paragraph">The rules stay attached to the schema, not to scripts or applications.</p>



<h2 class="wp-block-heading" id="h-iii-installing-postgresql-anonymizer-on-rhel-10-1-postgresql-18-1">III. Installing PostgreSQL Anonymizer on RHEL 10.1 (PostgreSQL 18.1)</h2>



<p class="wp-block-paragraph">In this guide, PostgreSQL 18.1 is already installed following dbi services standard. PostgreSQL binaries are located in:</p>



<ul class="wp-block-list">
<li>/u01/app/postgres/product/18/db_1/bin</li>
</ul>



<p class="wp-block-paragraph">The PostgreSQL data directory (PGDATA) is:</p>



<ul class="wp-block-list">
<li>/u02/pgdata/18/demo-cluster</li>
</ul>



<p class="wp-block-paragraph">We will only focus on installing and enabling the PostgreSQL Anonymizer extension.</p>



<h3 class="wp-block-heading" id="h-1-install-the-anonymizer-extension">1. Install the Anonymizer extension</h3>



<p class="wp-block-paragraph">Since we are installing from source, we&#8217;ll start by installing the necessary prerequisites. We&#8217;ll use cargo to handle the PGRX system requirements. You can find the official documentation <a href="https://postgresql-anonymizer.readthedocs.io/en/latest/INSTALL/#install-on-redhat-rocky-linux-alma-linux" id="https://postgresql-anonymizer.readthedocs.io/en/latest/INSTALL/#install-on-redhat-rocky-linux-alma-linux">here</a>, but keep in mind that I&#8217;ve updated the commands to reflect a more recent version.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
#Cargo install

curl --proto &#039;=https&#039; --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/env
rustc --version
cargo --version

# postgresql_anonymizer install

cargo install cargo-pgrx --version 0.16.1 --locked
cargo pgrx init --pg18 /u01/app/postgres/product/18/db_1/bin/pg_config
git clone https://gitlab.com/dalibo/postgresql_anonymizer.git
cd postgresql_anonymizer/
make extension PG_CONFIG=/u01/app/postgres/product/18/db_1/bin/pg_config PGVER=pg18
sudo make install PG_CONFIG=/u01/app/postgres/product/18/db_1/bin/pg_config PGVER=pg18
</pre></div>


<h3 class="wp-block-heading" id="h-2-enable-anonymizer-in-postgresql-conf">2. Enable Anonymizer in postgresql.conf</h3>



<p class="wp-block-paragraph">Because PostgreSQL Anonymizer hooks into query execution, it must be loaded at session start. Edit the configuration file:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
vi /u02/pgdata/18/demo-cluster/postgresql.conf
</pre></div>


<p class="wp-block-paragraph">Add or update:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
session_preload_libraries = &#039;anon&#039;
</pre></div>


<p class="wp-block-paragraph">Restart PostgreSQL:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
pg_ctl -D /u02/pgdata/18/demo-cluster restart
</pre></div>


<h3 class="wp-block-heading" id="h-3-install-and-enable-the-anonymizer-extension">3. Install and enable the Anonymizer extension</h3>



<p class="wp-block-paragraph">Connect as the PostgreSQL superuser:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
psql -U postgres
</pre></div>


<p class="wp-block-paragraph">Create a database:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
CREATE DATABASE anonymizer_demo;
</pre></div>


<p class="wp-block-paragraph">Create and initialize the extension:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
postgres=# \c anonymizer_demo
You are now connected to database &quot;anonymizer_demo&quot; as user &quot;postgres&quot;.
anonymizer_demo=# CREATE EXTENSION anon CASCADE;
CREATE EXTENSION

anonymizer_demo=# \dx
                                  List of installed extensions
  Name   | Version | Default version |   Schema   |                 Description
---------+---------+-----------------+------------+---------------------------------------------
 anon    | 3.0.0   | 3.0.0           | public     | Anonymization &amp; Data Masking for PostgreSQL
 plpgsql | 1.0     | 1.0             | pg_catalog | PL/pgSQL procedural language
(2 rows)

anonymizer_demo=# SELECT anon.init();
 init
------
 t
(1 row)
</pre></div>


<p class="wp-block-paragraph">anon.init() loads fake data dictionaries (names, companies, cities, etc.) used by anonymization functions.</p>



<h2 class="wp-block-heading" id="h-iv-demo-anonymizing-a-simple-table">IV. Demo: anonymizing a simple table</h2>



<p class="wp-block-paragraph">For this demo, I will keep it simple and create everything inside the postgres database and default schema, but I recommend you to follow the best practices and use a dedicated database, user and schema.</p>



<p class="wp-block-paragraph">Create sample data:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
CREATE TABLE customer (
  id         SERIAL PRIMARY KEY,
  first_name TEXT,
  last_name  TEXT,
  birthdate  DATE,
  email      TEXT,
  company    TEXT
);

INSERT INTO customer (first_name, last_name, birthdate, email, company) VALUES
(&#039;Alice&#039;, &#039;Martin&#039;, &#039;1987-02-14&#039;, &#039;alice.martin@example.com&#039;, &#039;Acme Corp&#039;),
(&#039;Bob&#039;,   &#039;Dupont&#039;, &#039;1979-11-03&#039;, &#039;bob.dupont@example.com&#039;,   &#039;Globex&#039;);

anonymizer_demo=# select * from customer;
 id |  full_name   | birthdate  |          email           |  company
----+--------------+------------+--------------------------+-----------
  1 | Alice Martin | 1987-02-14 | alice.martin@example.com | Acme Corp
  2 | Bob Dupont   | 1979-11-03 | bob.dupont@example.com   | Globex
(2 rows)
</pre></div>


<h3 class="wp-block-heading" id="h-1-static-masking-permanent-anonymization">1. Static masking (permanent anonymization)</h3>



<p class="wp-block-paragraph">Static masking is a &#8220;fire and forget&#8221; approach where the original sensitive data is physically overwritten on the disk with faked or scrambled values. This process is destructive. Once the data is masked, the original values are gone forever. This should only be performed on non-production environments (like Staging or Dev) or on a backup copy of your database.</p>



<p class="wp-block-paragraph">Before applying any rules, you must explicitly enable the static masking engine at both the database and role levels. This acts as a safety switch to prevent accidental data loss.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
-- Enable the extension for the current database
anonymizer_demo=# ALTER DATABASE anonymizer_demo SET anon.static_masking = TRUE;
ALTER DATABASE

-- Grant the postgres user permission to execute static masking operations
anonymizer_demo=# ALTER ROLE postgres SET anon.static_masking = TRUE;
ALTER ROLE
</pre></div>


<p class="wp-block-paragraph">Next, you define how the data should be transformed. We use <code>SECURITY LABEL</code> to attach masking logic to specific columns. This doesn&#8217;t change the data yet; it simply tells the anon extension which functions to use during the anonymization process.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
-- Replace names with realistic dummy values
anonymizer_demo=# SECURITY LABEL FOR anon ON COLUMN customer.first_name IS &#039;MASKED WITH FUNCTION anon.dummy_first_name()&#039;;
SECURITY LABEL
anonymizer_demo=# SECURITY LABEL FOR anon ON COLUMN customer.last_name IS &#039;MASKED WITH FUNCTION anon.dummy_last_name()&#039;;
SECURITY LABEL

-- Generate a random date within a specific age range (1950-2000)
anonymizer_demo=# SECURITY LABEL FOR anon ON column customer.birthdate IS &#039;MASKED WITH FUNCTION anon.random_date_between(&#039;&#039;1950-01-01&#039;&#039;, &#039;&#039;2000-12-31&#039;&#039;)&#039;;
SECURITY LABEL

-- Generate syntactically correct but fake emails and company names
anonymizer_demo=# SECURITY LABEL FOR anon ON column customer.email IS &#039;MASKED WITH FUNCTION anon.fake_email()&#039;;
SECURITY LABEL
anonymizer_demo=# SECURITY LABEL FOR anon ON column customer.company IS &#039;MASKED WITH FUNCTION anon.fake_company()&#039;;
SECURITY LABEL
</pre></div>


<p class="wp-block-paragraph">This is the final execution step. Running anonymize_database() triggers the engine to scan your rules and overwrite the table data globally. Depending on the size of your database, this may take some time as it performs UPDATE operations on the disk.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
anonymizer_demo=# SELECT anon.anonymize_database();
 anonymize_database
--------------------
 t
(1 row)
</pre></div>


<p class="wp-block-paragraph">The data rules have now been applied and the data have been anonymized. You can check the result with the following query:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
anonymizer_demo=# SELECT * FROM customer;
 id | birthdate  |        email         |    company     | first_name | last_name
----+------------+----------------------+----------------+------------+------------
  1 | 1960-11-30 | lpeters@example.net  | Brown and Sons | Chyna      | Mertz
  2 | 1997-09-08 | avaughan@example.com | Rice PLC       | Damien     | Williamson
(2 rows)
</pre></div>


<p class="wp-block-paragraph">You can now turn off static masking to continue with the next demo, dynamic masking:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
anonymizer_demo=# ALTER SYSTEM SET anon.static_masking TO off;
ALTER SYSTEM
anonymizer_demo=# ALTER ROLE postgres SET anon.static_masking TO off;
ALTER ROLE
</pre></div>


<h3 class="wp-block-heading" id="h-2-dynamic-masking">2. Dynamic masking</h3>



<p class="wp-block-paragraph">Dynamic masking allows you to hide sensitive information from specific users (like developers or analysts) while preserving the original data for administrators or the application itself. The masking happens in memory at the moment the query is executed.</p>



<p class="wp-block-paragraph">First, we tell the database to activate the transparent masking engine. This allows the anon extension to intercept queries from specific roles and apply masking rules before the results are returned.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
anonymizer_demo=# ALTER DATABASE anonymizer_demo SET anon.transparent_dynamic_masking = TRUE;
ALTER DATABASE
</pre></div>


<p class="wp-block-paragraph">To see show case masking in action, we will use two types of users: a masked user who sees fake data, and an unmasked user (like the superuser) who sees the actual data stored on disk.</p>



<p class="wp-block-paragraph">In this step, we create <code>demo_user</code> and &#8220;tag&#8221; them with a security label that forces the masking engine to engage whenever they log in.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
anonymizer_demo=# CREATE ROLE demo_user LOGIN;
CREATE ROLE
anonymizer_demo=# SECURITY LABEL FOR anon ON ROLE demo_user IS &#039;MASKED&#039;;
SECURITY LABEL
anonymizer_demo=# GRANT pg_read_all_data to demo_user;
GRANT ROLE

anonymizer_demo=# SECURITY LABEL FOR anon ON ROLE demo_user IS &#039;MASKED&#039;;
SECURITY LABEL

-- As PostgreSQL user:
-- Ensure the postgres user remains unmasked so we can see the &#039;real&#039; data

anonymizer_demo=# SECURITY LABEL FOR anon ON ROLE postgres IS NULL;
SECURITY LABEL
</pre></div>


<p class="wp-block-paragraph">We don&#8217;t need to redefine our masking rules (for names, emails, etc.) because the SECURITY LABEL definitions we created in the static masking section are still stored in the database schema.</p>



<p class="wp-block-paragraph">Watch what happens when we query the table as demo_user:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
anonymizer_demo=&gt; select * from customer;
 id | birthdate  |          email           |     company      | first_name | last_name
----+------------+--------------------------+------------------+------------+-----------
  1 | 1957-02-07 | walterkristi@example.org | Fernandez-Tucker | Emelie     | Rohan
  2 | 1950-05-15 | hannah76@example.com     | Leonard Group    | Jane       | Durgan
(2 rows)
</pre></div>


<p class="wp-block-paragraph">If we run the exact same command again, the output changes:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
anonymizer_demo=&gt; select * from customer;
 id | birthdate  |         email          |    company    | first_name | last_name
----+------------+------------------------+---------------+------------+------------
  1 | 1963-12-26 | blairpeter@example.com | Simpson Group | Tod        | Balistreri
  2 | 1971-09-14 | steven27@example.com   | Davis-Hardin  | Sonny      | Wintheiser
(2 rows)
</pre></div>


<p class="wp-block-paragraph">Because the data is being generated &#8220;on-the-fly&#8221; by the masking functions, the results are dynamic. Each request produces a fresh set of data.</p>



<p class="wp-block-paragraph">Now, let&#8217;s switch back to the postgres user. Since we removed the MASKED label from this role, the engine steps aside and shows us the actual data residing on the disk (which, in this case, is the data we masked statically in the previous step).</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
anonymizer_demo=# SELECT * FROM customer;
 id | birthdate  |        email         |    company     | first_name | last_name
----+------------+----------------------+----------------+------------+------------
  1 | 1960-11-30 | lpeters@example.net  | Brown and Sons | Chyna      | Mertz
  2 | 1997-09-08 | avaughan@example.com | Rice PLC       | Damien     | Williamson
(2 rows)
</pre></div>


<h3 class="wp-block-heading" id="h-3-anonymized-dump">3. Anonymized dump</h3>



<p class="wp-block-paragraph">An Anonymized Dump allows you to export your database into a <code>.sql</code> file where the sensitive data is already replaced by fake values. This is incredibly powerful because you can create a &#8220;safe&#8221; backup that can be shared with developers or consultants without ever giving them access to your live production server.</p>



<p class="wp-block-paragraph">Rather than using a superuser, we create a specific role whose sole purpose is to retrieve the masked version of the data during the export process.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
-- Create a specialized user for the dump process
anonymizer_demo=# CREATE ROLE demo_ano_dumper LOGIN PASSWORD &#039;secret&#039;;
CREATE ROLE

-- Force the masking engine to be active for this user session
anonymizer_demo=# ALTER ROLE demo_ano_dumper SET anon.transparent_dynamic_masking = TRUE;
ALTER ROLE

-- Apply the MASKED label to the role
anonymizer_demo=# SECURITY LABEL FOR anon ON ROLE demo_ano_dumper IS &#039;MASKED&#039;;
SECURITY LABEL

-- Grant permission to read all tables
anonymizer_demo=# GRANT pg_read_all_data TO demo_ano_dumper;
GRANT
</pre></div>


<p class="wp-block-paragraph">Now, we use the standard pg_dump utility. Because we are logging in as demo_ano_dumper, the anon extension intercepts the data export on-the-fly. We use a few specific flags to ensure the resulting file is clean and doesn&#8217;t contain the masking logic itself:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
/u01/app/postgres/product/18/db_1/bin/pg_dump anonymizer_demo --username=demo_ano_dumper --password --no-security-labels --exclude-extension=&quot;anon&quot; --file=anonymized_dump.sql
</pre></div>


<p class="wp-block-paragraph"><strong><code>--no-security-labels</code></strong>: Prevents the &#8220;MASKED&#8221; tags from being exported (the new database doesn&#8217;t need to know how the data was masked).</p>



<p class="wp-block-paragraph"><strong><code>--exclude-extension="anon"</code></strong>: Ensures the recipient doesn&#8217;t need the <code>anon</code> extension installed to restore the file.</p>



<p class="wp-block-paragraph">If you open the generated anonymized_dump.sql file in a text editor, you will see that the COPY commands contain the fake data, not the original sensitive information.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
--
-- Data for Name: customer; Type: TABLE DATA; Schema: public; Owner: postgres
--

COPY public.customer (id, birthdate, email, company, first_name, last_name) FROM stdin;
1       1980-12-17      cannonlauren@example.com        Carr-Doyle      Katharina       Shanahan
2       1998-03-27      sfox@example.net        Pollard and Sons        Ayla    Spencer
\.
</pre></div>


<h2 class="wp-block-heading" id="h-v-conclusion">V. Conclusion</h2>



<p class="wp-block-paragraph">In modern development, the goal is to work with realistic data without the real-world risk. PostgreSQL Anonymizer bridges this gap by allowing you to transform sensitive production information into safe, functional datasets.</p>



<p class="wp-block-paragraph">Now that we&#8217;ve explained how to use pg_anonymizer and covered all three methods, here is a quick guide on when to use each:</p>



<ul class="wp-block-list">
<li><strong>Static Masking:</strong> Best for &#8220;cleaning&#8221; a staging database after a production refresh.</li>



<li><strong>Dynamic Masking:</strong> Best for internal users (DBAs, support staff) who need to work on the live database but shouldn&#8217;t see production data.</li>



<li><strong>Anonymized Dump:</strong> Best for sharing data with external partners or creating local development environments.</li>
</ul>



<p class="wp-block-paragraph">In the end, whether you choose Static, Dynamic, or Dump masking, the benefits remain the same:</p>



<ul class="wp-block-list">
<li><strong>Utility:</strong> Because the data is masked with realistic functions (like <code>fake_email</code> or <code>dummy_first_name</code>), your application logic—like email validation or UI layout—still works perfectly.</li>



<li><strong>Compliance:</strong> Meet GDPR, HIPAA, and internal security requirements by default.</li>



<li><strong>Safety:</strong> Developers and analysts can work on real-world bugs and features without ever seeing a customer&#8217;s actual PII (Personally Identifiable Information).</li>
</ul>



<p class="wp-block-paragraph">I hope you enjoyed this guide and found these examples clear and easy to follow! My goal was to show that data privacy doesn&#8217;t have to be painful for your development workflow. Don&#8217;t forget to follow the extension latest news on the official website: <a href="https://postgresql-anonymizer.readthedocs.io/en/latest/">https://postgresql-anonymizer.readthedocs.io/en/latest/</a></p>



<p class="wp-block-paragraph">If you have any questions about these commands or how to implement them in your own environment, feel free to reach out <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f642.png" alt="🙂" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>
<p>L’article <a href="https://www.dbi-services.com/blog/postgresql-anonymizer-simple-data-masking-for-dbas/">PostgreSQL Anonymizer: Simple Data Masking for DBAs</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.dbi-services.com/blog/postgresql-anonymizer-simple-data-masking-for-dbas/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Monitoring PostgreSQL on Windows with PgHero and Docker</title>
		<link>https://www.dbi-services.com/blog/monitoring-postgresql-on-windows-with-pghero-and-docker/</link>
					<comments>https://www.dbi-services.com/blog/monitoring-postgresql-on-windows-with-pghero-and-docker/#respond</comments>
		
		<dc:creator><![CDATA[Joan Frey]]></dc:creator>
		<pubDate>Thu, 26 Feb 2026 09:35:33 +0000</pubDate>
				<category><![CDATA[Database Administration & Monitoring]]></category>
		<category><![CDATA[pghero]]></category>
		<category><![CDATA[PostgreSQL]]></category>
		<category><![CDATA[Windows]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=41528</guid>

					<description><![CDATA[<p>I know, I know, PostgreSQL usually feels more at home on Linux. But guess what? PgHero works just fine on Windows too, and I gave it a try myself. If you’re running PostgreSQL in a Docker container, this setup is surprisingly easily, and it gives you a great performance dashboard without touching your base system. [&#8230;]</p>
<p>L’article <a href="https://www.dbi-services.com/blog/monitoring-postgresql-on-windows-with-pghero-and-docker/">Monitoring PostgreSQL on Windows with PgHero and Docker</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p>I know, I know, PostgreSQL usually feels more at home on Linux. But guess what? PgHero works just fine on Windows too, and I gave it a try myself. If you’re running PostgreSQL in a Docker container, this setup is surprisingly easily, and it gives you a great performance dashboard without touching your base system.</p>
<p>In this post, I’ll show you how I set it up with Docker Compose, including running PgHero itself in a container. The goal: a fully contained stack that’s easy to spin up, monitor, and tear down anytime.</p>
<h2>What you need</h2>
<ul>
<li>Docker Desktop installed on Windows (I recommend using WSL2 for smooth volume mounting).</li>
<li>A PostgreSQL database running in Docker. We’ll build the stack together if you don’t have one yet. In my case, I decided to install PgHero when I was following one of the NestJs course, and used my existing stack.</li>
<li>Basic familiarity with Docker Compose and command-line.</li>
</ul>
<p>Optionally, you can create a folder on your PC to store database files, so your data sticks around even if the container stops. I will use a volume in this blog, to easily edit my PostgreSQL configuration.</p>
<h2>Step 1: Your Docker Compose setup</h2>
<p>Here’s a minimal Compose file for PostgreSQL, Redis, and PgHero. Save it somewhere on your machine, like <code>C:\docker\pghero-setup\docker-compose.yml</code>:</p>
<pre><code class="language-yaml">version: "3"
services:
  db:
    image: postgres
    restart: always
    ports:
      - "5438:5432"
    environment:
      POSTGRES_PASSWORD: pass123
      POSTGRES_USER: postgres
      POSTGRES_DB: postgres
    volumes:
      - "C:/Users/frj/Documents/Training/Nestjs/auth-extension/docker/postgres-data:/var/lib/postgresql/data"

  redis:
    image: redis
    ports:
      - "6379:6379"
    restart: always

  pghero:
    image: ankane/pghero
    depends_on:
      - db
    ports:
      - "8080:8080"
    environment:<br />      DATABASE_URL: postgres://postgres:pass123@db:5432/postgres
      PGHERO_USERNAME: admin
      PGHERO_PASSWORD: secret
    restart: always</code></pre>
<p><strong>Why this works:</strong></p>
<ul>
<li><code>db</code> is your PostgreSQL service. The bind mount points to a folder on your PC, so your data survives container restarts.</li>
<li>The <code>command</code> lines ensure PostgreSQL loads <code>pg_stat_statements</code>, which PgHero uses to track queries.</li>
<li><code>pghero</code> runs in its own container and exposes port 8080 for the dashboard.</li>
<li><code>depends_on: db</code> ensures PgHero waits for PostgreSQL to start.</li>
</ul>
<h2>Step 2: Monitoring multiple databases</h2>
<p>To be able to track more than one database, we create a <code>pghero.yml</code> file instead of using the <span style="color: initial">DATABASE_URL environment variable. Create the file in the same directory as docker-compose.yml:</span></p>
<pre><code class="language-yaml">databases:
  primary:
    url: postgres://postgres:pass123@db:5432/postgres
  analytics:
    url: postgres://postgres:pass123@db:5432/analytics</code></pre>
<p>Then update <code>docker-compose.yml</code> for PgHero:</p>
<pre><code class="language-yaml">  pghero:
    image: ankane/pghero
    depends_on:
      - db
    ports:
      - "8080:8080"
    environment:
      PGHERO_USERNAME: admin
      PGHERO_PASSWORD: secret
    volumes:
      - "./pghero.yml:/app/config/pghero.yml:ro"
    restart: always</code></pre>
<p>PgHero will now show a dropdown to switch between databases in the UI.</p>
<h2>Step 3: Start everything</h2>
<p>In the folder with your <code>docker-compose.yml</code>:</p>
<pre><code>docker-compose down  # stop any previous run
docker-compose up -d</code></pre>
<p>After a few seconds, open your browser at <a href="http://localhost:8080">http://localhost:8080</a>. Log in with the username and password you set (<code>admin</code> / <code>secret</code>). You should see the PgHero dashboard connected to your database.</p>


<figure class="wp-block-image size-large"><img fetchpriority="high" decoding="async" width="1024" height="557" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/11/image-4-1024x557.png" alt="" class="wp-image-41544" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/11/image-4-1024x557.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/11/image-4-300x163.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/11/image-4-768x418.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/11/image-4.png 1027w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<h2 class="wp-block-heading">Step 4: Enable query tracking</h2>



<p class="wp-block-paragraph">After starting PostgreSQL, connect to it and enable the <code>pg_stat_statements</code> extension. Inside your container, run:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
docker exec -it &amp;lt;db_container&amp;gt; psql -U postgres -d postgres
</pre></div>


<p class="wp-block-paragraph">Then in <code>psql</code>:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
</pre></div>


<p class="wp-block-paragraph">This makes sure PgHero can track queries, see long-running statements, and show useful stats. Note that you have to do it for each database.</p>



<figure class="wp-block-image size-full"><img decoding="async" width="1014" height="595" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/11/image-5.png" alt="" class="wp-image-41547" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/11/image-5.png 1014w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/11/image-5-300x176.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/11/image-5-768x451.png 768w" sizes="(max-width: 1014px) 100vw, 1014px" /></figure>



<h2 class="wp-block-heading">Step 5: Windows-specific tips</h2>



<ul class="wp-block-list">
<li>Use forward slashes (<code>C:/path/to/folder</code>) for volume paths in Compose.</li>



<li>Make sure the folder exists before starting PostgreSQL.</li>



<li>Check your <code>pg_hba.conf</code> if you see connection errors — Docker containers have their own internal IPs, and PostgreSQL needs to allow connections from them.</li>
</ul>



<h2 class="wp-block-heading">Step 6: Common troubleshooting</h2>



<ul class="wp-block-list">
<li><strong>500 Internal Server Error / cannot connect:</strong> Usually a problem with <code>pg_hba.conf</code>. Make sure your Docker network is allowed.</li>



<li><strong>No stats showing:</strong> Ensure <code>pg_stat_statements</code> is loaded and the extension is created.</li>



<li><strong>Dashboard not reachable:</strong> Check Docker mapped ports, firewall, and container logs (<code>docker logs &lt;container-id&gt;</code>).</li>
</ul>



<h2 class="wp-block-heading">Conclusion</h2>



<p class="wp-block-paragraph">And that’s it! You now have a Windows-based Docker setup with PostgreSQL and PgHero. You can see your query stats, monitor performance, and even track multiple databases from the same dashboard.</p>
<p>L’article <a href="https://www.dbi-services.com/blog/monitoring-postgresql-on-windows-with-pghero-and-docker/">Monitoring PostgreSQL on Windows with PgHero and Docker</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.dbi-services.com/blog/monitoring-postgresql-on-windows-with-pghero-and-docker/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Unit Testing PostgreSQL with pgTAP</title>
		<link>https://www.dbi-services.com/blog/unit-testing-postgresql-with-pgtap/</link>
					<comments>https://www.dbi-services.com/blog/unit-testing-postgresql-with-pgtap/#respond</comments>
		
		<dc:creator><![CDATA[Joan Frey]]></dc:creator>
		<pubDate>Fri, 29 Aug 2025 16:23:47 +0000</pubDate>
				<category><![CDATA[Database Administration & Monitoring]]></category>
		<category><![CDATA[Database management]]></category>
		<category><![CDATA[automated]]></category>
		<category><![CDATA[pgTAP]]></category>
		<category><![CDATA[PostgreSQL]]></category>
		<category><![CDATA[testing]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=37854</guid>

					<description><![CDATA[<p>Introduction Unit testing is a fundamental practice in software development, ensuring that individual components function correctly. When working with PostgreSQL, testing database logic—such as functions, triggers, and constraints—is crucial for maintaining data integrity and reliability. One powerful tool for this purpose is pgTAP.pgTAP is a PostgreSQL extension that provides a set of TAP (Test Anything [&#8230;]</p>
<p>L’article <a href="https://www.dbi-services.com/blog/unit-testing-postgresql-with-pgtap/">Unit Testing PostgreSQL with pgTAP</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></description>
										<content:encoded><![CDATA[<div class="wp-block-image">
<figure class="aligncenter size-full is-resized"><img decoding="async" width="800" height="800" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/04/image-2.png" alt="" class="wp-image-37881" style="width:412px;height:auto" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/04/image-2.png 800w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/04/image-2-300x300.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/04/image-2-150x150.png 150w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/04/image-2-768x768.png 768w" sizes="(max-width: 800px) 100vw, 800px" /></figure>
</div>


<h2 class="wp-block-heading" id="h-introduction">Introduction</h2>



<p class="wp-block-paragraph">Unit testing is a fundamental practice in software development, ensuring that individual components function correctly. When working with PostgreSQL, testing database logic—such as functions, triggers, and constraints—is crucial for maintaining data integrity and reliability. One powerful tool for this purpose is <strong>pgTAP</strong>.<br>pgTAP is a PostgreSQL extension that provides a set of TAP (Test Anything Protocol) functions for writing unit tests directly in SQL. It allows developers to test database functions, schemas, constraints, and much more in an automated and repeatable way.</p>



<h2 class="wp-block-heading" id="h-installing-pgtap">Installing pgTAP</h2>



<p class="wp-block-paragraph">Before using pgTAP, you need to install it on your PostgreSQL instance. You can install it from source as follows:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
10:18:56 postgres@ws-pgdev:/home/postgres/ &#x5B;sw] wget https://api.pgxn.org/dist/pgtap/1.3.3/pgtap-1.3.3.zip .
--2025-04-02 10:19:53--  https://api.pgxn.org/dist/pgtap/1.3.3/pgtap-1.3.3.zip
Resolving api.pgxn.org (api.pgxn.org)... 88.198.49.178
Connecting to api.pgxn.org (api.pgxn.org)|88.198.49.178|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 329966 (322K) &#x5B;application/zip]
Saving to: ‘pgtap-1.3.3.zip’

pgtap-1.3.3.zip                         100%&#x5B;============================================================================&gt;] 322.23K  --.-KB/s    in 0.1s

2025-04-02 10:19:54 (3.18 MB/s) - ‘pgtap-1.3.3.zip’ saved &#x5B;329966/329966]

--2025-04-02 10:19:54--  http://./
Resolving . (.)... failed: No address associated with hostname.
wget: unable to resolve host address ‘.’
FINISHED --2025-04-02 10:19:54--
Total wall clock time: 0.4s
Downloaded: 1 files, 322K in 0.1s (3.18 MB/s)

10:19:54 postgres@ws-pgdev:/home/postgres/ &#x5B;sw] unzip pgtap-1.3.3.zip
Archive:  pgtap-1.3.3.zip
b941782fada240afdb7057065eb3261a21e8512c
   creating: pgtap-1.3.3/
  inflating: pgtap-1.3.3/Changes
...
...

10:20:11 postgres@ws-pgdev:/home/postgres/ &#x5B;sw] cd pgtap-1.3.3/
11:11:58 postgres@ws-pgdev:/home/postgres/pgtap-1.3.3/ &#x5B;sw] make

GNUmake running against Postgres version 17.0, with pg_config located at /u01/app/postgres/product/17/db_0/bin

Makefile:186: To use pg_prove, TAP::Parser::SourceHandler::pgTAP Perl module
Makefile:187: must be installed from CPAN. To do so, simply run:
Makefile:188: cpan TAP::Parser::SourceHandler::pgTAP
cp sql/pgtap--0.95.0--0.96.0.sql.in sql/pgtap--0.95.0--0.96.0.sql
cp sql/pgtap--0.96.0--0.97.0.sql.in sql/pgtap--0.96.0--0.97.0.sql
cp sql/pgtap--0.97.0--0.98.0.sql.in sql/pgtap--0.97.0--0.98.0.sql
cp sql/pgtap--0.98.0--0.99.0.sql.in sql/pgtap--0.98.0--0.99.0.sql
cp sql/pgtap--0.99.0--1.0.0.sql.in sql/pgtap--0.99.0--1.0.0.sql
cp sql/pgtap.sql.in sql/pgtap.sql
sed -e &#039;s,MODULE_PATHNAME,$libdir/pgtap,g&#039; -e &#039;s,__OS__,linux,g&#039; -e &#039;s,__VERSION__,1.3,g&#039; sql/pgtap.sql &gt; sql/pgtap.tmp
mv sql/pgtap.tmp sql/pgtap.sql
&#039;/usr/bin/perl&#039; -e &#039;for (grep { /^CREATE /} reverse &lt;&gt;) { chomp; s/CREATE (OR REPLACE )?/DROP /; s/DROP (FUNCTION|VIEW|TYPE) /DROP $1 IF EXISTS /; s/ (DEFAUL                                T|=)&#x5B; ]+&#x5B;a-zA-Z0-9]+//g; print &quot;$_;\n&quot; }&#039; sql/pgtap.sql &gt; sql/uninstall_pgtap.sql
cp sql/pgtap.sql.in sql/pgtap-static.sql.tmp

*** Patching pgtap-static.sql with compat/install-9.6.patch
patching file sql/pgtap-static.sql.tmp

*** Patching pgtap-static.sql with compat/install-9.4.patch
patching file sql/pgtap-static.sql.tmp

*** Patching pgtap-static.sql with compat/install-9.2.patch
patching file sql/pgtap-static.sql.tmp

*** Patching pgtap-static.sql with compat/install-9.1.patch
patching file sql/pgtap-static.sql.tmp
sed -e &#039;s#MODULE_PATHNAME#$libdir/pgtap#g&#039; -e &#039;s#__OS__#linux#g&#039; -e &#039;s#__VERSION__#1.3#g&#039; sql/pgtap-static.sql.tmp &gt; sql/pgtap-static.sql
&#039;/usr/bin/perl&#039; compat/gencore 0 sql/pgtap-static.sql &gt; sql/pgtap-core.sql
&#039;/usr/bin/perl&#039; compat/gencore 1 sql/pgtap-static.sql &gt; sql/pgtap-schema.sql
cp sql/pgtap.sql sql/pgtap--1.3.3.sql
cp sql/pgtap-core.sql sql/pgtap-core--1.3.3.sql
cp sql/pgtap-schema.sql sql/pgtap-schema--1.3.3.sql

11:12:02 postgres@ws-pgdev:/home/postgres/pgtap-1.3.3/ &#x5B;sw] make install

GNUmake running against Postgres version 17.0, with pg_config located at /u01/app/postgres/product/17/db_0/bin

Makefile:186: To use pg_prove, TAP::Parser::SourceHandler::pgTAP Perl module
Makefile:187: must be installed from CPAN. To do so, simply run:
Makefile:188: cpan TAP::Parser::SourceHandler::pgTAP
mkdir -p &#039;/u01/app/postgres/product/17/db_0/share/extension&#039;
mkdir -p &#039;/u01/app/postgres/product/17/db_0/share/extension&#039;
mkdir -p &#039;/u01/app/postgres/product/17/db_0/share/doc/extension&#039;
/bin/sh /u01/app/postgres/product/17/db_0/lib/pgxs/src/makefiles/../../config/install-sh -c -m 644 .//pgtap.control &#039;/u01/app/postgres/product/17/db_0/share/extension/&#039;
/bin/sh /u01/app/postgres/product/17/db_0/lib/pgxs/src/makefiles/../../config/install-sh -c -m 644 .//sql/pgtap--0.90.0--0.91.0.sql .//sql/pgtap--0.91.0--0.92.0.sql .//sql/pgtap--0.92.0--0.93.0.sql .//sql/pgtap--0.93.0--0.94.0.sql .//sql/pgtap--0.94.0--0.95.0.sql .//sql/pgtap--0.95.0--0.96.0.sql .//sql/pgtap--0.96.0--0.97.0.sql .//sql/pgtap--0.97.0--0.98.0.sql .//sql/pgtap--0.98.0--0.99.0.sql .//sql/pgtap--0.99.0--1.0.0.sql .//sql/pgtap--1.0.0--1.1.0.sql .//sql/pgtap--1.1.0--1.2.0.sql .//sql/pgtap--1.2.0--1.3.0.sql .//sql/pgtap--1.3.0--1.3.1.sql .//sql/pgtap--1.3.1--1.3.2.sql .//sql/pgtap--1.3.2--1.3.3.sql .//sql/pgtap--1.3.3.sql .//sql/pgtap--unpackaged--0.91.0.sql .//sql/pgtap-core--1.3.3.sql .//sql/pgtap-core.sql .//sql/pgtap-schema--1.3.3.sql .//sql/pgtap-schema.sql .//sql/pgtap.sql .//sql/uninstall_pgtap.sql  &#039;/u01/app/postgres/product/17/db_0/share/extension/&#039;
/bin/sh /u01/app/postgres/product/17/db_0/lib/pgxs/src/makefiles/../../config/install-sh -c -m 644 .//doc/pgtap.mmd &#039;/u01/app/postgres/product/17/db_0/share/doc/extension/&#039;
</pre></div>


<p class="wp-block-paragraph">As mentioned in the output of the previous command, we need to run some commands to be able to use pg_prove, which we are going to use later:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
11:14:23 postgres@ws-pgdev:/home/postgres/pgtap-1.3.3/ &#x5B;sw] cpan TAP::Parser::SourceHandler::pgTAP
Loading internal logger. Log::Log4perl recommended for better logging

CPAN.pm requires configuration, but most of it can be done automatically.
If you answer &#039;no&#039; below, you will enter an interactive dialog for each
configuration option instead.

Would you like to configure as much as possible automatically? &#x5B;yes] yes

Warning: You do not have write permission for Perl library directories.

To install modules, you need to configure a local Perl library directory or
escalate your privileges.  CPAN can help you by bootstrapping the local::lib
module or by configuring itself to use &#039;sudo&#039; (if available).  You may also
resolve this problem manually if you need to customize your setup.

What approach do you want?  (Choose &#039;local::lib&#039;, &#039;sudo&#039; or &#039;manual&#039;)
 &#x5B;local::lib] sudo
Fetching with HTTP::Tiny:
https://cpan.org/authors/01mailrc.txt.gz
Reading &#039;/home/postgres/.cpan/sources/authors/01mailrc.txt.gz&#039;
............................................................................DONE
Fetching with HTTP::Tiny:
https://cpan.org/modules/02packages.details.txt.gz
Reading &#039;/home/postgres/.cpan/sources/modules/02packages.details.txt.gz&#039;
  Database was generated on Wed, 02 Apr 2025 08:29:02 GMT
..............
  New CPAN.pm version (v2.38) available.
  &#x5B;Currently running version is v2.33]
  You might want to try
    install CPAN
    reload cpan
  to both upgrade CPAN.pm and run the new version without leaving
  the current session.


..............................................................DONE
Fetching with HTTP::Tiny:
https://cpan.org/modules/03modlist.data.gz
Reading &#039;/home/postgres/.cpan/sources/modules/03modlist.data.gz&#039;
DONE
Writing /home/postgres/.cpan/Metadata
Running install for module &#039;TAP::Parser::SourceHandler::pgTAP&#039;
Fetching with HTTP::Tiny:
https://cpan.org/authors/id/D/DW/DWHEELER/TAP-Parser-SourceHandler-pgTAP-3.37.tar.gz
Fetching with HTTP::Tiny:
https://cpan.org/authors/id/D/DW/DWHEELER/CHECKSUMS
Checksum for /home/postgres/.cpan/sources/authors/id/D/DW/DWHEELER/TAP-Parser-SourceHandler-pgTAP-3.37.tar.gz ok
&#039;YAML&#039; not installed, will not store persistent state
Configuring D/DW/DWHEELER/TAP-Parser-SourceHandler-pgTAP-3.37.tar.gz with Build.PL
Created MYMETA.yml and MYMETA.json
Creating new &#039;Build&#039; script for &#039;TAP-Parser-SourceHandler-pgTAP&#039; version &#039;3.37&#039;
  DWHEELER/TAP-Parser-SourceHandler-pgTAP-3.37.tar.gz
  /usr/bin/perl Build.PL --installdirs site -- OK
Running Build for D/DW/DWHEELER/TAP-Parser-SourceHandler-pgTAP-3.37.tar.gz
Building TAP-Parser-SourceHandler-pgTAP
  DWHEELER/TAP-Parser-SourceHandler-pgTAP-3.37.tar.gz
  ./Build -- OK
Running Build test for DWHEELER/TAP-Parser-SourceHandler-pgTAP-3.37.tar.gz
t/source_handler.t .. ok
All tests successful.
Files=1, Tests=47,  1 wallclock secs ( 0.03 usr  0.00 sys +  0.08 cusr  0.12 csys =  0.23 CPU)
Result: PASS
  DWHEELER/TAP-Parser-SourceHandler-pgTAP-3.37.tar.gz
  ./Build test -- OK
Running Build install for DWHEELER/TAP-Parser-SourceHandler-pgTAP-3.37.tar.gz
Building TAP-Parser-SourceHandler-pgTAP
Installing /usr/local/man/man1/pg_prove.1p
Installing /usr/local/man/man1/pg_tapgen.1p
Installing /usr/local/share/perl/5.36.0/TAP/Parser/SourceHandler/pgTAP.pm
Installing /usr/local/man/man3/TAP::Parser::SourceHandler::pgTAP.3pm
Installing /usr/local/bin/pg_tapgen
Installing /usr/local/bin/pg_prove
  DWHEELER/TAP-Parser-SourceHandler-pgTAP-3.37.tar.gz
  sudo ./Build install  -- OK
11:15:03 postgres@ws-pgdev:/home/postgres/pgtap-1.3.3/ &#x5B;sw] mkdir -p &#039;/u01/app/postgres/product/17/db_0/share/extension&#039;
11:15:05 postgres@ws-pgdev:/home/postgres/pgtap-1.3.3/ &#x5B;sw] mkdir -p &#039;/u01/app/postgres/product/17/db_0/share/extension&#039;
mkdir -p &#039;/u01/app/postgres/product/17/db_0/share/doc/extension&#039;
11:15:10 postgres@ws-pgdev:/home/postgres/pgtap-1.3.3/ &#x5B;sw] /bin/sh /u01/app/postgres/product/17/db_0/lib/pgxs/src/makefiles/../../config/install-sh -c -m 64                                4 .//pgtap.control &#039;/u01/app/postgres/product/17/db_0/share/extension/&#039;
11:15:18 postgres@ws-pgdev:/home/postgres/pgtap-1.3.3/ &#x5B;sw] /bin/sh /u01/app/postgres/product/17/db_0/lib/pgxs/src/makefiles/../../config/install-sh -c -m 64                                4 .//sql/pgtap--0.90.0--0.91.0.sql .//sql/pgtap--0.91.0--0.92.0.sql .//sql/pgtap--0.92.0--0.93.0.sql .//sql/pgtap--0.93.0--0.94.0.sql .//sql/pgtap--0.94.0--0                                .95.0.sql .//sql/pgtap--0.95.0--0.96.0.sql .//sql/pgtap--0.96.0--0.97.0.sql .//sql/pgtap--0.97.0--0.98.0.sql .//sql/pgtap--0.98.0--0.99.0.sql .//sql/pgtap--0                                .99.0--1.0.0.sql .//sql/pgtap--1.0.0--1.1.0.sql .//sql/pgtap--1.1.0--1.2.0.sql .//sql/pgtap--1.2.0--1.3.0.sql .//sql/pgtap--1.3.0--1.3.1.sql .//sql/pgtap--1.                                3.1--1.3.2.sql .//sql/pgtap--1.3.2--1.3.3.sql .//sql/pgtap--1.3.3.sql .//sql/pgtap--unpackaged--0.91.0.sql .//sql/pgtap-core--1.3.3.sql .//sql/pgtap-core.sql                                 .//sql/pgtap-schema--1.3.3.sql .//sql/pgtap-schema.sql .//sql/pgtap.sql .//sql/uninstall_pgtap.sql  &#039;/u01/app/postgres/product/17/db_0/share/extension/&#039;
11:15:33 postgres@ws-pgdev:/home/postgres/pgtap-1.3.3/ &#x5B;sw] /bin/sh /u01/app/postgres/product/17/db_0/lib/pgxs/src/makefiles/../../config/install-sh -c -m 64                                4 .//doc/pgtap.mmd &#039;/u01/app/postgres/product/17/db_0/share/doc/extension/&#039;
11:15:37 postgres@ws-pgdev:/home/postgres/pgtap-1.3.3/ &#x5B;sw] pg_prove
No tests named and &#039;t&#039; directory not found at /usr/share/perl/5.36/App/Prove.pm line 522.

11:15:42 postgres@ws-pgdev:/home/postgres/pgtap-1.3.3/ &#x5B;sw] pg_prove --version
pg_prove 3.37
</pre></div>


<p class="wp-block-paragraph">You can check if pgTAP was installed properly using the following command:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
10:24:09 postgres@ws-pgdev:/home/postgres/pgtap-1.3.3/ &#x5B;sw] make installcheck

GNUmake running against Postgres version 17.0, with pg_config located at /u01/app/postgres/product/17/db_0/bin

Makefile:186: To use pg_prove, TAP::Parser::SourceHandler::pgTAP Perl module
Makefile:187: must be installed from CPAN. To do so, simply run:
Makefile:188: cpan TAP::Parser::SourceHandler::pgTAP
Using 89 parallel test connections
Rebuilding test/build/all_tests
Schedule changed to test/build/parallel.sch
cp `cat test/build/which_schedule` test/build/run.sch
echo &quot;# +++ regress install-check in  +++&quot; &amp;&amp; /u01/app/postgres/product/17/db_0/lib/pgxs/src/makefiles/../../src/test/regress/pg_regress --inputdir=./ --bindir=&#039;/u01/app/postgres/product/17/db_0/bin&#039;    --inputdir=test --max-connections=89 --schedule test/schedule/main.sch   --schedule test/build/run.sch
# +++ regress install-check in  +++
# using postmaster on Unix socket, port 5432
ok 1         - build                                     369 ms
...
ok 4         - hastap                                   1309 ms
# parallel group (35 tests):  matching istap do_tap moretap util performs_ok performs_within todotap check cmpok pg73 runjusttests roletap throwtap trigger usergroup enumtap policy runtests runnotests proctap fktap privs inheritance partitions valueset functap resultset aretap extension ownership ruletap pktap index unique
ok 5         + aretap                                   5911 ms
ok 6         + check                                    1558 ms
...
ok 39        + valueset                                 3784 ms
1..39
# All 39 tests passed.
</pre></div>


<p class="wp-block-paragraph">Once installed, enable it in your database:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
10:25:06 postgres@ws-pgdev:/home/postgres/pgtap-1.3.3/ &#x5B;sw] psql
psql (17.0 dbi services build)
Type &quot;help&quot; for help.

postgres=# \c hybrid
You are now connected to database &quot;hybrid&quot; as user &quot;postgres&quot;.
hybrid=# CREATE EXTENSION pgtap;
CREATE EXTENSION
hybrid=# \dx
                                                        List of installed extensions
        Name        | Version |    Schema    |                                          Description
--------------------+---------+--------------+-----------------------------------------------------------------------------------------------
 btree_gist         | 1.7     | training_app | support for indexing common datatypes in GiST
 orafce             | 4.14    | training_app | Functions and operators that emulate a subset of functions and packages from the Oracle RDBMS
 pg_stat_statements | 1.11    | public       | track planning and execution statistics of all SQL statements executed
 pg_trgm            | 1.6     | training_app | text similarity measurement and index searching based on trigrams
 pgcrypto           | 1.3     | training_app | cryptographic functions
 pgtap              | 1.3.3   | training_app | Unit testing for PostgreSQL
 plperl             | 1.0     | pg_catalog   | PL/Perl procedural language
 plpgsql            | 1.0     | pg_catalog   | PL/pgSQL procedural language
(8 rows)
</pre></div>


<h2 class="wp-block-heading" id="h-writing-your-first-pgtap-tests">Writing Your First pgTAP Tests</h2>



<p class="wp-block-paragraph">pgTAP provides a wide range of assertions for testing various database objects. Let&#8217;s go through some examples.</p>



<h3 class="wp-block-heading" id="h-1-testing-a-function">1. Testing a Function</h3>



<p class="wp-block-paragraph">Assume we have a function that verifies a password based on a specific pattern:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
CREATE OR REPLACE FUNCTION training_app.f_password_verify (pv_password TEXT) RETURNS BOOLEAN
AS $$
  SELECT pv_password ~ &#039;^(?=.{10,}$)(?=.*&#x5B;a-z])(?=.*&#x5B;A-Z])(?=.*&#x5B;0-9])(?=.*\W).*$&#039;;
$$ LANGUAGE sql;

hybrid=# \df f_password_verify
                                List of functions
    Schema    |       Name        | Result data type | Argument data types | Type
--------------+-------------------+------------------+---------------------+------
 training_app | f_password_verify | boolean          | pv_password text    | func
(1 row)

</pre></div>


<p class="wp-block-paragraph">To test this function with pgTAP:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
hybrid=# SELECT plan(4);
 plan
------
 1..4
(1 row)

hybrid=# SELECT ok(training_app.f_password_verify(&#039;ValidPass1!&#039;), &#039;Valid password should return true&#039;);
                    ok
------------------------------------------
 ok 1 - Valid password should return true
(1 row)

hybrid=# SELECT ok(NOT training_app.f_password_verify(&#039;short1!&#039;), &#039;Too short password should return false&#039;);
                      ok
-----------------------------------------------
 ok 2 - Too short password should return false
(1 row)

hybrid=# SELECT ok(NOT training_app.f_password_verify(&#039;NoNumberPass!&#039;), &#039;Password without a number should return false&#039;);
                          ok
------------------------------------------------------
 ok 3 - Password without a number should return false
(1 row)

hybrid=# SELECT ok(NOT training_app.f_password_verify(&#039;NoSpecialChar1&#039;), &#039;Password without special character should return false&#039;);
                              ok
---------------------------------------------------------------
 ok 4 - Password without special character should return false
(1 row)

hybrid=# SELECT * FROM finish();
 finish
--------
(0 rows)

</pre></div>


<h3 class="wp-block-heading" id="h-2-testing-table-constraints">2. Testing Table Constraints</h3>



<p class="wp-block-paragraph">Consider the <code>users</code> table with the following schema:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
hybrid=# \d users
                         Table &quot;training_app.users&quot;
  Column  |   Type    | Collation | Nullable |           Default
----------+-----------+-----------+----------+------------------------------
 id       | integer   |           | not null | generated always as identity
 username | text      |           | not null |
 password | text      |           | not null |
 created  | date      |           |          | now()
 validity | tstzrange |           |          |
Indexes:
    &quot;users_pkey&quot; PRIMARY KEY, btree (id)
    &quot;i_username_trgm&quot; gin (username gin_trgm_ops)
    &quot;i_users_username&quot; btree (username)
    &quot;i_users_username_btree_partial&quot; btree (created) WHERE created &gt;= &#039;2024-11-07&#039;::date AND created &lt; &#039;2024-11-08&#039;::date
Check constraints:
    &quot;user_check_username&quot; CHECK (username ~* &#039;&#x5B;A-Z0-9._%+-]+@&#x5B;A-Z0-9.-]+\.&#x5B;A-Z]{2,4}&#039;::text)
    &quot;user_check_username_length&quot; CHECK (length(username) &lt;= 72)
Referenced by:
    TABLE &quot;user_training&quot; CONSTRAINT &quot;fk_user_training_users&quot; FOREIGN KEY (user_id) REFERENCES users(id)
    TABLE &quot;users_history&quot; CONSTRAINT &quot;fk_users_history_user_id_users_id&quot; FOREIGN KEY (user_id) REFERENCES users(id)
Policies:
    POLICY &quot;policy_current_month&quot; FOR SELECT
      TO role_app_read_only
      USING (((EXTRACT(month FROM created))::integer = (EXTRACT(month FROM now()))::integer))
Triggers:
    t_log_user_history BEFORE UPDATE ON users FOR EACH ROW EXECUTE FUNCTION tf_user_history()

create or replace function training_app.tf_user_history() returns trigger as
$$
begin
   insert into training_app.users_history ( user_id, username, password, created, validity)
                                  values ( old.id, old.username, old.password, old.created, old.validity);
   return new;
end;                            
$$ language plpgsql;

create trigger t_log_user_history
   before update on training_app.users
   for each row
   execute procedure training_app.tf_user_history();
</pre></div>


<p class="wp-block-paragraph">To test the constraints, create a test file <code>test_users_constraints.sql</code>:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
10:57:07 postgres@ws-pgdev:/u01/app/postgres/local/dmk/tests/ &#x5B;sw] touch test_users_constraints.sql
10:57:37 postgres@ws-pgdev:/u01/app/postgres/local/dmk/tests/ &#x5B;sw] cat test_users_constraints.sql
</pre></div>

<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
BEGIN;
SELECT plan(2);

-- Test CHECK constraint on username format
SELECT throws_like(
  $$INSERT INTO training_app.users (username, password) VALUES (&#039;invalid_user&#039;, &#039;Password1!&#039;)$$,
  &#039;new row for relation &quot;users&quot; violates check constraint &quot;user_check_username&quot;&#039;,
  &#039;Invalid username should fail CHECK constraint&#039;
);

-- Test CHECK constraint on username length
SELECT throws_like(
  $$INSERT INTO training_app.users (username, password) VALUES (repeat(&#039;a&#039;, 73), &#039;Password1!&#039;)$$,
  &#039;new row for relation &quot;users&quot; violates check constraint &quot;user_check_username&quot;&#039;,
  &#039;Username exceeding 72 characters should fail CHECK constraint&#039;
);

SELECT * FROM finish();
ROLLBACK;
</pre></div>


<h3 class="wp-block-heading" id="h-running-tests">Running Tests</h3>



<p class="wp-block-paragraph">You can execute pgTAP tests using <code>pg_prove</code>, a command-line tool for running TAP tests. We are now going to test it with the file we just created, <em>test_users_constraints.sql</em>.</p>



<p class="wp-block-paragraph">Run it with <code>pg_prove</code>:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
11:45:14 postgres@ws-pgdev:/u01/app/postgres/local/dmk/tests/ &#x5B;sw] pg_prove -d hybrid -U postgres -p 5432 test_users_constraints.sql
test_users_constraints.sql .. ok
All tests successful.
Files=1, Tests=2,  0 wallclock secs ( 0.05 usr  0.01 sys +  0.00 cusr  0.01 csys =  0.07 CPU)
Result: PASS
</pre></div>


<h3 class="wp-block-heading" id="h-3-testing-triggers">3. Testing Triggers</h3>



<p class="wp-block-paragraph">To verify that our trigger correctly logs changes to the <code>users</code> table, we check:</p>



<ul class="wp-block-list">
<li>That the recorded historical data correctly reflects the old values before the update.</li>



<li>That an update on <code>users</code> triggers an insert into <code>users_history</code>.</li>
</ul>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
13:43:55 postgres@ws-pgdev:/u01/app/postgres/local/dmk/tests/ &#x5B;sw] cat test_trigger.sql
BEGIN;
SELECT plan(2);

-- Insert a test user with a valid email as username
INSERT INTO training_app.users (username, password) VALUES (&#039;testuser@example.com&#039;, &#039;TestPassword123!&#039;);

-- Update the user&#039;s username (this should activate the trigger)
UPDATE training_app.users SET username = &#039;updateduser@example.com&#039; WHERE username = &#039;testuser@example.com&#039;;

-- Check if the corresponding row is added to the users_history table
SELECT ok(
    (SELECT COUNT(*) FROM training_app.users_history WHERE user_id = (SELECT id FROM training_app.users WHERE username = &#039;updateduser@example.com&#039;)) &gt; 0,
    &#039;User history should be logged in users_history after update&#039;
);

-- Check if the values in users_history match the old values (before the update)
SELECT is(
    (SELECT username FROM training_app.users_history WHERE user_id = (SELECT id FROM training_app.users WHERE username = &#039;updateduser@example.com&#039;) ORDER BY created DESC LIMIT 1),
    &#039;testuser@example.com&#039;,
    &#039;Username in user history should match the old (pre-update) value&#039;
);

SELECT * FROM finish();
ROLLBACK;
</pre></div>


<p class="wp-block-paragraph">Execute the test using:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
13:58:38 postgres@ws-pgdev:/u01/app/postgres/local/dmk/tests/ &#x5B;sw] pg_prove -d hybrid -U postgres -p 5432 test_trigger.sql
test_trigger.sql .. ok
All tests successful.
Files=1, Tests=2,  0 wallclock secs ( 0.04 usr  0.01 sys +  0.01 cusr  0.01 csys =  0.07 CPU)
Result: PASS
</pre></div>


<h2 class="wp-block-heading" id="h-benefits-of-using-pgtap">Benefits of Using pgTAP</h2>



<ul class="wp-block-list">
<li><strong>Automated Testing</strong>: Helps maintain database integrity by catching errors early.</li>



<li><strong>SQL-Based</strong>: No need for external scripting languages; tests are written in SQL.</li>



<li><strong>Integration with CI/CD</strong>: Works with CI/CD pipelines to ensure database quality.</li>



<li><strong>Comprehensive Assertions</strong>: Supports functions, constraints, indexes, views, and more.</li>
</ul>



<h2 class="wp-block-heading" id="h-conclusion">Conclusion</h2>



<p class="wp-block-paragraph">pgTAP is a powerful tool for unit testing PostgreSQL databases. By incorporating it into your workflow, you can ensure that your database logic remains robust and reliable over time. Whether you&#8217;re testing functions, constraints, or triggers, pgTAP provides a structured and repeatable approach to database testing. You can find more information about pgTAP on the <a href="https://pgtap.org/">official website</a>.<br>Do you use pgTAP in your projects? Let me know in the comments how it has helped you!</p>
<p>L’article <a href="https://www.dbi-services.com/blog/unit-testing-postgresql-with-pgtap/">Unit Testing PostgreSQL with pgTAP</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.dbi-services.com/blog/unit-testing-postgresql-with-pgtap/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>How to add Column Filtering to Vuetify Data Table</title>
		<link>https://www.dbi-services.com/blog/how-to-add-column-filtering-to-vuetify-data-table/</link>
					<comments>https://www.dbi-services.com/blog/how-to-add-column-filtering-to-vuetify-data-table/#respond</comments>
		
		<dc:creator><![CDATA[Joan Frey]]></dc:creator>
		<pubDate>Mon, 25 Aug 2025 19:22:27 +0000</pubDate>
				<category><![CDATA[Web]]></category>
		<category><![CDATA[YaK]]></category>
		<category><![CDATA[vue]]></category>
		<category><![CDATA[vuejs]]></category>
		<category><![CDATA[vuetify]]></category>
		<category><![CDATA[yak]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=38050</guid>

					<description><![CDATA[<p>For a long time, my v-data-table-server implementation in the YaK only supported global search through a simple text input. While it was good enough for basic queries, I needed something more powerful—the ability to filter by individual columns using custom operators. Here’s a walkthrough of how I added column filters with v-select and v-text-field embedded [&#8230;]</p>
<p>L’article <a href="https://www.dbi-services.com/blog/how-to-add-column-filtering-to-vuetify-data-table/">How to add Column Filtering to Vuetify Data Table</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">For a long time, my v-data-table-server implementation in the <a href="https://yak4all.io/">YaK </a>only supported global search through a simple text input. While it was good enough for basic queries, I needed something more powerful—the ability to filter by individual columns using custom operators.</p>



<p class="wp-block-paragraph">Here’s a walkthrough of how I added column filters with v-select and v-text-field embedded in v-menu components right inside the table headers.</p>



<h2 class="wp-block-heading" id="h-the-problem">The Problem</h2>



<p class="wp-block-paragraph">My users wanted to search not just by a global term but with more granularity—for example:</p>



<ul class="wp-block-list">
<li>Show only items where provider contains “aws”</li>



<li>Filter out items with a specific state</li>



<li>View all entries with a name that contains “master”</li>
</ul>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="468" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/04/image-34-1024x468.png" alt="" class="wp-image-38053" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/04/image-34-1024x468.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/04/image-34-300x137.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/04/image-34-768x351.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/04/image-34-1536x702.png 1536w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/04/image-34.png 1761w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">The default Vuetify Data Table doesn’t support per-column filtering out of the box, so I needed to get creative, even if you can override the default filtering used with the <strong><a href="https://vuetifyjs.com/en/components/data-tables/data-and-display/#custom-filter">search</a></strong> prop.</p>



<h2 class="wp-block-heading" id="h-the-approach">The Approach</h2>



<ul class="wp-block-list">
<li><code>Attach filter UI elements to each column header</code></li>



<li><code>Bind filters to a filters object keyed by column title</code></li>



<li><code>Apply the filters to the data using a computed property</code></li>



<li><code>Change the icon and icon color on the header to indicate active filters</code></li>
</ul>



<h2 class="wp-block-heading" id="h-i-injecting-the-filter-ui-into-headers">I. Injecting the Filter UI into Headers</h2>



<p class="wp-block-paragraph">In the v-slot:[header.key], I added a v-menu that displays a filter operator dropdown and a text input:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: xml; title: ; notranslate">
&lt;template
  v-for=&quot;header in dataHeaders&quot;
  v-slot:&#x5B;header.${header.key}]=&quot;{ column }&quot;
  :key=&quot;header.key&quot;
&gt;
  {{ column.title }}
  &lt;v-menu :close-on-content-click=&quot;false&quot;&gt;
    &lt;template v-slot:activator=&quot;{ props }&quot;&gt;
      &lt;v-btn icon v-bind=&quot;props&quot; color=&quot;transparent&quot;&gt;
        &lt;v-icon v-if=&quot;filters&#x5B;column.title].value&quot;&gt;mdi-filter&lt;/v-icon&gt;
        &lt;v-icon v-else&gt;mdi-filter-outline&lt;/v-icon&gt;
      &lt;/v-btn&gt;
    &lt;/template&gt;
    &lt;div class=&quot;filter-menu&quot;&gt;
      &lt;v-select
        v-model=&quot;filters&#x5B;column.title].operator&quot;
        :items=&quot;&#x5B;&#039;=&#039;, &#039;!=&#039;]&quot;
        label=&quot;Operator&quot;
        variant=&quot;outlined&quot;
        density=&quot;compact&quot;
      &gt;&lt;/v-select&gt;
      &lt;v-text-field
        v-model=&quot;filters&#x5B;column.title].value&quot;
        label=&quot;Search Term&quot;
        variant=&quot;outlined&quot;
        clearable
        density=&quot;compact&quot;
      &gt;&lt;/v-text-field&gt;
    &lt;/div&gt;
  &lt;/v-menu&gt;
&lt;/template&gt;
</pre></div>


<p class="wp-block-paragraph">Each filter’s state is stored in a reactive object:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
const filters = ref&lt;Record&lt;string, { value: string; operator: string }&gt;&gt;({});
</pre></div>


<h2 class="wp-block-heading" id="h-ii-initializing-filters-for-each-column">II. Initializing Filters for Each Column</h2>



<p class="wp-block-paragraph">Once I loaded the table headers, I made sure to initialize the corresponding filters:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
dataHeaders.value.forEach((header) =&gt; {
    if (!filters.value&#x5B;header.title]) {
        filters.value&#x5B;header.title] = { value: &quot;&quot;, operator: &quot;=&quot; };
     }
});
</pre></div>


<p class="wp-block-paragraph">This ensured every column could be filtered independently and to avoid errors related to empty/null data.</p>



<h2 class="wp-block-heading" id="h-iii-applying-the-filters-to-the-data">III. Applying the Filters to the Data</h2>



<p class="wp-block-paragraph">I used a computed property to transform the data on the fly:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
const filteredData = computed(() =&gt; {
  return recordData.value.filter((item) =&gt; {
    return Object.entries(filters.value).every((&#x5B;filterKey, filter]) =&gt; {
      if (!filter.value) return true;

      const matchingKey = Object.keys(item).find((key) =&gt; {
        const normalizedFilterKey = filterKey.toLowerCase();
        return key.toLowerCase() === normalizedFilterKey ||
               key.toLowerCase().endsWith(&quot;name&quot;) &amp;&amp;
               key.toLowerCase().startsWith(normalizedFilterKey);
      });

      if (!matchingKey) return true;

      const itemValue = String(item&#x5B;matchingKey] || &quot;&quot;).toLowerCase();
      const filterValue = filter.value.toLowerCase();

      return filter.operator === &quot;=&quot;
        ? itemValue.includes(filterValue)
        : !itemValue.includes(filterValue);
    });
  });
});
</pre></div>


<p class="wp-block-paragraph">This filtering happens client-side after the data is fetched.</p>



<h2 class="wp-block-heading" id="h-iv-adding-ux-details">IV. Adding UX Details</h2>



<p class="wp-block-paragraph">A few extra things I did for better UX:</p>



<ul class="wp-block-list">
<li>Colored icons: A regular filter icon shows when a filter is active.</li>



<li>Clear filters button (optional): Could be added for each column.</li>



<li>Auto-focus: Focuses on the text field when the menu opens.</li>
</ul>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: xml; title: ; notranslate">
&lt;v-data-table-server
    v-model:items-per-page=&quot;itemsPerPage&quot;
    :items-per-page-options=&quot;itemsPerPageOptions&quot;
    :max-height=&quot;&#039;50vh&#039;&quot;
    fixed-header
    v-model=&quot;selected&quot;
    class=&quot;rounded-lg&quot;
    :headers=&quot;dataHeaders&quot;
    :items-length=&quot;recordLength&quot;
    :items=&quot;filteredData&quot;
    :loading=&quot;data.loading&quot;
    :search=&quot;search&quot;
    :select-strategy=&quot;&#039;all&#039;&quot;
    @update:options=&quot;loadRecord&quot;
    @click:row=&quot;onRowClick&quot;
    show-select
  &gt;
    &lt;template
      v-for=&quot;dataKey in dataHeaders&quot;
      v-slot:&#x5B;`item.${dataKey.key}`]=&quot;{ item }&quot;
    &gt;
      &lt;slot
        v-if=&quot;item&quot;
        :name=&quot;dataKey.key&quot;
        :value=&quot;item&#x5B;dataKey.key]&quot;
        :record=&quot;item&quot;
        &gt;{{ item&#x5B;dataKey.key] }}&lt;/slot
      &gt;
      &lt;span :key=&quot;`${dataKey.key}-placeholder`&quot; v-else&gt;-&lt;/span&gt;
    &lt;/template&gt;

    &lt;template
      v-for=&quot;header in dataHeaders&quot;
      v-slot:&#x5B;`header.${header.key}`]=&quot;{ column }&quot;
      :key=&quot;header.key&quot;
    &gt;
      {{ column.title }}
      &lt;v-menu :close-on-content-click=&quot;false&quot;&gt;
        &lt;template v-slot:activator=&quot;{ props }&quot;&gt;
          {{ filters&#x5B;header] }}
          &lt;v-btn
            icon
            v-bind=&quot;props&quot;
            color=&quot;rgba(255, 0, 0, 0.0)&quot;
            style=&quot;box-shadow: none&quot;
          &gt;
            &lt;v-icon v-if=&quot;filters&#x5B;column!.title!].value == &#039;&#039; ||filters&#x5B;column!.title!].value == null&quot; color=&quot;white&quot; size=&quot;small&quot;&gt;mdi-filter-outline&lt;/v-icon&gt;
            &lt;v-icon v-else color=&quot;orange&quot; size=&quot;small&quot;&gt;mdi-filter&lt;/v-icon&gt;
          &lt;/v-btn&gt;
        &lt;/template&gt;
        &lt;div style=&quot;background-color: white; width: 200px; border-radius: 10px;&quot;&gt;
          &lt;v-select
            v-model=&quot;filters&#x5B;column.title!].operator&quot;
            :items=&quot;&#x5B;&#039;=&#039;, &#039;!=&#039;]&quot;
            label=&quot;Select operator&quot;
            class=&quot;pt-4 pl-4 pr-4&quot;
            variant=&quot;outlined&quot;
            density=&quot;compact&quot;
          &gt;&lt;/v-select&gt;
          &lt;v-text-field
            v-model=&quot;filters&#x5B;column.title!].value&quot;
            class=&quot;pl-4 pr-4&quot;
            type=&quot;text&quot;
            label=&quot;Enter the search term&quot;
            :autofocus=&quot;true&quot;
            variant=&quot;outlined&quot;
            clearable
            density=&quot;compact&quot;
          &gt;&lt;/v-text-field&gt;
        &lt;/div&gt;
      &lt;/v-menu&gt;
    &lt;/template&gt;
  &lt;/v-data-table-server&gt;
</pre></div>


<h2 class="wp-block-heading" id="h-what-s-next">What’s Next?</h2>



<p class="wp-block-paragraph">After adding this new feature, the sort indicator next to each column header disappeared—although sorting still works as expected. I haven’t had time to look into it yet, but I’d like to find a solution. If you happen to figure it out, feel free to share it in the comments!</p>



<h2 class="wp-block-heading" id="h-where-to-find-the-complete-code">Where to find the complete code ?</h2>



<p class="wp-block-paragraph">You can check out the complete and latest code in the open-source project on GitLab:<br>👉 <a class="" href="https://gitlab.com/yak4all/yak_frontend/yak_ui">https://gitlab.com/yak4all/yak_frontend/yak_ui</a><br>The relevant logic lives in the <code>YakGrid.vue</code> component.</p>



<h2 class="wp-block-heading" id="h-conclusion">Conclusion</h2>



<p class="wp-block-paragraph">By embedding filter controls directly into column headers and managing state reactively, I was able to turn a basic Vuetify data table into a much more powerful data exploration tool.</p>



<p class="wp-block-paragraph">Let me know if you try this pattern or come up with improvements—I’m always up for iterating! You can also suggest changes or contribute directly on the GitLab repo of the Yak project.</p>
<p>L’article <a href="https://www.dbi-services.com/blog/how-to-add-column-filtering-to-vuetify-data-table/">How to add Column Filtering to Vuetify Data Table</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.dbi-services.com/blog/how-to-add-column-filtering-to-vuetify-data-table/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Exploring FlyonUI with Vue.js: A Fresh and Evolving Library</title>
		<link>https://www.dbi-services.com/blog/exploring-flyonui-with-vue-js-a-fresh-and-evolving-library/</link>
					<comments>https://www.dbi-services.com/blog/exploring-flyonui-with-vue-js-a-fresh-and-evolving-library/#respond</comments>
		
		<dc:creator><![CDATA[Joan Frey]]></dc:creator>
		<pubDate>Thu, 31 Oct 2024 16:13:43 +0000</pubDate>
				<category><![CDATA[Web]]></category>
		<category><![CDATA[CSS]]></category>
		<category><![CDATA[FlyonUI]]></category>
		<category><![CDATA[tailwind]]></category>
		<category><![CDATA[vue]]></category>
		<category><![CDATA[vuejs]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=35551</guid>

					<description><![CDATA[<p>As someone who has relied on Vuetify for most of my Vue.js projects, I&#8217;m always on the lookout for new libraries that can streamline development and keep designs modern. Recently, a new UI library called FlyonUI was released, and I decided to give it a spin. FlyonUI is a Tailwind CSS Components Library. To my [&#8230;]</p>
<p>L’article <a href="https://www.dbi-services.com/blog/exploring-flyonui-with-vue-js-a-fresh-and-evolving-library/">Exploring FlyonUI with Vue.js: A Fresh and Evolving Library</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">As someone who has relied on <strong>Vuetify</strong> for most of my Vue.js projects, I&#8217;m always on the lookout for new libraries that can streamline development and keep designs modern. Recently, a new UI library called <strong>FlyonUI</strong> was released, and I decided to give it a spin. FlyonUI is a Tailwind CSS Components Library. To my surprise, it turned out to be not only lightweight but also incredibly easy to set up and customize. In this post, I’ll walk you through the basics of integrating FlyonUI with Vue.js.</p>



<h2 class="wp-block-heading" id="h-why-i-m-interested-about-flyonui">Why I’m Interested About FlyonUI</h2>



<p class="wp-block-paragraph">Released just a few weeks ago, <strong>FlyonUI</strong> promises a fresh approach to Vue UI development. FlyonUI is an <strong>open-source</strong> (I love open-source projects) Tailwind CSS Components Library with semantic classes and powerful JS plugins. It’s designed to be intuitive, flexible and to have universal framework compatibility (Vuejs, React, Angular, &#8230;). I like their website and the documentation seems very complete, not even mentioning the fact that there is already more than 78 available components and 800 examples.</p>



<h2 class="wp-block-heading" id="h-prerequisites">Prerequisites</h2>



<ul class="wp-block-list">
<li>A basic understanding of Vue.js</li>



<li><strong>Node.js</strong> and <strong>npm</strong> installed</li>



<li>Vue.js running application set up with Tailwind CSS (<a href="https://tailwindcss.com/docs/guides/vite#vue">https://tailwindcss.com/docs/guides/vite#vue</a>)</li>
</ul>



<h2 class="wp-block-heading" id="h-i-install-flyonui">I. Install FlyonUI</h2>



<p class="wp-block-paragraph">I&#8217;m going to use the application I created for my previous blog, which you can find here: <a href="https://www.dbi-services.com/blog/vue-creating-an-awesome-parallax-effect-with-swiper/">https://www.dbi-services.com/blog/vue-creating-an-awesome-parallax-effect-with-swiper/</a>. To add FlyonUI, open your terminal in the project’s root folder and run:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
npm install flyon-ui
</pre></div>


<h2 class="wp-block-heading" id="h-ii-configure-flyonui-in-your-vue-app">II. Configure FlyonUI in Your Vue App</h2>



<p class="wp-block-paragraph">Now that FlyonUI is installed, let’s import it to make its components available across the app. Open <code>tailwind.config.js</code> and edit the file with the following code:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
/** @type {import(&#039;tailwindcss&#039;).Config} */
export default {
  content: &#x5B;
    &quot;./index.html&quot;,
    &quot;./src/**/*.{vue,js,ts,jsx,tsx}&quot;,
    &quot;./node_modules/flyonui/dist/js/*.js&quot;,
  ],
  theme: {
    extend: {},
  },
  plugins: &#x5B;
    require(&#039;flyonui&#039;),
    require(&#039;flyonui/plugin&#039;)
  ],
}
</pre></div>


<p class="wp-block-paragraph">Open <code>main.ts</code> and add the following code:</p>



<pre class="wp-block-code"><code>import "flyonui/flyonui";</code></pre>



<h2 class="wp-block-heading" id="h-iii-add-a-reinitialization-helper">III. Add a reinitialization helper </h2>



<p class="wp-block-paragraph">Add code in your route file (.<code>/src/router/index.ts</code>) to reinitialize components each time the page is refreshed.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
import { createRouter, createWebHistory } from &#039;vue-router&#039;
import HomeView from &#039;../views/HomeView.vue&#039;

import { type IStaticMethods } from &quot;flyonui/flyonui&quot;;
declare global {
  interface Window {
    HSStaticMethods: IStaticMethods;
  }
}

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: &#x5B;
    {
      path: &#039;/&#039;,
      name: &#039;home&#039;,
      component: HomeView
    },
    {
      path: &#039;/about&#039;,
      name: &#039;about&#039;,
      // route level code-splitting
      // this generates a separate chunk (About.&#x5B;hash].js) for this route
      // which is lazy-loaded when the route is visited.
      component: () =&gt; import(&#039;../views/AboutView.vue&#039;)
    }
  ]
})

router.afterEach((to, from, failure) =&gt; {
  if (!failure) {
    setTimeout(() =&gt; {
      window.HSStaticMethods.autoInit();
    }, 100)
  }
});

export default router
</pre></div>


<h2 class="wp-block-heading" id="h-iv-using-flyonui-components">IV. Using FlyonUI Components</h2>



<p class="wp-block-paragraph">FlyonUI components are simple and straightforward, so let’s jump into using a few popular ones.</p>



<h3 class="wp-block-heading" id="h-example-1-button">Example 1: Button</h3>



<p class="wp-block-paragraph">Now you can use FlyonUI components throughout your Vue application. Here’s an example of using a FlyonUI responsive button:</p>



<p class="wp-block-paragraph">&lt;button class=&#8221;btn btn-primary max-sm:btn-sm lg:btn-lg&#8221;&gt;Responsive&lt;/button&gt;</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="1024" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2024/10/image-90-1024x1024.png" alt="" class="wp-image-35568" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2024/10/image-90-1024x1024.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2024/10/image-90-300x300.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2024/10/image-90-150x150.png 150w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2024/10/image-90-768x767.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2024/10/image-90.png 1274w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<h3 class="wp-block-heading" id="h-example-2-add-a-card-component">Example 2: Add a card component</h3>



<p class="wp-block-paragraph">We’ll make it simple and place the card structure below the title of our swiper slide. You can add this code to your component:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
        &lt;div class=&quot;card group hover:shadow sm:max-w-sm&quot;&gt;
          &lt;figure&gt;
            &lt;img
              src=&quot;https://cdn.flyonui.com/fy-assets/components/card/image-8.png&quot;
              alt=&quot;Shoes&quot;
              class=&quot;transition-transform duration-500 group-hover:scale-110&quot;
            /&gt;
          &lt;/figure&gt;
          &lt;div class=&quot;card-body&quot;&gt;
            &lt;h5 class=&quot;card-title mb-2.5&quot;&gt;Card title&lt;/h5&gt;
            &lt;p class=&quot;mb-6&quot;&gt;
              Nike Air Max is a popular line of athletic shoes that feature
              Nike&#039;s signature Air cushioning technology in the sole.
            &lt;/p&gt;
            &lt;div class=&quot;card-actions&quot;&gt;
              &lt;button class=&quot;btn btn-primary&quot;&gt;Buy Now&lt;/button&gt;
              &lt;button class=&quot;btn btn-secondary btn-soft&quot;&gt;Add to cart&lt;/button&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;
</pre></div>


<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="1016" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2024/10/image-91-1024x1016.png" alt="" class="wp-image-35570" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2024/10/image-91-1024x1016.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2024/10/image-91-300x298.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2024/10/image-91-150x150.png 150w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2024/10/image-91-768x762.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2024/10/image-91.png 1278w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<h3 class="wp-block-heading" id="h-example-3-use-radio-buttons-to-control-css-theme">Example 3: Use radio buttons to control CSS theme</h3>



<p class="wp-block-paragraph">FlyonUI offers a range of pre-built themes that make it easy to customize the look and feel of your app. Each theme provides a cohesive color scheme applied across all FlyonUI components. To use a theme, simply add its name to your <code>tailwind.config.js</code>.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
/** @type {import(&#039;tailwindcss&#039;).Config} */
export default {
  content: &#x5B;
    &quot;./index.html&quot;,
    &quot;./src/**/*.{vue,js,ts,jsx,tsx}&quot;,
    &quot;./node_modules/flyonui/dist/js/*.js&quot;,
  ],
  theme: {
    extend: {},
  },
  flyonui: {
    themes: &#x5B;&quot;light&quot;, &quot;dark&quot;, &quot;gourmet&quot;, &quot;corporate&quot;, &quot;luxury&quot;, &quot;soft&quot;]
  },
  plugins: &#x5B;
    require(&#039;flyonui&#039;),
    require(&#039;flyonui/plugin&#039;)
  ],
}
</pre></div>


<p class="wp-block-paragraph">Now let&#8217;s add the radio buttons onto our slide and verify that the theme of our card is changing:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
        &lt;div class=&quot;join join-horizontal pb-3&quot;&gt;
          &lt;input
            type=&quot;radio&quot;
            name=&quot;theme-buttons&quot;
            class=&quot;btn theme-controller join-item&quot;
            aria-label=&quot;Default&quot;
            value=&quot;default&quot;
            checked
          /&gt;
          &lt;input
            type=&quot;radio&quot;
            name=&quot;theme-buttons&quot;
            class=&quot;btn theme-controller join-item&quot;
            aria-label=&quot;Corporate&quot;
            value=&quot;corporate&quot;
          /&gt;
          &lt;input
            type=&quot;radio&quot;
            name=&quot;theme-buttons&quot;
            class=&quot;btn theme-controller join-item&quot;
            aria-label=&quot;Gourmet&quot;
            value=&quot;gourmet&quot;
          /&gt;
        &lt;/div&gt;
</pre></div>


<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="1020" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2024/10/image-92-1024x1020.png" alt="" class="wp-image-35574" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2024/10/image-92-1024x1020.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2024/10/image-92-300x300.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2024/10/image-92-150x150.png 150w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2024/10/image-92-768x765.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2024/10/image-92.png 1275w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">Well, without big surprise, this is working as expected, simple and easy to use. I like how the Gourmet theme is even applying rounded class to our buttons.</p>



<h2 class="wp-block-heading" id="h-v-advanced-features">V. Advanced Features</h2>



<p class="wp-block-paragraph">FlyonUI also offers advanced components, such as tables, charts, and timeline, to build complex interfaces quickly. Refer to the FlyonUI documentation for detailed customization options, methods, and event handling for each component.</p>



<h2 class="wp-block-heading" id="h-conclusion">Conclusion</h2>



<p class="wp-block-paragraph">FlyonUI seems like an excellent library for Vue.js developers looking to create modern, responsive, and interactive UIs without a significant performance hit. In this guide, we’ve covered the basics of integrating FlyonUI into your Vue project and using several key components. </p>



<p class="wp-block-paragraph">Still, FlyonUI is a fresh and evolving library, and it&#8217;s clear that the team is still working to expand its capabilities. As you may notice on the official website, some sections are marked as &#8220;coming soon.&#8221; This means there&#8217;s more to look forward to as the library grows and introduces new features. So, stay tuned for updates, enjoy exploring the current components, a big thanks to the developers and have fun testing out FlyonUI!</p>
<p>L’article <a href="https://www.dbi-services.com/blog/exploring-flyonui-with-vue-js-a-fresh-and-evolving-library/">Exploring FlyonUI with Vue.js: A Fresh and Evolving Library</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.dbi-services.com/blog/exploring-flyonui-with-vue-js-a-fresh-and-evolving-library/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Vue &#8211; Creating an Awesome Parallax Effect with Swiper</title>
		<link>https://www.dbi-services.com/blog/vue-creating-an-awesome-parallax-effect-with-swiper/</link>
					<comments>https://www.dbi-services.com/blog/vue-creating-an-awesome-parallax-effect-with-swiper/#respond</comments>
		
		<dc:creator><![CDATA[Joan Frey]]></dc:creator>
		<pubDate>Thu, 31 Oct 2024 07:06:31 +0000</pubDate>
				<category><![CDATA[Web]]></category>
		<category><![CDATA[vue]]></category>
		<category><![CDATA[vue-awesome-swiper]]></category>
		<category><![CDATA[vuejs]]></category>
		<category><![CDATA[vuetify]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=35481</guid>

					<description><![CDATA[<p>Adding a parallax effect to your website brings a smooth, professional feel, making it more interactive and visually appealing. With Swiper for Vue, you can easily implement parallax effects for a slideshow. In this guide, we’ll create a parallax effect using Vue 3 and explore additional features to enhance this effect. I. Create a New [&#8230;]</p>
<p>L’article <a href="https://www.dbi-services.com/blog/vue-creating-an-awesome-parallax-effect-with-swiper/">Vue &#8211; Creating an Awesome Parallax Effect with Swiper</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Adding a parallax effect to your website brings a smooth, professional feel, making it more interactive and visually appealing. With <strong>Swiper</strong> for Vue, you can easily implement parallax effects for a slideshow. In this guide, we’ll create a parallax effect using Vue 3 and explore additional features to enhance this effect.</p>



<h2 class="wp-block-heading" id="h-i-create-a-new-vue-3-project">I. Create a New Vue 3 Project</h2>



<p class="wp-block-paragraph">Let&#8217;s start from scratch to create a Vue 3 app and set up <strong>Swiper</strong> to create a parallax slider. Here’s a step-by-step guide to set up the environment and build an awesome parallax effect.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
blogger@PC MINGW64 ~/Documents/Blogs/parallax-vue-awesome
$ npm init vue@latest my-parallax-app
Need to install the following packages:
  create-vue@3.11.2
Ok to proceed? (y) y

Vue.js - The Progressive JavaScript Framework

√ Add TypeScript? ... No / Yes
√ Add JSX Support? ... No / Yes
√ Add Vue Router for Single Page Application development? ... No / Yes
√ Add Pinia for state management? ... No / Yes
√ Add Vitest for Unit Testing? ... No / Yes
√ Add an End-to-End Testing Solution? » No
√ Add ESLint for code quality? ... No / Yes
√ Add Prettier for code formatting? ... No / Yes
√ Add Vue DevTools 7 extension for debugging? (experimental) ... No / Yes

Scaffolding project in C:\Users\blogger\Documents\Blogs\parallax-vue-awesome\my-parallax-app...

Done. Now run:

  cd my-parallax-app
  npm install
  npm run format
  npm run dev
</pre></div>


<h2 class="wp-block-heading" id="h-ii-install-vue-swiper">II. Install Vue Swiper</h2>



<p class="wp-block-paragraph">Next, install <strong>Swiper</strong>:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
npm install swiper
</pre></div>


<h2 class="wp-block-heading" id="h-iii-set-up-the-project-structure">III. Set Up the Project Structure</h2>



<p class="wp-block-paragraph">Inside the project, let’s create a new component for our parallax slider. We’ll place this in the <code>components</code> folder for organization.</p>



<p class="wp-block-paragraph">Create a file named <code>AwesomeParallax.vue</code> in the <code>src/components</code> folder:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
src
└── components
    └── ParallaxSlider.vue
</pre></div>


<p class="wp-block-paragraph">I also added the pictures that I&#8217;ll be using for the Parallax into the <code>assets</code> directory (Pictures are mine 📷). In the end, you should have at least the following structure:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
src
├── assets
│   ├── parallax1.jpg
├── components
│   └── AwesomeParallax.vue
├── views
│   └── HomeView.vue
└── App.vue
</pre></div>


<h2 class="wp-block-heading" id="h-iv-create-the-parallax-slider-component">IV. Create the Parallax Slider Component</h2>



<p class="wp-block-paragraph">Add the following code to Awesome<code>Parallax.vue</code>. This component will import the required Swiper modules for navigation, pagination, and the parallax effect. You can find the official documentation from Swiper here: <a href="https://swiperjs.com/demos#parallax" target="_blank" rel="noreferrer noopener">https://swiperjs.com/demos#parallax</a>.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
&lt;template&gt;
  &lt;swiper
    :style=&quot;{
      &#039;--swiper-navigation-color&#039;: &#039;#fff&#039;,
      &#039;--swiper-pagination-color&#039;: &#039;#fff&#039;,
    }&quot;
    :speed=&quot;600&quot;
    :parallax=&quot;true&quot;
    :pagination=&quot;{
      clickable: true,
    }&quot;
    :navigation=&quot;true&quot;
    :modules=&quot;modules&quot;
    class=&quot;mySwiper&quot;
    data-swiper-parallax-x
  &gt;
    &lt;template v-slot:container-start&gt;
      &lt;div class=&quot;parallax-bg&quot; data-swiper-parallax=&quot;-23%&quot;&gt;&lt;/div&gt;
    &lt;/template&gt;
    &lt;swiper-slide&gt;
      &lt;div class=&quot;title&quot; data-swiper-parallax=&quot;-300&quot;&gt;Slide 1&lt;/div&gt;
      &lt;div class=&quot;subtitle&quot; data-swiper-parallax=&quot;-200&quot;&gt;Subtitle&lt;/div&gt;
      &lt;div class=&quot;text&quot; data-swiper-parallax=&quot;-100&quot;&gt;
        &lt;p&gt;
          Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam
          dictum mattis velit, sit amet faucibus felis iaculis nec. Nulla
          laoreet justo vitae porttitor porttitor. Suspendisse in sem justo.
          Integer laoreet magna nec elit suscipit, ac laoreet nibh euismod.
          Aliquam hendrerit lorem at elit facilisis rutrum. Ut at ullamcorper
          velit. Nulla ligula nisi, imperdiet ut lacinia nec, tincidunt ut
          libero. Aenean feugiat non eros quis feugiat.
        &lt;/p&gt;
      &lt;/div&gt; &lt;/swiper-slide
    &gt;&lt;swiper-slide&gt;
      &lt;div class=&quot;title&quot; data-swiper-parallax=&quot;-300&quot;&gt;Slide 2&lt;/div&gt;
      &lt;div class=&quot;subtitle&quot; data-swiper-parallax=&quot;-200&quot;&gt;Subtitle&lt;/div&gt;
      &lt;div class=&quot;text&quot; data-swiper-parallax=&quot;-100&quot;&gt;
        &lt;p&gt;
          Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam
          dictum mattis velit, sit amet faucibus felis iaculis nec. Nulla
          laoreet justo vitae porttitor porttitor. Suspendisse in sem justo.
          Integer laoreet magna nec elit suscipit, ac laoreet nibh euismod.
          Aliquam hendrerit lorem at elit facilisis rutrum. Ut at ullamcorper
          velit. Nulla ligula nisi, imperdiet ut lacinia nec, tincidunt ut
          libero. Aenean feugiat non eros quis feugiat.
        &lt;/p&gt;
      &lt;/div&gt; &lt;/swiper-slide
    &gt;&lt;swiper-slide&gt;
      &lt;div class=&quot;title&quot; data-swiper-parallax=&quot;-300&quot;&gt;Slide 3&lt;/div&gt;
      &lt;div class=&quot;subtitle&quot; data-swiper-parallax=&quot;-200&quot;&gt;Subtitle&lt;/div&gt;
      &lt;div class=&quot;text&quot; data-swiper-parallax=&quot;-100&quot;&gt;
        &lt;p&gt;
          Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam
          dictum mattis velit, sit amet faucibus felis iaculis nec. Nulla
          laoreet justo vitae porttitor porttitor. Suspendisse in sem justo.
          Integer laoreet magna nec elit suscipit, ac laoreet nibh euismod.
          Aliquam hendrerit lorem at elit facilisis rutrum. Ut at ullamcorper
          velit. Nulla ligula nisi, imperdiet ut lacinia nec, tincidunt ut
          libero. Aenean feugiat non eros quis feugiat.
        &lt;/p&gt;
      &lt;/div&gt;
    &lt;/swiper-slide&gt;
  &lt;/swiper&gt;
&lt;/template&gt;
&lt;script lang=&quot;ts&quot;&gt;
// Import Swiper Vue.js components
import { Swiper, SwiperSlide } from &#039;swiper/vue&#039;

// Import Swiper styles
import &#039;swiper/css&#039;

import &#039;swiper/css/pagination&#039;
import &#039;swiper/css/navigation&#039;

// import required modules
import { Parallax, Pagination, Navigation } from &#039;swiper/modules&#039;

export default {
  components: {
    Swiper,
    SwiperSlide,
  },
  setup() {
    return {
      modules: &#x5B;Parallax, Pagination, Navigation],
    }
  },
}
&lt;/script&gt;

&lt;style&gt;
#app {
  height: 100%;
  display: contents;
}
html,
body {
  position: relative;
  height: 100%;
}

body {
  background: #eee;
  font-family:
    Helvetica Neue,
    Helvetica,
    Arial,
    sans-serif;
  font-size: 14px;
  color: #000;
  margin: 0;
  padding: 0;
}

.swiper {
  width: 100%;
  height: 100%;
  background: #000;
}

.swiper-slide {
  font-size: 18px;
  color: #fff;
  -webkit-box-sizing: border-box;
  box-sizing: border-box;
  padding: 40px 60px;
}

.parallax-bg {
  position: absolute;
  left: 0;
  top: 0;
  width: 130%;
  height: 100%;
  -webkit-background-size: cover;
  background-size: cover;
  background-position: center;
  background-image: url(&#039;../assets/parallax1.jpg&#039;);
}

.swiper-slide .title {
  font-size: 41px;
  font-weight: 300;
}

.swiper-slide .subtitle {
  font-size: 21px;
}

.swiper-slide .text {
  font-size: 14px;
  max-width: 400px;
  line-height: 1.3;
}
&lt;/style&gt;
</pre></div>


<p class="wp-block-paragraph">To achieve a full-screen parallax effect, I used <code>display: contents;</code> on the <code>#app</code> element. This allows the background to stretch across the viewport without being constrained by the parent element&#8217;s dimensions.</p>



<p class="wp-block-paragraph">However, setting the <code>.parallax-bg</code> width to <code>130%</code> can cause height issues, as the image might not scale proportionately, leading to excess height. Still, this is needed to achieve the parallax effect in our case, so it means that you need to choose your background image knowing that it is going to be cropped.</p>



<h2 class="wp-block-heading" id="h-v-use-the-component-in-homeview-vue">V. Use the Component in HomeView.vue</h2>



<p class="wp-block-paragraph">Now that we’ve created the Awesome<code>Parallax</code> component, we can add it to <code>HomeView.vue</code> to display it on the main page.</p>



<p class="wp-block-paragraph">Open <code>src/HomeView.vue</code> and replace its content with the following:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
&lt;template&gt;
    &lt;AwesomeParallax /&gt;
&lt;/template&gt;

&lt;script setup lang=&quot;ts&quot;&gt;
import AwesomeParallax from &#039;@/components/AwesomeParallax.vue&#039;
&lt;/script&gt;
</pre></div>


<p class="wp-block-paragraph">Then open <code>src/App.vue</code> and replace its content with the following:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
&lt;template&gt;
  &lt;HomeView /&gt;
&lt;/template&gt;

&lt;script setup lang=&quot;ts&quot;&gt;
import HomeView from &#039;@/views/HomeView.vue&#039;
&lt;/script&gt;
</pre></div>


<h2 class="wp-block-heading" id="h-vi-run-the-application">VI. Run the application</h2>



<p class="wp-block-paragraph">With everything in place, you can start the development server to see the parallax effect in action:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
npm run dev
</pre></div>


<p class="wp-block-paragraph">Navigate to <code>http://localhost:3000</code> in your browser to see your parallax slider. You should see a smoothly animated slider with a parallax background and Swiper&#8217;s navigation and pagination controls.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="482" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2024/10/image-88-1024x482.png" alt="" class="wp-image-35504" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2024/10/image-88-1024x482.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2024/10/image-88-300x141.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2024/10/image-88-768x361.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2024/10/image-88-1536x723.png 1536w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2024/10/image-88.png 1917w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<h2 class="wp-block-heading" id="h-vii-enhancing-the-parallax-effect">VII. Enhancing the Parallax Effect</h2>



<p class="wp-block-paragraph">Now that you have a working parallax slider, you can experiment with additional features, such as:</p>



<ul class="wp-block-list">
<li><strong>Slide Transition Duration</strong>: Customize the transition duration for smoother animations by editing the <code>:speed</code> prop adjustments on the <code>swiper</code> component.</li>



<li><strong>Center the text</strong>: To center the text, title, and subtitle in the middle of your screen, you can use flexbox properties on the <code>.swiper-slide</code> elements. Here’s how you can adjust your CSS:</li>
</ul>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
.swiper-slide {
  font-size: 18px;
  color: #fff;
  box-sizing: border-box;
  padding: 40px 60px;
  
  /* Flexbox properties for centering */
  display: flex;
  flex-direction: column; /* Stack elements vertically */
  justify-content: center; /* Center vertically */
  align-items: center; /* Center horizontally */
}

.swiper-slide .title {
  font-size: 41px;
  font-weight: 300;
  text-align: center; /* Center text */
}

.swiper-slide .subtitle {
  font-size: 21px;
  text-align: center; /* Center text */
}

.swiper-slide .text {
  font-size: 14px;
  max-width: 400px;
  line-height: 1.3;
  text-align: center; /* Center text */
}
</pre></div>


<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="501" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2024/10/image-89-1024x501.png" alt="" class="wp-image-35512" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2024/10/image-89-1024x501.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2024/10/image-89-300x147.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2024/10/image-89-768x376.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2024/10/image-89-1536x752.png 1536w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2024/10/image-89.png 1915w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<ul class="wp-block-list">
<li><strong>Adding Image Gradients</strong>: Layering gradients over the background images to create depth. To apply a gradient overlay on top of your background image while keeping the image itself, you can modify the <code>.parallax-bg</code> class to use both the background image and the gradient. Here&#8217;s how you can do it:</li>
</ul>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; highlight: [7,8,9]; title: ; notranslate">
.parallax-bg {
  position: absolute;
  left: 0;
  top: 0;
  width: 130%;
  height: 100%;
  background-image:
    linear-gradient(to right, rgba(255, 128, 0, 0.3), rgba(0, 0, 0, 0.9)),
    /* Gradient overlay */ url(&#039;../assets/parallax1.jpg&#039;); /* Background image */
  background-size: cover;
  background-position: center;
}
</pre></div>


<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="1920" height="945" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2024/10/chrome_W0asXYnhZN-1.gif" alt="" class="wp-image-35520" /></figure>



<ul class="wp-block-list">
<li><strong>Vertical parallax</strong>: Add the props <code>:direction="'vertical'"</code> to create a vertical parallax. In this case, don&#8217;t forget to edit the class parallax-bg to adjust the height and width property as needed:<br></li>
</ul>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; highlight: [13]; title: ; notranslate">
&lt;swiper
    :style=&quot;{
      &#039;--swiper-navigation-color&#039;: &#039;#fff&#039;,
      &#039;--swiper-pagination-color&#039;: &#039;#fff&#039;,
    }&quot;
    :speed=&quot;600&quot;
    :parallax=&quot;true&quot;
    :pagination=&quot;{
      clickable: true,
    }&quot;
    :navigation=&quot;true&quot;
    :modules=&quot;modules&quot;
    :direction=&quot;&#039;vertical&#039;&quot;
    class=&quot;mySwiper&quot;
    data-swiper-parallax-x
  &gt;

.parallax-bg {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 130%;
  background-image:
    linear-gradient(to right, rgba(255, 128, 0, 0.3), rgba(0, 0, 0, 0.9)),
    /* Gradient overlay */ url(&#039;../assets/parallax1.jpg&#039;); /* Background image */
  background-size: cover;
  background-position: center;
}
</pre></div>


<figure class="wp-block-image size-full is-resized"><img loading="lazy" decoding="async" width="2560" height="1272" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2024/10/chrome_Hvv9LrSsEA-ezgif.com-split.gif" alt="" class="wp-image-35532" style="width:840px;height:auto" /></figure>



<h2 class="wp-block-heading" id="h-conclusion">Conclusion</h2>



<p class="wp-block-paragraph">This guide demonstrated how to set up a Vue 3 application with a parallax effect, creating visually engaging user experiences. You can further enhance the parallax by adjusting animation speeds, or even incorporating dynamic slide content. This setup is highly flexible for building beautiful, interactive sliders in Vue apps. Don&#8217;t forget to check Swiper API to learn more about what is doable with this component.</p>
<p>L’article <a href="https://www.dbi-services.com/blog/vue-creating-an-awesome-parallax-effect-with-swiper/">Vue &#8211; Creating an Awesome Parallax Effect with Swiper</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.dbi-services.com/blog/vue-creating-an-awesome-parallax-effect-with-swiper/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>PostgreSQL 17: Enhancing JSON Support for Web Developers</title>
		<link>https://www.dbi-services.com/blog/postgresql-17-enhancing-json-support-for-web-developers/</link>
					<comments>https://www.dbi-services.com/blog/postgresql-17-enhancing-json-support-for-web-developers/#respond</comments>
		
		<dc:creator><![CDATA[Joan Frey]]></dc:creator>
		<pubDate>Fri, 18 Oct 2024 12:47:21 +0000</pubDate>
				<category><![CDATA[Development & Performance]]></category>
		<category><![CDATA[PostgreSQL]]></category>
		<category><![CDATA[json]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=35317</guid>

					<description><![CDATA[<p>PostgreSQL 17 introduces new features for working with JSON data. These features align PostgreSQL more closely with the SQL/JSON standard and improve the developer experience when dealing with semi-structured data. In this blog, we will explore the new JSON capabilities by walking through real-world examples using a table with a jsonb column. The JSON Data [&#8230;]</p>
<p>L’article <a href="https://www.dbi-services.com/blog/postgresql-17-enhancing-json-support-for-web-developers/">PostgreSQL 17: Enhancing JSON Support for Web Developers</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">PostgreSQL 17 introduces new features for working with JSON data. These features align PostgreSQL more closely with the SQL/JSON standard and improve the developer experience when dealing with semi-structured data. In this blog, we will explore the new JSON capabilities by walking through real-world examples using a table with a <code>jsonb</code> column.</p>



<h4 class="wp-block-heading" id="h-the-json-data-challenge-for-web-developers"><strong>The JSON Data Challenge for Web Developers</strong></h4>



<p class="wp-block-paragraph">When building modern web applications, it’s common to handle JSON data—whether it&#8217;s from API responses, user data, or configuration files. PostgreSQL has supported JSON for years, and with the release of PostgreSQL 17, working with JSON becomes even more streamlined.<br>Let’s start by creating a table to store user data in a <code>jsonb</code> column and use it to demonstrate the new features.</p>



<h2 class="wp-block-heading" id="h-i-creating-a-table-with-a-jsonb-column">I. Creating a Table with a <code>jsonb</code> Column</h2>



<p class="wp-block-paragraph">To demonstrate PostgreSQL 17’s new features, let’s create a simple table called <code>users</code> that stores user information in a <code>jsonb</code> column.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
postgres=# CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    name TEXT NOT NULL,
    profile JSONB
);
CREATE TABLE
</pre></div>


<p class="wp-block-paragraph">This table has three columns:</p>



<ul class="wp-block-list">
<li><code>id</code>: A unique identifier for each user.</li>



<li><code>name</code>: The user&#8217;s name.</li>



<li><code>profile</code>: A <code>jsonb</code> column that stores various information about the user, such as their age, preferences, and settings.</li>
</ul>



<h4 class="wp-block-heading" id="h-inserting-data"><strong>Inserting Data</strong></h4>



<p class="wp-block-paragraph">Now, we’ll insert some user data in JSON format into the <code>profile</code> column.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
postgres=# INSERT INTO users (name, profile)
VALUES
(&#039;John Doe&#039;, &#039;{&quot;age&quot;: 30, &quot;preferences&quot;: {&quot;newsletter&quot;: true, &quot;theme&quot;: &quot;dark&quot;}}&#039;),
(&#039;Jane Smith&#039;, &#039;{&quot;age&quot;: 25, &quot;preferences&quot;: {&quot;newsletter&quot;: false, &quot;theme&quot;: &quot;light&quot;}}&#039;),
(&#039;Alice Brown&#039;, &#039;{&quot;age&quot;: 35, &quot;preferences&quot;: {&quot;newsletter&quot;: true, &quot;theme&quot;: &quot;dark&quot;}, &quot;address&quot;:                        {&quot;city&quot;: &quot;Paris&quot;, &quot;country&quot;: &quot;France&quot;}}&#039;);
INSERT 0 3
</pre></div>


<p class="wp-block-paragraph">We now have three users with different JSON profiles, each containing details such as age, preferences for newsletters and themes, and, in Alice’s case, an address.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
postgres=# select * from users;
 id |    name     |                                                       profile
----+-------------+----------------------------------------------------------------------------------------------------------------------
  1 | John Doe    | {&quot;age&quot;: 30, &quot;preferences&quot;: {&quot;theme&quot;: &quot;dark&quot;, &quot;newsletter&quot;: true}}
  2 | Jane Smith  | {&quot;age&quot;: 25, &quot;preferences&quot;: {&quot;theme&quot;: &quot;light&quot;, &quot;newsletter&quot;: false}}
  3 | Alice Brown | {&quot;age&quot;: 35, &quot;address&quot;: {&quot;city&quot;: &quot;Paris&quot;, &quot;country&quot;: &quot;France&quot;}, &quot;preferences&quot;: {&quot;theme&quot;: &quot;dark&quot;, &quot;newsletter&quot;: true}}
(3 rows)
</pre></div>


<h2 class="wp-block-heading" id="h-ii-using-postgresql-17-s-new-json-features">II. Using PostgreSQL 17’s New JSON Features</h2>



<p class="wp-block-paragraph">With the table set up and populated, let’s explore the new JSON-related features in PostgreSQL 17, such as <code>JSON_TABLE</code>, SQL/JSON query functions, and enhanced <code>jsonpath</code> expressions.</p>



<h3 class="wp-block-heading" id="h-a-json-table-converting-json-into-tabular-format">a. <code>JSON_TABLE</code>: Converting JSON into Tabular Format</h3>



<p class="wp-block-paragraph">The <code>JSON_TABLE</code> function allows us to transform our <code>jsonb</code> data into rows and columns. This is particularly useful when we want to extract structured data from JSON documents stored in a relational database.</p>



<p class="wp-block-paragraph">Let’s extract the <code>age</code> and <code>theme</code> from the <code>profile</code> column and convert it into a tabular format:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
postgres=# SELECT *
FROM JSON_TABLE(
    (SELECT profile FROM users WHERE name = &#039;Alice Brown&#039;),
    &#039;$.preferences&#039; COLUMNS (theme TEXT PATH &#039;$.theme&#039;, newsletter BOOLEAN PATH &#039;$.newsletter&#039;)
) AS jt;
 theme | newsletter
-------+------------
 dark  | t
(1 row)
</pre></div>


<p class="wp-block-paragraph">Here, we extracted Alice Brown&#8217;s theme preference and whether she subscribes to the newsletter from her <code>profile</code>. The <strong><code>$</code></strong> symbol is essential for this operation.</p>



<p class="wp-block-paragraph">Here&#8217;s a breakdown of how this works:</p>



<ol class="wp-block-list">
<li><strong><code>$.preferences</code></strong>: This part refers to the <code>preferences</code> key at the root of the JSON document. Once this key is selected, it acts as the root for the columns inside the <code>preferences</code> object.</li>



<li><strong>Inside the <code>COLUMNS</code> clause</strong>:
<ul class="wp-block-list">
<li><strong><code>$.theme</code></strong>: Here, <strong><code>$</code></strong> refers to the root of the <strong><code>preferences</code></strong> object, not the root of the entire JSON document. So, it looks for the <code>theme</code> key inside the <code>preferences</code> object.</li>



<li><strong><code>$.newsletter</code></strong>: Similarly, <strong><code>$</code></strong> here refers to the root of the <strong><code>preferences</code></strong> object, and it looks for the <code>newsletter</code> key within that object.</li>
</ul>
</li>
</ol>



<p class="wp-block-paragraph">In this context, the <strong><code>$</code></strong> in the <code>COLUMNS</code> clause is &#8220;relative&#8221; to the object you are working with, which in this case is <code>preferences</code>. This is a key concept in using the <code>JSON_TABLE</code> function and <code>jsonpath</code> expressions in PostgreSQL—<strong><code>$</code></strong> adapts based on the context of the object you&#8217;re working with at that stage of the query.</p>



<h3 class="wp-block-heading" id="h-b-jsonb-build-object-creating-json-data-in-queries">b. jsonb_build_object: Creating JSON Data in Queries</h3>



<p class="wp-block-paragraph">PostgreSQL allows you to create JSON directly from SQL expressions, making it easier to work with JSON data dynamically. This function exists since PostgreSQL 12, but I believe that it makes sense to present it here.</p>



<p class="wp-block-paragraph">Let’s construct some JSON data based on our <code>users</code> table:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
postgres=# SELECT name,
       jsonb_build_object(
           &#039;age&#039;, profile-&gt;&gt;&#039;age&#039;,
           &#039;theme&#039;, profile-&gt;&#039;preferences&#039;-&gt;&gt;&#039;theme&#039;
       ) AS constructed_json
FROM users;
    name     |        constructed_json
-------------+---------------------------------
 John Doe    | {&quot;age&quot;: &quot;30&quot;, &quot;theme&quot;: &quot;dark&quot;}
 Jane Smith  | {&quot;age&quot;: &quot;25&quot;, &quot;theme&quot;: &quot;light&quot;}
 Alice Brown | {&quot;age&quot;: &quot;35&quot;, &quot;theme&quot;: &quot;dark&quot;}
(3 rows)
</pre></div>


<p class="wp-block-paragraph">This query dynamically builds a JSON object from the <code>age</code> and <code>theme</code> fields in the <code>profile</code> column.</p>



<h3 class="wp-block-heading" id="h-c-sql-json-query-functions-simplifying-json-queries">c. SQL/JSON Query Functions: Simplifying JSON Queries</h3>



<p class="wp-block-paragraph">PostgreSQL 17 introduces several new SQL/JSON query functions, such as <code>JSON_EXISTS</code>, <code>JSON_QUERY</code>, and <code>JSON_VALUE</code>. These functions allow you to query and extract values from JSON documents more efficiently.</p>



<h4 class="wp-block-heading"><strong>Example: Checking for the Existence of a Key</strong></h4>



<p class="wp-block-paragraph">Let’s check if the <code>address</code> key exists in John Doe&#8217;s profile:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
postgres=# SELECT JSON_EXISTS(profile, &#039;$.address&#039;)
FROM users
WHERE name = &#039;John Doe&#039;;
 json_exists
-------------
 f
(1 row)
</pre></div>


<h4 class="wp-block-heading" id="h-example-extracting-scalar-values"><strong>Example: Extracting Scalar Values</strong></h4>



<p class="wp-block-paragraph">We can use the <code>JSON_VALUE</code> function to extract specific values from a JSON document. For example, let’s extract the <code>city</code> from Alice’s address:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
postgres=# SELECT JSON_VALUE(profile, &#039;$.address.city&#039;)
FROM users
WHERE name = &#039;Alice Brown&#039;;
 json_value
------------
 Paris
(1 row)
</pre></div>


<p class="wp-block-paragraph">The <code>JSON_VALUE</code> function simplifies the process of extracting individual scalar values from JSON documents. Use <code>JSON_VALUE()</code> only when you expect the extracted value to be a single SQL/JSON scalar. Attempting to retrieve multiple values will result in an error. If the extracted value might be an object or an array, opt for the <code>JSON_QUERY</code> function instead.</p>



<h4 class="wp-block-heading" id="h-example-extract-specific-values-from-a-json-document"><strong>Example: extract specific values from a JSON document</strong></h4>



<p class="wp-block-paragraph">The <code>JSON_QUERY</code> function in PostgreSQL provides a powerful way to extract data from JSONB columns using path expressions. By leveraging its various options, developers can customize the output format, handle errors gracefully, and work efficiently with JSON data stored in PostgreSQL. Now, let&#8217;s use <code>JSON_QUERY</code> to extract the preferences for each user. We want to get the <code>preferences</code> object for each user&#8217;s profile.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
postgres=# SELECT
    name,
    JSON_QUERY(profile, &#039;$.preferences&#039;) AS user_preferences
FROM users;
    name     |            user_preferences
-------------+-----------------------------------------
 John Doe    | {&quot;theme&quot;: &quot;dark&quot;, &quot;newsletter&quot;: true}
 Jane Smith  | {&quot;theme&quot;: &quot;light&quot;, &quot;newsletter&quot;: false}
 Alice Brown | {&quot;theme&quot;: &quot;dark&quot;, &quot;newsletter&quot;: true}
(3 rows)
</pre></div>


<h3 class="wp-block-heading" id="h-d-enhanced-jsonpath-expressions">d. Enhanced <code>jsonpath</code> Expressions</h3>



<p class="wp-block-paragraph">PostgreSQL improves its support for <code>jsonpath</code> expressions, enabling more advanced queries. You can now cast JSON values into native PostgreSQL types, such as integers or booleans.</p>



<p class="wp-block-paragraph">Let’s extract Jane Smith’s <code>age</code> and cast it as an integer:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
postgres=# SELECT JSON_VALUE(profile, &#039;$.age&#039; RETURNING INT) AS age_int
FROM users
WHERE name = &#039;Jane Smith&#039;;
 age_int
---------
      25
(1 row)
</pre></div>


<p class="wp-block-paragraph">This query demonstrates how you can convert JSON data into a native PostgreSQL type using the <code>RETURNING</code> clause.</p>



<h2 class="wp-block-heading" id="h-conclusion">Conclusion</h2>



<p class="wp-block-paragraph">PostgreSQL 17 brings powerful new features for working with JSON data, making it easier for web developers to query and manipulate JSON in a database. We explored how <code>JSON_TABLE</code>, new SQL/JSON functions, and enhanced <code>jsonpath</code> expressions help convert JSON data into a more usable format, extract values, and even handle complex queries.</p>



<p class="wp-block-paragraph">These new tools make working with semi-structured data simpler, allowing you to build more efficient and flexible web applications. If you often work with JSON in PostgreSQL, upgrading to version 17 will streamline your workflow and enhance your capabilities.</p>



<p class="wp-block-paragraph">Stay tuned and don&#8217;t forget to check PostgreSQL 17 Release note <a href="https://www.postgresql.org/about/news/postgresql-17-released-2936/" target="_blank" rel="noreferrer noopener">https://www.postgresql.org/about/news/postgresql-17-released-2936/</a></p>
<p>L’article <a href="https://www.dbi-services.com/blog/postgresql-17-enhancing-json-support-for-web-developers/">PostgreSQL 17: Enhancing JSON Support for Web Developers</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.dbi-services.com/blog/postgresql-17-enhancing-json-support-for-web-developers/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Creating a Global and Custom Snackbar in Vue 3 Using Vuetify and Pinia</title>
		<link>https://www.dbi-services.com/blog/creating-a-global-snackbar-in-a-vue-3-application-using-vuetify-and-pinia/</link>
					<comments>https://www.dbi-services.com/blog/creating-a-global-snackbar-in-a-vue-3-application-using-vuetify-and-pinia/#respond</comments>
		
		<dc:creator><![CDATA[Joan Frey]]></dc:creator>
		<pubDate>Thu, 17 Oct 2024 16:25:20 +0000</pubDate>
				<category><![CDATA[Web]]></category>
		<category><![CDATA[global]]></category>
		<category><![CDATA[Global page]]></category>
		<category><![CDATA[Pinia]]></category>
		<category><![CDATA[snackbar]]></category>
		<category><![CDATA[vue]]></category>
		<category><![CDATA[vuejs]]></category>
		<category><![CDATA[vuetify]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=35281</guid>

					<description><![CDATA[<p>In this post, we&#8217;ll walk through how to implement a Global and Custom Snackbar in Vue 3 with Vuetify and Pinia for state management. We&#8217;ll set it up so that you can easily trigger the Snackbar from any component in your app. I. Set Up Your Vue App with Vuetify CLI To start, we’ll create [&#8230;]</p>
<p>L’article <a href="https://www.dbi-services.com/blog/creating-a-global-snackbar-in-a-vue-3-application-using-vuetify-and-pinia/">Creating a Global and Custom Snackbar in Vue 3 Using Vuetify and Pinia</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">In this post, we&#8217;ll walk through how to implement a Global and Custom Snackbar in Vue 3 with Vuetify and Pinia for state management. We&#8217;ll set it up so that you can easily trigger the Snackbar from any component in your app.</p>



<h2 class="wp-block-heading" id="h-i-set-up-your-vue-app-with-vuetify-cli">I. Set Up Your Vue App with Vuetify CLI</h2>



<p class="wp-block-paragraph">To start, we’ll create our Vue 3 app using the Vuetify CLI. This CLI simplifies the process of integrating Vuetify and sets up your project structure. </p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
npm create vuetify@latest
</pre></div>


<p class="wp-block-paragraph">Follow the prompts to choose your project settings and install dependencies. As we are going to use Pinia, don&#8217;t forget to choose this option. I decided to keep all the generated files to keep it simple and straightforward, but you can clean the code that was automatically generated. I just cleaned the component HelloWorld.vue, that now looks like this:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
&lt;template&gt;
  &lt;v-container class=&quot;fill-height&quot;&gt;
    &lt;v-responsive class=&quot;align-centerfill-height mx-auto&quot; max-width=&quot;900&quot;&gt;
    &lt;/v-responsive&gt;
  &lt;/v-container&gt;
&lt;/template&gt;

&lt;script setup lang=&quot;ts&quot;&gt;
&lt;/script&gt;
</pre></div>


<h2 class="wp-block-heading" id="h-ii-set-up-the-pinia-store-for-snackbar">II. Set Up the Pinia Store for Snackbar</h2>



<p class="wp-block-paragraph">Next, create a Pinia store (<code>SnackbarStore.ts</code>) to handle the state of the Snackbar (open/close, message, status, etc.). This will allow you to trigger the Snackbar from anywhere in the app.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
// SnackbarStore.ts inside stores directory

import { defineStore } from &#039;pinia&#039;
import { Ref, ref } from &#039;vue&#039;

export interface CustomAction {
  actionName: string;
  link: any;
}

export const useSnackbarStore = defineStore(&#039;SnackbarStore&#039;, () =&gt; {
  const isOpen = ref(false)
  const message = ref(&#039;&#039;)
  const status = ref(&#039;error&#039;)
  const customActions: Ref&lt;CustomAction&#x5B;]&gt; = ref(&#x5B;])

  const showSnackbar = (msg: string, st: string, optionalActions?: CustomAction&#x5B;]) =&gt; { // Change to accept an array
    message.value = msg
    status.value = st
    isOpen.value = true
    if (optionalActions) {
      customActions.value.push(...optionalActions) // Spread the array into customActions
    }
    if (status.value.toLowerCase() === &#039;success&#039;) {
      setTimeout(() =&gt; {
        closeSnackbar()
      }, 2500)
    }
  }

  function closeSnackbar () {
    isOpen.value = false
    customActions.value.splice(0, customActions.value.length) // Clear custom actions
  }

  return { isOpen, message, status, showSnackbar, closeSnackbar, customActions }
})
</pre></div>


<h2 class="wp-block-heading" id="h-iii-create-the-snackbar-component">III. Create the Snackbar Component</h2>



<p class="wp-block-paragraph">We&#8217;ll create a reusable Snackbar component (<code>GlobalSnackbar.vue</code>) that can be used anywhere in your application.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
&lt;template&gt;
  &lt;div class=&quot;text-center&quot;&gt;
    &lt;v-snackbar
      v-model=&quot;snackbarRef&quot;
      class=&quot;customSnackbar&quot;
      :color=&quot;snackbarStatus&quot;
      variant=&quot;outlined&quot;
      vertical
    &gt;
      &lt;div class=&quot;text-subtitle-1 pb-2 title-custom&quot;&gt;{{ snackbarStatus }}&lt;/div&gt;

      &lt;p&gt;{{ errorMsg }}&lt;/p&gt;

      &lt;template #actions&gt;
        &lt;v-btn
          v-for=&quot;act of snackbarActions&quot;
          :key=&quot;act.actionName&quot;
          :to=&quot;act.link&quot;
        &gt;
          {{ act.actionName }}
        &lt;/v-btn&gt;
        &lt;v-btn variant=&quot;tonal&quot; @click=&quot;closeSnackbar&quot;&gt; Close &lt;/v-btn&gt;
      &lt;/template&gt;
    &lt;/v-snackbar&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script lang=&quot;ts&quot; setup&gt;
  import { toRef } from &#039;vue&#039;
  import { CustomAction, useSnackbarStore } from &#039;@/stores/SnackbarStore&#039;

  // Define the properties that the component will receive
  const props = defineProps({
    snackbarShow: Boolean,
    errorMsg: String,
    snackbarStatus: String,
    snackbarActions: {
      type: Array as () =&gt; CustomAction&#x5B;], // Custom type to define snackbar actions
    },
  })

  const snackbar = useSnackbarStore()
  const snackbarRef = toRef(props, &#039;snackbarShow&#039;)

  const closeSnackbar = () =&gt; {
    snackbar.closeSnackbar()
  }
&lt;/script&gt;

&lt;style&gt;
.customSnackbar.v-snackbar--vertical .v-snackbar__wrapper {
  background-color: rgb(238, 238, 238);
}
.title-custom {
  text-transform: uppercase !important;
}
&lt;/style&gt;
</pre></div>


<h2 class="wp-block-heading" id="h-iv-modify-app-vue-to-include-the-global-snackbar">IV. Modify <code>App.vue</code> to Include the Global Snackbar</h2>



<p class="wp-block-paragraph">To ensure the Snackbar is available across your entire app, import it into <code>App.vue</code> and pass the required props from the <code>SnackbarStore</code>.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
&lt;template&gt;
  &lt;v-app&gt;
    &lt;v-main&gt;
      &lt;router-view /&gt;
    &lt;/v-main&gt;
    &lt;GlobalSnackbar
      :error-msg=&quot;snackbar.message&quot;
      :snackbar-actions=&quot;snackbar.customActions&quot;
      :snackbar-show=&quot;snackbar.isOpen&quot;
      :snackbar-status=&quot;snackbar.status&quot;
    /&gt;
  &lt;/v-app&gt;
&lt;/template&gt;

&lt;script lang=&quot;ts&quot; setup&gt;
  import { useSnackbarStore } from &#039;@/stores/SnackbarStore&#039;
  const snackbar = useSnackbarStore()
&lt;/script&gt;
</pre></div>


<p class="wp-block-paragraph">This step is essential to making sure the <code>GlobalSnackbar</code> component is part of the root layout and is always available to display messages.</p>



<h2 class="wp-block-heading" id="h-v-using-the-snackbar-in-a-component">V. Using the Snackbar in a Component</h2>



<p class="wp-block-paragraph">Now, let&#8217;s demonstrate how to trigger the Snackbar from any component. For this example, we’ll use a button that, when clicked, shows a success message with an optional action. We can edit HelloWord.vue with following code.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
&lt;template&gt;
  &lt;v-container class=&quot;fill-height&quot;&gt;
    &lt;v-responsive class=&quot;align-centerfill-height mx-auto&quot; max-width=&quot;900&quot;&gt;
      &lt;v-btn @click=&quot;openSnackbar()&quot;&gt;test&lt;/v-btn&gt;
    &lt;/v-responsive&gt;
  &lt;/v-container&gt;
&lt;/template&gt;

&lt;script setup lang=&quot;ts&quot;&gt;
  import { useSnackbarStore } from &#039;@/stores/SnackbarStore&#039;
  const snackbar = useSnackbarStore()

  const openSnackbar = () =&gt; {
    snackbar.showSnackbar(&#039;Show snackbar&#039;, &#039;success&#039;, &#x5B;
      { actionName: &#039;Go to Home&#039;, link: &#039;/home&#039; },
      { actionName: &#039;Go to Test&#039;, link: &#039;/test&#039; },
    ])
  }
&lt;/script&gt;
</pre></div>


<h2 class="wp-block-heading" id="h-vi-testing-the-snackbar">VI. Testing the Snackbar</h2>



<p class="wp-block-paragraph">Once everything is in place, try clicking the button to trigger the Snackbar. You should see the Snackbar with a success message and an optional action button appear at the bottom of the screen. And Logically, if you click on one of the custom action buttons, it will redirect you properly, but you will obviously see a blank page, as the view doesn&#8217;t exist.</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="521" height="493" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2024/10/image-49.png" alt="" class="wp-image-35295" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2024/10/image-49.png 521w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2024/10/image-49-300x284.png 300w" sizes="auto, (max-width: 521px) 100vw, 521px" /></figure>



<h2 class="wp-block-heading" id="h-conclusion">Conclusion</h2>



<p class="wp-block-paragraph">In this post, we created a global and custom Snackbar component in Vue 3 using Vuetify and Pinia. We set it up so that it can be easily triggered from anywhere in the app. By managing its state with Pinia, we keep the logic centralized and clean, making the Snackbar reusable across the entire application.</p>



<p class="wp-block-paragraph">Feel free to adjust the styling and functionality of the Snackbar to suit your needs. Now you have a flexible, easy-to-use notification system for your Vue 3 project!</p>
<p>L’article <a href="https://www.dbi-services.com/blog/creating-a-global-snackbar-in-a-vue-3-application-using-vuetify-and-pinia/">Creating a Global and Custom Snackbar in Vue 3 Using Vuetify and Pinia</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.dbi-services.com/blog/creating-a-global-snackbar-in-a-vue-3-application-using-vuetify-and-pinia/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>

<!--
Performance optimized by W3 Total Cache. Learn more: https://www.boldgrid.com/w3-total-cache/?utm_source=w3tc&utm_medium=footer_comment&utm_campaign=free_plugin

Page Caching using Disk: Enhanced 
Lazy Loading (feed)

Served from: www.dbi-services.com @ 2026-08-05 03:36:55 by W3 Total Cache
-->