Extensions
- rdf_fdw 2.0.0
- RDF Triplestore Foreign Data Wrapper for PostgreSQL
Documentation
- CHANGELOG
- Release Notes
README
Contents
- RDF Triplestore Foreign Data Wrapper for PostgreSQL (rdf_fdw)
- Index
- Requirements
- Build and Install
- Update
- Usage
- RDF Node Handling
- SPARQL Functions
- isIRI
- isBLANK
- isLITERAL
- isNUMERIC
- STR
- LANG
- DATATYPE
- IRI
- BNODE
- STRDT
- STLANG
- UUID
- STRUUID
- STRLEN
- SUBSTR
- UCASE
- LCASE
- STRSTARTS
- STRENDS
- CONTAINS
- STRBEFORE
- STRAFTER
- ENCODE_FOR_URI
- CONCAT
- LANGMATCHES
- REGEX
- REPLACE
- ABS
- ROUND
- CEIL
- FLOOR
- RAND
- YEAR
- MONTH
- DAY
- HOURS
- MINUTES
- SECONDS
- TIMEZONE
- TZ
- MD5
- LEX
- SPARQL describe
- Pushdown
- Examples
- Deploy with Docker
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.
Index
- Requirements
- Build and Install
- Update
- Usage
- RDF Node Handling
- SPARQL Functions
- SPARQL Describe
- Pushdown
- Examples
- Deploy with Docker
Requirements
- libxml2: version 2.5.0 or higher.
- libcurl: version 7.74.0 or higher.
- librdf: version 1.0.17 or higher.
- pkg-config: pkg-config 0.29.2 or higher.
- PostgreSQL: version 9.6 or higher.
- gcc and make to compile the code.
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 usingrdf_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 thepsql
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 thepsql
meta-command\deu[+]
or runSELECT * 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 thepsql
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 inrdf_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]
Usesameterm
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) returnxsd: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 isNULL
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
, andg
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 asLIMIT 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 SPARQLORDER 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
) orrdfnode
, 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
andANY
constructs with constant lists.SQL
IN
andANY
constructs are translated into the SPARQLIN
operator, which will be placed in aFILTER
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 tofalse
. - The underlying SPARQL query includes:
- a
GROUP BY
clause - solution modifiers like
OFFSET
,ORDER BY
,LIMIT
,DISTINCT
, orREDUCED
- subqueries or federated queries
- a
- 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
}'
);
- Pusdown of
WHERE
conditions involvingrdfnode
values and=
and!=
operators. All conditions are pushed asFILTER
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)
- Pusdown of
WHERE
conditions involvingrdfnode
values and=
,>
, and<
operators. All conditions are pushed asFILTER
expressions. Note that thetimpestamp
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)
- Pusdown of
WHERE
conditions withIN
andANY
constructs. All conditions are pushed down asFILTER
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)
- Pusdown of
WHERE
conditions involving SPARQL functions. The function calls forLANGMATCHES()
,LANG()
, andSTRENDS()
are pushed down inFILTER
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 asFILTER
expressions - the
FETCH FIRST 10 ROWS ONLY
was pushed down asLIMIT 10
- the lexical value of the literal in
loc_label
was extracted using the function LEX(). - the
xsd:dateTime
literal inmodified
was successfully converted totimestamp
using SQLCAST
.
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 SQLSELECT
andWHERE
clauses. - the conditions in the SQL
WHERE
clause were pushed down as SPARQLFILTER
conditions. - the SQL
ORDER BY
clause was pushed down as SPARQLORDER BY
. - the
FETCH FIRST ... ROWS ONLY
was pushed down as SPARQLLIMIT
- the column
country
has alanguage
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
Finally give the layer a name, select the geometry column and press Load.
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
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.
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!