Skip to content

Commit

Permalink
PostgreSQL Prolog 1.0
Browse files Browse the repository at this point in the history
  • Loading branch information
aarroyoc committed Sep 26, 2021
1 parent 22187f1 commit 0afdfeb
Show file tree
Hide file tree
Showing 7 changed files with 425 additions and 0 deletions.
40 changes: 40 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: Test
on: [push]

jobs:
test:
runs-on: ubuntu-20.04
services:
postgres:
image: postgres:13.4-alpine
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
POSTGRES_HOST_AUTH_METHOD: password
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- name: Install Rust cargo
uses: actions-rs/toolchain@v1
with:
toolchain: stable
profile: minimal
components: cargo
- name: Install Scryer Prolog
uses: logtalk-actions/setup-scryer-prolog@master
with:
scryer-prolog-version: latest
- name: Install Logtalk
uses: logtalk-actions/setup-logtalk@master
with:
logtalk-version: latest
- name: Checkout
uses: actions/checkout@v2
- name: Execute tests
run: logtalk_tester -p scryer
53 changes: 53 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,55 @@
# postgresql-prolog
A Prolog library to connect to PostgreSQL databases

# Compatible systems

* [Scryer Prolog](https://github.com/mthom/scryer-prolog)

# Installation

The library itself are just three Prolog files (postgresql.pl, messages.pl and types.pl). They need to be in the same folder. An easy way to install this library in your project is copying that files. Other way is using Git submodules to get this whole folder, and load the postgresql.pl file from there.

```
git submodule add https://github.com/aarroyoc/postgresql-prolog postgresql
```

# Usage

The library provides two predicates: `connect/6` and `query/3`.

```
connect(+User, +Password, +Host, +Port, +Database, -Connection)
```
Connects to a PostgreSQL server and tries to authenticate using `password` scheme. This is the only authentication method supported right now. Please, note that this auth method is not the default in some PostgreSQL setups, you changes are needed. If you're running PostgreSQL in Docker, you need to set the environment variable `POSTGRES_HOST_AUTH_METHOD` to `password`.

```
query(+Connection, +Query, -Result)
```
Executes a SQL query over a connection. Result can be:

- ok
- error(ErrorString)
- data(ColumnDescription, Rows)

ok is returned if the query doesn't output a table (INSERT, UPDATE, DELETE, CREATE TABLE, ...) and succeeds.

error(ErrorString) is returned if an error is found.

data(ColumnDescription, Rows) is returned when a query outputs a table (SELECT). ColumnDescription is a list of column names and Rows is a list of a list of each cell value.

# Examples

```
:- use_module('postgresql').
test :-
connect("postgres", "postgres", '127.0.0.1', 5432, "postgres", Connection),
query(Connection, "DROP TABLE IF EXISTS test_table", ok),
query(Connection, "CREATE TABLE test_table (id serial, name text)", ok),
query(Connection, "INSERT INTO test_table (name) VALUES ('test')", ok),
query(Connection, "SELECT * FROM test_table", Rows),
Rows = data(["id", "name"], [["1", "test"]]),
query(Connection, "UPDATE test_table SET name = 'test2' WHERE id = 1", ok),
query(Connection, "SELECT * FROM test_table", Rows2),
data(["id", "name"], [["1", "test2"]]).
```
142 changes: 142 additions & 0 deletions messages.pl
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
:- module(messages, [
startup_message/3,
auth_message/2,
password_message/2,
auth_ok_message/1,
query_message/2,
notice_message/1,
error_message/2,
command_complete_message/1,
empty_query_message/1,
row_description_message/2,
data_row_message/2,
ready_for_query_message/1
]).

:- use_module(library(lists)).
:- use_module(library(charsio)).

:- use_module('types').

% Message Formats
% https://www.postgresql.org/docs/current/protocol-message-formats.html

% StartupMessage
startup_message(User, Database, Bytes) :-
int32(196608, B1),
pstring("user", B2),
pstring(User, B3),
pstring("database", B4),
pstring(Database, B5),
append(B1, B2, B12),
append(B3, B4, B34),
append(B12, B34, B1234),
append(B1234, B5, B12345),
append(B12345, [0], B),
length(B, L),
BytesLength is L + 4,
int32(BytesLength, B0),
append(B0, B, Bytes).

% AuthenticationMD5Password
auth_message(md5, Salt, Bytes) :-
Bytes = [82,_,_,_,_|Bytes0],
Bytes0 = [B7, B6, B5, B4, B3, B2, B1, B0],
int32(5, [B7, B6, B5, B4]),
Salt = [B3, B2, B1, B0].

% AuthenticationCleartextPassword
auth_message(password, Bytes) :-
Bytes = [82,_,_,_,_|Bytes0],
Bytes0 = [B3, B2, B1, B0],
int32(3, [B3, B2, B1, B0]).

% PasswordMessage
password_message(Password, Bytes) :-
pstring(Password, Bytes0),
length(Bytes0, L),
RealLength is L + 4,
int32(RealLength, Bytes1),
append([112|Bytes1], Bytes0, Bytes).

% AuthenticationOk
auth_ok_message(Bytes) :-
Bytes = [82,0,0,0,8,0,0,0,0].

% Query
query_message(Query, Bytes) :-
pstring(Query, Bytes0),
length(Bytes0, L),
RealLength is L + 4,
int32(RealLength, Bytes1),
append([81|Bytes1], Bytes0, Bytes).

% ErrorResponse
error_message(Error, Bytes) :-
Bytes = [69,_,_,_,_,B0|Bytes0],
(B0 = 0 ->
Error = "No error message"
; pstring(Error, Bytes0)
).

% NoticeResponse
notice_message(Bytes) :-
Bytes = [78|_]. % Byte N

% EmptyQueryResponse
empty_query_message(Bytes) :-
Bytes = [73,_,_,_,_]. % Byte I

% CommandComplete
command_complete_message(Bytes) :-
Bytes = [67|_].

% RowDescription
row_description_message(Columns, Bytes) :-
Bytes = [84,_,_,_,_|Bytes0], % Byte T
Bytes0 = [B1, B0|Bytes1],
int16(Fields, [B1, B0]),
get_row_fields(Columns, Fields, Bytes1).

split(Bytes, Separator, Bytes0, Bytes1) :-
append(Bytes0, [Separator|Bytes1], Bytes).

get_row_fields([], 0, _).
get_row_fields([Column|Columns], Fields, Bytes) :-
split(Bytes, 0, StringBytes, R),
pstring(Column, StringBytes),
R = [_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_|NewBytes],
Fields0 is Fields - 1,
get_row_fields(Columns, Fields0, NewBytes).

% DataRow
data_row_message(Columns, Bytes) :-
Bytes = [68,_,_,_,_,B1,B0|Bytes0], % Byte D
int16(Fields, [B1, B0]),
get_fields(Columns, Fields, Bytes0).

get_fields([], 0, _).
get_fields([Column|Columns], Fields, Bytes) :-
Bytes = [B3, B2, B1, B0|Bytes0],
int32(Length, [B3, B2, B1, B0]),
Length = 4294967295,
Column = null,
Fields0 is Fields - 1,
get_fields(Columns, Fields0, Bytes0).

get_fields([Column|Columns], Fields, Bytes) :-
Bytes = [B3, B2, B1, B0|Bytes0],
int32(Length, [B3, B2, B1, B0]),
Length \= 4294967295,
take(Length, Bytes0, ColumnBytes, NewBytes),
chars_utf8bytes(Column, ColumnBytes),
Fields0 is Fields - 1,
get_fields(Columns, Fields0, NewBytes).

take(N, Bytes, Bytes0, Bytes1) :-
append(Bytes0, Bytes1, Bytes),
length(Bytes0, N).

% ReadyForQuery
ready_for_query_message(Bytes) :-
Bytes = [90|_].
109 changes: 109 additions & 0 deletions postgresql.pl
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
:- module(postgresql, [connect/6, query/3]).

:- use_module(library(lists)).
:- use_module(library(charsio)).
:- use_module(library(sockets)).

:- use_module('messages').
:- use_module('types').

connect(User, Password, Host, Port, Database, postgresql(Stream)) :-
socket_client_open(Host:Port, Stream, [type(binary)]),
startup_message(User, Database, BytesStartup),
put_bytes(Stream, BytesStartup),
get_bytes(Stream, BytesAuth),
auth_message(password, BytesAuth),
password_message(Password, BytesPassword),
put_bytes(Stream, BytesPassword),
get_bytes(Stream, BytesOk),
auth_ok_message(BytesOk),
flush_bytes(Stream).

flush_bytes(Stream) :-
get_bytes(Stream, Bytes),
(
Bytes = [90|_] ->
true
; flush_bytes(Stream)
).

query(postgresql(Stream), Query, Result) :-
query_message(Query, BytesQuery),
put_bytes(Stream, BytesQuery),
get_bytes(Stream, BytesResponse),
try_query_response(Stream, BytesResponse, Result).

% after a query message, the following messages can be received
% - CommandComplete
% - RowDescription -> N DataRow
% - EmptyQueryResponse
% - ErrorResponse
% - NoticeResponse
% and then a ReadyForQuery message
try_query_response(Stream, BytesResponse, Result) :-
command_complete_message(BytesResponse),!,
Result = ok,
get_bytes(Stream, BytesEnd),
ready_for_query_message(BytesEnd).

try_query_response(Stream, BytesResponse, Result) :-
row_description_message(ColumnsDescription, BytesResponse),!,
% then zero or more data rows
get_bytes(Stream, BytesData),
get_data_rows(Stream, ColumnsData, BytesData),
Result = data(ColumnsDescription, ColumnsData).
% until we get a command complete message

try_query_response(Stream, BytesResponse, Result) :-
empty_query_message(BytesResponse),!,
Result = [],
get_bytes(Stream, BytesEnd),
ready_for_query_message(BytesEnd).

try_query_response(Stream, BytesResponse, Result) :-
error_message(Error, BytesResponse),!,
Result = error(Error),
get_bytes(Stream, BytesEnd),
ready_for_query_message(BytesEnd).

try_query_response(Stream, BytesResponse, Result) :-
notice_message(BytesResponse),!,
get_bytes(Stream, BytesResponse0),
try_query_response(Stream, BytesResponse0, Result).

get_data_rows(Stream, [], BytesData) :-
command_complete_message(BytesData),!,
get_bytes(Stream, BytesData0),
ready_for_query_message(BytesData0).

get_data_rows(Stream, [Column|Columns], BytesData) :-
data_row_message(Column, BytesData),!,
get_bytes(Stream, BytesData0),
get_data_rows(Stream, Columns, BytesData0).


% https://www.postgresql.org/docs/current/protocol-flow.html#id-1.10.5.7.3

get_bytes(Stream, Bytes) :-
get_byte(Stream, BType),
get_byte(Stream, B3),
get_byte(Stream, B2),
get_byte(Stream, B1),
get_byte(Stream, B0),
int32(Length, [B3, B2, B1, B0]),
RemainingBytes is Length - 4,
get_bytes(Stream, RemainingBytes, Bytes0),
append([BType, B3, B2, B1, B0], Bytes0, Bytes),
!.

get_bytes(_, 0, []).
get_bytes(Stream, RemainingBytes, [B|Bytes]) :-
get_byte(Stream, B),
RemainingBytes1 is RemainingBytes - 1,
get_bytes(Stream, RemainingBytes1, Bytes).

put_bytes(_, []).
put_bytes(Stream, [Byte|Bytes]) :-
put_byte(Stream, Byte),
put_bytes(Stream, Bytes),
!.
7 changes: 7 additions & 0 deletions tester.lgt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
:- initialization((
set_logtalk_flag(report, warnings),
set_logtalk_flag(unknown_entities, silent),
logtalk_load(lgtunit(loader)),
logtalk_load('tests', [hook(lgtunit)]),
tests::run
)).
Loading

0 comments on commit 0afdfeb

Please sign in to comment.