├── .gitignore ├── .cargo └── config.toml ├── pgprolog.control ├── Cargo.toml ├── README.md ├── LICENSE └── src └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea/ 3 | /target 4 | *.iml 5 | **/*.rs.bk 6 | Cargo.lock 7 | -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.'cfg(target_os="macos")'] 2 | # Postgres symbols won't be available until runtime 3 | rustflags = ["-Clink-arg=-Wl,-undefined,dynamic_lookup"] 4 | -------------------------------------------------------------------------------- /pgprolog.control: -------------------------------------------------------------------------------- 1 | comment = 'pgprolog: Created by pgrx' 2 | default_version = '@CARGO_VERSION@' 3 | module_pathname = '$libdir/pgprolog' 4 | relocatable = false 5 | superuser = true 6 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pgprolog" 3 | version = "0.0.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | crate-type = ["cdylib"] 8 | 9 | [features] 10 | default = ["pg13"] 11 | pg11 = ["pgrx/pg11", "pgrx-tests/pg11" ] 12 | pg12 = ["pgrx/pg12", "pgrx-tests/pg12" ] 13 | pg13 = ["pgrx/pg13", "pgrx-tests/pg13" ] 14 | pg14 = ["pgrx/pg14", "pgrx-tests/pg14" ] 15 | pg15 = ["pgrx/pg15", "pgrx-tests/pg15" ] 16 | pg16 = ["pgrx/pg16", "pgrx-tests/pg16" ] 17 | pg_test = [] 18 | 19 | [dependencies] 20 | pgrx = "=0.11.3" 21 | scryer-prolog = "0.9.4" 22 | 23 | [dev-dependencies] 24 | pgrx-tests = "=0.11.3" 25 | 26 | [profile.dev] 27 | panic = "unwind" 28 | 29 | [profile.release] 30 | panic = "unwind" 31 | opt-level = 3 32 | lto = "fat" 33 | codegen-units = 1 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Prolog language for PostgreSQL 2 | 3 | This is a PostgreSQL extension that allows writing stored procedures in Prolog. 4 | 5 | This embeds [Scryer Prolog](https://www.scryer.pl) into a PostgreSQL extension. 6 | 7 | **Proof of concept!** Not ready for any actual use. 8 | 9 | 10 | ## Running 11 | 12 | You'll need [pgrx](https://github.com/pgcentralfoundation/pgrx) installed on your system. To install **pgrx** you can use cargo: 13 | ``` 14 | cargo install --locked cargo-pgrx 15 | cargo pgrx init 16 | ``` 17 | 18 | After that, run with `cargo pgrx run`. 19 | 20 | Then you can create the extension and language: 21 | ``` 22 | CREATE EXTENSION pgprolog; 23 | CREATE LANGUAGE plprolog HANDLER plprolog_call_handler; 24 | CREATE FUNCTION myfunc(a TEXT) RETURNS TEXT AS 'parent(alice,bob). handle(In,Out) :- parent(In,Out).' LANGUAGE plprolog; 25 | SELECT myfunc('alice'); 26 | ``` 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2024, Tatu Tarvainen and contributors 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use pgrx::prelude::*; 2 | use pgrx::fcinfo::*; 3 | use pgrx::spi::Spi; 4 | use scryer_prolog::machine; 5 | use scryer_prolog::machine::parsed_results::{ 6 | QueryResolution, prolog_value_to_json_string 7 | }; 8 | use std::time::{Duration, Instant}; 9 | 10 | pgrx::pg_module_magic!(); 11 | 12 | #[pg_extern(sql = "CREATE FUNCTION plprolog_call_handler() RETURNS language_handler LANGUAGE c AS 'MODULE_PATHNAME', '@FUNCTION_NAME@';")] 13 | unsafe fn plprolog_call_handler(fcinfo: pg_sys::FunctionCallInfo) -> pg_sys::Datum { 14 | let str : Option<&str> = pg_getarg(fcinfo, 0); 15 | let mut machine = machine::Machine::new_lib(); 16 | 17 | // We need procedure OID to get the actual source 18 | let oid : pg_sys::Oid = unsafe { 19 | fcinfo.as_ref().unwrap().flinfo.as_ref().expect("flinfo present").fn_oid 20 | }; 21 | 22 | // Consult the code as module 23 | let d1 = Instant::now(); 24 | let code = get_code(oid); 25 | pgrx::notice!("get code took: {:?}", d1.elapsed()); 26 | let d2 = Instant::now(); 27 | machine.load_module_string("plprolog_user", code.clone()); 28 | pgrx::notice!("consultation took: {:?}", d2.elapsed()); 29 | //format!("got code: {0}, arg: {1}", code, str.expect("argument")).into_datum().expect("result") 30 | // Then query handle(In,Out) 31 | let d3 = Instant::now(); 32 | let output = machine.run_query(format!("handle({0}, Out).", str.expect("argument present"))); 33 | pgrx::notice!("query took: {:?}", d3.elapsed()); 34 | let d4 = Instant::now(); 35 | let result : Option = 36 | match output { 37 | Ok(QueryResolution::Matches(results)) => { 38 | // FIXME: turn bindings into actual table result 39 | let out = results[0].bindings.get("Out").expect("Expected output binding"); 40 | let result_str = format!("got results: {0}", prolog_value_to_json_string(out.clone()).as_str()); 41 | result_str.into_datum() 42 | }, 43 | Ok(b) => format!("got truth: {0}", b.to_string()).into_datum(), 44 | Err(e) => format!("Got error: {0}", e).as_str().into_datum() 45 | }; 46 | let final_result = result.expect("output conversion"); 47 | pgrx::notice!("output conversion took: {:?}", d4.elapsed()); 48 | final_result 49 | } 50 | 51 | fn get_code(oid: pg_sys::Oid) -> String { 52 | match Spi::get_one::<&str>(format!("SELECT prosrc FROM pg_proc WHERE oid={0}", oid.as_u32().to_string()).as_str()) { 53 | Ok(Some(code)) => code.to_string(), 54 | Ok(None) => panic!("Code for procedure not found"), 55 | Err(err) => panic!("SPI error: {0}", err) 56 | } 57 | } 58 | --------------------------------------------------------------------------------