8000 feat: set values (#316) by adityamukho · Pull Request #19 · project-minigraf/minigraf-java · 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 @@ -17,7 +17,7 @@ path = "src/uniffi_bindgen.rs"
[dependencies]
# Pin to the exact released version. The release workflow updates this line.
# Unreleased v3 core (format v8). Switches to minigraf = "=3.0.0" at the v3.0.0 release.
minigraf = { git = "https://github.com/project-minigraf/minigraf", rev = "5f2ba46e0c5a9954d90e52f60daaad2128d91973" }
minigraf = { git = "https://github.com/project-minigraf/minigraf", rev = "7c8c554096209ddc2bf1342b9a4eef3ef77c67c3" }
uniffi = { version = "0.31.1", features = ["cli"] }
thiserror = "2.0.18"
serde_json = "1.0.149"
Expand Down
14 changes: 14 additions & 0 deletions consumer-test/src/test/java/ContractTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import java.util.Set;
import org.junit.jupiter.api.Test;

class ContractTest {
Expand Down Expand Up @@ -53,6 +54,19 @@ void preparedQueryRunsWithPlainAndTypedValues() throws Exception {
}
}

@Test
void setValuesComeBackAndBind() throws Exception {
try (MiniGrafDb db = Minigraf.openInMemory()) {
db.execute("(transact [[:alice :tags #{1 \"x\"}]])");
String tags = db.execute("(query [:find ?t :where [?e :tags ?t]])");
assertTrue(tags.contains("[[[\"x\",1]]]"), "a set value comes back as an array");
MiniGrafPreparedQuery pq =
db.prepare("(query [:find ?t :where [?e :tags ?t] [(in? 1 $s)]])");
String found = Minigraf.execute(pq, Map.of("s", Set.of(1, 2)));
assertTrue(found.contains("[[[\"x\",1]]]"), "a set binds");
}
}

@Test
void invalidDatalogThrowsMiniGrafException() throws Exception {
try (MiniGrafDb db = Minigraf.openInMemory()) {
Expand Down
7 changes: 7 additions & 0 deletions java/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,13 @@ or `Minigraf.entity(uuid)`, `Minigraf.txCount(n)` / `Minigraf.timestamp(ms)` for
`IllegalArgumentException`. A missing slot is `MiniGrafException.Api` with `[API-022]`,
a slot in an attribute position `[API-023]`, and a value of the wrong kind `[API-024]`.

A `Set` binds a set value, for `in?` and the other set predicates or a set-valued
attribute: `pq.execute("ids" to setOf(1, 2, 3))`, or `Set.of(1, 2, 3)` from Java. Its
elements are the plain values above or a `MiniGrafValue` (`MiniGrafValue.Keyword(":role/admin")`),
and a `UUID` element is a ref. A set inside a set is `MiniGrafException.Api` with
`[API-021]`; another kind of element, or a `List`, throws `IllegalArgumentException`. Set
values come back as JSON arrays from `execute` and as `MiniGrafValue.Set` in fact records.

Errors are `MiniGrafException.Parse`, `.Query`, `.Storage`, `.Api` (a bad argument, a
closed object, a bind mistake) or `.Other`.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,10 @@ object Minigraf {
* [v] as a bind value. `Boolean`; `Byte`, `Short`, `Int` and `Long` as integers;
* `Float` and `Double` as floats; `String`; `null`; and `java.util.UUID` (an
* entity, which also fits value and rule-argument slots) bind as they are, as do
* a [MiniGrafValue] and a [MiniGrafBindValue]. Any other type is
* [IllegalArgumentException].
* a [MiniGrafValue] and a [MiniGrafBindValue]. A `Set` binds a set value
* (minigraf#316): its elements are any of those plain values or a [MiniGrafValue],
* and a `UUID` element is a ref. Any other type, or a set element that i 10BC8 s not a
* value, is [IllegalArgumentException].
*/
fun bindValue(name: String, v: Any?): MiniGrafBindValue = when (v) {
null -> MiniGrafBindValue.Value(MiniGrafValue.Null)
Expand All @@ -118,12 +120,22 @@ fun bindValue(name: String, v: Any?): MiniGrafBindValue = when (v) {
is Float, is Double -> MiniGrafBindValue.Value(MiniGrafValue.Float64((v as Number).toDouble()))
is String -> MiniGrafBindValue.Value(MiniGrafValue.Text(v))
is UUID -> MiniGrafBindValue.Entity(v.toString())
is kotlin.collections.Set<*> -> MiniGrafBindValue.Value(MiniGrafValue.Set(v.map { setElement(name, it) }))
else -> throw IllegalArgumentException(
"slot \$$name: cannot bind a ${v::class.java.name}; use Boolean, an integer or " +
"floating type, String, null, UUID, a MiniGrafValue or a MiniGrafBindValue",
"floating type, String, null, UUID, Set, a MiniGrafValue or a MiniGrafBindValue",
)
}

/** An element of a `Set` bind value. A set element is passed on, for core's API-021. */
private fun setElement(name: String, e: Any?): MiniGrafValue = when (e) {
is UUID -> MiniGrafValue.Ref(e.toString())
else -> when (val b = bindValue(name, e)) {
is MiniGrafBindValue.Value -> b.value
else -> throw IllegalArgumentException("slot \$$name: a set element must be a value")
}
}

private fun bindValues(bindings: Map<String, Any?>): Map<String, MiniGrafBindValue> =
bindings.mapValues { (name, v) -> bindValue(name, v) }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,4 +119,48 @@ class PreparedTest {
db.execute("(transact [[:d :n 4]])")
assertEquals(1, rows(pq.execute("n" to 4)).size)
}

// ─── Sets (minigraf#316) ────────────────────────────────────────────────

@Test
fun kotlinSetsBindSetValues() {
val pq = db().prepare("(query [:find ?e :where [?e :n ?n] [(in? ?n \$s)]])")
assertEquals(2, rows(pq.execute("s" to setOf(1, 3))).size)
assertEquals(1, rows(pq.execute("s" to setOf(2L, 2L))).size)
assertEquals(0, rows(pq.execute("s" to emptySet<Int>())).size)
val typed = MiniGrafValue.Set(listOf(MiniGrafValue.Int64(1), MiniGrafValue.Int64(1)))
assertEquals(1, rows(pq.execute("s" to typed)).size)
}

@Test
fun setValuesComeBackTypedAndAsArrays() {
val db = db()
db.execute("(transact [[:a :tags #{1 :k \"x\"}]])")
val equal = db.prepare("(query [:find ?x :where [?e :tags \$s] [?e :n ?x]])")
val tags = setOf(1, "x", MiniGrafValue.Keyword(":k"))
assertEquals(listOf(listOf<Any?>(1)), rows(equal.execute("s" to tags)))
assertEquals(0, rows(equal.execute("s" to setOf(1, "x", ":k"))).size, "a string is no keyword")
val out = db.prepare("(query [:find ?t :where [:a :tags ?t]])")
assertEquals(listOf(listOf<Any?>(listOf("x", 1, ":k"))), rows(out.execute()))
val record = db.factLog(FactFilter(attributes = listOf(":tags"))).use { it.records(10).first() }
assertEquals(
MiniGrafValue.Set(listOf(MiniGrafValue.Text("x"), MiniGrafValue.Int64(1), MiniGrafValue.Keyword(":k"))),
record.value,
)
}

@Test
fun setElementForms() {
val db = db()
val a = UUID.fromString(entityA(db))
val pq = db.prepare("(query [:find ?x :where [\$e :n ?x] [(in? \$e \$s)]])")
assertEquals(listOf(listOf<Any?>(1)), rows(pq.execute("e" to a, "s" to setOf(a))), "a UUID element is a ref")
fun code(e: MiniGrafException.Api) = e.text.take(9)
assertEquals(
"[API-021]",
code(assertThrows<MiniGrafException.Api> { pq.execute("e" to a, "s" to setOf(setOf(1))) }),
)
assertThrows<IllegalArgumentException> { pq.execute("e" to a, "s" to setOf(MiniGrafBindValue.TxCount(1))) }
assertThrows<IllegalArgumentException> { pq.execute("e" to a, "s" to setOf(listOf(1))) }
}
}
59 changes: 58 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,8 @@ impl OpenOptions {
// ─── Values and fact records ─────────────────────────────────────────────────

/// A fact value with its type kept, so a record can be written back exactly.
/// `Ref` holds an entity UUID string.
/// `Ref` holds an entity UUID string. `Set` holds the elements of a set
/// value in canonical order; an element that is itself a `Set` is API-021.
#[derive(Debug, Clone, PartialEq, uniffi::Enum)]
pub enum MiniGrafValue {
Text { value: String },
Expand All @@ -208,6 +209,7 @@ pub enum MiniGrafValue {
Ref { value: String },
Keyword { value: String },
Null,
Set { value: Vec<MiniGrafValue> },
}

impl From<Value> for MiniGrafValue {
Expand All @@ -222,6 +224,14 @@ impl From<Value> for MiniGrafValue {
},
Value::Keyword(value) => MiniGrafValue::Keyword { value },
Value::Null => MiniGrafValue::Null,
Value::Set(set) => MiniGrafValue::Set {
value: set.iter().cloned().map(MiniGrafValue::from).collect(),
},
// A value kind newer than this binding: its debug form, until
// the binding learns it.
other => MiniGrafValue::Text {
value: format!("{other:?}"),
},
}
}
}
Expand All @@ -236,6 +246,12 @@ impl MiniGrafValue {
MiniGrafValue::Ref { value } => Value::Ref(parse_uuid(value, "ref value")?),
MiniGrafValue::Keyword { value } => Value::Keyword(value.clone()),
MiniGrafValue::Null => Value::Null,
MiniGrafValue::Set { value } => Value::Set(minigraf::ValueSet::new(
value
.iter()
.map(MiniGrafValue::to_core)
.collect::<Result<Vec<_>, _>>()?,
)?),
})
}
}
Expand Down Expand Up @@ -738,6 +754,9 @@ fn value_to_json(v: &Value) -> serde_json::Value {
Value::Ref(uuid) => JVal::String(uuid.to_string()),
Value::Keyword(k) => JVal::String(k.clone()),
Value::Null => JVal::Null,
Value::Set(set) => JVal::Array(set.iter().map(value_to_json).collect()),
// A value kind newer than this binding: its debug form.
other => JVal::String(format!("{other:?}")),
}
}

Expand Down Expand Up @@ -971,12 +990,50 @@ mod tests {
Value::Ref(id),
Value::Keyword(":k/w".into()),
Value::Null,
Value::Set(
minigraf::ValueSet::new([
Value::Integer(1),
Value::Keyword(":k/w".into()),
Value::Ref(id),
])
.expect("set"),
),
Value::Set(minigraf::ValueSet::default()),
] {
let back = MiniGrafValue::from(v.clone()).to_core().expect("to_core");
assert!(back == v, "value changed in round trip");
}
}

#[test]
fn set_elements_dedup_and_nesting_is_api_021() {
let MiniGrafValue::Int64 { value: 1 };
let back = MiniGrafValue::Set {
value: vec![one(), MiniGrafValue::Text { value: "a".into() }, one()],
}
.to_core()
.expect("to_core");
assert_eq!(
MiniGrafValue::from(back),
MiniGrafValue::Set {
value: vec![MiniGrafValue::Text { value: "a".into() }, one()]
},
"duplicates dropped, canonical order"
);
let e = err(MiniGrafValue::Set {
value: vec![MiniGrafValue::Set { value: vec![] }],
}
.to_core());
assert_eq!(e.code(), "API-021");
}

#[test]
fn set_is_a_json_array() {
let set =
minigraf::ValueSet::new([Value::Integer(2), Value::String("a".into())]).expect("set");
assert_eq!(value_to_json(&Value::Set(set)), serde_json::json!(["a", 2]));
}

#[test]
fn bad_uuid_is_api_017() {
let e = err(MiniGrafValue::Ref {
Expand Down
Loading
0