/* kc3
* Copyright from 2022 to 2025 kmx.io <contact@kmx.io>
*
* Permission is hereby granted to use this software granted the above
* copyright notice and this permission paragraph are included in all
* copies and substantial portions of this software.
*
* THIS SOFTWARE IS PROVIDED "AS-IS" WITHOUT ANY GUARANTEE OF
* PURPOSE AND PERFORMANCE. IN NO EVENT WHATSOEVER SHALL THE
* AUTHOR BE CONSIDERED LIABLE FOR THE USE AND PERFORMANCE OF
* THIS SOFTWARE.
*/
#include <errno.h>
#include <stdlib.h>
#include "alloc.h"
#include "assert.h"
#include "buf.h"
#include "buf_file.h"
#include "buf_inspect.h"
#include "buf_parse.h"
#include "compare.h"
#include "config.h"
#include "fact.h"
#include "facts.h"
#include "facts_cursor.h"
#include "facts_transaction.h"
#include "facts_with.h"
#include "file.h"
#include "io.h"
#include "list.h"
#include "log.h"
#include "marshall.h"
#include "marshall_read.h"
#include "rwlock.h"
#include "set__fact.h"
#include "set__tag.h"
#include "set_cursor__fact.h"
#include "skiplist__fact.h"
#include "sym.h"
#include "tag.h"
static int facts_compare_pfact_id_reverse (const void *a,
const void *b);
static sw facts_open_file_create (s_facts *facts, const s_str *path,
bool binary);
static sw facts_open_log (s_facts *facts, s_buf *buf, bool binary);
s_fact * facts_add_fact (s_facts *facts, s_fact *fact)
{
s_fact tmp = {0};
s_fact *f = NULL;
s_set_item__fact *item;
assert(facts);
assert(fact);
#if HAVE_PTHREAD
if (! rwlock_w(&facts->rwlock))
return NULL;
#endif
tmp.subject = facts_ref_tag(facts, fact->subject);
if (! tmp.subject) {
#if HAVE_PTHREAD
rwlock_unlock_w(&facts->rwlock);
#endif
return NULL;
}
tmp.predicate = facts_ref_tag(facts, fact->predicate);
if (! tmp.predicate) {
facts_unref_tag(facts, tmp.subject);
#if HAVE_PTHREAD
rwlock_unlock_w(&facts->rwlock);
#endif
return NULL;
}
tmp.object = facts_ref_tag(facts, fact->object);
if (! tmp.object) {
facts_unref_tag(facts, tmp.subject);
facts_unref_tag(facts, tmp.predicate);
#if HAVE_PTHREAD
rwlock_unlock_w(&facts->rwlock);
#endif
return NULL;
}
tmp.id = 0;
if ((item = set_get__fact(&facts->facts, &tmp))) {
#if HAVE_PTHREAD
rwlock_unlock_w(&facts->rwlock);
#endif
return &item->data;
}
tmp.id = facts->next_id;
if (facts->next_id == UW_MAX) {
err_puts("facts_add_fact: facts serial id exhausted");
assert(! "facts_add_fact: facts serial id exhausted");
goto ko;
}
item = set_add__fact(&facts->facts, &tmp);
if (! item)
goto ko;
f = &item->data;
if (! skiplist_insert__fact(facts->index_spo, f)) {
set_remove__fact(&facts->facts, f);
goto ko;
}
if (! skiplist_insert__fact(facts->index_pos, f)) {
skiplist_remove__fact(facts->index_spo, f);
set_remove__fact(&facts->facts, f);
goto ko;
}
if (! skiplist_insert__fact(facts->index_osp, f)) {
skiplist_remove__fact(facts->index_spo, f);
skiplist_remove__fact(facts->index_pos, f);
set_remove__fact(&facts->facts, f);
goto ko;
}
if (facts->log &&
! facts_log_add(facts->log, &tmp)) {
skiplist_remove__fact(facts->index_spo, f);
skiplist_remove__fact(facts->index_pos, f);
skiplist_remove__fact(facts->index_osp, f);
set_remove__fact(&facts->facts, f);
goto ko;
}
facts->next_id++;
#if HAVE_PTHREAD
rwlock_unlock_w(&facts->rwlock);
#endif
return f;
ko:
facts_unref_tag(facts, tmp.subject);
facts_unref_tag(facts, tmp.predicate);
facts_unref_tag(facts, tmp.object);
#if HAVE_PTHREAD
rwlock_unlock_w(&facts->rwlock);
#endif
return NULL;
}
s_fact * facts_add_tags (s_facts *facts, s_tag *subject,
s_tag *predicate, s_tag *object)
{
s_fact fact;
fact_init(&fact, subject, predicate, object);
return facts_add_fact(facts, &fact);
}
void facts_clean (s_facts *facts)
{
if (facts->log)
facts_close(facts);
facts_remove_all(facts);
skiplist_delete__fact(facts->index_osp);
skiplist_delete__fact(facts->index_pos);
skiplist_delete__fact(facts->index_spo);
set_clean__fact(&facts->facts);
set_clean__tag(&facts->tags);
#if HAVE_PTHREAD
rwlock_clean(&facts->rwlock);
#endif
}
void facts_close (s_facts *facts)
{
assert(facts->log);
log_close(facts->log);
log_delete(facts->log);
facts->log = NULL;
}
int facts_compare_pfact_id_reverse (const void *a, const void *b)
{
const s_fact *fa;
const s_fact *fb;
const s_fact * const *pfa;
const s_fact * const *pfb;
assert(a);
assert(b);
pfa = a;
pfb = b;
fa = *pfa;
fb = *pfb;
if (fa->id < fb->id)
return 1;
if (fa->id > fb->id)
return -1;
return 0;
}
s_facts ** facts_database (s_facts **facts)
{
*facts = facts_new();
return facts;
}
void facts_delete (s_facts *facts)
{
assert(facts);
facts_clean(facts);
free(facts);
}
// TODO: handle binary = true
sw facts_dump (s_facts *facts, s_buf *buf, bool binary)
{
s_facts_cursor cursor;
s_fact *fact;
s_marshall marshall = {0};
s_tag object;
s_tag predicate;
sw r;
sw result = 0;
s_tag subject;
assert(facts);
assert(buf);
if (binary) {
marshall_init(&marshall);
if (! marshall_facts(&marshall, false, facts) ||
(r = marshall_to_buf(&marshall, buf))) {
marshall_clean(&marshall);
return -1;
}
marshall_clean(&marshall);
return r;
}
tag_init_pvar(&subject, &g_sym_Tag);
tag_init_pvar(&predicate, &g_sym_Tag);
tag_init_pvar(&object, &g_sym_Tag);
if ((r = buf_write_1(buf,
"%{module: KC3.Facts.Dump,\n"
" version: 1}\n")) < 0)
return r;
result += r;
#if HAVE_PTHREAD
rwlock_r(&facts->rwlock);
#endif
facts_with_0(facts, &cursor, subject.data.pvar, predicate.data.pvar,
object.data.pvar);
if (! facts_cursor_next(&cursor, &fact))
goto clean;
while (fact) {
if ((r = buf_write_1(buf, "add ")) < 0)
goto clean;
result += r;
if ((r = buf_inspect_fact(buf, fact)) < 0)
goto clean;
result += r;
if ((r = buf_write_1(buf, "\n")) < 0)
goto clean;
result += r;
if (! facts_cursor_next(&cursor, &fact))
goto clean;
}
r = result;
clean:
tag_clean(&subject);
tag_clean(&predicate);
tag_clean(&object);
#if HAVE_PTHREAD
rwlock_unlock_r(&facts->rwlock);
#endif
return r;
}
sw facts_dump_file (s_facts *facts, const char *path, bool binary)
{
char b[BUF_SIZE];
s_buf buf;
FILE *fp;
sw r;
assert(facts);
assert(path);
buf_init(&buf, false, sizeof(b), b);
fp = file_open(path, "wb");
if (! fp)
return -1;
buf_file_open_w(&buf, fp);
r = facts_dump(facts, &buf, binary);
buf_file_close(&buf);
fclose(fp);
return r;
}
s_fact ** facts_find_fact (s_facts *facts, const s_fact *fact,
s_fact **dest)
{
s_fact f;
s_set_item__fact *item;
assert(facts);
assert(fact);
#if HAVE_PTHREAD
if (! rwlock_r(&facts->rwlock))
return NULL;
#endif
if (! facts_find_tag(facts, fact->subject, &f.subject))
return NULL;
if (! facts_find_tag(facts, fact->predicate, &f.predicate))
return NULL;
if (! facts_find_tag(facts, fact->object, &f.object))
return NULL;
*dest = NULL;
if (f.subject && f.predicate && f.object &&
(item = set_get__fact(&facts->facts, &f)))
*dest = &item->data;
#if HAVE_PTHREAD
rwlock_unlock_r(&facts->rwlock);
#endif
return dest;
}
s_fact ** facts_find_fact_by_tags (s_facts *facts, s_tag *subject,
s_tag *predicate, s_tag *object,
s_fact **dest)
{
s_fact f = {subject, predicate, object, 0};
return facts_find_fact(facts, &f, dest);
}
s_tag ** facts_find_tag (s_facts *facts, const s_tag *tag, s_tag **dest)
{
s_set_item__tag *item;
assert(facts);
assert(tag);
#if HAVE_PTHREAD
if (! rwlock_r(&facts->rwlock))
return NULL;
#endif
*dest = NULL;
if ((item = set_get__tag(&facts->tags, tag)))
*dest = &item->data;
#if HAVE_PTHREAD
rwlock_unlock_r(&facts->rwlock);
#endif
return dest;
}
s_facts * facts_init (s_facts *facts)
{
const u8 max_height = 10;
const double spacing = 2.7;
s_facts tmp = {0};
assert(facts);
set_init__tag(&tmp.tags, 1024);
set_init__fact(&tmp.facts, 1024);
tmp.index_spo = skiplist_new__fact(max_height, spacing);
assert(tmp.index_spo);
tmp.index_spo->compare = compare_fact;
tmp.index_pos = skiplist_new__fact(max_height, spacing);
assert(tmp.index_pos);
tmp.index_pos->compare = compare_fact_pos;
tmp.index_osp = skiplist_new__fact(max_height, spacing);
assert(tmp.index_osp);
tmp.index_osp->compare = compare_fact_osp;
#if HAVE_PTHREAD
rwlock_init(&facts->rwlock);
#endif
tmp.next_id = 1;
*facts = tmp;
return facts;
}
sw facts_load (s_facts *facts, s_buf *buf, const s_str *path,
bool binary)
{
s_fact eval_fact;
s_fact_w eval_fact_w;
s_fact_w fact_w;
sw r;
bool replace;
sw result = 0;
assert(facts);
assert(buf);
if (env_global()->trace) {
err_write_1("facts_load: ");
err_inspect_str(path);
err_write_1("\n");
}
if (binary)
return -1;
if ((r = buf_read_1(buf,
"%{module: KC3.Facts.Dump,\n"
" version: 1}\n")) <= 0) {
err_write_1("facts_load: invalid or missing header: ");
err_puts(path->ptr.pchar);
assert(! "facts_load: invalid or missing header");
return -1;
}
result += r;
#if HAVE_PTHREAD
rwlock_w(&facts->rwlock);
#endif
while (1) {
if ((r = buf_read_1(buf, "replace ")) < 0)
break;
if (r)
replace = true;
else {
replace = false;
if ((r = buf_read_1(buf, "add ")) <= 0)
break;
}
result += r;
if ((r = buf_parse_fact_w(buf, &fact_w)) <= 0) {
err_write_1("facts_load: invalid fact line ");
err_inspect_sw_decimal(buf->line);
err_write_1(": ");
err_puts(path->ptr.pchar);
err_inspect_buf(buf);
err_write_1("\n");
assert(! "facts_load: invalid fact");
goto ko;
}
result += r;
if ((r = buf_read_1(buf, "\n")) <= 0) {
fact_w_clean(&fact_w);
err_write_1("facts_load: missing newline line ");
err_inspect_sw_decimal(buf->line);
err_write_1(": ");
err_puts(path->ptr.pchar);
err_inspect_buf(buf);
assert(! "facts_load: missing newline");
goto ko;
}
result += r;
if (! fact_w_eval(&fact_w, &eval_fact_w)) {
fact_w_clean(&fact_w);
err_write_1("facts_load: failed to eval fact line ");
err_inspect_sw_decimal(buf->line);
err_write_1(": ");
err_puts(path->ptr.pchar);
err_inspect_buf(buf);
assert(! "facts_load: invalid fact");
goto ko;
}
if (! fact_init_fact_w(&eval_fact, &eval_fact_w)) {
fact_w_clean(&eval_fact_w);
fact_w_clean(&fact_w);
err_write_1("facts_load: failed to eval fact line ");
err_inspect_sw_decimal(buf->line);
err_write_1(": ");
err_puts(path->ptr.pchar);
err_inspect_buf(buf);
assert(! "facts_load: invalid fact");
goto ko;
}
if (replace) {
if (! facts_replace_fact(facts, &eval_fact)) {
fact_w_clean(&eval_fact_w);
fact_w_clean(&fact_w);
err_write_1("facts_load: failed to replace fact line ");
err_inspect_sw_decimal(buf->line);
err_write_1(": ");
err_puts(path->ptr.pchar);
assert(! "facts_load: failed to replace fact");
goto ko;
}
}
else {
if (! facts_add_fact(facts, &eval_fact)) {
fact_w_clean(&eval_fact_w);
fact_w_clean(&fact_w);
err_write_1("facts_load: failed to add fact line ");
err_inspect_sw_decimal(buf->line);
err_write_1(": ");
err_puts(path->ptr.pchar);
assert(! "facts_load: failed to add fact");
goto ko;
}
}
fact_w_clean(&fact_w);
fact_w_clean(&eval_fact_w);
}
if (env_global()->trace) {
err_write_1("facts_load: ");
err_inspect_str(path);
err_write_1(": OK\n");
}
#if HAVE_PTHREAD
rwlock_unlock_w(&facts->rwlock);
#endif
return result;
ko:
#if HAVE_PTHREAD
rwlock_unlock_w(&facts->rwlock);
#endif
return -1;
}
sw facts_load_file (s_facts *facts, const s_str *path, bool binary)
{
char b[BUF_SIZE];
s_buf buf;
FILE *fp;
sw result;
assert(facts);
assert(path);
buf_init(&buf, false, sizeof(b), b);
fp = file_open(path->ptr.pchar, "rb");
if (! fp)
return -1;
buf_file_open_r(&buf, fp);
result = facts_load(facts, &buf, path, binary);
buf_file_close(&buf);
fclose(fp);
return result;
}
sw facts_log_add (s_log *log, const s_fact *fact)
{
sw r;
sw result = 0;
assert(log);
assert(fact);
if (log->binary)
return -1;
if ((r = buf_write_1(&log->buf, "add ")) < 0)
return r;
result += r;
if ((r = buf_inspect_fact(&log->buf, fact)) < 0)
return r;
result += r;
if ((r = buf_write_1(&log->buf, "\n")) < 0)
return r;
result += r;
return result;
}
sw facts_log_remove (s_log *log, const s_fact *fact)
{
sw r;
sw result = 0;
assert(log);
assert(fact);
if (log->binary)
return -1;
if ((r = buf_write_1(&log->buf, "remove ")) < 0)
return r;
result += r;
if ((r = buf_inspect_fact(&log->buf, fact)) < 0)
return r;
result += r;
if ((r = buf_write_1(&log->buf, "\n")) < 0)
return r;
result += r;
return result;
}
s_facts * facts_new (void)
{
s_facts *facts;
facts = alloc(sizeof(s_facts));
if (! facts)
return NULL;
if (! facts_init(facts)) {
free(facts);
return NULL;
}
return facts;
}
sw facts_open_buf (s_facts *facts, s_buf *buf, const s_str *path,
bool binary)
{
sw r;
sw result = 0;
if ((r = facts_load(facts, buf, path, binary)) <= 0)
return r;
result += r;
if ((r = facts_open_log(facts, buf, binary)) < 0)
return r;
result += r;
return result;
}
sw facts_open_file (s_facts *facts, const s_str *path, bool binary)
{
FILE *fp;
char i[BUF_SIZE];
s_buf in;
sw r;
sw result = 0;
buf_init(&in, false, sizeof(i), i);
fp = fopen(path->ptr.pchar, "rb");
if (! fp) {
if (errno == ENOENT)
return facts_open_file_create(facts, path, binary);
return -1;
}
buf_file_open_r(&in, fp);
if ((r = facts_open_buf(facts, &in, path, binary)) < 0)
return r;
result += r;
buf_file_close(&in);
if (facts_dump_file(facts, path->ptr.pchar, binary) < 0)
return -1;
fp = file_open(path->ptr.pchar, "ab");
if (! fp)
return -1;
if (! (facts->log = log_new()))
return -1;
log_open(facts->log, fp, binary);
return result;
}
sw facts_open_file_create (s_facts *facts, const s_str *path,
bool binary)
{
FILE *fp;
s_buf *out;
sw r;
sw result = 0;
fp = file_open(path->ptr.pchar, "wb");
if (! fp)
return -1;
out = buf_new_alloc(BUF_SIZE);
buf_file_open_w(out, fp);
if ((r = facts_dump(facts, out, binary)) < 0)
return r;
result += r;
buf_flush(out);
if (! (facts->log = log_new()))
return -1;
buf_file_open_w(&facts->log->buf, fp);
return result;
}
sw facts_open_log (s_facts *facts, s_buf *buf, bool binary)
{
bool b;
s_fact_w fact_w;
s_fact fact;
sw r;
sw result = 0;
assert(facts);
assert(buf);
if (binary)
return -1; // TODO: do somethin
while (1) {
if ((r = buf_read_1(buf, "add ")) < 0)
break;
result += r;
if (r) {
if ((r = buf_parse_fact_w(buf, &fact_w)) <= 0)
break;
result += r;
if (! fact_init_fact_w(&fact, &fact_w))
return -1;
if (! facts_add_fact(facts, &fact))
return -1;
goto ok;
}
if ((r = buf_read_1(buf, "remove ")) <= 0)
break;
result += r;
if ((r = buf_parse_fact_w(buf, &fact_w)) <= 0)
break;
result += r;
if (! fact_init_fact_w(&fact, &fact_w))
return -1;
if (! facts_remove_fact(facts, &fact, &b))
return -1;
ok:
fact_w_clean(&fact_w);
if ((r = buf_read_1(buf, "\n")) <= 0)
break;
result += r;
}
return result;
}
s_tag * facts_ref_tag (s_facts *facts, s_tag *tag)
{
s_set_item__tag *item;
assert(facts);
assert(tag);
item = set_add__tag(&facts->tags, tag);
if (! item) {
err_write_1("facts_ref_tag: set_add__tag: ");
err_inspect_tag(tag);
err_write_1("\n");
assert(! "facts_ref_tag: set_add__tag");
return NULL;
}
item->usage++;
return &item->data;
}
s_facts * facts_remove_all (s_facts *facts)
{
bool b;
uw count;
s_set_cursor__fact cursor;
s_fact **f;
uw i;
uw j;
s_set_item__fact *item;
assert(facts);
count = facts->facts.count;
if (! count)
return facts;
f = alloc(count * sizeof(s_fact *));
if (! f)
return NULL;
i = 0;
set_cursor_init__fact(&facts->facts, &cursor);
while (i < count &&
(item = set_cursor_next__fact(&cursor))) {
f[i] = &item->data;
i++;
}
qsort(f, i, sizeof(s_fact *), facts_compare_pfact_id_reverse);
j = 0;
while (j < i) {
if (! facts_remove_fact(facts, f[j], &b) ||
! b) {
free(f);
return NULL;
}
j++;
}
free(f);
return facts;
}
bool * facts_remove_fact (s_facts *facts, const s_fact *fact,
bool *dest)
{
s_fact f;
s_fact *found;
assert(facts);
assert(fact);
#if HAVE_PTHREAD
rwlock_w(&facts->rwlock);
#endif
if (! facts_find_fact(facts, fact, &found))
return NULL;
*dest = false;
if (found) {
if (facts->log)
facts_log_remove(facts->log, found);
skiplist_remove__fact(facts->index_spo, found);
skiplist_remove__fact(facts->index_pos, found);
skiplist_remove__fact(facts->index_osp, found);
f = *found;
set_remove__fact(&facts->facts, found);
facts_unref_tag(facts, f.subject);
facts_unref_tag(facts, f.predicate);
facts_unref_tag(facts, f.object);
*dest = true;
}
#if HAVE_PTHREAD
rwlock_unlock_w(&facts->rwlock);
#endif
return dest;
}
bool * facts_remove_fact_tags (s_facts *facts, s_tag *subject,
s_tag *predicate,
s_tag *object,
bool *dest)
{
s_fact fact;
assert(facts);
assert(subject);
assert(predicate);
assert(object);
fact.subject = subject;
fact.predicate = predicate;
fact.object = object;
return facts_remove_fact(facts, &fact, dest);
}
s_fact * facts_replace_fact (s_facts *facts, s_fact *fact)
{
assert(facts);
assert(fact);
return facts_replace_tags(facts, fact->subject, fact->predicate,
fact->object);
}
s_fact * facts_replace_tags (s_facts *facts, s_tag *subject,
s_tag *predicate,
s_tag *object)
{
bool b;
s_facts_cursor cursor;
s_fact *fact;
s_list *list = NULL;
s_facts_transaction transaction;
s_tag pvar;
assert(facts);
assert(subject);
assert(predicate);
assert(object);
tag_init_pvar(&pvar, &g_sym_Tag);
#if HAVE_PTHREAD
if (! rwlock_w(&facts->rwlock))
return NULL;
#endif
if (! facts_with_tags(facts, &cursor, (s_tag *) subject,
(s_tag *) predicate, &pvar)) {
#if HAVE_PTHREAD
rwlock_unlock_w(&facts->rwlock);
#endif
return NULL;
}
if (! facts_cursor_next(&cursor, &fact)) {
#if HAVE_PTHREAD
rwlock_unlock_w(&facts->rwlock);
#endif
return NULL;
}
while (fact) {
list = list_new(list);
list->tag.type = TAG_FACT;
list->tag.data.fact = *fact;
if (! facts_cursor_next(&cursor, &fact)) {
#if HAVE_PTHREAD
rwlock_unlock_w(&facts->rwlock);
#endif
list_delete_all(list);
tag_clean(&pvar);
return NULL;
}
}
facts_transaction_start(facts, &transaction);
while (list) {
if (! facts_remove_fact(facts, &list->tag.data.fact, &b) ||
! b) {
list_delete_all(list);
facts_transaction_rollback(facts, &transaction);
facts_cursor_clean(&cursor);
tag_clean(&pvar);
#if HAVE_PTHREAD
rwlock_unlock_w(&facts->rwlock);
#endif
return NULL;
}
list = list_delete(list);
}
facts_cursor_clean(&cursor);
tag_clean(&pvar);
fact = facts_add_tags(facts, subject, predicate, object);
facts_transaction_end(facts, &transaction);
#if HAVE_PTHREAD
rwlock_unlock_w(&facts->rwlock);
#endif
return fact;
}
sw facts_save_file (s_facts *facts, const char *path, bool binary)
{
char b[BUF_SIZE];
s_buf buf;
FILE *fp;
sw r;
sw result = 0;
assert(facts);
assert(path);
assert(! facts->log);
buf_init(&buf, false, sizeof(b), b);
fp = file_open(path, "wb");
if (! fp)
return -1;
buf_file_open_w(&buf, fp);
if ((r = facts_dump(facts, &buf, binary)) < 0)
goto ko;
result += r;
buf_flush(&buf);
free(buf.user_ptr);
buf.user_ptr = NULL;
if (! (facts->log = log_new()))
goto ko;
if (! log_open(facts->log, fp, binary))
goto ko;
return result;
ko:
fclose(fp);
return r;
}
bool facts_unref_tag (s_facts *facts, const s_tag *tag)
{
s_set_item__tag *item;
assert(facts);
assert(tag);
item = set_get__tag(&facts->tags, tag);
if (item) {
item->usage--;
if (! item->usage)
set_remove_item__tag(&facts->tags, item);
return true;
}
return false;
}