diff --git a/HISTORY.asc b/HISTORY.asc index c4c896c..9b32c82 100644 --- a/HISTORY.asc +++ b/HISTORY.asc @@ -1,3 +1,10 @@ +STABLE +------ +=== Adopt the `stable` pseudo-version between releases + +=== Add `0.2.0--0.2.1` update script +`ALTER EXTENSION trunklet UPDATE` now works from 0.2.0. Previously there was no update path from 0.2.0. + 0.3.3 ----- === Add `ignore_missing_functions` agument to `template_language__remove()` diff --git a/sql/trunklet--0.2.0--0.2.1.sql b/sql/trunklet--0.2.0--0.2.1.sql new file mode 100644 index 0000000..9de514e --- /dev/null +++ b/sql/trunklet--0.2.0--0.2.1.sql @@ -0,0 +1,278 @@ +/* + * Using temp objects will result in the extension being dropped after session + * end. Create a real schema and then explicitly drop it instead. + */ +CREATE SCHEMA __trunklet; + +CREATE TABLE __trunklet.old_settings AS + SELECT name, setting + FROM pg_catalog.pg_settings + WHERE name IN ('client_min_messages', 'search_path') +; +SET client_min_messages = warning; +SET search_path = pg_catalog; + +CREATE OR REPLACE FUNCTION _trunklet.template__get( + template_id _trunklet.template.template_id%TYPE + , loose boolean DEFAULT false +) RETURNS _trunklet.template LANGUAGE plpgsql AS $body$ +DECLARE + r _trunklet.template; +BEGIN + SELECT * INTO STRICT r + FROM _trunklet.template t + WHERE t.template_id = template__get.template_id + ; + + RETURN r; +EXCEPTION + WHEN no_data_found THEN + IF loose THEN + RETURN NULL; + ELSE + RAISE EXCEPTION 'template not found' + USING ERRCODE = 'no_data_found' + , DETAIL = format( 'template id %s', template_id ) + ; + END IF; +END +$body$; +REVOKE ALL ON FUNCTION _trunklet.template__get( + template_id _trunklet.template.template_id%TYPE + , loose boolean +) FROM public; + +CREATE OR REPLACE FUNCTION trunklet.process_language( + language_name _trunklet.language.language_name%TYPE + , template text + , parameters anyelement +) RETURNS text LANGUAGE plpgsql + +-- !!!!!!! +SECURITY DEFINER SET search_path = pg_catalog +-- !!!!!!! + +AS $body$ +DECLARE +/* + * !!!!! SECURITY DEFINER !!!!! + */ + + r_language _trunklet.language; + sql text; + v_return text; +BEGIN + -- Can't do this during DECLARE (0A000: default value for row or record variable is not supported) + r_language := _trunklet.language__get( language_name ); + +/* + * !!!!! SECURITY DEFINER !!!!! + */ + PERFORM _trunklet.verify_type( language_name, r_language.template_type, pg_catalog.pg_typeof(template), 'template' ); + PERFORM _trunklet.verify_type( language_name, r_language.parameter_type, pg_catalog.pg_typeof(parameters), 'parameter' ); + + sql := format( + 'SELECT _trunklet_functions.%s( CAST($1 AS %s), CAST($2 AS %s) )' + , _trunklet.function_name( r_language.language_id, 'process' ) + , r_language.template_type + , r_language.parameter_type + ); + RAISE DEBUG E'execute %\nUSING %, %', sql, template, parameters; + EXECUTE sql INTO STRICT v_return USING template, parameters; + RAISE DEBUG '% returned %', sql, v_return; + + RETURN v_return; +END +$body$; + +CREATE OR REPLACE FUNCTION trunklet.process( + template_id _trunklet.template.template_id%TYPE + , parameters anyelement +) RETURNS text LANGUAGE SQL +-- !!! +SECURITY DEFINER SET search_path = pg_catalog +-- !!! +AS $body$ +SELECT trunklet.process_language( + -- language_id comes from template__get() below + (_trunklet.language__get(language_id)).language_name + , template + , parameters + ) + FROM _trunklet.template__get( template_id ) +$body$; +CREATE OR REPLACE FUNCTION trunklet.process( + template_name _trunklet.template.template_name%TYPE + , template_version _trunklet.template.template_version%TYPE + , parameters anyelement +) RETURNS text LANGUAGE SQL +-- !!! +SECURITY DEFINER SET search_path = pg_catalog +-- !!! +AS $body$ +SELECT trunklet.process_language( + -- language_id comes from template__get() below + (_trunklet.language__get(language_id)).language_name + , template + , parameters + ) + FROM _trunklet.template__get( template_name, template_version ) +$body$; +CREATE OR REPLACE FUNCTION trunklet.process( + template_name _trunklet.template.template_name%TYPE + , parameters anyelement +) RETURNS text LANGUAGE SQL AS $body$ +SELECT trunklet.process( template_name, 1, parameters ) +$body$; + + +/* + * trunklet.execute_into() + */ +CREATE OR REPLACE FUNCTION trunklet.execute_into__language( + language_name _trunklet.language.language_name%TYPE + , template text + , parameters anyelement +) RETURNS anyelement LANGUAGE plpgsql AS $body$ +DECLARE + sql CONSTANT text := trunklet.process_language(language_name, template, parameters); + + r record; +BEGIN + -- Define the structure of r + EXECUTE format('SELECT NULL::%s AS out', pg_typeof(parameters)) INTO STRICT r; + + RAISE DEBUG E'execute %', sql; + EXECUTE sql INTO STRICT r.out; + RAISE DEBUG '% returned %', sql, r.out; + + RETURN r.out; +END +$body$; +CREATE OR REPLACE FUNCTION trunklet.execute_into( + template_id _trunklet.template.template_id%TYPE + , parameters anyelement +) RETURNS anyelement LANGUAGE plpgsql AS $body$ +DECLARE + sql CONSTANT text := trunklet.process(template_id, parameters); + + r record; +BEGIN + -- Define the structure of r + EXECUTE format('SELECT NULL::%s AS out', pg_typeof(parameters)) INTO STRICT r; + + RAISE DEBUG E'execute %', sql; + EXECUTE sql INTO STRICT r.out; + RAISE DEBUG '% returned %', sql, r.out; + + RETURN r.out; +END +$body$; +CREATE OR REPLACE FUNCTION trunklet.execute_into( + template_name _trunklet.template.template_name%TYPE + , template_version _trunklet.template.template_version%TYPE + , parameters anyelement +) RETURNS anyelement LANGUAGE plpgsql AS $body$ +DECLARE + sql CONSTANT text := trunklet.process(template_name, template_version, parameters); + + r record; +BEGIN + -- Define the structure of r + EXECUTE format('SELECT NULL::%s AS out', pg_typeof(parameters)) INTO STRICT r; + + RAISE DEBUG E'execute %', sql; + EXECUTE sql INTO STRICT r.out; + RAISE DEBUG '% returned %', sql, r.out; + + RETURN r.out; +END +$body$; +CREATE OR REPLACE FUNCTION trunklet.execute_into( + template_name _trunklet.template.template_name%TYPE + , parameters anyelement +) RETURNS anyelement LANGUAGE SQL AS $body$ +SELECT trunklet.execute_into( template_name, 1, parameters ) +$body$; + + + + + +/* + * trunklet.execute_text() + */ +CREATE OR REPLACE FUNCTION trunklet.execute_text__language( + language_name _trunklet.language.language_name%TYPE + , template text + , parameters anyelement +) RETURNS text LANGUAGE plpgsql AS $body$ +DECLARE + sql CONSTANT text := trunklet.process_language(language_name, template, parameters); + + out text; +BEGIN + RAISE DEBUG E'execute %', sql; + EXECUTE sql INTO STRICT out; + RAISE DEBUG '% returned %', sql, out; + + RETURN out; +END +$body$; +CREATE OR REPLACE FUNCTION trunklet.execute_text( + template_id _trunklet.template.template_id%TYPE + , parameters anyelement +) RETURNS anyelement LANGUAGE plpgsql AS $body$ +DECLARE + sql CONSTANT text := trunklet.process(template_id, parameters); + + out text; +BEGIN + RAISE DEBUG E'execute %', sql; + EXECUTE sql INTO STRICT out; + RAISE DEBUG '% returned %', sql, out; + + RETURN out; +END +$body$; +CREATE OR REPLACE FUNCTION trunklet.execute_text( + template_name _trunklet.template.template_name%TYPE + , template_version _trunklet.template.template_version%TYPE + , parameters anyelement +) RETURNS anyelement LANGUAGE plpgsql AS $body$ +DECLARE + sql CONSTANT text := trunklet.process(template_name, template_version, parameters); + + out text; +BEGIN + RAISE DEBUG E'execute %', sql; + EXECUTE sql INTO STRICT out; + RAISE DEBUG '% returned %', sql, out; + + RETURN out; +END +$body$; +CREATE OR REPLACE FUNCTION trunklet.execute_text( + template_name _trunklet.template.template_name%TYPE + , parameters anyelement +) RETURNS anyelement LANGUAGE SQL AS $body$ +SELECT trunklet.execute_text( template_name, 1, parameters ) +$body$; + + + + + +SELECT _trunklet.exec( + format( + 'SET %I = %L' + , name + , setting + ) + ) + FROM __trunklet.old_settings +; +DROP TABLE __trunklet.old_settings; +DROP SCHEMA __trunklet; + +-- vi: expandtab sw=2 ts=2 diff --git a/sql/trunklet--0.2.0.sql b/sql/trunklet--0.2.0.sql new file mode 100644 index 0000000..3f23ef4 --- /dev/null +++ b/sql/trunklet--0.2.0.sql @@ -0,0 +1,673 @@ +/* + * Author: Jim C. Nasby + * Created at: 2015-01-11 17:47:56 -0600 + * + */ + +SET client_min_messages = warning; + +-- Set a safe search_path +SET search_path = pg_catalog; + +DO $do$ +BEGIN + CREATE ROLE trunklet__dependency; +EXCEPTION + WHEN duplicate_object THEN + -- TODO: Ensure options are what we expect + NULL; +END +$do$; + +CREATE SCHEMA _trunklet; +GRANT USAGE ON SCHEMA _trunklet TO trunklet__dependency; +COMMENT ON SCHEMA _trunklet IS $$Internal use functions for the trunklet extension.$$; + +CREATE SCHEMA _trunklet_functions; +-- TODO: Create a trunklet__usage role and use it here instead of public +GRANT USAGE ON SCHEMA _trunklet_functions TO public; -- Languages currently created as role that calls trunklet.template_language__add() +COMMENT ON SCHEMA _trunklet_functions IS $$Schema that contains support functions for languages registered in trunklet. Not intended for general use.$$; + +CREATE SCHEMA trunklet; +GRANT USAGE ON SCHEMA trunklet TO public; + +-- See also pgtemp.exec_as in test/core/functions.sql +CREATE OR REPLACE FUNCTION _trunklet.exec( + sql text +) RETURNS void LANGUAGE plpgsql AS $f$ +BEGIN + RAISE DEBUG 'Executing SQL %s', sql; + EXECUTE sql; +END +$f$; + +CREATE OR REPLACE FUNCTION _trunklet.name_sanity( + field_name text + , value text +) RETURNS boolean LANGUAGE plpgsql AS $body$ +DECLARE + error CONSTANT text := CASE + WHEN value IS NULL THEN 'be NULL' + WHEN value = '' THEN 'be blank' + -- (?n) means use newline sensitive mode + WHEN value ~ '(?n)^\s' THEN 'begin with whitespace' + WHEN value ~ '(?n)\s$' THEN 'end with whitespace' + END; +BEGIN + IF error IS NULL THEN + RETURN true; + END IF; + + RAISE EXCEPTION '% must not %', field_name, error + USING ERRCODE = 'invalid_parameter_value' + ; +END +$body$; + +CREATE TABLE _trunklet.language( + language_id serial PRIMARY KEY NOT NULL + , language_name varchar(100) UNIQUE NOT NULL CHECK(_trunklet.name_sanity( 'language_name', language_name )) + , parameter_type regtype NOT NULL + , template_type regtype NOT NULL + -- I don't think we'll need these fields, but better safe than sorry + , process_function_options text NOT NULL + , process_function_body text NOT NULL + , extract_parameters_options text NOT NULL + , extract_parameters_body text NOT NULL +); +COMMENT ON COLUMN _trunklet.language.parameter_type IS $$Data type used to pass parameters to templates in this language.$$; +COMMENT ON COLUMN _trunklet.language.parameter_type IS $$Data type used by templates in this language.$$; +-- TODO: Add AFTER UPDATE trigger to re-validate the type of all stored templates for a language if template_type changes +CREATE OR REPLACE FUNCTION _trunklet.language__get( + language_id _trunklet.language.language_id%TYPE +) RETURNS _trunklet.language STABLE LANGUAGE sql AS $body$ + SELECT * FROM _trunklet.language l WHERE l.language_id = language__get.language_id +$body$; +CREATE OR REPLACE FUNCTION _trunklet.language__get( + language_name _trunklet.language.language_name%TYPE +) RETURNS _trunklet.language STABLE LANGUAGE plpgsql AS $body$ +DECLARE + v_return _trunklet.language; +BEGIN + SELECT * INTO STRICT v_return + FROM _trunklet.language l + WHERE l.language_name = language__get.language_name + ; + RETURN v_return; +EXCEPTION + WHEN no_data_found THEN + RAISE EXCEPTION 'language "%" not found', language_name + USING ERRCODE = 'no_data_found' + ; +END +$body$; +CREATE OR REPLACE FUNCTION _trunklet.language__get_id( + language_name _trunklet.language.language_name%TYPE +) RETURNS _trunklet.language.language_id%TYPE STABLE LANGUAGE sql AS $body$ +SELECT (_trunklet.language__get( $1 )).language_id +$body$; + + +CREAtE OR REPLACE FUNCTION _trunklet.verify_type( + language_name _trunklet.language.language_name%TYPE + , allowed_type regtype + , supplied_type regtype + , which_type text +) RETURNS void LANGUAGE plpgsql AS $body$ +DECLARE + sql text; +BEGIN + /* Old variant code + IF supplied_type <> allowed_type THEN + RAISE EXCEPTION '%s for language "%" must by of type "%"', which_type, language_name, allowed_type + USING ERRCODE = 'data_exception' + ; + END IF; + */ + + /* + * This isn't strictly the same as what we previously did with variant. In + * this version, we just verify that you can cast from one type to the next. + * I think that's probably good enough. + */ + sql := format( + $$SELECT CAST( CAST( NULL AS %s ) AS %s )$$ + , supplied_type + , allowed_type + ); + RAISE DEBUG 'sql = %', sql; + EXECUTE sql; +END +$body$; + +CREATE OR REPLACE FUNCTION _trunklet.function_name( + language_id _trunklet.language.language_id%TYPE + , function_type text +) RETURNS text IMMUTABLE LANGUAGE sql AS $body$ +SELECT format( + 'language_id_%s__%s' + -- text version of language_id that is 0 padded. btrim shouldn't be necessary but is. + , btrim( to_char( + language_id + -- Get a string of 0's long enough to hold a max-sized int + , repeat( '0', length( (2^31-1)::int::text ) ) + ) ) + , function_type +); +$body$; + +CREATE OR REPLACE FUNCTION _trunklet.create_language_function( + language_id _trunklet.language.language_id%TYPE + , language_name _trunklet.language.language_name%TYPE + , return_type text + , function_arguments text + , function_options text + , function_body text + , function_type text +) RETURNS void LANGUAGE plpgsql AS $body$ +DECLARE + func_name CONSTANT text := _trunklet.function_name( language_id, function_type ); + func_full_name CONSTANT text := format( + -- Name template + $name$_trunklet_functions.%1$s(%s) + $name$ + , func_name + , function_arguments + ); + +BEGIN + RAISE DEBUG 'func_full_name = %', func_full_name; + PERFORM _trunklet.exec( + /* + * This is the SQL string we'll use to actually create the function. + */ + format( + + -- Actual function creation template + $temp$ +CREATE OR REPLACE FUNCTION %1$s RETURNS %2$s %3$s AS %4$L; +COMMENT ON FUNCTION %1$s IS $$%5$s function for trunklet language "%6$s". Not intended for general use.$$; +$temp$ + + -- Parameters for function template + , func_full_name + , return_type + , function_options + , function_body + , function_type + , language_name + ) + ); + + /* + * The language functions could be executed by any random user, so make + * certain that they're not security definer. + * + * Note that regprocedure pukes on variant.variant as a type name. That means + * we can't easily get our exact procedure, though in this case proname alone + * should be unique. We ultimately just need to ensure there's no SECDEF + * procedures at all... + */ + IF EXISTS( SELECT 1 FROM pg_proc WHERE proname = func_name AND prosecdef ) THEN + RAISE EXCEPTION 'language functions may not be SECURITY DEFINER' + USING DETAIL = format( 'language %s, %s function', language_name, function_type ) + ; + END IF; +END +$body$; + +CREATE OR REPLACE VIEW trunklet.template_language AS + SELECT + language_name + , parameter_type + , template_type + , process_function_options + , process_function_body + , extract_parameters_options + , extract_parameters_body + FROM _trunklet.language +; + +CREATE OR REPLACE FUNCTION trunklet.template_language__add( + language_name _trunklet.language.language_name%TYPE + , parameter_type _trunklet.language.parameter_type%TYPE + , template_type _trunklet.language.template_type%TYPE + , process_function_options _trunklet.language.process_function_options%TYPE + , process_function_body _trunklet.language.process_function_body%TYPE + , extract_parameters_options _trunklet.language.extract_parameters_options%TYPE + , extract_parameters_body _trunklet.language.extract_parameters_body%TYPE +) RETURNS void LANGUAGE plpgsql AS $body$ +<> +DECLARE + language_id _trunklet.language.language_id%TYPE; +BEGIN + -- Do explicit sanity check for better error messages + PERFORM _trunklet.name_sanity( 'language_name', language_name ); + + INSERT INTO _trunklet.language( + language_name + , parameter_type + , template_type + , process_function_options + , process_function_body + , extract_parameters_options + , extract_parameters_body + ) + SELECT + language_name + , parameter_type + , template_type + , process_function_options + , process_function_body + , extract_parameters_options + , extract_parameters_body + RETURNING language.language_id + INTO STRICT fn.language_id + ; + + PERFORM _trunklet.create_language_function( + language_id + , language_name + , 'text' + , format( + $args$ + template %s + , parameters %s +$args$ + , template_type + , parameter_type + ) + , process_function_options + , process_function_body + , 'process' + ); + + PERFORM _trunklet.create_language_function( + language_id + , language_name + , parameter_type::text + , format( + $args$ + parameters %s + , extract_list text[] +$args$ + , parameter_type + ) + , extract_parameters_options + , extract_parameters_body + , 'extract_parameters' + ); +END +$body$; +REVOKE ALL ON FUNCTION trunklet.template_language__add( + language_name _trunklet.language.language_name%TYPE + , parameter_type _trunklet.language.parameter_type%TYPE + , template_type _trunklet.language.template_type%TYPE + , process_function_options _trunklet.language.process_function_options%TYPE + , process_function_body _trunklet.language.process_function_body%TYPE + , extract_parameters_options _trunklet.language.extract_parameters_options%TYPE + , extract_parameters_body _trunklet.language.extract_parameters_body%TYPE +) FROM public; + + + +/* + * TEMPLATES + */ +CREATE TABLE _trunklet.template( + template_id serial NOT NULL PRIMARY KEY + , language_id int NOT NULL REFERENCES _trunklet.language + , template_name text NOT NULL CHECK(_trunklet.name_sanity( 'template_name', template_name )) + , template_version int NOT NULL + , template text NOT NULL -- TODO: Trigger to ensure template will cast to intended type + , CONSTRAINT template__u_template_name__template_version UNIQUE( template_name, template_version ) +); +GRANT REFERENCES ON _trunklet.template TO trunklet__dependency; + +CREATE OR REPLACE FUNCTION _trunklet.template__get( + template_name _trunklet.template.template_name%TYPE + , template_version _trunklet.template.template_version%TYPE DEFAULT 1 + , loose boolean DEFAULT false +) RETURNS _trunklet.template LANGUAGE plpgsql AS $body$ +DECLARE + r _trunklet.template; +BEGIN + SELECT * INTO STRICT r + FROM _trunklet.template t + WHERE t.template_name = template__get.template_name + AND t.template_version = template__get.template_version + ; + + RETURN r; +EXCEPTION + WHEN no_data_found THEN + IF loose THEN + RETURN NULL; + ELSE + RAISE EXCEPTION 'template name "%" at version % not found' + , template_name + , template_version + USING ERRCODE = 'no_data_found' + ; + END IF; +END +$body$; +REVOKE ALL ON FUNCTION _trunklet.template__get( + template_name _trunklet.template.template_name%TYPE + , template_version _trunklet.template.template_version%TYPE + , loose boolean +) FROM public; + +CREATE OR REPLACE FUNCTION trunklet.template__add( + language_name _trunklet.language.language_name%TYPE + , template_name _trunklet.template.template_name%TYPE + , template_version _trunklet.template.template_version%TYPE + , template _trunklet.template.template%TYPE +) RETURNS _trunklet.template.template_id%TYPE + +-- !!!!!!! +SECURITY DEFINER SET search_path=pg_catalog +-- !!!!!! + +LANGUAGE sql AS $body$ +INSERT INTO _trunklet.template( + language_id + , template_name + , template_version + , template + ) + SELECT + _trunklet.language__get_id( $1 ) + , $2 + , $3 + , $4 + RETURNING template_id +; +$body$; +CREATE OR REPLACE FUNCTION trunklet.template__add( + language_name _trunklet.language.language_name%TYPE + , template_name _trunklet.template.template_name%TYPE + , template _trunklet.template.template%TYPE +) RETURNS _trunklet.template.template_id%TYPE LANGUAGE sql AS $body$ +SELECT trunklet.template__add( $1, $2, 1, $3 ) +$body$; + +CREATE OR REPLACE FUNCTION trunklet.template__remove( + template_id _trunklet.template.template_id%TYPE +) RETURNS void + +-- !!!!!!! +SECURITY DEFINER SET search_path=pg_catalog +-- !!!!!! + +LANGUAGE sql AS $body$ +DELETE FROM _trunklet.template WHERE template_id = $1 +$body$; +CREATE OR REPLACE FUNCTION trunklet.template__remove( + template_name _trunklet.template.template_name%TYPE + , template_version _trunklet.template.template_version%TYPE DEFAULT 1 +) RETURNS void + +-- !!!!!!! +SECURITY DEFINER SET search_path=pg_catalog +-- !!!!!! + +LANGUAGE sql AS $body$ +SELECT trunklet.template__remove( (_trunklet.template__get( template_name, template_version )).template_id ) +$body$; + +CREATE OR REPLACE FUNCTION trunklet.template__dependency__add( + table_name text + , field_name name +) RETURNS void LANGUAGE plpgsql AS $body$ +DECLARE + -- Do this to sanitize input + o_table CONSTANT regclass := table_name; +BEGIN + PERFORM _trunklet.exec( format( 'ALTER TABLE %s ADD FOREIGN KEY( %I ) REFERENCES _trunklet.template', table_name, field_name ) ); +END +$body$; +CREATE OR REPLACE FUNCTION _trunklet.attnum__get( + table_name regclass + , field_name name +) RETURNS pg_attribute.attnum%TYPE LANGUAGE plpgsql AS $body$ +DECLARE + v_attnum pg_attribute.attnum%TYPE; +BEGIN + SELECT attnum INTO STRICT v_attnum + FROM pg_attribute WHERE attrelid = table_name AND attname = field_name + ; + + RETURN v_attnum; +EXCEPTION + WHEN no_data_found THEN + RAISE EXCEPTION 'column "%" does not exist', field_name + USING ERRCODE = 'undefined_column' + ; +END +$body$; + +CREATE OR REPLACE FUNCTION trunklet.template__dependency__remove( + table_name text + , field_name name +) RETURNS void LANGUAGE plpgsql AS $body$ +DECLARE + -- Do this to sanitize input + o_table CONSTANT regclass := table_name; + o_template CONSTANT regclass := '_trunklet.template'; + v_constraint_name name; + + -- Set these here so we don't accidentally re-trap + v_conkey smallint[] := array[ _trunklet.attnum__get( o_table, field_name ) ]; + v_confkey smallint[] := array[ _trunklet.attnum__get( o_template, 'template_id' ) ]; +BEGIN + BEGIN + SELECT conname INTO STRICT v_constraint_name + FROM pg_constraint + WHERE contype = 'f' + AND conrelid = o_table + AND confrelid = o_template + AND conkey = array[ _trunklet.attnum__get( o_table, field_name ) ] + AND confkey = array[ _trunklet.attnum__get( o_template, 'template_id' ) ] + ; + EXCEPTION + WHEN no_data_found THEN + RAISE EXCEPTION 'no template dependency on %.%', table_name, field_name + USING ERRCODE = 'undefined_object' + ; + END; + + PERFORM _trunklet.exec( format( 'ALTER TABLE %s DROP CONSTRAINT %I', table_name, v_constraint_name ) ); +END +$body$; + +CREATE OR REPLACE FUNCTION trunklet.process_language( + language_name _trunklet.language.language_name%TYPE + , template text + , parameters anyelement +) RETURNS text LANGUAGE plpgsql + +-- !!!!!!! +SECURITY DEFINER SET search_path = pg_catalog +-- !!!!!!! + +AS $body$ +DECLARE +/* + * !!!!! SECURITY DEFINER !!!!! + */ + --c_template_regtype CONSTANT regtype := variant.original_type(template); + + r_language _trunklet.language; + r_template _trunklet.template; + sql text; + v_return text; +BEGIN + /* + * Special case handling to support process( language_name, template_name, paramaters ) + * + * If "template" is text or varchar, then see if we have a stored template + * with that name. If we do, use it; otherwise treat "template" as an actual + * template. + */ + --IF c_template_regtype IN ('text'::regtype, 'varchar') THEN + r_template := _trunklet.template__get( template::text, loose := true ); + --END IF; + IF r_template IS NULL THEN + r_template.template := template; + END IF; + + + -- Can't do this during DECLARE (0A000: default value for row or record variable is not supported) + r_language := _trunklet.language__get( language_name ); + +/* + * !!!!! SECURITY DEFINER !!!!! + */ + PERFORM _trunklet.verify_type( language_name, r_language.template_type, pg_catalog.pg_typeof(template), 'template' ); + PERFORM _trunklet.verify_type( language_name, r_language.parameter_type, pg_catalog.pg_typeof(parameters), 'parameter' ); + + sql := format( + 'SELECT _trunklet_functions.%s( CAST($1 AS %s), CAST($2 AS %s) )' + , _trunklet.function_name( r_language.language_id, 'process' ) + , r_language.template_type + , r_language.parameter_type + ); + RAISE DEBUG E'execute %\nUSING %, %', sql, r_template.template, parameters; + EXECUTE sql INTO STRICT v_return USING r_template.template, parameters; + RAISE DEBUG '% returned %', sql, v_return; + + RETURN v_return; +END +$body$; +/* +CREATE OR REPLACE FUNCTION trunklet.process_language( + language_name _trunklet.language.language_name%TYPE + , template text + , parameters anyarray +) RETURNS text LANGUAGE sql AS $body$ +SELECT trunklet.process_language( + language_name + , template + , parameters::text +) +$body$; +*/ + +CREATE OR REPLACE FUNCTION trunklet.process( + template_name _trunklet.template.template_name%TYPE + , template_version _trunklet.template.template_version%TYPE + , parameters anyelement +) RETURNS text LANGUAGE SQL +-- !!! +SECURITY DEFINER SET search_path = pg_catalog +-- !!! +AS $body$ +SELECT trunklet.process_language( + (_trunklet.language__get(language_id)).language_name + , template + , parameters + ) + FROM _trunklet.template__get( template_name, template_version ) +$body$; +/* +CREATE OR REPLACE FUNCTION trunklet.process( + template_name _trunklet.template.template_name%TYPE + , template_version _trunklet.template.template_version%TYPE + , parameters anyarray +) RETURNS text LANGUAGE SQL AS $body$ +SELECT trunklet.process( + template_name + , template_version + , parameters::text +) +$body$; +*/ + +CREATE OR REPLACE FUNCTION trunklet.process( + template_name _trunklet.template.template_name%TYPE + , parameters anyelement +) RETURNS text LANGUAGE SQL AS $body$ +SELECT trunklet.process( template_name, 1, parameters ) +$body$; +/* +CREATE OR REPLACE FUNCTION trunklet.process( + template_name _trunklet.template.template_name%TYPE + , parameters anyarray +) RETURNS text LANGUAGE SQL AS $body$ +SELECT trunklet.process( + template_name + , parameters::text +) +$body$; +*/ + + + +/* + * trunklet.extract_parameters() + */ +CREATE OR REPLACE FUNCTION trunklet.extract_parameters( + language_name _trunklet.language.language_name%TYPE + , parameters anyelement + , extract_list text[] +) RETURNS anyelement LANGUAGE plpgsql + +-- !!!!!!! +SECURITY DEFINER SET search_path = pg_catalog +-- !!!!!!! + +AS $body$ +DECLARE + r_language _trunklet.language; + + sql text; + r record; +BEGIN + -- Can't do this during DECLARE (0A000: default value for row or record variable is not supported) + r_language := _trunklet.language__get( language_name ); + + PERFORM _trunklet.verify_type( language_name, r_language.parameter_type, pg_catalog.pg_typeof(parameters), 'parameter' ); + + sql := format( + 'SELECT _trunklet_functions.%s( CAST($1 AS %s), $2 )::%s AS out' + , _trunklet.function_name( r_language.language_id, 'extract_parameters' ) + , r_language.parameter_type + , pg_typeof(parameters) + ); + RAISE DEBUG 'EXECUTE % USING %, %' + , sql + , parameters, extract_list + ; + EXECUTE sql + INTO STRICT r + USING parameters, extract_list + ; + + RETURN r.out; +END +$body$; +/* +CREATE OR REPLACE FUNCTION trunklet.extract_parameters( + language_name _trunklet.language.language_name%TYPE + , parameters anyarray + , extract_list text[] +) RETURNS anyarray LANGUAGE plpgsql AS $body$ +DECLARE + r record; +BEGIN + EXECUTE format( + $$SELECT trunklet.extract_parameters($1, $2, $3)::%s AS out$$ + , pg_typeof(parameters) + ) + INTO r + USING language_name, parameters::text, extract_list + ; + RETURN r.out; +END +$body$; +*/ + + + +-- vi: expandtab sw=2 ts=2 diff --git a/sql/trunklet--0.2.1.sql b/sql/trunklet--0.2.1.sql new file mode 100644 index 0000000..6a32c74 --- /dev/null +++ b/sql/trunklet--0.2.1.sql @@ -0,0 +1,820 @@ +/* + * Author: Jim C. Nasby + * Created at: 2015-01-11 17:47:56 -0600 + * + */ + +/* + * Using temp objects will result in the extension being dropped after session + * end. Create a real schema and then explicitly drop it instead. + */ +CREATE SCHEMA __trunklet; + +CREATE TABLE __trunklet.old_settings AS + SELECT name, setting + FROM pg_catalog.pg_settings + WHERE name IN ('client_min_messages', 'search_path') +; +SET client_min_messages = warning; +SET search_path = pg_catalog; + +DO $do$ +BEGIN + CREATE ROLE trunklet__dependency; +EXCEPTION + WHEN duplicate_object THEN + -- TODO: Ensure options are what we expect + NULL; +END +$do$; + +CREATE SCHEMA _trunklet; +GRANT USAGE ON SCHEMA _trunklet TO trunklet__dependency; +COMMENT ON SCHEMA _trunklet IS $$Internal use functions for the trunklet extension.$$; + +CREATE SCHEMA _trunklet_functions; +-- TODO: Create a trunklet__usage role and use it here instead of public +GRANT USAGE ON SCHEMA _trunklet_functions TO public; -- Languages currently created as role that calls trunklet.template_language__add() +COMMENT ON SCHEMA _trunklet_functions IS $$Schema that contains support functions for languages registered in trunklet. Not intended for general use.$$; + +CREATE SCHEMA trunklet; +GRANT USAGE ON SCHEMA trunklet TO public; + +-- See also pgtemp.exec_as in test/core/functions.sql +CREATE OR REPLACE FUNCTION _trunklet.exec( + sql text +) RETURNS void LANGUAGE plpgsql AS $f$ +BEGIN + RAISE DEBUG 'Executing SQL %s', sql; + EXECUTE sql; +END +$f$; + +CREATE OR REPLACE FUNCTION _trunklet.name_sanity( + field_name text + , value text +) RETURNS boolean LANGUAGE plpgsql AS $body$ +DECLARE + error CONSTANT text := CASE + WHEN value IS NULL THEN 'be NULL' + WHEN value = '' THEN 'be blank' + -- (?n) means use newline sensitive mode + WHEN value ~ '(?n)^\s' THEN 'begin with whitespace' + WHEN value ~ '(?n)\s$' THEN 'end with whitespace' + END; +BEGIN + IF error IS NULL THEN + RETURN true; + END IF; + + RAISE EXCEPTION '% must not %', field_name, error + USING ERRCODE = 'invalid_parameter_value' + ; +END +$body$; + +CREATE TABLE _trunklet.language( + language_id serial PRIMARY KEY NOT NULL + , language_name varchar(100) UNIQUE NOT NULL CHECK(_trunklet.name_sanity( 'language_name', language_name )) + , parameter_type regtype NOT NULL + , template_type regtype NOT NULL + -- I don't think we'll need these fields, but better safe than sorry + , process_function_options text NOT NULL + , process_function_body text NOT NULL + , extract_parameters_options text NOT NULL + , extract_parameters_body text NOT NULL +); +COMMENT ON COLUMN _trunklet.language.parameter_type IS $$Data type used to pass parameters to templates in this language.$$; +COMMENT ON COLUMN _trunklet.language.parameter_type IS $$Data type used by templates in this language.$$; +-- TODO: Add AFTER UPDATE trigger to re-validate the type of all stored templates for a language if template_type changes +CREATE OR REPLACE FUNCTION _trunklet.language__get( + language_id _trunklet.language.language_id%TYPE +) RETURNS _trunklet.language STABLE LANGUAGE sql AS $body$ + SELECT * FROM _trunklet.language l WHERE l.language_id = language__get.language_id +$body$; +CREATE OR REPLACE FUNCTION _trunklet.language__get( + language_name _trunklet.language.language_name%TYPE +) RETURNS _trunklet.language STABLE LANGUAGE plpgsql AS $body$ +DECLARE + v_return _trunklet.language; +BEGIN + SELECT * INTO STRICT v_return + FROM _trunklet.language l + WHERE l.language_name = language__get.language_name + ; + RETURN v_return; +EXCEPTION + WHEN no_data_found THEN + RAISE EXCEPTION 'language "%" not found', language_name + USING ERRCODE = 'no_data_found' + ; +END +$body$; +CREATE OR REPLACE FUNCTION _trunklet.language__get_id( + language_name _trunklet.language.language_name%TYPE +) RETURNS _trunklet.language.language_id%TYPE STABLE LANGUAGE sql AS $body$ +SELECT (_trunklet.language__get( $1 )).language_id +$body$; + + +CREAtE OR REPLACE FUNCTION _trunklet.verify_type( + language_name _trunklet.language.language_name%TYPE + , allowed_type regtype + , supplied_type regtype + , which_type text +) RETURNS void LANGUAGE plpgsql AS $body$ +DECLARE + sql text; +BEGIN + /* Old variant code + IF supplied_type <> allowed_type THEN + RAISE EXCEPTION '%s for language "%" must by of type "%"', which_type, language_name, allowed_type + USING ERRCODE = 'data_exception' + ; + END IF; + */ + + /* + * This isn't strictly the same as what we previously did with variant. In + * this version, we just verify that you can cast from one type to the next. + * I think that's probably good enough. + */ + sql := format( + $$SELECT CAST( CAST( NULL AS %s ) AS %s )$$ + , supplied_type + , allowed_type + ); + RAISE DEBUG 'sql = %', sql; + EXECUTE sql; +END +$body$; + +CREATE OR REPLACE FUNCTION _trunklet.function_name( + language_id _trunklet.language.language_id%TYPE + , function_type text +) RETURNS text IMMUTABLE LANGUAGE sql AS $body$ +SELECT format( + 'language_id_%s__%s' + -- text version of language_id that is 0 padded. btrim shouldn't be necessary but is. + , btrim( to_char( + language_id + -- Get a string of 0's long enough to hold a max-sized int + , repeat( '0', length( (2^31-1)::int::text ) ) + ) ) + , function_type +); +$body$; + +CREATE OR REPLACE FUNCTION _trunklet.create_language_function( + language_id _trunklet.language.language_id%TYPE + , language_name _trunklet.language.language_name%TYPE + , return_type text + , function_arguments text + , function_options text + , function_body text + , function_type text +) RETURNS void LANGUAGE plpgsql AS $body$ +DECLARE + func_name CONSTANT text := _trunklet.function_name( language_id, function_type ); + func_full_name CONSTANT text := format( + -- Name template + $name$_trunklet_functions.%1$s(%s) + $name$ + , func_name + , function_arguments + ); + +BEGIN + RAISE DEBUG 'func_full_name = %', func_full_name; + PERFORM _trunklet.exec( + /* + * This is the SQL string we'll use to actually create the function. + */ + format( + + -- Actual function creation template + $temp$ +CREATE OR REPLACE FUNCTION %1$s RETURNS %2$s %3$s AS %4$L; +COMMENT ON FUNCTION %1$s IS $$%5$s function for trunklet language "%6$s". Not intended for general use.$$; +$temp$ + + -- Parameters for function template + , func_full_name + , return_type + , function_options + , function_body + , function_type + , language_name + ) + ); + + /* + * The language functions could be executed by any random user, so make + * certain that they're not security definer. + * + * Note that regprocedure pukes on variant.variant as a type name. That means + * we can't easily get our exact procedure, though in this case proname alone + * should be unique. We ultimately just need to ensure there's no SECDEF + * procedures at all... + */ + IF EXISTS( SELECT 1 FROM pg_proc WHERE proname = func_name AND prosecdef ) THEN + RAISE EXCEPTION 'language functions may not be SECURITY DEFINER' + USING DETAIL = format( 'language %s, %s function', language_name, function_type ) + ; + END IF; +END +$body$; + +CREATE OR REPLACE VIEW trunklet.template_language AS + SELECT + language_name + , parameter_type + , template_type + , process_function_options + , process_function_body + , extract_parameters_options + , extract_parameters_body + FROM _trunklet.language +; + +CREATE OR REPLACE FUNCTION trunklet.template_language__add( + language_name _trunklet.language.language_name%TYPE + , parameter_type _trunklet.language.parameter_type%TYPE + , template_type _trunklet.language.template_type%TYPE + , process_function_options _trunklet.language.process_function_options%TYPE + , process_function_body _trunklet.language.process_function_body%TYPE + , extract_parameters_options _trunklet.language.extract_parameters_options%TYPE + , extract_parameters_body _trunklet.language.extract_parameters_body%TYPE +) RETURNS void LANGUAGE plpgsql AS $body$ +<> +DECLARE + language_id _trunklet.language.language_id%TYPE; +BEGIN + -- Do explicit sanity check for better error messages + PERFORM _trunklet.name_sanity( 'language_name', language_name ); + + INSERT INTO _trunklet.language( + language_name + , parameter_type + , template_type + , process_function_options + , process_function_body + , extract_parameters_options + , extract_parameters_body + ) + SELECT + language_name + , parameter_type + , template_type + , process_function_options + , process_function_body + , extract_parameters_options + , extract_parameters_body + RETURNING language.language_id + INTO STRICT fn.language_id + ; + + PERFORM _trunklet.create_language_function( + language_id + , language_name + , 'text' + , format( + $args$ + template %s + , parameters %s +$args$ + , template_type + , parameter_type + ) + , process_function_options + , process_function_body + , 'process' + ); + + PERFORM _trunklet.create_language_function( + language_id + , language_name + , parameter_type::text + , format( + $args$ + parameters %s + , extract_list text[] +$args$ + , parameter_type + ) + , extract_parameters_options + , extract_parameters_body + , 'extract_parameters' + ); +END +$body$; +REVOKE ALL ON FUNCTION trunklet.template_language__add( + language_name _trunklet.language.language_name%TYPE + , parameter_type _trunklet.language.parameter_type%TYPE + , template_type _trunklet.language.template_type%TYPE + , process_function_options _trunklet.language.process_function_options%TYPE + , process_function_body _trunklet.language.process_function_body%TYPE + , extract_parameters_options _trunklet.language.extract_parameters_options%TYPE + , extract_parameters_body _trunklet.language.extract_parameters_body%TYPE +) FROM public; + + + +/* + * TEMPLATES + */ +CREATE TABLE _trunklet.template( + template_id serial NOT NULL PRIMARY KEY + , language_id int NOT NULL REFERENCES _trunklet.language + , template_name text NOT NULL CHECK(_trunklet.name_sanity( 'template_name', template_name )) + , template_version int NOT NULL + , template text NOT NULL -- TODO: Trigger to ensure template will cast to intended type + , CONSTRAINT template__u_template_name__template_version UNIQUE( template_name, template_version ) +); +GRANT REFERENCES ON _trunklet.template TO trunklet__dependency; + +CREATE OR REPLACE FUNCTION _trunklet.template__get( + template_id _trunklet.template.template_id%TYPE + , loose boolean DEFAULT false +) RETURNS _trunklet.template LANGUAGE plpgsql AS $body$ +DECLARE + r _trunklet.template; +BEGIN + SELECT * INTO STRICT r + FROM _trunklet.template t + WHERE t.template_id = template__get.template_id + ; + + RETURN r; +EXCEPTION + WHEN no_data_found THEN + IF loose THEN + RETURN NULL; + ELSE + RAISE EXCEPTION 'template not found' + USING ERRCODE = 'no_data_found' + , DETAIL = format( 'template id %s', template_id ) + ; + END IF; +END +$body$; +REVOKE ALL ON FUNCTION _trunklet.template__get( + template_id _trunklet.template.template_id%TYPE + , loose boolean +) FROM public; +CREATE OR REPLACE FUNCTION _trunklet.template__get( + template_name _trunklet.template.template_name%TYPE + , template_version _trunklet.template.template_version%TYPE DEFAULT 1 + , loose boolean DEFAULT false +) RETURNS _trunklet.template LANGUAGE plpgsql AS $body$ +DECLARE + r _trunklet.template; +BEGIN + SELECT * INTO STRICT r + FROM _trunklet.template t + WHERE t.template_name = template__get.template_name + AND t.template_version = template__get.template_version + ; + + RETURN r; +EXCEPTION + WHEN no_data_found THEN + IF loose THEN + RETURN NULL; + ELSE + RAISE EXCEPTION 'template name "%" at version % not found' + , template_name + , template_version + USING ERRCODE = 'no_data_found' + ; + END IF; +END +$body$; +REVOKE ALL ON FUNCTION _trunklet.template__get( + template_name _trunklet.template.template_name%TYPE + , template_version _trunklet.template.template_version%TYPE + , loose boolean +) FROM public; + +CREATE OR REPLACE FUNCTION trunklet.template__add( + language_name _trunklet.language.language_name%TYPE + , template_name _trunklet.template.template_name%TYPE + , template_version _trunklet.template.template_version%TYPE + , template _trunklet.template.template%TYPE +) RETURNS _trunklet.template.template_id%TYPE + +-- !!!!!!! +SECURITY DEFINER SET search_path=pg_catalog +-- !!!!!! + +LANGUAGE sql AS $body$ +INSERT INTO _trunklet.template( + language_id + , template_name + , template_version + , template + ) + SELECT + _trunklet.language__get_id( $1 ) + , $2 + , $3 + , $4 + RETURNING template_id +; +$body$; +CREATE OR REPLACE FUNCTION trunklet.template__add( + language_name _trunklet.language.language_name%TYPE + , template_name _trunklet.template.template_name%TYPE + , template _trunklet.template.template%TYPE +) RETURNS _trunklet.template.template_id%TYPE LANGUAGE sql AS $body$ +SELECT trunklet.template__add( $1, $2, 1, $3 ) +$body$; + +CREATE OR REPLACE FUNCTION trunklet.template__remove( + template_id _trunklet.template.template_id%TYPE +) RETURNS void + +-- !!!!!!! +SECURITY DEFINER SET search_path=pg_catalog +-- !!!!!! + +LANGUAGE sql AS $body$ +DELETE FROM _trunklet.template WHERE template_id = $1 +$body$; +CREATE OR REPLACE FUNCTION trunklet.template__remove( + template_name _trunklet.template.template_name%TYPE + , template_version _trunklet.template.template_version%TYPE DEFAULT 1 +) RETURNS void + +-- !!!!!!! +SECURITY DEFINER SET search_path=pg_catalog +-- !!!!!! + +LANGUAGE sql AS $body$ +SELECT trunklet.template__remove( (_trunklet.template__get( template_name, template_version )).template_id ) +$body$; + +CREATE OR REPLACE FUNCTION trunklet.template__dependency__add( + table_name text + , field_name name +) RETURNS void LANGUAGE plpgsql AS $body$ +DECLARE + -- Do this to sanitize input + o_table CONSTANT regclass := table_name; +BEGIN + PERFORM _trunklet.exec( format( 'ALTER TABLE %s ADD FOREIGN KEY( %I ) REFERENCES _trunklet.template', table_name, field_name ) ); +END +$body$; +CREATE OR REPLACE FUNCTION _trunklet.attnum__get( + table_name regclass + , field_name name +) RETURNS pg_attribute.attnum%TYPE LANGUAGE plpgsql AS $body$ +DECLARE + v_attnum pg_attribute.attnum%TYPE; +BEGIN + SELECT attnum INTO STRICT v_attnum + FROM pg_attribute WHERE attrelid = table_name AND attname = field_name + ; + + RETURN v_attnum; +EXCEPTION + WHEN no_data_found THEN + RAISE EXCEPTION 'column "%" does not exist', field_name + USING ERRCODE = 'undefined_column' + ; +END +$body$; + +CREATE OR REPLACE FUNCTION trunklet.template__dependency__remove( + table_name text + , field_name name +) RETURNS void LANGUAGE plpgsql AS $body$ +DECLARE + -- Do this to sanitize input + o_table CONSTANT regclass := table_name; + o_template CONSTANT regclass := '_trunklet.template'; + v_constraint_name name; + + -- Set these here so we don't accidentally re-trap + v_conkey smallint[] := array[ _trunklet.attnum__get( o_table, field_name ) ]; + v_confkey smallint[] := array[ _trunklet.attnum__get( o_template, 'template_id' ) ]; +BEGIN + BEGIN + SELECT conname INTO STRICT v_constraint_name + FROM pg_constraint + WHERE contype = 'f' + AND conrelid = o_table + AND confrelid = o_template + AND conkey = array[ _trunklet.attnum__get( o_table, field_name ) ] + AND confkey = array[ _trunklet.attnum__get( o_template, 'template_id' ) ] + ; + EXCEPTION + WHEN no_data_found THEN + RAISE EXCEPTION 'no template dependency on %.%', table_name, field_name + USING ERRCODE = 'undefined_object' + ; + END; + + PERFORM _trunklet.exec( format( 'ALTER TABLE %s DROP CONSTRAINT %I', table_name, v_constraint_name ) ); +END +$body$; + +CREATE OR REPLACE FUNCTION trunklet.process_language( + language_name _trunklet.language.language_name%TYPE + , template text + , parameters anyelement +) RETURNS text LANGUAGE plpgsql + +-- !!!!!!! +SECURITY DEFINER SET search_path = pg_catalog +-- !!!!!!! + +AS $body$ +DECLARE +/* + * !!!!! SECURITY DEFINER !!!!! + */ + + r_language _trunklet.language; + sql text; + v_return text; +BEGIN + -- Can't do this during DECLARE (0A000: default value for row or record variable is not supported) + r_language := _trunklet.language__get( language_name ); + +/* + * !!!!! SECURITY DEFINER !!!!! + */ + PERFORM _trunklet.verify_type( language_name, r_language.template_type, pg_catalog.pg_typeof(template), 'template' ); + PERFORM _trunklet.verify_type( language_name, r_language.parameter_type, pg_catalog.pg_typeof(parameters), 'parameter' ); + + sql := format( + 'SELECT _trunklet_functions.%s( CAST($1 AS %s), CAST($2 AS %s) )' + , _trunklet.function_name( r_language.language_id, 'process' ) + , r_language.template_type + , r_language.parameter_type + ); + RAISE DEBUG E'execute %\nUSING %, %', sql, template, parameters; + EXECUTE sql INTO STRICT v_return USING template, parameters; + RAISE DEBUG '% returned %', sql, v_return; + + RETURN v_return; +END +$body$; + +CREATE OR REPLACE FUNCTION trunklet.process( + template_id _trunklet.template.template_id%TYPE + , parameters anyelement +) RETURNS text LANGUAGE SQL +-- !!! +SECURITY DEFINER SET search_path = pg_catalog +-- !!! +AS $body$ +SELECT trunklet.process_language( + -- language_id comes from template__get() below + (_trunklet.language__get(language_id)).language_name + , template + , parameters + ) + FROM _trunklet.template__get( template_id ) +$body$; +CREATE OR REPLACE FUNCTION trunklet.process( + template_name _trunklet.template.template_name%TYPE + , template_version _trunklet.template.template_version%TYPE + , parameters anyelement +) RETURNS text LANGUAGE SQL +-- !!! +SECURITY DEFINER SET search_path = pg_catalog +-- !!! +AS $body$ +SELECT trunklet.process_language( + -- language_id comes from template__get() below + (_trunklet.language__get(language_id)).language_name + , template + , parameters + ) + FROM _trunklet.template__get( template_name, template_version ) +$body$; +CREATE OR REPLACE FUNCTION trunklet.process( + template_name _trunklet.template.template_name%TYPE + , parameters anyelement +) RETURNS text LANGUAGE SQL AS $body$ +SELECT trunklet.process( template_name, 1, parameters ) +$body$; + + +/* + * trunklet.execute_into() + */ +CREATE OR REPLACE FUNCTION trunklet.execute_into__language( + language_name _trunklet.language.language_name%TYPE + , template text + , parameters anyelement +) RETURNS anyelement LANGUAGE plpgsql AS $body$ +DECLARE + sql CONSTANT text := trunklet.process_language(language_name, template, parameters); + + r record; +BEGIN + -- Define the structure of r + EXECUTE format('SELECT NULL::%s AS out', pg_typeof(parameters)) INTO STRICT r; + + RAISE DEBUG E'execute %', sql; + EXECUTE sql INTO STRICT r.out; + RAISE DEBUG '% returned %', sql, r.out; + + RETURN r.out; +END +$body$; +CREATE OR REPLACE FUNCTION trunklet.execute_into( + template_id _trunklet.template.template_id%TYPE + , parameters anyelement +) RETURNS anyelement LANGUAGE plpgsql AS $body$ +DECLARE + sql CONSTANT text := trunklet.process(template_id, parameters); + + r record; +BEGIN + -- Define the structure of r + EXECUTE format('SELECT NULL::%s AS out', pg_typeof(parameters)) INTO STRICT r; + + RAISE DEBUG E'execute %', sql; + EXECUTE sql INTO STRICT r.out; + RAISE DEBUG '% returned %', sql, r.out; + + RETURN r.out; +END +$body$; +CREATE OR REPLACE FUNCTION trunklet.execute_into( + template_name _trunklet.template.template_name%TYPE + , template_version _trunklet.template.template_version%TYPE + , parameters anyelement +) RETURNS anyelement LANGUAGE plpgsql AS $body$ +DECLARE + sql CONSTANT text := trunklet.process(template_name, template_version, parameters); + + r record; +BEGIN + -- Define the structure of r + EXECUTE format('SELECT NULL::%s AS out', pg_typeof(parameters)) INTO STRICT r; + + RAISE DEBUG E'execute %', sql; + EXECUTE sql INTO STRICT r.out; + RAISE DEBUG '% returned %', sql, r.out; + + RETURN r.out; +END +$body$; +CREATE OR REPLACE FUNCTION trunklet.execute_into( + template_name _trunklet.template.template_name%TYPE + , parameters anyelement +) RETURNS anyelement LANGUAGE SQL AS $body$ +SELECT trunklet.execute_into( template_name, 1, parameters ) +$body$; + + + + + +/* + * trunklet.execute_text() + */ +CREATE OR REPLACE FUNCTION trunklet.execute_text__language( + language_name _trunklet.language.language_name%TYPE + , template text + , parameters anyelement +) RETURNS text LANGUAGE plpgsql AS $body$ +DECLARE + sql CONSTANT text := trunklet.process_language(language_name, template, parameters); + + out text; +BEGIN + RAISE DEBUG E'execute %', sql; + EXECUTE sql INTO STRICT out; + RAISE DEBUG '% returned %', sql, out; + + RETURN out; +END +$body$; +CREATE OR REPLACE FUNCTION trunklet.execute_text( + template_id _trunklet.template.template_id%TYPE + , parameters anyelement +) RETURNS anyelement LANGUAGE plpgsql AS $body$ +DECLARE + sql CONSTANT text := trunklet.process(template_id, parameters); + + out text; +BEGIN + RAISE DEBUG E'execute %', sql; + EXECUTE sql INTO STRICT out; + RAISE DEBUG '% returned %', sql, out; + + RETURN out; +END +$body$; +CREATE OR REPLACE FUNCTION trunklet.execute_text( + template_name _trunklet.template.template_name%TYPE + , template_version _trunklet.template.template_version%TYPE + , parameters anyelement +) RETURNS anyelement LANGUAGE plpgsql AS $body$ +DECLARE + sql CONSTANT text := trunklet.process(template_name, template_version, parameters); + + out text; +BEGIN + RAISE DEBUG E'execute %', sql; + EXECUTE sql INTO STRICT out; + RAISE DEBUG '% returned %', sql, out; + + RETURN out; +END +$body$; +CREATE OR REPLACE FUNCTION trunklet.execute_text( + template_name _trunklet.template.template_name%TYPE + , parameters anyelement +) RETURNS anyelement LANGUAGE SQL AS $body$ +SELECT trunklet.execute_text( template_name, 1, parameters ) +$body$; + + + + +/* + * trunklet.extract_parameters() + */ +CREATE OR REPLACE FUNCTION trunklet.extract_parameters( + language_name _trunklet.language.language_name%TYPE + , parameters anyelement + , extract_list text[] +) RETURNS anyelement LANGUAGE plpgsql + +-- !!!!!!! +SECURITY DEFINER SET search_path = pg_catalog +-- !!!!!!! + +AS $body$ +DECLARE + r_language _trunklet.language; + + sql text; + r record; +BEGIN + -- Can't do this during DECLARE (0A000: default value for row or record variable is not supported) + r_language := _trunklet.language__get( language_name ); + + PERFORM _trunklet.verify_type( language_name, r_language.parameter_type, pg_catalog.pg_typeof(parameters), 'parameter' ); + + sql := format( + 'SELECT _trunklet_functions.%s( CAST($1 AS %s), $2 )::%s AS out' + , _trunklet.function_name( r_language.language_id, 'extract_parameters' ) + , r_language.parameter_type + , pg_typeof(parameters) + ); + RAISE DEBUG 'EXECUTE % USING %, %' + , sql + , parameters, extract_list + ; + EXECUTE sql + INTO STRICT r + USING parameters, extract_list + ; + + RETURN r.out; +END +$body$; +/* +CREATE OR REPLACE FUNCTION trunklet.extract_parameters( + language_name _trunklet.language.language_name%TYPE + , parameters anyarray + , extract_list text[] +) RETURNS anyarray LANGUAGE plpgsql AS $body$ +DECLARE + r record; +BEGIN + EXECUTE format( + $$SELECT trunklet.extract_parameters($1, $2, $3)::%s AS out$$ + , pg_typeof(parameters) + ) + INTO r + USING language_name, parameters::text, extract_list + ; + RETURN r.out; +END +$body$; +*/ + + + +SELECT _trunklet.exec( + format( + 'SET %I = %L' + , name + , setting + ) + ) + FROM __trunklet.old_settings +; +DROP TABLE __trunklet.old_settings; +DROP SCHEMA __trunklet; + +-- vi: expandtab sw=2 ts=2 diff --git a/sql/trunklet--0.3.3--stable.sql b/sql/trunklet--0.3.3--stable.sql new file mode 100644 index 0000000..dc73d7b --- /dev/null +++ b/sql/trunklet--0.3.3--stable.sql @@ -0,0 +1 @@ +-- No actual SQL changes to this version bump diff --git a/test/expected/update.out b/test/expected/update.out new file mode 100644 index 0000000..d9d12d6 --- /dev/null +++ b/test/expected/update.out @@ -0,0 +1,9 @@ +\set ECHO none +1..5 + +1 +ok 1 - update 0.2.0 -> stable +ok 2 - stored template still processes after update +ok 3 - process(template_id, ...) exists after update +ok 4 - Function trunklet.execute_text(text, anyelement) should exist +ok 5 - Function trunklet.execute_into(integer, anyelement) should exist diff --git a/test/sql/update.sql b/test/sql/update.sql new file mode 100644 index 0000000..792fc9f --- /dev/null +++ b/test/sql/update.sql @@ -0,0 +1,43 @@ +\set ECHO none + +\i test/helpers/psql.sql + +BEGIN; +\i test/helpers/tap_setup.sql + +SELECT plan(5); + +-- Oldest committed install script; a language and template stored under it must survive the update chain. +CREATE EXTENSION trunklet VERSION '0.2.0'; +-- 0.2.0's install script leaves search_path = pg_catalog +SET search_path = tap; +SELECT trunklet.template_language__add( + 'update_test' + , 'text', 'text' + , 'LANGUAGE sql', 'SELECT replace($1, ''X'', $2)' + , 'LANGUAGE sql', 'SELECT $1' +); +SELECT trunklet.template__add('update_test', 'update_test_t', 'a X b'); + +ALTER EXTENSION trunklet UPDATE TO 'stable'; +-- The update chain also leaves search_path altered +SET search_path = tap; +SELECT pass('update 0.2.0 -> stable'); +SELECT is( + trunklet.process('update_test_t', 'Y'::text) + , 'a Y b' + , 'stored template still processes after update' +); +SELECT is( + trunklet.process( + (SELECT template_id FROM _trunklet.template WHERE template_name = 'update_test_t') + , 'Z'::text + ) + , 'a Z b' + , 'process(template_id, ...) exists after update' +); +SELECT has_function('trunklet', 'execute_text', ARRAY['text', 'anyelement']); +SELECT has_function('trunklet', 'execute_into', ARRAY['integer', 'anyelement']); + +SELECT finish(); +ROLLBACK; diff --git a/trunklet.control b/trunklet.control index b61c779..b80bfa8 100644 --- a/trunklet.control +++ b/trunklet.control @@ -1,4 +1,4 @@ # trunklet extension comment = 'A templating system for Postgres. Elephants don''t use templates, they use trunklets!' -default_version = '0.3.3' +default_version = 'stable' relocatable = false