-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
425 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"]]). | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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|_]. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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), | ||
!. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
)). |
Oops, something went wrong.