8000 feat: prepared queries with JSON bindings (minigraf#181) by adityamukho · Pull Request #11 · project-minigraf/minigraf-c · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ crate-type = ["cdylib", "staticlib"]
[dependencies]
# Unreleased v3 core (format v8). At the v3.0.0 release this goes back to an exact
# crates.io pin (minigraf = "=3.0.0"), which the release workflow updates.
minigraf = { git = "https://github.com/project-minigraf/minigraf", rev = "949813ce27273d0c2820690df844e76951cb7996" }
minigraf = { git = "https://github.com/project-minigraf/minigraf", rev = "5f2ba46e0c5a9954d90e52f60daaad2128d91973" }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

Expand Down
27 changes: 24 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,11 @@ Compile, from the unpacked archive:
- Strings returned by `minigraf_execute`, `minigraf_cursor_vars`,
`minigraf_cursor_next_batch` and `minigraf_fact_log_next_batch`, and messages stored
in an `error_out`, must be freed with `minigraf_string_free`.
- Databases must be closed with `minigraf_close`; cursors, fact logs and log writers
must be freed with `minigraf_cursor_free`, `minigraf_fact_log_free` and
`minigraf_log_writer_free`.
- Databases must be closed with `minigraf_close`; cursors, fact logs, log writers and
prepared queries must be freed with `minigraf_cursor_free`, `minigraf_fact_log_free`,
`minigraf_log_writer_free` and `minigraf_prepared_free`. `minigraf_close` releases
the database's prepared queries (later calls fail with `API-018`), but each must
still be freed.
- Strings returned by the `*_last_error` functions belong to the object and are valid
until its next call. Every error message starts with its code, such as `[API-015]`.
- Passing `NULL` to any function is safe (no-op or returns NULL/error).
Expand Down Expand Up @@ -79,6 +81,11 @@ Compile, from the unpacked archive:
| `minigraf_log_writer_finish(w)` | Commit and rename the file into place; later calls fail with `API-018` |
| `minigraf_log_writer_last_error(w)` | The error of the writer's last call |
| `minigraf_log_writer_free(w)` | Free the writer; an unfinished build is abandoned and leaves no file |
| `minigraf_prepare(db, datalog)` | Parse a query with `$slots` once; NULL on error (`minigraf_last_error`: `API-005`–`007` for a non-query, `API-023` for an attribute slot) |
| `minigraf_prepared_execute(pq, bindings_json)` | Run it with `bindings_json` (see below; NULL = `{}`) and return JSON like `execute`; NULL on error (`minigraf_prepared_last_error`) |
| `minigraf_prepared_query(pq, bindings_json)` | Run it and return a cursor |
| `minigraf_prepared_last_error(pq)` | The error of its last call: `API-017` (bad bindings), `API-022` (missing slot), `API-024` (wrong kind), `API-018` (database closed) |
| `minigraf_prepared_free(pq)` | Free a prepared query, also after `minigraf_close` |

A fact record is `{"entity": "<uuid>", "attribute": ":a/b", "value": {"type": "string", "value": "x"},
"tx_count": 1, "tx_id": 1700000000000, "valid_from": 1700000000000, "valid_to": 9223372036854775807,
Expand All @@ -87,6 +94,20 @@ A fact record is `{"entity": "<uuid>", "attribute": ":a/b", "value": {"type": "s
9223372036854775807 means forever. The batches `minigraf_fact_log_next_batch` returns can be passed
straight to `minigraf_log_writer_append_batch`; see `tests/c/etl_smoke.c`.

Bindings are a JSON object of slot name (without the `$`) to bind value: `true`/`false`, a
number (an integer when it has no fraction or exponent, so `2` is an integer and `2.0` a float),
a string, `null`, or a typed object: a value type above (`{"type": "keyword", "value": ":k"}`),
`{"type": "entity", "value": "<uuid>"}`, `{"type": "txCount", "value": 5}` or
`{"type": "timestamp", "value": 1700000000000}` for `:as-of`, and `{"type": "anyValidTime"}`
for `:valid-at`.

```c
MiniGrafPrepared *pq = minigraf_prepare(db, "(query [:find ?n :where [?e :age $age] [?e :name ?n]])");
char *thirty = minigraf_prepared_execute(pq, "{\"age\": 30}");
minigraf_string_free(thirty);
minigraf_prepared_free(pq);
```

## Building from source

```bash
Expand Down
14 changes: 14 additions & 0 deletions consumer-test/contract.c
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,21 @@ int main(int argc, char **argv) {
check(err != NULL && err[0] != '\0', "invalid Datalog sets minigraf_last_error");
if (err) printf(" %s\n", err);
minigraf_string_free(bad);

check(transact(mem, "(transact [[:bob :name \"Bob\"]])"), "transact Bob");
MiniGrafPrepared *pq = minigraf_prepare(mem, "(query [:find ?n :where [?e :name $name] [?e :name ?n]])");
check(pq != NULL, "prepare");
char *a = minigraf_prepared_execute(pq, "{\"name\": \"Alice\"}");
check(a != NULL && strstr(a, "[[\"Alice\"]]") != NULL, "prepared query finds Alice");
char *b = minigraf_prepared_execute(pq, "{\"name\": \"Bob\"}");
check(b != NULL && strstr(b, "[[\"Bob\"]]") != NULL, "prepared query finds Bob");
char *t = minigraf_prepared_execute(pq, "{\"name\": {\"type\": \"string\", \"value\": \"Alice\"}}");
check(t != NULL && strstr(t, "[[\"Alice\"]]") != NULL, "prepared query takes a typed value");
minigraf_string_free(a);
minigraf_string_free(b);
minigraf_string_free(t);
minigraf_close(mem);
minigraf_prepared_free(pq);

char path[4096];
snprintf(path, sizeof path, "%s/test.graph", argv[1]);
Expand Down
36 changes: 36 additions & 0 deletions include/minigraf.h
8CB3
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ typedef struct MiniGrafFactLog MiniGrafFactLog;

typedef struct MiniGrafLogWriter MiniGrafLogWriter;

typedef struct MiniGrafPrepared MiniGrafPrepared;

// Open a file-backed Minigraf database. Returns NULL on error.
struct MiniGrafDb *minigraf_open(const char *path);

Expand Down Expand Up @@ -74,6 +76,40 @@ const char *minigraf_cursor_last_error(struct MiniGrafCursor *cursor);
// Close a cursor and free it.
void minigraf_cursor_free(struct MiniGrafCursor *cursor);

// Parse and validate a `(query ...)` once, for running many times with
// different bind values in its `$name` slots. Returns NULL on error (call
// `minigraf_last_error` on `handle`): API-005/006/007 for a transact, retract
// or rule, API-023 for a slot in an attribute position. Free it with
// `minigraf_prepared_free`, also after `minigraf_close`.
struct MiniGrafPrepared *minigraf_prepare(struct MiniGrafDb *handle, const char *datalog);

// Run a prepared query with `bindings_json`, a JSON object of slot name
// (without the `$`) to bind value; NULL means `{}`. A bind value is `true`/
// `false`, a number (an integer when it has no fraction or exponent, else a
// float), a string, `null`, or a typed object: `{"type": "integer" | "float" |
// "string" | "boolean" | "keyword" | "ref" | "null", "value": ...}`,
// `{"type": "entity", "value": uuid}`, `{"type": "txCount" | "timestamp",
// "value": n}` or `{"type": "anyValidTime"}`. Returns the result as JSON,
// encoded like `minigraf_execute`'s (free it with `minigraf_string_free`), or
// NULL on error (call `minigraf_prepared_last_error`): API-017 for bindings
// that are not bind values, API-022 for a missing slot, API-024 for a bind
// value of the wrong kind, API-018 after `minigraf_close` of its database.
char *minigraf_prepared_execute(struct MiniGrafPrepared *prepared, const char *bindings_json);

// Run a prepared query with `bindings_json` (as `minigraf_prepared_execute`)
// and return a cursor over its rows; its answer is fixed when it opens.
// Returns NULL on error (call `minigraf_prepared_last_error`). Free the cursor
// with `minigraf_cursor_free`.
struct MiniGrafCursor *minigraf_prepared_query(struct MiniGrafPrepared *prepared,
const char *bindings_json);

// The error of the last call on `prepared`, or NULL if it had none. Valid
// until the next call on it.
const char *minigraf_prepared_last_error(struct MiniGrafPrepared *prepared);

// Free a prepared query. Call it also after `minigraf_close` of its database.
void minigraf_prepared_free(struct MiniGrafPrepared *prepared);

// Stream every fact record that `filter_json` keeps: a JSON object with any of
// `attributes`, `attribute_prefixes`, `entities` (UUID strings), `tx_from` and
// `tx_to` (inclusive), `order` (`"tx"` or `"storage"`) and `window`; NULL keeps
Expand Down
Loading
Loading
0