├── 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 |
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 | [](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 |
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
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 |
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 +
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 |
--------------------------------------------------------------------------------