diff --git a/Cargo.toml b/Cargo.toml index 91de005d..f2f868e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,4 +37,4 @@ tikv-jemallocator = "0.5" lto = true [workspace] -members = ["cozorocks", "cozohttp"] \ No newline at end of file +members = ["cozorocks", "cozohttp", "cozopy"] \ No newline at end of file diff --git a/cozopy/.gitignore b/cozopy/.gitignore new file mode 100644 index 00000000..7e86cf0c --- /dev/null +++ b/cozopy/.gitignore @@ -0,0 +1,140 @@ +### Python template +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + diff --git a/cozopy/Cargo.toml b/cozopy/Cargo.toml new file mode 100644 index 00000000..6ba97a3a --- /dev/null +++ b/cozopy/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "cozopy" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +name = "cozopy" +crate-type = ["cdylib"] + +[dependencies] +serde_json = "1.0.81" +anyhow = "1.0.58" +pyo3 = { version = "0.16.5", features = ["extension-module"] } +cozo = { path = ".." } diff --git a/cozopy/cozo/__init__.py b/cozopy/cozo/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cozopy/cozo/test.py b/cozopy/cozo/test.py new file mode 100644 index 00000000..0468b0a2 --- /dev/null +++ b/cozopy/cozo/test.py @@ -0,0 +1,123 @@ +import json + +from cozopy import CozoDbPy + + +class CozoDb: + def __init__(self, *args, **kwargs): + self.inner = CozoDbPy(*args, **kwargs) + + def tx_attr(self, payload): + return json.loads(self.inner.transact_attributes(json.dumps(payload, ensure_ascii=False))) + + def tx(self, payload): + return json.loads(self.inner.transact_triples(json.dumps(payload, ensure_ascii=False))) + + def run(self, payload): + return json.loads(self.inner.run_query(json.dumps(payload, ensure_ascii=False))) + + +if __name__ == '__main__': + db = CozoDb('_test', destroy_on_exit=True) + res = db.tx_attr({"attrs": [ + {"put": {"keyword": "person.idd", "cardinality": "one", "type": "string", "index": "identity", + "history": False}}, + {"put": {"keyword": "person.first_name", "cardinality": "one", "type": "string", "index": True}}, + {"put": {"keyword": "person.last_name", "cardinality": "one", "type": "string", "index": True}}, + {"put": {"keyword": "person.age", "cardinality": "one", "type": "int"}}, + {"put": {"keyword": "person.friend", "cardinality": "many", "type": "ref"}}, + {"put": {"keyword": "person.weight", "cardinality": "one", "type": "float"}}, + {"put": {"keyword": "person.covid", "cardinality": "one", "type": "bool"}}, + ] + }) + print(res) + print(db.tx_attr({ + "attrs": [ + {"put": {"id": res["results"][0][0], "keyword": ":person.id", "cardinality": "one", "type": "string", + "index": "identity", "history": False}}, + {"retract": {"id": res["results"][-1][0], "keyword": ":person.covid", "cardinality": "one", "type": "bool"}} + ] + })) + print(db.tx({ + "tx": [ + {"put": { + "_temp_id": "alice", + "person.first_name": "Alice", + "person.age": 7, + "person.last_name": "Amorist", + "person.id": "alice_amorist", + "person.weight": 25, + "person.friend": "eve"}}, + {"put": { + "_temp_id": "bob", + "person.first_name": "Bob", + "person.age": 70, + "person.last_name": "Wonderland", + "person.id": "bob_wonderland", + "person.weight": 100, + "person.friend": "alice" + }}, + {"put": { + "_temp_id": "eve", + "person.first_name": "Eve", + "person.age": 18, + "person.last_name": "Faking", + "person.id": "eve_faking", + "person.weight": 50, + "person.friend": [ + "alice", + "bob", + { + "person.first_name": "Charlie", + "person.age": 22, + "person.last_name": "Goodman", + "person.id": "charlie_goodman", + "person.weight": 120, + "person.friend": "eve" + } + ] + }}, + {"put": { + "_temp_id": "david", + "person.first_name": "David", + "person.age": 7, + "person.last_name": "Dull", + "person.id": "david_dull", + "person.weight": 25, + "person.friend": { + "_temp_id": "george", + "person.first_name": "George", + "person.age": 7, + "person.last_name": "Geomancer", + "person.id": "george_geomancer", + "person.weight": 25, + "person.friend": "george"}}}, + ] + })) + res = db.run({ + "q": [ + { + "rule": "ff", + "args": [["?a", "?b"], ["?a", "person.friend", "?b"]] + }, + { + "rule": "ff", + "args": [["?a", "?b"], ["?a", "person.friend", "?c"], {"rule": "ff", "args": ["?c", "?b"]}] + }, + { + "rule": "?", + "args": [["?a"], + {"not_exists": ["?a", "person.last_name", "Goodman"]}, + {"disj": [ + {"pred": "Eq", "args": ["?n", {"pred": "StrCat", "args": ["A", "l", "i", "c", "e"]}]}, + {"pred": "Eq", "args": ["?n", "Bob"]}, + {"pred": "Eq", "args": ["?n", 12345]}, + ]}, + {"rule": "ff", "args": [{"person.id": "alice_amorist"}, "?a"]}, + ["?a", "person.first_name", "?n"] + ] + } + ], + "out": {"friend": {"pull": "?a", "spec": ["person.first_name"]}} + }) + print(res) diff --git a/cozopy/src/lib.rs b/cozopy/src/lib.rs new file mode 100644 index 00000000..2e7a75ab --- /dev/null +++ b/cozopy/src/lib.rs @@ -0,0 +1,61 @@ +use pyo3::exceptions::PyException; +use pyo3::prelude::*; + +use cozo::{Db, DbBuilder}; + +#[pyclass(extends=PyException)] +struct ErrorBridge(cozo::Error); + +trait PyResultExt { + fn into_py_res(self) -> PyResult; +} + +impl PyResultExt for anyhow::Result { + fn into_py_res(self) -> PyResult { + match self { + Ok(t) => Ok(t), + Err(e) => Err(PyException::new_err(e.to_string())), + } + } +} + +#[pyclass] +struct CozoDbPy { + db: Db, +} + +#[pymethods] +impl CozoDbPy { + #[new] + #[args(create_if_missing = true, destroy_on_exit = false)] + fn new(path: &str, create_if_missing: bool, destroy_on_exit: bool) -> PyResult { + let builder = DbBuilder::default() + .path(path) + .create_if_missing(create_if_missing) + .destroy_on_exit(destroy_on_exit); + let db = Db::build(builder).into_py_res()?; + Ok(Self { db }) + } + pub fn transact_attributes(&self, payload: &str) -> PyResult { + let payload: serde_json::Value = serde_json::from_str(payload).unwrap(); + let ret = self.db.transact_attributes(&payload).into_py_res()?; + Ok(ret.to_string()) + } + pub fn transact_triples(&self, payload: &str) -> PyResult { + let payload: serde_json::Value = serde_json::from_str(payload).unwrap(); + let ret = self.db.transact_triples(&payload).into_py_res()?; + Ok(ret.to_string()) + } + pub fn run_query(&self, payload: &str) -> PyResult { + let payload: serde_json::Value = serde_json::from_str(payload).unwrap(); + let ret = self.db.run_query(&payload).into_py_res()?; + Ok(ret.to_string()) + } +} + +#[pymodule] +fn cozopy(_py: Python<'_>, m: &PyModule) -> PyResult<()> { + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/src/runtime/db.rs b/src/runtime/db.rs index 39fc6113..f313151a 100644 --- a/src/runtime/db.rs +++ b/src/runtime/db.rs @@ -1,8 +1,8 @@ use std::collections::BTreeMap; use std::env::temp_dir; use std::fmt::{Debug, Formatter}; -use std::sync::Arc; use std::sync::atomic::{AtomicU32, AtomicU64, AtomicUsize, Ordering}; +use std::sync::Arc; use anyhow::Result; use itertools::Itertools; @@ -11,8 +11,7 @@ use uuid::Uuid; use cozorocks::{DbBuilder, DbIter, RawRocksDb, RocksDb}; -use crate::AttrTxItem; -use crate::data::compare::{DB_KEY_PREFIX_LEN, rusty_cmp}; +use crate::data::compare::{rusty_cmp, DB_KEY_PREFIX_LEN}; use crate::data::encode::{ decode_ea_key, decode_value_from_key, decode_value_from_val, encode_eav_key, StorageTag, }; @@ -21,8 +20,9 @@ use crate::data::json::JsonValue; use crate::data::triple::StoreOp; use crate::data::tuple::{rusty_scratch_cmp, SCRATCH_DB_KEY_PREFIX_LEN}; use crate::data::value::DataValue; -use crate::runtime::transact::SessionTx; use crate::query::pull::CurrentPath; +use crate::runtime::transact::SessionTx; +use crate::AttrTxItem; pub struct Db { db: RocksDb, @@ -270,4 +270,9 @@ impl Db { let collected = collected.into_iter().map(|(_, v)| v).collect_vec(); Ok(json!(collected)) } + pub fn run_query(&self, payload: &JsonValue) -> Result { + let mut tx = self.transact()?; + let ret: Vec<_> = tx.run_query(payload)?.try_collect()?; + Ok(json!(ret)) + } }