<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Cqn on inetshell</title><link>https://inet.sh/en/tags/cqn/</link><description>Recent content in Cqn on inetshell</description><generator>Hugo</generator><language>en</language><lastBuildDate>Sat, 26 Sep 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://inet.sh/en/tags/cqn/index.xml" rel="self" type="application/rss+xml"/><item><title>Oracle CQN with Python: learn about table changes without polling (and why oracle_fdw fires no triggers)</title><link>https://inet.sh/en/posts/databases/oracle-cqn-python-without-polling/</link><pubDate>Sat, 26 Sep 2026 00:00:00 +0000</pubDate><guid>https://inet.sh/en/posts/databases/oracle-cqn-python-without-polling/</guid><description>Continuous Query Notification makes Oracle notify your application when the rows of a query change, with no polling: GRANT CHANGE NOTIFICATION, a connection with events=True, subscribe() and registerquery() in python-oracledb. What it needs from the network (the database connects back to the client, or client_initiated=True), the ORA-29977 from SELECT *, and the real case: syncing Oracle into PostgreSQL, because oracle_fdw fires no triggers when the Oracle side changes.</description><content:encoded><![CDATA[<h2 id="tldr">TL;DR</h2>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">GRANT</span><span class="w"> </span><span class="n">CHANGE</span><span class="w"> </span><span class="n">NOTIFICATION</span><span class="w"> </span><span class="k">TO</span><span class="w"> </span><span class="n">app_user</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">oracledb</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">on_change</span><span class="p">(</span><span class="n">message</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">query</span> <span class="ow">in</span> <span class="n">message</span><span class="o">.</span><span class="n">queries</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">for</span> <span class="n">table</span> <span class="ow">in</span> <span class="n">query</span><span class="o">.</span><span class="n">tables</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="k">for</span> <span class="n">row</span> <span class="ow">in</span> <span class="n">table</span><span class="o">.</span><span class="n">rows</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                <span class="nb">print</span><span class="p">(</span><span class="n">table</span><span class="o">.</span><span class="n">name</span><span class="p">,</span> <span class="n">row</span><span class="o">.</span><span class="n">operation</span><span class="p">,</span> <span class="n">row</span><span class="o">.</span><span class="n">rowid</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">conn</span> <span class="o">=</span> <span class="n">oracledb</span><span class="o">.</span><span class="n">connect</span><span class="p">(</span><span class="n">user</span><span class="o">=</span><span class="s2">&#34;app_user&#34;</span><span class="p">,</span> <span class="n">password</span><span class="o">=</span><span class="n">pw</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                        <span class="n">dsn</span><span class="o">=</span><span class="s2">&#34;db.example:1521/orclpdb1&#34;</span><span class="p">,</span> <span class="n">events</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">sub</span> <span class="o">=</span> <span class="n">conn</span><span class="o">.</span><span class="n">subscribe</span><span class="p">(</span><span class="n">callback</span><span class="o">=</span><span class="n">on_change</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                     <span class="n">operations</span><span class="o">=</span><span class="n">oracledb</span><span class="o">.</span><span class="n">OPCODE_INSERT</span> <span class="o">|</span> <span class="n">oracledb</span><span class="o">.</span><span class="n">OPCODE_UPDATE</span> <span class="o">|</span> <span class="n">oracledb</span><span class="o">.</span><span class="n">OPCODE_DELETE</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                     <span class="n">qos</span><span class="o">=</span><span class="n">oracledb</span><span class="o">.</span><span class="n">SUBSCR_QOS_QUERY</span> <span class="o">|</span> <span class="n">oracledb</span><span class="o">.</span><span class="n">SUBSCR_QOS_ROWIDS</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">sub</span><span class="o">.</span><span class="n">registerquery</span><span class="p">(</span><span class="s2">&#34;select id, status, updated_at from orders&#34;</span><span class="p">)</span>   <span class="c1"># explicit columns, never SELECT *</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">input</span><span class="p">(</span><span class="s2">&#34;Ctrl+C to exit</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Every <code>COMMIT</code> that changes the result of that query fires <code>on_change</code> with the affected <code>rowid</code>s. No polling, no triggers, no GoldenGate.</p>
<h2 id="why">Why</h2>
<p>When another system must react to changes in Oracle (invalidate a cache, replicate to another database, kick off a process), the usual approach is asking every N seconds with <code>WHERE updated_at &gt; :last</code>. It works until it does not: rows without a date column, deletes that leave no trace, useless load on the database.</p>
<p><strong>Continuous Query Notification</strong> flips the flow: you register a query and Oracle calls you when it changes. In <em>query</em> mode (<code>SUBSCR_QOS_QUERY</code>) it notifies only if the result of that query changes; in <em>object</em> mode (without that flag) it notifies on any change to the tables involved. <code>SUBSCR_QOS_ROWIDS</code> includes the <code>rowid</code>s in the notification, so the callback can read exactly those rows.</p>
<h2 id="the-case-replicating-oracle-into-postgresql">The case: replicating Oracle into PostgreSQL</h2>
<p><code>oracle_fdw</code> lets you query Oracle tables from PostgreSQL as if they were local, and it is tempting to think a trigger on the <em>foreign table</em> will fire when data changes in Oracle. It does not: triggers on a foreign table only fire for writes made <strong>from PostgreSQL</strong> through it. The <code>oracle_fdw</code> author says it plainly: there is no automatic way to keep a PostgreSQL table in sync with an Oracle table from the PostgreSQL side; Oracle has to trigger it.</p>
<p>With CQN, the small Python service is the missing piece:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">on_change</span><span class="p">(</span><span class="n">message</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">rowids</span> <span class="o">=</span> <span class="p">[</span><span class="n">row</span><span class="o">.</span><span class="n">rowid</span> <span class="k">for</span> <span class="n">q</span> <span class="ow">in</span> <span class="n">message</span><span class="o">.</span><span class="n">queries</span> <span class="k">for</span> <span class="n">t</span> <span class="ow">in</span> <span class="n">q</span><span class="o">.</span><span class="n">tables</span> <span class="k">for</span> <span class="n">row</span> <span class="ow">in</span> <span class="n">t</span><span class="o">.</span><span class="n">rows</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">    <span class="k">with</span> <span class="n">conn</span><span class="o">.</span><span class="n">cursor</span><span class="p">()</span> <span class="k">as</span> <span class="n">cur</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">cur</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">&#34;select id, status, updated_at from orders where rowid in (</span><span class="si">%s</span><span class="s2">)&#34;</span>
</span></span><span class="line"><span class="cl">                    <span class="o">%</span> <span class="s2">&#34;,&#34;</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;&#39;</span><span class="si">{</span><span class="n">r</span><span class="si">}</span><span class="s2">&#39;&#34;</span> <span class="k">for</span> <span class="n">r</span> <span class="ow">in</span> <span class="n">rowids</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">        <span class="k">for</span> <span class="n">id_</span><span class="p">,</span> <span class="n">status</span><span class="p">,</span> <span class="n">updated_at</span> <span class="ow">in</span> <span class="n">cur</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">pg</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">&#34;insert into orders (id, status, updated_at) values (</span><span class="si">%s</span><span class="s2">, </span><span class="si">%s</span><span class="s2">, </span><span class="si">%s</span><span class="s2">) &#34;</span>
</span></span><span class="line"><span class="cl">                       <span class="s2">&#34;on conflict (id) do update set status = excluded.status, updated_at = excluded.updated_at&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                       <span class="p">(</span><span class="n">id_</span><span class="p">,</span> <span class="n">status</span><span class="p">,</span> <span class="n">updated_at</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="n">pg</span><span class="o">.</span><span class="n">commit</span><span class="p">()</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Deletes arrive with <code>row.operation &amp; oracledb.OPCODE_DELETE</code> and a <code>rowid</code> that no longer exists: delete in PostgreSQL by the key you stored against that <code>rowid</code>, or flag the row.</p>
<h2 id="requirements">Requirements</h2>
<ul>
<li><strong>Permission</strong>: <code>GRANT CHANGE NOTIFICATION TO app_user</code>, besides <code>SELECT</code> on the tables.</li>
<li><strong><code>events=True</code></strong> on the connection that subscribes; without it <code>subscribe()</code> fails.</li>
<li><strong>Network back-channel.</strong> By default <strong>the database opens a connection towards your application</strong> to deliver notifications: the client machine needs an IP reachable from the Oracle server and an open port (<code>subscribe(..., ip_address=…, port=…)</code> pins which). Behind NAT, inside a container, or with a firewall in between, notifications simply never arrive and nobody tells you.</li>
<li><strong><code>client_initiated=True</code></strong> removes that problem: your app opens the connection. It requires Thick mode (<code>oracledb.init_oracle_client()</code> with Instant Client 19.4 or later) and a 19.4+ database. In Thin mode, python-oracledb&rsquo;s default, CQN works but only in the classic mode.</li>
<li><strong><code>job_queue_processes</code></strong> greater than zero on the database: notifications are dispatched through the job queue.</li>
</ul>
<h2 id="traps">Traps</h2>
<ul>
<li><strong><code>ORA-29977: Unsupported column type for query registration in guaranteed mode</code></strong>: in <em>query</em> mode some column types are not allowed (LOBs, among others), and <code>SELECT *</code> drags them in. List the columns you need, or drop <code>SUBSCR_QOS_QUERY</code> and work in <em>object</em> mode.</li>
<li><strong>Notifications are not durable</strong>: if your service is down during a change, it does not get it. On startup, reconcile first (by <code>updated_at</code>, or in full) and subscribe afterwards. <code>timeout=0</code> on <code>subscribe</code> keeps the subscription until the connection dies.</li>
<li><strong>One subscription per live connection</strong>: the <code>events=True</code> connection must stay open; give it keepalives and reconnect with a re-subscribe if it drops.</li>
<li><strong>Heavy volume</strong> (thousands of commits per second) is not CQN territory: that is where redo-log CDC comes in (GoldenGate, or Debezium with its Oracle connector over LogMiner).</li>
<li><strong><code>SELECT</code> with non-deterministic functions</strong> or over complex views may not register in <em>query</em> mode; simplify the query or fall back to <em>object</em> mode.</li>
</ul>
<p>Reference: <a href="https://python-oracledb.readthedocs.io/en/latest/user_guide/cqn.html">Continuous Query Notification</a> in the python-oracledb documentation and the <a href="https://github.com/laurenz/oracle_fdw/issues/342">oracle_fdw discussion</a>.</p>
]]></content:encoded></item></channel></rss>