diff options
author | David Robillard <d@drobilla.net> | 2023-03-31 17:17:41 -0400 |
---|---|---|
committer | David Robillard <d@drobilla.net> | 2023-12-02 18:49:08 -0500 |
commit | b5956c4dc6b065d664908104d5fc6752a87e3364 (patch) | |
tree | 6be1fa515891e759092bb9bea082e27c78bfb6de /src | |
parent | 439d6ec3d6dfbea74334beace790f500e61c9b7d (diff) | |
download | serd-b5956c4dc6b065d664908104d5fc6752a87e3364.tar.gz serd-b5956c4dc6b065d664908104d5fc6752a87e3364.tar.bz2 serd-b5956c4dc6b065d664908104d5fc6752a87e3364.zip |
Add model and serd-sort utility
With all the new functionality, the complexity of the serd-pipe command-line
interface is starting to push the limits of available flags. So, instead of
grafting on further options to control a model, this commit adds a new tool,
serd-sort, which acts somewhat like a stripped-down serd-pipe that stores
statements in a model in memory.
This keeps the complexity (including the user-facing complexity) of any one
tool down, since other more focused tools can be used for streaming tasks in a
pipeline.
In other words, abandon Swissarmyknifeism, take a page from the Unix
philosophy, and try to expose the model functionality to the command-line in a
dedicated focused tool. The model implementation is tested by using this tool
to run a subset of the usual test suites, and a special suite to test statement
sorting.
Diffstat (limited to 'src')
-rw-r--r-- | src/compare.c | 147 | ||||
-rw-r--r-- | src/compare.h | 37 | ||||
-rw-r--r-- | src/cursor.c | 224 | ||||
-rw-r--r-- | src/cursor.h | 71 | ||||
-rw-r--r-- | src/describe.c | 314 | ||||
-rw-r--r-- | src/inserter.c | 146 | ||||
-rw-r--r-- | src/model.c | 820 | ||||
-rw-r--r-- | src/model.h | 29 | ||||
-rw-r--r-- | src/reader.c | 2 | ||||
-rw-r--r-- | src/string.c | 4 |
10 files changed, 1793 insertions, 1 deletions
diff --git a/src/compare.c b/src/compare.c new file mode 100644 index 00000000..2ce49cc2 --- /dev/null +++ b/src/compare.c @@ -0,0 +1,147 @@ +// Copyright 2011-2021 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: ISC + +#include "compare.h" + +#include "statement.h" // IWYU pragma: keep + +#include "serd/node.h" +#include "serd/statement.h" +#include "zix/attributes.h" + +#include <assert.h> + +/// Compare a mandatory node with a node pattern +ZIX_PURE_FUNC static inline int +serd_node_wildcard_compare(const SerdNode* ZIX_NONNULL a, + const SerdNode* ZIX_NULLABLE b) +{ + assert(a); + return b ? serd_node_compare(a, b) : 0; +} + +/// Compare an optional graph node with a node pattern +static inline int +serd_node_graph_compare(const SerdNode* ZIX_NULLABLE a, + const SerdNode* ZIX_NULLABLE b) +{ + if (a == b) { + return 0; + } + + if (!a) { + return -1; + } + + if (!b) { + return 1; + } + + return serd_node_compare(a, b); +} + +/// Compare statements lexicographically, ignoring graph +int +serd_triple_compare(const void* const x, + const void* const y, + const void* const user_data) +{ + const int* const ordering = (const int*)user_data; + const SerdStatement* const s = (const SerdStatement*)x; + const SerdStatement* const t = (const SerdStatement*)y; + + for (unsigned i = 0U; i < 3U; ++i) { + const int o = ordering[i]; + assert(o < 3); + + const int c = serd_node_compare(s->nodes[o], t->nodes[o]); + if (c) { + return c; + } + } + + return 0; +} + +/** + Compare statments with statement patterns lexicographically, ignoring graph. + + Null nodes in the second argument are treated as wildcards, always less than + any node. +*/ +int +serd_triple_compare_pattern(const void* const x, + const void* const y, + const void* const user_data) +{ + const int* const ordering = (const int*)user_data; + const SerdStatement* const s = (const SerdStatement*)x; + const SerdStatement* const t = (const SerdStatement*)y; + + for (unsigned i = 0U; i < 3U; ++i) { + const int o = ordering[i]; + assert(o < 3); + + const int c = serd_node_wildcard_compare(s->nodes[o], t->nodes[o]); + if (c) { + return c; + } + } + + return 0; +} + +/// Compare statements lexicographically +int +serd_quad_compare(const void* const x, + const void* const y, + const void* const user_data) +{ + const int* const ordering = (const int*)user_data; + const SerdStatement* const s = (const SerdStatement*)x; + const SerdStatement* const t = (const SerdStatement*)y; + + for (unsigned i = 0U; i < 4U; ++i) { + const int o = ordering[i]; + const int c = + (o == SERD_GRAPH) + ? serd_node_graph_compare(s->nodes[SERD_GRAPH], t->nodes[SERD_GRAPH]) + : serd_node_compare(s->nodes[o], t->nodes[o]); + + if (c) { + return c; + } + } + + return 0; +} + +/** + Compare statments with statement patterns lexicographically. + + Null nodes in the second argument are treated as wildcards, always less than + any node. +*/ +int +serd_quad_compare_pattern(const void* const x, + const void* const y, + const void* const user_data) +{ + const int* const ordering = (const int*)user_data; + const SerdStatement* const s = (const SerdStatement*)x; + const SerdStatement* const t = (const SerdStatement*)y; + + for (unsigned i = 0U; i < 4U; ++i) { + const int o = ordering[i]; + const int c = + (o == SERD_GRAPH) + ? serd_node_graph_compare(s->nodes[SERD_GRAPH], t->nodes[SERD_GRAPH]) + : serd_node_wildcard_compare(s->nodes[o], t->nodes[o]); + + if (c) { + return c; + } + } + + return 0; +} diff --git a/src/compare.h b/src/compare.h new file mode 100644 index 00000000..f4dbc046 --- /dev/null +++ b/src/compare.h @@ -0,0 +1,37 @@ +// Copyright 2011-2021 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: ISC + +#ifndef SERD_SRC_COMPARE_H +#define SERD_SRC_COMPARE_H + +#include "zix/attributes.h" + +/// Compare statements lexicographically, ignoring graph +ZIX_PURE_FUNC int +serd_triple_compare(const void* x, const void* y, const void* user_data); + +/** + Compare statments with statement patterns lexicographically, ignoring graph. + + Null nodes in the second argument are treated as wildcards, always less than + any node. +*/ +ZIX_PURE_FUNC int +serd_triple_compare_pattern(const void* x, + const void* y, + const void* user_data); + +/// Compare statements lexicographically +ZIX_PURE_FUNC int +serd_quad_compare(const void* x, const void* y, const void* user_data); + +/** + Compare statments with statement patterns lexicographically. + + Null nodes in the second argument are treated as wildcards, always less than + any node. +*/ +ZIX_PURE_FUNC int +serd_quad_compare_pattern(const void* x, const void* y, const void* user_data); + +#endif // ZIX_SRC_COMPARE_H diff --git a/src/cursor.c b/src/cursor.c new file mode 100644 index 00000000..d6a86d19 --- /dev/null +++ b/src/cursor.c @@ -0,0 +1,224 @@ +// Copyright 2011-2020 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: ISC + +#include "cursor.h" + +#include "memory.h" +#include "model.h" +#include "node.h" +#include "statement.h" + +#include "serd/log.h" +#include "serd/memory.h" +#include "serd/statement.h" +#include "zix/attributes.h" +#include "zix/btree.h" +#include "zix/status.h" + +#include <assert.h> +#include <stdbool.h> +#include <string.h> + +static inline bool +statement_matches_quad(const SerdStatement* const statement, + const SerdNode* const quad[4]) +{ + return serd_statement_matches(statement, quad[0], quad[1], quad[2], quad[3]); +} + +ZIX_PURE_FUNC bool +serd_iter_in_range(const ZixBTreeIter iter, + const SerdNode* const pattern[4], + const ScanStrategy strategy) +{ + const SerdStatement* const statement = + (const SerdStatement*)zix_btree_get(iter); + + for (unsigned i = 0U; i < strategy.n_prefix; ++i) { + const unsigned field = orderings[strategy.order][i]; + if (!serd_node_pattern_match(statement->nodes[field], pattern[field])) { + return false; + } + } + + return true; +} + +static bool +serd_cursor_in_range(const SerdCursor* const cursor) +{ + return (cursor->strategy.mode == FILTER_EVERYTHING || + serd_iter_in_range(cursor->iter, cursor->pattern, cursor->strategy)); +} + +/// Seek forward as necessary until the cursor points to a matching statement +static SerdStatus +serd_cursor_seek_match(SerdCursor* const cursor) +{ + assert(cursor->strategy.mode == FILTER_EVERYTHING || + cursor->strategy.mode == FILTER_RANGE); + + for (; !zix_btree_iter_is_end(cursor->iter); + zix_btree_iter_increment(&cursor->iter)) { + if (!serd_cursor_in_range(cursor)) { + // Went past the end of the matching range, reset to end + cursor->iter = zix_btree_end_iter; + return SERD_FAILURE; + } + + const SerdStatement* s = (const SerdStatement*)zix_btree_get(cursor->iter); + if (statement_matches_quad(s, cursor->pattern)) { + break; // Found matching statement + } + } + + return SERD_SUCCESS; +} + +static bool +check_version(const SerdCursor* const cursor) +{ + if (cursor->version != cursor->model->version) { + serd_logf(cursor->model->world, + SERD_LOG_LEVEL_ERROR, + "attempt to use invalidated cursor"); + return false; + } + + return true; +} + +SerdCursor +serd_cursor_make(const SerdModel* const model, + const ZixBTreeIter iter, + const SerdNode* const pattern[4], + const ScanStrategy strategy) +{ + SerdCursor cursor = {model, + {pattern[0], pattern[1], pattern[2], pattern[3]}, + model->version, + iter, + strategy}; + + if (strategy.mode == FILTER_RANGE || strategy.mode == FILTER_EVERYTHING) { + serd_cursor_seek_match(&cursor); + } + +#ifndef NDEBUG + if (!zix_btree_iter_is_end(cursor.iter)) { + // Check that the cursor is at a matching statement + const SerdStatement* first = + (const SerdStatement*)zix_btree_get(cursor.iter); + assert(statement_matches_quad(first, pattern)); + + // Check that any nodes in the pattern are interned + for (unsigned i = 0U; i < 3; ++i) { + assert(!cursor.pattern[i] || cursor.pattern[i] == first->nodes[i]); + } + } +#endif + + return cursor; +} + +SerdCursor* +serd_cursor_copy(SerdAllocator* const allocator, const SerdCursor* const cursor) +{ + if (!cursor) { + return NULL; + } + + SerdCursor* const copy = + (SerdCursor* const)serd_amalloc(allocator, sizeof(SerdCursor)); + + if (copy) { + memcpy(copy, cursor, sizeof(SerdCursor)); + } + + return copy; +} + +const SerdStatement* +serd_cursor_get(const SerdCursor* const cursor) +{ + return ( + (cursor && !zix_btree_iter_is_end(cursor->iter) && check_version(cursor)) + ? (const SerdStatement*)zix_btree_get(cursor->iter) + : NULL); +} + +SerdStatus +serd_cursor_scan_next(SerdCursor* const cursor) +{ + if (zix_btree_iter_is_end(cursor->iter) || !check_version(cursor)) { + return SERD_BAD_CURSOR; + } + + switch (cursor->strategy.mode) { + case SCAN_EVERYTHING: + break; + + case SCAN_RANGE: + if (!serd_cursor_in_range(cursor)) { + // Went past the end of the matching range + cursor->iter = zix_btree_end_iter; + return SERD_FAILURE; + } + break; + + case FILTER_EVERYTHING: + case FILTER_RANGE: + // Seek forward to next match + return serd_cursor_seek_match(cursor); + } + + return SERD_SUCCESS; +} + +SerdStatus +serd_cursor_advance(SerdCursor* const cursor) +{ + if (!cursor) { + return SERD_FAILURE; + } + + if (zix_btree_iter_is_end(cursor->iter) || !check_version(cursor)) { + return SERD_BAD_CURSOR; + } + + const ZixStatus zst = zix_btree_iter_increment(&cursor->iter); + if (zst) { + assert(zst == ZIX_STATUS_REACHED_END); + return SERD_FAILURE; + } + + return serd_cursor_scan_next(cursor); +} + +bool +serd_cursor_is_end(const SerdCursor* const cursor) +{ + return !cursor || zix_btree_iter_is_end(cursor->iter); +} + +bool +serd_cursor_equals(const SerdCursor* const lhs, const SerdCursor* const rhs) +{ + if (serd_cursor_is_end(lhs) || serd_cursor_is_end(rhs)) { + return serd_cursor_is_end(lhs) && serd_cursor_is_end(rhs); + } + + /* We don't bother checking if the patterns match each other here, or if both + cursors have the same ordering, since both of these must be true if the + BTree iterators are equal. */ + + return (zix_btree_iter_equals(lhs->iter, rhs->iter) && + lhs->strategy.mode == rhs->strategy.mode && + lhs->strategy.n_prefix == rhs->strategy.n_prefix); +} + +void +serd_cursor_free(SerdAllocator* const allocator, SerdCursor* const cursor) +{ + serd_afree(allocator, cursor); +} diff --git a/src/cursor.h b/src/cursor.h new file mode 100644 index 00000000..d7de7471 --- /dev/null +++ b/src/cursor.h @@ -0,0 +1,71 @@ +// Copyright 2011-2020 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: ISC + +#ifndef SERD_SRC_CURSOR_H +#define SERD_SRC_CURSOR_H + +#include "serd/cursor.h" +#include "serd/model.h" +#include "serd/node.h" +#include "serd/status.h" +#include "zix/btree.h" + +#include <stdbool.h> +#include <stddef.h> + +#define N_STATEMENT_ORDERS 12u + +/// An iteration mode that determines what to skip and when to end +typedef enum { + SCAN_EVERYTHING, ///< Iterate over entire store + SCAN_RANGE, ///< Iterate over range with equal prefix + FILTER_EVERYTHING, ///< Iterate to end of store, filtering + FILTER_RANGE, ///< Iterate over range with equal prefix, filtering +} ScanMode; + +/// A strategy for searching and iterating over a statement index +typedef struct { + ScanMode mode; ///< Iteration mode + unsigned n_prefix; ///< Number of prefix nodes that match the index + SerdStatementOrder order; ///< Order of index to scan +} ScanStrategy; + +struct SerdCursorImpl { + const SerdModel* model; ///< Model being iterated over + const SerdNode* pattern[4]; ///< Search pattern (nodes in model or null) + size_t version; ///< Model version when iterator was created + ZixBTreeIter iter; ///< Current position in index + ScanStrategy strategy; ///< Index scanning strategy +}; + +/// Lookup table of ordered indices for each SerdStatementOrder +static const unsigned orderings[N_STATEMENT_ORDERS][4] = { + {0U, 1U, 2U, 3U}, // SPOG + {0U, 2U, 1U, 3U}, // SOPG + {2U, 1U, 0U, 3U}, // OPSG + {2U, 0U, 1U, 3U}, // OPSG + {1U, 0U, 2U, 3U}, // PSOG + {1U, 2U, 0U, 3U}, // PSOG + {3U, 0U, 1U, 2U}, // GSPO + {3U, 0U, 2U, 1U}, // GSPO + {3U, 2U, 1U, 0U}, // GOPS + {3U, 2U, 0U, 1U}, // GOPS + {3U, 1U, 0U, 2U}, // GPSO + {3U, 1U, 2U, 0U} // GPSO +}; + +SerdCursor +serd_cursor_make(const SerdModel* model, + ZixBTreeIter iter, + const SerdNode* const pattern[4], + ScanStrategy strategy); + +SerdStatus +serd_cursor_scan_next(SerdCursor* cursor); + +bool +serd_iter_in_range(ZixBTreeIter iter, + const SerdNode* const pattern[4], + ScanStrategy strategy); + +#endif // SERD_SRC_CURSOR_H diff --git a/src/describe.c b/src/describe.c new file mode 100644 index 00000000..a7012cf9 --- /dev/null +++ b/src/describe.c @@ -0,0 +1,314 @@ +// Copyright 2011-2023 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: ISC + +#include "cursor.h" +#include "model.h" +#include "world.h" + +// Define the types used in the hash interface for more type safety +#define ZIX_HASH_KEY_TYPE const SerdNode +#define ZIX_HASH_RECORD_TYPE const SerdNode + +#include "serd/cursor.h" +#include "serd/describe.h" +#include "serd/memory.h" +#include "serd/model.h" +#include "serd/node.h" +#include "serd/sink.h" +#include "serd/statement.h" +#include "serd/status.h" +#include "serd/world.h" +#include "zix/allocator.h" +#include "zix/attributes.h" +#include "zix/digest.h" +#include "zix/hash.h" +#include "zix/status.h" + +#include <assert.h> +#include <stdbool.h> +#include <stddef.h> + +typedef enum { NAMED, ANON_S, ANON_O, LIST_S, LIST_O } NodeStyle; + +typedef struct { + SerdAllocator* allocator; // Allocator for auxiliary structures + const SerdModel* model; // Model to read from + const SerdSink* sink; // Sink to write description to + ZixHash* list_subjects; // Nodes written in the current list or null + SerdDescribeFlags flags; // Flags to control description +} DescribeContext; + +static SerdStatus +write_range_statement(const DescribeContext* ctx, + unsigned depth, + SerdStatementFlags statement_flags, + const SerdStatement* statement, + const SerdNode* last_subject, + bool write_types); + +static NodeStyle +get_node_style(const SerdModel* const model, const SerdNode* const node) +{ + if (serd_node_type(node) != SERD_BLANK) { + return NAMED; // Non-blank node can't be anonymous + } + + const size_t n_as_object = serd_model_count(model, NULL, NULL, node, NULL); + if (n_as_object > 1) { + return NAMED; // Blank node referred to several times + } + + if (serd_model_count(model, node, model->world->rdf_first, NULL, NULL) == 1 && + serd_model_count(model, node, model->world->rdf_rest, NULL, NULL) == 1 && + !serd_model_ask(model, NULL, model->world->rdf_rest, node, NULL)) { + return n_as_object == 0 ? LIST_S : LIST_O; + } + + return n_as_object == 0 ? ANON_S : ANON_O; +} + +static const SerdNode* +identity(const SerdNode* const record) +{ + return record; +} + +static ZixHashCode +ptr_hash(const SerdNode* const ptr) +{ + return zix_digest(0U, &ptr, sizeof(SerdNode*)); +} + +static bool +ptr_equals(const SerdNode* const a, const SerdNode* const b) +{ + return *(const void* const*)a == *(const void* const*)b; +} + +static SerdStatus +write_pretty_range(const DescribeContext* const ctx, + const unsigned depth, + SerdCursor* const range, + const SerdNode* last_subject, + bool write_types) +{ + SerdStatus st = SERD_SUCCESS; + const SerdStatement* statement = serd_cursor_get(range); + + while (statement) { + // Write this statement (and possibly more to describe anonymous nodes) + if ((st = write_range_statement( + ctx, depth, 0U, statement, last_subject, write_types))) { + break; + } + + // Update the last subject and advance the cursor + last_subject = serd_statement_subject(statement); + st = serd_cursor_advance(range); + statement = serd_cursor_get(range); + } + + return st > SERD_FAILURE ? st : SERD_SUCCESS; +} + +static SerdStatus +write_list(const DescribeContext* const ctx, + const unsigned depth, + SerdStatementFlags flags, + const SerdNode* node, + const SerdNode* const graph) +{ + const SerdModel* const model = ctx->model; + const SerdWorld* const world = model->world; + const SerdSink* const sink = ctx->sink; + const SerdNode* const rdf_first = world->rdf_first; + const SerdNode* const rdf_rest = world->rdf_rest; + const SerdNode* const rdf_nil = world->rdf_nil; + SerdStatus st = SERD_SUCCESS; + + const SerdStatement* fs = + serd_model_get_statement(model, node, rdf_first, NULL, graph); + + assert(fs); // Shouldn't get here if it doesn't at least have an rdf:first + + while (!st && !serd_node_equals(node, rdf_nil)) { + // Write rdf:first statement for this node + if ((st = write_range_statement(ctx, depth, flags, fs, NULL, false))) { + return st; + } + + // Get rdf:rest statement + const SerdStatement* const rs = + serd_model_get_statement(model, node, rdf_rest, NULL, graph); + + if (!rs) { + // Terminate malformed list with missing rdf:rest + return serd_sink_write(sink, 0, node, rdf_rest, rdf_nil, graph); + } + + // Terminate if the next node has no rdf:first + const SerdNode* const next = serd_statement_object(rs); + if (!(fs = serd_model_get_statement(model, next, rdf_first, NULL, graph))) { + return serd_sink_write(sink, 0, node, rdf_rest, rdf_nil, graph); + } + + // Write rdf:next statement and move to the next node + st = serd_sink_write_statement(sink, 0, rs); + node = next; + flags = 0U; + } + + return st; +} + +static bool +skip_range_statement(const SerdModel* const model, + const SerdStatement* const statement) +{ + const SerdNode* const subject = serd_statement_subject(statement); + const NodeStyle subject_style = get_node_style(model, subject); + const SerdNode* const predicate = serd_statement_predicate(statement); + + if (subject_style == ANON_O || subject_style == LIST_O) { + return true; // Skip subject that will be inlined elsewhere + } + + if (subject_style == LIST_S && + (serd_node_equals(predicate, model->world->rdf_first) || + serd_node_equals(predicate, model->world->rdf_rest))) { + return true; // Skip list statement that write_list will handle + } + + return false; +} + +static SerdStatus +write_subject_types(const DescribeContext* const ctx, + const unsigned depth, + const SerdNode* const subject, + const SerdNode* const graph) +{ + SerdCursor* const t = serd_model_find(ctx->allocator, + ctx->model, + subject, + ctx->model->world->rdf_type, + NULL, + graph); + + const SerdStatus st = + t ? write_pretty_range(ctx, depth + 1, t, subject, true) : SERD_SUCCESS; + + serd_cursor_free(ctx->allocator, t); + return st; +} + +static bool +types_first_for_subject(const DescribeContext* const ctx, const NodeStyle style) +{ + return style != LIST_S && !(ctx->flags & SERD_NO_TYPE_FIRST); +} + +static SerdStatus +write_range_statement(const DescribeContext* const ctx, + const unsigned depth, + SerdStatementFlags statement_flags, + const SerdStatement* ZIX_NONNULL statement, + const SerdNode* ZIX_NULLABLE last_subject, + const bool write_types) +{ + const SerdModel* const model = ctx->model; + const SerdSink* const sink = ctx->sink; + const SerdNode* const subject = serd_statement_subject(statement); + const NodeStyle subject_style = get_node_style(model, subject); + const SerdNode* const predicate = serd_statement_predicate(statement); + const SerdNode* const object = serd_statement_object(statement); + const NodeStyle object_style = get_node_style(model, object); + const SerdNode* const graph = serd_statement_graph(statement); + SerdStatus st = SERD_SUCCESS; + + if (depth == 0U) { + if (skip_range_statement(model, statement)) { + return SERD_SUCCESS; // Skip subject that will be inlined elsewhere + } + + if (subject_style == LIST_S) { + // First write inline list subject, which this statement will follow + if (zix_hash_insert(ctx->list_subjects, subject) != ZIX_STATUS_EXISTS) { + st = write_list(ctx, 2, statement_flags | SERD_LIST_S, subject, graph); + } + } + } + + if (st) { + return st; + } + + // If this is a new subject, write types first if necessary + const bool types_first = types_first_for_subject(ctx, subject_style); + if (subject != last_subject && types_first) { + st = write_subject_types(ctx, depth, subject, graph); + } + + // Skip type statement if it would be written another time (just above) + if (subject_style != LIST_S && !write_types && + serd_node_equals(predicate, model->world->rdf_type)) { + return st; + } + + // Set up the flags for this statement + statement_flags |= + (((subject_style == ANON_S) * (SerdStatementFlags)SERD_EMPTY_S) | + ((object_style == ANON_O) * (SerdStatementFlags)SERD_ANON_O) | + ((object_style == LIST_O) * (SerdStatementFlags)SERD_LIST_O)); + + // Finally write this statement + if ((st = serd_sink_write_statement(sink, statement_flags, statement))) { + return st; + } + + if (object_style == ANON_O) { + // Follow an anonymous object with its description like "[ ... ]" + SerdCursor* const iter = + serd_model_find(ctx->allocator, model, object, NULL, NULL, NULL); + + if (!(st = write_pretty_range(ctx, depth + 1, iter, last_subject, false))) { + st = serd_sink_write_end(sink, object); + } + + serd_cursor_free(ctx->allocator, iter); + + } else if (object_style == LIST_O) { + // Follow a list object with its description like "( ... )" + st = write_list(ctx, depth + 1, 0U, object, graph); + } + + return st; +} + +SerdStatus +serd_describe_range(SerdAllocator* const allocator, + const SerdCursor* const range, + const SerdSink* sink, + const SerdDescribeFlags flags) +{ + if (serd_cursor_is_end(range)) { + return SERD_SUCCESS; + } + + assert(sink); + + SerdCursor copy = *range; + + ZixHash* const list_subjects = + zix_hash_new((ZixAllocator*)allocator, identity, ptr_hash, ptr_equals); + + SerdStatus st = SERD_BAD_ALLOC; + if (list_subjects) { + DescribeContext ctx = {allocator, range->model, sink, list_subjects, flags}; + + st = write_pretty_range(&ctx, 0, ©, NULL, (flags & SERD_NO_TYPE_FIRST)); + } + + zix_hash_free(list_subjects); + return st; +} diff --git a/src/inserter.c b/src/inserter.c new file mode 100644 index 00000000..f3b8631b --- /dev/null +++ b/src/inserter.c @@ -0,0 +1,146 @@ +// Copyright 2011-2020 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: ISC + +#include "memory.h" +#include "model.h" +#include "statement.h" + +#include "serd/caret.h" +#include "serd/event.h" +#include "serd/inserter.h" +#include "serd/log.h" +#include "serd/model.h" +#include "serd/node.h" +#include "serd/nodes.h" +#include "serd/sink.h" +#include "serd/statement.h" +#include "serd/status.h" +#include "serd/uri.h" +#include "serd/world.h" + +#include <assert.h> +#include <stdbool.h> +#include <stdlib.h> + +typedef struct { + SerdModel* model; + SerdNode* default_graph; +} SerdInserterData; + +static bool +can_insert(SerdWorld* const world, const SerdNode* const node) +{ + if (node) { + switch (serd_node_type(node)) { + case SERD_LITERAL: + return can_insert(world, serd_node_datatype(node)); + + case SERD_URI: + if (!serd_uri_string_has_scheme(serd_node_string(node))) { + serd_logf(world, + SERD_LOG_LEVEL_ERROR, + "attempt to insert relative URI <%s> into model", + serd_node_string(node)); + return false; + } + break; + + case SERD_BLANK: + case SERD_VARIABLE: + break; + } + } + + return true; +} + +static SerdStatus +serd_inserter_on_statement(SerdInserterData* const data, + const SerdStatementFlags flags, + const SerdStatement* const statement) +{ + (void)flags; + + SerdModel* const model = data->model; + SerdWorld* const world = model->world; + + // Check that every node is expanded so it is context-free + for (unsigned i = 0; i < 4; ++i) { + if (!can_insert(world, serd_statement_node(statement, (SerdField)i))) { + return SERD_BAD_DATA; + } + } + + const SerdNode* const s = + serd_nodes_intern(model->nodes, serd_statement_subject(statement)); + + const SerdNode* const p = + serd_nodes_intern(model->nodes, serd_statement_predicate(statement)); + + const SerdNode* const o = + serd_nodes_intern(model->nodes, serd_statement_object(statement)); + + const SerdNode* const g = serd_nodes_intern( + model->nodes, + serd_statement_graph(statement) ? serd_statement_graph(statement) + : data->default_graph); + + const SerdCaret* const caret = + (data->model->flags & SERD_STORE_CARETS) ? statement->caret : NULL; + + const SerdStatus st = + serd_model_add_with_caret(data->model, s, p, o, g, caret); + + return st > SERD_FAILURE ? st : SERD_SUCCESS; +} + +static SerdStatus +serd_inserter_on_event(void* const handle, const SerdEvent* const event) +{ + SerdInserterData* const data = (SerdInserterData*)handle; + + if (event->type == SERD_STATEMENT) { + return serd_inserter_on_statement( + data, event->statement.flags, event->statement.statement); + } + + return SERD_SUCCESS; +} + +static SerdInserterData* +serd_inserter_data_new(SerdModel* const model, + const SerdNode* const default_graph) +{ + SerdInserterData* const data = + (SerdInserterData*)serd_wcalloc(model->world, 1, sizeof(SerdInserterData)); + + if (data) { + data->model = model; + data->default_graph = serd_node_copy(model->allocator, default_graph); + } + + return data; +} + +static void +serd_inserter_data_free(void* const ptr) +{ + SerdInserterData* const data = (SerdInserterData*)ptr; + serd_node_free(data->model->allocator, data->default_graph); + serd_wfree(data->model->world, data); +} + +SerdSink* +serd_inserter_new(SerdModel* const model, const SerdNode* const default_graph) +{ + assert(model); + + SerdEventFunc func = serd_inserter_on_event; + SerdInserterData* const data = serd_inserter_data_new(model, default_graph); + + return data ? serd_sink_new(serd_world_allocator(model->world), + data, + func, + (SerdFreeFunc)serd_inserter_data_free) + : NULL; +} diff --git a/src/model.c b/src/model.c new file mode 100644 index 00000000..89b37d83 --- /dev/null +++ b/src/model.c @@ -0,0 +1,820 @@ +// Copyright 2011-2020 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: ISC + +#include "model.h" + +#include "caret.h" +#include "compare.h" +#include "cursor.h" +#include "memory.h" +#include "statement.h" + +#include "serd/caret.h" +#include "serd/log.h" +#include "serd/node.h" +#include "serd/statement.h" +#include "serd/status.h" +#include "zix/allocator.h" +#include "zix/btree.h" +#include "zix/status.h" + +#include <assert.h> +#include <stdbool.h> +#include <stddef.h> + +static const SerdNode* const everything_pattern[4] = {NULL, NULL, NULL, NULL}; + +/// A 3-bit signature for a triple pattern used as a table index +typedef enum { + SERD_SIGNATURE_XXX, // 000 (???) + SERD_SIGNATURE_XXO, // 001 (??O) + SERD_SIGNATURE_XPX, // 010 (?P?) + SERD_SIGNATURE_XPO, // 011 (?PO) + SERD_SIGNATURE_SXX, // 100 (S??) + SERD_SIGNATURE_SXO, // 101 (S?O) + SERD_SIGNATURE_SPX, // 110 (SP?) + SERD_SIGNATURE_SPO, // 111 (SPO) +} SerdPatternSignature; + +static SerdPatternSignature +serd_model_pattern_signature(const bool with_s, + const bool with_p, + const bool with_o) +{ + return (SerdPatternSignature)(((with_s ? 1U : 0U) << 2U) + + ((with_p ? 1U : 0U) << 1U) + + ((with_o ? 1U : 0U))); +} + +static ZixBTreeCompareFunc +serd_model_index_comparator(const SerdModel* const model, + const SerdStatementOrder order) +{ + return (order < SERD_ORDER_GSPO && !(model->flags & SERD_STORE_GRAPHS)) + ? serd_triple_compare + : serd_quad_compare; +} + +static ZixBTreeCompareFunc +serd_model_pattern_comparator(const SerdModel* const model, + const SerdStatementOrder order) +{ + return (order < SERD_ORDER_GSPO && !(model->flags & SERD_STORE_GRAPHS)) + ? serd_triple_compare_pattern + : serd_quad_compare_pattern; +} + +SerdStatus +serd_model_add_index(SerdModel* const model, const SerdStatementOrder order) +{ + assert(model); + + if (model->indices[order]) { + return SERD_FAILURE; + } + + const unsigned* const ordering = orderings[order]; + const ZixBTreeCompareFunc comparator = + serd_model_index_comparator(model, order); + + model->indices[order] = + zix_btree_new((ZixAllocator*)model->allocator, comparator, ordering); + + if (!model->indices[order]) { + return SERD_BAD_ALLOC; + } + + // Insert statements from the default index + ZixStatus zst = ZIX_STATUS_SUCCESS; + if (order != model->default_order) { + ZixBTree* const default_index = model->indices[model->default_order]; + for (ZixBTreeIter i = zix_btree_begin(default_index); + !zst && !zix_btree_iter_is_end(i); + zix_btree_iter_increment(&i)) { + zst = zix_btree_insert(model->indices[order], zix_btree_get(i)); + } + } + + return zst ? serd_model_drop_index(model, order) : SERD_SUCCESS; +} + +SerdStatus +serd_model_drop_index(SerdModel* const model, const SerdStatementOrder order) +{ + assert(model); + + if (!model->indices[order]) { + return SERD_FAILURE; + } + + if (order == model->default_order) { + return SERD_BAD_CALL; + } + + zix_btree_free(model->indices[order], NULL, NULL); + model->indices[order] = NULL; + return SERD_SUCCESS; +} + +static SerdModel* +serd_model_new_with_allocator(SerdAllocator* const allocator, + SerdWorld* const world, + const SerdStatementOrder default_order, + const SerdModelFlags flags) +{ + assert(world); + + SerdNodes* const nodes = serd_nodes_new(allocator); + if (!nodes) { + return NULL; + } + + SerdModel* model = + (SerdModel*)serd_acalloc(allocator, 1, sizeof(struct SerdModelImpl)); + + if (!model) { + serd_nodes_free(nodes); + return NULL; + } + + model->allocator = allocator; + model->world = world; + model->nodes = nodes; + model->default_order = default_order; + model->flags = flags; + + if (serd_model_add_index(model, default_order)) { + serd_nodes_free(nodes); + serd_wfree(world, model); + return NULL; + } + + const ScanStrategy end_strategy = {SCAN_EVERYTHING, 0U, default_order}; + + model->end = serd_cursor_make(model, + zix_btree_end(model->indices[default_order]), + everything_pattern, + end_strategy); + + return model; +} + +SerdModel* +serd_model_new(SerdWorld* const world, + const SerdStatementOrder default_order, + const SerdModelFlags flags) +{ + return serd_model_new_with_allocator( + serd_world_allocator(world), world, default_order, flags); +} + +static void +log_bad_index(const SerdModel* const model, + const char* const description, + const SerdStatementOrder index_order, + const bool s, + const bool p, + const bool o, + const bool g) +{ + static const char* const order_strings[] = { + "S P O G", + "S O P G", + "O P S G", + "O S P G", + "P S O G", + "P O S G", + "G S P O", + "G S O P", + "G O P S", + "G O S P", + "G P S O", + "G P O S", + }; + + serd_logf(model->world, + SERD_LOG_LEVEL_WARNING, + "%s index (%s) for (%s %s %s%s) query", + description, + order_strings[index_order], + s ? "S" : "?", + p ? "P" : "?", + o ? "O" : "?", + (model->flags & SERD_STORE_GRAPHS) ? g ? " G" : " ?" : ""); +} + +static SerdCursor +make_begin_cursor(const SerdModel* const model, const SerdStatementOrder order) +{ + if (!model->indices[order]) { + log_bad_index(model, "missing", order, false, false, false, false); + return model->end; + } + + const ScanStrategy strategy = {SCAN_EVERYTHING, 0U, order}; + + return serd_cursor_make(model, + zix_btree_begin(model->indices[order]), + everything_pattern, + strategy); +} + +SerdModel* +serd_model_copy(SerdAllocator* const allocator, const SerdModel* const model) +{ + assert(model); + + SerdModel* copy = serd_model_new_with_allocator( + allocator, model->world, model->default_order, model->flags); + + SerdCursor cursor = make_begin_cursor(model, model->default_order); + serd_model_insert_statements(copy, &cursor); + + assert(serd_model_size(model) == serd_model_size(copy)); + assert(serd_nodes_size(model->nodes) == serd_nodes_size(copy->nodes)); + return copy; +} + +bool +serd_model_equals(const SerdModel* const a, const SerdModel* const b) +{ + if (!a && !b) { + return true; + } + + if (!a || !b || serd_model_size(a) != serd_model_size(b)) { + return false; + } + + SerdCursor ia = make_begin_cursor(a, a->default_order); + SerdCursor ib = make_begin_cursor(b, b->default_order); + + while (!serd_cursor_is_end(&ia) && !serd_cursor_is_end(&ib)) { + if (!serd_statement_equals(serd_cursor_get(&ia), serd_cursor_get(&ib)) || + serd_cursor_advance(&ia) > SERD_FAILURE || + serd_cursor_advance(&ib) > SERD_FAILURE) { + return false; + } + } + + return serd_cursor_is_end(&ia) && serd_cursor_is_end(&ib); +} + +static void +serd_model_drop_statement(SerdModel* const model, + SerdStatement* const statement) +{ + assert(statement); + + for (unsigned i = 0U; i < 4; ++i) { + if (statement->nodes[i]) { + serd_nodes_deref(model->nodes, statement->nodes[i]); + } + } + + if (statement->caret && serd_caret_document(statement->caret)) { + serd_nodes_deref(model->nodes, serd_caret_document(statement->caret)); + } + + serd_statement_free(model->allocator, statement); +} + +typedef struct { + SerdAllocator* allocator; +} DestroyContext; + +static void +destroy_tree_statement(void* ptr, const void* user_data) +{ + const DestroyContext* const ctx = (const DestroyContext*)user_data; + + serd_statement_free(ctx->allocator, (SerdStatement*)ptr); +} + +void +serd_model_free(SerdModel* const model) +{ + if (!model) { + return; + } + + // Free all statements (which are owned by the default index) + ZixBTree* const default_index = model->indices[model->default_order]; + const DestroyContext ctx = {model->allocator}; + zix_btree_clear(default_index, destroy_tree_statement, &ctx); + + // Free indices themselves + for (unsigned i = 0U; i < N_STATEMENT_ORDERS; ++i) { + zix_btree_free(model->indices[i], NULL, NULL); + } + + serd_nodes_free(model->nodes); + serd_wfree(model->world, model); +} + +SerdWorld* +serd_model_world(SerdModel* const model) +{ + assert(model); + return model->world; +} + +const SerdNodes* +serd_model_nodes(const SerdModel* const model) +{ + assert(model); + return model->nodes; +} + +SerdStatementOrder +serd_model_default_order(const SerdModel* const model) +{ + assert(model); + return model->default_order; +} + +SerdModelFlags +serd_model_flags(const SerdModel* const model) +{ + assert(model); + return model->flags; +} + +size_t +serd_model_size(const SerdModel* const model) +{ + assert(model); + return zix_btree_size(model->indices[model->default_order]); +} + +bool +serd_model_empty(const SerdModel* const model) +{ + assert(model); + return serd_model_size(model) == 0; +} + +SerdCursor* +serd_model_begin_ordered(SerdAllocator* const allocator, + const SerdModel* const model, + const SerdStatementOrder order) +{ + assert(model); + + const SerdCursor cursor = make_begin_cursor(model, order); + + return serd_cursor_copy(allocator, &cursor); +} + +SerdCursor* +serd_model_begin(SerdAllocator* const allocator, const SerdModel* const model) +{ + assert(model); + return serd_model_begin_ordered(allocator, model, model->default_order); +} + +const SerdCursor* +serd_model_end(const SerdModel* const model) +{ + assert(model); + return &model->end; +} + +static SerdStatementOrder +simple_order(const SerdStatementOrder order) +{ + return order < SERD_ORDER_GSPO ? order : (SerdStatementOrder)(order - 6U); +} + +/// Return the best index scanning strategy for a pattern with given nodes +static ScanStrategy +serd_model_strategy(const SerdModel* const model, + const bool with_s, + const bool with_p, + const bool with_o, + const bool with_g) +{ +#define N_STRATEGY_TIERS 4U + + const SerdStatementOrder default_order = simple_order(model->default_order); + const ScanStrategy none = {SCAN_EVERYTHING, 0U, default_order}; + + const ScanStrategy strategies[N_STRATEGY_TIERS][8] = { + // Preferred perfect strategies: SPO, SOP, OPS, PSO + { + none, + {SCAN_RANGE, 1U, SERD_ORDER_OPS}, // ??O + {SCAN_RANGE, 1U, SERD_ORDER_PSO}, // ?P? + {SCAN_RANGE, 2U, SERD_ORDER_OPS}, // ?PO + {SCAN_RANGE, 1U, SERD_ORDER_SPO}, // S?? + {SCAN_RANGE, 2U, SERD_ORDER_SOP}, // S?O + {SCAN_RANGE, 2U, SERD_ORDER_SPO}, // SP? + {SCAN_RANGE, 3U, default_order} // SPO + }, + + // Alternate perfect strategies: adds OSP and POS + { + none, // ??? + {SCAN_RANGE, 1U, SERD_ORDER_OSP}, // ??O + {SCAN_RANGE, 1U, SERD_ORDER_POS}, // ?P? + {SCAN_RANGE, 2U, SERD_ORDER_POS}, // ?PO + {SCAN_RANGE, 1U, SERD_ORDER_SOP}, // S?? + {SCAN_RANGE, 2U, SERD_ORDER_OSP}, // S?O + {SCAN_RANGE, 2U, SERD_ORDER_PSO}, // SP? + none // SPO + }, + + // Preferred partial prefix strategies + { + none, // ??? + none, // ??O + none, // ?P? + {FILTER_RANGE, 1U, SERD_ORDER_PSO}, // ?PO + none, // S?? + {FILTER_RANGE, 1U, SERD_ORDER_SPO}, // S?O + {FILTER_RANGE, 1U, SERD_ORDER_SOP}, // SP? + none // SPO + }, + + // Alternate partial prefix strategies + { + none, // ??? + none, // ??O + none, // ?P? + {FILTER_RANGE, 1U, SERD_ORDER_OSP}, // ?PO + none, // S?? + {FILTER_RANGE, 1U, SERD_ORDER_OPS}, // S?O + {FILTER_RANGE, 1U, SERD_ORDER_POS}, // SP? + none // SPO + }, + }; + + // Construct a 3-bit signature for this triple pattern + const SerdPatternSignature sig = + serd_model_pattern_signature(with_s, with_p, with_o); + if (!sig && !with_g) { + return none; + } + + // Try to find a strategy we can support, from most to least preferred + for (unsigned t = 0U; t < N_STRATEGY_TIERS; ++t) { + ScanStrategy strategy = strategies[t][sig]; + const SerdStatementOrder triple_order = strategy.order; + + assert(strategy.order < SERD_ORDER_GSPO); + + if (strategy.n_prefix > 0) { + if (with_g) { + const SerdStatementOrder quad_order = + (SerdStatementOrder)(triple_order + 6U); + + if (model->indices[quad_order]) { + strategy.order = quad_order; + ++strategy.n_prefix; + return strategy; + } + } + + if (model->indices[triple_order]) { + return strategy; + } + } + } + + // Indices don't help with the triple, try to at least stay in the graph + if (with_g) { + for (unsigned i = SERD_ORDER_GSPO; i <= SERD_ORDER_GPOS; ++i) { + if (model->indices[i]) { + const ScanMode mode = sig == 0x000 ? SCAN_RANGE : FILTER_RANGE; + const ScanStrategy strategy = {mode, 1U, (SerdStatementOrder)i}; + + return strategy; + } + } + } + + // All is lost, regress to linear search + const ScanStrategy linear = {FILTER_EVERYTHING, 0U, model->default_order}; + return linear; +} + +static SerdCursor +serd_model_search(const SerdModel* const model, + const SerdNode* const s, + const SerdNode* const p, + const SerdNode* const o, + const SerdNode* const g) +{ + // Build a pattern of interned nodes + const SerdNode* pattern[4] = {serd_nodes_existing(model->nodes, s), + serd_nodes_existing(model->nodes, p), + serd_nodes_existing(model->nodes, o), + serd_nodes_existing(model->nodes, g)}; + + // If some node isn't in the model at all, no need to search for statements + const int n_given = !!s + !!p + !!o + !!g; + if (n_given != (!!pattern[0] + !!pattern[1] + !!pattern[2] + !!pattern[3])) { + return model->end; + } + + // Determine the best available search strategy + const ScanStrategy strategy = serd_model_strategy(model, s, p, o, g); + const SerdStatementOrder order = strategy.order; + ZixBTree* const index = model->indices[order]; + + // Issue any performance warnings, and return early for degenerate cases + switch (strategy.mode) { + case SCAN_EVERYTHING: + return make_begin_cursor(model, order); + case SCAN_RANGE: + break; + case FILTER_EVERYTHING: + log_bad_index(model, "using effectively linear", order, s, p, o, g); + return serd_cursor_make(model, zix_btree_begin(index), pattern, strategy); + case FILTER_RANGE: + log_bad_index(model, "filtering partial", order, s, p, o, g); + break; + } + + // Find the first statement matching the pattern in the index + ZixBTreeIter iter = zix_btree_end_iter; + zix_btree_lower_bound(index, + serd_model_pattern_comparator(model, order), + orderings[order], + pattern, + &iter); + + return (!zix_btree_iter_is_end(iter) && + serd_iter_in_range(iter, pattern, strategy)) + ? serd_cursor_make(model, iter, pattern, strategy) + : model->end; +} + +SerdCursor* +serd_model_find(SerdAllocator* const allocator, + const SerdModel* const model, + const SerdNode* const s, + const SerdNode* const p, + const SerdNode* const o, + const SerdNode* const g) +{ + assert(model); + + const SerdCursor cursor = serd_model_search(model, s, p, o, g); + + return zix_btree_iter_is_end(cursor.iter) + ? NULL + : serd_cursor_copy(allocator, &cursor); +} + +const SerdNode* +serd_model_get(const SerdModel* const model, + const SerdNode* const s, + const SerdNode* const p, + const SerdNode* const o, + const SerdNode* const g) +{ + assert(model); + + const SerdStatement* const statement = + serd_model_get_statement(model, s, p, o, g); + + return !statement ? NULL + : !s ? serd_statement_subject(statement) + : !p ? serd_statement_predicate(statement) + : !o ? serd_statement_object(statement) + : !g ? serd_statement_graph(statement) + : NULL; +} + +const SerdStatement* +serd_model_get_statement(const SerdModel* const model, + const SerdNode* const s, + const SerdNode* const p, + const SerdNode* const o, + const SerdNode* const g) +{ + assert(model); + + if ((bool)s + (bool)p + (bool)o != 2 && + (bool)s + (bool)p + (bool)o + (bool)g != 3) { + return NULL; + } + + const SerdCursor i = serd_model_search(model, s, p, o, g); + + return serd_cursor_get(&i); +} + +size_t +serd_model_count(const SerdModel* const model, + const SerdNode* const s, + const SerdNode* const p, + const SerdNode* const o, + const SerdNode* const g) +{ + assert(model); + + SerdCursor i = serd_model_search(model, s, p, o, g); + size_t count = 0; + + for (; !serd_cursor_is_end(&i); serd_cursor_advance(&i)) { + ++count; + } + + return count; +} + +bool +serd_model_ask(const SerdModel* const model, + const SerdNode* const s, + const SerdNode* const p, + const SerdNode* const o, + const SerdNode* const g) +{ + assert(model); + + const SerdCursor c = serd_model_search(model, s, p, o, g); + + return !serd_cursor_is_end(&c); +} + +static SerdCaret* +serd_model_intern_caret(SerdModel* const model, const SerdCaret* const caret) +{ + if (!caret) { + return NULL; + } + + SerdCaret* const copy = + (SerdCaret*)serd_acalloc(model->allocator, 1, sizeof(SerdCaret)); + + if (copy) { + copy->document = serd_nodes_intern(model->nodes, caret->document); + copy->line = caret->line; + copy->col = caret->col; + } + + return copy; +} + +SerdStatus +serd_model_add_with_caret(SerdModel* const model, + const SerdNode* const s, + const SerdNode* const p, + const SerdNode* const o, + const SerdNode* const g, + const SerdCaret* const caret) +{ + assert(model); + + SerdStatement* const statement = + serd_statement_new(model->allocator, s, p, o, g, NULL); + + if (!statement) { + return SERD_BAD_ALLOC; + } + + statement->caret = serd_model_intern_caret(model, caret); + + bool added = false; + for (unsigned i = 0U; i < N_STATEMENT_ORDERS; ++i) { + if (model->indices[i]) { + if (!zix_btree_insert(model->indices[i], statement)) { + added = true; + } else if ((SerdStatementOrder)i == model->default_order) { + break; // Statement already indexed + } + } + } + + ++model->version; + if (added) { + return SERD_SUCCESS; + } + + serd_model_drop_statement(model, statement); + return SERD_FAILURE; +} + +SerdStatus +serd_model_add(SerdModel* const model, + const SerdNode* const s, + const SerdNode* const p, + const SerdNode* const o, + const SerdNode* const g) +{ + return serd_model_add_with_caret(model, + serd_nodes_intern(model->nodes, s), + serd_nodes_intern(model->nodes, p), + serd_nodes_intern(model->nodes, o), + serd_nodes_intern(model->nodes, g), + NULL); +} + +SerdStatus +serd_model_insert(SerdModel* const model, const SerdStatement* const statement) +{ + SerdNodes* const nodes = model->nodes; + + return serd_model_add_with_caret( + model, + serd_nodes_intern(nodes, serd_statement_subject(statement)), + serd_nodes_intern(nodes, serd_statement_predicate(statement)), + serd_nodes_intern(nodes, serd_statement_object(statement)), + serd_nodes_intern(nodes, serd_statement_graph(statement)), + serd_statement_caret(statement)); +} + +SerdStatus +serd_model_insert_statements(SerdModel* const model, SerdCursor* const range) +{ + const SerdStatement* statement = serd_cursor_get(range); + SerdStatus st = SERD_SUCCESS; + + while (!st && statement) { + if (!(st = serd_model_insert(model, statement))) { + st = serd_cursor_advance(range); + } + + statement = serd_cursor_get(range); + } + + return st; +} + +SerdStatus +serd_model_erase(SerdModel* const model, SerdCursor* const cursor) +{ + assert(model); + assert(cursor); + + const SerdStatement* statement = serd_cursor_get(cursor); + SerdStatement* removed = NULL; + ZixStatus zst = ZIX_STATUS_SUCCESS; + + if (!statement) { + return SERD_FAILURE; + } + + // Erase from the index associated with this cursor + zst = zix_btree_remove(model->indices[cursor->strategy.order], + statement, + (void**)&removed, + &cursor->iter); + + // No error should be possible with a valid cursor + assert(!zst); + assert(removed); + assert(removed == statement); + + for (unsigned i = SERD_ORDER_SPO; i <= SERD_ORDER_GPOS; ++i) { + ZixBTree* index = model->indices[i]; + + if (index && i != (unsigned)cursor->strategy.order) { + SerdStatement* ignored = NULL; + ZixBTreeIter next = zix_btree_end_iter; + zst = zix_btree_remove(index, statement, (void**)&ignored, &next); + + assert(zst == ZIX_STATUS_SUCCESS || zst == ZIX_STATUS_NOT_FOUND); + assert(!ignored || ignored == removed); + } + } + serd_cursor_scan_next(cursor); + + serd_model_drop_statement(model, removed); + cursor->version = ++model->version; + + (void)zst; + return SERD_SUCCESS; +} + +SerdStatus +serd_model_erase_statements(SerdModel* const model, SerdCursor* const range) +{ + assert(model); + assert(range); + + SerdStatus st = SERD_SUCCESS; + + while (!st && !serd_cursor_is_end(range)) { + st = serd_model_erase(model, range); + } + + return st; +} + +SerdStatus +serd_model_clear(SerdModel* const model) +{ + assert(model); + + SerdStatus st = SERD_SUCCESS; + SerdCursor i = make_begin_cursor(model, model->default_order); + + while (!st && !serd_cursor_is_end(&i)) { + st = serd_model_erase(model, &i); + } + + return st; +} diff --git a/src/model.h b/src/model.h new file mode 100644 index 00000000..a67ba087 --- /dev/null +++ b/src/model.h @@ -0,0 +1,29 @@ +// Copyright 2011-2020 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: ISC + +#ifndef SERD_SRC_MODEL_H +#define SERD_SRC_MODEL_H + +#include "cursor.h" + +#include "serd/cursor.h" +#include "serd/memory.h" +#include "serd/model.h" +#include "serd/nodes.h" +#include "serd/world.h" +#include "zix/btree.h" + +#include <stddef.h> + +struct SerdModelImpl { + SerdAllocator* allocator; ///< Allocator for everything in this model + SerdWorld* world; ///< World this model is a part of + SerdNodes* nodes; ///< Interned nodes in this model + ZixBTree* indices[12]; ///< Trees of SerdStatement pointers + SerdCursor end; ///< End cursor (always the same) + size_t version; ///< Version incremented on every change + SerdStatementOrder default_order; ///< Order for main statement-owning index + SerdModelFlags flags; ///< Active indices and features +}; + +#endif // SERD_SRC_MODEL_H diff --git a/src/reader.c b/src/reader.c index b7a64ce4..5ce81962 100644 --- a/src/reader.c +++ b/src/reader.c @@ -89,7 +89,7 @@ tolerate_status(const SerdReader* const reader, const SerdStatus status) if (status == SERD_BAD_STREAM || status == SERD_BAD_STACK || status == SERD_BAD_WRITE || status == SERD_NO_DATA || - status == SERD_BAD_CALL) { + status == SERD_BAD_CALL || status == SERD_BAD_CURSOR) { return false; } diff --git a/src/string.c b/src/string.c index b8227100..ac964705 100644 --- a/src/string.c +++ b/src/string.c @@ -64,6 +64,10 @@ serd_strerror(const SerdStatus status) return "Invalid literal"; case SERD_BAD_PATTERN: return "Invalid statement pattern"; + case SERD_BAD_CURSOR: + return "Invalid cursor"; + case SERD_BAD_INDEX: + return "No suitable model index available"; } return "Unknown error"; |