├── test ├── sql │ ├── init.sql │ ├── striptags.sql │ └── tohtml.sql └── expected │ ├── init.out │ ├── striptags.out │ ├── tohtml_4.out │ └── tohtml.out ├── .gitignore ├── COPYING ├── plxslt.control ├── plxslt.sql ├── Makefile ├── .cirrus.yml ├── README.md └── plxslt.c /test/sql/init.sql: -------------------------------------------------------------------------------- 1 | \i plxslt.sql 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | *.so 3 | plxslt--?.sql 4 | /results/ 5 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petere/plxslt/HEAD/COPYING -------------------------------------------------------------------------------- /plxslt.control: -------------------------------------------------------------------------------- 1 | comment = 'PL/XSLT procedural language' 2 | default_version = 1 3 | relocatable = true 4 | -------------------------------------------------------------------------------- /plxslt.sql: -------------------------------------------------------------------------------- 1 | CREATE FUNCTION plxslt_handler() RETURNS language_handler 2 | AS '$libdir/plxslt' 3 | LANGUAGE C; 4 | 5 | CREATE FUNCTION plxslt_validator(oid) RETURNS void 6 | AS '$libdir/plxslt' 7 | LANGUAGE C; 8 | 9 | CREATE LANGUAGE xslt 10 | HANDLER plxslt_handler 11 | VALIDATOR plxslt_validator; 12 | -------------------------------------------------------------------------------- /test/expected/init.out: -------------------------------------------------------------------------------- 1 | \i plxslt.sql 2 | CREATE FUNCTION plxslt_handler() RETURNS language_handler 3 | AS '$libdir/plxslt' 4 | LANGUAGE C; 5 | CREATE FUNCTION plxslt_validator(oid) RETURNS void 6 | AS '$libdir/plxslt' 7 | LANGUAGE C; 8 | CREATE LANGUAGE xslt 9 | HANDLER plxslt_handler 10 | VALIDATOR plxslt_validator; 11 | -------------------------------------------------------------------------------- /test/sql/striptags.sql: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE FUNCTION striptags(xml) RETURNS text 2 | LANGUAGE xslt 3 | AS $$ 4 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | $$; 17 | 18 | 19 | SELECT striptags('hello'::xml); 20 | SELECT striptags(''::xml); 21 | -------------------------------------------------------------------------------- /test/expected/striptags.out: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE FUNCTION striptags(xml) RETURNS text 2 | LANGUAGE xslt 3 | AS $$ 4 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | $$; 17 | SELECT striptags('hello'::xml); 18 | striptags 19 | ----------- 20 | hello 21 | (1 row) 22 | 23 | SELECT striptags(''::xml); 24 | striptags 25 | ----------- 26 | 27 | (1 row) 28 | 29 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PG_CONFIG = pg_config 2 | PKG_CONFIG = pkg-config 3 | 4 | ifneq ($(shell $(PKG_CONFIG) --exists --print-errors libxml-2.0 libxslt && echo yes),yes) 5 | $(error libxml2 or libxslt is not installed properly) 6 | endif 7 | 8 | pg_version := $(word 2,$(shell $(PG_CONFIG) --version)) 9 | extensions_supported = $(filter-out 6.% 7.% 8.% 9.0%,$(pg_version)) 10 | 11 | 12 | MODULE_big = plxslt 13 | OBJS = plxslt.o 14 | 15 | EXTENSION = plxslt 16 | 17 | DATA = $(if $(extensions_supported),,plxslt.sql) 18 | DATA_built = $(if $(extensions_supported),plxslt--1.sql) 19 | 20 | EXTRA_CLEAN = plxslt--1.sql 21 | 22 | 23 | PG_CPPFLAGS += $(shell $(PKG_CONFIG) --cflags-only-I libxml-2.0 libxslt) 24 | SHLIB_LINK += $(shell $(PKG_CONFIG) --libs libxml-2.0 libxslt) 25 | 26 | REGRESS = init tohtml striptags 27 | REGRESS_OPTS = --inputdir=test 28 | 29 | PGXS := $(shell $(PG_CONFIG) --pgxs) 30 | include $(PGXS) 31 | 32 | 33 | plxslt--1.sql: plxslt.sql 34 | sed 's/^CREATE /CREATE OR REPLACE /' $< >$@ 35 | -------------------------------------------------------------------------------- /.cirrus.yml: -------------------------------------------------------------------------------- 1 | env: 2 | DEBIAN_FRONTEND: noninteractive 3 | LANG: C 4 | 5 | task: 6 | name: Linux (Debian/Ubuntu) 7 | matrix: 8 | - container: 9 | image: ubuntu:20.04 10 | env: 11 | matrix: 12 | - PGVERSION: 17 13 | - PGVERSION: 16 14 | - PGVERSION: 15 15 | - PGVERSION: 14 16 | - PGVERSION: 13 17 | - PGVERSION: 12 18 | - PGVERSION: 11 19 | - PGVERSION: 10 20 | - PGVERSION: 9.6 21 | - PGVERSION: 9.5 22 | - PGVERSION: 9.4 23 | - PGVERSION: 9.3 24 | setup_script: 25 | - apt-get update 26 | - apt-get -y install curl gnupg lsb-release 27 | - curl https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - 28 | - echo "deb http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main" | tee /etc/apt/sources.list.d/pgdg.list 29 | - apt-get update 30 | - apt-get -y install gcc make libxml2-dev libxslt-dev pkg-config postgresql-$PGVERSION postgresql-server-dev-$PGVERSION 31 | - pg_createcluster --start $PGVERSION test -p 55435 -- -A trust 32 | build_script: 33 | - PATH=/usr/lib/postgresql/$PGVERSION/bin:$PATH 34 | - make all 35 | - make install 36 | test_script: 37 | - PATH=/usr/lib/postgresql/$PGVERSION/bin:$PATH 38 | - PGPORT=55435 make installcheck PGUSER=postgres 39 | -------------------------------------------------------------------------------- /test/sql/tohtml.sql: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE FUNCTION tohtml(xml) RETURNS text 2 | LANGUAGE xslt 3 | AS $$ 4 | 9 | 10 | 14 | 15 | 16 | 17 | 19 | 21 | 22 | 23 | 24 | <xsl:value-of select="name(current())"/> 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 |
42 | 43 | 44 |
45 | 46 |
47 | $$; 48 | 49 | 50 | SELECT tohtml(query_to_xml($$VALUES (1, 2, 3), (4, 5, 6)$$, false, false, '')); 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | PL/XSLT Procedural Language Handler for PostgreSQL 2 | ================================================== 3 | 4 | PL/XSLT is a procedural language handler for PostgreSQL that allows you to write stored procedures in XSLT. A function definition looks like: 5 | 6 | CREATE FUNCTION foo(xml) RETURNS xml AS $$ 7 | 8 | 9 | ... 10 | 11 | $$ LANGUAGE xslt; 12 | 13 | Conditions on the function definition: 14 | 15 | - The first parameter must be of type `xml`. It will receive the input document. Every function must have at least this one parameter. 16 | 17 | - The following function parameters will be supplied to the stylesheet as XSL parameters. 18 | 19 | - The return type must match the output method specified by the stylesheet. If it is `xml`, then the return type must be `xml`; if it is `text` or `html`, the return type must be `text` or `varchar`. 20 | 21 | - Triggers are not supported. 22 | 23 | Refer to the `INSTALL` file for installation instructions and `COPYING` for the license. The distribution also contains a test suite which contains a simplistic demonstration of the functionality. 24 | 25 | I'm interested if anyone is using this. 26 | 27 | Peter Eisentraut 28 | 29 | [![Build Status](https://secure.travis-ci.org/petere/plxslt.png)](http://travis-ci.org/petere/plxslt) 30 | 31 | Installation 32 | ------------ 33 | 34 | Build: 35 | 36 | make 37 | make install 38 | 39 | Set the environment variable `PG_CONFIG` to the location of the `pg_config` program belonging to the desired PostgreSQL installation, if it is not found automatically. 40 | 41 | Load into database: 42 | 43 | CREATE EXTENSION plxslt; 44 | 45 | For versions before PostgreSQL 9.1: 46 | 47 | psql -f plxslt.sql 48 | -------------------------------------------------------------------------------- /test/expected/tohtml_4.out: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE FUNCTION tohtml(xml) RETURNS text 2 | LANGUAGE xslt 3 | AS $$ 4 | 9 | 10 | 14 | 15 | 16 | 17 | 19 | 21 | 22 | 23 | 24 | <xsl:value-of select="name(current())"/> 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 |
42 | 43 | 44 |
45 | 46 |
47 | $$; 48 | SELECT tohtml(query_to_xml($$VALUES (1, 2, 3), (4, 5, 6)$$, false, false, '')); 49 | tohtml 50 | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- 51 | 52 | 53 | table
123
456
54 | 55 | (1 row) 56 | 57 | -------------------------------------------------------------------------------- /test/expected/tohtml.out: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE FUNCTION tohtml(xml) RETURNS text 2 | LANGUAGE xslt 3 | AS $$ 4 | 9 | 10 | 14 | 15 | 16 | 17 | 19 | 21 | 22 | 23 | 24 | <xsl:value-of select="name(current())"/> 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 |
42 | 43 | 44 |
45 | 46 |
47 | $$; 48 | SELECT tohtml(query_to_xml($$VALUES (1, 2, 3), (4, 5, 6)$$, false, false, '')); 49 | tohtml 50 | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- 51 | + 52 | + 53 | table
123
456
+ 54 | 55 | (1 row) 56 | 57 | -------------------------------------------------------------------------------- /plxslt.c: -------------------------------------------------------------------------------- 1 | /* 2 | * PL/XSLT language handler 3 | * 4 | * Copyright © 2007-2013 by Peter Eisentraut 5 | * See the COPYING file for details. 6 | * 7 | */ 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #if defined(PG_VERSION_NUM) && PG_VERSION_NUM >= 90300 15 | #include 16 | #endif 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | 25 | #include 26 | #include 27 | 28 | #include 29 | #include 30 | #include 31 | 32 | 33 | PG_MODULE_MAGIC; 34 | 35 | 36 | #define _textout(x) (DatumGetCString(DirectFunctionCall1(textout, PointerGetDatum(&x)))) 37 | 38 | 39 | 40 | /* 41 | * Convert the C string "input" to a Datum of type "typeoid". 42 | */ 43 | static Datum 44 | cstring_to_type(char * input, Oid typeoid) 45 | { 46 | HeapTuple typetuple; 47 | Form_pg_type pg_type_entry; 48 | Datum ret; 49 | 50 | typetuple = SearchSysCache(TYPEOID, ObjectIdGetDatum(typeoid), 0, 0, 0); 51 | if (!HeapTupleIsValid(typetuple)) 52 | elog(ERROR, "cache lookup failed for type %u", typeoid); 53 | 54 | pg_type_entry = (Form_pg_type) GETSTRUCT(typetuple); 55 | 56 | ret = OidFunctionCall3(pg_type_entry->typinput, 57 | CStringGetDatum(input), 58 | 0, -1); 59 | 60 | ReleaseSysCache(typetuple); 61 | 62 | PG_RETURN_DATUM(ret); 63 | } 64 | 65 | 66 | 67 | /* 68 | * Convert the Datum "input" that is of type "typeoid" to a C string. 69 | */ 70 | static char * 71 | type_to_cstring(Datum input, Oid typeoid) 72 | { 73 | HeapTuple typetuple; 74 | Form_pg_type pg_type_entry; 75 | Datum ret; 76 | 77 | typetuple = SearchSysCache(TYPEOID, ObjectIdGetDatum(typeoid), 0, 0, 0); 78 | if (!HeapTupleIsValid(typetuple)) 79 | elog(ERROR, "cache lookup failed for type %u", typeoid); 80 | 81 | pg_type_entry = (Form_pg_type) GETSTRUCT(typetuple); 82 | 83 | ret = OidFunctionCall3(pg_type_entry->typoutput, 84 | input, 85 | 0, -1); 86 | 87 | ReleaseSysCache(typetuple); 88 | 89 | return DatumGetCString(ret); 90 | } 91 | 92 | 93 | 94 | /* 95 | * Internal handler function 96 | */ 97 | static Datum 98 | handler_internal(Oid function_oid, FunctionCallInfo fcinfo, bool execute) 99 | { 100 | HeapTuple proctuple; 101 | Form_pg_proc pg_proc_entry; 102 | char *sourcecode; 103 | Datum prosrcdatum; 104 | bool isnull; 105 | const char **xslt_params; 106 | int i; 107 | Oid *argtypes; 108 | char **argnames; 109 | char *argmodes; 110 | int numargs; 111 | xmlDocPtr ssdoc; 112 | xmlDocPtr xmldoc; 113 | xmlDocPtr resdoc; 114 | xsltStylesheetPtr stylesheet; 115 | int reslen; 116 | xmlChar *resstr; 117 | Datum resdatum; 118 | 119 | if (CALLED_AS_TRIGGER(fcinfo)) 120 | ereport(ERROR, 121 | (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), 122 | errmsg("trigger functions not supported"))); 123 | 124 | proctuple = SearchSysCache(PROCOID, ObjectIdGetDatum(function_oid), 0, 0, 0); 125 | if (!HeapTupleIsValid(proctuple)) 126 | elog(ERROR, "cache lookup failed for function %u", function_oid); 127 | 128 | prosrcdatum = SysCacheGetAttr(PROCOID, proctuple, Anum_pg_proc_prosrc, &isnull); 129 | if (isnull) 130 | elog(ERROR, "null prosrc"); 131 | 132 | sourcecode = pstrdup(DatumGetCString(DirectFunctionCall1(textout, 133 | prosrcdatum))); 134 | /* allow one blank line at the start */ 135 | if (sourcecode[0] == '\n') 136 | sourcecode++; 137 | 138 | numargs = get_func_arg_info(proctuple, 139 | &argtypes, &argnames, &argmodes); 140 | 141 | if (numargs < 1) 142 | ereport(ERROR, 143 | (errmsg("XSLT function must have at least one argument"))); 144 | 145 | if (argtypes[0] != XMLOID) 146 | ereport(ERROR, 147 | (errmsg("first argument of XSLT function must have type XML"))); 148 | 149 | #if 0 150 | xsltSetGenericErrorFunc(NULL, xmlGenericError); 151 | #endif 152 | 153 | ssdoc = xmlParseDoc((xmlChar *) sourcecode); /* XXX use backend's xml_parse here() */ 154 | stylesheet = xsltParseStylesheetDoc(ssdoc); /* XXX check error handling */ 155 | if (!stylesheet) 156 | ereport(ERROR, 157 | (errmsg("could not parse stylesheet"))); 158 | 159 | pg_proc_entry = (Form_pg_proc) GETSTRUCT(proctuple); 160 | 161 | { 162 | char *method; 163 | 164 | method = (char *) stylesheet->method; 165 | /* 166 | * TODO: This is strictly speaking not correct because the 167 | * default output method may be "html", but that can only 168 | * detected at run time, so punt for now. 169 | */ 170 | if (!method) 171 | method = "xml"; 172 | 173 | if (strcmp(method, "xml") == 0 && pg_proc_entry->prorettype != XMLOID) 174 | ereport(ERROR, 175 | (errcode(ERRCODE_DATATYPE_MISMATCH), 176 | errmsg("XSLT stylesheet has output method \"xml\" but return type of function is not xml"))); 177 | else if ((strcmp(method, "html") == 0 || strcmp(method, "text") == 0) 178 | && pg_proc_entry->prorettype != TEXTOID && pg_proc_entry->prorettype != VARCHAROID) 179 | ereport(ERROR, 180 | (errcode(ERRCODE_DATATYPE_MISMATCH), 181 | errmsg("XSLT stylesheet has output method \"%s\" but return type of function is not text or varchar", method))); 182 | } 183 | 184 | /* validation stops here */ 185 | if (!execute) 186 | { 187 | ReleaseSysCache(proctuple); 188 | PG_RETURN_VOID(); 189 | } 190 | 191 | /* execution begins here */ 192 | 193 | xslt_params = palloc(((numargs - 1) * 2 + 1) * sizeof(*xslt_params)); 194 | for (i = 0; i < numargs-1; i++) 195 | { 196 | xslt_params[i*2] = argnames[i+1]; 197 | xslt_params[i*2+1] = type_to_cstring(PG_GETARG_DATUM(i+1), 198 | argtypes[i+1]); 199 | } 200 | xslt_params[i*2] = NULL; 201 | 202 | { 203 | xmltype *arg0 = PG_GETARG_XML_P(0); 204 | // XXX this ought to use xml_parse() 205 | xmldoc = xmlParseMemory((char *) VARDATA(arg0), VARSIZE(arg0) - VARHDRSZ); 206 | } 207 | 208 | resdoc = xsltApplyStylesheet(stylesheet, xmldoc, xslt_params); 209 | if (!resdoc) 210 | elog(ERROR, "xsltApplyStylesheet() failed"); 211 | 212 | xmlFreeDoc(xmldoc); 213 | 214 | if (xsltSaveResultToString(&resstr, &reslen, resdoc, stylesheet) != 0) 215 | elog(ERROR, "result serialization failed"); 216 | 217 | xsltFreeStylesheet(stylesheet); 218 | xmlFreeDoc(resdoc); 219 | 220 | xsltCleanupGlobals(); 221 | xmlCleanupParser(); 222 | 223 | resdatum = cstring_to_type(resstr ? (char *) resstr : "", pg_proc_entry->prorettype); 224 | ReleaseSysCache(proctuple); 225 | PG_RETURN_DATUM(resdatum); 226 | } 227 | 228 | 229 | 230 | /* 231 | * The PL handler 232 | */ 233 | PG_FUNCTION_INFO_V1(plxslt_handler); 234 | Datum plxslt_handler(PG_FUNCTION_ARGS); 235 | 236 | Datum 237 | plxslt_handler(PG_FUNCTION_ARGS) 238 | { 239 | return handler_internal(fcinfo->flinfo->fn_oid, fcinfo, true); 240 | } 241 | 242 | 243 | 244 | /* 245 | * Validator function 246 | */ 247 | PG_FUNCTION_INFO_V1(plxslt_validator); 248 | Datum plxslt_validator(PG_FUNCTION_ARGS); 249 | 250 | Datum 251 | plxslt_validator(PG_FUNCTION_ARGS) 252 | { 253 | if (!CheckFunctionValidatorAccess(fcinfo->flinfo->fn_oid, PG_GETARG_OID(0))) 254 | PG_RETURN_VOID(); 255 | return handler_internal(PG_GETARG_OID(0), fcinfo, false); 256 | } 257 | --------------------------------------------------------------------------------