<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sergio Majé</title>
    <description>The latest articles on DEV Community by Sergio Majé (@smaje99).</description>
    <link>https://dev.to/smaje99</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F424071%2F09fb8cfe-571e-47ed-9d69-7d10929df7cb.jpeg</url>
      <title>DEV Community: Sergio Majé</title>
      <link>https://dev.to/smaje99</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/smaje99"/>
    <language>en</language>
    <item>
      <title>Processing XML in PostgreSQL: Do You Really Need PL/Python?</title>
      <dc:creator>Sergio Majé</dc:creator>
      <pubDate>Thu, 15 Oct 2026 17:07:51 +0000</pubDate>
      <link>https://dev.to/smaje99/processing-xml-in-postgresql-do-you-really-need-plpython-554o</link>
      <guid>https://dev.to/smaje99/processing-xml-in-postgresql-do-you-really-need-plpython-554o</guid>
      <description>&lt;p&gt;In my &lt;a href="https://dev.to/smaje99/why-i-ended-up-running-python-inside-postgresql-to-validate-xml-ii1"&gt;previous article&lt;/a&gt;, I explained how a university assignment led me to run Python inside PostgreSQL to validate XML documents against an XSD.&lt;/p&gt;

&lt;p&gt;It worked. I completed the assignment, learned something new about PostgreSQL, and moved on.&lt;/p&gt;

&lt;p&gt;Two years later, I revisited the project with a different question in mind: &lt;strong&gt;how much of that solution actually needed Python?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;PostgreSQL already provides functions for generating XML, querying its contents, and extracting values into relational columns. What it doesn't provide natively is validation against an arbitrary XML Schema Definition (XSD).&lt;/p&gt;

&lt;p&gt;But while reviewing the original files, I found something I hadn't fully appreciated at the time.&lt;/p&gt;

&lt;p&gt;Some XML documents produced by the SQL query didn't satisfy the very XSD they were supposed to follow.&lt;/p&gt;

&lt;p&gt;That turned a simple comparison of database features into a more interesting engineering question. Before deciding where XML validation should happen, we need to know whether the document and its schema agree on what valid data looks like.&lt;/p&gt;

&lt;p&gt;Let's explore what PostgreSQL can do on its own, what PL/Python added to the original solution, and how I would approach the same decision today.&lt;/p&gt;

&lt;h2&gt;
  
  
  One assignment, two different problems
&lt;/h2&gt;

&lt;p&gt;Looking back at the assignment, I realized I'd been treating "XML processing" as a single requirement.&lt;/p&gt;

&lt;p&gt;It was actually two separate workflows.&lt;/p&gt;

&lt;p&gt;The first started with relational data. I needed to query employee records, including their job history, and generate an XML document from the results.&lt;/p&gt;

&lt;p&gt;The second worked in the opposite direction. I needed to receive an XML document, validate it against an XSD, extract its values, and insert them into relational tables.&lt;/p&gt;

&lt;p&gt;Here's the difference:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GENERATING XML

Relational tables
       |
       v
      SQL
       |
       v
  XML document


RECEIVING XML

  XML document
       |
       v
 XSD validation
       |
       v
  SQL extraction
       |
       v
Relational tables
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both workflows involve XML, but that doesn't mean they require the same tools.&lt;/p&gt;

&lt;p&gt;Generating a document is about transforming relational data into an XML structure. Receiving a document may introduce an additional requirement: proving that the document follows a defined schema.&lt;/p&gt;

&lt;p&gt;That distinction matters because &lt;strong&gt;parsing XML and validating an XML contract are different operations&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;PostgreSQL handles much of the first workflow, and part of the second, without needing Python at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  The unexpected problem in my original assignment
&lt;/h2&gt;

&lt;p&gt;Before getting into the PostgreSQL features, there's a discovery worth discussing.&lt;/p&gt;

&lt;p&gt;One of my original SQL queries generated an XML document containing employee details and job history records.&lt;/p&gt;

&lt;p&gt;An employee could have no previous jobs, one previous job, or several. The relational data model naturally allowed those possibilities.&lt;/p&gt;

&lt;p&gt;The XML Schema, however, didn't account for all of them.&lt;/p&gt;

&lt;p&gt;It defined an optional &lt;code&gt;jobHistories&lt;/code&gt; container, but once that container appeared, the schema expected exactly one &lt;code&gt;jobHistory&lt;/code&gt; element.&lt;/p&gt;

&lt;p&gt;Why exactly one?&lt;/p&gt;

&lt;p&gt;Because the nested &lt;code&gt;jobHistory&lt;/code&gt; declaration didn't specify &lt;code&gt;minOccurs&lt;/code&gt; or &lt;code&gt;maxOccurs&lt;/code&gt;. In XSD, both default to &lt;code&gt;1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That became a problem when I recreated the query using three synthetic employees.&lt;/p&gt;

&lt;p&gt;One had no job history, another had one record, and the third had two.&lt;/p&gt;

&lt;p&gt;I generated the XML and checked each document against the unchanged historical XSD.&lt;/p&gt;

&lt;p&gt;Here's what happened:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Job history records&lt;/th&gt;
&lt;th&gt;Generated XML&lt;/th&gt;
&lt;th&gt;XSD result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;An empty &lt;code&gt;&amp;lt;jobHistory/&amp;gt;&lt;/code&gt; element&lt;/td&gt;
&lt;td&gt;Invalid&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;One populated &lt;code&gt;&amp;lt;jobHistory&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Valid&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Two &lt;code&gt;&amp;lt;jobHistory&amp;gt;&lt;/code&gt; elements&lt;/td&gt;
&lt;td&gt;Invalid&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The first failure happened because the query generated an empty &lt;code&gt;jobHistory&lt;/code&gt; element. The schema required that element to contain fields such as &lt;code&gt;startDate&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The third failed for a different reason. The XML contained two job history records, but the XSD allowed only one.&lt;/p&gt;

&lt;p&gt;And this is the important part: &lt;strong&gt;none of those failures meant PostgreSQL had generated malformed XML&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The documents were well-formed. They simply didn't satisfy the contract defined by the schema.&lt;/p&gt;

&lt;p&gt;That raises a question the validator cannot answer for us.&lt;/p&gt;

&lt;p&gt;Should the schema allow multiple job history records? Or was the query supposed to return only one?&lt;/p&gt;

&lt;p&gt;If an employee has no job history, should the document omit the optional container instead of generating an empty record?&lt;/p&gt;

&lt;p&gt;Those are questions about requirements and data modeling, not about which XML parser to use.&lt;/p&gt;

&lt;p&gt;A validator can tell us when two components disagree. It can't decide which one represents the correct business requirement.&lt;/p&gt;

&lt;p&gt;For me, that was the most useful discovery in the retrospective. Before worrying about whether to use PostgreSQL, Python, or something else for validation, I needed to make sure the XML and XSD described the same contract.&lt;/p&gt;

&lt;p&gt;The three cases are reproduced in &lt;a href="https://github.com/smaje99/postgresql-xml-plpython/blob/main/sql/04-historical-output-xsd-cases.sql" rel="noopener noreferrer"&gt;04-historical-output-xsd-cases.sql&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What PostgreSQL can already do with XML
&lt;/h2&gt;

&lt;p&gt;Now let's look at the operations that don't require Python.&lt;/p&gt;

&lt;p&gt;PostgreSQL supports a collection of SQL/XML features for working with XML data directly in SQL.&lt;/p&gt;

&lt;p&gt;Rather than listing every function, I'll focus on the ones that mattered for the assignment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Generating XML from relational data
&lt;/h3&gt;

&lt;p&gt;Suppose we want to generate an XML document containing an employee's name and salary.&lt;/p&gt;

&lt;p&gt;We can do that with SQL alone:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;XMLELEMENT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;NAME&lt;/span&gt; &lt;span class="n"&gt;employee&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;XMLFOREST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'Ada'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nv"&gt;"firstName"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;salary&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;document&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The resulting XML is equivalent to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;employee&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;firstName&amp;gt;&lt;/span&gt;Ada&lt;span class="nt"&gt;&amp;lt;/firstName&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;salary&amp;gt;&lt;/span&gt;1200.50&lt;span class="nt"&gt;&amp;lt;/salary&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/employee&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two functions do the work here.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;XMLELEMENT&lt;/code&gt; creates an XML element, while &lt;code&gt;XMLFOREST&lt;/code&gt; turns SQL expressions into child elements.&lt;/p&gt;

&lt;p&gt;For more complex documents, PostgreSQL also provides &lt;code&gt;XMLATTRIBUTES&lt;/code&gt; and &lt;code&gt;XMLAGG&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;XMLATTRIBUTES&lt;/code&gt; lets us add attributes to an element. &lt;code&gt;XMLAGG&lt;/code&gt; combines XML values from multiple rows, which is particularly useful when dealing with relationships such as an employee's job history.&lt;/p&gt;

&lt;p&gt;In the reconstructed experiment, I used these features to generate documents from employees with zero, one, and multiple job history records.&lt;/p&gt;

&lt;p&gt;The example also checks ordering, omitted null fields, and the expected output structure.&lt;/p&gt;

&lt;p&gt;You can find it in &lt;a href="https://github.com/smaje99/postgresql-xml-plpython/blob/main/sql/01-native-xml-generation.sql" rel="noopener noreferrer"&gt;01-native-xml-generation.sql&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;One detail is worth clarifying: the XML generation itself uses native PostgreSQL functions and doesn't require PL/Python. The reproducible test script also validates the generated document against an example XSD, and that additional validation step does use &lt;code&gt;lxml&lt;/code&gt; through PL/Python.&lt;/p&gt;

&lt;p&gt;In other words, PostgreSQL builds the document. Python checks whether it follows the chosen schema.&lt;/p&gt;

&lt;h3&gt;
  
  
  Extracting XML into relational columns
&lt;/h3&gt;

&lt;p&gt;The reverse operation is also supported natively.&lt;/p&gt;

&lt;p&gt;Let's say an application sends us this document:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;employee&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;firstName&amp;gt;&lt;/span&gt;Ada&lt;span class="nt"&gt;&amp;lt;/firstName&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;salary&amp;gt;&lt;/span&gt;1200.50&lt;span class="nt"&gt;&amp;lt;/salary&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/employee&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can turn those values into SQL columns using &lt;code&gt;XMLTABLE&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;employee&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;employee&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;salary&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;XMLTABLE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'/employee'&lt;/span&gt;
    &lt;span class="n"&gt;PASSING&lt;/span&gt; &lt;span class="n"&gt;XMLPARSE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;DOCUMENT&lt;/span&gt;
        &lt;span class="s1"&gt;'&amp;lt;employee&amp;gt;
           &amp;lt;firstName&amp;gt;Ada&amp;lt;/firstName&amp;gt;
           &amp;lt;salary&amp;gt;1200.50&amp;lt;/salary&amp;gt;
         &amp;lt;/employee&amp;gt;'&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;COLUMNS&lt;/span&gt;
        &lt;span class="n"&gt;first_name&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="n"&gt;PATH&lt;/span&gt; &lt;span class="s1"&gt;'firstName'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;salary&lt;/span&gt; &lt;span class="nb"&gt;numeric&lt;/span&gt; &lt;span class="n"&gt;PATH&lt;/span&gt; &lt;span class="s1"&gt;'salary'&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;employee&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result looks like any other SQL query:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;first_name&lt;/th&gt;
&lt;th&gt;salary&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ada&lt;/td&gt;
&lt;td&gt;1200.50&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;XMLPARSE&lt;/code&gt; interprets the XML document. &lt;code&gt;XMLTABLE&lt;/code&gt; then uses XPath expressions to locate the elements and convert their values into the SQL types we've specified.&lt;/p&gt;

&lt;p&gt;We haven't written any Python code.&lt;/p&gt;

&lt;p&gt;For extracting known values into relational columns, that's often all we need.&lt;/p&gt;

&lt;h3&gt;
  
  
  There's more than XMLTABLE
&lt;/h3&gt;

&lt;p&gt;PostgreSQL also provides &lt;code&gt;xpath()&lt;/code&gt;, &lt;code&gt;xpath_exists()&lt;/code&gt;, and &lt;code&gt;XMLEXISTS&lt;/code&gt; for querying XML documents.&lt;/p&gt;

&lt;p&gt;Namespaces require some care, though.&lt;/p&gt;

&lt;p&gt;In one of my tests, a document used a default XML namespace. A simple XPath expression such as &lt;code&gt;/employee&lt;/code&gt; returned no rows because it didn't match the namespace-qualified element.&lt;/p&gt;

&lt;p&gt;Declaring an explicit namespace alias through &lt;code&gt;XMLNAMESPACES&lt;/code&gt; and then using &lt;code&gt;/e:employee&lt;/code&gt; solved the problem.&lt;/p&gt;

&lt;p&gt;There's also a limitation worth remembering: PostgreSQL 18's XML querying features rely on XPath 1.0. They aren't a complete XQuery implementation.&lt;/p&gt;

&lt;p&gt;Still, these functions cover the generation and extraction operations I needed to demonstrate.&lt;/p&gt;

&lt;p&gt;So where does that leave PL/Python?&lt;/p&gt;

&lt;h2&gt;
  
  
  What PL/Python actually contributed
&lt;/h2&gt;

&lt;p&gt;The first article covered the implementation in detail, so I won't repeat the full validator here.&lt;/p&gt;

&lt;p&gt;The key idea was to use PostgreSQL's &lt;code&gt;plpython3u&lt;/code&gt; extension to execute Python and its &lt;code&gt;lxml&lt;/code&gt; library inside the database server.&lt;/p&gt;

&lt;p&gt;That allowed me to validate an incoming XML document against an XSD before extracting and inserting its values.&lt;/p&gt;

&lt;p&gt;Looking at the solution today, I can draw a much clearer boundary around what Python was doing.&lt;/p&gt;

&lt;p&gt;I didn't need it to build XML elements, execute XPath queries, convert a salary into a numeric column, or insert a row.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I needed it because the assignment required XSD validation inside PostgreSQL.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Those aren't interchangeable capabilities.&lt;/p&gt;

&lt;p&gt;PostgreSQL can recognize well-formed XML and perform conversions to SQL types. But that doesn't mean it has verified every rule in an XML schema.&lt;/p&gt;

&lt;p&gt;An XSD can define required elements, element ordering, occurrence limits, and accepted data types. PostgreSQL 18 doesn't provide built-in validation of arbitrary documents against those schemas.&lt;/p&gt;

&lt;p&gt;PL/Python gave me a way to fill that gap.&lt;/p&gt;

&lt;p&gt;It also introduced a different set of responsibilities.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;plpython3u&lt;/code&gt; language is untrusted. Python code runs within the PostgreSQL server's process, and creating functions in that language requires superuser privileges.&lt;/p&gt;

&lt;p&gt;Deploying this approach means dealing with Python dependencies, function ownership, access permissions, parser behavior, and database-server maintenance.&lt;/p&gt;

&lt;p&gt;The reconstructed experiment includes parser restrictions and tests for malformed or potentially problematic inputs. Those checks help make the experiment more controlled, but they don't turn it into a production-ready security design.&lt;/p&gt;

&lt;p&gt;For my university assignment, the trade-off was understandable. Keeping the validation inside PostgreSQL was part of the constraint I was trying to satisfy.&lt;/p&gt;

&lt;p&gt;In a real application, though, I'd first ask whether the database server was the right place for that responsibility.&lt;/p&gt;

&lt;h2&gt;
  
  
  What if validation happened in the application?
&lt;/h2&gt;

&lt;p&gt;Here's another approach.&lt;/p&gt;

&lt;p&gt;Suppose XML documents arrive through an API or an integration service. That component already has to receive the input, identify the request, and decide how to handle failures.&lt;/p&gt;

&lt;p&gt;Instead of sending the XML to PostgreSQL for schema validation, the application could validate it first using an XSD-capable library.&lt;/p&gt;

&lt;p&gt;The flow would look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; Incoming XML
      |
      v
 API / Ingestion service
      |
      v
   XSD validation
      |
   Valid?
   /    \
  No    Yes
  |      |
Reject  Extract data
           |
           v
       PostgreSQL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps the XML validation dependencies outside the database server and places input validation closer to the point where the document enters the system.&lt;/p&gt;

&lt;p&gt;It also lets PostgreSQL focus on storing and protecting the resulting data.&lt;/p&gt;

&lt;p&gt;At first glance, that separation is attractive.&lt;/p&gt;

&lt;p&gt;But there's a catch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Application-side validation only enforces the contract if every relevant write path goes through it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Imagine that an API checks every incoming XML document, but a batch-processing job can write directly to the same tables.&lt;/p&gt;

&lt;p&gt;That job might bypass the validation entirely.&lt;/p&gt;

&lt;p&gt;The system therefore needs a clear answer to questions such as who can write to the database, which operations are permitted, and what invariants PostgreSQL must enforce independently.&lt;/p&gt;

&lt;p&gt;Moving validation out of the database doesn't remove the need for a trustworthy persistence boundary.&lt;/p&gt;

&lt;p&gt;It changes where the responsibility lives.&lt;/p&gt;

&lt;p&gt;I haven't implemented or benchmarked an equivalent application-side validator as part of this experiment. So I can't honestly say that it would be faster, more secure, or easier to operate for this particular workload.&lt;/p&gt;

&lt;p&gt;Those outcomes would depend on the application's architecture, deployment environment, and security requirements.&lt;/p&gt;

&lt;p&gt;Still, it's an alternative I would definitely evaluate before choosing an in-database Python implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  So, when would I use PL/Python?
&lt;/h2&gt;

&lt;p&gt;After reviewing the project, I'd start with the actual requirement rather than the available extensions.&lt;/p&gt;

&lt;p&gt;Here's how I'd approach the different XML-related tasks:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Requirement&lt;/th&gt;
&lt;th&gt;Where I'd start&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Generate XML from relational rows&lt;/td&gt;
&lt;td&gt;PostgreSQL's native SQL/XML functions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Extract known fields from XML&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;XMLTABLE&lt;/code&gt; or XPath&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Validate an XSD inside PostgreSQL&lt;/td&gt;
&lt;td&gt;Evaluate PL/Python with an XSD-capable library&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Validate external XML before persistence&lt;/td&gt;
&lt;td&gt;An ingestion service or application, with controlled database writes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These aren't universal rules. They're starting points for evaluating a design.&lt;/p&gt;

&lt;p&gt;If the task is simply to generate an XML document from existing tables, I'd use the native SQL/XML features first.&lt;/p&gt;

&lt;p&gt;If I need to extract a handful of known values from XML, &lt;code&gt;XMLTABLE&lt;/code&gt; is a natural option to investigate.&lt;/p&gt;

&lt;p&gt;If schema validation must happen inside PostgreSQL, PL/Python becomes a possible solution, but one that needs deliberate consideration of its operational and security implications.&lt;/p&gt;

&lt;p&gt;And if I control the application receiving the XML, I'd examine whether that is the more appropriate validation boundary.&lt;/p&gt;

&lt;p&gt;Before making any of those choices, however, I'd verify that the schema actually represents the expected document.&lt;/p&gt;

&lt;p&gt;The job history mismatch taught me why that step shouldn't be taken for granted.&lt;/p&gt;

&lt;p&gt;Choosing a validator won't fix an incorrect contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  Looking back, what would I do differently?
&lt;/h2&gt;

&lt;p&gt;When I originally developed the assignment, my goal was fairly narrow: validate XML against an XSD while staying with PostgreSQL.&lt;/p&gt;

&lt;p&gt;I found a way to do that, and it was a worthwhile technical exercise. It introduced me to PL/Python and encouraged me to explore capabilities I hadn't previously used.&lt;/p&gt;

&lt;p&gt;But revisiting the project changed what I consider the most interesting part of the problem.&lt;/p&gt;

&lt;p&gt;Today, I wouldn't begin by asking how to run Python in PostgreSQL.&lt;/p&gt;

&lt;p&gt;I'd ask what the system needs to guarantee, which component owns that guarantee, and whether the XML schema correctly describes the data we expect to exchange.&lt;/p&gt;

&lt;p&gt;Only then would I choose the tools.&lt;/p&gt;

&lt;p&gt;The historical mismatch between the generated documents and their XSD makes that lesson particularly clear. The SQL could produce well-formed XML, and the validator could correctly reject it, while the underlying disagreement about the data contract remained unresolved.&lt;/p&gt;

&lt;p&gt;That's the difference between making a technical solution work and understanding whether the system is doing the right thing.&lt;/p&gt;

&lt;p&gt;PL/Python solved a specific limitation in my original assignment. I wouldn't rule it out today, but I wouldn't reach for it automatically either.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The real engineering decision isn't whether PostgreSQL can execute Python. It's whether executing Python inside PostgreSQL is the right way to meet the system's requirements.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And sometimes, the most useful thing about revisiting an old solution isn't finding a better implementation. It's learning to ask a better question.&lt;/p&gt;




&lt;h2&gt;
  
  
  Source code and references
&lt;/h2&gt;

&lt;p&gt;The reproducible examples are available in &lt;a href="https://github.com/smaje99/postgresql-xml-plpython" rel="noopener noreferrer"&gt;smaje99/postgresql-xml-plpython&lt;/a&gt;, published under the GPL-3.0-only license.&lt;/p&gt;

&lt;p&gt;The recorded experiments use PostgreSQL 18.6.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/smaje99/postgresql-xml-plpython/blob/main/sql/01-native-xml-generation.sql" rel="noopener noreferrer"&gt;Native SQL/XML generation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/smaje99/postgresql-xml-plpython/blob/main/sql/02-native-sql-xml.sql" rel="noopener noreferrer"&gt;XML queries, extraction, and namespaces&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/smaje99/postgresql-xml-plpython/blob/main/sql/04-historical-output-xsd-cases.sql" rel="noopener noreferrer"&gt;Historical XML/XSD compatibility tests&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.postgresql.org/docs/18/functions-xml.html" rel="noopener noreferrer"&gt;PostgreSQL 18 XML functions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.postgresql.org/docs/18/xml-limits-conformance.html" rel="noopener noreferrer"&gt;PostgreSQL SQL/XML limitations&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.postgresql.org/docs/18/datatype-xml.html" rel="noopener noreferrer"&gt;PostgreSQL XML data type&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Version note:&lt;/strong&gt; As of October 15, 2026, PostgreSQL 19 RC 1 was available. Its development documentation still did not describe built-in XSD validation. I haven't tested these examples on PostgreSQL 19; the experimental results discussed here come from PostgreSQL 18.6. PostgreSQL 19 remains a prerelease, so this observation is provisional.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article presents a retrospective analysis of an educational experiment, not a universal production architecture recommendation.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>python</category>
      <category>xml</category>
      <category>database</category>
    </item>
    <item>
      <title>Why I Ended Up Running Python Inside PostgreSQL to Validate XML</title>
      <dc:creator>Sergio Majé</dc:creator>
      <pubDate>Tue, 13 Oct 2026 03:31:23 +0000</pubDate>
      <link>https://dev.to/smaje99/why-i-ended-up-running-python-inside-postgresql-to-validate-xml-ii1</link>
      <guid>https://dev.to/smaje99/why-i-ended-up-running-python-inside-postgresql-to-validate-xml-ii1</guid>
      <description>&lt;p&gt;Two years ago, a university database assignment led me to do something I hadn't expected: run Python code inside PostgreSQL.&lt;/p&gt;

&lt;p&gt;The assignment was straightforward on paper. I needed to accept an XML document, validate it against an XSD schema, and insert the data into relational tables.&lt;/p&gt;

&lt;p&gt;I had chosen PostgreSQL for the project. I was already familiar with some of its capabilities, and I wanted to use the assignment to explore the database further.&lt;/p&gt;

&lt;p&gt;Things got interesting when I was halfway through the implementation.&lt;/p&gt;

&lt;p&gt;PostgreSQL could parse XML, extract values, and turn them into relational data. What I couldn't find was a built-in way to validate a document against an XML Schema Definition (XSD).&lt;/p&gt;

&lt;p&gt;Oracle XML DB supported that kind of validation, so switching databases was an option. But I had already invested time in PostgreSQL, and I was curious about whether I could make it work.&lt;/p&gt;

&lt;p&gt;That's how I discovered PL/Python.&lt;/p&gt;

&lt;p&gt;In this article, I'll walk through the problem, show how I combined PostgreSQL with Python's &lt;code&gt;lxml&lt;/code&gt; library, and explain what I learned when I revisited the solution two years later.&lt;/p&gt;

&lt;p&gt;This is a practical experiment rather than a recommendation to move XML validation into every database.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: reading XML wasn't enough
&lt;/h2&gt;

&lt;p&gt;Let's start with a simple example.&lt;/p&gt;

&lt;p&gt;Suppose an application receives employee records as XML documents. Each document contains information that eventually needs to be stored in a database.&lt;/p&gt;

&lt;p&gt;Here's one of our sample records:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;employee&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;firstName&amp;gt;&lt;/span&gt;Ada&lt;span class="nt"&gt;&amp;lt;/firstName&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;salary&amp;gt;&lt;/span&gt;1200.50&lt;span class="nt"&gt;&amp;lt;/salary&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;hireDate&amp;gt;&lt;/span&gt;2024-01-31&lt;span class="nt"&gt;&amp;lt;/hireDate&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/employee&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing particularly complicated here. We have a name, a salary, and a hiring date.&lt;/p&gt;

&lt;p&gt;PostgreSQL provides tools for extracting these values, so getting them into relational columns isn't especially difficult.&lt;/p&gt;

&lt;p&gt;But receiving a document doesn't mean we should trust its contents.&lt;/p&gt;

&lt;p&gt;What if the salary looked like this?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;salary&amp;gt;&lt;/span&gt;quite a lot&lt;span class="nt"&gt;&amp;lt;/salary&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As far as XML syntax is concerned, there's nothing wrong with that element. The opening and closing tags match, and the content is valid text.&lt;/p&gt;

&lt;p&gt;Our application, however, expects a number.&lt;/p&gt;

&lt;p&gt;This is where an XSD comes in.&lt;/p&gt;

&lt;p&gt;An XML Schema Definition describes what a document is supposed to look like. It can define required elements, their order, accepted data types, and optional fields.&lt;/p&gt;

&lt;p&gt;For our employee document, the rules might say that &lt;code&gt;firstName&lt;/code&gt; must be a string, &lt;code&gt;salary&lt;/code&gt; must be a decimal, and &lt;code&gt;hireDate&lt;/code&gt; must be a date.&lt;/p&gt;

&lt;p&gt;Those rules introduce an important distinction:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A document can be well-formed XML without being valid according to an XSD.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That difference was at the heart of my assignment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where PostgreSQL stopped short
&lt;/h2&gt;

&lt;p&gt;PostgreSQL has supported XML operations for years.&lt;/p&gt;

&lt;p&gt;It includes an &lt;code&gt;xml&lt;/code&gt; data type and functions such as &lt;code&gt;XMLPARSE&lt;/code&gt;, &lt;code&gt;xpath()&lt;/code&gt;, and &lt;code&gt;XMLTABLE&lt;/code&gt;. These are useful when you need to inspect XML documents or work with their contents in SQL.&lt;/p&gt;

&lt;p&gt;For instance, consider this query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;xml_is_well_formed_document&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s1"&gt;'&amp;lt;employee&amp;gt;
     &amp;lt;firstName&amp;gt;Ada&amp;lt;/firstName&amp;gt;
     &amp;lt;salary&amp;gt;quite a lot&amp;lt;/salary&amp;gt;
     &amp;lt;hireDate&amp;gt;2024-01-31&amp;lt;/hireDate&amp;gt;
   &amp;lt;/employee&amp;gt;'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PostgreSQL returns &lt;code&gt;true&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;And that's correct. The document is well-formed XML.&lt;/p&gt;

&lt;p&gt;The problem is that a successful syntax check tells us nothing about whether &lt;code&gt;quite a lot&lt;/code&gt; is an acceptable salary under our schema.&lt;/p&gt;

&lt;p&gt;PostgreSQL 18's documentation makes the limitation clear: while the database can parse and process XML, it doesn't provide built-in validation against XML Schema.&lt;/p&gt;

&lt;p&gt;I could extract the values and rely on SQL types and constraints to catch certain problems. But that wouldn't be the same as validating the entire document against the rules defined in an XSD.&lt;/p&gt;

&lt;p&gt;I needed to add a missing capability without replacing everything PostgreSQL already handled well.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enter PL/Python
&lt;/h2&gt;

&lt;p&gt;One feature I particularly like about PostgreSQL is its support for procedural languages.&lt;/p&gt;

&lt;p&gt;Besides SQL and PL/pgSQL, PostgreSQL can run functions written in other languages, including Python.&lt;/p&gt;

&lt;p&gt;The relevant extension is called &lt;code&gt;plpython3u&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Once the appropriate server-side package is installed, the language can be enabled in a database using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;plpython3u&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This allows a PostgreSQL function to execute Python code inside the database server's process.&lt;/p&gt;

&lt;p&gt;That last part matters.&lt;/p&gt;

&lt;p&gt;I wasn't trying to build a separate Python service that would communicate with PostgreSQL. The assignment kept the validation logic inside the database, so I needed Python to run there.&lt;/p&gt;

&lt;p&gt;The other half of the solution was &lt;code&gt;lxml&lt;/code&gt;, a Python library with support for XML Schema validation.&lt;/p&gt;

&lt;p&gt;With those two pieces, I could keep the workflow simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Incoming XML + XSD
        |
        v
 PL/Python + lxml
        |
   XSD validation
      /     \
    Fail    Pass
     |        |
   Reject  XMLTABLE
              |
              v
          SQL columns
              |
              v
            INSERT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Python would check the document against the schema. PostgreSQL would handle the relational side.&lt;/p&gt;

&lt;p&gt;That division of responsibilities gave me what I needed for the assignment without attempting to reimplement PostgreSQL's existing XML features.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the XSD validation step
&lt;/h2&gt;

&lt;p&gt;Before looking at the Python code, let's define the rules for our sample employee.&lt;/p&gt;

&lt;p&gt;A minimal XSD could look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;xs:schema&lt;/span&gt; &lt;span class="na"&gt;xmlns:xs=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/2001/XMLSchema"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;xs:element&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"employee"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;xs:complexType&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;xs:sequence&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;xs:element&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"firstName"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"xs:string"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;xs:element&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"salary"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"xs:decimal"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;xs:element&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"hireDate"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"xs:date"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;xs:element&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"nickname"&lt;/span&gt;
                    &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"xs:string"&lt;/span&gt;
                    &lt;span class="na"&gt;minOccurs=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/xs:sequence&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/xs:complexType&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/xs:element&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/xs:schema&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you haven't worked much with XSD, focus on two attributes.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;type&lt;/code&gt; attribute defines the kind of value each element accepts. And &lt;code&gt;minOccurs="0"&lt;/code&gt; makes the &lt;code&gt;nickname&lt;/code&gt; element optional.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;xs:sequence&lt;/code&gt; declaration also specifies the order in which those elements should appear.&lt;/p&gt;

&lt;p&gt;Now we need Python to interpret these rules and check an incoming XML document.&lt;/p&gt;

&lt;p&gt;The core validation logic is surprisingly small:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;lxml&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;etree&lt;/span&gt;

&lt;span class="c1"&gt;# Parse the XSD and build a validator.
&lt;/span&gt;&lt;span class="n"&gt;xsd_root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;etree&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromstring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;xsd_bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="n"&gt;schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;etree&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;XMLSchema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;xsd_root&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Parse the incoming XML document.
&lt;/span&gt;&lt;span class="n"&gt;xml_root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;etree&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromstring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;xml_bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="c1"&gt;# Validate the document against the schema.
&lt;/span&gt;&lt;span class="n"&gt;is_valid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;xml_root&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a simplified excerpt, not a complete standalone function. The actual implementation includes parser configuration, error handling, and input checks.&lt;/p&gt;

&lt;p&gt;The important part is how &lt;code&gt;etree.XMLSchema&lt;/code&gt; and &lt;code&gt;schema.validate()&lt;/code&gt; work together.&lt;/p&gt;

&lt;p&gt;First, &lt;code&gt;lxml&lt;/code&gt; parses the schema. Then it compiles the schema into a validator. Finally, the validator checks whether the document follows its rules.&lt;/p&gt;

&lt;p&gt;If we provide &lt;code&gt;1200.50&lt;/code&gt; as the salary, the document passes that type check. If we use &lt;code&gt;quite a lot&lt;/code&gt;, it fails because the value doesn't match &lt;code&gt;xs:decimal&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;And unlike PostgreSQL's well-formedness check, this validation can also detect missing required elements, unexpected elements, and other violations of the schema.&lt;/p&gt;

&lt;h3&gt;
  
  
  Returning useful errors
&lt;/h3&gt;

&lt;p&gt;My original university implementation raised a PostgreSQL error whenever XSD validation failed.&lt;/p&gt;

&lt;p&gt;That worked for rejecting the document, but it wasn't particularly useful for understanding different failure scenarios.&lt;/p&gt;

&lt;p&gt;When I reconstructed the experiment, I changed the helper to return structured JSON results.&lt;/p&gt;

&lt;p&gt;Instead of exposing only a boolean, the result identifies whether the failure happened while parsing the XML, preparing the XSD, or validating the document against it.&lt;/p&gt;

&lt;p&gt;For example, a schema-validation failure can produce a result with the following fields:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"valid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"stage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"xsd_validation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"xsd_invalid"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The helper also includes diagnostic details when available.&lt;/p&gt;

&lt;p&gt;This makes it easier for the SQL layer to handle a rejected document consistently.&lt;/p&gt;

&lt;p&gt;You can inspect the complete implementation in &lt;a href="https://github.com/smaje99/postgresql-xml-plpython/blob/main/sql/03-xsd-validation-cases.sql" rel="noopener noreferrer"&gt;03-xsd-validation-cases.sql&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turning validated XML into SQL rows
&lt;/h2&gt;

&lt;p&gt;Once the XML passes validation, we still need to store its data.&lt;/p&gt;

&lt;p&gt;I could have written that part in Python too, but PostgreSQL already has a suitable tool: &lt;code&gt;XMLTABLE&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It lets us read values from an XML document and present them as columns in a SQL query.&lt;/p&gt;

&lt;p&gt;Here's a small example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;XMLTABLE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'/employee'&lt;/span&gt;
    &lt;span class="n"&gt;PASSING&lt;/span&gt; &lt;span class="n"&gt;XMLPARSE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;DOCUMENT&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;employee&amp;gt;
                    &amp;lt;firstName&amp;gt;Ada&amp;lt;/firstName&amp;gt;
                    &amp;lt;salary&amp;gt;1200.50&amp;lt;/salary&amp;gt;
                    &amp;lt;hireDate&amp;gt;2024-01-31&amp;lt;/hireDate&amp;gt;
                  &amp;lt;/employee&amp;gt;'&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;COLUMNS&lt;/span&gt;
        &lt;span class="n"&gt;first_name&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="n"&gt;PATH&lt;/span&gt; &lt;span class="s1"&gt;'firstName'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;salary&lt;/span&gt; &lt;span class="nb"&gt;numeric&lt;/span&gt; &lt;span class="n"&gt;PATH&lt;/span&gt; &lt;span class="s1"&gt;'salary'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;hire_date&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="n"&gt;PATH&lt;/span&gt; &lt;span class="s1"&gt;'hireDate'&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;employee&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The query produces a relational row with a name, a numeric salary, and a date.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;XMLTABLE&lt;/code&gt; does more than select text from the document. It also converts values into the SQL types we specify.&lt;/p&gt;

&lt;p&gt;Still, successful SQL type conversion doesn't mean an XML document satisfies its XSD. The schema may impose additional requirements that cannot be inferred from these three columns alone.&lt;/p&gt;

&lt;p&gt;That's why I kept schema validation and relational extraction as separate steps.&lt;/p&gt;

&lt;p&gt;In the reproducible implementation, a PL/pgSQL function calls the Python validator first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;validation_result&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="n"&gt;xml_research&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;validate_xml_xsd_case&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;xml_text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;xsd_text&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;validation_result&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'valid'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;TRUE&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
    &lt;span class="n"&gt;RAISE&lt;/span&gt; &lt;span class="n"&gt;EXCEPTION&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt;
        &lt;span class="n"&gt;ERRCODE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'22023'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;MESSAGE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s1"&gt;'XML rejected during %s: %s'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;validation_result&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'stage'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;validation_result&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'category'&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This excerpt runs inside a PL/pgSQL function.&lt;/p&gt;

&lt;p&gt;If validation fails, the function raises an exception before reaching the &lt;code&gt;INSERT&lt;/code&gt;. I chose SQLSTATE &lt;code&gt;22023&lt;/code&gt; as the error code for these rejections in the experiment.&lt;/p&gt;

&lt;p&gt;If validation succeeds, the function uses &lt;code&gt;XMLTABLE&lt;/code&gt; to extract the values and insert them into a temporary relational table.&lt;/p&gt;

&lt;p&gt;The result is a small processing pipeline in which &lt;strong&gt;Python validates the XML contract and PostgreSQL handles the database operations&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Revisiting the implementation two years later
&lt;/h2&gt;

&lt;p&gt;The original assignment was built on PostgreSQL 15.&lt;/p&gt;

&lt;p&gt;For this article, I wanted something more useful than a description of an old university project. So I revisited the implementation and created a reproducible experiment using PostgreSQL 18.6.&lt;/p&gt;

&lt;p&gt;The repository is available here:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/smaje99/postgresql-xml-plpython" rel="noopener noreferrer"&gt;smaje99/postgresql-xml-plpython&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It includes the Docker configuration, SQL scripts, synthetic XML fixtures, and validation tests.&lt;/p&gt;

&lt;p&gt;You can run the examples locally without configuring PostgreSQL and PL/Python manually on your machine.&lt;/p&gt;

&lt;h3&gt;
  
  
  Running the experiment
&lt;/h3&gt;

&lt;p&gt;You'll need Docker Engine with the Compose plugin, plus Python 3 to generate a local password.&lt;/p&gt;

&lt;p&gt;Start by cloning the repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/smaje99/postgresql-xml-plpython.git
&lt;span class="nb"&gt;cd &lt;/span&gt;postgresql-xml-plpython
&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Generate a password:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'import secrets; print(secrets.token_urlsafe(32))'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the generated value to &lt;code&gt;.env&lt;/code&gt; as &lt;code&gt;POSTGRES_PASSWORD&lt;/code&gt;, then start the database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;--build&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;
docker compose ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To run the validation and insertion tests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;--no-deps&lt;/span&gt; postgres sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="s1"&gt;'PGPASSWORD="$POSTGRES_PASSWORD" psql \
-v ON_ERROR_STOP=1 \
-h postgres \
-U "$POSTGRES_USER" \
-d "$POSTGRES_DB" \
-f /workspace/sql/03-xsd-validation-cases.sql'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script checks its own expected outcomes and raises an error if a test fails.&lt;/p&gt;

&lt;p&gt;The repository's README also covers the other XML experiments, including generating XML from relational tables. That was another part of the original assignment, but the focus here is validating incoming documents before storing their data.&lt;/p&gt;

&lt;p&gt;When you're done, use &lt;code&gt;docker compose stop&lt;/code&gt; to stop the database while keeping its data volume. The README also explains how to remove the disposable environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the tests showed
&lt;/h2&gt;

&lt;p&gt;The updated experiment includes 17 validation cases.&lt;/p&gt;

&lt;p&gt;They cover more than just valid and invalid salaries. I also tested missing required elements, invalid dates, malformed XML, malformed XSD, unexpected elements, and namespace mismatches.&lt;/p&gt;

&lt;p&gt;Other cases examine how the parser handles DTD declarations, entities, external schema references, oversized inputs, and deeply nested documents.&lt;/p&gt;

&lt;p&gt;In the recorded PostgreSQL 18.6 environment, all 17 cases matched their expected results.&lt;/p&gt;

&lt;p&gt;The persistence tests inserted two valid employee documents: one with only the required fields and another with an optional nickname.&lt;/p&gt;

&lt;p&gt;The temporary table ended up containing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;first_name | salary  | hire_date  | nickname
-----------+---------+------------+----------
Ada        | 1200.50 | 2024-01-31 | NULL
Ada        | 1200.50 | 2024-01-31 | Countess
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The remaining 15 cases were rejected with SQLSTATE &lt;code&gt;22023&lt;/code&gt;, and the tests verified that they added no rows.&lt;/p&gt;

&lt;p&gt;Revisiting the old implementation also exposed a small issue in its error handling.&lt;/p&gt;

&lt;p&gt;The historical function called &lt;code&gt;plpy.error()&lt;/code&gt; when a document failed validation, followed by a &lt;code&gt;return False&lt;/code&gt;. But &lt;code&gt;plpy.error()&lt;/code&gt; raises an exception, so execution never reaches that return statement.&lt;/p&gt;

&lt;p&gt;In the newer experiment, the Python helper returns structured validation information, and the PL/pgSQL wrapper decides whether to raise a SQL exception.&lt;/p&gt;

&lt;p&gt;It's a clearer separation, and it makes the behavior easier to test.&lt;/p&gt;

&lt;p&gt;But passing a test suite doesn't automatically make the solution production-ready.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would worry about in a real system
&lt;/h2&gt;

&lt;p&gt;The biggest consideration is PL/Python itself.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;u&lt;/code&gt; in &lt;code&gt;plpython3u&lt;/code&gt; stands for &lt;em&gt;untrusted&lt;/em&gt;. PostgreSQL does not sandbox this language as a trusted procedural language, and only superusers can create functions written in it.&lt;/p&gt;

&lt;p&gt;Running Python inside the database server is powerful, but it also introduces security and operational responsibilities.&lt;/p&gt;

&lt;p&gt;You need to think about who owns the functions, who can execute them, which permissions they have, and what happens when a dependency or runtime needs updating.&lt;/p&gt;

&lt;p&gt;XML parsing deserves particular care as well.&lt;/p&gt;

&lt;p&gt;In the reproducible experiment, the &lt;code&gt;lxml&lt;/code&gt; parser is configured to disable entity resolution, DTD loading, network access, and huge-tree mode. The helper rejects external schema directives and enforces explicit limits on input size and nesting depth.&lt;/p&gt;

&lt;p&gt;Those restrictions are part of the laboratory's test policy. They shouldn't be treated as universally safe settings or a substitute for a full security review.&lt;/p&gt;

&lt;p&gt;There's another architectural question: &lt;strong&gt;who gets to decide which XSD is trusted?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In this experiment, both the XML and the XSD are passed to the function. The parser restrictions address certain technical risks, but they don't establish ownership, approval, or versioning rules for schemas.&lt;/p&gt;

&lt;p&gt;A real application would need to deal with that separately.&lt;/p&gt;

&lt;p&gt;Performance is also an open question. The helper compiles the schema during validation and performs additional parsing checks. I haven't benchmarked the implementation under load, so I can't claim that it scales efficiently or outperforms application-side validation.&lt;/p&gt;

&lt;p&gt;The recorded environment used PostgreSQL 18.6, Python 3.11.2, lxml 4.9.2.0, and libxml2 2.9.14. I haven't tested this implementation across every PostgreSQL release or verified its deployment on managed PostgreSQL services.&lt;/p&gt;

&lt;p&gt;These limitations don't invalidate the experiment. They define what it actually demonstrates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Would I still choose PL/Python today?
&lt;/h2&gt;

&lt;p&gt;I enjoyed working on this solution because it pushed me to explore PostgreSQL beyond the features I already knew.&lt;/p&gt;

&lt;p&gt;For the original assignment, it made sense. I needed to keep the validation workflow inside the database, and PL/Python let me add XSD validation while continuing to use PostgreSQL's SQL/XML features.&lt;/p&gt;

&lt;p&gt;But if I were designing a production system that receives XML from external users or services today, I wouldn't automatically put the validator inside PostgreSQL.&lt;/p&gt;

&lt;p&gt;I'd first look at the application's boundaries.&lt;/p&gt;

&lt;p&gt;Where should a document be accepted or rejected? Who owns the schema? How should validation errors reach the client? And what would placing Python inside the database mean for deployment, security, and maintenance?&lt;/p&gt;

&lt;p&gt;Validating XML in the application before it reaches the database is another reasonable option. Whether that approach is preferable depends on the system's requirements and constraints.&lt;/p&gt;

&lt;p&gt;That's the question I want to explore in the follow-up article: &lt;strong&gt;Do we really need PL/Python to process XML in PostgreSQL?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For now, this experiment taught me something valuable.&lt;/p&gt;

&lt;p&gt;Knowing that a database can do something is useful. Understanding when it should do it is an entirely different engineering decision.&lt;/p&gt;

&lt;p&gt;And sometimes, exploring a limitation is what helps you understand a tool better.&lt;/p&gt;




&lt;h2&gt;
  
  
  Source code and further reading
&lt;/h2&gt;

&lt;p&gt;The reproducible experiment is published under the GPL-3.0-only license.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/smaje99/postgresql-xml-plpython" rel="noopener noreferrer"&gt;Source code on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/smaje99/postgresql-xml-plpython/blob/main/sql/03-xsd-validation-cases.sql" rel="noopener noreferrer"&gt;XSD validation and persistence implementation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.postgresql.org/docs/18/datatype-xml.html" rel="noopener noreferrer"&gt;PostgreSQL 18 XML data type documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.postgresql.org/docs/18/functions-xml.html" rel="noopener noreferrer"&gt;PostgreSQL XML functions and XMLTABLE&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.postgresql.org/docs/18/plpython.html" rel="noopener noreferrer"&gt;PostgreSQL PL/Python documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oracle.com/en/database/oracle/oracle-database/26/adxdb/transformation-and-validation-of-XMLType-data.html" rel="noopener noreferrer"&gt;Oracle XML DB schema validation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;This is an educational experiment, not a production-ready XML validation service.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>python</category>
      <category>xml</category>
      <category>database</category>
    </item>
  </channel>
</rss>
