<?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>Python-Oracledb on inetshell</title><link>https://inet.sh/es/tags/python-oracledb/</link><description>Recent content in Python-Oracledb on inetshell</description><generator>Hugo</generator><language>es</language><lastBuildDate>Sat, 26 Sep 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://inet.sh/es/tags/python-oracledb/index.xml" rel="self" type="application/rss+xml"/><item><title>Oracle CQN con Python: enterarte de los cambios de una tabla sin polling (y por qué oracle_fdw no dispara triggers)</title><link>https://inet.sh/es/posts/databases/oracle-cqn-python-sin-polling/</link><pubDate>Sat, 26 Sep 2026 00:00:00 +0000</pubDate><guid>https://inet.sh/es/posts/databases/oracle-cqn-python-sin-polling/</guid><description>Continuous Query Notification hace que Oracle avise a tu aplicación cuando cambian las filas de una consulta, sin polling: GRANT CHANGE NOTIFICATION, conexión con events=True, subscribe() y registerquery() en python-oracledb. Qué exige de red (la base conecta de vuelta al cliente, o client_initiated=True), el ORA-29977 del SELECT *, y el caso real: sincronizar Oracle hacia PostgreSQL, porque oracle_fdw no dispara triggers cuando cambia el lado Oracle.</description><content:encoded><![CDATA[<h2 id="en-corto">En corto</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"># columnas explícitas, nunca 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 para salir</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>Cada <code>COMMIT</code> que cambie el resultado de esa consulta dispara <code>on_change</code> con los <code>rowid</code> afectados. Sin polling, sin triggers, sin GoldenGate.</p>
<h2 id="por-qué">Por qué</h2>
<p>Cuando otro sistema necesita reaccionar a cambios en Oracle (invalidar una caché, replicar hacia otra base, disparar un proceso), lo habitual es preguntar cada N segundos con <code>WHERE updated_at &gt; :last</code>. Funciona hasta que no: filas sin columna de fecha, borrados que no dejan rastro, carga inútil sobre la base.</p>
<p><strong>Continuous Query Notification</strong> invierte el flujo: registras una consulta y Oracle te llama cuando cambia. En modo <em>query</em> (<code>SUBSCR_QOS_QUERY</code>) avisa solo si cambia el resultado de esa consulta; en modo <em>object</em> (sin ese flag) avisa ante cualquier cambio en las tablas implicadas. <code>SUBSCR_QOS_ROWIDS</code> incluye los <code>rowid</code> en la notificación, con lo que el callback puede leer exactamente esas filas.</p>
<h2 id="el-caso-replicar-oracle-a-postgresql">El caso: replicar Oracle a PostgreSQL</h2>
<p><code>oracle_fdw</code> deja consultar tablas de Oracle desde PostgreSQL como si fueran locales, y es tentador pensar que un trigger sobre la <em>foreign table</em> se disparará cuando cambien los datos en Oracle. No: los triggers de una foreign table solo saltan con escrituras hechas <strong>desde PostgreSQL</strong> a través de ella. El autor de <code>oracle_fdw</code> lo dice tal cual: no hay forma automática de mantener sincronizada una tabla de PostgreSQL con una de Oracle desde el lado PostgreSQL; eso tiene que dispararlo Oracle.</p>
<p>Con CQN el pequeño servicio en Python es la pieza que falta:</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>Los borrados llegan con <code>row.operation &amp; oracledb.OPCODE_DELETE</code> y un <code>rowid</code> que ya no existe: borra en PostgreSQL por la clave que guardaste asociada a ese <code>rowid</code>, o marca la fila.</p>
<h2 id="requisitos">Requisitos</h2>
<ul>
<li><strong>Permiso</strong>: <code>GRANT CHANGE NOTIFICATION TO app_user</code>, además de <code>SELECT</code> sobre las tablas.</li>
<li><strong><code>events=True</code></strong> en la conexión que se suscribe; sin eso <code>subscribe()</code> falla.</li>
<li><strong>Red de vuelta.</strong> Por defecto es <strong>la base la que abre una conexión hacia tu aplicación</strong> para entregar las notificaciones: la máquina del cliente necesita una IP alcanzable desde el servidor Oracle y un puerto abierto (<code>subscribe(..., ip_address=…, port=…)</code> fija cuáles). Detrás de NAT, en un contenedor o con firewall entre medias, las notificaciones simplemente no llegan y nadie avisa.</li>
<li><strong><code>client_initiated=True</code></strong> elimina ese problema: la conexión la abre tu app. Requiere modo Thick (<code>oracledb.init_oracle_client()</code> con Instant Client 19.4 o superior) y base 19.4 o superior. En modo Thin, el por defecto de python-oracledb, CQN funciona pero solo en el modo clásico.</li>
<li><strong><code>job_queue_processes</code></strong> mayor que cero en la base: las notificaciones se despachan por el job queue.</li>
</ul>
<h2 id="trampas">Trampas</h2>
<ul>
<li><strong><code>ORA-29977: Unsupported column type for query registration in guaranteed mode</code></strong>: en modo <em>query</em> hay tipos de columna que Oracle no admite (LOB, entre otros), y <code>SELECT *</code> los arrastra. Lista las columnas que necesitas o quita <code>SUBSCR_QOS_QUERY</code> y trabaja en modo <em>object</em>.</li>
<li><strong>Las notificaciones no son durables</strong>: si tu servicio está caído durante un cambio, no lo recibe. Al arrancar, haz una reconciliación (por <code>updated_at</code>, o completa) y después suscríbete. <code>timeout=0</code> en <code>subscribe</code> mantiene la suscripción hasta que la conexión muera.</li>
<li><strong>Una suscripción por conexión viva</strong>: la conexión con <code>events=True</code> tiene que seguir abierta; ponle keepalives y reconecta con re-suscripción si se cae.</li>
<li><strong>Mucho volumen</strong> (miles de commits por segundo) no es territorio de CQN: ahí entra un CDC por redo log (GoldenGate, o Debezium con su conector de Oracle vía LogMiner).</li>
<li><strong><code>SELECT</code> con funciones no deterministas</strong> o sobre vistas complejas puede no registrarse en modo <em>query</em>; simplifica la consulta o cae a modo <em>object</em>.</li>
</ul>
<p>Referencia: <a href="https://python-oracledb.readthedocs.io/en/latest/user_guide/cqn.html">Continuous Query Notification</a> en la documentación de python-oracledb y la <a href="https://github.com/laurenz/oracle_fdw/issues/342">discusión en oracle_fdw</a>.</p>
]]></content:encoded></item></channel></rss>