rdf_fdw 2.0.0

This Release
rdf_fdw 2.0.0
Date
Status
Stable
Other Releases
Abstract
RDF Triplestore Foreign Data Wrapper for PostgreSQL
Description
A PostgreSQL Foreign Data Wrapper to easily access RDF triplestores via SPARQL endpoints, including pushdown of several SQL Query clauses.
Released By
jim
License
MIT
Resources
Special Files
Tags

Extensions

rdf_fdw 2.0.0
RDF Triplestore Foreign Data Wrapper for PostgreSQL

Documentation

CHANGELOG
Release Notes

README


RDF Triplestore Foreign Data Wrapper for PostgreSQL (rdf_fdw)

rdf_fdw is a PostgreSQL Foreign Data Wrapper that enables seamless access to RDF triplestores via SPARQL endpoints. It supports pushdown of many SQL clauses and includes built-in implementations of most SPARQL 1.1 functions.

CI

Index

Requirements

In an Ubuntu environment you can install all dependencies with the following command:

apt-get install -y make gcc postgresql-server-dev-17 libxml2-dev libcurl4-gnutls-dev librdf0-dev pkg-config

[!NOTE]
postgresql-server-dev-17 only installs the libraries for PostgreSQL 17. Change it if you’re using another PostgreSQL version.

Build and Install

Ensure pg_config is properly set before running make. This executable is typically found in your PostgreSQL installation’s bin directory.

$ cd rdf_fdw
$ make

After compilation, install the Foreign Data Wrapper:

$ make install

Then, create the extension in PostgreSQL:

CREATE EXTENSION rdf_fdw;

To install a specific version, use:

CREATE EXTENSION rdf_fdw WITH VERSION '2.0';

To run the predefined regression tests:

$ make PGUSER=postgres installcheck

[!NOTE]
rdf_fdw loads all retrieved RDF data into memory before converting it for PostgreSQL. If you expect large data volumes, ensure that PostgreSQL has sufficient memory, or retrieve data in chunks using rdf_fdw_clone_table or a custom script.

Update

To update the extension’s version you must first build and install the binaries and then run ALTER EXTENSION:

ALTER EXTENSION rdf_fdw UPDATE;

To update to an specific version use UPDATE TO and the full version number, e.g.

ALTER EXTENSION rdf_fdw UPDATE TO '2.0';

Usage

To use rdf_fdw, you must first create a SERVER that connects to a SPARQL endpoint. Then, define a FOREIGN TABLE that specifies the SPARQL instructions used to retrieve data from the endpoint. This section walks through all the steps required to set up and query RDF data using the foreign data wrapper.

CREATE SERVER

The SQL command CREATE SERVER defines a new foreign server. The user who defines the server becomes its owner. A SERVER requires an endpoint to specify where rdf_fdw should send SPARQL queries.

The following example creates a SERVER that connects to the DBpedia SPARQL endpoint:

CREATE SERVER dbpedia
FOREIGN DATA WRAPPER rdf_fdw 
OPTIONS (endpoint 'https://dbpedia.org/sparql');

Server Options

| Server Option | Type | Description | |—————|–––––––––––|––––––––––––––––––––––––––––––––––––––––––––––––––––––––––| | endpoint | required | URL address of the SPARQL Endpoint. | enable_pushdown | optional | Globally enables or disables pushdown of SQL clauses into SPARQL (default true) | format | optional | The rdf_fdw expects the result sets to be encoded in the SPARQL Query Results XML Format, which is typically enforced by setting the MIME type application/sparql-results+xml in the Accept HTTP request header. However, some products deviate from this standard and expect a different value, e.g. xml, rdf-xml. If the expected parameter differs from the official MIME type, it should be specified explicitly (default application/sparql-results+xml). | http_proxy | optional | Proxy for HTTP requests. | proxy_user | optional | User for proxy server authentication. | proxy_user_password | optional | Password for proxy server authentication. | connect_timeout | optional | Connection timeout for HTTP requests in seconds (default 300 seconds). | connect_retry | optional | Number of attempts to retry a request in case of failure (default 3 times). | request_redirect | optional | Enables URL redirect issued by the server (default false). | request_max_redirect | optional | Specifies the maximum number of URL redirects allowed. If this limit is reached, any further redirection will result in an error. Leaving this parameter unset or setting it to 0 allows an unlimited number of redirects. | custom | optional | One or more parameters expected by the configured RDF triplestore. Multiple parameters separated by &, e.g. signal_void=on&signal_unconnected=on. Custom parameters are appended to the request URL. | query_param | optional | The request parameter in which the SPARQL endpoint expects the query in an HTTP request. Most endpoints expect the SPARQL query to be in the parameter query - and this is the rdf_fdw default value. So, chances are you’ll never need to touch this server option.

[!NOTE]
To visualise the foreign server’s options use the psql meta-command \des[+]

CREATE USER MAPPING

CREATE USER MAPPING defines a mapping of a PostgreSQL user to an user in the target triplestore. For instance, to map the PostgreSQL user postgres to the user admin in the SERVER named graphdb:

CREATE SERVER graphdb
FOREIGN DATA WRAPPER rdf_fdw
OPTIONS (endpoint 'http://192.168.178.27:7200/repositories/myrepo');

CREATE USER MAPPING FOR postgres
SERVER graphdb OPTIONS (user 'admin', password 'secret');

| Option | Type | Description | |—|—|—| | user | required | name of the user for authentication | | password | optional | password of the user set in the option user |

The rdf_fdw will try to authenticate the given user using HTTP Basic Authentication - no other authentication method is currently supported. This feature can be ignored if the triplestore does not require user authentication.

[!NOTE]
To visualise created user mappings use the psql meta-command \deu[+] or run SELECT * FROM pg_user_mappings in a client of your choice.

CREATE FOREIGN TABLE

Foreign Tables from the rdf_fdw work as a proxy between PostgreSQL clients and RDF Triplestores. Each column in a FOREIGN TABLE must be mapped to a SPARQL variable. This allows PostgreSQL to extract and assign results from the SPARQL query into the right column.

Table Options

| Option | Type | Description | |—————|———––|––––––––––––––––––––––––––––––––––––––––––––––––––––––––––| | sparql | required | The raw SPARQL query to be executed | | log_sparql | optional | Logs the exact SPARQL query executed. Useful for verifying modifications to the query due to pushdown. Default true | | enable_pushdown | optional | Enables or disables pushdown of SQL clauses into SPARQL for a specific foreign table. Overrides the SERVER option enable_pushdown |

Columns can use one of two data type categories:

RDF Node

The custom rdfnode type is designed to handle full RDF nodes, including both IRIs and literals with optional language tags or datatypes. It preserves the structure and semantics of RDF terms and is ideal when you need to manipulate or inspect RDF-specific details. Columns of this type only support the variable.

Column Options

| Option | Type | Description | |—————|———––|––––––––––––––––––––––––––––––––––––––––––––––––––––––––––| | variable | required | Maps the table column to a SPARQL variable used in the table option sparql. A variable must start with either ? or $ (? or $ are not part of the variable name!). The name must be a string with the following characters: [a-z], [A-Z],[0-9] |

Example:

CREATE FOREIGN TABLE hbf (
  p rdfnode OPTIONS (variable '?p'),
  o rdfnode OPTIONS (variable '?o')
)
SERVER linkedgeodata OPTIONS (
  log_sparql 'true',
  sparql 
    'SELECT ?p ?o 
     WHERE {<http://linkedgeodata.org/triplify/node376142577> ?p ?o}');

PostgreSQL native types

Alternatively, columns can be declared using PostgreSQL native types such as text, date, int, boolean, numeric, timestamp, etc. These are suitable for typed RDF literals or when you want automatic casting into PostgreSQL types. Native types support a wider range of column options:

Column Options

| Option | Type | Description | |—————|———––|––––––––––––––––––––––––––––––––––––––––––––––––––––––––––| | variable | required | Maps the table column to a SPARQL variable used in the table option sparql. A variable must start with either ? or $ (? or $ are not part of the variable name!). The name must be a string with the following characters: [a-z], [A-Z],[0-9] | | expression | optional | Similar to variable, but instead of a SPARQL variable, it can handle expressions, such as function calls. Any expression supported by the data source can be used. | | language | optional | RDF language tag, e.g. en,de,pt,es,pl, etc. This option ensures that the pushdown feature correctly sets the literal language tag in FILTER expressions. Set it to * to make FILTER expressions ignore language tags when comparing literals. |
| literal_type | optional | Data type for typed literals, e.g. xsd:string, xsd:date, xsd:boolean. This option ensures that the pushdown feature correctly sets the literal type of expressions from SQL WHERE conditions. Set it to * to make FILTER expressions ignore data types when comparing literals. | | nodetype | optional | Type of the RDF node. Expected values are literal or iri. This option helps the query planner to optimize SPARQL FILTER expressions when the WHERE conditions are pushed down (default literal) |

The following example creates a FOREIGN TABLE connected to the server dbpedia. SELECT queries executed against this table will execute the SPARQL query set in the OPTION sparql, and its result sets are mapped to each column of the table via the column OPTION variable.

CREATE FOREIGN TABLE film (
  film_id text    OPTIONS (variable '?film',     nodetype 'iri'),
  name text       OPTIONS (variable '?name',     nodetype 'literal', literal_type 'xsd:string'),
  released date   OPTIONS (variable '?released', nodetype 'literal', literal_type 'xsd:date'),
  runtime int     OPTIONS (variable '?runtime',  nodetype 'literal', literal_type 'xsd:integer'),
  abstract text   OPTIONS (variable '?abstract', nodetype 'literal', literal_type 'xsd:string')
)
SERVER dbpedia OPTIONS (
  sparql '
    PREFIX dbr: <http://dbpedia.org/resource/>
    PREFIX dbp: <http://dbpedia.org/property/>
    PREFIX dbo: <http://dbpedia.org/ontology/>

    SELECT DISTINCT ?film ?name ?released ?runtime ?abstract
    WHERE
    {
      ?film a dbo:Film ;
            rdfs:comment ?abstract ;
            dbp:name ?name ;
            dbp:released ?released ;
            dbp:runtime ?runtime .
      FILTER (LANG ( ?abstract ) = "en")
      FILTER (datatype(?released) = xsd:date)
      FILTER (datatype(?runtime) = xsd:integer)
     }
'); 

[!NOTE]
To visualise the foreign table’s columns and options use the psql meta-commands \d[+] or \det[+]

ALTER FOREIGN TABLE and ALTER SERVER

All options and parameters set to a FOREIGN TABLE or SERVER can be changed, dropped, and new ones can be added using the ALTER FOREIGN TABLE and ALTER SERVER commands.

Adding options

ALTER FOREIGN TABLE film OPTIONS (ADD enable_pushdown 'false',
                                  ADD log_sparql 'true');

ALTER SERVER dbpedia OPTIONS (ADD enable_pushdown 'false');

Changing previously configured options

ALTER FOREIGN TABLE film OPTIONS (SET enable_pushdown 'false');

ALTER SERVER dbpedia OPTIONS (SET enable_pushdown 'true');

Dropping options

ALTER FOREIGN TABLE film OPTIONS (DROP enable_pushdown,
                                  DROP log_sparql);

ALTER SERVER dbpedia OPTIONS (DROP enable_pushdown);

rdf_fdw_version

text rdf_fdw_version();

Description

Shows the version of the installed rdf_fdw and its main libraries.


Usage

SELECT rdf_fdw_version();
                                                                                                      rdf_fdw_version                                                                                                      
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 rdf_fdw = 2.0.0-dev, libxml/2.9.14, librdf/1.0.17, libcurl/7.88.1 GnuTLS/3.7.9 zlib/1.2.13 brotli/1.0.9 zstd/1.5.4 libidn2/2.3.3 libpsl/0.21.2 (+libidn2/2.3.3) libssh2/1.10.0 nghttp2/1.52.0 librtmp/2.3 OpenLDAP/2.5.13
(1 row)

rdf_fdw_clone_table

void rdf_fdw_clone_table(
  foreign_table text,
  target_table text,
  begin_offset int,
  fetch_size int,
  max_records int,
  orderby_column text,
  sort_order text,
  create_table boolean,
  verbose boolean,
  commit_page boolean
)
  • PostgreSQL 11+ only

Description

This procedure is designed to copy data from a FOREIGN TABLE to an ordinary TABLE. It provides the possibility to retrieve the data set in batches, so that known issues related to triplestore limits and client’s memory don’t bother too much.

Parameters

foreign_table (required): FOREIGN TABLE from where the data has to be copied.

target_table (required): heap TABLE where the data from the FOREIGN TABLE is copied to.

begin_offset: starting point in the SPARQL query pagination. Default 0.

fetch_size: size of the page fetched from the triplestore. Default is the value set at fetch_size in either SERVER or FOREIGN TABLE. In case SERVER and FOREIGN TABLE do not set fetch_size, the default will be set to 100.

max_records: maximum number of records that should be retrieved from the FOREIGN TABLE. Default 0, which means no limit.

orderby_column: ordering column used for the pagination - just like in SQL ORDER BY. Default '', which means that the function will chose a column to use in the ORDER BY clause on its own. That is, the procedure will try to set the first column with the option nodetype set to iri. If the table has no iri typed nodetype, the first column will be chosen as ordering column. If you do not wish to have an ORDER BY clause at al, set this parameter to NULL.

sort_order: ASC or DESC to sort the data returned in ascending or descending order, respectivelly. Default ASC.

create_table: creates the table set in target_table before harvesting the FOREIGN TABLE. Default false.

verbose: prints debugging messages in the standard output. Default false.

commit_page: commits the inserted records immediatelly or only after the transaction is finished. Useful for those who want records to be discarded in case of an error - following the principle of everything or nothing. Default true, which means that all inserts will me committed immediatelly.


Usage Example

CREATE SERVER dbpedia
FOREIGN DATA WRAPPER rdf_fdw 
OPTIONS (endpoint 'https://dbpedia.org/sparql');

CREATE FOREIGN TABLE public.dbpedia_cities (
  uri text           OPTIONS (variable '?city', nodetype 'iri'),
  city_name text     OPTIONS (variable '?name', nodetype 'literal', literal_type 'xsd:string'),
  elevation numeric  OPTIONS (variable '?elevation', nodetype 'literal', literal_type 'xsd:integer')
)
SERVER dbpedia OPTIONS (
  sparql '
    PREFIX dbo:  <http://dbpedia.org/ontology/>
    PREFIX foaf: <http://xmlns.com/foaf/0.1/>
    PREFIX dbr:  <http://dbpedia.org/resource/>
    SELECT *
    {?city a dbo:City ;
      foaf:name ?name ;
      dbo:federalState dbr:North_Rhine-Westphalia ;
      dbo:elevation ?elevation
    }
');

/*
 * Materialize all records from the FOREIGN TABLE 'public.dbpedia_cities' in 
 * the table 'public.t1_local'. 
 */ 
CALL rdf_fdw_clone_table(
      foreign_table => 'dbpedia_cities',
      target_table  => 't1_local',
      create_table => true);

SELECT * FROM t1_local;
                            uri                            |      city_name      | elevation 
-----------------------------------------------------------+---------------------+-----------
 http://dbpedia.org/resource/Aachen                        | Aachen              |     173.0
 http://dbpedia.org/resource/Bielefeld                     | Bielefeld           |     118.0
 http://dbpedia.org/resource/Dortmund                      | Dortmund            |      86.0
 http://dbpedia.org/resource/Düsseldorf                    | Düsseldorf          |      38.0
 http://dbpedia.org/resource/Gelsenkirchen                 | Gelsenkirchen       |      60.0
 http://dbpedia.org/resource/Hagen                         | Hagen               |     106.0
 http://dbpedia.org/resource/Hamm                          | Hamm                |      37.7
 http://dbpedia.org/resource/Herne,_North_Rhine-Westphalia | Herne               |      65.0
 http://dbpedia.org/resource/Krefeld                       | Krefeld             |      39.0
 http://dbpedia.org/resource/Mönchengladbach               | Mönchengladbach     |      70.0
 http://dbpedia.org/resource/Mülheim                       | Mülheim an der Ruhr |      26.0
 http://dbpedia.org/resource/Münster                       | Münster             |      60.0
 http://dbpedia.org/resource/Remscheid                     | Remscheid           |     365.0
(13 rows)

RDF Node Handling

The rdf_fdw extension introduces a custom data type called rdfnode that represents full RDF nodes exactly as they appear in a triplestore. It supports:

  • IRIs (e.g., <http://example.org/resource>)
  • Plain literals (e.g., "42")
  • Literals with language tags (e.g., "foo"@es)
  • Typed literals (e.g., "42"^^xsd:integer)

This type is useful when you want to inspect or preserve the full structure of RDF terms—including their language tags or datatypes—rather than just working with the value.

Casting Between rdfnode and Native PostgreSQL Types

Although rdfnode preserves the full RDF term, you can cast it to standard PostgreSQL types like text, int, or date when you only care about the literal value. Likewise, native PostgreSQL values can be cast into rdfnode, with appropriate RDF serialization.

From rdfnode to PostgreSQL:

SELECT CAST('"42"^^<http://www.w3.org/2001/XMLSchema#int>'::rdfnode AS int);
 int4 
------
   42
(1 row)

SELECT CAST('"42.73"^^<http://www.w3.org/2001/XMLSchema#float>'::rdfnode AS numeric);
 numeric 
---------
   42.73
(1 row)

SELECT CAST('"2025-05-16"^^<http://www.w3.org/2001/XMLSchema#date>'::rdfnode AS date);
    date    
------------
 2025-05-16
(1 row)

SELECT CAST('"2025-05-16T06:41:50"^^<http://www.w3.org/2001/XMLSchema#dateTime>'::rdfnode AS timestamp);
      timestamp      
---------------------
 2025-05-16 06:41:50
(1 row)

From PostgreSQL to rdfnode:

SELECT CAST('"foo"^^xsd:string' AS rdfnode);
                     rdfnode                      
--------------------------------------------------
 "foo"^^<http://www.w3.org/2001/XMLSchema#string>
(1 row)

SELECT CAST(42.73 AS rdfnode);
                       rdfnode                       
-----------------------------------------------------
 "42.73"^^<http://www.w3.org/2001/XMLSchema#decimal>
(1 row)

SELECT CAST(422892987223 AS rdfnode);
                         rdfnode                         
---------------------------------------------------------
 "422892987223"^^<http://www.w3.org/2001/XMLSchema#long>
(1 row)

SELECT CAST(CURRENT_DATE AS rdfnode);
                     current_date                      
-------------------------------------------------------
 "2025-05-16"^^<http://www.w3.org/2001/XMLSchema#date>
(1 row)

SELECT CAST(CURRENT_TIMESTAMP AS rdfnode);
                             current_timestamp                              
----------------------------------------------------------------------------
 "2025-05-16T06:41:50.221129Z"^^<http://www.w3.org/2001/XMLSchema#dateTime>
(1 row)

Choosing Between rdfnode and Native PostgreSQL Types

You can define foreign table columns using either:

  • rdfnode (recommended) — Use this when you want to preserve the full RDF term, including language tags, datatypes, and IRIs. This is also required if you want to use SPARQL functions, which do not support native PostgreSQL types.

  • PostgreSQL native types (e.g., text, int, date) — Use these when you prefer automatic type coercion and simpler SQL filtering, treating RDF values more like regular PostgreSQL data.

In short:

  • Use rdfnode when you need full RDF semantics or access to SPARQL-specific features.
  • Use native types when you prefer SQL-like convenience and don’t require RDF semantics or SPARQL functions.

Comparison of rdfnode with Native PostgreSQL Types

rdfnode supports standard comparison operators like =, !=, <, <=, >, >= — just like in SPARQL. Comparisons follow SPARQL 1.1 RDFterm-equal rules.

Examples: rdfnode vs rdfnode

SELECT '"foo"@en'::rdfnode = '"foo"@fr'::rdfnode;
 ?column? 
----------
 f
(1 row)

SELECT '"foo"^^xsd:string'::rdfnode > '"foobar"^^xsd:string'::rdfnode;
 ?column? 
----------
 f
(1 row)

SELECT '"foo"^^xsd:string'::rdfnode < '"foobar"^^xsd:string'::rdfnode;
 ?column? 
----------
 t
(1 row)

 SELECT '"42"^^xsd:int'::rdfnode = '"42"^^xsd:short'::rdfnode;
 ?column? 
----------
 t
(1 row)

 SELECT '"73.42"^^xsd:float'::rdfnode < '"100"^^xsd:short'::rdfnode;
 ?column? 
----------
 t
(1 row)

The rdfnode data type also allow comparisons with PostgreSQL native data types, such as int, date, numeric, etc.

Examples: rdfnode vs PostgreSQL types

SELECT '"42"^^xsd:int'::rdfnode = 42;
 ?column? 
----------
 t
(1 row)

SELECT '"2010-01-08"^^xsd:date'::rdfnode < '2020-12-30'::date;
 ?column? 
----------
 t
(1 row)

SELECT '"42.73"^^xsd:decimal'::rdfnode > 42;
 ?column? 
----------
 t
(1 row)

SELECT '"42.73"^^xsd:decimal'::rdfnode < 42.99;
 ?column? 
----------
 t
(1 row)

SELECT '"2025-05-19T10:45:42Z"^^xsd:dateTime'::rdfnode = '2025-05-19 10:45:42'::timestamp;
 ?column? 
----------
 t
(1 row)

SPARQL Functions

rdf_fdw implements most of the SPARQL 1.1 built-in functions, exposing them as SQL-callable functions under the dedicated sparql schema. This avoids conflicts with similarly named built-in PostgreSQL functions such as round, replace, or ceil. These functions operate on RDF values retrieved through FOREIGN TABLEs and can be used in SQL queries or as part of pushdown expressions. They adhere closely to SPARQL semantics, including handling of RDF literals, language tags, datatypes, and null propagation rules, enabling expressive and standards-compliant RDF querying directly inside PostgreSQL.

⚠️ Note on SPARQL Compatibility

While most RDF triplestores claim SPARQL 1.1 compliance, their behavior often diverges from the standard—particularly in how they handle literals with language tags or datatypes. For example, the following query may produce different results depending on the backend:

SELECT (REPLACE("foo"@en, "o"@de, "xx"@fr) AS ?str) {}
  • Virtuoso: "fxxxx"
  • Blazegraph: Unknown error: incompatible operand for REPLACE: “o”@de
  • GraphDB: "fxxxx"@en

Such inconsistencies can lead to unexpected or confusing results. To avoid surprises:

  • Always test how your target triplestore handles tagged or typed literals.
  • Consider simpler (less performant) alternatives like STR when working with language-tagged values.
  • Enable the log_sparql option in rdf_fdw to compare the number of records returned by the SPARQL endpoint with those visible in PostgreSQL. If the counts differ, it likely means some records were filtered out locally due to incompatible behavior in pushdown function evaluation.

BOUND

sparql.bound(value rdfnode) → boolean

Returns true if the RDF node is bound to a value, and false otherwise. Values like NaN or INF are considered bound.

Example:

SELECT sparql.bound(NULL), sparql.bound('"NaN"^^xsd:double');
 bound | bound 
-------+-------
 f     | t
(1 row)

COALESCE

sparql.coalesce(value1 rdfnode, value2 rdfnode, ... ) → rdfnode

Returns the first bound RDF node from the argument list. If none of the inputs are bound (i.e., all are NULL), returns NULL. The behavior mimics the SPARQL 1.1 COALESCE() function, valuating arguments from left to right. This is useful when working with optional data where fallback values are needed.

Example:

 SELECT sparql.coalesce(NULL, NULL, '"foo"^^xsd:string');
                     coalesce                     
--------------------------------------------------
 "foo"^^<http://www.w3.org/2001/XMLSchema#string>
(1 row)

SAMETERM

sparql.sameTerm(a rdfnode, b rdfnode) → boolean

Returns true if the two RDF nodes are exactly the same term, and false otherwise. This comparison is strict and includes datatype, language tag, and node type (e.g., literal vs IRI). The behavior follows SPARQL 1.1’s sameTerm functional form, which does not allow coercion or implicit casting — unlike = or IS NOT DISTINCT FROM.

Examples:

SELECT sparql.sameterm('"42"^^xsd:int', '"42"^^xsd:long');
 sameterm 
----------
 f
(1 row)

SELECT sparql.sameterm('"foo"@en', '"foo"@en');
 sameterm 
----------
 t
(1 row)

SELECT sparql.sameterm('"foo"@en', '"foo"@fr');
 sameterm 
----------
 f
(1 row)

[!NOTE]
Use sameterm when you need exact RDF identity, including type and language tag. For value-based comparison with implicit coercion, use = instead.

isIRI

sparql.isiri(value rdfnode) → boolean

Returns true if the given RDF node is an IRI, and false otherwise. This function implements the SPARQL 1.1 isIRI() test, which checks whether the term is an IRI—not a literal, blank node, or unbound value.

Examples:

SELECT sparql.isIRI('<https://foo.bar/>'); 
 isiri 
-------
 t
(1 row)

SELECT sparql.isIRI('"foo"^^xsd:string');
 isiri 
-------
 f
(1 row)

SELECT sparql.isIRI('_:bnode42');
 isiri 
-------
 f
(1 row)

SELECT sparql.isIRI(NULL);
 isiri 
-------
 f
(1 row)

[!NOTE]
isURI is an alternate spelling for the isIRI function.

isBLANK

sparql.isblank(value rdfnode) → boolean

Returns true if the given RDF node is a blank node, and false otherwise. This function implements the SPARQL 1.1 isBlank() function, which is used to detect anonymous resources (blank nodes) in RDF graphs.

SELECT sparql.isblank('_:bnode42');
 isblank 
---------
 t
(1 row)

SELECT sparql.isblank('"foo"^^xsd:string');
 isblank 
---------
 f
(1 row)

isLITERAL

sparql.isliteral(value rdfnode) → boolean

Returns true if the given RDF node is a literal, and false otherwise. This function implements the SPARQL 1.1 isLiteral() test. It returns false for IRIs, blank nodes, and unbound (NULL) values.

Examples:

postgres=# SELECT sparql.isliteral('"foo"^^xsd:string');
 isliteral 
-----------
 t
(1 row)

postgres=# SELECT sparql.isliteral('"foo"^^@es');
 isliteral 
-----------
 t
(1 row)

postgres=# SELECT sparql.isliteral('_:bnode42');
 isliteral 
-----------
 f
(1 row)

postgres=# SELECT sparql.isliteral('<http://foo.bar>');
 isliteral 
-----------
 f
(1 row)

postgres=# SELECT sparql.isliteral(NULL);
 isliteral 
-----------
 f
(1 row)

isNUMERIC

sparql.isnumeric(term rdfnode) → boolean

Returns true if the RDF node is a literal with a numeric datatype (such as xsd:int, xsd:decimal, etc.), and false otherwise. See the SPARQL 1.1 section on Operand Data Types for more details.

Examples:

SELECT sparql.isnumeric('"42"^^xsd:integer');
 isnumeric 
-----------
 t
(1 row)

SELECT sparql.isnumeric('"42.73"^^xsd:decimal');
 isnumeric 
-----------
 t
(1 row)

SELECT sparql.isnumeric('"42.73"^^xsd:string');
 isnumeric 
-----------
 f
(1 row)

SELECT sparql.isnumeric(NULL);
 isnumeric 
-----------
 f
(1 row)

STR

sparql.str(value rdfnode) → rdfnode

Returns the lexical form (the string content) of the RDF node, as described at This implements the SPARQL 1.1 str() specification. For literals, this means stripping away the language tag or datatype. For IRIs, it returns the IRI string. For blank nodes, returns their label.

Examples:

SELECT sparql.str('"foo"@en');
  str  
-------
 "foo"
(1 row)

SELECT sparql.str('"foo"^^xsd:string');
  str  
-------
 "foo"
(1 row)

SELECT sparql.str('<http://foo.bar>');
       str        
------------------
 "http://foo.bar"
(1 row)

LANG

sparql.str(value rdfnode) → rdfnode

Returns the language tag of the literal, or an empty string if none exists. Implements the SPARQL 1.1 LANG() function. All other RDF nodes — including IRIs, blank nodes, and typed literals — return an empty string.

SELECT sparql.lang('"foo"@es');
 lang 
------
 es
(1 row)

SELECT sparql.lang('"foo"');
 lang 
------
 
(1 row)

SELECT sparql.lang('"foo"^^xsd:string');
 lang 
------
 
(1 row)

DATATYPE

sparql.datatype(value rdfnode) → rdfnode

Returns the datatype IRI of a literal RDF node.

  • For typed literals, returns the declared datatype (e.g., xsd:int, xsd:dateTime, etc.).
  • For plain (untyped) literals, returns xsd:string.
  • For language-tagged literals, returns rdf:langString.
  • For non-literals (IRIs, blank nodes), returns NULL.

This behavior complies with both SPARQL 1.1 and RDF 1.1.

Examples:

SELECT sparql.datatype('"42"^^xsd:int');
                datatype                
----------------------------------------
 <http://www.w3.org/2001/XMLSchema#int>
(1 row)

SELECT sparql.datatype('"foo"');
                 datatype                  
-------------------------------------------
 <http://www.w3.org/2001/XMLSchema#string>
(1 row)

SELECT sparql.datatype('"foo"@de');
                        datatype                         
---------------------------------------------------------
 <http://www.w3.org/1999/02/22-rdf-syntax-ns#langString>
(1 row)

SELECT sparql.datatype('<http://foo.bar>');
 datatype 
----------
 NULL
(1 row)

SELECT sparql.datatype('_:bnode42');
 datatype 
----------
 NULL
(1 row)

[!NOTE]
Keep in mind that some triplestores (like Virtuoso) return xsd:anyURI for IRIs, but this behaviour is not defined in SPARQL 1.1 and is not standard-compliant.

IRI

sparql.iri(value rdfnode) → rdfnode

Constructs an RDF IRI from a string. Implements the SPARQL 1.1 IRI() function. If the input is not a valid IRI, the function still wraps it as-is into an RDF IRI. No validation is performed.

Examples:

SELECT sparql.iri('http://foo.bar');
       iri        
------------------
 <http://foo.bar>
(1 row)

BNODE

sparql.bnode(value rdfnode DEFAULT NULL) → rdfnode

Constructs a blank node. If a string is provided, it’s used as the label. If called with no argument, generates an automatically scoped blank node identifier. Implements the SPARQL 1.1 BNODE() function.

Examples:

SELECT sparql.bnode('foo');
 bnode 
-------
 _:foo
(1 row)

SELECT sparql.bnode('"foo"^^xsd:string');
 bnode 
-------
 _:foo
(1 row)

SELECT sparql.bnode();
       bnode        
--------------------
 _:b800704569809508
(1 row)

STRDT

sparql.strdt(lexical rdfnode, datatype_iri rdfnode) → rdfnode

Constructs a typed literal from a lexical string and a datatype IRI. Implements the SPARQL 1.1 STRDT() function. This function can also be used to change the datatype of an existing literal by extracting its lexical form (e.g., with sparql.str()) and applying a new datatype.

Examples:

SELECT sparql.strdt('42','xsd:int');
                    strdt                     
----------------------------------------------
 "42"^^<http://www.w3.org/2001/XMLSchema#int>
(1 row)

SELECT sparql.strdt('2025-01-01', 'http://www.w3.org/2001/XMLSchema#date');
                         strdt                         
-------------------------------------------------------
 "2025-01-01"^^<http://www.w3.org/2001/XMLSchema#date>
(1 row)

SELECT sparql.strdt('"2025-01-01"^^xsd:string', 'http://www.w3.org/2001/XMLSchema#date');
                         strdt                         
-------------------------------------------------------
 "2025-01-01"^^<http://www.w3.org/2001/XMLSchema#date>
(1 row)

STLANG

sparql.strlang(lexical rdfnode, lang_tag rdfnode) → rdfnode

Constructs a language-tagged literal from a string and a language code. Implements the SPARQL 1.1 STRLANG() function. You can also use this function to re-tag an existing literal by extracting its lexical form and assigning a new language tag.

Examples:

SELECT sparql.strlang('foo','pt');
 strlang  
----------
 "foo"@pt
(1 row)

SELECT sparql.strlang('"foo"@pt','es');
 strlang  
----------
 "foo"@es
(1 row)

UUID

sparql.uuid() → rdfnode

Generates a fresh, globally unique IRI. Implements the SPARQL 1.1 UUID() function.

Example:

SELECT sparql.uuid();
                      uuid                       
-------------------------------------------------
 <urn:uuid:1beda602-2e35-4d13-a907-071454d2fce7>
(1 row)

STRUUID

sparql.struuid() → rdfnode

Generates a fresh, random UUID as a plain literal string. Implements the SPARQL 1.1 STRUUID() function. Each call returns a unique string literal containing the UUID. This is useful when you want to store or display the UUID as text rather than an IRI.

Example:

SELECT sparql.struuid();
                struuid                 
----------------------------------------
 "25a55e10-f789-4aab-bb7f-05f2ba495fd2"
(1 row)

STRLEN

sparql.strlen(value rdfnode) → int

Returns the number of characters in the lexical form of the RDF node. Implements the SPARQL 1.1 STRLEN() function.

Examples:

SELECT sparql.strlen('"foo"');
 strlen 
--------
      3
(1 row)

SELECT sparql.strlen('"foo"@de');
 strlen 
--------
      3
(1 row)

SELECT sparql.strlen('"foo"^^xsd:string');
 strlen 
--------
      3
(1 row)

SELECT sparql.strlen('"42"^^xsd:int');
 strlen 
--------
      2
(1 row)

SUBSTR

sparql.substr(value rdfnode, start int, length int DEFAULT NULL) → rdfnode

Extracts a substring from the lexical form of the RDF node. Implements the SPARQL 1.1 SUBSTR() function.

  • The start index is 1-based.
  • If length is omitted, returns everything to the end of the string.
  • returns NULL if any of the arguments is NULL

Examples:

SELECT sparql.substr('"foobar"', 1, 3);
 substr 
--------
 "foo"
(1 row)

postgres=# SELECT sparql.substr('"foobar"', 4);
 substr 
--------
 "bar"
(1 row)

UCASE

sparql.ucase(value rdfnode) → rdfnode

Converts the lexical form of the literal to uppercase. Implements the SPARQL 1.1 UCASE() function.

Examples:

SELECT sparql.ucase('"foo"');
 ucase 
-------
 "FOO"
(1 row)

SELECT sparql.ucase('"foo"@en');
  ucase   
----------
 "FOO"@en
(1 row)

SELECT sparql.ucase('"foo"^^xsd:string');
                      ucase                       
--------------------------------------------------
 "FOO"^^<http://www.w3.org/2001/XMLSchema#string>
(1 row)

LCASE

sparql.lcase(value rdfnode) → rdfnode

Converts the lexical form of the literal to lowercase. Implements the SPARQL 1.1 LCASE() function.

Examples:

SELECT sparql.lcase('"FOO"');
 lcase 
-------
 "foo"
(1 row)

SELECT sparql.lcase('"FOO"@en');
  lcase   
----------
 "foo"@en
(1 row)

SELECT sparql.lcase('"FOO"^^xsd:string');
                      lcase                       
--------------------------------------------------
 "foo"^^<http://www.w3.org/2001/XMLSchema#string>
(1 row)

STRSTARTS

sparql.strstarts(value rdfnode, prefix rdfnode) → boolean

Returns true if the lexical form of the RDF node starts with the given string. Implements the SPARQL 1.1 STRSTARTS() function.

Examples:

SELECT sparql.strstarts('"foobar"^^xsd:string', '"foo"^^xsd:string');
 strstarts 
-----------
 t
(1 row)

SELECT sparql.strstarts('"foobar"@en', '"foo"^^xsd:string');
 strstarts 
-----------
 t
(1 row)

STRENDS

sparql.strends(value rdfnode, suffix rdfnode) → boolean

Returns true if the lexical form of the RDF node ends with the given string. Implements the SPARQL 1.1 STRENDS() function.

Examples:

SELECT sparql.strends('"foobar"^^xsd:string', '"bar"^^xsd:string');
 strends 
---------
 t
(1 row)

postgres=# SELECT sparql.strends('"foobar"@en', '"bar"^^xsd:string');
 strends 
---------
 t
(1 row)

CONTAINS

sparql.contains(value rdfnode, substring rdfnode) → boolean

Returns true if the lexical form of the RDF node contains the given substring. Implements the SPARQL 1.1 CONTAINS() function.

Examples:

SELECT sparql.contains('"_foobar_"^^xsd:string', '"foo"');
 contains 
----------
 t
(1 row)

SELECT sparql.contains('"_foobar_"^^xsd:string', '"foo"@en');
 contains 
----------
 t
(1 row)

STRBEFORE

sparql.strbefore(value rdfnode, delimiter rdfnode) → rdfnode

Returns the substring before the first occurrence of the delimiter in the lexical form. If the delimiter is not found, returns an empty string. Implements the SPARQL 1.1 STRBEFORE() function.

Examples:

SELECT sparql.strbefore('"foobar"^^xsd:string','"bar"^^xsd:string');
                    strbefore                     
--------------------------------------------------
 "foo"^^<http://www.w3.org/2001/XMLSchema#string>
(1 row)

SELECT sparql.strbefore('"foobar"@en','"bar"^^xsd:string');
 strbefore 
-----------
 "foo"@en
(1 row)

SELECT sparql.strbefore('"foobar"','"bar"^^xsd:string');
 strbefore 
-----------
 "foo"
(1 row)

SELECT sparql.strbefore('"foobar"','"bar"');
 strbefore 
-----------
 "foo"
(1 row)

STRAFTER

sparql.strafter(value rdfnode, delimiter rdfnode) → rdfnode

Returns the substring after the first occurrence of the delimiter in the lexical form. If the delimiter is not found, returns an empty string. Implements the SPARQL 1.1 STRAFTER() function.

Examples:

SELECT sparql.strafter('"foobar"^^xsd:string','"foo"^^xsd:string');
                     strafter                     
--------------------------------------------------
 "bar"^^<http://www.w3.org/2001/XMLSchema#string>
(1 row)

SELECT sparql.strafter('"foobar"@en','"foo"^^xsd:string');
 strafter 
----------
 "bar"@en
(1 row)

SELECT sparql.strafter('"foobar"','"foo"^^xsd:string');
 strafter 
----------
 "bar"
(1 row)

SELECT sparql.strafter('"foobar"','"foo"');
 strafter 
----------
 "bar"
(1 row)

ENCODE_FOR_URI

sparql.encode_for_uri(value rdfnode) → rdfnode

Returns a URI-safe version of the lexical form by percent-encoding special characters. Implements the SPARQL 1.1 ENCODE_FOR_URI() function.

SELECT sparql.encode_for_uri('"foo&bar!"');
 encode_for_uri 
----------------
 "foo%26bar%21"
(1 row)

CONCAT

sparql.concat(value1 rdfnode, value2 rdfnode, ...) → rdfnode

Concatenates all input strings into one. Implements the SPARQL 1.1 CONCAT() function.

SELECT sparql.concat('"foo"@en','"&"@en', '"bar"@en');
    concat    
--------------
 "foo&bar"@en
(1 row)

postgres=# SELECT sparql.concat('"foo"^^xsd:string','"&"^^xsd:string', '"bar"^^xsd:string');
                        concat                        
------------------------------------------------------
 "foo&bar"^^<http://www.w3.org/2001/XMLSchema#string>
(1 row)

postgres=# SELECT sparql.concat('"foo"','"&"', '"bar"');
  concat   
-----------
 "foo&bar"
(1 row)

LANGMATCHES

sparql.langmatches(lang_tag rdfnode, pattern rdfnode) → boolean

Checks whether a language tag matches a language pattern (e.g., en matches en-US). Implements the SPARQL 1.1 LANGMATCHES() function.

Example:

SELECT sparql.langmatches('en', 'en');
 langmatches 
-------------
 t
(1 row)

SELECT sparql.langmatches('en-US', 'en');
 langmatches 
-------------
 t
(1 row)

SELECT sparql.langmatches('en', 'de');
 langmatches 
-------------
 f
(1 row)

SELECT sparql.langmatches('en', '*');
 langmatches 
-------------
 t
(1 row)

REGEX

sparql.regex(value rdfnode, pattern rdfnode, flags rdfnode DEFAULT '') → boolean

Checks if the lexical form matches the given regular expression. Implements the SPARQL 1.1 REGEX() function.

  • Supported flags: i (case-insensitive)

Example:

SELECT sparql.regex('"Hello World"', '^hello', 'i');
 regex 
-------
 t
(1 row)

REPLACE

sparql.replace(value rdfnode, pattern rdfnode, replacement rdfnode, flags rdfnode DEFAULT '') → rdfnode

Replaces parts of the lexical form using a regular expression. Implements the SPARQL 1.1 REPLACE() function.

  • Supports i, m, and g flags.
SELECT sparql.replace('"foo bar foo"', 'foo', 'baz', 'g');
    replace    
---------------
 "baz bar baz"
(1 row)

ABS

sparql.abs(value rdfnode) → numeric

Returns the absolute value of a numeric literal. Implements the SPARQL 1.1 ABS() function.

Examples:

SELECT sparql.abs('"-42"^^xsd:int');
                     abs                      
----------------------------------------------
 "42"^^<http://www.w3.org/2001/XMLSchema#int>
(1 row)

SELECT sparql.abs('"3.14"^^xsd:decimal');
                        abs                         
----------------------------------------------------
 "3.14"^^<http://www.w3.org/2001/XMLSchema#decimal>
(1 row)

ROUND

sparql.round(value rdfnode) → numeric

Rounds the numeric literal to the nearest integer. Implements the SPARQL 1.1 ROUND() function.

Examples:

SELECT sparql.round('"2.5"^^xsd:decimal');
                      round                      
-------------------------------------------------
 "3"^^<http://www.w3.org/2001/XMLSchema#decimal>
(1 row)

SELECT sparql.round('"-2.5"^^xsd:float');
                     round                      
------------------------------------------------
 "-2"^^<http://www.w3.org/2001/XMLSchema#float>
(1 row)

CEIL

sparql.ceil(value rdfnode) → numeric

Returns the smallest integer greater than or equal to the numeric value. Implements the SPARQL 1.1 CEIL() function.

Examples:

SELECT sparql.ceil('"3.14"^^xsd:decimal');
                      ceil                       
-------------------------------------------------
 "4"^^<http://www.w3.org/2001/XMLSchema#decimal>
(1 row)

SELECT sparql.ceil('"-2.1"^^xsd:float');
                      ceil                      
------------------------------------------------
 "-2"^^<http://www.w3.org/2001/XMLSchema#float>
(1 row)

FLOOR

sparql.floor(value rdfnode) → numeric

Returns the greatest integer less than or equal to the numeric value. Implements the SPARQL 1.1 FLOOR() function.

Examples:

SELECT sparql.floor('"3.9"^^xsd:decimal');
                      floor                      
-------------------------------------------------
 "3"^^<http://www.w3.org/2001/XMLSchema#decimal>
(1 row)

SELECT sparql.floor('"-2.1"^^xsd:float');
                     floor                      
------------------------------------------------
 "-3"^^<http://www.w3.org/2001/XMLSchema#float>
(1 row)

RAND

sparql.rand() → rdfnode

Returns a random floating-point number between 0.0 and 1.0. Implements the SPARQL 1.1 RAND() function.

Examples:

SELECT sparql.rand();
                               rand                               
------------------------------------------------------------------
 "0.14079881274421657"^^<http://www.w3.org/2001/XMLSchema#double>
(1 row)

YEAR

sparql.year(value rdfnode) → int

Returns the year component of an xsd:dateTime or xsd:date literal. Implements the SPARQL 1.1 YEAR() function.

Example:

SELECT sparql.year('"2025-05-17T14:00:00Z"^^xsd:dateTime');
 year 
------
 2025

MONTH

sparql.month(value rdfnode) → int

Returns the month component (1–12) from a datetime or date. Implements the SPARQL 1.1 MONTH() function.

Example:

SELECT sparql.month('"2025-05-17T14:00:00Z"^^xsd:dateTime');
 month 
-------
     5
(1 row)

DAY

sparql.day(value rdfnode) → int

Returns the day of the month from a date or datetime literal. Implements the SPARQL 1.1 DAY() function.

Example:

SELECT sparql.day('"2025-05-17T14:00:00Z"^^xsd:dateTime');
 day 
-----
  17
(1 row)

HOURS

sparql.hours(value rdfnode) → int

Extracts the hour (0–23) from a datetime literal. Implements the SPARQL 1.1 HOURS() function.

Example:

SELECT sparql.hours('"2025-05-17T14:00:00Z"^^xsd:dateTime');
 hours 
-------
    14
(1 row)

MINUTES

sparql.minutes(value rdfnode) → int

Returns the minute component (0–59) of a datetime literal. Implements the SPARQL 1.1 MINUTES() function.

Example:

SELECT sparql.minutes('"2025-05-17T14:42:37Z"^^xsd:dateTime');
 minutes 
---------
      42
(1 row)

SECONDS

sparql.seconds(value rdfnode) → int

Returns the seconds (including fractions) from a datetime literal. Implements the SPARQL 1.1 SECONDS() function.

Example:

SELECT sparql.seconds('"2025-05-17T14:42:37Z"^^xsd:dateTime');
 seconds 
---------
      37
(1 row)

TIMEZONE

sparql.timezone(datetime rdfnode) → rdfnode

Returns the timezone offset as a duration literal (e.g., “PT2H”), or NULL if none. Implements the SPARQL 1.1 TIMEZONE() function.

Example:

SELECT sparql.timezone('"2025-05-17T10:00:00+02:00"^^xsd:dateTime');
                          timezone                          
------------------------------------------------------------
 "PT2H"^^<http://www.w3.org/2001/XMLSchema#dayTimeDuration>
(1 row)

TZ

sparql.tz(datetime rdfnode) → rdfnode

Returns the timezone offset as a string (e.g., +02:00 or Z). Implements the SPARQL 1.1 TZ() function.

Examples:

SELECT sparql.tz('"2025-05-17T10:00:00+02:00"^^xsd:dateTime');
    tz    
----------
 "+02:00"
(1 row)

SELECT sparql.tz('"2025-05-17T08:00:00Z"^^xsd:dateTime');
 tz  
-----
 "Z"
(1 row)

MD5

sparql.md5(value rdfnode) → rdfnode

Returns the MD5 hash of the lexical form of the input RDF literal, encoded as a lowercase hexadecimal string. Implements the SPARQL 1.1 MD5() function. The result is returned as a plain literal (xsd:string).

Examples:

SELECT sparql.md5('"foo"');
                md5                 
------------------------------------
 "acbd18db4cc2f85cedef654fccc4a4d8"
(1 row)

SELECT sparql.md5('42'::rdfnode);
                md5                 
------------------------------------
 "a1d0c6e83f027327d8461063f4ac58a6"
(1 row)

LEX

sparql.lex(value rdfnode) → rdfnode

Extracts the lexical value of a given rdfnode. This isa convenience function that is not part of the SPARQL 1.1 standard.

Examples:

SELECT sparql.lex('"foo"^^xsd:string');
 lex 
-----
 foo
(1 row)

SELECT sparql.lex('"foo"@es');
 lex 
-----
 foo
(1 row)

SPARQL describe

sparql.describe(server text, query text, raw_literal boolean, base_uri text) → triple

Description

The sparql.describe function executes a SPARQL DESCRIBE query against a specified RDF triplestore SERVER. It retrieves RDF triples describing a resource (or resources) identified by the query and returns them as a table with three columns: subject, predicate, and object. This function is useful for exploring RDF data by fetching detailed descriptions of resources from a triplestore. The function leverages the Redland RDF library (librdf) to parse the RDF/XML response from the triplestore into triples, which are then returned as rows in the result set.

Parameters

server (required): The name of the foreign server (defined via CREATE SERVER) that specifies the SPARQL endpoint to query. This must correspond to an existing rdf_fdw server configuration. Cannot be empty or NULL.

describe_query (required): A valid SPARQL DESCRIBE query string (e.g., DESCRIBE <http://example.org/resource>). Cannot be empty or NULL.

raw_literal: Controls how literal values in the object column are formatted (default true):

  • true: Preserves the full RDF literal syntax, including datatype (e.g., "123"^^<http://www.w3.org/2001/XMLSchema#integer>) or language tags (e.g., "hello"@en).
  • false: Strips datatype and language tags, returning only the literal value (e.g., "123" or "hello").

base_uri: The base URI used to resolve relative URIs in the RDF/XML response from the triplestore. If empty, defaults to “http://rdf_fdw.postgresql.org/”. Useful for ensuring correct URI resolution in the parsed triples.

Return Value

Returns a table with the following rdfnode columns:

  • subject: The subject of each RDF triple, typically a URI or blank node identifier.
  • predicate: The predicate (property) of each RDF triple, always a URI.
  • object: The object of each RDF triple, which may be a URI, blank node, or literal value.

Usage Example

CREATE SERVER wikidata
FOREIGN DATA WRAPPER rdf_fdw
OPTIONS (endpoint 'https://query.wikidata.org/sparql');

SELECT subject, predicate, object
FROM rdf_fdw_describe('wikidata', 'DESCRIBE <http://www.wikidata.org/entity/Q61308849>', true);

INFO:  SPARQL query sent to 'https://query.wikidata.org/sparql':

DESCRIBE <http://www.wikidata.org/entity/Q61308849>


                 subject                  |                 predicate                  |                               object
------------------------------------------+--------------------------------------------+------------------------------------------------------------------------------
 http://www.wikidata.org/entity/Q61308849 | http://www.wikidata.org/prop/direct/P3999  | "2015-01-01T00:00:00Z"^^<http://www.w3.org/2001/XMLSchema#dateTime>
 http://www.wikidata.org/entity/Q61308849 | http://schema.org/dateModified             | "2024-05-01T21:36:41Z"^^<http://www.w3.org/2001/XMLSchema#dateTime>
 http://www.wikidata.org/entity/Q61308849 | http://schema.org/version                  | "2142303130"^^<http://www.w3.org/2001/XMLSchema#integer>
 http://www.wikidata.org/entity/Q61308849 | http://www.wikidata.org/prop/direct/P127   | http://www.wikidata.org/entity/Q349450
...
 http://www.wikidata.org/entity/Q61308849 | http://www.wikidata.org/prop/direct/P625   | "Point(-133.03 69.43)"^^<http://www.opengis.net/ont/geosparql#wktLiteral>
(37 rows)

Pushdown

A pushdown is the ability to translate SQL queries so that operations—such as sorting, formatting, and filtering—are performed directly in the data source rather than in PostgreSQL. This feature can significantly reduce the number of records retrieved from the data source.

For example, if a SQL LIMIT clause is not pushed down, the target system will perform a full scan of the data source, prepare the entire result set for transfer, send it to PostgreSQL over the network, and only then will PostgreSQL discard the unnecessary data. Depending on the total number of records, this process can be extremely inefficient.

In a nutshell, the rdf_fdw extension attempts to translate SQL into SPARQL queries. However, due to fundamental differences between the two languages, this is not always straightforward. As a rule of thumb, it is often best to keep SQL queries involving foreign tables as simple as possible. The rdf_fdw supports pushdown of most SPARQL 1.1 built-in functions and several PostgreSQL instructions, such as LIMIT and IN/NOT IN.

LIMIT

LIMIT clauses are pushed down only if the SQL query does not contain aggregates and when all conditions in the WHERE clause can be translated to SPARQL.

| SQL | SPARQL| | – | — | | LIMIT x| LIMIT x | FETCH FIRST x ROWS | LIMIT x | | FETCH FIRST ROW ONLY | LIMIT 1 |

OFFSET pushdown is not supported, meaning that OFFSET filters will be applied locally in PostgreSQL.

Example:

SELECT s, p, o FROM rdbms
WHERE 
  p = '<http://www.w3.org/2000/01/rdf-schema#label>' AND
  sparql.langmatches(sparql.lang(o), 'es')
FETCH FIRST 5 ROWS ONLY;

INFO:  SPARQL query sent to 'https://query.wikidata.org/sparql':

SELECT ?s ?p ?o 
{
      ?s wdt:P31 wd:Q3932296 .
          ?s ?p ?o
         
 ## rdf_fdw pushdown conditions ##
 FILTER(?p = <http://www.w3.org/2000/01/rdf-schema#label>)
 FILTER(LANGMATCHES(LANG(?o), "es"))
}
LIMIT 5

INFO:  SPARQL returned 5 records.

                    s                     |                      p                       |             o             
------------------------------------------+----------------------------------------------+---------------------------
 <http://www.wikidata.org/entity/Q850>    | <http://www.w3.org/2000/01/rdf-schema#label> | "MySQL"@es
 <http://www.wikidata.org/entity/Q60463>  | <http://www.w3.org/2000/01/rdf-schema#label> | "Ingres"@es
 <http://www.wikidata.org/entity/Q192490> | <http://www.w3.org/2000/01/rdf-schema#label> | "PostgreSQL"@es
 <http://www.wikidata.org/entity/Q215819> | <http://www.w3.org/2000/01/rdf-schema#label> | "Microsoft SQL Server"@es
 <http://www.wikidata.org/entity/Q80426>  | <http://www.w3.org/2000/01/rdf-schema#label> | "Vectorwise"@es
(5 rows)
  • FETCH FIRST 5 ROWS ONLY was pushed down as LIMIT 5 in the SPARQL query.

ORDER BY

ORDER BY can be pushed down if the data types can be translated into SPARQL.

| SQL | SPARQL| | – | — | | ORDER BY x ASC, ORDER BY x | ORDER BY ASC(x)| | ORDER BY x DESC |ORDER BY DESC(x) |

Example:

SELECT s, p, o FROM rdbms
WHERE 
  p = '<http://www.w3.org/2000/01/rdf-schema#label>' AND
  sparql.langmatches(sparql.lang(o), 'es')
ORDER BY s ASC, o DESC
FETCH FIRST 5 ROWS ONLY;

INFO:  SPARQL query sent to 'https://query.wikidata.org/sparql':

SELECT ?s ?p ?o 
{
      ?s wdt:P31 wd:Q3932296 .
          ?s ?p ?o
         
 ## rdf_fdw pushdown conditions ##
 FILTER(?p = <http://www.w3.org/2000/01/rdf-schema#label>)
 FILTER(LANGMATCHES(LANG(?o), "es"))
}
ORDER BY  ASC (?s)  DESC (?o)
LIMIT 5

INFO:  SPARQL returned 5 records.

                     s                      |                      p                       |            o             
--------------------------------------------+----------------------------------------------+--------------------------
 <http://www.wikidata.org/entity/Q1012765>  | <http://www.w3.org/2000/01/rdf-schema#label> | "SQL Express Edition"@es
 <http://www.wikidata.org/entity/Q1050734>  | <http://www.w3.org/2000/01/rdf-schema#label> | "Informix"@es
 <http://www.wikidata.org/entity/Q12621393> | <http://www.w3.org/2000/01/rdf-schema#label> | "Tibero"@es
 <http://www.wikidata.org/entity/Q1493683>  | <http://www.w3.org/2000/01/rdf-schema#label> | "MySQL Clúster"@es
 <http://www.wikidata.org/entity/Q15275385> | <http://www.w3.org/2000/01/rdf-schema#label> | "SingleStore"@es
(5 rows)
  • ORDER BY s ASC, o DESC was pushed down as SPARQL ORDER BY ASC (?s) DESC (?o)

DISTINCT

DISTINCT is pushed down to the SPARQL SELECT statement just as in SQL. However, if the configured SPARQL query already includes a DISTINCT or REDUCED modifier, the SQL DISTINCT won’t be pushed down. Since there is no SPARQL equivalent for DISTINCT ON, this feature cannot be pushed down.

Example:

SELECT DISTINCT p, o FROM rdbms
WHERE 
  p = '<http://www.w3.org/2000/01/rdf-schema#label>' AND
  sparql.langmatches(sparql.lang(o), 'de');

INFO:  SPARQL query sent to 'https://query.wikidata.org/sparql':

SELECT DISTINCT ?p ?o 
{
      ?s wdt:P31 wd:Q3932296 .
          ?s ?p ?o
         
 ## rdf_fdw pushdown conditions ##
 FILTER(?p = <http://www.w3.org/2000/01/rdf-schema#label>)
 FILTER(LANGMATCHES(LANG(?o), "de"))
}
ORDER BY ASC (?p)  ASC (?o)

INFO:  SPARQL returned 43 records.

                      p                       |                 o                 
----------------------------------------------+-----------------------------------
 <http://www.w3.org/2000/01/rdf-schema#label> | "4th Dimension"@de
 <http://www.w3.org/2000/01/rdf-schema#label> | "Amazon Redshift"@de
 <http://www.w3.org/2000/01/rdf-schema#label> | "ArcSDE"@de
...
 <http://www.w3.org/2000/01/rdf-schema#label> | "dBASE Mac"@de
(43 rows)
  • DISTINCT was pushed down to SPARQL.

WHERE

The rdf_fdw extension supports pushdown of many SQL expressions in the WHERE clause. When applicable, these expressions are translated into SPARQL FILTER clauses, allowing filtering to occur directly at the RDF data source.

The following expressions in the WHERE clause are eligible for pushdown:

  • Comparisons involving PostgreSQL data types (e.g., integer, text, boolean) or rdfnode, when used with the supported operators:

    Supported Data Types and Operators

    | Data type | Operators | |————————————————————|—————————————| | rdfnode | =, !=,<>, >, >=, <, <= | | text, char, varchar, name | =, <>, !=, ~~, !~~, ~~*,!~~* | | date, timestamp, timestamp with time zone | =, <>, !=, >, >=, <, <= | | smallint, int, bigint, numeric, double precision | =, <>, !=, >, >=, <, <= | | boolean | IS, IS NOT |

  • IN/NOT IN and ANY constructs with constant lists.

    SQL IN and ANY constructs are translated into the SPARQL IN operator, which will be placed in a FILTER evaluation, as long as the list has the supported data types.

  • ✅ Nearly all supported SPARQL functions are pushdown-capable, including:

    • LANG(), DATATYPE(), STR(), isBLANK(), isIRI(), etc.
    • Numeric, string, and datetime functions such as ROUND(), STRLEN(), YEAR(), and others.

    ⚠️ Exceptions: Due to their volatile nature, the SPARQL functions RAND() and NOW() cannot be pushed down. Because their results cannot be reproduced consistently by PostgreSQL, any rows returned from the endpoint would be filtered out locally during re-evaluation.

Conditions That Prevent Pushdown

A WHERE condition will not be pushed down if:

  • The option enable_pushdown is set to false.
  • The underlying SPARQL query includes:
    • a GROUP BY clause
    • solution modifiers like OFFSET, ORDER BY, LIMIT, DISTINCT, or REDUCED
    • subqueries or federated queries
  • The condition includes an unsupported data type or operator.
  • The condition contains OR logical operators (not yet supported).

Pushdown Examples

For the examples in this section consider this SERVER and FOREIGN TABLE setting (Wikidata):

CREATE SERVER wikidata
FOREIGN DATA WRAPPER rdf_fdw 
OPTIONS (endpoint 'https://query.wikidata.org/sparql');

CREATE FOREIGN TABLE rdbms (
  s rdfnode OPTIONS (variable '?s'),
  p rdfnode OPTIONS (variable '?p'),
  o rdfnode OPTIONS (variable '?o')
)
SERVER wikidata OPTIONS (
  sparql 
    'SELECT * {
      ?s wdt:P31 wd:Q3932296 .
	  ?s ?p ?o
	 }'
);
  1. Pusdown of WHERE conditions involving rdfnode values and = and != operators. All conditions are pushed as FILTER expressions.

SELECT s, o FROM rdbms
WHERE 
  o = '"PostgreSQL"@es' AND
  o <> '"Oracle"@es';

INFO:  SPARQL query sent to 'https://query.wikidata.org/sparql':

SELECT ?s ?o 
{
      ?s wdt:P31 wd:Q3932296 .
          ?s ?p ?o
         
 ## rdf_fdw pushdown conditions ##
 FILTER(?o = "PostgreSQL"@es)
 FILTER(?o != "Oracle"@es)
}

INFO:  SPARQL returned 1 record.

                    s                     |        o        
------------------------------------------+-----------------
 <http://www.wikidata.org/entity/Q192490> | "PostgreSQL"@es
(1 row)
  1. Pusdown of WHERE conditions involving rdfnode values and =, >, and < operators. All conditions are pushed as FILTER expressions. Note that the timpestamp values are automatically cast to the correspondent XSD data type.
SELECT s, o FROM rdbms
WHERE 
  p = '<http://www.wikidata.org/prop/direct/P577>' AND
  o > '1996-01-01'::timestamp AND o < '1996-12-31'::timestamp;

INFO:  SPARQL query sent to 'https://query.wikidata.org/sparql':

SELECT ?s ?p ?o 
{
      ?s wdt:P31 wd:Q3932296 .
          ?s ?p ?o
         
 ## rdf_fdw pushdown conditions ##
 FILTER(?p = <http://www.wikidata.org/prop/direct/P577>)
 FILTER(?o > "1996-01-01T00:00:00"^^<http://www.w3.org/2001/XMLSchema#dateTime>)
 FILTER(?o < "1996-12-31T00:00:00"^^<http://www.w3.org/2001/XMLSchema#dateTime>)
}

INFO:  SPARQL returned 1 record.

                    s                     |                                  o                                  
------------------------------------------+---------------------------------------------------------------------
 <http://www.wikidata.org/entity/Q192490> | "1996-07-08T00:00:00Z"^^<http://www.w3.org/2001/XMLSchema#dateTime>
(1 row)
  1. Pusdown of WHERE conditions with IN and ANY constructs. All conditions are pushed down as FILTER expressions.
SELECT p, o FROM rdbms
WHERE 
  p = '<http://www.w3.org/2000/01/rdf-schema#label>' AND
  o IN ('"PostgreSQL"@en', '"IBM Db2"@fr', '"MySQL"@es');

INFO:  SPARQL query sent to 'https://query.wikidata.org/sparql':

SELECT ?p ?o 
{
      ?s wdt:P31 wd:Q3932296 .
          ?s ?p ?o         
 ## rdf_fdw pushdown conditions ##
 FILTER(?p = <http://www.w3.org/2000/01/rdf-schema#label>)
 FILTER(?o IN ("PostgreSQL"@en, "IBM Db2"@fr, "MySQL"@es))
}

INFO:  SPARQL returned 3 records.

                      p                       |        o        
----------------------------------------------+-----------------
 <http://www.w3.org/2000/01/rdf-schema#label> | "MySQL"@es
 <http://www.w3.org/2000/01/rdf-schema#label> | "PostgreSQL"@en
 <http://www.w3.org/2000/01/rdf-schema#label> | "IBM Db2"@fr
(3 rows)
SELECT p, o FROM rdbms
WHERE 
  p = '<http://www.w3.org/2000/01/rdf-schema#label>' AND
  o = ANY(ARRAY['"PostgreSQL"@en'::rdfnode,'"IBM Db2"@fr'::rdfnode,'"MySQL"@es'::rdfnode]);

INFO:  SPARQL query sent to 'https://query.wikidata.org/sparql':

SELECT ?p ?o 
{
      ?s wdt:P31 wd:Q3932296 .
          ?s ?p ?o
         
 ## rdf_fdw pushdown conditions ##
 FILTER(?p = <http://www.w3.org/2000/01/rdf-schema#label>)
 FILTER(?o IN ("PostgreSQL"@en, "IBM Db2"@fr, "MySQL"@es))
}

INFO:  SPARQL returned 3 records.

                      p                       |        o        
----------------------------------------------+-----------------
 <http://www.w3.org/2000/01/rdf-schema#label> | "MySQL"@es
 <http://www.w3.org/2000/01/rdf-schema#label> | "PostgreSQL"@en
 <http://www.w3.org/2000/01/rdf-schema#label> | "IBM Db2"@fr
(3 rows)
  1. Pusdown of WHERE conditions involving SPARQL functions. The function calls for LANGMATCHES(), LANG(), and STRENDS() are pushed down in FILTER expressions.
SELECT s, o FROM rdbms
WHERE 
  p = sparql.iri('http://www.w3.org/2000/01/rdf-schema#label') AND
  sparql.langmatches(sparql.lang(o), 'de') AND
  sparql.strends(o, sparql.strlang('SQL','de'));

INFO:  SPARQL query sent to 'https://query.wikidata.org/sparql':

SELECT ?s ?p ?o 
{
      ?s wdt:P31 wd:Q3932296 .
          ?s ?p ?o
         
 ## rdf_fdw pushdown conditions ##
 FILTER(?p = <http://www.w3.org/2000/01/rdf-schema#label>)
 FILTER(LANGMATCHES(LANG(?o), "de"))
 FILTER(STRENDS(?o, "SQL"@de))
}

INFO:  SPARQL returned 4 records.

                     s                     |        o        
-------------------------------------------+-----------------
 <http://www.wikidata.org/entity/Q850>     | "MySQL"@de
 <http://www.wikidata.org/entity/Q192490>  | "PostgreSQL"@de
 <http://www.wikidata.org/entity/Q5014224> | "CSQL"@de
 <http://www.wikidata.org/entity/Q6862049> | "Mimer SQL"@de
(4 rows)

Examples

LinkedGeoData

Retrieve all amenities 100 from Leipzig Central Station that are wheelchair accessible and had its entry modified after January 1st, 2015.

CREATE SERVER linkedgeodata 
FOREIGN DATA WRAPPER rdf_fdw 
OPTIONS (endpoint 'http://linkedgeodata.org/sparql');

CREATE FOREIGN TABLE leipzig_hbf (
  hbf_iri    rdfnode OPTIONS (variable '?s'),
  modified   rdfnode OPTIONS (variable '?mod'),
  loc_iri    rdfnode OPTIONS (variable '?x'),
  loc_label  rdfnode OPTIONS (variable '?l'), 
  wheelchair rdfnode OPTIONS (variable '?wc')

) SERVER linkedgeodata OPTIONS (
  log_sparql 'true',
  sparql '
PREFIX lgdo: <http://linkedgeodata.org/ontology/>
PREFIX geom: <http://geovocab.org/geometry#>
PREFIX ogc: <http://www.opengis.net/ont/geosparql#>
PREFIX owl: <http://www.w3.org/2002/07/owl#>

SELECT * {
  ?s owl:sameAs <http://dbpedia.org/resource/Leipzig_Hauptbahnhof> ;
    geom:geometry [ogc:asWKT ?sg] .

  ?x a lgdo:Amenity ;
    rdfs:label ?l ;
	<http://purl.org/dc/terms/modified> ?mod ;
	<http://linkedgeodata.org/ontology/wheelchair> ?wc ;
    geom:geometry [ogc:asWKT ?xg] .
    FILTER(bif:st_intersects (?sg, ?xg, 0.1)) .
}'); 

SELECT loc_iri, sparql.lex(loc_label), CAST(modified AS timestamp)
FROM leipzig_hbf
WHERE 
  sparql.contains(loc_label, 'bahnhof') AND  
  modified > '2015-01-01'::date AND 
  wheelchair = true
FETCH FIRST 10 ROWS ONLY;

INFO:  SPARQL query sent to 'http://linkedgeodata.org/sparql':

PREFIX lgdo: <http://linkedgeodata.org/ontology/>
PREFIX geom: <http://geovocab.org/geometry#>
PREFIX ogc: <http://www.opengis.net/ont/geosparql#>
PREFIX owl: <http://www.w3.org/2002/07/owl#>

SELECT ?mod ?x ?l ?wc 
{
  ?s owl:sameAs <http://dbpedia.org/resource/Leipzig_Hauptbahnhof> ;
    geom:geometry [ogc:asWKT ?sg] .

  ?x a lgdo:Amenity ;
    rdfs:label ?l ;
        <http://purl.org/dc/terms/modified> ?mod ;
        <http://linkedgeodata.org/ontology/wheelchair> ?wc ;
    geom:geometry [ogc:asWKT ?xg] .
    FILTER(bif:st_intersects (?sg, ?xg, 0.1)) .

 ## rdf_fdw pushdown conditions ##
 FILTER(CONTAINS(?l, "bahnhof"))
 FILTER(?mod > "2015-01-01"^^<http://www.w3.org/2001/XMLSchema#date>)
 FILTER(?wc = "true"^^<http://www.w3.org/2001/XMLSchema#boolean>)
}
LIMIT 10

INFO:  SPARQL returned 2 records.

                     loc_iri                      |            lex            |      modified       
--------------------------------------------------+---------------------------+---------------------
 <http://linkedgeodata.org/triplify/way165354553> | Parkplatz am Hauptbahnhof | 2015-03-10 16:46:08
 <http://linkedgeodata.org/triplify/way90961368>  | Hauptbahnhof, Ostseite    | 2015-04-29 14:13:14
(2 rows)

In this query we can observe that:

  • all WHERE conditions were pushed down as FILTER expressions
  • the FETCH FIRST 10 ROWS ONLY was pushed down as LIMIT 10
  • the lexical value of the literal in loc_label was extracted using the function LEX().
  • the xsd:dateTime literal in modified was successfully converted to timestamp using SQL CAST.

DBpedia

CREATE SERVER dbpedia
FOREIGN DATA WRAPPER rdf_fdw 
OPTIONS (endpoint 'https://dbpedia.org/sparql');

CREATE FOREIGN TABLE politicians (
  uri text        OPTIONS (variable '?person',     nodetype 'iri'),
  name text       OPTIONS (variable '?personname', nodetype 'literal', literal_type 'xsd:string'),
  birthdate date  OPTIONS (variable '?birthdate',  nodetype 'literal', literal_type 'xsd:date'),
  party text      OPTIONS (variable '?partyname',  nodetype 'literal', literal_type 'xsd:string'),
  country text    OPTIONS (variable '?country',    nodetype 'literal', language 'en')
)
SERVER dbpedia OPTIONS (
  sparql '
    PREFIX dbp: <http://dbpedia.org/property/>
    PREFIX dbo: <http://dbpedia.org/ontology/>

    SELECT *
    WHERE {
      ?person 
          a dbo:Politician;
          dbo:birthDate ?birthdate;
          dbp:name ?personname;
          dbo:party ?party .       
        ?party 
          dbp:country ?country;
          rdfs:label ?partyname .
        FILTER NOT EXISTS {?person dbo:deathDate ?died}
        FILTER(LANG(?partyname) = "de")
      } 
');

SELECT name, birthdate, party
FROM politicians
WHERE 
  country IN ('Germany','France') AND 
  birthdate > '1995-12-31' AND
  party <> ''
ORDER BY birthdate DESC, party ASC
FETCH FIRST 5 ROWS ONLY;

INFO:  SPARQL query sent to 'https://dbpedia.org/sparql':

PREFIX dbp: <http://dbpedia.org/property/>
PREFIX dbo: <http://dbpedia.org/ontology/> 

SELECT ?personname ?birthdate ?partyname ?country 
{
      ?person 
          a dbo:Politician;
          dbo:birthDate ?birthdate;
          dbp:name ?personname;
          dbo:party ?party .       
        ?party 
          dbp:country ?country;
          rdfs:label ?partyname .
        FILTER NOT EXISTS {?person dbo:deathDate ?died}
        FILTER(LANG(?partyname) = "de")
      
 ## rdf_fdw pushdown conditions ##
 FILTER(?country IN ("Germany"@en, "France"@en))
 FILTER(?birthdate > "1995-12-31"^^<http://www.w3.org/2001/XMLSchema#date>)
 FILTER(?partyname != ""^^<http://www.w3.org/2001/XMLSchema#string>)
}
ORDER BY  DESC (?birthdate)  ASC (?partyname)
LIMIT 5

INFO:  SPARQL returned 5 records.

        name        | birthdate  |                  party                  
--------------------+------------+-----------------------------------------
 Louis Boyard       | 2000-08-26 | La France insoumise
 Klara Schedlich    | 2000-01-04 | Bündnis 90/Die Grünen
 Pierrick Berteloot | 1999-01-11 | Rassemblement National
 Niklas Wagener     | 1998-04-16 | Bündnis 90/Die Grünen
 Jakob Blankenburg  | 1997-08-05 | Sozialdemokratische Partei Deutschlands
(5 rows)

In this example we can observe that:

  • the executed SPARQL query was logged.
  • the SPARQL SELECT was modified to retrieve only the columns used in the SQL SELECT and WHERE clauses.
  • the conditions in the SQL WHERE clause were pushed down as SPARQL FILTER conditions.
  • the SQL ORDER BY clause was pushed down as SPARQL ORDER BY.
  • the FETCH FIRST ... ROWS ONLY was pushed down as SPARQL LIMIT
  • the column country has a language option, and its value is used as a language tag in the SPARQL expression: FILTER(?country IN ("Germany"@en, "France"@en))

Import data into QGIS

The rdf_fdw can also be used as a bridge between GIS (Geographic Information Systems) and RDF Triplestores. This example demonstrates how to retrieve geographic coordinates of all German public universities from DBpedia, create WKT (Well Known Text) literals, and import the data into QGIS to visualize it on a map.

[!NOTE]
This example requires the extension PostGIS.

CREATE SERVER dbpedia
FOREIGN DATA WRAPPER rdf_fdw 
OPTIONS (endpoint 'https://dbpedia.org/sparql');

CREATE FOREIGN TABLE german_public_universities (
  id text                   OPTIONS (variable '?uri', nodetype 'iri'),
  name text                 OPTIONS (variable '?name',nodetype 'literal'),
  lon numeric               OPTIONS (variable '?lon', nodetype 'literal'),
  lat numeric               OPTIONS (variable '?lat', nodetype 'literal'),
  geom geometry(point,4326) OPTIONS (variable '?wkt', nodetype 'literal',
                                    expression 'CONCAT("POINT(",?lon," ",?lat,")") AS ?wkt')
) SERVER dbpedia OPTIONS (
  sparql '
    PREFIX geo: <http://www.w3.org/2003/01/geo/wgs84_pos#>
    PREFIX dbp: <http://dbpedia.org/property/>
    PREFIX dbo: <http://dbpedia.org/ontology/>
    PREFIX dbr:  <http://dbpedia.org/resource/>
    SELECT ?uri ?name ?lon ?lat
    WHERE {
      ?uri dbo:type dbr:Public_university ;
        dbp:name ?name;
        geo:lat ?lat; 
        geo:long ?lon; 
        dbp:country dbr:Germany
      }
  ');

Now that we have our FOREIGN TABLE in place, we just need to create a New PostGIS Connection in QGIS and go to Database > DB Manager …, select the table we just created and query the data using SQL:

SELECT id, name, geom
FROM german_public_universities

unis

Finally give the layer a name, select the geometry column and press Load.

unis

Publish FOREIGN TABLE as WFS layer in GeoServer

Just like with an ordinary TABLE in PostgreSQL, it is possible to create and publish FOREIGN TABLES as WFS layers in GeoServer.

First create the FOREIGN TABLE:

CREATE SERVER wikidata
FOREIGN DATA WRAPPER rdf_fdw 
OPTIONS (endpoint 'https://query.wikidata.org/sparql');

CREATE FOREIGN TABLE museums_brittany (
  id text                   OPTIONS (variable '?villeId', nodetype 'iri'),
  label text                OPTIONS (variable '?museumLabel',nodetype 'literal'),
  ville text                OPTIONS (variable '?villeIdLabel', nodetype 'literal'),
  geom geometry(point,4326) OPTIONS (variable '?coord', nodetype 'literal')
) SERVER wikidata OPTIONS (
  sparql '
    SELECT DISTINCT ?museumLabel ?villeId ?villeIdLabel ?coord
    WHERE
    {
      ?museum wdt:P539 ?museofile. # french museofile Id
      ?museum wdt:P131* wd:Q12130. # in Brittany
      ?museum wdt:P131 ?villeId.   # city of the museum
      ?museum wdt:P625 ?coord .    # wkt literal      
      SERVICE wikibase:label { bd:serviceParam wikibase:language "fr". } # french label
    }
  ');

Then set up the Workspace and Store, go to Layers -> Add a new layer, select the proper workspace and go to Configure new SQL view… to create a layer create a layer with a native SQL statement:

SELECT id, label, ville, geom
FROM museums_brittany

geoserver

Afer that set the geometery column and identifier, and hit Save. Finally, fill in the remaining layer attributes, such as Style, Bounding Boxes and Spatial Reference System, and click Save - see this instructions for more details. After that you’ll be able to reach your layer from a standard OGC WFS client, e.g. using QGIS.

geoserver-wfs

geoserver-wfs-map

Deploy with Docker

To deploy the rdf_fdw with docker just pick one of the supported PostgreSQL versions, install the requirements and compile the source code. For example, a rdf_fdw Dockerfile for PostgreSQL 17 should look like this (minimal example):

FROM postgres:17

RUN apt-get update && \
    apt-get install -y make gcc postgresql-server-dev-17 libxml2-dev libcurl4-gnutls-dev librdf0-dev pkg-config

RUN mkdir /extensions
COPY ./rdf_fdw-2.0.0.tar.gz /extensions/
WORKDIR /extensions

RUN tar xvzf rdf_fdw-2.0.0.tar.gz && \
    cd rdf_fdw-2.0.0 && \
    make -j && \
    make install

To build the image save it in a Dockerfile and run the following command in the root directory - this will create an image called rdf_fdw_image.:

 $ docker build -t rdf_fdw_image .

After successfully building the image you’re ready to run or create the container ..

$ docker run --name my_container -e POSTGRES_HOST_AUTH_METHOD=trust rdf_fdw_image

.. and then finally you’re able to create and use the extension!

$ docker exec -u postgres my_container psql -d mydatabase -c "CREATE EXTENSION rdf_fdw;"

For testers and developers

Think you’re cool enough? Try compiling the latest commits from source!

Dockerfile

FROM postgres:17

RUN apt-get update && \
    apt-get install -y git make gcc postgresql-server-dev-17 libxml2-dev libcurl4-gnutls-dev librdf0-dev pkg-config

WORKDIR /

RUN git clone https://github.com/jimjonesbr/rdf_fdw.git && \
    cd rdf_fdw && \
    make -j && \
    make install

Deployment

 $ docker build -t rdf_fdw_image .
 $ docker run --name my_container -e POSTGRES_HOST_AUTH_METHOD=trust rdf_fdw_image
 $ docker exec -u postgres my_container psql -d mydatabase -c "CREATE EXTENSION rdf_fdw;"

If you’ve found a bug or have general comments, do not hesitate to open an issue. Any feedback is much appreciated!