├── .clang-format ├── .github └── workflows │ └── ci.yaml ├── .gitignore ├── .gitmodules ├── LICENSE ├── Makefile ├── README.md ├── nix ├── mustach.nix └── xpg.nix ├── plmustache.control.in ├── shell.nix ├── sql └── plmustache.sql ├── src ├── build.c ├── build.h ├── observation.c ├── observation.h ├── pg_prelude.h └── plmustache.c └── test ├── expected ├── errors.out ├── interpolation.out └── sections.out └── sql ├── errors.sql ├── interpolation.sql └── sections.sql /.clang-format: -------------------------------------------------------------------------------- 1 | AlignAfterOpenBracket: Align 2 | AlignConsecutiveAssignments: true 3 | AlignConsecutiveDeclarations: true 4 | AlignOperands: true 5 | AllowAllParametersOfDeclarationOnNextLine: false 6 | AllowShortFunctionsOnASingleLine: Empty 7 | AllowShortLoopsOnASingleLine: false 8 | AlwaysBreakAfterDefinitionReturnType: None 9 | AlwaysBreakAfterReturnType: None 10 | AlwaysBreakBeforeMultilineStrings: false 11 | BinPackArguments: true 12 | BinPackParameters: true 13 | ColumnLimit: 1000 14 | IndentPPDirectives: AfterHash 15 | MaxEmptyLinesToKeep: 1 16 | PointerAlignment: Right 17 | SpaceBeforeAssignmentOperators: true 18 | SpaceBeforeParens: ControlStatements 19 | SpaceInEmptyParentheses: false 20 | SpacesBeforeTrailingComments: 1 21 | BracedInitializerIndentWidth: 2 22 | 23 | # includes 24 | IncludeBlocks: Preserve 25 | SortIncludes: true 26 | 27 | # indentation 28 | IndentWidth: 2 29 | TabWidth: 2 30 | UseTab: Never 31 | 32 | # if 33 | AllowShortIfStatementsOnASingleLine: true 34 | 35 | # case 36 | IndentCaseLabels: false 37 | AllowShortCaseLabelsOnASingleLine: true 38 | AlignConsecutiveShortCaseStatements: 39 | Enabled: true 40 | AcrossEmptyLines: true 41 | AcrossComments: true 42 | AlignCaseColons: true 43 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | pg-version: ['12', '13', '14', '15', '16', '17'] 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | with: 16 | submodules: true 17 | 18 | - name: Install Nix 19 | uses: cachix/install-nix-action@v30 20 | with: 21 | nix_path: nixpkgs=channel:nixos-unstable 22 | 23 | - name: Use Cachix Cache 24 | uses: cachix/cachix-action@v10 25 | with: 26 | name: nxpg 27 | authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} 28 | 29 | - name: Build 30 | run: nix-shell --run "xpg -v ${{ matrix.pg-version }} build" 31 | 32 | - name: Run tests 33 | run: nix-shell --run "xpg -v ${{ matrix.pg-version }} test" 34 | 35 | - if: ${{ failure() }} 36 | run: | 37 | cat regression.out 38 | cat regression.diffs 39 | 40 | coverage: 41 | 42 | runs-on: ubuntu-latest 43 | 44 | strategy: 45 | matrix: 46 | pg-version: ['17'] 47 | 48 | steps: 49 | - uses: actions/checkout@v4 50 | with: 51 | submodules: true 52 | 53 | - name: Install Nix 54 | uses: cachix/install-nix-action@v30 55 | with: 56 | nix_path: nixpkgs=channel:nixos-unstable 57 | 58 | - name: Use Cachix Cache 59 | uses: cachix/cachix-action@v10 60 | with: 61 | name: nxpg 62 | authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} 63 | 64 | - name: Run coverage 65 | run: nix-shell --run "xpg -v ${{ matrix.pg-version }} coverage" 66 | 67 | - name: Send coverage to Coveralls 68 | uses: coverallsapp/github-action@v2.3.6 69 | with: 70 | github-token: ${{ secrets.GITHUB_TOKEN }} 71 | files: ./build-${{ matrix.pg-version }}/coverage.info 72 | 73 | style: 74 | 75 | runs-on: ubuntu-latest 76 | 77 | steps: 78 | - uses: actions/checkout@v4 79 | with: 80 | submodules: true 81 | 82 | - name: Install Nix 83 | uses: cachix/install-nix-action@v30 84 | with: 85 | nix_path: nixpkgs=channel:nixos-unstable 86 | 87 | - name: Use Cachix Cache 88 | uses: cachix/cachix-action@v10 89 | with: 90 | name: nxpg 91 | authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} 92 | 93 | - name: Run style check 94 | run: nix-shell --run "plmustache-style-check" 95 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | *.lex.c 3 | results/ 4 | .history 5 | *.so 6 | *.bc 7 | *.gcda 8 | *.gcno 9 | tags 10 | plmustache.control 11 | plmustache--*.sql 12 | coverage.info 13 | coverage_html 14 | regression.diffs 15 | regression.out 16 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "mustach"] 2 | path = mustach 3 | url = https://gitlab.com/jobol/mustach.git 4 | branch = 1.2.6 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023 Steve Chavez, Wolfgang Walther 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included 12 | in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SRC_DIR = src 2 | BUILD_DIR ?= build 3 | 4 | # the `-Wno`s quiet C90 warnings 5 | PG_CFLAGS = -std=c11 -Wextra -Wall -Werror \ 6 | -Wno-declaration-after-statement \ 7 | -Wno-vla \ 8 | -Wno-long-long \ 9 | -Wno-missing-field-initializers 10 | ifeq ($(COVERAGE), 1) 11 | PG_CFLAGS += --coverage 12 | endif 13 | 14 | EXTENSION = plmustache 15 | EXTVERSION = 0.1 16 | SED ?= sed 17 | 18 | DATA = $(wildcard sql/*--*.sql) 19 | 20 | EXTRA_CLEAN = sql/$(EXTENSION)--$(EXTVERSION).sql $(EXTENSION).control 21 | 22 | TESTS = $(wildcard test/sql/*.sql) 23 | REGRESS = $(patsubst test/sql/%.sql,%,$(TESTS)) 24 | REGRESS_OPTS = --inputdir=test 25 | 26 | MODULE_big = $(EXTENSION) 27 | SRC = $(wildcard $(SRC_DIR)/*.c) 28 | OBJS = $(patsubst $(SRC_DIR)/%.c, $(BUILD_DIR)/%.o, $(SRC)) 29 | 30 | PG_CONFIG = pg_config 31 | SHLIB_LINK = -lmustach 32 | 33 | # Find from system headers 34 | PG_CPPFLAGS := $(CPPFLAGS) -DEXTVERSION=\"$(EXTVERSION)\" 35 | 36 | build: $(BUILD_DIR)/$(EXTENSION).so $(BUILD_DIR)/$(EXTENSION)--$(EXTVERSION).sql $(BUILD_DIR)/$(EXTENSION).control 37 | 38 | $(BUILD_DIR)/.gitignore: 39 | mkdir -p $(BUILD_DIR) 40 | echo "*" > $(BUILD_DIR)/.gitignore 41 | 42 | $(BUILD_DIR)/%.o: $(SRC_DIR)/%.c $(BUILD_DIR)/.gitignore 43 | $(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@ 44 | 45 | $(BUILD_DIR)/$(EXTENSION)--$(EXTVERSION).sql: sql/$(EXTENSION).sql 46 | cp $< $@ 47 | 48 | $(BUILD_DIR)/$(EXTENSION).control: 49 | $(SED) "s/@EXTVERSION@/$(EXTVERSION)/g" $(EXTENSION).control.in > $@ 50 | 51 | $(BUILD_DIR)/$(EXTENSION).so: $(EXTENSION).so 52 | mv $? $@ 53 | 54 | PGXS := $(shell $(PG_CONFIG) --pgxs) 55 | include $(PGXS) 56 | 57 | .PHONY: test 58 | test: 59 | make installcheck 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # plmustache 2 | 3 | [![Coverage Status](https://coveralls.io/repos/github/PostgREST/plmustache/badge.svg)](https://coveralls.io/github/PostgREST/plmustache) 4 | 5 | A PostgreSQL extension that provides a language handler for [Mustache](https://mustache.github.io/) templates using https://gitlab.com/jobol/mustach. 6 | 7 | ## Roadmap 8 | 9 | - [x] variable interpolation 10 | - [x] sections 11 | - [x] bools 12 | - [ ] arrays 13 | - [ ] partials 14 | - [ ] inheritance 15 | - [ ] lambdas 16 | 17 | ## Features 18 | 19 | ### Variables 20 | 21 | Variables are handled as per the [mustache spec](https://mustache.github.io/mustache.5.html), a `{{key}}` variable will be interpolated. 22 | 23 | ```sql 24 | create or replace function win_money(you text, qt money, at timestamptz) returns text as $$ 25 | Hello {{you}}! 26 | You just won {{qt}} at {{at}}. 27 | $$ language plmustache; 28 | 29 | select win_money('Sir Meowalot', '12000', now()); 30 | win_money 31 | ----------------------------------------------------------- 32 | Hello Sir Meowalot! + 33 | You just won $12,000.00 at 2023-12-04 07:44:26.915735-05. 34 | (1 row) 35 | ``` 36 | 37 | #### Escaped and Unescaped 38 | 39 | A double mustache `{{key}}` will be escaped and a triple mustache `{{{key}}}` will not be escaped. 40 | 41 | ```sql 42 | create or replace function escape_me(tag text) returns text as $$ 43 | {{tag}} 44 | $$ language plmustache; 45 | 46 | select escape_me(''); 47 | escape_me 48 | ------------------------------------- 49 | <script>evil()</script> 50 | (1 row) 51 | 52 | create or replace function do_not_escape_me(tag text) returns text as $$ 53 | {{{tag}}} 54 | $$ language plmustache; 55 | 56 | select do_not_escape_me(''); 57 | do_not_escape_me 58 | ------------------------- 59 | 60 | (1 row) 61 | ``` 62 | 63 | ### Sections 64 | 65 | Boolean sections: 66 | 67 | ```sql 68 | create or replace function show_cat(cat text, show bool default true) returns text as $$ 69 | {{#show}} 70 | A cat appears, it's {{cat}}. 71 | {{/show}} 72 | {{^show}} 73 | A mysterious cat is hiding. 74 | {{/show}} 75 | $$ language plmustache; 76 | 77 | select show_cat('Mr. Sleepy'); 78 | show_cat 79 | --------------------------------- 80 | A cat appears, it's Mr. Sleepy.+ 81 | 82 | (1 row) 83 | 84 | select show_cat('Mr. Sleepy', false); 85 | show_cat 86 | ----------------------------- 87 | A mysterious cat is hiding.+ 88 | 89 | (1 row) 90 | ``` 91 | 92 | Array iterators: 93 | 94 | ```sql 95 | create or replace function hello_cats(cats text[]) returns text as $$ 96 | Say hello to: {{#cats}}{{.}}, {{/cats}} 97 | $$ language plmustache; 98 | 99 | 100 | postgres=# select hello_cats('{Sir Meowalot, Mr. Sleepy, Paquito}'); 101 | hello_cats 102 | --------------------------------------------------- 103 | Say hello to: Sir Meowalot, Mr. Sleepy, Paquito, 104 | (1 row) 105 | ``` 106 | 107 | ## Installation 108 | 109 | Clone the repo and submodules: 110 | 111 | ``` 112 | git clone --recurse-submodules https://github.com/PostgREST/plmustache 113 | ``` 114 | 115 | Build mustach: 116 | 117 | ``` 118 | cd mustach 119 | make && sudo make install 120 | sudo ldconfig 121 | ``` 122 | 123 | Build plmustache: 124 | 125 | ``` 126 | cd .. 127 | 128 | make && sudo make install 129 | ``` 130 | 131 | Then on SQL you can do: 132 | 133 | ```sql 134 | CREATE EXTENSION plmustache; 135 | ``` 136 | 137 | plmustache is tested on Postgres 12, 13, 14, 15, 16. 138 | 139 | ## Development 140 | 141 | For testing on your local database: 142 | 143 | ``` 144 | make installcheck 145 | ``` 146 | 147 | For an isolated and reproducible enviroment you can use [Nix](https://nixos.org/download.html). 148 | 149 | ``` 150 | $ nix-shell 151 | 152 | $ with-pg-15 psql 153 | 154 | $ with-pg-15 make installcheck 155 | ``` 156 | -------------------------------------------------------------------------------- /nix/mustach.nix: -------------------------------------------------------------------------------- 1 | { lib, stdenv, pkg-config }: 2 | 3 | stdenv.mkDerivation rec { 4 | pname = "mustach"; 5 | version = "1.2.5"; 6 | 7 | buildInputs = [ pkg-config ]; 8 | 9 | makeFlags = [ "PREFIX=$(out)" "VERSION=${version}"]; 10 | 11 | src = ../mustach; 12 | } 13 | -------------------------------------------------------------------------------- /nix/xpg.nix: -------------------------------------------------------------------------------- 1 | { fetchFromGitHub, lib } : 2 | let 3 | dep = fetchFromGitHub { 4 | owner = "steve-chavez"; 5 | repo = "xpg"; 6 | rev = "v1.2.1"; 7 | sha256 = "sha256-wmS2FdfA0FvjeuIZ2fWs3sByFIDYCJTBi/gmswVaY+A="; 8 | }; 9 | xpg = (import dep).xpg; 10 | in 11 | xpg 12 | -------------------------------------------------------------------------------- /plmustache.control.in: -------------------------------------------------------------------------------- 1 | default_version = '@EXTVERSION@' 2 | relocatable = false 3 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | with import (builtins.fetchTarball { 2 | name = "25.05-pre"; 3 | url = "https://github.com/NixOS/nixpkgs/archive/refs/tags/25.05-pre.tar.gz"; 4 | sha256 = "sha256:0b96wqvk3hs98dhfrmdhqmx9ibac4kjpanpd1pig19jaglanqnxr"; 5 | }) {}; 6 | let 7 | style = 8 | writeShellScriptBin "plmustache-style" '' 9 | ${clang-tools}/bin/clang-format -i src/* 10 | ''; 11 | styleCheck = 12 | writeShellScriptBin "plmustache-style-check" '' 13 | ${clang-tools}/bin/clang-format -i src/* 14 | ${git}/bin/git diff-index --exit-code HEAD -- '*.c' 15 | ''; 16 | in 17 | mkShell 18 | { 19 | buildInputs = [ 20 | (callPackage ./nix/mustach.nix {}) 21 | (callPackage ./nix/xpg.nix {}) 22 | styleCheck 23 | style 24 | ]; 25 | 26 | shellHook = '' 27 | export HISTFILE=.history 28 | ''; 29 | } 30 | -------------------------------------------------------------------------------- /sql/plmustache.sql: -------------------------------------------------------------------------------- 1 | CREATE FUNCTION plmustache_handler() 2 | RETURNS language_handler 3 | LANGUAGE C AS 'plmustache'; 4 | 5 | CREATE FUNCTION plmustache_inline_handler(internal) 6 | RETURNS void 7 | LANGUAGE C AS 'plmustache'; 8 | 9 | CREATE FUNCTION plmustache_validator(oid) 10 | RETURNS void 11 | LANGUAGE C AS 'plmustache'; 12 | 13 | CREATE TRUSTED LANGUAGE plmustache 14 | HANDLER plmustache_handler 15 | INLINE plmustache_inline_handler 16 | VALIDATOR plmustache_validator; 17 | -------------------------------------------------------------------------------- /src/build.c: -------------------------------------------------------------------------------- 1 | #include "pg_prelude.h" 2 | 3 | #include "build.h" 4 | #include "observation.h" 5 | 6 | const char DOT = '.'; 7 | 8 | static bool is_implicit_iterator(const char *str) { 9 | return str[0] == DOT; 10 | } 11 | 12 | static int plmustache_section_enter(void *userdata, const char *name) { 13 | plmustache_ctx *ctx = (plmustache_ctx *)userdata; 14 | 15 | for (size_t i = 0; i < ctx->num_params; i++) { 16 | plmustache_param *prm = &ctx->params[i]; 17 | 18 | if (strcmp(prm->prm_name, name) == 0) { 19 | ctx->section_key = prm->prm_name; 20 | ctx->section_arr_length = prm->is_array ? prm->prm_arr_length : 0; 21 | ctx->section_idx = 0; 22 | return prm->enters_section; 23 | } 24 | } 25 | 26 | return 0; 27 | } 28 | 29 | static int plmustache_section_next(void *userdata) { 30 | plmustache_ctx *ctx = (plmustache_ctx *)userdata; 31 | return ctx->section_idx < ctx->section_arr_length; 32 | } 33 | 34 | static int plmustache_section_leave(void *userdata) { 35 | plmustache_ctx *ctx = (plmustache_ctx *)userdata; 36 | ctx->section_key = NULL; 37 | ctx->section_idx = 0; 38 | ctx->section_arr_length = 0; 39 | return MUSTACH_OK; 40 | } 41 | 42 | static int plmustache_get_variable(void *userdata, const char *name, struct mustach_sbuf *sbuf) { 43 | plmustache_ctx *ctx = (plmustache_ctx *)userdata; 44 | 45 | if (is_implicit_iterator(name)) { 46 | for (size_t i = 0; i < ctx->num_params; i++) { 47 | plmustache_param *prm = &ctx->params[i]; 48 | 49 | if (strcmp(prm->prm_name, ctx->section_key) == 0) { 50 | sbuf->value = prm->prm_arr[ctx->section_idx]; 51 | } 52 | } 53 | ctx->section_idx = ctx->section_idx + 1; 54 | } else 55 | for (size_t i = 0; i < ctx->num_params; i++) { 56 | plmustache_param *prm = &ctx->params[i]; 57 | 58 | if (strcmp(name, prm->prm_name) == 0) { 59 | sbuf->value = prm->prm_value; 60 | } 61 | } 62 | 63 | return MUSTACH_OK; 64 | } 65 | 66 | struct mustach_itf plmustache_mustach_itf = { 67 | 68 | .enter = plmustache_section_enter, 69 | .next = plmustache_section_next, 70 | .leave = plmustache_section_leave, 71 | .get = plmustache_get_variable 72 | 73 | }; 74 | 75 | static char *datum_to_cstring(Datum datum, Oid typeoid) { 76 | Oid out_func; 77 | bool is_varlena; 78 | getTypeOutputInfo(typeoid, &out_func, &is_varlena); 79 | 80 | return OidOutputFunctionCall(out_func, datum); 81 | } 82 | 83 | static plmustache_param *build_params(plmustache_call_info call_info, plmustache_obs_handler observer) { 84 | plmustache_param *params = palloc0(sizeof(plmustache_param) * call_info.numargs); 85 | 86 | for (size_t i = 0; i < call_info.numargs; i++) { 87 | params[i].prm_name = call_info.argnames[i]; 88 | NullableDatum arg = call_info.argvalues[i]; 89 | Oid arg_type = call_info.argtypes[i]; 90 | Oid array_elem_type = get_element_type(arg_type); 91 | bool arg_is_array = array_elem_type != InvalidOid; 92 | 93 | if (arg.isnull) { 94 | params[i].prm_value = NULL; 95 | params[i].enters_section = false; 96 | params[i].is_array = false; 97 | } else { 98 | params[i].prm_value = datum_to_cstring(arg.value, call_info.argtypes[i]); 99 | if (arg_type == BOOLOID) 100 | params[i].enters_section = DatumGetBool(arg.value); 101 | else 102 | params[i].enters_section = true; 103 | 104 | if (arg_is_array) { 105 | params[i].is_array = true; 106 | ArrayType *array = DatumGetArrayTypeP(arg.value); 107 | ArrayIterator array_iterator = array_create_iterator(array, 0, NULL); 108 | int arr_ndim = ARR_NDIM(array); 109 | int arr_length = ArrayGetNItems(arr_ndim, ARR_DIMS(array)); 110 | if (arr_ndim > 1) observer((plmustache_observation){ERROR_NO_MULTIDIM}); 111 | 112 | if (arr_length > 0) { 113 | Datum value; 114 | bool isnull; 115 | int j = 0; 116 | params[i].prm_arr_length = arr_length; 117 | params[i].prm_arr = palloc0(sizeof(char *) * arr_length); 118 | 119 | while (array_iterate(array_iterator, &value, &isnull)) { 120 | params[i].prm_arr[j] = isnull ? NULL : datum_to_cstring(value, array_elem_type); 121 | j++; 122 | } 123 | } else 124 | params[i].enters_section = false; 125 | } 126 | } 127 | } 128 | 129 | return params; 130 | } 131 | 132 | plmustache_ctx build_mustache_ctx(plmustache_call_info call_info, plmustache_obs_handler observer) { 133 | plmustache_ctx ctx = {0}; 134 | 135 | // remove the newlines from the start and the end of the function body 136 | ctx.tpl = TextDatumGetCString(DirectFunctionCall2(btrim, call_info.prosrc, CStringGetTextDatum("\n"))); 137 | 138 | if (call_info.numargs > 0) { 139 | ctx.num_params = call_info.numargs; 140 | ctx.params = build_params(call_info, observer); 141 | } 142 | 143 | return ctx; 144 | } 145 | 146 | plmustache_ctx free_plmustache_ctx(plmustache_ctx ctx) { 147 | if (ctx.params) pfree(ctx.params); 148 | if (ctx.tpl) pfree(ctx.tpl); 149 | ctx = (plmustache_ctx){0}; 150 | return ctx; 151 | } 152 | 153 | plmustache_call_info build_call_info(Oid function_oid, __attribute__((unused)) FunctionCallInfo fcinfo, plmustache_obs_handler observer) { 154 | HeapTuple proc_tuple = SearchSysCache(PROCOID, ObjectIdGetDatum(function_oid), 0, 0, 0); 155 | if (!HeapTupleIsValid(proc_tuple)) observer((plmustache_observation){ERROR_NO_OID, .error_function_oid = function_oid}); 156 | 157 | bool is_null; 158 | Oid prorettype = SysCacheGetAttr(PROCOID, proc_tuple, Anum_pg_proc_prorettype, &is_null); 159 | if (is_null) observer((plmustache_observation){ERROR_NO_RETTYPE}); 160 | 161 | Datum prosrc = SysCacheGetAttr(PROCOID, proc_tuple, Anum_pg_proc_prosrc, &is_null); 162 | if (is_null) observer((plmustache_observation){ERROR_NO_SRC}); 163 | 164 | Oid *argtypes; 165 | char **argnames; 166 | char *argmodes; 167 | size_t numargs = get_func_arg_info(proc_tuple, &argtypes, &argnames, &argmodes); 168 | 169 | if (argmodes) // argmodes is non-NULL when any of the parameters are OUT or INOUT 170 | observer((plmustache_observation){ERROR_NOT_ONLY_IN_PARAMS}); 171 | 172 | // This already prevents functions from being used as triggers. 173 | // So it's not necessary to use CALLED_AS_TRIGGER and CALLED_AS_EVENT_TRIGGER 174 | if (getBaseType(prorettype) != TEXTOID) observer((plmustache_observation){ERROR_NO_TEXT_RET}); 175 | 176 | // when having a single unnamed parameter, the argnames are NULL 177 | if (!argnames && numargs == 1) observer((plmustache_observation){ERROR_UNNAMED_PARAMS}); 178 | for (size_t i = 0; i < numargs; i++) { 179 | if (strlen(argnames[i]) == 0) observer((plmustache_observation){ERROR_UNNAMED_PARAMS}); 180 | if (is_implicit_iterator(argnames[i])) observer((plmustache_observation){ERROR_PARAM_IMPLICIT_ITERATOR, .error_implicit_iterator = DOT}); 181 | } 182 | 183 | return (plmustache_call_info){proc_tuple, prosrc, numargs, argtypes, argnames, fcinfo->args}; 184 | } 185 | -------------------------------------------------------------------------------- /src/build.h: -------------------------------------------------------------------------------- 1 | #ifndef BUILD_H 2 | #define BUILD_H 3 | 4 | #include "observation.h" 5 | 6 | #include 7 | 8 | typedef struct { 9 | bool enters_section; 10 | bool is_array; 11 | size_t prm_arr_length; 12 | char *prm_name; 13 | char *prm_value; 14 | char **prm_arr; 15 | } plmustache_param; 16 | 17 | typedef struct { 18 | size_t num_params; 19 | plmustache_param *params; 20 | char *section_key; 21 | size_t section_idx; 22 | size_t section_arr_length; 23 | char *tpl; 24 | } plmustache_ctx; 25 | 26 | typedef struct { 27 | HeapTuple proc_tuple; 28 | Datum prosrc; 29 | size_t numargs; 30 | Oid *argtypes; 31 | char **argnames; 32 | NullableDatum *argvalues; 33 | } plmustache_call_info; 34 | 35 | extern struct mustach_itf plmustache_mustach_itf; 36 | 37 | plmustache_call_info build_call_info(Oid function_oid, FunctionCallInfo fcinfo, plmustache_obs_handler observer); 38 | 39 | plmustache_ctx build_mustache_ctx(plmustache_call_info call_info, plmustache_obs_handler observer); 40 | 41 | plmustache_ctx free_plmustache_ctx(plmustache_ctx ctx); 42 | 43 | #endif 44 | -------------------------------------------------------------------------------- /src/observation.c: -------------------------------------------------------------------------------- 1 | #include "observation.h" 2 | #include "pg_prelude.h" 3 | 4 | #include 5 | 6 | static const char *mustach_strerror(int mustach_code) { 7 | switch (mustach_code) { 8 | case MUSTACH_ERROR_SYSTEM : return "system error"; 9 | case MUSTACH_ERROR_UNEXPECTED_END : return "unexpected end"; 10 | case MUSTACH_ERROR_EMPTY_TAG : return "empty tag"; 11 | case MUSTACH_ERROR_TAG_TOO_LONG : return "tag is too long"; 12 | case MUSTACH_ERROR_BAD_SEPARATORS : return "bad separators"; 13 | case MUSTACH_ERROR_TOO_DEEP : return "too deep"; 14 | case MUSTACH_ERROR_CLOSING : return "closing"; 15 | case MUSTACH_ERROR_BAD_UNESCAPE_TAG : return "bad unescape tag"; 16 | case MUSTACH_ERROR_INVALID_ITF : return "invalid itf"; 17 | case MUSTACH_ERROR_ITEM_NOT_FOUND : return "item not found"; 18 | case MUSTACH_ERROR_PARTIAL_NOT_FOUND: return "partial not found"; 19 | case MUSTACH_ERROR_UNDEFINED_TAG : return "undefined tag"; 20 | default : return "unknown"; 21 | } 22 | } 23 | 24 | void ereporter(plmustache_observation o) { 25 | switch (o.obs_type) { 26 | case ERROR_NO_OID : ereport(ERROR, errmsg("could not find function with oid %u", o.error_function_oid)); break; 27 | case ERROR_NO_RETTYPE : ereport(ERROR, errmsg("pg_proc.prorettype is NULL")); break; 28 | case ERROR_NO_SRC : ereport(ERROR, errmsg("pg_proc.prosrc is NULL")); break; 29 | case ERROR_NOT_ONLY_IN_PARAMS : ereport(ERROR, errmsg("plmustache can only have IN parameters")); break; 30 | case ERROR_NO_TEXT_RET : ereport(ERROR, errmsg("plmustache functions can only return the text type or a domain based on the text type")); break; 31 | case ERROR_UNNAMED_PARAMS : ereport(ERROR, errmsg("plmustache can only have named parameters")); break; 32 | case ERROR_PARAM_IMPLICIT_ITERATOR: ereport(ERROR, errmsg("parameters cannot be named the same as the implicit iterator '%c'", o.error_implicit_iterator)); break; 33 | case ERROR_NO_DO_BLOCKS : ereport(ERROR, errmsg("plmustache doesn't allow DO blocks")); break; 34 | case ERROR_MUSTACH : ereport(ERROR, errmsg("plmustache template processing failed: %s", mustach_strerror(o.error_mustach_code))); break; 35 | case ERROR_NO_MULTIDIM : ereport(ERROR, errmsg("support for multidimensional arrays is not implemented")); break; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/observation.h: -------------------------------------------------------------------------------- 1 | #ifndef OBSERVATION_H 2 | #define OBSERVATION_H 3 | 4 | #include "pg_prelude.h" 5 | 6 | typedef enum { 7 | 8 | ERROR_NO_OID = 1, 9 | ERROR_NO_RETTYPE, 10 | ERROR_NO_SRC, 11 | ERROR_NOT_ONLY_IN_PARAMS, 12 | ERROR_NO_TEXT_RET, 13 | ERROR_UNNAMED_PARAMS, 14 | ERROR_PARAM_IMPLICIT_ITERATOR, 15 | ERROR_NO_DO_BLOCKS, 16 | ERROR_MUSTACH, 17 | ERROR_NO_MULTIDIM 18 | 19 | } plmustache_observation_type; 20 | 21 | typedef struct { 22 | plmustache_observation_type obs_type; 23 | union { 24 | Oid error_function_oid; 25 | Oid error_type_oid; 26 | int error_mustach_code; 27 | char error_implicit_iterator; 28 | }; 29 | } plmustache_observation; 30 | 31 | typedef void (*plmustache_obs_handler)(plmustache_observation obs); 32 | 33 | void ereporter(plmustache_observation o); 34 | 35 | #endif 36 | -------------------------------------------------------------------------------- /src/pg_prelude.h: -------------------------------------------------------------------------------- 1 | // pragmas needed to pass compiling with -Wextra 2 | #pragma GCC diagnostic push 3 | #pragma GCC diagnostic ignored "-Wunused-parameter" 4 | #pragma GCC diagnostic ignored "-Wsign-compare" 5 | 6 | // all the following includes depend on this one 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | #include 36 | #include 37 | #include 38 | #include 39 | #include 40 | #include 41 | #include 42 | #include 43 | #include 44 | #include 45 | #include 46 | #include 47 | 48 | #pragma GCC diagnostic pop 49 | -------------------------------------------------------------------------------- /src/plmustache.c: -------------------------------------------------------------------------------- 1 | #include "build.h" 2 | #include "observation.h" 3 | #include "pg_prelude.h" 4 | 5 | #include 6 | 7 | PG_MODULE_MAGIC; 8 | 9 | PG_FUNCTION_INFO_V1(plmustache_handler); 10 | Datum plmustache_handler(PG_FUNCTION_ARGS) { 11 | Oid function_oid = fcinfo->flinfo->fn_oid; 12 | 13 | plmustache_call_info call_info = build_call_info(function_oid, fcinfo, ereporter); 14 | 15 | plmustache_ctx ctx = build_mustache_ctx(call_info, ereporter); 16 | 17 | char *mustache_result; 18 | size_t mustache_result_size; 19 | int mustach_code = mustach_mem(ctx.tpl, 0, &plmustache_mustach_itf, &ctx, 0, &mustache_result, &mustache_result_size); 20 | 21 | if (mustach_code < 0) { 22 | ereporter((plmustache_observation){ERROR_MUSTACH, .error_mustach_code = mustach_code}); 23 | } 24 | 25 | ctx = free_plmustache_ctx(ctx); 26 | 27 | ReleaseSysCache(call_info.proc_tuple); 28 | 29 | PG_RETURN_TEXT_P(cstring_to_text(mustache_result)); 30 | } 31 | 32 | PG_FUNCTION_INFO_V1(plmustache_inline_handler); 33 | Datum plmustache_inline_handler(__attribute__((unused)) PG_FUNCTION_ARGS) { 34 | ereporter((plmustache_observation){ERROR_NO_DO_BLOCKS}); 35 | PG_RETURN_VOID(); 36 | } 37 | 38 | PG_FUNCTION_INFO_V1(plmustache_validator); 39 | Datum plmustache_validator(PG_FUNCTION_ARGS) { 40 | Oid function_oid = PG_GETARG_OID(0); 41 | 42 | if (!CheckFunctionValidatorAccess(fcinfo->flinfo->fn_oid, function_oid)) PG_RETURN_VOID(); 43 | 44 | if (!check_function_bodies) PG_RETURN_VOID(); 45 | 46 | plmustache_call_info call_info = build_call_info(function_oid, fcinfo, ereporter); 47 | 48 | ReleaseSysCache(call_info.proc_tuple); 49 | 50 | PG_RETURN_VOID(); 51 | } 52 | -------------------------------------------------------------------------------- /test/expected/errors.out: -------------------------------------------------------------------------------- 1 | create extension if not exists plmustache; 2 | \echo 3 | 4 | create or replace function tag_too_long() returns text as 5 | $$Hello, {{xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx}}$$ 6 | language plmustache; 7 | \echo 8 | 9 | select tag_too_long(); 10 | ERROR: plmustache template processing failed: tag is too long 11 | \echo 12 | 13 | create or replace function unexpected_end() returns text as 14 | $$Hello, {{}$$ 15 | language plmustache; 16 | \echo 17 | 18 | select unexpected_end(); 19 | ERROR: plmustache template processing failed: unexpected end 20 | \echo 21 | 22 | DO language plmustache $$ Hello $$; 23 | ERROR: plmustache doesn't allow DO blocks 24 | \echo 25 | 26 | create function hello_no_ret_text(x text) returns int as $$ 27 | Hello, {{x}} 28 | $$ language plmustache; 29 | ERROR: plmustache functions can only return the text type or a domain based on the text type 30 | \echo 31 | 32 | create function hello_no_ret_text(x text) returns float as $$ 33 | Hello, {{x}} 34 | $$ language plmustache; 35 | ERROR: plmustache functions can only return the text type or a domain based on the text type 36 | \echo 37 | 38 | create function hello_no_ret_text(x text) returns trigger as $$ 39 | Hello, {{x}} 40 | $$ language plmustache; 41 | ERROR: plmustache functions can only return the text type or a domain based on the text type 42 | \echo 43 | 44 | create function hello_no_ret_text(x text) returns event_trigger as $$ 45 | Hello, {{x}} 46 | $$ language plmustache; 47 | ERROR: plmustache functions can only return the text type or a domain based on the text type 48 | \echo 49 | 50 | create function hello_out(out x line) as $$ 51 | Hello, {{x}} 52 | $$ language plmustache; 53 | ERROR: plmustache can only have IN parameters 54 | \echo 55 | 56 | create function hello_inout(inout x line) as $$ 57 | Hello, {{x}} 58 | $$ language plmustache; 59 | ERROR: plmustache can only have IN parameters 60 | \echo 61 | 62 | create function hello_nonnamed_param(text) returns text as $$ 63 | Hello, {{x}} 64 | $$ language plmustache; 65 | ERROR: plmustache can only have named parameters 66 | \echo 67 | 68 | create function hello_nonnamed_param(x text, text) returns text as $$ 69 | Hello, {{x}} 70 | $$ language plmustache; 71 | ERROR: plmustache can only have named parameters 72 | \echo 73 | 74 | create function hello_nonnamed_param(text, y text) returns text as $$ 75 | Hello, {{x}} 76 | $$ language plmustache; 77 | ERROR: plmustache can only have named parameters 78 | \echo 79 | 80 | create domain "text/plain" as text; 81 | create or replace function hi() returns "text/plain" as 82 | $$Hi, you$$ 83 | language plmustache; 84 | \echo 85 | 86 | -- validation happens even at runtime in case check_function_bodies is turned off 87 | set check_function_bodies to off; 88 | \echo 89 | 90 | create function hello_invalid(text) returns int as $$ 91 | Hello, {{x}} 92 | $$ language plmustache; 93 | \echo 94 | 95 | select hello_invalid('foo'); 96 | ERROR: plmustache functions can only return the text type or a domain based on the text type 97 | \echo 98 | 99 | set check_function_bodies to on; 100 | \echo 101 | 102 | create or replace function iterator_name_disallowed_1("." int) returns text as $$ 103 | Hello 104 | $$ language plmustache; 105 | ERROR: parameters cannot be named the same as the implicit iterator '.' 106 | \echo 107 | 108 | create or replace function iterator_name_disallowed_2(a text, "." int) returns text as $$ 109 | Hello 110 | $$ language plmustache; 111 | ERROR: parameters cannot be named the same as the implicit iterator '.' 112 | -------------------------------------------------------------------------------- /test/expected/interpolation.out: -------------------------------------------------------------------------------- 1 | create extension if not exists plmustache; 2 | NOTICE: extension "plmustache" already exists, skipping 3 | \echo 4 | 5 | create or replace function hello() returns text as 6 | $$Hello, you$$ 7 | language plmustache; 8 | \echo 9 | 10 | select hello(); 11 | hello 12 | ------------ 13 | Hello, you 14 | (1 row) 15 | 16 | create or replace function hello(x1 text) returns text as 17 | $$Hello, {{x1}}$$ 18 | language plmustache; 19 | \echo 20 | 21 | select hello('world'); 22 | hello 23 | -------------- 24 | Hello, world 25 | (1 row) 26 | 27 | create or replace function hello(x1 text, x2 text) returns text as 28 | $$Hello, {{x1}} and {{x2}}$$ 29 | language plmustache; 30 | \echo 31 | 32 | select hello('Jane', 'John'); 33 | hello 34 | ---------------------- 35 | Hello, Jane and John 36 | (1 row) 37 | 38 | create or replace function hello(x1 int, x2 float, x3 json) returns text as 39 | $$Hello, {{x1}}, {{x2}} and {{x3}}$$ 40 | language plmustache; 41 | \echo 42 | 43 | select hello(1, 3.40, '{"abc": "xyz"}'); 44 | hello 45 | ------------------------------------------------------ 46 | Hello, 1, 3.4 and {"abc": "xyz"} 47 | (1 row) 48 | 49 | create or replace function hello_not_found_key(x1 int) returns text as 50 | $$Hello, {{not_found}}$$ 51 | language plmustache; 52 | \echo 53 | 54 | select hello_not_found_key(1); 55 | hello_not_found_key 56 | --------------------- 57 | Hello, 58 | (1 row) 59 | 60 | -- surrounding newlines are stripped, but not inside newlines 61 | create or replace function haiku(x1 text, x2 text, x3 text) returns text as $$ 62 | {{x1}} 63 | {{x2}} 64 | {{x3}} 65 | $$ 66 | language plmustache; 67 | \echo 68 | 69 | select haiku('The earth shakes', 'just enough', 'to remind us'); 70 | haiku 71 | ------------------ 72 | The earth shakes+ 73 | just enough + 74 | to remind us 75 | (1 row) 76 | 77 | create or replace function hello(p point) returns text as $$ 78 | Hello, {{p}} 79 | $$ language plmustache; 80 | \echo 81 | 82 | select hello(point(3,4)); 83 | hello 84 | -------------- 85 | Hello, (3,4) 86 | (1 row) 87 | 88 | create or replace function hello(x int[]) returns text as $$ 89 | Hello, {{x}} 90 | $$ language plmustache; 91 | \echo 92 | 93 | select hello(ARRAY[1,2,3]); 94 | hello 95 | ---------------- 96 | Hello, {1,2,3} 97 | (1 row) 98 | 99 | create or replace function hello_w_comment(x text) returns text as $$ 100 | Hello,{{! ignore me }} {{x}} 101 | $$ language plmustache; 102 | select hello_w_comment('ignored'); 103 | hello_w_comment 104 | ----------------- 105 | Hello, ignored 106 | (1 row) 107 | 108 | create or replace function escape_me(tag text) returns text as $$ 109 | {{tag}} 110 | $$ language plmustache; 111 | select escape_me(''); 112 | escape_me 113 | ------------------------------------- 114 | <script>evil()</script> 115 | (1 row) 116 | 117 | create or replace function do_not_escape_me(tag text) returns text as $$ 118 | {{{tag}}} 119 | $$ language plmustache; 120 | select do_not_escape_me(''); 121 | do_not_escape_me 122 | ------------------------- 123 | 124 | (1 row) 125 | 126 | -------------------------------------------------------------------------------- /test/expected/sections.out: -------------------------------------------------------------------------------- 1 | create extension if not exists plmustache; 2 | NOTICE: extension "plmustache" already exists, skipping 3 | \echo 4 | 5 | create or replace function bool_section(x1 text, x2 bool) returns text as $$ 6 | Section: {{#x2}}Show {{x1}}{{/x2}} 7 | $$ language plmustache; 8 | \echo 9 | 10 | select bool_section('Something', true); 11 | bool_section 12 | ------------------------- 13 | Section: Show Something 14 | (1 row) 15 | 16 | select bool_section('Something', false); 17 | bool_section 18 | -------------- 19 | Section: 20 | (1 row) 21 | 22 | create or replace function bool_section_inverted(x1 text, x2 bool) returns text as $$ 23 | Section: Show {{#x2}}{{x1}}{{/x2}}{{^x2}}Nothing{{/x2}} 24 | $$ language plmustache; 25 | \echo 26 | 27 | select bool_section_inverted('Something', true); 28 | bool_section_inverted 29 | ------------------------- 30 | Section: Show Something 31 | (1 row) 32 | 33 | select bool_section_inverted('Something', false); 34 | bool_section_inverted 35 | ----------------------- 36 | Section: Show Nothing 37 | (1 row) 38 | 39 | create or replace function foo_test(foo text) returns text as $$ 40 | {{#foo}}foo is {{foo}}{{/foo}} 41 | $$ language plmustache; 42 | \echo 43 | 44 | select foo_test('bar'); 45 | foo_test 46 | ------------ 47 | foo is bar 48 | (1 row) 49 | 50 | create or replace function foo_test(foo bool) returns text as $$ 51 | foo is {{#foo}}{{foo}}{{/foo}}{{^foo}}{{foo}}{{/foo}} 52 | $$ language plmustache; 53 | \echo 54 | 55 | select foo_test(true); 56 | foo_test 57 | ---------- 58 | foo is t 59 | (1 row) 60 | 61 | select foo_test(false); 62 | foo_test 63 | ---------- 64 | foo is f 65 | (1 row) 66 | 67 | create or replace function foo_null_test(foo bool) returns text as $$ 68 | foo is {{#foo}}full{{/foo}}{{^foo}}null{{/foo}} 69 | $$ language plmustache; 70 | \echo 71 | 72 | select foo_null_test(null); 73 | foo_null_test 74 | --------------- 75 | foo is null 76 | (1 row) 77 | 78 | select foo_null_test(true); 79 | foo_null_test 80 | --------------- 81 | foo is full 82 | (1 row) 83 | 84 | create or replace function foo_array(arr text[]) returns text as $$ 85 | arr is {{#arr}}{{.}}, {{/arr}} 86 | $$ language plmustache; 87 | \echo 88 | 89 | select foo_array(ARRAY['one', 'two', 'three']); 90 | foo_array 91 | -------------------------- 92 | arr is one, two, three, 93 | (1 row) 94 | 95 | create or replace function foo_array(arr int[]) returns text as $$ 96 | arr is {{#arr}}{{.}}, {{/arr}} 97 | $$ language plmustache; 98 | \echo 99 | 100 | select foo_array(ARRAY[1, 2, 3]::int[]); 101 | foo_array 102 | ------------------ 103 | arr is 1, 2, 3, 104 | (1 row) 105 | 106 | -- empty array is handled properly 107 | select foo_array(ARRAY[]::int[]); 108 | foo_array 109 | ----------- 110 | arr is 111 | (1 row) 112 | 113 | create or replace function mixed_var_array(var int, arr int[]) returns text as $$ 114 | var is {{var}}, arr is {{#arr}}{{.}}, {{/arr}} 115 | $$ language plmustache; 116 | \echo 117 | 118 | select mixed_var_array(4, ARRAY[1, 2, 3]::int[]); 119 | mixed_var_array 120 | ---------------------------- 121 | var is 4, arr is 1, 2, 3, 122 | (1 row) 123 | 124 | create or replace function mixed_array_var(arr int[], var int) returns text as $$ 125 | arr is {{#arr}}{{.}}, {{/arr}} var is {{var}} 126 | $$ language plmustache; 127 | \echo 128 | 129 | select mixed_array_var(ARRAY[1, 2, 3]::int[], 4); 130 | mixed_array_var 131 | --------------------------- 132 | arr is 1, 2, 3, var is 4 133 | (1 row) 134 | 135 | create or replace function mixed_array_var_array(arr1 int[], var text, arr2 int[]) returns text as $$ 136 | arr1 is {{#arr1}}{{.}}, {{/arr1}} var is {{var}} 137 | arr2 is {{#arr2}}{{.}}, {{/arr2}} var is {{var}} 138 | $$ language plmustache; 139 | \echo 140 | 141 | select mixed_array_var_array(ARRAY[1, 2, 3], 'something', ARRAY[4, 5, 6]); 142 | mixed_array_var_array 143 | ------------------------------------ 144 | arr1 is 1, 2, 3, var is something+ 145 | arr2 is 4, 5, 6, var is something 146 | (1 row) 147 | 148 | create or replace function nested_array(arr int[]) returns text as $$ 149 | arr is {{#arr}}{{.}}, {{/arr}} 150 | $$ language plmustache; 151 | \echo 152 | 153 | select nested_array(ARRAY[[1,2,3], [4,5,6]]); 154 | ERROR: support for multidimensional arrays is not implemented 155 | select nested_array(ARRAY[[[1,2], [3,4]], [[5,6], [7,8]]]); 156 | ERROR: support for multidimensional arrays is not implemented 157 | -------------------------------------------------------------------------------- /test/sql/errors.sql: -------------------------------------------------------------------------------- 1 | create extension if not exists plmustache; 2 | \echo 3 | 4 | create or replace function tag_too_long() returns text as 5 | $$Hello, {{xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx}}$$ 6 | language plmustache; 7 | \echo 8 | 9 | select tag_too_long(); 10 | \echo 11 | 12 | create or replace function unexpected_end() returns text as 13 | $$Hello, {{}$$ 14 | language plmustache; 15 | \echo 16 | 17 | select unexpected_end(); 18 | \echo 19 | 20 | DO language plmustache $$ Hello $$; 21 | \echo 22 | 23 | create function hello_no_ret_text(x text) returns int as $$ 24 | Hello, {{x}} 25 | $$ language plmustache; 26 | \echo 27 | 28 | create function hello_no_ret_text(x text) returns float as $$ 29 | Hello, {{x}} 30 | $$ language plmustache; 31 | \echo 32 | 33 | create function hello_no_ret_text(x text) returns trigger as $$ 34 | Hello, {{x}} 35 | $$ language plmustache; 36 | \echo 37 | 38 | create function hello_no_ret_text(x text) returns event_trigger as $$ 39 | Hello, {{x}} 40 | $$ language plmustache; 41 | \echo 42 | 43 | create function hello_out(out x line) as $$ 44 | Hello, {{x}} 45 | $$ language plmustache; 46 | \echo 47 | 48 | create function hello_inout(inout x line) as $$ 49 | Hello, {{x}} 50 | $$ language plmustache; 51 | \echo 52 | 53 | create function hello_nonnamed_param(text) returns text as $$ 54 | Hello, {{x}} 55 | $$ language plmustache; 56 | \echo 57 | 58 | create function hello_nonnamed_param(x text, text) returns text as $$ 59 | Hello, {{x}} 60 | $$ language plmustache; 61 | \echo 62 | 63 | create function hello_nonnamed_param(text, y text) returns text as $$ 64 | Hello, {{x}} 65 | $$ language plmustache; 66 | \echo 67 | 68 | create domain "text/plain" as text; 69 | create or replace function hi() returns "text/plain" as 70 | $$Hi, you$$ 71 | language plmustache; 72 | \echo 73 | 74 | -- validation happens even at runtime in case check_function_bodies is turned off 75 | set check_function_bodies to off; 76 | \echo 77 | 78 | create function hello_invalid(text) returns int as $$ 79 | Hello, {{x}} 80 | $$ language plmustache; 81 | \echo 82 | 83 | select hello_invalid('foo'); 84 | \echo 85 | 86 | set check_function_bodies to on; 87 | \echo 88 | 89 | create or replace function iterator_name_disallowed_1("." int) returns text as $$ 90 | Hello 91 | $$ language plmustache; 92 | \echo 93 | 94 | create or replace function iterator_name_disallowed_2(a text, "." int) returns text as $$ 95 | Hello 96 | $$ language plmustache; 97 | -------------------------------------------------------------------------------- /test/sql/interpolation.sql: -------------------------------------------------------------------------------- 1 | create extension if not exists plmustache; 2 | \echo 3 | 4 | create or replace function hello() returns text as 5 | $$Hello, you$$ 6 | language plmustache; 7 | \echo 8 | 9 | select hello(); 10 | 11 | create or replace function hello(x1 text) returns text as 12 | $$Hello, {{x1}}$$ 13 | language plmustache; 14 | \echo 15 | 16 | select hello('world'); 17 | 18 | create or replace function hello(x1 text, x2 text) returns text as 19 | $$Hello, {{x1}} and {{x2}}$$ 20 | language plmustache; 21 | \echo 22 | 23 | select hello('Jane', 'John'); 24 | 25 | create or replace function hello(x1 int, x2 float, x3 json) returns text as 26 | $$Hello, {{x1}}, {{x2}} and {{x3}}$$ 27 | language plmustache; 28 | \echo 29 | 30 | select hello(1, 3.40, '{"abc": "xyz"}'); 31 | 32 | create or replace function hello_not_found_key(x1 int) returns text as 33 | $$Hello, {{not_found}}$$ 34 | language plmustache; 35 | \echo 36 | 37 | select hello_not_found_key(1); 38 | 39 | -- surrounding newlines are stripped, but not inside newlines 40 | create or replace function haiku(x1 text, x2 text, x3 text) returns text as $$ 41 | {{x1}} 42 | {{x2}} 43 | {{x3}} 44 | $$ 45 | language plmustache; 46 | \echo 47 | 48 | select haiku('The earth shakes', 'just enough', 'to remind us'); 49 | 50 | create or replace function hello(p point) returns text as $$ 51 | Hello, {{p}} 52 | $$ language plmustache; 53 | \echo 54 | 55 | select hello(point(3,4)); 56 | 57 | create or replace function hello(x int[]) returns text as $$ 58 | Hello, {{x}} 59 | $$ language plmustache; 60 | \echo 61 | 62 | select hello(ARRAY[1,2,3]); 63 | 64 | create or replace function hello_w_comment(x text) returns text as $$ 65 | Hello,{{! ignore me }} {{x}} 66 | $$ language plmustache; 67 | 68 | select hello_w_comment('ignored'); 69 | 70 | create or replace function escape_me(tag text) returns text as $$ 71 | {{tag}} 72 | $$ language plmustache; 73 | 74 | select escape_me(''); 75 | 76 | create or replace function do_not_escape_me(tag text) returns text as $$ 77 | {{{tag}}} 78 | $$ language plmustache; 79 | 80 | select do_not_escape_me(''); 81 | -------------------------------------------------------------------------------- /test/sql/sections.sql: -------------------------------------------------------------------------------- 1 | create extension if not exists plmustache; 2 | \echo 3 | 4 | create or replace function bool_section(x1 text, x2 bool) returns text as $$ 5 | Section: {{#x2}}Show {{x1}}{{/x2}} 6 | $$ language plmustache; 7 | \echo 8 | 9 | select bool_section('Something', true); 10 | 11 | select bool_section('Something', false); 12 | 13 | create or replace function bool_section_inverted(x1 text, x2 bool) returns text as $$ 14 | Section: Show {{#x2}}{{x1}}{{/x2}}{{^x2}}Nothing{{/x2}} 15 | $$ language plmustache; 16 | \echo 17 | 18 | select bool_section_inverted('Something', true); 19 | 20 | select bool_section_inverted('Something', false); 21 | 22 | create or replace function foo_test(foo text) returns text as $$ 23 | {{#foo}}foo is {{foo}}{{/foo}} 24 | $$ language plmustache; 25 | \echo 26 | 27 | select foo_test('bar'); 28 | 29 | create or replace function foo_test(foo bool) returns text as $$ 30 | foo is {{#foo}}{{foo}}{{/foo}}{{^foo}}{{foo}}{{/foo}} 31 | $$ language plmustache; 32 | \echo 33 | 34 | select foo_test(true); 35 | 36 | select foo_test(false); 37 | 38 | create or replace function foo_null_test(foo bool) returns text as $$ 39 | foo is {{#foo}}full{{/foo}}{{^foo}}null{{/foo}} 40 | $$ language plmustache; 41 | \echo 42 | 43 | select foo_null_test(null); 44 | 45 | select foo_null_test(true); 46 | 47 | create or replace function foo_array(arr text[]) returns text as $$ 48 | arr is {{#arr}}{{.}}, {{/arr}} 49 | $$ language plmustache; 50 | \echo 51 | 52 | select foo_array(ARRAY['one', 'two', 'three']); 53 | 54 | create or replace function foo_array(arr int[]) returns text as $$ 55 | arr is {{#arr}}{{.}}, {{/arr}} 56 | $$ language plmustache; 57 | \echo 58 | 59 | select foo_array(ARRAY[1, 2, 3]::int[]); 60 | 61 | -- empty array is handled properly 62 | select foo_array(ARRAY[]::int[]); 63 | 64 | create or replace function mixed_var_array(var int, arr int[]) returns text as $$ 65 | var is {{var}}, arr is {{#arr}}{{.}}, {{/arr}} 66 | $$ language plmustache; 67 | \echo 68 | 69 | select mixed_var_array(4, ARRAY[1, 2, 3]::int[]); 70 | 71 | create or replace function mixed_array_var(arr int[], var int) returns text as $$ 72 | arr is {{#arr}}{{.}}, {{/arr}} var is {{var}} 73 | $$ language plmustache; 74 | \echo 75 | 76 | select mixed_array_var(ARRAY[1, 2, 3]::int[], 4); 77 | 78 | create or replace function mixed_array_var_array(arr1 int[], var text, arr2 int[]) returns text as $$ 79 | arr1 is {{#arr1}}{{.}}, {{/arr1}} var is {{var}} 80 | arr2 is {{#arr2}}{{.}}, {{/arr2}} var is {{var}} 81 | $$ language plmustache; 82 | \echo 83 | 84 | select mixed_array_var_array(ARRAY[1, 2, 3], 'something', ARRAY[4, 5, 6]); 85 | 86 | create or replace function nested_array(arr int[]) returns text as $$ 87 | arr is {{#arr}}{{.}}, {{/arr}} 88 | $$ language plmustache; 89 | \echo 90 | 91 | select nested_array(ARRAY[[1,2,3], [4,5,6]]); 92 | 93 | select nested_array(ARRAY[[[1,2], [3,4]], [[5,6], [7,8]]]); 94 | --------------------------------------------------------------------------------