#include #include #include #include "wayy_db/wayy_db.hpp" #include namespace py = pybind11; // GIL release guard for concurrent read operations using release_gil = py::call_guard; using namespace wayy_db; // Namespace alias to avoid collision with local variable namespace wdb_ops = wayy_db::ops; // Helper to convert numpy dtype to WayyDB DType DType numpy_dtype_to_wayy(py::dtype dt) { if (dt.is(py::dtype::of())) return DType::Int64; if (dt.is(py::dtype::of())) return DType::Float64; if (dt.is(py::dtype::of())) return DType::Symbol; if (dt.is(py::dtype::of())) return DType::Bool; throw std::runtime_error("Unsupported numpy dtype"); } // Helper to get numpy dtype from WayyDB DType py::dtype wayy_dtype_to_numpy(DType dt) { switch (dt) { case DType::Int64: case DType::Timestamp: case DType::Decimal6: return py::dtype::of(); case DType::Float64: return py::dtype::of(); case DType::Symbol: return py::dtype::of(); case DType::Bool: return py::dtype::of(); case DType::String: throw std::runtime_error("String columns use StringColumn, not numpy"); } throw std::runtime_error("Unknown dtype"); } // Helper: convert Python dict to std::unordered_map std::unordered_map py_dict_to_any_map( py::dict d, Table& table) { std::unordered_map result; for (auto& [key, val] : d) { std::string col_name = py::str(key); DType dt = table.column_dtype(col_name); if (dt == DType::String) { result[col_name] = std::string(py::str(val)); } else if (dt == DType::Int64 || dt == DType::Timestamp || dt == DType::Decimal6) { result[col_name] = py::cast(val); } else if (dt == DType::Float64) { result[col_name] = py::cast(val); } else if (dt == DType::Symbol) { result[col_name] = py::cast(val); } else if (dt == DType::Bool) { result[col_name] = py::cast(val); } } return result; } PYBIND11_MODULE(_core, m, py::mod_gil_not_used()) { m.doc() = "WayyDB: High-performance columnar time-series database (free-threading safe)"; // DType enum py::enum_(m, "DType") .value("Int64", DType::Int64) .value("Float64", DType::Float64) .value("Timestamp", DType::Timestamp) .value("Symbol", DType::Symbol) .value("Bool", DType::Bool) .value("String", DType::String) .value("Decimal6", DType::Decimal6) .export_values(); // Exceptions py::register_exception(m, "WayyException"); py::register_exception(m, "ColumnNotFound"); py::register_exception(m, "TypeMismatch"); py::register_exception(m, "InvalidOperation"); // Column class py::class_(m, "Column") .def_property_readonly("name", &Column::name) .def_property_readonly("dtype", &Column::dtype) .def_property_readonly("size", &Column::size) .def("__len__", &Column::size) .def("to_numpy", [](Column& self) -> py::array { py::dtype dt = wayy_dtype_to_numpy(self.dtype()); return py::array(dt, {self.size()}, {dtype_size(self.dtype())}, self.data(), py::cast(self)); }, py::return_value_policy::reference_internal, "Zero-copy view as numpy array") .def("is_valid", &Column::is_valid, py::arg("row"), "Check if row is valid (not null/deleted)") .def("count_valid", &Column::count_valid, "Count non-null/non-deleted rows"); // StringColumn class py::class_(m, "StringColumn") .def(py::init(), py::arg("name") = "") .def_property_readonly("name", &StringColumn::name) .def_property_readonly("dtype", &StringColumn::dtype) .def_property_readonly("size", &StringColumn::size) .def("__len__", &StringColumn::size) .def("get", &StringColumn::get, py::arg("row"), "Get string at row index") .def("append", &StringColumn::append, py::arg("val"), "Append a string value") .def("set", &StringColumn::set, py::arg("row"), py::arg("val"), "Set string at row index") .def("is_valid", &StringColumn::is_valid, py::arg("row")) .def("count_valid", &StringColumn::count_valid) .def("to_list", &StringColumn::to_vector, "Get all strings as a Python list"); // Table class py::class_(m, "Table") .def(py::init(), py::arg("name") = "") .def_property_readonly("name", &Table::name) .def_property_readonly("num_rows", &Table::num_rows) .def_property_readonly("num_columns", &Table::num_columns) .def_property_readonly("sorted_by", [](const Table& t) -> py::object { if (t.sorted_by()) return py::cast(*t.sorted_by()); return py::none(); }) .def_property_readonly("primary_key", [](const Table& t) -> py::object { if (t.primary_key()) return py::cast(*t.primary_key()); return py::none(); }) .def("__len__", &Table::num_rows) .def("has_column", &Table::has_column) .def("column", py::overload_cast(&Table::column), py::return_value_policy::reference_internal) .def("__getitem__", py::overload_cast(&Table::column), py::return_value_policy::reference_internal) .def("has_string_column", &Table::has_string_column) .def("string_column", py::overload_cast(&Table::string_column), py::return_value_policy::reference_internal) .def("column_dtype", &Table::column_dtype, py::arg("name"), "Get the DType of any column (fixed or string)") .def("column_names", &Table::column_names) .def("set_sorted_by", &Table::set_sorted_by) .def("set_primary_key", &Table::set_primary_key, py::arg("col_name"), "Set the primary key column and build hash index") .def("rebuild_index", &Table::rebuild_index, "Rebuild the primary key hash index") // CRUD operations .def("append_row", [](Table& self, py::dict values) -> size_t { auto map = py_dict_to_any_map(values, self); return self.append_row(map); }, py::arg("values"), "Append a row from a dict, returns row index") .def("update_row", [](Table& self, py::object pk, py::dict values) -> bool { auto map = py_dict_to_any_map(values, self); if (py::isinstance(pk)) { return self.update_row(py::cast(pk), map); } else { return self.update_row(std::string(py::str(pk)), map); } }, py::arg("pk"), py::arg("values"), "Update row by primary key") .def("delete_row", [](Table& self, py::object pk) -> bool { if (py::isinstance(pk)) { return self.delete_row(py::cast(pk)); } else { return self.delete_row(std::string(py::str(pk))); } }, py::arg("pk"), "Soft-delete row by primary key") .def("find_row", [](const Table& self, py::object pk) -> py::object { std::optional row; if (py::isinstance(pk)) { row = self.find_row(py::cast(pk)); } else { row = self.find_row(std::string(py::str(pk))); } if (row) return py::cast(*row); return py::none(); }, py::arg("pk"), "Find row index by primary key") .def("where_eq", [](const Table& self, const std::string& col, py::object val) -> py::list { std::vector rows; DType dt = self.column_dtype(col); if (dt == DType::String) { rows = self.where_eq(col, std::string(py::str(val))); } else { rows = self.where_eq(col, py::cast(val)); } py::list result; for (auto r : rows) result.append(r); return result; }, py::arg("col"), py::arg("val"), "Filter rows where col == val") .def("compact", &Table::compact, "Physically remove deleted rows and rebuild index") .def("save", &Table::save) .def_static("load", &Table::load) .def_static("mmap", &Table::mmap) .def("add_column_from_numpy", [](Table& self, const std::string& name, py::array arr, DType dtype) { py::buffer_info buf = arr.request(); if (buf.ndim != 1) { throw std::runtime_error("Array must be 1-dimensional"); } // Copy data into owned buffer size_t elem_size = dtype_size(dtype); std::vector data(buf.size * elem_size); std::memcpy(data.data(), buf.ptr, data.size()); self.add_column(Column(name, dtype, std::move(data))); }, py::arg("name"), py::arg("array"), py::arg("dtype")) .def("add_string_column_from_list", [](Table& self, const std::string& name, py::list strings) { StringColumn sc(name); for (auto& item : strings) { if (item.is_none()) { sc.append_null(); } else { sc.append(std::string(py::str(item))); } } self.add_string_column(std::move(sc)); }, py::arg("name"), py::arg("strings"), "Add a string column from a Python list") .def("to_dict", [](Table& self) -> py::dict { py::dict result; for (const auto& col_name : self.column_names()) { if (self.has_string_column(col_name)) { auto& scol = self.string_column(col_name); result[py::cast(col_name)] = py::cast(scol.to_vector()); } else { Column& col = self.column(col_name); py::dtype dt = wayy_dtype_to_numpy(col.dtype()); // Make a copy for the dict py::array arr(dt, {col.size()}, {dtype_size(col.dtype())}, col.data()); result[py::cast(col_name)] = arr.attr("copy")(); } } return result; }); // Database class py::class_(m, "Database") .def(py::init<>()) .def(py::init(), py::arg("path")) .def_property_readonly("path", &Database::path) .def_property_readonly("is_persistent", &Database::is_persistent) .def("tables", &Database::tables) .def("has_table", &Database::has_table) .def("table", &Database::table, py::return_value_policy::reference_internal) .def("__getitem__", &Database::table, py::return_value_policy::reference_internal) .def("create_table", &Database::create_table, py::return_value_policy::reference_internal) .def("add_table", [](Database& db, Table& table) { db.add_table(std::move(table)); }) .def("drop_table", &Database::drop_table) .def("save", &Database::save) .def("refresh", &Database::refresh) .def("checkpoint", &Database::checkpoint, "Flush WAL, save all tables, truncate WAL"); // Operations submodule py::module_ ops_mod = m.def_submodule("ops", "WayyDB operations"); // Aggregations - use lambdas to avoid overload issues // All aggregations release the GIL for concurrent execution ops_mod.def("sum", [](const Column& col) { return wdb_ops::sum(col); }, py::arg("col"), release_gil(), "Sum of column values"); ops_mod.def("avg", [](const Column& col) { return wdb_ops::avg(col); }, py::arg("col"), release_gil(), "Average of column values"); ops_mod.def("min", [](const Column& col) { return wdb_ops::min_val(col); }, py::arg("col"), release_gil(), "Minimum value"); ops_mod.def("max", [](const Column& col) { return wdb_ops::max_val(col); }, py::arg("col"), release_gil(), "Maximum value"); ops_mod.def("std", [](const Column& col) { return wdb_ops::std_dev(col); }, py::arg("col"), release_gil(), "Standard deviation"); // Joins - release GIL for concurrent execution ops_mod.def("aj", &wdb_ops::aj, py::arg("left"), py::arg("right"), py::arg("on"), py::arg("as_of"), release_gil(), "As-of join: find most recent right row for each left row"); ops_mod.def("wj", &wdb_ops::wj, py::arg("left"), py::arg("right"), py::arg("on"), py::arg("as_of"), py::arg("window_before"), py::arg("window_after"), release_gil(), "Window join: find all right rows within time window"); // Window functions (returning numpy arrays) // These compute with GIL released, then briefly reacquire to create numpy array ops_mod.def("mavg", [](Column& col, size_t window) -> py::array_t { std::vector result; { py::gil_scoped_release release; result = wdb_ops::mavg(col.as_float64(), window); } return py::array_t(result.size(), result.data()); }, py::arg("col"), py::arg("window"), "Moving average"); ops_mod.def("msum", [](Column& col, size_t window) -> py::array_t { std::vector result; { py::gil_scoped_release release; result = wdb_ops::msum(col.as_float64(), window); } return py::array_t(result.size(), result.data()); }, py::arg("col"), py::arg("window"), "Moving sum"); ops_mod.def("mstd", [](Column& col, size_t window) -> py::array_t { std::vector result; { py::gil_scoped_release release; result = wdb_ops::mstd(col.as_float64(), window); } return py::array_t(result.size(), result.data()); }, py::arg("col"), py::arg("window"), "Moving standard deviation"); ops_mod.def("mmin", [](Column& col, size_t window) -> py::array_t { std::vector result; { py::gil_scoped_release release; result = wdb_ops::mmin(col.as_float64(), window); } return py::array_t(result.size(), result.data()); }, py::arg("col"), py::arg("window"), "Moving minimum"); ops_mod.def("mmax", [](Column& col, size_t window) -> py::array_t { std::vector result; { py::gil_scoped_release release; result = wdb_ops::mmax(col.as_float64(), window); } return py::array_t(result.size(), result.data()); }, py::arg("col"), py::arg("window"), "Moving maximum"); ops_mod.def("ema", [](Column& col, double alpha) -> py::array_t { std::vector result; { py::gil_scoped_release release; result = wdb_ops::ema(col.as_float64(), alpha); } return py::array_t(result.size(), result.data()); }, py::arg("col"), py::arg("alpha"), "Exponential moving average"); ops_mod.def("diff", [](Column& col, size_t periods) -> py::array_t { std::vector result; { py::gil_scoped_release release; result = wdb_ops::diff(col.as_float64(), periods); } return py::array_t(result.size(), result.data()); }, py::arg("col"), py::arg("periods") = 1, "Difference between consecutive values"); ops_mod.def("pct_change", [](Column& col, size_t periods) -> py::array_t { std::vector result; { py::gil_scoped_release release; result = wdb_ops::pct_change(col.as_float64(), periods); } return py::array_t(result.size(), result.data()); }, py::arg("col"), py::arg("periods") = 1, "Percent change"); ops_mod.def("shift", [](Column& col, int64_t n) -> py::array_t { std::vector result; { py::gil_scoped_release release; result = wdb_ops::shift(col.as_float64(), n); } return py::array_t(result.size(), result.data()); }, py::arg("col"), py::arg("n"), "Shift values by n positions"); // Version info m.attr("__version__") = "0.2.0"; }