Mingcedar

Product: Mingcedar CONTROL

Python and SQL: queries, writes, and shared data access | Mingcedar

Complete shared modules and offline dependencies, then practice SQLite, business-database access, inserts, queries, and updates. Choose either PostgreSQL or MySQL.

Published Updated

<div class="manual-doc-article manual-doc-article-section"><h2 id="python-sql">Developer tutorial: Python and SQL: queries, writes, and shared data access</h2>

Practice with the standard library, then connect to an external business database using psycopg or PyMySQL with parameterized queries, transactions, and duplicate-request reconciliation.

<div class="doc-card-list doc-tutorial">

<article class="doc-card">

<h3 id="python-sql-item-1">Prerequisites and exercise order</h3>

<p>Complete shared modules and offline dependencies, then practice SQLite, business-database access, inserts, queries, and updates. Choose either PostgreSQL or MySQL.</p>

<ul>

<li>The example table belongs to an external business database. Do not write directly to Mingcedar tag_latest or runtime configuration tables.</li>

<li>Run Test executes ordinary Python calls in the script. SQL, HTTP, and mail operations may take effect; connect to a separate practice database or endpoint first.</li>

<li>Connection settings live in common/business_config.py. The password comes from this project’s DATABASE_PASSWORD Secret, explicitly configured for the project rather than inherited from the host.</li>

</ul>

</article>

<article class="doc-card">

<h3 id="python-sql-item-2">1. Run the SQL exercise without additional packages</h3>

<p>Save as scripts/sql_basics.py and click Run Test. It creates and modifies an in-memory table, returning [(&#39;measurement-001&#39;, 13.0)]. Data does not persist after execution.</p>

<ol>

<li>Select the practice project on Code and create sql_basics under scripts/.</li>

<li>Copy the complete code, save, select sql_basics.py, and click Run Test.</li>

<li>Inspect the output, then run again. It should still contain one row because each run creates a new in-memory database.</li>

</ol>

<div class="manual-code" data-code-language="python"><div class="manual-code-toolbar"><span>Python</span><span class="manual-code-tools"><small>Python</small><button type="button" data-copy-code data-copied="Copied" data-failed="Select and copy the code" aria-label="Copy code: Python">Copy code</button><span class="manual-code-status" role="status"></span></span></div><pre tabindex="0" class="docs-code-block shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#6A737D"># scripts/sql_basics.py — invocation-local SQL practice</span></span>

<span class="line"><span style="color:#D73A49">import</span><span style="color:#24292E"> sqlite3</span></span>

<span class="line"></span>

<span class="line"><span style="color:#D73A49">def</span><span style="color:#6F42C1"> main</span><span style="color:#24292E">(ctx):</span></span>

<span class="line"><span style="color:#24292E"> connection </span><span style="color:#D73A49">=</span><span style="color:#24292E"> sqlite3.connect(</span><span style="color:#032F62">":memory:"</span><span style="color:#24292E">)</span></span>

<span class="line"><span style="color:#D73A49"> try</span><span style="color:#24292E">:</span></span>

<span class="line"><span style="color:#D73A49"> with</span><span style="color:#24292E"> connection:</span></span>

<span class="line"><span style="color:#24292E"> connection.execute(</span><span style="color:#032F62">"CREATE TABLE measurements (record_id TEXT PRIMARY KEY, value REAL NOT NULL)"</span><span style="color:#24292E">)</span></span>

<span class="line"><span style="color:#24292E"> connection.execute(</span><span style="color:#032F62">"INSERT INTO measurements VALUES (?, ?)"</span><span style="color:#24292E">, (</span><span style="color:#032F62">"measurement-001"</span><span style="color:#24292E">, </span><span style="color:#005CC5">12.5</span><span style="color:#24292E">))</span></span>

<span class="line"><span style="color:#24292E"> connection.execute(</span><span style="color:#032F62">"UPDATE measurements SET value = ? WHERE record_id = ?"</span><span style="color:#24292E">, (</span><span style="color:#005CC5">13.0</span><span style="color:#24292E">, </span><span style="color:#032F62">"measurement-001"</span><span style="color:#24292E">))</span></span>

<span class="line"><span style="color:#24292E"> rows </span><span style="color:#D73A49">=</span><span style="color:#24292E"> connection.execute(</span><span style="color:#032F62">"SELECT record_id, value FROM measurements WHERE record_id = ?"</span><span style="color:#24292E">,</span></span>

<span class="line"><span style="color:#24292E"> (</span><span style="color:#032F62">"measurement-001"</span><span style="color:#24292E">,)).fetchall()</span></span>

<span class="line"><span style="color:#005CC5"> print</span><span style="color:#24292E">(rows)</span></span>

<span class="line"><span style="color:#D73A49"> return</span><span style="color:#24292E"> {</span><span style="color:#032F62">"rows"</span><span style="color:#24292E">: rows}</span></span>

<span class="line"><span style="color:#D73A49"> finally</span><span style="color:#24292E">:</span></span>

<span class="line"><span style="color:#24292E"> connection.close()</span></span>

<span class="line"></span>

<span class="line"><span style="color:#D73A49">if</span><span style="color:#005CC5"> __name__</span><span style="color:#D73A49"> ==</span><span style="color:#032F62"> "__main__"</span><span style="color:#24292E">:</span></span>

<span class="line"><span style="color:#24292E"> main({})</span></span>

<span class="line"></span></code></pre></div>

<p class="doc-step-result"><strong>Expected result</strong> Console shows [(&#39;measurement-001&#39;, 13.0)]: INSERT stores 12.5, UPDATE changes it to 13.0, and SELECT reads it back.</p>

</article>

<article class="doc-card">

<h3 id="python-sql-item-3">2. Prepare the external business table</h3>

<p>Have the maintainer create the table in a dedicated business database and grant the script account access. Reuse record_id for the same record; allocate a new ID for a new record.</p>

<ol>

<li>Obtain the business database host, port, database name, script account, and CA file from its administrator. Confirm the target is the external business database.</li>

<li>Execute the CREATE TABLE statement once in that database’s SQL client, not in the Python editor.</li>

<li>Verify that integration_measurements exists and the script account can SELECT, INSERT, and UPDATE it.</li>

<li>Choose a record_id for this exercise and use it consistently for insert, query, and update. Do not store the literal &lt;record-id&gt; placeholder.</li>

</ol>

<div class="manual-code" data-code-language="sql"><div class="manual-code-toolbar"><span>SQL</span><span class="manual-code-tools"><small>SQL</small><button type="button" data-copy-code data-copied="Copied" data-failed="Select and copy the code" aria-label="Copy code: SQL">Copy code</button><span class="manual-code-status" role="status"></span></span></div><pre tabindex="0" class="docs-code-block shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#D73A49">CREATE</span><span style="color:#D73A49"> TABLE</span><span style="color:#6F42C1"> integration_measurements</span><span style="color:#24292E"> (</span></span>

<span class="line"><span style="color:#24292E"> record_id </span><span style="color:#D73A49">VARCHAR</span><span style="color:#24292E">(</span><span style="color:#005CC5">128</span><span style="color:#24292E">) </span><span style="color:#D73A49">PRIMARY KEY</span><span style="color:#24292E">,</span></span>

<span class="line"><span style="color:#24292E"> tag_ref </span><span style="color:#D73A49">VARCHAR</span><span style="color:#24292E">(</span><span style="color:#005CC5">512</span><span style="color:#24292E">) </span><span style="color:#D73A49">NOT NULL</span><span style="color:#24292E">,</span></span>

<span class="line"><span style="color:#D73A49"> value</span><span style="color:#D73A49"> DOUBLE PRECISION</span><span style="color:#D73A49"> NOT NULL</span></span>

<span class="line"><span style="color:#24292E">);</span></span></code></pre></div>

<p class="doc-step-result"><strong>Expected result</strong> The business table exists with the three columns shown. No measurement has been written yet.</p>

</article>

<article class="doc-card">

<h3 id="python-sql-item-6">3. Configure the address, certificate, and password</h3>

<p>Store the database password in Secrets under Settings → Python → Python Packages → Advanced Project Settings. The configuration below reads that explicitly configured Secret through os.environ.</p>

<ol>

<li>Select the project, expand Secrets, enter DATABASE_PASSWORD as the name and the database password as the value, then click Create Secret.</li>

<li>Confirm DATABASE_PASSWORD appears in the list. Its value is not shown again; do not put it in ordinary environment variables or logs.</li>

<li>Click Save and Apply Configuration so a new runtime version uses this configuration revision. Creating or rotating a Secret does not change existing published versions.</li>

<li>On Code, create business_config under common/ and copy the template below. Keep the configuration for your database branch.</li>

<li>Replace host, database name, user, and CA path. Typical ports are 5432 for PostgreSQL and 3306 for MySQL; use your deployed values. A container must be able to read its CA path.</li>

<li>Save the file. The code reads the password from DATABASE_PASSWORD; do not hardcode it.</li>

</ol>

<div class="manual-code" data-code-language="python"><div class="manual-code-toolbar"><span>common/business_config.py</span><span class="manual-code-tools"><small>Python</small><button type="button" data-copy-code data-copied="Copied" data-failed="Select and copy the code" aria-label="Copy code: common/business_config.py">Copy code</button><span class="manual-code-status" role="status"></span></span></div><pre tabindex="0" class="docs-code-block shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#6A737D"># common/business_config.py — configuration template, no real credentials</span></span>

<span class="line"><span style="color:#D73A49">import</span><span style="color:#24292E"> os</span></span>

<span class="line"></span>

<span class="line"><span style="color:#005CC5">POSTGRES_CONFIG</span><span style="color:#D73A49"> =</span><span style="color:#24292E"> {</span></span>

<span class="line"><span style="color:#032F62"> "host"</span><span style="color:#24292E">: </span><span style="color:#032F62">"&#x3C;database-host>"</span><span style="color:#24292E">, </span><span style="color:#032F62">"port"</span><span style="color:#24292E">: </span><span style="color:#005CC5">5432</span><span style="color:#24292E">,</span></span>

<span class="line"><span style="color:#032F62"> "dbname"</span><span style="color:#24292E">: </span><span style="color:#032F62">"&#x3C;database-name>"</span><span style="color:#24292E">, </span><span style="color:#032F62">"user"</span><span style="color:#24292E">: </span><span style="color:#032F62">"&#x3C;database-user>"</span><span style="color:#24292E">,</span></span>

<span class="line"><span style="color:#032F62"> "password"</span><span style="color:#24292E">: os.environ[</span><span style="color:#032F62">"DATABASE_PASSWORD"</span><span style="color:#24292E">], </span><span style="color:#032F62">"sslmode"</span><span style="color:#24292E">: </span><span style="color:#032F62">"verify-full"</span><span style="color:#24292E">,</span></span>

<span class="line"><span style="color:#032F62"> "sslrootcert"</span><span style="color:#24292E">: </span><span style="color:#032F62">"&#x3C;ca-file-path>"</span><span style="color:#24292E">,</span></span>

<span class="line"><span style="color:#24292E">}</span></span>

<span class="line"><span style="color:#005CC5">MYSQL_CONFIG</span><span style="color:#D73A49"> =</span><span style="color:#24292E"> {</span></span>

<span class="line"><span style="color:#032F62"> "host"</span><span style="color:#24292E">: </span><span style="color:#032F62">"&#x3C;database-host>"</span><span style="color:#24292E">, </span><span style="color:#032F62">"port"</span><span style="color:#24292E">: </span><span style="color:#005CC5">3306</span><span style="color:#24292E">,</span></span>

<span class="line"><span style="color:#032F62"> "database"</span><span style="color:#24292E">: </span><span style="color:#032F62">"&#x3C;database-name>"</span><span style="color:#24292E">, </span><span style="color:#032F62">"user"</span><span style="color:#24292E">: </span><span style="color:#032F62">"&#x3C;database-user>"</span><span style="color:#24292E">,</span></span>

<span class="line"><span style="color:#032F62"> "password"</span><span style="color:#24292E">: os.environ[</span><span style="color:#032F62">"DATABASE_PASSWORD"</span><span style="color:#24292E">], </span><span style="color:#032F62">"charset"</span><span style="color:#24292E">: </span><span style="color:#032F62">"utf8mb4"</span><span style="color:#24292E">,</span></span>

<span class="line"><span style="color:#032F62"> "ssl_ca"</span><span style="color:#24292E">: </span><span style="color:#032F62">"&#x3C;ca-file-path>"</span><span style="color:#24292E">, </span><span style="color:#032F62">"ssl_verify_cert"</span><span style="color:#24292E">: </span><span style="color:#005CC5">True</span><span style="color:#24292E">,</span></span>

<span class="line"><span style="color:#032F62"> "ssl_verify_identity"</span><span style="color:#24292E">: </span><span style="color:#005CC5">True</span><span style="color:#24292E">,</span></span>

<span class="line"><span style="color:#24292E">}</span></span>

<span class="line"></span></code></pre></div>

<p class="doc-step-result"><strong>Expected result</strong> Confirm the Secret, replaced placeholders, and applied configuration. Verify access with a read-only query before inserting.</p>

<div class="doc-step-troubleshooting"><p><strong>If the result differs</strong></p><ul>

<li>KeyError: DATABASE_PASSWORD: check the selected project, exact Secret name, save result, and configuration version in use.</li>

<li>Certificate error: verify the host against the certificate name, CA path, and file permissions; retain certificate verification.</li>

</ul></div>

</article>

<article class="doc-card">

<h3 id="python-sql-item-4">4A. Save the PostgreSQL insert function</h3>

<p>Save as common/postgres_store.py. config supplies host, port, dbname, user, and password, plus deployment-required sslmode/sslrootcert. The connection context commits on normal exit, rolls back on exceptions, and closes the connection. Parameters are passed separately from SQL text.</p>

<ol>

<li>Run the dependency probe and verify that psycopg imports successfully.</li>

<li>Create postgres_store under common/, copy the complete function below, and save.</li>

<li>This file only defines database functions; later entrypoints import and call them.</li>

</ol>

<div class="manual-code" data-code-language="python"><div class="manual-code-toolbar"><span>common/postgres_store.py</span><span class="manual-code-tools"><small>Python</small><button type="button" data-copy-code data-copied="Copied" data-failed="Select and copy the code" aria-label="Copy code: common/postgres_store.py">Copy code</button><span class="manual-code-status" role="status"></span></span></div><pre tabindex="0" class="docs-code-block shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#6A737D"># common/postgres_store.py</span></span>

<span class="line"><span style="color:#D73A49">import</span><span style="color:#24292E"> psycopg</span></span>

<span class="line"></span>

<span class="line"><span style="color:#D73A49">def</span><span style="color:#6F42C1"> save_measurement</span><span style="color:#24292E">(config, record_id, tag_ref, value):</span></span>

<span class="line"><span style="color:#D73A49"> with</span><span style="color:#24292E"> psycopg.connect(</span><span style="color:#D73A49">**</span><span style="color:#24292E">config, </span><span style="color:#E36209">connect_timeout</span><span style="color:#D73A49">=</span><span style="color:#005CC5">5</span><span style="color:#24292E">,</span></span>

<span class="line"><span style="color:#E36209"> options</span><span style="color:#D73A49">=</span><span style="color:#032F62">"-c statement_timeout=5000"</span><span style="color:#24292E">) </span><span style="color:#D73A49">as</span><span style="color:#24292E"> connection:</span></span>

<span class="line"><span style="color:#D73A49"> with</span><span style="color:#24292E"> connection.cursor() </span><span style="color:#D73A49">as</span><span style="color:#24292E"> cursor:</span></span>

<span class="line"><span style="color:#24292E"> cursor.execute(</span></span>

<span class="line"><span style="color:#032F62"> "INSERT INTO integration_measurements (record_id, tag_ref, value) "</span></span>

<span class="line"><span style="color:#032F62"> "VALUES (</span><span style="color:#005CC5">%s</span><span style="color:#032F62">, </span><span style="color:#005CC5">%s</span><span style="color:#032F62">, </span><span style="color:#005CC5">%s</span><span style="color:#032F62">) ON CONFLICT (record_id) DO NOTHING"</span><span style="color:#24292E">,</span></span>

<span class="line"><span style="color:#24292E"> (record_id, tag_ref, value),</span></span>

<span class="line"><span style="color:#24292E"> )</span></span>

<span class="line"><span style="color:#24292E"> cursor.execute(</span></span>

<span class="line"><span style="color:#032F62"> "SELECT tag_ref, value FROM integration_measurements WHERE record_id = </span><span style="color:#005CC5">%s</span><span style="color:#032F62">"</span><span style="color:#24292E">,</span></span>

<span class="line"><span style="color:#24292E"> (record_id,),</span></span>

<span class="line"><span style="color:#24292E"> )</span></span>

<span class="line"><span style="color:#24292E"> stored </span><span style="color:#D73A49">=</span><span style="color:#24292E"> cursor.fetchone()</span></span>

<span class="line"><span style="color:#D73A49"> if</span><span style="color:#24292E"> stored </span><span style="color:#D73A49">!=</span><span style="color:#24292E"> (tag_ref, value):</span></span>

<span class="line"><span style="color:#D73A49"> raise</span><span style="color:#005CC5"> ValueError</span><span style="color:#24292E">(</span><span style="color:#032F62">"Record ID already exists with a different payload"</span><span style="color:#24292E">)</span></span>

<span class="line"><span style="color:#D73A49"> return</span><span style="color:#24292E"> {</span><span style="color:#032F62">"record_id"</span><span style="color:#24292E">: record_id, </span><span style="color:#032F62">"tag_ref"</span><span style="color:#24292E">: stored[</span><span style="color:#005CC5">0</span><span style="color:#24292E">], </span><span style="color:#032F62">"value"</span><span style="color:#24292E">: stored[</span><span style="color:#005CC5">1</span><span style="color:#24292E">]}</span></span>

<span class="line"></span></code></pre></div>

<p class="doc-step-result"><strong>Expected result</strong> Use the PostgreSQL module names and configuration constants in subsequent entrypoints.</p>

</article>

<article class="doc-card">

<h3 id="sql-postgres-query-update">4A continued: query and update functions</h3>

<p>For PostgreSQL, append this code after save_measurement in common/postgres_store.py. Keep import psycopg at the top.</p>

<div class="manual-code" data-code-language="python"><div class="manual-code-toolbar"><span>common/postgres_store.py</span><span class="manual-code-tools"><small>Python</small><button type="button" data-copy-code data-copied="Copied" data-failed="Select and copy the code" aria-label="Copy code: common/postgres_store.py">Copy code</button><span class="manual-code-status" role="status"></span></span></div><pre tabindex="0" class="docs-code-block shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#6A737D"># Append to common/postgres_store.py; psycopg is imported above.</span></span>

<span class="line"><span style="color:#D73A49">def</span><span style="color:#6F42C1"> list_measurements</span><span style="color:#24292E">(config, after</span><span style="color:#D73A49">=</span><span style="color:#032F62">""</span><span style="color:#24292E">):</span></span>

<span class="line"><span style="color:#D73A49"> with</span><span style="color:#24292E"> psycopg.connect(</span><span style="color:#D73A49">**</span><span style="color:#24292E">config, </span><span style="color:#E36209">connect_timeout</span><span style="color:#D73A49">=</span><span style="color:#005CC5">5</span><span style="color:#24292E">,</span></span>

<span class="line"><span style="color:#E36209"> options</span><span style="color:#D73A49">=</span><span style="color:#032F62">"-c statement_timeout=5000"</span><span style="color:#24292E">) </span><span style="color:#D73A49">as</span><span style="color:#24292E"> connection:</span></span>

<span class="line"><span style="color:#D73A49"> with</span><span style="color:#24292E"> connection.cursor() </span><span style="color:#D73A49">as</span><span style="color:#24292E"> cursor:</span></span>

<span class="line"><span style="color:#24292E"> cursor.execute(</span></span>

<span class="line"><span style="color:#032F62"> "SELECT record_id, tag_ref, value FROM integration_measurements "</span></span>

<span class="line"><span style="color:#032F62"> "WHERE record_id > </span><span style="color:#005CC5">%s</span><span style="color:#032F62"> ORDER BY record_id LIMIT 100"</span><span style="color:#24292E">, (after,)</span></span>

<span class="line"><span style="color:#24292E"> )</span></span>

<span class="line"><span style="color:#D73A49"> return</span><span style="color:#24292E"> cursor.fetchall()</span></span>

<span class="line"></span>

<span class="line"><span style="color:#D73A49">def</span><span style="color:#6F42C1"> update_measurement</span><span style="color:#24292E">(config, record_id, value):</span></span>

<span class="line"><span style="color:#D73A49"> with</span><span style="color:#24292E"> psycopg.connect(</span><span style="color:#D73A49">**</span><span style="color:#24292E">config, </span><span style="color:#E36209">connect_timeout</span><span style="color:#D73A49">=</span><span style="color:#005CC5">5</span><span style="color:#24292E">,</span></span>

<span class="line"><span style="color:#E36209"> options</span><span style="color:#D73A49">=</span><span style="color:#032F62">"-c statement_timeout=5000"</span><span style="color:#24292E">) </span><span style="color:#D73A49">as</span><span style="color:#24292E"> connection:</span></span>

<span class="line"><span style="color:#D73A49"> with</span><span style="color:#24292E"> connection.cursor() </span><span style="color:#D73A49">as</span><span style="color:#24292E"> cursor:</span></span>

<span class="line"><span style="color:#24292E"> cursor.execute(</span></span>

<span class="line"><span style="color:#032F62"> "SELECT record_id FROM integration_measurements "</span></span>

<span class="line"><span style="color:#032F62"> "WHERE record_id = </span><span style="color:#005CC5">%s</span><span style="color:#032F62"> FOR UPDATE"</span><span style="color:#24292E">, (record_id,)</span></span>

<span class="line"><span style="color:#24292E"> )</span></span>

<span class="line"><span style="color:#D73A49"> if</span><span style="color:#24292E"> cursor.fetchone() </span><span style="color:#D73A49">is</span><span style="color:#005CC5"> None</span><span style="color:#24292E">:</span></span>

<span class="line"><span style="color:#D73A49"> raise</span><span style="color:#005CC5"> ValueError</span><span style="color:#24292E">(</span><span style="color:#032F62">"Record does not exist"</span><span style="color:#24292E">)</span></span>

<span class="line"><span style="color:#24292E"> cursor.execute(</span></span>

<span class="line"><span style="color:#032F62"> "UPDATE integration_measurements SET value = </span><span style="color:#005CC5">%s</span><span style="color:#032F62"> WHERE record_id = </span><span style="color:#005CC5">%s</span><span style="color:#032F62">"</span><span style="color:#24292E">,</span></span>

<span class="line"><span style="color:#24292E"> (value, record_id),</span></span>

<span class="line"><span style="color:#24292E"> )</span></span>

<span class="line"><span style="color:#D73A49"> return</span><span style="color:#24292E"> {</span><span style="color:#032F62">"record_id"</span><span style="color:#24292E">: record_id, </span><span style="color:#032F62">"value"</span><span style="color:#24292E">: value}</span></span>

<span class="line"></span></code></pre></div>

<p class="doc-step-result"><strong>Expected result</strong> The file now defines save_measurement, list_measurements, and update_measurement.</p>

</article>

<article class="doc-card">

<h3 id="python-sql-item-5">4B. Save the MySQL insert function</h3>

<p>Save as common/mysql_store.py. config supplies host, port, database, user, password, charset=&quot;utf8mb4&quot;, and required TLS settings. The function explicitly commits or rolls back and closes the connection. It uses the default tuple cursor.</p>

<ol>

<li>Run the dependency probe and verify that pymysql imports successfully.</li>

<li>Create mysql_store under common/, copy the complete function below, and save.</li>

<li>This file only defines database functions; later entrypoints import and call them.</li>

</ol>

<div class="manual-code" data-code-language="python"><div class="manual-code-toolbar"><span>common/mysql_store.py</span><span class="manual-code-tools"><small>Python</small><button type="button" data-copy-code data-copied="Copied" data-failed="Select and copy the code" aria-label="Copy code: common/mysql_store.py">Copy code</button><span class="manual-code-status" role="status"></span></span></div><pre tabindex="0" class="docs-code-block shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#6A737D"># common/mysql_store.py</span></span>

<span class="line"><span style="color:#D73A49">import</span><span style="color:#24292E"> pymysql</span></span>

<span class="line"></span>

<span class="line"><span style="color:#D73A49">def</span><span style="color:#6F42C1"> save_measurement</span><span style="color:#24292E">(config, record_id, tag_ref, value):</span></span>

<span class="line"><span style="color:#24292E"> connection </span><span style="color:#D73A49">=</span><span style="color:#24292E"> pymysql.connect(</span><span style="color:#D73A49">**</span><span style="color:#24292E">config, </span><span style="color:#E36209">connect_timeout</span><span style="color:#D73A49">=</span><span style="color:#005CC5">5</span><span style="color:#24292E">, </span><span style="color:#E36209">read_timeout</span><span style="color:#D73A49">=</span><span style="color:#005CC5">5</span><span style="color:#24292E">,</span></span>

<span class="line"><span style="color:#E36209"> write_timeout</span><span style="color:#D73A49">=</span><span style="color:#005CC5">5</span><span style="color:#24292E">, </span><span style="color:#E36209">autocommit</span><span style="color:#D73A49">=</span><span style="color:#005CC5">False</span><span style="color:#24292E">)</span></span>

<span class="line"><span style="color:#D73A49"> try</span><span style="color:#24292E">:</span></span>

<span class="line"><span style="color:#D73A49"> with</span><span style="color:#24292E"> connection.cursor() </span><span style="color:#D73A49">as</span><span style="color:#24292E"> cursor:</span></span>

<span class="line"><span style="color:#24292E"> cursor.execute(</span></span>

<span class="line"><span style="color:#032F62"> "INSERT INTO integration_measurements (record_id, tag_ref, value) "</span></span>

<span class="line"><span style="color:#032F62"> "VALUES (</span><span style="color:#005CC5">%s</span><span style="color:#032F62">, </span><span style="color:#005CC5">%s</span><span style="color:#032F62">, </span><span style="color:#005CC5">%s</span><span style="color:#032F62">) ON DUPLICATE KEY UPDATE record_id = record_id"</span><span style="color:#24292E">,</span></span>

<span class="line"><span style="color:#24292E"> (record_id, tag_ref, value),</span></span>

<span class="line"><span style="color:#24292E"> )</span></span>

<span class="line"><span style="color:#24292E"> cursor.execute(</span></span>

<span class="line"><span style="color:#032F62"> "SELECT tag_ref, value FROM integration_measurements WHERE record_id = </span><span style="color:#005CC5">%s</span><span style="color:#032F62">"</span><span style="color:#24292E">,</span></span>

<span class="line"><span style="color:#24292E"> (record_id,),</span></span>

<span class="line"><span style="color:#24292E"> )</span></span>

<span class="line"><span style="color:#24292E"> stored </span><span style="color:#D73A49">=</span><span style="color:#24292E"> cursor.fetchone()</span></span>

<span class="line"><span style="color:#D73A49"> if</span><span style="color:#24292E"> stored </span><span style="color:#D73A49">!=</span><span style="color:#24292E"> (tag_ref, value):</span></span>

<span class="line"><span style="color:#D73A49"> raise</span><span style="color:#005CC5"> ValueError</span><span style="color:#24292E">(</span><span style="color:#032F62">"Record ID already exists with a different payload"</span><span style="color:#24292E">)</span></span>

<span class="line"><span style="color:#24292E"> connection.commit()</span></span>

<span class="line"><span style="color:#D73A49"> return</span><span style="color:#24292E"> {</span><span style="color:#032F62">"record_id"</span><span style="color:#24292E">: record_id, </span><span style="color:#032F62">"tag_ref"</span><span style="color:#24292E">: stored[</span><span style="color:#005CC5">0</span><span style="color:#24292E">], </span><span style="color:#032F62">"value"</span><span style="color:#24292E">: stored[</span><span style="color:#005CC5">1</span><span style="color:#24292E">]}</span></span>

<span class="line"><span style="color:#D73A49"> except</span><span style="color:#005CC5"> Exception</span><span style="color:#24292E">:</span></span>

<span class="line"><span style="color:#24292E"> connection.rollback()</span></span>

<span class="line"><span style="color:#D73A49"> raise</span></span>

<span class="line"><span style="color:#D73A49"> finally</span><span style="color:#24292E">:</span></span>

<span class="line"><span style="color:#24292E"> connection.close()</span></span>

<span class="line"></span></code></pre></div>

<p class="doc-step-result"><strong>Expected result</strong> Use the MySQL module names and configuration constants in subsequent entrypoints.</p>

</article>

<article class="doc-card">

<h3 id="sql-mysql-query-update">4B continued: query and update functions</h3>

<p>For MySQL/MariaDB, append this code after save_measurement in common/mysql_store.py. Keep import pymysql. The query always closes its connection; an update failure rolls back.</p>

<div class="manual-code" data-code-language="python"><div class="manual-code-toolbar"><span>common/mysql_store.py</span><span class="manual-code-tools"><small>Python</small><button type="button" data-copy-code data-copied="Copied" data-failed="Select and copy the code" aria-label="Copy code: common/mysql_store.py">Copy code</button><span class="manual-code-status" role="status"></span></span></div><pre tabindex="0" class="docs-code-block shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#6A737D"># Append to common/mysql_store.py; pymysql is imported above.</span></span>

<span class="line"><span style="color:#D73A49">def</span><span style="color:#6F42C1"> list_measurements</span><span style="color:#24292E">(config, after</span><span style="color:#D73A49">=</span><span style="color:#032F62">""</span><span style="color:#24292E">):</span></span>

<span class="line"><span style="color:#24292E"> connection </span><span style="color:#D73A49">=</span><span style="color:#24292E"> pymysql.connect(</span><span style="color:#D73A49">**</span><span style="color:#24292E">config, </span><span style="color:#E36209">connect_timeout</span><span style="color:#D73A49">=</span><span style="color:#005CC5">5</span><span style="color:#24292E">,</span></span>

<span class="line"><span style="color:#E36209"> read_timeout</span><span style="color:#D73A49">=</span><span style="color:#005CC5">5</span><span style="color:#24292E">, </span><span style="color:#E36209">write_timeout</span><span style="color:#D73A49">=</span><span style="color:#005CC5">5</span><span style="color:#24292E">, </span><span style="color:#E36209">autocommit</span><span style="color:#D73A49">=</span><span style="color:#005CC5">False</span><span style="color:#24292E">)</span></span>

<span class="line"><span style="color:#D73A49"> try</span><span style="color:#24292E">:</span></span>

<span class="line"><span style="color:#D73A49"> with</span><span style="color:#24292E"> connection.cursor() </span><span style="color:#D73A49">as</span><span style="color:#24292E"> cursor:</span></span>

<span class="line"><span style="color:#24292E"> cursor.execute(</span></span>

<span class="line"><span style="color:#032F62"> "SELECT record_id, tag_ref, value FROM integration_measurements "</span></span>

<span class="line"><span style="color:#032F62"> "WHERE record_id > </span><span style="color:#005CC5">%s</span><span style="color:#032F62"> ORDER BY record_id LIMIT 100"</span><span style="color:#24292E">, (after,)</span></span>

<span class="line"><span style="color:#24292E"> )</span></span>

<span class="line"><span style="color:#D73A49"> return</span><span style="color:#24292E"> cursor.fetchall()</span></span>

<span class="line"><span style="color:#D73A49"> finally</span><span style="color:#24292E">:</span></span>

<span class="line"><span style="color:#24292E"> connection.close()</span></span>

<span class="line"></span>

<span class="line"><span style="color:#D73A49">def</span><span style="color:#6F42C1"> update_measurement</span><span style="color:#24292E">(config, record_id, value):</span></span>

<span class="line"><span style="color:#24292E"> connection </span><span style="color:#D73A49">=</span><span style="color:#24292E"> pymysql.connect(</span><span style="color:#D73A49">**</span><span style="color:#24292E">config, </span><span style="color:#E36209">connect_timeout</span><span style="color:#D73A49">=</span><span style="color:#005CC5">5</span><span style="color:#24292E">,</span></span>

<span class="line"><span style="color:#E36209"> read_timeout</span><span style="color:#D73A49">=</span><span style="color:#005CC5">5</span><span style="color:#24292E">, </span><span style="color:#E36209">write_timeout</span><span style="color:#D73A49">=</span><span style="color:#005CC5">5</span><span style="color:#24292E">, </span><span style="color:#E36209">autocommit</span><span style="color:#D73A49">=</span><span style="color:#005CC5">False</span><span style="color:#24292E">)</span></span>

<span class="line"><span style="color:#D73A49"> try</span><span style="color:#24292E">:</span></span>

<span class="line"><span style="color:#D73A49"> with</span><span style="color:#24292E"> connection.cursor() </span><span style="color:#D73A49">as</span><span style="color:#24292E"> cursor:</span></span>

<span class="line"><span style="color:#24292E"> cursor.execute(</span></span>

<span class="line"><span style="color:#032F62"> "SELECT record_id FROM integration_measurements "</span></span>

<span class="line"><span style="color:#032F62"> "WHERE record_id = </span><span style="color:#005CC5">%s</span><span style="color:#032F62"> FOR UPDATE"</span><span style="color:#24292E">, (record_id,)</span></span>

<span class="line"><span style="color:#24292E"> )</span></span>

<span class="line"><span style="color:#D73A49"> if</span><span style="color:#24292E"> cursor.fetchone() </span><span style="color:#D73A49">is</span><span style="color:#005CC5"> None</span><span style="color:#24292E">:</span></span>

<span class="line"><span style="color:#D73A49"> raise</span><span style="color:#005CC5"> ValueError</span><span style="color:#24292E">(</span><span style="color:#032F62">"Record does not exist"</span><span style="color:#24292E">)</span></span>

<span class="line"><span style="color:#24292E"> cursor.execute(</span></span>

<span class="line"><span style="color:#032F62"> "UPDATE integration_measurements SET value = </span><span style="color:#005CC5">%s</span><span style="color:#032F62"> WHERE record_id = </span><span style="color:#005CC5">%s</span><span style="color:#032F62">"</span><span style="color:#24292E">,</span></span>

<span class="line"><span style="color:#24292E"> (value, record_id),</span></span>

<span class="line"><span style="color:#24292E"> )</span></span>

<span class="line"><span style="color:#24292E"> connection.commit()</span></span>

<span class="line"><span style="color:#D73A49"> return</span><span style="color:#24292E"> {</span><span style="color:#032F62">"record_id"</span><span style="color:#24292E">: record_id, </span><span style="color:#032F62">"value"</span><span style="color:#24292E">: value}</span></span>

<span class="line"><span style="color:#D73A49"> except</span><span style="color:#005CC5"> Exception</span><span style="color:#24292E">:</span></span>

<span class="line"><span style="color:#24292E"> connection.rollback()</span></span>

<span class="line"><span style="color:#D73A49"> raise</span></span>

<span class="line"><span style="color:#D73A49"> finally</span><span style="color:#24292E">:</span></span>

<span class="line"><span style="color:#24292E"> connection.close()</span></span>

<span class="line"></span></code></pre></div>

<p class="doc-step-result"><strong>Expected result</strong> The file now defines save_measurement, list_measurements, and update_measurement.</p>

</article>

<article class="doc-card">

<h3 id="python-sql-item-9">5. Run a read-only query first</h3>

<p>Create scripts/read_measurements.py, copy the code, adjust both imports for your database, save, and run. An empty-table result [] also confirms access.</p>

<ol>

<li>Keep postgres_store and POSTGRES_CONFIG for PostgreSQL; change both to mysql_store and MYSQL_CONFIG for MySQL.</li>

<li>Inspect rows. Each page contains at most 100 records ordered by record_id.</li>

<li>For the next page, change the entrypoint’s after from an empty string to the returned next_after and run again. An empty page ends traversal; this order is not acquisition-time order.</li>

</ol>

<div class="manual-code" data-code-language="python"><div class="manual-code-toolbar"><span>scripts/read_measurements.py</span><span class="manual-code-tools"><small>Python</small><button type="button" data-copy-code data-copied="Copied" data-failed="Select and copy the code" aria-label="Copy code: scripts/read_measurements.py">Copy code</button><span class="manual-code-status" role="status"></span></span></div><pre tabindex="0" class="docs-code-block shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#6A737D"># scripts/read_measurements.py</span></span>

<span class="line"><span style="color:#D73A49">from</span><span style="color:#24292E"> ..common.postgres_store </span><span style="color:#D73A49">import</span><span style="color:#24292E"> list_measurements</span></span>

<span class="line"><span style="color:#D73A49">from</span><span style="color:#24292E"> ..common.business_config </span><span style="color:#D73A49">import</span><span style="color:#005CC5"> POSTGRES_CONFIG</span><span style="color:#D73A49"> as</span><span style="color:#005CC5"> DATABASE_CONFIG</span></span>

<span class="line"><span style="color:#6A737D"># For MySQL, change postgres_store to mysql_store and POSTGRES_CONFIG to MYSQL_CONFIG.</span></span>

<span class="line"></span>

<span class="line"><span style="color:#D73A49">def</span><span style="color:#6F42C1"> main</span><span style="color:#24292E">(ctx):</span></span>

<span class="line"><span style="color:#24292E"> rows </span><span style="color:#D73A49">=</span><span style="color:#24292E"> list_measurements(</span><span style="color:#005CC5">DATABASE_CONFIG</span><span style="color:#24292E">, ctx.get(</span><span style="color:#032F62">"after"</span><span style="color:#24292E">, </span><span style="color:#032F62">""</span><span style="color:#24292E">))</span></span>

<span class="line"><span style="color:#005CC5"> print</span><span style="color:#24292E">(rows)</span></span>

<span class="line"><span style="color:#D73A49"> return</span><span style="color:#24292E"> {</span><span style="color:#032F62">"rows"</span><span style="color:#24292E">: rows, </span><span style="color:#032F62">"next_after"</span><span style="color:#24292E">: rows[</span><span style="color:#D73A49">-</span><span style="color:#005CC5">1</span><span style="color:#24292E">][</span><span style="color:#005CC5">0</span><span style="color:#24292E">] </span><span style="color:#D73A49">if</span><span style="color:#24292E"> rows </span><span style="color:#D73A49">else</span><span style="color:#005CC5"> None</span><span style="color:#24292E">}</span></span>

<span class="line"></span>

<span class="line"><span style="color:#D73A49">if</span><span style="color:#005CC5"> __name__</span><span style="color:#D73A49"> ==</span><span style="color:#032F62"> "__main__"</span><span style="color:#24292E">:</span></span>

<span class="line"><span style="color:#24292E"> main({</span><span style="color:#032F62">"after"</span><span style="color:#24292E">: </span><span style="color:#032F62">""</span><span style="color:#24292E">})</span></span>

<span class="line"></span></code></pre></div>

<p class="doc-step-result"><strong>Expected result</strong> The result is [] or a list of existing records, without connection, authentication, or table-permission errors.</p>

</article>

<article class="doc-card">

<h3 id="python-sql-item-7">6. Insert one business record</h3>

<p>Save as scripts/store_measurement.py; replace the record ID, TagRef, and both database imports. Run Test writes value=12.5 to the configured database. First confirm that the target is the business table used for this exercise. Event calls to main(ctx) pass record_id, tag_ref, and value.</p>

<ol>

<li>Create store_measurement under scripts/, copy the complete code, and select the same database branch as the query script.</li>

<li>Replace &lt;record-id&gt; and tag_ref at the bottom; keep value=12.5.</li>

<li>Select store_measurement.py and execute Run Test once.</li>

<li>Run read_measurements.py again and find that record_id with the expected tag_ref and value=12.5.</li>

</ol>

<div class="manual-code" data-code-language="python"><div class="manual-code-toolbar"><span>scripts/store_measurement.py</span><span class="manual-code-tools"><small>Python</small><button type="button" data-copy-code data-copied="Copied" data-failed="Select and copy the code" aria-label="Copy code: scripts/store_measurement.py">Copy code</button><span class="manual-code-status" role="status"></span></span></div><pre tabindex="0" class="docs-code-block shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#6A737D"># scripts/store_measurement.py</span></span>

<span class="line"><span style="color:#D73A49">from</span><span style="color:#24292E"> ..common.postgres_store </span><span style="color:#D73A49">import</span><span style="color:#24292E"> save_measurement</span></span>

<span class="line"><span style="color:#D73A49">from</span><span style="color:#24292E"> ..common.business_config </span><span style="color:#D73A49">import</span><span style="color:#005CC5"> POSTGRES_CONFIG</span><span style="color:#D73A49"> as</span><span style="color:#005CC5"> DATABASE_CONFIG</span></span>

<span class="line"><span style="color:#6A737D"># For MySQL, use mysql_store and MYSQL_CONFIG instead.</span></span>

<span class="line"></span>

<span class="line"><span style="color:#D73A49">def</span><span style="color:#6F42C1"> main</span><span style="color:#24292E">(ctx):</span></span>

<span class="line"><span style="color:#D73A49"> return</span><span style="color:#24292E"> save_measurement(</span></span>

<span class="line"><span style="color:#005CC5"> DATABASE_CONFIG</span><span style="color:#24292E">, ctx[</span><span style="color:#032F62">"record_id"</span><span style="color:#24292E">], ctx[</span><span style="color:#032F62">"tag_ref"</span><span style="color:#24292E">], </span><span style="color:#005CC5">float</span><span style="color:#24292E">(ctx[</span><span style="color:#032F62">"value"</span><span style="color:#24292E">])</span></span>

<span class="line"><span style="color:#24292E"> )</span></span>

<span class="line"></span>

<span class="line"><span style="color:#D73A49">if</span><span style="color:#005CC5"> __name__</span><span style="color:#D73A49"> ==</span><span style="color:#032F62"> "__main__"</span><span style="color:#24292E">:</span></span>

<span class="line"><span style="color:#24292E"> main({</span><span style="color:#032F62">"record_id"</span><span style="color:#24292E">: </span><span style="color:#032F62">"&#x3C;record-id>"</span><span style="color:#24292E">,</span></span>

<span class="line"><span style="color:#032F62"> "tag_ref"</span><span style="color:#24292E">: </span><span style="color:#032F62">"hq://io/&#x3C;io-domain-id>/&#x3C;device-uid>/&#x3C;tag-uid>"</span><span style="color:#24292E">, </span><span style="color:#032F62">"value"</span><span style="color:#24292E">: </span><span style="color:#005CC5">12.5</span><span style="color:#24292E">})</span></span>

<span class="line"></span></code></pre></div>

<p class="doc-step-result"><strong>Expected result</strong> The query script reads the new record. Repeating the same ID and content does not add a row; reusing the ID with different content raises an error.</p>

</article>

<article class="doc-card">

<h3 id="python-sql-item-8">7. Update the record and query again</h3>

<p>Create scripts/update_measurement.py. This example explicitly changes the previous record to 13.0. UPDATE has different semantics from repeating an INSERT.</p>

<ol>

<li>Adjust both imports for your database.</li>

<li>Replace &lt;record-id&gt; with the ID just inserted, save, and run once.</li>

<li>Run the read-only query again: the record should have value=13.0 without increasing the row count.</li>

<li>Try an ID that does not exist. Expect Record does not exist, and do not convert that error into success.</li>

</ol>

<div class="manual-code" data-code-language="python"><div class="manual-code-toolbar"><span>scripts/update_measurement.py</span><span class="manual-code-tools"><small>Python</small><button type="button" data-copy-code data-copied="Copied" data-failed="Select and copy the code" aria-label="Copy code: scripts/update_measurement.py">Copy code</button><span class="manual-code-status" role="status"></span></span></div><pre tabindex="0" class="docs-code-block shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#6A737D"># scripts/update_measurement.py</span></span>

<span class="line"><span style="color:#D73A49">from</span><span style="color:#24292E"> ..common.postgres_store </span><span style="color:#D73A49">import</span><span style="color:#24292E"> update_measurement</span></span>

<span class="line"><span style="color:#D73A49">from</span><span style="color:#24292E"> ..common.business_config </span><span style="color:#D73A49">import</span><span style="color:#005CC5"> POSTGRES_CONFIG</span><span style="color:#D73A49"> as</span><span style="color:#005CC5"> DATABASE_CONFIG</span></span>

<span class="line"><span style="color:#6A737D"># For MySQL, change postgres_store to mysql_store and POSTGRES_CONFIG to MYSQL_CONFIG.</span></span>

<span class="line"></span>

<span class="line"><span style="color:#D73A49">def</span><span style="color:#6F42C1"> main</span><span style="color:#24292E">(ctx):</span></span>

<span class="line"><span style="color:#24292E"> result </span><span style="color:#D73A49">=</span><span style="color:#24292E"> update_measurement(</span><span style="color:#005CC5">DATABASE_CONFIG</span><span style="color:#24292E">, ctx[</span><span style="color:#032F62">"record_id"</span><span style="color:#24292E">], </span><span style="color:#005CC5">float</span><span style="color:#24292E">(ctx[</span><span style="color:#032F62">"value"</span><span style="color:#24292E">]))</span></span>

<span class="line"><span style="color:#005CC5"> print</span><span style="color:#24292E">(result)</span></span>

<span class="line"><span style="color:#D73A49"> return</span><span style="color:#24292E"> result</span></span>

<span class="line"></span>

<span class="line"><span style="color:#D73A49">if</span><span style="color:#005CC5"> __name__</span><span style="color:#D73A49"> ==</span><span style="color:#032F62"> "__main__"</span><span style="color:#24292E">:</span></span>

<span class="line"><span style="color:#24292E"> main({</span><span style="color:#032F62">"record_id"</span><span style="color:#24292E">: </span><span style="color:#032F62">"&#x3C;record-id>"</span><span style="color:#24292E">, </span><span style="color:#032F62">"value"</span><span style="color:#24292E">: </span><span style="color:#005CC5">13.0</span><span style="color:#24292E">})</span></span>

<span class="line"></span></code></pre></div>

<p class="doc-step-result"><strong>Expected result</strong> The existing row is updated. A missing row raises an error and rolls back. Inserting the same ID with the old value=12.5 now reports a payload conflict.</p>

</article>

<article class="doc-card">

<h3 id="python-sql-item-10">Database connection and execution issues checks</h3>

<p>Verify connectivity, authentication, TLS, table permissions, parameters, and transactions before scheduling. Do not replace database exceptions with success results.</p>

<ul>

<li>Connection error: check host, port, DNS, certificates, and account from the actual Script service/container network. Keep TLS verification enabled.</li>

<li>Type error: verify SQL column types and Python values. Use explicit timezones and parameter binding.</li>

<li>Execution or commit timeout: a lost connection can leave a committed transaction. Query by record_id before deciding what to do next; do not retry blindly.</li>

<li>Verify duplicate business requests do not create duplicate rows, failures remain visible, and connections close.</li>

<li>A SQL transaction covers only its database. A PLC write and SQL commit are not a cross-system atomic transaction; reconcile them using business state.</li>

</ul>

</article>

</div>

</div>